Compare commits

...

1 Commits

Author SHA1 Message Date
lh 471efc25cf feat(nmNum): 新增自动拟合结构化结果与统一误差统计
- 记录拟合耗时、求解调用次数、参数结果及结束状态
- 独立计算压力、压差和 Bourdet 导数统一曲线误差
- 输出运行 JSON、80 点曲线 CSV 和汇总 CSV
- 增加完整拟合 1800 秒超时控制和导出错误追踪
- 同步自动拟合结果摘要及中文翻译
15 hours ago

Binary file not shown.

@ -77,6 +77,10 @@ Reason: %1</source>
<source>PSO optimization stop request processed</source>
<translation>PSO</translation>
</message>
<message>
<source>Simulation stopped by user</source>
<translation></translation>
</message>
<message>
<source>Stop request received but optimization is not running</source>
<translation></translation>
@ -413,6 +417,30 @@ Reason: %1</source>
<source>Optimization failed</source>
<translation></translation>
</message>
<message>
<source>Automatic fitting run time limit reached</source>
<translation></translation>
</message>
<message>
<source>Automatic fitting stopped after reaching the run time limit</source>
<translation></translation>
</message>
<message>
<source>Final full-field calculation skipped after run time limit</source>
<translation></translation>
</message>
<message>
<source>Final full-field calculation reached the run time limit</source>
<translation></translation>
</message>
<message>
<source>Automatic fitting exceeded the run time limit</source>
<translation></translation>
</message>
<message>
<source>=== AUTOMATIC FITTING TIMED OUT ===</source>
<translation>=== ===</translation>
</message>
<message>
<source>Unknown reason</source>
<translation></translation>
@ -530,8 +558,8 @@ Reason: %1</source>
<translation>%1</translation>
</message>
<message>
<source>=== PSO Run Summary ===</source>
<translation>=== PSO ===</translation>
<source>=== Automatic Fitting Run Summary ===</source>
<translation>=== ===</translation>
</message>
<message>
<source>Stop reason: %1</source>
@ -561,6 +589,10 @@ Reason: %1</source>
<source>Artifacts: trace_meta=%1</source>
<translation>=%1</translation>
</message>
<message>
<source>Result artifact export failed: %1</source>
<translation>%1</translation>
</message>
<message>
<source>Surrogate scoring attempt %1/%2: python=%3, mlRoot=%4</source>
<translation> %1/%2Python=%3mlRoot=%4</translation>
@ -3463,6 +3495,10 @@ Supported types: Vertical, Vertical Fractured, and Horizontal Multi-Fractured We
<source>Optimization Failed</source>
<translation></translation>
</message>
<message>
<source>Optimization Timed Out</source>
<translation></translation>
</message>
<message>
<source>Swi</source>
<translation></translation>
@ -3520,6 +3556,64 @@ Supported types: Vertical, Vertical Fractured, and Horizontal Multi-Fractured We
<source>The minimum value of %1 must be greater than zero for automatic fitting.</source>
<translation>%1 </translation>
</message>
<message>
<source>Invalid (%1)</source>
<translation>%1</translation>
</message>
<message>
<source>Final status: %1
</source>
<translation>%1
</translation>
</message>
<message>
<source>Stop reason: %1
</source>
<translation>%1
</translation>
</message>
<message>
<source>Fitting wall time: %1 ms
</source>
<translation>%1 ms
</translation>
</message>
<message>
<source>Iterations: %1
</source>
<translation>%1
</translation>
</message>
<message>
<source>Model solver calls: %1
</source>
<translation>%1
</translation>
</message>
<message>
<source>Unified curve error: %1
</source>
<translation>线%1
</translation>
</message>
<message>
<source>Result JSON: %1</source>
<translation> JSON%1</translation>
</message>
<message>
<source>Result export failed: %1
</source>
<translation>%1
</translation>
</message>
<message>
<source>Result JSON: Not created</source>
<translation> JSON</translation>
</message>
<message>
<source>Result Export Failed</source>
<translation></translation>
</message>
</context>
<context>
<name>nmWxAutomaticfitting</name>
@ -3742,6 +3836,50 @@ Supported types: Vertical, Vertical Fractured, and Horizontal Multi-Fractured We
<source>Failed (%1%)</source>
<translation> (%1%)</translation>
</message>
<message>
<source>Stopped</source>
<translation></translation>
</message>
<message>
<source>Timed out</source>
<translation></translation>
</message>
<message>
<source>Final status: %1</source>
<translation>%1</translation>
</message>
<message>
<source>Stop reason: %1</source>
<translation>%1</translation>
</message>
<message>
<source>Fitting wall time: %1 ms</source>
<translation>%1 ms</translation>
</message>
<message>
<source>Iterations: %1, model solver calls: %2</source>
<translation>%1%2</translation>
</message>
<message>
<source>Unified curve error: %1</source>
<translation>线%1</translation>
</message>
<message>
<source>Unified curve error: Invalid (%1)</source>
<translation>线%1</translation>
</message>
<message>
<source>Result JSON: %1</source>
<translation> JSON%1</translation>
</message>
<message>
<source>Result export failed: %1</source>
<translation>%1</translation>
</message>
<message>
<source>Result JSON: Not created</source>
<translation> JSON</translation>
</message>
<message>
<source>Close</source>
<translation></translation>

