diff --git a/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h b/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h index 501434f..edc0897 100644 --- a/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h +++ b/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h @@ -4,14 +4,19 @@ #include #include #include +#include +#include #include #include #include +#include #include "nmCalculationDefine.h" #include "nmCalculation_global.h" #include "pch.h" class nmDataAnalyzeManager; +class vtkDoubleArray; +class vtkUnstructuredGrid; // 主窗口现在直接创建DLL求解线程任务,因此类需要导出,供nmSubWnd模块跨DLL使用。 class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { @@ -38,6 +43,13 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { /** @brief 获取本次求解的PEBI网格数量。 */ int getPebiCount() const; + /** + * @brief 在主线程一次性提交已经完整校验的求解结果。 + * @param pDataManager 启动任务时绑定的数据管理器,必须仍然存活且版本一致。 + * @return 全部结果提交成功返回 true;自动拟合任务和过期任务返回 false。 + */ + bool commitResult(nmDataAnalyzeManager* pDataManager); + /** * @brief 按显示井名设置自动拟合目标井,内部立即转换并保存 WellCode。 * @note 井名为空或查找失败时保持完整结果保存模式。 @@ -53,13 +65,15 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { private: /** @brief 执行当前唯一的 PEBI 求解模式。 */ bool execute(); + /** @brief 成对释放构造时登记的 DataManager 后台使用权。 */ + void releaseDataManagerUse(); - /** @brief 构造 DLL 输入、执行求解并提交仍然有效的结果。 */ + /** @brief 构造 DLL 输入、执行求解并生成仍然有效的局部结果快照。 */ bool execPebiMode(); - /** @brief 按求解器井顺序将压力、双对数和半对数结果映射回井对象。 */ - bool savePebiModeResult(HX_NWTM_MODEL_OUTPUT& p1, - int nModelType, - const HX_NWTM_GRID_OUTPUT1& oGridOutput); + /** @brief 按求解器井顺序组装压力、曲线和场结果快照,不修改 DataManager。 */ + bool buildPebiModeResult(HX_NWTM_MODEL_OUTPUT& p1, + int nModelType, + const HX_NWTM_GRID_OUTPUT1& oGridOutput); /** @brief 输出精简版 DLL 输入日志,用于定位求解异常。 */ void logHX_NWTM_MODEL_INPUT_Simplified(const HX_NWTM_MODEL_INPUT& p0); @@ -67,9 +81,20 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { bool saveHX_NWTM_MODEL_INPUT_ToTxt(const HX_NWTM_MODEL_INPUT& p0, const QString& filePath); private: + /** @brief 一口真实井在本次求解中完整生成的待提交结果。 */ + struct nmPebiWellResultSnapshot + { + QString m_sWellCode; ///< 稳定井编码。 + QVector > m_vecPressure; ///< 时间和井底压力。 + QVector > m_vecLogLog; ///< 时间、压差及导数。 + QVector > m_vecSemiLog; ///< 时间和半对数压力。 + QPointF m_oLocation; ///< 结果网格中的井坐标。 + }; + QString m_sPostprocessingDir; ///< 本次求解使用的后处理输出目录。 /** @brief 创建任务时捕获所属数据中心,工作线程不得读取全局当前窗口。 */ nmDataAnalyzeManager* m_pDataManager; + bool m_bManagerUseActive; ///< 是否仍持有 DataManager 后台使用权。 /** @brief 创建任务时捕获网格输入版本,防止回填过期网格上的结果。 */ quint64 m_nGridInputRevision; /** @brief 创建任务时捕获求解输入版本,防止角色或参数变化后回填旧结果。 */ @@ -83,6 +108,12 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { QVector > m_autoFitResultPressure; ///< 目标井原始压力结果。 QVector > m_autoFitResultLogLog; ///< 目标井双对数结果。 QVector > m_autoFitResultSemiLog; ///< 目标井半对数结果。 + QVector m_vecPendingWellResults; ///< 完整求解的井结果快照。 + QMap > m_mapPendingTimeSteps; ///< 场压力时间步快照。 + vtkSmartPointer m_pPendingResultGrid; ///< 本次结果绑定的基础网格副本。 + double m_dPendingScalarMin; ///< 全部场压力的最小值。 + double m_dPendingScalarMax; ///< 全部场压力的最大值。 + bool m_bPendingFullResultReady; ///< 是否已有可由主线程一次提交的完整结果。 private slots: //void slotTaskUpdateProgress(); diff --git a/Include/nmNum/nmCalculation/nmCalculationPebiGridTask.h b/Include/nmNum/nmCalculation/nmCalculationPebiGridTask.h index 6fc14a7..140688d 100644 --- a/Include/nmNum/nmCalculation/nmCalculationPebiGridTask.h +++ b/Include/nmNum/nmCalculation/nmCalculationPebiGridTask.h @@ -15,6 +15,8 @@ public: /** @brief 创建绑定到指定分析窗口的后台网格生成任务。 */ explicit nmCalculationPebiGridTask(nmDataAnalyzeManager* pDataManager, QObject *parent = 0); + /** @brief 等待线程结束,并释放 DataManager 后台使用权。 */ + ~nmCalculationPebiGridTask(); signals: /** @brief 无论成功或失败均通知主线程结束本轮网格任务。 */ @@ -25,8 +27,12 @@ protected: void run() override; private: + /** @brief 成对释放构造时登记的 DataManager 后台使用权。 */ + void releaseDataManagerUse(); + /** @brief 启动任务时捕获所属数据管理器,线程内不再读取全局当前窗口。 */ nmDataAnalyzeManager* m_pDataManager; + bool m_bManagerUseActive; ///< 是否仍持有 DataManager 后台使用权。 }; #endif // NMCALCULATIONPEBIGRIDTASK_H diff --git a/Include/nmNum/nmData/nmDataAnalyzeManager.h b/Include/nmNum/nmData/nmDataAnalyzeManager.h index a0f3469..134a028 100644 --- a/Include/nmNum/nmData/nmDataAnalyzeManager.h +++ b/Include/nmNum/nmData/nmDataAnalyzeManager.h @@ -20,6 +20,8 @@ #include #include #include +#include +#include #include #include @@ -447,6 +449,11 @@ public: nmDataNumericalAnalysisCase* getNumericalAnalysisCase(); /** @brief 返回只读数值分析方案,管理器保留对象所有权。 */ const nmDataNumericalAnalysisCase* getNumericalAnalysisCase() const; + + /** @brief 后台任务开始使用本管理器;与 endBackgroundUse() 必须成对调用。 */ + void beginBackgroundUse(); + /** @brief 后台任务结束使用本管理器;最后一个任务退出时唤醒析构等待。 */ + void endBackgroundUse(); /** @brief 设置主分析井 WellCode;空编码用于清空主井。 */ void setPrimaryWellCode(const QString& sWellCode); /** @brief 返回当前主分析井 WellCode。 */ @@ -730,6 +737,14 @@ public: void sigGeometryListChanged(); private: + /** @brief 创建本数据管理器的成果窗口;只用于读取本成果上下文,不拥有对象。 */ + iSubWndFitting* m_pOwnerFitting; + /** @brief 保护后台使用计数和析构等待条件。 */ + QMutex m_oBackgroundUseMutex; + /** @brief DataManager 析构时等待全部后台任务停止访问。 */ + QWaitCondition m_oNoBackgroundUseCondition; + int m_nBackgroundUseCount; ///< 当前持有 DataManager 使用权的后台任务数量。 + // 存储井的数据 QVector m_vWellData; diff --git a/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp b/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp index 31baf56..36e4bad 100644 --- a/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp @@ -62,10 +62,10 @@ 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; // 代理模型训练域:渗透率上界。 +// 数据生成范围保持一致。PSO 中渗透率使用 mD,因此将训练配置中的 Darcy 范围 +// 乘以 1000 后再门控;超出范围时强制退回真实求解器。 +static const double kSurrogateKMin = 1.0; // 代理模型训练域:渗透率下界,单位 mD。 +static const double kSurrogateKMax = 10000.0; // 代理模型训练域:渗透率上界,单位 mD。 static const double kSurrogateSkinMin = -10.0; // 代理模型训练域:skin 下界。 static const double kSurrogateSkinMax = 10.0; // 代理模型训练域:skin 上界。 static const double kSurrogateWellboreCMin = 1.0e-4; // 代理模型训练域:井筒储集下界。 @@ -203,8 +203,8 @@ static bool getSurrogateParameterDomain(NM_SOLVER_MODEL_TYPE solverModelType, Su return true; case SMT_Gas_VariablePvt: - domain->kMin = 4.5e-6; - domain->kMax = 6.2e-6; + domain->kMin = 4.5e-3; + domain->kMax = 6.2e-3; domain->skinMin = 0.0; domain->skinMax = 4.5; domain->wellboreCMin = 5.0e-5; @@ -6577,15 +6577,17 @@ double nmCalculationAutoFitPSO::calculateLogLogCurveError( // 无法比较的采样行先跳过;有限但非正的导数无法进入双对数空间, // 当前数据又没有逐点有效掩码,因此遇到这种导数时判本次评价无效。 auto prepareCurve = [valueFloor](const QVector >& data, + int firstIndex, QVector* pressure, QVector* derivative) -> bool { if(!pressure || !derivative || data.size() < 3 || data[0].size() != data[1].size() || - data[0].size() != data[2].size()) { + data[0].size() != data[2].size() || + firstIndex < 0 || firstIndex >= data[0].size()) { return false; } - for(int i = 0; i < data[0].size(); ++i) { + for(int i = firstIndex; i < data[0].size(); ++i) { if(!isFiniteNumber(data[0][i]) || !isFiniteNumber(data[1][i]) || !isFiniteNumber(data[2][i]) || @@ -6633,8 +6635,9 @@ double nmCalculationAutoFitPSO::calculateLogLogCurveError( QVector targetDerivative; QVector resultPressure; QVector resultDerivative; - if(!prepareCurve(target, &targetPressure, &targetDerivative) || - !prepareCurve(result, &resultPressure, &resultDerivative)) { + // 模拟结果生成时已经跳过 DLL 首点,因此目标曲线也从第二点开始比较。 + if(!prepareCurve(target, 1, &targetPressure, &targetDerivative) || + !prepareCurve(result, 0, &resultPressure, &resultDerivative)) { return invalidLoss; } @@ -6730,7 +6733,14 @@ double nmCalculationAutoFitPSO::calculateLogLogCurveError( (targetLogMaxX - targetLogMinX) / (numPoints - 1); commonLogX[i] = logX; - commonX[i] = qExp(logX); + // 首尾直接使用原始端点,避免 exp(log(t)) 的舍入误差越过严格插值边界。 + if(i == 0) { + commonX[i] = targetMinX; + } else if(i == numPoints - 1) { + commonX[i] = targetMaxX; + } else { + commonX[i] = qExp(logX); + } if(!interpolateLogValue( targetPressure, commonX[i], @@ -7026,10 +7036,13 @@ double nmCalculationAutoFitPSO::calculateLogLogCurveError( return false; } - double shiftedX = qExp( - qBound(resultLogMinX, - shiftedLogX, - resultLogMaxX)); + // 从对数时间还原后再限制到原始端点,避免舍入误差越过插值边界。 + double shiftedX = qBound( + resultMinX, + qExp(qBound(resultLogMinX, + shiftedLogX, + resultLogMaxX)), + resultMaxX); double resultLogPressure = 0.0; double resultLogDerivative = 0.0; if(!interpolateLogValue( diff --git a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp index 02c56f1..18eb13d 100644 --- a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp @@ -21,6 +21,7 @@ #include "nmDataPvtParaForPebi.h" #include +#include #include #include #include @@ -317,13 +318,23 @@ nmCalculationDllPebiSolverTask::nmCalculationDllPebiSolverTask( m_pDataManager(pDataManager != nullptr ? pDataManager : nmDataAnalyzeManager::getCurrentInstance()), + m_bManagerUseActive(false), m_nGridInputRevision(0), m_nResultInputRevision(0), m_lastRunSucceeded(false), m_nSolveTimeMs(-1), - m_nPebiCount(-1) + m_nPebiCount(-1), + m_dPendingScalarMin(0.0), + m_dPendingScalarMax(0.0), + m_bPendingFullResultReady(false) { - // 任务创建时冻结两个输入版本;非模态求解期间界面仍可编辑数据。 + // 第一步:构造后到 run() 结束前阻止所属成果提前释放 DataManager。 + if(m_pDataManager != nullptr) { + m_pDataManager->beginBackgroundUse(); + m_bManagerUseActive = true; + } + + // 第二步:任务创建时冻结两个输入版本,提交前还会再次核对。 if(m_pDataManager != nullptr && m_pDataManager->getNumericalAnalysisCase() != nullptr) { m_nGridInputRevision = @@ -335,10 +346,12 @@ nmCalculationDllPebiSolverTask::nmCalculationDllPebiSolverTask( nmCalculationDllPebiSolverTask::~nmCalculationDllPebiSolverTask() { + // Qt 4.8 没有可靠的外部 DLL 协作取消接口。强制 terminate() 可能让 DLL、 + // VTK 或 STL 对象停在持锁/半析构状态,因此析构只能等待 run() 正常退出。 if(isRunning()) { - terminate(); - wait(3000); + wait(); } + releaseDataManagerUse(); } void nmCalculationDllPebiSolverTask::run() @@ -346,9 +359,19 @@ void nmCalculationDllPebiSolverTask::run() m_nSolveTimeMs = -1; m_nPebiCount = -1; m_lastRunSucceeded = this->execute(); + // 完成信号可能触发成果关闭;必须在发信号前结束 DataManager 使用期。 + releaseDataManagerUse(); emit sig_calculateDone(m_lastRunSucceeded); } +void nmCalculationDllPebiSolverTask::releaseDataManagerUse() +{ + if(m_bManagerUseActive && m_pDataManager != nullptr) { + m_pDataManager->endBackgroundUse(); + m_bManagerUseActive = false; + } +} + bool nmCalculationDllPebiSolverTask::wasSuccessful() const { return m_lastRunSucceeded; @@ -409,10 +432,12 @@ bool nmCalculationDllPebiSolverTask::execPebiMode() pAnalysisCase->getResultInputRevision() != m_nResultInputRevision) { return false; } - if(m_sAutoFitTargetWellCode.isEmpty()) { - // 完整重算开始后立即废弃上一轮有效标记;失败任务不能继续保存旧结果。 - pDataInstance->discardPebiResults(); - } + // 旧结果在新结果完整生成并通过主线程版本校验前保持不变。失败任务只丢弃 + // 自己的局部快照,不能提前清空上一轮仍然完整的成果。 + m_bPendingFullResultReady = false; + m_vecPendingWellResults.clear(); + m_mapPendingTimeSteps.clear(); + m_pPendingResultGrid = nullptr; // 载入Pebi动态库 HMODULE dll = LoadLibrary(L"HX_NWTM.dll"); @@ -849,10 +874,10 @@ bool nmCalculationDllPebiSolverTask::execPebiMode() return false; } - // 保存计算结果 - bool success = this->savePebiModeResult(p1, - modelType, - oGridOutput1); + // 只构造任务局部结果;完整求解由主线程在完成回调中一次性提交。 + bool success = this->buildPebiModeResult(p1, + modelType, + oGridOutput1); FreeLibrary(dll); // 卸载 DLL return success; @@ -892,7 +917,7 @@ std::vector HX_logderivative(const std::vector& x, const std::ve return d; } -bool nmCalculationDllPebiSolverTask::savePebiModeResult( +bool nmCalculationDllPebiSolverTask::buildPebiModeResult( HX_NWTM_MODEL_OUTPUT& p1, int modelType, const HX_NWTM_GRID_OUTPUT1& oGridOutput) @@ -964,11 +989,6 @@ bool nmCalculationDllPebiSolverTask::savePebiModeResult( } } - // 粒子评价不修改全局井位置,完整求解仍按原流程重建映射. - if(!autoFitTargetOnly) { - pDataInstance->clearWellLocations(); - } - // 遍历每口井,处理其数据 for(int wellIdx = 0; wellIdx < vecWellsOrder.size(); ++wellIdx) { const nmSolverWellRef& oWellRef = vecWellsOrder[wellIdx]; @@ -1039,7 +1059,8 @@ bool nmCalculationDllPebiSolverTask::savePebiModeResult( // 准备流量段数据 QVector vecTimeQ = pWellData->getFlowPoints(); - int nTimeNumQ = vecTimeQ.size() - 1; // 移除第一个 0 点 + // 防御空流量数据,避免 -1 转成 std::vector 的巨大无符号长度。 + int nTimeNumQ = qMax(0, vecTimeQ.size() - 1); // 移除第一个 0 点 std::vector timeQ(nTimeNumQ); std::vector q(nTimeNumQ); @@ -1052,9 +1073,22 @@ bool nmCalculationDllPebiSolverTask::savePebiModeResult( std::vector logPreResultFromDll; // 存储 DLL 的计算结果 int iSectionFlowIndex = pWellData->getIndexF(); - HMODULE hMod_solver = LoadLibrary(L"singlePhaseSolverDll.dll"); - - if(hMod_solver) { + // 第一步:无产量观察井只接收压力结果,不调用依赖产量制度的曲线 DLL。 + HMODULE hMod_solver = nTimeNumQ > 0 + ? LoadLibrary(L"singlePhaseSolverDll.dll") : nullptr; + + if(nTimeNumQ <= 0) { + // 第二步:保持固定的数据列结构,便于结果保存和后续读取; + // 各列为空明确表示该观察井没有可展示的双对数、半对数结果。 + vvecLogLog.clear(); + vvecLogLog.append(QVector()); + vvecLogLog.append(QVector()); + vvecLogLog.append(QVector()); + + vvecSemiLog.clear(); + vvecSemiLog.append(QVector()); + vvecSemiLog.append(QVector()); + } else if(hMod_solver) { typedef bool (*PreLog)(const std::vector&, const int&, double*, double*, int, std::vector&); PreLog preLogFun = (PreLog)GetProcAddress(hMod_solver, "logLogPre"); @@ -1135,14 +1169,17 @@ bool nmCalculationDllPebiSolverTask::savePebiModeResult( autoFitTargetFound = true; break; } else { - // 所有真实井都保存压力;观察井的双对数和半对数保持为空。 - pWellData->setResultPressure(vvecPressure); - pWellData->setResultLogLog(vvecLogLog); - pWellData->setResultSemiLog(vvecSemiLog); - - QPointF ptWellCoords(pWellData->getX().getValue().toDouble(), pWellData->getY().getValue().toDouble()); - pDataInstance->addWellLocation(oWellRef.m_sWellCode, - ptWellCoords); + // 所有真实井先写入任务局部快照;观察井的双对数和半对数保持为空。 + // 任意后续步骤失败时,整个快照会被丢弃,旧成果不会被局部覆盖。 + nmPebiWellResultSnapshot oWellResult; + oWellResult.m_sWellCode = oWellRef.m_sWellCode; + oWellResult.m_vecPressure = vvecPressure; + oWellResult.m_vecLogLog = vvecLogLog; + oWellResult.m_vecSemiLog = vvecSemiLog; + oWellResult.m_oLocation = QPointF( + pWellData->getX().getValue().toDouble(), + pWellData->getY().getValue().toDouble()); + m_vecPendingWellResults.append(oWellResult); } } @@ -1195,14 +1232,13 @@ bool nmCalculationDllPebiSolverTask::savePebiModeResult( } } - // 新结果提交前清空上一轮时间键,防止较短的新时间轴残留旧帧。 - pDataInstance->clearTimeSteps(); - - // 在保存时间步数据前,先计算标量范围 + // 第五步:在局部时间步映射中构造场压力,同时计算全局标量范围。 + // 此处不触碰 DataManager,较短的新时间轴会在主线程整体替换时自然清除旧帧。 + m_mapPendingTimeSteps.clear(); double dMinP = DBL_MAX; double dMaxP = -DBL_MAX; - // 4. 为每个时间步保存压力场到 nmDataAnalyzeManager 的 QMap 中 + // 为每个时间步生成完整的 VTK 压力数组。 for(size_t timeIdx = 0; timeIdx < p1.p.size(); ++timeIdx) { // 获取当前时间步的时间值 double currentTime = p1.t[timeIdx]; @@ -1235,29 +1271,113 @@ bool nmCalculationDllPebiSolverTask::savePebiModeResult( dMinP = qMin(dMinP, dCurrentTimeMin); dMaxP = qMax(dMaxP, dCurrentTimeMax); - // 将时间与对应的压力数据存储到 nmDataAnalyzeManager - pDataInstance->addTimeStep(currentTime, pressureData); + m_mapPendingTimeSteps.insert(currentTime, pressureData); } - // 保存全局范围到管理器 - pDataInstance->setScalarRangeP(dMinP, dMaxP); - if(pDataInstance->getTimeStepKeys().size() != - static_cast(p1.t.size())) { + // QMap 使用时间作为唯一键;重复时间会覆盖旧帧,因此必须检查数量一致。 + if(m_mapPendingTimeSteps.size() != static_cast(p1.t.size())) { return false; } - vtkSmartPointer pResultGrid = + // 第六步:复制与本次场数据对应的基础网格,并在最后才发布“快照就绪”。 + m_pPendingResultGrid = pDataInstance->getUnstructuredGridCopy(); - if(pResultGrid == nullptr || pResultGrid->GetNumberOfCells() <= 0) { + if(m_pPendingResultGrid == nullptr || + m_pPendingResultGrid->GetNumberOfCells() <= 0) { + return false; + } + + int nExpectedWellResultCount = 0; + for(int nIndex = 0; nIndex < vecWellsOrder.size(); ++nIndex) { + if(vecWellsOrder[nIndex].m_eEntryKind == NM_SolverEntry_Well) { + ++nExpectedWellResultCount; + } + } + if(m_vecPendingWellResults.size() != nExpectedWellResultCount) { + return false; + } + + m_dPendingScalarMin = dMinP; + m_dPendingScalarMax = dMaxP; + m_bPendingFullResultReady = true; + return true; +} + +bool nmCalculationDllPebiSolverTask::commitResult( + nmDataAnalyzeManager* pDataManager) +{ + // 第一步:自动拟合只返回目标曲线,不允许提交完整成果;手工求解必须回到 + // DataManager 所属线程执行,保证界面看不到逐项替换过程中的中间状态。 + if(!m_sAutoFitTargetWellCode.isEmpty() || + !m_bPendingFullResultReady || + pDataManager == nullptr || + pDataManager != m_pDataManager || + QThread::currentThread() != pDataManager->thread()) { + return false; + } + + nmDataNumericalAnalysisCase* pAnalysisCase = + pDataManager->getNumericalAnalysisCase(); + if(pAnalysisCase == nullptr || + pAnalysisCase->getGridInputRevision() != m_nGridInputRevision || + pAnalysisCase->getResultInputRevision() != m_nResultInputRevision || + !pAnalysisCase->isGridValid() || + m_mapPendingTimeSteps.isEmpty() || + m_pPendingResultGrid == nullptr || + m_pPendingResultGrid->GetNumberOfCells() <= 0) { return false; } - pDataInstance->setResultBaseGrid(pResultGrid); - if(!pDataInstance->getNumericalAnalysisCase()->markResultsAvailableIfCurrent( + // 第二步:在改动旧成果前一次性解析全部目标井,并检查 WellCode 不重复。 + QVector vecTargetWells; + QSet setWellCodes; + vecTargetWells.reserve(m_vecPendingWellResults.size()); + for(int nIndex = 0; nIndex < m_vecPendingWellResults.size(); ++nIndex) { + const nmPebiWellResultSnapshot& oWellResult = + m_vecPendingWellResults[nIndex]; + nmDataWellBase* pWellData = + pDataManager->findWellByCode(oWellResult.m_sWellCode); + if(pWellData == nullptr || + setWellCodes.contains(oWellResult.m_sWellCode)) { + return false; + } + setWellCodes.insert(oWellResult.m_sWellCode); + vecTargetWells.append(pWellData); + } + + // 第三步:最后一次登记版本。登记失败时旧成果仍未被修改;登记成功后当前 + // 主线程事件不会被其他编辑操作插入,因此后续替换不存在可恢复失败分支。 + if(!pAnalysisCase->markResultsAvailableIfCurrent( m_nGridInputRevision, m_nResultInputRevision)) { return false; } + // 第四步:版本检查和对象解析全部通过后,在当前主线程事件内整体替换。 + // 这些 setter 不发事件也不包含可恢复失败分支,外部只能在本函数返回后看到新成果。 + pDataManager->clearWellLocations(); + for(int nIndex = 0; nIndex < m_vecPendingWellResults.size(); ++nIndex) { + const nmPebiWellResultSnapshot& oWellResult = + m_vecPendingWellResults[nIndex]; + nmDataWellBase* pWellData = vecTargetWells[nIndex]; + pWellData->setResultPressure(oWellResult.m_vecPressure); + pWellData->setResultLogLog(oWellResult.m_vecLogLog); + pWellData->setResultSemiLog(oWellResult.m_vecSemiLog); + pDataManager->addWellLocation(oWellResult.m_sWellCode, + oWellResult.m_oLocation); + } + + pDataManager->clearTimeSteps(); + QMap >::const_iterator oTimeIt = + m_mapPendingTimeSteps.constBegin(); + for(; oTimeIt != m_mapPendingTimeSteps.constEnd(); ++oTimeIt) { + pDataManager->addTimeStep(oTimeIt.key(), oTimeIt.value()); + } + pDataManager->setScalarRangeP(m_dPendingScalarMin, + m_dPendingScalarMax); + pDataManager->setResultBaseGrid(m_pPendingResultGrid); + + // 第五步:清除待提交标志,防止重复完成信号再次覆盖后续结果。 + m_bPendingFullResultReady = false; return true; } diff --git a/Src/nmNum/nmCalculation/nmCalculationPebiGridTask.cpp b/Src/nmNum/nmCalculation/nmCalculationPebiGridTask.cpp index f718cf3..a2e06af 100644 --- a/Src/nmNum/nmCalculation/nmCalculationPebiGridTask.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationPebiGridTask.cpp @@ -1,13 +1,27 @@ #include "nmCalculationPebiGridTask.h" #include "nmCalculationPebiGrid.h" +#include "nmDataAnalyzeManager.h" nmCalculationPebiGridTask::nmCalculationPebiGridTask( nmDataAnalyzeManager* pDataManager, QObject *parent) : QThread(parent), - m_pDataManager(pDataManager) + m_pDataManager(pDataManager), + m_bManagerUseActive(false) { + if(m_pDataManager != nullptr) { + m_pDataManager->beginBackgroundUse(); + m_bManagerUseActive = true; + } +} + +nmCalculationPebiGridTask::~nmCalculationPebiGridTask() +{ + if(isRunning()) { + wait(); + } + releaseDataManagerUse(); } void nmCalculationPebiGridTask::run() @@ -16,6 +30,16 @@ void nmCalculationPebiGridTask::run() // 生成网格. bool success = nmCalculationPebiGrid::getInstance()->meshGenPebi( m_pDataManager); + // 完成通知可能引起成果窗口销毁,发信号前先解除 DataManager 使用权。 + releaseDataManagerUse(); // 无论成功或失败都通知窗口,使其可以结束本轮任务状态并处理后续待刷新请求。 emit pebiGridGenerated(success); } + +void nmCalculationPebiGridTask::releaseDataManagerUse() +{ + if(m_bManagerUseActive && m_pDataManager != nullptr) { + m_pDataManager->endBackgroundUse(); + m_bManagerUseActive = false; + } +} diff --git a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp index 4569154..454a981 100644 --- a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp +++ b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp @@ -508,6 +508,8 @@ ZX_DEFINE_DYNAMIC(DataAnalyzeManager, nmDataAnalyzeManager) nmDataAnalyzeManager::nmDataAnalyzeManager(): ZxDataObjectBin(0) { + m_pOwnerFitting = nullptr; + m_nBackgroundUseCount = 0; m_reservoirData = nullptr; m_outlineData = nullptr; m_pMeasuringScaleData = nullptr; @@ -571,6 +573,15 @@ ZX_DEFINE_DYNAMIC(DataAnalyzeManager, nmDataAnalyzeManager) // 初始化静态成员 nmDataAnalyzeManager::~nmDataAnalyzeManager() { + // 第一步:任何窗口销毁顺序下,都必须先等后台网格/求解任务停止访问。 + // wait() 会暂时释放互斥量,因此任务仍能调用 endBackgroundUse() 正常退出。 + { + QMutexLocker oLocker(&m_oBackgroundUseMutex); + while(m_nBackgroundUseCount > 0) { + m_oNoBackgroundUseCondition.wait(&m_oBackgroundUseMutex); + } + } + // 图元只引用数据,数据统一由DataManager释放。 // 关闭成果或流动段分析后,清理该分析窗口对应的全部数据。 // 这两个成员是借用引用或容器内对象的别名,不单独delete。 @@ -635,6 +646,9 @@ nmDataAnalyzeManager* nmDataAnalyzeManager::getInstanceByFitting(iSubWndFitting* } nmDataAnalyzeManager* pInstance = new nmDataAnalyzeManager(); + // DataManager 与创建它的成果窗口一一绑定。后续后台任务读取 PVT、拟压力等 + // 上下文时必须使用该窗口,不能随界面当前页签切换到另一份成果。 + pInstance->m_pOwnerFitting = pSubWndF; s_mapDataAnalManager[pSubWndF] = pInstance; return pInstance; } @@ -663,6 +677,26 @@ nmDataAnalyzeManager* nmDataAnalyzeManager::getCurrentInstance() } } +void nmDataAnalyzeManager::beginBackgroundUse() +{ + QMutexLocker oLocker(&m_oBackgroundUseMutex); + ++m_nBackgroundUseCount; +} + +void nmDataAnalyzeManager::endBackgroundUse() +{ + QMutexLocker oLocker(&m_oBackgroundUseMutex); + if(m_nBackgroundUseCount <= 0) { + Q_ASSERT(false); + return; + } + + --m_nBackgroundUseCount; + if(m_nBackgroundUseCount == 0) { + m_oNoBackgroundUseCondition.wakeAll(); + } +} + nmDataWellBase *nmDataAnalyzeManager::createWell(NM_WELL_MODEL eWellType) { // 根据当前Fitting窗口来获取对应的井相关数据 @@ -3111,7 +3145,8 @@ bool nmDataAnalyzeManager::getPebiPseudoPressureTable(std::vector& press pseudoPressure.clear(); nmDataAnalyzeContextProvider* context = nmDataAnalyzeContext::provider(); - iSubWndFitting* fitting = nmDataAnalyzeManager::getCurrentFitting(); + // 使用本 DataManager 创建时绑定的成果窗口;切换当前页签不能改变数据来源。 + iSubWndFitting* fitting = m_pOwnerFitting; VVecDouble pseudoResult; if(context == nullptr || fitting == nullptr @@ -4086,8 +4121,9 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) oWellJson["WellCode"].GetString()); const NM_CASE_WELL_MODE eMode = static_cast( oWellJson["Mode"].GetInt()); - const bool bModeValid = eMode == NM_CaseWell_RateControlled || - eMode == NM_CaseWell_Observation; + // Include Other Wells 只保存主动生产/注入井;无产量观察井由 Map + // 自动推导,不允许以观察角色混入这个持久化列表。 + const bool bModeValid = eMode == NM_CaseWell_RateControlled; if(sWellCode == sSavedPrimaryWellCode || !mapSavedWellTypes.contains(sWellCode) || setIncludedWellCodes.contains(sWellCode) || @@ -4110,7 +4146,9 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) return false; } - // 第四步:求解器顺序只允许有效真实井或显式手工裂缝占位。 + // 第四步:此时井的历史流量尚未恢复,无法判断一口 Map 井最终属于 + // 主动井还是自动观察井。因此这里只校验真实井存在、井型一致且不重复; + // 历史数据加载完成后,再与最终有效计算井集合做严格一致性校验。 QSet setOrderedWellCodes; const rapidjson::Value& vecOrderJson = oCaseJson["SolverWellOrder"]; for(rapidjson::SizeType nIndex = 0; @@ -4137,7 +4175,7 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) oOrderJson["EntryKind"].GetInt()); if(eEntryKind == NM_SolverEntry_Well) { - if(!setEffectiveWellCodes.contains(sWellCode) || + if(!mapSavedWellTypes.contains(sWellCode) || setOrderedWellCodes.contains(sWellCode) || mapSavedWellTypes.value(sWellCode, NM_WELL_MODEL::Unknow_Well) != eWellType) { qWarning() << "SolverWellOrder contains an invalid real well:" @@ -4158,11 +4196,17 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) vecSavedSolverOrder.append(nmSolverWellRef( static_cast(nIndex), eWellType, sWellCode, eEntryKind)); } - if(!vecSavedSolverOrder.isEmpty() && - setOrderedWellCodes != setEffectiveWellCodes) { - qWarning() << "SolverWellOrder does not contain every effective well:" - << filePath; - return false; + if(!vecSavedSolverOrder.isEmpty()) { + // 主井和已启用的显式包含井无需依赖历史数据即可确定,必须已经在顺序中。 + // 顺序中的其他 Map 井可能是稍后才能识别的无产量观察井,当前不能拒绝。 + QSet::const_iterator oEffectiveIt = setEffectiveWellCodes.constBegin(); + for(; oEffectiveIt != setEffectiveWellCodes.constEnd(); ++oEffectiveIt) { + if(!setOrderedWellCodes.contains(*oEffectiveIt)) { + qWarning() << "SolverWellOrder does not contain a configured well:" + << *oEffectiveIt; + return false; + } + } } // 清空现有数据,确保从头加载新数据 @@ -4536,7 +4580,9 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) return false; } - // 井对象全部加载完成后,严格按 WellCode 恢复数值分析方案。 + // 井对象全部加载完成后,先按 WellCode 原样恢复已经通过结构校验的分析方案。 + // 这里不能调用会检查流量点的公开 setter,因为历史流量要到 loadNmResult() + // 的二进制恢复阶段才可用;业务有效性在历史数据恢复后统一确认。 nmDataWellBase* pPrimaryWell = findWellByCode(sSavedPrimaryWellCode); if(pPrimaryWell == nullptr) { qWarning() << "PrimaryWellCode is missing from numerical project:" @@ -4547,10 +4593,11 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) m_oNumericalAnalysisCase.setPrimaryWellCode(sSavedPrimaryWellCode); m_oNumericalAnalysisCase.setPrimaryWellMode(eSavedPrimaryWellMode); m_oNumericalAnalysisCase.setPebiGridControl(dSavedPebiGridControl); - setIncludedCalculationWells(vecSavedIncludedWells); + m_oNumericalAnalysisCase.setIncludedWells(vecSavedIncludedWells); m_oNumericalAnalysisCase.setIncludeOtherWells(bSavedIncludeOtherWells); m_oNumericalAnalysisCase.setSolverWellOrder(vecSavedSolverOrder); - setCurrentResultWellCode(sSavedCurrentResultWellCode.isEmpty() + m_oNumericalAnalysisCase.setCurrentResultWellCode( + sSavedCurrentResultWellCode.isEmpty() ? sSavedPrimaryWellCode : sSavedCurrentResultWellCode); @@ -5212,8 +5259,24 @@ bool nmDataAnalyzeManager::loadNmResult(QString sLoadAnalDir) // 第六步:二进制数据完整后,再生成各井用于曲线显示的压力、流量点。 this->loadWellPreAndFlow(); - // 主动井必须能够从项目井目录恢复有效产量制度;观察井允许没有流量点。 + // 第七步:历史数据恢复后再执行依赖流量的业务校验。 + // IncludedWells 即使当前总开关关闭,也只能保存有有效产量制度的井。 + QVector vecIncludedWells = + getIncludedCalculationWells(); + for(int nIndex = 0; nIndex < vecIncludedWells.size(); ++nIndex) { + const nmCalculationWellRef& oWellRef = vecIncludedWells[nIndex]; + nmDataWellBase* pWellData = findWellByCode(oWellRef.m_sWellCode); + if(!isSupportedNumericalWell(pWellData) || + pWellData->getFlowPoints().size() < 2) { + qWarning() << "Failed to restore included rate well:" + << oWellRef.m_sWellCode; + return false; + } + } + + // 主动井必须能够恢复有效产量制度;自动观察井允许没有流量点。 QVector vecEffectiveWells = getEffectiveCalculationWells(); + QSet setEffectiveWellCodes; for(int nIndex = 0; nIndex < vecEffectiveWells.size(); ++nIndex) { const nmCalculationWellRef& oWellRef = vecEffectiveWells[nIndex]; nmDataWellBase* pWellData = findWellByCode(oWellRef.m_sWellCode); @@ -5224,6 +5287,32 @@ bool nmDataAnalyzeManager::loadNmResult(QString sLoadAnalDir) << oWellRef.m_sWellCode; return false; } + setEffectiveWellCodes.insert(oWellRef.m_sWellCode); + } + + // 保存的求解器槽位允许夹有手工裂缝,但其中全部真实井必须与最终有效井 + // 完全一致。该校验放在这里,才能正确识别 Map 中自动加入的无产量观察井。 + QVector vecLoadedSolverOrder = getSolverWellOrder(); + if(!vecLoadedSolverOrder.isEmpty()) { + QSet setOrderedWellCodes; + for(int nIndex = 0; nIndex < vecLoadedSolverOrder.size(); ++nIndex) { + const nmSolverWellRef& oWellRef = vecLoadedSolverOrder[nIndex]; + if(oWellRef.m_eEntryKind == NM_SolverEntry_Well) { + setOrderedWellCodes.insert(oWellRef.m_sWellCode); + } + } + if(setOrderedWellCodes != setEffectiveWellCodes) { + qWarning() << "Loaded SolverWellOrder differs from effective wells:" + << sLoadAnalDir; + return false; + } + } + + // 结果井属于显示状态,但也必须在流量恢复后满足当前结果井规则。 + if(!isWellAvailableForResult(getCurrentResultWellCode())) { + qWarning() << "Loaded result WellCode is not available:" + << getCurrentResultWellCode(); + return false; } if(bHasCurrentGridFile && bHasCompleteResultFiles) { diff --git a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp index 9eeea30..d426205 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp @@ -183,14 +183,18 @@ nmSubWndMain::~nmSubWndMain() } if (m_pSolverTask) { - // 窗口销毁时只解除本窗口的回调;若线程仍在运行,让线程自己结束后释放。 + // 第一步:停止向正在析构的窗口投递完成回调。 disconnect(m_pSolverTask, SIGNAL(sig_calculateDone(bool)), this, SLOT(on_solverTaskFinished(bool))); - if (m_pSolverTask == s_pRunningSolverTask && !m_pSolverTask->isRunning()) { - s_pRunningSolverTask = nullptr; + // 第二步:DataManager 会在成果窗口销毁后释放,而任务仍读取该对象。 + // Qt 4.8 下只能等待外部 DLL 正常返回,绝不能让裸指针越过管理器生命周期。 + if(m_pSolverTask->isRunning()) { + m_pSolverTask->wait(); } - if (!m_pSolverTask->isRunning()) { - m_pSolverTask->deleteLater(); + if (m_pSolverTask == s_pRunningSolverTask) { + s_pRunningSolverTask = nullptr; } + // 第三步:线程已经停止,当前窗口仍是任务的明确所有者,可直接释放。 + delete m_pSolverTask; m_pSolverTask = nullptr; } @@ -1633,7 +1637,6 @@ void nmSubWndMain::solveAndAnalyze() pDataManager); s_pRunningSolverTask = m_pSolverTask; connect(m_pSolverTask, SIGNAL(sig_calculateDone(bool)), this, SLOT(on_solverTaskFinished(bool))); - connect(m_pSolverTask, SIGNAL(finished()), m_pSolverTask, SLOT(deleteLater())); // 1. 创建并配置进度对话框 if (m_pProgressDlg == nullptr) @@ -1641,7 +1644,9 @@ void nmSubWndMain::solveAndAnalyze() // TODO:关联到父窗口 m_pProgressDlg = new QProgressDialog(tr("Calculating, please wait..."), QString(), 0, 100, getMainWindow()); m_pProgressDlg->setWindowTitle(tr("Solver Progress")); - m_pProgressDlg->setWindowModality(Qt::NonModal); // 模态,锁定父窗口 + // 求解任务仍需读取 DataManager 中的 Qt 容器。应用级模态会冻结所有成果的 + // 编辑入口,避免 Qt 4.8 下后台读与前台写同时发生数据竞争。 + m_pProgressDlg->setWindowModality(Qt::ApplicationModal); // 某些系统下,对话框右上角的关闭按钮可能仍存 // 使用 WindowFlags 彻底禁用关闭按钮 @@ -2048,6 +2053,13 @@ bool nmSubWndMain::initPreHistory(QVector > &vvecHistoryData, QV bool nmSubWndMain::onConfirmClosing() { + // 正常关闭入口在求解结束前保持当前成果和 DataManager 存活;应用退出等绕过 + // 该确认流程的路径仍由析构函数 wait() 兜底。 + if(m_pSolverTask != nullptr && m_pSolverTask->isRunning()) { + QMessageBox::information(this, tr("solver error"), tr("task is running!")); + return false; + } + return iSubWndBaseAF::onConfirmClosing(); } @@ -2137,10 +2149,17 @@ void nmSubWndMain::on_solverTaskFinished(bool isSuccessed) int nPebiCount = pTask != nullptr ? pTask->getPebiCount() : -1; int nSolveTimeMs = pTask != nullptr ? pTask->getSolveTimeMs() : -1; + // 后台线程只生成局部结果快照。回到主线程后先验证所属 DataManager 和 + // 输入版本,再整体替换旧成果;提交失败与求解失败使用同一条错误路径。 + if(isSuccessed) { + isSuccessed = pTask->commitResult(m_pSolverDataManager.data()); + } m_pSolverTask = nullptr; if(pTask == s_pRunningSolverTask) { s_pRunningSolverTask = nullptr; } + // 正常完成路径由主线程延迟释放任务;析构路径则会等待并直接释放。 + pTask->deleteLater(); // 第二步:在清空捕获上下文前完成结果合并,确保始终回写原拟合窗口。 NM_Calculation_Result result = isSuccessed ? NM_Calculation_Result_Success : NM_Calculation_Result_Fail; diff --git a/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp b/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp index d390569..a9104af 100644 --- a/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp +++ b/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp @@ -821,6 +821,12 @@ void nmWxNumericalDesign::setupConnections() if(m_pDataManager != nullptr) { connect(m_pDataManager, SIGNAL(dataChanged()), this, SLOT(onWellCollectionChanged()), Qt::UniqueConnection); + // createWell() 会在井对象登记时先发送 dataChanged(),此时 WellCode、 + // 坐标和历史流量可能尚未填充。sigWellAdded 表示井已完整建立,必须再次 + // 刷新 Include Other Wells,才能正确识别刚加入 Map 的有产量井。 + connect(m_pDataManager, + SIGNAL(sigWellAdded(QString,QString,QStringList)), + this, SLOT(onWellCollectionChanged()), Qt::UniqueConnection); } connect(m_pResetDiagnosticButton, SIGNAL(clicked()), this, SLOT(onResetFromDiagnosticClicked())); // "从诊断复位"按钮连接 connect(m_pResetAnalyticalButton, SIGNAL(clicked()), this, SLOT(onResetFromAnalyticalClicked())); // "从解析复位"按钮连接