Files
pickling-mes/backend/app/api/prediction.py
wangyu 193da0018f feat: 移除PDI和订单号字段,新增设备巡检模块
- 从物料跟踪页面移除订单号列和表单字段
- 从导航菜单移除PDI管理,添加设备巡检
- 新增InspectionLocation和InspectionRecord后端模型和API
- 新增设备巡检前端页面(左侧点位列表,右侧设备和历史记录)
2026-05-27 16:38:40 +08:00

295 lines
12 KiB
Python

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime
from app.schemas.common import Response
from app.services.auth_service import get_current_user
from app.services.prediction import (
AcidSpeedModel,
TensionModel,
QualityPredictionModel,
AcidConsumptionModel,
_load_cal,
_save_cal,
)
router = APIRouter()
# ─────────────────────────────────────────────────────────────────────────────
# Prediction request schemas
# ─────────────────────────────────────────────────────────────────────────────
class AcidSpeedRequest(BaseModel):
thickness: float = Field(..., gt=0)
width: float = Field(..., gt=0)
steel_grade: str
acid_conc_list: List[float]
acid_temp_list: List[float]
scale_weight: Optional[float] = 8.5
target_pi: Optional[float] = 95.0
class TensionRequest(BaseModel):
thickness: float = Field(..., gt=0)
width: float = Field(..., gt=0)
yield_strength: float = Field(..., gt=0)
tension_coef: Optional[float] = 0.25
class QualityRequest(BaseModel):
thickness: float = Field(..., gt=0)
avg_speed: float = Field(..., gt=0)
acid_conc_avg: float = Field(..., gt=0)
acid_temp_avg: float = Field(..., gt=0)
scale_weight: Optional[float] = 8.5
class ConsumptionRequest(BaseModel):
thickness: float = Field(..., gt=0)
width: float = Field(..., gt=0)
coil_weight_kg: float = Field(..., gt=0)
has_regen_station: Optional[bool] = True
# ─────────────────────────────────────────────────────────────────────────────
# Calibration request schemas
# ─────────────────────────────────────────────────────────────────────────────
class AcidCalibRequest(BaseModel):
# 需要重建模型实例的上下文参数
thickness: float = Field(..., gt=0)
width: float = Field(..., gt=0)
steel_grade: str
acid_conc_list: List[float]
acid_temp_list: List[float]
scale_weight: Optional[float] = 8.5
# 校准输入
actual_max_speed: float = Field(..., gt=0, description="实测质量合格时的最高速度 m/min")
actual_quality_ok: bool = Field(..., description="该速度下质量是否合格")
note: Optional[str] = None
class TensionCalibRequest(BaseModel):
thickness: float = Field(..., gt=0)
width: float = Field(..., gt=0)
yield_strength: float = Field(..., gt=0)
tension_coef: Optional[float] = 0.25
zone: str = Field(..., description="测量位置,如 s1_roller")
measured_kn: float = Field(..., gt=0, description="实测张力 kN")
note: Optional[str] = None
class QualityCalibRequest(BaseModel):
thickness: float = Field(..., gt=0)
avg_speed: float = Field(..., gt=0)
acid_conc_avg: float = Field(..., gt=0)
acid_temp_avg: float = Field(..., gt=0)
scale_weight: Optional[float] = 8.5
actual_grade: str = Field(..., description="实际质检等级 A1/A2/B1/B2/C")
note: Optional[str] = None
# ─────────────────────────────────────────────────────────────────────────────
# Helper: append calibration history
# ─────────────────────────────────────────────────────────────────────────────
def _append_history(model_key: str, k_before: float, k_after: float,
input_data: dict, note: str = ""):
cal = _load_cal()
history = cal.get("history", [])
history.insert(0, {
"ts": datetime.now().isoformat(timespec="seconds"),
"model": model_key,
"k_before": k_before,
"k_after": k_after,
"input": input_data,
"note": note or "",
})
cal["history"] = history[:100]
_save_cal(cal)
# ─────────────────────────────────────────────────────────────────────────────
# Prediction endpoints
# ─────────────────────────────────────────────────────────────────────────────
@router.post("/acid-speed", response_model=Response[dict])
async def predict_acid_speed(body: AcidSpeedRequest, _=Depends(get_current_user)):
try:
model = AcidSpeedModel(
thickness=body.thickness, width=body.width,
steel_grade=body.steel_grade,
acid_conc_list=body.acid_conc_list,
acid_temp_list=body.acid_temp_list,
scale_weight=body.scale_weight, target_pi=body.target_pi,
)
result = model.calculate()
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))
return Response.ok(result)
@router.post("/tension", response_model=Response[dict])
async def predict_tension(body: TensionRequest, _=Depends(get_current_user)):
model = TensionModel(
thickness=body.thickness, width=body.width,
yield_strength=body.yield_strength, tension_coef=body.tension_coef,
)
return Response.ok(model.calculate())
@router.post("/quality", response_model=Response[dict])
async def predict_quality(body: QualityRequest, _=Depends(get_current_user)):
model = QualityPredictionModel(
thickness=body.thickness, avg_speed=body.avg_speed,
acid_conc_avg=body.acid_conc_avg, acid_temp_avg=body.acid_temp_avg,
scale_weight=body.scale_weight,
)
return Response.ok(model.calculate())
@router.post("/consumption", response_model=Response[dict])
async def predict_consumption(body: ConsumptionRequest, _=Depends(get_current_user)):
model = AcidConsumptionModel(
thickness=body.thickness, width=body.width,
coil_weight_kg=body.coil_weight_kg,
has_regen_station=body.has_regen_station,
)
return Response.ok(model.calculate())
# ─────────────────────────────────────────────────────────────────────────────
# Calibration endpoints
# ─────────────────────────────────────────────────────────────────────────────
TENSION_ZONES = ["inlet","s1_roller","acid_entry","acid1","acid2","acid3","rinse","leveler","s2_roller","outlet"]
@router.get("/calibration", response_model=Response[dict])
async def get_calibration(_=Depends(get_current_user)):
"""返回各模型当前校准系数和历史记录"""
cal = _load_cal()
tension_zone_kcal = {
z: cal.get(f"tension_zone_{z}", 1.0) for z in TENSION_ZONES
}
return Response.ok({
"acid_speed_kcal": cal.get("acid_speed_kcal", 1.0),
"tension_zone_kcal": tension_zone_kcal,
"quality_kcal": cal.get("quality_kcal", 1.0),
"history": cal.get("history", []),
})
@router.post("/calibration/acid-speed", response_model=Response[dict])
async def calibrate_acid_speed(body: AcidCalibRequest, _=Depends(get_current_user)):
"""录入实测数据,更新酸洗速度模型校准系数"""
try:
model = AcidSpeedModel(
thickness=body.thickness, width=body.width,
steel_grade=body.steel_grade,
acid_conc_list=body.acid_conc_list,
acid_temp_list=body.acid_temp_list,
scale_weight=body.scale_weight,
)
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))
k_before = model.K_cal
predicted_speed = model.calculate()["max_speed"]
k_after = model.calibrate(
actual_max_speed=body.actual_max_speed,
actual_quality_ok=body.actual_quality_ok,
)
_append_history(
"acid_speed", k_before, k_after,
{"actual_speed": body.actual_max_speed,
"quality_ok": body.actual_quality_ok,
"predicted_speed": predicted_speed},
body.note or "",
)
return Response.ok({
"k_before": k_before,
"k_after": k_after,
"predicted_speed": predicted_speed,
"adjustment": round((k_after / k_before - 1) * 100, 2),
})
@router.post("/calibration/tension", response_model=Response[dict])
async def calibrate_tension(body: TensionCalibRequest, _=Depends(get_current_user)):
"""录入实测张力,仅更新指定区段的校准系数"""
model = TensionModel(
thickness=body.thickness, width=body.width,
yield_strength=body.yield_strength, tension_coef=body.tension_coef,
)
calc = model.calculate()
predicted_kn = calc["zones"].get(body.zone, {}).get("tension_kN", 0)
k_before = model.zone_kcal.get(body.zone, 1.0)
new_zone_kcal = model.calibrate(zone=body.zone, measured_kn=body.measured_kn)
k_after = new_zone_kcal.get(body.zone, 1.0)
_append_history(
"tension", k_before, k_after,
{"zone": body.zone,
"measured_kn": body.measured_kn,
"predicted_kn": predicted_kn},
body.note or "",
)
return Response.ok({
"zone": body.zone,
"k_before": k_before,
"k_after": k_after,
"predicted_kn": predicted_kn,
"measured_kn": body.measured_kn,
"adjustment": round((k_after / k_before - 1) * 100, 2),
"zone_kcal": new_zone_kcal,
})
@router.post("/calibration/quality", response_model=Response[dict])
async def calibrate_quality(body: QualityCalibRequest, _=Depends(get_current_user)):
"""录入实际质检等级,更新质量模型校准系数"""
model = QualityPredictionModel(
thickness=body.thickness, avg_speed=body.avg_speed,
acid_conc_avg=body.acid_conc_avg, acid_temp_avg=body.acid_temp_avg,
scale_weight=body.scale_weight,
)
k_before = model.K_cal
calc = model.calculate()
predicted_grade = calc["overall_grade"]
k_after = model.calibrate(actual_grade=body.actual_grade)
_append_history(
"quality", k_before, k_after,
{"actual_grade": body.actual_grade,
"predicted_grade": predicted_grade},
body.note or "",
)
return Response.ok({
"k_before": k_before,
"k_after": k_after,
"predicted_grade": predicted_grade,
"actual_grade": body.actual_grade,
"adjustment": round((k_after / k_before - 1) * 100, 2),
})
@router.post("/calibration/reset/{model_key}", response_model=Response[dict])
async def reset_calibration(model_key: str, _=Depends(get_current_user)):
"""将指定模型的校准系数全部重置为 1.0"""
cal = _load_cal()
if model_key == "tension":
# 重置所有区段
for z in TENSION_ZONES:
cal[f"tension_zone_{z}"] = 1.0
_append_history("tension", None, 1.0, {"action": "reset_all_zones"})
elif model_key in ("acid_speed", "quality"):
key = f"{model_key}_kcal"
k_before = cal.get(key, 1.0)
cal[key] = 1.0
_append_history(model_key, k_before, 1.0, {"action": "reset"})
else:
raise HTTPException(status_code=404, detail="未知模型")
_save_cal(cal)
return Response.ok({"model": model_key, "reset": True})