# -*- coding: utf-8 -*- """自动拟合候选曲线目标函数。 本模块提供轻量级的曲线误差计算,用于比较目标曲线和代理模型预测曲线的匹配度。 目标函数与 C++ 真实求解器的当前误差公式保持一致,并分别计算 log_pressure 与 log_derivative 两条曲线的贡献,适合在参数筛选、PSO 候选排序和局部邻域验证中复用。 """ from __future__ import annotations import numpy as np def split_curve_by_layout(curve: np.ndarray, layout: dict) -> dict[str, np.ndarray]: """按照 curve_layout 拆分曲线。""" parts: dict[str, np.ndarray] = {} for part in layout["parts"]: start = int(part["start"]) end = int(part["end"]) parts[str(part["name"])] = np.asarray(curve[start:end], dtype=np.float64) return parts def calculate_curve_objective_1d(target: np.ndarray, pred: np.ndarray) -> float: """在自然对数曲线上计算与 C++ calculatePointError 等价的均方根误差。""" target = np.asarray(target, dtype=np.float64).reshape(-1) pred = np.asarray(pred, dtype=np.float64).reshape(-1) if target.size == 0 or pred.size != target.size: return float("inf") if not (np.isfinite(target).all() and np.isfinite(pred).all()): return float("inf") # 模型曲线已经是 ln(raw)。因此 C++ 中的 logError 就是两列之差; # raw 相对误差可稳定地化简为 1-exp(-|delta_log|),无需先 exp 回原始尺度。 log_error = np.abs(target - pred) relative_error = -np.expm1(-log_error) point_error = 0.7 * log_error + 0.3 * relative_error return float(np.sqrt(np.mean(point_error**2))) def dual_log_objective( curve_target: np.ndarray, curve_pred: np.ndarray, curve_layout: dict, w_pressure: float = 0.5, w_derivative: float = 0.5, ) -> dict[str, float]: """分别计算压力和导数目标,并按权重合成双对数自动拟合目标。""" parts_target = split_curve_by_layout(curve_target, curve_layout) parts_pred = split_curve_by_layout(curve_pred, curve_layout) p_obj = calculate_curve_objective_1d(parts_target["log_pressure"], parts_pred["log_pressure"]) d_obj = calculate_curve_objective_1d(parts_target["log_derivative"], parts_pred["log_derivative"]) total_w = max(float(w_pressure) + float(w_derivative), 1e-12) combined = (float(w_pressure) * p_obj + float(w_derivative) * d_obj) / total_w return { "log_pressure_objective": float(p_obj), "log_derivative_objective": float(d_obj), "dual_log_objective": float(combined), } def prediction_curve_time_from_meta(meta: dict, curve_layout: dict) -> np.ndarray: """读取预处理模型数据中保存的固定物理时间网格。""" mode = str(meta.get("curve_time_mode", "missing")) if mode != "fixed": raise ValueError( "surrogate scoring requires curve_time_mode='fixed'; " f"got curve_time_mode={mode!r}" ) raw_time = meta.get("prediction_curve_time") if raw_time is None: raise ValueError("curve_time_mode is fixed but prediction_curve_time is missing") prediction_time = np.asarray(raw_time, dtype=np.float64).reshape(-1) pressure_part = next( (part for part in curve_layout["parts"] if str(part["name"]) == "log_pressure"), None, ) if pressure_part is None: raise ValueError("curve_layout has no log_pressure part") expected_size = int(pressure_part["end"]) - int(pressure_part["start"]) if prediction_time.size != expected_size: raise ValueError( f"prediction_curve_time has {prediction_time.size} points; expected {expected_size}" ) if not np.isfinite(prediction_time).all() or np.any(prediction_time <= 0.0): raise ValueError("prediction_curve_time must contain positive finite values") if np.any(np.diff(prediction_time) <= 0.0): raise ValueError("prediction_curve_time must be strictly increasing") return prediction_time def _prepare_timed_raw_curve( time: np.ndarray, pressure: np.ndarray, derivative: np.ndarray, ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """清洗并排序一条原始压力/导数曲线,不在此处进行重采样。""" time = np.asarray(time, dtype=np.float64).reshape(-1) pressure = np.asarray(pressure, dtype=np.float64).reshape(-1) derivative = np.asarray(derivative, dtype=np.float64).reshape(-1) if time.size != pressure.size or time.size != derivative.size: raise ValueError("time, pressure and derivative lengths do not match") valid = ( np.isfinite(time) & np.isfinite(pressure) & np.isfinite(derivative) & (time > 0.0) ) time = time[valid] pressure = pressure[valid] derivative = derivative[valid] if time.size < 3: raise ValueError("timed curve has fewer than three valid points") order = np.argsort(time, kind="stable") time = time[order] pressure = pressure[order] derivative = derivative[order] keep = np.ones(time.size, dtype=bool) keep[1:] = time[1:] > time[:-1] time = time[keep] pressure = pressure[keep] derivative = derivative[keep] if time.size < 3: raise ValueError("timed curve has fewer than three unique time points") return time, pressure, derivative def _calculate_raw_curve_objective_1d(target: np.ndarray, pred: np.ndarray) -> float: """在原始数值上复现 nmCalculationAutoFitPSO::calculatePointError。""" target = np.asarray(target, dtype=np.float64).reshape(-1) pred = np.asarray(pred, dtype=np.float64).reshape(-1) if target.size == 0 or pred.size != target.size: return float("inf") if not (np.isfinite(target).all() and np.isfinite(pred).all()): return float("inf") target_abs = np.maximum(np.abs(target), 1.0e-10) pred_abs = np.maximum(np.abs(pred), 1.0e-10) log_error = np.abs(np.log(target_abs) - np.log(pred_abs)) relative_error = np.abs(target - pred) / np.maximum( np.maximum(np.abs(target), np.abs(pred)), 1.0e-10, ) point_error = 0.7 * log_error + 0.3 * relative_error return float(np.sqrt(np.mean(point_error**2))) def timed_raw_dual_objective( target_time: np.ndarray, target_pressure: np.ndarray, target_derivative: np.ndarray, pred_time: np.ndarray, pred_pressure: np.ndarray, pred_derivative: np.ndarray, n_common_points: int = 50, w_pressure: float = 0.5, w_derivative: float = 0.5, ) -> dict[str, float]: """按照 C++ 目标函数的规则,在同一物理时间网格上比较两条原始曲线。""" if int(n_common_points) < 2: raise ValueError("n_common_points must be at least two") target_time, target_pressure, target_derivative = _prepare_timed_raw_curve( target_time, target_pressure, target_derivative, ) pred_time, pred_pressure, pred_derivative = _prepare_timed_raw_curve( pred_time, pred_pressure, pred_derivative, ) overlap_start = max(float(target_time[0]), float(pred_time[0])) overlap_end = min(float(target_time[-1]), float(pred_time[-1])) if overlap_start <= 0.0 or overlap_start >= overlap_end: return { "log_pressure_objective": float("inf"), "log_derivative_objective": float("inf"), "dual_log_objective": float("inf"), } common_time = np.geomspace(overlap_start, overlap_end, int(n_common_points)) target_pressure_common = np.interp(common_time, target_time, target_pressure) target_derivative_common = np.interp(common_time, target_time, target_derivative) pred_pressure_common = np.interp(common_time, pred_time, pred_pressure) pred_derivative_common = np.interp(common_time, pred_time, pred_derivative) p_obj = _calculate_raw_curve_objective_1d( target_pressure_common, pred_pressure_common, ) d_obj = _calculate_raw_curve_objective_1d( target_derivative_common, pred_derivative_common, ) total_w = max(float(w_pressure) + float(w_derivative), 1.0e-12) combined = (float(w_pressure) * p_obj + float(w_derivative) * d_obj) / total_w return { "log_pressure_objective": float(p_obj), "log_derivative_objective": float(d_obj), "dual_log_objective": float(combined), } def timed_dual_log_objective( target_time: np.ndarray, target_pressure: np.ndarray, target_derivative: np.ndarray, pred_time: np.ndarray, pred_curve: np.ndarray, curve_layout: dict, n_common_points: int = 50, w_pressure: float = 0.5, w_derivative: float = 0.5, ) -> dict[str, float]: """按照 C++ 时间对齐规则,将预测对数曲线与原始目标曲线进行比较。""" pred_parts = split_curve_by_layout(pred_curve, curve_layout) pred_log_pressure = pred_parts["log_pressure"] pred_log_derivative = pred_parts["log_derivative"] pred_time = np.asarray(pred_time, dtype=np.float64).reshape(-1) if pred_time.size != pred_log_pressure.size or pred_time.size != pred_log_derivative.size: raise ValueError("pred_time length does not match predicted curve parts") target_time, target_pressure, target_derivative = _prepare_timed_raw_curve( target_time, target_pressure, target_derivative, ) # 不使用超出目标曲线实际时间范围的固定网格输出做插值;这些位置在训练时已由掩码排除。 covered = pred_time <= target_time[-1] if int(np.sum(covered)) < 3: raise ValueError("target range contains fewer than three prediction time points") pred_time = pred_time[covered] pred_log_pressure = pred_log_pressure[covered] pred_log_derivative = pred_log_derivative[covered] with np.errstate(over="ignore", invalid="ignore"): pred_pressure = np.exp(pred_log_pressure) pred_derivative = np.exp(pred_log_derivative) target_end = float(target_time[-1]) if pred_time[-1] < target_end: dt = float(pred_time[-1] - pred_time[-2]) if dt <= 0.0: raise ValueError("prediction time grid has a non-positive final interval") fraction = (target_end - float(pred_time[-1])) / dt pressure_end = pred_pressure[-1] + fraction * (pred_pressure[-1] - pred_pressure[-2]) derivative_end = pred_derivative[-1] + fraction * ( pred_derivative[-1] - pred_derivative[-2] ) pred_time = np.append(pred_time, target_end) pred_pressure = np.append(pred_pressure, max(float(pressure_end), 1.0e-300)) pred_derivative = np.append(pred_derivative, max(float(derivative_end), 1.0e-300)) return timed_raw_dual_objective( target_time=target_time, target_pressure=target_pressure, target_derivative=target_derivative, pred_time=pred_time, pred_pressure=pred_pressure, pred_derivative=pred_derivative, n_common_points=n_common_points, w_pressure=w_pressure, w_derivative=w_derivative, )