@ -8,6 +8,7 @@
#include <QStringList>
#include <QMutex>
#include <QDateTime>
#include <QElapsedTimer>
#include <QFile>
#include <limits>
@ -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<double> timeHr;
QVector<double> targetPressureMpa;
QVector<double> fittedPressureMpa;
QVector<double> targetDeltaPMpa;
QVector<double> fittedDeltaPMpa;
QVector<double> targetDerivativeMpa;
QVector<double> 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<AutoFitParameterResult> 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<QVector<double> > currentPressureData;
QVector<QVector<double> > bestPressureData;
QVector<QVector<double> > currentLogLogData;
QVector<QVector<double> > bestLogLogData;
QVector<double> 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<QVector<double> >& targetData);
void setTargetPressureData(const QVector<QVector<double> >& targetData);
bool startAutoFitting();
void stopFitting();
QVector<double> 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<QVector<double> >& targetPressureData,
const QVector<QVector<double> >& 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<QVector<double> >* curve,
QVector<QVector<double> >* 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<QVector<double> >& 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<QVector<double> > m_lastEvaluatedLogLogData; // 最近一次真实求解得到的 result log-log 曲线。
QVector<QVector<double> > m_globalBestLogLogData; // 当前全局最优对应的 result log-log 曲线。
QVector<QVector<double> > m_lastEvaluatedPressureData; // 最近一次真实求解得到的原始压力曲线。
QVector<QVector<double> > m_globalBestPressureData; // 当前全局最优对应的原始压力曲线。
mutable AutoFitObjectiveBreakdown m_lastObjectiveBreakdown; // 最近一次损失评价的误差分解。
QVector<QVector<double> > m_userInitialLogLogData; // 用户初始解对应的 result log-log 曲线,用于精英保护。
QVector<QVector<double> > m_userInitialPressureData; // 用户初始解对应的原始压力曲线,用于精英保护。
AutoFitObjectiveBreakdown m_userInitialObjectiveBreakdown; // 用户初始解对应的误差分解。
// ===== 优化配置 =====
@ -380,6 +516,8 @@ private:
QVector<double> m_parameterUpper; // 完整 10 个参数的搜索上界。
QVector<int> m_enabledParamIndices; // 被勾选参数在完整 10 维体系中的索引。
QVector<QVector<double> > m_targetLogLogData; // 目标井 history log-log 曲线time/pressure/derivative。
QVector<QVector<double> > m_targetPressureData; // 界面传入的目标井原始压力曲线time/pressure。
QVector<QVector<double> > m_frozenTargetPressureData; // 本次运行开始时冻结的目标压力曲线。
QString m_targetWellName; // 目标井名称;读写井参数和读取模拟曲线都依赖它。
// ===== 算法配置 =====
@ -394,11 +532,16 @@ private:
int m_totalEvaluations; // 真实求解器评价总次数,包含粒子评价和方向试算。
int m_successfulEvaluations; // 真实求解器成功且误差有效的评价次数。
QVector<double> 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; // 并发控制

@ -160,7 +160,6 @@ private:
bool m_isFinished;
double m_bestFitnessEver;
QDateTime m_startTime;
};
#endif // NMWXAUTOMATICFITTINGSTART_H

