diff --git a/Include/nmNum/nmCalculation/nmCalculationAutoFitPSO.h b/Include/nmNum/nmCalculation/nmCalculationAutoFitPSO.h index 4a29ca7..e276284 100644 --- a/Include/nmNum/nmCalculation/nmCalculationAutoFitPSO.h +++ b/Include/nmNum/nmCalculation/nmCalculationAutoFitPSO.h @@ -24,16 +24,20 @@ class QProcess; // 不是完整的 11 个储层/井筒参数。完整参数向量会在写 trace 或调用代理模型时 // 通过 buildTraceParameterVector() 重新组装。 // -// surrogate* 和 screeningDecision 是 PSO 加速筛选的辅助字段。代理模型只决定 -// “这一代哪些粒子需要继续跑真实求解器”,不会直接更新 pbest / gbest。 +// surrogate* 和 screeningDecision 是 PSO 加速筛选的辅助字段。真实 pbest / gbest +// 始终只由求解器更新;T1-T5 可另外维护不参与最终结果的 guideBestPosition 来修正速度方向。 struct AutoFitParticle { QVector > currentLogLogData; QVector > bestLogLogData; QVector position; // 当前位置(参数值) QVector velocity; // 速度 - QVector bestPosition; // 个体最优位置 + QVector bestPosition; // 真实求解器确认的个体最优位置 + QVector guideBestPosition; // 仅用于速度更新的引导位置;不会参与真实 gbest/最终结果 double fitness; // 当前适应度 - double bestFitness; // 个体最优适应度 + double bestFitness; // 真实求解器确认的个体最优适应度 + double guideBestObjective; // guideBestPosition 对应的真实或代理目标值 + bool guideBestFromSurrogate; // true 表示当前引导位置尚未经过真实求解器验证 + double pbestRelativeImprovementThisIteration; // 本代真实评价相对旧 pbest 的改善幅度 bool evaluatedThisIteration; bool lastEvaluationSuccess; @@ -46,6 +50,9 @@ struct AutoFitParticle { AutoFitParticle() : fitness(1e10) , bestFitness(1e10) + , guideBestObjective(1e10) + , guideBestFromSurrogate(false) + , pbestRelativeImprovementThisIteration(0.0) , evaluatedThisIteration(false) , lastEvaluationSuccess(false) , lastEvaluationElapsedMs(-1) @@ -241,13 +248,14 @@ private: int getSurrogateWarmupIterations() const; int getSurrogateFullSolverInterval() const; QVector buildSurrogateEvaluationMask(); - bool writeSurrogateCandidateCsv(const QString& candidatePath) const; + bool writeSurrogateCandidateCsv(const QString& candidatePath, bool includeGuideCandidates) const; bool runSurrogateScoringProcess(const QString& candidatePath, const QString& scorePath, QString* failureReason = nullptr); bool runSurrogateScoringScriptOnce(const QString& candidatePath, const QString& scorePath, QString* failureReason = nullptr); - bool ensureSurrogateScoringServer(QString* failureReason = nullptr); + bool ensureSurrogateScoringServer(QString* failureReason = nullptr, bool waitForReady = true); bool requestSurrogateScoresFromServer(const QString& candidatePath, const QString& scorePath, QString* failureReason = nullptr); void stopSurrogateScoringServer(); - QVector readSurrogateScores(const QString& scorePath) const; + void resetGuideBestToVerified(); + QVector readSurrogateScores(const QString& scorePath, int expectedCount) const; bool forceSolverByFallbackGate(const QVector& selectedParameters) const; bool isStrongDecliningProductionSchedule(double* endStartRatio = nullptr) const; bool isSurrogateRunContextSupported(QString* reason) const; @@ -369,6 +377,7 @@ private: int m_surrogateMinFloorParticleCount; // 因最低真实求解比例被补选中的粒子累计数。 QProcess* m_surrogateScoringProcess; // 常驻 Python 代理评分 server 进程。 QString m_surrogateScoringServerKey; // 标识当前 server 对应的 python/script/meta/tag/stage。 + bool m_surrogateScoringServerReady; // Python 已加载模型并输出 ready=true。 private: // 模拟拟合相关成员 diff --git a/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp b/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp index 727b7ef..4b0f622 100644 --- a/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp @@ -47,13 +47,14 @@ const int nmCalculationAutoFitPSO::CONVERGENCE_CHECK_INTERVAL = 5; // - min_solver_fraction:每代至少有一定比例粒子跑真实求解器,保证 pbest/gbest 仍由真实误差支撑; // - warmup_iterations:前几代全量真实评价,让每个粒子先拿到可靠的个体最优; // - full_solver_interval:周期性全量审计,用于纠偏代理模型排序误差。 -static const double kSurrogateKeepFraction = 0.50; // 常规工况:代理排序前 50% 粒子进入真实求解器。 +static const double kSurrogateKeepFraction = 0.40; // 常规工况:代理排序前 40% 粒子进入真实求解器。 static const double kSurrogateHighRiskKeepFraction = 0.70; // 高风险流量制度:保留更多 top 粒子。 static const double kSurrogateAuditFraction = 0.05; // 从被筛掉候选中随机审计 5%。 -static const double kSurrogateMinSolverFraction = 0.55; // 常规工况:每代至少 55% 粒子跑真实求解器。 +static const double kSurrogateMinSolverFraction = 0.45; // 常规工况:每代至少 45% 粒子跑真实求解器。 static const double kSurrogateHighRiskMinSolverFraction = 0.80; // 高风险流量制度:每代至少 80% 粒子跑真实求解器。 static const int kSurrogateWarmupIterations = 2; // 前 2 代全量真实评价,给每个粒子建立 pbest。 static const int kSurrogateFullSolverInterval = 10; // 每 10 代做一次全量真实求解审计。 +static const double kSurrogateGuidePbestMinRelativeImprovement = 1.0e-4; // 引导 pbest 至少改善 0.01%,过滤数值噪声。 static const int kSurrogateScoringMaxAttempts = 2; // Python 评分失败时最多重试 2 次。 static const bool kUseFixedPsoSeed = true; // 固定随机种子便于复现 trace 和交接演示。 static const unsigned int kFixedPsoSeed = 1792008679u; // 当前固定种子值。 @@ -73,10 +74,17 @@ 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 double kSurrogateCfMin = 1.0e-6; // 变化 PVT 模型的 Cf 训练域下界。 -static const double kSurrogateCfMax = 5.0e-3; // 变化 PVT 模型的 Cf 训练域上界。 +static const double kSurrogateConstPvtCfFixed = 4.3e-4; // T1/T3 模型输入使用训练数据中的固定 Cf。 +static const double kSurrogateT5CfFixed = 3.0e-4; // T5 专用训练数据中的固定 Cf。 +static const double kSurrogateConstPvtCtFixed = 1.0e-2; // T1/T3 训练数据中的固定 Ct。 +static const double kSurrogateFixedParamRelTol = 1.0e-4; // 固定参数仅允许 0.01% 相对偏差。 +static const double kSurrogateCfMin = 1.0e-5; // T2/T4 训练数据中的 Cf 下界。 +static const double kSurrogateCfMax = 1.0e-2; // T2/T4 训练数据中的 Cf 上界。 +static const char kSurrogateProcessedPath[] = "C:/Users/Asus/Desktop/test/data/processed/dataset_T1_T4_fixed_time_40000.pkl"; +static const char kSurrogateModelPath[] = "C:/Users/Asus/Desktop/test/models/T1_T4_fixed_time_40000_autofit_v1/forward_surrogate_best.pt"; +static const char kT5SurrogateProcessedPath[] = "C:/Users/Asus/Desktop/test/data/processed/dataset_T5_fixedcf_random_18448.pkl"; +static const char kT5SurrogateModelPath[] = "C:/Users/Asus/Desktop/test/models/T5_fixedcf_random_18448_delta_v1/forward_surrogate_best.pt"; +static const double kSurrogateTrainingInitialPressure = 34.47; static const int kSurrogateMinProdSections = 3; // 代理训练流量制度:生产段数量下界。 static const int kSurrogateMaxProdSections = 5; // 代理训练流量制度:生产段数量上界。 static const double kSurrogateProdTimeMin = 24.0; // 代理训练流量制度:生产总时长下界。 @@ -105,7 +113,7 @@ static inline bool isInClosedRange(double value, double lower, double upper) return isFiniteNumber(value) && value >= lower && value <= upper; } -// 判断 value 是否接近 reference。用于 Cf 这类“近似固定”的训练域检查。 +// 判断 value 是否接近 reference。用于 Ct/Cf 等固定训练参数的运行域检查。 static inline bool isNearRelative(double value, double reference, double relTol) { if(!isFiniteNumber(value) || !isFiniteNumber(reference)) { @@ -115,6 +123,117 @@ static inline bool isNearRelative(double value, double reference, double relTol) return qAbs(value - reference) <= qMax(1e-12, qAbs(reference) * relTol); } +static NM_SOLVER_MODEL_TYPE currentSolverModelType() +{ + nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); + return dataManager ? dataManager->getSolverModelType() : SMT_Oil_ConstPvt; +} + +static QString surrogateProcessedPath(NM_SOLVER_MODEL_TYPE solverModelType) +{ + if(solverModelType == SMT_Gas_VariablePvt) { + return QDir::cleanPath(QString::fromLatin1(kT5SurrogateProcessedPath)); + } + + return QDir::cleanPath(QString::fromLatin1(kSurrogateProcessedPath)); +} + +static QString surrogateModelPath(NM_SOLVER_MODEL_TYPE solverModelType) +{ + if(solverModelType == SMT_Gas_VariablePvt) { + return QDir::cleanPath(QString::fromLatin1(kT5SurrogateModelPath)); + } + + return QDir::cleanPath(QString::fromLatin1(kSurrogateModelPath)); +} + +struct SurrogateParameterDomain +{ + double kMin; + double kMax; + double skinMin; + double skinMax; + double wellboreCMin; + double wellboreCMax; + double phiMin; + double phiMax; + double hMin; + double hMax; + double cfMin; + double cfMax; + double cfFixed; + double cfRelTol; + double ctFixed; + double ctRelTol; + bool checkCf; + bool checkCt; + bool variableCf; +}; + +// 每种相态只能在生成该相态训练数据时使用的参数范围内调用代理模型。 +static bool getSurrogateParameterDomain(NM_SOLVER_MODEL_TYPE solverModelType, SurrogateParameterDomain* domain) +{ + if(!domain) { + return false; + } + + domain->kMin = kSurrogateKMin; + domain->kMax = kSurrogateKMax; + domain->skinMin = kSurrogateSkinMin; + domain->skinMax = kSurrogateSkinMax; + domain->wellboreCMin = kSurrogateWellboreCMin; + domain->wellboreCMax = kSurrogateWellboreCMax; + domain->phiMin = kSurrogatePhiMin; + domain->phiMax = kSurrogatePhiMax; + domain->hMin = kSurrogateHMin; + domain->hMax = kSurrogateHMax; + domain->cfMin = kSurrogateCfMin; + domain->cfMax = kSurrogateCfMax; + domain->cfFixed = kSurrogateT5CfFixed; + domain->cfRelTol = kSurrogateFixedParamRelTol; + domain->ctFixed = kSurrogateConstPvtCtFixed; + domain->ctRelTol = kSurrogateFixedParamRelTol; + domain->checkCf = false; + domain->checkCt = false; + domain->variableCf = false; + + switch(solverModelType) { + case SMT_Oil_ConstPvt: + case SMT_Water_ConstPvt: + domain->checkCt = true; + return true; + + case SMT_Oil_VariablePvt: + domain->checkCf = true; + domain->variableCf = true; + return true; + + case SMT_Water_VariablePvt: + domain->checkCf = true; + domain->variableCf = true; + return true; + + case SMT_Gas_VariablePvt: + domain->kMin = 4.5e-6; + domain->kMax = 6.2e-6; + domain->skinMin = 0.0; + domain->skinMax = 4.5; + domain->wellboreCMin = 5.0e-5; + domain->wellboreCMax = 1.6e-4; + domain->phiMin = 8.0e-3; + domain->phiMax = 4.0e-2; + domain->hMin = 7.0; + domain->hMax = 11.0; + domain->cfFixed = kSurrogateT5CfFixed; + domain->checkCf = true; + domain->variableCf = false; + return true; + + default: + return false; + } +} + // Windows 下的毫秒级睡眠封装。用于重试间隔、UI 事件循环间隔和模拟模式节奏控制。 static inline void msleep(int ms) { @@ -391,6 +510,7 @@ nmCalculationAutoFitPSO::nmCalculationAutoFitPSO(QObject* parent) , m_surrogateMinFloorParticleCount(0) , m_surrogateScoringProcess(nullptr) , m_surrogateScoringServerKey("") + , m_surrogateScoringServerReady(false) , m_simulationMode(false) , m_simulationTimer(nullptr) @@ -1079,7 +1199,7 @@ void nmCalculationAutoFitPSO::writeTraceRow(int generation, double pbestObjective) { // 写一行 trace。generation=-1/particleIndex=-1 表示用户初始解; - // 普通粒子行的 phase 为 particle_solver 或 particle_not_evaluated。 + // 普通粒子行的 phase 为 particle_solver、particle_verified_cache 或 particle_not_evaluated。 // 被代理筛掉的粒子 solver_objective 写空值,但仍保留 surrogate_objective 和 decision。 if(!m_traceFile.isOpen()) { return; @@ -1148,9 +1268,14 @@ void nmCalculationAutoFitPSO::writeIterationTraceRows() double solverObjective = particle.evaluatedThisIteration ? particle.fitness : std::numeric_limits::quiet_NaN(); + QString phase = !particle.evaluatedThisIteration + ? "particle_not_evaluated" + : (particle.screeningDecision == "initial_solution_cache" + ? "particle_verified_cache" + : "particle_solver"); writeTraceRow(m_currentIteration, i, - particle.evaluatedThisIteration ? "particle_solver" : "particle_not_evaluated", + phase, particle.position, solverObjective, particle.lastEvaluationSuccess, @@ -1226,7 +1351,11 @@ QString nmCalculationAutoFitPSO::getSurrogateTag() const { // 当前 C++ 绑定的代理模型实验标识,对应 ML 侧训练输出目录/checkpoint 命名。 // 如果以后换模型,需要同步更新这里和 ML 脚本可识别的 tag。 - return "T1_T4_merged_40000"; + if(currentSolverModelType() == SMT_Gas_VariablePvt) { + return "T5_fixedcf_random_18448_delta_v1"; + } + + return "T1_T4_fixed_time_40000_autofit_v1"; } QString nmCalculationAutoFitPSO::getSurrogateStage() const @@ -1273,7 +1402,7 @@ int nmCalculationAutoFitPSO::getSurrogateFullSolverInterval() const return kSurrogateFullSolverInterval; } -bool nmCalculationAutoFitPSO::writeSurrogateCandidateCsv(const QString& candidatePath) const +bool nmCalculationAutoFitPSO::writeSurrogateCandidateCsv(const QString& candidatePath, bool includeGuideCandidates) const { // 写给 Python 代理模型的候选粒子 CSV。 // @@ -1282,7 +1411,7 @@ bool nmCalculationAutoFitPSO::writeSurrogateCandidateCsv(const QString& candidat // particle_id,k,skin,wellboreC,phi,h,Cf,solverType // // 这里写的是“完整参数体系”中的关键字段,不是粒子内部紧凑向量。 - // 例如用户没有勾选 Cf 时,Cf 会从当前 DataManager 取值写入 CSV。 + // T1/T3 的 Cf 不影响真实求解,模型输入固定为训练值,避免界面 Cf 把模型带出训练分布。 QFile file(candidatePath); if(!file.open(QIODevice::WriteOnly | QIODevice::Text)) { @@ -1299,11 +1428,16 @@ bool nmCalculationAutoFitPSO::writeSurrogateCandidateCsv(const QString& candidat } int solverType = static_cast(dataManager->getSolverModelType()); + bool useConstPvtTrainingCf = solverType == static_cast(SMT_Oil_ConstPvt) || + solverType == static_cast(SMT_Water_ConstPvt); // 代理模型只需要这些输入字段;其它参数当前不在训练输入集中。 out << "particle_id,k,skin,wellboreC,phi,h,Cf,solverType\n"; for(int i = 0; i < m_swarm.size(); ++i) { QVector params = buildTraceParameterVector(m_swarm[i].position); + QString cfForModel = useConstPvtTrainingCf + ? QString::number(kSurrogateConstPvtCfFixed, 'g', 17) + : traceParamAt(params, 7); QStringList cols; cols << QString::number(i) << traceParamAt(params, 0) @@ -1311,11 +1445,36 @@ bool nmCalculationAutoFitPSO::writeSurrogateCandidateCsv(const QString& candidat << traceParamAt(params, 2) << traceParamAt(params, 3) << traceParamAt(params, 5) - << traceParamAt(params, 7) + << cfForModel << QString::number(solverType); out << cols.join(",") << "\n"; } + if(includeGuideCandidates) { + // T1-T5 都需要判断当前位置是否优于自己的 guide pbest。当前位置和 guide 必须在 + // 同一次代理推理中评分,避免把模型预测误差与真实求解器误差直接比较。 + for(int i = 0; i < m_swarm.size(); ++i) { + const QVector& guidePosition = + m_swarm[i].guideBestPosition.size() == m_swarm[i].position.size() + ? m_swarm[i].guideBestPosition + : m_swarm[i].bestPosition; + QVector params = buildTraceParameterVector(guidePosition); + QString cfForModel = useConstPvtTrainingCf + ? QString::number(kSurrogateConstPvtCfFixed, 'g', 17) + : traceParamAt(params, 7); + QStringList cols; + cols << QString::number(m_swarm.size() + i) + << traceParamAt(params, 0) + << traceParamAt(params, 1) + << traceParamAt(params, 2) + << traceParamAt(params, 3) + << traceParamAt(params, 5) + << cfForModel + << QString::number(solverType); + out << cols.join(",") << "\n"; + } + } + file.flush(); file.close(); return true; @@ -1341,11 +1500,11 @@ bool nmCalculationAutoFitPSO::runSurrogateScoringProcess(const QString& candidat return runSurrogateScoringScriptOnce(candidatePath, scorePath, failureReason); } -bool nmCalculationAutoFitPSO::ensureSurrogateScoringServer(QString* failureReason) +bool nmCalculationAutoFitPSO::ensureSurrogateScoringServer(QString* failureReason, bool waitForReady) { // 确保常驻代理评分 server 已启动且与当前 run 配置匹配。 // - // serverKey 由 python 路径、脚本路径、trace meta、tag、stage 组成。 + // serverKey 由 Python 路径、脚本路径、跟踪元数据、模型数据路径、模型标识和阶段组成。 // 只要其中任何一个变化,就停止旧进程并启动新进程,避免拿旧模型或旧目标曲线评分。 if(m_traceMetaFilePath.isEmpty() || !QFileInfo(m_traceMetaFilePath).exists()) { QString reason = "trace meta file is missing"; @@ -1373,51 +1532,71 @@ bool nmCalculationAutoFitPSO::ensureSurrogateScoringServer(QString* failureReaso } QString pythonPath = getPythonExecutablePath(); - QString serverKey = QString("%1|%2|%3|%4|%5") + NM_SOLVER_MODEL_TYPE solverModelType = currentSolverModelType(); + QString processedPath = surrogateProcessedPath(solverModelType); + QString modelPath = surrogateModelPath(solverModelType); + QString serverKey = QString("%1|%2|%3|%4|%5|%6|%7") .arg(pythonPath) .arg(scriptPath) .arg(m_traceMetaFilePath) + .arg(processedPath) + .arg(modelPath) .arg(getSurrogateTag()) .arg(getSurrogateStage()); - if(m_surrogateScoringProcess && - m_surrogateScoringProcess->state() == QProcess::Running && - m_surrogateScoringServerKey == serverKey) { - return true; - } + bool matchingProcessRunning = m_surrogateScoringProcess && + m_surrogateScoringProcess->state() == QProcess::Running && + m_surrogateScoringServerKey == serverKey; - stopSurrogateScoringServer(); + if(!matchingProcessRunning) { + stopSurrogateScoringServer(); - QStringList args; - // -u 让 Python stdout/stderr 不缓冲,C++ 可以及时读到 ready/ok 行。 - args << "-u" - << scriptPath - << "--trace-meta" << m_traceMetaFilePath - << "--tag" << getSurrogateTag() - << "--stage" << getSurrogateStage(); + QStringList args; + // -u 关闭 Python 标准输出和错误输出的缓冲,C++ 可以及时读到就绪或完成消息。 + args << "-u" + << scriptPath + << "--trace-meta" << m_traceMetaFilePath + << "--processed" << processedPath + << "--model" << modelPath + << "--tag" << getSurrogateTag() + << "--stage" << getSurrogateStage(); - m_surrogateScoringProcess = new QProcess(this); - m_surrogateScoringProcess->setWorkingDirectory(mlRoot); + m_surrogateScoringProcess = new QProcess(this); + m_surrogateScoringProcess->setWorkingDirectory(mlRoot); - emit logMessageGenerated(tr("Starting surrogate scoring server: python=%1, mlRoot=%2") - .arg(pythonPath) - .arg(mlRoot)); + emit logMessageGenerated(tr("Starting surrogate scoring server: python=%1, mlRoot=%2") + .arg(pythonPath) + .arg(mlRoot)); - m_surrogateScoringProcess->start(pythonPath, args); + m_surrogateScoringProcess->start(pythonPath, args); - if(!m_surrogateScoringProcess->waitForStarted(15000)) { - QString reason = QString("failed to start scoring server: python=%1, script=%2, error=%3") - .arg(pythonPath) - .arg(scriptPath) - .arg(m_surrogateScoringProcess->errorString()); - DEBUG_OUT(reason); - stopSurrogateScoringServer(); + if(!m_surrogateScoringProcess->waitForStarted(15000)) { + QString reason = QString("failed to start scoring server: python=%1, script=%2, error=%3") + .arg(pythonPath) + .arg(scriptPath) + .arg(m_surrogateScoringProcess->errorString()); + DEBUG_OUT(reason); + stopSurrogateScoringServer(); - if(failureReason) { - *failureReason = reason; + if(failureReason) { + *failureReason = reason; + } + + return false; } - return false; + m_surrogateScoringServerKey = serverKey; + m_surrogateScoringServerReady = false; + } + + // 预热调用只负责尽早启动 Python,让模型加载与初始解、预热代真实求解重叠。 + // 第一次真正评分时会再次进入本函数并等待 ready=true。 + if(!waitForReady) { + return true; + } + + if(m_surrogateScoringServerReady) { + return true; } QTime timer; @@ -1452,7 +1631,7 @@ bool nmCalculationAutoFitPSO::ensureSurrogateScoringServer(QString* failureReaso readyLine = m_surrogateScoringProcess->readLine().trimmed(); if(readyLine.contains("\"ready\"") && readyLine.contains("true")) { - m_surrogateScoringServerKey = serverKey; + m_surrogateScoringServerReady = true; QString stdErr = QString::fromLocal8Bit(m_surrogateScoringProcess->readAllStandardError()).trimmed(); if(!stdErr.isEmpty()) { @@ -1599,6 +1778,8 @@ void nmCalculationAutoFitPSO::stopSurrogateScoringServer() // 停止常驻 Python 评分进程。 // 先发送 quit 指令,短时间内不退出再 kill,避免应用关闭时遗留 python.exe。 if(!m_surrogateScoringProcess) { + m_surrogateScoringServerKey.clear(); + m_surrogateScoringServerReady = false; return; } @@ -1616,6 +1797,7 @@ void nmCalculationAutoFitPSO::stopSurrogateScoringServer() delete m_surrogateScoringProcess; m_surrogateScoringProcess = nullptr; m_surrogateScoringServerKey.clear(); + m_surrogateScoringServerReady = false; } bool nmCalculationAutoFitPSO::runSurrogateScoringScriptOnce(const QString& candidatePath, const QString& scorePath, QString* failureReason) @@ -1655,11 +1837,16 @@ bool nmCalculationAutoFitPSO::runSurrogateScoringScriptOnce(const QString& candi } QString pythonPath = getPythonExecutablePath(); + NM_SOLVER_MODEL_TYPE solverModelType = currentSolverModelType(); + QString processedPath = surrogateProcessedPath(solverModelType); + QString modelPath = surrogateModelPath(solverModelType); QStringList args; args << scriptPath << "--candidates" << candidatePath << "--trace-meta" << m_traceMetaFilePath << "--output" << scorePath + << "--processed" << processedPath + << "--model" << modelPath << "--tag" << getSurrogateTag() << "--stage" << getSurrogateStage(); @@ -1798,7 +1985,7 @@ bool nmCalculationAutoFitPSO::runSurrogateScoringScriptOnce(const QString& candi return false; } -QVector nmCalculationAutoFitPSO::readSurrogateScores(const QString& scorePath) const +QVector nmCalculationAutoFitPSO::readSurrogateScores(const QString& scorePath, int expectedCount) const { // 读取 Python 输出的评分 CSV。 // @@ -1806,7 +1993,7 @@ QVector nmCalculationAutoFitPSO::readSurrogateScores(const QString& scor // particle_id,surrogate_objective,...,success // 本函数只读取 particle_id、score 和 success 标志。失败或缺失的粒子保持 NaN, // 后续 buildSurrogateEvaluationMask() 会通过 fallback_gate 强制跑真实求解器。 - QVector scores(m_swarm.size(), std::numeric_limits::quiet_NaN()); + QVector scores(expectedCount, std::numeric_limits::quiet_NaN()); QFile file(scorePath); if(!file.open(QIODevice::ReadOnly | QIODevice::Text)) { @@ -1863,16 +2050,12 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co } NM_SOLVER_MODEL_TYPE solverModelType = dataManager->getSolverModelType(); - bool isVariablePvt = solverModelType == SMT_Oil_VariablePvt || - solverModelType == SMT_Water_VariablePvt; - bool isSupportedSolverType = solverModelType == SMT_Oil_ConstPvt || - solverModelType == SMT_Oil_VariablePvt || - solverModelType == SMT_Water_ConstPvt || - solverModelType == SMT_Water_VariablePvt; + SurrogateParameterDomain parameterDomain; + bool isSupportedSolverType = getSurrogateParameterDomain(solverModelType, ¶meterDomain); if(!isSupportedSolverType) { if(reason) { - *reason = QString("solverType=%1 is outside the T1-T4 surrogate model") + *reason = QString("solverType=%1 is outside the T1-T5 surrogate model") .arg(static_cast(solverModelType)); } return false; @@ -1925,12 +2108,12 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co return false; } - // 参数 gate:变化 PVT 的 T2/T4 允许 Cf 参与拟合;常数 PVT 的 T1/T3 仍固定 Cf。 + // 参数门控:只有训练数据中 Cf 实际变化的 T2/T4 允许 Cf 参与拟合。 // 用户勾选其它参数时,代理无法可靠反映这些参数变化,直接禁用代理筛选。 QVector allowedParamIndices; allowedParamIndices << 0 << 1 << 2 << 3 << 5; - if(isVariablePvt) { + if(parameterDomain.variableCf) { allowedParamIndices << 7; } @@ -1948,14 +2131,39 @@ bool nmCalculationAutoFitPSO::isSurrogateRunContextSupported(QString* reason) co } nmDataReservoir reservoirData = dataManager->getReservoirDataCopy(); - double cf = reservoirData.getCf().getValue().toDouble(); + if(parameterDomain.checkCt) { + double ct = reservoirData.getCt().getValue().toDouble(); + + if(!isNearRelative(ct, parameterDomain.ctFixed, parameterDomain.ctRelTol)) { + if(reason) { + *reason = QString("Ct=%1 does not match surrogate training value %2") + .arg(ct, 0, 'g', 10) + .arg(parameterDomain.ctFixed, 0, 'g', 10); + } + + return false; + } + } + + // 固定 Cf 的 T5 在整次拟合入口检查;T2/T4 的可变 Cf 留到逐粒子阶段检查。 + if(parameterDomain.checkCf && !parameterDomain.variableCf) { + double cf = reservoirData.getCf().getValue().toDouble(); + bool cfSupported = isNearRelative(cf, parameterDomain.cfFixed, parameterDomain.cfRelTol); + if(!cfSupported) { + if(reason) { + *reason = QString("Cf=%1 is outside surrogate domain").arg(cf, 0, 'g', 10); + } + + return false; + } + } - bool cfSupported = isVariablePvt - ? isInClosedRange(cf, kSurrogateCfMin, kSurrogateCfMax) - : isNearRelative(cf, kSurrogateCfFixed, kSurrogateCfRelTol); - if(!cfSupported) { + double initialPressure = reservoirData.getInitialPressure().getValue().toDouble(); + + if(!isNearRelative(initialPressure, kSurrogateTrainingInitialPressure, 1.0e-4)) { if(reason) { - *reason = QString("Cf=%1 is outside surrogate domain").arg(cf, 0, 'g', 10); + *reason = QString("initialPressure=%1 does not match surrogate training context") + .arg(initialPressure, 0, 'g', 10); } return false; @@ -2078,49 +2286,56 @@ bool nmCalculationAutoFitPSO::isSurrogateCandidateInDomain(const QVector double h = params.size() > 5 ? params[5] : std::numeric_limits::quiet_NaN(); double cf = params.size() > 7 ? params[7] : std::numeric_limits::quiet_NaN(); - if(!isInClosedRange(k, kSurrogateKMin, kSurrogateKMax)) { + nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); + NM_SOLVER_MODEL_TYPE solverModelType = dataManager + ? dataManager->getSolverModelType() + : SMT_Oil_ConstPvt; + SurrogateParameterDomain parameterDomain; + + if(!getSurrogateParameterDomain(solverModelType, ¶meterDomain)) { + if(reason) *reason = QString("solverType=%1").arg(static_cast(solverModelType)); + + return false; + } + + if(!isInClosedRange(k, parameterDomain.kMin, parameterDomain.kMax)) { if(reason) *reason = QString("k=%1").arg(k, 0, 'g', 10); return false; } - if(!isInClosedRange(skin, kSurrogateSkinMin, kSurrogateSkinMax)) { + if(!isInClosedRange(skin, parameterDomain.skinMin, parameterDomain.skinMax)) { if(reason) *reason = QString("skin=%1").arg(skin, 0, 'g', 10); return false; } - if(!isInClosedRange(wellboreC, kSurrogateWellboreCMin, kSurrogateWellboreCMax)) { + if(!isInClosedRange(wellboreC, parameterDomain.wellboreCMin, parameterDomain.wellboreCMax)) { if(reason) *reason = QString("wellboreC=%1").arg(wellboreC, 0, 'g', 10); return false; } - if(!isInClosedRange(phi, kSurrogatePhiMin, kSurrogatePhiMax)) { + if(!isInClosedRange(phi, parameterDomain.phiMin, parameterDomain.phiMax)) { if(reason) *reason = QString("phi=%1").arg(phi, 0, 'g', 10); return false; } - if(!isInClosedRange(h, kSurrogateHMin, kSurrogateHMax)) { + if(!isInClosedRange(h, parameterDomain.hMin, parameterDomain.hMax)) { if(reason) *reason = QString("h=%1").arg(h, 0, 'g', 10); return false; } - nmDataAnalyzeManager* dataManager = nmDataAnalyzeManager::getCurrentInstance(); - NM_SOLVER_MODEL_TYPE solverModelType = dataManager - ? dataManager->getSolverModelType() - : SMT_Oil_ConstPvt; - bool isVariablePvt = solverModelType == SMT_Oil_VariablePvt || - solverModelType == SMT_Water_VariablePvt; - bool cfSupported = isVariablePvt - ? isInClosedRange(cf, kSurrogateCfMin, kSurrogateCfMax) - : isNearRelative(cf, kSurrogateCfFixed, kSurrogateCfRelTol); - if(!cfSupported) { - if(reason) *reason = QString("Cf=%1").arg(cf, 0, 'g', 10); + // 只有参与搜索的 T2/T4 Cf 需要逐粒子检查;T5 固定 Cf 已在运行入口确认。 + if(parameterDomain.checkCf && parameterDomain.variableCf) { + bool cfSupported = isInClosedRange(cf, parameterDomain.cfMin, parameterDomain.cfMax); + if(!cfSupported) { + if(reason) *reason = QString("Cf=%1").arg(cf, 0, 'g', 10); - return false; + return false; + } } return true; @@ -2230,6 +2445,21 @@ bool nmCalculationAutoFitPSO::isStrongDecliningProductionSchedule(double* endSta return ratio < kSurrogateStrongDeclineEndStartRatio; } +void nmCalculationAutoFitPSO::resetGuideBestToVerified() +{ + // 全量真实评价或代理降级时,丢弃尚未验证的引导位置。 + // 真实 bestPosition/bestFitness 始终是唯一可信的 pbest/gbest 数据源。 + for(int i = 0; i < m_swarm.size(); ++i) { + AutoFitParticle& particle = m_swarm[i]; + + if(particle.bestPosition.size() == particle.position.size()) { + particle.guideBestPosition = particle.bestPosition; + particle.guideBestObjective = particle.bestFitness; + particle.guideBestFromSurrogate = false; + } + } +} + QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() { // 返回值 evaluateMask 与 m_swarm 一一对应: @@ -2253,6 +2483,7 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() if(!isSurrogateScreeningEnabled()) { // 用户关闭 PSO acceleration:保持原始 PSO 行为,所有粒子都跑真实求解器。 + resetGuideBestToVerified(); return evaluateMask; } @@ -2267,6 +2498,7 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() m_swarm[i].screeningDecision = "context_gate_full_solver"; } + resetGuideBestToVerified(); emit logMessageGenerated(tr("Surrogate screening disabled for this run context: %1").arg(contextReason)); return evaluateMask; } @@ -2280,6 +2512,7 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() m_swarm[i].screeningDecision = "warmup_full_solver"; } + resetGuideBestToVerified(); return evaluateMask; } @@ -2293,6 +2526,7 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() m_swarm[i].screeningDecision = "periodic_full_solver"; } + resetGuideBestToVerified(); emit logMessageGenerated(tr("Surrogate audit iteration %1: running full solver for all particles") .arg(m_currentIteration + 1)); return evaluateMask; @@ -2314,7 +2548,11 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() QString scoringFailureReason; - if(!writeSurrogateCandidateCsv(candidatePath) || !runSurrogateScoringProcess(candidatePath, scorePath, &scoringFailureReason)) { + // 运行上下文已经通过 T1-T5 代理模型门控,所有支持相态都使用引导 pbest。 + const bool includeGuideCandidates = true; + + if(!writeSurrogateCandidateCsv(candidatePath, includeGuideCandidates) || + !runSurrogateScoringProcess(candidatePath, scorePath, &scoringFailureReason)) { // Python 环境、checkpoint、processed 数据或脚本异常时都走这里。 // 降级策略是全量真实求解器,保证代理模块故障不会中断 PSO 拟合。 emit logMessageGenerated(tr("Surrogate screening unavailable; falling back to full solver for iteration %1") @@ -2324,14 +2562,30 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() emit logMessageGenerated(tr("Surrogate scoring failure detail: %1").arg(scoringFailureReason)); } + resetGuideBestToVerified(); return evaluateMask; } - QVector scores = readSurrogateScores(scorePath); + int expectedScoreCount = m_swarm.size() * (includeGuideCandidates ? 2 : 1); + QVector allScores = readSurrogateScores(scorePath, expectedScoreCount); + QVector scores(m_swarm.size(), std::numeric_limits::quiet_NaN()); + QVector guideScores(m_swarm.size(), std::numeric_limits::quiet_NaN()); QVector finiteScoreIndices; // 只接受成功写出的有限代理目标函数。失败候选后面会通过 fallback 强制真实评价。 - for(int i = 0; i < scores.size(); ++i) { + for(int i = 0; i < m_swarm.size(); ++i) { + if(i < allScores.size()) { + scores[i] = allScores[i]; + } + + if(includeGuideCandidates && m_swarm.size() + i < allScores.size()) { + guideScores[i] = allScores[m_swarm.size() + i]; + + if(isFiniteNumber(guideScores[i])) { + m_swarm[i].guideBestObjective = guideScores[i]; + } + } + m_swarm[i].surrogateObjective = scores[i]; if(isFiniteNumber(scores[i])) { @@ -2341,6 +2595,7 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() if(finiteScoreIndices.isEmpty()) { emit logMessageGenerated(tr("Surrogate scoring returned no valid scores; falling back to full solver")); + resetGuideBestToVerified(); return evaluateMask; } @@ -2454,7 +2709,33 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() } } - emit logMessageGenerated(tr("Surrogate screening iteration %1: selected %2/%3 particles (keep=%4, audit=%5, min_solver=%6, fallback=%7, domain=%8)") + int guidePbestUpdateCount = 0; + + // 单纯按本代绝对误差 top-k 可能漏掉“当前还不够好、但正在显著改善自己 + // pbest”的探索粒子。对真正被筛掉的粒子,仅更新一套 + // 用于个体认知速度项的引导位置;真实 bestPosition、bestFitness 和 gbest 完全不变。 + if(includeGuideCandidates) { + for(int i = 0; i < m_swarm.size(); ++i) { + AutoFitParticle& particle = m_swarm[i]; + + if(evaluateMask[i] || !isFiniteNumber(scores[i]) || !isFiniteNumber(guideScores[i])) { + continue; + } + + double requiredImprovement = qMax(1.0e-10, + qAbs(guideScores[i]) * kSurrogateGuidePbestMinRelativeImprovement); + + if(guideScores[i] - scores[i] > requiredImprovement) { + particle.guideBestPosition = particle.position; + particle.guideBestObjective = scores[i]; + particle.guideBestFromSurrogate = true; + particle.screeningDecision = "screened_out_guide_pbest"; + guidePbestUpdateCount++; + } + } + } + + emit logMessageGenerated(tr("Surrogate screening iteration %1: selected %2/%3 particles (keep=%4, audit=%5, min_solver=%6, fallback=%7, domain=%8, guide_pbest=%9)") .arg(m_currentIteration + 1) .arg(selectedCount) .arg(m_swarm.size()) @@ -2462,7 +2743,8 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() .arg(getSurrogateAuditFraction(), 0, 'f', 2) .arg(getSurrogateMinSolverFraction(), 0, 'f', 2) .arg(fallbackCount) - .arg(domainFallbackCount)); + .arg(domainFallbackCount) + .arg(guidePbestUpdateCount)); m_surrogateActiveIterationCount++; @@ -2473,7 +2755,7 @@ QVector nmCalculationAutoFitPSO::buildSurrogateEvaluationMask() m_surrogateSelectedParticleCount++; } - if(decision == "screened_out") { + if(decision.startsWith("screened_out")) { m_surrogateScreenedParticleCount++; } else if(decision == "surrogate_topk") { m_surrogateTopKParticleCount++; @@ -2695,6 +2977,22 @@ bool nmCalculationAutoFitPSO::startAutoFitting() m_initialValues = savedInitialValues; initializeTraceFile(); + // 尽早启动代理评分进程,但不在这里等待模型加载完成。 + // Python 会与初始解及前两代真实求解并行预热;第一次需要评分时再等待就绪。 + if(isSurrogateScreeningEnabled()) { + QString contextReason; + + if(isSurrogateRunContextSupported(&contextReason)) { + QString prewarmFailureReason; + + if(ensureSurrogateScoringServer(&prewarmFailureReason, false)) { + emit logMessageGenerated(tr("Surrogate model prewarming started in background")); + } else if(!prewarmFailureReason.isEmpty()) { + emit logMessageGenerated(tr("Surrogate model prewarm deferred: %1").arg(prewarmFailureReason)); + } + } + } + if(!savedInitialValues.isEmpty()) { m_userInitialSolution = savedInitialValues; emit logMessageGenerated(tr("=== Evaluating Initial Solution (Elite Protection) ===")); @@ -2719,10 +3017,14 @@ bool nmCalculationAutoFitPSO::startAutoFitting() QTime initialEvalTimer; initialEvalTimer.start(); + // 初始精英解也是一次真实 DLL 调用。第一代第0号粒子会复用它, + // 因此在这里计数,保证汇总中的评价次数等于实际求解调用数。 + m_totalEvaluations++; m_userInitialFitness = evaluateFitness(m_userInitialSolution); int initialEvalElapsedMs = initialEvalTimer.elapsed(); if(m_userInitialFitness < 1e9) { + m_successfulEvaluations++; m_hasValidUserSolution = true; m_globalBestFitness = m_userInitialFitness; m_globalBestPosition = m_userInitialSolution; @@ -2813,6 +3115,7 @@ bool nmCalculationAutoFitPSO::startAutoFitting() particle.evaluatedThisIteration = false; particle.lastEvaluationSuccess = false; particle.lastEvaluationElapsedMs = -1; + particle.pbestRelativeImprovementThisIteration = 0.0; particle.selectedForSolver = false; if(particle.screeningDecision.isEmpty()) { @@ -3230,7 +3533,11 @@ void nmCalculationAutoFitPSO::initializeSwarm() particle.position.resize(dimensions); particle.velocity.resize(dimensions); particle.bestPosition.resize(dimensions); + particle.guideBestPosition.resize(dimensions); particle.bestFitness = 1e10; + particle.guideBestObjective = 1e10; + particle.guideBestFromSurrogate = false; + particle.pbestRelativeImprovementThisIteration = 0.0; particle.fitness = 1e10; particle.evaluatedThisIteration = false; particle.lastEvaluationSuccess = false; @@ -3291,6 +3598,7 @@ void nmCalculationAutoFitPSO::initializeSwarm() } particle.bestPosition = particle.position; + particle.guideBestPosition = particle.position; } } @@ -3306,31 +3614,89 @@ void nmCalculationAutoFitPSO::updateParticle(int particleIndex) particle.evaluatedThisIteration = false; particle.lastEvaluationSuccess = false; particle.lastEvaluationElapsedMs = -1; + particle.pbestRelativeImprovementThisIteration = 0.0; particle.selectedForSolver = true; if(particle.screeningDecision.isEmpty()) { particle.screeningDecision = "full_solver"; } - QTime evalTimer; - evalTimer.start(); - particle.fitness = evaluateFitness(particle.position); - particle.currentLogLogData = m_lastEvaluatedLogLogData; - particle.lastEvaluationElapsedMs = evalTimer.elapsed(); - particle.evaluatedThisIteration = true; - particle.lastEvaluationSuccess = (particle.fitness < 1e9); - m_totalEvaluations++; + bool reuseInitialSolution = m_currentIteration == 0 && + particleIndex == 0 && + m_hasValidUserSolution && + m_userInitialFitness < 1e9 && + particle.position.size() == m_userInitialSolution.size(); - if(particle.fitness < 1e9) { - m_successfulEvaluations++; + for(int i = 0; reuseInitialSolution && i < particle.position.size(); ++i) { + double tolerance = qMax(1.0e-12, qAbs(m_userInitialSolution[i]) * 1.0e-12); + reuseInitialSolution = qAbs(particle.position[i] - m_userInitialSolution[i]) <= tolerance; + } + + if(reuseInitialSolution) { + // 初始解在进入粒子群前已经真实求解过。第一代第0号粒子位置完全相同, + // 直接复用真实误差和曲线,避免一次重复 DLL 调用且不改变 PSO 数学状态。 + particle.fitness = m_userInitialFitness; + particle.currentLogLogData = m_userInitialLogLogData; + particle.lastEvaluationElapsedMs = 0; + particle.evaluatedThisIteration = true; + particle.lastEvaluationSuccess = true; + particle.screeningDecision = "initial_solution_cache"; + emit logMessageGenerated(tr("Particle 1 reused the verified initial solution")); + } else { + QTime evalTimer; + evalTimer.start(); + particle.fitness = evaluateFitness(particle.position); + particle.currentLogLogData = m_lastEvaluatedLogLogData; + particle.lastEvaluationElapsedMs = evalTimer.elapsed(); + particle.evaluatedThisIteration = true; + particle.lastEvaluationSuccess = (particle.fitness < 1e9); + m_totalEvaluations++; + + if(particle.fitness < 1e9) { + m_successfulEvaluations++; + } } // 更新个体最优 pbest。这里使用的是真实求解器误差 particle.fitness, // 不是代理模型给出的 surrogateObjective。 - if(particle.fitness < particle.bestFitness) { + double previousBestFitness = particle.bestFitness; + + if(particle.fitness < previousBestFitness) { + particle.pbestRelativeImprovementThisIteration = previousBestFitness >= 1e9 + ? 1.0 + : (previousBestFitness - particle.fitness) / + qMax(1e-10, qAbs(previousBestFitness)); + + // 如果当前位置尚未成为新的真实全局最优,而上一代存在尚未真实验证、且代理 + // 仍判断更优的 guide,就保留它继续引导速度;真实 pbest 仍照常更新。 + bool preserveSurrogateGuide = false; + + bool currentBeatsGlobalBest = particle.fitness < m_globalBestFitness; + + if(!currentBeatsGlobalBest && + isSurrogateScreeningEnabled() && + particle.guideBestFromSurrogate && + isFiniteNumber(particle.guideBestObjective) && + isFiniteNumber(particle.surrogateObjective)) { + double requiredImprovement = qMax(1.0e-10, + qAbs(particle.guideBestObjective) * + kSurrogateGuidePbestMinRelativeImprovement); + preserveSurrogateGuide = particle.surrogateObjective - + particle.guideBestObjective > requiredImprovement; + } + particle.bestFitness = particle.fitness; particle.bestPosition = particle.position; particle.bestLogLogData = particle.currentLogLogData; + + if(!preserveSurrogateGuide) { + particle.guideBestPosition = particle.position; + particle.guideBestObjective = isFiniteNumber(particle.surrogateObjective) + ? particle.surrogateObjective + : particle.fitness; + particle.guideBestFromSurrogate = false; + } + DEBUG_OUT(QString("Particle %1 improved: error = %2") .arg(particleIndex).arg(particle.fitness, 0, 'e', 4)); } @@ -3440,8 +3806,14 @@ void nmCalculationAutoFitPSO::updateGlobalBest() void nmCalculationAutoFitPSO::updateVelocityAndPosition() { + bool useSurrogateGuidePbest = isSurrogateScreeningEnabled(); + for(int i = 0; i < m_swarm.size(); ++i) { AutoFitParticle& particle = m_swarm[i]; + const QVector& cognitiveBest = useSurrogateGuidePbest && + particle.guideBestPosition.size() == particle.position.size() + ? particle.guideBestPosition + : particle.bestPosition; for(int j = 0; j < particle.position.size(); ++j) { // PSO速度更新公式: @@ -3452,7 +3824,7 @@ void nmCalculationAutoFitPSO::updateVelocityAndPosition() double r2 = random01(); particle.velocity[j] = m_inertiaWeight * particle.velocity[j] + - m_cognitiveParam * r1 * (particle.bestPosition[j] - particle.position[j]) + + m_cognitiveParam * r1 * (cognitiveBest[j] - particle.position[j]) + m_socialParam * r2 * (m_globalBestPosition[j] - particle.position[j]); // 速度限制。不同参数量纲差异很大,所以按该参数搜索区间的一定比例限速。 @@ -4853,26 +5225,26 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() dllTask->start(); // 等待完成。最大等待 1 小时,适配大模型慢算;用户停止时会 terminate。 - int waitTime = 0; const int maxWait = 3600000; // 1h超时 - const int checkInterval = 500; - - while(waitTime < maxWait) { - QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 100); - - if(!dllTask->isRunning()) { + const int checkInterval = 50; + QTime waitTimer; + waitTimer.start(); + + while(waitTimer.elapsed() < maxWait) { + // wait(timeout) 会在线程一完成时立即返回,避免原来固定 msleep(500) + // 带来的每次 0~500ms 额外等待;50ms 间隔仍可及时处理停止请求和界面事件。 + if(dllTask->wait(checkInterval)) { DEBUG_OUT("DLL solver task completed"); break; } + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, checkInterval); + if(m_shouldStop) { DEBUG_OUT("DLL solver task terminated by user"); dllTask->terminate(); break; } - - msleep(checkInterval); - waitTime += checkInterval; } // 超时处理 @@ -5371,23 +5743,30 @@ double nmCalculationAutoFitPSO::calculateAverageVelocity() const double nmCalculationAutoFitPSO::calculateParticleStagnationRate() const { - // 粒子停滞率:当前 fitness 与 pbest 很接近时认为该粒子停滞。 - // 该指标用于辅助判断真收敛或局部最优。 + // 粒子停滞率:只统计本代经过真实求解器评价的粒子。 + // 被代理筛掉的粒子保留的是旧适应度,不能据此判断本代是否停滞。 if(m_swarm.isEmpty()) return 0.0; int stagnantParticles = 0; + int evaluatedParticles = 0; for(int i = 0; i < m_swarm.size(); ++i) { - // 如果当前适应度与个体最优非常接近,认为该粒子停滞 - double improvement = (m_swarm[i].bestFitness - m_swarm[i].fitness) / - qMax(1e-10, qAbs(m_swarm[i].bestFitness)); + if(!m_swarm[i].evaluatedThisIteration || !m_swarm[i].lastEvaluationSuccess) { + continue; + } + + evaluatedParticles++; - if(qAbs(improvement) < 1e-6) { // 改进小于0.0001% + // updateParticle() 会在计算该字段后才覆盖 pbest,因此“刚改善的粒子” + // 不会再被错误地当作停滞。 + if(m_swarm[i].pbestRelativeImprovementThisIteration <= 1e-6) { // 改进不超过0.0001% stagnantParticles++; } } - return static_cast(stagnantParticles) / m_swarm.size(); + return evaluatedParticles > 0 + ? static_cast(stagnantParticles) / evaluatedParticles + : 0.0; } double nmCalculationAutoFitPSO::calculateFitnessVariance(int windowSize) const