295 lines
12 KiB
Python
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})
|