@ -0,0 +1,476 @@
#include "nmCalculationAutoFitPSO.h"
#include <QtCore/qmath.h>
#include <algorithm>
#include <cmath>
#ifdef Q_OS_WIN
#include <float.h>
#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<AutoFitPressurePoint> autoFitNormalizePressureCurve(
const QVector<QVector<double> >& pressureData)
{
QVector<AutoFitPressurePoint> 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<AutoFitPressurePoint> 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<AutoFitPressurePoint>& 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<double>& timeHr,
const QVector<double>& deltaPMpa,
QVector<double>* 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<double>::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<double>::quiet_NaN())
, pressureRmseMpa(std::numeric_limits<double>::quiet_NaN())
, pressureMaxAbsErrorMpa(std::numeric_limits<double>::quiet_NaN())
, logDeltaPRmseDecade(std::numeric_limits<double>::quiet_NaN())
, logDerivativeRmseDecade(std::numeric_limits<double>::quiet_NaN())
, unifiedCurveError(std::numeric_limits<double>::quiet_NaN())
, sampleCount(0)
, validDerivativeCount(0)
{
}
AutoFitParameterResult::AutoFitParameterResult()
: initialValue(std::numeric_limits<double>::quiet_NaN())
, lowerBound(std::numeric_limits<double>::quiet_NaN())
, upperBound(std::numeric_limits<double>::quiet_NaN())
, finalValue(std::numeric_limits<double>::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<double>::quiet_NaN())
, initialInternalError(std::numeric_limits<double>::quiet_NaN())
, finalInternalError(std::numeric_limits<double>::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<QVector<double> >& targetPressureData,
const QVector<QVector<double> >& 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<AutoFitPressurePoint> targetPoints =
autoFitNormalizePressureCurve(targetPressureData);
const QVector<AutoFitPressurePoint> 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<double>(i) / static_cast<double>(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<double>(sampleCount));
metrics.pressureMaxAbsErrorMpa = pressureMaxAbsError;
if(validLogDeltaPCount > 0) {
metrics.logDeltaPRmseDecade = qSqrt(
logDeltaPSquaredSum /
static_cast<double>(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<double>(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;
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,908 @@
#include "nmCalculationAutoFitPSO.h"
#include "nmDataAnalyzeManager.h"
#include "nmDataReservoir.h"
#include "nmDataWellBase.h"
#include "iBase/iUtils/ZxBaseUtil.h"
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QMap>
#include <QMutexLocker>
#include <QTextStream>
#include <QUuid>
#include <QtCore/qmath.h>
#include <cmath>
#include "rapidjson/document.h"
#include "rapidjson/prettywriter.h"
#include "rapidjson/stringbuffer.h"
#ifdef Q_OS_WIN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <wincrypt.h>
#include <float.h>
#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<rapidjson::SizeType>(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<const BYTE*>(content.constData()),
static_cast<DWORD>(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<const char*>(hashBytes),
static_cast<int>(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<double>& 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<qint64>(RUN_TIME_LIMIT_MS) -
m_workflowWallTimer.elapsed();
return remaining > 0
? static_cast<int>(qMin(remaining,
static_cast<qint64>(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<double>::quiet_NaN();
m_lastRunResult.finalInternalError =
m_globalBestFitness < 1.0e9
? m_globalBestFitness
: std::numeric_limits<double>::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<QVector<double> >& 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<double>::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<double>::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<double>::quiet_NaN()) << ','
<< autoFitCsvNumber(i < metrics.fittedDerivativeMpa.size()
? metrics.fittedDerivativeMpa[i]
: std::numeric_limits<double>::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<double>(), allocator).Move(), allocator);
frozenCurve.AddMember("pressure_mpa", autoFitJsonDoubleArray(
m_frozenTargetPressureData.size() > 1
? m_frozenTargetPressureData[1]
: QVector<double>(), 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<double>(), allocator).Move(), allocator);
globalBestCurve.AddMember("pressure_mpa", autoFitJsonDoubleArray(
m_globalBestPressureData.size() > 1
? m_globalBestPressureData[1]
: QVector<double>(), allocator).Move(), allocator);
root.AddMember("global_best_pressure", globalBestCurve, allocator);
rapidjson::StringBuffer buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
root.Accept(writer);
QFile file(m_lastRunResult.resultJsonPath);
if(!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
return false;
}
const qint64 size = static_cast<qint64>(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<QString, double> 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;
}

@ -15,6 +15,7 @@
#pragma comment(lib, "mGuiAnal.lib")
#include <QApplication>
#include <QFileInfo>
#include <QHeaderView>
#include <cmath>
#include <float.h>
@ -1450,6 +1451,15 @@ void nmWxAutomaticFitting::startAutoFitting(const QVector<QVector<double>>& 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()

@ -3,6 +3,7 @@
#include <QtCore/QEvent>
#include <QtGui/QMouseEvent>
#include <QtGui/QFont>
#include <QFileInfo>
#include <QScrollBar>
#include <QList>
#include <QPainter>
@ -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<double>(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<int>(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()

@ -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

Loading…
Cancel
Save