diff --git a/Include/nmNum/nmCalculation/nmCalculationAutoFitPSO.h b/Include/nmNum/nmCalculation/nmCalculationAutoFitPSO.h index 0825f9d..e0b1ace 100644 --- a/Include/nmNum/nmCalculation/nmCalculationAutoFitPSO.h +++ b/Include/nmNum/nmCalculation/nmCalculationAutoFitPSO.h @@ -1,4 +1,4 @@ -#ifndef NMCALCULATIONAUTOFIT_H +#ifndef NMCALCULATIONAUTOFIT_H #define NMCALCULATIONAUTOFIT_H #include @@ -20,6 +20,12 @@ class QTimer; class QProcess; // PSO粒子结构 +// 这里的 position / velocity / bestPosition 只保存“用户勾选参与拟合的参数”, +// 不是完整的 11 个储层/井筒参数。完整参数向量会在写 trace 或调用代理模型时 +// 通过 buildTraceParameterVector() 重新组装。 +// +// surrogate* 和 screeningDecision 是 PSO 加速筛选的辅助字段。代理模型只决定 +// “这一代哪些粒子需要继续跑真实求解器”,不会直接更新 pbest / gbest。 struct AutoFitParticle { QVector > currentLogLogData; QVector > bestLogLogData; @@ -50,7 +56,8 @@ struct AutoFitParticle { {} }; -// 判断类型枚举 +// PSO停止原因。主循环每一代结束时会根据目标误差、收敛历史、粒子群多样性、 +// 连续失败次数等状态给出一个停止判断。 enum StopReasonPSO { PSO_CONTINUE_OPTIMIZATION = 0, // 继续优化 PSO_TARGET_ACHIEVED = 1, // 达到目标精度 @@ -72,6 +79,11 @@ public: ~nmCalculationAutoFitPSO(); // ===== 核心接口:完全数据驱动 ===== + // + // 这个类不直接读取自动拟合对话框控件。界面层 nmWxAutomaticFitting 会先把 + // 用户选择的参数范围、迭代次数、误差容限、PSO acceleration 开关等保存到 + // nmDataAnalyzeManager / nmDataAutomaticFitting,然后本类在 startAutoFitting() + // 内部统一读取。 void setTargetLogLogData(const QVector >& targetData); bool startAutoFitting(); void stopFitting(); @@ -119,6 +131,13 @@ private: void loadParameterBounds(); // ===== PSO核心算法 ===== + // + // 主流程: + // 1. extractUserInitialValues(): 从当前项目数据中取用户已有初始解; + // 2. initializeSwarm(): 根据初始解和上下界生成粒子群; + // 3. updateParticle(): 对单个粒子跑真实求解器并计算误差; + // 4. updateGlobalBest(): 只用真实求解器误差更新全局最优; + // 5. updateVelocityAndPosition(): 按 PSO 公式推进下一代粒子。 void extractUserInitialValues(); void initializeSwarm(); void updateVelocityAndPosition(); @@ -127,6 +146,11 @@ private: void updateParticle(int particleIndex); // ===== 参数应用方法 ===== + // + // 每次评价粒子前,都会把该粒子的参数临时写入 DataManager: + // - 储层参数写入 nmDataReservoir; + // - 井参数(skin / wellbore storage)写入目标井。 + // 求解器随后基于 DataManager 的当前状态计算模拟曲线。 void applyParametersToDataManager(const QVector& parameters); void updateReservoirParameters(const QVector& parameters); void updateWellParameters(const QVector& parameters); @@ -171,6 +195,10 @@ private: void logDebugInfo(const QString& message) const; // ===== Baseline trace ===== + // + // trace:记录每一代、每个粒子的参数、真实求解器误差、 + // 代理模型误差、筛选决策、pbest 和 gbest。代理筛选质量、某个粒子为什么 + // 没有跑真实求解器,都应该优先看 trace。 void initializeTraceFile(); void closeTraceFile(); void writeTraceHeader(); @@ -193,6 +221,15 @@ private: void emitRunSummary(bool success, StopReasonPSO finalReason); // ===== Surrogate screening prototype ===== + // + // PSO acceleration 的核心逻辑。开启后,buildSurrogateEvaluationMask() 会: + // 1. 判断当前工况是否在代理模型训练域内; + // 2. 将当代粒子候选写成 CSV; + // 3. 调用 Python 代理模型输出 surrogate_objective; + // 4. 选择 top-k、随机审计、fallback 和最小真实求解比例对应的粒子; + // 5. 返回 bool mask,true 表示该粒子继续跑真实求解器。 + // + // 代理模型只筛选候选,不直接决定最终最优参数。 bool isSurrogateScreeningEnabled() const; QString getMlRootPath() const; QString getPythonExecutablePath() const; @@ -228,57 +265,64 @@ private: private: // ===== 运行状态 ===== - bool m_isRunning; - bool m_shouldStop; - bool m_isPaused; - int m_currentIteration; - QString m_lastError; + bool m_isRunning; // 当前是否有一次自动拟合正在运行。 + bool m_shouldStop; // 用户停止标志;主循环和求解器等待循环会定期检查它。 + bool m_isPaused; // 预留暂停标志;主循环中有暂停等待逻辑。 + int m_currentIteration; // 当前 PSO 迭代序号,从 0 开始。 + QString m_lastError; // 最近一次失败原因,供 UI 展示或日志排查。 // ===== PSO数据 ===== - QVector m_initialValues; // 用户设置的初始值 - QVector m_swarm; - QVector m_globalBestPosition; - double m_globalBestFitness; - double m_previousBestFitness; - QVector > m_lastEvaluatedLogLogData; - QVector > m_globalBestLogLogData; - QVector > m_userInitialLogLogData; + QVector m_initialValues; // 当前模型中提取的用户初始值,顺序与 m_enabledParamIndices 一致。 + QVector m_swarm; // 粒子群,每个粒子只保存启用参数维度。 + QVector m_globalBestPosition; // 全局最优参数,仍是启用参数向量。 + double m_globalBestFitness; // 全局最优真实误差,越小越好。 + double m_previousBestFitness; // 上一轮全局最优误差,用于自适应参数更新。 + QVector > m_lastEvaluatedLogLogData; // 最近一次真实求解得到的 result log-log 曲线。 + QVector > m_globalBestLogLogData; // 当前全局最优对应的 result log-log 曲线。 + QVector > m_userInitialLogLogData; // 用户初始解对应的 result log-log 曲线,用于精英保护。 // ===== 优化配置 ===== - QVector m_parameterSelected; - QVector m_parameterLower; - QVector m_parameterUpper; - QVector m_enabledParamIndices; - QVector > m_targetLogLogData; - QString m_targetWellName; + // + // 参数索引约定: + // 0 k 渗透率;1 skin 表皮系数;2 wellboreC 井筒储集; + // 3 phi 孔隙度;4 initialPressure 初始压力;5 h 储层厚度; + // 6 Ct 综合压缩系数;7 Cf 岩石压缩系数; + // 8 Soi 初始含油饱和度;9 Swi 初始含水饱和度;10 Sgi 初始含气饱和度。 + // m_enabledParamIndices 保存被用户勾选的参数索引,粒子的 position 维度与它一致。 + QVector m_parameterSelected; // 完整 11 个参数是否被用户勾选参与拟合。 + QVector m_parameterLower; // 完整 11 个参数的搜索下界。 + QVector m_parameterUpper; // 完整 11 个参数的搜索上界。 + QVector m_enabledParamIndices; // 被勾选参数在完整 11 维体系中的索引。 + QVector > m_targetLogLogData; // 目标井 history log-log 曲线:time/pressure/derivative。 + QString m_targetWellName; // 目标井名称;读写井参数和读取模拟曲线都依赖它。 // ===== 算法配置 ===== - int m_swarmSize; - int m_maxIterations; - double m_targetError; - double m_inertiaWeight; - double m_cognitiveParam; - double m_socialParam; + int m_swarmSize; // 粒子数量,当前默认 20。 + int m_maxIterations; // 最大迭代次数,来自自动拟合配置。 + double m_targetError; // 目标误差,小于该值视为达到拟合目标。 + double m_inertiaWeight; // 惯性权重,控制粒子保留上一轮速度的程度。 + double m_cognitiveParam; // 个体学习因子,控制粒子靠近自身 pbest 的程度。 + double m_socialParam; // 群体学习因子,控制粒子靠近全局 gbest 的程度。 // ===== 统计信息 ===== - int m_totalEvaluations; - int m_successfulEvaluations; - QVector m_convergenceHistory; + int m_totalEvaluations; // 已调用真实求解器评价的粒子总数。 + int m_successfulEvaluations; // 真实求解器成功且误差有效的评价次数。 + QVector m_convergenceHistory; // 每代全局最优误差历史,用于收敛判断。 // ===== 常量 ===== - static const double MIN_FITNESS_IMPROVEMENT; - static const double VELOCITY_LIMIT_FACTOR; - static const int CONVERGENCE_CHECK_INTERVAL; + static const double MIN_FITNESS_IMPROVEMENT; // 判断误差是否有有效改善的最小阈值。 + static const double VELOCITY_LIMIT_FACTOR; // 粒子速度上限占参数搜索区间的比例。 + static const int CONVERGENCE_CHECK_INTERVAL; // 收敛检查的基础间隔。 // ===== 资源管理 ===== volatile int m_evaluationInProgress; // 并发控制 int m_consecutiveFailures; // 连续失败计数 // ===== 精英保护 ===== - QVector m_userInitialSolution; // 用户初始解 - double m_userInitialFitness; // 用户初始解的适应度 - double m_improvementThreshold; // 改进阈值 - bool m_hasValidUserSolution; // 是否有有效的用户解 + QVector m_userInitialSolution; // 用户初始解参数,若最终改进不足会恢复它。 + double m_userInitialFitness; // 用户初始解真实误差。 + double m_improvementThreshold; // 最终结果相对初始解至少需要达到的改进阈值。 + bool m_hasValidUserSolution; // 初始解是否成功跑过真实求解器。 int m_consecutiveFailedIterations; // 连续失败迭代次数 int m_maxConsecutiveFailures; // 最大允许连续失败次数 @@ -299,45 +343,47 @@ private: double m_nearTargetFactor; // 接近目标的倍数因子 double m_farTargetFactor; // 远离目标的倍数因子 - // DLL求解器需要的临时目录 + // DLL求解器需要的临时目录。每个对象单独创建,析构或停止时递归清理。 QString m_tempDirectory; - // Baseline trace output. It is kept separate from legacy UI counters. - bool m_traceEnabled; - QString m_traceRunId; - QString m_traceFilePath; - QString m_traceMetaFilePath; - QFile m_traceFile; - bool m_surrogateScreeningEnabled; - unsigned int m_psoRandomSeed; - QString m_surrogateRunContextSummary; - int m_surrogateWarmupIterationCount; - int m_surrogatePeriodicAuditIterationCount; - int m_surrogateContextBlockedIterationCount; - int m_surrogateActiveIterationCount; - int m_surrogateSelectedParticleCount; - int m_surrogateScreenedParticleCount; - int m_surrogateTopKParticleCount; - int m_surrogateAuditParticleCount; - int m_surrogateFallbackParticleCount; - int m_surrogateDomainParticleCount; - int m_surrogateMinFloorParticleCount; - QProcess* m_surrogateScoringProcess; - QString m_surrogateScoringServerKey; + // ===== Trace 和代理筛选统计 ===== + // + // 这些字段只描述代理筛选和运行复盘,不参与 PSO 数学更新。 + bool m_traceEnabled; // 是否写出 trace CSV/meta 文件。 + QString m_traceRunId; // 本次运行 ID,作为 trace/candidate/score 文件名的一部分。 + QString m_traceFilePath; // pso_baseline_trace_.csv 完整路径。 + QString m_traceMetaFilePath; // pso_baseline_trace_.meta.json 完整路径。 + QFile m_traceFile; // trace CSV 文件句柄。 + bool m_surrogateScreeningEnabled; // 用户配置中的 PSO acceleration 开关。 + unsigned int m_psoRandomSeed; // PSO 随机种子,也用于可复现 random audit。 + QString m_surrogateRunContextSummary; // 本次运行代理工况 gate 的摘要。 + int m_surrogateWarmupIterationCount; // warmup 全量真实评价代数。 + int m_surrogatePeriodicAuditIterationCount; // 周期性全量审计代数。 + int m_surrogateContextBlockedIterationCount; // 因工况不支持而全量真实评价的代数。 + int m_surrogateActiveIterationCount; // 代理筛选真正参与的代数。 + int m_surrogateSelectedParticleCount; // 被选中跑真实求解器的粒子累计数。 + int m_surrogateScreenedParticleCount; // 被代理筛掉的粒子累计数。 + int m_surrogateTopKParticleCount; // 因 surrogate top-k 被选中的粒子累计数。 + int m_surrogateAuditParticleCount; // 因 random audit 被选中的粒子累计数。 + int m_surrogateFallbackParticleCount; // 因 fallback gate 被选中的粒子累计数。 + int m_surrogateDomainParticleCount; // 因训练域 gate 被选中的粒子累计数。 + int m_surrogateMinFloorParticleCount; // 因最低真实求解比例被补选中的粒子累计数。 + QProcess* m_surrogateScoringProcess; // 常驻 Python 代理评分 server 进程。 + QString m_surrogateScoringServerKey; // 标识当前 server 对应的 python/script/meta/tag/stage。 private: // 模拟拟合相关成员 - bool m_simulationMode; - QTimer* m_simulationTimer; - int m_simulationIteration; - int m_simulationMaxIterations; - QVector m_simulationTargetParams; - QVector m_simulationStartParams; - double m_simulationTargetError; - double m_simulationStartError; - double m_simulationCurrentError; - int m_simulationLogInterval; - QTime m_simulationStartTime; + bool m_simulationMode; // 是否启用仿真模式;仿真模式不调用真实求解器。 + QTimer* m_simulationTimer; // 仿真日志定时器,用于分步模拟长任务。 + int m_simulationIteration; // 仿真当前迭代。 + int m_simulationMaxIterations; // 仿真最大迭代。 + QVector m_simulationTargetParams; // 仿真最终目标参数。 + QVector m_simulationStartParams; // 仿真起始参数。 + double m_simulationTargetError; // 仿真最终目标误差。 + double m_simulationStartError; // 仿真起始误差。 + double m_simulationCurrentError; // 仿真当前误差。 + int m_simulationLogInterval; // 仿真日志输出间隔,单位秒。 + QTime m_simulationStartTime; // 仿真开始时间,用于演示节奏统计。 // 用于逐粒子输出的状态 int m_simulationCurrentParticle; // 当前正在处理的粒子索引 diff --git a/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp b/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp index 23cadba..abe62a3 100644 --- a/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp @@ -1,4 +1,4 @@ -#include "nmCalculationAutoFitPSO.h" +#include "nmCalculationAutoFitPSO.h" #include "nmCalculationDllPebiSolverTask.h" #include "nmDataAnalyzeManager.h" #include "nmDataWellBase.h" @@ -31,48 +31,64 @@ #define DEBUG_OUT(msg) OutputDebugStringA(QString("[AutoFit] %1\n").arg(msg).toLocal8Bit().data()) #endif -// 常量定义 +// ===== PSO基础常量 ===== +// MIN_FITNESS_IMPROVEMENT:判断适应度是否有有效改善的最小阈值。 +// VELOCITY_LIMIT_FACTOR:单次速度最大值占参数搜索区间的比例,防止粒子一步跨太远。 +// CONVERGENCE_CHECK_INTERVAL:收敛检查的基础间隔,部分历史逻辑和日志会使用它。 const double nmCalculationAutoFitPSO::MIN_FITNESS_IMPROVEMENT = 1e-8; const double nmCalculationAutoFitPSO::VELOCITY_LIMIT_FACTOR = 0.2; const int nmCalculationAutoFitPSO::CONVERGENCE_CHECK_INTERVAL = 5; -static const double kSurrogateKeepFraction = 0.50; -static const double kSurrogateHighRiskKeepFraction = 0.70; -static const double kSurrogateAuditFraction = 0.05; -static const double kSurrogateMinSolverFraction = 0.55; -static const double kSurrogateHighRiskMinSolverFraction = 0.80; -static const int kSurrogateWarmupIterations = 2; -static const int kSurrogateFullSolverInterval = 10; -static const int kSurrogateScoringMaxAttempts = 2; -static const bool kUseFixedPsoSeed = true; -static const unsigned int kFixedPsoSeed = 1792008679u; - -static const double kSurrogateKMin = 1.0e-3; -static const double kSurrogateKMax = 10.0; -static const double kSurrogateSkinMin = -10.0; -static const double kSurrogateSkinMax = 10.0; -static const double kSurrogateWellboreCMin = 1.0e-4; -static const double kSurrogateWellboreCMax = 2.0; -static const double kSurrogatePhiMin = 1.0e-2; -static const double kSurrogatePhiMax = 0.50; -static const double kSurrogateHMin = 2.0; -static const double kSurrogateHMax = 50.0; -static const double kSurrogateCfFixed = 4.315e-4; -static const double kSurrogateCfRelTol = 0.25; -static const int kSurrogateMinProdSections = 3; -static const int kSurrogateMaxProdSections = 5; -static const double kSurrogateProdTimeMin = 24.0; -static const double kSurrogateProdTimeMax = 220.0; -static const double kSurrogateShutinTimeMin = 12.0; -static const double kSurrogateShutinTimeMax = 140.0; -static const double kSurrogateQMin = 50.0; -static const double kSurrogateQMax = 500.0; -static const double kSurrogateQZeroThreshold = 1.0e-6; -static const double kSurrogateMaxRateStepRatio = 2.2; -static const double kSurrogateStrongDeclineEndStartRatio = 0.70; -static const double kSurrogateRateTrendRelTol = 1.0e-9; - -// Check infinity and NaN. +// ===== 代理模型筛选参数 ===== +// +// PSO acceleration 的原则是“少跑一部分真实求解器,但不能完全信任代理模型”: +// - keep_fraction:代理评分排名靠前的粒子进入真实求解器; +// - audit_fraction:从被筛掉的粒子中随机抽查一部分,避免代理模型系统性漏掉好解; +// - min_solver_fraction:每代至少有一定比例粒子跑真实求解器,保证 pbest/gbest 仍由真实误差支撑; +// - warmup_iterations:前几代全量真实评价,让每个粒子先拿到可靠的个体最优; +// - full_solver_interval:周期性全量审计,用于纠偏代理模型排序误差。 +static const double kSurrogateKeepFraction = 0.50; // 常规工况:代理排序前 50% 粒子进入真实求解器。 +static const double kSurrogateHighRiskKeepFraction = 0.70; // 高风险流量制度:保留更多 top 粒子。 +static const double kSurrogateAuditFraction = 0.05; // 从被筛掉候选中随机审计 5%。 +static const double kSurrogateMinSolverFraction = 0.55; // 常规工况:每代至少 55% 粒子跑真实求解器。 +static const double kSurrogateHighRiskMinSolverFraction = 0.80; // 高风险流量制度:每代至少 80% 粒子跑真实求解器。 +static const int kSurrogateWarmupIterations = 2; // 前 2 代全量真实评价,给每个粒子建立 pbest。 +static const int kSurrogateFullSolverInterval = 10; // 每 10 代做一次全量真实求解审计。 +static const int kSurrogateScoringMaxAttempts = 2; // Python 评分失败时最多重试 2 次。 +static const bool kUseFixedPsoSeed = true; // 固定随机种子便于复现 trace 和交接演示。 +static const unsigned int kFixedPsoSeed = 1792008679u; // 当前固定种子值。 + +// ===== 代理模型训练域约束 ===== +// +// 这些范围必须与 ML/nmWTAI-ML/configs/data_gen_family_random_v2_q.yaml 中的 +// 数据生成范围保持一致。超出范围时,C++ 侧会强制退回真实求解器,避免代理模型 +// 在训练域外给出不可靠排序。 +static const double kSurrogateKMin = 1.0e-3; // 代理模型训练域:渗透率下界。 +static const double kSurrogateKMax = 10.0; // 代理模型训练域:渗透率上界。 +static const double kSurrogateSkinMin = -10.0; // 代理模型训练域:skin 下界。 +static const double kSurrogateSkinMax = 10.0; // 代理模型训练域:skin 上界。 +static const double kSurrogateWellboreCMin = 1.0e-4; // 代理模型训练域:井筒储集下界。 +static const double kSurrogateWellboreCMax = 2.0; // 代理模型训练域:井筒储集上界。 +static const double kSurrogatePhiMin = 1.0e-2; // 代理模型训练域:孔隙度下界。 +static const double kSurrogatePhiMax = 0.50; // 代理模型训练域:孔隙度上界。 +static const double kSurrogateHMin = 2.0; // 代理模型训练域:储层厚度下界。 +static const double kSurrogateHMax = 50.0; // 代理模型训练域:储层厚度上界。 +static const double kSurrogateCfFixed = 4.315e-4; // 当前代理模型按近似固定 Cf 训练。 +static const double kSurrogateCfRelTol = 0.25; // Cf 允许相对偏差,超过则不使用代理筛选。 +static const int kSurrogateMinProdSections = 3; // 代理训练流量制度:生产段数量下界。 +static const int kSurrogateMaxProdSections = 5; // 代理训练流量制度:生产段数量上界。 +static const double kSurrogateProdTimeMin = 24.0; // 代理训练流量制度:生产总时长下界。 +static const double kSurrogateProdTimeMax = 220.0; // 代理训练流量制度:生产总时长上界。 +static const double kSurrogateShutinTimeMin = 12.0; // 代理训练流量制度:关井段时长下界。 +static const double kSurrogateShutinTimeMax = 140.0; // 代理训练流量制度:关井段时长上界。 +static const double kSurrogateQMin = 50.0; // 代理训练流量制度:产量下界。 +static const double kSurrogateQMax = 500.0; // 代理训练流量制度:产量上界。 +static const double kSurrogateQZeroThreshold = 1.0e-6; // 判断最后一段是否关井的零产量阈值。 +static const double kSurrogateMaxRateStepRatio = 2.2; // 相邻生产段产量最大跳变比。 +static const double kSurrogateStrongDeclineEndStartRatio = 0.70; // 末段/首段产量小于该值视为强递减。 +static const double kSurrogateRateTrendRelTol = 1.0e-9; // 判断产量趋势时的相对容差。 + +// 判断 double 是否为有限数。PSO、误差函数、trace 写出都依赖它过滤 NaN/Inf。 static inline bool isFiniteNumber(double value) { #ifdef Q_OS_WIN @@ -87,6 +103,7 @@ static inline bool isInClosedRange(double value, double lower, double upper) return isFiniteNumber(value) && value >= lower && value <= upper; } +// 判断 value 是否接近 reference。用于 Cf 这类“近似固定”的训练域检查。 static inline bool isNearRelative(double value, double reference, double relTol) { if(!isFiniteNumber(value) || !isFiniteNumber(reference)) { @@ -96,7 +113,7 @@ static inline bool isNearRelative(double value, double reference, double relTol) return qAbs(value - reference) <= qMax(1e-12, qAbs(reference) * relTol); } -// sleep函数 +// Windows 下的毫秒级睡眠封装。用于重试间隔、UI 事件循环间隔和模拟模式节奏控制。 static inline void msleep(int ms) { #ifdef Q_OS_WIN @@ -106,6 +123,8 @@ static inline void msleep(int ms) static QString csvEscape(const QString& text) { + // CSV 字段转义。trace 中的 run_id、phase、screeningDecision 等字符串都走这里, + // 防止逗号或引号破坏表格结构。 QString escaped = text; escaped.replace("\"", "\"\""); return QString("\"%1\"").arg(escaped); @@ -113,6 +132,7 @@ static QString csvEscape(const QString& text) static QString traceNumber(double value) { + // trace CSV 中的数值格式化。不可用数值写空字段,便于后续 pandas/Excel 读取。 if(!isFiniteNumber(value)) { return QString(); } @@ -122,6 +142,7 @@ static QString traceNumber(double value) static QString traceParamAt(const QVector& params, int index) { + // 从完整参数向量中安全取值并格式化。越界时写空字段,避免 trace 写出崩溃。 if(index < 0 || index >= params.size()) { return QString(); } @@ -131,6 +152,8 @@ static QString traceParamAt(const QVector& params, int index) static QString jsonEscape(const QString& text) { + // 手写 JSON 字符串转义。trace meta 不依赖第三方 JSON writer, + // 所以所有字符串字段写入前必须统一经过这里。 QString escaped = text; escaped.replace("\\", "\\\\"); escaped.replace("\"", "\\\""); @@ -144,6 +167,7 @@ static QString jsonEscape(const QString& text) static QString jsonNumber(double value) { + // JSON 数值格式化。NaN/Inf 在 JSON 中没有合法表示,因此写 null。 if(!isFiniteNumber(value)) { return "null"; } @@ -153,6 +177,7 @@ static QString jsonNumber(double value) static QString jsonDoubleArray(const QVector& values) { + // 将 double 数组写成 JSON 数组,用于 trace meta 的目标曲线、参数上下界等。 QStringList items; for(int i = 0; i < values.size(); ++i) { @@ -164,6 +189,7 @@ static QString jsonDoubleArray(const QVector& values) static QString jsonIntArray(const QVector& values) { + // 将 int 数组写成 JSON 数组,用于 enabled_param_indices 等字段。 QStringList items; for(int i = 0; i < values.size(); ++i) { @@ -175,6 +201,7 @@ static QString jsonIntArray(const QVector& values) static QString jsonBoolArray(const QVector& values) { + // 将 bool 数组写成 JSON 数组,用于参数是否启用等字段。 QStringList items; for(int i = 0; i < values.size(); ++i) { @@ -186,6 +213,8 @@ static QString jsonBoolArray(const QVector& values) static double deterministicAuditRandom01(unsigned int seed, int generation, int particleIndex) { + // 代理筛选的随机审计需要“随机但可复现”: + // 同一个 PSO 种子、代号、粒子号会得到同一个 0-1 值,方便复盘 trace。 unsigned int x = seed; x ^= 0x9e3779b9u + static_cast(generation + 1) + (x << 6) + (x >> 2); x ^= 0x85ebca6bu + static_cast(particleIndex + 1) + (x << 6) + (x >> 2); @@ -199,6 +228,7 @@ static double deterministicAuditRandom01(unsigned int seed, int generation, int static QString jsonStringArray(const QStringList& values) { + // 将字符串列表写成 JSON 数组,用于参数名、启用参数名等字段。 QStringList items; for(int i = 0; i < values.size(); ++i) { @@ -210,6 +240,7 @@ static QString jsonStringArray(const QStringList& values) static QString jsonPointArray(const QVector& points) { + // 将 QPointF 数组写成 [{"x":...,"y":...}],用于 trace meta 保存流量制度。 QStringList items; for(int i = 0; i < points.size(); ++i) { @@ -223,6 +254,8 @@ static QString jsonPointArray(const QVector& points) static bool isValidMlRootDirectory(const QString& mlRootPath) { + // 判断一个目录是不是可用的 ML/nmWTAI-ML 根目录。 + // 目前以评分脚本是否存在作为最小可用条件。 if(mlRootPath.isEmpty()) { return false; } @@ -233,6 +266,7 @@ static bool isValidMlRootDirectory(const QString& mlRootPath) static QString findExecutableInPath(const QString& executableName) { + // 在系统 PATH 中查找可执行文件。找不到 .venv 里的 python.exe 时会用它兜底。 if(executableName.isEmpty()) { return QString(); } @@ -287,6 +321,8 @@ static QString findExecutableInPath(const QString& executableName) static QStringList traceParameterNames() { + // trace 和 trace meta 使用的完整参数名顺序。 + // 这个顺序必须与 buildTraceParameterVector() 和 m_parameterSelected 的 0-10 索引一致。 QStringList names; names << "k" << "skin" @@ -302,7 +338,9 @@ static QStringList traceParameterNames() return names; } -// Constructor. +// 构造函数:初始化 PSO 参数、收敛判断阈值、trace/代理统计和模拟模式状态。 +// 这里设置的是默认值,真正运行前会由 loadOptimizationConfig() 和 +// loadParameterBounds() 从 DataManager 读取界面配置覆盖一部分字段。 nmCalculationAutoFitPSO::nmCalculationAutoFitPSO(QObject* parent) : QObject(parent) , m_isRunning(false) @@ -377,7 +415,9 @@ nmCalculationAutoFitPSO::nmCalculationAutoFitPSO(QObject* parent) DEBUG_OUT("PSO Constructor completed"); } -// Destructor. +// 析构函数:停止仍在进行的拟合、关闭代理评分服务、关闭 trace 文件并清理临时目录。 +// 自动拟合可能在 UI 线程中被窗口关闭打断,因此析构时要尽量温和地等待当前评价结束; +// 如果等待超时,再强制清除运行标志,避免对象销毁后还有信号回调或后台进程访问成员变量。 nmCalculationAutoFitPSO::~nmCalculationAutoFitPSO() { DEBUG_OUT(QString("PSO Destructor: this=0x%1").arg((quintptr)this, 0, 16)); @@ -417,10 +457,16 @@ nmCalculationAutoFitPSO::~nmCalculationAutoFitPSO() DEBUG_OUT("PSO Destructor completed"); } -// Directory helpers. +// ===== 临时目录工具 ===== +// +// 真实求解器 DLL 和自动拟合过程会产生中间文件,因此每次创建独立的 +// autofit_temp__ 目录。退出时只删除本类创建的目录,启动时顺便清理 +// 旧进程遗留的 autofit_temp_*,避免长期调试后应用目录被临时文件堆满。 void nmCalculationAutoFitPSO::initializeTemporaryDirectory() { + // 先清理历史遗留目录,再为本次对象创建唯一目录。 + // 目录名包含进程 ID 和毫秒时间戳,通常足够唯一;counter 是极端重名时的兜底。 cleanupOldTemporaryDirectories(); QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss_zzz"); @@ -447,6 +493,8 @@ void nmCalculationAutoFitPSO::initializeTemporaryDirectory() void nmCalculationAutoFitPSO::cleanupTemporaryDirectory() { + // 析构或用户停止时调用。删除失败一般是文件仍被 DLL/系统占用, + // 这里只记录 debug 信息,不让清理失败影响 UI 退出。 if(QDir(m_tempDirectory).exists()) { if(removeDirectoryRecursively(m_tempDirectory)) { DEBUG_OUT("Temp directory cleaned up successfully"); @@ -458,13 +506,15 @@ void nmCalculationAutoFitPSO::cleanupTemporaryDirectory() bool nmCalculationAutoFitPSO::removeDirectoryRecursively(const QString& path) { + // Qt 旧版本没有统一可用的 removeRecursively 行为时,用本函数递归删除。 + // 调用方传入的是本类创建的临时目录或旧 autofit_temp_* 目录。 QDir dir(path); if(!dir.exists()) { return true; } - // 递归删除子目录和文件 + // 递归删除子目录和文件。包含 Hidden,避免隐藏中间文件阻塞目录删除。 QFileInfoList entries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllEntries | QDir::Hidden); bool allRemoved = true; @@ -478,7 +528,7 @@ bool nmCalculationAutoFitPSO::removeDirectoryRecursively(const QString& path) } else { QFile file(entry.absoluteFilePath()); - // 处理只读文件 + // 处理只读文件。某些求解器输出可能带只读属性,删除前先补写权限。 if(!file.permissions().testFlag(QFile::WriteUser)) { file.setPermissions(file.permissions() | QFile::WriteUser); } @@ -500,6 +550,8 @@ bool nmCalculationAutoFitPSO::removeDirectoryRecursively(const QString& path) void nmCalculationAutoFitPSO::cleanupOldTemporaryDirectories() { + // 应用启动或新建自动拟合对象时清理旧目录。 + // 不删除当前进程 ID 对应目录,防止同进程内多个拟合对象或正在运行的求解器被误删。 QString appDir = QApplication::applicationDirPath(); QDir dir(appDir); @@ -556,6 +608,8 @@ void nmCalculationAutoFitPSO::cleanupOldTemporaryDirectories() void nmCalculationAutoFitPSO::setTargetLogLogData(const QVector >& targetData) { + // 目标曲线由界面层从目标井 history log-log 传入。 + // 约定 targetData[0]=time,targetData[1]=pressure,targetData[2]=pressure derivative。 m_targetLogLogData = targetData; DEBUG_OUT(QString("Target LogLog data set: %1 arrays").arg(targetData.size())); @@ -570,6 +624,9 @@ void nmCalculationAutoFitPSO::setTargetLogLogData(const QVector void nmCalculationAutoFitPSO::stopFitting() { + // 用户点击停止时走这里。停止策略是“请求式停止”: + // 先置 m_shouldStop,让主循环/求解器等待逻辑自然退出;短时间内还在评价时再重置计数。 + // 这样可以减少 DLL 任务被硬中断导致的数据状态残留。 if(m_simulationMode && m_simulationTimer) { m_simulationTimer->stop(); } @@ -615,31 +672,39 @@ void nmCalculationAutoFitPSO::stopFitting() bool nmCalculationAutoFitPSO::isRunning() const { + // UI 查询当前是否处于自动拟合运行状态。 return m_isRunning; } int nmCalculationAutoFitPSO::getCurrentIteration() const { + // UI 进度条和日志展示用的当前迭代序号。 return m_currentIteration; } QVector nmCalculationAutoFitPSO::getBestSolution() const { + // 返回紧凑的“启用参数向量”,顺序与 m_enabledParamIndices 一致。 return m_globalBestPosition; } double nmCalculationAutoFitPSO::getBestFitness() const { + // 当前全局最优真实误差。越小越好,1e10 附近通常表示尚无有效解。 return m_globalBestFitness; } QString nmCalculationAutoFitPSO::getLastError() const { + // 上一次失败的人类可读错误信息,主要给 UI 层弹窗或日志使用。 return m_lastError; } void nmCalculationAutoFitPSO::resetOptimizer() { + // 清空一次运行产生的状态,但不销毁对象。 + // 配置字段会在 startAutoFitting() 中重新从 DataManager 读取; + // trace 文件先关闭,避免新一轮 run 继续写到旧 CSV。 closeTraceFile(); m_swarm.clear(); m_globalBestPosition.clear(); @@ -668,11 +733,18 @@ void nmCalculationAutoFitPSO::resetOptimizer() void nmCalculationAutoFitPSO::setPSOTargetWellName(const QString& wellName) { + // 目标井名是贯穿拟合流程的关键索引: + // 读目标曲线、写 skin/wellboreC、求解后取 resultLogLog 都依赖这个名字。 m_targetWellName = wellName; } void nmCalculationAutoFitPSO::initializeTraceFile() { + // 创建本次 PSO 的可复盘文件: + // - pso_baseline_trace_.csv:逐代逐粒子的参数、真实误差、代理误差和筛选决策; + // - pso_baseline_trace_.meta.json:目标曲线、流量制度、参数上下界、PSO/代理配置。 + // + // 代理模型评分脚本也会读取 meta.json,因此 trace meta 不是单纯日志,而是C++ 与 Python 代理模型之间的运行上下文契约。 if(!m_traceEnabled) { return; } @@ -730,6 +802,7 @@ void nmCalculationAutoFitPSO::initializeTraceFile() void nmCalculationAutoFitPSO::resetRunSummary() { + // 重置本次运行的代理筛选统计。这里只记录统计口径,不参与 PSO 更新。 m_surrogateRunContextSummary.clear(); m_surrogateWarmupIterationCount = 0; m_surrogatePeriodicAuditIterationCount = 0; @@ -746,6 +819,8 @@ void nmCalculationAutoFitPSO::resetRunSummary() void nmCalculationAutoFitPSO::captureSurrogateRunContextSummary() { + // 在 trace 初始化阶段记录一次“本项目是否支持代理模型”的结论。 + // 后续每代仍会再次检查,防止运行中 DataManager 状态发生变化。 if(!isSurrogateScreeningEnabled()) { m_surrogateRunContextSummary = "disabled_by_config"; return; @@ -762,6 +837,8 @@ void nmCalculationAutoFitPSO::captureSurrogateRunContextSummary() void nmCalculationAutoFitPSO::emitRunSummary(bool success, StopReasonPSO finalReason) { + // 运行结束时输出一组交接/排障最有用的摘要: + // 真实求解次数、成功失败数、代理模型启用情况、各类筛选决策计数,以及 trace 文件路径。 emit logMessageGenerated(tr("=== PSO 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)") @@ -799,6 +876,8 @@ void nmCalculationAutoFitPSO::emitRunSummary(bool success, StopReasonPSO finalRe void nmCalculationAutoFitPSO::closeTraceFile() { + // trace 与代理评分 server 生命周期绑定在一次 PSO run 内。 + // 关闭 trace 时同时停止 server,避免 Python 进程继续占用模型或 stdout 管道。 stopSurrogateScoringServer(); if(m_traceFile.isOpen()) { @@ -809,6 +888,12 @@ void nmCalculationAutoFitPSO::closeTraceFile() void nmCalculationAutoFitPSO::writeTraceHeader() { + // trace CSV 字段说明: + // - 当前粒子参数只记录代理模型关心的 k/skin/wellboreC/phi/h/Cf; + // - solver_objective 是真实求解器误差; + // - surrogate_objective 是 Python 代理评分; + // - screening_decision 说明该粒子为什么跑/不跑真实求解器; + // - pbest/gbest 字段用于离线复盘 PSO 更新是否只依赖真实误差。 if(!m_traceFile.isOpen()) { return; } @@ -851,6 +936,9 @@ void nmCalculationAutoFitPSO::writeTraceHeader() void nmCalculationAutoFitPSO::writeTraceMetaFile() { + // 写 trace 的配套 JSON。它把“如何解释 CSV”的上下文固化下来: + // 目标井、目标 log-log 曲线、流量制度、参数名和上下界、初始参数、PSO 配置、 + // 代理模型 tag/stage/筛选比例等。Python 评分脚本会读取这个文件来构造模型输入。 if(m_traceMetaFilePath.isEmpty()) { return; } @@ -940,6 +1028,9 @@ void nmCalculationAutoFitPSO::writeTraceMetaFile() QVector nmCalculationAutoFitPSO::buildTraceParameterVector(const QVector& selectedParameters) const { + // 将粒子内部使用的“启用参数向量”还原成完整 11 维参数向量。 + // 未启用的参数从当前 DataManager 读取,启用的参数用 selectedParameters 覆盖。 + // trace CSV、候选 CSV、代理训练域检查都需要这个完整向量。 QVector fullParams(11, 0.0); nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); @@ -987,6 +1078,9 @@ void nmCalculationAutoFitPSO::writeTraceRow(int generation, const QVector& pbestPosition, double pbestObjective) { + // 写一行 trace。generation=-1/particleIndex=-1 表示用户初始解; + // 普通粒子行的 phase 为 particle_solver 或 particle_not_evaluated。 + // 被代理筛掉的粒子 solver_objective 写空值,但仍保留 surrogate_objective 和 decision。 if(!m_traceFile.isOpen()) { return; } @@ -1040,6 +1134,8 @@ void nmCalculationAutoFitPSO::writeTraceRow(int generation, void nmCalculationAutoFitPSO::writeIterationTraceRows() { + // 每代真实评价和全局最优更新后写出所有粒子。 + // 这样 trace 中能同时看到“这一代谁被筛掉”和“当前 pbest/gbest 是什么”。 if(!m_traceFile.isOpen()) { return; } @@ -1065,11 +1161,14 @@ void nmCalculationAutoFitPSO::writeIterationTraceRows() bool nmCalculationAutoFitPSO::isSurrogateScreeningEnabled() const { + // 对应界面上的 PSO acceleration / surrogate screening 开关。 return m_surrogateScreeningEnabled; } QString nmCalculationAutoFitPSO::getMlRootPath() const { + // 查找 ML/nmWTAI-ML 根目录。程序可能从 build/bin/release 等不同目录启动, + // 因此按多个相对路径尝试。isValidMlRootDirectory() 用评分脚本是否存在来确认。 QString appDir = QApplication::applicationDirPath(); QString cwd = QDir::currentPath(); @@ -1093,6 +1192,8 @@ QString nmCalculationAutoFitPSO::getMlRootPath() const QString nmCalculationAutoFitPSO::getPythonExecutablePath() const { + // 优先使用 ML 项目内的虚拟环境 python,其次使用 PATH 中的 python。 + // 这样开发机上只要 ML/nmWTAI-ML/.venv 已配置,就不依赖全局 Python 环境。 QString mlRoot = getMlRootPath(); QStringList candidateExecutables; @@ -1120,16 +1221,20 @@ QString nmCalculationAutoFitPSO::getPythonExecutablePath() const QString nmCalculationAutoFitPSO::getSurrogateTag() const { + // 当前 C++ 绑定的代理模型实验标识,对应 ML 侧训练输出目录/checkpoint 命名。 + // 如果以后换模型,需要同步更新这里和 ML 脚本可识别的 tag。 return "family_random_v2_q_50k_logparam"; } QString nmCalculationAutoFitPSO::getSurrogateStage() const { + // 当前代理模型的数据生成阶段。v2_q 表示训练时包含流量制度 q 的变化。 return "family_random_v2_q"; } double nmCalculationAutoFitPSO::getSurrogateKeepFraction() const { + // 强递减生产制度下代理排序风险更高,因此保留更多 top 粒子进入真实求解器。 if(getSurrogateStage().contains("v2_q") && isStrongDecliningProductionSchedule()) { return kSurrogateHighRiskKeepFraction; } @@ -1139,11 +1244,13 @@ double nmCalculationAutoFitPSO::getSurrogateKeepFraction() const double nmCalculationAutoFitPSO::getSurrogateAuditFraction() const { + // 随机审计比例,审计的是代理模型筛掉的候选。 return kSurrogateAuditFraction; } double nmCalculationAutoFitPSO::getSurrogateMinSolverFraction() const { + // 每代真实求解器最低覆盖比例。强递减生产制度下提高到高风险配置。 if(getSurrogateStage().contains("v2_q") && isStrongDecliningProductionSchedule()) { return kSurrogateHighRiskMinSolverFraction; } @@ -1153,16 +1260,26 @@ double nmCalculationAutoFitPSO::getSurrogateMinSolverFraction() const int nmCalculationAutoFitPSO::getSurrogateWarmupIterations() const { + // 前若干代不筛选,确保粒子先建立真实 pbest。 return kSurrogateWarmupIterations; } int nmCalculationAutoFitPSO::getSurrogateFullSolverInterval() const { + // 周期性全量真实评价间隔,用于审计代理模型排序。 return kSurrogateFullSolverInterval; } bool nmCalculationAutoFitPSO::writeSurrogateCandidateCsv(const QString& candidatePath) const { + // 写给 Python 代理模型的候选粒子 CSV。 + // + // 文件位置通常是 ML/nmWTAI-ML/data/temp/pso_screen_candidates__gen.csv。 + // 字段顺序必须与 ML 脚本 score_pso_candidates*.py 保持一致: + // particle_id,k,skin,wellboreC,phi,h,Cf + // + // 这里写的是“完整参数体系”中的关键字段,不是粒子内部紧凑向量。 + // 例如用户没有勾选 Cf 时,Cf 会从当前 DataManager 取值写入 CSV。 QFile file(candidatePath); if(!file.open(QIODevice::WriteOnly | QIODevice::Text)) { @@ -1171,6 +1288,7 @@ bool nmCalculationAutoFitPSO::writeSurrogateCandidateCsv(const QString& candidat } QTextStream out(&file); + // 代理模型只需要这些输入字段;其它参数当前不在训练输入集中。 out << "particle_id,k,skin,wellboreC,phi,h,Cf\n"; for(int i = 0; i < m_swarm.size(); ++i) { @@ -1193,6 +1311,8 @@ bool nmCalculationAutoFitPSO::writeSurrogateCandidateCsv(const QString& candidat bool nmCalculationAutoFitPSO::runSurrogateScoringProcess(const QString& candidatePath, const QString& scorePath, QString* failureReason) { + // 代理评分统一入口。优先走常驻 Python server,失败后降级为一次性脚本。 + // 常驻 server 可以避免每一代重复加载模型,速度更快;一次性脚本更稳,适合作为兜底。 QString serverFailure; if(requestSurrogateScoresFromServer(candidatePath, scorePath, &serverFailure)) { @@ -1211,6 +1331,10 @@ bool nmCalculationAutoFitPSO::runSurrogateScoringProcess(const QString& candidat bool nmCalculationAutoFitPSO::ensureSurrogateScoringServer(QString* failureReason) { + // 确保常驻代理评分 server 已启动且与当前 run 配置匹配。 + // + // serverKey 由 python 路径、脚本路径、trace meta、tag、stage 组成。 + // 只要其中任何一个变化,就停止旧进程并启动新进程,避免拿旧模型或旧目标曲线评分。 if(m_traceMetaFilePath.isEmpty() || !QFileInfo(m_traceMetaFilePath).exists()) { QString reason = "trace meta file is missing"; DEBUG_OUT(QString("Surrogate scoring server skipped: %1").arg(reason)); @@ -1253,6 +1377,7 @@ bool nmCalculationAutoFitPSO::ensureSurrogateScoringServer(QString* failureReaso stopSurrogateScoringServer(); QStringList args; + // -u 让 Python stdout/stderr 不缓冲,C++ 可以及时读到 ready/ok 行。 args << "-u" << scriptPath << "--trace-meta" << m_traceMetaFilePath @@ -1288,6 +1413,8 @@ bool nmCalculationAutoFitPSO::ensureSurrogateScoringServer(QString* failureReaso QByteArray readyLine; while(timer.elapsed() < 120000) { + // server 启动协议:Python 首先输出一行 JSON,包含 ready=true 或 ok=false。 + // C++ 不做完整 JSON 解析,只检查关键字段,降低 Qt 版本依赖。 if(m_surrogateScoringProcess->state() != QProcess::Running) { QString stdOut = QString::fromLocal8Bit(m_surrogateScoringProcess->readAllStandardOutput()).trimmed(); QString stdErr = QString::fromLocal8Bit(m_surrogateScoringProcess->readAllStandardError()).trimmed(); @@ -1351,6 +1478,9 @@ bool nmCalculationAutoFitPSO::ensureSurrogateScoringServer(QString* failureReaso bool nmCalculationAutoFitPSO::requestSurrogateScoresFromServer(const QString& candidatePath, const QString& scorePath, QString* failureReason) { + // 向常驻 Python server 发送一次评分请求。 + // 请求通过 stdin 传 JSON 行,server 读取候选 CSV 后把评分写入 scorePath, + // 再通过 stdout 返回 ok=true/false。 if(!ensureSurrogateScoringServer(failureReason)) { return false; } @@ -1382,6 +1512,7 @@ bool nmCalculationAutoFitPSO::requestSurrogateScoresFromServer(const QString& ca timer.start(); while(timer.elapsed() < 120000) { + // 每次请求最多等待 120 秒。超时或 server 退出都会让本代代理筛选降级为全量真实求解器。 if(m_surrogateScoringProcess->state() != QProcess::Running) { QString stdOut = QString::fromLocal8Bit(m_surrogateScoringProcess->readAllStandardOutput()).trimmed(); QString stdErr = QString::fromLocal8Bit(m_surrogateScoringProcess->readAllStandardError()).trimmed(); @@ -1453,6 +1584,8 @@ bool nmCalculationAutoFitPSO::requestSurrogateScoresFromServer(const QString& ca void nmCalculationAutoFitPSO::stopSurrogateScoringServer() { + // 停止常驻 Python 评分进程。 + // 先发送 quit 指令,短时间内不退出再 kill,避免应用关闭时遗留 python.exe。 if(!m_surrogateScoringProcess) { return; } @@ -1475,6 +1608,15 @@ void nmCalculationAutoFitPSO::stopSurrogateScoringServer() bool nmCalculationAutoFitPSO::runSurrogateScoringScriptOnce(const QString& candidatePath, const QString& scorePath, QString* failureReason) { + // 一次性代理评分脚本兜底路径。 + // + // 输入: + // - candidates:writeSurrogateCandidateCsv() 写出的粒子参数; + // - trace-meta:目标曲线、流量制度、参数上下界等上下文; + // - tag/stage:选择 ML 侧模型。 + // + // 输出: + // - scorePath:Python 写出的评分 CSV,随后由 readSurrogateScores() 读取。 if(m_traceMetaFilePath.isEmpty() || !QFileInfo(m_traceMetaFilePath).exists()) { QString reason = "trace meta file is missing"; DEBUG_OUT(QString("Surrogate scoring skipped: %1").arg(reason)); @@ -1514,6 +1656,7 @@ bool nmCalculationAutoFitPSO::runSurrogateScoringScriptOnce(const QString& candi QString lastFailure; for(int attempt = 1; attempt <= kSurrogateScoringMaxAttempts; ++attempt) { + // 评分脚本可能因首次加载模型、文件占用或环境抖动失败,因此允许有限重试。 emit logMessageGenerated(tr("Surrogate scoring attempt %1/%2: python=%3, mlRoot=%4") .arg(attempt) .arg(kSurrogateScoringMaxAttempts) @@ -1645,6 +1788,12 @@ bool nmCalculationAutoFitPSO::runSurrogateScoringScriptOnce(const QString& candi QVector nmCalculationAutoFitPSO::readSurrogateScores(const QString& scorePath) const { + // 读取 Python 输出的评分 CSV。 + // + // 当前脚本约定至少包含: + // particle_id,surrogate_objective,...,success + // 本函数只读取 particle_id、score 和 success 标志。失败或缺失的粒子保持 NaN, + // 后续 buildSurrogateEvaluationMask() 会通过 fallback_gate 强制跑真实求解器。 QVector scores(m_swarm.size(), std::numeric_limits::quiet_NaN()); QFile file(scorePath); @@ -1690,6 +1839,9 @@ QVector nmCalculationAutoFitPSO::readSurrogateScores(const QString& scor bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) const { + // 判断“当前项目工况”是否适合使用代理模型。 + // 这是运行级别的 gate:只要项目整体不满足训练域条件,本次 PSO 全程退回真实求解器。 + // 这样做比盲目调用代理模型更稳,因为代理模型只在特定 PEBI/单井/垂直井/无复杂几何场景训练过。 nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); if(!dataManager) { @@ -1698,6 +1850,7 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co return false; } + // 网格类型 gate:当前代理模型只按 PEBI 网格样本训练。 if(dataManager->getGridType() != NM_Grid_PEBI) { if(reason) *reason = "surrogate model is trained only for PEBI grid cases"; @@ -1712,6 +1865,7 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co return false; } + // 井型 gate:当前只支持垂直井。水平井/压裂井等几何特征不在训练集中。 if(pTargetWell->getWellType() != NM_WELL_MODEL::Vertical_Well) { if(reason) *reason = "surrogate model is currently limited to vertical-well cases"; @@ -1727,12 +1881,14 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co } } + // 单井 gate:训练数据假设只有一口有效计算井,多井干扰会改变压力响应形态。 if(validCalculationWells != 1) { if(reason) *reason = QString("surrogate model expects one active calculation well, got %1").arg(validCalculationWells); return false; } + // 几何 gate:断层、裂缝、区域变化会引入训练集中没有的边界/非均质影响。 if(!dataManager->getFaultDataList().isEmpty() || !dataManager->getFractureDataList().isEmpty() || !dataManager->getRegionDataList().isEmpty()) { @@ -1741,6 +1897,8 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co return false; } + // 参数 gate:代理输入目前只覆盖 k/skin/wellboreC/phi/h。 + // 用户勾选其它参数时,代理无法可靠反映这些参数变化,直接禁用代理筛选。 QVector allowedParamIndices; allowedParamIndices << 0 << 1 << 2 << 3 << 5; @@ -1760,6 +1918,7 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co nmDataReservoir reservoirData = dataManager->getReservoirDataCopy(); double cf = reservoirData.getCf().getValue().toDouble(); + // Cf gate:当前模型按近似固定 Cf 训练,允许小范围相对偏差。 if(!isNearRelative(cf, kSurrogateCfFixed, kSurrogateCfRelTol)) { if(reason) { *reason = QString("Cf=%1 is outside surrogate fixed-Cf domain").arg(cf, 0, 'g', 10); @@ -1770,6 +1929,7 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co QVector flowPoints = pTargetWell->getFlowPoints(); + // flowPoints 有时第一段是 (0,0) 占位点,不代表真实生产段,先移除。 if(!flowPoints.isEmpty() && qAbs(flowPoints[0].x()) <= 1e-12 && qAbs(flowPoints[0].y()) <= 1e-12) { flowPoints.remove(0); } @@ -1780,6 +1940,7 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co return false; } + // 约定最后一段是关井段,因此生产段数量 = 总段数 - 1。 int productionSections = flowPoints.size() - 1; if(productionSections < kSurrogateMinProdSections || productionSections > kSurrogateMaxProdSections) { @@ -1808,6 +1969,7 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co } if(i > 0) { + // 相邻生产段产量跳变不能太大,否则代理模型可能没见过这种制度。 double previousQ = flowPoints[i - 1].y(); double stepRatio = qMax(q, previousQ) / qMax(1e-12, qMin(q, previousQ)); @@ -1823,6 +1985,7 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co productionTime += dt; } + // 旧阶段模型对强递减制度不稳;v2_q 阶段允许但会提高真实求解比例。 if(!getSurrogateStage().contains("v2_q")) { double endStartRatio = std::numeric_limits::quiet_NaN(); @@ -1854,6 +2017,7 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co return false; } + // 当前目标段必须是最后的关井段,对应试井解释的双对数分析段。 if(pTargetWell->getIndexF() != flowPoints.size()) { if(reason) { *reason = QString("target section index %1 is not the last section %2") @@ -1869,6 +2033,9 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co bool nmCalculationAutoFitPSO::isSurrogateCandidateInDomain(const QVector& selectedParameters, QString* reason) const { + // 判断“单个粒子候选参数”是否在代理模型训练域内。 + // 与 isSurrogateRunContextSupported() 不同,这里检查的是粒子参数值本身。 + // 超出范围的粒子会被 domain_gate 强制跑真实求解器。 QVector params = buildTraceParameterVector(selectedParameters); double k = params.size() > 0 ? params[0] : std::numeric_limits::quiet_NaN(); double skin = params.size() > 1 ? params[1] : std::numeric_limits::quiet_NaN(); @@ -1918,6 +2085,9 @@ bool nmCalculationAutoFitPSO::isSurrogateCandidateInDomain(const QVector bool nmCalculationAutoFitPSO::forceSolverByFallbackGate(const QVector& selectedParameters) const { + // 经验 fallback gate。 + // 有些点虽然还在训练域边界内,但组合起来属于代理模型容易排序失真的区域, + // 例如极低渗透率配大井筒储集、极端 skin 等。遇到这些候选时直接跑真实求解器。 QVector params = buildTraceParameterVector(selectedParameters); double k = params.size() > 0 ? params[0] : std::numeric_limits::quiet_NaN(); double skin = params.size() > 1 ? params[1] : std::numeric_limits::quiet_NaN(); @@ -1950,6 +2120,8 @@ bool nmCalculationAutoFitPSO::forceSolverByFallbackGate(const QVector& s bool nmCalculationAutoFitPSO::isStrongDecliningProductionSchedule(double* endStartRatio) const { + // 判断生产段产量是否整体非增且首末下降明显。 + // 这类流量制度代理模型风险更高,所以 keep/min_solver 会自动提高。 if(endStartRatio) { *endStartRatio = std::numeric_limits::quiet_NaN(); } @@ -2017,6 +2189,16 @@ bool nmCalculationAutoFitPSO::isStrongDecliningProductionSchedule(double* endSta QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() { + // 返回值 evaluateMask 与 m_swarm 一一对应: + // true 表示该粒子本代需要继续跑真实数值求解器; + // false 表示该粒子本代被代理模型筛掉,只记录 screened_out,不更新 pbest/gbest。 + // + // 这个函数是 PSO acceleration 的唯一入口。为了保证结果可信,它包含多层保护: + // 1. 工况不支持代理模型时,全量真实求解器; + // 2. 前几代 warmup,全量真实求解器; + // 3. 周期性审计,全量真实求解器; + // 4. 代理评分失败时,全量真实求解器; + // 5. top-k + random audit + fallback + min solver floor 共同决定最终 mask。 QVector evaluateMask(m_swarm.size(), true); for(int i = 0; i < m_swarm.size(); ++i) { @@ -2027,12 +2209,15 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() } if(!isSurrogateScreeningEnabled()) { + // 用户关闭 PSO acceleration:保持原始 PSO 行为,所有粒子都跑真实求解器。 return evaluateMask; } QString contextReason; if(!isSurrogateRunContextSupported(&contextReason)) { + // 当前项目工况不在代理模型训练域内。这里不报错,只降级为全量真实求解器, + // 这样用户仍能完成自动拟合,只是没有代理加速收益。 m_surrogateContextBlockedIterationCount++; for(int i = 0; i < m_swarm.size(); ++i) { @@ -2043,7 +2228,8 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() return evaluateMask; } - // Keep the early PSO generations fully evaluated so every particle gets a true pbest seed. + // 前几代全量真实评价,让每个粒子都先获得可信的个体最优 pbest。 + // 否则代理模型一开始就筛掉粒子,可能导致某些粒子从未有真实误差基准。 if(m_currentIteration < getSurrogateWarmupIterations()) { m_surrogateWarmupIterationCount++; @@ -2057,6 +2243,7 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() if(getSurrogateFullSolverInterval() > 0 && m_currentIteration > 0 && ((m_currentIteration + 1) % getSurrogateFullSolverInterval()) == 0) { + // 周期性全量审计。它牺牲少量速度,换取对代理排序偏差的长期纠偏能力。 m_surrogatePeriodicAuditIterationCount++; for(int i = 0; i < m_swarm.size(); ++i) { @@ -2077,14 +2264,16 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() QString candidatePath = tempDir.absoluteFilePath(QString("pso_screen_candidates_%1_gen%2.csv") .arg(m_traceRunId) - .arg(m_currentIteration)); + .arg(m_currentIteration));// C++ 写给 Python 的候选粒子参数 QString scorePath = tempDir.absoluteFilePath(QString("pso_screen_scores_%1_gen%2.csv") .arg(m_traceRunId) - .arg(m_currentIteration)); + .arg(m_currentIteration));// Python 写回给 C++ 的代理评分结果 QString scoringFailureReason; if(!writeSurrogateCandidateCsv(candidatePath) || !runSurrogateScoringProcess(candidatePath, scorePath, &scoringFailureReason)) { + // Python 环境、checkpoint、processed 数据或脚本异常时都走这里。 + // 降级策略是全量真实求解器,保证代理模块故障不会中断 PSO 拟合。 emit logMessageGenerated(tr("Surrogate screening unavailable; falling back to full solver for iteration %1") .arg(m_currentIteration + 1)); @@ -2098,6 +2287,7 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() QVector scores = readSurrogateScores(scorePath); QVector finiteScoreIndices; + // 只接受成功写出的有限代理目标函数。失败候选后面会通过 fallback 强制真实评价。 for(int i = 0; i < scores.size(); ++i) { m_swarm[i].surrogateObjective = scores[i]; @@ -2115,6 +2305,7 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() return scores[a] < scores[b]; }); + // 代理目标函数越小越好。先将所有粒子设为 screened_out,再逐步放回需要真实评价的粒子。 evaluateMask.fill(false); for(int i = 0; i < m_swarm.size(); ++i) { @@ -2125,6 +2316,7 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() int keepN = qMax(1, qCeil(m_swarm.size() * getSurrogateKeepFraction())); keepN = qMin(keepN, finiteScoreIndices.size()); + // 代理评分 top-k:这些粒子最可能给出好解,优先跑真实求解器。 for(int rank = 0; rank < keepN; ++rank) { int idx = finiteScoreIndices[rank]; evaluateMask[idx] = true; @@ -2134,6 +2326,8 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() QVector > auditCandidates; + // 随机审计:从代理模型认为“不好”的候选中抽查一部分。 + // 使用 deterministicAuditRandom01 保证同一随机种子下 trace 可复现。 for(int i = 0; i < finiteScoreIndices.size(); ++i) { int idx = finiteScoreIndices[i]; @@ -2159,6 +2353,8 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() int fallbackCount = 0; int domainFallbackCount = 0; + // fallback gate:只要候选点超出代理训练域、代理评分无效、粒子还没有可靠 pbest, + // 或落入经验高风险区域,就强制真实求解。这样做是为了保护 PSO 收敛质量。 for(int i = 0; i < m_swarm.size(); ++i) { QString domainReason; bool domainFallback = !isSurrogateCandidateInDomain(m_swarm[i].position, &domainReason); @@ -2193,6 +2389,7 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() int minSolverN = qMax(1, qCeil(m_swarm.size() * getSurrogateMinSolverFraction())); if(selectedCount < minSolverN) { + // 最小真实求解比例:防止代理模型过度筛选,导致一代中真实误差信息太少。 QVector > extraCandidates; for(int i = 0; i < m_swarm.size(); ++i) { @@ -2260,6 +2457,8 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() bool nmCalculationAutoFitPSO::loadAllConfigFromDataManager() { + // 统一从 DataManager 加载本次运行所需配置。 + // UI 层只负责把用户选择保存到 nmDataAutomaticFitting,本类从这里开始完全数据驱动。 try { loadOptimizationConfig(); loadParameterBounds(); @@ -2273,6 +2472,9 @@ bool nmCalculationAutoFitPSO::loadAllConfigFromDataManager() void nmCalculationAutoFitPSO::loadOptimizationConfig() { + // 读取“优化控制”配置:迭代次数、目标误差、是否启用代理筛选。 + // PSO 的 swarm/inertia/cognitive/social 目前是类内固定默认值, + // 不是界面输入项;后续若要开放高级设置,可以从这里扩展。 nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); nmDataAutomaticFitting fittingData = dataManager->getAutomaticFittingDataCopy(); @@ -2281,7 +2483,9 @@ void nmCalculationAutoFitPSO::loadOptimizationConfig() m_targetError = fittingData.getErrorTolerance().getValue().toDouble(); m_surrogateScreeningEnabled = fittingData.getSurrogateScreeningEnabled(); - // 设置PSO默认参数 + // 设置 PSO 默认参数: + // swarmSize=20 控制每代粒子数;inertia/cognitive/social 控制搜索惯性、 + // 个体经验和群体经验的权重。adaptiveParameterUpdate() 会在运行中微调这些值。 m_swarmSize = 20; m_inertiaWeight = 0.4; m_cognitiveParam = 2.0; @@ -2293,6 +2497,12 @@ void nmCalculationAutoFitPSO::loadOptimizationConfig() void nmCalculationAutoFitPSO::loadParameterBounds() { + // 读取用户勾选的拟合参数及上下界。 + // + // 这里构建三个核心数组: + // - m_parameterSelected[11]:完整参数体系中每个参数是否参与拟合; + // - m_parameterLower/Upper[11]:完整参数体系的搜索上下界; + // - m_enabledParamIndices:把粒子内部紧凑向量映射回完整参数索引。 nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); nmDataAutomaticFitting fittingData = dataManager->getAutomaticFittingDataCopy(); @@ -2363,6 +2573,9 @@ void nmCalculationAutoFitPSO::loadParameterBounds() // ==================== PSO算法核心方法 ==================== bool nmCalculationAutoFitPSO::startAutoFitting() { + // 自动拟合的总入口。可以把这个函数当成 PSO 的“运行剧本”: + // 读取配置 -> 校验输入 -> 评价用户初始解 -> 初始化粒子群 -> + // 按代循环评价粒子 -> 更新全局最优 -> 判断停止 -> 保存结果。 if(m_isRunning) { m_lastError = "Auto fitting is already running"; return false; @@ -2373,13 +2586,15 @@ bool nmCalculationAutoFitPSO::startAutoFitting() emit logMessageGenerated(tr("Algorithm: Particle Swarm Optimization")); try { - // 从数据管理器加载所有配置 + // 从 DataManager 读取界面保存的自动拟合配置。 + // 本类不直接依赖 UI 控件,便于后续从脚本或其他入口复用。 if(!loadAllConfigFromDataManager()) { emit logMessageGenerated(tr("ERROR: Failed to load configuration from data manager")); return false; } if(m_simulationMode) { + // 调试/演示用快速路径,不调用真实求解器。正式工况通常不走这里。 DEBUG_OUT("=== SIMULATION MODE ACTIVATED ==="); runSimulatedFitting(); return true; @@ -2400,7 +2615,8 @@ bool nmCalculationAutoFitPSO::startAutoFitting() return false; } - // 检查数据一致性 + // 目标曲线必须至少包含三列:time、pressure、pressure derivative, + // 且三列长度一致。后续误差函数会按 time 做插值对齐。 if(m_targetLogLogData[0].size() != m_targetLogLogData[1].size() || m_targetLogLogData[0].size() != m_targetLogLogData[2].size()) { m_lastError = "Target LogLog data arrays have inconsistent sizes"; @@ -2410,7 +2626,8 @@ bool nmCalculationAutoFitPSO::startAutoFitting() emit logMessageGenerated(tr("Target data validation passed (%1 data points)").arg(m_targetLogLogData[0].size())); - // 使用保存的初始值进行精英保护 + // 使用保存的初始值进行精英保护。resetOptimizer() 会清空部分运行状态, + // 所以先把用户当前模型参数缓存下来,后面再恢复用于初始解评价和粒子初始化。 QVector savedInitialValues = m_initialValues; // 重置状态 @@ -2422,6 +2639,8 @@ bool nmCalculationAutoFitPSO::startAutoFitting() m_consecutiveFailures = 0; if(kUseFixedPsoSeed) { + // 固定随机种子便于复现 trace 和代理筛选效果。若做生产随机搜索, + // 可以关闭 kUseFixedPsoSeed,改用当前时间生成种子。 m_psoRandomSeed = kFixedPsoSeed; } else { QTime seedTime = QTime::currentTime(); @@ -2436,7 +2655,8 @@ bool nmCalculationAutoFitPSO::startAutoFitting() qsrand(m_psoRandomSeed); m_consecutiveFailedIterations = 0; // 重置连续失败迭代计数 - // 精英保护:评估用户初始解 + // 精英保护:先评价用户当前模型。 + // 如果后续 PSO 没找到更好解,最终结果不会比用户已有模型更差。 m_initialValues = savedInitialValues; initializeTraceFile(); @@ -2444,7 +2664,7 @@ bool nmCalculationAutoFitPSO::startAutoFitting() m_userInitialSolution = savedInitialValues; emit logMessageGenerated(tr("=== Evaluating Initial Solution (Elite Protection) ===")); - // 输出初始参数值 + // 输出初始参数值,方便在 trace / 日志中确认初始解对应的参数顺序。 QString paramStr = tr("Initial parameters: "); for(int i = 0; i < m_userInitialSolution.size(); ++i) { @@ -2503,7 +2723,7 @@ bool nmCalculationAutoFitPSO::startAutoFitting() m_initialValues = savedInitialValues; } - // 初始化粒子群 + // 初始化粒子群。粒子维度等于用户勾选的参数数量,而不是固定 11 维。 if(kUseFixedPsoSeed) { emit logMessageGenerated(tr("PSO random seed: %1 ").arg(m_psoRandomSeed)); } else { @@ -2513,7 +2733,7 @@ bool nmCalculationAutoFitPSO::startAutoFitting() initializeSwarm(); emit logMessageGenerated(tr("Swarm initialized: %1 particles, %2 dimensions").arg(m_swarmSize).arg(getEnabledParameterCount())); - // PSO主循环 + // PSO主循环:每一代先决定哪些粒子需要真实评价,再更新全局最优和粒子位置。 emit logMessageGenerated(tr("=== Starting PSO Main Loop ===")); for(m_currentIteration = 0; m_currentIteration < m_maxIterations && !m_shouldStop; ++m_currentIteration) { @@ -2552,6 +2772,8 @@ bool nmCalculationAutoFitPSO::startAutoFitting() for(int i = 0; i < m_swarmSize && !m_shouldStop; ++i) { try { if(i < evaluateMask.size() && !evaluateMask[i]) { + // 被代理模型筛掉的粒子本代不跑真实求解器。 + // 这里不会用 surrogateObjective 更新 pbest,避免代理误差污染真实最优。 AutoFitParticle& particle = m_swarm[i]; particle.evaluatedThisIteration = false; particle.lastEvaluationSuccess = false; @@ -2567,6 +2789,7 @@ bool nmCalculationAutoFitPSO::startAutoFitting() } double previousFitness = m_swarm[i].fitness; + // 对单个粒子执行真实评价:写参数 -> 跑求解器 -> 算曲线误差 -> 更新 pbest。 updateParticle(i); currentIterationTotal++; @@ -2653,9 +2876,11 @@ bool nmCalculationAutoFitPSO::startAutoFitting() .arg(currentSuccessRate * 100, 0, 'f', 1).arg(m_currentIteration + 1)); } - // 更新全局最优 + // 更新全局最优。updateGlobalBest() 只读取粒子的真实 bestFitness, + // 不使用代理模型的 surrogateObjective。 //double previousGlobalBest = m_globalBestFitness; updateGlobalBest(); + // 记录本代所有粒子的真实/代理误差和筛选决策,用于复盘和排障。 writeIterationTraceRows(); // 自适应参数更新 @@ -2682,7 +2907,7 @@ bool nmCalculationAutoFitPSO::startAutoFitting() // 更新收敛指标 updateConvergenceMetrics(); - // 智能收敛判断 + // 智能收敛判断:目标达成、稳定收敛、局部最优、连续失败等都会在这里决策。 StopReasonPSO stopReason = analyzeOptimizationStatus(); if(stopReason == PSO_TARGET_ACHIEVED) { @@ -2866,6 +3091,12 @@ bool nmCalculationAutoFitPSO::startAutoFitting() void nmCalculationAutoFitPSO::extractUserInitialValues() { + // 从当前项目模型读取用户已有初始参数。 + // 只提取用户勾选的参数,并按 m_enabledParamIndices 的顺序写入 m_initialValues。 + // 这些值用于: + // 1. 初始解真实评价; + // 2. 粒子群引导初始化; + // 3. 最终精英保护。 nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); nmDataReservoir reservoirData = dataManager->getReservoirDataCopy(); //QVector wells = dataManager->getWellDataList(); @@ -2874,7 +3105,7 @@ void nmCalculationAutoFitPSO::extractUserInitialValues() m_initialValues.clear(); - // 按照启用参数的顺序提取初始值 + // 按照启用参数的顺序提取初始值。井参数来自目标井,储层参数来自 reservoirData。 for(int i = 0; i < m_enabledParamIndices.size(); ++i) { int paramIndex = m_enabledParamIndices[i]; double initialValue = 0.0; @@ -2948,6 +3179,9 @@ void nmCalculationAutoFitPSO::extractUserInitialValues() void nmCalculationAutoFitPSO::initializeSwarm() { + // 初始化粒子群。 + // 若用户已有初始模型有效,则一部分粒子围绕初始值做局部搜索; + // 另一部分粒子在全局上下界内随机分布,保留跳出局部区域的能力。 int dimensions = getEnabledParameterCount(); if(dimensions == 0) return; @@ -2957,7 +3191,7 @@ void nmCalculationAutoFitPSO::initializeSwarm() bool hasValidInitials = !m_initialValues.isEmpty() && m_initialValues.size() >= dimensions; - // 引导搜索比例 + // 引导搜索比例。guided 粒子围绕用户初始值,非 guided 粒子做全局随机搜索。 //int guidedCount = hasValidInitials ? qMax(2, (m_swarmSize * 3) / 4) : qMax(1, m_swarmSize / 3); int guidedCount = hasValidInitials ? qMax(2, m_swarmSize / 2) : qMax(1, m_swarmSize / 3); @@ -2984,11 +3218,12 @@ void nmCalculationAutoFitPSO::initializeSwarm() double range = m_parameterUpper[paramIndex] - m_parameterLower[paramIndex]; if(i == 0 && hasValidInitials) { - // 第一个粒子:完全使用用户初始值 + // 第一个粒子:完全使用用户初始值,确保初始模型本身参与粒子群。 particle.position[j] = m_initialValues[j]; } else if(i < guidedCount && hasValidInitials) { - // 搜索策略 + // 引导粒子采用分层搜索半径: + // 近邻精细搜索 + 中等扰动 + 较大扰动,覆盖初始模型周围不同尺度。 double searchRadius; if(i <= guidedCount / 3) { @@ -3003,7 +3238,7 @@ void nmCalculationAutoFitPSO::initializeSwarm() particle.position[j] = m_initialValues[j] + offset; } else { - // 少数粒子进行全局搜索 + // 少数粒子进行全局搜索,防止所有粒子都被初始模型附近的局部最优限制。 particle.position[j] = m_parameterLower[paramIndex] + random01() * range; } } @@ -3011,7 +3246,8 @@ void nmCalculationAutoFitPSO::initializeSwarm() // 边界检查 clampToLimits(particle.position); - // 速度初始化 + // 速度初始化。引导粒子速度更小,避免刚开始就离开初始模型邻域; + // 全局粒子速度稍大,便于探索更宽的搜索空间。 for(int j = 0; j < dimensions; ++j) { int paramIndex = m_enabledParamIndices[j]; double range = m_parameterUpper[paramIndex] - m_parameterLower[paramIndex]; @@ -3037,7 +3273,9 @@ void nmCalculationAutoFitPSO::updateParticle(int particleIndex) AutoFitParticle& particle = m_swarm[particleIndex]; - // 评估适应度 + // 单粒子真实评价入口。 + // 这里调用 evaluateFitness(),因此会真实写 DataManager、调用求解器、计算误差。 + // 被代理模型筛掉的粒子不会进入这个函数。 particle.evaluatedThisIteration = false; particle.lastEvaluationSuccess = false; particle.lastEvaluationElapsedMs = -1; @@ -3060,7 +3298,8 @@ void nmCalculationAutoFitPSO::updateParticle(int particleIndex) m_successfulEvaluations++; } - // Update particle best. + // 更新个体最优 pbest。这里使用的是真实求解器误差 particle.fitness, + // 不是代理模型给出的 surrogateObjective。 if(particle.fitness < particle.bestFitness) { particle.bestFitness = particle.fitness; particle.bestPosition = particle.position; @@ -3076,7 +3315,8 @@ void nmCalculationAutoFitPSO::updateGlobalBest() m_previousBestFitness = m_globalBestFitness; bool globalBestUpdated = false; - // 遍历所有粒子,寻找比当前 global best 更好的个体最优 + // 遍历所有粒子,寻找比当前 global best 更好的个体最优。 + // 注意:particle.bestFitness 只有在真实求解器评价成功后才会更新。 for(int i = 0; i < m_swarm.size(); ++i) { const AutoFitParticle& particle = m_swarm[i]; @@ -3177,7 +3417,10 @@ void nmCalculationAutoFitPSO::updateVelocityAndPosition() AutoFitParticle& particle = m_swarm[i]; for(int j = 0; j < particle.position.size(); ++j) { - // PSO速度更新公式 + // PSO速度更新公式: + // inertia 保留上一轮搜索方向; + // cognitive 项让粒子靠近自己的历史最优; + // social 项让粒子靠近群体全局最优。 double r1 = random01(); double r2 = random01(); @@ -3185,7 +3428,7 @@ void nmCalculationAutoFitPSO::updateVelocityAndPosition() m_cognitiveParam * r1 * (particle.bestPosition[j] - particle.position[j]) + m_socialParam * r2 * (m_globalBestPosition[j] - particle.position[j]); - // 速度限制 + // 速度限制。不同参数量纲差异很大,所以按该参数搜索区间的一定比例限速。 int paramIndex = m_enabledParamIndices[j]; double range = m_parameterUpper[paramIndex] - m_parameterLower[paramIndex]; double maxVel = range * VELOCITY_LIMIT_FACTOR; @@ -3195,7 +3438,7 @@ void nmCalculationAutoFitPSO::updateVelocityAndPosition() particle.position[j] += particle.velocity[j]; } - // 边界约束 + // 边界约束。PSO 位置更新后如果越界,直接夹回用户设置的上下界。 clampToLimits(particle.position); } } @@ -3359,6 +3602,14 @@ void nmCalculationAutoFitPSO::updateVelocityAndPosition() double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameters) { + // 粒子适应度评价函数,也是自动拟合最核心的闭环: + // 1. 校验粒子参数是否在用户设置的上下界和基本物理范围内; + // 2. 将参数写入 DataManager 的储层/目标井对象; + // 3. 调用真实数值求解器,生成模拟结果; + // 4. 从目标井读取模拟后的 result log-log 曲线; + // 5. 与目标 history log-log 曲线计算误差,误差越小代表拟合越好。 + // + // 返回 1e10 表示该粒子评价失败或结果不可用。PSO 会把它当成很差的解。 const QString funcName = QString("evaluateError[%1]").arg(m_currentIteration); static int callCount = 0; callCount++; @@ -3377,7 +3628,8 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter DEBUG_OUT(QString("%1: %2").arg(funcName).arg(paramStr)); - // 1. 参数有效性检查 - 添加详细失败原因 + // 1. 参数有效性检查。这里先在 PSO 层拦截明显非法的候选, + // 避免把非有限数、越界值或极端危险值传给求解器。 if(!validateParameters(parameters)) { DEBUG_OUT(QString("%1: Call #%2 - VALIDATION FAILED").arg(funcName).arg(callCount)); @@ -3404,7 +3656,8 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter .arg(i).arg(parameters[i]).arg(upper)); } - // 检查危险值 + // 检查危险值。这些条件不是严格物理模型定义, + // 而是工程保护:避免求解器在明显异常输入下崩溃或返回无意义曲线。 switch(paramIndex) { case 0: // 渗透率 if(parameters[i] <= 1e-6) { @@ -3436,7 +3689,7 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter DEBUG_OUT(QString("%1: Call #%2 - Parameters validated OK").arg(funcName).arg(callCount)); - // 2. 数据管理器检查 + // 2. 数据管理器检查。后续参数写回和求解器组装都依赖当前 DataManager。 nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); if(!dataManager) { @@ -3446,7 +3699,8 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter DEBUG_OUT(QString("%1: Call #%2 - DataManager OK").arg(funcName).arg(callCount)); - // 3. 应用参数 + // 3. 应用参数。parameters 的顺序与 m_enabledParamIndices 对齐, + // applyParametersToDataManager() 会把它们拆分写入储层参数和目标井参数。 try { DEBUG_OUT(QString("%1: Call #%2 - Applying parameters...").arg(funcName).arg(callCount)); applyParametersToDataManager(parameters); @@ -3457,7 +3711,8 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter return 1e10; } - // 4. 运行求解器 + // 4. 运行求解器。真实求解器偶发失败时允许重试,避免一次 DLL 调用异常 + // 直接让整个粒子评价失败。 QVector> solverResult; const int maxRetries = 2; bool solverSuccess = false; @@ -3512,7 +3767,8 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter return 1e10; } - // 5. 获取LogLog数据 + // 5. 获取 LogLog 数据。runSolver() 会更新 DataManager 中目标井的计算结果, + // 这里再从目标井读取 resultLogLogData 作为模拟曲线。 QVector> resultLogLogData; try { @@ -3569,7 +3825,7 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter return 1e10; } - // 6. 计算误差 + // 6. 计算误差。这里比较的是目标井 history log-log 与当前模拟 result log-log。 double error; try { @@ -3586,6 +3842,7 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter DEBUG_OUT(QString("%1: Call #%2 - SUCCESS! Error = %3") .arg(funcName).arg(callCount).arg(error, 0, 'e', 6)); + // 保存最后一次有效曲线,供 updateParticle() 写入粒子 currentLogLogData。 m_lastEvaluatedLogLogData = resultLogLogData; } catch(const std::exception& e) { @@ -3611,6 +3868,9 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter void nmCalculationAutoFitPSO::applyParametersToDataManager(const QVector& parameters) { + // 将粒子的“启用参数向量”写回项目数据。 + // parameters 的维度必须等于用户勾选的参数数量,顺序由 m_enabledParamIndices 决定。 + // 这里不直接跑求解器,只负责把 DataManager 调整到该粒子对应的模型状态。 if(parameters.size() != getEnabledParameterCount()) { return; } @@ -3621,9 +3881,13 @@ void nmCalculationAutoFitPSO::applyParametersToDataManager(const QVector void nmCalculationAutoFitPSO::updateReservoirParameters(const QVector& parameters) { + // 更新储层级参数。井级参数 skin/wellboreC 不在这里改,由 updateWellParameters() 负责。 + // 这里先取 DataManager 中 reservoir 的副本,修改后再整体写回 DataManager。 nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); nmDataReservoir reservoirData = dataManager->getReservoirDataCopy(); + // paramIndex 是粒子 position 中的索引;i 是完整 11 个参数体系中的索引。 + // 只有 m_parameterSelected[i] 为 true 时,才从 parameters 中消费一个值。 int paramIndex = 0; for(int i = 0; i < m_parameterSelected.size() && i < 11; ++i) { @@ -3678,9 +3942,13 @@ void nmCalculationAutoFitPSO::updateReservoirParameters(const QVector& p void nmCalculationAutoFitPSO::updateWellParameters(const QVector& parameters) { + // 更新目标井上的拟合参数。目前井级可拟合参数主要是: + // - skin:写入第一个 perforation; + // - wellboreC:写入井筒储集系数。 + // 如果目标井不存在或没有射孔数据,这里只记录 debug,不抛异常。 nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); - // 获取井列表,更新第一口井的参数 + // 只更新当前目标井的井参数,避免多井项目中误改其他井。 //QVector wells = dataManager->getWellDataList(); nmDataWellBase* pWell = dataManager->findWellByName(m_targetWellName); @@ -3729,6 +3997,8 @@ void nmCalculationAutoFitPSO::updateWellToDataManager(nmDataWellBase* pWell) nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); NM_WELL_MODEL wellType = pWell->getWellType(); + // DataManager 内部按井型维护不同容器。修改基类指针后,需要根据实际井型 + // 调用对应 update 接口,才能让后续求解器组装读到最新 skin / wellboreC。 switch(wellType) { case NM_WELL_MODEL::Vertical_Well: { nmDataVerticalWell* pVerticalWell = dynamic_cast(pWell); @@ -3775,6 +4045,8 @@ void nmCalculationAutoFitPSO::updateWellToDataManager(nmDataWellBase* pWell) QVector> nmCalculationAutoFitPSO::runSolver() { + // 真实求解器入口。目前默认走 DLL 方式,保留 runSolverExe() 是历史/备用路径。 + // evaluateFitness() 不关心具体求解器形态,只要求返回可验证的双对数结果。 //return runSolverExe(); return runSolverDll(); } @@ -3783,6 +4055,9 @@ QVector> nmCalculationAutoFitPSO::runSolver() QVector nmCalculationAutoFitPSO::interpolateData( const QVector& source, const QVector& targetX) const { + // 将一条曲线插值到指定 targetX 网格。 + // 双对数误差计算前会分别把目标曲线和模拟曲线插到同一个 commonX 上, + // 这样每个时间点才能逐点比较。 QVector result; if(source.isEmpty() || targetX.isEmpty()) { @@ -3790,7 +4065,8 @@ QVector nmCalculationAutoFitPSO::interpolateData( return result; } - // 数据清理和验证合并 + // 数据清理和验证合并:跳过 NaN/Inf 和极端大值,尽量保留可用点继续计算。 + // 这里不立即把整个粒子判失败,是为了提高求解器偶发异常输出下的鲁棒性。 QVector validSource; const double MAX_REASONABLE_VALUE = 1e12; @@ -3825,7 +4101,7 @@ QVector nmCalculationAutoFitPSO::interpolateData( DEBUG_OUT(QString("Data cleaning: %1 -> %2 valid points") .arg(source.size()).arg(validSource.size())); - // 对清理后的数据进行排序 + // 对清理后的数据按 X 排序。后续线性插值要求横坐标单调。 QVector sortedSource = validSource; for(int i = 0; i < sortedSource.size() - 1; ++i) { @@ -3853,7 +4129,10 @@ QVector nmCalculationAutoFitPSO::interpolateData( DEBUG_OUT(QString("Source X range: [%1, %2], Target X range: [%3, %4]") .arg(sourceMinX).arg(sourceMaxX).arg(targetMinX).arg(targetMaxX)); - // 插值算法 + // 插值算法: + // - 在源数据范围内用线性内插; + // - 目标 X 落在源范围外时做有限斜率外推; + // - 找不到区间时使用最近点兜底。 for(int i = 0; i < targetX.size(); ++i) { double x = targetX[i]; @@ -3951,6 +4230,9 @@ QVector nmCalculationAutoFitPSO::interpolateData( void nmCalculationAutoFitPSO::adaptiveParameterUpdate(int iteration) { + // 自适应调整 PSO 系数。 + // 随迭代推进降低惯性权重,让搜索从全局探索逐步转向局部收敛; + // 当近期改进很小时,提高 cognitive、降低 social,让粒子更多参考自身历史最优。 double progress = static_cast(iteration) / m_maxIterations; // 更快降低惯性权重,增强局部搜索 m_inertiaWeight = 0.4 - 0.25 * progress; @@ -3973,6 +4255,8 @@ void nmCalculationAutoFitPSO::adaptiveParameterUpdate(int iteration) void nmCalculationAutoFitPSO::saveOptimizationResult() { + // 当前函数只做日志记录。真正把最优参数写回项目数据的是 + // startAutoFitting() 结束阶段的 applyParametersToDataManager(m_globalBestPosition)。 DEBUG_OUT(QString("Optimization result: error=%1, evaluations=%2/%3") .arg(m_globalBestFitness, 0, 'e', 4) .arg(m_successfulEvaluations) @@ -3981,6 +4265,10 @@ void nmCalculationAutoFitPSO::saveOptimizationResult() void nmCalculationAutoFitPSO::validateAndProtectFinalResult() { + // 最终精英保护。 + // PSO 是随机启发式算法,某些工况下可能没有找到比用户初始模型更好的结果。 + // 这里用初始解误差与最终全局最优误差做比较,如果改进不足,就恢复初始解, + // 避免“自动拟合”把已有模型调坏。 if(!m_hasValidUserSolution) { emit logMessageGenerated(tr("No initial solution for elite protection")); return; @@ -4024,11 +4312,15 @@ void nmCalculationAutoFitPSO::validateAndProtectFinalResult() double nmCalculationAutoFitPSO::random01() const { + // PSO 使用 Qt 的 qrand/qsrand。随机种子在 startAutoFitting() 中设置, + // 当前默认固定种子,便于复现实验和 trace。 return static_cast(qrand()) / RAND_MAX; } void nmCalculationAutoFitPSO::clampToLimits(QVector& parameters) const { + // 将粒子位置夹回用户给定上下界。parameters 是紧凑的启用参数向量, + // 需要通过 m_enabledParamIndices 找到它在完整参数体系中的上下界。 for(int i = 0; i < parameters.size() && i < m_enabledParamIndices.size(); ++i) { int paramIndex = m_enabledParamIndices[i]; @@ -4041,6 +4333,7 @@ void nmCalculationAutoFitPSO::clampToLimits(QVector& parameters) const int nmCalculationAutoFitPSO::getEnabledParameterCount() const { + // 返回粒子维度,即用户勾选参与拟合的参数数量。 int count = 0; for(int i = 0; i < m_parameterSelected.size(); ++i) { @@ -4054,6 +4347,9 @@ int nmCalculationAutoFitPSO::getEnabledParameterCount() const bool nmCalculationAutoFitPSO::validateParameters(const QVector& parameters) const { + // 参数合法性检查分两层: + // 1. 与用户界面设置一致:维度、有限数、上下界; + // 2. 求解器保护:拦截会导致数值崩溃或明显无物理意义的极端值。 if(parameters.size() != getEnabledParameterCount()) { return false; } @@ -4121,7 +4417,9 @@ bool nmCalculationAutoFitPSO::validateParameters(const QVector& paramete bool nmCalculationAutoFitPSO::validateLogLogData(const QVector>& logLogData) const { - // 检查基本结构 + // 校验双对数曲线结构。约定: + // logLogData[0]=time,logLogData[1]=pressure,logLogData[2]=pressure derivative。 + // 三列必须长度一致,且至少有足够点数用于插值和误差计算。 if(logLogData.size() < 3) { DEBUG_OUT("LogLog data has less than 3 arrays"); return false; @@ -4164,6 +4462,8 @@ bool nmCalculationAutoFitPSO::validateLogLogData(const QVector>& bool nmCalculationAutoFitPSO::validateInitialValues() const { + // 检查当前模型读取出的初始参数是否和用户勾选维度一致,并且在上下界内。 + // 如果初始值越界,算法仍可继续,但日志会提示,因为精英保护可能不可用或效果变差。 if(m_initialValues.size() != m_enabledParamIndices.size()) { DEBUG_OUT("Initial values count mismatch with enabled parameters"); return false; @@ -4198,7 +4498,8 @@ bool nmCalculationAutoFitPSO::validateInitialValues() const bool nmCalculationAutoFitPSO::validateSolverResult(const QVector>& result) const { - // 基本检查 + // 校验求解器压力结果。这里检查的是 pressure result,至少需要 time 和 pressure 两列。 + // result log-log 的结构会在 validateLogLogData() 中另行检查。 if(result.size() < 2) { DEBUG_OUT("Solver result has less than 2 arrays"); return false; @@ -4249,6 +4550,8 @@ bool nmCalculationAutoFitPSO::validateSolverResult(const QVector double nmCalculationAutoFitPSO::calculateCurveError( const QVector& curve1, const QVector& curve2) const { + // 两条已经对齐到同一 X 网格的曲线误差。 + // 使用“相对误差为主、绝对误差为辅”的点误差,并对极端值做保护。 if(curve1.size() != curve2.size() || curve1.isEmpty()) { return 1e10; } @@ -4281,7 +4584,7 @@ double nmCalculationAutoFitPSO::calculateCurveError( continue; } - // 自适应权重:根据Y值大小调整,添加上限 + // 自适应权重:Y 值越大权重越小,避免大幅值段完全主导误差。 double weightFactor = qMin(100.0, qAbs(y1) * 0.01); double weight = 1.0 / (1.0 + weightFactor); @@ -4327,13 +4630,21 @@ double nmCalculationAutoFitPSO::calculateLogLogCurveError( const QVector >& target, const QVector >& result) const { + // 双对数曲线误差计算。 + // + // target 通常来自目标井历史曲线,result 来自当前粒子参数下的模拟曲线。 + // 两条曲线的时间点往往不完全一致,所以这里先取两者时间范围的重叠区间, + // 再在公共时间网格上插值对齐,最后分别计算压力曲线和压力导数曲线误差。 + // + // 返回值越小表示拟合越好;返回 1e10 表示曲线无效或无法比较。 // 验证数据 if(!validateLogLogData(target) || !validateLogLogData(result)) { return 1e10; } try { - // 数据对齐:找到X值的重叠区域 + // 数据对齐:找到目标曲线与模拟曲线 time 轴的重叠区域。 + // 不在重叠区域内的点不参与误差,避免外推导致误差失真。 double targetMinX = target[0][0]; double targetMaxX = target[0][0]; @@ -4360,7 +4671,8 @@ double nmCalculationAutoFitPSO::calculateLogLogCurveError( return 1e10; } - // 生成公共X网格进行插值 + // 生成公共 X 网格进行插值。使用对数均匀网格,是为了给早期时间段 + // 更多分辨率;试井双对数曲线的早期形态通常对参数识别很敏感。 QVector commonX; int numPoints = 50; @@ -4389,7 +4701,7 @@ double nmCalculationAutoFitPSO::calculateLogLogCurveError( return 1e10; } - // 插值目标曲线 + // 插值目标曲线。target[1] 是压力,target[2] 是压力导数。 QVector targetCurve1, targetCurve2; for(int i = 0; i < target[0].size(); ++i) { @@ -4409,7 +4721,7 @@ double nmCalculationAutoFitPSO::calculateLogLogCurveError( QVector alignedTarget1 = interpolateData(targetCurve1, commonX); QVector alignedTarget2 = interpolateData(targetCurve2, commonX); - // 插值结果曲线 + // 插值结果曲线。result 与 target 使用同一 commonX,保证逐点可比。 QVector resultCurve1, resultCurve2; for(int i = 0; i < result[0].size(); ++i) { @@ -4442,7 +4754,8 @@ double nmCalculationAutoFitPSO::calculateLogLogCurveError( return 1e10; } - // 计算两条曲线的误差 + // 计算两条曲线的误差。当前压力和导数各占 50%。 + // 如果后续要让导数形态更重要,可以从这里调整权重。 double error1 = calculateCurveError(alignedTarget1, alignedResult1); double error2 = calculateCurveError(alignedTarget2, alignedResult2); @@ -4491,21 +4804,28 @@ double nmCalculationAutoFitPSO::calculateWeightedPointError( return 1e10; } - // 对数空间的相对误差 + // 对数空间误差:关注数量级差异,适合双对数曲线。 double logTarget = qLn(qMax(1e-10, qAbs(target))); double logResult = qLn(qMax(1e-10, qAbs(result))); double logError = qAbs(logTarget - logResult); - // 线性空间的相对误差 + // 线性空间相对误差:补充约束实际幅值差异。 double yMax = qMax(qAbs(target), qAbs(result)); double relativeError = qAbs(target - result) / qMax(1e-10, yMax); - // 组合误差:对数误差占70%,线性误差占30% + // 组合误差:对数误差占 70%,线性误差占 30%。 + // timeWeight 当前没有参与最终公式,保留参数是为了后续按时间段加权扩展。 return 0.7 * logError + 0.3 * relativeError; } QVector> nmCalculationAutoFitPSO::runSolverDll() { + // DLL 求解器路径。 + // 这个函数负责把当前 DataManager 中的项目状态交给底层数值求解器, + // 并让目标井生成 result log-log 数据。evaluateFitness() 后续会从目标井读取该结果。 + // + // 如果这里失败,通常需要优先检查:HX_NWTM.dll、license、网格/井数据是否完整、 + // 目标井是否存在,以及 DataManager 中刚写入的参数是否导致求解器异常。 DEBUG_OUT("SOLVER DLL START"); if(m_evaluationInProgress > 0) { @@ -4530,10 +4850,10 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() DEBUG_OUT("Starting DLL solver execution..."); - // 异步执行 + // 异步执行 DLL 任务。循环等待期间持续 processEvents,保证界面不会完全卡死。 dllTask->start(); - // 等待完成 + // 等待完成。最大等待 1 小时,适配大模型慢算;用户停止时会 terminate。 int waitTime = 0; const int maxWait = 3600000; // 1h超时 const int checkInterval = 500; @@ -4569,7 +4889,7 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() return result; } - // 验证结果数据是否已更新 + // 验证结果数据是否已更新。DLL 任务会把结果写回 DataManager 中的目标井对象。 nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); //QVector wells = dataManager->getWellDataList(); nmDataWellBase* pTargetWell = dataManager->findWellByName(m_targetWellName); @@ -4581,7 +4901,8 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() return result; } - // 验证结果数据 + // 验证结果数据。evaluateFitness() 最终用的是 logLogResult, + // 但这里返回 pressureResult 给 validateSolverResult() 做基本求解成功判断。 QVector> pressureResult = pTargetWell->getResultPressure(); QVector> logLogResult = pTargetWell->getResultLogLog(); @@ -4607,7 +4928,8 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() DEBUG_OUT(QString("Got DLL solver result: %1 points").arg(result[0].size())); m_consecutiveFailures = 0; - // 检查结果是否与之前不同 + // 检查结果是否与之前不同。若连续粒子得到完全相同的压力曲线, + // 可能说明参数没有正确写入 DataManager,或求解器缓存/状态没有刷新。 static QVector lastPressureResult; bool isDifferentFromLast = false; @@ -4821,6 +5143,10 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() StopReasonPSO nmCalculationAutoFitPSO::analyzeOptimizationStatus() { + // 按优先级判断本轮是否停止。 + // 目标达成和最大迭代是硬条件;真收敛/局部最优需要足够历史数据支撑。 + // 返回值只描述原因,真正的日志和 UI 收尾在 startAutoFitting() 中处理。 + // 1. 检查用户停止 if(m_shouldStop) { return PSO_USER_STOPPED; @@ -4861,6 +5187,9 @@ StopReasonPSO nmCalculationAutoFitPSO::analyzeOptimizationStatus() bool nmCalculationAutoFitPSO::checkTrueConvergence() const { + // 真收敛判断:不是只看“最近没有改进”,而是同时看解质量、误差稳定性、 + // 粒子群多样性、平均速度、长期改进和粒子停滞率。 + // 这样可以减少把局部卡住误判成正常收敛的概率。 if(m_convergenceHistory.size() < m_trueConvergenceWindow) { return false; } @@ -4924,6 +5253,8 @@ bool nmCalculationAutoFitPSO::checkTrueConvergence() const bool nmCalculationAutoFitPSO::checkLocalOptimumTrap() const { + // 局部最优判断:当误差离目标还远,但短期几乎不改进, + // 且粒子群过度聚集/异常分散或大部分粒子停滞时,认为可能卡住。 if(m_convergenceHistory.size() < m_localOptimumWindow) { return false; } @@ -4970,6 +5301,8 @@ bool nmCalculationAutoFitPSO::checkLocalOptimumTrap() const double nmCalculationAutoFitPSO::calculateSwarmDiversity() const { + // 粒子群多样性:逐维计算粒子位置标准差,再按该参数搜索范围归一化。 + // 返回值越小,说明粒子越集中;越大,说明搜索仍然分散。 if(m_swarm.size() < 2) return 0.0; int dimensions = getEnabledParameterCount(); @@ -5011,6 +5344,8 @@ double nmCalculationAutoFitPSO::calculateSwarmDiversity() const double nmCalculationAutoFitPSO::calculateAverageVelocity() const { + // 平均速度:逐维速度绝对值按参数范围归一化。 + // 收敛阶段速度应逐渐变小。 if(m_swarm.isEmpty()) return 0.0; int dimensions = getEnabledParameterCount(); @@ -5033,6 +5368,8 @@ double nmCalculationAutoFitPSO::calculateAverageVelocity() const double nmCalculationAutoFitPSO::calculateParticleStagnationRate() const { + // 粒子停滞率:当前 fitness 与 pbest 很接近时认为该粒子停滞。 + // 该指标用于辅助判断真收敛或局部最优。 if(m_swarm.isEmpty()) return 0.0; int stagnantParticles = 0; @@ -5052,6 +5389,8 @@ double nmCalculationAutoFitPSO::calculateParticleStagnationRate() const double nmCalculationAutoFitPSO::calculateFitnessVariance(int windowSize) const { + // 最近 windowSize 代全局最优误差的方差。 + // 方差越小,说明全局最优曲线越稳定。 if(m_convergenceHistory.size() < windowSize) { return 1e10; // 数据不足,返回大值 } @@ -5080,6 +5419,8 @@ double nmCalculationAutoFitPSO::calculateFitnessVariance(int windowSize) const double nmCalculationAutoFitPSO::calculateLongTermImprovement(int windowSize) const { + // 最近 windowSize 代相对改进幅度。 + // 数据不足时返回 1.0,表示“还不能认为没有改进”。 if(m_convergenceHistory.size() < windowSize) { return 1.0; // 数据不足,假设有改进 } @@ -5092,6 +5433,7 @@ double nmCalculationAutoFitPSO::calculateLongTermImprovement(int windowSize) con QString nmCalculationAutoFitPSO::getStopReasonDescription(StopReasonPSO reason) const { + // 将停止枚举转成人类可读文本,用于日志和运行摘要。 switch(reason) { case PSO_TARGET_ACHIEVED: return tr("Target error achieved"); @@ -5121,6 +5463,8 @@ QString nmCalculationAutoFitPSO::getStopReasonDescription(StopReasonPSO reason) void nmCalculationAutoFitPSO::updateConvergenceMetrics() { + // 每代结束后缓存收敛辅助指标,只保留最近 50 个点,避免长时间运行无限增长。 + // 更新多样性历史 m_diversityHistory.append(calculateSwarmDiversity()); @@ -5148,12 +5492,16 @@ void nmCalculationAutoFitPSO::updateConvergenceMetrics() void nmCalculationAutoFitPSO::setSimulationMode(bool enabled) { + // 仿真模式开关。它用于 UI 演示/调试进度和日志,不调用真实 DLL 求解器。 + // 正式自动拟合流程通常保持 false。 m_simulationMode = enabled; DEBUG_OUT(QString("Simulation mode %1").arg(enabled ? "enabled" : "disabled")); } void nmCalculationAutoFitPSO::setSimulationTargetParams(const QVector& targetParams, double targetError) { + // 设置仿真模式的“目标参数”和“目标误差”。 + // 仿真过程会把参数和误差平滑地从初始值过渡到这里。 m_simulationTargetParams = targetParams; m_simulationTargetError = targetError; DEBUG_OUT(QString("Simulation target params set: %1 parameters, target error: %2") @@ -5162,6 +5510,8 @@ void nmCalculationAutoFitPSO::setSimulationTargetParams(const QVector& t QVector > nmCalculationAutoFitPSO::buildSimulatedLogLogData(double progress) const { + // 为仿真模式生成一条看起来逐渐贴近目标曲线的 log-log 曲线。 + // progress=0 表示偏离目标较多,progress=1 基本等于目标曲线。 QVector > simulated; if(m_targetLogLogData.size() < 3) { @@ -5198,6 +5548,8 @@ QVector > nmCalculationAutoFitPSO::buildSimulatedLogLogData(doub void nmCalculationAutoFitPSO::runSimulatedFitting() { + // 仿真模式入口:模拟完整 PSO 日志、进度、bestCurveUpdated 信号和最终写回。 + // 它不评价粒子、不调用代理模型、不调用 DLL,只用于界面联调或演示自动拟合流程。 DEBUG_OUT("=== SIMULATED PSO AUTO FITTING START ==="); m_isRunning = true; m_shouldStop = false; @@ -5269,6 +5621,8 @@ void nmCalculationAutoFitPSO::runSimulatedFitting() void nmCalculationAutoFitPSO::onSimulationTimerTick() { + // 仿真模式用 QTimer 分步输出日志,避免一次性刷完,效果更接近真实长任务。 + // 检查是否需要停止 if(m_shouldStop) { m_simulationTimer->stop(); @@ -5325,6 +5679,9 @@ void nmCalculationAutoFitPSO::onSimulationTimerTick() void nmCalculationAutoFitPSO::emitSimulationLog(int iteration) { + // 旧版仿真日志函数:一次性输出某一代的模拟粒子情况。 + // 当前 runSimulatedFitting() 主要通过 startNewSimulationIteration()/emitParticleLog() + // 分步输出,但保留本函数便于调试或恢复旧演示节奏。 m_simulationCurrentError = calculateSimulatedError(iteration); if(m_simulationCurrentError < m_globalBestFitness) { @@ -5410,6 +5767,8 @@ void nmCalculationAutoFitPSO::emitSimulationLog(int iteration) double nmCalculationAutoFitPSO::calculateSimulatedError(int iteration) { + // 生成随迭代递减的模拟误差,并叠加少量随机扰动。 + // 使用非线性 decay,让前期下降较快、后期逐渐变慢,更像真实优化曲线。 if(iteration <= 0) return m_simulationStartError; if(iteration >= m_simulationMaxIterations) return m_simulationTargetError; @@ -5429,6 +5788,8 @@ double nmCalculationAutoFitPSO::calculateSimulatedError(int iteration) QVector nmCalculationAutoFitPSO::calculateSimulatedParams(int iteration) { + // 生成随迭代从初始参数平滑靠近目标参数的模拟参数。 + // 返回向量仍然是“启用参数向量”,可直接传给 applyParametersToDataManager()。 QVector currentParams; if(iteration <= 0) return m_simulationStartParams; @@ -5455,6 +5816,7 @@ QVector nmCalculationAutoFitPSO::calculateSimulatedParams(int iteration) void nmCalculationAutoFitPSO::startNewSimulationIteration() { + // 开始仿真模式的一代:更新误差/最优曲线,输出该代标题和累计评价数。 m_simulationIteration++; m_simulationCurrentParticle = 0; m_simulationSuccessCount = 0; @@ -5494,6 +5856,8 @@ void nmCalculationAutoFitPSO::startNewSimulationIteration() void nmCalculationAutoFitPSO::emitParticleLog(int particleIndex) { + // 输出单个仿真粒子的日志。这里随机生成失败、显著改进、微小改进、无改进四类情况, + // 目的是让 UI 日志表现接近真实 PSO,而不是用于任何真实优化计算。 int particleId = particleIndex + 1; // 为每个粒子生成模拟的误差值 @@ -5536,6 +5900,7 @@ void nmCalculationAutoFitPSO::emitParticleLog(int particleIndex) void nmCalculationAutoFitPSO::finishCurrentIteration() { + // 完成仿真模式的一代:输出统计、收敛检查和进度更新,然后等待下一次 timer tick。 m_simulationIterationStarted = false; bool shouldOutputDetail = (m_simulationIteration % qMax(1, m_simulationMaxIterations / 10) == 0) || @@ -5591,6 +5956,8 @@ void nmCalculationAutoFitPSO::finishCurrentIteration() void nmCalculationAutoFitPSO::finishSimulation() { + // 仿真模式正常结束:直接采用预设目标参数/目标误差作为最终结果, + // 发送与真实拟合相同风格的日志、进度和 fittingFinished 信号。 m_globalBestPosition = m_simulationTargetParams; m_globalBestFitness = m_simulationTargetError; m_globalBestLogLogData = buildSimulatedLogLogData(1.0); @@ -5634,4 +6001,4 @@ 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); -} +} \ No newline at end of file diff --git a/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp b/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp index 4e17855..92b997a 100644 --- a/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp +++ b/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp @@ -771,22 +771,22 @@ void nmWxAutomaticFitting::startAutoFitting(const QVector>& targ m_autoFitterPSO->setTargetLogLogData(targetData); m_autoFitterPSO->setPSOTargetWellName(targetWellName); - // 特定井名时使用快速路径 - if (targetWellName == "VerticalWell1") { + //// 特定井名时使用快速路径 + //if (targetWellName == "VerticalWell1") { - m_autoFitterPSO->setSimulationMode(true); + // m_autoFitterPSO->setSimulationMode(true); - // 构建目标参数向量(按启用参数的顺序) - QVector targetParams; + // // 构建目标参数向量(按启用参数的顺序) + // QVector targetParams; - if(m_kCheckBox->isChecked()) targetParams.append(0.0033); // 渗透率 - if(m_sCheckBox->isChecked()) targetParams.append(1.75); // 表皮系数 - if(m_cCheckBox->isChecked()) targetParams.append(0.23); // 井筒储集系数 - if(m_phiCheckBox->isChecked()) targetParams.append(0.189); // 孔隙度 + // if(m_kCheckBox->isChecked()) targetParams.append(0.0033); // 渗透率 + // if(m_sCheckBox->isChecked()) targetParams.append(1.75); // 表皮系数 + // if(m_cCheckBox->isChecked()) targetParams.append(0.23); // 井筒储集系数 + // if(m_phiCheckBox->isChecked()) targetParams.append(0.189); // 孔隙度 - double targetError = 0.02291; - m_autoFitterPSO->setSimulationTargetParams(targetParams, targetError); - } + // double targetError = 0.02291; + // m_autoFitterPSO->setSimulationTargetParams(targetParams, targetError); + //} //else if (targetWellName == "W0009_1") { // m_autoFitterPSO->setSimulationMode(true); diff --git a/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp b/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp index dfdd143..c585118 100644 --- a/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp +++ b/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp @@ -577,13 +577,13 @@ void nmWxAutomaticfittingStart::setAutoFitter(nmCalculationAutoFitPSO* autoFitte if (m_autoFitterPSO) { connect(m_autoFitterPSO, SIGNAL(progressUpdated(int, double)), - this, SLOT(onFittingProgress(int, double))); + this, SLOT(onFittingProgress(int, double)));//更新进度条、当前迭代数、当前误差 connect(m_autoFitterPSO, SIGNAL(fittingFinished(bool, QString)), - this, SLOT(onFittingFinished(bool, QString))); + this, SLOT(onFittingFinished(bool, QString)));//显示结束状态 connect(m_autoFitterPSO, SIGNAL(logMessageGenerated(QString)), - this, SLOT(onLogMessageReceived(QString))); + this, SLOT(onLogMessageReceived(QString)));//把 PSO 内部日志显示到窗口 connect(m_autoFitterPSO, SIGNAL(bestCurveUpdated(QVector >,QVector >,int,double)), - this, SLOT(onBestCurveUpdated(QVector >,QVector >,int,double))); + this, SLOT(onBestCurveUpdated(QVector >,QVector >,int,double)));//把目标曲线和当前最优模拟曲线画出来 // 启用停止按钮 stopButton->setEnabled(true);