""" 工艺预测模型 — 灰箱(Gray-box)架构 设计思路: 物理结构来自 Arrhenius 酸洗动力学,参数取自公开文献实验值, 而非理论推导。每个模型内置校准系数 K_cal(初始=1.0), 投产后可通过 calibrate() 方法用实测结果回归更新, 使模型随数据积累逐步收敛到真实工况。 关键文献依据: [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 """ import math import json import os from typing import List, Dict, Any, Optional, Tuple # ── 校准系数持久化路径 ──────────────────────────────────────────────────────── _CAL_FILE = os.path.join(os.path.dirname(__file__), "cal_coeffs.json") def _load_cal() -> Dict[str, float]: try: with open(_CAL_FILE) as f: return json.load(f) except Exception: return {} def _save_cal(d: Dict[str, float]): with open(_CAL_FILE, "w") as f: json.dump(d, f, indent=2) # ───────────────────────────────────────────────────────────────────────────── # 1. 酸洗速度模型(Gray-box) # ───────────────────────────────────────────────────────────────────────────── 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 校准系数,支持投产后在线标定 """ # 文献实验值(碳钢 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, ): 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 self.acid_conc_list = acid_conc_list self.acid_temp_list = acid_temp_list self.scale_weight = scale_weight 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 _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 = [], [] 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 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: 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), "under_pickling_risk": self._risk_level(self.V_MIN, pi), "warning": "酸液条件不足,即使最低速下酸洗指数仍低于目标,请检查酸浓度和温度", "K_cal": self.K_cal, } lo, hi, best_v = 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 else: hi = mid - 0.5 best_v = math.floor(best_v) total_pi, pi_per_tank, rt_per_tank = self._compute_pi(best_v) 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, } 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 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=出现欠酸洗 """ predicted = self.calculate()["max_speed"] if not actual_quality_ok: # 预测速度偏高,缩减 K_cal adjustment = 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) return self.K_cal # ───────────────────────────────────────────────────────────────────────────── # 2. 张力设定模型 # ───────────────────────────────────────────────────────────────────────────── class TensionModel: """ 张力模型:基于截面积×屈服强度,区间比例系数参考酸洗线工程手册。 每个区段独立校准系数 zone_kcal[zone],互不干扰。 """ # 各区基准比例系数(酸洗线工程实践均值) 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, } ZONE_NAMES_CN = { "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 __init__( self, thickness: float, width: float, yield_strength: float, tension_coef: float = 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 zones = {} for zone, ratio in self.ZONE_RATIOS.items(): k = self.zone_kcal.get(zone, 1.0) zones[zone] = { "tension_kN": round(t_base_kn * ratio * k, 2), "ratio": ratio, "k_cal": k, "name_cn": self.ZONE_NAMES_CN[zone], } 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) return { "T_max": t_max_kn, "T_base": round(t_base_kn, 2), "cross_section_mm2": round(cross_section, 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, } def calibrate(self, zone: str, measured_kn: float) -> Dict[str, float]: """仅更新指定区段的校准系数,其他区段不变""" 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) return self.zone_kcal # ───────────────────────────────────────────────────────────────────────────── # 3. 质量预测模型 # ───────────────────────────────────────────────────────────────────────────── class QualityPredictionModel: """ 欠酸洗风险 + 质量等级预测。 v2 变化: - 使用与 AcidSpeedModel 一致的文献参数(Ea/R=5413, n=1.2) - 欠酸洗风险特征阈值参考 arXiv:1207.0911 的 decision-tree 结论 - 增加铁离子浓度(FeCl₂)对酸洗能力的抑制修正 - 支持投产后用实际质量等级校准评分阈值 """ 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(铁离子抑制效应) ): self.thickness = thickness self.avg_speed = avg_speed self.acid_conc_avg = acid_conc_avg self.acid_temp_avg = acid_temp_avg self.scale_weight = scale_weight 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 _surface_score(self, pi_score: float) -> float: # 最优速度区间 80-140 m/min(文献[4] 欠酸洗风险判别边界) if self.avg_speed < 60: speed_score = 80.0 elif self.avg_speed <= 140: speed_score = 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) def _grade(self, pi: float, surface: float) -> str: c = (pi + surface) / 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]: recs = [] if self.fe_conc_avg > 80: recs.append(f"铁离子浓度偏高({self.fe_conc_avg:.0f} g/L),酸洗能力受抑制,建议加速换酸或补充新酸") if pi < 80: recs.append("酸洗指数偏低,建议提高酸液浓度至 180 g/L 以上,或将温度升至 80°C") if pi < 65: 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") if self.avg_speed > 150: recs.append(f"线速过高({self.avg_speed:.0f} m/min),欠酸洗风险,建议不超过 140 m/min") if self.scale_weight > 12.0: 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) return { "pi_score": pi, "surface_score": surface, "overall_grade": self._grade(pi, surface), "recommendations": self._recommendations(pi, surface), "K_cal": self.K_cal, } def calibrate(self, actual_grade: str) -> float: """传入实际质检等级,更新评分校准系数""" 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) return self.K_cal # ───────────────────────────────────────────────────────────────────────────── # 4. 消耗预测模型 # ───────────────────────────────────────────────────────────────────────────── class AcidConsumptionModel: """ 单卷资源消耗预测。 单位消耗定额取自浙江企鹅1250mm规格书; 酸耗额外引入铁离子浓度修正(FeCl₂ 越高酸液越快失效,换酸频率越高)。 """ 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,影响换酸频率 ): self.thickness = thickness self.width = width self.coil_weight_kg = coil_weight_kg self.has_regen_station = has_regen_station self.fe_conc_avg = fe_conc_avg def calculate(self) -> Dict[str, Any]: weight_t = 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 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), "cooling_water_unit_m3_per_t": self.COOLING_UNIT, "fe_conc_factor": round(fe_factor, 3), }