From 6ae24cb14de9fcea5fc9b9582b715b8819a31df8 Mon Sep 17 00:00:00 2001 From: wangyu <823267011@qq.com> Date: Wed, 27 May 2026 17:31:25 +0800 Subject: [PATCH] Add PyTorch/ONNX prediction models with physics fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Train 3 MLP networks (acid speed 14→1, tension 4→10, quality 6→2) on 12,000 synthetic samples generated from physics models + noise - Export pre-trained ONNX models to pt_models/ directory - Rewrite prediction.py: ONNX inference first, physics fallback if unavailable - Add onnxruntime + numpy to requirements.txt (Aliyun mirror for Docker) - Use Tsinghua mirror in Dockerfile for pip installs Co-Authored-By: Claude Sonnet 4.6 --- backend/Dockerfile | 2 +- backend/app/services/prediction.py | 584 ++++++++---------- .../app/services/pt_models/acid_speed.onnx | Bin 0 -> 50252 bytes backend/app/services/pt_models/quality.onnx | Bin 0 -> 11189 bytes backend/app/services/pt_models/scalers.json | 118 ++++ backend/app/services/pt_models/tension.onnx | Bin 0 -> 28653 bytes backend/requirements.txt | 2 + backend/train_models.py | 256 ++++++++ 8 files changed, 642 insertions(+), 320 deletions(-) create mode 100644 backend/app/services/pt_models/acid_speed.onnx create mode 100644 backend/app/services/pt_models/quality.onnx create mode 100644 backend/app/services/pt_models/scalers.json create mode 100644 backend/app/services/pt_models/tension.onnx create mode 100644 backend/train_models.py diff --git a/backend/Dockerfile b/backend/Dockerfile index c57f001..cce6ab9 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,6 +1,6 @@ FROM python:3.12-slim WORKDIR /app COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt COPY . . CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/app/services/prediction.py b/backend/app/services/prediction.py index 84432f9..600ef38 100644 --- a/backend/app/services/prediction.py +++ b/backend/app/services/prediction.py @@ -1,33 +1,22 @@ """ -工艺预测模型 — 灰箱(Gray-box)架构 +工艺预测模型 — 灰箱物理模型 + ONNX 神经网络双栈 -设计思路: - 物理结构来自 Arrhenius 酸洗动力学,参数取自公开文献实验值, - 而非理论推导。每个模型内置校准系数 K_cal(初始=1.0), - 投产后可通过 calibrate() 方法用实测结果回归更新, - 使模型随数据积累逐步收敛到真实工况。 +推理优先级: + 1. ONNX 模型(onnxruntime,若 pt_models/ 目录存在则加载) + 2. 物理灰箱模型(Arrhenius 解析解,始终可用) -关键文献依据: - [1] 碳钢 HCl 酸洗活化能:Ea ≈ 40~50 kJ/mol(实验测定均值取 45 kJ/mol) - 来源:Hydrochloric Acid Pickling Process Optimization in Metal Wire, - IJSSST Vol-16 No-5; IspatGuru Pickling of Hot Rolled Strip - [2] H⁺ 浓度动力学阶次:1.0~2.0阶(取保守值 1.2) - 来源:Optimizing pickling process for 30Cr13 steel, - ScienceDirect 2025; neural network MPC studies - [3] 温度效应校验:速率每升温 6~8°C 翻倍(Ea≈45 kJ/mol 时对应约 7°C) - [4] 欠酸洗风险判别特征:strip thickness, speed, conc, temp - 来源:Prediction of under pickling defects on steel strip surface, - arXiv:1207.0911 - [5] 速度优化:Nelder-Mead simplex 已在实际 1450mm 酸洗线验证 - 来源:Zhu et al., Advances in Mechanical Engineering, 2016 +训练:运行 backend/train_models.py 重新生成 pt_models/*.onnx +校准:K_cal 系数持久化在 cal_coeffs.json,两个栈都使用同一套 K_cal """ import math import json import os +from pathlib import Path from typing import List, Dict, Any, Optional, Tuple +from loguru import logger -# ── 校准系数持久化路径 ──────────────────────────────────────────────────────── -_CAL_FILE = os.path.join(os.path.dirname(__file__), "cal_coeffs.json") +# ── 校准系数持久化 ──────────────────────────────────────────────────────────── +_CAL_FILE = Path(__file__).parent / "cal_coeffs.json" def _load_cal() -> Dict[str, float]: try: @@ -41,56 +30,78 @@ def _save_cal(d: Dict[str, float]): json.dump(d, f, indent=2) +# ── ONNX 推理层 ─────────────────────────────────────────────────────────────── +_PT_DIR = Path(__file__).parent / "pt_models" +_scalers: Optional[Dict] = None +_sess: Dict[str, Any] = {} + +try: + import onnxruntime as ort + import numpy as _np + + _sp = _PT_DIR / "scalers.json" + if _sp.exists(): + with open(_sp) as f: + _scalers = json.load(f) + + for _name in ("acid_speed", "tension", "quality"): + _p = _PT_DIR / f"{_name}.onnx" + if _p.exists(): + _sess[_name] = ort.InferenceSession( + str(_p), providers=["CPUExecutionProvider"] + ) + if _sess: + logger.info(f"PT models loaded: {list(_sess.keys())}") +except ImportError: + logger.warning("onnxruntime not installed — using physics fallback") + + +def _pt_infer(name: str, x_raw: List[float]) -> Optional[List[float]]: + """标准化 → ONNX 推理 → 反标准化,返回输出向量;失败返回 None。""" + if name not in _sess or _scalers is None: + return None + try: + sc = _scalers[name] + xm = _np.array(sc["X_mean"], dtype=_np.float32) + xs = _np.array(sc["X_std"], dtype=_np.float32) + ym = _np.array(sc["y_mean"], dtype=_np.float32) + ys = _np.array(sc["y_std"], dtype=_np.float32) + x = (_np.array(x_raw, dtype=_np.float32) - xm) / xs + raw = _sess[name].run(None, {"input": x.reshape(1, -1)})[0][0] + return (raw * ys + ym).tolist() + except Exception as e: + logger.warning(f"PT infer {name} failed: {e}") + return None + + # ───────────────────────────────────────────────────────────────────────────── -# 1. 酸洗速度模型(Gray-box) +# 1. 酸洗速度模型 # ───────────────────────────────────────────────────────────────────────────── class AcidSpeedModel: """ - 基于文献实测参数的 Arrhenius 灰箱模型。 - - 与上一版本的关键差异: - - Ea/R: 3000 K → 5413 K(45 kJ/mol 实验值,文献[1]) - - 浓度指数: 0.6 → 1.2(H⁺ 二阶动力学,文献[2]) - - 增加氧化铁皮结构修正(FeO/Fe₃O₄双层模型,文献[4]) - - 内置 K_cal 校准系数,支持投产后在线标定 + 灰箱: Arrhenius 动力学 + 二分搜索 + PT栈: 14 维输入 → 最大速度 (m/min) + 输入: [thickness, scale_weight, conc×6, temp×6] """ + TANK_LENGTH = 18.0 + NUM_TANKS = 6 + K0 = 0.075 + EA_R = 5413.0 + T_REF = 348.15 + C_REF = 180.0 + N_CONC = 1.2 + V_MIN = 20.0 + V_MAX = 180.0 + SCALE_RATE_FACTOR = 0.70 * 1.0 + 0.20 * 0.25 + 0.10 * 0.15 + CAL_KEY = "acid_speed_kcal" - # 文献实验值(碳钢 HCl 连续酸洗) - TANK_LENGTH = 18.0 # m,单槽有效长度(按设备规格书) - NUM_TANKS = 6 - # K0 由实际工况反推: - # 目标:75°C、180 g/L 正常酸液条件下,最大速度约 120~130 m/min(PI≥95%) - # 推导:t_total = 6×18/(125/60)=51.8s,k=ln(20)/51.8=0.058 s⁻¹ - # k=K0×SCALE_RATE_FACTOR → K0=0.058/0.765≈0.075 s⁻¹ - K0 = 0.075 # 指前因子 s⁻¹,由设备规格反推标定 - EA_R = 5413.0 # Ea/R (K),Ea=45 kJ/mol / R=8.314(文献实验值[1]) - T_REF = 348.15 # 参考温度 75°C (K) - C_REF = 180.0 # 参考游离酸浓度 g/L - N_CONC = 1.2 # 浓度动力学阶次(文献[2] 取保守值) - V_MIN = 20.0 - V_MAX = 180.0 - CAL_KEY = "acid_speed_kcal" - - # 氧化铁皮结构系数(FeO 快速溶解 + Fe₃O₄ 慢速溶解,文献[4]) - # 热轧碳钢铁皮组成约:FeO 70%,Fe₃O₄ 20%,Fe₂O₃ 10% - # FeO 溶速约为 Fe₃O₄ 的 4 倍;有效速率取加权平均 - SCALE_RATE_FACTOR = 0.70 * 1.0 + 0.20 * 0.25 + 0.10 * 0.15 # ≈ 0.765 - - def __init__( - self, - thickness: float, # mm - width: float, # mm - steel_grade: str, - acid_conc_list: List[float], # 各槽游离酸 g/L - acid_temp_list: List[float], # 各槽温度 °C - scale_weight: float = 8.5, # g/m²,氧化铁皮重量 - target_pi: float = 95.0, - ): + def __init__(self, thickness, width, steel_grade, + acid_conc_list, acid_temp_list, + scale_weight=8.5, target_pi=95.0): if len(acid_conc_list) != self.NUM_TANKS: raise ValueError(f"acid_conc_list 需要 {self.NUM_TANKS} 个元素") if len(acid_temp_list) != self.NUM_TANKS: raise ValueError(f"acid_temp_list 需要 {self.NUM_TANKS} 个元素") - self.thickness = thickness self.width = width self.steel_grade = steel_grade @@ -100,110 +111,87 @@ class AcidSpeedModel: self.target_pi = target_pi self.K_cal = _load_cal().get(self.CAL_KEY, 1.0) - def _k_i(self, conc: float, temp_c: float) -> float: - """单槽有效酸洗速率常数(含文献参数 + 铁皮结构修正)""" - T_k = temp_c + 273.15 - arrhenius = math.exp(-self.EA_R * (1.0 / T_k - 1.0 / self.T_REF)) - conc_factor = max(conc / self.C_REF, 0.01) ** self.N_CONC - # 铁皮越厚,有效接触面积越低(正比于 1/scale_weight^0.3 经验修正) - scale_corr = (8.5 / max(self.scale_weight, 1.0)) ** 0.3 - return self.K0 * arrhenius * conc_factor * self.SCALE_RATE_FACTOR * scale_corr * self.K_cal + def _k_i(self, conc, temp_c): + T_k = temp_c + 273.15 + arrhenius = math.exp(-self.EA_R * (1.0/T_k - 1.0/self.T_REF)) + c_factor = max(conc/self.C_REF, 0.01) ** self.N_CONC + scale_corr = (8.5 / max(self.scale_weight, 1.0)) ** 0.3 + return self.K0 * arrhenius * c_factor * self.SCALE_RATE_FACTOR * scale_corr * self.K_cal - def _compute_pi(self, v_mpm: float) -> Tuple[float, List[float], List[float]]: - v_mps = v_mpm / 60.0 - pi_prev = 0.0 - pi_per_tank, rt_per_tank = [], [] + def _compute_pi(self, v_mpm): + v_mps = v_mpm / 60.0 + pi = 0.0 + pp, rt = [], [] for i in range(self.NUM_TANKS): - t_i = self.TANK_LENGTH / v_mps - k_i = self._k_i(self.acid_conc_list[i], self.acid_temp_list[i]) - # 精确解析解:dPI/dt = k*(1-PI/100) → PI_new = 100-(100-PI_old)*exp(-k*t) - # 避免 Euler 一阶近似在 k*t 较大时的严重失真 - pi_prev = 100.0 - (100.0 - pi_prev) * math.exp(-k_i * t_i) - pi_per_tank.append(round(pi_prev, 2)) - rt_per_tank.append(round(t_i, 1)) - return pi_prev, pi_per_tank, rt_per_tank + t_i = self.TANK_LENGTH / v_mps + k_i = self._k_i(self.acid_conc_list[i], self.acid_temp_list[i]) + pi = 100.0 - (100.0 - pi) * math.exp(-k_i * t_i) + pp.append(round(pi, 2)); rt.append(round(t_i, 1)) + return pi, pp, rt - def calculate(self) -> Dict[str, Any]: - # Nelder-Mead 单维退化为二分搜索(文献[5]验证有效) - pi_at_min, _, _ = self._compute_pi(self.V_MIN) - if pi_at_min < self.target_pi: + def _risk_level(self, speed, pi): + avg_conc = sum(self.acid_conc_list)/len(self.acid_conc_list) + avg_temp = sum(self.acid_temp_list)/len(self.acid_temp_list) + s = 0 + if pi < 85: s += 3 + elif pi < 92: s += 1 + if speed > 140: s += 2 + if avg_conc < 120: s += 2 + if avg_temp < 68: s += 2 + if self.thickness > 4.0: s += 1 + return "HIGH" if s >= 5 else "MEDIUM" if s >= 2 else "LOW" + + def _physics_result(self): + pi_min, _, _ = self._compute_pi(self.V_MIN) + if pi_min < self.target_pi: pi, pp, rt = self._compute_pi(self.V_MIN) return { - "max_speed": self.V_MIN, - "pi_per_tank": pp, - "residence_time_per_tank": rt, - "total_pi": round(pi, 2), + "max_speed": self.V_MIN, "pi_per_tank": pp, + "residence_time_per_tank": rt, "total_pi": round(pi, 2), "under_pickling_risk": self._risk_level(self.V_MIN, pi), - "warning": "酸液条件不足,即使最低速下酸洗指数仍低于目标,请检查酸浓度和温度", - "K_cal": self.K_cal, + "warning": "酸液条件不足,建议检查酸浓度和温度", + "K_cal": self.K_cal, "source": "physics", } - - lo, hi, best_v = self.V_MIN, self.V_MAX, self.V_MIN + lo, hi, best = self.V_MIN, self.V_MAX, self.V_MIN while hi - lo >= 0.5: mid = (lo + hi) / 2.0 - pi_mid, _, _ = self._compute_pi(mid) - if pi_mid >= self.target_pi: - best_v = mid; lo = mid + 0.5 + if self._compute_pi(mid)[0] >= self.target_pi: + best = mid; lo = mid + 0.5 else: hi = mid - 0.5 - - best_v = math.floor(best_v) - total_pi, pi_per_tank, rt_per_tank = self._compute_pi(best_v) - + best = math.floor(best) + pi, pp, rt = self._compute_pi(best) return { - "max_speed": best_v, - "pi_per_tank": pi_per_tank, - "residence_time_per_tank": rt_per_tank, - "total_pi": round(total_pi, 2), - "under_pickling_risk": self._risk_level(best_v, total_pi), - "warning": None, - "K_cal": self.K_cal, + "max_speed": best, "pi_per_tank": pp, + "residence_time_per_tank": rt, "total_pi": round(pi, 2), + "under_pickling_risk": self._risk_level(best, pi), + "warning": None, "K_cal": self.K_cal, "source": "physics", } - def _risk_level(self, speed: float, pi: float) -> str: - """ - 欠酸洗风险评估(文献[4] decision-tree 特征阈值) - 输入:speed(m/min), pi(%),结合厚度、浓度综合判断 - """ - avg_conc = sum(self.acid_conc_list) / len(self.acid_conc_list) - avg_temp = sum(self.acid_temp_list) / len(self.acid_temp_list) - # 文献给出的欠酸洗高风险条件组合 - risk_score = 0 - if pi < 85: risk_score += 3 - elif pi < 92: risk_score += 1 - if speed > 140: risk_score += 2 - if avg_conc < 120: risk_score += 2 - if avg_temp < 68: risk_score += 2 - if self.thickness > 4.0: risk_score += 1 + def calculate(self) -> Dict[str, Any]: + x = [self.thickness, self.scale_weight] + self.acid_conc_list + self.acid_temp_list + pt = _pt_infer("acid_speed", x) + if pt is not None: + raw_speed = pt[0] * self.K_cal + best = int(max(self.V_MIN, min(self.V_MAX, round(raw_speed)))) + pi, pp, rt = self._compute_pi(best) + return { + "max_speed": best, "pi_per_tank": pp, + "residence_time_per_tank": rt, "total_pi": round(pi, 2), + "under_pickling_risk": self._risk_level(best, pi), + "warning": None, "K_cal": self.K_cal, "source": "pt", + } + return self._physics_result() - if risk_score >= 5: return "HIGH" - elif risk_score >= 2: return "MEDIUM" - else: return "LOW" - - def calibrate(self, actual_max_speed: float, - actual_quality_ok: bool) -> float: - """ - 投产后标定接口: - 传入某卷的实际最大可用速度(操作员确认质量合格时的速度), - 用简单比例更新 K_cal,使模型逐步向真实工况收敛。 - - actual_max_speed: 实际测得质量合格的最高速度 (m/min) - actual_quality_ok: True=该速度下质量合格,False=出现欠酸洗 - """ + def calibrate(self, actual_max_speed, actual_quality_ok): predicted = self.calculate()["max_speed"] if not actual_quality_ok: - # 预测速度偏高,缩减 K_cal - adjustment = 0.95 + adj = 0.95 else: ratio = actual_max_speed / max(predicted, 1.0) - # 平滑更新,避免单次样本过拟合 - adjustment = 1.0 + 0.3 * (ratio - 1.0) - adjustment = max(0.7, min(1.3, adjustment)) - - self.K_cal = round(self.K_cal * adjustment, 4) - cal = _load_cal() - cal[self.CAL_KEY] = self.K_cal - _save_cal(cal) + adj = max(0.7, min(1.3, 1.0 + 0.3*(ratio - 1.0))) + self.K_cal = round(self.K_cal * adj, 4) + cal = _load_cal(); cal[self.CAL_KEY] = self.K_cal; _save_cal(cal) return self.K_cal @@ -212,106 +200,91 @@ class AcidSpeedModel: # ───────────────────────────────────────────────────────────────────────────── class TensionModel: """ - 张力模型:基于截面积×屈服强度,区间比例系数参考酸洗线工程手册。 - 每个区段独立校准系数 zone_kcal[zone],互不干扰。 + 灰箱: T_base = coef × σ_yield × A,各区段比例系数 + PT栈: 4 维输入 → 10 区段张力 kN + 输入: [thickness, width, yield_strength, tension_coef] """ - - # 各区基准比例系数(酸洗线工程实践均值) ZONE_RATIOS = { - "inlet": 1.00, - "s1_roller": 0.85, - "acid_entry": 0.78, - "acid1": 0.72, - "acid2": 0.68, - "acid3": 0.68, - "rinse": 0.70, - "leveler": 0.76, - "s2_roller": 0.88, - "outlet": 1.00, + "inlet": 1.00, "s1_roller": 0.85, "acid_entry": 0.78, + "acid1": 0.72, "acid2": 0.68, "acid3": 0.68, + "rinse": 0.70, "leveler": 0.76, "s2_roller": 0.88, "outlet": 1.00, } ZONE_NAMES_CN = { - "inlet": "入口张力辊", - "s1_roller": "S1夹送辊", - "acid_entry": "酸洗入口辊", - "acid1": "1#酸槽", - "acid2": "2#酸槽", - "acid3": "3#酸槽", - "rinse": "漂洗段辊", - "leveler": "拉矫机", - "s2_roller": "S2夹送辊", - "outlet": "出口张力辊", + "inlet": "入口张力辊", "s1_roller": "S1夹送辊", + "acid_entry": "酸洗入口辊", "acid1": "1#酸槽", + "acid2": "2#酸槽", "acid3": "3#酸槽", + "rinse": "漂洗段辊", "leveler": "拉矫机", + "s2_roller": "S2夹送辊", "outlet": "出口张力辊", } @staticmethod - def _zone_cal_key(zone: str) -> str: - return f"tension_zone_{zone}" + def _zone_cal_key(zone): return f"tension_zone_{zone}" - def __init__( - self, - thickness: float, - width: float, - yield_strength: float, - tension_coef: float = 0.25, - ): + def __init__(self, thickness, width, yield_strength, tension_coef=0.25): self.thickness = thickness self.width = width self.yield_strength = yield_strength self.tension_coef = tension_coef cal = _load_cal() - # 每个区段独立加载自己的校准系数,默认 1.0 - self.zone_kcal: Dict[str, float] = { - z: cal.get(self._zone_cal_key(z), 1.0) - for z in self.ZONE_RATIOS - } - - def calculate(self) -> Dict[str, Any]: - cross_section = self.thickness * self.width # mm² - # T_max 是理论基准值(不含区段校准,区段校准在各 zone 内单独乘) - t_base_kn = (self.tension_coef * self.yield_strength - * cross_section / 1000.0) # kN + self.zone_kcal = {z: cal.get(self._zone_cal_key(z), 1.0) for z in self.ZONE_RATIOS} + def _physics_zones(self, t_base_kn): zones = {} for zone, ratio in self.ZONE_RATIOS.items(): - k = self.zone_kcal.get(zone, 1.0) + k = self.zone_kcal[zone] zones[zone] = { "tension_kN": round(t_base_kn * ratio * k, 2), - "ratio": ratio, - "k_cal": k, - "name_cn": self.ZONE_NAMES_CN[zone], + "ratio": ratio, "k_cal": k, + "name_cn": self.ZONE_NAMES_CN[zone], } + return zones + + def calculate(self) -> Dict[str, Any]: + cross = self.thickness * self.width + t_base = self.tension_coef * self.yield_strength * cross / 1000.0 + + pt = _pt_infer("tension", [self.thickness, self.width, self.yield_strength, self.tension_coef]) + if pt is not None and _scalers and "tension" in _scalers: + zone_names = _scalers["tension"].get("zone_names", list(self.ZONE_RATIOS.keys())) + zones = {} + for i, zone in enumerate(zone_names): + k = self.zone_kcal.get(zone, 1.0) + kn = round(max(0.1, pt[i]) * k, 2) + zones[zone] = { + "tension_kN": kn, + "ratio": self.ZONE_RATIOS.get(zone, 1.0), + "k_cal": k, + "name_cn": self.ZONE_NAMES_CN.get(zone, zone), + } + source = "pt" + else: + zones = self._physics_zones(t_base) + source = "physics" density = 7850.0 - mass_per_m = density * (self.thickness / 1000.0) * (self.width / 1000.0) - accel_kn = round(mass_per_m * (30.0 / 60.0) / 1000.0, 3) - t_max_kn = round(t_base_kn * self.zone_kcal.get("inlet", 1.0), 2) + mass_per_m = density * (self.thickness/1000.0) * (self.width/1000.0) + accel_kn = round(mass_per_m * (30.0/60.0) / 1000.0, 3) + t_max = round(t_base * self.zone_kcal.get("inlet", 1.0), 2) return { - "T_max": t_max_kn, - "T_base": round(t_base_kn, 2), - "cross_section_mm2": round(cross_section, 1), - "zones": zones, + "T_max": t_max, "T_base": round(t_base, 2), + "cross_section_mm2": round(cross, 1), + "zones": zones, "weld_speed_limit": 60.0, - "weld_tension_kN": round(t_max_kn * 0.60, 2), - "accel_tension": accel_kn, - "zone_kcal": self.zone_kcal, + "weld_tension_kN": round(t_max * 0.60, 2), + "accel_tension": accel_kn, + "zone_kcal": self.zone_kcal, + "source": source, } - def calibrate(self, zone: str, measured_kn: float) -> Dict[str, float]: - """仅更新指定区段的校准系数,其他区段不变""" + def calibrate(self, zone, measured_kn): if zone not in self.ZONE_RATIOS: raise ValueError(f"未知区段: {zone}") - t_base = (self.tension_coef * self.yield_strength - * self.thickness * self.width / 1000.0) - predicted = t_base * self.ZONE_RATIOS[zone] * self.zone_kcal[zone] - ratio = measured_kn / max(predicted, 0.1) - # 平滑更新,步长 40%,范围限制在 [0.5, 2.0] - adjustment = 1.0 + 0.4 * (ratio - 1.0) - adjustment = max(0.5, min(2.0, adjustment)) - new_k = round(self.zone_kcal[zone] * adjustment, 4) - self.zone_kcal[zone] = new_k - cal = _load_cal() - cal[self._zone_cal_key(zone)] = new_k - _save_cal(cal) + t_base = self.tension_coef * self.yield_strength * self.thickness * self.width / 1000.0 + pred = t_base * self.ZONE_RATIOS[zone] * self.zone_kcal[zone] + adj = max(0.5, min(2.0, 1.0 + 0.4*(measured_kn/max(pred,0.1) - 1.0))) + self.zone_kcal[zone] = round(self.zone_kcal[zone] * adj, 4) + cal = _load_cal(); cal[self._zone_cal_key(zone)] = self.zone_kcal[zone]; _save_cal(cal) return self.zone_kcal @@ -320,29 +293,18 @@ class TensionModel: # ───────────────────────────────────────────────────────────────────────────── class QualityPredictionModel: """ - 欠酸洗风险 + 质量等级预测。 - - v2 变化: - - 使用与 AcidSpeedModel 一致的文献参数(Ea/R=5413, n=1.2) - - 欠酸洗风险特征阈值参考 arXiv:1207.0911 的 decision-tree 结论 - - 增加铁离子浓度(FeCl₂)对酸洗能力的抑制修正 - - 支持投产后用实际质量等级校准评分阈值 + 灰箱: Arrhenius PI 计算 + 速度惩罚 + PT栈: 6 维输入 → [pi_score, surface_score] + 输入: [thickness, avg_speed, acid_conc_avg, acid_temp_avg, scale_weight, fe_conc_avg] """ - EA_R = 5413.0 - T_REF = 348.15 - C_REF = 180.0 - N_CONC = 1.2 + EA_R = 5413.0 + T_REF = 348.15 + C_REF = 180.0 + N_CONC = 1.2 CAL_KEY = "quality_kcal" - def __init__( - self, - thickness: float, - avg_speed: float, - acid_conc_avg: float, # 游离酸均值 g/L - acid_temp_avg: float, # 温度均值 °C - scale_weight: float = 8.5, - fe_conc_avg: float = 60.0, # FeCl₂ 浓度 g/L(铁离子抑制效应) - ): + def __init__(self, thickness, avg_speed, acid_conc_avg, acid_temp_avg, + scale_weight=8.5, fe_conc_avg=60.0): self.thickness = thickness self.avg_speed = avg_speed self.acid_conc_avg = acid_conc_avg @@ -351,108 +313,96 @@ class QualityPredictionModel: self.fe_conc_avg = fe_conc_avg self.K_cal = _load_cal().get(self.CAL_KEY, 1.0) - def _pickling_index_score(self) -> float: - T_k = self.acid_temp_avg + 273.15 - arrhenius = math.exp(-self.EA_R * (1.0 / T_k - 1.0 / self.T_REF)) - conc_factor = max(self.acid_conc_avg / self.C_REF, 0.01) ** self.N_CONC - # 铁离子抑制:FeCl₂ > 80 g/L 时显著降低酸洗速率(文献经验) - fe_inhibition = 1.0 - max(0.0, (self.fe_conc_avg - 80.0) / 200.0) * 0.35 - scale_corr = (8.5 / max(self.scale_weight, 1.0)) ** 0.3 - exposure = (1.20 * arrhenius * conc_factor * fe_inhibition - * scale_corr * 18.0 * 6) / (self.avg_speed / 60.0) - pi_score = 100.0 * (1.0 - math.exp(-exposure / 10.0)) - return min(max(pi_score * self.K_cal, 0.0), 100.0) + def _pi(self): + T_k = self.acid_temp_avg + 273.15 + arr = math.exp(-self.EA_R*(1.0/T_k - 1.0/self.T_REF)) + c_factor = max(self.acid_conc_avg/self.C_REF, 0.01)**self.N_CONC + fe_inh = 1.0 - max(0.0, (self.fe_conc_avg-80.0)/200.0)*0.35 + scale_corr = (8.5/max(self.scale_weight,1.0))**0.3 + exposure = (1.2*arr*c_factor*fe_inh*scale_corr*18.0*6) / (self.avg_speed/60.0) + return min(max(100.0*(1.0-math.exp(-exposure/10.0))*self.K_cal, 0), 100) - def _surface_score(self, pi_score: float) -> float: - # 最优速度区间 80-140 m/min(文献[4] 欠酸洗风险判别边界) + def _surface(self, pi): if self.avg_speed < 60: - speed_score = 80.0 + ss = 80.0 elif self.avg_speed <= 140: - speed_score = 80.0 + 15.0 * (self.avg_speed - 60) / 80.0 + ss = 80.0 + 15.0*(self.avg_speed-60)/80.0 else: - over = (self.avg_speed - 140) / 40.0 - speed_score = 95.0 - 30.0 * over - return min(max(pi_score * 0.65 + speed_score * 0.35, 0.0), 100.0) + ss = 95.0 - 30.0*((self.avg_speed-140)/40.0) + return min(max(pi*0.65 + ss*0.35, 0), 100) - def _grade(self, pi: float, surface: float) -> str: - c = (pi + surface) / 2.0 + def _grade(self, pi, suf): + c = (pi+suf)/2.0 if c >= 90: return "A1" if c >= 80: return "A2" if c >= 70: return "B1" if c >= 60: return "B2" return "C" - def _recommendations(self, pi: float, surface: float) -> List[str]: + def _recommendations(self, pi, suf): recs = [] if self.fe_conc_avg > 80: - recs.append(f"铁离子浓度偏高({self.fe_conc_avg:.0f} g/L),酸洗能力受抑制,建议加速换酸或补充新酸") + recs.append(f"铁离子浓度偏高({self.fe_conc_avg:.0f} g/L),建议加速换酸") if pi < 80: - recs.append("酸洗指数偏低,建议提高酸液浓度至 180 g/L 以上,或将温度升至 80°C") + recs.append("酸洗指数偏低,建议提高酸液浓度至 180 g/L 以上,或升温至 80°C") if pi < 65: - recs.append(f"欠酸洗风险高,建议将线速降至 {max(self.avg_speed*0.75, 20):.0f} m/min 以下") + recs.append(f"欠酸洗风险高,建议将线速降至 {max(self.avg_speed*0.75, 20):.0f} m/min") if self.acid_temp_avg < 70: recs.append(f"酸液温度偏低({self.acid_temp_avg:.1f}°C),建议升温至 75~85°C") if self.acid_conc_avg < 120: - recs.append(f"游离酸浓度偏低({self.acid_conc_avg:.0f} g/L),建议补充新酸至 150 g/L") + recs.append(f"游离酸浓度偏低({self.acid_conc_avg:.0f} g/L),建议补充新酸") if self.avg_speed > 150: - recs.append(f"线速过高({self.avg_speed:.0f} m/min),欠酸洗风险,建议不超过 140 m/min") + recs.append(f"线速过高({self.avg_speed:.0f} m/min),欠酸洗风险") if self.scale_weight > 12.0: - recs.append(f"氧化铁皮偏重({self.scale_weight:.1f} g/m²),建议检查加热炉气氛控制") + recs.append(f"氧化铁皮偏重({self.scale_weight:.1f} g/m²),建议检查加热炉气氛") if not recs: recs.append("工艺参数在正常范围内,当前设定可继续保持") return recs def calculate(self) -> Dict[str, Any]: - pi = round(self._pickling_index_score(), 1) - surface = round(self._surface_score(pi), 1) + x = [self.thickness, self.avg_speed, self.acid_conc_avg, + self.acid_temp_avg, self.scale_weight, self.fe_conc_avg] + pt = _pt_infer("quality", x) + + if pt is not None: + pi = round(float(min(max(pt[0]*self.K_cal, 0), 100)), 1) + suf = round(float(min(max(pt[1]*self.K_cal, 0), 100)), 1) + src = "pt" + else: + pi = round(self._pi(), 1) + suf = round(self._surface(pi), 1) + src = "physics" + return { - "pi_score": pi, - "surface_score": surface, - "overall_grade": self._grade(pi, surface), - "recommendations": self._recommendations(pi, surface), - "K_cal": self.K_cal, + "pi_score": pi, "surface_score": suf, + "overall_grade": self._grade(pi, suf), + "recommendations": self._recommendations(pi, suf), + "K_cal": self.K_cal, "source": src, } - def calibrate(self, actual_grade: str) -> float: - """传入实际质检等级,更新评分校准系数""" + def calibrate(self, actual_grade): grade_map = {"A1": 95, "A2": 85, "B1": 75, "B2": 65, "C": 50} - actual_score = grade_map.get(actual_grade, 75) - result = self.calculate() - predicted_score = (result["pi_score"] + result["surface_score"]) / 2.0 - ratio = actual_score / max(predicted_score, 1.0) - adjustment = 1.0 + 0.3 * (ratio - 1.0) - adjustment = max(0.7, min(1.3, adjustment)) - self.K_cal = round(self.K_cal * adjustment, 4) - cal = _load_cal() - cal[self.CAL_KEY] = self.K_cal - _save_cal(cal) + target = grade_map.get(actual_grade, 75) + res = self.calculate() + pred = (res["pi_score"] + res["surface_score"]) / 2.0 + adj = max(0.7, min(1.3, 1.0 + 0.3*(target/max(pred,1.0) - 1.0))) + self.K_cal = round(self.K_cal * adj, 4) + cal = _load_cal(); cal[self.CAL_KEY] = self.K_cal; _save_cal(cal) return self.K_cal # ───────────────────────────────────────────────────────────────────────────── -# 4. 消耗预测模型 +# 4. 消耗预测模型(无 PT 版本,定额+修正公式足够) # ───────────────────────────────────────────────────────────────────────────── class AcidConsumptionModel: - """ - 单卷资源消耗预测。 - 单位消耗定额取自浙江企鹅1250mm规格书; - 酸耗额外引入铁离子浓度修正(FeCl₂ 越高酸液越快失效,换酸频率越高)。 - """ + ACID_WITH_REGEN = 2.0 + ACID_WITHOUT_REGEN = 35.0 + STEAM_UNIT = 39.8 + POWER_UNIT = 14.0 + COOLING_UNIT = 1.21 - ACID_WITH_REGEN = 2.0 # kg/t - ACID_WITHOUT_REGEN = 35.0 # kg/t - STEAM_UNIT = 39.8 # kg/t - POWER_UNIT = 14.0 # kWh/t - COOLING_UNIT = 1.21 # m³/t - - def __init__( - self, - thickness: float, - width: float, - coil_weight_kg: float, - has_regen_station: bool = True, - fe_conc_avg: float = 60.0, # FeCl₂ g/L,影响换酸频率 - ): + def __init__(self, thickness, width, coil_weight_kg, + has_regen_station=True, fe_conc_avg=60.0): self.thickness = thickness self.width = width self.coil_weight_kg = coil_weight_kg @@ -460,23 +410,19 @@ class AcidConsumptionModel: self.fe_conc_avg = fe_conc_avg def calculate(self) -> Dict[str, Any]: - weight_t = self.coil_weight_kg / 1000.0 + wt = self.coil_weight_kg / 1000.0 acid_base = self.ACID_WITH_REGEN if self.has_regen_station else self.ACID_WITHOUT_REGEN - - # 铁离子修正:FeCl₂ > 100 g/L 时酸液利用率下降,有效酸耗上升 - fe_factor = 1.0 + max(0.0, (self.fe_conc_avg - 100.0) / 100.0) * 0.4 + fe_factor = 1.0 + max(0.0, (self.fe_conc_avg-100.0)/100.0)*0.4 acid_unit = round(acid_base * fe_factor, 3) - return { - "coil_weight_t": round(weight_t, 3), - "has_regen_station": self.has_regen_station, - "acid_consumption_kg": round(acid_unit * weight_t, 2), - "acid_unit_kg_per_t": acid_unit, - "steam_consumption_kg": round(self.STEAM_UNIT * weight_t, 2), - "steam_unit_kg_per_t": self.STEAM_UNIT, - "power_consumption_kwh": round(self.POWER_UNIT * weight_t, 2), - "power_unit_kwh_per_t": self.POWER_UNIT, - "cooling_water_m3": round(self.COOLING_UNIT * weight_t, 3), + "coil_weight_t": round(wt, 3), + "acid_consumption_kg": round(acid_unit * wt, 2), + "acid_unit_kg_per_t": acid_unit, + "steam_consumption_kg": round(self.STEAM_UNIT * wt, 2), + "steam_unit_kg_per_t": self.STEAM_UNIT, + "power_consumption_kwh": round(self.POWER_UNIT * wt, 2), + "power_unit_kwh_per_t": self.POWER_UNIT, + "cooling_water_m3": round(self.COOLING_UNIT * wt, 3), "cooling_water_unit_m3_per_t": self.COOLING_UNIT, - "fe_conc_factor": round(fe_factor, 3), + "fe_conc_factor": round(fe_factor, 3), } diff --git a/backend/app/services/pt_models/acid_speed.onnx b/backend/app/services/pt_models/acid_speed.onnx new file mode 100644 index 0000000000000000000000000000000000000000..d1fbafd73f1c8ec879a11821a94716bc2b657f4f GIT binary patch literal 50252 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

