diff --git a/Include/nmNum/nmCalculation/nmCalculationAutoFitLM.h b/Include/nmNum/nmCalculation/nmCalculationAutoFitLM.h new file mode 100644 index 0000000..5621d10 --- /dev/null +++ b/Include/nmNum/nmCalculation/nmCalculationAutoFitLM.h @@ -0,0 +1,200 @@ +#ifndef NMCALCULATIONAUTOFITLM_H +#define NMCALCULATIONAUTOFITLM_H + +#include +#include +#include +#include +#include +#include +#include + +#include "nmCalculation_global.h" + +class nmDataWellBase; + +// 双对数曲线误差分解。total 是 LM 候选接受和排序的唯一依据, +// 其余诊断量用于有限差分灵敏度分析和信赖域选参。 +struct AutoFitObjectiveBreakdownLM { + bool valid; + double total; + double pressureLoss; + double derivativeLoss; + QVector residualVector; + double verticalCommonBias; + double verticalLoss; + bool verticalReliable; + double horizontalPhysicalShift; + double horizontalLoss; + bool horizontalReliable; + bool registrationAmbiguous; + double shapeLoss; + double lateDerivativeSlopeBias; + double lateDerivativeTrendLoss; + bool lateDerivativeTrendReliable; + double coverage; + + AutoFitObjectiveBreakdownLM() + : valid(false) + , total(1.0e10) + , pressureLoss(std::numeric_limits::quiet_NaN()) + , derivativeLoss(std::numeric_limits::quiet_NaN()) + , verticalCommonBias(std::numeric_limits::quiet_NaN()) + , verticalLoss(std::numeric_limits::quiet_NaN()) + , verticalReliable(false) + , horizontalPhysicalShift(std::numeric_limits::quiet_NaN()) + , horizontalLoss(std::numeric_limits::quiet_NaN()) + , horizontalReliable(false) + , registrationAmbiguous(false) + , shapeLoss(std::numeric_limits::quiet_NaN()) + , lateDerivativeSlopeBias(std::numeric_limits::quiet_NaN()) + , lateDerivativeTrendLoss(std::numeric_limits::quiet_NaN()) + , lateDerivativeTrendReliable(false) + , coverage(std::numeric_limits::quiet_NaN()) + {} +}; + +// 有限差分 + LM/信赖域拟合的停止原因。 +enum StopReasonLM { + LM_CONTINUE_OPTIMIZATION = 0, + LM_TARGET_ACHIEVED = 1, + LM_TRUE_CONVERGENCE = 2, + LM_LOCAL_OPTIMUM = 3, + LM_MAX_ITERATIONS = 4, + LM_USER_STOPPED = 5, + LM_CONSECUTIVE_FAILURES = 6, + LM_OPTIMIZATION_FAILED = 7 +}; + +class NMCALCULATION_EXPORT nmCalculationAutoFitLM : public QObject +{ + Q_OBJECT + +public: + explicit nmCalculationAutoFitLM(QObject* parent = 0); + ~nmCalculationAutoFitLM(); + + void setTargetLogLogData(const QVector >& targetData); + bool startAutoFitting(); + void stopFitting(); + QVector getBestSolution() const; + double getBestFitness() const; + AutoFitObjectiveBreakdownLM getLastObjectiveBreakdown() const; + QString getLastError() const; + bool isRunning() const; + int getCurrentIteration() const; + void resetOptimizer(); + void setTargetWellName(const QString& wellName); + +signals: + void progressUpdated(int iteration, double bestFitness); + void fittingFinished(bool success, const QString& message); + void bestCurveUpdated(QVector > targetData, + QVector > bestData, + int iteration, + double fitness); + void logMessageGenerated(const QString& message); + +private: + void initializeTemporaryDirectory(); + void cleanupTemporaryDirectory(); + bool removeDirectoryRecursively(const QString& path); + void cleanupOldTemporaryDirectories(); + + bool loadAllConfigFromDataManager(); + void loadOptimizationConfig(); + void loadParameterBounds(); + void extractUserInitialValues(); + + // 有限差分 + LM/信赖域核心算法。 + StopReasonLM runTrustRegionFitting(); + bool evaluateTrustRegionPoint(const QVector& parameters, + double* fitness, + AutoFitObjectiveBreakdownLM* breakdown, + QVector >* curve, + int* elapsedMs); + double evaluateFitness(const QVector& parameters); + + void applyParametersToDataManager(const QVector& parameters); + void updateReservoirParameters(const QVector& parameters); + void updateWellParameters(const QVector& parameters); + void updateWellToDataManager(nmDataWellBase* pWell); + + QVector > runSolver(); + QVector > runSolverDll(); + bool runFinalFullSolver(); + void saveOptimizationResult(); + void validateAndProtectFinalResult(); + QString getStopReasonDescription(StopReasonLM reason) const; + int getEnabledParameterCount() const; + + // LM 运行轨迹。 + void initializeTraceFile(); + void closeTraceFile(); + void writeTraceHeader(); + void writeTraceMetaFile(); + void writeTraceRow(int iteration, + int parameterIndex, + const QString& phase, + const QVector& parameters, + double solverObjective, + bool solverSuccess, + int elapsedMs, + const QString& decision, + const AutoFitObjectiveBreakdownLM* objectiveBreakdown = nullptr); + QVector buildTraceParameterVector(const QVector& selectedParameters) const; + void emitRunSummary(bool success, StopReasonLM finalReason); + + bool validateParameters(const QVector& parameters) const; + bool validateLogLogData(const QVector >& logLogData) const; + bool validateInitialValues() const; + bool validateSolverResult(const QVector >& result) const; + double calculateLogLogCurveError(const QVector >& target, + const QVector >& result) const; + +private: + bool m_isRunning; + bool m_shouldStop; + int m_currentIteration; + QString m_lastError; + + QVector m_initialValues; + QVector m_globalBestPosition; + double m_globalBestFitness; + AutoFitObjectiveBreakdownLM m_globalBestObjectiveBreakdown; + QVector > m_lastEvaluatedLogLogData; + QVector > m_globalBestLogLogData; + mutable AutoFitObjectiveBreakdownLM m_lastObjectiveBreakdown; + QVector > m_userInitialLogLogData; + AutoFitObjectiveBreakdownLM m_userInitialObjectiveBreakdown; + + // 参数索引:0 k,1 skin,2 wellboreC,3 phi,4 h,5 Ct, + // 6 Cf,7 Swi,8 Dfc,9 fractureHalfLength。 + QVector m_parameterSelected; + QVector m_parameterLower; + QVector m_parameterUpper; + QVector m_enabledParamIndices; + QVector > m_targetLogLogData; + QString m_targetWellName; + + int m_maxIterations; + double m_targetError; + int m_totalEvaluations; + int m_successfulEvaluations; + + volatile int m_evaluationInProgress; + int m_consecutiveFailures; + int m_maxConsecutiveFailures; + + QVector m_userInitialSolution; + double m_userInitialFitness; + bool m_hasValidUserSolution; + + QString m_tempDirectory; + QString m_traceRunId; + QString m_traceFilePath; + QString m_traceMetaFilePath; + QFile m_traceFile; +}; + +#endif // NMCALCULATIONAUTOFITLM_H diff --git a/Include/nmNum/nmCalculation/nmCalculationAutoFitPSO.h b/Include/nmNum/nmCalculation/nmCalculationAutoFitPSO.h index 9731259..e59db09 100644 --- a/Include/nmNum/nmCalculation/nmCalculationAutoFitPSO.h +++ b/Include/nmNum/nmCalculation/nmCalculationAutoFitPSO.h @@ -9,7 +9,6 @@ #include #include #include -#include #include "nmCalculation_global.h" @@ -20,69 +19,6 @@ class nmDataWellBase; class QTimer; class QProcess; -// 双对数曲线误差分解。该结构同时保存用于候选排序的主目标,以及用于判断 -// 曲线上下、左右和形状偏差的诊断量。total 是唯一的接受和排序依据,诊断量 -// 只参与信赖域选参,不能再次叠加到 total,否则会重复计算同一批曲线残差。 -struct AutoFitObjectiveBreakdown { - // valid 表示本次曲线评价完整有效;无效评价统一保留 total=1e10。 - // pressureLoss 和 derivativeLoss 均在 log(value) 空间按固定网格计算。 - bool valid; - double total; - double pressureLoss; - double derivativeLoss; - // 固定目标网格上的普通对数残差。非代理搜索使用它建立完整 Jacobian, - // 向量平方和与 total 的平方一致。 - QVector residualVector; - - // 上下偏差使用压力和导数残差共享的算术平均中心。 - // verticalCommonBias 为正表示模拟曲线整体偏高,为负表示整体偏低; - // verticalReliable=false 时仍保留数值,但不能据此确定参数调整方向。 - double verticalCommonBias; - double verticalLoss; - bool verticalReliable; - - // 水平偏差在 log(time) 坐标中计算。physicalShift 为正表示模拟曲线相对 - // 目标偏右,即相同曲线特征在模拟结果中出现得更晚。 - double horizontalPhysicalShift; - double horizontalLoss; - bool horizontalReliable; - // true 表示当前曲线无法可靠区分上下和左右误差;此时禁止使用两类有符号 - // 诊断量选参,但去除公共中心后的 shapeLoss 仍可用于局部选参。 - bool registrationAmbiguous; - - // 去除公共均值中心和可信左右偏差后剩余的整体形状误差;verticalReliable - // 只控制能否把公共中心解释为上下参数方向,不改变 shape 的中心化公式。 - double shapeLoss; - - // 兼容现有 trace 列。当前非代理搜索不再单独识别或调度晚期分量。 - double lateDerivativeSlopeBias; - double lateDerivativeTrendLoss; - bool lateDerivativeTrendReliable; - - // 模拟曲线对目标固定网格的有效覆盖率,取覆盖点比例与连续 log-time - // 跨度比例中的较小值。低于损失函数门槛时本次评价直接无效。 - double coverage; - - AutoFitObjectiveBreakdown() - : valid(false) - , total(1.0e10) - , pressureLoss(std::numeric_limits::quiet_NaN()) - , derivativeLoss(std::numeric_limits::quiet_NaN()) - , verticalCommonBias(std::numeric_limits::quiet_NaN()) - , verticalLoss(std::numeric_limits::quiet_NaN()) - , verticalReliable(false) - , horizontalPhysicalShift(std::numeric_limits::quiet_NaN()) - , horizontalLoss(std::numeric_limits::quiet_NaN()) - , horizontalReliable(false) - , registrationAmbiguous(false) - , shapeLoss(std::numeric_limits::quiet_NaN()) - , lateDerivativeSlopeBias(std::numeric_limits::quiet_NaN()) - , lateDerivativeTrendLoss(std::numeric_limits::quiet_NaN()) - , lateDerivativeTrendReliable(false) - , coverage(std::numeric_limits::quiet_NaN()) - {} -}; - // PSO粒子结构 // 这里的 position / velocity / bestPosition 只保存“用户勾选参与拟合的参数”, // 不是完整的 11 个储层/井筒参数。完整参数向量会在写 trace 或调用代理模型时 @@ -97,8 +33,6 @@ struct AutoFitParticle { QVector velocity; // 速度 QVector bestPosition; // 真实求解器确认的个体最优位置 QVector guideBestPosition; // 仅用于速度更新的引导位置;不会参与真实 gbest/最终结果 - AutoFitObjectiveBreakdown currentObjectiveBreakdown; // 当前真实评价对应的误差分解 - AutoFitObjectiveBreakdown bestObjectiveBreakdown; // pbest 对应的误差分解 double fitness; // 当前适应度 double bestFitness; // 真实求解器确认的个体最优适应度 double guideBestObjective; // guideBestPosition 对应的真实或代理目标值 @@ -162,7 +96,6 @@ public: void stopFitting(); QVector getBestSolution() const; double getBestFitness() const; - AutoFitObjectiveBreakdown getLastObjectiveBreakdown() const; QString getLastError() const; bool isRunning() const; int getCurrentIteration() const; @@ -204,26 +137,20 @@ private: void loadOptimizationConfig(); void loadParameterBounds(); - // ===== 自动拟合核心算法 ===== + // ===== PSO核心算法 ===== // - // 代理开启时保留原 PSO 筛选流程;代理关闭时使用真实求解器驱动的 - // 诊断灵敏度信赖域搜索,不依赖 pbest/gbest 速度公式。 + // 主流程: + // 1. extractUserInitialValues(): 从当前项目数据中取用户已有初始解; + // 2. initializeSwarm(): 根据初始解和上下界生成粒子群; + // 3. updateParticle(): 对单个粒子跑真实求解器并计算误差; + // 4. updateGlobalBest(): 只用真实求解器误差更新全局最优; + // 5. updateVelocityAndPosition(): 按 PSO 公式推进下一代粒子。 void extractUserInitialValues(); void initializeSwarm(); void updateVelocityAndPosition(); double evaluateFitness(const QVector& parameters); void updateGlobalBest(); void updateParticle(int particleIndex); - // 非代理拟合入口:建立有限差分灵敏度,按诊断分量选择参数,再用有界 - // LM/信赖域产生候选;所有候选最终都由真实求解器总误差决定是否接受。 - StopReasonPSO runTrustRegionFitting(); - // 对一个信赖域候选执行完整真实评价,并一次性返回误差、诊断量、曲线和耗时。 - // 返回 false 表示求解失败、损失无效或用户已请求停止。 - bool evaluateTrustRegionPoint(const QVector& parameters, - double* fitness, - AutoFitObjectiveBreakdown* breakdown, - QVector >* curve, - int* elapsedMs); // ===== 参数应用方法 ===== // @@ -239,7 +166,6 @@ private: // ===== 求解器相关 ===== QVector > runSolver(); QVector> runSolverDll(); - bool runFinalFullSolver(); QVector> runSolverExe(); // ===== 数据处理 ===== @@ -294,8 +220,7 @@ private: double surrogateObjective, const QString& screeningDecision, const QVector& pbestPosition, - double pbestObjective, - const AutoFitObjectiveBreakdown* objectiveBreakdown = nullptr); + double pbestObjective); void writeIterationTraceRows(); QVector buildTraceParameterVector(const QVector& selectedParameters) const; void resetRunSummary(); @@ -351,34 +276,30 @@ private: bool m_isRunning; // 当前是否有一次自动拟合正在运行。 bool m_shouldStop; // 用户停止标志;主循环和求解器等待循环会定期检查它。 bool m_isPaused; // 预留暂停标志;主循环中有暂停等待逻辑。 - int m_currentIteration; // 当前自动拟合迭代序号,从 0 开始。 + int m_currentIteration; // 当前 PSO 迭代序号,从 0 开始。 QString m_lastError; // 最近一次失败原因,供 UI 展示或日志排查。 - // ===== 优化状态数据 ===== + // ===== PSO数据 ===== QVector m_initialValues; // 当前模型中提取的用户初始值,顺序与 m_enabledParamIndices 一致。 QVector m_swarm; // 粒子群,每个粒子只保存启用参数维度。 - QVector m_globalBestPosition; // 真实求解器确认的当前最优参数。 + QVector m_globalBestPosition; // 全局最优参数,仍是启用参数向量。 double m_globalBestFitness; // 全局最优真实误差,越小越好。 double m_previousBestFitness; // 上一轮全局最优误差,用于自适应参数更新。 - AutoFitObjectiveBreakdown m_globalBestObjectiveBreakdown; // 真实 gbest 对应的误差分解。 QVector > m_lastEvaluatedLogLogData; // 最近一次真实求解得到的 result log-log 曲线。 QVector > m_globalBestLogLogData; // 当前全局最优对应的 result log-log 曲线。 - mutable AutoFitObjectiveBreakdown m_lastObjectiveBreakdown; // 最近一次损失评价的误差分解。 QVector > m_userInitialLogLogData; // 用户初始解对应的 result log-log 曲线,用于精英保护。 - AutoFitObjectiveBreakdown m_userInitialObjectiveBreakdown; // 用户初始解对应的误差分解。 // ===== 优化配置 ===== // // 参数索引约定: // 0 k 渗透率;1 skin 表皮系数;2 wellboreC 井筒储集; // 3 phi 孔隙度;4 h 储层厚度;5 Ct 综合压缩系数; - // 6 Cf 岩石压缩系数;7 Swi 初始含水饱和度; - // 8 Dfc 裂缝导流能力;9 fractureHalfLength 裂缝半长。 + // 6 Cf 岩石压缩系数;7 Swi 初始含水饱和度。 // m_enabledParamIndices 保存被用户勾选的参数索引,粒子的 position 维度与它一致。 - QVector m_parameterSelected; // 完整 10 个参数是否被用户勾选参与拟合。 - QVector m_parameterLower; // 完整 10 个参数的搜索下界。 - QVector m_parameterUpper; // 完整 10 个参数的搜索上界。 - QVector m_enabledParamIndices; // 被勾选参数在完整 10 维体系中的索引。 + QVector m_parameterSelected; // 完整 8 个参数是否被用户勾选参与拟合。 + QVector m_parameterLower; // 完整 8 个参数的搜索下界。 + QVector m_parameterUpper; // 完整 8 个参数的搜索上界。 + QVector m_enabledParamIndices; // 被勾选参数在完整 8 维体系中的索引。 QVector > m_targetLogLogData; // 目标井 history log-log 曲线:time/pressure/derivative。 QString m_targetWellName; // 目标井名称;读写井参数和读取模拟曲线都依赖它。 @@ -391,7 +312,7 @@ private: double m_socialParam; // 群体学习因子,控制粒子靠近全局 gbest 的程度。 // ===== 统计信息 ===== - int m_totalEvaluations; // 真实求解器评价总次数,包含粒子评价和方向试算。 + int m_totalEvaluations; // 已调用真实求解器评价的粒子总数。 int m_successfulEvaluations; // 真实求解器成功且误差有效的评价次数。 QVector m_convergenceHistory; // 每代全局最优误差历史,用于收敛判断。 @@ -407,7 +328,7 @@ private: // ===== 精英保护 ===== QVector m_userInitialSolution; // 用户初始解参数,若最终改进不足会恢复它。 double m_userInitialFitness; // 用户初始解真实误差。 - double m_improvementThreshold; // 仅用于日志区分显著改进和微小改进。 + double m_improvementThreshold; // 最终结果相对初始解至少需要达到的改进阈值。 bool m_hasValidUserSolution; // 初始解是否成功跑过真实求解器。 int m_consecutiveFailedIterations; // 连续失败迭代次数 @@ -436,8 +357,8 @@ private: // 这些字段只描述代理筛选和运行复盘,不参与 PSO 数学更新。 bool m_traceEnabled; // 是否写出 trace CSV/meta 文件。 QString m_traceRunId; // 本次运行 ID,作为 trace/candidate/score 文件名的一部分。 - QString m_traceFilePath; // 本次自动拟合 trace CSV 的完整路径。 - QString m_traceMetaFilePath; // 与 trace 匹配的 meta JSON 完整路径。 + QString m_traceFilePath; // pso_baseline_trace_.csv 完整路径。 + QString m_traceMetaFilePath; // pso_baseline_trace_.meta.json 完整路径。 QFile m_traceFile; // trace CSV 文件句柄。 bool m_surrogateScreeningEnabled; // 用户配置中的 PSO acceleration 开关。 unsigned int m_psoRandomSeed; // PSO 随机种子,也用于可复现 random audit。 diff --git a/Include/nmNum/nmSubWxs/nmWxAutomaticFitting.h b/Include/nmNum/nmSubWxs/nmWxAutomaticFitting.h index a8c4f76..8d10dc8 100644 --- a/Include/nmNum/nmSubWxs/nmWxAutomaticFitting.h +++ b/Include/nmNum/nmSubWxs/nmWxAutomaticFitting.h @@ -21,6 +21,7 @@ #include "nmDataWellBase.h" #include "nmDataAutomaticFitting.h" #include "nmCalculationAutoFitPSO.h" +#include "nmCalculationAutoFitLM.h" #include "nmWxAutomaticfittingStart.h" #include "nmSubWxs_global.h" @@ -109,6 +110,7 @@ private: // 自动拟合相关成员 nmCalculationAutoFitPSO* m_autoFitterPSO; + nmCalculationAutoFitLM* m_autoFitterLM; QProgressDialog* m_progressDialog; QTimer* m_progressTimer; bool m_autoParameterRanges; diff --git a/Include/nmNum/nmSubWxs/nmWxAutomaticFittingStart.h b/Include/nmNum/nmSubWxs/nmWxAutomaticFittingStart.h index 3e9067e..45aa745 100644 --- a/Include/nmNum/nmSubWxs/nmWxAutomaticFittingStart.h +++ b/Include/nmNum/nmSubWxs/nmWxAutomaticFittingStart.h @@ -23,9 +23,11 @@ #include #include "nmCalculationAutoFitPSO.h" +#include "nmCalculationAutoFitLM.h" // 前向声明 class nmCalculationAutoFitPSO; +class nmCalculationAutoFitLM; class QPainter; class QColor; class QPaintEvent; @@ -74,8 +76,9 @@ public: explicit nmWxAutomaticfittingStart(QWidget *parent = 0); ~nmWxAutomaticfittingStart(); - // PSO算法接口 + // 算法接口 void setAutoFitter(nmCalculationAutoFitPSO* autoFitter); + void setAutoFitter(nmCalculationAutoFitLM* autoFitter); // 通用设置接口 @@ -151,6 +154,8 @@ private: // 算法实例 nmCalculationAutoFitPSO* m_autoFitterPSO; + nmCalculationAutoFitLM* m_autoFitterLM; + QString m_algorithmName; // 拟合参数 int m_maxIterations; diff --git a/Src/nmNum/nmCalculation/nmCalculationAutoFitLM.cpp b/Src/nmNum/nmCalculation/nmCalculationAutoFitLM.cpp new file mode 100644 index 0000000..cccb04c --- /dev/null +++ b/Src/nmNum/nmCalculation/nmCalculationAutoFitLM.cpp @@ -0,0 +1,4368 @@ +#include "nmCalculationAutoFitLM.h" +#include "nmCalculationDllPebiSolverTask.h" +#include "nmDataAnalyzeManager.h" +#include "nmDataWellBase.h" +#include "nmDataVerticalWell.h" +#include "nmDataVerticalFracturedWell.h" +#include "nmDataHorizontalFracturedWell.h" +#include "nmDataReservoir.h" +#include "nmDataAutomaticFitting.h" + +#include "nmCalculationPebiGrid.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_WIN +#include +#include +#define DEBUG_OUT(msg) OutputDebugStringA(QString("[AutoFitLM] %1\n").arg(msg).toLocal8Bit().data()) +#endif + +static inline bool isFiniteNumber(double value) +{ +#ifdef Q_OS_WIN + return _finite(value) != 0; +#else + return std::isfinite(value); +#endif +} + +// 从嵌套 RMSE 中提取被消除的独立误差贡献。 +static double nestedRmsContribution(double reducedModelLoss, + double fullModelLoss) +{ + return qSqrt(qMax(0.0, + reducedModelLoss * reducedModelLoss - + fullModelLoss * fullModelLoss)); +} + +static inline void msleep(int ms) +{ +#ifdef Q_OS_WIN + Sleep(ms); +#else + Q_UNUSED(ms); +#endif +} + +static QString csvEscape(const QString& text) +{ + QString escaped = text; + escaped.replace("\"", "\"\""); + return QString("\"%1\"").arg(escaped); +} + +static QString traceNumber(double value) +{ + return isFiniteNumber(value) ? QString::number(value, 'g', 17) : QString(); +} + +static QString traceParamAt(const QVector& params, int index) +{ + return (index >= 0 && index < params.size()) + ? traceNumber(params[index]) + : QString(); +} + +static QString jsonEscape(const QString& text) +{ + QString escaped = text; + escaped.replace("\\", "\\\\"); + escaped.replace("\"", "\\\""); + escaped.replace("\b", "\\b"); + escaped.replace("\f", "\\f"); + escaped.replace("\n", "\\n"); + escaped.replace("\r", "\\r"); + escaped.replace("\t", "\\t"); + return QString("\"%1\"").arg(escaped); +} + +static QString jsonNumber(double value) +{ + return isFiniteNumber(value) ? QString::number(value, 'g', 17) : QString("null"); +} + +static QString jsonDoubleArray(const QVector& values) +{ + QStringList items; + for(int i = 0; i < values.size(); ++i) { + items << jsonNumber(values[i]); + } + return QString("[%1]").arg(items.join(",")); +} + +static QString jsonIntArray(const QVector& values) +{ + QStringList items; + for(int i = 0; i < values.size(); ++i) { + items << QString::number(values[i]); + } + return QString("[%1]").arg(items.join(",")); +} + +static QString jsonBoolArray(const QVector& values) +{ + QStringList items; + for(int i = 0; i < values.size(); ++i) { + items << (values[i] ? "true" : "false"); + } + return QString("[%1]").arg(items.join(",")); +} + +static QString jsonStringArray(const QStringList& values) +{ + QStringList items; + for(int i = 0; i < values.size(); ++i) { + items << jsonEscape(values[i]); + } + return QString("[%1]").arg(items.join(",")); +} + +// 信赖域搜索统一在 [0, 1] 内部坐标工作。正值参数使用对数坐标,使内部相同步长 +// 表示近似相同的相对变化,避免 k、C、Ct、Cf 等跨数量级参数被线性尺度支配; +// skin 可为负数、Swi 的物理意义是线性比例,因此二者保持有界线性坐标。 +static bool useTrustRegionLogScale(int parameterIndex, double lower, double upper) +{ + return parameterIndex != 1 && parameterIndex != 7 && + lower > 0.0 && upper > lower; +} + +static double toTrustRegionCoordinate(double value, + int parameterIndex, + double lower, + double upper) +{ + // 所有进入优化器的物理值先投影到用户上下界,再转换成无量纲坐标。 + // 这样有限差分步长、信赖半径和参数间相关性可以在统一尺度上比较。 + value = qMax(lower, qMin(upper, value)); + + if(useTrustRegionLogScale(parameterIndex, lower, upper)) { + return (qLn(value) - qLn(lower)) / (qLn(upper) - qLn(lower)); + } + + return upper > lower ? (value - lower) / (upper - lower) : 0.0; +} + +static double fromTrustRegionCoordinate(double coordinate, + int parameterIndex, + double lower, + double upper) +{ + // 候选内部坐标先限制在 [0,1],再执行上述映射的逆变换,保证写回 + // DataManager 的参数始终位于用户设置的物理范围内。 + coordinate = qMax(0.0, qMin(1.0, coordinate)); + + if(useTrustRegionLogScale(parameterIndex, lower, upper)) { + return qExp(qLn(lower) + coordinate * (qLn(upper) - qLn(lower))); + } + + return lower + coordinate * (upper - lower); +} + +enum TrustRegionErrorComponent +{ + TRUST_REGION_VERTICAL_COMPONENT = 0, + TRUST_REGION_HORIZONTAL_COMPONENT, + TRUST_REGION_SHAPE_COMPONENT, + TRUST_REGION_TOTAL_COMPONENT +}; + +// 一次真实求解的完整快照。除了参数和总误差,还保存内部坐标、诊断分量和 +// 双对数曲线,因此拒绝候选后可以完整恢复上一个已接受工作点。 +struct TrustRegionEvaluation +{ + QVector parameters; + QVector coordinates; + AutoFitObjectiveBreakdownLM breakdown; + QVector > curve; + double fitness; + int elapsedMs; + bool valid; + + TrustRegionEvaluation() + : fitness(1.0e10) + , elapsedMs(-1) + , valid(false) + {} +}; + +// LM 只使用固定长度、全部有限的普通残差。 +static bool trustRegionResidualsValid( + const AutoFitObjectiveBreakdownLM& breakdown) +{ + // 损失函数固定使用 80 个压力点和 80 个导数点。严格校验长度,避免 + // Jacobian 沿用旧维度后访问另一候选的短残差向量。 + if(!breakdown.valid || breakdown.residualVector.size() != 160) { + return false; + } + + for(int i = 0; i < breakdown.residualVector.size(); ++i) { + if(!isFiniteNumber(breakdown.residualVector[i])) { + return false; + } + } + return true; +} + +// 计算向量二范数的平方,避免在只比较能量或计算正规方程时反复开方。 +static double trustRegionSquaredNorm(const QVector& values) +{ + double sum = 0.0; + for(int i = 0; i < values.size(); ++i) { + sum += values[i] * values[i]; + } + return sum; +} + +// 计算同维向量内积;维度不一致表示局部模型无效,返回零让调用方放弃修正。 +static double trustRegionDotProduct(const QVector& left, + const QVector& right) +{ + if(left.size() != right.size()) { + return 0.0; + } + + double sum = 0.0; + for(int i = 0; i < left.size(); ++i) { + sum += left[i] * right[i]; + } + return sum; +} + +// trace 和运行日志使用稳定的英文标识,便于现有离线脚本继续按字段筛选。 +static QString trustRegionComponentName(int component) +{ + if(component == TRUST_REGION_VERTICAL_COMPONENT) { + return "vertical"; + } + if(component == TRUST_REGION_HORIZONTAL_COMPONENT) { + return "horizontal"; + } + if(component == TRUST_REGION_SHAPE_COMPONENT) { + return "shape"; + } + return "total"; +} + +// 三类损失量纲一致,直接选择当前最大的可靠分量;都很小时退回总残差梯度。 +static int trustRegionDominantComponent( + const AutoFitObjectiveBreakdownLM& breakdown, + double diagnosisThreshold) +{ + int component = TRUST_REGION_TOTAL_COMPONENT; + double largestLoss = diagnosisThreshold; + + if(breakdown.verticalReliable && + isFiniteNumber(breakdown.verticalLoss) && + breakdown.verticalLoss > largestLoss) { + component = TRUST_REGION_VERTICAL_COMPONENT; + largestLoss = breakdown.verticalLoss; + } + if(breakdown.horizontalReliable && + !breakdown.registrationAmbiguous && + isFiniteNumber(breakdown.horizontalLoss) && + breakdown.horizontalLoss > largestLoss) { + component = TRUST_REGION_HORIZONTAL_COMPONENT; + largestLoss = breakdown.horizontalLoss; + } + if(isFiniteNumber(breakdown.shapeLoss) && + breakdown.shapeLoss > largestLoss) { + component = TRUST_REGION_SHAPE_COMPONENT; + } + + return component; +} + +// 求解选中参数对应的阻尼正规方程。上下和左右诊断量保留方向;形状没有 +// 天然正负,因此使用 shapeLoss 对参数的局部导数。参数最多八维,使用带 +// 部分主元的高斯消元即可处理该小矩阵,并在主元退化时明确返回失败。 +static bool solveTrustRegionLinearSystem( + QVector > matrix, + QVector rightHandSide, + QVector* solution) +{ + if(!solution || matrix.isEmpty() || + matrix.size() != rightHandSide.size()) { + return false; + } + + const int size = matrix.size(); + for(int i = 0; i < size; ++i) { + if(matrix[i].size() != size) { + return false; + } + } + + for(int column = 0; column < size; ++column) { + int pivotRow = column; + double pivotMagnitude = qAbs(matrix[column][column]); + for(int row = column + 1; row < size; ++row) { + double magnitude = qAbs(matrix[row][column]); + if(magnitude > pivotMagnitude) { + pivotMagnitude = magnitude; + pivotRow = row; + } + } + if(pivotMagnitude <= 1.0e-14) { + return false; + } + + if(pivotRow != column) { + qSwap(matrix[pivotRow], matrix[column]); + qSwap(rightHandSide[pivotRow], rightHandSide[column]); + } + + for(int row = column + 1; row < size; ++row) { + double factor = matrix[row][column] / + matrix[column][column]; + matrix[row][column] = 0.0; + for(int nextColumn = column + 1; + nextColumn < size; ++nextColumn) { + matrix[row][nextColumn] -= + factor * matrix[column][nextColumn]; + } + rightHandSide[row] -= factor * rightHandSide[column]; + } + } + + solution->fill(0.0, size); + for(int row = size - 1; row >= 0; --row) { + double value = rightHandSide[row]; + for(int column = row + 1; column < size; ++column) { + value -= matrix[row][column] * (*solution)[column]; + } + double pivot = matrix[row][row]; + if(qAbs(pivot) <= 1.0e-14) { + return false; + } + (*solution)[row] = value / pivot; + if(!isFiniteNumber((*solution)[row])) { + return false; + } + } + return true; +} + +// 计算两个 Jacobian 列向量的绝对余弦相似度。接近 1 表示两个参数在当前 +// 工作点对曲线的影响几乎相同,联合调整容易产生不可辨识方向。 +static double trustRegionJacobianColumnCorrelation( + const QVector >& jacobian, + int leftColumn, + int rightColumn) +{ + double product = 0.0; + double leftNorm = 0.0; + double rightNorm = 0.0; + for(int row = 0; row < jacobian.size(); ++row) { + if(leftColumn >= jacobian[row].size() || + rightColumn >= jacobian[row].size()) { + return 1.0; + } + double left = jacobian[row][leftColumn]; + double right = jacobian[row][rightColumn]; + product += left * right; + leftNorm += left * left; + rightNorm += right * right; + } + + if(leftNorm <= 1.0e-20 || rightNorm <= 1.0e-20) { + return 0.0; + } + return qAbs(product) / qSqrt(leftNorm * rightNorm); +} + +// 每次接受一个真实候选后,使用满足最新割线条件的秩一修正更新完整残差 +// Jacobian。这样模型吸收了刚得到的真实变化,又不必立即逐参数重新试算。 +static void updateTrustRegionJacobian( + QVector >* jacobian, + const QVector& oldResidual, + const QVector& newResidual, + const QVector& coordinateStep) +{ + if(!jacobian || jacobian->size() != oldResidual.size() || + oldResidual.size() != newResidual.size()) { + return; + } + + double denominator = trustRegionSquaredNorm(coordinateStep); + if(denominator <= 1.0e-12) { + return; + } + + for(int row = 0; row < jacobian->size(); ++row) { + if((*jacobian)[row].size() != coordinateStep.size()) { + return; + } + + double predictedChange = 0.0; + for(int column = 0; column < coordinateStep.size(); ++column) { + predictedChange += + (*jacobian)[row][column] * coordinateStep[column]; + } + double correction = + (newResidual[row] - oldResidual[row] - predictedChange) / + denominator; + for(int column = 0; column < coordinateStep.size(); ++column) { + (*jacobian)[row][column] += + correction * coordinateStep[column]; + } + } +} + +// 对上下偏差、左右偏差和形状损失的梯度执行同样的割线秩一修正,使诊断 +// 选参模型与完整残差 Jacobian 保持在同一个已接受工作点。 +static void updateTrustRegionScalarGradient( + QVector* gradient, + double oldValue, + double newValue, + const QVector& coordinateStep) +{ + if(!gradient || gradient->size() != coordinateStep.size() || + !isFiniteNumber(oldValue) || !isFiniteNumber(newValue)) { + return; + } + + double denominator = trustRegionSquaredNorm(coordinateStep); + if(denominator <= 1.0e-12) { + return; + } + + double predictedChange = trustRegionDotProduct( + *gradient, coordinateStep); + double correction = + (newValue - oldValue - predictedChange) / denominator; + for(int i = 0; i < gradient->size(); ++i) { + (*gradient)[i] += correction * coordinateStep[i]; + } +} + +static QStringList traceParameterNames() +{ + QStringList names; + names << "k" + << "skin" + << "wellboreC" + << "phi" + << "h" + << "Ct" + << "Cf" + << "Swi" + << "Dfc" + << "fractureHalfLength"; + return names; +} + +nmCalculationAutoFitLM::nmCalculationAutoFitLM(QObject* parent) + : QObject(parent) + , m_isRunning(false) + , m_shouldStop(false) + , m_currentIteration(0) + , m_globalBestFitness(1e10) + , m_maxIterations(100) + , m_targetError(0.001) + , m_totalEvaluations(0) + , m_successfulEvaluations(0) + , m_evaluationInProgress(0) + , m_consecutiveFailures(0) + , m_userInitialFitness(1e10) + , m_maxConsecutiveFailures(3) + , m_hasValidUserSolution(false) + , m_targetWellName("") + , m_traceRunId("") + , m_traceFilePath("") + , m_traceMetaFilePath("") +{ + // LM 对象只初始化信赖域运行状态和求解器临时目录。 + initializeTemporaryDirectory(); + DEBUG_OUT("LM automatic fitting calculator initialized"); +} + +// 析构函数:停止仍在进行的拟合、关闭 trace 文件并清理临时目录。 +// 自动拟合可能在 UI 线程中被窗口关闭打断,因此析构时要尽量温和地等待当前评价结束; +// 如果等待超时,再强制清除运行标志,避免对象销毁后还有信号回调访问成员变量。 +nmCalculationAutoFitLM::~nmCalculationAutoFitLM() +{ + if(m_isRunning) { + m_shouldStop = true; + int waitCount = 0; + while(m_isRunning && waitCount < 100) { + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 50); + msleep(50); + waitCount++; + } + if(m_isRunning) { + DEBUG_OUT("Force stopping LM fitting after timeout"); + m_isRunning = false; + } + } + + closeTraceFile(); + cleanupTemporaryDirectory(); + disconnect(this, nullptr, nullptr, nullptr); +} + +// ===== 临时目录工具 ===== +// +// 真实求解器 DLL 和自动拟合过程会产生中间文件,因此每次创建独立的 +// autofit_temp__ 目录。退出时只删除本类创建的目录,启动时顺便清理 +// 旧进程遗留的 autofit_temp_*,避免长期调试后应用目录被临时文件堆满。 + +void nmCalculationAutoFitLM::initializeTemporaryDirectory() +{ + // 先清理历史遗留目录,再为本次对象创建唯一目录。 + // 目录名包含进程 ID 和毫秒时间戳,通常足够唯一;counter 是极端重名时的兜底。 + cleanupOldTemporaryDirectories(); + + QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss_zzz"); + QString processId = QString::number(QCoreApplication::applicationPid()); + + m_tempDirectory = QApplication::applicationDirPath() + + "/autofit_temp_" + processId + "_" + timestamp; + + int counter = 0; + QString originalPath = m_tempDirectory; + + while(QDir(m_tempDirectory).exists() && counter < 100) { + m_tempDirectory = originalPath + "_" + QString::number(counter); + counter++; + } + + if(QDir().mkpath(m_tempDirectory)) { + DEBUG_OUT(QString("Initialized temp directory: %1").arg(m_tempDirectory)); + } else { + DEBUG_OUT(QString("Warning: Failed to create temp directory: %1").arg(m_tempDirectory)); + m_tempDirectory = QApplication::applicationDirPath(); + } +} + +void nmCalculationAutoFitLM::cleanupTemporaryDirectory() +{ + // 析构或用户停止时调用。删除失败一般是文件仍被 DLL/系统占用, + // 这里只记录 debug 信息,不让清理失败影响 UI 退出。 + if(QDir(m_tempDirectory).exists()) { + if(removeDirectoryRecursively(m_tempDirectory)) { + DEBUG_OUT("Temp directory cleaned up successfully"); + } else { + DEBUG_OUT("Warning: Failed to clean up temp directory completely"); + } + } +} + +bool nmCalculationAutoFitLM::removeDirectoryRecursively(const QString& path) +{ + // Qt 旧版本没有统一可用的 removeRecursively 行为时,用本函数递归删除。 + // 调用方传入的是本类创建的临时目录或旧 autofit_temp_* 目录。 + QDir dir(path); + + if(!dir.exists()) { + return true; + } + + // 递归删除子目录和文件。包含 Hidden,避免隐藏中间文件阻塞目录删除。 + QFileInfoList entries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries | QDir::Hidden); + bool allRemoved = true; + + for(int i = 0; i < entries.size(); ++i) { + const QFileInfo& entry = entries[i]; + + if(entry.isDir()) { + if(!removeDirectoryRecursively(entry.absoluteFilePath())) { + allRemoved = false; + } + } else { + QFile file(entry.absoluteFilePath()); + + // 处理只读文件。某些求解器输出可能带只读属性,删除前先补写权限。 + if(!file.permissions().testFlag(QFile::WriteUser)) { + file.setPermissions(file.permissions() | QFile::WriteUser); + } + + if(!file.remove()) { + DEBUG_OUT(QString("Failed to remove file: %1").arg(entry.absoluteFilePath())); + allRemoved = false; + } + } + } + + // 删除目录本身 + if(allRemoved) { + return dir.rmdir(path); + } + + return false; +} + +void nmCalculationAutoFitLM::cleanupOldTemporaryDirectories() +{ + // 应用启动或新建自动拟合对象时清理旧目录。 + // 不删除当前进程 ID 对应目录,防止同进程内多个拟合对象或正在运行的求解器被误删。 + QString appDir = QApplication::applicationDirPath(); + QDir dir(appDir); + + // 获取当前进程ID,避免删除当前进程可能使用的目录 + QString currentProcessId = QString::number(QCoreApplication::applicationPid()); + + // 查找所有以 "autofit_temp_" 开头的目录 + QStringList filters; + filters << "autofit_temp_*"; + QFileInfoList tempDirs = dir.entryInfoList(filters, QDir::Dirs | QDir::NoDotAndDotDot); + + if(tempDirs.isEmpty()) { + DEBUG_OUT("No old temporary directories found"); + return; + } + + DEBUG_OUT(QString("Found %1 potential old temporary directories to clean up").arg(tempDirs.size())); + + int cleanedCount = 0; + int failedCount = 0; + int skippedCount = 0; + + for(int i = 0; i < tempDirs.size(); ++i) { + const QFileInfo& dirInfo = tempDirs[i]; + QString dirPath = dirInfo.absoluteFilePath(); + QString dirName = dirInfo.fileName(); + + // 检查是否是当前进程的目录(虽然理论上不应该存在,但为了安全起见) + if(dirName.contains("_" + currentProcessId + "_")) { + DEBUG_OUT(QString("Skipping current process directory: %1").arg(dirName)); + skippedCount++; + continue; + } + + // 尝试删除目录 + DEBUG_OUT(QString("Attempting to remove old temp directory: %1").arg(dirName)); + + if(removeDirectoryRecursively(dirPath)) { + DEBUG_OUT(QString("Successfully cleaned up: %1").arg(dirName)); + cleanedCount++; + } else { + DEBUG_OUT(QString("Failed to clean up: %1 (may be in use by another process)").arg(dirName)); + failedCount++; + } + } + + // 输出清理统计信息 + DEBUG_OUT(QString("Old temp directories cleanup summary: %1 removed, %2 failed, %3 skipped") + .arg(cleanedCount).arg(failedCount).arg(skippedCount)); +} + + +// ==================== 公共接口方法 ==================== + +void nmCalculationAutoFitLM::setTargetLogLogData(const QVector >& targetData) +{ + // 目标曲线由界面层从目标井 history log-log 传入。 + // 约定 targetData[0]=time,targetData[1]=pressure,targetData[2]=pressure derivative。 + m_targetLogLogData = targetData; + DEBUG_OUT(QString("Target LogLog data set: %1 arrays").arg(targetData.size())); + + if(targetData.size() >= 3) { + DEBUG_OUT(QString("LogLog data points: X=%1, Y1=%2, Y2=%3") + .arg(targetData[0].size()) + .arg(targetData[1].size()) + .arg(targetData[2].size())); + } +} + + +void nmCalculationAutoFitLM::stopFitting() +{ + // 用户点击停止时只设置请求标志,让 LM 主循环和求解器等待逻辑自然退出。 + if(m_isRunning) { + emit logMessageGenerated(tr("=== User Stop Request Received ===")); + emit logMessageGenerated(tr("Gracefully stopping LM automatic fitting...")); + m_shouldStop = true; + + // 给当前评价一个短暂的自然退出时间。若仍在运行, + // runSolverDll() 会在下一个等待周期检查 m_shouldStop 并结束任务。 + int waitCount = 0; + + while(m_evaluationInProgress > 0 && waitCount < 30) { + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 50); + msleep(50); + waitCount++; + } + + if(m_evaluationInProgress > 0) { + emit logMessageGenerated(tr("Waiting for current solver evaluation to stop...")); + } + + emit logMessageGenerated(tr("LM automatic fitting stop request processed")); + } else { + emit logMessageGenerated(tr("Stop request received but optimization is not running")); + } +} + +bool nmCalculationAutoFitLM::isRunning() const +{ + // UI 查询当前是否处于自动拟合运行状态。 + return m_isRunning; +} + +int nmCalculationAutoFitLM::getCurrentIteration() const +{ + // UI 进度条和日志展示用的当前迭代序号。 + return m_currentIteration; +} + +QVector nmCalculationAutoFitLM::getBestSolution() const +{ + // 返回紧凑的“启用参数向量”,顺序与 m_enabledParamIndices 一致。 + return m_globalBestPosition; +} + +double nmCalculationAutoFitLM::getBestFitness() const +{ + // 当前全局最优真实误差。越小越好,1e10 附近通常表示尚无有效解。 + return m_globalBestFitness; +} + +AutoFitObjectiveBreakdownLM nmCalculationAutoFitLM::getLastObjectiveBreakdown() const +{ + // 返回最近一次损失评价的误差分解,供界面或后续优化逻辑读取。 + return m_lastObjectiveBreakdown; +} + +QString nmCalculationAutoFitLM::getLastError() const +{ + // 上一次失败的人类可读错误信息,主要给 UI 层弹窗或日志使用。 + return m_lastError; +} + +void nmCalculationAutoFitLM::resetOptimizer() +{ + // 清空一次运行产生的状态,但不销毁对象。 + // 配置字段会在 startAutoFitting() 中重新从 DataManager 读取; + // trace 文件先关闭,避免新一轮 run 继续写到旧 CSV。 + closeTraceFile(); + m_globalBestPosition.clear(); + m_globalBestFitness = 1e10; + m_globalBestObjectiveBreakdown = AutoFitObjectiveBreakdownLM(); + m_lastEvaluatedLogLogData.clear(); + m_globalBestLogLogData.clear(); + m_lastObjectiveBreakdown = AutoFitObjectiveBreakdownLM(); + m_userInitialLogLogData.clear(); + m_userInitialObjectiveBreakdown = AutoFitObjectiveBreakdownLM(); + m_currentIteration = 0; + m_totalEvaluations = 0; + m_successfulEvaluations = 0; + m_lastError.clear(); + m_initialValues.clear(); + m_userInitialSolution.clear(); + m_userInitialFitness = 1e10; + m_hasValidUserSolution = false; + m_traceMetaFilePath.clear(); + + DEBUG_OUT("LM optimizer reset"); +} + +void nmCalculationAutoFitLM::setTargetWellName(const QString& wellName) +{ + // 目标井名是贯穿拟合流程的关键索引: + // 读目标曲线、写 skin/wellboreC、求解后取 resultLogLog 都依赖这个名字。 + m_targetWellName = wellName; +} + +void nmCalculationAutoFitLM::initializeTraceFile() +{ + // LM 轨迹独立保存在应用目录,不依赖机器学习目录或外部评分进程。 + closeTraceFile(); + m_traceRunId = QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss_zzz"); + QDir traceDir(QDir(QApplication::applicationDirPath()).absoluteFilePath("autofit_lm_trace")); + + if(!traceDir.exists() && !QDir().mkpath(traceDir.absolutePath())) { + DEBUG_OUT(QString("Failed to create LM trace directory: %1").arg(traceDir.absolutePath())); + m_traceFilePath.clear(); + m_traceMetaFilePath.clear(); + return; + } + + m_traceFilePath = traceDir.absoluteFilePath( + QString("lm_trust_region_trace_%1.csv").arg(m_traceRunId)); + m_traceMetaFilePath = traceDir.absoluteFilePath( + QString("lm_trust_region_trace_%1.meta.json").arg(m_traceRunId)); + m_traceFile.setFileName(m_traceFilePath); + + if(!m_traceFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + DEBUG_OUT(QString("Failed to open LM trace file: %1").arg(m_traceFilePath)); + m_traceFilePath.clear(); + m_traceMetaFilePath.clear(); + return; + } + + writeTraceHeader(); + writeTraceMetaFile(); + emit logMessageGenerated(tr("LM automatic fitting trace: %1").arg(m_traceFilePath)); +} + +void nmCalculationAutoFitLM::closeTraceFile() +{ + if(m_traceFile.isOpen()) { + m_traceFile.flush(); + m_traceFile.close(); + } +} + +void nmCalculationAutoFitLM::writeTraceHeader() +{ + if(!m_traceFile.isOpen()) { + return; + } + + QStringList cols; + cols << "run_id" + << "iteration" + << "parameter_index" + << "phase" + << "k" + << "skin" + << "wellboreC" + << "phi" + << "h" + << "Ct" + << "Cf" + << "Swi" + << "Dfc" + << "fractureHalfLength" + << "solver_objective" + << "solver_success" + << "elapsed_ms" + << "decision" + << "enabled_param_indices" + << "pressure_loss" + << "derivative_loss" + << "vertical_common_bias" + << "vertical_loss" + << "vertical_reliable" + << "horizontal_physical_shift" + << "horizontal_loss" + << "horizontal_reliable" + << "shape_loss" + << "late_trend_loss" + << "late_slope_bias" + << "late_trend_reliable" + << "registration_ambiguous" + << "coverage"; + + QTextStream out(&m_traceFile); + out << cols.join(",") << "\n"; +} + +void nmCalculationAutoFitLM::writeTraceMetaFile() +{ + if(m_traceMetaFilePath.isEmpty()) { + return; + } + + QFile metaFile(m_traceMetaFilePath); + if(!metaFile.open(QIODevice::WriteOnly | QIODevice::Text)) { + DEBUG_OUT(QString("Failed to open LM trace meta file: %1").arg(m_traceMetaFilePath)); + m_traceMetaFilePath.clear(); + return; + } + + QStringList parameterNames = traceParameterNames(); + QStringList enabledNames; + for(int i = 0; i < m_enabledParamIndices.size(); ++i) { + int paramIndex = m_enabledParamIndices[i]; + enabledNames << ((paramIndex >= 0 && paramIndex < parameterNames.size()) + ? parameterNames[paramIndex] + : QString::number(paramIndex)); + } + + QVector initialFullParams = buildTraceParameterVector(m_initialValues); + QVector targetTime = m_targetLogLogData.size() > 0 + ? m_targetLogLogData[0] : QVector(); + QVector targetPressure = m_targetLogLogData.size() > 1 + ? m_targetLogLogData[1] : QVector(); + QVector targetDerivative = m_targetLogLogData.size() > 2 + ? m_targetLogLogData[2] : QVector(); + + QTextStream out(&metaFile); + out << "{\n"; + out << " \"schema_version\": 1,\n"; + out << " \"trace_type\": \"finite_difference_lm_trust_region\",\n"; + out << " \"run_id\": " << jsonEscape(m_traceRunId) << ",\n"; + out << " \"created_at\": " + << jsonEscape(QDateTime::currentDateTime().toString(Qt::ISODate)) << ",\n"; + out << " \"trace_csv\": " << jsonEscape(QFileInfo(m_traceFilePath).fileName()) << ",\n"; + out << " \"target\": {\n"; + out << " \"well_name\": " << jsonEscape(m_targetWellName) << ",\n"; + out << " \"time\": " << jsonDoubleArray(targetTime) << ",\n"; + out << " \"pressure\": " << jsonDoubleArray(targetPressure) << ",\n"; + out << " \"derivative\": " << jsonDoubleArray(targetDerivative) << "\n"; + out << " },\n"; + out << " \"lm\": {\n"; + out << " \"max_iterations\": " << m_maxIterations << ",\n"; + out << " \"target_error\": " << jsonNumber(m_targetError) << "\n"; + out << " },\n"; + out << " \"parameters\": {\n"; + out << " \"names\": " << jsonStringArray(parameterNames) << ",\n"; + out << " \"enabled_indices\": " << jsonIntArray(m_enabledParamIndices) << ",\n"; + out << " \"enabled_names\": " << jsonStringArray(enabledNames) << ",\n"; + out << " \"selected_flags\": " << jsonBoolArray(m_parameterSelected) << ",\n"; + out << " \"lower\": " << jsonDoubleArray(m_parameterLower) << ",\n"; + out << " \"upper\": " << jsonDoubleArray(m_parameterUpper) << ",\n"; + out << " \"initial_selected\": " << jsonDoubleArray(m_initialValues) << ",\n"; + out << " \"initial_full\": " << jsonDoubleArray(initialFullParams) << "\n"; + out << " }\n"; + out << "}\n"; + metaFile.flush(); + metaFile.close(); +} + +void nmCalculationAutoFitLM::writeTraceRow( + int iteration, + int parameterIndex, + const QString& phase, + const QVector& parameters, + double solverObjective, + bool solverSuccess, + int elapsedMs, + const QString& decision, + const AutoFitObjectiveBreakdownLM* objectiveBreakdown) +{ + if(!m_traceFile.isOpen()) { + return; + } + + QVector fullParams = buildTraceParameterVector(parameters); + QStringList enabledIndices; + for(int i = 0; i < m_enabledParamIndices.size(); ++i) { + enabledIndices << QString::number(m_enabledParamIndices[i]); + } + + QStringList cols; + cols << csvEscape(m_traceRunId) + << QString::number(iteration) + << QString::number(parameterIndex) + << csvEscape(phase); + for(int i = 0; i < 10; ++i) { + cols << traceParamAt(fullParams, i); + } + cols << traceNumber(solverObjective) + << QString::number(solverSuccess ? 1 : 0) + << QString::number(elapsedMs) + << csvEscape(decision) + << csvEscape(enabledIndices.join(";")); + + if(objectiveBreakdown && objectiveBreakdown->valid) { + cols << traceNumber(objectiveBreakdown->pressureLoss) + << traceNumber(objectiveBreakdown->derivativeLoss) + << traceNumber(objectiveBreakdown->verticalCommonBias) + << traceNumber(objectiveBreakdown->verticalLoss) + << QString::number(objectiveBreakdown->verticalReliable ? 1 : 0) + << traceNumber(objectiveBreakdown->horizontalPhysicalShift) + << traceNumber(objectiveBreakdown->horizontalLoss) + << QString::number(objectiveBreakdown->horizontalReliable ? 1 : 0) + << traceNumber(objectiveBreakdown->shapeLoss) + << traceNumber(objectiveBreakdown->lateDerivativeTrendLoss) + << traceNumber(objectiveBreakdown->lateDerivativeSlopeBias) + << QString::number(objectiveBreakdown->lateDerivativeTrendReliable ? 1 : 0) + << QString::number(objectiveBreakdown->registrationAmbiguous ? 1 : 0) + << traceNumber(objectiveBreakdown->coverage); + } else { + for(int i = 0; i < 14; ++i) { + cols << QString(); + } + } + + QTextStream out(&m_traceFile); + out << cols.join(",") << "\n"; + m_traceFile.flush(); +} + +void nmCalculationAutoFitLM::emitRunSummary(bool success, StopReasonLM finalReason) +{ + // 汇总仅描述 LM 迭代和真实求解器评价。 + emit logMessageGenerated(tr("=== LM Run Summary ===")); + emit logMessageGenerated(tr("Stop reason: %1").arg(getStopReasonDescription(finalReason))); + emit logMessageGenerated( + tr("Result: %1, final error=%2, iterations=%3, evaluations=%4 (successful=%5, failed=%6)") + .arg(success ? "SUCCESS" : "FAILED") + .arg(m_globalBestFitness, 0, 'e', 4) + .arg(m_currentIteration + 1) + .arg(m_totalEvaluations) + .arg(m_successfulEvaluations) + .arg(m_totalEvaluations - m_successfulEvaluations)); + + if(!m_traceFilePath.isEmpty()) { + emit logMessageGenerated(tr("Artifacts: trace=%1").arg(m_traceFilePath)); + } + if(!m_traceMetaFilePath.isEmpty()) { + emit logMessageGenerated(tr("Artifacts: trace_meta=%1").arg(m_traceMetaFilePath)); + } +} + +QVector nmCalculationAutoFitLM::buildTraceParameterVector(const QVector& selectedParameters) const +{ + // 将 LM 内部使用的“启用参数向量”还原成完整 10 维参数向量。 + // 未启用的参数从当前 DataManager 读取,启用的参数用 selectedParameters 覆盖。 + // trace CSV 和 meta 使用该完整向量记录一次候选评价。 + QVector fullParams(10, 0.0); + + nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); + + if(dataManager) { + nmDataReservoir reservoirData = dataManager->getReservoirDataCopy(); + fullParams[0] = reservoirData.getPermeability().getValue().toDouble(); + fullParams[3] = reservoirData.getPorosity().getValue().toDouble(); + fullParams[4] = reservoirData.getThickness().getValue().toDouble(); + fullParams[5] = reservoirData.getCt().getValue().toDouble(); + fullParams[6] = reservoirData.getCf().getValue().toDouble(); + fullParams[7] = reservoirData.getSwi().getValue().toDouble(); + + nmDataWellBase* pTargetWell = dataManager->findWellByName(m_targetWellName); + + if(pTargetWell) { + nmDataPerforation* perforation = pTargetWell->getPerforation(0); + if(perforation) { + fullParams[1] = perforation->getSkin().getValue().toDouble(); + } + fullParams[2] = pTargetWell->getWellboreStorage().getValue().toDouble(); + + // Dfc 只存在于两类压裂井,普通井在完整向量中保持为 0。 + if(pTargetWell->getWellType() == NM_WELL_MODEL::Vertical_Fractured_Well) { + nmDataVerticalFracturedWell* fracturedWell = + dynamic_cast(pTargetWell); + if(fracturedWell) { + fullParams[8] = fracturedWell->getDfc().getValue().toDouble(); + fullParams[9] = fracturedWell->getFractureHalfLength().getValue().toDouble(); + } + } else if(pTargetWell->getWellType() == NM_WELL_MODEL::Horizontal_Fractured_Well) { + nmDataHorizontalFracturedWell* fracturedWell = + dynamic_cast(pTargetWell); + if(fracturedWell) { + fullParams[8] = fracturedWell->getDfc().getValue().toDouble(); + fullParams[9] = fracturedWell->getFractureHalfLength().getValue().toDouble(); + } + } + } + } + + for(int i = 0; i < selectedParameters.size() && i < m_enabledParamIndices.size(); ++i) { + int paramIndex = m_enabledParamIndices[i]; + + if(paramIndex >= 0 && paramIndex < fullParams.size()) { + fullParams[paramIndex] = selectedParameters[i]; + } + } + + return fullParams; +} + +// ==================== 数据加载方法 ==================== + +bool nmCalculationAutoFitLM::loadAllConfigFromDataManager() +{ + // 统一从 DataManager 加载本次运行所需配置。 + // UI 层只负责把用户选择保存到 nmDataAutomaticFitting,本类从这里开始完全数据驱动。 + try { + loadOptimizationConfig(); + loadParameterBounds(); + extractUserInitialValues(); // 直接提取初始值,无需条件判断 + return true; + } catch(...) { + m_lastError = "Failed to load configuration from data manager"; + return false; + } +} + +void nmCalculationAutoFitLM::loadOptimizationConfig() +{ + // LM 只读取迭代次数和目标误差,其他数值控制保留在现有算法实现中。 + nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); + nmDataAutomaticFitting fittingData = dataManager->getAutomaticFittingDataCopy(); + + m_maxIterations = fittingData.getIterationCount().getValue().toInt(); + m_targetError = fittingData.getErrorTolerance().getValue().toDouble(); + DEBUG_OUT(QString("Loaded LM config: iterations=%1, error=%2") + .arg(m_maxIterations).arg(m_targetError)); +} + +void nmCalculationAutoFitLM::loadParameterBounds() +{ + // 读取用户勾选的拟合参数及上下界。 + // + // 这里构建三个核心数组: + // - m_parameterSelected[10]:完整参数体系中每个参数是否参与拟合; + // - m_parameterLower/Upper[10]:完整参数体系的搜索上下界; + // - m_enabledParamIndices:把粒子内部紧凑向量映射回完整参数索引。 + nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); + nmDataAutomaticFitting fittingData = dataManager->getAutomaticFittingDataCopy(); + + // 获取参数选择状态 + m_parameterSelected.resize(10); + m_parameterSelected[0] = fittingData.getPermeabilitySelected(); + m_parameterSelected[1] = fittingData.getSkinSelected(); + m_parameterSelected[2] = fittingData.getWellboreStorageSelected(); + m_parameterSelected[3] = fittingData.getPorositySelected(); + m_parameterSelected[4] = fittingData.getThicknessSelected(); + m_parameterSelected[5] = fittingData.getCtSelected(); + m_parameterSelected[6] = fittingData.getCfSelected(); + m_parameterSelected[7] = fittingData.getSwiSelected(); + m_parameterSelected[8] = fittingData.getFractureConductivitySelected(); + m_parameterSelected[9] = fittingData.getFractureHalfLengthSelected(); + + // 获取参数边界 + m_parameterLower.resize(10); + m_parameterUpper.resize(10); + + m_parameterLower[0] = fittingData.getPermeabilityMin().getValue().toDouble(); + m_parameterUpper[0] = fittingData.getPermeabilityMax().getValue().toDouble(); + + m_parameterLower[1] = fittingData.getSkinMin().getValue().toDouble(); + m_parameterUpper[1] = fittingData.getSkinMax().getValue().toDouble(); + + m_parameterLower[2] = fittingData.getWellboreStorageMin().getValue().toDouble(); + m_parameterUpper[2] = fittingData.getWellboreStorageMax().getValue().toDouble(); + + m_parameterLower[3] = fittingData.getPorosityMin().getValue().toDouble(); + m_parameterUpper[3] = fittingData.getPorosityMax().getValue().toDouble(); + + m_parameterLower[4] = fittingData.getThicknessMin().getValue().toDouble(); + m_parameterUpper[4] = fittingData.getThicknessMax().getValue().toDouble(); + + m_parameterLower[5] = fittingData.getCtMin().getValue().toDouble(); + m_parameterUpper[5] = fittingData.getCtMax().getValue().toDouble(); + + m_parameterLower[6] = fittingData.getCfMin().getValue().toDouble(); + m_parameterUpper[6] = fittingData.getCfMax().getValue().toDouble(); + + m_parameterLower[7] = fittingData.getSwiMin().getValue().toDouble(); + m_parameterUpper[7] = fittingData.getSwiMax().getValue().toDouble(); + + m_parameterLower[8] = fittingData.getFractureConductivityMin().getValue().toDouble(); + m_parameterUpper[8] = fittingData.getFractureConductivityMax().getValue().toDouble(); + + m_parameterLower[9] = fittingData.getFractureHalfLengthMin().getValue().toDouble(); + m_parameterUpper[9] = fittingData.getFractureHalfLengthMax().getValue().toDouble(); + + // 更新启用参数索引 + m_enabledParamIndices.clear(); + + for(int i = 0; i < m_parameterSelected.size(); ++i) { + if(m_parameterSelected[i]) { + m_enabledParamIndices.append(i); + } + } + + DEBUG_OUT(QString("Loaded parameter bounds: %1 enabled parameters") + .arg(m_enabledParamIndices.size())); +} + +// ==================== 自动拟合核心方法 ==================== +bool nmCalculationAutoFitLM::startAutoFitting() +{ + // 总入口只负责准备数据、调用有限差分 + LM/信赖域,并写回最终结果。 + StopReasonLM finalReason = LM_CONTINUE_OPTIMIZATION; + + if(m_isRunning) { + m_lastError = "Auto fitting is already running"; + return false; + } + + try { + if(!loadAllConfigFromDataManager()) { + emit logMessageGenerated(tr("ERROR: Failed to load configuration from data manager")); + return false; + } + + emit logMessageGenerated(tr("Algorithm: Finite Difference + LM")); + const int enabledParams = getEnabledParameterCount(); + emit logMessageGenerated(tr("Enabled parameters count: %1").arg(enabledParams)); + + if(enabledParams == 0) { + m_lastError = "No parameters enabled for optimization"; + emit logMessageGenerated(tr("ERROR: No parameters enabled for optimization")); + return false; + } + + if(m_targetLogLogData.size() < 3) { + m_lastError = "Target LogLog data is empty or insufficient"; + emit logMessageGenerated(tr("ERROR: Target LogLog data is empty or insufficient")); + return false; + } + + if(m_targetLogLogData[0].size() != m_targetLogLogData[1].size() || + m_targetLogLogData[0].size() != m_targetLogLogData[2].size()) { + m_lastError = "Target LogLog data arrays have inconsistent sizes"; + emit logMessageGenerated(tr("ERROR: Target LogLog data arrays have inconsistent sizes")); + return false; + } + + if(m_targetWellName.isEmpty()) { + m_lastError = "Target well name is empty"; + emit logMessageGenerated(tr("ERROR: Target well name is empty")); + return false; + } + + emit logMessageGenerated( + tr("Candidate evaluation mode: solve all wells, retain target well '%1' only") + .arg(m_targetWellName)); + + // resetOptimizer() 会清空运行状态,因此先保存从 DataManager 提取的初始值。 + QVector savedInitialValues = m_initialValues; + resetOptimizer(); + m_isRunning = true; + m_shouldStop = false; + m_currentIteration = 0; + m_consecutiveFailures = 0; + m_initialValues = savedInitialValues; + initializeTraceFile(); + + // 先用真实求解器评价用户当前模型,供最终精英保护使用。 + if(!savedInitialValues.isEmpty()) { + m_userInitialSolution = savedInitialValues; + emit logMessageGenerated(tr("=== Evaluating Initial Solution (Elite Protection) ===")); + + QString paramStr = tr("Initial parameters: "); + for(int i = 0; i < m_userInitialSolution.size(); ++i) { + paramStr += QString("[%1]=%2 ") + .arg(i).arg(m_userInitialSolution[i], 0, 'f', 6); + } + emit logMessageGenerated(paramStr); + + try { + QTime initialEvalTimer; + initialEvalTimer.start(); + m_totalEvaluations++; + m_userInitialFitness = evaluateFitness(m_userInitialSolution); + const int initialEvalElapsedMs = initialEvalTimer.elapsed(); + + if(m_userInitialFitness < 1e9) { + m_successfulEvaluations++; + m_hasValidUserSolution = true; + m_globalBestFitness = m_userInitialFitness; + m_globalBestPosition = m_userInitialSolution; + m_userInitialLogLogData = m_lastEvaluatedLogLogData; + m_globalBestLogLogData = m_userInitialLogLogData; + m_userInitialObjectiveBreakdown = m_lastObjectiveBreakdown; + m_globalBestObjectiveBreakdown = m_userInitialObjectiveBreakdown; + emit logMessageGenerated(tr("Initial solution evaluation successful")); + emit logMessageGenerated( + tr("Initial Error: %1").arg(m_userInitialFitness, 0, 'e', 4)); + emit bestCurveUpdated(m_targetLogLogData, + m_globalBestLogLogData, + 0, + m_globalBestFitness); + } else { + m_hasValidUserSolution = false; + emit logMessageGenerated(tr("Initial solution evaluation failed")); + } + + writeTraceRow(-1, + -1, + "initial_solution", + m_userInitialSolution, + m_userInitialFitness, + m_userInitialFitness < 1e9, + initialEvalElapsedMs, + m_hasValidUserSolution ? "valid" : "invalid", + m_hasValidUserSolution + ? &m_userInitialObjectiveBreakdown : nullptr); + } catch(...) { + m_hasValidUserSolution = false; + emit logMessageGenerated(tr("Exception during initial solution evaluation")); + } + + m_initialValues = savedInitialValues; + } + + finalReason = runTrustRegionFitting(); + validateAndProtectFinalResult(); + + if(!m_globalBestPosition.isEmpty() && m_globalBestObjectiveBreakdown.valid) { + // 精英保护之后记录最终行,保证轨迹与实际写回参数一致。 + writeTraceRow(m_currentIteration, + -1, + "trust_region_final", + m_globalBestPosition, + m_globalBestFitness, + m_globalBestFitness < 1.0e9, + -1, + "final_result", + &m_globalBestObjectiveBreakdown); + } + + if(m_globalBestFitness < m_targetError) { + finalReason = LM_TARGET_ACHIEVED; + } + } catch(const std::exception& e) { + m_lastError = QString(tr("Critical exception in automatic fitting: %1")).arg(e.what()); + emit logMessageGenerated(tr("CRITICAL ERROR: %1").arg(e.what())); + closeTraceFile(); + cleanupTemporaryDirectory(); + m_isRunning = false; + emit fittingFinished(false, m_lastError); + return false; + } catch(...) { + m_lastError = tr("Unknown critical exception in automatic fitting"); + emit logMessageGenerated(tr("CRITICAL ERROR: Unknown exception in automatic fitting")); + closeTraceFile(); + cleanupTemporaryDirectory(); + m_isRunning = false; + emit fittingFinished(false, m_lastError); + return false; + } + + bool finalFullSolverSucceeded = true; + bool finalFullSolverExecuted = false; + + if(!m_globalBestPosition.isEmpty()) { + try { + emit logMessageGenerated(tr("Applying optimized parameters to model...")); + applyParametersToDataManager(m_globalBestPosition); + + // 裂缝参数改变时恢复最终已接受参数对应的 PEBI 缓存。 + const bool fractureGridParameterSelected = + (m_parameterSelected.size() > 8 && m_parameterSelected[8]) || + (m_parameterSelected.size() > 9 && m_parameterSelected[9]); + if(fractureGridParameterSelected) { + nmCalculationPebiGrid* pebiGrid = nmCalculationPebiGrid::getInstance(); + if(!pebiGrid || !pebiGrid->generateOutputPara()) { + throw std::runtime_error("Failed to refresh final fracture parameters"); + } + } + + if(m_shouldStop) { + emit logMessageGenerated( + tr("Final full-field calculation skipped after user stop")); + } else { + emit logMessageGenerated( + tr("Running final full-field calculation with optimized parameters...")); + finalFullSolverExecuted = true; + finalFullSolverSucceeded = runFinalFullSolver(); + + if(finalFullSolverSucceeded) { + emit logMessageGenerated( + tr("Final full-field calculation completed successfully")); + } else if(m_shouldStop) { + finalFullSolverExecuted = false; + finalFullSolverSucceeded = true; + emit logMessageGenerated( + tr("Final full-field calculation stopped by user")); + } else { + m_lastError = + tr("Optimized parameters were found, but the final full-field calculation failed"); + emit logMessageGenerated( + tr("ERROR: Final full-field calculation failed")); + } + } + + saveOptimizationResult(); + emit logMessageGenerated(tr("=== Optimization Results ===")); + emit logMessageGenerated( + tr("Final error: %1").arg(m_globalBestFitness, 0, 'e', 4)); + emit logMessageGenerated( + tr("Total iterations: %1").arg(m_currentIteration + 1)); + emit logMessageGenerated( + tr("Total evaluations: %1 (successful: %2)") + .arg(m_totalEvaluations).arg(m_successfulEvaluations)); + + QString finalParams = tr("Optimized parameters: "); + for(int i = 0; i < m_globalBestPosition.size(); ++i) { + finalParams += QString("[%1]=%2 ") + .arg(i).arg(m_globalBestPosition[i], 0, 'f', 6); + } + emit logMessageGenerated(finalParams); + + if(finalFullSolverExecuted && finalFullSolverSucceeded) { + emit logMessageGenerated( + tr("Parameters and full-field results applied successfully to data manager")); + } else if(!finalFullSolverExecuted) { + emit logMessageGenerated( + tr("Optimized parameters applied to data manager")); + } + } catch(const std::exception& e) { + finalFullSolverSucceeded = false; + m_lastError = QString("Failed to apply final parameters: %1").arg(e.what()); + emit logMessageGenerated( + tr("ERROR: Failed to apply final parameters: %1").arg(e.what())); + } catch(...) { + finalFullSolverSucceeded = false; + m_lastError = "Failed to apply final parameters due to unknown error"; + emit logMessageGenerated( + tr("ERROR: Unknown error applying final parameters")); + } + } + + m_isRunning = false; + bool success = false; + QString message; + + if(finalReason == LM_TARGET_ACHIEVED) { + success = true; + message = QString(tr("Target achieved. Best error: %1, Iterations: %2")) + .arg(m_globalBestFitness, 0, 'e', 4) + .arg(m_currentIteration + 1); + emit logMessageGenerated(tr("=== LM AUTOMATIC FITTING SUCCESSFUL ===")); + } else if(finalReason == LM_TRUE_CONVERGENCE) { + success = true; + message = QString( + tr("Automatic fitting converged to a stable solution. Best error: %1, Iterations: %2")) + .arg(m_globalBestFitness, 0, 'e', 4) + .arg(m_currentIteration + 1); + emit logMessageGenerated(tr("=== LM AUTOMATIC FITTING CONVERGED ===")); + } else if(finalReason == LM_LOCAL_OPTIMUM) { + success = true; + message = QString( + tr("Automatic fitting reached a local optimum. Best error: %1, Iterations: %2")) + .arg(m_globalBestFitness, 0, 'e', 4) + .arg(m_currentIteration + 1); + emit logMessageGenerated(tr("=== LM AUTOMATIC FITTING - LOCAL OPTIMUM ===")); + } else if(finalReason == LM_MAX_ITERATIONS) { + success = true; + message = QString(tr("Max iterations reached. Best error: %1, Iterations: %2")) + .arg(m_globalBestFitness, 0, 'e', 4) + .arg(m_currentIteration + 1); + emit logMessageGenerated(tr("=== LM AUTOMATIC FITTING - MAX ITERATIONS ===")); + } else if(finalReason == LM_USER_STOPPED) { + success = true; + message = QString(tr("Best error: %1, Iterations: %2")) + .arg(m_globalBestFitness, 0, 'e', 4) + .arg(m_currentIteration + 1); + emit logMessageGenerated(tr("=== LM AUTOMATIC FITTING STOPPED BY USER ===")); + } else if(finalReason == LM_CONSECUTIVE_FAILURES) { + message = QString( + tr("Automatic fitting failed due to consecutive failures. Best error: %1, Iterations: %2")) + .arg(m_globalBestFitness, 0, 'e', 4) + .arg(m_currentIteration + 1); + emit logMessageGenerated(tr("=== LM AUTOMATIC FITTING FAILED ===")); + } else { + message = QString( + tr("Automatic fitting ended unexpectedly. Best error: %1, Iterations: %2")) + .arg(m_globalBestFitness, 0, 'e', 4) + .arg(m_currentIteration + 1); + emit logMessageGenerated(tr("=== LM AUTOMATIC FITTING - UNKNOWN END ===")); + } + + if(!finalFullSolverSucceeded) { + success = false; + message = m_lastError; + } + + emitRunSummary(success, finalReason); + emit progressUpdated(m_maxIterations, m_globalBestFitness); + QApplication::processEvents(); + msleep(200); + QApplication::processEvents(); + closeTraceFile(); + emit fittingFinished(success, message); + cleanupTemporaryDirectory(); + return success; +} + +void nmCalculationAutoFitLM::extractUserInitialValues() +{ + // 从当前项目模型读取用户已有初始参数。 + // 只提取用户勾选的参数,并按 m_enabledParamIndices 的顺序写入 m_initialValues。 + // 这些值用于初始解真实评价、LM 起点和最终精英保护。 + nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); + nmDataReservoir reservoirData = dataManager->getReservoirDataCopy(); + //QVector wells = dataManager->getWellDataList(); + + nmDataWellBase* pTargetWell = dataManager->findWellByName(m_targetWellName); + + m_initialValues.clear(); + + // 按照启用参数的顺序提取初始值。井参数来自目标井,储层参数来自 reservoirData。 + for(int i = 0; i < m_enabledParamIndices.size(); ++i) { + int paramIndex = m_enabledParamIndices[i]; + double initialValue = 0.0; + + switch(paramIndex) { + case 0: // 渗透率 + initialValue = reservoirData.getPermeability().getValue().toDouble(); + break; + + case 1: // 表皮系数 + if(pTargetWell) { + initialValue = pTargetWell->getPerforation(0)->getSkin().getValue().toDouble(); + } + + break; + + case 2: // 井筒储集系数 + if(pTargetWell) { + initialValue = pTargetWell->getWellboreStorage().getValue().toDouble(); + } + + break; + + case 3: // 孔隙度 + initialValue = reservoirData.getPorosity().getValue().toDouble(); + break; + + case 4: // 储层厚度 + initialValue = reservoirData.getThickness().getValue().toDouble(); + break; + + case 5: // 综合压缩系数 + initialValue = reservoirData.getCt().getValue().toDouble(); + break; + + case 6: // 岩石压缩系数 + initialValue = reservoirData.getCf().getValue().toDouble(); + break; + + case 7: // 初始含水饱和度 + initialValue = reservoirData.getSwi().getValue().toDouble(); + break; + + case 8: // 裂缝导流能力 + if(pTargetWell && pTargetWell->getWellType() == NM_WELL_MODEL::Vertical_Fractured_Well) { + nmDataVerticalFracturedWell* fracturedWell = + dynamic_cast(pTargetWell); + if(fracturedWell) { + initialValue = fracturedWell->getDfc().getValue().toDouble(); + } + } else if(pTargetWell && pTargetWell->getWellType() == NM_WELL_MODEL::Horizontal_Fractured_Well) { + nmDataHorizontalFracturedWell* fracturedWell = + dynamic_cast(pTargetWell); + if(fracturedWell) { + initialValue = fracturedWell->getDfc().getValue().toDouble(); + } + } + break; + + case 9: // 裂缝半长 + if(pTargetWell && pTargetWell->getWellType() == NM_WELL_MODEL::Vertical_Fractured_Well) { + nmDataVerticalFracturedWell* fracturedWell = + dynamic_cast(pTargetWell); + if(fracturedWell) { + initialValue = fracturedWell->getFractureHalfLength().getValue().toDouble(); + } + } else if(pTargetWell && pTargetWell->getWellType() == NM_WELL_MODEL::Horizontal_Fractured_Well) { + nmDataHorizontalFracturedWell* fracturedWell = + dynamic_cast(pTargetWell); + if(fracturedWell) { + initialValue = fracturedWell->getFractureHalfLength().getValue().toDouble(); + } + } + break; + } + + m_initialValues.append(initialValue); + } + + DEBUG_OUT(QString("Extracted %1 user initial values").arg(m_initialValues.size())); + + for(int i = 0; i < m_initialValues.size(); ++i) { + DEBUG_OUT(QString(" Initial[%1] = %2").arg(i).arg(m_initialValues[i], 0, 'e', 3)); + } + + // 验证初始值 + if(!validateInitialValues()) { + DEBUG_OUT("Warning: Some initial values are outside parameter bounds"); + } +} + +bool nmCalculationAutoFitLM::evaluateTrustRegionPoint( + const QVector& parameters, + double* fitness, + AutoFitObjectiveBreakdownLM* breakdown, + QVector >* curve, + int* elapsedMs) +{ + if(!fitness || !breakdown || !curve || !elapsedMs || m_shouldStop) { + return false; + } + + // evaluateFitness() 会写入 DataManager 并调用真实求解器。这里统一统计 + // 真实评价次数和耗时,同时严格要求固定残差、诊断结构和结果曲线均有效。 + QTime timer; + timer.start(); + *fitness = evaluateFitness(parameters); + *elapsedMs = timer.elapsed(); + *breakdown = m_lastObjectiveBreakdown; + *curve = m_lastEvaluatedLogLogData; + ++m_totalEvaluations; + + bool valid = isFiniteNumber(*fitness) && *fitness < 1.0e9 && + breakdown->valid && + trustRegionResidualsValid(*breakdown) && + !curve->isEmpty(); + if(valid) { + ++m_successfulEvaluations; + } + + return valid; +} + +StopReasonLM nmCalculationAutoFitLM::runTrustRegionFitting() +{ + const int dimensions = getEnabledParameterCount(); + if(dimensions <= 0 || m_enabledParamIndices.size() != dimensions) { + m_lastError = tr("No valid parameters are available for trust-region fitting"); + return LM_OPTIMIZATION_FAILED; + } + + // 真实求解次数比“外层迭代次数”更能反映耗时。预算至少允许完成一次全参数 + // 灵敏度和两次候选评价,同时避免连续重建 Jacobian 导致运行时间失控。 + const int maximumEvaluations = qMax( + m_totalEvaluations + dimensions + 2, + qMax(20, m_maxIterations * 3)); + // 下列步长均位于归一化内部坐标:0.04 表示参数范围的 4%,信赖半径 + // 限制一次联合移动的二范数,相关性门槛用于排除响应近乎共线的参数。 + const double sensitivityStep = 0.04; + const double minimumCoordinateStep = 1.0e-5; + const double minimumTrustRadius = 2.0e-3; + const double maximumTrustRadius = 0.30; + const double columnCorrelationLimit = 0.995; + const double diagnosisThreshold = 1.0e-5; + // 误差下降至少达到绝对 1e-5 且相对当前有效基准 0.2% 才算有效改善。 + // 更小的下降仍保留为最佳解,但不能反复清除停滞状态、延长拟合时间。 + const double effectiveRelativeImprovement = 2.0e-3; + const double effectiveAbsoluteImprovement = 1.0e-5; + const int maximumIneffectiveSteps = 3; + + // damping 是 LM 阻尼;拒绝或预测失准时增大,真实下降与预测一致时减小。 + // 两组累计量控制 Jacobian 重建,避免长期使用已偏离当前工作点的局部模型。 + double trustRadius = 0.12; + double damping = 1.0e-2; + int consecutiveRejectedSteps = 0; + int consecutiveSolverFailures = 0; + int acceptedSinceRebuild = 0; + int consecutiveIneffectiveSteps = 0; + double movementSinceRebuild = 0.0; + bool rebuildRequested = true; + bool modelRebuiltAtMinimumRadius = false; + bool stagnationConfirmationRequested = false; + StopReasonLM stopReason = LM_MAX_ITERATIONS; + + // jacobian 的行对应固定 160 维残差,列对应用户勾选的参数。 + // 三个 gradient 单独描述诊断分量对参数的局部变化,只用于本轮选参。 + QVector > jacobian; + QVector verticalGradient(dimensions, 0.0); + QVector horizontalGradient(dimensions, 0.0); + QVector shapeGradient(dimensions, 0.0); + QVector jacobianColumnValid(dimensions, false); + + // 参数向量的顺序始终与 m_enabledParamIndices 一致,不能按完整参数索引 + // 直接访问;下面两个转换函数集中维护这层映射关系。 + auto coordinatesFromParameters = [&](const QVector& parameters) + -> QVector { + QVector coordinates(dimensions, 0.0); + for(int i = 0; i < dimensions; ++i) { + int parameterIndex = m_enabledParamIndices[i]; + coordinates[i] = toTrustRegionCoordinate( + parameters[i], parameterIndex, + m_parameterLower[parameterIndex], + m_parameterUpper[parameterIndex]); + } + return coordinates; + }; + + auto parametersFromCoordinates = [&](const QVector& coordinates) + -> QVector { + QVector parameters(dimensions, 0.0); + for(int i = 0; i < dimensions; ++i) { + int parameterIndex = m_enabledParamIndices[i]; + parameters[i] = fromTrustRegionCoordinate( + coordinates[i], parameterIndex, + m_parameterLower[parameterIndex], + m_parameterUpper[parameterIndex]); + } + return parameters; + }; + + auto restoreEvaluationState = [&](const TrustRegionEvaluation& evaluation) { + // evaluateFitness() 会把试算参数写入 DataManager。无论候选是否接受, + // 下一次计算前都恢复到唯一的已接受工作点,防止失败试算污染后续求解。 + applyParametersToDataManager(evaluation.parameters); + m_lastObjectiveBreakdown = evaluation.breakdown; + m_lastEvaluatedLogLogData = evaluation.curve; + }; + + // 只有真实总误差更小的工作点才能发布为全局最优;曲线和诊断快照必须 + // 与参数同步更新,防止界面显示或最终精英保护使用错配的数据。 + auto publishAcceptedPoint = [&](const TrustRegionEvaluation& evaluation) { + m_globalBestPosition = evaluation.parameters; + m_globalBestFitness = evaluation.fitness; + m_globalBestObjectiveBreakdown = evaluation.breakdown; + m_globalBestLogLogData = evaluation.curve; + emit bestCurveUpdated(m_targetLogLogData, + m_globalBestLogLogData, + m_currentIteration + 1, + m_globalBestFitness); + }; + + auto processPauseAndStop = [&]() -> bool { + QApplication::processEvents(); + return !m_shouldStop; + }; + + // current 始终代表唯一已接受工作点。优先复用启动阶段已经真实验证的 + // 用户初始解,避免在信赖域入口重复调用一次昂贵求解器。 + TrustRegionEvaluation current; + if(m_hasValidUserSolution && + m_globalBestPosition.size() == dimensions && + trustRegionResidualsValid(m_globalBestObjectiveBreakdown) && + !m_globalBestLogLogData.isEmpty()) { + current.parameters = m_globalBestPosition; + current.coordinates = coordinatesFromParameters(current.parameters); + current.breakdown = m_globalBestObjectiveBreakdown; + current.curve = m_globalBestLogLogData; + current.fitness = m_globalBestFitness; + current.elapsedMs = 0; + current.valid = true; + } else { + // 用户初始解无效时只做一次确定性的范围中点回退;所有正值参数在对数 + // 坐标取中点,避免线性中点过分偏向跨数量级范围的上界。 + current.coordinates.fill(0.5, dimensions); + current.parameters = parametersFromCoordinates(current.coordinates); + current.valid = evaluateTrustRegionPoint( + current.parameters, + ¤t.fitness, + ¤t.breakdown, + ¤t.curve, + ¤t.elapsedMs); + writeTraceRow(-1, -1, + "trust_region_midpoint", + current.parameters, + current.fitness, + current.valid, + current.elapsedMs, + current.valid ? "midpoint_valid" : "midpoint_invalid", + current.valid ? ¤t.breakdown : nullptr); + if(!current.valid) { + m_lastError = tr("The initial solution and parameter-range midpoint are both invalid"); + return m_shouldStop + ? LM_USER_STOPPED + : LM_OPTIMIZATION_FAILED; + } + publishAcceptedPoint(current); + } + + restoreEvaluationState(current); + emit logMessageGenerated( + tr("Trust-region initial error: %1; evaluation budget: %2") + .arg(current.fitness, 0, 'e', 4) + .arg(maximumEvaluations)); + + // 有效改善始终相对“上一次有效改善后的误差”累计判断,避免一连串微小 + // 下降每次都清零计数;累计达到门槛后才开始新的有效改善基准。 + double effectiveImprovementBaseline = current.fitness; + auto registerEffectiveImprovement = [&](double fitness) -> bool { + const double requiredImprovement = qMax( + effectiveAbsoluteImprovement, + qAbs(effectiveImprovementBaseline) * + effectiveRelativeImprovement); + const double improvement = effectiveImprovementBaseline - fitness; + if(improvement < requiredImprovement) { + return false; + } + + effectiveImprovementBaseline = fitness; + consecutiveIneffectiveSteps = 0; + stagnationConfirmationRequested = false; + return true; + }; + + // 连续三次没有有效改善时只请求一次灵敏度重建。重建完成后由主循环 + // 直接检查累计改善,仍达不到门槛就判定局部收敛,不再继续微小试探。 + auto recordIneffectiveStep = [&]() -> bool { + ++consecutiveIneffectiveSteps; + if(consecutiveIneffectiveSteps < maximumIneffectiveSteps) { + return false; + } + + if(stagnationConfirmationRequested) { + return true; + } + + consecutiveIneffectiveSteps = 0; + stagnationConfirmationRequested = true; + rebuildRequested = true; + emit logMessageGenerated( + tr("No effective improvement for %1 consecutive steps; " + "rebuilding sensitivity model for confirmation") + .arg(maximumIneffectiveSteps)); + return false; + }; + + emit logMessageGenerated( + tr("Effective improvement threshold: max(%1, %2% of baseline error); " + "%3 consecutive ineffective steps trigger convergence confirmation") + .arg(effectiveAbsoluteImprovement, 0, 'e', 2) + .arg(effectiveRelativeImprovement * 100.0, 0, 'f', 2) + .arg(maximumIneffectiveSteps)); + + if(current.fitness < m_targetError) { + return LM_TARGET_ACHIEVED; + } + + // 在同一个真实工作点逐参数做单边差分。首选可用空间更大的方向;只有该方向 + // 求解失败时才补算反方向,因此初次建模通常每个参数只增加一次真实求解。 + auto rebuildSensitivity = [&]() -> bool { + const TrustRegionEvaluation base = current; + const int residualCount = base.breakdown.residualVector.size(); + if(residualCount <= 0) { + return false; + } + + jacobian = QVector >( + residualCount, QVector(dimensions, 0.0)); + verticalGradient.fill(0.0, dimensions); + horizontalGradient.fill(0.0, dimensions); + shapeGradient.fill(0.0, dimensions); + jacobianColumnValid.fill(false, dimensions); + + TrustRegionEvaluation bestProbe; + int bestProbeColumn = -1; + double bestProbeDelta = 0.0; + // 差分步长不超过参数范围的 4%,信赖域收缩后同步减小,但保留 0.5% + // 下限,避免步长太小使求解器数值噪声淹没真实灵敏度。 + const double finiteDifferenceStep = qMin( + sensitivityStep, + qMax(5.0e-3, trustRadius * 0.5)); + + for(int column = 0; + column < dimensions && + m_totalEvaluations < maximumEvaluations && + processPauseAndStop(); + ++column) { + // 单边差分优先选择离边界空间更大的方向;首方向求解无效时才反向 + // 补算,因此正常情况下每个参数只消耗一次真实求解。 + double positiveRoom = 1.0 - base.coordinates[column]; + double negativeRoom = base.coordinates[column]; + double preferredSign = positiveRoom >= negativeRoom ? 1.0 : -1.0; + bool columnBuilt = false; + + for(int directionAttempt = 0; + directionAttempt < 2 && + !columnBuilt && + m_totalEvaluations < maximumEvaluations; + ++directionAttempt) { + double direction = directionAttempt == 0 + ? preferredSign : -preferredSign; + double availableRoom = direction > 0.0 + ? positiveRoom : negativeRoom; + double deltaMagnitude = qMin( + finiteDifferenceStep, availableRoom); + if(deltaMagnitude < minimumCoordinateStep) { + continue; + } + + TrustRegionEvaluation probe; + probe.coordinates = base.coordinates; + probe.coordinates[column] += direction * deltaMagnitude; + probe.parameters = parametersFromCoordinates(probe.coordinates); + probe.valid = evaluateTrustRegionPoint( + probe.parameters, + &probe.fitness, + &probe.breakdown, + &probe.curve, + &probe.elapsedMs); + + QString decision = probe.valid + ? "sensitivity_valid" + : (directionAttempt == 0 + ? "sensitivity_retry_opposite" + : "sensitivity_invalid"); + writeTraceRow(m_currentIteration, + column, + "trust_region_sensitivity", + probe.parameters, + probe.fitness, + probe.valid, + probe.elapsedMs, + decision, + probe.valid ? &probe.breakdown : nullptr); + + if(!probe.valid) { + restoreEvaluationState(base); + continue; + } + + double delta = probe.coordinates[column] - + base.coordinates[column]; + if(qAbs(delta) < minimumCoordinateStep || + probe.breakdown.residualVector.size() != residualCount) { + restoreEvaluationState(base); + continue; + } + + // 第 column 列是固定残差向量相对内部参数坐标的有限差分: + // J[:,column] = (r_probe-r_base)/delta。 + for(int row = 0; row < residualCount; ++row) { + jacobian[row][column] = + (probe.breakdown.residualVector[row] - + base.breakdown.residualVector[row]) / delta; + } + + // 有符号诊断量只有在基点和试算点都可靠时才能计算方向梯度; + // shapeLoss 无方向可靠性标志,始终记录其局部变化率。 + if(base.breakdown.verticalReliable && + probe.breakdown.verticalReliable && + !base.breakdown.registrationAmbiguous && + !probe.breakdown.registrationAmbiguous) { + verticalGradient[column] = + (probe.breakdown.verticalCommonBias - + base.breakdown.verticalCommonBias) / delta; + } + if(base.breakdown.horizontalReliable && + probe.breakdown.horizontalReliable && + !base.breakdown.registrationAmbiguous && + !probe.breakdown.registrationAmbiguous) { + horizontalGradient[column] = + (probe.breakdown.horizontalPhysicalShift - + base.breakdown.horizontalPhysicalShift) / delta; + } + shapeGradient[column] = + (probe.breakdown.shapeLoss - + base.breakdown.shapeLoss) / delta; + jacobianColumnValid[column] = true; + columnBuilt = true; + + if(probe.fitness < base.fitness && + (!bestProbe.valid || + probe.fitness < bestProbe.fitness)) { + bestProbe = probe; + bestProbeColumn = column; + bestProbeDelta = delta; + } + restoreEvaluationState(base); + } + } + + int validColumnCount = 0; + for(int i = 0; i < jacobianColumnValid.size(); ++i) { + if(jacobianColumnValid[i]) { + ++validColumnCount; + } + } + if(validColumnCount == 0 || m_shouldStop) { + restoreEvaluationState(base); + return false; + } + + // 灵敏度试算本身若找到更优真实解也应保留。所有列先基于同一个 base + // 建完,再用该已知割线把 Jacobian 平移到新工作点,避免边算边移动基点。 + if(bestProbe.valid && bestProbeColumn >= 0) { + QVector acceptedStep(dimensions, 0.0); + acceptedStep[bestProbeColumn] = bestProbeDelta; + updateTrustRegionJacobian( + &jacobian, + base.breakdown.residualVector, + bestProbe.breakdown.residualVector, + acceptedStep); + if(base.breakdown.verticalReliable && + bestProbe.breakdown.verticalReliable) { + updateTrustRegionScalarGradient( + &verticalGradient, + base.breakdown.verticalCommonBias, + bestProbe.breakdown.verticalCommonBias, + acceptedStep); + } + if(base.breakdown.horizontalReliable && + bestProbe.breakdown.horizontalReliable) { + updateTrustRegionScalarGradient( + &horizontalGradient, + base.breakdown.horizontalPhysicalShift, + bestProbe.breakdown.horizontalPhysicalShift, + acceptedStep); + } + updateTrustRegionScalarGradient( + &shapeGradient, + base.breakdown.shapeLoss, + bestProbe.breakdown.shapeLoss, + acceptedStep); + + current = bestProbe; + publishAcceptedPoint(current); + restoreEvaluationState(current); + writeTraceRow(m_currentIteration, + bestProbeColumn, + "trust_region_sensitivity_accept", + current.parameters, + current.fitness, + true, + 0, + "accepted_cached_probe", + ¤t.breakdown); + emit logMessageGenerated( + tr("Sensitivity probe accepted: error reduced to %1") + .arg(current.fitness, 0, 'e', 4)); + } else { + restoreEvaluationState(current); + } + + acceptedSinceRebuild = 0; + movementSinceRebuild = 0.0; + consecutiveRejectedSteps = 0; + rebuildRequested = false; + // 若重建过程中接受了试算点,当前模型已通过割线平移而不是在新点完整 + // 重算;再遇到最小半径停滞时仍允许做一次真正的新点重建。 + modelRebuiltAtMinimumRadius = + trustRadius <= minimumTrustRadius * 1.01 && + !bestProbe.valid; + emit logMessageGenerated( + tr("Sensitivity model rebuilt: %1/%2 parameter columns valid") + .arg(validColumnCount) + .arg(dimensions)); + return true; + }; + + int completedIterations = 0; + for(int iteration = 0; + iteration < m_maxIterations && + m_totalEvaluations < maximumEvaluations && + !m_shouldStop; + ++iteration) { + m_currentIteration = iteration; + completedIterations = iteration + 1; + + if(!processPauseAndStop()) { + break; + } + if(rebuildRequested) { + const bool confirmingStagnation = + stagnationConfirmationRequested; + if(!rebuildSensitivity()) { + stopReason = m_shouldStop + ? LM_USER_STOPPED + : LM_LOCAL_OPTIMUM; + break; + } + if(current.fitness < m_targetError) { + stopReason = LM_TARGET_ACHIEVED; + break; + } + if(m_totalEvaluations >= maximumEvaluations) { + stopReason = LM_MAX_ITERATIONS; + break; + } + const bool rebuildEffective = + registerEffectiveImprovement(current.fitness); + if(confirmingStagnation && !rebuildEffective) { + emit logMessageGenerated( + tr("Sensitivity rebuild produced no effective improvement; " + "local convergence detected")); + stopReason = LM_LOCAL_OPTIMUM; + break; + } + } + + // 先确定当前最突出的可靠诊断误差,用其梯度回答“哪些参数最能改善 + // 当前问题”;实际 LM 方向仍由完整残差梯度和 Jacobian 共同计算。 + int dominantComponent = trustRegionDominantComponent( + current.breakdown, diagnosisThreshold); + const QVector* componentGradient = nullptr; + if(dominantComponent == TRUST_REGION_VERTICAL_COMPONENT) { + componentGradient = &verticalGradient; + } else if(dominantComponent == TRUST_REGION_HORIZONTAL_COMPONENT) { + componentGradient = &horizontalGradient; + } else if(dominantComponent == TRUST_REGION_SHAPE_COMPONENT) { + componentGradient = &shapeGradient; + } + + // 主目标采用 0.5*||r||^2,其对参数的梯度为 J^T*r。这里不再叠加 + // vertical/horizontal/shape,保证诊断分量不会改变真实接受目标。 + QVector totalGradient(dimensions, 0.0); + for(int column = 0; column < dimensions; ++column) { + if(!jacobianColumnValid[column]) { + continue; + } + for(int row = 0; row < jacobian.size(); ++row) { + totalGradient[column] += + jacobian[row][column] * + current.breakdown.residualVector[row]; + } + } + + // 每轮最多联合调整三个灵敏参数。按当前诊断梯度绝对值由大到小选取, + // 并剔除 Jacobian 响应过度共线的列,降低弱可辨识参数互相补偿的风险。 + QVector selectedColumns; + QVector alreadyConsidered(dimensions, false); + for(int selection = 0; selection < qMin(3, dimensions); ++selection) { + int bestColumn = -1; + double bestScore = 0.0; + for(int column = 0; column < dimensions; ++column) { + if(alreadyConsidered[column] || + !jacobianColumnValid[column]) { + continue; + } + + double score = componentGradient + ? qAbs((*componentGradient)[column]) + : qAbs(totalGradient[column]); + if(!isFiniteNumber(score) || score <= bestScore) { + continue; + } + + bool excessivelyCorrelated = false; + for(int selectedIndex = 0; + selectedIndex < selectedColumns.size(); + ++selectedIndex) { + if(trustRegionJacobianColumnCorrelation( + jacobian, + column, + selectedColumns[selectedIndex]) > + columnCorrelationLimit) { + excessivelyCorrelated = true; + break; + } + } + if(!excessivelyCorrelated) { + bestColumn = column; + bestScore = score; + } + } + if(bestColumn < 0 || bestScore <= 1.0e-12) { + break; + } + selectedColumns.append(bestColumn); + alreadyConsidered[bestColumn] = true; + } + + // 诊断梯度接近零时,说明该分量在当前局部无法可靠选参,退回完整残差 + // 梯度,但接受标准仍然只有真实 total,诊断值不会重复计入目标函数。 + if(selectedColumns.isEmpty() && componentGradient) { + dominantComponent = TRUST_REGION_TOTAL_COMPONENT; + componentGradient = nullptr; + alreadyConsidered.fill(false, dimensions); + for(int selection = 0; + selection < qMin(3, dimensions); + ++selection) { + int bestColumn = -1; + double bestScore = 0.0; + for(int column = 0; column < dimensions; ++column) { + if(alreadyConsidered[column] || + !jacobianColumnValid[column]) { + continue; + } + double score = qAbs(totalGradient[column]); + if(score <= bestScore) { + continue; + } + bool excessivelyCorrelated = false; + for(int selectedIndex = 0; + selectedIndex < selectedColumns.size(); + ++selectedIndex) { + if(trustRegionJacobianColumnCorrelation( + jacobian, + column, + selectedColumns[selectedIndex]) > + columnCorrelationLimit) { + excessivelyCorrelated = true; + break; + } + } + if(!excessivelyCorrelated) { + bestColumn = column; + bestScore = score; + } + } + if(bestColumn < 0 || bestScore <= 1.0e-12) { + break; + } + selectedColumns.append(bestColumn); + alreadyConsidered[bestColumn] = true; + } + } + + // 当前局部没有可用方向时先缩小半径并重建灵敏度;只有已经在最小 + // 半径完整重建后仍无方向,才把它判定为局部最优。 + if(selectedColumns.isEmpty()) { + if(trustRadius <= minimumTrustRadius * 1.01 && + modelRebuiltAtMinimumRadius) { + stopReason = LM_LOCAL_OPTIMUM; + break; + } + trustRadius = qMax(minimumTrustRadius, trustRadius * 0.5); + damping = qMin(1.0e8, damping * 4.0); + rebuildRequested = true; + if(recordIneffectiveStep()) { + stopReason = LM_LOCAL_OPTIMUM; + break; + } + continue; + } + + // 在选中参数子空间构造 LM 正规方程: + // (J^T*J + damping*diag(J^T*J))*step = -J^T*r。 + // 对角缩放使不同参数列的灵敏度量级差异不会直接改变阻尼强弱。 + const int selectedCount = selectedColumns.size(); + QVector > normalMatrix( + selectedCount, QVector(selectedCount, 0.0)); + QVector rightHandSide(selectedCount, 0.0); + for(int left = 0; left < selectedCount; ++left) { + int leftColumn = selectedColumns[left]; + rightHandSide[left] = -totalGradient[leftColumn]; + for(int right = 0; right < selectedCount; ++right) { + int rightColumn = selectedColumns[right]; + for(int row = 0; row < jacobian.size(); ++row) { + normalMatrix[left][right] += + jacobian[row][leftColumn] * + jacobian[row][rightColumn]; + } + } + double diagonalScale = qMax( + 1.0e-10, normalMatrix[left][left]); + normalMatrix[left][left] += damping * diagonalScale; + } + + QVector selectedStep; + bool solved = solveTrustRegionLinearSystem( + normalMatrix, rightHandSide, &selectedStep); + QVector coordinateStep(dimensions, 0.0); + if(solved) { + for(int i = 0; i < selectedCount; ++i) { + coordinateStep[selectedColumns[i]] = selectedStep[i]; + } + } + + double stepNorm = qSqrt(trustRegionSquaredNorm(coordinateStep)); + if(!solved || !isFiniteNumber(stepNorm) || + stepNorm < minimumCoordinateStep) { + // 正规方程退化时使用投影最速下降方向,仍只移动本轮已选择的参数。 + coordinateStep.fill(0.0, dimensions); + double gradientNormSquared = 0.0; + for(int i = 0; i < selectedCount; ++i) { + int column = selectedColumns[i]; + double stepDirection = -totalGradient[column]; + if((current.coordinates[column] <= minimumCoordinateStep && + stepDirection < 0.0) || + (current.coordinates[column] >= + 1.0 - minimumCoordinateStep && + stepDirection > 0.0)) { + stepDirection = 0.0; + } + coordinateStep[column] = stepDirection; + gradientNormSquared += stepDirection * stepDirection; + } + double gradientNorm = qSqrt(gradientNormSquared); + if(gradientNorm > minimumCoordinateStep) { + double scale = trustRadius / gradientNorm; + for(int i = 0; i < selectedCount; ++i) { + int column = selectedColumns[i]; + coordinateStep[column] *= scale; + } + } + stepNorm = qSqrt(trustRegionSquaredNorm(coordinateStep)); + } + + // LM 解只给出局部模型建议方向;若超出当前信赖半径,保持方向不变并 + // 等比例截短,避免一次试算离开 Jacobian 有效的局部区域。 + if(stepNorm > trustRadius && stepNorm > 0.0) { + double scale = trustRadius / stepNorm; + for(int i = 0; i < coordinateStep.size(); ++i) { + coordinateStep[i] *= scale; + } + } + + // 将 LM 步长投影到用户给定的参数范围,实际用于预测下降的也是投影后步长。 + QVector candidateCoordinates = current.coordinates; + for(int i = 0; i < dimensions; ++i) { + candidateCoordinates[i] = qBound( + 0.0, + current.coordinates[i] + coordinateStep[i], + 1.0); + coordinateStep[i] = candidateCoordinates[i] - + current.coordinates[i]; + } + stepNorm = qSqrt(trustRegionSquaredNorm(coordinateStep)); + + // 用线性模型 r_new ~= r_current + J*step 预测残差,再用平方能量 + // 的下降量与真实候选下降量比较,作为调整阻尼和半径的依据。 + QVector predictedResidual = + current.breakdown.residualVector; + for(int row = 0; row < jacobian.size(); ++row) { + for(int column = 0; column < dimensions; ++column) { + predictedResidual[row] += + jacobian[row][column] * coordinateStep[column]; + } + } + double predictedReduction = 0.5 * + (trustRegionSquaredNorm(current.breakdown.residualVector) - + trustRegionSquaredNorm(predictedResidual)); + + // 无实际移动或模型预测不下降时没有必要调用昂贵求解器。将它按一次 + // 拒绝处理,并在连续发生后重建灵敏度,防止继续沿失效模型试算。 + if(stepNorm < minimumCoordinateStep || + !isFiniteNumber(predictedReduction) || + predictedReduction <= 1.0e-14) { + trustRadius = qMax(minimumTrustRadius, trustRadius * 0.5); + damping = qMin(1.0e8, damping * 4.0); + ++consecutiveRejectedSteps; + if(consecutiveRejectedSteps >= 2) { + if(trustRadius <= minimumTrustRadius * 1.01 && + modelRebuiltAtMinimumRadius) { + stopReason = LM_LOCAL_OPTIMUM; + break; + } + rebuildRequested = true; + } + if(recordIneffectiveStep()) { + stopReason = LM_LOCAL_OPTIMUM; + break; + } + continue; + } + + TrustRegionEvaluation candidate; + candidate.coordinates = candidateCoordinates; + candidate.parameters = parametersFromCoordinates(candidate.coordinates); + candidate.valid = evaluateTrustRegionPoint( + candidate.parameters, + &candidate.fitness, + &candidate.breakdown, + &candidate.curve, + &candidate.elapsedMs); + + if(!candidate.valid) { + // 求解失败的候选不能改变 current。先完整恢复上一个已接受参数和 + // 对应误差快照,再缩小信赖域;连续失败达到上限才终止整个拟合。 + ++consecutiveSolverFailures; + ++consecutiveRejectedSteps; + trustRadius = qMax(minimumTrustRadius, trustRadius * 0.5); + damping = qMin(1.0e8, damping * 4.0); + writeTraceRow(m_currentIteration, + -1, + "trust_region_candidate", + candidate.parameters, + candidate.fitness, + false, + candidate.elapsedMs, + "solver_invalid", + nullptr); + restoreEvaluationState(current); + if(consecutiveRejectedSteps >= 2) { + rebuildRequested = true; + } + if(consecutiveSolverFailures >= m_maxConsecutiveFailures) { + stopReason = LM_CONSECUTIVE_FAILURES; + break; + } + if(recordIneffectiveStep()) { + stopReason = LM_LOCAL_OPTIMUM; + break; + } + continue; + } + + consecutiveSolverFailures = 0; + // 有效候选即使最终被拒绝,也提供了一条真实割线,可用于修正下一轮 + // 局部模型;是否成为新工作点仍只由下面的 total 严格比较决定。 + const AutoFitObjectiveBreakdownLM oldBreakdown = current.breakdown; + updateTrustRegionJacobian( + &jacobian, + oldBreakdown.residualVector, + candidate.breakdown.residualVector, + coordinateStep); + if(oldBreakdown.verticalReliable && + candidate.breakdown.verticalReliable && + !oldBreakdown.registrationAmbiguous && + !candidate.breakdown.registrationAmbiguous) { + updateTrustRegionScalarGradient( + &verticalGradient, + oldBreakdown.verticalCommonBias, + candidate.breakdown.verticalCommonBias, + coordinateStep); + } + if(oldBreakdown.horizontalReliable && + candidate.breakdown.horizontalReliable && + !oldBreakdown.registrationAmbiguous && + !candidate.breakdown.registrationAmbiguous) { + updateTrustRegionScalarGradient( + &horizontalGradient, + oldBreakdown.horizontalPhysicalShift, + candidate.breakdown.horizontalPhysicalShift, + coordinateStep); + } + updateTrustRegionScalarGradient( + &shapeGradient, + oldBreakdown.shapeLoss, + candidate.breakdown.shapeLoss, + coordinateStep); + + // reductionRatio 衡量局部线性模型的可信度:接近 1 表示预测准确; + // 值较小表示虽然可能下降,但模型低估了非线性,需要收紧下一步。 + double actualReduction = 0.5 * + (current.fitness * current.fitness - + candidate.fitness * candidate.fitness); + double reductionRatio = actualReduction / predictedReduction; + bool accepted = candidate.fitness < current.fitness; + QString componentName = trustRegionComponentName(dominantComponent); + + if(accepted) { + // 真实总误差下降后才正式替换 current,并同步发布参数、曲线和诊断。 + // 模型预测可靠时减小阻尼并可扩大半径,预测较差时保守收缩。 + current = candidate; + publishAcceptedPoint(current); + restoreEvaluationState(current); + ++acceptedSinceRebuild; + movementSinceRebuild += stepNorm; + consecutiveRejectedSteps = 0; + + if(reductionRatio > 0.75) { + damping = qMax(1.0e-8, damping * 0.5); + if(stepNorm >= trustRadius * 0.8) { + trustRadius = qMin( + maximumTrustRadius, trustRadius * 1.6); + } + } else if(reductionRatio > 0.25) { + damping = qMax(1.0e-8, damping * 0.8); + } else { + damping = qMin(1.0e8, damping * 2.0); + trustRadius = qMax( + minimumTrustRadius, trustRadius * 0.75); + } + + if(acceptedSinceRebuild >= 6 || + movementSinceRebuild >= 0.30) { + rebuildRequested = true; + } + modelRebuiltAtMinimumRadius = false; + } else { + // 拒绝时 candidate 只保留在 trace 中,DataManager 和内存状态都恢复 + // 到 current。连续拒绝说明割线模型可能失真,因此请求重新试算灵敏度。 + ++consecutiveRejectedSteps; + damping = qMin(1.0e8, damping * 4.0); + trustRadius = qMax(minimumTrustRadius, trustRadius * 0.5); + restoreEvaluationState(current); + if(consecutiveRejectedSteps >= 2) { + rebuildRequested = true; + } + } + + // 候选只要更优就继续作为 current 保存;是否足以解除停滞,则统一 + // 相对上一次有效改善基准判断。拒绝和微小改善都会累计无效次数。 + const bool effectiveImprovement = + registerEffectiveImprovement(current.fitness); + if(!effectiveImprovement && recordIneffectiveStep()) { + stopReason = LM_LOCAL_OPTIMUM; + } + + writeTraceRow(m_currentIteration, + -1, + "trust_region_candidate", + candidate.parameters, + candidate.fitness, + true, + candidate.elapsedMs, + accepted + ? QString("accepted_%1").arg(componentName) + : QString("rejected_%1").arg(componentName), + &candidate.breakdown); + + emit logMessageGenerated( + tr("Iteration %1: focus=%2, parameters=%3, error=%4, result=%5") + .arg(iteration + 1) + .arg(componentName) + .arg(selectedColumns.size()) + .arg(candidate.fitness, 0, 'e', 4) + .arg(accepted ? tr("accepted") : tr("rejected"))); + emit progressUpdated(iteration + 1, m_globalBestFitness); + + if(stopReason == LM_LOCAL_OPTIMUM) { + break; + } + + if(current.fitness < m_targetError) { + stopReason = LM_TARGET_ACHIEVED; + break; + } + if(trustRadius <= minimumTrustRadius * 1.01 && + consecutiveRejectedSteps >= 2) { + if(modelRebuiltAtMinimumRadius) { + stopReason = LM_LOCAL_OPTIMUM; + break; + } + rebuildRequested = true; + } + } + + if(completedIterations > 0) { + m_currentIteration = completedIterations - 1; + } + restoreEvaluationState(current); + + if(m_shouldStop) { + return LM_USER_STOPPED; + } + if(current.fitness < m_targetError) { + return LM_TARGET_ACHIEVED; + } + if(stopReason == LM_CONSECUTIVE_FAILURES || + stopReason == LM_LOCAL_OPTIMUM || + stopReason == LM_OPTIMIZATION_FAILED) { + return stopReason; + } + return LM_MAX_ITERATIONS; +} + +double nmCalculationAutoFitLM::evaluateFitness(const QVector& parameters) +{ + // LM 候选评价函数,也是自动拟合最核心的闭环: + // 1. 校验候选参数是否在用户设置的上下界和基本物理范围内; + // 2. 将参数写入 DataManager 的储层/目标井对象; + // 3. 调用真实数值求解器,生成模拟结果; + // 4. 从本次求解任务读取目标井 result log-log 曲线; + // 5. 与目标 history log-log 曲线计算误差,误差越小代表拟合越好。 + // + // 返回 1e10 表示候选评价失败或结果不可用。 + const QString funcName = QString("evaluateError[%1]").arg(m_currentIteration); + static int callCount = 0; + callCount++; + m_lastEvaluatedLogLogData.clear(); + m_lastObjectiveBreakdown = AutoFitObjectiveBreakdownLM(); + + try { + DEBUG_OUT(QString("%1: Call #%2 - Starting evaluation with %3 parameters") + .arg(funcName).arg(callCount).arg(parameters.size())); + + // 打印参数值 + QString paramStr = "Parameters: "; + + for(int i = 0; i < parameters.size(); ++i) { + paramStr += QString("[%1]=%2 ").arg(i).arg(parameters[i], 0, 'f', 6); + } + + DEBUG_OUT(QString("%1: %2").arg(funcName).arg(paramStr)); + + // 1. 参数有效性检查。这里先拦截明显非法的候选, + // 避免把非有限数、越界值或极端危险值传给求解器。 + if(!validateParameters(parameters)) { + DEBUG_OUT(QString("%1: Call #%2 - VALIDATION FAILED").arg(funcName).arg(callCount)); + + // 详细检查每个参数 + for(int i = 0; i < parameters.size(); ++i) { + if(!isFiniteNumber(parameters[i])) { + DEBUG_OUT(QString(" -> Param[%1] is NOT finite: %2").arg(i).arg(parameters[i])); + } + + if(i < m_enabledParamIndices.size()) { + int paramIndex = m_enabledParamIndices[i]; + + if(paramIndex >= 0 && paramIndex < m_parameterLower.size()) { + double lower = m_parameterLower[paramIndex]; + double upper = m_parameterUpper[paramIndex]; + + if(parameters[i] < lower) { + DEBUG_OUT(QString(" -> Param[%1]=%2 < lower bound %3") + .arg(i).arg(parameters[i]).arg(lower)); + } + + if(parameters[i] > upper) { + DEBUG_OUT(QString(" -> Param[%1]=%2 > upper bound %3") + .arg(i).arg(parameters[i]).arg(upper)); + } + + // 检查危险值。这些条件不是严格物理模型定义, + // 而是工程保护:避免求解器在明显异常输入下崩溃或返回无意义曲线。 + switch(paramIndex) { + case 0: // 渗透率 + if(parameters[i] <= 1e-6) { + DEBUG_OUT(QString(" -> REJECTED: Permeability too small: %1").arg(parameters[i])); + } + + break; + + case 2: // 井筒储集系数 + if(parameters[i] <= 1e-8) { + DEBUG_OUT(QString(" -> REJECTED: Wellbore storage too small: %1").arg(parameters[i])); + } + + break; + + case 3: // 孔隙度 + if(parameters[i] <= 1e-4 || parameters[i] >= 0.95) { + DEBUG_OUT(QString(" -> REJECTED: Unrealistic porosity: %1").arg(parameters[i])); + } + + break; + } + } + } + } + + return 1e10; + } + + DEBUG_OUT(QString("%1: Call #%2 - Parameters validated OK").arg(funcName).arg(callCount)); + + // 2. 数据管理器检查。后续参数写回和求解器组装都依赖当前 DataManager。 + nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); + + if(!dataManager) { + DEBUG_OUT(QString("%1: Call #%2 - DataManager is NULL").arg(funcName).arg(callCount)); + return 1e10; + } + + DEBUG_OUT(QString("%1: Call #%2 - DataManager OK").arg(funcName).arg(callCount)); + + // 3. 应用参数。parameters 的顺序与 m_enabledParamIndices 对齐, + // applyParametersToDataManager() 会把它们拆分写入储层参数和目标井参数。 + try { + DEBUG_OUT(QString("%1: Call #%2 - Applying parameters...").arg(funcName).arg(callCount)); + applyParametersToDataManager(parameters); + DEBUG_OUT(QString("%1: Call #%2 - Parameters applied successfully").arg(funcName).arg(callCount)); + } catch(const std::exception& e) { + DEBUG_OUT(QString("%1: Call #%2 - FAILED to apply parameters: %3") + .arg(funcName).arg(callCount).arg(e.what())); + return 1e10; + } + + // Dfc 和裂缝半长都通过 PEBI 裂缝数组传入,不属于每次求解都会重新组装的 Base/CS 参数。 + // 勾选任一裂缝参数时刷新网格输出,保证本次真实试算使用新的导流能力和端点坐标。 + const bool fractureGridParameterSelected = + (m_parameterSelected.size() > 8 && m_parameterSelected[8]) || + (m_parameterSelected.size() > 9 && m_parameterSelected[9]); + if(fractureGridParameterSelected) { + nmCalculationPebiGrid* pebiGrid = nmCalculationPebiGrid::getInstance(); + if(!pebiGrid || !pebiGrid->generateOutputPara()) { + DEBUG_OUT(QString("%1: Call #%2 - Failed to refresh PEBI fracture parameters") + .arg(funcName).arg(callCount)); + return 1e10; + } + } + + // 4. 运行求解器。真实求解器偶发失败时允许重试,避免一次 DLL 调用异常 + // 直接让整个粒子评价失败。 + QVector> solverResult; + const int maxRetries = 2; + bool solverSuccess = false; + + for(int retry = 0; retry <= maxRetries; ++retry) { + if(m_shouldStop) { + DEBUG_OUT(QString("%1: Call #%2 - User stop requested").arg(funcName).arg(callCount)); + return 1e10; + } + + try { + DEBUG_OUT(QString("%1: Call #%2 - Solver attempt %3/%4") + .arg(funcName).arg(callCount).arg(retry + 1).arg(maxRetries + 1)); + + if(retry > 0) { + DEBUG_OUT(QString("%1: Call #%2 - Retry delay...").arg(funcName).arg(callCount)); + msleep(1000); + } + + solverResult = runSolver(); + + // 详细检查求解器结果 + if(solverResult.isEmpty()) { + DEBUG_OUT(QString("%1: Call #%2 - Solver returned EMPTY result").arg(funcName).arg(callCount)); + } else { + DEBUG_OUT(QString("%1: Call #%2 - Solver returned %3 arrays") + .arg(funcName).arg(callCount).arg(solverResult.size())); + + for(int i = 0; i < solverResult.size(); ++i) { + DEBUG_OUT(QString(" -> Array[%1] size: %2").arg(i).arg(solverResult[i].size())); + } + + if(validateSolverResult(solverResult)) { + DEBUG_OUT(QString("%1: Call #%2 - Solver result VALIDATED on attempt %3") + .arg(funcName).arg(callCount).arg(retry + 1)); + solverSuccess = true; + break; + } else { + DEBUG_OUT(QString("%1: Call #%2 - Solver result VALIDATION FAILED on attempt %3") + .arg(funcName).arg(callCount).arg(retry + 1)); + } + } + + } catch(const std::exception& e) { + DEBUG_OUT(QString("%1: Call #%2 - Solver EXCEPTION on attempt %3: %4") + .arg(funcName).arg(callCount).arg(retry + 1).arg(e.what())); + } + } + + if(!solverSuccess) { + DEBUG_OUT(QString("%1: Call #%2 - ALL SOLVER ATTEMPTS FAILED").arg(funcName).arg(callCount)); + return 1e10; + } + + // 5. 获取 LogLog 数据。runSolverDll() 直接从求解任务复制目标井曲线, + // 不再依赖 DataManager 中可能被其它井或上一粒子改写的共享结果。 + QVector> resultLogLogData = m_lastEvaluatedLogLogData; + + try { + if(!validateLogLogData(resultLogLogData)) { + DEBUG_OUT(QString("%1: Call #%2 - LogLog data VALIDATION FAILED") + .arg(funcName).arg(callCount)); + + // 详细输出LogLog数据问题 + if(resultLogLogData.size() < 3) { + DEBUG_OUT(QString(" -> LogLog arrays count: %1 (need 3)") + .arg(resultLogLogData.size())); + } else { + DEBUG_OUT(QString(" -> LogLog array sizes: X=%1, Y1=%2, Y2=%3") + .arg(resultLogLogData[0].size()) + .arg(resultLogLogData[1].size()) + .arg(resultLogLogData[2].size())); + + // 检查数据有效性 + for(int i = 0; i < qMin(5, resultLogLogData[0].size()); ++i) { + if(!isFiniteNumber(resultLogLogData[0][i]) || + !isFiniteNumber(resultLogLogData[1][i]) || + !isFiniteNumber(resultLogLogData[2][i])) { + DEBUG_OUT(QString(" -> Invalid data at index %1: X=%2, Y1=%3, Y2=%4") + .arg(i) + .arg(resultLogLogData[0][i]) + .arg(resultLogLogData[1][i]) + .arg(resultLogLogData[2][i])); + } + } + } + + return 1e10; + } + + DEBUG_OUT(QString("%1: Call #%2 - LogLog data validated, size: %3") + .arg(funcName).arg(callCount).arg(resultLogLogData[0].size())); + + } catch(const std::exception& e) { + DEBUG_OUT(QString("%1: Call #%2 - Error getting LogLog data: %3") + .arg(funcName).arg(callCount).arg(e.what())); + return 1e10; + } + + // 6. 计算误差。这里比较的是目标井 history log-log 与当前模拟 result log-log。 + double error; + + try { + DEBUG_OUT(QString("%1: Call #%2 - Calculating error...") + .arg(funcName).arg(callCount)); + + error = calculateLogLogCurveError(m_targetLogLogData, resultLogLogData); + + if(!isFiniteNumber(error) || error < 0) { + DEBUG_OUT(QString("%1: Call #%2 - INVALID error value: %3") + .arg(funcName).arg(callCount).arg(error)); + return 1e10; + } + + DEBUG_OUT(QString("%1: Call #%2 - SUCCESS! Error = %3") + .arg(funcName).arg(callCount).arg(error, 0, 'e', 6)); + // 保存最后一次有效曲线,供 LM 候选评价和精英保护复用。 + m_lastEvaluatedLogLogData = resultLogLogData; + + } catch(const std::exception& e) { + DEBUG_OUT(QString("%1: Call #%2 - Error calculation FAILED: %3") + .arg(funcName).arg(callCount).arg(e.what())); + return 1e10; + } + + return error; + + } catch(const std::exception& e) { + DEBUG_OUT(QString("%1: Call #%2 - TOP-LEVEL EXCEPTION: %3") + .arg(funcName).arg(callCount).arg(e.what())); + return 1e10; + } catch(...) { + DEBUG_OUT(QString("%1: Call #%2 - UNKNOWN TOP-LEVEL EXCEPTION") + .arg(funcName).arg(callCount)); + return 1e10; + } +} + +// ==================== 参数应用方法 ==================== + +void nmCalculationAutoFitLM::applyParametersToDataManager(const QVector& parameters) +{ + // 将粒子的“启用参数向量”写回项目数据。 + // parameters 的维度必须等于用户勾选的参数数量,顺序由 m_enabledParamIndices 决定。 + // 这里不直接跑求解器,只负责把 DataManager 调整到该粒子对应的模型状态。 + if(parameters.size() != getEnabledParameterCount()) { + return; + } + + updateReservoirParameters(parameters); + updateWellParameters(parameters); +} + +void nmCalculationAutoFitLM::updateReservoirParameters(const QVector& parameters) +{ + // 更新储层级参数。井级参数 skin/wellboreC 不在这里改,由 updateWellParameters() 负责。 + // 这里先取 DataManager 中 reservoir 的副本,修改后再整体写回 DataManager。 + nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); + nmDataReservoir reservoirData = dataManager->getReservoirDataCopy(); + + // paramIndex 是粒子 position 中的索引;i 是完整 10 个参数体系中的索引。 + // 只有 m_parameterSelected[i] 为 true 时,才从 parameters 中消费一个值。 + int paramIndex = 0; + + for(int i = 0; i < m_parameterSelected.size(); ++i) { + if(m_parameterSelected[i] && paramIndex < parameters.size()) { + double value = parameters[paramIndex]; + + switch(i) { + case 0: // 渗透率 + reservoirData.getPermeability().setValue(value); + break; + + case 3: // 孔隙度 + reservoirData.getPorosity().setValue(value); + break; + + case 4: // 储层厚度 + reservoirData.getThickness().setValue(value); + break; + + case 5: // 综合压缩系数 + reservoirData.getCt().setValue(value); + break; + + case 6: // 岩石压缩系数 + reservoirData.getCf().setValue(value); + break; + + case 7: // 初始含水饱和度 + reservoirData.getSwi().setValue(value); + break; + } + + paramIndex++; + } + } + + // 更新数据管理器 + dataManager->updateReservoirData(reservoirData); +} + +void nmCalculationAutoFitLM::updateWellParameters(const QVector& parameters) +{ + // 更新目标井上的拟合参数。目前井级可拟合参数主要是: + // - skin:写入第一个 perforation; + // - wellboreC:写入井筒储集系数; + // - Dfc/裂缝半长:只写入垂直压裂井或多段压裂水平井。 + // 如果目标井不存在或没有射孔数据,这里只记录 debug,不抛异常。 + nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); + + // 只更新当前目标井的井参数,避免多井项目中误改其他井。 + //QVector wells = dataManager->getWellDataList(); + nmDataWellBase* pWell = dataManager->findWellByName(m_targetWellName); + + if(!pWell) return; + + //nmDataWellBase* pWell = wells[0]; // 使用第一口井 + + int paramIndex = 0; + + for(int i = 0; i < m_parameterSelected.size(); ++i) { + if(m_parameterSelected[i] && paramIndex < parameters.size()) { + double value = parameters[paramIndex]; + + switch(i) { + case 1: { // 表皮系数 + nmDataPerforation* perf = pWell->getPerforation(0); + + if(perf) { + nmDataAttribute skinAttr = perf->getSkin(); + skinAttr.setValue(value); + perf->setSkin(skinAttr); + } + } + break; + + case 2: { // 井筒储集系数 + nmDataAttribute wellboreAttr = pWell->getWellboreStorage(); + wellboreAttr.setValue(value); + pWell->setWellboreStorage(wellboreAttr); + } + break; + + case 8: { // 裂缝导流能力 + if(pWell->getWellType() == NM_WELL_MODEL::Vertical_Fractured_Well) { + nmDataVerticalFracturedWell* fracturedWell = + dynamic_cast(pWell); + if(fracturedWell) { + nmDataAttribute dfc = fracturedWell->getDfc(); + dfc.setValue(value); + fracturedWell->setDfc(dfc); + } + } else if(pWell->getWellType() == NM_WELL_MODEL::Horizontal_Fractured_Well) { + nmDataHorizontalFracturedWell* fracturedWell = + dynamic_cast(pWell); + if(fracturedWell) { + nmDataAttribute dfc = fracturedWell->getDfc(); + dfc.setValue(value); + fracturedWell->setDfc(dfc); + } + } + } + break; + + case 9: { // 裂缝半长 + // 直接修改井对象中的属性,复用已有信号重算裂缝端点。 + if(pWell->getWellType() == NM_WELL_MODEL::Vertical_Fractured_Well) { + nmDataVerticalFracturedWell* fracturedWell = + dynamic_cast(pWell); + if(fracturedWell) { + fracturedWell->getFractureHalfLength().setValue(value); + } + } else if(pWell->getWellType() == NM_WELL_MODEL::Horizontal_Fractured_Well) { + nmDataHorizontalFracturedWell* fracturedWell = + dynamic_cast(pWell); + if(fracturedWell) { + fracturedWell->getFractureHalfLength().setValue(value); + } + } + } + break; + } + + paramIndex++; + } + } + + // 根据井类型更新到数据管理器 + updateWellToDataManager(pWell); +} + +void nmCalculationAutoFitLM::updateWellToDataManager(nmDataWellBase* pWell) +{ + if(!pWell) return; + + nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); + NM_WELL_MODEL wellType = pWell->getWellType(); + + // DataManager 内部按井型维护不同容器。修改基类指针后,需要根据实际井型 + // 调用对应 update 接口,才能让后续求解器组装读到最新 skin / wellboreC。 + switch(wellType) { + case NM_WELL_MODEL::Vertical_Well: { + nmDataVerticalWell* pVerticalWell = dynamic_cast(pWell); + + if(pVerticalWell) { + QVector wells; + wells.append(*pVerticalWell); + dataManager->updateVerticalWells(wells); + } + + break; + } + + case NM_WELL_MODEL::Vertical_Fractured_Well: { + nmDataVerticalFracturedWell* pVFracturedWell = dynamic_cast(pWell); + + if(pVFracturedWell) { + QVector wells; + wells.append(*pVFracturedWell); + dataManager->updateVerticalFracturedWells(wells); + } + + break; + } + + case NM_WELL_MODEL::Horizontal_Fractured_Well: { + nmDataHorizontalFracturedWell* pHFracturedWell = dynamic_cast(pWell); + + if(pHFracturedWell) { + QVector wells; + wells.append(*pHFracturedWell); + dataManager->updateHorizontalFracturedWells(wells); + } + + break; + } + + default: + break; + } +} + +// ==================== 求解器相关方法 ==================== + +QVector> nmCalculationAutoFitLM::runSolver() +{ + // 真实求解器统一走 DLL 方式,返回值由 evaluateFitness() 继续校验。 + return runSolverDll(); +} + +// ==================== 数据处理方法 ==================== +// ==================== 算法辅助方法 ==================== + +void nmCalculationAutoFitLM::saveOptimizationResult() +{ + // 当前函数只做日志记录。真正把最优参数写回项目数据的是 + // startAutoFitting() 结束阶段的 applyParametersToDataManager(m_globalBestPosition)。 + DEBUG_OUT(QString("Optimization result: error=%1, evaluations=%2/%3") + .arg(m_globalBestFitness, 0, 'e', 4) + .arg(m_successfulEvaluations) + .arg(m_totalEvaluations)); +} + +void nmCalculationAutoFitLM::validateAndProtectFinalResult() +{ + // 最终精英保护只阻止无效结果或真正变差的结果。任何真实误差下降都应保留, + // 不能再用固定百分比门槛把已经找到的更优解恢复成初始值。 + if(!m_hasValidUserSolution) { + emit logMessageGenerated(tr("No initial solution for elite protection")); + return; + } + + emit logMessageGenerated(tr("=== Final Result Validation (Elite Protection) ===")); + + // 使用已有的评估结果 + double finalFitness = m_globalBestFitness; + double initialFitness = m_userInitialFitness; + + emit logMessageGenerated(tr("Comparing results: Initial=%1, Final=%2") + .arg(initialFitness, 0, 'e', 4).arg(finalFitness, 0, 'e', 4)); + + bool finalValid = isFiniteNumber(finalFitness) && + finalFitness < 1.0e9 && + m_globalBestPosition.size() == + m_userInitialSolution.size() && + !m_globalBestLogLogData.isEmpty() && + m_globalBestObjectiveBreakdown.valid; + if(finalValid) { + double improvement = initialFitness - finalFitness; + double relativeImprovement = + improvement / qMax(1.0e-10, qAbs(initialFitness)); + emit logMessageGenerated(tr("Improvement: %1 (%2%)") + .arg(improvement, 0, 'e', 4) + .arg(relativeImprovement * 100, 0, 'f', 2)); + } + + if(!finalValid || finalFitness > initialFitness) { + emit logMessageGenerated( + tr("Elite protection triggered: final result is invalid or worse than initial")); + emit logMessageGenerated(tr("Restoring initial solution as final result")); + + m_globalBestFitness = initialFitness; + m_globalBestPosition = m_userInitialSolution; + m_globalBestLogLogData = m_userInitialLogLogData; + m_globalBestObjectiveBreakdown = m_userInitialObjectiveBreakdown; + emit bestCurveUpdated(m_targetLogLogData, m_globalBestLogLogData, m_currentIteration + 1, m_globalBestFitness); + + emit logMessageGenerated(tr("Initial solution restored successfully")); + } else { + emit logMessageGenerated( + tr("Final result validated - solution is not worse than initial")); + } +} + +// ==================== 工具方法 ==================== + +int nmCalculationAutoFitLM::getEnabledParameterCount() const +{ + // 返回粒子维度,即用户勾选参与拟合的参数数量。 + int count = 0; + + for(int i = 0; i < m_parameterSelected.size(); ++i) { + if(m_parameterSelected[i]) count++; + } + + return count; +} + +// ==================== 验证和处理方法 ==================== + +bool nmCalculationAutoFitLM::validateParameters(const QVector& parameters) const +{ + // 参数物理范围已由拟合窗口统一校验;候选评价只检查维度、有限数和 + // 用户设置的上下界,避免另一套硬编码阈值与实际搜索范围冲突。 + if(parameters.size() != getEnabledParameterCount()) { + return false; + } + + for(int i = 0; i < parameters.size(); ++i) { + if(!isFiniteNumber(parameters[i])) { + return false; + } + + // 检查参数范围 + if(i < m_enabledParamIndices.size()) { + int paramIndex = m_enabledParamIndices[i]; + + if(paramIndex >= 0 && paramIndex < m_parameterLower.size()) { + if(parameters[i] < m_parameterLower[paramIndex] || + parameters[i] > m_parameterUpper[paramIndex]) { + return false; + } + } + } + } + + return true; +} + +bool nmCalculationAutoFitLM::validateLogLogData(const QVector>& logLogData) const +{ + // 校验双对数曲线结构。约定: + // logLogData[0]=time,logLogData[1]=pressure,logLogData[2]=pressure derivative。 + // 三列必须长度一致,且至少有足够点数用于插值和误差计算。 + if(logLogData.size() < 3) { + DEBUG_OUT("LogLog data has less than 3 arrays"); + return false; + } + + // 检查数组大小一致性 + int size = logLogData[0].size(); + + if(size == 0) { + DEBUG_OUT("Empty LogLog data"); + return false; + } + + if(logLogData[1].size() != size || logLogData[2].size() != size) { + DEBUG_OUT(QString("LogLog data size mismatch: X=%1, Y1=%2, Y2=%3") + .arg(logLogData[0].size()) + .arg(logLogData[1].size()) + .arg(logLogData[2].size())); + return false; + } + + // 检查最小数据点数 + if(size < 5) { + DEBUG_OUT(QString("Too few LogLog data points: %1").arg(size)); + return false; + } + + // 数据有效性检查 + for(int i = 0; i < size; ++i) { + if(!isFiniteNumber(logLogData[0][i]) || + !isFiniteNumber(logLogData[1][i]) || + !isFiniteNumber(logLogData[2][i])) { + DEBUG_OUT(QString("Invalid LogLog data at index %1").arg(i)); + return false; + } + } + + return true; +} + +bool nmCalculationAutoFitLM::validateInitialValues() const +{ + // 检查当前模型读取出的初始参数是否和用户勾选维度一致,并且在上下界内。 + // 如果初始值越界,算法仍可继续,但日志会提示,因为精英保护可能不可用或效果变差。 + if(m_initialValues.size() != m_enabledParamIndices.size()) { + DEBUG_OUT("Initial values count mismatch with enabled parameters"); + return false; + } + + bool allValid = true; + + for(int i = 0; i < m_initialValues.size(); ++i) { + int paramIndex = m_enabledParamIndices[i]; + double value = m_initialValues[i]; + + if(!isFiniteNumber(value)) { + DEBUG_OUT(QString("Initial value[%1] is not finite: %2").arg(i).arg(value)); + allValid = false; + continue; + } + + if(paramIndex < m_parameterLower.size() && paramIndex < m_parameterUpper.size()) { + double minVal = m_parameterLower[paramIndex]; + double maxVal = m_parameterUpper[paramIndex]; + + if(value < minVal || value > maxVal) { + DEBUG_OUT(QString("Initial value[%1] = %2 is outside bounds [%3, %4]") + .arg(i).arg(value).arg(minVal).arg(maxVal)); + allValid = false; + } + } + } + + return allValid; +} + +bool nmCalculationAutoFitLM::validateSolverResult(const QVector>& result) const +{ + // 校验求解器压力结果。这里检查的是 pressure result,至少需要 time 和 pressure 两列。 + // result log-log 的结构会在 validateLogLogData() 中另行检查。 + if(result.size() < 2) { + DEBUG_OUT("Solver result has less than 2 arrays"); + return false; + } + + if(result[0].size() != result[1].size()) { + DEBUG_OUT(QString("Size mismatch: X=%1, Y=%2").arg(result[0].size()).arg(result[1].size())); + return false; + } + + if(result[0].size() == 0) { + DEBUG_OUT("Empty solver result"); + return false; + } + + // 检查最小数据点数 + if(result[0].size() < 10) { + DEBUG_OUT(QString("Too few data points: %1").arg(result[0].size())); + return false; + } + + // 数据有效性检查 + for(int i = 0; i < result[0].size(); ++i) { + if(!isFiniteNumber(result[0][i]) || !isFiniteNumber(result[1][i])) { + DEBUG_OUT(QString("Invalid data at index %1: X=%2, Y=%3") + .arg(i).arg(result[0][i]).arg(result[1][i])); + return false; + } + } + + // 检查X值单调性 + bool isMonotonic = true; + + for(int i = 1; i < result[0].size(); ++i) { + if(result[0][i] <= result[0][i - 1]) { + isMonotonic = false; + break; + } + } + + if(!isMonotonic) { + DEBUG_OUT("X values are not monotonically increasing"); + } + + return true; +} + +double nmCalculationAutoFitLM::calculateLogLogCurveError( + const QVector >& target, + const QVector >& result) const +{ + // 主目标只比较固定网格上的压力和导数残差;上下、左右和形状只负责诊断 + // 误差来源和选择参数,避免同一残差在 total 中被重复计算。整个计算过程均 + // 位于 log(time)-log(value) 坐标,因此得到的是相对尺度偏差而非原始压力量纲。 + const double invalidLoss = 1.0e10; + const double valueFloor = 1.0e-12; + const double minimumCoverage = 0.95; + const int numPoints = 80; + m_lastObjectiveBreakdown = AutoFitObjectiveBreakdownLM(); + + if(!validateLogLogData(target) || !validateLogLogData(result)) { + return invalidLoss; + } + + try { + // 无法比较的采样行先跳过;有限但非正的导数无法进入双对数空间, + // 当前数据又没有逐点有效掩码,因此遇到这种导数时判本次评价无效。 + auto prepareCurve = [valueFloor](const QVector >& data, + int firstIndex, + QVector* pressure, + QVector* derivative) -> bool { + if(!pressure || !derivative || data.size() < 3 || + data[0].size() != data[1].size() || + data[0].size() != data[2].size() || + firstIndex < 0 || firstIndex >= data[0].size()) { + return false; + } + + for(int i = firstIndex; i < data[0].size(); ++i) { + if(!isFiniteNumber(data[0][i]) || + !isFiniteNumber(data[1][i]) || + !isFiniteNumber(data[2][i]) || + data[0][i] <= 0.0 || + data[1][i] <= 0.0) { + continue; + } + if(data[2][i] <= 0.0) { + return false; + } + + pressure->append(QPointF(data[0][i], data[1][i])); + derivative->append( + QPointF(data[0][i], qMax(data[2][i], valueFloor))); + } + + // 求解器输出可能不是严格升序,且同一时刻可能出现重复记录。 + // 插值前统一排序并让后出现的记录覆盖同时间旧值,保证横坐标严格递增。 + auto sortAndUnique = [](QVector* curve) { + std::stable_sort( + curve->begin(), curve->end(), + [](const QPointF& left, const QPointF& right) { + return left.x() < right.x(); + }); + + QVector unique; + unique.reserve(curve->size()); + for(int i = 0; i < curve->size(); ++i) { + if(unique.isEmpty() || + curve->at(i).x() > unique.last().x()) { + unique.append(curve->at(i)); + } else { + unique[unique.size() - 1] = curve->at(i); + } + } + *curve = unique; + }; + + sortAndUnique(pressure); + sortAndUnique(derivative); + return pressure->size() >= 3 && derivative->size() >= 3; + }; + + QVector targetPressure; + QVector targetDerivative; + QVector resultPressure; + QVector resultDerivative; + // 模拟结果已跳过 DLL 首点,因此误差计算同步忽略目标曲线首点。 + if(!prepareCurve(target, 1, &targetPressure, &targetDerivative) || + !prepareCurve(result, 0, &resultPressure, &resultDerivative)) { + return invalidLoss; + } + + // 在双对数坐标中插值。二分定位用于后面的多次水平配准试算。 + auto interpolateLogValue = [valueFloor]( + const QVector& curve, + double x, + double* value) -> bool { + if(!value || curve.size() < 2 || x <= 0.0 || + x < curve.first().x() || x > curve.last().x()) { + return false; + } + + int low = 0; + int high = curve.size() - 1; + while(low < high) { + int middle = low + (high - low) / 2; + if(curve[middle].x() < x) { + low = middle + 1; + } else { + high = middle; + } + } + + int right = qBound(1, low, curve.size() - 1); + int left = right - 1; + double leftLogX = qLn(curve[left].x()); + double rightLogX = qLn(curve[right].x()); + double denominator = rightLogX - leftLogX; + double leftLogY = + qLn(qMax(qAbs(curve[left].y()), valueFloor)); + double rightLogY = + qLn(qMax(qAbs(curve[right].y()), valueFloor)); + + if(qAbs(denominator) <= 1.0e-12) { + *value = leftLogY; + } else { + double ratio = (qLn(x) - leftLogX) / denominator; + *value = leftLogY + + ratio * (rightLogY - leftLogY); + } + return isFiniteNumber(*value); + }; + + // 覆盖率通过后若只缺少首尾少量点,用模拟曲线自身的端点斜率作短距离 + // 双对数外推。该外推只用于主损失的固定网格,不参与水平配准搜索。 + auto extrapolateEndpointLogValue = [valueFloor]( + const QVector& curve, + double x, + double* value) -> bool { + if(!value || curve.size() < 2 || x <= 0.0) { + return false; + } + int left = x < curve.first().x() + ? 0 + : curve.size() - 2; + int right = left + 1; + double leftLogX = qLn(curve[left].x()); + double rightLogX = qLn(curve[right].x()); + double denominator = rightLogX - leftLogX; + if(qAbs(denominator) <= 1.0e-12) { + return false; + } + double leftLogY = + qLn(qMax(qAbs(curve[left].y()), valueFloor)); + double rightLogY = + qLn(qMax(qAbs(curve[right].y()), valueFloor)); + double ratio = (qLn(x) - leftLogX) / denominator; + *value = leftLogY + ratio * (rightLogY - leftLogY); + return isFiniteNumber(*value); + }; + + const double targetMinX = targetPressure.first().x(); + const double targetMaxX = targetPressure.last().x(); + const double resultMinX = resultPressure.first().x(); + const double resultMaxX = resultPressure.last().x(); + if(targetMinX <= 0.0 || targetMaxX <= targetMinX || + resultMinX <= 0.0 || resultMaxX <= resultMinX) { + return invalidLoss; + } + + QVector commonX(numPoints); + QVector commonLogX(numPoints); + QVector targetLogPressure(numPoints); + QVector targetLogDerivative(numPoints); + const double targetLogMinX = qLn(targetMinX); + const double targetLogMaxX = qLn(targetMaxX); + + // 固定使用目标曲线的完整 log-time 网格,候选之间不会因采样点不同而失去可比性。 + for(int i = 0; i < numPoints; ++i) { + double logX = targetLogMinX + + static_cast(i) * + (targetLogMaxX - targetLogMinX) / + (numPoints - 1); + commonLogX[i] = logX; + // 首尾直接使用原始端点,避免 exp(log(t)) 的舍入误差越过严格插值边界。 + if(i == 0) { + commonX[i] = targetMinX; + } else if(i == numPoints - 1) { + commonX[i] = targetMaxX; + } else { + commonX[i] = qExp(logX); + } + + if(!interpolateLogValue( + targetPressure, commonX[i], + &targetLogPressure[i]) || + !interpolateLogValue( + targetDerivative, commonX[i], + &targetLogDerivative[i])) { + return invalidLoss; + } + } + + QVector pressureResidual( + numPoints, std::numeric_limits::quiet_NaN()); + QVector derivativeResidual( + numPoints, std::numeric_limits::quiet_NaN()); + int firstSupported = -1; + int lastSupported = -1; + int supportedCount = 0; + + // 残差定义为“模拟减目标”:正值表示模拟曲线偏高,负值表示偏低。 + for(int i = 0; i < numPoints; ++i) { + if(commonX[i] < resultMinX || commonX[i] > resultMaxX) { + continue; + } + + double resultLogPressure = 0.0; + double resultLogDerivative = 0.0; + if(!interpolateLogValue( + resultPressure, commonX[i], + &resultLogPressure) || + !interpolateLogValue( + resultDerivative, commonX[i], + &resultLogDerivative)) { + // 已位于结果时间范围内却无法插值说明数据存在内部断点,不能补线。 + return invalidLoss; + } + + pressureResidual[i] = + resultLogPressure - targetLogPressure[i]; + derivativeResidual[i] = + resultLogDerivative - targetLogDerivative[i]; + ++supportedCount; + if(firstSupported < 0) { + firstSupported = i; + } + lastSupported = i; + } + + // 覆盖率同时约束“有效点数量”和“连续时间跨度”。取两者较小值可避免 + // 点数很多但只集中在局部时段的候选被误认为覆盖充分。 + AutoFitObjectiveBreakdownLM breakdown; + breakdown.coverage = supportedCount > 0 + ? static_cast(supportedCount) / + numPoints + : 0.0; + if(firstSupported >= 0 && lastSupported >= firstSupported) { + double targetSpan = + qMax(1.0e-12, targetLogMaxX - targetLogMinX); + double coveredSpan = + commonLogX[lastSupported] - commonLogX[firstSupported]; + breakdown.coverage = qMin( + breakdown.coverage, + qMax(0.0, coveredSpan / targetSpan)); + } + + // 覆盖率只判断候选是否有效,不再加入固定惩罚,避免所有误差被整体抬高。 + if(breakdown.coverage < minimumCoverage) { + breakdown.total = invalidLoss; + m_lastObjectiveBreakdown = breakdown; + return invalidLoss; + } + + // 通过门槛后最多只缺少首尾少量目标点。按模拟曲线端点趋势补齐后, + // 每个候选仍在固定 80 点上计算均方根误差,不能靠少算难拟合端点获益。 + for(int i = 0; i < numPoints; ++i) { + if(isFiniteNumber(pressureResidual[i]) && + isFiniteNumber(derivativeResidual[i])) { + continue; + } + double resultLogPressure = 0.0; + double resultLogDerivative = 0.0; + if(!extrapolateEndpointLogValue( + resultPressure, commonX[i], + &resultLogPressure) || + !extrapolateEndpointLogValue( + resultDerivative, commonX[i], + &resultLogDerivative)) { + return invalidLoss; + } + pressureResidual[i] = + resultLogPressure - targetLogPressure[i]; + derivativeResidual[i] = + resultLogDerivative - targetLogDerivative[i]; + } + + // 在指定中心附近计算普通均方根误差。 + auto rmseAround = []( + const QVector& values, + int begin, + int end, + double center) -> double { + double sum = 0.0; + int count = 0; + int validBegin = qMax(0, begin); + int validEnd = + qMin(end, static_cast(values.size())); + + for(int i = validBegin; i < validEnd; ++i) { + if(!isFiniteNumber(values[i])) { + continue; + } + + double difference = values[i] - center; + sum += difference * difference; + ++count; + } + + return count > 0 + ? qSqrt(sum / count) + : std::numeric_limits::quiet_NaN(); + }; + + auto rmse = [&rmseAround]( + const QVector& values, + int begin, + int end) -> double { + return rmseAround(values, begin, end, 0.0); + }; + + // 普通算术平均中心保留上下偏差的符号。 + auto meanCenterRange = []( + const QVector& values, + int begin, + int end) -> double { + int validBegin = qMax(0, begin); + int validEnd = + qMin(end, static_cast(values.size())); + double center = 0.0; + int count = 0; + + for(int i = validBegin; i < validEnd; ++i) { + if(isFiniteNumber(values[i])) { + center += values[i]; + ++count; + } + } + if(count == 0) { + return std::numeric_limits::quiet_NaN(); + } + return center / count; + }; + + // 压力和导数合并后只求一个公共中心,表示两条曲线共同的上下位移。 + // 分别去中心会把压力与导数之间真实的相对形状差异一并消除。 + auto commonMeanCenterRange = [&meanCenterRange]( + const QVector& pressureValues, + const QVector& derivativeValues, + int begin, + int end) -> double { + QVector combined; + int validBegin = qMax(0, begin); + int validEnd = qMin( + end, + qMin(static_cast(pressureValues.size()), + static_cast(derivativeValues.size()))); + combined.reserve(2 * qMax(0, validEnd - validBegin)); + + for(int i = validBegin; i < validEnd; ++i) { + if(isFiniteNumber(pressureValues[i])) { + combined.append(pressureValues[i]); + } + if(isFiniteNumber(derivativeValues[i])) { + combined.append(derivativeValues[i]); + } + } + return meanCenterRange(combined, 0, combined.size()); + }; + + // 两个通道按能量等权合并,返回值与单通道 RMSE 保持同一量纲。 + auto jointRmseAround = [&rmseAround]( + const QVector& pressureValues, + const QVector& derivativeValues, + int begin, + int end, + double center) -> double { + double pressureLoss = rmseAround( + pressureValues, begin, end, center); + double derivativeLoss = rmseAround( + derivativeValues, begin, end, center); + if(!isFiniteNumber(pressureLoss) || + !isFiniteNumber(derivativeLoss)) { + return std::numeric_limits::quiet_NaN(); + } + return qSqrt(0.5 * + (pressureLoss * pressureLoss + + derivativeLoss * derivativeLoss)); + }; + + // 主目标始终使用未做上下或左右校正的完整曲线误差。 + breakdown.pressureLoss = + rmse(pressureResidual, 0, numPoints); + breakdown.derivativeLoss = + rmse(derivativeResidual, 0, numPoints); + + // 压力和导数各占一半权重。缩放后 residualVector 的二范数就是 + // sqrt(0.5 * pressureLoss^2 + 0.5 * derivativeLoss^2)。 + const double residualScale = qSqrt(0.5 / numPoints); + breakdown.residualVector.reserve(2 * numPoints); + for(int i = 0; i < numPoints; ++i) { + breakdown.residualVector.append( + residualScale * + pressureResidual[i]); + } + for(int i = 0; i < numPoints; ++i) { + breakdown.residualVector.append( + residualScale * + derivativeResidual[i]); + } + + const double logGridStep = + (targetLogMaxX - targetLogMinX) / + (numPoints - 1); + const double resultLogMinX = qLn(resultMinX); + const double resultLogMaxX = qLn(resultMaxX); + // 左右配准只在所有候选位移都共同覆盖的固定区间比较,至少保留 80% + // 目标点;每个 log-time 网格间隔再细分为 8 份,提高位移分辨率。 + const int minimumRegistrationPoints = + (numPoints * 4) / 5; + const int shiftSubdivisions = 8; + int maximumShiftIntervals = 4; + int registrationBegin = 0; + int registrationEnd = numPoints; + double maximumPhysicalShift = + maximumShiftIntervals * logGridStep; + + // 所有位移候选使用同一组目标点。结果范围不足时逐步缩小最大位移, + // 但用于配准的固定公共区间不得少于目标网格的 80%。 + auto updateRegistrationRange = [&](double maximumShift) { + registrationBegin = 0; + registrationEnd = numPoints; + while(registrationBegin < registrationEnd && + commonLogX[registrationBegin] - maximumShift < + resultLogMinX - 1.0e-12) { + ++registrationBegin; + } + while(registrationEnd > registrationBegin && + commonLogX[registrationEnd - 1] + maximumShift > + resultLogMaxX + 1.0e-12) { + --registrationEnd; + } + }; + + updateRegistrationRange(maximumPhysicalShift); + while(maximumShiftIntervals > 0 && + registrationEnd - registrationBegin < + minimumRegistrationPoints) { + --maximumShiftIntervals; + maximumPhysicalShift = + maximumShiftIntervals * logGridStep; + updateRegistrationRange(maximumPhysicalShift); + } + if(registrationEnd - registrationBegin < + minimumRegistrationPoints) { + return invalidLoss; + } + + // physicalShift 为正表示模拟曲线偏右;对齐时在目标时刻右侧读取模拟值。 + auto buildShiftResidual = [&]( + double physicalShift, + int compareBegin, + int compareEnd, + QVector* shiftedPressure, + QVector* shiftedDerivative) -> bool { + if(!shiftedPressure || !shiftedDerivative) { + return false; + } + + shiftedPressure->fill( + std::numeric_limits::quiet_NaN(), + numPoints); + shiftedDerivative->fill( + std::numeric_limits::quiet_NaN(), + numPoints); + + int validBegin = qMax(0, compareBegin); + int validEnd = qMin(numPoints, compareEnd); + for(int i = validBegin; i < validEnd; ++i) { + double shiftedLogX = + commonLogX[i] + physicalShift; + if(shiftedLogX < resultLogMinX - 1.0e-12 || + shiftedLogX > + resultLogMaxX + 1.0e-12) { + return false; + } + + // 对数时间还原后再次限制到原始端点,避免 exp(log(t)) 的 + // 舍入误差越过严格插值边界。 + double shiftedX = qBound( + resultMinX, + qExp(qBound(resultLogMinX, + shiftedLogX, + resultLogMaxX)), + resultMaxX); + double resultLogPressure = 0.0; + double resultLogDerivative = 0.0; + if(!interpolateLogValue( + resultPressure, shiftedX, + &resultLogPressure) || + !interpolateLogValue( + resultDerivative, shiftedX, + &resultLogDerivative)) { + return false; + } + + (*shiftedPressure)[i] = + resultLogPressure - targetLogPressure[i]; + (*shiftedDerivative)[i] = + resultLogDerivative - targetLogDerivative[i]; + } + return true; + }; + + // 损失相同时优先选择绝对位移更小的候选,避免平坦 profile 在数值噪声 + // 下无故偏向搜索边界。 + auto isBetterProfileValue = []( + double loss, + double shift, + double bestLoss, + double bestShift) -> bool { + const double tolerance = 1.0e-12; + return loss < bestLoss - tolerance || + (qAbs(loss - bestLoss) <= tolerance && + qAbs(shift) < qAbs(bestShift)); + }; + + const int halfShiftStepCount = + maximumShiftIntervals * shiftSubdivisions; + const double physicalShiftStep = + logGridStep / shiftSubdivisions; + double zeroShiftCenteredLoss = + std::numeric_limits::quiet_NaN(); + double bestCenteredLoss = + std::numeric_limits::infinity(); + double bestPhysicalShift = 0.0; + int bestShiftStep = 0; + double bestPressureLoss = + std::numeric_limits::infinity(); + double bestPressureShift = 0.0; + double bestDerivativeLoss = + std::numeric_limits::infinity(); + double bestDerivativeShift = 0.0; + QVector profileLosses( + 2 * halfShiftStepCount + 1, + std::numeric_limits::quiet_NaN()); + QVector profileCommonBiases( + 2 * halfShiftStepCount + 1, + std::numeric_limits::quiet_NaN()); + QVector shiftedPressureResidual; + QVector shiftedDerivativeResidual; + + // 位移和公共上下偏移联合求解,避免“先扣上下还是先扣左右”的顺序依赖。 + for(int shiftStep = -halfShiftStepCount; + shiftStep <= halfShiftStepCount; + ++shiftStep) { + double physicalShift = + shiftStep * physicalShiftStep; + if(!buildShiftResidual( + physicalShift, + registrationBegin, + registrationEnd, + &shiftedPressureResidual, + &shiftedDerivativeResidual)) { + continue; + } + + double commonBias = commonMeanCenterRange( + shiftedPressureResidual, + shiftedDerivativeResidual, + registrationBegin, + registrationEnd); + double centeredLoss = jointRmseAround( + shiftedPressureResidual, + shiftedDerivativeResidual, + registrationBegin, + registrationEnd, + commonBias); + double pressureBias = meanCenterRange( + shiftedPressureResidual, + registrationBegin, + registrationEnd); + double derivativeBias = meanCenterRange( + shiftedDerivativeResidual, + registrationBegin, + registrationEnd); + double pressureLoss = rmseAround( + shiftedPressureResidual, + registrationBegin, + registrationEnd, + pressureBias); + double derivativeLoss = rmseAround( + shiftedDerivativeResidual, + registrationBegin, + registrationEnd, + derivativeBias); + + if(!isFiniteNumber(centeredLoss) || + !isFiniteNumber(pressureLoss) || + !isFiniteNumber(derivativeLoss)) { + continue; + } + int profileIndex = shiftStep + halfShiftStepCount; + profileLosses[profileIndex] = centeredLoss; + profileCommonBiases[profileIndex] = commonBias; + if(shiftStep == 0) { + zeroShiftCenteredLoss = centeredLoss; + } + if(isBetterProfileValue( + centeredLoss, + physicalShift, + bestCenteredLoss, + bestPhysicalShift)) { + bestCenteredLoss = centeredLoss; + bestPhysicalShift = physicalShift; + bestShiftStep = shiftStep; + } + if(isBetterProfileValue( + pressureLoss, + physicalShift, + bestPressureLoss, + bestPressureShift)) { + bestPressureLoss = pressureLoss; + bestPressureShift = physicalShift; + } + if(isBetterProfileValue( + derivativeLoss, + physicalShift, + bestDerivativeLoss, + bestDerivativeShift)) { + bestDerivativeLoss = derivativeLoss; + bestDerivativeShift = physicalShift; + } + } + + if(!isFiniteNumber(zeroShiftCenteredLoss) || + !isFiniteNumber(bestCenteredLoss) || + !isFiniteNumber(bestPressureLoss) || + !isFiniteNumber(bestDerivativeLoss)) { + return invalidLoss; + } + + // horizontalGain 是“允许水平位移”相对“固定零位移”减少的均方能量。 + // 只有改善足够明显且最优点不是边界,才把位移解释为可靠左右偏差。 + double horizontalGain = nestedRmsContribution( + zeroShiftCenteredLoss, bestCenteredLoss); + double horizontalSignalThreshold = + qMax(1.0e-5, zeroShiftCenteredLoss * 0.02); + int bestProfileIndex = bestShiftStep + halfShiftStepCount; + double nearbyProfileLoss = + std::numeric_limits::infinity(); + int leftProfileIndex = + bestProfileIndex - shiftSubdivisions; + int rightProfileIndex = + bestProfileIndex + shiftSubdivisions; + if(leftProfileIndex >= 0 && + leftProfileIndex < profileLosses.size() && + isFiniteNumber(profileLosses[leftProfileIndex])) { + nearbyProfileLoss = qMin( + nearbyProfileLoss, + profileLosses[leftProfileIndex]); + } + if(rightProfileIndex >= 0 && + rightProfileIndex < profileLosses.size() && + isFiniteNumber(profileLosses[rightProfileIndex])) { + nearbyProfileLoss = qMin( + nearbyProfileLoss, + profileLosses[rightProfileIndex]); + } + double profileContrast = isFiniteNumber(nearbyProfileLoss) + ? nestedRmsContribution( + nearbyProfileLoss, + bestCenteredLoss) + : 0.0; + bool flatRegistrationProfile = + profileContrast <= horizontalSignalThreshold; + + // 平台曲线的 profile 也可能很平,但公共 bias 在各个位移下保持稳定, + // 此时仍能可靠判断上下。只有近优位移会明显改变 bias 才说明上下/左右不可辨识。 + double minimumNearOptimalBias = + std::numeric_limits::infinity(); + double maximumNearOptimalBias = + -std::numeric_limits::infinity(); + for(int i = 0; i < profileLosses.size(); ++i) { + if(isFiniteNumber(profileLosses[i]) && + isFiniteNumber(profileCommonBiases[i]) && + profileLosses[i] <= + bestCenteredLoss + horizontalSignalThreshold) { + minimumNearOptimalBias = qMin( + minimumNearOptimalBias, + profileCommonBiases[i]); + maximumNearOptimalBias = qMax( + maximumNearOptimalBias, + profileCommonBiases[i]); + } + } + double nearOptimalBiasSpread = + isFiniteNumber(minimumNearOptimalBias) && + isFiniteNumber(maximumNearOptimalBias) + ? maximumNearOptimalBias - minimumNearOptimalBias + : std::numeric_limits::infinity(); + bool commonBiasStable = nearOptimalBiasSpread <= 1.0e-2; + bool horizontalAtBoundary = + halfShiftStepCount > 0 && + qAbs(bestShiftStep) == halfShiftStepCount; + // 压力和导数通道分别求出的最佳位移若方向相反或相差过大,说明一个 + // 单一水平平移无法解释两条曲线,此时标记配准歧义并禁用左右引导。 + bool pressureShiftDetected = + qAbs(bestPressureShift) >= + 0.5 * physicalShiftStep; + bool derivativeShiftDetected = + qAbs(bestDerivativeShift) >= + 0.5 * physicalShiftStep; + bool channelShiftConflict = + pressureShiftDetected && + derivativeShiftDetected && + (bestPressureShift * bestDerivativeShift < 0.0 || + qAbs(bestPressureShift - bestDerivativeShift) > + 2.0 * logGridStep); + + breakdown.horizontalLoss = horizontalGain; + breakdown.horizontalReliable = + maximumShiftIntervals > 0 && + !horizontalAtBoundary && + !channelShiftConflict && + !flatRegistrationProfile && + horizontalGain > horizontalSignalThreshold && + qAbs(bestPhysicalShift) >= + 0.5 * physicalShiftStep; + breakdown.registrationAmbiguous = + channelShiftConflict || + (qAbs(bestPhysicalShift) >= + 0.5 * physicalShiftStep && + !breakdown.horizontalReliable) || + (flatRegistrationProfile && !commonBiasStable); + breakdown.horizontalPhysicalShift = + breakdown.horizontalReliable + ? bestPhysicalShift + : 0.0; + + // 可信水平位移确定后,在该位移实际覆盖的最大区间重新计算上下和形状。 + int diagnosticBegin = 0; + int diagnosticEnd = numPoints; + while(diagnosticBegin < diagnosticEnd && + commonLogX[diagnosticBegin] + + breakdown.horizontalPhysicalShift < + resultLogMinX - 1.0e-12) { + ++diagnosticBegin; + } + while(diagnosticEnd > diagnosticBegin && + commonLogX[diagnosticEnd - 1] + + breakdown.horizontalPhysicalShift > + resultLogMaxX + 1.0e-12) { + --diagnosticEnd; + } + if(diagnosticEnd - diagnosticBegin < + minimumRegistrationPoints || + !buildShiftResidual( + breakdown.horizontalPhysicalShift, + diagnosticBegin, + diagnosticEnd, + &shiftedPressureResidual, + &shiftedDerivativeResidual)) { + return invalidLoss; + } + + double commonBias = commonMeanCenterRange( + shiftedPressureResidual, + shiftedDerivativeResidual, + diagnosticBegin, + diagnosticEnd); + double rawAlignedLoss = jointRmseAround( + shiftedPressureResidual, + shiftedDerivativeResidual, + diagnosticBegin, + diagnosticEnd, + 0.0); + double centeredAlignedLoss = jointRmseAround( + shiftedPressureResidual, + shiftedDerivativeResidual, + diagnosticBegin, + diagnosticEnd, + commonBias); + if(!isFiniteNumber(commonBias) || + !isFiniteNumber(rawAlignedLoss) || + !isFiniteNumber(centeredAlignedLoss)) { + return invalidLoss; + } + + // 原始对齐误差减去公共中心后的能量差定义为上下误差贡献。只有它相对 + // 当前对齐误差足够明显,且配准无歧义时,公共 bias 才可用于有符号选参。 + breakdown.verticalCommonBias = commonBias; + breakdown.verticalLoss = nestedRmsContribution( + rawAlignedLoss, centeredAlignedLoss); + breakdown.verticalReliable = + !breakdown.registrationAmbiguous && + breakdown.verticalLoss > + qMax(1.0e-5, rawAlignedLoss * 0.02); + + QVector shapePressure( + numPoints, std::numeric_limits::quiet_NaN()); + QVector shapeDerivative( + numPoints, std::numeric_limits::quiet_NaN()); + for(int i = diagnosticBegin; i < diagnosticEnd; ++i) { + if(isFiniteNumber(shiftedPressureResidual[i])) { + shapePressure[i] = + shiftedPressureResidual[i] - commonBias; + } + if(isFiniteNumber(shiftedDerivativeResidual[i])) { + shapeDerivative[i] = + shiftedDerivativeResidual[i] - commonBias; + } + } + + // shapeLoss 是去除可信左右位移和公共均值中心后的剩余误差。 + double shapePressureLoss = rmse( + shapePressure, diagnosticBegin, diagnosticEnd); + double shapeDerivativeLoss = rmse( + shapeDerivative, diagnosticBegin, diagnosticEnd); + if(!isFiniteNumber(shapePressureLoss) || + !isFiniteNumber(shapeDerivativeLoss)) { + return invalidLoss; + } + breakdown.shapeLoss = qSqrt( + 0.5 * + (shapePressureLoss * shapePressureLoss + + shapeDerivativeLoss * shapeDerivativeLoss)); + + // 现阶段不识别或单独调度晚期流动段;保留字段只为了维持现有 trace 列。 + breakdown.lateDerivativeSlopeBias = 0.0; + breakdown.lateDerivativeTrendLoss = 0.0; + breakdown.lateDerivativeTrendReliable = false; + + if(!isFiniteNumber(breakdown.pressureLoss) || + !isFiniteNumber(breakdown.derivativeLoss) || + !isFiniteNumber(breakdown.verticalCommonBias) || + !isFiniteNumber(breakdown.verticalLoss) || + !isFiniteNumber(breakdown.horizontalPhysicalShift) || + !isFiniteNumber(breakdown.horizontalLoss) || + !isFiniteNumber(breakdown.shapeLoss) || + breakdown.residualVector.size() != 2 * numPoints) { + return invalidLoss; + } + + // LM 总目标等于固定残差向量的二范数;压力和导数各占一半能量。 + // 上下、左右和形状分量不参与候选排序与接受。 + breakdown.total = qSqrt( + 0.5 * breakdown.pressureLoss * breakdown.pressureLoss + + 0.5 * breakdown.derivativeLoss * breakdown.derivativeLoss); + breakdown.valid = + isFiniteNumber(breakdown.total) && + breakdown.total >= 0.0; + m_lastObjectiveBreakdown = breakdown; + + DEBUG_OUT( + QString("LogLog objective: pressure=%1, derivative=%2, vertical=%3, horizontal=%4, shape=%5, ambiguous=%6, shift=%7, coverage=%8, total=%9") + .arg(breakdown.pressureLoss, 0, 'e', 4) + .arg(breakdown.derivativeLoss, 0, 'e', 4) + .arg(breakdown.verticalLoss, 0, 'e', 4) + .arg(breakdown.horizontalLoss, 0, 'e', 4) + .arg(breakdown.shapeLoss, 0, 'e', 4) + .arg(breakdown.registrationAmbiguous) + .arg(breakdown.horizontalPhysicalShift, 0, 'e', 4) + .arg(breakdown.coverage, 0, 'f', 4) + .arg(breakdown.total, 0, 'e', 4)); + + return breakdown.valid + ? qMin(1.0e9, breakdown.total) + : invalidLoss; + } catch(const std::exception& e) { + DEBUG_OUT( + QString("Exception in LogLog error calculation: %1") + .arg(e.what())); + return invalidLoss; + } catch(...) { + DEBUG_OUT("Unknown exception in LogLog error calculation"); + return invalidLoss; + } +} + +QVector> nmCalculationAutoFitLM::runSolverDll() +{ + // DLL 求解器路径。 + // 这个函数负责把当前 DataManager 中的项目状态交给底层数值求解器, + // 数值求解仍包含全部计算井,以保留井间干扰;后处理只提取目标井曲线。 + // + // 如果这里失败,通常需要优先检查:HX_NWTM.dll、license、网格/井数据是否完整、 + // 目标井是否存在,以及 DataManager 中刚写入的参数是否导致求解器异常。 + DEBUG_OUT("SOLVER DLL START"); + + if(m_evaluationInProgress > 0) { + DEBUG_OUT("DLL Solver already running, skipping"); + return QVector>(); + } + + ++m_evaluationInProgress; + m_lastEvaluatedLogLogData.clear(); + QVector> result; + nmCalculationDllPebiSolverTask* dllTask = nullptr; + + try { + DEBUG_OUT("Creating DLL solver task"); + dllTask = new nmCalculationDllPebiSolverTask(m_tempDirectory); + + if(m_targetWellName.isEmpty()) { + DEBUG_OUT("Target well name is empty - target-only solver cannot start"); + delete dllTask; + --m_evaluationInProgress; + return result; + } + + dllTask->setAutoFitTargetWell(m_targetWellName); + + if(m_shouldStop) { + DEBUG_OUT("Should stop - cleaning up and returning empty result"); + delete dllTask; + --m_evaluationInProgress; + return result; + } + + DEBUG_OUT("Starting DLL solver execution..."); + + // 异步执行 DLL 任务。循环等待期间持续 processEvents,保证界面不会完全卡死。 + dllTask->start(); + + // 等待完成。最大等待 1 小时,适配大模型慢算;用户停止时会 terminate。 + const int maxWait = 3600000; // 1h超时 + const int checkInterval = 50; + QTime waitTimer; + waitTimer.start(); + + while(waitTimer.elapsed() < maxWait) { + // wait(timeout) 会在线程一完成时立即返回,避免原来固定 msleep(500) + // 带来的每次 0~500ms 额外等待;50ms 间隔仍可及时处理停止请求和界面事件。 + if(dllTask->wait(checkInterval)) { + DEBUG_OUT("DLL solver task completed"); + break; + } + + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, checkInterval); + + if(m_shouldStop) { + DEBUG_OUT("DLL solver task terminated by user"); + dllTask->terminate(); + break; + } + } + + // 超时处理 + if(dllTask->isRunning()) { + DEBUG_OUT("DLL solver task timeout, terminating..."); + dllTask->terminate(); + dllTask->wait(2000); + + delete dllTask; + dllTask = nullptr; + --m_evaluationInProgress; + m_consecutiveFailures++; + return result; + } + + // 线程结束后检查真实执行结果,防止失败时复用上一粒子的旧曲线。 + dllTask->wait(); + if(!dllTask->wasSuccessful()) { + DEBUG_OUT("DLL solver task reported failure"); + delete dllTask; + dllTask = nullptr; + --m_evaluationInProgress; + m_consecutiveFailures++; + return result; + } + + // 任务结束后复制其局部结果,删除任务前不再持有任务内部引用。 + QVector> pressureResult = dllTask->getAutoFitResultPressure(); + QVector> logLogResult = dllTask->getAutoFitResultLogLog(); + + DEBUG_OUT(QString("DLL result verification - Pressure arrays: %1, LogLog arrays: %2") + .arg(pressureResult.size()).arg(logLogResult.size())); + + if(pressureResult.size() >= 2) { + DEBUG_OUT(QString("Pressure result - Time points: %1, Pressure points: %2") + .arg(pressureResult[0].size()).arg(pressureResult[1].size())); + + if(pressureResult[0].size() > 0) { + DEBUG_OUT(QString("Sample pressure data - Time[0]: %1, Time[last]: %2, P[0]: %3, P[last]: %4") + .arg(pressureResult[0][0]) + .arg(pressureResult[0][pressureResult[0].size() - 1]) + .arg(pressureResult[1][0]) + .arg(pressureResult[1][pressureResult[1].size() - 1])); + } + } + + // 数据有效性检查 + if(pressureResult.size() >= 2 + && pressureResult[0].size() > 0 + && pressureResult[1].size() > 0 + && validateLogLogData(logLogResult)) { + result = pressureResult; + m_lastEvaluatedLogLogData = logLogResult; + DEBUG_OUT(QString("Got DLL solver result: %1 points").arg(result[0].size())); + m_consecutiveFailures = 0; + + // 检查结果是否与之前不同。若连续粒子得到完全相同的压力曲线, + // 可能说明参数没有正确写入 DataManager,或求解器缓存/状态没有刷新。 + static QVector lastPressureResult; + bool isDifferentFromLast = false; + + if(lastPressureResult.isEmpty() || lastPressureResult.size() != pressureResult[1].size()) { + isDifferentFromLast = true; + } else { + for(int i = 0; i < qMin(5, pressureResult[1].size()); ++i) { + if(qAbs(lastPressureResult[i] - pressureResult[1][i]) > 1e-12) { + isDifferentFromLast = true; + break; + } + } + } + + if(isDifferentFromLast) { + DEBUG_OUT("RESULT VERIFICATION: Got NEW result data from DLL"); + lastPressureResult = pressureResult[1]; + } else { + DEBUG_OUT("!!! WARNING: Result data appears to be identical to previous run !!!"); + } + + } else { + DEBUG_OUT("DLL solver result is empty or invalid"); + DEBUG_OUT(QString("Pressure result size: %1, Array sizes: %2, %3") + .arg(pressureResult.size()) + .arg(pressureResult.size() > 0 ? pressureResult[0].size() : 0) + .arg(pressureResult.size() > 1 ? pressureResult[1].size() : 0)); + m_consecutiveFailures++; + } + + } catch(const std::bad_alloc& e) { + DEBUG_OUT(QString("Memory allocation failed in DLL solver: %1").arg(e.what())); + m_consecutiveFailures++; + } catch(const std::exception& e) { + DEBUG_OUT(QString("Exception in DLL solver: %1").arg(e.what())); + m_consecutiveFailures++; + } catch(...) { + DEBUG_OUT("Unknown exception in DLL solver"); + m_consecutiveFailures++; + } + + // 清理DLL任务 + if(dllTask) { + if(dllTask->isRunning()) { + dllTask->terminate(); + dllTask->wait(3000); + } + + DEBUG_OUT("Cleaning up DLL solver task..."); + delete dllTask; + dllTask = nullptr; + } + + QApplication::processEvents(QEventLoop::AllEvents, 100); + --m_evaluationInProgress; + + DEBUG_OUT(QString("SOLVER DLL END - ResultPoints: %1") + .arg(result.isEmpty() ? 0 : result[0].size())); + + return result; +} + +bool nmCalculationAutoFitLM::runFinalFullSolver() +{ + // 不设置目标井名,任务按原完整模式保存全部井和网格结果。 + if(m_evaluationInProgress > 0) { + DEBUG_OUT("Cannot start final full-field solver while another evaluation is running"); + return false; + } + + ++m_evaluationInProgress; + nmCalculationDllPebiSolverTask dllTask(m_tempDirectory); + dllTask.start(); + + const int maxWait = 3600000; + const int checkInterval = 50; + QTime waitTimer; + waitTimer.start(); + + while(waitTimer.elapsed() < maxWait) { + if(dllTask.wait(checkInterval)) { + break; + } + + // 最终完整计算可能持续较长时间,此处需处理停止按钮事件。 + QApplication::processEvents(QEventLoop::AllEvents, checkInterval); + + if(!dllTask.isRunning()) { + break; + } + + if(m_shouldStop) { + DEBUG_OUT("Final full-field solver terminated by user"); + dllTask.terminate(); + dllTask.wait(2000); + --m_evaluationInProgress; + return false; + } + } + + if(dllTask.isRunning()) { + DEBUG_OUT("Final full-field solver timeout, terminating task"); + dllTask.terminate(); + dllTask.wait(2000); + --m_evaluationInProgress; + return false; + } + + dllTask.wait(); + const bool succeeded = dllTask.wasSuccessful(); + --m_evaluationInProgress; + return succeeded; +} + +QString nmCalculationAutoFitLM::getStopReasonDescription(StopReasonLM reason) const +{ + // 将停止枚举转成人类可读文本,用于日志和运行摘要。 + switch(reason) { + case LM_TARGET_ACHIEVED: + return tr("Target error achieved"); + + case LM_TRUE_CONVERGENCE: + return tr("Algorithm converged to stable solution"); + + case LM_LOCAL_OPTIMUM: + return tr("Local optimum detected"); + + case LM_MAX_ITERATIONS: + return tr("Maximum iterations reached"); + + case LM_USER_STOPPED: + return tr("Stopped by user request"); + + case LM_CONSECUTIVE_FAILURES: + return tr("Too many consecutive failures"); + + case LM_OPTIMIZATION_FAILED: + return tr("Optimization failed"); + + default: + return tr("Unknown reason"); + } +} + diff --git a/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp b/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp index 6f0bd52..d0deda2 100644 --- a/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp @@ -104,17 +104,6 @@ static inline bool isFiniteNumber(double value) #endif } -// 两个 RMSE 的差不能直接解释为被消除的独立误差。RMSE 的平方才对应 -// 均方能量,因此先计算 reduced^2-full^2,再开方恢复原量纲。这里用于分别 -// 提取“消除公共上下偏差”和“消除水平位移”实际减少的误差贡献。 -static double nestedRmsContribution(double reducedModelLoss, - double fullModelLoss) -{ - return qSqrt(qMax(0.0, - reducedModelLoss * reducedModelLoss - - fullModelLoss * fullModelLoss)); -} - static inline bool isInClosedRange(double value, double lower, double upper) { return isFiniteNumber(value) && value >= lower && value <= upper; @@ -432,7 +421,7 @@ static QString findExecutableInPath(const QString& executableName) static QStringList traceParameterNames() { // trace 和 trace meta 使用的完整参数名顺序。 - // 这个顺序必须与 buildTraceParameterVector() 和 m_parameterSelected 的 0-9 索引一致。 + // 这个顺序必须与 buildTraceParameterVector() 和 m_parameterSelected 的 0-7 索引一致。 QStringList names; names << "k" << "skin" @@ -441,9 +430,7 @@ static QStringList traceParameterNames() << "h" << "Ct" << "Cf" - << "Swi" - << "Dfc" - << "fractureHalfLength"; + << "Swi"; return names; } @@ -734,8 +721,8 @@ void nmCalculationAutoFitPSO::setTargetLogLogData(const QVector void nmCalculationAutoFitPSO::stopFitting() { // 用户点击停止时走这里。停止策略是“请求式停止”: - // 只置 m_shouldStop,让主循环/求解器等待逻辑退出并自行维护任务计数。 - // 不在这里清理临时目录或强制清零计数,避免与正在返回的 DLL 任务竞争。 + // 先置 m_shouldStop,让主循环/求解器等待逻辑自然退出;短时间内还在评价时再重置计数。 + // 这样可以减少 DLL 任务被硬中断导致的数据状态残留。 if(m_simulationMode && m_simulationTimer) { m_simulationTimer->stop(); } @@ -749,28 +736,25 @@ void nmCalculationAutoFitPSO::stopFitting() m_shouldStop = true; - if(m_simulationMode) { - m_isRunning = false; - closeTraceFile(); - cleanupTemporaryDirectory(); - emit logMessageGenerated(tr("PSO simulation stop request processed")); - return; - } - - // 给当前评价一个短暂的自然退出时间。若仍在运行, - // runSolverDll() 会在下一个等待周期检查 m_shouldStop 并结束任务。 + // 等待当前评估完成,缩短超时时间 int waitCount = 0; - while(m_evaluationInProgress > 0 && waitCount < 30) { + while(m_evaluationInProgress > 0 && waitCount < 30) { // 减少等待时间 QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 50); msleep(50); waitCount++; } + // 超时时强制重置 if(m_evaluationInProgress > 0) { - emit logMessageGenerated(tr("Waiting for current solver evaluation to stop...")); + DEBUG_OUT("Force resetting evaluation counter"); + emit logMessageGenerated(tr("Force stopping current evaluation...")); + m_evaluationInProgress = 0; } + // 确保运行标志被清除 + m_isRunning = false; + emit logMessageGenerated(tr("PSO optimization stop request processed")); DEBUG_OUT("Stop request processed"); } else { @@ -778,6 +762,8 @@ void nmCalculationAutoFitPSO::stopFitting() emit logMessageGenerated(tr("Stop request received but optimization is not running")); } + closeTraceFile(); + cleanupTemporaryDirectory(); } bool nmCalculationAutoFitPSO::isRunning() const @@ -804,12 +790,6 @@ double nmCalculationAutoFitPSO::getBestFitness() const return m_globalBestFitness; } -AutoFitObjectiveBreakdown nmCalculationAutoFitPSO::getLastObjectiveBreakdown() const -{ - // 返回最近一次损失评价的误差分解,供界面或后续优化逻辑读取。 - return m_lastObjectiveBreakdown; -} - QString nmCalculationAutoFitPSO::getLastError() const { // 上一次失败的人类可读错误信息,主要给 UI 层弹窗或日志使用。 @@ -826,12 +806,9 @@ void nmCalculationAutoFitPSO::resetOptimizer() m_globalBestPosition.clear(); m_globalBestFitness = 1e10; m_previousBestFitness = 1e10; - m_globalBestObjectiveBreakdown = AutoFitObjectiveBreakdown(); m_lastEvaluatedLogLogData.clear(); m_globalBestLogLogData.clear(); - m_lastObjectiveBreakdown = AutoFitObjectiveBreakdown(); m_userInitialLogLogData.clear(); - m_userInitialObjectiveBreakdown = AutoFitObjectiveBreakdown(); m_currentIteration = 0; m_totalEvaluations = 0; m_successfulEvaluations = 0; @@ -859,8 +836,9 @@ void nmCalculationAutoFitPSO::setPSOTargetWellName(const QString& wellName) void nmCalculationAutoFitPSO::initializeTraceFile() { - // 创建本次自动拟合的可复盘文件。代理 PSO 与非代理信赖域使用不同前缀, - // 防止代理回放脚本把信赖域记录误当成最新 PSO 粒子记录。 + // 创建本次 PSO 的可复盘文件: + // - pso_baseline_trace_.csv:逐代逐粒子的参数、真实误差、代理误差和筛选决策; + // - pso_baseline_trace_.meta.json:目标曲线、流量制度、参数上下界、PSO/代理配置。 // // 代理模型评分脚本也会读取 meta.json,因此 trace meta 不是单纯日志,而是C++ 与 Python 代理模型之间的运行上下文契约。 if(!m_traceEnabled) { @@ -879,13 +857,8 @@ void nmCalculationAutoFitPSO::initializeTraceFile() return; } - QString tracePrefix = isSurrogateScreeningEnabled() - ? "pso_baseline_trace" - : "trust_region_trace"; - m_traceFilePath = traceDir.absoluteFilePath( - QString("%1_%2.csv").arg(tracePrefix).arg(m_traceRunId)); - m_traceMetaFilePath = traceDir.absoluteFilePath( - QString("%1_%2.meta.json").arg(tracePrefix).arg(m_traceRunId)); + m_traceFilePath = traceDir.absoluteFilePath(QString("pso_baseline_trace_%1.csv").arg(m_traceRunId)); + m_traceMetaFilePath = traceDir.absoluteFilePath(QString("pso_baseline_trace_%1.meta.json").arg(m_traceRunId)); m_traceFile.setFileName(m_traceFilePath); if(!m_traceFile.open(QIODevice::WriteOnly | QIODevice::Text)) { @@ -897,12 +870,11 @@ void nmCalculationAutoFitPSO::initializeTraceFile() writeTraceHeader(); writeTraceMetaFile(); - DEBUG_OUT(QString("Automatic fitting trace initialized: %1").arg(m_traceFilePath)); - emit logMessageGenerated(tr("Automatic fitting trace: %1").arg(m_traceFilePath)); + DEBUG_OUT(QString("PSO baseline trace initialized: %1").arg(m_traceFilePath)); + emit logMessageGenerated(tr("PSO baseline trace: %1").arg(m_traceFilePath)); if(!m_traceMetaFilePath.isEmpty()) { - emit logMessageGenerated( - tr("Automatic fitting trace meta: %1").arg(m_traceMetaFilePath)); + emit logMessageGenerated(tr("PSO baseline trace meta: %1").arg(m_traceMetaFilePath)); } emit logMessageGenerated(tr("PSO surrogate screening: %1, model=%2, keep=%3, audit=%4, warmup=%5, min_solver=%6") @@ -1013,13 +985,11 @@ void nmCalculationAutoFitPSO::closeTraceFile() void nmCalculationAutoFitPSO::writeTraceHeader() { // trace CSV 字段说明: - // - 代理模式保持原 k/skin/wellboreC/phi/h/Ct/Cf 契约; - // - 非代理模式额外记录 Swi/Dfc/裂缝半长,便于复盘信赖域调整; + // - 当前粒子参数只记录代理模型关心的 k/skin/wellboreC/phi/h/Ct/Cf; // - solver_objective 是真实求解器误差; // - surrogate_objective 是 Python 代理评分; // - screening_decision 说明该粒子为什么跑/不跑真实求解器; - // - pbest/gbest 字段用于离线复盘 PSO 更新是否只依赖真实误差; - // - 末尾诊断字段记录同一次真实评价的分量误差,便于核对引导方向和接受结果。 + // - pbest/gbest 字段用于离线复盘 PSO 更新是否只依赖真实误差。 if(!m_traceFile.isOpen()) { return; } @@ -1035,13 +1005,8 @@ void nmCalculationAutoFitPSO::writeTraceHeader() << "phi" << "h" << "Ct" - << "Cf"; - if(!isSurrogateScreeningEnabled()) { - cols << "Swi" - << "Dfc" - << "fractureHalfLength"; - } - cols << "solver_objective" + << "Cf" + << "solver_objective" << "solver_success" << "elapsed_ms" << "surrogate_objective" @@ -1053,45 +1018,16 @@ void nmCalculationAutoFitPSO::writeTraceHeader() << "pbest_phi" << "pbest_h" << "pbest_Ct" - << "pbest_Cf"; - if(!isSurrogateScreeningEnabled()) { - cols << "pbest_Swi" - << "pbest_Dfc" - << "pbest_fractureHalfLength"; - } - cols << "gbest_objective" + << "pbest_Cf" + << "gbest_objective" << "gbest_k" << "gbest_skin" << "gbest_wellboreC" << "gbest_phi" << "gbest_h" << "gbest_Ct" - << "gbest_Cf"; - if(!isSurrogateScreeningEnabled()) { - cols << "gbest_Swi" - << "gbest_Dfc" - << "gbest_fractureHalfLength"; - } - cols << "enabled_param_indices" - << "pressure_loss" - << "derivative_loss"; - if(!isSurrogateScreeningEnabled()) { - cols << "vertical_common_bias"; - } - cols << "vertical_loss"; - if(!isSurrogateScreeningEnabled()) { - cols << "vertical_reliable" - << "horizontal_physical_shift"; - } - cols << "horizontal_loss"; - if(!isSurrogateScreeningEnabled()) { - cols << "horizontal_reliable"; - } - cols << "shape_loss" - << "late_trend_loss" - << "late_slope_bias" - << "late_trend_reliable" - << "registration_ambiguous"; + << "gbest_Cf" + << "enabled_param_indices"; QTextStream out(&m_traceFile); out << cols.join(",") << "\n"; @@ -1133,14 +1069,8 @@ void nmCalculationAutoFitPSO::writeTraceMetaFile() QTextStream out(&metaFile); out << "{\n"; - // 非代理 v5 增加带符号诊断列;代理 PSO 保留原 v3 字段和目标,避免改变 - // 已有模型的训练和回放契约。 - out << " \"schema_version\": " - << (isSurrogateScreeningEnabled() ? 3 : 7) << ",\n"; - out << " \"trace_type\": " - << jsonEscape(isSurrogateScreeningEnabled() - ? "pso_baseline_replay_meta" - : "diagnostic_trust_region_meta") << ",\n"; + out << " \"schema_version\": 1,\n"; + out << " \"trace_type\": \"pso_baseline_replay_meta\",\n"; out << " \"run_id\": " << jsonEscape(m_traceRunId) << ",\n"; out << " \"created_at\": " << jsonEscape(QDateTime::currentDateTime().toString(Qt::ISODate)) << ",\n"; out << " \"trace_csv\": " << jsonEscape(QFileInfo(m_traceFilePath).fileName()) << ",\n"; @@ -1197,10 +1127,10 @@ void nmCalculationAutoFitPSO::writeTraceMetaFile() QVector nmCalculationAutoFitPSO::buildTraceParameterVector(const QVector& selectedParameters) const { - // 将粒子内部使用的“启用参数向量”还原成完整 10 维参数向量。 + // 将粒子内部使用的“启用参数向量”还原成完整 8 维参数向量。 // 未启用的参数从当前 DataManager 读取,启用的参数用 selectedParameters 覆盖。 // trace CSV、候选 CSV、代理训练域检查都需要这个完整向量。 - QVector fullParams(10, 0.0); + QVector fullParams(8, 0.0); nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); @@ -1216,28 +1146,8 @@ QVector nmCalculationAutoFitPSO::buildTraceParameterVector(const QVector nmDataWellBase* pTargetWell = dataManager->findWellByName(m_targetWellName); if(pTargetWell) { - nmDataPerforation* perforation = pTargetWell->getPerforation(0); - if(perforation) { - fullParams[1] = perforation->getSkin().getValue().toDouble(); - } + fullParams[1] = pTargetWell->getPerforation(0)->getSkin().getValue().toDouble(); fullParams[2] = pTargetWell->getWellboreStorage().getValue().toDouble(); - - // Dfc 只存在于两类压裂井,普通井在完整向量中保持为 0。 - if(pTargetWell->getWellType() == NM_WELL_MODEL::Vertical_Fractured_Well) { - nmDataVerticalFracturedWell* fracturedWell = - dynamic_cast(pTargetWell); - if(fracturedWell) { - fullParams[8] = fracturedWell->getDfc().getValue().toDouble(); - fullParams[9] = fracturedWell->getFractureHalfLength().getValue().toDouble(); - } - } else if(pTargetWell->getWellType() == NM_WELL_MODEL::Horizontal_Fractured_Well) { - nmDataHorizontalFracturedWell* fracturedWell = - dynamic_cast(pTargetWell); - if(fracturedWell) { - fullParams[8] = fracturedWell->getDfc().getValue().toDouble(); - fullParams[9] = fracturedWell->getFractureHalfLength().getValue().toDouble(); - } - } } } @@ -1262,8 +1172,7 @@ void nmCalculationAutoFitPSO::writeTraceRow(int generation, double surrogateObjective, const QString& screeningDecision, const QVector& pbestPosition, - double pbestObjective, - const AutoFitObjectiveBreakdown* objectiveBreakdown) + double pbestObjective) { // 写一行 trace。generation=-1/particleIndex=-1 表示用户初始解; // 普通粒子行的 phase 为 particle_solver、particle_verified_cache 或 particle_not_evaluated。 @@ -1293,13 +1202,8 @@ void nmCalculationAutoFitPSO::writeTraceRow(int generation, << traceParamAt(currentParams, 3) << traceParamAt(currentParams, 4) << traceParamAt(currentParams, 5) - << traceParamAt(currentParams, 6); - if(!isSurrogateScreeningEnabled()) { - cols << traceParamAt(currentParams, 7) - << traceParamAt(currentParams, 8) - << traceParamAt(currentParams, 9); - } - cols << traceNumber(solverObjective) + << traceParamAt(currentParams, 6) + << traceNumber(solverObjective) << QString::number(solverSuccess ? 1 : 0) << QString::number(elapsedMs) << traceNumber(surrogateObjective) @@ -1311,55 +1215,16 @@ void nmCalculationAutoFitPSO::writeTraceRow(int generation, << traceParamAt(pbestParams, 3) << traceParamAt(pbestParams, 4) << traceParamAt(pbestParams, 5) - << traceParamAt(pbestParams, 6); - if(!isSurrogateScreeningEnabled()) { - cols << traceParamAt(pbestParams, 7) - << traceParamAt(pbestParams, 8) - << traceParamAt(pbestParams, 9); - } - cols << traceNumber(m_globalBestFitness) + << traceParamAt(pbestParams, 6) + << traceNumber(m_globalBestFitness) << traceParamAt(gbestParams, 0) << traceParamAt(gbestParams, 1) << traceParamAt(gbestParams, 2) << traceParamAt(gbestParams, 3) << traceParamAt(gbestParams, 4) << traceParamAt(gbestParams, 5) - << traceParamAt(gbestParams, 6); - if(!isSurrogateScreeningEnabled()) { - cols << traceParamAt(gbestParams, 7) - << traceParamAt(gbestParams, 8) - << traceParamAt(gbestParams, 9); - } - cols << csvEscape(enabledIndices.join(";")); - - if(objectiveBreakdown && objectiveBreakdown->valid) { - cols << traceNumber(objectiveBreakdown->pressureLoss) - << traceNumber(objectiveBreakdown->derivativeLoss); - if(!isSurrogateScreeningEnabled()) { - cols << traceNumber(objectiveBreakdown->verticalCommonBias); - } - cols << traceNumber(objectiveBreakdown->verticalLoss); - if(!isSurrogateScreeningEnabled()) { - cols << QString::number(objectiveBreakdown->verticalReliable ? 1 : 0) - << traceNumber(objectiveBreakdown->horizontalPhysicalShift); - } - cols << traceNumber(objectiveBreakdown->horizontalLoss); - if(!isSurrogateScreeningEnabled()) { - cols << QString::number(objectiveBreakdown->horizontalReliable ? 1 : 0); - } - cols << traceNumber(objectiveBreakdown->shapeLoss) - << traceNumber(objectiveBreakdown->lateDerivativeTrendLoss) - << traceNumber(objectiveBreakdown->lateDerivativeSlopeBias) - << QString::number(objectiveBreakdown->lateDerivativeTrendReliable ? 1 : 0) - << QString::number(objectiveBreakdown->registrationAmbiguous ? 1 : 0); - } else { - // 未运行真实求解器或评价无效时保持列数一致,诊断字段写空值。 - int diagnosticColumnCount = - isSurrogateScreeningEnabled() ? 9 : 13; - for(int i = 0; i < diagnosticColumnCount; ++i) { - cols << QString(); - } - } + << traceParamAt(gbestParams, 6) + << csvEscape(enabledIndices.join(";")); QTextStream out(&m_traceFile); out << cols.join(",") << "\n"; @@ -1392,11 +1257,9 @@ void nmCalculationAutoFitPSO::writeIterationTraceRows() particle.lastEvaluationSuccess, particle.lastEvaluationElapsedMs, particle.surrogateObjective, - particle.screeningDecision, - particle.bestPosition, - particle.bestFitness, - particle.evaluatedThisIteration - ? &particle.currentObjectiveBreakdown : nullptr); + particle.screeningDecision, + particle.bestPosition, + particle.bestFitness); } } @@ -2926,14 +2789,14 @@ void nmCalculationAutoFitPSO::loadParameterBounds() // 读取用户勾选的拟合参数及上下界。 // // 这里构建三个核心数组: - // - m_parameterSelected[10]:完整参数体系中每个参数是否参与拟合; - // - m_parameterLower/Upper[10]:完整参数体系的搜索上下界; + // - m_parameterSelected[8]:完整参数体系中每个参数是否参与拟合; + // - m_parameterLower/Upper[8]:完整参数体系的搜索上下界; // - m_enabledParamIndices:把粒子内部紧凑向量映射回完整参数索引。 nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); nmDataAutomaticFitting fittingData = dataManager->getAutomaticFittingDataCopy(); // 获取参数选择状态 - m_parameterSelected.resize(10); + m_parameterSelected.resize(8); m_parameterSelected[0] = fittingData.getPermeabilitySelected(); m_parameterSelected[1] = fittingData.getSkinSelected(); m_parameterSelected[2] = fittingData.getWellboreStorageSelected(); @@ -2942,12 +2805,10 @@ void nmCalculationAutoFitPSO::loadParameterBounds() m_parameterSelected[5] = fittingData.getCtSelected(); m_parameterSelected[6] = fittingData.getCfSelected(); m_parameterSelected[7] = fittingData.getSwiSelected(); - m_parameterSelected[8] = fittingData.getFractureConductivitySelected(); - m_parameterSelected[9] = fittingData.getFractureHalfLengthSelected(); // 获取参数边界 - m_parameterLower.resize(10); - m_parameterUpper.resize(10); + m_parameterLower.resize(8); + m_parameterUpper.resize(8); m_parameterLower[0] = fittingData.getPermeabilityMin().getValue().toDouble(); m_parameterUpper[0] = fittingData.getPermeabilityMax().getValue().toDouble(); @@ -2973,12 +2834,6 @@ void nmCalculationAutoFitPSO::loadParameterBounds() m_parameterLower[7] = fittingData.getSwiMin().getValue().toDouble(); m_parameterUpper[7] = fittingData.getSwiMax().getValue().toDouble(); - m_parameterLower[8] = fittingData.getFractureConductivityMin().getValue().toDouble(); - m_parameterUpper[8] = fittingData.getFractureConductivityMax().getValue().toDouble(); - - m_parameterLower[9] = fittingData.getFractureHalfLengthMin().getValue().toDouble(); - m_parameterUpper[9] = fittingData.getFractureHalfLengthMax().getValue().toDouble(); - // 更新启用参数索引 m_enabledParamIndices.clear(); @@ -2992,19 +2847,21 @@ void nmCalculationAutoFitPSO::loadParameterBounds() .arg(m_enabledParamIndices.size())); } -// ==================== 自动拟合核心方法 ==================== +// ==================== PSO算法核心方法 ==================== bool nmCalculationAutoFitPSO::startAutoFitting() { - // 自动拟合总入口:代理开启时保留原 PSO 筛选流程;代理关闭时改走 - // 诊断灵敏度信赖域搜索。两条路径共用初始解评价、真实求解器和结果写回。 - StopReasonPSO finalReason = PSO_CONTINUE_OPTIMIZATION; - bool useParticleSwarm = false; - + // 自动拟合的总入口。可以把这个函数当成 PSO 的“运行剧本”: + // 读取配置 -> 校验输入 -> 评价用户初始解 -> 初始化粒子群 -> + // 按代循环评价粒子 -> 更新全局最优 -> 判断停止 -> 保存结果。 if(m_isRunning) { m_lastError = "Auto fitting is already running"; return false; } + // 发送初始化日志 + //emit logMessageGenerated(tr("=== PSO Automatic Fitting Started ===")); + emit logMessageGenerated(tr("Algorithm: Particle Swarm Optimization")); + try { // 从 DataManager 读取界面保存的自动拟合配置。 // 本类不直接依赖 UI 控件,便于后续从脚本或其他入口复用。 @@ -3013,11 +2870,6 @@ bool nmCalculationAutoFitPSO::startAutoFitting() return false; } - useParticleSwarm = isSurrogateScreeningEnabled(); - emit logMessageGenerated(useParticleSwarm - ? tr("Algorithm: Particle Swarm Optimization") - : tr("Algorithm: Diagnostic Trust-Region Search")); - if(m_simulationMode) { // 调试/演示用快速路径,不调用真实求解器。正式工况通常不走这里。 DEBUG_OUT("=== SIMULATION MODE ACTIVATED ==="); @@ -3051,15 +2903,6 @@ bool nmCalculationAutoFitPSO::startAutoFitting() emit logMessageGenerated(tr("Target data validation passed (%1 data points)").arg(m_targetLogLogData[0].size())); - if(m_targetWellName.isEmpty()) { - m_lastError = "Target well name is empty"; - emit logMessageGenerated(tr("ERROR: Target well name is empty")); - return false; - } - - emit logMessageGenerated(tr("Particle evaluation mode: solve all wells, retain target well '%1' only") - .arg(m_targetWellName)); - // 使用保存的初始值进行精英保护。resetOptimizer() 会清空部分运行状态, // 所以先把用户当前模型参数缓存下来,后面再恢复用于初始解评价和粒子初始化。 QVector savedInitialValues = m_initialValues; @@ -3147,8 +2990,6 @@ bool nmCalculationAutoFitPSO::startAutoFitting() m_globalBestPosition = m_userInitialSolution; m_userInitialLogLogData = m_lastEvaluatedLogLogData; m_globalBestLogLogData = m_userInitialLogLogData; - m_userInitialObjectiveBreakdown = m_lastObjectiveBreakdown; - m_globalBestObjectiveBreakdown = m_userInitialObjectiveBreakdown; emit logMessageGenerated(tr("Initial solution evaluation successful")); emit logMessageGenerated(tr("Initial Error: %1").arg(m_userInitialFitness, 0, 'e', 4)); @@ -3167,11 +3008,9 @@ bool nmCalculationAutoFitPSO::startAutoFitting() m_userInitialFitness < 1e9, initialEvalElapsedMs, std::numeric_limits::quiet_NaN(), - "initial_solution", - m_hasValidUserSolution ? m_userInitialSolution : QVector(), - m_userInitialFitness, - m_hasValidUserSolution - ? &m_userInitialObjectiveBreakdown : nullptr); + "initial_solution", + m_hasValidUserSolution ? m_userInitialSolution : QVector(), + m_userInitialFitness); } catch(...) { m_hasValidUserSolution = false; emit logMessageGenerated(tr("Exception during initial solution evaluation")); @@ -3181,13 +3020,12 @@ bool nmCalculationAutoFitPSO::startAutoFitting() m_initialValues = savedInitialValues; } - if(useParticleSwarm) { - // 初始化粒子群。粒子维度等于用户勾选的参数数量,而不是固定 11 维。 - if(kUseFixedPsoSeed) { - emit logMessageGenerated(tr("PSO random seed: %1 ").arg(m_psoRandomSeed)); - } else { - emit logMessageGenerated(tr("PSO random seed: %1 ").arg(m_psoRandomSeed)); - } + // 初始化粒子群。粒子维度等于用户勾选的参数数量,而不是固定 11 维。 + if(kUseFixedPsoSeed) { + emit logMessageGenerated(tr("PSO random seed: %1 ").arg(m_psoRandomSeed)); + } else { + emit logMessageGenerated(tr("PSO random seed: %1 ").arg(m_psoRandomSeed)); + } initializeSwarm(); emit logMessageGenerated(tr("Swarm initialized: %1 particles, %2 dimensions").arg(m_swarmSize).arg(getEnabledParameterCount())); @@ -3336,7 +3174,9 @@ bool nmCalculationAutoFitPSO::startAutoFitting() .arg(currentSuccessRate * 100, 0, 'f', 1).arg(m_currentIteration + 1)); } - // 只从真实求解器确认的粒子 bestFitness 更新全局最优,不使用代理分数。 + // 更新全局最优。updateGlobalBest() 只读取粒子的真实 bestFitness, + // 不使用代理模型的 surrogateObjective。 + //double previousGlobalBest = m_globalBestFitness; updateGlobalBest(); // 记录本代所有粒子的真实/代理误差和筛选决策,用于复盘和排障。 writeIterationTraceRows(); @@ -3425,40 +3265,11 @@ bool nmCalculationAutoFitPSO::startAutoFitting() } } - finalReason = analyzeOptimizationStatus(); - } else { - // 非代理路径不初始化粒子,也不使用 pbest/gbest 速度更新。 - finalReason = runTrustRegionFitting(); - } - // 最终结果验证和保护 validateAndProtectFinalResult(); - if(!useParticleSwarm && !m_globalBestPosition.isEmpty() && - m_globalBestObjectiveBreakdown.valid) { - // 精英保护可能恢复用户初始解,最终行必须在保护之后写入,确保 trace - // 中最后记录的就是实际回写 DataManager 的参数,而非最后一次接受候选。 - writeTraceRow(m_currentIteration, -1, - "trust_region_final", - m_globalBestPosition, - m_globalBestFitness, - m_globalBestFitness < 1.0e9, - -1, - std::numeric_limits::quiet_NaN(), - "final_result", - m_globalBestPosition, - m_globalBestFitness, - &m_globalBestObjectiveBreakdown); - } - - if(m_globalBestFitness < m_targetError) { - finalReason = PSO_TARGET_ACHIEVED; - } - } catch(const std::exception& e) { - m_lastError = useParticleSwarm - ? QString(tr("Critical exception in PSO main loop: %1")).arg(e.what()) - : QString(tr("Critical exception in automatic fitting: %1")).arg(e.what()); + m_lastError = QString(tr("Critical exception in PSO main loop: %1")).arg(e.what()); emit logMessageGenerated(tr("CRITICAL ERROR: %1").arg(e.what())); closeTraceFile(); cleanupTemporaryDirectory(); @@ -3466,9 +3277,7 @@ bool nmCalculationAutoFitPSO::startAutoFitting() emit fittingFinished(false, m_lastError); return false; } catch(...) { - m_lastError = useParticleSwarm - ? QString(tr("Unknown critical exception in PSO main loop")) - : QString(tr("Unknown critical exception in automatic fitting")); + m_lastError = QString(tr("Unknown critical exception in PSO main loop")); emit logMessageGenerated(tr("CRITICAL ERROR: Unknown exception in PSO main loop")); closeTraceFile(); cleanupTemporaryDirectory(); @@ -3477,49 +3286,13 @@ bool nmCalculationAutoFitPSO::startAutoFitting() return false; } - bool finalFullSolverSucceeded = true; - bool finalFullSolverExecuted = false; + m_isRunning = false; // 应用最终参数 if(!m_globalBestPosition.isEmpty()) { try { emit logMessageGenerated(tr("Applying optimized parameters to model...")); applyParametersToDataManager(m_globalBestPosition); - - // 即使用户此时停止、不再执行最终完整计算,也要把 PEBI 缓存恢复为 - // 最终已接受的裂缝参数,避免缓存仍停留在最后一个被拒绝的候选值。 - const bool fractureGridParameterSelected = - (m_parameterSelected.size() > 8 && m_parameterSelected[8]) || - (m_parameterSelected.size() > 9 && m_parameterSelected[9]); - if(fractureGridParameterSelected) { - nmCalculationPebiGrid* pebiGrid = nmCalculationPebiGrid::getInstance(); - if(!pebiGrid || !pebiGrid->generateOutputPara()) { - throw std::runtime_error("Failed to refresh final fracture parameters"); - } - } - - if(m_shouldStop) { - // 手动停止优先保持快速返回,仅写回已确认的最优参数。 - emit logMessageGenerated(tr("Final full-field calculation skipped after user stop")); - } else { - // 粒子阶段只保留目标井临时曲线。正常结束后用最优参数完整计算一次, - // 将全部井曲线和网格压力场写回项目,该次不计入 PSO 粒子评价数。 - emit logMessageGenerated(tr("Running final full-field calculation with optimized parameters...")); - finalFullSolverExecuted = true; - finalFullSolverSucceeded = runFinalFullSolver(); - - if(finalFullSolverSucceeded) { - emit logMessageGenerated(tr("Final full-field calculation completed successfully")); - } else if(m_shouldStop) { - finalFullSolverExecuted = false; - finalFullSolverSucceeded = true; - emit logMessageGenerated(tr("Final full-field calculation stopped by user")); - } else { - emit logMessageGenerated(tr("ERROR: Final full-field calculation failed")); - m_lastError = tr("Optimized parameters were found, but the final full-field calculation failed"); - } - } - saveOptimizationResult(); // 输出最终优化结果 @@ -3538,94 +3311,56 @@ bool nmCalculationAutoFitPSO::startAutoFitting() emit logMessageGenerated(finalParams); - if(finalFullSolverExecuted && finalFullSolverSucceeded) { - emit logMessageGenerated(tr("Parameters and full-field results applied successfully to data manager")); - } else if(!finalFullSolverExecuted) { - emit logMessageGenerated(tr("Optimized parameters applied to data manager")); - } + emit logMessageGenerated(tr("Parameters applied successfully to data manager")); } catch(const std::exception& e) { - finalFullSolverSucceeded = false; emit logMessageGenerated(tr("ERROR: Failed to apply final parameters: %1").arg(e.what())); m_lastError = QString("Failed to apply final parameters: %1").arg(e.what()); } catch(...) { - finalFullSolverSucceeded = false; emit logMessageGenerated(tr("ERROR: Unknown error applying final parameters")); m_lastError = "Failed to apply final parameters due to unknown error"; } } - m_isRunning = false; - // 判断系统确定最终结果 bool success; QString message; + StopReasonPSO finalReason = analyzeOptimizationStatus(); if(finalReason == PSO_TARGET_ACHIEVED) { success = true; message = QString(tr("Target achieved. Best error: %1, Iterations: %2")) .arg(m_globalBestFitness, 0, 'e', 4).arg(m_currentIteration + 1); - emit logMessageGenerated(useParticleSwarm - ? tr("=== PSO OPTIMIZATION SUCCESSFUL ===") - : tr("=== AUTOMATIC FITTING SUCCESSFUL ===")); + emit logMessageGenerated(tr("=== PSO OPTIMIZATION SUCCESSFUL ===")); } else if(finalReason == PSO_TRUE_CONVERGENCE) { success = true; - message = useParticleSwarm - ? QString(tr("PSO optimization converged to stable solution. Best error: %1, Iterations: %2")) - .arg(m_globalBestFitness, 0, 'e', 4).arg(m_currentIteration + 1) - : QString(tr("Automatic fitting converged to a stable solution. Best error: %1, Iterations: %2")) - .arg(m_globalBestFitness, 0, 'e', 4).arg(m_currentIteration + 1); - emit logMessageGenerated(useParticleSwarm - ? tr("=== PSO OPTIMIZATION CONVERGED ===") - : tr("=== AUTOMATIC FITTING CONVERGED ===")); + message = QString(tr("PSO optimization converged to stable solution. Best error: %1, Iterations: %2")) + .arg(m_globalBestFitness, 0, 'e', 4).arg(m_currentIteration + 1); + emit logMessageGenerated(tr("=== PSO OPTIMIZATION CONVERGED ===")); } else if(finalReason == PSO_LOCAL_OPTIMUM) { success = true; - message = useParticleSwarm - ? QString(tr("PSO optimization trapped in local optimum. Best error: %1, Iterations: %2")) - .arg(m_globalBestFitness, 0, 'e', 4).arg(m_currentIteration + 1) - : QString(tr("Automatic fitting reached a local optimum. Best error: %1, Iterations: %2")) - .arg(m_globalBestFitness, 0, 'e', 4).arg(m_currentIteration + 1); - emit logMessageGenerated(useParticleSwarm - ? tr("=== PSO OPTIMIZATION - LOCAL OPTIMUM ===") - : tr("=== AUTOMATIC FITTING - LOCAL OPTIMUM ===")); + message = QString(tr("PSO optimization trapped in local optimum. Best error: %1, Iterations: %2")) + .arg(m_globalBestFitness, 0, 'e', 4).arg(m_currentIteration + 1); + emit logMessageGenerated(tr("=== PSO OPTIMIZATION - LOCAL OPTIMUM ===")); } else if(finalReason == PSO_MAX_ITERATIONS) { success = true; message = QString(tr("Max iterations reached. Best error: %1, Iterations: %2")) .arg(m_globalBestFitness, 0, 'e', 4).arg(m_currentIteration + 1); - emit logMessageGenerated(useParticleSwarm - ? tr("=== PSO OPTIMIZATION - MAX ITERATIONS ===") - : tr("=== AUTOMATIC FITTING - MAX ITERATIONS ===")); + emit logMessageGenerated(tr("=== PSO OPTIMIZATION - MAX ITERATIONS ===")); } else if(finalReason == PSO_USER_STOPPED) { success = true; message = QString(tr("Best error: %1, Iterations: %2")) .arg(m_globalBestFitness, 0, 'e', 4).arg(m_currentIteration + 1); - emit logMessageGenerated(useParticleSwarm - ? tr("=== PSO OPTIMIZATION STOPPED BY USER ===") - : tr("=== AUTOMATIC FITTING STOPPED BY USER ===")); + emit logMessageGenerated(tr("=== PSO OPTIMIZATION STOPPED BY USER ===")); } else if(finalReason == PSO_CONSECUTIVE_FAILURES) { success = false; - message = useParticleSwarm - ? QString(tr("PSO optimization failed due to consecutive failures. Best error: %1, Iterations: %2")) - .arg(m_globalBestFitness, 0, 'e', 4).arg(m_currentIteration + 1) - : QString(tr("Automatic fitting failed due to consecutive failures. Best error: %1, Iterations: %2")) - .arg(m_globalBestFitness, 0, 'e', 4).arg(m_currentIteration + 1); - emit logMessageGenerated(useParticleSwarm - ? tr("=== PSO OPTIMIZATION FAILED ===") - : tr("=== AUTOMATIC FITTING FAILED ===")); + message = QString(tr("PSO optimization failed due to consecutive failures. Best error: %1, Iterations: %2")) + .arg(m_globalBestFitness, 0, 'e', 4).arg(m_currentIteration + 1); + emit logMessageGenerated(tr("=== PSO OPTIMIZATION FAILED ===")); } else { success = false; - message = useParticleSwarm - ? QString(tr("PSO optimization ended unexpectedly. Best error: %1, Iterations: %2")) - .arg(m_globalBestFitness, 0, 'e', 4).arg(m_currentIteration + 1) - : QString(tr("Automatic fitting ended unexpectedly. Best error: %1, Iterations: %2")) - .arg(m_globalBestFitness, 0, 'e', 4).arg(m_currentIteration + 1); - emit logMessageGenerated(useParticleSwarm - ? tr("=== PSO OPTIMIZATION - UNKNOWN END ===") - : tr("=== AUTOMATIC FITTING - UNKNOWN END ===")); - } - - if(!finalFullSolverSucceeded) { - success = false; - message = m_lastError; + message = QString(tr("PSO optimization ended unexpectedly. Best error: %1, Iterations: %2")) + .arg(m_globalBestFitness, 0, 'e', 4).arg(m_currentIteration + 1); + emit logMessageGenerated(tr("=== PSO OPTIMIZATION - UNKNOWN END ===")); } emitRunSummary(success, finalReason); @@ -3711,38 +3446,6 @@ void nmCalculationAutoFitPSO::extractUserInitialValues() case 7: // 初始含水饱和度 initialValue = reservoirData.getSwi().getValue().toDouble(); break; - - case 8: // 裂缝导流能力 - if(pTargetWell && pTargetWell->getWellType() == NM_WELL_MODEL::Vertical_Fractured_Well) { - nmDataVerticalFracturedWell* fracturedWell = - dynamic_cast(pTargetWell); - if(fracturedWell) { - initialValue = fracturedWell->getDfc().getValue().toDouble(); - } - } else if(pTargetWell && pTargetWell->getWellType() == NM_WELL_MODEL::Horizontal_Fractured_Well) { - nmDataHorizontalFracturedWell* fracturedWell = - dynamic_cast(pTargetWell); - if(fracturedWell) { - initialValue = fracturedWell->getDfc().getValue().toDouble(); - } - } - break; - - case 9: // 裂缝半长 - if(pTargetWell && pTargetWell->getWellType() == NM_WELL_MODEL::Vertical_Fractured_Well) { - nmDataVerticalFracturedWell* fracturedWell = - dynamic_cast(pTargetWell); - if(fracturedWell) { - initialValue = fracturedWell->getFractureHalfLength().getValue().toDouble(); - } - } else if(pTargetWell && pTargetWell->getWellType() == NM_WELL_MODEL::Horizontal_Fractured_Well) { - nmDataHorizontalFracturedWell* fracturedWell = - dynamic_cast(pTargetWell); - if(fracturedWell) { - initialValue = fracturedWell->getFractureHalfLength().getValue().toDouble(); - } - } - break; } m_initialValues.append(initialValue); @@ -3787,8 +3490,6 @@ void nmCalculationAutoFitPSO::initializeSwarm() particle.velocity.resize(dimensions); particle.bestPosition.resize(dimensions); particle.guideBestPosition.resize(dimensions); - particle.currentObjectiveBreakdown = AutoFitObjectiveBreakdown(); - particle.bestObjectiveBreakdown = AutoFitObjectiveBreakdown(); particle.bestFitness = 1e10; particle.guideBestObjective = 1e10; particle.guideBestFromSurrogate = false; @@ -3854,1442 +3555,154 @@ void nmCalculationAutoFitPSO::initializeSwarm() particle.bestPosition = particle.position; particle.guideBestPosition = particle.position; - - // 第一个粒子可能直接复用用户初始解,因此同步保存初始解的误差分解, - // 后续误差引导只能使用真实求解器确认过的 breakdown。 - if(i == 0 && m_hasValidUserSolution) { - particle.currentObjectiveBreakdown = m_userInitialObjectiveBreakdown; - particle.bestObjectiveBreakdown = m_userInitialObjectiveBreakdown; - } } } -// 信赖域搜索统一在 [0, 1] 内部坐标工作。正值参数使用对数坐标,使内部相同步长 -// 表示近似相同的相对变化,避免 k、C、Ct、Cf 等跨数量级参数被线性尺度支配; -// skin 可为负数、Swi 的物理意义是线性比例,因此二者保持有界线性坐标。 -static bool useTrustRegionLogScale(int parameterIndex, double lower, double upper) +void nmCalculationAutoFitPSO::updateParticle(int particleIndex) { - return parameterIndex != 1 && parameterIndex != 7 && - lower > 0.0 && upper > lower; -} + if(particleIndex < 0 || particleIndex >= m_swarm.size()) return; -static double toTrustRegionCoordinate(double value, - int parameterIndex, - double lower, - double upper) -{ - // 所有进入优化器的物理值先投影到用户上下界,再转换成无量纲坐标。 - // 这样有限差分步长、信赖半径和参数间相关性可以在统一尺度上比较。 - value = qMax(lower, qMin(upper, value)); + AutoFitParticle& particle = m_swarm[particleIndex]; + + // 单粒子真实评价入口。 + // 这里调用 evaluateFitness(),因此会真实写 DataManager、调用求解器、计算误差。 + // 被代理模型筛掉的粒子不会进入这个函数。 + particle.evaluatedThisIteration = false; + particle.lastEvaluationSuccess = false; + particle.lastEvaluationElapsedMs = -1; + particle.pbestRelativeImprovementThisIteration = 0.0; + particle.selectedForSolver = true; - if(useTrustRegionLogScale(parameterIndex, lower, upper)) { - return (qLn(value) - qLn(lower)) / (qLn(upper) - qLn(lower)); + if(particle.screeningDecision.isEmpty()) { + particle.screeningDecision = "full_solver"; } - return upper > lower ? (value - lower) / (upper - lower) : 0.0; -} + bool reuseInitialSolution = m_currentIteration == 0 && + particleIndex == 0 && + m_hasValidUserSolution && + m_userInitialFitness < 1e9 && + particle.position.size() == m_userInitialSolution.size(); -static double fromTrustRegionCoordinate(double coordinate, - int parameterIndex, - double lower, - double upper) -{ - // 候选内部坐标先限制在 [0,1],再执行上述映射的逆变换,保证写回 - // DataManager 的参数始终位于用户设置的物理范围内。 - coordinate = qMax(0.0, qMin(1.0, coordinate)); + for(int i = 0; reuseInitialSolution && i < particle.position.size(); ++i) { + double tolerance = qMax(1.0e-12, qAbs(m_userInitialSolution[i]) * 1.0e-12); + reuseInitialSolution = qAbs(particle.position[i] - m_userInitialSolution[i]) <= tolerance; + } + + if(reuseInitialSolution) { + // 初始解在进入粒子群前已经真实求解过。第一代第0号粒子位置完全相同, + // 直接复用真实误差和曲线,避免一次重复 DLL 调用且不改变 PSO 数学状态。 + particle.fitness = m_userInitialFitness; + particle.currentLogLogData = m_userInitialLogLogData; + particle.lastEvaluationElapsedMs = 0; + particle.evaluatedThisIteration = true; + particle.lastEvaluationSuccess = true; + particle.screeningDecision = "initial_solution_cache"; + emit logMessageGenerated(tr("Particle 1 reused the verified initial solution")); + } else { + QTime evalTimer; + evalTimer.start(); + particle.fitness = evaluateFitness(particle.position); + particle.currentLogLogData = m_lastEvaluatedLogLogData; + particle.lastEvaluationElapsedMs = evalTimer.elapsed(); + particle.evaluatedThisIteration = true; + particle.lastEvaluationSuccess = (particle.fitness < 1e9); + m_totalEvaluations++; - if(useTrustRegionLogScale(parameterIndex, lower, upper)) { - return qExp(qLn(lower) + coordinate * (qLn(upper) - qLn(lower))); + if(particle.fitness < 1e9) { + m_successfulEvaluations++; + } } - return lower + coordinate * (upper - lower); -} + // 更新个体最优 pbest。这里使用的是真实求解器误差 particle.fitness, + // 不是代理模型给出的 surrogateObjective。 + double previousBestFitness = particle.bestFitness; -enum TrustRegionErrorComponent -{ - TRUST_REGION_VERTICAL_COMPONENT = 0, - TRUST_REGION_HORIZONTAL_COMPONENT, - TRUST_REGION_SHAPE_COMPONENT, - TRUST_REGION_TOTAL_COMPONENT -}; + if(particle.fitness < previousBestFitness) { + particle.pbestRelativeImprovementThisIteration = previousBestFitness >= 1e9 + ? 1.0 + : (previousBestFitness - particle.fitness) / + qMax(1e-10, qAbs(previousBestFitness)); -// 一次真实求解的完整快照。除了参数和总误差,还保存内部坐标、诊断分量和 -// 双对数曲线,因此拒绝候选后可以完整恢复上一个已接受工作点。 -struct TrustRegionEvaluation -{ - QVector parameters; - QVector coordinates; - AutoFitObjectiveBreakdown breakdown; - QVector > curve; - double fitness; - int elapsedMs; - bool valid; - - TrustRegionEvaluation() - : fitness(1.0e10) - , elapsedMs(-1) - , valid(false) - {} -}; + // 如果当前位置尚未成为新的真实全局最优,而上一代存在尚未真实验证、且代理 + // 仍判断更优的 guide,就保留它继续引导速度;真实 pbest 仍照常更新。 + bool preserveSurrogateGuide = false; -// LM 只使用固定长度、全部有限的普通残差。代理路径不会进入本搜索器。 -static bool trustRegionResidualsValid( - const AutoFitObjectiveBreakdown& breakdown) -{ - // 损失函数固定使用 80 个压力点和 80 个导数点。严格校验长度,避免 - // Jacobian 沿用旧维度后访问另一候选的短残差向量。 - if(!breakdown.valid || breakdown.residualVector.size() != 160) { - return false; - } + bool currentBeatsGlobalBest = particle.fitness < m_globalBestFitness; - for(int i = 0; i < breakdown.residualVector.size(); ++i) { - if(!isFiniteNumber(breakdown.residualVector[i])) { - return false; + if(!currentBeatsGlobalBest && + isSurrogateScreeningEnabled() && + particle.guideBestFromSurrogate && + isFiniteNumber(particle.guideBestObjective) && + isFiniteNumber(particle.surrogateObjective)) { + double requiredImprovement = qMax(1.0e-10, + qAbs(particle.guideBestObjective) * + kSurrogateGuidePbestMinRelativeImprovement); + preserveSurrogateGuide = particle.surrogateObjective - + particle.guideBestObjective > requiredImprovement; } - } - return true; -} -// 计算向量二范数的平方,避免在只比较能量或计算正规方程时反复开方。 -static double trustRegionSquaredNorm(const QVector& values) -{ - double sum = 0.0; - for(int i = 0; i < values.size(); ++i) { - sum += values[i] * values[i]; - } - return sum; -} + particle.bestFitness = particle.fitness; + particle.bestPosition = particle.position; + particle.bestLogLogData = particle.currentLogLogData; -// 计算同维向量内积;维度不一致表示局部模型无效,返回零让调用方放弃修正。 -static double trustRegionDotProduct(const QVector& left, - const QVector& right) -{ - if(left.size() != right.size()) { - return 0.0; - } + if(!preserveSurrogateGuide) { + particle.guideBestPosition = particle.position; + particle.guideBestObjective = isFiniteNumber(particle.surrogateObjective) + ? particle.surrogateObjective + : particle.fitness; + particle.guideBestFromSurrogate = false; + } - double sum = 0.0; - for(int i = 0; i < left.size(); ++i) { - sum += left[i] * right[i]; + DEBUG_OUT(QString("Particle %1 improved: error = %2") + .arg(particleIndex).arg(particle.fitness, 0, 'e', 4)); } - return sum; } -// trace 和运行日志使用稳定的英文标识,便于现有离线脚本继续按字段筛选。 -static QString trustRegionComponentName(int component) +void nmCalculationAutoFitPSO::updateGlobalBest() { - if(component == TRUST_REGION_VERTICAL_COMPONENT) { - return "vertical"; - } - if(component == TRUST_REGION_HORIZONTAL_COMPONENT) { - return "horizontal"; - } - if(component == TRUST_REGION_SHAPE_COMPONENT) { - return "shape"; - } - return "total"; -} + // 保存上一轮的全局最优,用于后续自适应参数调整等 + m_previousBestFitness = m_globalBestFitness; + bool globalBestUpdated = false; -// 三类损失量纲一致,直接选择当前最大的可靠分量;都很小时退回总残差梯度。 -static int trustRegionDominantComponent( - const AutoFitObjectiveBreakdown& breakdown, - double diagnosisThreshold) -{ - int component = TRUST_REGION_TOTAL_COMPONENT; - double largestLoss = diagnosisThreshold; + // 遍历所有粒子,寻找比当前 global best 更好的个体最优。 + // 注意:particle.bestFitness 只有在真实求解器评价成功后才会更新。 + for(int i = 0; i < m_swarm.size(); ++i) { + const AutoFitParticle& particle = m_swarm[i]; - if(breakdown.verticalReliable && - isFiniteNumber(breakdown.verticalLoss) && - breakdown.verticalLoss > largestLoss) { - component = TRUST_REGION_VERTICAL_COMPONENT; - largestLoss = breakdown.verticalLoss; - } - if(breakdown.horizontalReliable && - !breakdown.registrationAmbiguous && - isFiniteNumber(breakdown.horizontalLoss) && - breakdown.horizontalLoss > largestLoss) { - component = TRUST_REGION_HORIZONTAL_COMPONENT; - largestLoss = breakdown.horizontalLoss; - } - if(isFiniteNumber(breakdown.shapeLoss) && - breakdown.shapeLoss > largestLoss) { - component = TRUST_REGION_SHAPE_COMPONENT; - } + // 只要个体最优比当前全局最优小,就认为是更好的解 + if(particle.bestFitness < m_globalBestFitness) { - return component; -} + double improvement = m_globalBestFitness - particle.bestFitness; + double relativeImprovement = + improvement / qMax(1e-10, qAbs(m_globalBestFitness)); -// 求解选中参数对应的阻尼正规方程。上下和左右诊断量保留方向;形状没有 -// 天然正负,因此使用 shapeLoss 对参数的局部导数。参数最多八维,使用带 -// 部分主元的高斯消元即可处理该小矩阵,并在主元退化时明确返回失败。 -static bool solveTrustRegionLinearSystem( - QVector > matrix, - QVector rightHandSide, - QVector* solution) -{ - if(!solution || matrix.isEmpty() || - matrix.size() != rightHandSide.size()) { - return false; - } + // 区分显著改进和微小改进,但无论如何都会更新全局最优 + if(relativeImprovement > m_improvementThreshold) { + DEBUG_OUT(QString(tr("Global best updated with %1% improvement: %2")) + .arg(relativeImprovement * 100.0, 0, 'f', 3) + .arg(particle.bestFitness, 0, 'e', 4)); + } else { + DEBUG_OUT(QString(tr("Global best updated (minor improvement %1% < %2%) to %3")) + .arg(relativeImprovement * 100.0, 0, 'f', 3) + .arg(m_improvementThreshold * 100.0, 0, 'f', 2) + .arg(particle.bestFitness, 0, 'e', 4)); + } - const int size = matrix.size(); - for(int i = 0; i < size; ++i) { - if(matrix[i].size() != size) { - return false; + // 无论相对改进是否超过阈值,都要更新全局最优 + m_globalBestFitness = particle.bestFitness; + m_globalBestPosition = particle.bestPosition; + m_globalBestLogLogData = particle.bestLogLogData; + globalBestUpdated = true; + emit bestCurveUpdated(m_targetLogLogData, m_globalBestLogLogData, m_currentIteration + 1, m_globalBestFitness); } } - for(int column = 0; column < size; ++column) { - int pivotRow = column; - double pivotMagnitude = qAbs(matrix[column][column]); - for(int row = column + 1; row < size; ++row) { - double magnitude = qAbs(matrix[row][column]); - if(magnitude > pivotMagnitude) { - pivotMagnitude = magnitude; - pivotRow = row; - } - } - if(pivotMagnitude <= 1.0e-14) { - return false; - } - - if(pivotRow != column) { - qSwap(matrix[pivotRow], matrix[column]); - qSwap(rightHandSide[pivotRow], rightHandSide[column]); - } + // 只有在本轮没有找到任何更好的解时才考虑恢复用户初始解 + if(!globalBestUpdated && m_hasValidUserSolution && m_userInitialFitness < m_globalBestFitness) { - for(int row = column + 1; row < size; ++row) { - double factor = matrix[row][column] / - matrix[column][column]; - matrix[row][column] = 0.0; - for(int nextColumn = column + 1; - nextColumn < size; ++nextColumn) { - matrix[row][nextColumn] -= - factor * matrix[column][nextColumn]; - } - rightHandSide[row] -= factor * rightHandSide[column]; - } - } - - solution->fill(0.0, size); - for(int row = size - 1; row >= 0; --row) { - double value = rightHandSide[row]; - for(int column = row + 1; column < size; ++column) { - value -= matrix[row][column] * (*solution)[column]; - } - double pivot = matrix[row][row]; - if(qAbs(pivot) <= 1.0e-14) { - return false; - } - (*solution)[row] = value / pivot; - if(!isFiniteNumber((*solution)[row])) { - return false; - } - } - return true; -} - -// 计算两个 Jacobian 列向量的绝对余弦相似度。接近 1 表示两个参数在当前 -// 工作点对曲线的影响几乎相同,联合调整容易产生不可辨识方向。 -static double trustRegionJacobianColumnCorrelation( - const QVector >& jacobian, - int leftColumn, - int rightColumn) -{ - double product = 0.0; - double leftNorm = 0.0; - double rightNorm = 0.0; - for(int row = 0; row < jacobian.size(); ++row) { - if(leftColumn >= jacobian[row].size() || - rightColumn >= jacobian[row].size()) { - return 1.0; - } - double left = jacobian[row][leftColumn]; - double right = jacobian[row][rightColumn]; - product += left * right; - leftNorm += left * left; - rightNorm += right * right; - } - - if(leftNorm <= 1.0e-20 || rightNorm <= 1.0e-20) { - return 0.0; - } - return qAbs(product) / qSqrt(leftNorm * rightNorm); -} - -// 每次接受一个真实候选后,使用满足最新割线条件的秩一修正更新完整残差 -// Jacobian。这样模型吸收了刚得到的真实变化,又不必立即逐参数重新试算。 -static void updateTrustRegionJacobian( - QVector >* jacobian, - const QVector& oldResidual, - const QVector& newResidual, - const QVector& coordinateStep) -{ - if(!jacobian || jacobian->size() != oldResidual.size() || - oldResidual.size() != newResidual.size()) { - return; - } - - double denominator = trustRegionSquaredNorm(coordinateStep); - if(denominator <= 1.0e-12) { - return; - } - - for(int row = 0; row < jacobian->size(); ++row) { - if((*jacobian)[row].size() != coordinateStep.size()) { - return; - } - - double predictedChange = 0.0; - for(int column = 0; column < coordinateStep.size(); ++column) { - predictedChange += - (*jacobian)[row][column] * coordinateStep[column]; - } - double correction = - (newResidual[row] - oldResidual[row] - predictedChange) / - denominator; - for(int column = 0; column < coordinateStep.size(); ++column) { - (*jacobian)[row][column] += - correction * coordinateStep[column]; - } - } -} - -// 对上下偏差、左右偏差和形状损失的梯度执行同样的割线秩一修正,使诊断 -// 选参模型与完整残差 Jacobian 保持在同一个已接受工作点。 -static void updateTrustRegionScalarGradient( - QVector* gradient, - double oldValue, - double newValue, - const QVector& coordinateStep) -{ - if(!gradient || gradient->size() != coordinateStep.size() || - !isFiniteNumber(oldValue) || !isFiniteNumber(newValue)) { - return; - } - - double denominator = trustRegionSquaredNorm(coordinateStep); - if(denominator <= 1.0e-12) { - return; - } - - double predictedChange = trustRegionDotProduct( - *gradient, coordinateStep); - double correction = - (newValue - oldValue - predictedChange) / denominator; - for(int i = 0; i < gradient->size(); ++i) { - (*gradient)[i] += correction * coordinateStep[i]; - } -} - -bool nmCalculationAutoFitPSO::evaluateTrustRegionPoint( - const QVector& parameters, - double* fitness, - AutoFitObjectiveBreakdown* breakdown, - QVector >* curve, - int* elapsedMs) -{ - if(!fitness || !breakdown || !curve || !elapsedMs || m_shouldStop) { - return false; - } - - // evaluateFitness() 会写入 DataManager 并调用真实求解器。这里统一统计 - // 真实评价次数和耗时,同时严格要求固定残差、诊断结构和结果曲线均有效。 - QTime timer; - timer.start(); - *fitness = evaluateFitness(parameters); - *elapsedMs = timer.elapsed(); - *breakdown = m_lastObjectiveBreakdown; - *curve = m_lastEvaluatedLogLogData; - ++m_totalEvaluations; - - bool valid = isFiniteNumber(*fitness) && *fitness < 1.0e9 && - breakdown->valid && - trustRegionResidualsValid(*breakdown) && - !curve->isEmpty(); - if(valid) { - ++m_successfulEvaluations; - } - - return valid; -} - -StopReasonPSO nmCalculationAutoFitPSO::runTrustRegionFitting() -{ - const int dimensions = getEnabledParameterCount(); - if(dimensions <= 0 || m_enabledParamIndices.size() != dimensions) { - m_lastError = tr("No valid parameters are available for trust-region fitting"); - return PSO_OPTIMIZATION_FAILED; - } - - // 真实求解次数比“外层迭代次数”更能反映耗时。预算至少允许完成一次全参数 - // 灵敏度和两次候选评价,同时避免连续重建 Jacobian 导致运行时间失控。 - const int maximumEvaluations = qMax( - m_totalEvaluations + dimensions + 2, - qMax(20, m_maxIterations * 3)); - // 下列步长均位于归一化内部坐标:0.04 表示参数范围的 4%,信赖半径 - // 限制一次联合移动的二范数,相关性门槛用于排除响应近乎共线的参数。 - const double sensitivityStep = 0.04; - const double minimumCoordinateStep = 1.0e-5; - const double minimumTrustRadius = 2.0e-3; - const double maximumTrustRadius = 0.30; - const double columnCorrelationLimit = 0.995; - const double diagnosisThreshold = 1.0e-5; - // 误差下降至少达到绝对 1e-5 且相对当前有效基准 0.2% 才算有效改善。 - // 更小的下降仍保留为最佳解,但不能反复清除停滞状态、延长拟合时间。 - const double effectiveRelativeImprovement = 2.0e-3; - const double effectiveAbsoluteImprovement = 1.0e-5; - const int maximumIneffectiveSteps = 3; - - // damping 是 LM 阻尼;拒绝或预测失准时增大,真实下降与预测一致时减小。 - // 两组累计量控制 Jacobian 重建,避免长期使用已偏离当前工作点的局部模型。 - double trustRadius = 0.12; - double damping = 1.0e-2; - int consecutiveRejectedSteps = 0; - int consecutiveSolverFailures = 0; - int acceptedSinceRebuild = 0; - int consecutiveIneffectiveSteps = 0; - double movementSinceRebuild = 0.0; - bool rebuildRequested = true; - bool modelRebuiltAtMinimumRadius = false; - bool stagnationConfirmationRequested = false; - StopReasonPSO stopReason = PSO_MAX_ITERATIONS; - - // jacobian 的行对应固定 160 维残差,列对应用户勾选的参数。 - // 三个 gradient 单独描述诊断分量对参数的局部变化,只用于本轮选参。 - QVector > jacobian; - QVector verticalGradient(dimensions, 0.0); - QVector horizontalGradient(dimensions, 0.0); - QVector shapeGradient(dimensions, 0.0); - QVector jacobianColumnValid(dimensions, false); - - // 参数向量的顺序始终与 m_enabledParamIndices 一致,不能按完整参数索引 - // 直接访问;下面两个转换函数集中维护这层映射关系。 - auto coordinatesFromParameters = [&](const QVector& parameters) - -> QVector { - QVector coordinates(dimensions, 0.0); - for(int i = 0; i < dimensions; ++i) { - int parameterIndex = m_enabledParamIndices[i]; - coordinates[i] = toTrustRegionCoordinate( - parameters[i], parameterIndex, - m_parameterLower[parameterIndex], - m_parameterUpper[parameterIndex]); - } - return coordinates; - }; - - auto parametersFromCoordinates = [&](const QVector& coordinates) - -> QVector { - QVector parameters(dimensions, 0.0); - for(int i = 0; i < dimensions; ++i) { - int parameterIndex = m_enabledParamIndices[i]; - parameters[i] = fromTrustRegionCoordinate( - coordinates[i], parameterIndex, - m_parameterLower[parameterIndex], - m_parameterUpper[parameterIndex]); - } - return parameters; - }; - - auto restoreEvaluationState = [&](const TrustRegionEvaluation& evaluation) { - // evaluateFitness() 会把试算参数写入 DataManager。无论候选是否接受, - // 下一次计算前都恢复到唯一的已接受工作点,防止失败试算污染后续求解。 - applyParametersToDataManager(evaluation.parameters); - m_lastObjectiveBreakdown = evaluation.breakdown; - m_lastEvaluatedLogLogData = evaluation.curve; - }; - - // 只有真实总误差更小的工作点才能发布为全局最优;曲线和诊断快照必须 - // 与参数同步更新,防止界面显示或最终精英保护使用错配的数据。 - auto publishAcceptedPoint = [&](const TrustRegionEvaluation& evaluation) { - m_previousBestFitness = m_globalBestFitness; - m_globalBestPosition = evaluation.parameters; - m_globalBestFitness = evaluation.fitness; - m_globalBestObjectiveBreakdown = evaluation.breakdown; - m_globalBestLogLogData = evaluation.curve; - emit bestCurveUpdated(m_targetLogLogData, - m_globalBestLogLogData, - m_currentIteration + 1, - m_globalBestFitness); - }; - - auto processPauseAndStop = [&]() -> bool { - while(m_isPaused && !m_shouldStop) { - QApplication::processEvents(); - msleep(100); - } - QApplication::processEvents(); - return !m_shouldStop; - }; - - // current 始终代表唯一已接受工作点。优先复用启动阶段已经真实验证的 - // 用户初始解,避免在信赖域入口重复调用一次昂贵求解器。 - TrustRegionEvaluation current; - if(m_hasValidUserSolution && - m_globalBestPosition.size() == dimensions && - trustRegionResidualsValid(m_globalBestObjectiveBreakdown) && - !m_globalBestLogLogData.isEmpty()) { - current.parameters = m_globalBestPosition; - current.coordinates = coordinatesFromParameters(current.parameters); - current.breakdown = m_globalBestObjectiveBreakdown; - current.curve = m_globalBestLogLogData; - current.fitness = m_globalBestFitness; - current.elapsedMs = 0; - current.valid = true; - } else { - // 用户初始解无效时只做一次确定性的范围中点回退;所有正值参数在对数 - // 坐标取中点,避免线性中点过分偏向跨数量级范围的上界。 - current.coordinates.fill(0.5, dimensions); - current.parameters = parametersFromCoordinates(current.coordinates); - current.valid = evaluateTrustRegionPoint( - current.parameters, - ¤t.fitness, - ¤t.breakdown, - ¤t.curve, - ¤t.elapsedMs); - writeTraceRow(-1, -1, - "trust_region_midpoint", - current.parameters, - current.fitness, - current.valid, - current.elapsedMs, - std::numeric_limits::quiet_NaN(), - current.valid ? "midpoint_valid" : "midpoint_invalid", - QVector(), - 1.0e10, - current.valid ? ¤t.breakdown : nullptr); - if(!current.valid) { - m_lastError = tr("The initial solution and parameter-range midpoint are both invalid"); - return m_shouldStop - ? PSO_USER_STOPPED - : PSO_OPTIMIZATION_FAILED; - } - publishAcceptedPoint(current); - } - - restoreEvaluationState(current); - m_convergenceHistory.append(current.fitness); - emit logMessageGenerated( - tr("Trust-region initial error: %1; evaluation budget: %2") - .arg(current.fitness, 0, 'e', 4) - .arg(maximumEvaluations)); - - // 有效改善始终相对“上一次有效改善后的误差”累计判断,避免一连串微小 - // 下降每次都清零计数;累计达到门槛后才开始新的有效改善基准。 - double effectiveImprovementBaseline = current.fitness; - auto registerEffectiveImprovement = [&](double fitness) -> bool { - const double requiredImprovement = qMax( - effectiveAbsoluteImprovement, - qAbs(effectiveImprovementBaseline) * - effectiveRelativeImprovement); - const double improvement = effectiveImprovementBaseline - fitness; - if(improvement < requiredImprovement) { - return false; - } - - effectiveImprovementBaseline = fitness; - consecutiveIneffectiveSteps = 0; - stagnationConfirmationRequested = false; - return true; - }; - - // 连续三次没有有效改善时只请求一次灵敏度重建。重建完成后由主循环 - // 直接检查累计改善,仍达不到门槛就判定局部收敛,不再继续微小试探。 - auto recordIneffectiveStep = [&]() -> bool { - ++consecutiveIneffectiveSteps; - if(consecutiveIneffectiveSteps < maximumIneffectiveSteps) { - return false; - } - - if(stagnationConfirmationRequested) { - return true; - } - - consecutiveIneffectiveSteps = 0; - stagnationConfirmationRequested = true; - rebuildRequested = true; - emit logMessageGenerated( - tr("No effective improvement for %1 consecutive steps; " - "rebuilding sensitivity model for confirmation") - .arg(maximumIneffectiveSteps)); - return false; - }; - - emit logMessageGenerated( - tr("Effective improvement threshold: max(%1, %2% of baseline error); " - "%3 consecutive ineffective steps trigger convergence confirmation") - .arg(effectiveAbsoluteImprovement, 0, 'e', 2) - .arg(effectiveRelativeImprovement * 100.0, 0, 'f', 2) - .arg(maximumIneffectiveSteps)); - - if(current.fitness < m_targetError) { - return PSO_TARGET_ACHIEVED; - } - - // 在同一个真实工作点逐参数做单边差分。首选可用空间更大的方向;只有该方向 - // 求解失败时才补算反方向,因此初次建模通常每个参数只增加一次真实求解。 - auto rebuildSensitivity = [&]() -> bool { - const TrustRegionEvaluation base = current; - const int residualCount = base.breakdown.residualVector.size(); - if(residualCount <= 0) { - return false; - } - - jacobian = QVector >( - residualCount, QVector(dimensions, 0.0)); - verticalGradient.fill(0.0, dimensions); - horizontalGradient.fill(0.0, dimensions); - shapeGradient.fill(0.0, dimensions); - jacobianColumnValid.fill(false, dimensions); - - TrustRegionEvaluation bestProbe; - int bestProbeColumn = -1; - double bestProbeDelta = 0.0; - // 差分步长不超过参数范围的 4%,信赖域收缩后同步减小,但保留 0.5% - // 下限,避免步长太小使求解器数值噪声淹没真实灵敏度。 - const double finiteDifferenceStep = qMin( - sensitivityStep, - qMax(5.0e-3, trustRadius * 0.5)); - - for(int column = 0; - column < dimensions && - m_totalEvaluations < maximumEvaluations && - processPauseAndStop(); - ++column) { - // 单边差分优先选择离边界空间更大的方向;首方向求解无效时才反向 - // 补算,因此正常情况下每个参数只消耗一次真实求解。 - double positiveRoom = 1.0 - base.coordinates[column]; - double negativeRoom = base.coordinates[column]; - double preferredSign = positiveRoom >= negativeRoom ? 1.0 : -1.0; - bool columnBuilt = false; - - for(int directionAttempt = 0; - directionAttempt < 2 && - !columnBuilt && - m_totalEvaluations < maximumEvaluations; - ++directionAttempt) { - double direction = directionAttempt == 0 - ? preferredSign : -preferredSign; - double availableRoom = direction > 0.0 - ? positiveRoom : negativeRoom; - double deltaMagnitude = qMin( - finiteDifferenceStep, availableRoom); - if(deltaMagnitude < minimumCoordinateStep) { - continue; - } - - TrustRegionEvaluation probe; - probe.coordinates = base.coordinates; - probe.coordinates[column] += direction * deltaMagnitude; - probe.parameters = parametersFromCoordinates(probe.coordinates); - probe.valid = evaluateTrustRegionPoint( - probe.parameters, - &probe.fitness, - &probe.breakdown, - &probe.curve, - &probe.elapsedMs); - - QString decision = probe.valid - ? "sensitivity_valid" - : (directionAttempt == 0 - ? "sensitivity_retry_opposite" - : "sensitivity_invalid"); - writeTraceRow(m_currentIteration, - column, - "trust_region_sensitivity", - probe.parameters, - probe.fitness, - probe.valid, - probe.elapsedMs, - std::numeric_limits::quiet_NaN(), - decision, - base.parameters, - base.fitness, - probe.valid ? &probe.breakdown : nullptr); - - if(!probe.valid) { - restoreEvaluationState(base); - continue; - } - - double delta = probe.coordinates[column] - - base.coordinates[column]; - if(qAbs(delta) < minimumCoordinateStep || - probe.breakdown.residualVector.size() != residualCount) { - restoreEvaluationState(base); - continue; - } - - // 第 column 列是固定残差向量相对内部参数坐标的有限差分: - // J[:,column] = (r_probe-r_base)/delta。 - for(int row = 0; row < residualCount; ++row) { - jacobian[row][column] = - (probe.breakdown.residualVector[row] - - base.breakdown.residualVector[row]) / delta; - } - - // 有符号诊断量只有在基点和试算点都可靠时才能计算方向梯度; - // shapeLoss 无方向可靠性标志,始终记录其局部变化率。 - if(base.breakdown.verticalReliable && - probe.breakdown.verticalReliable && - !base.breakdown.registrationAmbiguous && - !probe.breakdown.registrationAmbiguous) { - verticalGradient[column] = - (probe.breakdown.verticalCommonBias - - base.breakdown.verticalCommonBias) / delta; - } - if(base.breakdown.horizontalReliable && - probe.breakdown.horizontalReliable && - !base.breakdown.registrationAmbiguous && - !probe.breakdown.registrationAmbiguous) { - horizontalGradient[column] = - (probe.breakdown.horizontalPhysicalShift - - base.breakdown.horizontalPhysicalShift) / delta; - } - shapeGradient[column] = - (probe.breakdown.shapeLoss - - base.breakdown.shapeLoss) / delta; - jacobianColumnValid[column] = true; - columnBuilt = true; - - if(probe.fitness < base.fitness && - (!bestProbe.valid || - probe.fitness < bestProbe.fitness)) { - bestProbe = probe; - bestProbeColumn = column; - bestProbeDelta = delta; - } - restoreEvaluationState(base); - } - } - - int validColumnCount = 0; - for(int i = 0; i < jacobianColumnValid.size(); ++i) { - if(jacobianColumnValid[i]) { - ++validColumnCount; - } - } - if(validColumnCount == 0 || m_shouldStop) { - restoreEvaluationState(base); - return false; - } - - // 灵敏度试算本身若找到更优真实解也应保留。所有列先基于同一个 base - // 建完,再用该已知割线把 Jacobian 平移到新工作点,避免边算边移动基点。 - if(bestProbe.valid && bestProbeColumn >= 0) { - QVector acceptedStep(dimensions, 0.0); - acceptedStep[bestProbeColumn] = bestProbeDelta; - updateTrustRegionJacobian( - &jacobian, - base.breakdown.residualVector, - bestProbe.breakdown.residualVector, - acceptedStep); - if(base.breakdown.verticalReliable && - bestProbe.breakdown.verticalReliable) { - updateTrustRegionScalarGradient( - &verticalGradient, - base.breakdown.verticalCommonBias, - bestProbe.breakdown.verticalCommonBias, - acceptedStep); - } - if(base.breakdown.horizontalReliable && - bestProbe.breakdown.horizontalReliable) { - updateTrustRegionScalarGradient( - &horizontalGradient, - base.breakdown.horizontalPhysicalShift, - bestProbe.breakdown.horizontalPhysicalShift, - acceptedStep); - } - updateTrustRegionScalarGradient( - &shapeGradient, - base.breakdown.shapeLoss, - bestProbe.breakdown.shapeLoss, - acceptedStep); - - current = bestProbe; - publishAcceptedPoint(current); - restoreEvaluationState(current); - m_convergenceHistory.append(current.fitness); - writeTraceRow(m_currentIteration, - bestProbeColumn, - "trust_region_sensitivity_accept", - current.parameters, - current.fitness, - true, - 0, - std::numeric_limits::quiet_NaN(), - "accepted_cached_probe", - current.parameters, - current.fitness, - ¤t.breakdown); - emit logMessageGenerated( - tr("Sensitivity probe accepted: error reduced to %1") - .arg(current.fitness, 0, 'e', 4)); - } else { - restoreEvaluationState(current); - } - - acceptedSinceRebuild = 0; - movementSinceRebuild = 0.0; - consecutiveRejectedSteps = 0; - rebuildRequested = false; - // 若重建过程中接受了试算点,当前模型已通过割线平移而不是在新点完整 - // 重算;再遇到最小半径停滞时仍允许做一次真正的新点重建。 - modelRebuiltAtMinimumRadius = - trustRadius <= minimumTrustRadius * 1.01 && - !bestProbe.valid; - emit logMessageGenerated( - tr("Sensitivity model rebuilt: %1/%2 parameter columns valid") - .arg(validColumnCount) - .arg(dimensions)); - return true; - }; - - int completedIterations = 0; - for(int iteration = 0; - iteration < m_maxIterations && - m_totalEvaluations < maximumEvaluations && - !m_shouldStop; - ++iteration) { - m_currentIteration = iteration; - completedIterations = iteration + 1; - - if(!processPauseAndStop()) { - break; - } - if(rebuildRequested) { - const bool confirmingStagnation = - stagnationConfirmationRequested; - if(!rebuildSensitivity()) { - stopReason = m_shouldStop - ? PSO_USER_STOPPED - : PSO_LOCAL_OPTIMUM; - break; - } - if(current.fitness < m_targetError) { - stopReason = PSO_TARGET_ACHIEVED; - break; - } - if(m_totalEvaluations >= maximumEvaluations) { - stopReason = PSO_MAX_ITERATIONS; - break; - } - const bool rebuildEffective = - registerEffectiveImprovement(current.fitness); - if(confirmingStagnation && !rebuildEffective) { - emit logMessageGenerated( - tr("Sensitivity rebuild produced no effective improvement; " - "local convergence detected")); - stopReason = PSO_LOCAL_OPTIMUM; - break; - } - } - - // 先确定当前最突出的可靠诊断误差,用其梯度回答“哪些参数最能改善 - // 当前问题”;实际 LM 方向仍由完整残差梯度和 Jacobian 共同计算。 - int dominantComponent = trustRegionDominantComponent( - current.breakdown, diagnosisThreshold); - const QVector* componentGradient = nullptr; - if(dominantComponent == TRUST_REGION_VERTICAL_COMPONENT) { - componentGradient = &verticalGradient; - } else if(dominantComponent == TRUST_REGION_HORIZONTAL_COMPONENT) { - componentGradient = &horizontalGradient; - } else if(dominantComponent == TRUST_REGION_SHAPE_COMPONENT) { - componentGradient = &shapeGradient; - } - - // 主目标采用 0.5*||r||^2,其对参数的梯度为 J^T*r。这里不再叠加 - // vertical/horizontal/shape,保证诊断分量不会改变真实接受目标。 - QVector totalGradient(dimensions, 0.0); - for(int column = 0; column < dimensions; ++column) { - if(!jacobianColumnValid[column]) { - continue; - } - for(int row = 0; row < jacobian.size(); ++row) { - totalGradient[column] += - jacobian[row][column] * - current.breakdown.residualVector[row]; - } - } - - // 每轮最多联合调整三个灵敏参数。按当前诊断梯度绝对值由大到小选取, - // 并剔除 Jacobian 响应过度共线的列,降低弱可辨识参数互相补偿的风险。 - QVector selectedColumns; - QVector alreadyConsidered(dimensions, false); - for(int selection = 0; selection < qMin(3, dimensions); ++selection) { - int bestColumn = -1; - double bestScore = 0.0; - for(int column = 0; column < dimensions; ++column) { - if(alreadyConsidered[column] || - !jacobianColumnValid[column]) { - continue; - } - - double score = componentGradient - ? qAbs((*componentGradient)[column]) - : qAbs(totalGradient[column]); - if(!isFiniteNumber(score) || score <= bestScore) { - continue; - } - - bool excessivelyCorrelated = false; - for(int selectedIndex = 0; - selectedIndex < selectedColumns.size(); - ++selectedIndex) { - if(trustRegionJacobianColumnCorrelation( - jacobian, - column, - selectedColumns[selectedIndex]) > - columnCorrelationLimit) { - excessivelyCorrelated = true; - break; - } - } - if(!excessivelyCorrelated) { - bestColumn = column; - bestScore = score; - } - } - if(bestColumn < 0 || bestScore <= 1.0e-12) { - break; - } - selectedColumns.append(bestColumn); - alreadyConsidered[bestColumn] = true; - } - - // 诊断梯度接近零时,说明该分量在当前局部无法可靠选参,退回完整残差 - // 梯度,但接受标准仍然只有真实 total,诊断值不会重复计入目标函数。 - if(selectedColumns.isEmpty() && componentGradient) { - dominantComponent = TRUST_REGION_TOTAL_COMPONENT; - componentGradient = nullptr; - alreadyConsidered.fill(false, dimensions); - for(int selection = 0; - selection < qMin(3, dimensions); - ++selection) { - int bestColumn = -1; - double bestScore = 0.0; - for(int column = 0; column < dimensions; ++column) { - if(alreadyConsidered[column] || - !jacobianColumnValid[column]) { - continue; - } - double score = qAbs(totalGradient[column]); - if(score <= bestScore) { - continue; - } - bool excessivelyCorrelated = false; - for(int selectedIndex = 0; - selectedIndex < selectedColumns.size(); - ++selectedIndex) { - if(trustRegionJacobianColumnCorrelation( - jacobian, - column, - selectedColumns[selectedIndex]) > - columnCorrelationLimit) { - excessivelyCorrelated = true; - break; - } - } - if(!excessivelyCorrelated) { - bestColumn = column; - bestScore = score; - } - } - if(bestColumn < 0 || bestScore <= 1.0e-12) { - break; - } - selectedColumns.append(bestColumn); - alreadyConsidered[bestColumn] = true; - } - } - - // 当前局部没有可用方向时先缩小半径并重建灵敏度;只有已经在最小 - // 半径完整重建后仍无方向,才把它判定为局部最优。 - if(selectedColumns.isEmpty()) { - if(trustRadius <= minimumTrustRadius * 1.01 && - modelRebuiltAtMinimumRadius) { - stopReason = PSO_LOCAL_OPTIMUM; - break; - } - trustRadius = qMax(minimumTrustRadius, trustRadius * 0.5); - damping = qMin(1.0e8, damping * 4.0); - rebuildRequested = true; - if(recordIneffectiveStep()) { - stopReason = PSO_LOCAL_OPTIMUM; - break; - } - continue; - } - - // 在选中参数子空间构造 LM 正规方程: - // (J^T*J + damping*diag(J^T*J))*step = -J^T*r。 - // 对角缩放使不同参数列的灵敏度量级差异不会直接改变阻尼强弱。 - const int selectedCount = selectedColumns.size(); - QVector > normalMatrix( - selectedCount, QVector(selectedCount, 0.0)); - QVector rightHandSide(selectedCount, 0.0); - for(int left = 0; left < selectedCount; ++left) { - int leftColumn = selectedColumns[left]; - rightHandSide[left] = -totalGradient[leftColumn]; - for(int right = 0; right < selectedCount; ++right) { - int rightColumn = selectedColumns[right]; - for(int row = 0; row < jacobian.size(); ++row) { - normalMatrix[left][right] += - jacobian[row][leftColumn] * - jacobian[row][rightColumn]; - } - } - double diagonalScale = qMax( - 1.0e-10, normalMatrix[left][left]); - normalMatrix[left][left] += damping * diagonalScale; - } - - QVector selectedStep; - bool solved = solveTrustRegionLinearSystem( - normalMatrix, rightHandSide, &selectedStep); - QVector coordinateStep(dimensions, 0.0); - if(solved) { - for(int i = 0; i < selectedCount; ++i) { - coordinateStep[selectedColumns[i]] = selectedStep[i]; - } - } - - double stepNorm = qSqrt(trustRegionSquaredNorm(coordinateStep)); - if(!solved || !isFiniteNumber(stepNorm) || - stepNorm < minimumCoordinateStep) { - // 正规方程退化时使用投影最速下降方向,仍只移动本轮已选择的参数。 - coordinateStep.fill(0.0, dimensions); - double gradientNormSquared = 0.0; - for(int i = 0; i < selectedCount; ++i) { - int column = selectedColumns[i]; - double stepDirection = -totalGradient[column]; - if((current.coordinates[column] <= minimumCoordinateStep && - stepDirection < 0.0) || - (current.coordinates[column] >= - 1.0 - minimumCoordinateStep && - stepDirection > 0.0)) { - stepDirection = 0.0; - } - coordinateStep[column] = stepDirection; - gradientNormSquared += stepDirection * stepDirection; - } - double gradientNorm = qSqrt(gradientNormSquared); - if(gradientNorm > minimumCoordinateStep) { - double scale = trustRadius / gradientNorm; - for(int i = 0; i < selectedCount; ++i) { - int column = selectedColumns[i]; - coordinateStep[column] *= scale; - } - } - stepNorm = qSqrt(trustRegionSquaredNorm(coordinateStep)); - } - - // LM 解只给出局部模型建议方向;若超出当前信赖半径,保持方向不变并 - // 等比例截短,避免一次试算离开 Jacobian 有效的局部区域。 - if(stepNorm > trustRadius && stepNorm > 0.0) { - double scale = trustRadius / stepNorm; - for(int i = 0; i < coordinateStep.size(); ++i) { - coordinateStep[i] *= scale; - } - } - - // 将 LM 步长投影到用户给定的参数范围,实际用于预测下降的也是投影后步长。 - QVector candidateCoordinates = current.coordinates; - for(int i = 0; i < dimensions; ++i) { - candidateCoordinates[i] = qBound( - 0.0, - current.coordinates[i] + coordinateStep[i], - 1.0); - coordinateStep[i] = candidateCoordinates[i] - - current.coordinates[i]; - } - stepNorm = qSqrt(trustRegionSquaredNorm(coordinateStep)); - - // 用线性模型 r_new ~= r_current + J*step 预测残差,再用平方能量 - // 的下降量与真实候选下降量比较,作为调整阻尼和半径的依据。 - QVector predictedResidual = - current.breakdown.residualVector; - for(int row = 0; row < jacobian.size(); ++row) { - for(int column = 0; column < dimensions; ++column) { - predictedResidual[row] += - jacobian[row][column] * coordinateStep[column]; - } - } - double predictedReduction = 0.5 * - (trustRegionSquaredNorm(current.breakdown.residualVector) - - trustRegionSquaredNorm(predictedResidual)); - - // 无实际移动或模型预测不下降时没有必要调用昂贵求解器。将它按一次 - // 拒绝处理,并在连续发生后重建灵敏度,防止继续沿失效模型试算。 - if(stepNorm < minimumCoordinateStep || - !isFiniteNumber(predictedReduction) || - predictedReduction <= 1.0e-14) { - trustRadius = qMax(minimumTrustRadius, trustRadius * 0.5); - damping = qMin(1.0e8, damping * 4.0); - ++consecutiveRejectedSteps; - if(consecutiveRejectedSteps >= 2) { - if(trustRadius <= minimumTrustRadius * 1.01 && - modelRebuiltAtMinimumRadius) { - stopReason = PSO_LOCAL_OPTIMUM; - break; - } - rebuildRequested = true; - } - if(recordIneffectiveStep()) { - stopReason = PSO_LOCAL_OPTIMUM; - break; - } - continue; - } - - TrustRegionEvaluation candidate; - candidate.coordinates = candidateCoordinates; - candidate.parameters = parametersFromCoordinates(candidate.coordinates); - candidate.valid = evaluateTrustRegionPoint( - candidate.parameters, - &candidate.fitness, - &candidate.breakdown, - &candidate.curve, - &candidate.elapsedMs); - - if(!candidate.valid) { - // 求解失败的候选不能改变 current。先完整恢复上一个已接受参数和 - // 对应误差快照,再缩小信赖域;连续失败达到上限才终止整个拟合。 - ++consecutiveSolverFailures; - ++consecutiveRejectedSteps; - trustRadius = qMax(minimumTrustRadius, trustRadius * 0.5); - damping = qMin(1.0e8, damping * 4.0); - writeTraceRow(m_currentIteration, - -1, - "trust_region_candidate", - candidate.parameters, - candidate.fitness, - false, - candidate.elapsedMs, - std::numeric_limits::quiet_NaN(), - "solver_invalid", - current.parameters, - current.fitness, - nullptr); - restoreEvaluationState(current); - if(consecutiveRejectedSteps >= 2) { - rebuildRequested = true; - } - if(consecutiveSolverFailures >= m_maxConsecutiveFailures) { - stopReason = PSO_CONSECUTIVE_FAILURES; - break; - } - if(recordIneffectiveStep()) { - stopReason = PSO_LOCAL_OPTIMUM; - break; - } - continue; - } - - consecutiveSolverFailures = 0; - // 有效候选即使最终被拒绝,也提供了一条真实割线,可用于修正下一轮 - // 局部模型;是否成为新工作点仍只由下面的 total 严格比较决定。 - const AutoFitObjectiveBreakdown oldBreakdown = current.breakdown; - updateTrustRegionJacobian( - &jacobian, - oldBreakdown.residualVector, - candidate.breakdown.residualVector, - coordinateStep); - if(oldBreakdown.verticalReliable && - candidate.breakdown.verticalReliable && - !oldBreakdown.registrationAmbiguous && - !candidate.breakdown.registrationAmbiguous) { - updateTrustRegionScalarGradient( - &verticalGradient, - oldBreakdown.verticalCommonBias, - candidate.breakdown.verticalCommonBias, - coordinateStep); - } - if(oldBreakdown.horizontalReliable && - candidate.breakdown.horizontalReliable && - !oldBreakdown.registrationAmbiguous && - !candidate.breakdown.registrationAmbiguous) { - updateTrustRegionScalarGradient( - &horizontalGradient, - oldBreakdown.horizontalPhysicalShift, - candidate.breakdown.horizontalPhysicalShift, - coordinateStep); - } - updateTrustRegionScalarGradient( - &shapeGradient, - oldBreakdown.shapeLoss, - candidate.breakdown.shapeLoss, - coordinateStep); - - // reductionRatio 衡量局部线性模型的可信度:接近 1 表示预测准确; - // 值较小表示虽然可能下降,但模型低估了非线性,需要收紧下一步。 - double actualReduction = 0.5 * - (current.fitness * current.fitness - - candidate.fitness * candidate.fitness); - double reductionRatio = actualReduction / predictedReduction; - bool accepted = candidate.fitness < current.fitness; - QString componentName = trustRegionComponentName(dominantComponent); - - if(accepted) { - // 真实总误差下降后才正式替换 current,并同步发布参数、曲线和诊断。 - // 模型预测可靠时减小阻尼并可扩大半径,预测较差时保守收缩。 - current = candidate; - publishAcceptedPoint(current); - restoreEvaluationState(current); - ++acceptedSinceRebuild; - movementSinceRebuild += stepNorm; - consecutiveRejectedSteps = 0; - m_convergenceHistory.append(current.fitness); - - if(reductionRatio > 0.75) { - damping = qMax(1.0e-8, damping * 0.5); - if(stepNorm >= trustRadius * 0.8) { - trustRadius = qMin( - maximumTrustRadius, trustRadius * 1.6); - } - } else if(reductionRatio > 0.25) { - damping = qMax(1.0e-8, damping * 0.8); - } else { - damping = qMin(1.0e8, damping * 2.0); - trustRadius = qMax( - minimumTrustRadius, trustRadius * 0.75); - } - - if(acceptedSinceRebuild >= 6 || - movementSinceRebuild >= 0.30) { - rebuildRequested = true; - } - modelRebuiltAtMinimumRadius = false; - } else { - // 拒绝时 candidate 只保留在 trace 中,DataManager 和内存状态都恢复 - // 到 current。连续拒绝说明割线模型可能失真,因此请求重新试算灵敏度。 - ++consecutiveRejectedSteps; - damping = qMin(1.0e8, damping * 4.0); - trustRadius = qMax(minimumTrustRadius, trustRadius * 0.5); - restoreEvaluationState(current); - if(consecutiveRejectedSteps >= 2) { - rebuildRequested = true; - } - } - - // 候选只要更优就继续作为 current 保存;是否足以解除停滞,则统一 - // 相对上一次有效改善基准判断。拒绝和微小改善都会累计无效次数。 - const bool effectiveImprovement = - registerEffectiveImprovement(current.fitness); - if(!effectiveImprovement && recordIneffectiveStep()) { - stopReason = PSO_LOCAL_OPTIMUM; - } - - writeTraceRow(m_currentIteration, - -1, - "trust_region_candidate", - candidate.parameters, - candidate.fitness, - true, - candidate.elapsedMs, - std::numeric_limits::quiet_NaN(), - accepted - ? QString("accepted_%1").arg(componentName) - : QString("rejected_%1").arg(componentName), - current.parameters, - current.fitness, - &candidate.breakdown); - - emit logMessageGenerated( - tr("Iteration %1: focus=%2, parameters=%3, error=%4, result=%5") - .arg(iteration + 1) - .arg(componentName) - .arg(selectedColumns.size()) - .arg(candidate.fitness, 0, 'e', 4) - .arg(accepted ? tr("accepted") : tr("rejected"))); - emit progressUpdated(iteration + 1, m_globalBestFitness); - - if(stopReason == PSO_LOCAL_OPTIMUM) { - break; - } - - if(current.fitness < m_targetError) { - stopReason = PSO_TARGET_ACHIEVED; - break; - } - if(trustRadius <= minimumTrustRadius * 1.01 && - consecutiveRejectedSteps >= 2) { - if(modelRebuiltAtMinimumRadius) { - stopReason = PSO_LOCAL_OPTIMUM; - break; - } - rebuildRequested = true; - } - } - - if(completedIterations > 0) { - m_currentIteration = completedIterations - 1; - } - restoreEvaluationState(current); - - if(m_shouldStop) { - return PSO_USER_STOPPED; - } - if(current.fitness < m_targetError) { - return PSO_TARGET_ACHIEVED; - } - if(stopReason == PSO_CONSECUTIVE_FAILURES || - stopReason == PSO_LOCAL_OPTIMUM || - stopReason == PSO_OPTIMIZATION_FAILED) { - return stopReason; - } - return PSO_MAX_ITERATIONS; -} - -void nmCalculationAutoFitPSO::updateParticle(int particleIndex) -{ - if(particleIndex < 0 || particleIndex >= m_swarm.size()) return; - - AutoFitParticle& particle = m_swarm[particleIndex]; - - // 单粒子真实评价入口。 - // 这里调用 evaluateFitness(),因此会真实写 DataManager、调用求解器、计算误差。 - // 被代理模型筛掉的粒子不会进入这个函数。 - particle.evaluatedThisIteration = false; - particle.lastEvaluationSuccess = false; - particle.lastEvaluationElapsedMs = -1; - particle.pbestRelativeImprovementThisIteration = 0.0; - particle.selectedForSolver = true; - - if(particle.screeningDecision.isEmpty()) { - particle.screeningDecision = "full_solver"; - } - - bool reuseInitialSolution = m_currentIteration == 0 && - particleIndex == 0 && - m_hasValidUserSolution && - m_userInitialFitness < 1e9 && - particle.position.size() == m_userInitialSolution.size(); - - for(int i = 0; reuseInitialSolution && i < particle.position.size(); ++i) { - double tolerance = qMax(1.0e-12, qAbs(m_userInitialSolution[i]) * 1.0e-12); - reuseInitialSolution = qAbs(particle.position[i] - m_userInitialSolution[i]) <= tolerance; - } - - if(reuseInitialSolution) { - // 初始解在进入粒子群前已经真实求解过。第一代第0号粒子位置完全相同, - // 直接复用真实误差和曲线,避免一次重复 DLL 调用且不改变 PSO 数学状态。 - particle.fitness = m_userInitialFitness; - particle.currentLogLogData = m_userInitialLogLogData; - particle.currentObjectiveBreakdown = m_userInitialObjectiveBreakdown; - particle.lastEvaluationElapsedMs = 0; - particle.evaluatedThisIteration = true; - particle.lastEvaluationSuccess = true; - particle.screeningDecision = "initial_solution_cache"; - emit logMessageGenerated(tr("Particle 1 reused the verified initial solution")); - } else { - QTime evalTimer; - evalTimer.start(); - particle.fitness = evaluateFitness(particle.position); - particle.currentLogLogData = m_lastEvaluatedLogLogData; - particle.currentObjectiveBreakdown = m_lastObjectiveBreakdown; - particle.lastEvaluationElapsedMs = evalTimer.elapsed(); - particle.evaluatedThisIteration = true; - particle.lastEvaluationSuccess = (particle.fitness < 1e9); - m_totalEvaluations++; - - if(particle.fitness < 1e9) { - m_successfulEvaluations++; - } - } - - // 更新个体最优 pbest。这里使用的是真实求解器误差 particle.fitness, - // 不是代理模型给出的 surrogateObjective。 - double previousBestFitness = particle.bestFitness; - - if(particle.fitness < previousBestFitness) { - particle.pbestRelativeImprovementThisIteration = previousBestFitness >= 1e9 - ? 1.0 - : (previousBestFitness - particle.fitness) / - qMax(1e-10, qAbs(previousBestFitness)); - - // 如果当前位置尚未成为新的真实全局最优,而上一代存在尚未真实验证、且代理 - // 仍判断更优的 guide,就保留它继续引导速度;真实 pbest 仍照常更新。 - bool preserveSurrogateGuide = false; - - bool currentBeatsGlobalBest = particle.fitness < m_globalBestFitness; - - if(!currentBeatsGlobalBest && - isSurrogateScreeningEnabled() && - particle.guideBestFromSurrogate && - isFiniteNumber(particle.guideBestObjective) && - isFiniteNumber(particle.surrogateObjective)) { - double requiredImprovement = qMax(1.0e-10, - qAbs(particle.guideBestObjective) * - kSurrogateGuidePbestMinRelativeImprovement); - preserveSurrogateGuide = particle.surrogateObjective - - particle.guideBestObjective > requiredImprovement; - } - - particle.bestFitness = particle.fitness; - particle.bestPosition = particle.position; - particle.bestLogLogData = particle.currentLogLogData; - particle.bestObjectiveBreakdown = particle.currentObjectiveBreakdown; - - if(!preserveSurrogateGuide) { - particle.guideBestPosition = particle.position; - particle.guideBestObjective = isFiniteNumber(particle.surrogateObjective) - ? particle.surrogateObjective - : particle.fitness; - particle.guideBestFromSurrogate = false; - } - - DEBUG_OUT(QString("Particle %1 improved: error = %2") - .arg(particleIndex).arg(particle.fitness, 0, 'e', 4)); - } -} - -void nmCalculationAutoFitPSO::updateGlobalBest() -{ - // 保存上一轮的全局最优,用于后续自适应参数调整等 - m_previousBestFitness = m_globalBestFitness; - bool globalBestUpdated = false; - - // 遍历所有粒子,寻找比当前 global best 更好的个体最优。 - // 注意:particle.bestFitness 只有在真实求解器评价成功后才会更新。 - for(int i = 0; i < m_swarm.size(); ++i) { - const AutoFitParticle& particle = m_swarm[i]; - - // 只要个体最优比当前全局最优小,就认为是更好的解 - if(particle.bestFitness < m_globalBestFitness) { - - double improvement = m_globalBestFitness - particle.bestFitness; - double relativeImprovement = - improvement / qMax(1e-10, qAbs(m_globalBestFitness)); - - // 区分显著改进和微小改进,但无论如何都会更新全局最优 - if(relativeImprovement > m_improvementThreshold) { - DEBUG_OUT(QString(tr("Global best updated with %1% improvement: %2")) - .arg(relativeImprovement * 100.0, 0, 'f', 3) - .arg(particle.bestFitness, 0, 'e', 4)); - } else { - DEBUG_OUT(QString(tr("Global best updated (minor improvement %1% < %2%) to %3")) - .arg(relativeImprovement * 100.0, 0, 'f', 3) - .arg(m_improvementThreshold * 100.0, 0, 'f', 2) - .arg(particle.bestFitness, 0, 'e', 4)); - } - - // 无论相对改进是否超过阈值,都要更新全局最优 - m_globalBestFitness = particle.bestFitness; - m_globalBestPosition = particle.bestPosition; - m_globalBestLogLogData = particle.bestLogLogData; - m_globalBestObjectiveBreakdown = particle.bestObjectiveBreakdown; - globalBestUpdated = true; - emit bestCurveUpdated(m_targetLogLogData, m_globalBestLogLogData, m_currentIteration + 1, m_globalBestFitness); - } - } - - // 只有在本轮没有找到任何更好的解时才考虑恢复用户初始解 - if(!globalBestUpdated && m_hasValidUserSolution && m_userInitialFitness < m_globalBestFitness) { - - double improvement = m_globalBestFitness - m_userInitialFitness; - double relativeImprovement = - improvement / qMax(1e-10, qAbs(m_globalBestFitness)); + double improvement = m_globalBestFitness - m_userInitialFitness; + double relativeImprovement = + improvement / qMax(1e-10, qAbs(m_globalBestFitness)); DEBUG_OUT("Elite protection: Restoring user initial solution as global best"); DEBUG_OUT(QString("Elite solution is better than current best by %1%") @@ -5298,7 +3711,6 @@ void nmCalculationAutoFitPSO::updateGlobalBest() m_globalBestFitness = m_userInitialFitness; m_globalBestPosition = m_userInitialSolution; m_globalBestLogLogData = m_userInitialLogLogData; - m_globalBestObjectiveBreakdown = m_userInitialObjectiveBreakdown; emit bestCurveUpdated(m_targetLogLogData, m_globalBestLogLogData, m_currentIteration + 1, m_globalBestFitness); } } @@ -5375,7 +3787,6 @@ void nmCalculationAutoFitPSO::updateVelocityAndPosition() int paramIndex = m_enabledParamIndices[j]; double range = m_parameterUpper[paramIndex] - m_parameterLower[paramIndex]; double maxVel = range * VELOCITY_LIMIT_FACTOR; - particle.velocity[j] = qMax(-maxVel, qMin(maxVel, particle.velocity[j])); // 更新位置 @@ -5550,7 +3961,7 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter // 1. 校验粒子参数是否在用户设置的上下界和基本物理范围内; // 2. 将参数写入 DataManager 的储层/目标井对象; // 3. 调用真实数值求解器,生成模拟结果; - // 4. 从本次求解任务读取目标井 result log-log 曲线; + // 4. 从目标井读取模拟后的 result log-log 曲线; // 5. 与目标 history log-log 曲线计算误差,误差越小代表拟合越好。 // // 返回 1e10 表示该粒子评价失败或结果不可用。PSO 会把它当成很差的解。 @@ -5558,7 +3969,6 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter static int callCount = 0; callCount++; m_lastEvaluatedLogLogData.clear(); - m_lastObjectiveBreakdown = AutoFitObjectiveBreakdown(); try { DEBUG_OUT(QString("%1: Call #%2 - Starting evaluation with %3 parameters") @@ -5656,20 +4066,6 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter return 1e10; } - // Dfc 和裂缝半长都通过 PEBI 裂缝数组传入,不属于每次求解都会重新组装的 Base/CS 参数。 - // 勾选任一裂缝参数时刷新网格输出,保证本次真实试算使用新的导流能力和端点坐标。 - const bool fractureGridParameterSelected = - (m_parameterSelected.size() > 8 && m_parameterSelected[8]) || - (m_parameterSelected.size() > 9 && m_parameterSelected[9]); - if(fractureGridParameterSelected) { - nmCalculationPebiGrid* pebiGrid = nmCalculationPebiGrid::getInstance(); - if(!pebiGrid || !pebiGrid->generateOutputPara()) { - DEBUG_OUT(QString("%1: Call #%2 - Failed to refresh PEBI fracture parameters") - .arg(funcName).arg(callCount)); - return 1e10; - } - } - // 4. 运行求解器。真实求解器偶发失败时允许重试,避免一次 DLL 调用异常 // 直接让整个粒子评价失败。 QVector> solverResult; @@ -5726,11 +4122,24 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter return 1e10; } - // 5. 获取 LogLog 数据。runSolverDll() 直接从求解任务复制目标井曲线, - // 不再依赖 DataManager 中可能被其它井或上一粒子改写的共享结果。 - QVector> resultLogLogData = m_lastEvaluatedLogLogData; + // 5. 获取 LogLog 数据。runSolver() 会更新 DataManager 中目标井的计算结果, + // 这里再从目标井读取 resultLogLogData 作为模拟曲线。 + QVector> resultLogLogData; try { + nmDataWellBase* pTargetWell = dataManager->findWellByName(m_targetWellName); + + if(!pTargetWell) { + DEBUG_OUT(QString("%1: Call #%2 - Target well '%3' NOT FOUND") + .arg(funcName).arg(callCount).arg(m_targetWellName)); + return 1e10; + } + + DEBUG_OUT(QString("%1: Call #%2 - Target well found: %3") + .arg(funcName).arg(callCount).arg(m_targetWellName)); + + resultLogLogData = pTargetWell->getResultLogLog(); + if(!validateLogLogData(resultLogLogData)) { DEBUG_OUT(QString("%1: Call #%2 - LogLog data VALIDATION FAILED") .arg(funcName).arg(callCount)); @@ -5832,7 +4241,7 @@ void nmCalculationAutoFitPSO::updateReservoirParameters(const QVector& p nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); nmDataReservoir reservoirData = dataManager->getReservoirDataCopy(); - // paramIndex 是粒子 position 中的索引;i 是完整 10 个参数体系中的索引。 + // paramIndex 是粒子 position 中的索引;i 是完整 8 个参数体系中的索引。 // 只有 m_parameterSelected[i] 为 true 时,才从 parameters 中消费一个值。 int paramIndex = 0; @@ -5878,8 +4287,7 @@ void nmCalculationAutoFitPSO::updateWellParameters(const QVector& parame { // 更新目标井上的拟合参数。目前井级可拟合参数主要是: // - skin:写入第一个 perforation; - // - wellboreC:写入井筒储集系数; - // - Dfc/裂缝半长:只写入垂直压裂井或多段压裂水平井。 + // - wellboreC:写入井筒储集系数。 // 如果目标井不存在或没有射孔数据,这里只记录 debug,不抛异常。 nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); @@ -5915,45 +4323,6 @@ void nmCalculationAutoFitPSO::updateWellParameters(const QVector& parame pWell->setWellboreStorage(wellboreAttr); } break; - - case 8: { // 裂缝导流能力 - if(pWell->getWellType() == NM_WELL_MODEL::Vertical_Fractured_Well) { - nmDataVerticalFracturedWell* fracturedWell = - dynamic_cast(pWell); - if(fracturedWell) { - nmDataAttribute dfc = fracturedWell->getDfc(); - dfc.setValue(value); - fracturedWell->setDfc(dfc); - } - } else if(pWell->getWellType() == NM_WELL_MODEL::Horizontal_Fractured_Well) { - nmDataHorizontalFracturedWell* fracturedWell = - dynamic_cast(pWell); - if(fracturedWell) { - nmDataAttribute dfc = fracturedWell->getDfc(); - dfc.setValue(value); - fracturedWell->setDfc(dfc); - } - } - } - break; - - case 9: { // 裂缝半长 - // 直接修改井对象中的属性,复用已有信号重算裂缝端点。 - if(pWell->getWellType() == NM_WELL_MODEL::Vertical_Fractured_Well) { - nmDataVerticalFracturedWell* fracturedWell = - dynamic_cast(pWell); - if(fracturedWell) { - fracturedWell->getFractureHalfLength().setValue(value); - } - } else if(pWell->getWellType() == NM_WELL_MODEL::Horizontal_Fractured_Well) { - nmDataHorizontalFracturedWell* fracturedWell = - dynamic_cast(pWell); - if(fracturedWell) { - fracturedWell->getFractureHalfLength().setValue(value); - } - } - } - break; } paramIndex++; @@ -6239,8 +4608,10 @@ void nmCalculationAutoFitPSO::saveOptimizationResult() void nmCalculationAutoFitPSO::validateAndProtectFinalResult() { - // 最终精英保护只阻止无效结果或真正变差的结果。任何真实误差下降都应保留, - // 不能再用固定百分比门槛把已经找到的更优解恢复成初始值。 + // 最终精英保护。 + // PSO 是随机启发式算法,某些工况下可能没有找到比用户初始模型更好的结果。 + // 这里用初始解误差与最终全局最优误差做比较,如果改进不足,就恢复初始解, + // 避免“自动拟合”把已有模型调坏。 if(!m_hasValidUserSolution) { emit logMessageGenerated(tr("No initial solution for elite protection")); return; @@ -6255,36 +4626,28 @@ void nmCalculationAutoFitPSO::validateAndProtectFinalResult() emit logMessageGenerated(tr("Comparing results: Initial=%1, Final=%2") .arg(initialFitness, 0, 'e', 4).arg(finalFitness, 0, 'e', 4)); - bool finalValid = isFiniteNumber(finalFitness) && - finalFitness < 1.0e9 && - m_globalBestPosition.size() == - m_userInitialSolution.size() && - !m_globalBestLogLogData.isEmpty() && - m_globalBestObjectiveBreakdown.valid; - if(finalValid) { - double improvement = initialFitness - finalFitness; - double relativeImprovement = - improvement / qMax(1.0e-10, qAbs(initialFitness)); - emit logMessageGenerated(tr("Improvement: %1 (%2%)") - .arg(improvement, 0, 'e', 4) - .arg(relativeImprovement * 100, 0, 'f', 2)); - } + // 计算改进程度 + double improvement = initialFitness - finalFitness; + double relativeImprovement = improvement / qMax(1e-10, qAbs(initialFitness)); - if(!finalValid || finalFitness > initialFitness) { - emit logMessageGenerated( - tr("Elite protection triggered: final result is invalid or worse than initial")); + emit logMessageGenerated(tr("Improvement: %1 (%2%)") + .arg(improvement, 0, 'e', 4).arg(relativeImprovement * 100, 0, 'f', 2)); + + if(relativeImprovement < m_improvementThreshold) { + emit logMessageGenerated(tr("Elite protection triggered: insufficient improvement")); + emit logMessageGenerated(tr("Threshold: %1%, Actual: %2%") + .arg(m_improvementThreshold * 100, 0, 'f', 2) + .arg(relativeImprovement * 100, 0, 'f', 4)); emit logMessageGenerated(tr("Restoring initial solution as final result")); m_globalBestFitness = initialFitness; m_globalBestPosition = m_userInitialSolution; m_globalBestLogLogData = m_userInitialLogLogData; - m_globalBestObjectiveBreakdown = m_userInitialObjectiveBreakdown; emit bestCurveUpdated(m_targetLogLogData, m_globalBestLogLogData, m_currentIteration + 1, m_globalBestFitness); emit logMessageGenerated(tr("Initial solution restored successfully")); } else { - emit logMessageGenerated( - tr("Final result validated - solution is not worse than initial")); + emit logMessageGenerated(tr("Final result validated - significant improvement achieved")); } } @@ -6327,8 +4690,9 @@ int nmCalculationAutoFitPSO::getEnabledParameterCount() const bool nmCalculationAutoFitPSO::validateParameters(const QVector& parameters) const { - // 参数物理范围已由拟合窗口统一校验;候选评价只检查维度、有限数和 - // 用户设置的上下界,避免另一套硬编码阈值与实际搜索范围冲突。 + // 参数合法性检查分两层: + // 1. 与用户界面设置一致:维度、有限数、上下界; + // 2. 求解器保护:拦截会导致数值崩溃或明显无物理意义的极端值。 if(parameters.size() != getEnabledParameterCount()) { return false; } @@ -6351,6 +4715,46 @@ bool nmCalculationAutoFitPSO::validateParameters(const QVector& paramete } } + // 直接拦截会导致求解器数值崩溃的参数值 + for(int i = 0; i < parameters.size() && i < m_enabledParamIndices.size(); ++i) { + int paramIndex = m_enabledParamIndices[i]; + double value = parameters[i]; + + switch(paramIndex) { + case 0: // 渗透率:必须大于零 + if(value <= 1e-8) { + DEBUG_OUT(QString("Rejecting near-zero permeability: %1").arg(value)); + return false; + } + + break; + + case 2: // 井筒储集系数:必须大于零 + if(value <= 1e-10) { + DEBUG_OUT(QString("Rejecting near-zero wellbore storage: %1").arg(value)); + return false; + } + + break; + + case 3: // 孔隙度:必须在合理范围 + if(value <= 1e-6 || value >= 0.99) { + DEBUG_OUT(QString("Rejecting unrealistic porosity: %1").arg(value)); + return false; + } + + break; + + case 5: // 综合压缩系数:必须大于零 + if(value <= 1e-8) { + DEBUG_OUT(QString("Rejecting near-zero total compressibility: %1").arg(value)); + return false; + } + + break; + } + } + return true; } @@ -6551,879 +4955,170 @@ double nmCalculationAutoFitPSO::calculateLogLogCurveError( const QVector >& target, const QVector >& result) const { - // 主目标只比较固定网格上的压力和导数残差;上下、左右和形状只负责诊断 - // 误差来源和选择参数,避免同一残差在 total 中被重复计算。整个计算过程均 - // 位于 log(time)-log(value) 坐标,因此得到的是相对尺度偏差而非原始压力量纲。 - const double invalidLoss = 1.0e10; - const double valueFloor = 1.0e-12; - const double minimumCoverage = 0.95; - const int numPoints = 80; - m_lastObjectiveBreakdown = AutoFitObjectiveBreakdown(); - + // 双对数曲线误差计算。 + // + // target 通常来自目标井历史曲线,result 来自当前粒子参数下的模拟曲线。 + // 两条曲线的时间点往往不完全一致,所以这里先取两者时间范围的重叠区间, + // 再在公共时间网格上插值对齐,最后分别计算压力曲线和压力导数曲线误差。 + // + // 返回值越小表示拟合越好;返回 1e10 表示曲线无效或无法比较。 + // 验证数据 if(!validateLogLogData(target) || !validateLogLogData(result)) { - return invalidLoss; + return 1e10; } try { - // 无法比较的采样行先跳过;有限但非正的导数无法进入双对数空间, - // 当前数据又没有逐点有效掩码,因此遇到这种导数时判本次评价无效。 - auto prepareCurve = [valueFloor](const QVector >& data, - int firstIndex, - QVector* pressure, - QVector* derivative) -> bool { - if(!pressure || !derivative || data.size() < 3 || - data[0].size() != data[1].size() || - data[0].size() != data[2].size() || - firstIndex < 0 || firstIndex >= data[0].size()) { - return false; - } + // 数据对齐:找到目标曲线与模拟曲线 time 轴的重叠区域。 + // 不在重叠区域内的点不参与误差,避免外推导致误差失真。 + double targetMinX = target[0][0]; + double targetMaxX = target[0][0]; - for(int i = firstIndex; i < data[0].size(); ++i) { - if(!isFiniteNumber(data[0][i]) || - !isFiniteNumber(data[1][i]) || - !isFiniteNumber(data[2][i]) || - data[0][i] <= 0.0 || - data[1][i] <= 0.0) { - continue; - } - if(data[2][i] <= 0.0) { - return false; - } + for(int i = 1; i < target[0].size(); ++i) { + if(target[0][i] < targetMinX) targetMinX = target[0][i]; - pressure->append(QPointF(data[0][i], data[1][i])); - derivative->append( - QPointF(data[0][i], qMax(data[2][i], valueFloor))); - } + if(target[0][i] > targetMaxX) targetMaxX = target[0][i]; + } - // 求解器输出可能不是严格升序,且同一时刻可能出现重复记录。 - // 插值前统一排序并让后出现的记录覆盖同时间旧值,保证横坐标严格递增。 - auto sortAndUnique = [](QVector* curve) { - std::stable_sort( - curve->begin(), curve->end(), - [](const QPointF& left, const QPointF& right) { - return left.x() < right.x(); - }); - - QVector unique; - unique.reserve(curve->size()); - for(int i = 0; i < curve->size(); ++i) { - if(unique.isEmpty() || - curve->at(i).x() > unique.last().x()) { - unique.append(curve->at(i)); - } else { - unique[unique.size() - 1] = curve->at(i); - } - } - *curve = unique; - }; - - sortAndUnique(pressure); - sortAndUnique(derivative); - return pressure->size() >= 3 && derivative->size() >= 3; - }; - - QVector targetPressure; - QVector targetDerivative; - QVector resultPressure; - QVector resultDerivative; - // 模拟结果已跳过 DLL 首点,因此误差计算同步忽略目标曲线首点。 - if(!prepareCurve(target, 1, &targetPressure, &targetDerivative) || - !prepareCurve(result, 0, &resultPressure, &resultDerivative)) { - return invalidLoss; - } - - // 在双对数坐标中插值。二分定位用于后面的多次水平配准试算。 - auto interpolateLogValue = [valueFloor]( - const QVector& curve, - double x, - double* value) -> bool { - if(!value || curve.size() < 2 || x <= 0.0 || - x < curve.first().x() || x > curve.last().x()) { - return false; - } + double resultMinX = result[0][0]; + double resultMaxX = result[0][0]; - int low = 0; - int high = curve.size() - 1; - while(low < high) { - int middle = low + (high - low) / 2; - if(curve[middle].x() < x) { - low = middle + 1; - } else { - high = middle; - } - } + for(int i = 1; i < result[0].size(); ++i) { + if(result[0][i] < resultMinX) resultMinX = result[0][i]; - int right = qBound(1, low, curve.size() - 1); - int left = right - 1; - double leftLogX = qLn(curve[left].x()); - double rightLogX = qLn(curve[right].x()); - double denominator = rightLogX - leftLogX; - double leftLogY = - qLn(qMax(qAbs(curve[left].y()), valueFloor)); - double rightLogY = - qLn(qMax(qAbs(curve[right].y()), valueFloor)); - - if(qAbs(denominator) <= 1.0e-12) { - *value = leftLogY; - } else { - double ratio = (qLn(x) - leftLogX) / denominator; - *value = leftLogY + - ratio * (rightLogY - leftLogY); - } - return isFiniteNumber(*value); - }; - - // 覆盖率通过后若只缺少首尾少量点,用模拟曲线自身的端点斜率作短距离 - // 双对数外推。该外推只用于主损失的固定网格,不参与水平配准搜索。 - auto extrapolateEndpointLogValue = [valueFloor]( - const QVector& curve, - double x, - double* value) -> bool { - if(!value || curve.size() < 2 || x <= 0.0) { - return false; - } - int left = x < curve.first().x() - ? 0 - : curve.size() - 2; - int right = left + 1; - double leftLogX = qLn(curve[left].x()); - double rightLogX = qLn(curve[right].x()); - double denominator = rightLogX - leftLogX; - if(qAbs(denominator) <= 1.0e-12) { - return false; - } - double leftLogY = - qLn(qMax(qAbs(curve[left].y()), valueFloor)); - double rightLogY = - qLn(qMax(qAbs(curve[right].y()), valueFloor)); - double ratio = (qLn(x) - leftLogX) / denominator; - *value = leftLogY + ratio * (rightLogY - leftLogY); - return isFiniteNumber(*value); - }; - - const double targetMinX = targetPressure.first().x(); - const double targetMaxX = targetPressure.last().x(); - const double resultMinX = resultPressure.first().x(); - const double resultMaxX = resultPressure.last().x(); - if(targetMinX <= 0.0 || targetMaxX <= targetMinX || - resultMinX <= 0.0 || resultMaxX <= resultMinX) { - return invalidLoss; - } - - QVector commonX(numPoints); - QVector commonLogX(numPoints); - QVector targetLogPressure(numPoints); - QVector targetLogDerivative(numPoints); - const double targetLogMinX = qLn(targetMinX); - const double targetLogMaxX = qLn(targetMaxX); - - // 固定使用目标曲线的完整 log-time 网格,候选之间不会因采样点不同而失去可比性。 - for(int i = 0; i < numPoints; ++i) { - double logX = targetLogMinX + - static_cast(i) * - (targetLogMaxX - targetLogMinX) / - (numPoints - 1); - commonLogX[i] = logX; - // 首尾直接使用原始端点,避免 exp(log(t)) 的舍入误差越过严格插值边界。 - if(i == 0) { - commonX[i] = targetMinX; - } else if(i == numPoints - 1) { - commonX[i] = targetMaxX; - } else { - commonX[i] = qExp(logX); - } - - if(!interpolateLogValue( - targetPressure, commonX[i], - &targetLogPressure[i]) || - !interpolateLogValue( - targetDerivative, commonX[i], - &targetLogDerivative[i])) { - return invalidLoss; - } + if(result[0][i] > resultMaxX) resultMaxX = result[0][i]; } - QVector pressureResidual( - numPoints, std::numeric_limits::quiet_NaN()); - QVector derivativeResidual( - numPoints, std::numeric_limits::quiet_NaN()); - int firstSupported = -1; - int lastSupported = -1; - int supportedCount = 0; + double overlapMinX = qMax(targetMinX, resultMinX); + double overlapMaxX = qMin(targetMaxX, resultMaxX); - // 残差定义为“模拟减目标”:正值表示模拟曲线偏高,负值表示偏低。 - for(int i = 0; i < numPoints; ++i) { - if(commonX[i] < resultMinX || commonX[i] > resultMaxX) { - continue; - } + if(overlapMinX >= overlapMaxX) { + DEBUG_OUT("No overlap between target and result LogLog curves"); + return 1e10; + } - double resultLogPressure = 0.0; - double resultLogDerivative = 0.0; - if(!interpolateLogValue( - resultPressure, commonX[i], - &resultLogPressure) || - !interpolateLogValue( - resultDerivative, commonX[i], - &resultLogDerivative)) { - // 已位于结果时间范围内却无法插值说明数据存在内部断点,不能补线。 - return invalidLoss; - } + // 生成公共 X 网格进行插值。使用对数均匀网格,是为了给早期时间段 + // 更多分辨率;试井双对数曲线的早期形态通常对参数识别很敏感。 + QVector commonX; + int numPoints = 50; - pressureResidual[i] = - resultLogPressure - targetLogPressure[i]; - derivativeResidual[i] = - resultLogDerivative - targetLogDerivative[i]; - ++supportedCount; - if(firstSupported < 0) { - firstSupported = i; - } - lastSupported = i; - } - - // 覆盖率同时约束“有效点数量”和“连续时间跨度”。取两者较小值可避免 - // 点数很多但只集中在局部时段的候选被误认为覆盖充分。 - AutoFitObjectiveBreakdown breakdown; - breakdown.coverage = supportedCount > 0 - ? static_cast(supportedCount) / - numPoints - : 0.0; - if(firstSupported >= 0 && lastSupported >= firstSupported) { - double targetSpan = - qMax(1.0e-12, targetLogMaxX - targetLogMinX); - double coveredSpan = - commonLogX[lastSupported] - commonLogX[firstSupported]; - breakdown.coverage = qMin( - breakdown.coverage, - qMax(0.0, coveredSpan / targetSpan)); - } - - // 覆盖率只判断候选是否有效,不再加入固定惩罚,避免所有误差被整体抬高。 - if(breakdown.coverage < minimumCoverage) { - breakdown.total = invalidLoss; - m_lastObjectiveBreakdown = breakdown; - return invalidLoss; - } - - // 通过门槛后最多只缺少首尾少量目标点。按模拟曲线端点趋势补齐后, - // 每个候选仍在固定 80 点上计算均方根误差,不能靠少算难拟合端点获益。 - for(int i = 0; i < numPoints; ++i) { - if(isFiniteNumber(pressureResidual[i]) && - isFiniteNumber(derivativeResidual[i])) { - continue; - } - double resultLogPressure = 0.0; - double resultLogDerivative = 0.0; - if(!extrapolateEndpointLogValue( - resultPressure, commonX[i], - &resultLogPressure) || - !extrapolateEndpointLogValue( - resultDerivative, commonX[i], - &resultLogDerivative)) { - return invalidLoss; - } - pressureResidual[i] = - resultLogPressure - targetLogPressure[i]; - derivativeResidual[i] = - resultLogDerivative - targetLogDerivative[i]; - } - - // 在指定中心附近计算普通均方根误差。 - auto rmseAround = []( - const QVector& values, - int begin, - int end, - double center) -> double { - double sum = 0.0; - int count = 0; - int validBegin = qMax(0, begin); - int validEnd = - qMin(end, static_cast(values.size())); - - for(int i = validBegin; i < validEnd; ++i) { - if(!isFiniteNumber(values[i])) { + if(overlapMinX > 0 && overlapMaxX > 0) { + // 对数空间均匀分布 + double logMin = qLn(overlapMinX); + double logMax = qLn(overlapMaxX); + + for(int i = 0; i < numPoints; ++i) { + double logX = logMin + i * (logMax - logMin) / (numPoints - 1); + double x = qExp(logX); + + // 数值保护 + if(!isFiniteNumber(x) || x <= 0) { continue; } - double difference = values[i] - center; - sum += difference * difference; - ++count; + commonX.append(x); } - return count > 0 - ? qSqrt(sum / count) - : std::numeric_limits::quiet_NaN(); - }; - - auto rmse = [&rmseAround]( - const QVector& values, - int begin, - int end) -> double { - return rmseAround(values, begin, end, 0.0); - }; - - // 普通算术平均中心保留上下偏差的符号。 - auto meanCenterRange = []( - const QVector& values, - int begin, - int end) -> double { - int validBegin = qMax(0, begin); - int validEnd = - qMin(end, static_cast(values.size())); - double center = 0.0; - int count = 0; - - for(int i = validBegin; i < validEnd; ++i) { - if(isFiniteNumber(values[i])) { - center += values[i]; - ++count; - } - } - if(count == 0) { - return std::numeric_limits::quiet_NaN(); - } - return center / count; - }; - - // 压力和导数合并后只求一个公共中心,表示两条曲线共同的上下位移。 - // 分别去中心会把压力与导数之间真实的相对形状差异一并消除。 - auto commonMeanCenterRange = [&meanCenterRange]( - const QVector& pressureValues, - const QVector& derivativeValues, - int begin, - int end) -> double { - QVector combined; - int validBegin = qMax(0, begin); - int validEnd = qMin( - end, - qMin(static_cast(pressureValues.size()), - static_cast(derivativeValues.size()))); - combined.reserve(2 * qMax(0, validEnd - validBegin)); - - for(int i = validBegin; i < validEnd; ++i) { - if(isFiniteNumber(pressureValues[i])) { - combined.append(pressureValues[i]); - } - if(isFiniteNumber(derivativeValues[i])) { - combined.append(derivativeValues[i]); - } - } - return meanCenterRange(combined, 0, combined.size()); - }; - - // 两个通道按能量等权合并,返回值与单通道 RMSE 保持同一量纲。 - auto jointRmseAround = [&rmseAround]( - const QVector& pressureValues, - const QVector& derivativeValues, - int begin, - int end, - double center) -> double { - double pressureLoss = rmseAround( - pressureValues, begin, end, center); - double derivativeLoss = rmseAround( - derivativeValues, begin, end, center); - if(!isFiniteNumber(pressureLoss) || - !isFiniteNumber(derivativeLoss)) { - return std::numeric_limits::quiet_NaN(); - } - return qSqrt(0.5 * - (pressureLoss * pressureLoss + - derivativeLoss * derivativeLoss)); - }; - - // 主目标始终使用未做上下或左右校正的完整曲线误差。 - breakdown.pressureLoss = - rmse(pressureResidual, 0, numPoints); - breakdown.derivativeLoss = - rmse(derivativeResidual, 0, numPoints); - - // 压力和导数各占一半权重。缩放后 residualVector 的二范数就是 - // sqrt(0.5 * pressureLoss^2 + 0.5 * derivativeLoss^2)。 - const double residualScale = qSqrt(0.5 / numPoints); - breakdown.residualVector.reserve(2 * numPoints); - for(int i = 0; i < numPoints; ++i) { - breakdown.residualVector.append( - residualScale * - pressureResidual[i]); - } - for(int i = 0; i < numPoints; ++i) { - breakdown.residualVector.append( - residualScale * - derivativeResidual[i]); - } - - const double logGridStep = - (targetLogMaxX - targetLogMinX) / - (numPoints - 1); - const double resultLogMinX = qLn(resultMinX); - const double resultLogMaxX = qLn(resultMaxX); - // 左右配准只在所有候选位移都共同覆盖的固定区间比较,至少保留 80% - // 目标点;每个 log-time 网格间隔再细分为 8 份,提高位移分辨率。 - const int minimumRegistrationPoints = - (numPoints * 4) / 5; - const int shiftSubdivisions = 8; - int maximumShiftIntervals = 4; - int registrationBegin = 0; - int registrationEnd = numPoints; - double maximumPhysicalShift = - maximumShiftIntervals * logGridStep; - - // 所有位移候选使用同一组目标点。结果范围不足时逐步缩小最大位移, - // 但用于配准的固定公共区间不得少于目标网格的 80%。 - auto updateRegistrationRange = [&](double maximumShift) { - registrationBegin = 0; - registrationEnd = numPoints; - while(registrationBegin < registrationEnd && - commonLogX[registrationBegin] - maximumShift < - resultLogMinX - 1.0e-12) { - ++registrationBegin; - } - while(registrationEnd > registrationBegin && - commonLogX[registrationEnd - 1] + maximumShift > - resultLogMaxX + 1.0e-12) { - --registrationEnd; - } - }; - - updateRegistrationRange(maximumPhysicalShift); - while(maximumShiftIntervals > 0 && - registrationEnd - registrationBegin < - minimumRegistrationPoints) { - --maximumShiftIntervals; - maximumPhysicalShift = - maximumShiftIntervals * logGridStep; - updateRegistrationRange(maximumPhysicalShift); - } - if(registrationEnd - registrationBegin < - minimumRegistrationPoints) { - return invalidLoss; - } - - // physicalShift 为正表示模拟曲线偏右;对齐时在目标时刻右侧读取模拟值。 - auto buildShiftResidual = [&]( - double physicalShift, - int compareBegin, - int compareEnd, - QVector* shiftedPressure, - QVector* shiftedDerivative) -> bool { - if(!shiftedPressure || !shiftedDerivative) { - return false; - } + DEBUG_OUT("Using log-uniform grid for better early-time coverage"); + } - shiftedPressure->fill( - std::numeric_limits::quiet_NaN(), - numPoints); - shiftedDerivative->fill( - std::numeric_limits::quiet_NaN(), - numPoints); - - int validBegin = qMax(0, compareBegin); - int validEnd = qMin(numPoints, compareEnd); - for(int i = validBegin; i < validEnd; ++i) { - double shiftedLogX = - commonLogX[i] + physicalShift; - if(shiftedLogX < resultLogMinX - 1.0e-12 || - shiftedLogX > - resultLogMaxX + 1.0e-12) { - return false; - } + if(commonX.isEmpty()) { + DEBUG_OUT("Failed to generate common X grid"); + return 1e10; + } - // 对数时间还原后再次限制到原始端点,避免 exp(log(t)) 的 - // 舍入误差越过严格插值边界。 - double shiftedX = qBound( - resultMinX, - qExp(qBound(resultLogMinX, - shiftedLogX, - resultLogMaxX)), - resultMaxX); - double resultLogPressure = 0.0; - double resultLogDerivative = 0.0; - if(!interpolateLogValue( - resultPressure, shiftedX, - &resultLogPressure) || - !interpolateLogValue( - resultDerivative, shiftedX, - &resultLogDerivative)) { - return false; - } + // 插值目标曲线。target[1] 是压力,target[2] 是压力导数。 + QVector targetCurve1, targetCurve2; - (*shiftedPressure)[i] = - resultLogPressure - targetLogPressure[i]; - (*shiftedDerivative)[i] = - resultLogDerivative - targetLogDerivative[i]; - } - return true; - }; - - // 损失相同时优先选择绝对位移更小的候选,避免平坦 profile 在数值噪声 - // 下无故偏向搜索边界。 - auto isBetterProfileValue = []( - double loss, - double shift, - double bestLoss, - double bestShift) -> bool { - const double tolerance = 1.0e-12; - return loss < bestLoss - tolerance || - (qAbs(loss - bestLoss) <= tolerance && - qAbs(shift) < qAbs(bestShift)); - }; - - const int halfShiftStepCount = - maximumShiftIntervals * shiftSubdivisions; - const double physicalShiftStep = - logGridStep / shiftSubdivisions; - double zeroShiftCenteredLoss = - std::numeric_limits::quiet_NaN(); - double bestCenteredLoss = - std::numeric_limits::infinity(); - double bestPhysicalShift = 0.0; - int bestShiftStep = 0; - double bestPressureLoss = - std::numeric_limits::infinity(); - double bestPressureShift = 0.0; - double bestDerivativeLoss = - std::numeric_limits::infinity(); - double bestDerivativeShift = 0.0; - QVector profileLosses( - 2 * halfShiftStepCount + 1, - std::numeric_limits::quiet_NaN()); - QVector profileCommonBiases( - 2 * halfShiftStepCount + 1, - std::numeric_limits::quiet_NaN()); - QVector shiftedPressureResidual; - QVector shiftedDerivativeResidual; - - // 位移和公共上下偏移联合求解,避免“先扣上下还是先扣左右”的顺序依赖。 - for(int shiftStep = -halfShiftStepCount; - shiftStep <= halfShiftStepCount; - ++shiftStep) { - double physicalShift = - shiftStep * physicalShiftStep; - if(!buildShiftResidual( - physicalShift, - registrationBegin, - registrationEnd, - &shiftedPressureResidual, - &shiftedDerivativeResidual)) { - continue; + for(int i = 0; i < target[0].size(); ++i) { + // 检查数据有效性 + if(isFiniteNumber(target[0][i]) && isFiniteNumber(target[1][i]) && + isFiniteNumber(target[2][i])) { + targetCurve1.append(QPointF(target[0][i], target[1][i])); + targetCurve2.append(QPointF(target[0][i], target[2][i])); } + } - double commonBias = commonMeanCenterRange( - shiftedPressureResidual, - shiftedDerivativeResidual, - registrationBegin, - registrationEnd); - double centeredLoss = jointRmseAround( - shiftedPressureResidual, - shiftedDerivativeResidual, - registrationBegin, - registrationEnd, - commonBias); - double pressureBias = meanCenterRange( - shiftedPressureResidual, - registrationBegin, - registrationEnd); - double derivativeBias = meanCenterRange( - shiftedDerivativeResidual, - registrationBegin, - registrationEnd); - double pressureLoss = rmseAround( - shiftedPressureResidual, - registrationBegin, - registrationEnd, - pressureBias); - double derivativeLoss = rmseAround( - shiftedDerivativeResidual, - registrationBegin, - registrationEnd, - derivativeBias); - - if(!isFiniteNumber(centeredLoss) || - !isFiniteNumber(pressureLoss) || - !isFiniteNumber(derivativeLoss)) { - continue; - } - int profileIndex = shiftStep + halfShiftStepCount; - profileLosses[profileIndex] = centeredLoss; - profileCommonBiases[profileIndex] = commonBias; - if(shiftStep == 0) { - zeroShiftCenteredLoss = centeredLoss; - } - if(isBetterProfileValue( - centeredLoss, - physicalShift, - bestCenteredLoss, - bestPhysicalShift)) { - bestCenteredLoss = centeredLoss; - bestPhysicalShift = physicalShift; - bestShiftStep = shiftStep; - } - if(isBetterProfileValue( - pressureLoss, - physicalShift, - bestPressureLoss, - bestPressureShift)) { - bestPressureLoss = pressureLoss; - bestPressureShift = physicalShift; - } - if(isBetterProfileValue( - derivativeLoss, - physicalShift, - bestDerivativeLoss, - bestDerivativeShift)) { - bestDerivativeLoss = derivativeLoss; - bestDerivativeShift = physicalShift; - } + if(targetCurve1.isEmpty() || targetCurve2.isEmpty()) { + DEBUG_OUT("Target curves are empty after filtering"); + return 1e10; } - if(!isFiniteNumber(zeroShiftCenteredLoss) || - !isFiniteNumber(bestCenteredLoss) || - !isFiniteNumber(bestPressureLoss) || - !isFiniteNumber(bestDerivativeLoss)) { - return invalidLoss; - } - - // horizontalGain 是“允许水平位移”相对“固定零位移”减少的均方能量。 - // 只有改善足够明显且最优点不是边界,才把位移解释为可靠左右偏差。 - double horizontalGain = nestedRmsContribution( - zeroShiftCenteredLoss, bestCenteredLoss); - double horizontalSignalThreshold = - qMax(1.0e-5, zeroShiftCenteredLoss * 0.02); - int bestProfileIndex = bestShiftStep + halfShiftStepCount; - double nearbyProfileLoss = - std::numeric_limits::infinity(); - int leftProfileIndex = - bestProfileIndex - shiftSubdivisions; - int rightProfileIndex = - bestProfileIndex + shiftSubdivisions; - if(leftProfileIndex >= 0 && - leftProfileIndex < profileLosses.size() && - isFiniteNumber(profileLosses[leftProfileIndex])) { - nearbyProfileLoss = qMin( - nearbyProfileLoss, - profileLosses[leftProfileIndex]); - } - if(rightProfileIndex >= 0 && - rightProfileIndex < profileLosses.size() && - isFiniteNumber(profileLosses[rightProfileIndex])) { - nearbyProfileLoss = qMin( - nearbyProfileLoss, - profileLosses[rightProfileIndex]); - } - double profileContrast = isFiniteNumber(nearbyProfileLoss) - ? nestedRmsContribution( - nearbyProfileLoss, - bestCenteredLoss) - : 0.0; - bool flatRegistrationProfile = - profileContrast <= horizontalSignalThreshold; - - // 平台曲线的 profile 也可能很平,但公共 bias 在各个位移下保持稳定, - // 此时仍能可靠判断上下。只有近优位移会明显改变 bias 才说明上下/左右不可辨识。 - double minimumNearOptimalBias = - std::numeric_limits::infinity(); - double maximumNearOptimalBias = - -std::numeric_limits::infinity(); - for(int i = 0; i < profileLosses.size(); ++i) { - if(isFiniteNumber(profileLosses[i]) && - isFiniteNumber(profileCommonBiases[i]) && - profileLosses[i] <= - bestCenteredLoss + horizontalSignalThreshold) { - minimumNearOptimalBias = qMin( - minimumNearOptimalBias, - profileCommonBiases[i]); - maximumNearOptimalBias = qMax( - maximumNearOptimalBias, - profileCommonBiases[i]); + QVector alignedTarget1 = interpolateData(targetCurve1, commonX); + QVector alignedTarget2 = interpolateData(targetCurve2, commonX); + + // 插值结果曲线。result 与 target 使用同一 commonX,保证逐点可比。 + QVector resultCurve1, resultCurve2; + + for(int i = 0; i < result[0].size(); ++i) { + // 检查数据有效性 + if(isFiniteNumber(result[0][i]) && isFiniteNumber(result[1][i]) && + isFiniteNumber(result[2][i])) { + resultCurve1.append(QPointF(result[0][i], result[1][i])); + resultCurve2.append(QPointF(result[0][i], result[2][i])); } } - double nearOptimalBiasSpread = - isFiniteNumber(minimumNearOptimalBias) && - isFiniteNumber(maximumNearOptimalBias) - ? maximumNearOptimalBias - minimumNearOptimalBias - : std::numeric_limits::infinity(); - bool commonBiasStable = nearOptimalBiasSpread <= 1.0e-2; - bool horizontalAtBoundary = - halfShiftStepCount > 0 && - qAbs(bestShiftStep) == halfShiftStepCount; - // 压力和导数通道分别求出的最佳位移若方向相反或相差过大,说明一个 - // 单一水平平移无法解释两条曲线,此时标记配准歧义并禁用左右引导。 - bool pressureShiftDetected = - qAbs(bestPressureShift) >= - 0.5 * physicalShiftStep; - bool derivativeShiftDetected = - qAbs(bestDerivativeShift) >= - 0.5 * physicalShiftStep; - bool channelShiftConflict = - pressureShiftDetected && - derivativeShiftDetected && - (bestPressureShift * bestDerivativeShift < 0.0 || - qAbs(bestPressureShift - bestDerivativeShift) > - 2.0 * logGridStep); - - breakdown.horizontalLoss = horizontalGain; - breakdown.horizontalReliable = - maximumShiftIntervals > 0 && - !horizontalAtBoundary && - !channelShiftConflict && - !flatRegistrationProfile && - horizontalGain > horizontalSignalThreshold && - qAbs(bestPhysicalShift) >= - 0.5 * physicalShiftStep; - breakdown.registrationAmbiguous = - channelShiftConflict || - (qAbs(bestPhysicalShift) >= - 0.5 * physicalShiftStep && - !breakdown.horizontalReliable) || - (flatRegistrationProfile && !commonBiasStable); - breakdown.horizontalPhysicalShift = - breakdown.horizontalReliable - ? bestPhysicalShift - : 0.0; - - // 可信水平位移确定后,在该位移实际覆盖的最大区间重新计算上下和形状。 - int diagnosticBegin = 0; - int diagnosticEnd = numPoints; - while(diagnosticBegin < diagnosticEnd && - commonLogX[diagnosticBegin] + - breakdown.horizontalPhysicalShift < - resultLogMinX - 1.0e-12) { - ++diagnosticBegin; - } - while(diagnosticEnd > diagnosticBegin && - commonLogX[diagnosticEnd - 1] + - breakdown.horizontalPhysicalShift > - resultLogMaxX + 1.0e-12) { - --diagnosticEnd; - } - if(diagnosticEnd - diagnosticBegin < - minimumRegistrationPoints || - !buildShiftResidual( - breakdown.horizontalPhysicalShift, - diagnosticBegin, - diagnosticEnd, - &shiftedPressureResidual, - &shiftedDerivativeResidual)) { - return invalidLoss; - } - - double commonBias = commonMeanCenterRange( - shiftedPressureResidual, - shiftedDerivativeResidual, - diagnosticBegin, - diagnosticEnd); - double rawAlignedLoss = jointRmseAround( - shiftedPressureResidual, - shiftedDerivativeResidual, - diagnosticBegin, - diagnosticEnd, - 0.0); - double centeredAlignedLoss = jointRmseAround( - shiftedPressureResidual, - shiftedDerivativeResidual, - diagnosticBegin, - diagnosticEnd, - commonBias); - if(!isFiniteNumber(commonBias) || - !isFiniteNumber(rawAlignedLoss) || - !isFiniteNumber(centeredAlignedLoss)) { - return invalidLoss; - } - - // 原始对齐误差减去公共中心后的能量差定义为上下误差贡献。只有它相对 - // 当前对齐误差足够明显,且配准无歧义时,公共 bias 才可用于有符号选参。 - breakdown.verticalCommonBias = commonBias; - breakdown.verticalLoss = nestedRmsContribution( - rawAlignedLoss, centeredAlignedLoss); - breakdown.verticalReliable = - !breakdown.registrationAmbiguous && - breakdown.verticalLoss > - qMax(1.0e-5, rawAlignedLoss * 0.02); - - QVector shapePressure( - numPoints, std::numeric_limits::quiet_NaN()); - QVector shapeDerivative( - numPoints, std::numeric_limits::quiet_NaN()); - for(int i = diagnosticBegin; i < diagnosticEnd; ++i) { - if(isFiniteNumber(shiftedPressureResidual[i])) { - shapePressure[i] = - shiftedPressureResidual[i] - commonBias; - } - if(isFiniteNumber(shiftedDerivativeResidual[i])) { - shapeDerivative[i] = - shiftedDerivativeResidual[i] - commonBias; - } + + if(resultCurve1.isEmpty() || resultCurve2.isEmpty()) { + DEBUG_OUT("Result curves are empty after filtering"); + return 1e10; } - // shapeLoss 是去除可信左右位移和公共均值中心后的剩余误差。 - double shapePressureLoss = rmse( - shapePressure, diagnosticBegin, diagnosticEnd); - double shapeDerivativeLoss = rmse( - shapeDerivative, diagnosticBegin, diagnosticEnd); - if(!isFiniteNumber(shapePressureLoss) || - !isFiniteNumber(shapeDerivativeLoss)) { - return invalidLoss; - } - breakdown.shapeLoss = qSqrt( - 0.5 * - (shapePressureLoss * shapePressureLoss + - shapeDerivativeLoss * shapeDerivativeLoss)); - - // 现阶段不识别或单独调度晚期流动段;保留字段只为了维持现有 trace 列。 - breakdown.lateDerivativeSlopeBias = 0.0; - breakdown.lateDerivativeTrendLoss = 0.0; - breakdown.lateDerivativeTrendReliable = false; - - if(!isFiniteNumber(breakdown.pressureLoss) || - !isFiniteNumber(breakdown.derivativeLoss) || - !isFiniteNumber(breakdown.verticalCommonBias) || - !isFiniteNumber(breakdown.verticalLoss) || - !isFiniteNumber(breakdown.horizontalPhysicalShift) || - !isFiniteNumber(breakdown.horizontalLoss) || - !isFiniteNumber(breakdown.shapeLoss) || - breakdown.residualVector.size() != 2 * numPoints) { - return invalidLoss; + QVector alignedResult1 = interpolateData(resultCurve1, commonX); + QVector alignedResult2 = interpolateData(resultCurve2, commonX); + + // 检查插值结果 + if(alignedTarget1.isEmpty() || alignedTarget2.isEmpty() || + alignedResult1.isEmpty() || alignedResult2.isEmpty()) { + DEBUG_OUT("LogLog interpolation failed"); + return 1e10; } - if(isSurrogateScreeningEnabled()) { - // 代理模型仍按原目标比较,不能用未参与训练的新损失改变候选排序。 - breakdown.total = - 0.5 * breakdown.pressureLoss + - 0.5 * breakdown.derivativeLoss; + if(alignedTarget1.size() != alignedResult1.size() || + alignedTarget2.size() != alignedResult2.size()) { + DEBUG_OUT("LogLog interpolation size mismatch"); + return 1e10; + } + + // 计算两条曲线的误差。当前压力和导数各占 50%。 + // 如果后续要让导数形态更重要,可以从这里调整权重。 + double error1 = calculateCurveError(alignedTarget1, alignedResult1); + double error2 = calculateCurveError(alignedTarget2, alignedResult2); + + // 检查个别误差是否有效 + if(!isFiniteNumber(error1) || error1 > 1e9) { + DEBUG_OUT(QString("Curve1 error is invalid: %1").arg(error1)); + error1 = 1e10; + } + + if(!isFiniteNumber(error2) || error2 > 1e9) { + DEBUG_OUT(QString("Curve2 error is invalid: %1").arg(error2)); + error2 = 1e10; + } + + // 组合误差 - 添加保护 + double combinedError; + + if(error1 > 1e9 && error2 > 1e9) { + combinedError = 1e10; + } else if(error1 > 1e9) { + combinedError = error2; + } else if(error2 > 1e9) { + combinedError = error1; } else { - // 非代理总目标等于固定 160 维普通残差的二范数;压力和 - // 导数各占一半能量。上下、左右和形状分量不参与候选排序与接受。 - breakdown.total = qSqrt( - 0.5 * breakdown.pressureLoss * breakdown.pressureLoss + - 0.5 * breakdown.derivativeLoss * breakdown.derivativeLoss); - } - breakdown.valid = - isFiniteNumber(breakdown.total) && - breakdown.total >= 0.0; - m_lastObjectiveBreakdown = breakdown; - - DEBUG_OUT( - QString("LogLog objective: pressure=%1, derivative=%2, vertical=%3, horizontal=%4, shape=%5, ambiguous=%6, shift=%7, coverage=%8, total=%9") - .arg(breakdown.pressureLoss, 0, 'e', 4) - .arg(breakdown.derivativeLoss, 0, 'e', 4) - .arg(breakdown.verticalLoss, 0, 'e', 4) - .arg(breakdown.horizontalLoss, 0, 'e', 4) - .arg(breakdown.shapeLoss, 0, 'e', 4) - .arg(breakdown.registrationAmbiguous) - .arg(breakdown.horizontalPhysicalShift, 0, 'e', 4) - .arg(breakdown.coverage, 0, 'f', 4) - .arg(breakdown.total, 0, 'e', 4)); - - return breakdown.valid - ? qMin(1.0e9, breakdown.total) - : invalidLoss; + combinedError = 0.5 * error1 + 0.5 * error2; + } + + DEBUG_OUT(QString("LogLog errors: Curve1=%1, Curve2=%2, Combined=%3") + .arg(error1, 0, 'e', 4).arg(error2, 0, 'e', 4).arg(combinedError, 0, 'e', 4)); + + return qMin(1e9, combinedError); + } catch(const std::exception& e) { - DEBUG_OUT( - QString("Exception in LogLog error calculation: %1") - .arg(e.what())); - return invalidLoss; + DEBUG_OUT(QString("Exception in LogLog error calculation: %1").arg(e.what())); + return 1e10; } catch(...) { DEBUG_OUT("Unknown exception in LogLog error calculation"); - return invalidLoss; + return 1e10; } } @@ -7450,7 +5145,7 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() { // DLL 求解器路径。 // 这个函数负责把当前 DataManager 中的项目状态交给底层数值求解器, - // 数值求解仍包含全部计算井,以保留井间干扰;后处理只提取目标井曲线。 + // 并让目标井生成 result log-log 数据。evaluateFitness() 后续会从目标井读取该结果。 // // 如果这里失败,通常需要优先检查:HX_NWTM.dll、license、网格/井数据是否完整、 // 目标井是否存在,以及 DataManager 中刚写入的参数是否导致求解器异常。 @@ -7462,7 +5157,6 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() } ++m_evaluationInProgress; - m_lastEvaluatedLogLogData.clear(); QVector> result; nmCalculationDllPebiSolverTask* dllTask = nullptr; @@ -7470,15 +5164,6 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() DEBUG_OUT("Creating DLL solver task"); dllTask = new nmCalculationDllPebiSolverTask(m_tempDirectory); - if(m_targetWellName.isEmpty()) { - DEBUG_OUT("Target well name is empty - target-only solver cannot start"); - delete dllTask; - --m_evaluationInProgress; - return result; - } - - dllTask->setAutoFitTargetWell(m_targetWellName); - if(m_shouldStop) { DEBUG_OUT("Should stop - cleaning up and returning empty result"); delete dllTask; @@ -7538,9 +5223,22 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() return result; } - // 任务结束后复制其局部结果,删除任务前不再持有任务内部引用。 - QVector> pressureResult = dllTask->getAutoFitResultPressure(); - QVector> logLogResult = dllTask->getAutoFitResultLogLog(); + // 验证结果数据是否已更新。DLL 任务会把结果写回 DataManager 中的目标井对象。 + nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); + //QVector wells = dataManager->getWellDataList(); + nmDataWellBase* pTargetWell = dataManager->findWellByName(m_targetWellName); + + if(!pTargetWell) { + DEBUG_OUT("No wells found in data manager after DLL execution"); + delete dllTask; + --m_evaluationInProgress; + return result; + } + + // 验证结果数据。evaluateFitness() 最终用的是 logLogResult, + // 但这里返回 pressureResult 给 validateSolverResult() 做基本求解成功判断。 + QVector> pressureResult = pTargetWell->getResultPressure(); + QVector> logLogResult = pTargetWell->getResultLogLog(); DEBUG_OUT(QString("DLL result verification - Pressure arrays: %1, LogLog arrays: %2") .arg(pressureResult.size()).arg(logLogResult.size())); @@ -7559,12 +5257,8 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() } // 数据有效性检查 - if(pressureResult.size() >= 2 - && pressureResult[0].size() > 0 - && pressureResult[1].size() > 0 - && validateLogLogData(logLogResult)) { + if(pressureResult.size() >= 2 && pressureResult[0].size() > 0 && pressureResult[1].size() > 0) { result = pressureResult; - m_lastEvaluatedLogLogData = logLogResult; DEBUG_OUT(QString("Got DLL solver result: %1 points").arg(result[0].size())); m_consecutiveFailures = 0; @@ -7632,58 +5326,6 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() return result; } -bool nmCalculationAutoFitPSO::runFinalFullSolver() -{ - // 不设置目标井名,任务按原完整模式保存全部井和网格结果。 - if(m_evaluationInProgress > 0) { - DEBUG_OUT("Cannot start final full-field solver while another evaluation is running"); - return false; - } - - ++m_evaluationInProgress; - nmCalculationDllPebiSolverTask dllTask(m_tempDirectory); - dllTask.start(); - - const int maxWait = 3600000; - const int checkInterval = 50; - QTime waitTimer; - waitTimer.start(); - - while(waitTimer.elapsed() < maxWait) { - if(dllTask.wait(checkInterval)) { - break; - } - - // 最终完整计算可能持续较长时间,此处需处理停止按钮事件。 - QApplication::processEvents(QEventLoop::AllEvents, checkInterval); - - if(!dllTask.isRunning()) { - break; - } - - if(m_shouldStop) { - DEBUG_OUT("Final full-field solver terminated by user"); - dllTask.terminate(); - dllTask.wait(2000); - --m_evaluationInProgress; - return false; - } - } - - if(dllTask.isRunning()) { - DEBUG_OUT("Final full-field solver timeout, terminating task"); - dllTask.terminate(); - dllTask.wait(2000); - --m_evaluationInProgress; - return false; - } - - dllTask.wait(); - const bool succeeded = dllTask.wasSuccessful(); - --m_evaluationInProgress; - return succeeded; -} - //QVector> nmCalculationAutoFitPSO::runSolverExe() //{ // DEBUG_OUT("SOLVER EXE START"); diff --git a/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp b/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp index 0a11b33..28304d0 100644 --- a/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp +++ b/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp @@ -1,5 +1,6 @@ #include "nmWxAutomaticFitting.h" #include "nmCalculationAutoFitPSO.h" +#include "nmCalculationAutoFitLM.h" #include "nmWxAutomaticfittingStart.h" #include "nmWxParameterProperty.h" #include "nmDataAnalyzeManager.h" @@ -593,6 +594,7 @@ bool nmWxAutomaticFitting::validateParameterTable(QString& errorMessage, int par nmWxAutomaticFitting::nmWxAutomaticFitting(QWidget *parent) : iDlgBase(parent) , m_autoFitterPSO(nullptr) + , m_autoFitterLM(nullptr) , m_progressDialog(nullptr) , m_progressTimer(nullptr) , m_progressMonitor(nullptr) @@ -675,6 +677,9 @@ nmWxAutomaticFitting::~nmWxAutomaticFitting() if (m_autoFitterPSO) { disconnect(m_autoFitterPSO, nullptr, this, nullptr); } + if (m_autoFitterLM) { + disconnect(m_autoFitterLM, nullptr, this, nullptr); + } DEBUG_UI("AutoFitting destructor - completed"); } @@ -890,6 +895,7 @@ void nmWxAutomaticFitting::setupControlPanel() QLabel* algorithmLabel = new QLabel(tr("Algorithm:")); m_algorithmCombo = new QComboBox(); m_algorithmCombo->addItem(tr("PSO (Particle Swarm)")); + m_algorithmCombo->addItem(tr("Finite Difference + LM")); m_algorithmCombo->setCurrentIndex(0); m_algorithmCombo->setMaximumWidth(160); m_algorithmCombo->setMinimumWidth(160); @@ -1446,10 +1452,18 @@ void nmWxAutomaticFitting::startAutoFitting(const QVector>& targ // 先清理之前的实例 cleanupFitting(); - DEBUG_UI("Creating PSO auto fitter"); - m_autoFitterPSO = new nmCalculationAutoFitPSO(this); - m_autoFitterPSO->setTargetLogLogData(targetData); - m_autoFitterPSO->setPSOTargetWellName(targetWellName); + const bool useLM = m_algorithmCombo && m_algorithmCombo->currentIndex() == 1; + if(useLM) { + DEBUG_UI("Creating finite difference + LM auto fitter"); + m_autoFitterLM = new nmCalculationAutoFitLM(this); + m_autoFitterLM->setTargetLogLogData(targetData); + m_autoFitterLM->setTargetWellName(targetWellName); + } else { + DEBUG_UI("Creating PSO auto fitter"); + m_autoFitterPSO = new nmCalculationAutoFitPSO(this); + m_autoFitterPSO->setTargetLogLogData(targetData); + m_autoFitterPSO->setPSOTargetWellName(targetWellName); + } //// 特定井名时使用快速路径 //if (targetWellName == "VerticalWell1") { @@ -1485,13 +1499,22 @@ void nmWxAutomaticFitting::startAutoFitting(const QVector>& targ m_progressMonitor = new nmWxAutomaticfittingStart(this); - m_progressMonitor->setAutoFitter(m_autoFitterPSO); + if(m_autoFitterLM) { + m_progressMonitor->setAutoFitter(m_autoFitterLM); + } else { + m_progressMonitor->setAutoFitter(m_autoFitterPSO); + } m_progressMonitor->setPseudoPressureMode( nmDataAnalyzeManager::getCurrentInstance()->getSolverModelType() == SMT_Gas_VariablePvt); m_progressMonitor->setTargetLogLogData(targetData); - connect(m_autoFitterPSO, SIGNAL(fittingFinished(bool, QString)), - this, SLOT(onFittingFinished(bool, QString))); + if(m_autoFitterLM) { + connect(m_autoFitterLM, SIGNAL(fittingFinished(bool, QString)), + this, SLOT(onFittingFinished(bool, QString))); + } else { + connect(m_autoFitterPSO, SIGNAL(fittingFinished(bool, QString)), + this, SLOT(onFittingFinished(bool, QString))); + } int maxIterations = m_iterationEdit->text().toInt(); double targetError = m_errorLimitEdit->text().toDouble(); @@ -1511,6 +1534,8 @@ void nmWxAutomaticFitting::runAutoFitting() if(m_autoFitterPSO) { m_autoFitterPSO->startAutoFitting(); + } else if(m_autoFitterLM) { + m_autoFitterLM->startAutoFitting(); } } @@ -1526,23 +1551,30 @@ void nmWxAutomaticFitting::onFittingFinished(bool success, const QString& messag disconnect(m_autoFitterPSO, SIGNAL(fittingFinished(bool, QString)), this, SLOT(onFittingFinished(bool, QString))); } + if (m_autoFitterLM) { + disconnect(m_autoFitterLM, SIGNAL(fittingFinished(bool, QString)), + this, SLOT(onFittingFinished(bool, QString))); + } if(success) { // 只有成功拟合的结果才用于生成下一轮范围,失败结果不污染当前配置。 updateBestParametersToTable(); QString resultInfo; - if(m_autoFitterPSO) { - double bestFitness = m_autoFitterPSO->getBestFitness(); + if(m_autoFitterPSO || m_autoFitterLM) { + double bestFitness = m_autoFitterLM + ? m_autoFitterLM->getBestFitness() + : m_autoFitterPSO->getBestFitness(); + const QString algorithmName = m_autoFitterLM ? "LM" : "PSO"; // 检查是否是用户停止的情况 if(message.contains("stopped by user", Qt::CaseInsensitive)) { - resultInfo = tr("PSO Optimization stopped by user:\n"); + resultInfo = tr("%1 Optimization stopped by user:\n").arg(algorithmName); resultInfo += tr("Best Error: %1\n").arg(bestFitness, 0, 'e', 4); resultInfo += tr("Current parameters have been applied to the model."); QMessageBox::information(this, tr("Optimization Stopped"), resultInfo); } else { - resultInfo = tr("PSO Optimization completed:\n"); + resultInfo = tr("%1 Optimization completed:\n").arg(algorithmName); resultInfo += tr("Best Error: %1\n").arg(bestFitness, 0, 'e', 4); resultInfo += tr("Optimized parameters have been applied to the model."); QMessageBox::information(this, tr("Optimization Completed"), resultInfo); @@ -1558,6 +1590,8 @@ void nmWxAutomaticFitting::onStopFitting() { if(m_autoFitterPSO && m_autoFitterPSO->isRunning()) { m_autoFitterPSO->stopFitting(); + } else if(m_autoFitterLM && m_autoFitterLM->isRunning()) { + m_autoFitterLM->stopFitting(); } } @@ -1601,6 +1635,24 @@ void nmWxAutomaticFitting::cleanupFitting() DEBUG_UI("PSO fitter cleaned up"); } + if (m_autoFitterLM) { + DEBUG_UI("Stopping and disconnecting LM fitter"); + disconnect(m_autoFitterLM, nullptr, nullptr, nullptr); + + if (m_autoFitterLM->isRunning()) { + m_autoFitterLM->stopFitting(); + int waitCount = 0; + while (m_autoFitterLM->isRunning() && waitCount < 50) { + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 100); + waitCount++; + } + } + + delete m_autoFitterLM; + m_autoFitterLM = nullptr; + DEBUG_UI("LM fitter cleaned up"); + } + // 清理进度监控 if (m_progressMonitor) { // 先断开进度监控的信号连接 @@ -1628,6 +1680,8 @@ void nmWxAutomaticFitting::updateBestParametersToTable() // 获取最佳解决方案 if (m_autoFitterPSO) { bestSolution = m_autoFitterPSO->getBestSolution(); + } else if(m_autoFitterLM) { + bestSolution = m_autoFitterLM->getBestSolution(); } if (bestSolution.isEmpty()) return; diff --git a/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp b/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp index acb1a3f..e230942 100644 --- a/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp +++ b/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp @@ -383,6 +383,8 @@ nmWxAutomaticfittingStart::nmWxAutomaticfittingStart(QWidget *parent) , chartGroup(nullptr) , curveChart(nullptr) , m_autoFitterPSO(nullptr) + , m_autoFitterLM(nullptr) + , m_algorithmName("PSO") , m_maxIterations(100) , m_targetError(0.001) , m_wellName("") @@ -578,6 +580,8 @@ void nmWxAutomaticfittingStart::setupControlArea() void nmWxAutomaticfittingStart::setAutoFitter(nmCalculationAutoFitPSO* autoFitter) { m_autoFitterPSO = autoFitter; + m_autoFitterLM = nullptr; + m_algorithmName = "PSO"; // 更新算法类型显示 algorithmTypeValue->setText("PSO"); @@ -600,6 +604,30 @@ void nmWxAutomaticfittingStart::setAutoFitter(nmCalculationAutoFitPSO* autoFitte } } +void nmWxAutomaticfittingStart::setAutoFitter(nmCalculationAutoFitLM* autoFitter) +{ + m_autoFitterPSO = nullptr; + m_autoFitterLM = autoFitter; + m_algorithmName = "Finite Difference + LM"; + + algorithmTypeValue->setText(m_algorithmName); + algorithmTypeValue->setStyleSheet("QLabel { color: blue; font-weight: bold; }"); + + if (m_autoFitterLM) { + connect(m_autoFitterLM, SIGNAL(progressUpdated(int, double)), + this, SLOT(onFittingProgress(int, double))); + connect(m_autoFitterLM, SIGNAL(fittingFinished(bool, QString)), + this, SLOT(onFittingFinished(bool, QString))); + connect(m_autoFitterLM, SIGNAL(logMessageGenerated(QString)), + this, SLOT(onLogMessageReceived(QString))); + connect(m_autoFitterLM, SIGNAL(bestCurveUpdated(QVector >,QVector >,int,double)), + this, SLOT(onBestCurveUpdated(QVector >,QVector >,int,double))); + + stopButton->setEnabled(true); + addLogMessage(tr("%1 auto fitting started").arg(m_algorithmName)); + } +} + void nmWxAutomaticfittingStart::setFittingParameters(int maxIterations, double targetError, const QString& wellName) { m_maxIterations = maxIterations; @@ -612,7 +640,7 @@ void nmWxAutomaticfittingStart::setFittingParameters(int maxIterations, double t progressBar->setRange(0, maxIterations); - const QString algorithmName = "PSO"; + const QString algorithmName = m_algorithmName; addLogMessage(tr("%1 fitting parameters set: MaxIterations=%2, TargetAccuracy=%3, TargetWell=%4") .arg(algorithmName).arg(maxIterations).arg(formatScientific(targetError)).arg(wellName)); } @@ -621,7 +649,7 @@ void nmWxAutomaticfittingStart::markFittingStarted() { m_startTime = QDateTime::currentDateTime(); - const QString algorithmName = "PSO"; + const QString algorithmName = m_algorithmName; QString timestamp = m_startTime.toString("yyyy-MM-dd hh:mm:ss"); addLogMessage(tr("=== %1 Fitting Session Started at %2 ===") @@ -636,7 +664,7 @@ void nmWxAutomaticfittingStart::setSelectedParameters(const QStringList& paramet // 立即更新参数表格 updateParameterTable(); - const QString algorithmName = "PSO"; + const QString algorithmName = m_algorithmName; addLogMessage(tr("%1 selected parameters: %2").arg(algorithmName).arg(parameterNames.join(", "))); } @@ -687,7 +715,7 @@ void nmWxAutomaticfittingStart::onFittingFinished(bool success, const QString& m { m_isFinished = true; - const QString algorithmName = "PSO"; + const QString algorithmName = m_algorithmName; // 更新状态 if (success) { @@ -771,10 +799,11 @@ void nmWxAutomaticfittingStart::onFittingFinished(bool success, const QString& m void nmWxAutomaticfittingStart::onStopButtonClicked() { - const bool isRunning = m_autoFitterPSO && m_autoFitterPSO->isRunning(); + const bool isRunning = (m_autoFitterPSO && m_autoFitterPSO->isRunning()) || + (m_autoFitterLM && m_autoFitterLM->isRunning()); if (isRunning) { - const QString algorithmName = "PSO"; + const QString algorithmName = m_algorithmName; int ret = QMessageBox::question(this, tr("Confirm Stop"), tr("Are you sure you want to stop the %1 fitting process?").arg(algorithmName), QMessageBox::Yes | QMessageBox::No, @@ -783,6 +812,8 @@ void nmWxAutomaticfittingStart::onStopButtonClicked() if (ret == QMessageBox::Yes) { if (m_autoFitterPSO) { m_autoFitterPSO->stopFitting(); + } else if (m_autoFitterLM) { + m_autoFitterLM->stopFitting(); } addLogMessage(tr("User requested to stop %1 fitting").arg(algorithmName)); } @@ -809,6 +840,11 @@ void nmWxAutomaticfittingStart::updateParameterTable() if (i < bestSolution.size()) { valueText = formatScientific(bestSolution[i]); } + } else if (m_autoFitterLM) { + QVector bestSolution = m_autoFitterLM->getBestSolution(); + if (i < bestSolution.size()) { + valueText = formatScientific(bestSolution[i]); + } } QTableWidgetItem* valueItem = new QTableWidgetItem(valueText); @@ -842,8 +878,9 @@ QString nmWxAutomaticfittingStart::formatScientific(double value) void nmWxAutomaticfittingStart::closeEvent(QCloseEvent *event) { - const bool isRunning = m_autoFitterPSO && m_autoFitterPSO->isRunning(); - const QString algorithmName = "PSO"; + const bool isRunning = (m_autoFitterPSO && m_autoFitterPSO->isRunning()) || + (m_autoFitterLM && m_autoFitterLM->isRunning()); + const QString algorithmName = m_algorithmName; if (isRunning && !m_isFinished) { int ret = QMessageBox::question(this, tr("Confirm Close"), @@ -854,6 +891,8 @@ void nmWxAutomaticfittingStart::closeEvent(QCloseEvent *event) if (ret == QMessageBox::Yes) { if (m_autoFitterPSO) { m_autoFitterPSO->stopFitting(); + } else if (m_autoFitterLM) { + m_autoFitterLM->stopFitting(); } event->accept(); } else {