Commit 8160a34d authored by luwei's avatar luwei

修改

parent 4601d53b
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.utils.response import success_response
......@@ -9,15 +9,6 @@ router = APIRouter()
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')
def get_categories():
return success_response(data=service.get_category_tree())
......@@ -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}')
def update_file(file_id: str, request: FileUpdateRequest):
try:
......
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.utils.response import success_response
......@@ -8,11 +8,6 @@ router = APIRouter()
service = EvalService()
class EvalRequest(BaseModel):
model_id: int
package_id: int
@router.get('/packages')
def list_packages(
category_id: str = Query(default=''),
......
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.utils.response import success_response
......@@ -8,33 +8,6 @@ router = APIRouter()
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')
......
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.utils.response import success_response
......@@ -9,31 +15,6 @@ router = APIRouter()
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')
def get_categories():
return success_response(data=service.get_category_tree())
......@@ -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')
def preview_package(request: PreviewRequest):
try:
......
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.utils.response import success_response
......@@ -8,34 +8,6 @@ router = APIRouter()
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 ──────────────────────────────────────────────────────────────────
@router.get('/packages')
......
......@@ -97,7 +97,8 @@ def train_lstm(
train_records: list of dicts for the training set.
val_records: list of dicts for the validation set.
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.
on_progress: callback(pct, epoch, train_loss, val_loss) called after each epoch.
cancel_event: when set, training stops with InterruptedError.
......@@ -114,6 +115,11 @@ def train_lstm(
batch_size = max(1, int(params.get('batch_size', 32)))
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 ────────────────────────────────────────────────────
train_data = _extract_features(train_records)
val_data = _extract_features(val_records)
......@@ -121,8 +127,8 @@ def train_lstm(
min_required = seq_len + 10
if len(train_data) < min_required:
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'
'\u8bf7\u68c0\u67e5\u6570\u636e\u5305\u5185\u5bb9\u6216\u51cf\u5c0f\u5e8f\u5217\u957f\u5ea6\u3002'
f'训练集有效数据量不足:需至少 {min_required} 条,当前仅 {len(train_data)} 条。'
'请检查数据包内容或减小序列长度。'
)
# min-max normalisation fitted on train set only
......@@ -161,134 +167,15 @@ def train_lstm(
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
criterion = nn.MSELoss()
train_loss = 0.0
val_loss: float | None = None
for epoch in range(epochs):
if cancel_event.is_set():
raise InterruptedError('\u8bad\u7ec3\u5df2\u53d6\u6d88')
# ── 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,
# ── scheduler ────────────────────────────────────────────────────────────
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer,
mode='min',
factor=lr_factor,
patience=lr_patience,
min_lr=lr_min,
)
# ── 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
val_loss: float | None = None
......@@ -316,16 +203,11 @@ def train_lstm(
val_pred = model(X_val_t)
val_loss = criterion(val_pred, y_val_t).item()
pct = int((epoch + 1) / epochs * 100)
on_progress(pct, train_loss, val_loss)
# 根据 val_loss 动态调整学习率
scheduler.step(val_loss)
# ── test-set evaluation (completely hidden during training) ──────────────
test_loss: float | None = None
if has_test:
model.eval()
with torch.no_grad():
test_pred = model(X_test_t)
test_loss = criterion(test_pred, y_test_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)
......@@ -348,5 +230,5 @@ def train_lstm(
return {
'train_loss': round(float(train_loss), 6),
'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 @@
25,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
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