diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index 2bab4b7..f0d6183 100644 Binary files a/Bin/Config/Lang/cn/nmNum_cn.qm and b/Bin/Config/Lang/cn/nmNum_cn.qm differ diff --git a/Bin/Config/Lang/cn/nmNum_cn.ts b/Bin/Config/Lang/cn/nmNum_cn.ts index 75552d4..cf34736 100644 --- a/Bin/Config/Lang/cn/nmNum_cn.ts +++ b/Bin/Config/Lang/cn/nmNum_cn.ts @@ -77,6 +77,10 @@ Reason: %1 PSO optimization stop request processed PSO优化停止请求已处理 + + Simulation stopped by user + 模拟计算已被用户停止 + Stop request received but optimization is not running 收到停止请求但优化未运行 @@ -413,6 +417,30 @@ Reason: %1 Optimization failed 优化失败 + + Automatic fitting run time limit reached + 达到自动拟合运行时间上限 + + + Automatic fitting stopped after reaching the run time limit + 自动拟合达到运行时间上限后停止 + + + Final full-field calculation skipped after run time limit + 达到运行时间上限,已跳过最终全场计算 + + + Final full-field calculation reached the run time limit + 最终全场计算达到运行时间上限 + + + Automatic fitting exceeded the run time limit + 自动拟合已超过运行时间上限 + + + === AUTOMATIC FITTING TIMED OUT === + === 自动拟合超时 === + Unknown reason 未知原因 @@ -530,8 +558,8 @@ Reason: %1 代理运行上下文检查:%1 - === PSO Run Summary === - === PSO 运行摘要 === + === Automatic Fitting Run Summary === + === 自动拟合运行摘要 === Stop reason: %1 @@ -561,6 +589,10 @@ Reason: %1 Artifacts: trace_meta=%1 产物:跟踪元数据=%1 + + Result artifact export failed: %1 + 结果文件导出失败:%1 + Surrogate scoring attempt %1/%2: python=%3, mlRoot=%4 代理评分尝试 %1/%2,Python=%3,mlRoot=%4 @@ -3463,6 +3495,10 @@ Supported types: Vertical, Vertical Fractured, and Horizontal Multi-Fractured We Optimization Failed 拟合失败 + + Optimization Timed Out + 拟合超时 + Swi 初始含水饱和度 @@ -3520,6 +3556,64 @@ Supported types: Vertical, Vertical Fractured, and Horizontal Multi-Fractured We The minimum value of %1 must be greater than zero for automatic fitting. 自动拟合时,%1 的最小值和初始值必须大于零。 + + Invalid (%1) + 无效(%1) + + + Final status: %1 + + 最终状态:%1 + + + + Stop reason: %1 + + 停止原因:%1 + + + + Fitting wall time: %1 ms + + 拟合总耗时:%1 ms + + + + Iterations: %1 + + 迭代次数:%1 + + + + Model solver calls: %1 + + 实际模型调用次数:%1 + + + + Unified curve error: %1 + + 统一曲线误差:%1 + + + + Result JSON: %1 + 结果 JSON:%1 + + + Result export failed: %1 + + 结果导出失败:%1 + + + + Result JSON: Not created + 结果 JSON:未生成 + + + Result Export Failed + 结果导出失败 + nmWxAutomaticfitting @@ -3742,6 +3836,50 @@ Supported types: Vertical, Vertical Fractured, and Horizontal Multi-Fractured We Failed (%1%) 失败 (%1%) + + Stopped + 已停止 + + + Timed out + 已超时 + + + Final status: %1 + 最终状态:%1 + + + Stop reason: %1 + 停止原因:%1 + + + Fitting wall time: %1 ms + 拟合总耗时:%1 ms + + + Iterations: %1, model solver calls: %2 + 迭代次数:%1,实际模型调用次数:%2 + + + Unified curve error: %1 + 统一曲线误差:%1 + + + Unified curve error: Invalid (%1) + 统一曲线误差:无效(%1) + + + Result JSON: %1 + 结果 JSON:%1 + + + Result export failed: %1 + 结果导出失败:%1 + + + Result JSON: Not created + 结果 JSON:未生成 + Close 关闭 diff --git a/Include/nmNum/nmCalculation/nmCalculationAutoFitPSO.h b/Include/nmNum/nmCalculation/nmCalculationAutoFitPSO.h index 9731259..be5ffa6 100644 --- a/Include/nmNum/nmCalculation/nmCalculationAutoFitPSO.h +++ b/Include/nmNum/nmCalculation/nmCalculationAutoFitPSO.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -20,6 +21,100 @@ class nmDataWellBase; class QTimer; class QProcess; +// 自动拟合统一曲线指标。所有误差均由冻结的原始压力曲线独立计算, +// 不复用优化器内部目标函数;invalidReason 用于解释为何不能参与正式统计。 +struct NMCALCULATION_EXPORT AutoFitCurveMetrics { + bool valid; + bool passed; + QString invalidReason; + double coverage; + double pressureRmseMpa; + double pressureMaxAbsErrorMpa; + double logDeltaPRmseDecade; + double logDerivativeRmseDecade; + double unifiedCurveError; + int sampleCount; + int validDerivativeCount; + QVector timeHr; + QVector targetPressureMpa; + QVector fittedPressureMpa; + QVector targetDeltaPMpa; + QVector fittedDeltaPMpa; + QVector targetDerivativeMpa; + QVector fittedDerivativeMpa; + + AutoFitCurveMetrics(); +}; + +// 单个启用参数的完整反演结果。参数名称顺序统一复用 traceParameterNames(), +// 保证界面、trace、JSON 和汇总 CSV 使用同一个参数契约。 +struct NMCALCULATION_EXPORT AutoFitParameterResult { + QString name; + QString unit; + double initialValue; + double lowerBound; + double upperBound; + double finalValue; + + AutoFitParameterResult(); +}; + +// 一次正式自动拟合的权威运行记录。耗时单位统一为毫秒;值为 -1 表示该阶段 +// 未执行,写出 JSON/CSV 时必须保留为空值,不能伪造为零。 +struct NMCALCULATION_EXPORT AutoFitRunResult { + QString runId; + QString status; // SUCCESS、COMPLETED、STOPPED、FAILED 或 TIMEOUT。 + QString stopReason; + QDateTime startedAt; + QDateTime finishedAt; + QString targetWell; + QString phase; + QString algorithm; + QString solverType; + int ompThreads; + int iluReuseSteps; + qint64 optimizationWallTimeMs; + qint64 workflowWallTimeMs; + qint64 solverTimeSumMs; + qint64 finalSolverTimeMs; + int iterationCount; + int parameterEvaluationCount; + int modelSolverCallCount; + int finalSolverCallCount; + int solverSuccessCount; // 仅统计优化阶段 DLL 调用,最终全场求解使用独立状态。 + int solverFailureCount; // 失败重试按每次实际 DLL 调用分别累计。 + int solverTimeoutCount; // 优化阶段因完整运行预算耗尽而终止的 DLL 调用数。 + int optimizationPebiCount; + int finalPebiCount; + int pebiCount; + QString finalSolverStatus; + double initialPressureMpa; + double initialInternalError; + double finalInternalError; + QString targetCurveSha256; + AutoFitCurveMetrics curveMetrics; + QVector parameters; + QString projectPath; + QString resultDirectory; + QString resultJsonPath; + QString curveCsvPath; + QString runsCsvPath; + QString traceCsvPath; + QString traceMetaJsonPath; + QString fullFieldPressurePath; + QString artifactError; + + AutoFitRunResult(); + void recordOptimizationSolverCall(bool success, + bool timeout, + qint64 solveTimeMs, + int pebiCount); + void recordFinalSolverCall(bool success, + bool timeout, + qint64 solveTimeMs, + int pebiCount); +}; + // 双对数曲线误差分解。该结构同时保存用于候选排序的主目标,以及用于判断 // 曲线上下、左右和形状偏差的诊断量。total 是唯一的接受和排序依据,诊断量 // 只参与信赖域选参,不能再次叠加到 total,否则会重复计算同一批曲线残差。 @@ -91,6 +186,8 @@ struct AutoFitObjectiveBreakdown { // surrogate* 和 screeningDecision 是 PSO 加速筛选的辅助字段。真实 pbest / gbest // 始终只由求解器更新;T1-T5 可另外维护不参与最终结果的 guideBestPosition 来修正速度方向。 struct AutoFitParticle { + QVector > currentPressureData; + QVector > bestPressureData; QVector > currentLogLogData; QVector > bestLogLogData; QVector position; // 当前位置(参数值) @@ -139,7 +236,8 @@ enum StopReasonPSO { PSO_MAX_ITERATIONS = 4, // 达到最大迭代数 PSO_USER_STOPPED = 5, // 用户停止 PSO_CONSECUTIVE_FAILURES = 6, // 连续失败停止 - PSO_OPTIMIZATION_FAILED = 7 // 优化失败 + PSO_OPTIMIZATION_FAILED = 7, // 优化失败 + PSO_TIME_LIMIT_REACHED = 8 // 完整拟合运行达到时间上限 }; class NMCALCULATION_EXPORT nmCalculationAutoFitPSO : public QObject @@ -158,15 +256,22 @@ public: // nmDataAnalyzeManager / nmDataAutomaticFitting,然后本类在 startAutoFitting() // 内部统一读取。 void setTargetLogLogData(const QVector >& targetData); + void setTargetPressureData(const QVector >& targetData); bool startAutoFitting(); void stopFitting(); QVector getBestSolution() const; double getBestFitness() const; AutoFitObjectiveBreakdown getLastObjectiveBreakdown() const; + AutoFitRunResult getLastRunResult() const; QString getLastError() const; bool isRunning() const; int getCurrentIteration() const; void resetOptimizer(); + static AutoFitCurveMetrics calculateUnifiedCurveMetrics( + const QVector >& targetPressureData, + const QVector >& fittedPressureData, + double initialPressureMpa, + int sampleCount = 80); void setPSOTargetWellName(const QString& wellName); //QString getTargetWellName() const; @@ -193,6 +298,10 @@ signals: private: +#ifdef NM_AUTOFIT_RESULT_TESTS + friend class nmCalculationAutoFitPSOTestAccessor; +#endif + // 临时目录管理 void initializeTemporaryDirectory(); void cleanupTemporaryDirectory(); @@ -223,6 +332,7 @@ private: double* fitness, AutoFitObjectiveBreakdown* breakdown, QVector >* curve, + QVector >* pressureCurve, int* elapsedMs); // ===== 参数应用方法 ===== @@ -301,6 +411,27 @@ private: void resetRunSummary(); void captureSurrogateRunContextSummary(); void emitRunSummary(bool success, StopReasonPSO finalReason); + static QStringList traceParameterNames(); + + // ===== 结构化运行结果 ===== + void initializeRunResult(); + void captureRunConfiguration(bool useParticleSwarm); + void beginRunTiming(); + void markOptimizationFinished(); + void markWorkflowFinished(); + bool isRunTimeLimitReached(); + int remainingRunTimeMs(); + void finalizeRunResult(const QString& status, + const QString& stopReason, + bool writeArtifacts = true); + void captureRunParameters(); + bool writeStructuredRunArtifacts(); + bool writeRunResultJson(); + bool writeRunCurveCsv(); + bool appendRunSummaryCsv(); + QString getAutoFitOutputRoot() const; + QString calculateTargetCurveHash( + const QVector >& pressureData) const; // ===== Surrogate screening prototype ===== // @@ -350,8 +481,10 @@ private: // ===== 运行状态 ===== bool m_isRunning; // 当前是否有一次自动拟合正在运行。 bool m_shouldStop; // 用户停止标志;主循环和求解器等待循环会定期检查它。 + bool m_runTimedOut; // 完整拟合运行达到统一时间上限,不与用户停止混用。 bool m_isPaused; // 预留暂停标志;主循环中有暂停等待逻辑。 int m_currentIteration; // 当前自动拟合迭代序号,从 0 开始。 + int m_completedIterationCount; // 本次运行已进入的优化迭代数;首次评价不计为迭代。 QString m_lastError; // 最近一次失败原因,供 UI 展示或日志排查。 // ===== 优化状态数据 ===== @@ -363,8 +496,11 @@ private: AutoFitObjectiveBreakdown m_globalBestObjectiveBreakdown; // 真实 gbest 对应的误差分解。 QVector > m_lastEvaluatedLogLogData; // 最近一次真实求解得到的 result log-log 曲线。 QVector > m_globalBestLogLogData; // 当前全局最优对应的 result log-log 曲线。 + QVector > m_lastEvaluatedPressureData; // 最近一次真实求解得到的原始压力曲线。 + QVector > m_globalBestPressureData; // 当前全局最优对应的原始压力曲线。 mutable AutoFitObjectiveBreakdown m_lastObjectiveBreakdown; // 最近一次损失评价的误差分解。 QVector > m_userInitialLogLogData; // 用户初始解对应的 result log-log 曲线,用于精英保护。 + QVector > m_userInitialPressureData; // 用户初始解对应的原始压力曲线,用于精英保护。 AutoFitObjectiveBreakdown m_userInitialObjectiveBreakdown; // 用户初始解对应的误差分解。 // ===== 优化配置 ===== @@ -380,6 +516,8 @@ private: QVector m_parameterUpper; // 完整 10 个参数的搜索上界。 QVector m_enabledParamIndices; // 被勾选参数在完整 10 维体系中的索引。 QVector > m_targetLogLogData; // 目标井 history log-log 曲线:time/pressure/derivative。 + QVector > m_targetPressureData; // 界面传入的目标井原始压力曲线:time/pressure。 + QVector > m_frozenTargetPressureData; // 本次运行开始时冻结的目标压力曲线。 QString m_targetWellName; // 目标井名称;读写井参数和读取模拟曲线都依赖它。 // ===== 算法配置 ===== @@ -394,11 +532,16 @@ private: int m_totalEvaluations; // 真实求解器评价总次数,包含粒子评价和方向试算。 int m_successfulEvaluations; // 真实求解器成功且误差有效的评价次数。 QVector m_convergenceHistory; // 每代全局最优误差历史,用于收敛判断。 + AutoFitRunResult m_lastRunResult; // 最近一次正式运行的结构化权威结果。 + QElapsedTimer m_optimizationWallTimer; // 从首次参数评价到最优参数确定的单调时钟。 + QElapsedTimer m_workflowWallTimer; // 在优化耗时基础上包含最终全场计算的单调时钟。 + bool m_runTimingStarted; // 两类单调时钟是否已经从首次评价前启动。 // ===== 常量 ===== static const double MIN_FITNESS_IMPROVEMENT; // 判断误差是否有有效改善的最小阈值。 static const double VELOCITY_LIMIT_FACTOR; // 粒子速度上限占参数搜索区间的比例。 static const int CONVERGENCE_CHECK_INTERVAL; // 收敛检查的基础间隔。 + static const int RUN_TIME_LIMIT_MS; // 正式协议规定的一次完整拟合运行上限。 // ===== 资源管理 ===== volatile int m_evaluationInProgress; // 并发控制 diff --git a/Include/nmNum/nmSubWxs/nmWxAutomaticFittingStart.h b/Include/nmNum/nmSubWxs/nmWxAutomaticFittingStart.h index 3e9067e..1e60f87 100644 --- a/Include/nmNum/nmSubWxs/nmWxAutomaticFittingStart.h +++ b/Include/nmNum/nmSubWxs/nmWxAutomaticFittingStart.h @@ -160,7 +160,6 @@ private: bool m_isFinished; double m_bestFitnessEver; - QDateTime m_startTime; }; #endif // NMWXAUTOMATICFITTINGSTART_H diff --git a/Src/nmNum/nmCalculation/nmCalculationAutoFitMetrics.cpp b/Src/nmNum/nmCalculation/nmCalculationAutoFitMetrics.cpp new file mode 100644 index 0000000..c6dcfd1 --- /dev/null +++ b/Src/nmNum/nmCalculation/nmCalculationAutoFitMetrics.cpp @@ -0,0 +1,476 @@ +#include "nmCalculationAutoFitPSO.h" + +#include +#include +#include + +#ifdef Q_OS_WIN +#include +#endif + +namespace { + +static bool autoFitMetricIsFinite(double value) +{ +#ifdef Q_OS_WIN + return _finite(value) != 0; +#else + return std::isfinite(value); +#endif +} + +static void autoFitAppendMetricReason(QString* reasons, + const QString& reason) +{ + if(!reasons || reason.isEmpty() || reasons->contains(reason)) { + return; + } + if(!reasons->isEmpty()) { + reasons->append(';'); + } + reasons->append(reason); +} + +struct AutoFitPressurePoint +{ + double timeHr; + double pressureMpa; +}; + +static bool autoFitPressurePointLess(const AutoFitPressurePoint& left, + const AutoFitPressurePoint& right) +{ + return left.timeHr < right.timeHr; +} + +// 清理压力曲线并按时间升序排列。相同时间只保留最后一个值,避免插值区间为零。 +static QVector autoFitNormalizePressureCurve( + const QVector >& pressureData) +{ + QVector points; + if(pressureData.size() < 2) { + return points; + } + + const int count = qMin(pressureData[0].size(), pressureData[1].size()); + points.reserve(count); + for(int i = 0; i < count; ++i) { + const double timeHr = pressureData[0][i]; + const double pressureMpa = pressureData[1][i]; + if(!autoFitMetricIsFinite(timeHr) || timeHr <= 0.0 || + !autoFitMetricIsFinite(pressureMpa)) { + continue; + } + AutoFitPressurePoint point; + point.timeHr = timeHr; + point.pressureMpa = pressureMpa; + points.append(point); + } + + std::sort(points.begin(), points.end(), autoFitPressurePointLess); + QVector uniquePoints; + uniquePoints.reserve(points.size()); + for(int i = 0; i < points.size(); ++i) { + if(!uniquePoints.isEmpty() && + qAbs(uniquePoints.last().timeHr - points[i].timeHr) <= + qMax(1.0e-14, points[i].timeHr * 1.0e-12)) { + uniquePoints.last() = points[i]; + } else { + uniquePoints.append(points[i]); + } + } + return uniquePoints; +} + +// 压力在 log10(t) 坐标中线性插值,与七算例统一误差协议保持一致。 +static bool autoFitInterpolatePressure( + const QVector& points, + double logTime, + double* pressureMpa) +{ + if(!pressureMpa || points.size() < 2) { + return false; + } + + const double timeHr = qPow(10.0, logTime); + if(timeHr < points.first().timeHr || timeHr > points.last().timeHr) { + return false; + } + + int left = 0; + int right = points.size() - 1; + while(right - left > 1) { + const int middle = left + (right - left) / 2; + if(points[middle].timeHr <= timeHr) { + left = middle; + } else { + right = middle; + } + } + + if(qAbs(timeHr - points[left].timeHr) <= + qMax(1.0e-14, timeHr * 1.0e-12)) { + *pressureMpa = points[left].pressureMpa; + return true; + } + if(qAbs(timeHr - points[right].timeHr) <= + qMax(1.0e-14, timeHr * 1.0e-12)) { + *pressureMpa = points[right].pressureMpa; + return true; + } + + const double leftLogTime = qLn(points[left].timeHr) / qLn(10.0); + const double rightLogTime = qLn(points[right].timeHr) / qLn(10.0); + const double denominator = rightLogTime - leftLogTime; + if(denominator <= 0.0) { + return false; + } + const double ratio = (logTime - leftLogTime) / denominator; + *pressureMpa = points[left].pressureMpa + + (points[right].pressureMpa - points[left].pressureMpa) * ratio; + return autoFitMetricIsFinite(*pressureMpa); +} + +// 在统一时间网格上按 Bourdet 三点公式计算 d(DeltaP)/d(ln t)。首尾点 +// 没有完整邻点,保持 NaN 并且不参与导数 RMSE。 +static bool autoFitCalculateBourdetDerivative( + const QVector& timeHr, + const QVector& deltaPMpa, + QVector* derivativeMpa, + QString* invalidReason, + bool* hasInvalidDerivative) +{ + if(hasInvalidDerivative) { + *hasInvalidDerivative = false; + } + if(!derivativeMpa || timeHr.size() != deltaPMpa.size() || + timeHr.size() < 3) { + if(invalidReason) { + *invalidReason = "INSUFFICIENT_DERIVATIVE_POINTS"; + } + return false; + } + + const double invalidValue = std::numeric_limits::quiet_NaN(); + derivativeMpa->fill(invalidValue, timeHr.size()); + for(int i = 1; i < timeHr.size() - 1; ++i) { + const double leftInterval = qLn(timeHr[i] / timeHr[i - 1]); + const double rightInterval = qLn(timeHr[i + 1] / timeHr[i]); + const double totalInterval = leftInterval + rightInterval; + if(!autoFitMetricIsFinite(leftInterval) || leftInterval <= 0.0 || + !autoFitMetricIsFinite(rightInterval) || rightInterval <= 0.0 || + !autoFitMetricIsFinite(totalInterval) || totalInterval <= 0.0) { + if(invalidReason) { + *invalidReason = "INVALID_TIME_INTERVAL"; + } + return false; + } + + const double leftSlope = + (deltaPMpa[i] - deltaPMpa[i - 1]) / leftInterval; + const double rightSlope = + (deltaPMpa[i + 1] - deltaPMpa[i]) / rightInterval; + const double derivative = + leftSlope * rightInterval / totalInterval + + rightSlope * leftInterval / totalInterval; + if(!autoFitMetricIsFinite(derivative) || derivative <= 0.0) { + // 单点异常不破坏其余 Bourdet 点。该位置保持 NaN,由调用方从 + // 双对数导数 RMSE 中剔除,同时保留整条曲线的数据质量失败标志。 + if(hasInvalidDerivative) { + *hasInvalidDerivative = true; + } + continue; + } + (*derivativeMpa)[i] = derivative; + } + return true; +} + +} // namespace + +AutoFitCurveMetrics::AutoFitCurveMetrics() + : valid(false) + , passed(false) + , coverage(std::numeric_limits::quiet_NaN()) + , pressureRmseMpa(std::numeric_limits::quiet_NaN()) + , pressureMaxAbsErrorMpa(std::numeric_limits::quiet_NaN()) + , logDeltaPRmseDecade(std::numeric_limits::quiet_NaN()) + , logDerivativeRmseDecade(std::numeric_limits::quiet_NaN()) + , unifiedCurveError(std::numeric_limits::quiet_NaN()) + , sampleCount(0) + , validDerivativeCount(0) +{ +} + +AutoFitParameterResult::AutoFitParameterResult() + : initialValue(std::numeric_limits::quiet_NaN()) + , lowerBound(std::numeric_limits::quiet_NaN()) + , upperBound(std::numeric_limits::quiet_NaN()) + , finalValue(std::numeric_limits::quiet_NaN()) +{ +} + +AutoFitRunResult::AutoFitRunResult() + : ompThreads(-1) + , iluReuseSteps(-1) + , optimizationWallTimeMs(-1) + , workflowWallTimeMs(-1) + , solverTimeSumMs(0) + , finalSolverTimeMs(-1) + , iterationCount(0) + , parameterEvaluationCount(0) + , modelSolverCallCount(0) + , finalSolverCallCount(0) + , solverSuccessCount(0) + , solverFailureCount(0) + , solverTimeoutCount(0) + , optimizationPebiCount(-1) + , finalPebiCount(-1) + , pebiCount(-1) + , finalSolverStatus("NOT_RUN") + , initialPressureMpa(std::numeric_limits::quiet_NaN()) + , initialInternalError(std::numeric_limits::quiet_NaN()) + , finalInternalError(std::numeric_limits::quiet_NaN()) +{ +} + +void AutoFitRunResult::recordOptimizationSolverCall(bool success, + bool timeout, + qint64 solveTimeMs, + int pebiCount) +{ + ++modelSolverCallCount; + if(timeout) { + ++solverTimeoutCount; + } else if(success) { + ++solverSuccessCount; + } else { + ++solverFailureCount; + } + if(solveTimeMs >= 0) { + solverTimeSumMs += solveTimeMs; + } + if(pebiCount >= 0) { + optimizationPebiCount = pebiCount; + this->pebiCount = pebiCount; + } +} + +void AutoFitRunResult::recordFinalSolverCall(bool success, + bool timeout, + qint64 solveTimeMs, + int pebiCount) +{ + ++finalSolverCallCount; + finalSolverTimeMs = solveTimeMs >= 0 ? solveTimeMs : -1; + finalPebiCount = pebiCount >= 0 ? pebiCount : -1; + if(pebiCount >= 0) { + this->pebiCount = pebiCount; + } + if(timeout) { + finalSolverStatus = "TIMEOUT"; + } else { + finalSolverStatus = success ? "SUCCESS" : "FAILED"; + } +} + +AutoFitRunResult nmCalculationAutoFitPSO::getLastRunResult() const +{ + return m_lastRunResult; +} + +AutoFitCurveMetrics nmCalculationAutoFitPSO::calculateUnifiedCurveMetrics( + const QVector >& targetPressureData, + const QVector >& fittedPressureData, + double initialPressureMpa, + int sampleCount) +{ + AutoFitCurveMetrics metrics; + if(!autoFitMetricIsFinite(initialPressureMpa)) { + metrics.invalidReason = "INVALID_INITIAL_PRESSURE"; + return metrics; + } + if(sampleCount < 3) { + metrics.invalidReason = "INVALID_SAMPLE_COUNT"; + return metrics; + } + + const QVector targetPoints = + autoFitNormalizePressureCurve(targetPressureData); + const QVector fittedPoints = + autoFitNormalizePressureCurve(fittedPressureData); + if(targetPoints.size() < 2) { + metrics.invalidReason = "MISSING_TARGET_PRESSURE_DATA"; + return metrics; + } + if(fittedPoints.size() < 2) { + metrics.invalidReason = "MISSING_FITTED_PRESSURE_DATA"; + return metrics; + } + + const double targetMinLogTime = + qLn(targetPoints.first().timeHr) / qLn(10.0); + const double targetMaxLogTime = + qLn(targetPoints.last().timeHr) / qLn(10.0); + const double fittedMinLogTime = + qLn(fittedPoints.first().timeHr) / qLn(10.0); + const double fittedMaxLogTime = + qLn(fittedPoints.last().timeHr) / qLn(10.0); + const double targetLogSpan = targetMaxLogTime - targetMinLogTime; + if(!autoFitMetricIsFinite(targetLogSpan) || targetLogSpan <= 0.0) { + metrics.invalidReason = "INVALID_TARGET_TIME_RANGE"; + return metrics; + } + + const double commonMinLogTime = qMax(targetMinLogTime, fittedMinLogTime); + const double commonMaxLogTime = qMin(targetMaxLogTime, fittedMaxLogTime); + const double commonLogSpan = commonMaxLogTime - commonMinLogTime; + metrics.coverage = qBound(0.0, commonLogSpan / targetLogSpan, 1.0); + if(!autoFitMetricIsFinite(commonLogSpan) || commonLogSpan <= 0.0) { + metrics.invalidReason = "NO_COMMON_TIME_RANGE"; + return metrics; + } + + metrics.sampleCount = sampleCount; + metrics.timeHr.reserve(sampleCount); + metrics.targetPressureMpa.reserve(sampleCount); + metrics.fittedPressureMpa.reserve(sampleCount); + metrics.targetDeltaPMpa.reserve(sampleCount); + metrics.fittedDeltaPMpa.reserve(sampleCount); + + double pressureSquaredSum = 0.0; + double pressureMaxAbsError = 0.0; + double logDeltaPSquaredSum = 0.0; + int validLogDeltaPCount = 0; + bool hasInvalidDeltaP = false; + for(int i = 0; i < sampleCount; ++i) { + const double ratio = sampleCount > 1 + ? static_cast(i) / static_cast(sampleCount - 1) + : 0.0; + const double logTime = commonMinLogTime + commonLogSpan * ratio; + double targetPressure = 0.0; + double fittedPressure = 0.0; + if(!autoFitInterpolatePressure(targetPoints, logTime, &targetPressure) || + !autoFitInterpolatePressure(fittedPoints, logTime, &fittedPressure)) { + metrics.invalidReason = "PRESSURE_INTERPOLATION_FAILED"; + return metrics; + } + + const double timeHr = qPow(10.0, logTime); + const double targetDeltaP = qAbs(initialPressureMpa - targetPressure); + const double fittedDeltaP = qAbs(initialPressureMpa - fittedPressure); + metrics.timeHr.append(timeHr); + metrics.targetPressureMpa.append(targetPressure); + metrics.fittedPressureMpa.append(fittedPressure); + metrics.targetDeltaPMpa.append(targetDeltaP); + metrics.fittedDeltaPMpa.append(fittedDeltaP); + + const double pressureResidual = fittedPressure - targetPressure; + pressureSquaredSum += pressureResidual * pressureResidual; + pressureMaxAbsError = qMax(pressureMaxAbsError, qAbs(pressureResidual)); + if(!autoFitMetricIsFinite(targetDeltaP) || targetDeltaP <= 0.0 || + !autoFitMetricIsFinite(fittedDeltaP) || fittedDeltaP <= 0.0) { + hasInvalidDeltaP = true; + continue; + } + + const double logResidual = + qLn(fittedDeltaP / targetDeltaP) / qLn(10.0); + if(autoFitMetricIsFinite(logResidual)) { + logDeltaPSquaredSum += logResidual * logResidual; + ++validLogDeltaPCount; + } else { + hasInvalidDeltaP = true; + } + } + + metrics.pressureRmseMpa = + qSqrt(pressureSquaredSum / static_cast(sampleCount)); + metrics.pressureMaxAbsErrorMpa = pressureMaxAbsError; + if(validLogDeltaPCount > 0) { + metrics.logDeltaPRmseDecade = qSqrt( + logDeltaPSquaredSum / + static_cast(validLogDeltaPCount)); + } else { + autoFitAppendMetricReason(&metrics.invalidReason, + "NO_VALID_LOG_DELTA_P_SAMPLES"); + } + if(hasInvalidDeltaP) { + autoFitAppendMetricReason(&metrics.invalidReason, + "NON_POSITIVE_OR_INVALID_DELTA_P"); + } + + QString targetDerivativeReason; + QString fittedDerivativeReason; + bool targetHasInvalidDerivative = false; + bool fittedHasInvalidDerivative = false; + if(!autoFitCalculateBourdetDerivative(metrics.timeHr, + metrics.targetDeltaPMpa, + &metrics.targetDerivativeMpa, + &targetDerivativeReason, + &targetHasInvalidDerivative)) { + autoFitAppendMetricReason(&metrics.invalidReason, + targetDerivativeReason); + return metrics; + } + if(!autoFitCalculateBourdetDerivative(metrics.timeHr, + metrics.fittedDeltaPMpa, + &metrics.fittedDerivativeMpa, + &fittedDerivativeReason, + &fittedHasInvalidDerivative)) { + autoFitAppendMetricReason(&metrics.invalidReason, + fittedDerivativeReason); + return metrics; + } + + double logDerivativeSquaredSum = 0.0; + for(int i = 1; i < sampleCount - 1; ++i) { + const double targetDerivative = metrics.targetDerivativeMpa[i]; + const double fittedDerivative = metrics.fittedDerivativeMpa[i]; + if(!autoFitMetricIsFinite(targetDerivative) || + targetDerivative <= 0.0 || + !autoFitMetricIsFinite(fittedDerivative) || + fittedDerivative <= 0.0) { + continue; + } + const double logResidual = + qLn(fittedDerivative / targetDerivative) / qLn(10.0); + if(!autoFitMetricIsFinite(logResidual)) { + targetHasInvalidDerivative = true; + continue; + } + logDerivativeSquaredSum += logResidual * logResidual; + ++metrics.validDerivativeCount; + } + + if(metrics.validDerivativeCount > 0) { + metrics.logDerivativeRmseDecade = qSqrt( + logDerivativeSquaredSum / + static_cast(metrics.validDerivativeCount)); + } else { + autoFitAppendMetricReason(&metrics.invalidReason, + "NO_VALID_LOG_DERIVATIVE_SAMPLES"); + } + if(targetHasInvalidDerivative || fittedHasInvalidDerivative) { + autoFitAppendMetricReason(&metrics.invalidReason, + "NON_POSITIVE_OR_INVALID_DERIVATIVE"); + } + if(autoFitMetricIsFinite(metrics.logDeltaPRmseDecade) && + autoFitMetricIsFinite(metrics.logDerivativeRmseDecade)) { + metrics.unifiedCurveError = qSqrt( + (metrics.logDeltaPRmseDecade * metrics.logDeltaPRmseDecade + + metrics.logDerivativeRmseDecade * metrics.logDerivativeRmseDecade) / + 2.0); + } + + if(metrics.coverage < 0.95) { + autoFitAppendMetricReason(&metrics.invalidReason, + "TIME_COVERAGE_BELOW_0_95"); + } + + metrics.valid = metrics.invalidReason.isEmpty(); + metrics.passed = metrics.valid && + metrics.logDeltaPRmseDecade <= 0.02 && + metrics.logDerivativeRmseDecade <= 0.02; + return metrics; +} diff --git a/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp b/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp index cb510f6..066fbdb 100644 --- a/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp @@ -38,6 +38,7 @@ const double nmCalculationAutoFitPSO::MIN_FITNESS_IMPROVEMENT = 1e-8; const double nmCalculationAutoFitPSO::VELOCITY_LIMIT_FACTOR = 0.2; const int nmCalculationAutoFitPSO::CONVERGENCE_CHECK_INTERVAL = 5; +const int nmCalculationAutoFitPSO::RUN_TIME_LIMIT_MS = 1800000; // ===== 代理模型筛选参数 ===== // @@ -104,6 +105,37 @@ static inline bool isFiniteNumber(double value) #endif } +// DLL 的线程状态成功只表示计算过程返回,不能说明数组可用于本次评价。 +// 时间允许包含求解器输出的 t=0 点,但至少要有一个正时间点供双对数处理。 +static bool isValidAutoFitPressureResult( + const QVector >& pressureData) +{ + if(pressureData.size() < 2 || pressureData[0].isEmpty() || + pressureData[0].size() != pressureData[1].size()) { + return false; + } + + double firstPositiveTime = -1.0; + bool hasDistinctPositiveTime = false; + for(int i = 0; i < pressureData[0].size(); ++i) { + const double timeHr = pressureData[0][i]; + const double pressureMpa = pressureData[1][i]; + if(!isFiniteNumber(timeHr) || timeHr < 0.0 || + !isFiniteNumber(pressureMpa)) { + return false; + } + if(timeHr > 0.0) { + if(firstPositiveTime < 0.0) { + firstPositiveTime = timeHr; + } else if(qAbs(timeHr - firstPositiveTime) > + qMax(1.0e-14, timeHr * 1.0e-12)) { + hasDistinctPositiveTime = true; + } + } + } + return hasDistinctPositiveTime; +} + // 两个 RMSE 的差不能直接解释为被消除的独立误差。RMSE 的平方才对应 // 均方能量,因此先计算 reduced^2-full^2,再开方恢复原量纲。这里用于分别 // 提取“消除公共上下偏差”和“消除水平位移”实际减少的误差贡献。 @@ -429,7 +461,7 @@ static QString findExecutableInPath(const QString& executableName) return QString(); } -static QStringList traceParameterNames() +QStringList nmCalculationAutoFitPSO::traceParameterNames() { // trace 和 trace meta 使用的完整参数名顺序。 // 这个顺序必须与 buildTraceParameterVector() 和 m_parameterSelected 的 0-9 索引一致。 @@ -454,8 +486,10 @@ nmCalculationAutoFitPSO::nmCalculationAutoFitPSO(QObject* parent) : QObject(parent) , m_isRunning(false) , m_shouldStop(false) + , m_runTimedOut(false) , m_isPaused(false) , m_currentIteration(0) + , m_completedIterationCount(0) , m_globalBestFitness(1e10) , m_previousBestFitness(1e10) , m_swarmSize(20) @@ -466,6 +500,7 @@ nmCalculationAutoFitPSO::nmCalculationAutoFitPSO(QObject* parent) , m_socialParam(1.2) , m_totalEvaluations(0) , m_successfulEvaluations(0) + , m_runTimingStarted(false) , m_evaluationInProgress(0) , m_consecutiveFailures(0) , m_improvementThreshold(0.05) @@ -730,6 +765,14 @@ void nmCalculationAutoFitPSO::setTargetLogLogData(const QVector } } +void nmCalculationAutoFitPSO::setTargetPressureData( + const QVector >& targetData) +{ + // 原始压力曲线仅供独立统一误差使用;运行开始时会再复制到冻结快照, + // 避免拟合过程中界面数据刷新改变本次基准。 + m_targetPressureData = targetData; +} + void nmCalculationAutoFitPSO::stopFitting() { @@ -751,9 +794,16 @@ void nmCalculationAutoFitPSO::stopFitting() if(m_simulationMode) { m_isRunning = false; + m_shouldStop = true; + m_globalBestPosition = calculateSimulatedParams(m_simulationIteration); + m_globalBestFitness = m_simulationCurrentError; + m_completedIterationCount = qMax(0, m_simulationIteration); + markOptimizationFinished(); + finalizeRunResult("STOPPED", "USER_STOPPED_SIMULATION"); closeTraceFile(); cleanupTemporaryDirectory(); emit logMessageGenerated(tr("PSO simulation stop request processed")); + emit fittingFinished(true, tr("Simulation stopped by user")); return; } @@ -829,10 +879,14 @@ void nmCalculationAutoFitPSO::resetOptimizer() m_globalBestObjectiveBreakdown = AutoFitObjectiveBreakdown(); m_lastEvaluatedLogLogData.clear(); m_globalBestLogLogData.clear(); + m_lastEvaluatedPressureData.clear(); + m_globalBestPressureData.clear(); m_lastObjectiveBreakdown = AutoFitObjectiveBreakdown(); m_userInitialLogLogData.clear(); + m_userInitialPressureData.clear(); m_userInitialObjectiveBreakdown = AutoFitObjectiveBreakdown(); m_currentIteration = 0; + m_completedIterationCount = 0; m_totalEvaluations = 0; m_successfulEvaluations = 0; m_convergenceHistory.clear(); @@ -870,7 +924,10 @@ void nmCalculationAutoFitPSO::initializeTraceFile() closeTraceFile(); captureSurrogateRunContextSummary(); - m_traceRunId = QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss_zzz"); + m_traceRunId = m_lastRunResult.runId; + if(m_traceRunId.isEmpty()) { + m_traceRunId = QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss_zzz"); + } QDir traceDir(QDir(getMlRootPath()).absoluteFilePath("data/temp")); if(!traceDir.exists() && !QDir().mkpath(traceDir.absolutePath())) { @@ -963,10 +1020,12 @@ void nmCalculationAutoFitPSO::emitRunSummary(bool success, StopReasonPSO finalRe { // 运行结束时输出一组交接/排障最有用的摘要: // 真实求解次数、成功失败数、代理模型启用情况、各类筛选决策计数,以及 trace 文件路径。 - emit logMessageGenerated(tr("=== PSO Run Summary ===")); + emit logMessageGenerated(tr("=== Automatic Fitting 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_lastRunResult.status.isEmpty() + ? (success ? "SUCCESS" : "FAILED") + : m_lastRunResult.status) .arg(m_globalBestFitness, 0, 'e', 4) .arg(m_currentIteration + 1) .arg(m_totalEvaluations) @@ -1602,6 +1661,12 @@ bool nmCalculationAutoFitPSO::runSurrogateScoringProcess(const QString& candidat if(requestSurrogateScoresFromServer(candidatePath, scorePath, &serverFailure)) { return true; } + if(m_runTimedOut) { + if(failureReason) { + *failureReason = "automatic fitting run time limit reached"; + } + return false; + } emit logMessageGenerated(tr("Surrogate scoring server unavailable; falling back to one-shot Python scoring")); @@ -1676,7 +1741,10 @@ bool nmCalculationAutoFitPSO::ensureSurrogateScoringServer(QString* failureReaso m_surrogateScoringProcess->start(pythonPath, args); - if(!m_surrogateScoringProcess->waitForStarted(15000)) { + const int startWait = qMin(15000, remainingRunTimeMs()); + if(startWait <= 0 || + !m_surrogateScoringProcess->waitForStarted(startWait)) { + isRunTimeLimitReached(); QString reason = QString("failed to start scoring server: python=%1, script=%2, error=%3") .arg(pythonPath) .arg(scriptPath) @@ -1709,7 +1777,8 @@ bool nmCalculationAutoFitPSO::ensureSurrogateScoringServer(QString* failureReaso timer.start(); QByteArray readyLine; - while(timer.elapsed() < 120000) { + const int readyWaitLimit = qMin(120000, remainingRunTimeMs()); + while(timer.elapsed() < readyWaitLimit && !isRunTimeLimitReached()) { // server 启动协议:Python 首先输出一行 JSON,包含 ready=true 或 ok=false。 // C++ 不做完整 JSON 解析,只检查关键字段,降低 Qt 版本依赖。 if(m_surrogateScoringProcess->state() != QProcess::Running) { @@ -1729,7 +1798,11 @@ bool nmCalculationAutoFitPSO::ensureSurrogateScoringServer(QString* failureReaso } if(!m_surrogateScoringProcess->canReadLine()) { - m_surrogateScoringProcess->waitForReadyRead(500); + const int readWait = qMin(500, remainingRunTimeMs()); + if(readWait <= 0) { + break; + } + m_surrogateScoringProcess->waitForReadyRead(readWait); QApplication::processEvents(); continue; } @@ -1761,7 +1834,9 @@ bool nmCalculationAutoFitPSO::ensureSurrogateScoringServer(QString* failureReaso } QString stdErr = QString::fromLocal8Bit(m_surrogateScoringProcess->readAllStandardError()).trimmed(); - QString reason = QString("scoring server ready timeout: stderr=%1") + QString reason = m_runTimedOut + ? QString("automatic fitting run time limit reached while waiting for scoring server") + : QString("scoring server ready timeout: stderr=%1") .arg(stdErr.isEmpty() ? QString("") : stdErr); DEBUG_OUT(reason); stopSurrogateScoringServer(); @@ -1791,7 +1866,10 @@ bool nmCalculationAutoFitPSO::requestSurrogateScoresFromServer(const QString& ca QByteArray bytes = command.toUtf8(); qint64 written = m_surrogateScoringProcess->write(bytes); - if(written != bytes.size() || !m_surrogateScoringProcess->waitForBytesWritten(10000)) { + const int writeWait = qMin(10000, remainingRunTimeMs()); + if(written != bytes.size() || writeWait <= 0 || + !m_surrogateScoringProcess->waitForBytesWritten(writeWait)) { + isRunTimeLimitReached(); QString reason = QString("failed to send scoring request to server: written=%1 expected=%2 error=%3") .arg(written) .arg(bytes.size()) @@ -1808,7 +1886,8 @@ bool nmCalculationAutoFitPSO::requestSurrogateScoresFromServer(const QString& ca QTime timer; timer.start(); - while(timer.elapsed() < 120000) { + const int scoringWaitLimit = qMin(120000, remainingRunTimeMs()); + while(timer.elapsed() < scoringWaitLimit && !isRunTimeLimitReached()) { // 每次请求最多等待 120 秒。超时或 server 退出都会让本代代理筛选降级为全量真实求解器。 if(m_surrogateScoringProcess->state() != QProcess::Running) { QString stdOut = QString::fromLocal8Bit(m_surrogateScoringProcess->readAllStandardOutput()).trimmed(); @@ -1826,7 +1905,11 @@ bool nmCalculationAutoFitPSO::requestSurrogateScoresFromServer(const QString& ca } if(!m_surrogateScoringProcess->canReadLine()) { - m_surrogateScoringProcess->waitForReadyRead(500); + const int readWait = qMin(500, remainingRunTimeMs()); + if(readWait <= 0) { + break; + } + m_surrogateScoringProcess->waitForReadyRead(readWait); QApplication::processEvents(); continue; } @@ -1869,7 +1952,9 @@ bool nmCalculationAutoFitPSO::requestSurrogateScoresFromServer(const QString& ca } } - QString reason = "scoring server request timed out"; + QString reason = m_runTimedOut + ? "automatic fitting run time limit reached during scoring request" + : "scoring server request timed out"; DEBUG_OUT(reason); if(failureReason) { @@ -1956,6 +2041,12 @@ bool nmCalculationAutoFitPSO::runSurrogateScoringScriptOnce(const QString& candi QString lastFailure; for(int attempt = 1; attempt <= kSurrogateScoringMaxAttempts; ++attempt) { + if(isRunTimeLimitReached()) { + if(failureReason) { + *failureReason = "automatic fitting run time limit reached before scoring process start"; + } + return false; + } // 评分脚本可能因首次加载模型、文件占用或环境抖动失败,因此允许有限重试。 emit logMessageGenerated(tr("Surrogate scoring attempt %1/%2: python=%3, mlRoot=%4") .arg(attempt) @@ -1967,7 +2058,9 @@ bool nmCalculationAutoFitPSO::runSurrogateScoringScriptOnce(const QString& candi process.setWorkingDirectory(mlRoot); process.start(pythonPath, args); - if(!process.waitForStarted(10000)) { + const int startWait = qMin(10000, remainingRunTimeMs()); + if(startWait <= 0 || !process.waitForStarted(startWait)) { + isRunTimeLimitReached(); lastFailure = QString("failed to start scoring process (attempt %1/%2): python=%3, script=%4, error=%5") .arg(attempt) .arg(kSurrogateScoringMaxAttempts) @@ -1976,7 +2069,7 @@ bool nmCalculationAutoFitPSO::runSurrogateScoringScriptOnce(const QString& candi .arg(process.errorString()); DEBUG_OUT(lastFailure); - if(attempt < kSurrogateScoringMaxAttempts) { + if(attempt < kSurrogateScoringMaxAttempts && !m_runTimedOut) { emit logMessageGenerated(tr("Surrogate scoring start failed, retrying once: %1").arg(process.errorString())); msleep(500); continue; @@ -1989,12 +2082,16 @@ bool nmCalculationAutoFitPSO::runSurrogateScoringScriptOnce(const QString& candi return false; } - if(!process.waitForFinished(120000)) { + const int finishWait = qMin(120000, remainingRunTimeMs()); + if(finishWait <= 0 || !process.waitForFinished(finishWait)) { + isRunTimeLimitReached(); process.kill(); process.waitForFinished(3000); QString stdOut = QString::fromLocal8Bit(process.readAllStandardOutput()).trimmed(); QString stdErr = QString::fromLocal8Bit(process.readAllStandardError()).trimmed(); - lastFailure = QString("scoring process timed out (attempt %1/%2): python=%3, script=%4, stdout=%5, stderr=%6") + lastFailure = m_runTimedOut + ? QString("automatic fitting run time limit reached during scoring process") + : QString("scoring process timed out (attempt %1/%2): python=%3, script=%4, stdout=%5, stderr=%6") .arg(attempt) .arg(kSurrogateScoringMaxAttempts) .arg(pythonPath) @@ -2003,7 +2100,7 @@ bool nmCalculationAutoFitPSO::runSurrogateScoringScriptOnce(const QString& candi .arg(stdErr.isEmpty() ? QString("") : stdErr); DEBUG_OUT(lastFailure); - if(attempt < kSurrogateScoringMaxAttempts) { + if(attempt < kSurrogateScoringMaxAttempts && !m_runTimedOut) { emit logMessageGenerated(tr("Surrogate scoring timed out, retrying once")); msleep(500); continue; @@ -2031,7 +2128,7 @@ bool nmCalculationAutoFitPSO::runSurrogateScoringScriptOnce(const QString& candi .arg(stdErr.isEmpty() ? QString("") : stdErr); DEBUG_OUT(lastFailure); - if(attempt < kSurrogateScoringMaxAttempts) { + if(attempt < kSurrogateScoringMaxAttempts && !m_runTimedOut) { emit logMessageGenerated(tr("Surrogate scoring process failed, retrying once: exitCode=%1").arg(process.exitCode())); msleep(500); continue; @@ -2055,7 +2152,7 @@ bool nmCalculationAutoFitPSO::runSurrogateScoringScriptOnce(const QString& candi .arg(stdErr.isEmpty() ? QString("") : stdErr); DEBUG_OUT(lastFailure); - if(attempt < kSurrogateScoringMaxAttempts) { + if(attempt < kSurrogateScoringMaxAttempts && !m_runTimedOut) { emit logMessageGenerated(tr("Surrogate scoring produced no score file, retrying once")); msleep(500); continue; @@ -3005,15 +3102,22 @@ bool nmCalculationAutoFitPSO::startAutoFitting() return false; } + // 每次正式入口先创建唯一 RunID 和冻结目标压力。后续任何验证失败或异常 + // 都使用同一结果对象收口,保证失败路径同样能够留下结构化证据。 + initializeRunResult(); + try { // 从 DataManager 读取界面保存的自动拟合配置。 // 本类不直接依赖 UI 控件,便于后续从脚本或其他入口复用。 if(!loadAllConfigFromDataManager()) { emit logMessageGenerated(tr("ERROR: Failed to load configuration from data manager")); + finalizeRunResult("FAILED", m_lastError); + emit fittingFinished(false, m_lastError); return false; } useParticleSwarm = isSurrogateScreeningEnabled(); + captureRunConfiguration(useParticleSwarm); emit logMessageGenerated(useParticleSwarm ? tr("Algorithm: Particle Swarm Optimization") : tr("Algorithm: Diagnostic Trust-Region Search")); @@ -3031,12 +3135,16 @@ bool nmCalculationAutoFitPSO::startAutoFitting() if(enabledParams == 0) { m_lastError = "No parameters enabled for optimization"; emit logMessageGenerated(tr("ERROR: No parameters enabled for optimization")); + finalizeRunResult("FAILED", m_lastError); + emit fittingFinished(false, m_lastError); return false; } if(m_targetLogLogData.isEmpty() || m_targetLogLogData.size() < 3) { m_lastError = "Target LogLog data is empty or insufficient"; emit logMessageGenerated(tr("ERROR: Target LogLog data is empty or insufficient")); + finalizeRunResult("FAILED", m_lastError); + emit fittingFinished(false, m_lastError); return false; } @@ -3046,6 +3154,8 @@ bool nmCalculationAutoFitPSO::startAutoFitting() 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")); + finalizeRunResult("FAILED", m_lastError); + emit fittingFinished(false, m_lastError); return false; } @@ -3054,6 +3164,8 @@ bool nmCalculationAutoFitPSO::startAutoFitting() if(m_targetWellName.isEmpty()) { m_lastError = "Target well name is empty"; emit logMessageGenerated(tr("ERROR: Target well name is empty")); + finalizeRunResult("FAILED", m_lastError); + emit fittingFinished(false, m_lastError); return false; } @@ -3110,6 +3222,10 @@ bool nmCalculationAutoFitPSO::startAutoFitting() } } + // 两类墙钟从首次参数评价前同时启动。workflow 继续跨过最终全场计算, + // 文件写出和界面消息均发生在 finalizeRunResult() 计时结束之后。 + beginRunTiming(); + if(!savedInitialValues.isEmpty()) { m_userInitialSolution = savedInitialValues; emit logMessageGenerated(tr("=== Evaluating Initial Solution (Elite Protection) ===")); @@ -3129,13 +3245,19 @@ bool nmCalculationAutoFitPSO::startAutoFitting() if(m_userInitialSolution.isEmpty()) { emit logMessageGenerated(tr("ERROR: m_userInitialSolution is empty!")); + m_lastError = "Initial parameter vector is empty"; + finalizeRunResult("FAILED", m_lastError); + m_isRunning = false; + closeTraceFile(); + emit fittingFinished(false, m_lastError); + cleanupTemporaryDirectory(); return false; } QTime initialEvalTimer; initialEvalTimer.start(); - // 初始精英解也是一次真实 DLL 调用。第一代第0号粒子会复用它, - // 因此在这里计数,保证汇总中的评价次数等于实际求解调用数。 + // 初始精英解是一次参数评价。第一代第0号粒子会复用它,因此只在 + // 这里计一次;失败重试产生的额外 DLL 调用由结构化结果另行累计。 m_totalEvaluations++; m_userInitialFitness = evaluateFitness(m_userInitialSolution); int initialEvalElapsedMs = initialEvalTimer.elapsed(); @@ -3146,7 +3268,9 @@ bool nmCalculationAutoFitPSO::startAutoFitting() m_globalBestFitness = m_userInitialFitness; m_globalBestPosition = m_userInitialSolution; m_userInitialLogLogData = m_lastEvaluatedLogLogData; + m_userInitialPressureData = m_lastEvaluatedPressureData; m_globalBestLogLogData = m_userInitialLogLogData; + m_globalBestPressureData = m_userInitialPressureData; m_userInitialObjectiveBreakdown = m_lastObjectiveBreakdown; m_globalBestObjectiveBreakdown = m_userInitialObjectiveBreakdown; @@ -3181,7 +3305,9 @@ bool nmCalculationAutoFitPSO::startAutoFitting() m_initialValues = savedInitialValues; } - if(useParticleSwarm) { + if(m_runTimedOut) { + finalReason = PSO_TIME_LIMIT_REACHED; + } else if(useParticleSwarm) { // 初始化粒子群。粒子维度等于用户勾选的参数数量,而不是固定 11 维。 if(kUseFixedPsoSeed) { emit logMessageGenerated(tr("PSO random seed: %1 ").arg(m_psoRandomSeed)); @@ -3195,7 +3321,12 @@ bool nmCalculationAutoFitPSO::startAutoFitting() // PSO主循环:每一代先决定哪些粒子需要真实评价,再更新全局最优和粒子位置。 emit logMessageGenerated(tr("=== Starting PSO Main Loop ===")); - for(m_currentIteration = 0; m_currentIteration < m_maxIterations && !m_shouldStop; ++m_currentIteration) { + for(m_currentIteration = 0; + m_currentIteration < m_maxIterations && + !m_shouldStop && + !m_runTimedOut; + ++m_currentIteration) { + m_completedIterationCount = m_currentIteration + 1; // 每次迭代都输出标题,或者只在重要迭代输出详细信息 bool shouldOutputDetail = (m_currentIteration % qMax(1, m_maxIterations / 10) == 0) || (m_currentIteration < 5) || @@ -3212,11 +3343,16 @@ bool nmCalculationAutoFitPSO::startAutoFitting() } // 检查暂停状态 - while(m_isPaused && !m_shouldStop) { + while(m_isPaused && !m_shouldStop && !isRunTimeLimitReached()) { QApplication::processEvents(); msleep(100); } + if(isRunTimeLimitReached()) { + emit logMessageGenerated(tr("Automatic fitting stopped after reaching the run time limit")); + break; + } + if(m_shouldStop) { emit logMessageGenerated(tr("Optimization stopped by user request")); break; @@ -3228,7 +3364,9 @@ bool nmCalculationAutoFitPSO::startAutoFitting() int currentIterationSkipped = 0; QVector evaluateMask = buildSurrogateEvaluationMask(); - for(int i = 0; i < m_swarmSize && !m_shouldStop; ++i) { + for(int i = 0; + i < m_swarmSize && !m_shouldStop && !m_runTimedOut; + ++i) { try { if(i < evaluateMask.size() && !evaluateMask[i]) { // 被代理模型筛掉的粒子本代不跑真实求解器。 @@ -3287,7 +3425,6 @@ bool nmCalculationAutoFitPSO::startAutoFitting() m_swarm[i].fitness = 1e10; currentIterationTotal++; currentIterationFailed++; - m_totalEvaluations++; } catch(...) { if(shouldOutputDetail) { emit logMessageGenerated(tr(" Particle %1: Unknown exception").arg(i + 1)); @@ -3296,11 +3433,10 @@ bool nmCalculationAutoFitPSO::startAutoFitting() m_swarm[i].fitness = 1e10; currentIterationTotal++; currentIterationFailed++; - m_totalEvaluations++; } } - if(m_shouldStop) break; + if(m_runTimedOut || m_shouldStop) break; double currentSuccessRate = currentIterationTotal > 0 ? (double)currentIterationSuccessful / currentIterationTotal : 0.0; @@ -3451,7 +3587,7 @@ bool nmCalculationAutoFitPSO::startAutoFitting() &m_globalBestObjectiveBreakdown); } - if(m_globalBestFitness < m_targetError) { + if(!m_runTimedOut && m_globalBestFitness < m_targetError) { finalReason = PSO_TARGET_ACHIEVED; } @@ -3460,23 +3596,29 @@ bool nmCalculationAutoFitPSO::startAutoFitting() ? QString(tr("Critical exception in PSO main loop: %1")).arg(e.what()) : 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; + finalizeRunResult("FAILED", m_lastError); + closeTraceFile(); emit fittingFinished(false, m_lastError); + cleanupTemporaryDirectory(); return false; } catch(...) { m_lastError = useParticleSwarm ? QString(tr("Unknown critical exception in PSO main loop")) : QString(tr("Unknown critical exception in automatic fitting")); emit logMessageGenerated(tr("CRITICAL ERROR: Unknown exception in PSO main loop")); - closeTraceFile(); - cleanupTemporaryDirectory(); m_isRunning = false; + finalizeRunResult("FAILED", m_lastError); + closeTraceFile(); emit fittingFinished(false, m_lastError); + cleanupTemporaryDirectory(); return false; } + // 精英保护完成后最优参数已经确定;从这里开始的参数回写和最终全场求解 + // 不计入与 Saphir Auto Match 对比的 optimization_wall_time_ms。 + markOptimizationFinished(); + bool finalFullSolverSucceeded = true; bool finalFullSolverExecuted = false; @@ -3491,14 +3633,17 @@ bool nmCalculationAutoFitPSO::startAutoFitting() const bool fractureGridParameterSelected = (m_parameterSelected.size() > 8 && m_parameterSelected[8]) || (m_parameterSelected.size() > 9 && m_parameterSelected[9]); - if(fractureGridParameterSelected) { + if(fractureGridParameterSelected && !isRunTimeLimitReached()) { nmCalculationPebiGrid* pebiGrid = nmCalculationPebiGrid::getInstance(); if(!pebiGrid || !pebiGrid->generateOutputPara()) { throw std::runtime_error("Failed to refresh final fracture parameters"); } } - if(m_shouldStop) { + if(m_runTimedOut) { + // 预算已经耗尽时只保留已确认参数,不再启动最终全场求解。 + emit logMessageGenerated(tr("Final full-field calculation skipped after run time limit")); + } else if(m_shouldStop) { // 手动停止优先保持快速返回,仅写回已确认的最优参数。 emit logMessageGenerated(tr("Final full-field calculation skipped after user stop")); } else { @@ -3510,6 +3655,9 @@ bool nmCalculationAutoFitPSO::startAutoFitting() if(finalFullSolverSucceeded) { emit logMessageGenerated(tr("Final full-field calculation completed successfully")); + } else if(m_runTimedOut) { + emit logMessageGenerated(tr("Final full-field calculation reached the run time limit")); + m_lastError = tr("Automatic fitting exceeded the run time limit"); } else if(m_shouldStop) { finalFullSolverExecuted = false; finalFullSolverSucceeded = true; @@ -3520,6 +3668,9 @@ bool nmCalculationAutoFitPSO::startAutoFitting() } } + // workflow 墙钟到最终全场计算返回为止。后续结果日志和结构化文件写出 + // 均不能进入该耗时,确保同一字段在不同界面和批量汇总中口径一致。 + markWorkflowFinished(); saveOptimizationResult(); // 输出最终优化结果 @@ -3553,14 +3704,24 @@ bool nmCalculationAutoFitPSO::startAutoFitting() m_lastError = "Failed to apply final parameters due to unknown error"; } } + // 没有可应用的最优参数或最终应用阶段发生异常时,也在这里冻结工作流耗时。 + // 正常路径已经提前冻结,该方法重复调用不会改变第一次记录的值。 + markWorkflowFinished(); m_isRunning = false; // 判断系统确定最终结果 + if(m_runTimedOut) { + finalReason = PSO_TIME_LIMIT_REACHED; + } bool success; QString message; - if(finalReason == PSO_TARGET_ACHIEVED) { + if(finalReason == PSO_TIME_LIMIT_REACHED || m_runTimedOut) { + success = false; + message = tr("Automatic fitting exceeded the run time limit"); + emit logMessageGenerated(tr("=== AUTOMATIC FITTING TIMED OUT ===")); + } else 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); @@ -3623,11 +3784,41 @@ bool nmCalculationAutoFitPSO::startAutoFitting() : tr("=== AUTOMATIC FITTING - UNKNOWN END ===")); } - if(!finalFullSolverSucceeded) { + if(!finalFullSolverSucceeded && !m_runTimedOut) { success = false; message = m_lastError; } + QString structuredStatus; + if(m_runTimedOut || finalReason == PSO_TIME_LIMIT_REACHED) { + structuredStatus = "TIMEOUT"; + } else if(!finalFullSolverSucceeded || + finalReason == PSO_CONSECUTIVE_FAILURES || + finalReason == PSO_OPTIMIZATION_FAILED) { + structuredStatus = "FAILED"; + } else if(finalReason == PSO_USER_STOPPED || m_shouldStop) { + structuredStatus = "STOPPED"; + } else if(finalReason == PSO_TARGET_ACHIEVED) { + structuredStatus = "SUCCESS"; + } else { + // 达到迭代上限、稳定收敛和局部最优都属于正常完成,但不能与 + // 达到目标误差混写为 SUCCESS。 + structuredStatus = "COMPLETED"; + } + QString structuredStopReason; + if(m_runTimedOut || finalReason == PSO_TIME_LIMIT_REACHED) { + structuredStopReason = getStopReasonDescription(PSO_TIME_LIMIT_REACHED); + } else if(!finalFullSolverSucceeded && !m_lastError.isEmpty()) { + structuredStopReason = m_lastError; + } else if(m_shouldStop && finalReason != PSO_USER_STOPPED) { + structuredStopReason = m_lastRunResult.finalSolverStatus == "STOPPED" + ? "USER_STOPPED_DURING_FINAL_FULL_FIELD_SOLVE" + : "USER_STOPPED_AFTER_OPTIMIZATION"; + } else { + structuredStopReason = getStopReasonDescription(finalReason); + } + finalizeRunResult(structuredStatus, structuredStopReason); + emitRunSummary(success, finalReason); // 先发送最终进度更新,确保进度条达到100% @@ -3921,6 +4112,7 @@ struct TrustRegionEvaluation QVector coordinates; AutoFitObjectiveBreakdown breakdown; QVector > curve; + QVector > pressureCurve; double fitness; int elapsedMs; bool valid; @@ -4187,9 +4379,11 @@ bool nmCalculationAutoFitPSO::evaluateTrustRegionPoint( double* fitness, AutoFitObjectiveBreakdown* breakdown, QVector >* curve, + QVector >* pressureCurve, int* elapsedMs) { - if(!fitness || !breakdown || !curve || !elapsedMs || m_shouldStop) { + if(!fitness || !breakdown || !curve || !pressureCurve || + !elapsedMs || m_shouldStop || isRunTimeLimitReached()) { return false; } @@ -4201,6 +4395,7 @@ bool nmCalculationAutoFitPSO::evaluateTrustRegionPoint( *elapsedMs = timer.elapsed(); *breakdown = m_lastObjectiveBreakdown; *curve = m_lastEvaluatedLogLogData; + *pressureCurve = m_lastEvaluatedPressureData; ++m_totalEvaluations; bool valid = isFiniteNumber(*fitness) && *fitness < 1.0e9 && @@ -4297,6 +4492,7 @@ StopReasonPSO nmCalculationAutoFitPSO::runTrustRegionFitting() applyParametersToDataManager(evaluation.parameters); m_lastObjectiveBreakdown = evaluation.breakdown; m_lastEvaluatedLogLogData = evaluation.curve; + m_lastEvaluatedPressureData = evaluation.pressureCurve; }; // 只有真实总误差更小的工作点才能发布为全局最优;曲线和诊断快照必须 @@ -4307,6 +4503,7 @@ StopReasonPSO nmCalculationAutoFitPSO::runTrustRegionFitting() m_globalBestFitness = evaluation.fitness; m_globalBestObjectiveBreakdown = evaluation.breakdown; m_globalBestLogLogData = evaluation.curve; + m_globalBestPressureData = evaluation.pressureCurve; emit bestCurveUpdated(m_targetLogLogData, m_globalBestLogLogData, m_currentIteration + 1, @@ -4314,12 +4511,12 @@ StopReasonPSO nmCalculationAutoFitPSO::runTrustRegionFitting() }; auto processPauseAndStop = [&]() -> bool { - while(m_isPaused && !m_shouldStop) { + while(m_isPaused && !m_shouldStop && !isRunTimeLimitReached()) { QApplication::processEvents(); msleep(100); } QApplication::processEvents(); - return !m_shouldStop; + return !m_shouldStop && !isRunTimeLimitReached(); }; // current 始终代表唯一已接受工作点。优先复用启动阶段已经真实验证的 @@ -4333,6 +4530,7 @@ StopReasonPSO nmCalculationAutoFitPSO::runTrustRegionFitting() current.coordinates = coordinatesFromParameters(current.parameters); current.breakdown = m_globalBestObjectiveBreakdown; current.curve = m_globalBestLogLogData; + current.pressureCurve = m_globalBestPressureData; current.fitness = m_globalBestFitness; current.elapsedMs = 0; current.valid = true; @@ -4346,6 +4544,7 @@ StopReasonPSO nmCalculationAutoFitPSO::runTrustRegionFitting() ¤t.fitness, ¤t.breakdown, ¤t.curve, + ¤t.pressureCurve, ¤t.elapsedMs); writeTraceRow(-1, -1, "trust_region_midpoint", @@ -4360,9 +4559,10 @@ StopReasonPSO nmCalculationAutoFitPSO::runTrustRegionFitting() 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; + if(m_runTimedOut) { + return PSO_TIME_LIMIT_REACHED; + } + return m_shouldStop ? PSO_USER_STOPPED : PSO_OPTIMIZATION_FAILED; } publishAcceptedPoint(current); } @@ -4487,6 +4687,7 @@ StopReasonPSO nmCalculationAutoFitPSO::runTrustRegionFitting() &probe.fitness, &probe.breakdown, &probe.curve, + &probe.pressureCurve, &probe.elapsedMs); QString decision = probe.valid @@ -4569,7 +4770,7 @@ StopReasonPSO nmCalculationAutoFitPSO::runTrustRegionFitting() ++validColumnCount; } } - if(validColumnCount == 0 || m_shouldStop) { + if(validColumnCount == 0 || m_shouldStop || m_runTimedOut) { restoreEvaluationState(base); return false; } @@ -4649,10 +4850,12 @@ StopReasonPSO nmCalculationAutoFitPSO::runTrustRegionFitting() for(int iteration = 0; iteration < m_maxIterations && m_totalEvaluations < maximumEvaluations && - !m_shouldStop; + !m_shouldStop && + !m_runTimedOut; ++iteration) { m_currentIteration = iteration; completedIterations = iteration + 1; + m_completedIterationCount = completedIterations; if(!processPauseAndStop()) { break; @@ -4661,9 +4864,11 @@ StopReasonPSO nmCalculationAutoFitPSO::runTrustRegionFitting() const bool confirmingStagnation = stagnationConfirmationRequested; if(!rebuildSensitivity()) { - stopReason = m_shouldStop - ? PSO_USER_STOPPED - : PSO_LOCAL_OPTIMUM; + stopReason = m_runTimedOut + ? PSO_TIME_LIMIT_REACHED + : (m_shouldStop + ? PSO_USER_STOPPED + : PSO_LOCAL_OPTIMUM); break; } if(current.fitness < m_targetError) { @@ -4950,6 +5155,7 @@ StopReasonPSO nmCalculationAutoFitPSO::runTrustRegionFitting() &candidate.fitness, &candidate.breakdown, &candidate.curve, + &candidate.pressureCurve, &candidate.elapsedMs); if(!candidate.valid) { @@ -5127,6 +5333,9 @@ StopReasonPSO nmCalculationAutoFitPSO::runTrustRegionFitting() } restoreEvaluationState(current); + if(m_runTimedOut) { + return PSO_TIME_LIMIT_REACHED; + } if(m_shouldStop) { return PSO_USER_STOPPED; } @@ -5176,6 +5385,7 @@ void nmCalculationAutoFitPSO::updateParticle(int particleIndex) // 直接复用真实误差和曲线,避免一次重复 DLL 调用且不改变 PSO 数学状态。 particle.fitness = m_userInitialFitness; particle.currentLogLogData = m_userInitialLogLogData; + particle.currentPressureData = m_userInitialPressureData; particle.currentObjectiveBreakdown = m_userInitialObjectiveBreakdown; particle.lastEvaluationElapsedMs = 0; particle.evaluatedThisIteration = true; @@ -5185,13 +5395,16 @@ void nmCalculationAutoFitPSO::updateParticle(int particleIndex) } else { QTime evalTimer; evalTimer.start(); + // 参数评价次数在进入目标函数前确定;内部最多三次 DLL 重试只增加 + // modelSolverCallCount,不重复增加本计数。 + m_totalEvaluations++; particle.fitness = evaluateFitness(particle.position); particle.currentLogLogData = m_lastEvaluatedLogLogData; + particle.currentPressureData = m_lastEvaluatedPressureData; particle.currentObjectiveBreakdown = m_lastObjectiveBreakdown; particle.lastEvaluationElapsedMs = evalTimer.elapsed(); particle.evaluatedThisIteration = true; particle.lastEvaluationSuccess = (particle.fitness < 1e9); - m_totalEvaluations++; if(particle.fitness < 1e9) { m_successfulEvaluations++; @@ -5229,6 +5442,7 @@ void nmCalculationAutoFitPSO::updateParticle(int particleIndex) particle.bestFitness = particle.fitness; particle.bestPosition = particle.position; particle.bestLogLogData = particle.currentLogLogData; + particle.bestPressureData = particle.currentPressureData; particle.bestObjectiveBreakdown = particle.currentObjectiveBreakdown; if(!preserveSurrogateGuide) { @@ -5278,6 +5492,7 @@ void nmCalculationAutoFitPSO::updateGlobalBest() m_globalBestFitness = particle.bestFitness; m_globalBestPosition = particle.bestPosition; m_globalBestLogLogData = particle.bestLogLogData; + m_globalBestPressureData = particle.bestPressureData; m_globalBestObjectiveBreakdown = particle.bestObjectiveBreakdown; globalBestUpdated = true; emit bestCurveUpdated(m_targetLogLogData, m_globalBestLogLogData, m_currentIteration + 1, m_globalBestFitness); @@ -5298,6 +5513,7 @@ void nmCalculationAutoFitPSO::updateGlobalBest() m_globalBestFitness = m_userInitialFitness; m_globalBestPosition = m_userInitialSolution; m_globalBestLogLogData = m_userInitialLogLogData; + m_globalBestPressureData = m_userInitialPressureData; m_globalBestObjectiveBreakdown = m_userInitialObjectiveBreakdown; emit bestCurveUpdated(m_targetLogLogData, m_globalBestLogLogData, m_currentIteration + 1, m_globalBestFitness); } @@ -5558,7 +5774,11 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter static int callCount = 0; callCount++; m_lastEvaluatedLogLogData.clear(); + m_lastEvaluatedPressureData.clear(); m_lastObjectiveBreakdown = AutoFitObjectiveBreakdown(); + if(isRunTimeLimitReached()) { + return 1e10; + } try { DEBUG_OUT(QString("%1: Call #%2 - Starting evaluation with %3 parameters") @@ -5661,7 +5881,7 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter const bool fractureGridParameterSelected = (m_parameterSelected.size() > 8 && m_parameterSelected[8]) || (m_parameterSelected.size() > 9 && m_parameterSelected[9]); - if(fractureGridParameterSelected) { + if(fractureGridParameterSelected && !isRunTimeLimitReached()) { nmCalculationPebiGrid* pebiGrid = nmCalculationPebiGrid::getInstance(); if(!pebiGrid || !pebiGrid->generateOutputPara()) { DEBUG_OUT(QString("%1: Call #%2 - Failed to refresh PEBI fracture parameters") @@ -5677,6 +5897,11 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter bool solverSuccess = false; for(int retry = 0; retry <= maxRetries; ++retry) { + if(isRunTimeLimitReached()) { + DEBUG_OUT(QString("%1: Call #%2 - Run time limit reached") + .arg(funcName).arg(callCount)); + return 1e10; + } if(m_shouldStop) { DEBUG_OUT(QString("%1: Call #%2 - User stop requested").arg(funcName).arg(callCount)); return 1e10; @@ -5688,7 +5913,11 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter if(retry > 0) { DEBUG_OUT(QString("%1: Call #%2 - Retry delay...").arg(funcName).arg(callCount)); - msleep(1000); + const int retryDelay = qMin(1000, remainingRunTimeMs()); + if(retryDelay <= 0) { + return 1e10; + } + msleep(retryDelay); } solverResult = runSolver(); @@ -6278,6 +6507,7 @@ void nmCalculationAutoFitPSO::validateAndProtectFinalResult() m_globalBestFitness = initialFitness; m_globalBestPosition = m_userInitialSolution; m_globalBestLogLogData = m_userInitialLogLogData; + m_globalBestPressureData = m_userInitialPressureData; m_globalBestObjectiveBreakdown = m_userInitialObjectiveBreakdown; emit bestCurveUpdated(m_targetLogLogData, m_globalBestLogLogData, m_currentIteration + 1, m_globalBestFitness); @@ -7451,6 +7681,22 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() m_lastEvaluatedLogLogData.clear(); QVector> result; nmCalculationDllPebiSolverTask* dllTask = nullptr; + bool dllTaskStarted = false; + bool dllTaskRecorded = false; + bool dllTaskStoppedByUser = false; + auto recordDllTask = [&](bool success, bool timeout) { + if(!dllTask || !dllTaskStarted || dllTaskRecorded) { + return; + } + // 任务对象销毁前读取 DLL 内部耗时和本次网格数。失败或超时时 getter + // 可能返回 -1,结果文件会按空值保存,不能写成伪造的零。 + m_lastRunResult.recordOptimizationSolverCall( + success, + timeout, + dllTask->getSolveTimeMs(), + dllTask->getPebiCount()); + dllTaskRecorded = true; + }; try { DEBUG_OUT("Creating DLL solver task"); @@ -7465,8 +7711,10 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() dllTask->setAutoFitTargetWell(m_targetWellName); - if(m_shouldStop) { - DEBUG_OUT("Should stop - cleaning up and returning empty result"); + if(m_shouldStop || isRunTimeLimitReached()) { + DEBUG_OUT(m_shouldStop + ? "Should stop - cleaning up and returning empty result" + : "Run time limit reached before DLL solver start"); delete dllTask; --m_evaluationInProgress; return result; @@ -7476,35 +7724,44 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() // 异步执行 DLL 任务。循环等待期间持续 processEvents,保证界面不会完全卡死。 dllTask->start(); + dllTaskStarted = true; - // 等待完成。最大等待 1 小时,适配大模型慢算;用户停止时会 terminate。 - const int maxWait = 3600000; // 1h超时 + // 每次 DLL 调用只使用完整拟合运行的剩余预算。1800 秒属于从首次 + // 参数评价到最终全场计算结束的统一上限,不会为每次调用重新计时。 const int checkInterval = 50; - QTime waitTimer; - waitTimer.start(); - - while(waitTimer.elapsed() < maxWait) { + while(!isRunTimeLimitReached()) { + const int waitInterval = qMin(checkInterval, remainingRunTimeMs()); + if(waitInterval <= 0) { + break; + } // wait(timeout) 会在线程一完成时立即返回,避免原来固定 msleep(500) // 带来的每次 0~500ms 额外等待;50ms 间隔仍可及时处理停止请求和界面事件。 - if(dllTask->wait(checkInterval)) { + if(dllTask->wait(waitInterval)) { + isRunTimeLimitReached(); DEBUG_OUT("DLL solver task completed"); break; } - QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, checkInterval); + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, + waitInterval); if(m_shouldStop) { DEBUG_OUT("DLL solver task terminated by user"); + dllTaskStoppedByUser = true; dllTask->terminate(); + dllTask->wait(2000); break; } } - // 超时处理 + // 当前任务只有用户停止或完整运行预算耗尽时才会在仍运行状态下退出等待。 if(dllTask->isRunning()) { - DEBUG_OUT("DLL solver task timeout, terminating..."); + DEBUG_OUT(m_runTimedOut + ? "DLL solver task reached run time limit, terminating..." + : "DLL solver task terminated by user..."); dllTask->terminate(); dllTask->wait(2000); + recordDllTask(false, m_runTimedOut && !dllTaskStoppedByUser); delete dllTask; dllTask = nullptr; @@ -7513,17 +7770,35 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() return result; } + if(m_runTimedOut) { + recordDllTask(false, true); + delete dllTask; + dllTask = nullptr; + --m_evaluationInProgress; + m_consecutiveFailures++; + return result; + } + + if(dllTaskStoppedByUser) { + recordDllTask(false, false); + delete dllTask; + dllTask = nullptr; + --m_evaluationInProgress; + m_consecutiveFailures++; + return result; + } + // 线程结束后检查真实执行结果,防止失败时复用上一粒子的旧曲线。 dllTask->wait(); if(!dllTask->wasSuccessful()) { DEBUG_OUT("DLL solver task reported failure"); + recordDllTask(false, false); delete dllTask; dllTask = nullptr; --m_evaluationInProgress; m_consecutiveFailures++; return result; } - // 任务结束后复制其局部结果,删除任务前不再持有任务内部引用。 QVector> pressureResult = dllTask->getAutoFitResultPressure(); QVector> logLogResult = dllTask->getAutoFitResultLogLog(); @@ -7531,7 +7806,9 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() DEBUG_OUT(QString("DLL result verification - Pressure arrays: %1, LogLog arrays: %2") .arg(pressureResult.size()).arg(logLogResult.size())); - if(pressureResult.size() >= 2) { + if(pressureResult.size() >= 2 && + !pressureResult[0].isEmpty() && + pressureResult[0].size() == pressureResult[1].size()) { DEBUG_OUT(QString("Pressure result - Time points: %1, Pressure points: %2") .arg(pressureResult[0].size()).arg(pressureResult[1].size())); @@ -7545,12 +7822,12 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() } // 数据有效性检查 - if(pressureResult.size() >= 2 - && pressureResult[0].size() > 0 - && pressureResult[1].size() > 0 + if(isValidAutoFitPressureResult(pressureResult) && validateLogLogData(logLogResult)) { + recordDllTask(true, false); result = pressureResult; m_lastEvaluatedLogLogData = logLogResult; + m_lastEvaluatedPressureData = pressureResult; DEBUG_OUT(QString("Got DLL solver result: %1 points").arg(result[0].size())); m_consecutiveFailures = 0; @@ -7578,6 +7855,7 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() } } else { + recordDllTask(false, false); DEBUG_OUT("DLL solver result is empty or invalid"); DEBUG_OUT(QString("Pressure result size: %1, Array sizes: %2, %3") .arg(pressureResult.size()) @@ -7603,6 +7881,7 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() dllTask->terminate(); dllTask->wait(3000); } + recordDllTask(false, false); DEBUG_OUT("Cleaning up DLL solver task..."); delete dllTask; @@ -7625,23 +7904,42 @@ bool nmCalculationAutoFitPSO::runFinalFullSolver() DEBUG_OUT("Cannot start final full-field solver while another evaluation is running"); return false; } + if(isRunTimeLimitReached()) { + DEBUG_OUT("Run time limit reached before final full-field solver start"); + m_lastRunResult.finalSolverStatus = "TIMEOUT"; + return false; + } ++m_evaluationInProgress; nmCalculationDllPebiSolverTask dllTask(m_tempDirectory); dllTask.start(); - const int maxWait = 3600000; - const int checkInterval = 50; - QTime waitTimer; - waitTimer.start(); + bool finalTaskRecorded = false; + auto recordFinalTask = [&](bool success, bool timeout) { + if(finalTaskRecorded) { + return; + } + m_lastRunResult.recordFinalSolverCall( + success, + timeout, + dllTask.getSolveTimeMs(), + dllTask.getPebiCount()); + finalTaskRecorded = true; + }; - while(waitTimer.elapsed() < maxWait) { - if(dllTask.wait(checkInterval)) { + const int checkInterval = 50; + while(!isRunTimeLimitReached()) { + const int waitInterval = qMin(checkInterval, remainingRunTimeMs()); + if(waitInterval <= 0) { + break; + } + if(dllTask.wait(waitInterval)) { + isRunTimeLimitReached(); break; } // 最终完整计算可能持续较长时间,此处需处理停止按钮事件。 - QApplication::processEvents(QEventLoop::AllEvents, checkInterval); + QApplication::processEvents(QEventLoop::AllEvents, waitInterval); if(!dllTask.isRunning()) { break; @@ -7651,21 +7949,37 @@ bool nmCalculationAutoFitPSO::runFinalFullSolver() DEBUG_OUT("Final full-field solver terminated by user"); dllTask.terminate(); dllTask.wait(2000); + recordFinalTask(false, false); + m_lastRunResult.finalSolverStatus = "STOPPED"; --m_evaluationInProgress; return false; } } + if(m_runTimedOut) { + if(dllTask.isRunning()) { + DEBUG_OUT("Final full-field solver reached run time limit, terminating task"); + dllTask.terminate(); + dllTask.wait(2000); + } + recordFinalTask(false, true); + m_lastRunResult.finalSolverStatus = "TIMEOUT"; + --m_evaluationInProgress; + return false; + } + if(dllTask.isRunning()) { - DEBUG_OUT("Final full-field solver timeout, terminating task"); + DEBUG_OUT("Final full-field solver stopped before completion"); dllTask.terminate(); dllTask.wait(2000); + recordFinalTask(false, false); --m_evaluationInProgress; return false; } dllTask.wait(); const bool succeeded = dllTask.wasSuccessful(); + recordFinalTask(succeeded, false); --m_evaluationInProgress; return succeeded; } @@ -7825,37 +8139,43 @@ StopReasonPSO nmCalculationAutoFitPSO::analyzeOptimizationStatus() // 目标达成和最大迭代是硬条件;真收敛/局部最优需要足够历史数据支撑。 // 返回值只描述原因,真正的日志和 UI 收尾在 startAutoFitting() 中处理。 - // 1. 检查用户停止 + // 1. 检查完整运行时间上限。超时优先于其他收敛条件,避免恰好在 + // 求解返回时达到目标误差后又被错误记为 SUCCESS。 + if(isRunTimeLimitReached()) { + return PSO_TIME_LIMIT_REACHED; + } + + // 2. 检查用户停止 if(m_shouldStop) { return PSO_USER_STOPPED; } - // 2. 检查连续失败 + // 3. 检查连续失败 if(m_consecutiveFailedIterations >= m_maxConsecutiveFailures) { return PSO_CONSECUTIVE_FAILURES; } - // 3. 检查是否达到目标精度 + // 4. 检查是否达到目标精度 if(m_globalBestFitness < m_targetError) { return PSO_TARGET_ACHIEVED; } - // 4. 检查是否达到最大迭代数 + // 5. 检查是否达到最大迭代数 if(m_currentIteration >= m_maxIterations - 1) { return PSO_MAX_ITERATIONS; } - // 5. 需要足够的历史数据才能判断收敛 + // 6. 需要足够的历史数据才能判断收敛 if(m_convergenceHistory.size() < m_localOptimumWindow) { return PSO_CONTINUE_OPTIMIZATION; } - // 6. 检查真正的收敛 + // 7. 检查真正的收敛 if(m_convergenceHistory.size() >= m_trueConvergenceWindow && checkTrueConvergence()) { return PSO_TRUE_CONVERGENCE; } - // 7. 检查局部最优陷阱 + // 8. 检查局部最优陷阱 if(checkLocalOptimumTrap()) { return PSO_LOCAL_OPTIMUM; } @@ -8134,6 +8454,9 @@ QString nmCalculationAutoFitPSO::getStopReasonDescription(StopReasonPSO reason) case PSO_OPTIMIZATION_FAILED: return tr("Optimization failed"); + case PSO_TIME_LIMIT_REACHED: + return tr("Automatic fitting run time limit reached"); + default: return tr("Unknown reason"); } @@ -8229,6 +8552,9 @@ void nmCalculationAutoFitPSO::runSimulatedFitting() // 仿真模式入口:模拟完整 PSO 日志、进度、bestCurveUpdated 信号和最终写回。 // 它不评价粒子、不调用代理模型、不调用 DLL,只用于界面联调或演示自动拟合流程。 DEBUG_OUT("=== SIMULATED PSO AUTO FITTING START ==="); + const QVector simulationInitialValues = m_initialValues; + resetOptimizer(); + m_initialValues = simulationInitialValues; m_isRunning = true; m_shouldStop = false; m_simulationIteration = 0; @@ -8237,6 +8563,9 @@ void nmCalculationAutoFitPSO::runSimulatedFitting() m_simulationSuccessCount = 0; m_simulationFailCount = 0; m_simulationIterationStarted = false; + m_lastRunResult.algorithm = "SIMULATION"; + m_lastRunResult.solverType = "NONE"; + beginRunTiming(); // 设置起始误差 m_simulationStartError = 0.7324; @@ -8639,6 +8968,7 @@ void nmCalculationAutoFitPSO::finishSimulation() m_globalBestPosition = m_simulationTargetParams; m_globalBestFitness = m_simulationTargetError; m_globalBestLogLogData = buildSimulatedLogLogData(1.0); + m_completedIterationCount = m_simulationMaxIterations; emit bestCurveUpdated(m_targetLogLogData, m_globalBestLogLogData, m_simulationMaxIterations, m_globalBestFitness); applyParametersToDataManager(m_globalBestPosition); @@ -8671,6 +9001,8 @@ void nmCalculationAutoFitPSO::finishSimulation() emit logMessageGenerated(tr("=== PSO OPTIMIZATION CONVERGED ===")); m_isRunning = false; + markOptimizationFinished(); + finalizeRunResult("COMPLETED", "SIMULATION_COMPLETED"); emit progressUpdated(m_simulationMaxIterations, m_globalBestFitness); QApplication::processEvents(); @@ -8679,4 +9011,5 @@ void nmCalculationAutoFitPSO::finishSimulation() QString message = QString(tr("PSO optimization converged to stable solution. Best error: %1, Iterations: %2")) .arg(m_globalBestFitness, 0, 'e', 4).arg(m_simulationMaxIterations); emit fittingFinished(true, message); + cleanupTemporaryDirectory(); } diff --git a/Src/nmNum/nmCalculation/nmCalculationAutoFitRunResult.cpp b/Src/nmNum/nmCalculation/nmCalculationAutoFitRunResult.cpp new file mode 100644 index 0000000..2df2b83 --- /dev/null +++ b/Src/nmNum/nmCalculation/nmCalculationAutoFitRunResult.cpp @@ -0,0 +1,908 @@ +#include "nmCalculationAutoFitPSO.h" + +#include "nmDataAnalyzeManager.h" +#include "nmDataReservoir.h" +#include "nmDataWellBase.h" +#include "iBase/iUtils/ZxBaseUtil.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rapidjson/document.h" +#include "rapidjson/prettywriter.h" +#include "rapidjson/stringbuffer.h" + +#ifdef Q_OS_WIN +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#endif + +namespace { + +static bool autoFitResultIsFinite(double value) +{ +#ifdef Q_OS_WIN + return _finite(value) != 0; +#else + return std::isfinite(value); +#endif +} + +static bool autoFitResultIsNan(double value) +{ +#ifdef Q_OS_WIN + return _isnan(value) != 0; +#else + return std::isnan(value); +#endif +} + +static bool autoFitCurvePassedForRun(const QString& status, + const AutoFitCurveMetrics& metrics) +{ + return status == "SUCCESS" && + metrics.valid && + autoFitResultIsFinite(metrics.coverage) && metrics.coverage >= 0.95 && + autoFitResultIsFinite(metrics.logDeltaPRmseDecade) && + metrics.logDeltaPRmseDecade <= 0.02 && + autoFitResultIsFinite(metrics.logDerivativeRmseDecade) && + metrics.logDerivativeRmseDecade <= 0.02; +} + +static QByteArray autoFitCanonicalDouble(double value) +{ + if(autoFitResultIsFinite(value)) { + return QByteArray("FINITE:") + + QString::number(value, 'g', 17).toLatin1(); + } + if(autoFitResultIsNan(value)) { + return QByteArray("NAN"); + } + return value > 0.0 + ? QByteArray("POSITIVE_INFINITY") + : QByteArray("NEGATIVE_INFINITY"); +} + +static rapidjson::Value autoFitJsonNumber(double value) +{ + rapidjson::Value jsonValue; + if(autoFitResultIsFinite(value)) { + jsonValue.SetDouble(value); + } else { + jsonValue.SetNull(); + } + return jsonValue; +} + +static rapidjson::Value autoFitJsonInteger(qint64 value) +{ + rapidjson::Value jsonValue; + if(value >= 0) { + jsonValue.SetInt64(value); + } else { + jsonValue.SetNull(); + } + return jsonValue; +} + +static rapidjson::Value autoFitJsonString( + const QString& value, + rapidjson::Document::AllocatorType& allocator) +{ + const QByteArray utf8 = value.toUtf8(); + rapidjson::Value jsonValue; + jsonValue.SetString(utf8.constData(), + static_cast(utf8.size()), allocator); + return jsonValue; +} + +static QString autoFitDateTimeText(const QDateTime& value) +{ + return value.isValid() + ? value.toString("yyyy-MM-ddTHH:mm:ss.zzz") + : QString(); +} + +static QByteArray autoFitCalculateSha256(const QByteArray& content) +{ +#ifdef Q_OS_WIN + HCRYPTPROV provider = 0; + HCRYPTHASH hash = 0; + QByteArray digest; + if(!CryptAcquireContext(&provider, NULL, NULL, PROV_RSA_AES, + CRYPT_VERIFYCONTEXT)) { + return digest; + } + if(!CryptCreateHash(provider, CALG_SHA_256, 0, 0, &hash)) { + CryptReleaseContext(provider, 0); + return digest; + } + const bool hashed = CryptHashData(hash, + reinterpret_cast(content.constData()), + static_cast(content.size()), 0) != FALSE; + BYTE hashBytes[32] = { 0 }; + DWORD hashSize = sizeof(hashBytes); + if(hashed && CryptGetHashParam(hash, HP_HASHVAL, hashBytes, + &hashSize, 0)) { + digest = QByteArray(reinterpret_cast(hashBytes), + static_cast(hashSize)); + } + CryptDestroyHash(hash); + CryptReleaseContext(provider, 0); + return digest; +#else + Q_UNUSED(content); + return QByteArray(); +#endif +} + +static QString autoFitCsvField(const QString& value) +{ + QString escaped = value; + escaped.replace('"', "\"\""); + return QString("\"%1\"").arg(escaped); +} + +static QString autoFitCsvNumber(double value) +{ + return autoFitResultIsFinite(value) + ? QString::number(value, 'g', 17) + : QString(); +} + +static QString autoFitCsvInteger(qint64 value) +{ + return value >= 0 ? QString::number(value) : QString(); +} + +static void autoFitAppendArtifactError(QString* errors, + const QString& error) +{ + if(!errors || error.isEmpty() || errors->contains(error)) { + return; + } + if(!errors->isEmpty()) { + errors->append(';'); + } + errors->append(error); +} + +static QString autoFitPhaseName(NM_SOLVER_MODEL_TYPE modelType) +{ + switch(modelType) { + case SMT_Oil_ConstPvt: + return "OIL_CONSTANT_PVT"; + case SMT_Oil_VariablePvt: + return "OIL_VARIABLE_PVT"; + case SMT_Water_ConstPvt: + return "WATER_CONSTANT_PVT"; + case SMT_Water_VariablePvt: + return "WATER_VARIABLE_PVT"; + case SMT_Gas_VariablePvt: + return "GAS_VARIABLE_PVT"; + case SMT_Gas_PseudoPressure: + return "GAS_PSEUDO_PRESSURE"; + case SMT_Oil_Gas_TwoPhase: + return "OIL_GAS_TWO_PHASE"; + case SMT_Oil_Water_TwoPhase: + return "OIL_WATER_TWO_PHASE"; + case SMT_Gas_Water_TwoPhase: + return "GAS_WATER_TWO_PHASE"; + case SMT_Oil_Gas_Water_ThreePhase: + return "OIL_GAS_WATER_THREE_PHASE"; + default: + return "UNKNOWN"; + } +} + +static QStringList autoFitParameterUnits() +{ + QStringList units; + units << "mD" + << "" + << "m^3/MPa" + << "" + << "m" + << "MPa^-1" + << "MPa^-1" + << "" + << "mD.m" + << "m"; + return units; +} + +static rapidjson::Value autoFitJsonDoubleArray( + const QVector& values, + rapidjson::Document::AllocatorType& allocator) +{ + rapidjson::Value array(rapidjson::kArrayType); + for(int i = 0; i < values.size(); ++i) { + array.PushBack(autoFitJsonNumber(values[i]).Move(), allocator); + } + return array; +} + +} // namespace + +void nmCalculationAutoFitPSO::initializeRunResult() +{ + closeTraceFile(); + m_lastRunResult = AutoFitRunResult(); + m_runTimingStarted = false; + m_runTimedOut = false; + // 配置校验也可能提前失败,因此在读取配置前先清空上一次运行的统计和 + // 最优曲线,避免失败结果错误引用上一轮的调用次数、参数或统一误差。 + m_currentIteration = 0; + m_completedIterationCount = 0; + m_totalEvaluations = 0; + m_successfulEvaluations = 0; + m_lastError.clear(); + m_initialValues.clear(); + m_userInitialSolution.clear(); + m_userInitialFitness = 1.0e10; + m_hasValidUserSolution = false; + m_parameterSelected.clear(); + m_parameterLower.clear(); + m_parameterUpper.clear(); + m_enabledParamIndices.clear(); + m_globalBestPosition.clear(); + m_globalBestPressureData.clear(); + m_globalBestFitness = 1.0e10; + m_traceRunId.clear(); + m_traceFilePath.clear(); + m_traceMetaFilePath.clear(); + m_lastRunResult.runId = QString("AF-%1-%2-%3") + .arg(QDateTime::currentDateTime().toString("yyyyMMdd-hhmmss-zzz")) + .arg(QCoreApplication::applicationPid()) + .arg(QUuid::createUuid().toString().remove('{').remove('}').remove('-').left(8)); + m_lastRunResult.startedAt = QDateTime::currentDateTime(); + m_lastRunResult.targetWell = m_targetWellName; + m_lastRunResult.status = "FAILED"; + m_lastRunResult.stopReason = "RUN_INITIALIZATION"; + m_lastRunResult.projectPath = QDir::cleanPath(ZxBaseUtil::getCurProjectDir()); + // 即使后续配置读取失败,也尽量保留当前工程可取得的相态与求解器信息; + // 算法只有在配置读取成功后才能确定,失败路径明确记为不可用。 + captureRunConfiguration(false); + m_lastRunResult.algorithm = "UNAVAILABLE"; + nmDataAnalyzeManager* pDataManager = nmDataAnalyzeManager::getCurrentInstance(); + if(pDataManager && pDataManager->getReservoirData()) { + m_lastRunResult.initialPressureMpa = + pDataManager->getReservoirData()->getInitialPressure() + .getValue().toDouble(); + } + + m_frozenTargetPressureData = m_targetPressureData; + if(m_frozenTargetPressureData.size() < 2) { + nmDataWellBase* pTargetWell = pDataManager + ? pDataManager->findWellByName(m_targetWellName) + : nullptr; + if(pTargetWell) { + m_frozenTargetPressureData = pTargetWell->getHistoryPressure(); + } + } + m_lastRunResult.targetCurveSha256 = + calculateTargetCurveHash(m_frozenTargetPressureData); + + const QString outputRoot = getAutoFitOutputRoot(); + m_lastRunResult.resultDirectory = + QDir(outputRoot).absoluteFilePath(m_lastRunResult.runId); + m_lastRunResult.resultJsonPath = + QDir(m_lastRunResult.resultDirectory).absoluteFilePath("autofit_result.json"); + m_lastRunResult.curveCsvPath = + QDir(m_lastRunResult.resultDirectory).absoluteFilePath("autofit_curve.csv"); + m_lastRunResult.runsCsvPath = + QDir(outputRoot).absoluteFilePath("autofit_runs.csv"); + + const QString pebiRoot = ZxBaseUtil::getCurWellDirOf("Nm/Solver"); + m_lastRunResult.fullFieldPressurePath = QDir(pebiRoot).absoluteFilePath( + QString("output/Pebi/%1/Pressure.txt").arg(m_targetWellName)); +} + +void nmCalculationAutoFitPSO::captureRunConfiguration(bool useParticleSwarm) +{ + m_lastRunResult.phase = "UNKNOWN"; + m_lastRunResult.algorithm = useParticleSwarm + ? "PSO_WITH_SURROGATE_SCREENING" + : "DIAGNOSTIC_TRUST_REGION"; + m_lastRunResult.solverType = "UNKNOWN"; + nmDataAnalyzeManager* pDataManager = nmDataAnalyzeManager::getCurrentInstance(); + if(!pDataManager) { + return; + } + + m_lastRunResult.phase = autoFitPhaseName(pDataManager->getSolverModelType()); + m_lastRunResult.solverType = + pDataManager->getPebiSolverType() == + nmDataAnalyzeManager::PebiSolverCpuAccelerated + ? "CPU_ACCELERATED" + : "ORIGINAL"; + m_lastRunResult.ompThreads = pDataManager->getPebiOmpThreads(); + m_lastRunResult.iluReuseSteps = pDataManager->getPebiIluReuseSteps(); +} + +void nmCalculationAutoFitPSO::beginRunTiming() +{ + m_optimizationWallTimer.restart(); + m_workflowWallTimer.restart(); + m_runTimingStarted = true; +} + +void nmCalculationAutoFitPSO::markOptimizationFinished() +{ + if(m_runTimingStarted && m_lastRunResult.optimizationWallTimeMs < 0) { + m_lastRunResult.optimizationWallTimeMs = m_optimizationWallTimer.elapsed(); + } +} + +void nmCalculationAutoFitPSO::markWorkflowFinished() +{ + if(m_runTimingStarted && m_lastRunResult.workflowWallTimeMs < 0) { + m_lastRunResult.workflowWallTimeMs = m_workflowWallTimer.elapsed(); + } +} + +bool nmCalculationAutoFitPSO::isRunTimeLimitReached() +{ + if(m_runTimedOut) { + return true; + } + // workflow_wall_time_ms 一旦冻结,后续文件写出、日志和界面清理均不再 + // 属于正式运行预算,不能在收口之后把既有状态反向改成 TIMEOUT。 + if(m_lastRunResult.workflowWallTimeMs >= 0) { + return false; + } + if(!m_runTimingStarted || + m_workflowWallTimer.elapsed() < RUN_TIME_LIMIT_MS) { + return false; + } + + // 超时是完整拟合运行的独立结束路径,不能复用用户停止标志,否则最终 + // JSON 会把 TIMEOUT 错记成 STOPPED,求解器调用统计也会丢失超时次数。 + m_runTimedOut = true; + // 超时边界就是本次运行的参数确定和工作流结束时刻。终止后台任务所需的 + // 清理等待不属于拟合耗时,否则同一超时会因线程退出速度不同得到不同记录。 + if(m_lastRunResult.optimizationWallTimeMs < 0) { + m_lastRunResult.optimizationWallTimeMs = RUN_TIME_LIMIT_MS; + } + if(m_lastRunResult.workflowWallTimeMs < 0) { + m_lastRunResult.workflowWallTimeMs = RUN_TIME_LIMIT_MS; + } + emit logMessageGenerated(tr("Automatic fitting run time limit reached")); + return true; +} + +int nmCalculationAutoFitPSO::remainingRunTimeMs() +{ + if(isRunTimeLimitReached()) { + return 0; + } + if(!m_runTimingStarted) { + return RUN_TIME_LIMIT_MS; + } + + const qint64 remaining = static_cast(RUN_TIME_LIMIT_MS) - + m_workflowWallTimer.elapsed(); + return remaining > 0 + ? static_cast(qMin(remaining, + static_cast(RUN_TIME_LIMIT_MS))) + : 0; +} + +void nmCalculationAutoFitPSO::captureRunParameters() +{ + m_lastRunResult.parameters.clear(); + const QStringList parameterNames = traceParameterNames(); + const QStringList parameterUnits = autoFitParameterUnits(); + for(int selectedIndex = 0; + selectedIndex < m_enabledParamIndices.size(); + ++selectedIndex) { + const int parameterIndex = m_enabledParamIndices[selectedIndex]; + if(parameterIndex < 0 || parameterIndex >= parameterNames.size()) { + continue; + } + + AutoFitParameterResult parameter; + parameter.name = parameterNames[parameterIndex]; + parameter.unit = parameterIndex < parameterUnits.size() + ? parameterUnits[parameterIndex] + : QString(); + if(selectedIndex < m_initialValues.size()) { + parameter.initialValue = m_initialValues[selectedIndex]; + } else if(selectedIndex < m_userInitialSolution.size()) { + parameter.initialValue = m_userInitialSolution[selectedIndex]; + } + if(parameterIndex < m_parameterLower.size()) { + parameter.lowerBound = m_parameterLower[parameterIndex]; + } + if(parameterIndex < m_parameterUpper.size()) { + parameter.upperBound = m_parameterUpper[parameterIndex]; + } + if(selectedIndex < m_globalBestPosition.size()) { + parameter.finalValue = m_globalBestPosition[selectedIndex]; + } + m_lastRunResult.parameters.append(parameter); + } +} + +void nmCalculationAutoFitPSO::finalizeRunResult(const QString& status, + const QString& stopReason, + bool writeArtifacts) +{ + markOptimizationFinished(); + markWorkflowFinished(); + m_lastRunResult.finishedAt = QDateTime::currentDateTime(); + m_lastRunResult.status = status; + m_lastRunResult.stopReason = stopReason; + m_lastRunResult.iterationCount = qMax(0, m_completedIterationCount); + m_lastRunResult.parameterEvaluationCount = m_totalEvaluations; + m_lastRunResult.initialInternalError = m_hasValidUserSolution + ? m_userInitialFitness + : std::numeric_limits::quiet_NaN(); + m_lastRunResult.finalInternalError = + m_globalBestFitness < 1.0e9 + ? m_globalBestFitness + : std::numeric_limits::quiet_NaN(); + m_lastRunResult.traceCsvPath = m_traceFilePath; + m_lastRunResult.traceMetaJsonPath = m_traceMetaFilePath; + captureRunParameters(); + + m_lastRunResult.curveMetrics = calculateUnifiedCurveMetrics( + m_frozenTargetPressureData, + m_globalBestPressureData, + m_lastRunResult.initialPressureMpa, + 80); + // 协议中的 curve_passed 不只是曲线数值判据,还要求原生任务真正达到 + // SUCCESS。COMPLETED、STOPPED 和 FAILED 即使保留了一条好曲线也不能通过。 + m_lastRunResult.curveMetrics.passed = autoFitCurvePassedForRun( + m_lastRunResult.status, m_lastRunResult.curveMetrics); + + if(writeArtifacts) { + const bool artifactsWritten = writeStructuredRunArtifacts(); + if(!artifactsWritten) { + emit logMessageGenerated(tr("Result artifact export failed: %1") + .arg(m_lastRunResult.artifactError)); + } + } +} + +QString nmCalculationAutoFitPSO::getAutoFitOutputRoot() const +{ + const QString solverRoot = ZxBaseUtil::getCurProjectDirOf("Nm/Solver"); + return QDir(solverRoot).absoluteFilePath("output/AutoFit"); +} + +QString nmCalculationAutoFitPSO::calculateTargetCurveHash( + const QVector >& pressureData) const +{ + QByteArray canonical; + canonical.append("AUTOFIT_TARGET_PRESSURE_V2\r\n"); + canonical.append("column_count="); + canonical.append(QByteArray::number(pressureData.size())); + canonical.append("\r\n"); + for(int column = 0; column < pressureData.size(); ++column) { + canonical.append("column="); + canonical.append(QByteArray::number(column)); + canonical.append(",length="); + canonical.append(QByteArray::number(pressureData[column].size())); + canonical.append("\r\n"); + for(int row = 0; row < pressureData[column].size(); ++row) { + canonical.append("row="); + canonical.append(QByteArray::number(row)); + canonical.append(",value="); + canonical.append(autoFitCanonicalDouble(pressureData[column][row])); + canonical.append("\r\n"); + } + } + return QString::fromLatin1(autoFitCalculateSha256(canonical).toHex().toUpper()); +} + +bool nmCalculationAutoFitPSO::writeStructuredRunArtifacts() +{ + m_lastRunResult.artifactError.clear(); + // 写出层再次执行原生状态门控,防止测试入口或未来批处理入口绕过 + // finalizeRunResult() 后产生 curve_passed=1 的非 SUCCESS 记录。 + m_lastRunResult.curveMetrics.passed = autoFitCurvePassedForRun( + m_lastRunResult.status, m_lastRunResult.curveMetrics); + if(m_lastRunResult.resultDirectory.isEmpty() || + !QDir().mkpath(m_lastRunResult.resultDirectory)) { + m_lastRunResult.artifactError = "CREATE_RESULT_DIRECTORY_FAILED"; + return false; + } + + bool allSucceeded = true; + if(!writeRunCurveCsv()) { + autoFitAppendArtifactError(&m_lastRunResult.artifactError, + "WRITE_CURVE_CSV_FAILED"); + allSucceeded = false; + } + + // JSON 先于汇总 CSV 写出。若 JSON 失败,artifact_error 会随随后追加的 + // 汇总行持久化,避免 CSV 只留下一个不存在的 JSON 路径却没有失败原因。 + const bool jsonSucceeded = writeRunResultJson(); + if(!jsonSucceeded) { + autoFitAppendArtifactError(&m_lastRunResult.artifactError, + "WRITE_RESULT_JSON_FAILED"); + allSucceeded = false; + } + + const bool summarySucceeded = appendRunSummaryCsv(); + if(!summarySucceeded) { + autoFitAppendArtifactError(&m_lastRunResult.artifactError, + "APPEND_RUNS_CSV_FAILED"); + allSucceeded = false; + // 首次 JSON 已成功时再覆盖一次,使 JSON 也记录汇总追加失败。 + if(jsonSucceeded && !writeRunResultJson()) { + autoFitAppendArtifactError(&m_lastRunResult.artifactError, + "REWRITE_RESULT_JSON_FAILED"); + } + } + return allSucceeded; +} + +bool nmCalculationAutoFitPSO::writeRunCurveCsv() +{ + QFile file(m_lastRunResult.curveCsvPath); + if(!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { + return false; + } + QTextStream stream(&file); + stream.setCodec("UTF-8"); + stream << "point_no,time_hr,target_pressure_mpa,fitted_pressure_mpa," + "target_delta_p_mpa,fitted_delta_p_mpa,target_derivative_mpa," + "fitted_derivative_mpa,pressure_residual_mpa," + "log_delta_p_residual_decade,log_derivative_residual_decade\n"; + + const AutoFitCurveMetrics& metrics = m_lastRunResult.curveMetrics; + for(int i = 0; i < metrics.timeHr.size(); ++i) { + const double pressureResidual = + metrics.fittedPressureMpa[i] - metrics.targetPressureMpa[i]; + double logDeltaPResidual = + std::numeric_limits::quiet_NaN(); + if(autoFitResultIsFinite(metrics.targetDeltaPMpa[i]) && + metrics.targetDeltaPMpa[i] > 0.0 && + autoFitResultIsFinite(metrics.fittedDeltaPMpa[i]) && + metrics.fittedDeltaPMpa[i] > 0.0) { + logDeltaPResidual = + qLn(metrics.fittedDeltaPMpa[i] / + metrics.targetDeltaPMpa[i]) / + qLn(10.0); + } + double logDerivativeResidual = + std::numeric_limits::quiet_NaN(); + if(i < metrics.targetDerivativeMpa.size() && + i < metrics.fittedDerivativeMpa.size() && + autoFitResultIsFinite(metrics.targetDerivativeMpa[i]) && + metrics.targetDerivativeMpa[i] > 0.0 && + autoFitResultIsFinite(metrics.fittedDerivativeMpa[i]) && + metrics.fittedDerivativeMpa[i] > 0.0) { + logDerivativeResidual = + qLn(metrics.fittedDerivativeMpa[i] / + metrics.targetDerivativeMpa[i]) / + qLn(10.0); + } + + stream << (i + 1) << ',' + << autoFitCsvNumber(metrics.timeHr[i]) << ',' + << autoFitCsvNumber(metrics.targetPressureMpa[i]) << ',' + << autoFitCsvNumber(metrics.fittedPressureMpa[i]) << ',' + << autoFitCsvNumber(metrics.targetDeltaPMpa[i]) << ',' + << autoFitCsvNumber(metrics.fittedDeltaPMpa[i]) << ',' + << autoFitCsvNumber(i < metrics.targetDerivativeMpa.size() + ? metrics.targetDerivativeMpa[i] + : std::numeric_limits::quiet_NaN()) << ',' + << autoFitCsvNumber(i < metrics.fittedDerivativeMpa.size() + ? metrics.fittedDerivativeMpa[i] + : std::numeric_limits::quiet_NaN()) << ',' + << autoFitCsvNumber(pressureResidual) << ',' + << autoFitCsvNumber(logDeltaPResidual) << ',' + << autoFitCsvNumber(logDerivativeResidual) << "\n"; + } + stream.flush(); + const bool succeeded = stream.status() == QTextStream::Ok; + file.close(); + return succeeded; +} + +bool nmCalculationAutoFitPSO::writeRunResultJson() +{ + rapidjson::Document root; + root.SetObject(); + rapidjson::Document::AllocatorType& allocator = root.GetAllocator(); + root.AddMember("schema_version", 1, allocator); + root.AddMember("run_id", autoFitJsonString( + m_lastRunResult.runId, allocator).Move(), allocator); + root.AddMember("status", autoFitJsonString( + m_lastRunResult.status, allocator).Move(), allocator); + root.AddMember("stop_reason", autoFitJsonString( + m_lastRunResult.stopReason, allocator).Move(), allocator); + root.AddMember("started_at", autoFitJsonString( + autoFitDateTimeText(m_lastRunResult.startedAt), allocator).Move(), allocator); + root.AddMember("finished_at", autoFitJsonString( + autoFitDateTimeText(m_lastRunResult.finishedAt), allocator).Move(), allocator); + + rapidjson::Value context(rapidjson::kObjectType); + context.AddMember("target_well", autoFitJsonString( + m_lastRunResult.targetWell, allocator).Move(), allocator); + context.AddMember("phase", autoFitJsonString( + m_lastRunResult.phase, allocator).Move(), allocator); + context.AddMember("algorithm", autoFitJsonString( + m_lastRunResult.algorithm, allocator).Move(), allocator); + context.AddMember("solver_type", autoFitJsonString( + m_lastRunResult.solverType, allocator).Move(), allocator); + context.AddMember("openmp_threads", autoFitJsonInteger( + m_lastRunResult.ompThreads).Move(), allocator); + context.AddMember("ilu_reuse_steps", autoFitJsonInteger( + m_lastRunResult.iluReuseSteps).Move(), allocator); + context.AddMember("project_path", autoFitJsonString( + m_lastRunResult.projectPath, allocator).Move(), allocator); + context.AddMember("target_curve_sha256", autoFitJsonString( + m_lastRunResult.targetCurveSha256, allocator).Move(), allocator); + context.AddMember("initial_pressure_mpa", autoFitJsonNumber( + m_lastRunResult.initialPressureMpa).Move(), allocator); + root.AddMember("context", context, allocator); + + rapidjson::Value timing(rapidjson::kObjectType); + timing.AddMember("optimization_wall_time_ms", autoFitJsonInteger( + m_lastRunResult.optimizationWallTimeMs).Move(), allocator); + timing.AddMember("workflow_wall_time_ms", autoFitJsonInteger( + m_lastRunResult.workflowWallTimeMs).Move(), allocator); + timing.AddMember("solver_time_sum_ms", autoFitJsonInteger( + m_lastRunResult.solverTimeSumMs).Move(), allocator); + timing.AddMember("final_solver_time_ms", autoFitJsonInteger( + m_lastRunResult.finalSolverTimeMs).Move(), allocator); + root.AddMember("timing", timing, allocator); + + rapidjson::Value counts(rapidjson::kObjectType); + counts.AddMember("iterations", m_lastRunResult.iterationCount, allocator); + counts.AddMember("parameter_evaluations", + m_lastRunResult.parameterEvaluationCount, allocator); + counts.AddMember("model_solver_calls", + m_lastRunResult.modelSolverCallCount, allocator); + counts.AddMember("final_solver_calls", + m_lastRunResult.finalSolverCallCount, allocator); + counts.AddMember("solver_successes", + m_lastRunResult.solverSuccessCount, allocator); + counts.AddMember("solver_failures", + m_lastRunResult.solverFailureCount, allocator); + counts.AddMember("solver_timeouts", + m_lastRunResult.solverTimeoutCount, allocator); + counts.AddMember("optimization_pebi_count", autoFitJsonInteger( + m_lastRunResult.optimizationPebiCount).Move(), allocator); + counts.AddMember("final_pebi_count", autoFitJsonInteger( + m_lastRunResult.finalPebiCount).Move(), allocator); + counts.AddMember("pebi_count", autoFitJsonInteger( + m_lastRunResult.pebiCount).Move(), allocator); + counts.AddMember("final_solver_status", autoFitJsonString( + m_lastRunResult.finalSolverStatus, allocator).Move(), allocator); + root.AddMember("counts", counts, allocator); + + rapidjson::Value objective(rapidjson::kObjectType); + objective.AddMember("initial_internal_error", autoFitJsonNumber( + m_lastRunResult.initialInternalError).Move(), allocator); + objective.AddMember("final_internal_error", autoFitJsonNumber( + m_lastRunResult.finalInternalError).Move(), allocator); + root.AddMember("optimizer_objective", objective, allocator); + + const AutoFitCurveMetrics& metrics = m_lastRunResult.curveMetrics; + rapidjson::Value curveMetrics(rapidjson::kObjectType); + curveMetrics.AddMember("valid", metrics.valid, allocator); + curveMetrics.AddMember("passed", metrics.passed, allocator); + curveMetrics.AddMember("invalid_reason", autoFitJsonString( + metrics.invalidReason, allocator).Move(), allocator); + curveMetrics.AddMember("sample_count", metrics.sampleCount, allocator); + curveMetrics.AddMember("valid_derivative_count", + metrics.validDerivativeCount, allocator); + curveMetrics.AddMember("coverage", autoFitJsonNumber( + metrics.coverage).Move(), allocator); + curveMetrics.AddMember("pressure_rmse_mpa", autoFitJsonNumber( + metrics.pressureRmseMpa).Move(), allocator); + curveMetrics.AddMember("pressure_max_abs_error_mpa", autoFitJsonNumber( + metrics.pressureMaxAbsErrorMpa).Move(), allocator); + curveMetrics.AddMember("log_delta_p_rmse_decade", autoFitJsonNumber( + metrics.logDeltaPRmseDecade).Move(), allocator); + curveMetrics.AddMember("log_derivative_rmse_decade", autoFitJsonNumber( + metrics.logDerivativeRmseDecade).Move(), allocator); + curveMetrics.AddMember("unified_curve_error", autoFitJsonNumber( + metrics.unifiedCurveError).Move(), allocator); + curveMetrics.AddMember("coverage_threshold", 0.95, allocator); + curveMetrics.AddMember("log_rmse_threshold_decade", 0.02, allocator); + curveMetrics.AddMember("unified_curve_error_formula", autoFitJsonString( + "sqrt((log_delta_p_rmse_decade^2 + log_derivative_rmse_decade^2) / 2)", + allocator).Move(), allocator); + root.AddMember("curve_metrics", curveMetrics, allocator); + + rapidjson::Value parameters(rapidjson::kArrayType); + for(int i = 0; i < m_lastRunResult.parameters.size(); ++i) { + const AutoFitParameterResult& parameter = m_lastRunResult.parameters[i]; + rapidjson::Value item(rapidjson::kObjectType); + item.AddMember("name", autoFitJsonString( + parameter.name, allocator).Move(), allocator); + item.AddMember("unit", autoFitJsonString( + parameter.unit, allocator).Move(), allocator); + item.AddMember("initial_value", autoFitJsonNumber( + parameter.initialValue).Move(), allocator); + item.AddMember("lower_bound", autoFitJsonNumber( + parameter.lowerBound).Move(), allocator); + item.AddMember("upper_bound", autoFitJsonNumber( + parameter.upperBound).Move(), allocator); + item.AddMember("final_value", autoFitJsonNumber( + parameter.finalValue).Move(), allocator); + parameters.PushBack(item, allocator); + } + root.AddMember("parameters", parameters, allocator); + + rapidjson::Value evidence(rapidjson::kObjectType); + evidence.AddMember("result_json", autoFitJsonString( + m_lastRunResult.resultJsonPath, allocator).Move(), allocator); + evidence.AddMember("curve_csv", autoFitJsonString( + m_lastRunResult.curveCsvPath, allocator).Move(), allocator); + evidence.AddMember("runs_csv", autoFitJsonString( + m_lastRunResult.runsCsvPath, allocator).Move(), allocator); + evidence.AddMember("trace_csv", autoFitJsonString( + m_lastRunResult.traceCsvPath, allocator).Move(), allocator); + evidence.AddMember("trace_meta_json", autoFitJsonString( + m_lastRunResult.traceMetaJsonPath, allocator).Move(), allocator); + evidence.AddMember("full_field_pressure", autoFitJsonString( + m_lastRunResult.fullFieldPressurePath, allocator).Move(), allocator); + evidence.AddMember("artifact_error", autoFitJsonString( + m_lastRunResult.artifactError, allocator).Move(), allocator); + root.AddMember("evidence", evidence, allocator); + + rapidjson::Value frozenCurve(rapidjson::kObjectType); + frozenCurve.AddMember("time_hr", autoFitJsonDoubleArray( + m_frozenTargetPressureData.size() > 0 + ? m_frozenTargetPressureData[0] + : QVector(), allocator).Move(), allocator); + frozenCurve.AddMember("pressure_mpa", autoFitJsonDoubleArray( + m_frozenTargetPressureData.size() > 1 + ? m_frozenTargetPressureData[1] + : QVector(), allocator).Move(), allocator); + root.AddMember("frozen_target_pressure", frozenCurve, allocator); + + // 保留最优参数真实评价返回的原始压力点,80 点曲线只用于统一误差复核, + // 不能替代求解器原始采样结果。 + rapidjson::Value globalBestCurve(rapidjson::kObjectType); + globalBestCurve.AddMember("time_hr", autoFitJsonDoubleArray( + m_globalBestPressureData.size() > 0 + ? m_globalBestPressureData[0] + : QVector(), allocator).Move(), allocator); + globalBestCurve.AddMember("pressure_mpa", autoFitJsonDoubleArray( + m_globalBestPressureData.size() > 1 + ? m_globalBestPressureData[1] + : QVector(), allocator).Move(), allocator); + root.AddMember("global_best_pressure", globalBestCurve, allocator); + + rapidjson::StringBuffer buffer; + rapidjson::PrettyWriter writer(buffer); + root.Accept(writer); + + QFile file(m_lastRunResult.resultJsonPath); + if(!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + return false; + } + const qint64 size = static_cast(buffer.GetSize()); + const qint64 written = file.write(buffer.GetString(), size); + file.close(); + return written == size; +} + +bool nmCalculationAutoFitPSO::appendRunSummaryCsv() +{ + static QMutex s_runsCsvMutex; + QMutexLocker locker(&s_runsCsvMutex); + QDir rootDir = QFileInfo(m_lastRunResult.runsCsvPath).absoluteDir(); + if(!rootDir.exists() && !QDir().mkpath(rootDir.absolutePath())) { + return false; + } + + QFile file(m_lastRunResult.runsCsvPath); + const bool writeHeader = !file.exists() || file.size() == 0; + if(!file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) { + return false; + } + QTextStream stream(&file); + stream.setCodec("UTF-8"); + stream.setGenerateByteOrderMark(writeHeader); + + const QStringList parameterNames = traceParameterNames(); + if(writeHeader) { + QStringList header; + header << "run_id" << "status" << "stop_reason" + << "started_at" << "finished_at" << "target_well" + << "phase" << "algorithm" << "solver_type" + << "openmp_threads" << "ilu_reuse_steps" + << "optimization_wall_time_ms" << "workflow_wall_time_ms" + << "solver_time_sum_ms" << "final_solver_time_ms" + << "iterations" << "parameter_evaluations" + << "model_solver_calls" << "final_solver_calls" + << "solver_successes" << "solver_failures" << "solver_timeouts" + << "final_solver_status" << "optimization_pebi_count" + << "final_pebi_count" << "pebi_count" << "initial_internal_error" + << "initial_pressure_mpa" << "final_internal_error" << "curve_metrics_valid" + << "curve_invalid_reason" << "coverage" + << "pressure_rmse_mpa" << "pressure_max_abs_error_mpa" + << "log_delta_p_rmse_decade" + << "log_derivative_rmse_decade" << "unified_curve_error" + << "curve_passed" << "target_curve_sha256"; + for(int i = 0; i < parameterNames.size(); ++i) { + header << QString("final_%1").arg(parameterNames[i]); + } + header << "result_json_path" << "curve_csv_path" + << "trace_csv_path" << "trace_meta_json_path" << "project_path" + << "artifact_error"; + stream << header.join(QString(",")) << "\n"; + } + + QMap finalParameters; + for(int i = 0; i < m_lastRunResult.parameters.size(); ++i) { + finalParameters.insert(m_lastRunResult.parameters[i].name, + m_lastRunResult.parameters[i].finalValue); + } + + const AutoFitCurveMetrics& metrics = m_lastRunResult.curveMetrics; + QStringList row; + row << autoFitCsvField(m_lastRunResult.runId) + << autoFitCsvField(m_lastRunResult.status) + << autoFitCsvField(m_lastRunResult.stopReason) + << autoFitCsvField(autoFitDateTimeText(m_lastRunResult.startedAt)) + << autoFitCsvField(autoFitDateTimeText(m_lastRunResult.finishedAt)) + << autoFitCsvField(m_lastRunResult.targetWell) + << autoFitCsvField(m_lastRunResult.phase) + << autoFitCsvField(m_lastRunResult.algorithm) + << autoFitCsvField(m_lastRunResult.solverType) + << autoFitCsvInteger(m_lastRunResult.ompThreads) + << autoFitCsvInteger(m_lastRunResult.iluReuseSteps) + << autoFitCsvInteger(m_lastRunResult.optimizationWallTimeMs) + << autoFitCsvInteger(m_lastRunResult.workflowWallTimeMs) + << autoFitCsvInteger(m_lastRunResult.solverTimeSumMs) + << autoFitCsvInteger(m_lastRunResult.finalSolverTimeMs) + << QString::number(m_lastRunResult.iterationCount) + << QString::number(m_lastRunResult.parameterEvaluationCount) + << QString::number(m_lastRunResult.modelSolverCallCount) + << QString::number(m_lastRunResult.finalSolverCallCount) + << QString::number(m_lastRunResult.solverSuccessCount) + << QString::number(m_lastRunResult.solverFailureCount) + << QString::number(m_lastRunResult.solverTimeoutCount) + << autoFitCsvField(m_lastRunResult.finalSolverStatus) + << autoFitCsvInteger(m_lastRunResult.optimizationPebiCount) + << autoFitCsvInteger(m_lastRunResult.finalPebiCount) + << autoFitCsvInteger(m_lastRunResult.pebiCount) + << autoFitCsvNumber(m_lastRunResult.initialInternalError) + << autoFitCsvNumber(m_lastRunResult.initialPressureMpa) + << autoFitCsvNumber(m_lastRunResult.finalInternalError) + << (metrics.valid ? "1" : "0") + << autoFitCsvField(metrics.invalidReason) + << autoFitCsvNumber(metrics.coverage) + << autoFitCsvNumber(metrics.pressureRmseMpa) + << autoFitCsvNumber(metrics.pressureMaxAbsErrorMpa) + << autoFitCsvNumber(metrics.logDeltaPRmseDecade) + << autoFitCsvNumber(metrics.logDerivativeRmseDecade) + << autoFitCsvNumber(metrics.unifiedCurveError) + << (metrics.passed ? "1" : "0") + << autoFitCsvField(m_lastRunResult.targetCurveSha256); + for(int i = 0; i < parameterNames.size(); ++i) { + row << (finalParameters.contains(parameterNames[i]) + ? autoFitCsvNumber(finalParameters.value(parameterNames[i])) + : QString()); + } + row << autoFitCsvField(m_lastRunResult.resultJsonPath) + << autoFitCsvField(m_lastRunResult.curveCsvPath) + << autoFitCsvField(m_lastRunResult.traceCsvPath) + << autoFitCsvField(m_lastRunResult.traceMetaJsonPath) + << autoFitCsvField(m_lastRunResult.projectPath) + << autoFitCsvField(m_lastRunResult.artifactError); + stream << row.join(QString(",")) << "\n"; + stream.flush(); + return stream.status() == QTextStream::Ok; +} diff --git a/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp b/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp index 0a11b33..4776c64 100644 --- a/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp +++ b/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp @@ -15,6 +15,7 @@ #pragma comment(lib, "mGuiAnal.lib") #include +#include #include #include #include @@ -1450,6 +1451,15 @@ void nmWxAutomaticFitting::startAutoFitting(const QVector>& targ m_autoFitterPSO = new nmCalculationAutoFitPSO(this); m_autoFitterPSO->setTargetLogLogData(targetData); m_autoFitterPSO->setPSOTargetWellName(targetWellName); + nmDataAnalyzeManager* pDataManager = nmDataAnalyzeManager::getCurrentInstance(); + nmDataWellBase* pTargetWell = pDataManager + ? pDataManager->findWellByName(targetWellName) + : nullptr; + if(pTargetWell) { + // 原始压力与双对数目标分别传入。优化器在正式运行开始时冻结压力快照, + // 独立统一误差不再复用优化器内部目标值。 + m_autoFitterPSO->setTargetPressureData(pTargetWell->getHistoryPressure()); + } //// 特定井名时使用快速路径 //if (targetWellName == "VerticalWell1") { @@ -1526,32 +1536,51 @@ void nmWxAutomaticFitting::onFittingFinished(bool success, const QString& messag disconnect(m_autoFitterPSO, SIGNAL(fittingFinished(bool, QString)), this, SLOT(onFittingFinished(bool, QString))); } - if(success) { - // 只有成功拟合的结果才用于生成下一轮范围,失败结果不污染当前配置。 - updateBestParametersToTable(); - - QString resultInfo; + if(!m_autoFitterPSO) { + QMessageBox::warning(this, tr("Optimization Failed"), message); + return; + } - if(m_autoFitterPSO) { - double bestFitness = m_autoFitterPSO->getBestFitness(); + const AutoFitRunResult runResult = m_autoFitterPSO->getLastRunResult(); + if(runResult.status != "FAILED") { + // 正常完成和用户停止仍保留已确认的最佳参数;FAILED 不更新下一轮范围。 + updateBestParametersToTable(); + } - // 检查是否是用户停止的情况 - if(message.contains("stopped by user", Qt::CaseInsensitive)) { - resultInfo = tr("PSO Optimization stopped by user:\n"); - 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("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); - } - } + QString unifiedError = runResult.curveMetrics.valid + ? QString::number(runResult.curveMetrics.unifiedCurveError, 'g', 10) + : tr("Invalid (%1)").arg(runResult.curveMetrics.invalidReason); + QString resultInfo; + resultInfo += tr("Final status: %1\n").arg(runResult.status); + resultInfo += tr("Stop reason: %1\n").arg(runResult.stopReason); + resultInfo += tr("Fitting wall time: %1 ms\n") + .arg(runResult.optimizationWallTimeMs >= 0 + ? QString::number(runResult.optimizationWallTimeMs) + : QString("N/A")); + resultInfo += tr("Iterations: %1\n").arg(runResult.iterationCount); + resultInfo += tr("Model solver calls: %1\n").arg(runResult.modelSolverCallCount); + resultInfo += tr("Unified curve error: %1\n").arg(unifiedError); + if(!runResult.artifactError.isEmpty()) { + resultInfo += tr("Result export failed: %1\n").arg(runResult.artifactError); + } + const bool resultJsonExists = !runResult.resultJsonPath.isEmpty() && + QFileInfo(runResult.resultJsonPath).isFile(); + resultInfo += resultJsonExists + ? tr("Result JSON: %1").arg(runResult.resultJsonPath) + : tr("Result JSON: Not created"); + + if(runResult.status == "FAILED") { + QMessageBox::warning(this, tr("Optimization Failed"), resultInfo); + } else if(runResult.status == "TIMEOUT") { + QMessageBox::warning(this, tr("Optimization Timed Out"), resultInfo); + } else if(!runResult.artifactError.isEmpty()) { + QMessageBox::warning(this, tr("Result Export Failed"), resultInfo); + } else if(runResult.status == "STOPPED") { + QMessageBox::information(this, tr("Optimization Stopped"), resultInfo); } else { - // 只有真正失败的情况才显示警告 - QMessageBox::warning(this, tr("Optimization Failed"), message); + QMessageBox::information(this, tr("Optimization Completed"), resultInfo); } + Q_UNUSED(success); } void nmWxAutomaticFitting::onStopFitting() diff --git a/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp b/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp index acb1a3f..e7e0a35 100644 --- a/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp +++ b/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -388,7 +389,6 @@ nmWxAutomaticfittingStart::nmWxAutomaticfittingStart(QWidget *parent) , m_wellName("") , m_isFinished(false) , m_bestFitnessEver(1e10) - , m_startTime() { setupUI(); @@ -619,10 +619,8 @@ void nmWxAutomaticfittingStart::setFittingParameters(int maxIterations, double t void nmWxAutomaticfittingStart::markFittingStarted() { - m_startTime = QDateTime::currentDateTime(); - const QString algorithmName = "PSO"; - QString timestamp = m_startTime.toString("yyyy-MM-dd hh:mm:ss"); + QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); addLogMessage(tr("=== %1 Fitting Session Started at %2 ===") .arg(algorithmName) @@ -686,53 +684,69 @@ void nmWxAutomaticfittingStart::onFittingProgress(int iteration, double fitness) void nmWxAutomaticfittingStart::onFittingFinished(bool success, const QString& message) { m_isFinished = true; - const QString algorithmName = "PSO"; + const AutoFitRunResult runResult = m_autoFitterPSO + ? m_autoFitterPSO->getLastRunResult() + : AutoFitRunResult(); - // 更新状态 - if (success) { + // 进度窗口只消费优化器已经完成的权威结果,不再自行计算耗时或解析消息文本。 + if(runResult.status == "SUCCESS") { progressBar->setValue(m_maxIterations); - - // 区分正常完成和用户停止 - if(message.contains("stopped by user", Qt::CaseInsensitive)) { - addLogMessage(tr("%1 fitting stopped by user: %2").arg(algorithmName).arg(message)); - - // 更新进度条显示停止状态 - progressBar->setFormat(tr("Stopped: 100%")); - progressBar->setStyleSheet( - "QProgressBar { border: 2px solid grey; border-radius: 5px; text-align: center; }" - "QProgressBar::chunk { background-color: #FF9800; }" // 橙色表示停止 - ); - } else if(message.contains("target achieved", Qt::CaseInsensitive)) { - addLogMessage(tr("%1 fitting completed successfully - target achieved: %2").arg(algorithmName).arg(message)); - - // 更新进度条显示成功状态 - progressBar->setFormat(tr("Target Achieved: 100%")); - progressBar->setStyleSheet( - "QProgressBar { border: 2px solid grey; border-radius: 5px; text-align: center; }" - "QProgressBar::chunk { background-color: #4CAF50; }" // 绿色表示成功 - ); - } else { - addLogMessage(tr("%1 fitting completed successfully: %2").arg(algorithmName).arg(message)); - - // 更新进度条显示完成状态 - progressBar->setFormat(tr("Completed: 100%")); - progressBar->setStyleSheet( - "QProgressBar { border: 2px solid grey; border-radius: 5px; text-align: center; }" - "QProgressBar::chunk { background-color: #2196F3; }" // 蓝色表示完成 - ); - } + progressBar->setFormat(tr("Target Achieved: 100%")); + progressBar->setStyleSheet( + "QProgressBar { border: 2px solid grey; border-radius: 5px; text-align: center; }" + "QProgressBar::chunk { background-color: #4CAF50; }"); + } else if(runResult.status == "COMPLETED") { + progressBar->setValue(m_maxIterations); + progressBar->setFormat(tr("Completed: 100%")); + progressBar->setStyleSheet( + "QProgressBar { border: 2px solid grey; border-radius: 5px; text-align: center; }" + "QProgressBar::chunk { background-color: #2196F3; }"); + } else if(runResult.status == "STOPPED") { + progressBar->setFormat(tr("Stopped")); + progressBar->setStyleSheet( + "QProgressBar { border: 2px solid grey; border-radius: 5px; text-align: center; }" + "QProgressBar::chunk { background-color: #FF9800; }"); + } else if(runResult.status == "TIMEOUT") { + progressBar->setFormat(tr("Timed out")); + progressBar->setStyleSheet( + "QProgressBar { border: 2px solid grey; border-radius: 5px; text-align: center; }" + "QProgressBar::chunk { background-color: #F44336; }"); } else { - // 真正的失败情况 - addLogMessage(tr("%1 fitting failed: %2").arg(algorithmName).arg(message)); - - // 更新进度条显示失败状态 - int currentProgress = progressBar->value(); - progressBar->setFormat(tr("Failed (%1%)").arg((double)currentProgress / m_maxIterations * 100, 0, 'f', 1)); + const int currentProgress = progressBar->value(); + progressBar->setFormat(tr("Failed (%1%)") + .arg(m_maxIterations > 0 + ? static_cast(currentProgress) / m_maxIterations * 100.0 + : 0.0, + 0, 'f', 1)); progressBar->setStyleSheet( "QProgressBar { border: 2px solid grey; border-radius: 5px; text-align: center; }" - "QProgressBar::chunk { background-color: #F44336; }" // 红色表示失败 - ); + "QProgressBar::chunk { background-color: #F44336; }"); + } + + addLogMessage(tr("Final status: %1").arg(runResult.status)); + addLogMessage(tr("Stop reason: %1").arg(runResult.stopReason)); + addLogMessage(tr("Fitting wall time: %1 ms") + .arg(runResult.optimizationWallTimeMs >= 0 + ? QString::number(runResult.optimizationWallTimeMs) + : QString("N/A"))); + addLogMessage(tr("Iterations: %1, model solver calls: %2") + .arg(runResult.iterationCount) + .arg(runResult.modelSolverCallCount)); + addLogMessage(runResult.curveMetrics.valid + ? tr("Unified curve error: %1") + .arg(runResult.curveMetrics.unifiedCurveError, 0, 'g', 10) + : tr("Unified curve error: Invalid (%1)") + .arg(runResult.curveMetrics.invalidReason)); + if(!runResult.artifactError.isEmpty()) { + addLogMessage(tr("Result export failed: %1") + .arg(runResult.artifactError)); + } + if(!runResult.resultJsonPath.isEmpty() && + QFileInfo(runResult.resultJsonPath).isFile()) { + addLogMessage(tr("Result JSON: %1").arg(runResult.resultJsonPath)); + } else { + addLogMessage(tr("Result JSON: Not created")); } // 禁用停止按钮 @@ -746,27 +760,14 @@ void nmWxAutomaticfittingStart::onFittingFinished(bool success, const QString& m // 最终更新参数表格 updateParameterTable(); - // 添加完成时间戳 - QDateTime endTime = QDateTime::currentDateTime(); - QString timestamp = endTime.toString("yyyy-MM-dd hh:mm:ss"); + // 完成时间同样读取结构化结果,避免界面各自取时导致显示不一致。 + QString timestamp = runResult.finishedAt.isValid() + ? runResult.finishedAt.toString("yyyy-MM-dd hh:mm:ss") + : QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"); addLogMessage(tr("=== %1 Fitting Session Ended at %2 ===") .arg(algorithmName).arg(timestamp)); - - // 如果有开始时间,则计算总耗时 - if (m_startTime.isValid()) { - qint64 ms = m_startTime.msecsTo(endTime); - double seconds = ms / 1000.0; - - int minutes = static_cast(seconds / 60.0); - double remainSec = seconds - minutes * 60.0; - - // 同时给总秒数和分秒 - addLogMessage(tr("Total fitting time: %1 s (%2 min %3 s)") - .arg(seconds, 0, 'f', 2) - .arg(minutes) - .arg(remainSec, 0, 'f', 2)); - } - + Q_UNUSED(success); + Q_UNUSED(message); } void nmWxAutomaticfittingStart::onStopButtonClicked() diff --git a/Src4/nmNum/nmCalculation/nmCalculation.pro b/Src4/nmNum/nmCalculation/nmCalculation.pro index 458b09f..8a0540f 100644 --- a/Src4/nmNum/nmCalculation/nmCalculation.pro +++ b/Src4/nmNum/nmCalculation/nmCalculation.pro @@ -36,6 +36,6 @@ INCLUDEPATH += \ INCLUDEPATH += $${geoHome}/3rd/JSON/rapidjson-1.1.0/include/ -LIBS += -liLogs -liBase -lmProjectManager -liDataEngine -liDataPool \ +LIBS += -liLogs -liBase -liUtils -lmProjectManager -liDataEngine -liDataPool \ -lmProjectManager \ -lnmData