45%1Zco81=%43= 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 literal 0 HcmV?d00001 diff --git a/backend/app/services/pt_models/quality.onnx b/backend/app/services/pt_models/quality.onnx new file mode 100644 index 0000000000000000000000000000000000000000..8d6c535c4bd0a2dbf21d44580819ce1ca6e29dfd GIT binary patch literal 11189 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 E18S$$GXMYp literal 0 HcmV?d00001 diff --git a/backend/requirements.txt b/backend/requirements.txt index 2b68318..0f828d4 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -18,3 +18,5 @@ redis==5.0.4 aioredis==2.0.1 httpx==0.27.0 loguru==0.7.2 +onnxruntime==1.18.0 +numpy==1.26.4 diff --git a/backend/train_models.py b/backend/train_models.py new file mode 100644 index 0000000..d7c4f83 --- /dev/null +++ b/backend/train_models.py @@ -0,0 +1,256 @@ +""" +本地训练脚本 — 生成合成数据、训练 MLP、导出 ONNX +运行方式(在 backend/ 目录下): + python train_models.py + +依赖(仅本地训练用,不进 Docker): + pip install torch onnx onnxruntime scikit-learn numpy +""" +import sys, json, time +from pathlib import Path + +import numpy as np +import torch +import torch.nn as nn +import torch.optim as optim +from torch.utils.data import DataLoader, TensorDataset + +sys.path.insert(0, str(Path(__file__).parent)) +from app.services.prediction import AcidSpeedModel, TensionModel, QualityPredictionModel + +PT_DIR = Path(__file__).parent / "app" / "services" / "pt_models" +PT_DIR.mkdir(parents=True, exist_ok=True) + +SEED = 2024 +N = 12000 +np.random.seed(SEED) +torch.manual_seed(SEED) + +TENSION_ZONES = [ + "inlet", "s1_roller", "acid_entry", + "acid1", "acid2", "acid3", + "rinse", "leveler", "s2_roller", "outlet", +] + + +# ─── 网络结构 ─────────────────────────────────────────────────────────────── + +class MLP(nn.Module): + def __init__(self, in_dim: int, out_dim: int, hidden=(128, 64, 32)): + super().__init__() + layers: list = [] + prev = in_dim + for h in hidden: + layers += [nn.Linear(prev, h), nn.ReLU()] + prev = h + layers.append(nn.Linear(prev, out_dim)) + self.net = nn.Sequential(*layers) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.net(x) + + +# ─── 训练通用函数 ─────────────────────────────────────────────────────────── + +def fit(model: nn.Module, X: np.ndarray, y: np.ndarray, + epochs=300, lr=1e-3, batch_size=512) -> nn.Module: + Xt = torch.from_numpy(X) + yt = torch.from_numpy(y) + dl = DataLoader(TensorDataset(Xt, yt), batch_size=batch_size, shuffle=True) + opt = optim.AdamW(model.parameters(), lr=lr, weight_decay=1e-4) + sched = optim.lr_scheduler.CosineAnnealingLR(opt, epochs) + loss_fn = nn.MSELoss() + + model.train() + for ep in range(1, epochs + 1): + tot = 0.0 + for xb, yb in dl: + opt.zero_grad() + loss = loss_fn(model(xb), yb) + loss.backward() + opt.step() + tot += loss.item() * len(xb) + sched.step() + if ep % 100 == 0: + print(f" ep {ep:3d}/{epochs} RMSE={((tot/len(Xt))**0.5):.5f}") + return model + + +def z_scale(arr: np.ndarray, mean=None, std=None): + if mean is None: + mean = arr.mean(axis=0) + std = arr.std(axis=0) + 1e-8 + return ((arr - mean) / std).astype(np.float32), mean, std + + +def export_onnx(model: nn.Module, in_dim: int, path: Path): + model.eval() + dummy = torch.zeros(1, in_dim) + torch.onnx.export( + model, dummy, str(path), + input_names=["input"], output_names=["output"], + dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}}, + opset_version=17, + ) + print(f" → {path.name} ({path.stat().st_size//1024} KB)") + + +# ─── 1. 酸洗速度模型 ──────────────────────────────────────────────────────── +# 输入(14): thickness, scale_weight, conc×6, temp×6 +# 输出(1): max_speed + +def gen_acid_speed(n: int): + rng = np.random.default_rng(SEED) + Xs, ys = [], [] + skip = 0 + while len(Xs) < n: + t = rng.uniform(0.5, 8.0) + sw = rng.uniform(4.0, 18.0) + conc = rng.uniform(60, 240, 6).tolist() + temp = rng.uniform(52, 87, 6).tolist() + tpi = rng.uniform(88, 97) + try: + m = AcidSpeedModel( + thickness=t, width=1000.0, steel_grade="Q235", + acid_conc_list=conc, acid_temp_list=temp, + scale_weight=sw, target_pi=tpi, + ) + spd = float(m.calculate()["max_speed"]) + except Exception: + skip += 1 + continue + + # 模拟真实工况偏差:±6% 相对噪声 + 钢种系数扰动 + steel_factor = rng.choice([0.92, 0.96, 1.00, 1.03, 1.06]) + noise = rng.normal(1.0, 0.06) + spd_n = float(np.clip(spd * noise * steel_factor, 20, 180)) + + Xs.append([t, sw] + conc + temp) + ys.append([spd_n]) + + print(f" acid_speed: {len(Xs)} samples (skipped {skip})") + return np.array(Xs, np.float32), np.array(ys, np.float32) + + +# ─── 2. 张力模型 ──────────────────────────────────────────────────────────── +# 输入(4): thickness, width, yield_strength, tension_coef +# 输出(10): 10 区段张力 kN + +def gen_tension(n: int): + rng = np.random.default_rng(SEED + 1) + Xs, ys = [], [] + while len(Xs) < n: + t = rng.uniform(0.5, 8.0) + w = rng.uniform(600, 1600) + ys_ = rng.uniform(150, 600) + tc = rng.uniform(0.15, 0.35) + + m = TensionModel(thickness=t, width=w, yield_strength=ys_, tension_coef=tc) + res = m.calculate() + tensions = [res["zones"][z]["tension_kN"] for z in TENSION_ZONES] + + # 各区段独立噪声(实测张力传感器精度约 ±4%) + noise = rng.normal(1.0, 0.04, 10) + tensions_n = [float(np.clip(v * noise[i], 0.1, 9999)) for i, v in enumerate(tensions)] + + Xs.append([t, w, ys_, tc]) + ys.append(tensions_n) + + print(f" tension: {len(Xs)} samples") + return np.array(Xs, np.float32), np.array(ys, np.float32) + + +# ─── 3. 质量预测模型 ───────────────────────────────────────────────────────── +# 输入(6): thickness, avg_speed, acid_conc_avg, acid_temp_avg, scale_weight, fe_conc_avg +# 输出(2): pi_score, surface_score + +def gen_quality(n: int): + rng = np.random.default_rng(SEED + 2) + Xs, ys = [], [] + while len(Xs) < n: + t = rng.uniform(0.5, 8.0) + spd = rng.uniform(20, 180) + conc = rng.uniform(60, 240) + temp = rng.uniform(50, 90) + sw = rng.uniform(4, 18) + fe = rng.uniform(20, 130) + + m = QualityPredictionModel( + thickness=t, avg_speed=spd, + acid_conc_avg=conc, acid_temp_avg=temp, + scale_weight=sw, fe_conc_avg=fe, + ) + res = m.calculate() + pi = res["pi_score"] + suf = res["surface_score"] + + # ±6% 噪声模拟质检测量不确定度 + pi_n = float(np.clip(pi * rng.normal(1.0, 0.06), 0, 100)) + suf_n = float(np.clip(suf * rng.normal(1.0, 0.06), 0, 100)) + + Xs.append([t, spd, conc, temp, sw, fe]) + ys.append([pi_n, suf_n]) + + print(f" quality: {len(Xs)} samples") + return np.array(Xs, np.float32), np.array(ys, np.float32) + + +# ─── 主流程 ───────────────────────────────────────────────────────────────── + +def main(): + scalers: dict = {} + t0 = time.time() + + # ── 酸洗速度 ── + print("\n[1/3] 酸洗速度模型") + X, y = gen_acid_speed(N) + Xn, Xm, Xs = z_scale(X) + yn, ym, ys_ = z_scale(y) + model = MLP(14, 1, hidden=(128, 64, 32)) + print(" 训练中...") + fit(model, Xn, yn, epochs=300) + export_onnx(model, 14, PT_DIR / "acid_speed.onnx") + scalers["acid_speed"] = { + "X_mean": Xm.tolist(), "X_std": Xs.tolist(), + "y_mean": ym.tolist(), "y_std": ys_.tolist(), + } + + # ── 张力 ── + print("\n[2/3] 张力模型") + X, y = gen_tension(N) + Xn, Xm, Xs = z_scale(X) + yn, ym, ys_ = z_scale(y) + model = MLP(4, 10, hidden=(64, 64, 32)) + print(" 训练中...") + fit(model, Xn, yn, epochs=300) + export_onnx(model, 4, PT_DIR / "tension.onnx") + scalers["tension"] = { + "X_mean": Xm.tolist(), "X_std": Xs.tolist(), + "y_mean": ym.tolist(), "y_std": ys_.tolist(), + "zone_names": TENSION_ZONES, + } + + # ── 质量 ── + print("\n[3/3] 质量预测模型") + X, y = gen_quality(N) + Xn, Xm, Xs = z_scale(X) + yn, ym, ys_ = z_scale(y) + model = MLP(6, 2, hidden=(64, 32)) + print(" 训练中...") + fit(model, Xn, yn, epochs=300) + export_onnx(model, 6, PT_DIR / "quality.onnx") + scalers["quality"] = { + "X_mean": Xm.tolist(), "X_std": Xs.tolist(), + "y_mean": ym.tolist(), "y_std": ys_.tolist(), + } + + # ── 保存 scaler 参数 ── + scaler_path = PT_DIR / "scalers.json" + with open(scaler_path, "w") as f: + json.dump(scalers, f, indent=2) + print(f"\n scalers → {scaler_path.name}") + print(f"\n完成 ({time.time()-t0:.1f}s)\n") + + +if __name__ == "__main__": + main()