Commit 8160a34d authored by luwei's avatar luwei

修改

parent 4601d53b
from fastapi import APIRouter, File, Form, HTTPException, Query, Response, UploadFile from fastapi import APIRouter, File, Form, HTTPException, Query, Response, UploadFile
from pydantic import BaseModel, Field
from app.schemas.data_management import CategoryCreateRequest, CategoryUpdateRequest, FileUpdateRequest
from app.services.data_management_service import DataManagementService from app.services.data_management_service import DataManagementService
from app.utils.response import success_response from app.utils.response import success_response
...@@ -9,15 +9,6 @@ router = APIRouter() ...@@ -9,15 +9,6 @@ router = APIRouter()
service = DataManagementService() service = DataManagementService()
class CategoryCreateRequest(BaseModel):
name: str = Field(min_length=1, max_length=100)
parent_id: str | int | None = Field(default='all')
class CategoryUpdateRequest(BaseModel):
name: str = Field(min_length=1, max_length=100)
@router.get('/categories') @router.get('/categories')
def get_categories(): def get_categories():
return success_response(data=service.get_category_tree()) return success_response(data=service.get_category_tree())
...@@ -91,12 +82,6 @@ def download_template(): ...@@ -91,12 +82,6 @@ def download_template():
) )
class FileUpdateRequest(BaseModel):
filename: str | None = Field(default=None, min_length=1, max_length=255)
remark: str | None = Field(default=None, max_length=500)
category_id: str | None = Field(default=None)
@router.put('/files/{file_id}') @router.put('/files/{file_id}')
def update_file(file_id: str, request: FileUpdateRequest): def update_file(file_id: str, request: FileUpdateRequest):
try: try:
......
from fastapi import APIRouter, HTTPException, Query from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel
from app.schemas.eval_management import EvalRequest
from app.services.eval_service import EvalService from app.services.eval_service import EvalService
from app.utils.response import success_response from app.utils.response import success_response
...@@ -8,11 +8,6 @@ router = APIRouter() ...@@ -8,11 +8,6 @@ router = APIRouter()
service = EvalService() service = EvalService()
class EvalRequest(BaseModel):
model_id: int
package_id: int
@router.get('/packages') @router.get('/packages')
def list_packages( def list_packages(
category_id: str = Query(default=''), category_id: str = Query(default=''),
......
from fastapi import APIRouter, HTTPException, Query from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from app.schemas.monitor import CreateExperimentRequest
from app.services.monitor_service import MonitorService from app.services.monitor_service import MonitorService
from app.utils.response import success_response from app.utils.response import success_response
...@@ -8,33 +8,6 @@ router = APIRouter() ...@@ -8,33 +8,6 @@ router = APIRouter()
service = MonitorService() service = MonitorService()
# ── 请求 Schema ───────────────────────────────────────────────────────────────
class MPCParamsSchema(BaseModel):
P: int = Field(default=20, ge=5, le=100, description='预测时域')
M: int = Field(default=5, ge=1, le=20, description='控制时域')
Q: float = Field(default=10.0, gt=0, description='跟踪误差权重')
R: float = Field(default=0.1, gt=0, description='控制增量权重')
alpha: float = Field(default=0.8, gt=0, lt=1, description='参考轨迹柔化系数')
u_min: float = Field(default=0.0, description='电流下限(A)')
u_max: float = Field(default=10.0, description='电流上限(A)')
du_min: float = Field(default=-2.0, description='电流增量下限(A/步)')
du_max: float = Field(default=2.0, description='电流增量上限(A/步)')
y_min: float = Field(default=-50.0, description='温度约束下限(°C)')
y_max: float = Field(default=200.0, description='温度约束上限(°C)')
correction_gain: float = Field(default=0.5, ge=0, le=1, description='反馈校正增益')
class CreateExperimentRequest(BaseModel):
name: str = Field(min_length=1, max_length=255)
model_id: int
input_csv_path: str = Field(min_length=1, description='读取传感器数据的CSV文件路径')
output_csv_path: str = Field(min_length=1, description='输出曲线数据的CSV文件路径')
target_temp: float = Field(description='目标温度(°C)')
sampling_interval: float = Field(default=1.0, gt=0, le=3600, description='采样周期(秒)')
mpc_params: MPCParamsSchema = Field(default_factory=MPCParamsSchema)
# ── 元数据接口 ──────────────────────────────────────────────────────────────── # ── 元数据接口 ────────────────────────────────────────────────────────────────
@router.get('/models') @router.get('/models')
......
from fastapi import APIRouter, HTTPException, Query from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from app.schemas.package_management import (
CategoryCreateRequest,
CategoryUpdateRequest,
PackageCreateRequest,
PackageUpdateRequest,
PreviewRequest,
)
from app.services.package_management_service import PackageManagementService from app.services.package_management_service import PackageManagementService
from app.utils.response import success_response from app.utils.response import success_response
...@@ -9,31 +15,6 @@ router = APIRouter() ...@@ -9,31 +15,6 @@ router = APIRouter()
service = PackageManagementService() service = PackageManagementService()
class CategoryCreateRequest(BaseModel):
name: str = Field(min_length=1, max_length=100)
parent_id: str | int | None = Field(default='all')
class CategoryUpdateRequest(BaseModel):
name: str = Field(min_length=1, max_length=100)
class CleanRules(BaseModel):
enabled: bool = False
newton_interp: bool = False
current_min: float | None = None
current_max: float | None = None
voltage_min: float | None = None
voltage_max: float | None = None
temperature_min: float | None = None
temperature_max: float | None = None
class SmoothConfig(BaseModel):
enabled: bool = False
window: int = Field(default=5, ge=2, le=500)
@router.get('/categories') @router.get('/categories')
def get_categories(): def get_categories():
return success_response(data=service.get_category_tree()) return success_response(data=service.get_category_tree())
...@@ -81,32 +62,6 @@ def list_all_data_files( ...@@ -81,32 +62,6 @@ def list_all_data_files(
)) ))
class PackageUpdateRequest(BaseModel):
name: str = Field(min_length=1, max_length=255)
category_id: str | int | None = Field(default=None)
remark: str | None = Field(default=None)
class PackageCreateRequest(BaseModel):
name: str = Field(min_length=1, max_length=255)
category_id: str | int | None = Field(default=None)
remark: str | None = Field(default=None)
file_ids: list[int] = Field(default_factory=list)
clean_rules: CleanRules | None = Field(default=None)
smooth: SmoothConfig | None = Field(default=None)
auto_split: bool = Field(default=False)
row_start: int | None = Field(default=None, ge=1)
row_end: int | None = Field(default=None, ge=1)
class PreviewRequest(BaseModel):
file_ids: list[int] = Field(default_factory=list)
clean_rules: CleanRules | None = Field(default=None)
smooth: SmoothConfig | None = Field(default=None)
row_start: int | None = Field(default=None, ge=1)
row_end: int | None = Field(default=None, ge=1)
@router.post('/preview') @router.post('/preview')
def preview_package(request: PreviewRequest): def preview_package(request: PreviewRequest):
try: try:
......
from fastapi import APIRouter, HTTPException, Query from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from app.schemas.train_management import CreateTaskRequest, SaveModelRequest, UpdateModelRequest
from app.services.train_service import TrainService from app.services.train_service import TrainService
from app.utils.response import success_response from app.utils.response import success_response
...@@ -8,34 +8,6 @@ router = APIRouter() ...@@ -8,34 +8,6 @@ router = APIRouter()
service = TrainService() service = TrainService()
# ── request schemas ───────────────────────────────────────────────────────────
class LSTMParams(BaseModel):
seq_len: int = Field(default=20, ge=5, le=500)
hidden_size: int = Field(default=64, ge=8, le=1024)
num_layers: int = Field(default=2, ge=1, le=8)
epochs: int = Field(default=50, ge=1, le=2000)
batch_size: int = Field(default=32, ge=1, le=512)
learning_rate: float = Field(default=0.001, gt=0, le=1)
class CreateTaskRequest(BaseModel):
model_name: str = Field(min_length=1, max_length=255)
train_package_id: int
val_package_id: int
params: LSTMParams = Field(default_factory=LSTMParams)
class SaveModelRequest(BaseModel):
model_name: str | None = Field(default=None, max_length=255)
description: str | None = None
class UpdateModelRequest(BaseModel):
model_name: str | None = Field(default=None, max_length=255)
description: str | None = None
# ── packages ────────────────────────────────────────────────────────────────── # ── packages ──────────────────────────────────────────────────────────────────
@router.get('/packages') @router.get('/packages')
......
...@@ -97,7 +97,8 @@ def train_lstm( ...@@ -97,7 +97,8 @@ def train_lstm(
train_records: list of dicts for the training set. train_records: list of dicts for the training set.
val_records: list of dicts for the validation set. val_records: list of dicts for the validation set.
params: hyper-parameter dict (seq_len, hidden_size, num_layers, params: hyper-parameter dict (seq_len, hidden_size, num_layers,
epochs, batch_size, learning_rate). epochs, batch_size, learning_rate,
lr_factor, lr_patience, lr_min).
save_path: destination .pt file. save_path: destination .pt file.
on_progress: callback(pct, epoch, train_loss, val_loss) called after each epoch. on_progress: callback(pct, epoch, train_loss, val_loss) called after each epoch.
cancel_event: when set, training stops with InterruptedError. cancel_event: when set, training stops with InterruptedError.
...@@ -114,15 +115,20 @@ def train_lstm( ...@@ -114,15 +115,20 @@ def train_lstm(
batch_size = max(1, int(params.get('batch_size', 32))) batch_size = max(1, int(params.get('batch_size', 32)))
lr = float(params.get('learning_rate', 0.001)) lr = float(params.get('learning_rate', 0.001))
# ── scheduler 超参数 ──────────────────────────────────────────────────────
lr_factor = float(params.get('lr_factor', 0.5))
lr_patience = max(1, int(params.get('lr_patience', 5)))
lr_min = float(params.get('lr_min', 1e-6))
# ── data preparation ──────────────────────────────────────────────────── # ── data preparation ────────────────────────────────────────────────────
train_data = _extract_features(train_records) train_data = _extract_features(train_records)
val_data = _extract_features(val_records) val_data = _extract_features(val_records)
min_required = seq_len + 10 min_required = seq_len + 10
if len(train_data) < min_required: if len(train_data) < min_required:
raise ValueError( raise ValueError(
f'\u8bad\u7ec3\u96c6\u6709\u6548\u6570\u636e\u91cf\u4e0d\u8db3\uff1a\u9700\u81f3\u5c11 {min_required} \u6761\uff0c\u5f53\u524d\u4ec5 {len(train_data)} \u6761\u3002' f'训练集有效数据量不足:需至少 {min_required} 条,当前仅 {len(train_data)} 条。'
'\u8bf7\u68c0\u67e5\u6570\u636e\u5305\u5185\u5bb9\u6216\u51cf\u5c0f\u5e8f\u5217\u957f\u5ea6\u3002' '请检查数据包内容或减小序列长度。'
) )
# min-max normalisation fitted on train set only # min-max normalisation fitted on train set only
...@@ -161,134 +167,15 @@ def train_lstm( ...@@ -161,134 +167,15 @@ def train_lstm(
optimizer = torch.optim.Adam(model.parameters(), lr=lr) optimizer = torch.optim.Adam(model.parameters(), lr=lr)
criterion = nn.MSELoss() criterion = nn.MSELoss()
train_loss = 0.0 # ── scheduler ────────────────────────────────────────────────────────────
val_loss: float | None = None scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer,
for epoch in range(epochs): mode='min',
if cancel_event.is_set(): factor=lr_factor,
raise InterruptedError('\u8bad\u7ec3\u5df2\u53d6\u6d88') patience=lr_patience,
min_lr=lr_min,
# ── train step ───────────────────────────────────────────────────────
model.train()
epoch_loss = 0.0
for xb, yb in train_loader:
optimizer.zero_grad()
pred = model(xb)
loss = criterion(pred, yb)
loss.backward()
optimizer.step()
epoch_loss += loss.item() * len(xb)
train_loss = epoch_loss / len(X_train)
# ── val step ─────────────────────────────────────────────────────────
if has_val:
model.eval()
with torch.no_grad():
val_pred = model(X_val_t)
val_loss = criterion(val_pred, y_val_t).item()
pct = int((epoch + 1) / epochs * 100)
on_progress(pct, epoch + 1, train_loss, val_loss)
# ── persist ──────────────────────────────────────────────────────────────
save_path.parent.mkdir(parents=True, exist_ok=True)
torch.save(
{
'model_state': model.state_dict(),
'params': params,
'data_min': data_min.tolist(),
'data_max': data_max.tolist(),
'feature_cols': FEATURE_COLS,
'target_col': TARGET_COL,
'input_size': input_size,
'hidden_size': hidden_size,
'num_layers': num_layers,
'seq_len': seq_len,
},
save_path,
)
return {
'train_loss': round(float(train_loss), 6),
'val_loss': round(float(val_loss), 6) if val_loss is not None else None,
}
"""
Train an LSTM model on *records* and persist it to *save_path*.
Args:
records: list of dicts with keys in FEATURE_COLS.
params: hyper-parameter dict (seq_len, hidden_size, num_layers,
epochs, batch_size, learning_rate).
train_ratio is ignored – fixed 70/15/15 split is used.
save_path: destination .pt file.
on_progress: callback(pct, train_loss, val_loss) called after each epoch.
cancel_event: when set, training stops with InterruptedError.
Returns:
{'train_loss': float, 'val_loss': float|None, 'test_loss': float|None}
"""
_check_torch()
seq_len = max(1, int(params.get('seq_len', 20)))
hidden_size = max(1, int(params.get('hidden_size', 64)))
num_layers = max(1, int(params.get('num_layers', 2)))
epochs = max(1, int(params.get('epochs', 50)))
batch_size = max(1, int(params.get('batch_size', 32)))
lr = float(params.get('learning_rate', 0.001))
# ── data preparation ────────────────────────────────────────────────────
data = _extract_features(records)
min_required = seq_len + 10
if len(data) < min_required:
raise ValueError(
f'有效数据量不足:需至少 {min_required} 条,当前仅 {len(data)} 条。'
'请检查数据包内容或减小序列长度。'
)
# min-max normalisation per feature
data_min = data.min(axis=0)
data_max = data.max(axis=0)
data_range = data_max - data_min
data_range[data_range == 0] = 1.0
data_norm = (data - data_min) / data_range
X, y = _make_sequences(data_norm, seq_len)
# Fixed 70 / 15 / 15 split
n_total = len(X)
n_train = max(1, int(n_total * _TRAIN_RATIO))
n_val = max(1, int(n_total * _VAL_RATIO))
# test gets the remainder so the three parts always sum to n_total
X_train, y_train = X[:n_train], y[:n_train]
X_val, y_val = X[n_train : n_train + n_val], y[n_train : n_train + n_val]
X_test, y_test = X[n_train + n_val :], y[n_train + n_val :]
device = torch.device('cpu')
X_train_t = torch.tensor(X_train).to(device)
y_train_t = torch.tensor(y_train).to(device)
has_val = len(X_val) > 0
has_test = len(X_test) > 0
if has_val:
X_val_t = torch.tensor(X_val).to(device)
y_val_t = torch.tensor(y_val).to(device)
if has_test:
X_test_t = torch.tensor(X_test).to(device)
y_test_t = torch.tensor(y_test).to(device)
train_loader = DataLoader(
TensorDataset(X_train_t, y_train_t),
batch_size=batch_size,
shuffle=True,
) )
# ── model ────────────────────────────────────────────────────────────────
input_size = len(FEATURE_COLS)
model = _LSTMModel(input_size, hidden_size, num_layers).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
criterion = nn.MSELoss()
train_loss = 0.0 train_loss = 0.0
val_loss: float | None = None val_loss: float | None = None
...@@ -316,16 +203,11 @@ def train_lstm( ...@@ -316,16 +203,11 @@ def train_lstm(
val_pred = model(X_val_t) val_pred = model(X_val_t)
val_loss = criterion(val_pred, y_val_t).item() val_loss = criterion(val_pred, y_val_t).item()
pct = int((epoch + 1) / epochs * 100) # 根据 val_loss 动态调整学习率
on_progress(pct, train_loss, val_loss) scheduler.step(val_loss)
# ── test-set evaluation (completely hidden during training) ────────────── pct = int((epoch + 1) / epochs * 100)
test_loss: float | None = None on_progress(pct, epoch + 1, train_loss, val_loss)
if has_test:
model.eval()
with torch.no_grad():
test_pred = model(X_test_t)
test_loss = criterion(test_pred, y_test_t).item()
# ── persist ────────────────────────────────────────────────────────────── # ── persist ──────────────────────────────────────────────────────────────
save_path.parent.mkdir(parents=True, exist_ok=True) save_path.parent.mkdir(parents=True, exist_ok=True)
...@@ -347,6 +229,6 @@ def train_lstm( ...@@ -347,6 +229,6 @@ def train_lstm(
return { return {
'train_loss': round(float(train_loss), 6), 'train_loss': round(float(train_loss), 6),
'val_loss': round(float(val_loss), 6) if val_loss is not None else None, 'val_loss': round(float(val_loss), 6) if val_loss is not None else None,
'test_loss': round(float(test_loss), 6) if test_loss is not None else None,
} }
from pydantic import BaseModel, Field
class CategoryCreateRequest(BaseModel):
name: str = Field(min_length=1, max_length=100)
parent_id: str | int | None = Field(default='all')
class CategoryUpdateRequest(BaseModel):
name: str = Field(min_length=1, max_length=100)
class FileUpdateRequest(BaseModel):
filename: str | None = Field(default=None, min_length=1, max_length=255)
remark: str | None = Field(default=None, max_length=500)
category_id: str | None = Field(default=None)
from pydantic import BaseModel
class EvalRequest(BaseModel):
model_id: int
package_id: int
from pydantic import BaseModel, Field
class MPCParamsSchema(BaseModel):
P: int = Field(default=20, ge=5, le=100, description='预测时域')
M: int = Field(default=5, ge=1, le=20, description='控制时域')
Q: float = Field(default=10.0, gt=0, description='跟踪误差权重')
R: float = Field(default=0.1, gt=0, description='控制增量权重')
alpha: float = Field(default=0.8, gt=0, lt=1, description='参考轨迹柔化系数')
u_min: float = Field(default=0.0, description='电流下限(A)')
u_max: float = Field(default=10.0, description='电流上限(A)')
du_min: float = Field(default=-2.0, description='电流增量下限(A/步)')
du_max: float = Field(default=2.0, description='电流增量上限(A/步)')
y_min: float = Field(default=-50.0, description='温度约束下限(°C)')
y_max: float = Field(default=200.0, description='温度约束上限(°C)')
correction_gain: float = Field(default=0.5, ge=0, le=1, description='反馈校正增益')
class CreateExperimentRequest(BaseModel):
name: str = Field(min_length=1, max_length=255)
model_id: int
input_csv_path: str = Field(min_length=1, description='读取传感器数据的CSV文件路径')
output_csv_path: str = Field(min_length=1, description='输出曲线数据的CSV文件路径')
target_temp: float = Field(description='目标温度(°C)')
sampling_interval: float = Field(default=1.0, gt=0, le=3600, description='采样周期(秒)')
mpc_params: MPCParamsSchema = Field(default_factory=MPCParamsSchema)
from pydantic import BaseModel, Field
class CategoryCreateRequest(BaseModel):
name: str = Field(min_length=1, max_length=100)
parent_id: str | int | None = Field(default='all')
class CategoryUpdateRequest(BaseModel):
name: str = Field(min_length=1, max_length=100)
class CleanRules(BaseModel):
enabled: bool = False
newton_interp: bool = False
current_min: float | None = None
current_max: float | None = None
voltage_min: float | None = None
voltage_max: float | None = None
temperature_min: float | None = None
temperature_max: float | None = None
class SmoothConfig(BaseModel):
enabled: bool = False
window: int = Field(default=5, ge=2, le=500)
class PackageUpdateRequest(BaseModel):
name: str = Field(min_length=1, max_length=255)
category_id: str | int | None = Field(default=None)
remark: str | None = Field(default=None)
class PackageCreateRequest(BaseModel):
name: str = Field(min_length=1, max_length=255)
category_id: str | int | None = Field(default=None)
remark: str | None = Field(default=None)
file_ids: list[int] = Field(default_factory=list)
clean_rules: CleanRules | None = Field(default=None)
smooth: SmoothConfig | None = Field(default=None)
auto_split: bool = Field(default=False)
row_start: int | None = Field(default=None, ge=1)
row_end: int | None = Field(default=None, ge=1)
class PreviewRequest(BaseModel):
file_ids: list[int] = Field(default_factory=list)
clean_rules: CleanRules | None = Field(default=None)
smooth: SmoothConfig | None = Field(default=None)
row_start: int | None = Field(default=None, ge=1)
row_end: int | None = Field(default=None, ge=1)
from pydantic import BaseModel, Field
class LSTMParams(BaseModel):
seq_len: int = Field(default=20, ge=5, le=500)
hidden_size: int = Field(default=64, ge=8, le=1024)
num_layers: int = Field(default=2, ge=1, le=8)
epochs: int = Field(default=50, ge=1, le=2000)
batch_size: int = Field(default=32, ge=1, le=512)
learning_rate: float = Field(default=0.001, gt=0, le=1)
class CreateTaskRequest(BaseModel):
model_name: str = Field(min_length=1, max_length=255)
train_package_id: int
val_package_id: int
params: LSTMParams = Field(default_factory=LSTMParams)
class SaveModelRequest(BaseModel):
model_name: str | None = Field(default=None, max_length=255)
description: str | None = None
class UpdateModelRequest(BaseModel):
model_name: str | None = Field(default=None, max_length=255)
description: str | None = None
...@@ -27,3 +27,15 @@ ...@@ -27,3 +27,15 @@
25,20.534,0.1796,23.4272,35.0 25,20.534,0.1796,23.4272,35.0
26,20.534,0.1796,23.4272,35.0 26,20.534,0.1796,23.4272,35.0
27,20.534,0.1949,23.4272,35.0 27,20.534,0.1949,23.4272,35.0
28,20.534,0.0,23.4272,35.0
29,20.534,0.1796,23.4272,35.0
30,20.534,0.1796,23.4272,35.0
31,20.534,0.0,23.4272,35.0
32,20.534,0.1796,23.4272,35.0
33,20.534,0.1796,23.4272,35.0
34,20.534,0.1949,23.4272,35.0
35,20.534,0.1949,23.4272,35.0
36,20.534,0.1949,23.4272,35.0
37,20.534,0.1949,23.4272,35.0
38,20.534,0.1949,23.4272,35.0
39,20.534,0.1354,23.4272,35.0
<script setup>
defineProps({
title: {
type: String,
default: '页面建设中',
},
})
</script>
<template>
<section class="uc-page">
<div class="uc-card">
<h2>{{ title }}</h2>
<p>该模块正在建设中,可先使用“数据管理”完成数据上传与查看。</p>
</div>
</section>
</template>
<style lang="scss" scoped>
.uc-page {
height: 100%;
display: grid;
place-items: center;
background: var(--bg-page);
}
.uc-card {
width: min(480px, 100%);
border-radius: 4px;
padding: 40px 32px;
background: var(--bg-white);
border: 1px solid var(--border-color);
box-shadow: var(--shadow-card);
text-align: center;
h2 {
margin: 0 0 12px;
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
}
p {
margin: 0;
color: var(--text-secondary);
font-size: 13px;
line-height: 1.6;
}
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment