diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index 86188074..349e5953 100644 Binary files a/Bin/Config/Lang/cn/nmNum_cn.qm and b/Bin/Config/Lang/cn/nmNum_cn.qm differ diff --git a/Bin/Config/Lang/cn/nmNum_cn.ts b/Bin/Config/Lang/cn/nmNum_cn.ts index 4b98e7d4..42ebb3f8 100644 --- a/Bin/Config/Lang/cn/nmNum_cn.ts +++ b/Bin/Config/Lang/cn/nmNum_cn.ts @@ -2351,6 +2351,14 @@ Reason: %1 nmSubWndGrid + + Generating grid... + 正在生成网格... + + + Grid generation failed + 网格生成失败 + nmSubWndMain diff --git a/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h b/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h index edc08975..a0579713 100644 --- a/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h +++ b/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h @@ -17,6 +17,7 @@ class nmDataAnalyzeManager; class vtkDoubleArray; class vtkUnstructuredGrid; +struct nmPebiSolverInputSnapshot; // 主窗口现在直接创建DLL求解线程任务,因此类需要导出,供nmSubWnd模块跨DLL使用。 class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { @@ -26,10 +27,12 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { * @brief 创建绑定到指定分析窗口的 PEBI 求解任务。 * @param sPostprocessingDir 后处理文件输出目录。 * @param pDataManager 任务所属数据管理器,不转移所有权。 + * @param sAutoFitTargetWellName 自动拟合目标井名;空值表示完整成果计算。 * @param parent Qt 父对象。 */ nmCalculationDllPebiSolverTask(QString sPostprocessingDir, nmDataAnalyzeManager* pDataManager = nullptr, + const QString& sAutoFitTargetWellName = QString(), QObject *parent = nullptr); ~nmCalculationDllPebiSolverTask(); @@ -50,11 +53,6 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { */ bool commitResult(nmDataAnalyzeManager* pDataManager); - /** - * @brief 按显示井名设置自动拟合目标井,内部立即转换并保存 WellCode。 - * @note 井名为空或查找失败时保持完整结果保存模式。 - */ - void setAutoFitTargetWell(const QString& wellName); /** @brief 返回自动拟合目标井的原始压力结果副本。 */ QVector > getAutoFitResultPressure() const; /** @brief 返回自动拟合目标井的双对数结果副本。 */ @@ -67,6 +65,8 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { bool execute(); /** @brief 成对释放构造时登记的 DataManager 后台使用权。 */ void releaseDataManagerUse(); + /** @brief 在任务创建线程一次性捕获网格、井、储层和求解器设置。 */ + bool captureInputSnapshot(const QString& sAutoFitTargetWellName); /** @brief 构造 DLL 输入、执行求解并生成仍然有效的局部结果快照。 */ bool execPebiMode(); @@ -94,7 +94,10 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { QString m_sPostprocessingDir; ///< 本次求解使用的后处理输出目录。 /** @brief 创建任务时捕获所属数据中心,工作线程不得读取全局当前窗口。 */ nmDataAnalyzeManager* m_pDataManager; + /** @brief 任务独占的完整值快照;析构时释放,后台禁止访问 DataManager。 */ + nmPebiSolverInputSnapshot* m_pInputSnapshot; bool m_bManagerUseActive; ///< 是否仍持有 DataManager 后台使用权。 + bool m_bInputSnapshotValid; ///< 构造阶段是否已完成全部输入校验。 /** @brief 创建任务时捕获网格输入版本,防止回填过期网格上的结果。 */ quint64 m_nGridInputRevision; /** @brief 创建任务时捕获求解输入版本,防止角色或参数变化后回填旧结果。 */ diff --git a/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h b/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h index 191d32e3..ae47ff48 100644 --- a/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h +++ b/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h @@ -4,14 +4,60 @@ #include "nmCalculation_global.h" #include #include +#include #include "pch.h" +#include "nmDataBinaryTools.h" +#include "nmDataNumericalAnalysisCase.h" #include #include class nmDataAnalyzeManager; +/** + * @brief 一次 PEBI 网格任务使用的不可变输入快照。 + * + * 该结构只保存值类型,不持有 DataManager、QObject 或井对象指针。主线程在 + * 启动任务前完整填充,后台线程随后只读取本结构,从而避免与 Map 编辑并发访问。 + */ +struct nmPebiGridInputSnapshot +{ + /** @brief 创建空快照;m_bValid 为 false,不能直接提交给网格 DLL。 */ + nmPebiGridInputSnapshot() + : m_nGridInputRevision(0), + m_bValid(false) + { + } + + HX_NWTM_GRID_INPUT m_oGridInput; ///< 边界、井、断层和裂缝等 DLL 输入。 + nmDataBinaryTools::NM_PEBI_SCENE m_oScene; ///< 与本次网格输入一致的 ML 场景数据。 + QVector m_vecSolverWellOrder; ///< DLL 数组槽位对应的稳定井编码顺序。 + QString m_sLicensePath; ///< 网格 DLL 授权文件路径副本。 + quint64 m_nGridInputRevision; ///< 捕获时的几何输入版本。 + bool m_bValid; ///< 主线程是否已完成全部输入校验。 +}; + +/** + * @brief 后台 PEBI 计算产生、尚未写回分析成果的局部结果。 + */ +struct nmPebiGridResult +{ + /** @brief 创建空结果;只有 m_bSucceeded 为 true 时才允许提交。 */ + nmPebiGridResult() + : m_pUnstructuredGrid(nullptr), + m_nPebiCount(-1), + m_bSucceeded(false) + { + } + + HX_NWTM_GRID_OUTPUT1 m_oGridOutput1; ///< 网格 DLL 主输出。 + HX_NWTM_GRID_OUTPUT2 m_oGridOutput2; ///< 网格 DLL 辅助输出。 + vtkSmartPointer m_pUnstructuredGrid; ///< 局部构造的 VTK 网格。 + int m_nPebiCount; ///< DLL 返回的 PEBI 单元数量。 + bool m_bSucceeded; ///< DLL 和 VTK 转换是否全部成功。 +}; + class NMCALCULATION_EXPORT nmCalculationPebiGrid { public: @@ -20,8 +66,17 @@ public: nmCalculationPebiGrid(); ~nmCalculationPebiGrid(); - /** @brief 为指定数据管理器生成 PEBI 网格并登记输入版本。 */ - bool meshGenPebi(nmDataAnalyzeManager* pDataManager = nullptr); + /** @brief 在调用线程从 DataManager 复制一份完整且不含指针的网格输入。 */ + bool captureInputSnapshot(nmDataAnalyzeManager* pDataManager, + nmPebiGridInputSnapshot& oSnapshot); + /** @brief 仅使用值快照调用网格 DLL,并把输出保存在局部结果中。 */ + bool calculateSnapshot(const nmPebiGridInputSnapshot& oSnapshot, + nmPebiGridResult& oResult, + bool bCreateUnstructuredGrid = true); + /** @brief 在主线程校验输入版本,并一次性提交井顺序、网格和单例缓存。 */ + bool commitSnapshotResult(nmDataAnalyzeManager* pDataManager, + const nmPebiGridInputSnapshot& oSnapshot, + const nmPebiGridResult& oResult); /** @brief 设置 PEBI 网格控制值,并同步到指定分析方案。 */ void setGridControl(double dGridControl, nmDataAnalyzeManager* pDataManager = nullptr); @@ -30,13 +85,28 @@ public: private: /** @brief 从绑定数据管理器构造边界输入。 */ - bool meshGenPebiBoundary(HX_NWTM_GRID_INPUT& inputObj); + bool meshGenPebiBoundary(nmDataAnalyzeManager* pDataManager, + HX_NWTM_GRID_INPUT& inputObj); /** @brief 按有效计算井集合构造井几何并记录 DLL 井顺序。 */ - bool meshGenPebiWells(HX_NWTM_GRID_INPUT& inputObj); + bool meshGenPebiWells(nmDataAnalyzeManager* pDataManager, + HX_NWTM_GRID_INPUT& inputObj, + QVector& vecSolverWellOrder); /** @brief 从 Map 构造断层输入。 */ - bool meshGenPebiFault(HX_NWTM_GRID_INPUT& inputObj); + bool meshGenPebiFault(nmDataAnalyzeManager* pDataManager, + HX_NWTM_GRID_INPUT& inputObj); /** @brief 从 Map 构造手工裂缝输入和求解器占位条目。 */ - bool meshGenPebiCrack(HX_NWTM_GRID_INPUT& inputObj); + bool meshGenPebiCrack(nmDataAnalyzeManager* pDataManager, + HX_NWTM_GRID_INPUT& inputObj, + QVector& vecSolverWellOrder); + /** @brief 从主线程数据构造与网格输入一致的 ML 场景值快照。 */ + bool buildPebiScene(nmDataAnalyzeManager* pDataManager, + const HX_NWTM_GRID_INPUT& oGridInput, + const QVector& vecSolverWellOrder, + nmDataBinaryTools::NM_PEBI_SCENE& oScene); + /** @brief 更新单例中的 DLL 输入输出缓存,不改变网格有效版本。 */ + void commitOutputCache(nmDataAnalyzeManager* pDataManager, + const nmPebiGridInputSnapshot& oSnapshot, + const nmPebiGridResult& oResult); // 生成vtk数据 void genPebiVTK(const HX_NWTM_GRID_OUTPUT1& P1, QString vtkDir); @@ -49,11 +119,7 @@ private: void logInputParameters(const HX_NWTM_GRID_INPUT& input); public: - /** @brief 调用网格 DLL 生成 p1、p2,并校验有效井与求解器顺序一致。 */ - bool generateOutputPara(nmDataAnalyzeManager* pDataManager = nullptr); - /** @brief 判断单例缓存是否确实属于指定分析窗口且输入版本仍然有效。 */ - bool isGridCurrentFor(const nmDataAnalyzeManager* pDataManager) const; - /** @brief 在同一把锁内校验所属分析并复制完整网格快照。 */ + /** @brief 非阻塞复制当前有效网格;DLL忙碌或缓存不匹配时返回 false。 */ bool copyCurrentGridFor(const nmDataAnalyzeManager* pDataManager, HX_NWTM_GRID_OUTPUT1& oGridOutput1, HX_NWTM_GRID_OUTPUT2& oGridOutput2, @@ -74,6 +140,7 @@ private: HX_NWTM_GRID_OUTPUT2 p2; ///< 最近一次网格 DLL 辅助输出。 double m_dGridControl; ///< 对应 HX_NWTM_GRID_INPUT::GridControl。 int m_nPebiCount; ///< 最近一次成功网格的单元数量,未生成时为 -1。 + quint64 m_nCachedGridInputRevision; ///< 单例 DLL 输出对应的网格输入版本。 /** @brief 当前单例缓存所属的数据管理器,不拥有对象。 */ nmDataAnalyzeManager* m_pDataManager; diff --git a/Include/nmNum/nmCalculation/nmCalculationPebiGridTask.h b/Include/nmNum/nmCalculation/nmCalculationPebiGridTask.h index 140688d7..3dc3aba4 100644 --- a/Include/nmNum/nmCalculation/nmCalculationPebiGridTask.h +++ b/Include/nmNum/nmCalculation/nmCalculationPebiGridTask.h @@ -4,6 +4,7 @@ #include #include "nmCalculation_global.h" +#include "nmCalculationPebiGrid.h" class nmDataAnalyzeManager; @@ -17,6 +18,8 @@ public: QObject *parent = 0); /** @brief 等待线程结束,并释放 DataManager 后台使用权。 */ ~nmCalculationPebiGridTask(); + /** @brief 在主线程按版本提交后台局部结果;过期结果返回 false。 */ + bool commitResult(nmDataAnalyzeManager* pDataManager); signals: /** @brief 无论成功或失败均通知主线程结束本轮网格任务。 */ @@ -33,6 +36,9 @@ private: /** @brief 启动任务时捕获所属数据管理器,线程内不再读取全局当前窗口。 */ nmDataAnalyzeManager* m_pDataManager; bool m_bManagerUseActive; ///< 是否仍持有 DataManager 后台使用权。 + bool m_bSnapshotValid; ///< 构造阶段是否成功获得完整输入快照。 + nmPebiGridInputSnapshot m_oInputSnapshot; ///< 主线程创建、后台只读的值快照。 + nmPebiGridResult m_oResult; ///< 后台局部结果,只允许主线程提交。 }; #endif // NMCALCULATIONPEBIGRIDTASK_H diff --git a/Include/nmNum/nmData/nmDataAnalyzeManager.h b/Include/nmNum/nmData/nmDataAnalyzeManager.h index 134a028a..9c6c5d53 100644 --- a/Include/nmNum/nmData/nmDataAnalyzeManager.h +++ b/Include/nmNum/nmData/nmDataAnalyzeManager.h @@ -487,6 +487,16 @@ public: void insertSolverWell(int nIndex, const nmSolverWellRef& oWellRef); /** @brief 返回网格确定的 DLL 井槽位顺序,与包含井选择相互独立。 */ QVector getSolverWellOrder() const; + /** @brief 整体替换 DLL 井槽位顺序,并由分析方案统一重编号。 */ + void setSolverWellOrder(const QVector& vecSolverWellOrder); + /** + * @brief 按输入版本一次性提交后台生成的井顺序和 VTK 网格。 + * @return 输入仍为当前版本且提交成功时返回 true;过期结果不修改现有成果。 + */ + bool commitPebiGridResult( + quint64 nGridInputRevision, + const QVector& vecSolverWellOrder, + vtkSmartPointer pGrid); /** @brief 将当前输入版本登记为已成功生成网格。 */ void markPebiGridBuilt(); /** @brief 仅在后台划分期间输入未变化时登记网格成功。 */ diff --git a/Include/nmNum/nmSubWnd/nmSubWndGrid.h b/Include/nmNum/nmSubWnd/nmSubWndGrid.h index 7d3e0fe4..11662030 100644 --- a/Include/nmNum/nmSubWnd/nmSubWndGrid.h +++ b/Include/nmNum/nmSubWnd/nmSubWndGrid.h @@ -7,6 +7,8 @@ class nmWxGridVTKContainerWidget; class nmDataAnalyzeManager; class nmCalculationPebiGridTask; class QTimer; +class QLabel; +class QStackedLayout; class NM_SUB_WND_EXPORT nmSubWndGrid : public iSubWndBaseFit { Q_OBJECT @@ -53,9 +55,16 @@ class NM_SUB_WND_EXPORT nmSubWndGrid : public iSubWndBaseFit { bool generatePebiGrid(); private: + /** @brief 切换到渐变加载背景,并恢复“正在生成网格”提示。 */ + void showGridLoading(); + /** @brief 用 DataManager 中的最新网格重建 VTK 页并切换到结果显示。 */ + void showGridResult(); + /** @brief 网格失败时恢复旧网格;没有旧网格则显示明确失败提示。 */ + void showGridFailure(); - QVBoxLayout* m_mainLayout; ///< 网格窗口主布局。 + QStackedLayout* m_mainLayout; ///< 在加载背景和 VTK 结果之间切换的主布局。 nmWxGridVTKContainerWidget* m_vtkWidget; ///< 显示所属分析网格的 VTK 控件。 + QLabel* m_pGridLoadingLabel; ///< 后台划分期间显示的渐变背景和状态文字。 QTimer* m_pDebounceTimer; ///< 本窗口独立防抖定时器,避免跨窗口刷新。 QPointer m_pDataManager; ///< 窗口所属数据管理器,销毁后自动置空。 QPointer m_pGridTask; ///< 当前后台网格任务,完成后自动回收。 diff --git a/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp b/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp index 36e4bade..73bfb82b 100644 --- a/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationAutoFitPSO.cpp @@ -3486,26 +3486,6 @@ bool nmCalculationAutoFitPSO::startAutoFitting() emit logMessageGenerated(tr("Applying optimized parameters to model...")); applyParametersToDataManager(m_globalBestPosition); - // 即使用户此时停止、不再执行最终完整计算,也要把 PEBI 缓存恢复为 - // 最终已接受的裂缝参数,避免缓存仍停留在最后一个被拒绝的候选值。 - const bool fractureGridParameterSelected = - (m_parameterSelected.size() > 8 && m_parameterSelected[8]) || - (m_parameterSelected.size() > 9 && m_parameterSelected[9]); - if(fractureGridParameterSelected) { - // 第一步:最终参数已写回当前分析,网格刷新必须继续绑定同一数据管理器。 - nmDataAnalyzeManager* pDataManager = - nmDataAnalyzeManager::getCurrentInstance(); - if(pDataManager == nullptr) { - throw std::runtime_error("Current data manager is unavailable"); - } - - // 第二步:仅重建裂缝参数相关的 PEBI 输出,供最终全场求解复用。 - nmCalculationPebiGrid* pebiGrid = nmCalculationPebiGrid::getInstance(); - if(!pebiGrid || !pebiGrid->generateOutputPara(pDataManager)) { - throw std::runtime_error("Failed to refresh final fracture parameters"); - } - } - if(m_shouldStop) { // 手动停止优先保持快速返回,仅写回已确认的最优参数。 emit logMessageGenerated(tr("Final full-field calculation skipped after user stop")); @@ -5664,23 +5644,8 @@ double nmCalculationAutoFitPSO::evaluateFitness(const QVector& parameter return 1e10; } - // Dfc 和裂缝半长都通过 PEBI 裂缝数组传入,不属于每次求解都会重新组装的 Base/CS 参数。 - // 勾选任一裂缝参数时刷新网格输出,保证本次真实试算使用新的导流能力和端点坐标。 - const bool fractureGridParameterSelected = - (m_parameterSelected.size() > 8 && m_parameterSelected[8]) || - (m_parameterSelected.size() > 9 && m_parameterSelected[9]); - if(fractureGridParameterSelected) { - // 粒子参数改变裂缝几何时,必须刷新同一分析窗口的网格 DLL 输入。 - nmCalculationPebiGrid* pebiGrid = nmCalculationPebiGrid::getInstance(); - if(!pebiGrid || !pebiGrid->generateOutputPara(dataManager)) { - DEBUG_OUT(QString("%1: Call #%2 - Failed to refresh PEBI fracture parameters") - .arg(funcName).arg(callCount)); - return 1e10; - } - } - - // 4. 运行求解器。真实求解器偶发失败时允许重试,避免一次 DLL 调用异常 - // 直接让整个粒子评价失败。 + // 4. 运行求解器。裂缝参数使网格失效时,任务会从不可变输入快照生成一次 + // 局部网格并直接用于本次模型计算,不再预生成后又重复建网。 QVector> solverResult; const int maxRetries = 2; bool solverSuccess = false; @@ -7483,8 +7448,10 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() try { DEBUG_OUT("Creating DLL solver task"); - dllTask = new nmCalculationDllPebiSolverTask(m_tempDirectory, - pDataManager); + dllTask = new nmCalculationDllPebiSolverTask( + m_tempDirectory, + pDataManager, + m_targetWellName); if(m_targetWellName.isEmpty()) { DEBUG_OUT("Target well name is empty - target-only solver cannot start"); @@ -7493,8 +7460,6 @@ QVector> nmCalculationAutoFitPSO::runSolverDll() return result; } - dllTask->setAutoFitTargetWell(m_targetWellName); - if(m_shouldStop) { DEBUG_OUT("Should stop - cleaning up and returning empty result"); delete dllTask; @@ -7703,7 +7668,9 @@ bool nmCalculationAutoFitPSO::runFinalFullSolver() } dllTask.wait(); - const bool succeeded = dllTask.wasSuccessful(); + // 完整求解只在当前线程一次提交网格和全部井结果。自动拟合粒子任务不会走这里。 + const bool succeeded = dllTask.wasSuccessful() && + dllTask.commitResult(pDataManager); --m_evaluationInProgress; return succeeded; } diff --git a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp index 18eb13da..84cee884 100644 --- a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp @@ -34,16 +34,60 @@ #include #include +#include + +/** + * @brief 一口求解井在任务启动时捕获的后处理输入。 + * + * DLL 模型输入已经保存在场景快照中;这里仅保留生成双对数、半对数和结果井位置 + * 仍需要的数据,后台线程不再回到 DataManager 查井对象。 + */ +struct nmPebiSolverWellInputSnapshot +{ + nmPebiSolverWellInputSnapshot() + : m_nFlowSectionIndex(1), + m_bRateControlled(false), + m_bRealWell(false) + { + } -namespace { - -// PVT数组读到时才覆盖,读不到时保留pch.h中的默认参数 -void assignPvtVectorIfNotEmpty(dVec1& target, const QVector& source) + QString m_sWellCode; ///< 项目内稳定井编码。 + QVector m_vecFlowPoints; ///< 包含首个零点的原始产量制度。 + QPointF m_oLocation; ///< 捕获时的 Map 坐标。 + int m_nFlowSectionIndex; ///< 双对数预处理使用的流量段下标。 + bool m_bRateControlled; ///< 是否为有产量的主动生产/注入井。 + bool m_bRealWell; ///< false 表示仅占 DLL 槽位的手工裂缝。 +}; + +/** + * @brief 一次 PEBI 求解任务独占的完整不可变输入。 + */ +struct nmPebiSolverInputSnapshot { - if(!source.isEmpty()) { - target = dVec1(source.toStdVector()); + nmPebiSolverInputSnapshot() + : m_nSolverType(nmDataAnalyzeManager::PebiSolverOriginal), + m_nOmpThreads(1), + m_nIluReuseSteps(1), + m_bRequiresGridCalculation(false), + m_bGridResultNeedsCommit(false), + m_bAutoFitTargetOnly(false) + { } -} + + nmPebiGridInputSnapshot m_oGridInput; ///< 网格、场景、井顺序和授权路径值快照。 + nmPebiGridResult m_oGridResult; ///< 已有网格副本或后台新生成的局部网格。 + QVector m_vecWellInputs; ///< 与 DLL 井槽位严格对齐。 + QVector m_vecPropertyDataSets; ///< 属性插值值副本。 + vtkSmartPointer m_pBaseGrid; ///< 完整成果使用的基础 VTK 网格。 + int m_nSolverType; ///< PEBI 求解器实现类型。 + int m_nOmpThreads; ///< CPU 加速求解线程数。 + int m_nIluReuseSteps; ///< ILU 预条件复用步数。 + bool m_bRequiresGridCalculation; ///< 后台是否需先用网格值快照计算一次网格。 + bool m_bGridResultNeedsCommit; ///< 完整求解成功后是否需在主线程提交新网格。 + bool m_bAutoFitTargetOnly; ///< true 时只构造目标井临时曲线,不提交成果。 +}; + +namespace { bool isFiniteSolverNumber(double value) { @@ -162,6 +206,82 @@ bool applyPropertyInterpolation( return true; } +/** + * @brief 用网格输出和捕获的场景值组装一次完整模型输入。 + */ +bool buildModelInputFromSnapshot( + const nmPebiSolverInputSnapshot& oSnapshot, + const HX_NWTM_GRID_OUTPUT2& oGridOutput, + HX_NWTM_MODEL_INPUT& oModelInput, + QString& sErrorMessage) +{ + const nmDataBinaryTools::NM_PEBI_SCENE& oScene = + oSnapshot.m_oGridInput.m_oScene; + oModelInput = HX_NWTM_MODEL_INPUT(oGridOutput); + + // 第一步:恢复井槽位、产量制度和井筒参数。场景与网格快照在同一次捕获中 + // 生成,因此这些外层数组天然使用同一个 DLL 下标顺序。 + oModelInput.T = oScene.solverType; + oModelInput.Rate.t = oScene.Rate.t; + oModelInput.Rate.qo = oScene.Rate.qo; + oModelInput.Rate.qg = oScene.Rate.qg; + oModelInput.Rate.qw = oScene.Rate.qw; + oModelInput.CS.C = oScene.CS.C; + oModelInput.CS.S = oScene.CS.S; + + // 第二步:恢复完整 PVT 数组。不能在工作线程重新读取 PVT 对象,否则界面 + // 切换模型或编辑表格时仍会与求解器发生容器并发访问。 + oModelInput.PVT.p = oScene.PVT.p; + oModelInput.PVT.pb = oScene.PVT.pb; + oModelInput.PVT.Rso = oScene.PVT.Rso; + oModelInput.PVT.Bo = oScene.PVT.Bo; + oModelInput.PVT.Co = oScene.PVT.Co; + oModelInput.PVT.miuo = oScene.PVT.miuo; + oModelInput.PVT.rouo = oScene.PVT.rouo; + oModelInput.PVT.Rv = oScene.PVT.Rv; + oModelInput.PVT.Bg = oScene.PVT.Bg; + oModelInput.PVT.Cg = oScene.PVT.Cg; + oModelInput.PVT.miug = oScene.PVT.miug; + oModelInput.PVT.roug = oScene.PVT.roug; + oModelInput.PVT.Z = oScene.PVT.Z; + oModelInput.PVT.Rsw = oScene.PVT.Rsw; + oModelInput.PVT.Bw = oScene.PVT.Bw; + oModelInput.PVT.Cw = oScene.PVT.Cw; + oModelInput.PVT.miuw = oScene.PVT.miuw; + oModelInput.PVT.rouw = oScene.PVT.rouw; + oModelInput.PVT.V = oScene.PVT.V; + oModelInput.PVT.k_kinitial = oScene.PVT.k_kinitial; + oModelInput.PVT.Cf_Cfinitial = oScene.PVT.Cf_Cfinitial; + oModelInput.PVT.So = oScene.PVT.So; + oModelInput.PVT.Kro = oScene.PVT.Kro; + oModelInput.PVT.Sg = oScene.PVT.Sg; + oModelInput.PVT.Krg = oScene.PVT.Krg; + oModelInput.PVT.Sw = oScene.PVT.Sw; + oModelInput.PVT.Krw = oScene.PVT.Krw; + + // 第三步:先用储层参考值填满全部网格单元,再按捕获的属性数据组覆盖。 + oModelInput.Base.Pi = oScene.Base.Pi; + oModelInput.Base.Cti = oScene.Base.Cti; + oModelInput.Base.Cf = oScene.Base.Cf; + oModelInput.Base.Soi = oScene.Base.Soi; + oModelInput.Base.Sgi = oScene.Base.Sgi; + oModelInput.Base.Swi = oScene.Base.Swi; + oModelInput.Base.d = oScene.Base.d; + oModelInput.Base.dt_Min = oScene.Base.dt_Min; + oModelInput.Base.dt_Max = oScene.Base.dt_Max; + + const size_t nCellCount = oGridOutput.Trinodexy.size(); + oModelInput.Base.k = dVec1(nCellCount, oScene.Base.k_ref); + oModelInput.Base.phi = dVec1(nCellCount, oScene.Base.phi_ref); + oModelInput.Base.h = dVec1(nCellCount, oScene.Base.h_ref); + + return applyPropertyInterpolation( + oModelInput, + oSnapshot.m_vecPropertyDataSets, + oSnapshot.m_oGridInput.m_sLicensePath, + sErrorMessage); +} + bool isReasonableLogLogValue(double value) { const double maxReasonableAbsValue = 1.0e12; @@ -204,107 +324,6 @@ CalPseudoPressure getPseudoPressureConverter() return converter; } -/// @brief 将常数PVT单值扩展为200元素数组(PEBI求解器要求数组输入) -/// @param dValue 常数PVT值 -/// @param nSize 数组大小,默认200 -/// @return 包含nSize个dValue元素的dVec1数组 -dVec1 expandConstPvt(double dValue, int nSize = 200) -{ - return dVec1(nSize, dValue); -} - -/// @brief 生成线性占位压力数组(常数PVT无曲线数据时使用) -/// @param nSize 数组大小,默认200 -/// @return [1, 2, ..., nSize] 的 dVec1 数组 -dVec1 generateDummyPressure(int nSize = 200) -{ - dVec1 vecP(nSize, 0); - for(int i = 0; i < nSize; ++i) { vecP[i] = (i + 1.0); } - return vecP; -} - -/// @brief 根据求解器模型类型填充PVT输入数据 -/// 常数PVT模型从 reservoir 读单值并扩展为200元素数组 -/// 变化PVT模型从 pebiPvtPara 读曲线数组 -/// @param input PEBI求解器输入结构 -/// @param eModelType 求解器模型类型 -/// @param pPvt PEBI PVT参数对象(变化PVT时提供曲线数组) -/// @param pRes 油藏数据对象(常数PVT时提供单值) -void fillPvtInputByModel(HX_NWTM_MODEL_INPUT& input, - NM_SOLVER_MODEL_TYPE eModelType, - nmDataPvtParaForPebi* pPvt, - nmDataReservoir* pRes) -{ - switch(eModelType) { - case SMT_Oil_ConstPvt: - // 油相常数PVT:不依赖pPvt,生成占位压力数组 + 单值扩展为200元素数组 - input.PVT.p = generateDummyPressure(); - if(pRes != nullptr) { - input.PVT.Bo = expandConstPvt(pRes->getBo().getValue().toDouble()); - input.PVT.miuo = expandConstPvt(pRes->getMiuo().getValue().toDouble()); - } - break; - - case SMT_Oil_VariablePvt: - // 油相变化PVT:从 pebiPvtPara 读曲线数组 - if(pPvt != nullptr) { - assignPvtVectorIfNotEmpty(input.PVT.p, pPvt->getPressure()); - assignPvtVectorIfNotEmpty(input.PVT.Bo, pPvt->getBo()); - assignPvtVectorIfNotEmpty(input.PVT.Co, pPvt->getCo()); - assignPvtVectorIfNotEmpty(input.PVT.miuo, pPvt->getMiuo()); - } - break; - - case SMT_Water_ConstPvt: - // 水相常数PVT:不依赖pPvt,生成占位压力数组 + 单值扩展为200元素数组 - input.PVT.p = generateDummyPressure(); - if(pRes != nullptr) { - input.PVT.Bw = expandConstPvt(pRes->getBw().getValue().toDouble()); - input.PVT.miuw = expandConstPvt(pRes->getMiuw().getValue().toDouble()); - } - break; - - case SMT_Water_VariablePvt: - // 水相变化PVT:从 pebiPvtPara 读曲线数组 - if(pPvt != nullptr) { - assignPvtVectorIfNotEmpty(input.PVT.p, pPvt->getPressure()); - assignPvtVectorIfNotEmpty(input.PVT.Bw, pPvt->getBw()); - assignPvtVectorIfNotEmpty(input.PVT.Cw, pPvt->getCw()); - assignPvtVectorIfNotEmpty(input.PVT.miuw, pPvt->getMiuw()); - } - break; - - case SMT_Gas_VariablePvt: - case SMT_Gas_PseudoPressure: - // 气相变化PVT/拟压力:从 pebiPvtPara 读曲线数组 - if(pPvt != nullptr) { - assignPvtVectorIfNotEmpty(input.PVT.p, pPvt->getPressure()); - assignPvtVectorIfNotEmpty(input.PVT.Bg, pPvt->getBg()); - assignPvtVectorIfNotEmpty(input.PVT.Cg, pPvt->getCg()); - assignPvtVectorIfNotEmpty(input.PVT.miug, pPvt->getMiug()); - } - break; - - case SMT_Oil_Water_TwoPhase: - // 油水两相:从 pebiPvtPara 读油+水曲线数组 + 相渗数据 - if(pPvt != nullptr) { - assignPvtVectorIfNotEmpty(input.PVT.p, pPvt->getPressure()); - assignPvtVectorIfNotEmpty(input.PVT.Bo, pPvt->getBo()); - assignPvtVectorIfNotEmpty(input.PVT.miuo, pPvt->getMiuo()); - assignPvtVectorIfNotEmpty(input.PVT.Bw, pPvt->getBw()); - assignPvtVectorIfNotEmpty(input.PVT.miuw, pPvt->getMiuw()); - - // 相渗与饱和度数据(PEBI油水两相不需要PVT.Sw数组,初始含水饱和度通过Base.Swi传递) - assignPvtVectorIfNotEmpty(input.PVT.So, pPvt->getSo()); - assignPvtVectorIfNotEmpty(input.PVT.Kro, pPvt->getKro()); - assignPvtVectorIfNotEmpty(input.PVT.Krw, pPvt->getKrw()); - } - break; - - default: - break; - } -} } @@ -312,13 +331,16 @@ void fillPvtInputByModel(HX_NWTM_MODEL_INPUT& input, nmCalculationDllPebiSolverTask::nmCalculationDllPebiSolverTask( QString sPostprocessingDir, nmDataAnalyzeManager* pDataManager, + const QString& sAutoFitTargetWellName, QObject *parent): QThread(parent), m_sPostprocessingDir(sPostprocessingDir), m_pDataManager(pDataManager != nullptr ? pDataManager : nmDataAnalyzeManager::getCurrentInstance()), + m_pInputSnapshot(new nmPebiSolverInputSnapshot()), m_bManagerUseActive(false), + m_bInputSnapshotValid(false), m_nGridInputRevision(0), m_nResultInputRevision(0), m_lastRunSucceeded(false), @@ -334,14 +356,10 @@ nmCalculationDllPebiSolverTask::nmCalculationDllPebiSolverTask( m_bManagerUseActive = true; } - // 第二步:任务创建时冻结两个输入版本,提交前还会再次核对。 - if(m_pDataManager != nullptr && - m_pDataManager->getNumericalAnalysisCase() != nullptr) { - m_nGridInputRevision = - m_pDataManager->getNumericalAnalysisCase()->getGridInputRevision(); - m_nResultInputRevision = - m_pDataManager->getNumericalAnalysisCase()->getResultInputRevision(); - } + // 第二步:在创建线程内一次性冻结全部值输入。run() 启动后不得再读取 + // DataManager、井、储层、PVT 或属性插值容器。 + m_bInputSnapshotValid = + captureInputSnapshot(sAutoFitTargetWellName); } nmCalculationDllPebiSolverTask::~nmCalculationDllPebiSolverTask() @@ -352,6 +370,8 @@ nmCalculationDllPebiSolverTask::~nmCalculationDllPebiSolverTask() wait(); } releaseDataManagerUse(); + delete m_pInputSnapshot; + m_pInputSnapshot = nullptr; } void nmCalculationDllPebiSolverTask::run() @@ -372,6 +392,144 @@ void nmCalculationDllPebiSolverTask::releaseDataManagerUse() } } +bool nmCalculationDllPebiSolverTask::captureInputSnapshot( + const QString& sAutoFitTargetWellName) +{ + if(m_pDataManager == nullptr || m_pInputSnapshot == nullptr || + QThread::currentThread() != m_pDataManager->thread()) { + qWarning() << "PEBI solver input must be captured on the DataManager thread."; + return false; + } + + nmDataNumericalAnalysisCase* pAnalysisCase = + m_pDataManager->getNumericalAnalysisCase(); + if(pAnalysisCase == nullptr) { + return false; + } + + // 第一步:冻结几何和求解输入版本。提交阶段会再次核对,期间发生任何编辑 + // 都只会让旧任务结果作废,不会影响后台正在读取的值快照。 + m_nGridInputRevision = pAnalysisCase->getGridInputRevision(); + m_nResultInputRevision = pAnalysisCase->getResultInputRevision(); + + const nmPebiSolverInputSnapshot oEmptySnapshot; + *m_pInputSnapshot = oEmptySnapshot; + m_pInputSnapshot->m_bAutoFitTargetOnly = + !sAutoFitTargetWellName.isEmpty(); + + if(m_pInputSnapshot->m_bAutoFitTargetOnly) { + nmDataWellBase* pTargetWell = + m_pDataManager->findWellByName(sAutoFitTargetWellName); + if(pTargetWell == nullptr || pTargetWell->getWellCode().isEmpty()) { + qWarning() << "Auto-fit target well is unavailable:" + << sAutoFitTargetWellName; + return false; + } + m_sAutoFitTargetWellCode = pTargetWell->getWellCode(); + } else { + m_sAutoFitTargetWellCode.clear(); + } + + // 第二步:捕获网格输入和求解场景。该调用只复制值,不执行 DLL,也不导出 + // CSV,因此任务创建不会等待另一个网格 DLL 或进行无关磁盘写入。 + nmCalculationPebiGrid* pGridService = + nmCalculationPebiGrid::getInstance(); + if(pGridService == nullptr || + !pGridService->captureInputSnapshot( + m_pDataManager, m_pInputSnapshot->m_oGridInput) || + m_pInputSnapshot->m_oGridInput.m_nGridInputRevision != + m_nGridInputRevision) { + return false; + } + + const QVector& vecWellOrder = + m_pInputSnapshot->m_oGridInput.m_vecSolverWellOrder; + m_pInputSnapshot->m_vecWellInputs.clear(); + m_pInputSnapshot->m_vecWellInputs.reserve(vecWellOrder.size()); + + // 第三步:捕获结果后处理仍需使用的井数据。数组与网格快照中的求解器顺序 + // 一一对应,手工裂缝保留空占位,真实井必须能通过 WellCode 唯一解析。 + for(int nWellIndex = 0; + nWellIndex < vecWellOrder.size(); + ++nWellIndex) { + const nmSolverWellRef& oWellRef = vecWellOrder[nWellIndex]; + nmPebiSolverWellInputSnapshot oWellInput; + oWellInput.m_sWellCode = oWellRef.m_sWellCode; + + if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture) { + m_pInputSnapshot->m_vecWellInputs.append(oWellInput); + continue; + } + + nmDataWellBase* pWellData = + m_pDataManager->findWellByCode(oWellRef.m_sWellCode); + if(pWellData == nullptr) { + qWarning() << "Solver well code is missing from Map:" + << oWellRef.m_sWellCode; + return false; + } + + oWellInput.m_bRealWell = true; + oWellInput.m_vecFlowPoints = pWellData->getFlowPoints(); + oWellInput.m_nFlowSectionIndex = pWellData->getIndexF(); + oWellInput.m_oLocation = QPointF( + pWellData->getX().getValue().toDouble(), + pWellData->getY().getValue().toDouble()); + oWellInput.m_bRateControlled = + m_pDataManager->getCalculationWellMode( + oWellRef.m_sWellCode) == NM_CaseWell_RateControlled; + + if(oWellInput.m_bRateControlled && + oWellInput.m_vecFlowPoints.size() < 2) { + qWarning() << "Rate-controlled well has no valid rate schedule:" + << oWellRef.m_sWellCode; + return false; + } + + m_pInputSnapshot->m_vecWellInputs.append(oWellInput); + } + + // 第四步:复制属性插值及 DLL 求解配置。这些设置只影响模型求解结果, + // 不应在工作线程中再次从 DataManager 查询。 + m_pInputSnapshot->m_vecPropertyDataSets = + m_pDataManager->getPropertyInterpolationDataSets(); + m_pInputSnapshot->m_nSolverType = + m_pDataManager->getPebiSolverType(); + m_pInputSnapshot->m_nOmpThreads = + m_pDataManager->getPebiOmpThreads(); + m_pInputSnapshot->m_nIluReuseSteps = + m_pDataManager->getPebiIluReuseSteps(); + + // 第五步:已有有效网格直接复制 DLL 输出;否则只登记“需要计算”,真正的 + // 网格 DLL 在后台使用上面的值快照执行一次,不再读写 DataManager。 + HX_NWTM_GRID_OUTPUT1 oGridOutput1; + HX_NWTM_GRID_OUTPUT2 oGridOutput2; + int nPebiCount = -1; + if(pGridService->copyCurrentGridFor(m_pDataManager, + oGridOutput1, + oGridOutput2, + nPebiCount)) { + m_pInputSnapshot->m_oGridResult.m_oGridOutput1 = oGridOutput1; + m_pInputSnapshot->m_oGridResult.m_oGridOutput2 = oGridOutput2; + m_pInputSnapshot->m_oGridResult.m_nPebiCount = nPebiCount; + m_pInputSnapshot->m_oGridResult.m_bSucceeded = true; + + if(!m_pInputSnapshot->m_bAutoFitTargetOnly) { + m_pInputSnapshot->m_pBaseGrid = + m_pDataManager->getUnstructuredGridCopy(); + if(m_pInputSnapshot->m_pBaseGrid == nullptr || + m_pInputSnapshot->m_pBaseGrid->GetNumberOfCells() <= 0) { + return false; + } + } + } else { + m_pInputSnapshot->m_bRequiresGridCalculation = true; + } + + return m_pInputSnapshot->m_vecWellInputs.size() == + vecWellOrder.size(); +} + bool nmCalculationDllPebiSolverTask::wasSuccessful() const { return m_lastRunSucceeded; @@ -387,14 +545,6 @@ int nmCalculationDllPebiSolverTask::getPebiCount() const return m_nPebiCount; } -void nmCalculationDllPebiSolverTask::setAutoFitTargetWell(const QString& wellName) -{ - nmDataWellBase* pWellData = m_pDataManager != nullptr - ? m_pDataManager->findWellByName(wellName) : nullptr; - m_sAutoFitTargetWellCode = pWellData != nullptr - ? pWellData->getWellCode() : QString(); -} - QVector > nmCalculationDllPebiSolverTask::getAutoFitResultPressure() const { return m_autoFitResultPressure; @@ -418,482 +568,142 @@ bool nmCalculationDllPebiSolverTask::execute() bool nmCalculationDllPebiSolverTask::execPebiMode() { - nmDataAnalyzeManager* pDataInstance = m_pDataManager; - nmCalculationPebiGrid* pGridInstance = nmCalculationPebiGrid::getInstance(); - if(pDataInstance == nullptr || pGridInstance == nullptr) { - return false; - } - - // 第一步:任务排队期间输入已经变化时直接作废,不启动一次注定过期的 DLL 求解。 - const nmDataNumericalAnalysisCase* pAnalysisCase = - pDataInstance->getNumericalAnalysisCase(); - if(pAnalysisCase == nullptr || - pAnalysisCase->getGridInputRevision() != m_nGridInputRevision || - pAnalysisCase->getResultInputRevision() != m_nResultInputRevision) { - return false; - } - // 旧结果在新结果完整生成并通过主线程版本校验前保持不变。失败任务只丢弃 - // 自己的局部快照,不能提前清空上一轮仍然完整的成果。 - m_bPendingFullResultReady = false; - m_vecPendingWellResults.clear(); - m_mapPendingTimeSteps.clear(); - m_pPendingResultGrid = nullptr; - - // 载入Pebi动态库 - HMODULE dll = LoadLibrary(L"HX_NWTM.dll"); - - if(dll) { - // 定义函数指针类型 - typedef void (*HX_NWTM_GRID_Func)(HX_NWTM_GRID_OUTPUT1&, HX_NWTM_GRID_OUTPUT2&, const HX_NWTM_GRID_INPUT&, std::string); - typedef void (*HX_NWTM_MODEL_Fun)(HX_NWTM_MODEL_OUTPUT&, const HX_NWTM_MODEL_INPUT&, std::string); - typedef void (*SetIntValueFunc)(int); - typedef int (*GetIntValueFunc)(); - - // 获取函数地址 - HX_NWTM_MODEL_Fun HX_NWTM_MODEL = (HX_NWTM_MODEL_Fun)GetProcAddress(dll, "HX_NWTM_MODEL"); - SetIntValueFunc setSolverType = (SetIntValueFunc)GetProcAddress(dll, "set_solvetype"); - SetIntValueFunc setOmpThreads = (SetIntValueFunc)GetProcAddress(dll, "set_omp_threads"); - SetIntValueFunc setIluReuseSteps = (SetIntValueFunc)GetProcAddress(dll, "set_ilu_reuse_steps"); - GetIntValueFunc getSolveTime = (GetIntValueFunc)GetProcAddress(dll, "getsolvetime"); - - if(HX_NWTM_MODEL == nullptr || setSolverType == nullptr - || setOmpThreads == nullptr || setIluReuseSteps == nullptr - || getSolveTime == nullptr) { - QString sLogMessage = "Failed to resolve PEBI solver configuration interface."; - qWarning() << sLogMessage; - zxLogInstance::getInstance()->writeLogF(sLogMessage); - FreeLibrary(dll); - return false; - } - - if(HX_NWTM_MODEL) { - HX_NWTM_GRID_OUTPUT1 oGridOutput1; - HX_NWTM_GRID_OUTPUT2 oGridOutput2; - int nGridPebiCount = -1; - - // 第一步:网格无效时按当前分析方案完整重建 DLL 输出和 VTK 网格, - // 不能只生成 p1/p2 后把缺少 VTK 对应关系的半成品交给求解器。 - if(!pGridInstance->isGridCurrentFor(pDataInstance)) { - if(!pGridInstance->meshGenPebi(pDataInstance)) { - FreeLibrary(dll); - return false; - } - } - - // 第二步:在同一把网格锁内复制完整快照,后续求解不再读取可变单例输出。 - if(!pGridInstance->copyCurrentGridFor(pDataInstance, - oGridOutput1, - oGridOutput2, - nGridPebiCount)) { - FreeLibrary(dll); - return false; - } - - // 参数定义,使用默认参数 - HX_NWTM_MODEL_OUTPUT p1; - HX_NWTM_MODEL_INPUT p0(oGridOutput2); - nmDataReservoir* pReservoirData = pDataInstance->getReservoirData(); - // 根据PVT参数获取结果选择求解器模型类型 - const int modelType = static_cast(pDataInstance->getSolverModelType()); - p0.T = modelType; - - // 获取PEBI网格存入井的顺序 - QVector vecWellsOrder = - pDataInstance->getSolverWellOrder(); - - p0.CS.C.resize(vecWellsOrder.size()); - p0.CS.S.resize(vecWellsOrder.size()); - - p0.Rate.t.resize(vecWellsOrder.size()); - p0.Rate.qo.resize(vecWellsOrder.size()); - p0.Rate.qw.resize(vecWellsOrder.size()); - p0.Rate.qg.resize(vecWellsOrder.size()); - - // 遍历井数据 - for(int i = 0; i < vecWellsOrder.size(); ++i) { - const nmSolverWellRef& oWellRef = vecWellsOrder[i]; - NM_WELL_MODEL wellType = oWellRef.m_eWellType; - - // 手工裂缝只占据 DLL 数组下标,不具有井筒参数和产量制度。 - if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture) { - // 清空裂缝的数据,因为头文件内部有五口井的数据,会传递给裂缝 - p0.CS.C[i] = 0; - p0.CS.S[i] = 0; - p0.Rate.t[i].clear(); - p0.Rate.qo[i].clear(); - p0.Rate.qw[i].clear(); - p0.Rate.qg[i].clear(); - continue; - } - - nmDataWellBase* pBaseWellData = - pDataInstance->findWellByCode(oWellRef.m_sWellCode); - if(pBaseWellData == nullptr) { - qWarning() << "Solver well code is missing from Map:" - << oWellRef.m_sWellCode; - FreeLibrary(dll); - return false; - } - - // 主动井必须有至少一个去除零点后的产量段;观察井允许完全没有产量制度。 - const NM_CASE_WELL_MODE eWellMode = - pDataInstance->getCalculationWellMode( - oWellRef.m_sWellCode); - if(eWellMode == NM_CaseWell_RateControlled && - pBaseWellData->getFlowPoints().size() < 2) { - qWarning() << "Rate-controlled well has no valid rate schedule:" - << oWellRef.m_sWellCode; - FreeLibrary(dll); - return false; - } - - // 处理直井 - if(wellType == NM_WELL_MODEL::Vertical_Well) { - nmDataVerticalWell* pWellData = - dynamic_cast(pBaseWellData); - if(pWellData == nullptr) { - return false; - } - - // 井筒储集系数 - p0.CS.C[i] = pWellData->getWellboreStorage().getValue().toDouble(); - // 表皮系数 - //p0.CS.S[i] = pWellData->getSkin().getValue().toDouble(); - // 获取第一段射孔的表皮系数 - p0.CS.S[i] = pWellData->getPerforation(0)->getSkin().getValue().toDouble(); - - // 流量数据 - QVector vecTimeQ = pWellData->getFlowPoints(); - - // 分离时间(x)和流量(y)数据 - QVector timeDataF; - QVector rateData; - - foreach(const QPointF& point, vecTimeQ) { - timeDataF.append(point.x()); // 时间数据 - rateData.append(point.y()); // 流量数据 - } - - // 删除 timeDataF 中的第一个元素 - if(!timeDataF.isEmpty()) { - timeDataF.remove(0); - } - - // 删除 rateData 中的第一个元素 - if(!rateData.isEmpty()) { - rateData.remove(0); - } - - //timeDataF.append(2000); // 时间数据 - //rateData.append(2000); // 流量数据 - - // TODO:根据井类型来分类,现在将油气水三个流量设置为一致 - - // 油井 - 填充到 qo - dVec1& timeValues = p0.Rate.t[i]; - timeValues.resize(timeDataF.size()); - std::copy(timeDataF.begin(), timeDataF.end(), timeValues.begin()); - p0.Rate.qo[i].resize(rateData.size()); - std::copy(rateData.begin(), rateData.end(), p0.Rate.qo[i].begin()); - - // 气井 - 填充到 qg - p0.Rate.qg[i].resize(rateData.size()); - std::copy(rateData.begin(), rateData.end(), p0.Rate.qg[i].begin()); - - // 水井 - 填充到 qw - p0.Rate.qw[i].resize(rateData.size()); - std::copy(rateData.begin(), rateData.end(), p0.Rate.qw[i].begin()); - } - else if(wellType == NM_WELL_MODEL::Vertical_Fractured_Well) { - nmDataVerticalFracturedWell* pWellData = - dynamic_cast(pBaseWellData); - if(pWellData == nullptr) { - return false; - } - - // 井筒储集系数 - p0.CS.C[i] = pWellData->getWellboreStorage().getValue().toDouble(); - // 表皮系数 - //p0.CS.S[i] = pWellData->getSkin().getValue().toDouble(); - // 获取第一段射孔的表皮系数 - p0.CS.S[i] = pWellData->getPerforation(0)->getSkin().getValue().toDouble(); - - // 流量数据 - QVector vecTimeQ = pWellData->getFlowPoints(); - // 分离时间(x)和流量(y)数据 - QVector timeDataF; - QVector rateData; - - foreach(const QPointF& point, vecTimeQ) { - timeDataF.append(point.x()); // 时间数据 - rateData.append(point.y()); // 流量数据 - } - - // 删除 timeDataF 中的第一个元素 - if(!timeDataF.isEmpty()) { - timeDataF.remove(0); - } - - // 删除 rateData 中的第一个元素 - if(!rateData.isEmpty()) { - rateData.remove(0); - } - - //timeDataF.append(2000); // 时间数据 - //rateData.append(2000); // 流量数据 - - // TODO:根据井类型来分类,现在将油气水三个流量设置为一致 - - // 油井 - 填充到 qo - dVec1& timeValues = p0.Rate.t[i]; - timeValues.resize(timeDataF.size()); - std::copy(timeDataF.begin(), timeDataF.end(), timeValues.begin()); - p0.Rate.qo[i].resize(rateData.size()); - std::copy(rateData.begin(), rateData.end(), p0.Rate.qo[i].begin()); - - // 气井 - 填充到 qg - p0.Rate.qg[i].resize(rateData.size()); - std::copy(rateData.begin(), rateData.end(), p0.Rate.qg[i].begin()); - - // 水井 - 填充到 qw - p0.Rate.qw[i].resize(rateData.size()); - std::copy(rateData.begin(), rateData.end(), p0.Rate.qw[i].begin()); - } - else if(wellType == NM_WELL_MODEL::Horizontal_Fractured_Well) { - nmDataHorizontalFracturedWell* pWellData = - dynamic_cast(pBaseWellData); - if(pWellData == nullptr) { - return false; - } - - // 井筒储集系数 - p0.CS.C[i] = pWellData->getWellboreStorage().getValue().toDouble(); - // 表皮系数 - //p0.CS.S[i] = pWellData->getSkin().getValue().toDouble(); - // 获取第一段射孔的表皮系数 - p0.CS.S[i] = pWellData->getPerforation(0)->getSkin().getValue().toDouble(); - - // 流量数据 - QVector vecTimeQ = pWellData->getFlowPoints(); - - // 分离时间(x)和流量(y)数据 - QVector timeDataF; - QVector rateData; + if(!m_bInputSnapshotValid || m_pInputSnapshot == nullptr) { + return false; + } - foreach(const QPointF& point, vecTimeQ) { - timeDataF.append(point.x()); // 时间数据 - rateData.append(point.y()); // 流量数据 - } + // 第一步:清空本次任务自己的输出。旧成果仍保留在 DataManager 中,只有主线程 + // 最终提交成功后才会被整体替换。 + m_bPendingFullResultReady = false; + m_vecPendingWellResults.clear(); + m_mapPendingTimeSteps.clear(); + m_pPendingResultGrid = nullptr; + m_autoFitResultPressure.clear(); + m_autoFitResultLogLog.clear(); + m_autoFitResultSemiLog.clear(); - // 删除 timeDataF 中的第一个元素 - if(!timeDataF.isEmpty()) { - timeDataF.remove(0); - } + nmPebiGridResult& oGridResult = m_pInputSnapshot->m_oGridResult; + if(m_pInputSnapshot->m_bRequiresGridCalculation) { + // 第二步:网格无效时只使用构造阶段捕获的值快照调用一次网格 DLL。 + // 自动拟合候选不创建 VTK;完整求解创建局部 VTK,完成后回主线程提交。 + const bool bCreateUnstructuredGrid = + !m_pInputSnapshot->m_bAutoFitTargetOnly; + if(!nmCalculationPebiGrid::getInstance()->calculateSnapshot( + m_pInputSnapshot->m_oGridInput, + oGridResult, + bCreateUnstructuredGrid)) { + return false; + } - // 删除 rateData 中的第一个元素 - if(!rateData.isEmpty()) { - rateData.remove(0); - } + if(bCreateUnstructuredGrid) { + if(oGridResult.m_pUnstructuredGrid == nullptr || + oGridResult.m_pUnstructuredGrid->GetNumberOfCells() <= 0) { + return false; + } - //timeDataF.append(2000); // 时间数据 - //rateData.append(2000); // 流量数据 + m_pInputSnapshot->m_pBaseGrid = + vtkSmartPointer::New(); + m_pInputSnapshot->m_pBaseGrid->DeepCopy( + oGridResult.m_pUnstructuredGrid); + m_pInputSnapshot->m_bGridResultNeedsCommit = true; + } + } - // TODO:根据井类型来分类,现在将油气水三个流量设置为一致 + if(!oGridResult.m_bSucceeded || + oGridResult.m_oGridOutput1.PEBI_cell.p.empty()) { + return false; + } - // 油井 - 填充到 qo - dVec1& timeValues = p0.Rate.t[i]; - timeValues.resize(timeDataF.size()); - std::copy(timeDataF.begin(), timeDataF.end(), timeValues.begin()); - p0.Rate.qo[i].resize(rateData.size()); - std::copy(rateData.begin(), rateData.end(), p0.Rate.qo[i].begin()); + // 第三步:由场景值快照重建完整模型输入。属性插值可以耗时,但它只读取任务 + // 自己的点集副本,因此放在工作线程不会阻塞或竞争界面数据。 + HX_NWTM_MODEL_INPUT oModelInput; + QString sInterpolationError; + if(!buildModelInputFromSnapshot(*m_pInputSnapshot, + oGridResult.m_oGridOutput2, + oModelInput, + sInterpolationError)) { + const QString sLogMessage = + QString("Property interpolation failed: %1") + .arg(sInterpolationError); + qWarning() << sLogMessage; + zxLogInstance::getInstance()->writeLogF(sLogMessage); + return false; + } - // 气井 - 填充到 qg - p0.Rate.qg[i].resize(rateData.size()); - std::copy(rateData.begin(), rateData.end(), p0.Rate.qg[i].begin()); + // 第四步:解析并配置模型 DLL。此后直到 DLL 返回都只使用局部输出和输入。 + HMODULE hModelModule = LoadLibrary(L"HX_NWTM.dll"); + if(hModelModule == nullptr) { + qWarning() << "Failed to load HX_NWTM.dll. Error code:" + << GetLastError(); + return false; + } - // 水井 - 填充到 qw - p0.Rate.qw[i].resize(rateData.size()); - std::copy(rateData.begin(), rateData.end(), p0.Rate.qw[i].begin()); - } - } - - // 第三步:统一整理每个外层槽位的产量角色和相别。 - // 上面的井型分支只负责提取一条原始产量曲线;这里集中决定它进入 qo/qg/qw, - // 避免同一口井同时向三个相写入相同源汇项。 - for(int nWellIndex = 0; - nWellIndex < vecWellsOrder.size(); - ++nWellIndex) { - const nmSolverWellRef& oWellRef = vecWellsOrder[nWellIndex]; - const bool bObservationWell = - oWellRef.m_eEntryKind == NM_SolverEntry_Well && - pDataInstance->getCalculationWellMode( - oWellRef.m_sWellCode) == NM_CaseWell_Observation; - - if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture || - bObservationWell) { - // 观察井和手工裂缝保留 Rate[i] 槽位,但不向压力方程提供源汇项。 - p0.Rate.t[nWellIndex].clear(); - p0.Rate.qo[nWellIndex].clear(); - p0.Rate.qg[nWellIndex].clear(); - p0.Rate.qw[nWellIndex].clear(); - continue; - } + typedef void (*HX_NWTM_MODEL_Fun)( + HX_NWTM_MODEL_OUTPUT&, + const HX_NWTM_MODEL_INPUT&, + std::string); + typedef void (*SetIntValueFunc)(int); + typedef int (*GetIntValueFunc)(); + + HX_NWTM_MODEL_Fun pfnModel = + reinterpret_cast( + GetProcAddress(hModelModule, "HX_NWTM_MODEL")); + SetIntValueFunc pfnSetSolverType = + reinterpret_cast( + GetProcAddress(hModelModule, "set_solvetype")); + SetIntValueFunc pfnSetOmpThreads = + reinterpret_cast( + GetProcAddress(hModelModule, "set_omp_threads")); + SetIntValueFunc pfnSetIluReuseSteps = + reinterpret_cast( + GetProcAddress(hModelModule, "set_ilu_reuse_steps")); + GetIntValueFunc pfnGetSolveTime = + reinterpret_cast( + GetProcAddress(hModelModule, "getsolvetime")); + + if(pfnModel == nullptr || pfnSetSolverType == nullptr || + pfnSetOmpThreads == nullptr || pfnSetIluReuseSteps == nullptr || + pfnGetSolveTime == nullptr) { + qWarning() << "Failed to resolve PEBI solver configuration interface."; + FreeLibrary(hModelModule); + return false; + } - const dVec1 vecRate = p0.Rate.qo[nWellIndex]; - p0.Rate.qo[nWellIndex].assign(vecRate.size(), 0.0); - p0.Rate.qg[nWellIndex].assign(vecRate.size(), 0.0); - p0.Rate.qw[nWellIndex].assign(vecRate.size(), 0.0); - - switch(static_cast(modelType)) { - case SMT_Gas_VariablePvt: - case SMT_Gas_PseudoPressure: - p0.Rate.qg[nWellIndex] = vecRate; - break; - case SMT_Water_ConstPvt: - case SMT_Water_VariablePvt: - p0.Rate.qw[nWellIndex] = vecRate; - break; - default: - // 当前界面只提供一条总产量曲线,多相模型暂按定产油量解释。 - p0.Rate.qo[nWellIndex] = vecRate; - break; - } - } - - // PVT数据:根据求解器模型类型填充PVT输入数据 - nmDataPvtParaForPebi* pebiPvtPara = pDataInstance->getPebiPvtPara(); - fillPvtInputByModel(p0, static_cast(p0.T), pebiPvtPara, pReservoirData); - - - // 基础数据(储层参数?) - if(pReservoirData != nullptr) { - p0.Base.Pi = pReservoirData->getInitialPressure().getValue().toDouble(); // 初始压力 - p0.Base.Cti = pReservoirData->getCt().getValue().toDouble(); // 综合压缩系数 - p0.Base.Cf = pReservoirData->getCf().getValue().toDouble(); // 岩石压缩系数 - p0.Base.Soi = pReservoirData->getSoi().getValue().toDouble(); // 初始含油饱和度 - p0.Base.Sgi = pReservoirData->getSgi().getValue().toDouble(); // 初始含气饱和度 - p0.Base.Swi = pReservoirData->getSwi().getValue().toDouble(); // 初始含水饱和度 - // 根据网格单元数设置每个单元的参数 - size_t cellCount = oGridOutput2.Trinodexy.size(); - // 数据层以mD保存渗透率,PEBI输入数组要求使用D。 - const double dPermeabilityDarcy = - nmCalculationUtils::milliDarcyToDarcy( - pReservoirData->getPermeability().getValue().toDouble()); - p0.Base.k = dVec1(cellCount, dPermeabilityDarcy); - p0.Base.phi = dVec1(cellCount, pReservoirData->getPorosity().getValue().toDouble()); - p0.Base.h = dVec1(cellCount, pReservoirData->getThickness().getValue().toDouble()); - } - - // 未启用的属性保留上面的单值数组;启用后才使用所选数据组覆盖. - QString interpolationError; - if(!applyPropertyInterpolation( - p0, - pDataInstance->getPropertyInterpolationDataSets(), - pDataInstance->getLicensePath(), - interpolationError)) { - QString logMessage = QString("Property interpolation failed: %1") - .arg(interpolationError); - qWarning() << logMessage; - zxLogInstance::getInstance()->writeLogF(logMessage); - FreeLibrary(dll); - return false; - } - - // 获取时间步长 - nmDataTimeStepSetting* pTimeStepSetting = pDataInstance->getTimeStep(); - if(pTimeStepSetting) { - p0.Base.d = pTimeStepSetting->getTimeGrowthExponent().getValue().toDouble(); - p0.Base.dt_Min = pTimeStepSetting->getMinDeltaTAttribute().getValue().toDouble(); - p0.Base.dt_Max = pTimeStepSetting->getMaxDeltaTAttribute().getValue().toDouble(); - } - - try { - nmDataAnalyzeManager* pDataManager = m_pDataManager; - QString licensePath = pDataManager->getLicensePath(); - std::string licensePathStd = licensePath.toStdString(); - int nSolverType = pDataManager->getPebiSolverType(); - setSolverType(nSolverType); - if(nSolverType == nmDataAnalyzeManager::PebiSolverCpuAccelerated) { - setOmpThreads(pDataManager->getPebiOmpThreads()); - setIluReuseSteps(pDataManager->getPebiIluReuseSteps()); - } + HX_NWTM_MODEL_OUTPUT oModelOutput; + try { + pfnSetSolverType(m_pInputSnapshot->m_nSolverType); + if(m_pInputSnapshot->m_nSolverType == + nmDataAnalyzeManager::PebiSolverCpuAccelerated) { + pfnSetOmpThreads(m_pInputSnapshot->m_nOmpThreads); + pfnSetIluReuseSteps(m_pInputSnapshot->m_nIluReuseSteps); + } - HX_NWTM_MODEL(p1, p0, licensePathStd); - m_nPebiCount = nGridPebiCount; - m_nSolveTimeMs = getSolveTime(); - if(m_sAutoFitTargetWellCode.isEmpty()) { - QString sSolveLog = QString("PEBI count: %1, solve time: %2 ms") - .arg(m_nPebiCount).arg(m_nSolveTimeMs); - qDebug() << sSolveLog; - zxLogInstance::getInstance()->writeLogF(sSolveLog); - } - } catch(const std::exception& e) { - // 捕获 C++ 标准异常 - qDebug() << QString("C++ Exception during HX_NWTM_MODEL call: %1").arg(e.what()); - // 输出到日志窗口 - logHX_NWTM_MODEL_INPUT_Simplified(p0); - - // 生成一个带有时间戳的文件名 - QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss"); - QString logFileName = QString("AAA_HX_NWTM_MODEL_INPUT_error_%1.txt").arg(timestamp); - - QString appDirPath = QCoreApplication::applicationDirPath(); - QString logFilePath = QDir(appDirPath).filePath(logFileName); - - // 将 p0 的内容写入文件 - this->saveHX_NWTM_MODEL_INPUT_ToTxt(p0, logFilePath); - - FreeLibrary(dll); // 异常发生,释放 DLL - return false; - } catch(...) { - // 捕获结构化异常 (SEH) - qDebug() << "SEH Exception Occurred during HX_NWTM_MODEL call"; - // 输出到日志窗口 - logHX_NWTM_MODEL_INPUT_Simplified(p0); - - // 生成一个带有时间戳的文件名 - QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss"); - QString logFileName = QString("AAA_HX_NWTM_MODEL_INPUT_error_%1.txt").arg(timestamp); - - QString appDirPath = QCoreApplication::applicationDirPath(); - QString logFilePath = QDir(appDirPath).filePath(logFileName); - - // 将 p0 的内容写入文件 - this->saveHX_NWTM_MODEL_INPUT_ToTxt(p0, logFilePath); - FreeLibrary(dll); // 异常发生,释放 DLL - return false; - } - - // DLL 返回后再次检查版本,求解期间发生的井位、角色或参数编辑会使本任务作废。 - pAnalysisCase = pDataInstance->getNumericalAnalysisCase(); - if(pAnalysisCase == nullptr || - pAnalysisCase->getGridInputRevision() != m_nGridInputRevision || - pAnalysisCase->getResultInputRevision() != m_nResultInputRevision || - !pAnalysisCase->isGridValid()) { - FreeLibrary(dll); - return false; - } - - // 只构造任务局部结果;完整求解由主线程在完成回调中一次性提交。 - bool success = this->buildPebiModeResult(p1, - modelType, - oGridOutput1); - - FreeLibrary(dll); // 卸载 DLL - return success; - - } else { - DWORD error = GetLastError(); - printf("Failed to resolve HX_NWTM_GRID function. Error code: %lu\n", error); - } - - FreeLibrary(dll); // 卸载 DLL - } else { - DWORD error = GetLastError(); - printf("Failed to load DLL. Error code: %lu\n", error); - } + pfnModel(oModelOutput, + oModelInput, + m_pInputSnapshot->m_oGridInput.m_sLicensePath + .toStdString()); + m_nPebiCount = oGridResult.m_nPebiCount; + m_nSolveTimeMs = pfnGetSolveTime(); + } catch(const std::exception& e) { + qWarning() << QString("C++ Exception during HX_NWTM_MODEL call: %1") + .arg(e.what()); + logHX_NWTM_MODEL_INPUT_Simplified(oModelInput); + FreeLibrary(hModelModule); + return false; + } catch(...) { + qWarning() << "SEH Exception Occurred during HX_NWTM_MODEL call"; + logHX_NWTM_MODEL_INPUT_Simplified(oModelInput); + FreeLibrary(hModelModule); + return false; + } - return false; + // 第五步:把井曲线和场压力构造成任务局部结果。该函数同样只读取输入快照。 + const bool bSucceeded = buildPebiModeResult( + oModelOutput, + oModelInput.T, + oGridResult.m_oGridOutput1); + FreeLibrary(hModelModule); + return bSucceeded; } std::vector HX_logderivative(const std::vector& x, const std::vector& y, const int& n) @@ -922,12 +732,12 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( int modelType, const HX_NWTM_GRID_OUTPUT1& oGridOutput) { - nmDataAnalyzeManager* pDataInstance = m_pDataManager; - if(pDataInstance == nullptr) { + if(m_pInputSnapshot == nullptr) { return false; } - const bool autoFitTargetOnly = !m_sAutoFitTargetWellCode.isEmpty(); + const bool autoFitTargetOnly = + m_pInputSnapshot->m_bAutoFitTargetOnly; bool autoFitTargetFound = false; m_autoFitResultPressure.clear(); m_autoFitResultLogLog.clear(); @@ -948,13 +758,17 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( return false; } - // 获取参与求解的井的顺序 - QVector vecWellsOrder = - pDataInstance->getSolverWellOrder(); + // 井顺序与后处理输入都来自任务启动时的同一份快照。 + const QVector& vecWellsOrder = + m_pInputSnapshot->m_oGridInput.m_vecSolverWellOrder; + const QVector& vecWellInputs = + m_pInputSnapshot->m_vecWellInputs; // 第一步:真实井必须逐口具有与公共时间轴等长的井底压力。 // 观察井虽然没有源汇项,也必须由 DLL 返回压力,否则不能形成有效多井结果。 - if(vecWellsOrder.isEmpty() || p1.t.empty()) { + if(vecWellsOrder.isEmpty() || + vecWellInputs.size() != vecWellsOrder.size() || + p1.t.empty()) { return false; } for(size_t nTimeIndex = 0; nTimeIndex < p1.t.size(); ++nTimeIndex) { @@ -972,7 +786,8 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( if(nIndex >= static_cast(p1.pw.size()) || p1.pw[nIndex].empty() || p1.pw[nIndex].size() != p1.t.size() || - pDataInstance->findWellByCode(oWellRef.m_sWellCode) == nullptr) { + !vecWellInputs[nIndex].m_bRealWell || + vecWellInputs[nIndex].m_sWellCode != oWellRef.m_sWellCode) { qWarning() << "Incomplete well pressure returned for WellCode:" << oWellRef.m_sWellCode; return false; @@ -992,6 +807,8 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( // 遍历每口井,处理其数据 for(int wellIdx = 0; wellIdx < vecWellsOrder.size(); ++wellIdx) { const nmSolverWellRef& oWellRef = vecWellsOrder[wellIdx]; + const nmPebiSolverWellInputSnapshot& oWellInput = + vecWellInputs[wellIdx]; if(autoFitTargetOnly && oWellRef.m_sWellCode != m_sAutoFitTargetWellCode) { @@ -1016,17 +833,16 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( vvecPressure.append(currentWellTime); vvecPressure.append(currentWellPressure); - // 3.2 计算双对数和半对数曲线数据并存储到局部变量 - nmDataWellBase* pWellData = - pDataInstance->findWellByCode(oWellRef.m_sWellCode); - if(pWellData == nullptr) { + // 3.2 计算双对数和半对数曲线数据。井对象可能已被界面修改,后台只允许 + // 使用构造阶段复制出的流量、流动段和坐标。 + if(!oWellInput.m_bRealWell || + oWellInput.m_sWellCode != oWellRef.m_sWellCode) { return false; } const bool bRateControlled = - pDataInstance->getCalculationWellMode( - oWellRef.m_sWellCode) == NM_CaseWell_RateControlled && - pWellData->getFlowPoints().size() >= 2; + oWellInput.m_bRateControlled && + oWellInput.m_vecFlowPoints.size() >= 2; vvecLogLog.clear(); vvecSemiLog.clear(); @@ -1058,7 +874,8 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( } // 准备流量段数据 - QVector vecTimeQ = pWellData->getFlowPoints(); + const QVector& vecTimeQ = + oWellInput.m_vecFlowPoints; // 防御空流量数据,避免 -1 转成 std::vector 的巨大无符号长度。 int nTimeNumQ = qMax(0, vecTimeQ.size() - 1); // 移除第一个 0 点 std::vector timeQ(nTimeNumQ); @@ -1071,7 +888,8 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( // 调用外部 DLL 计算双对数曲线 std::vector logPreResultFromDll; // 存储 DLL 的计算结果 - int iSectionFlowIndex = pWellData->getIndexF(); + const int iSectionFlowIndex = + oWellInput.m_nFlowSectionIndex; // 第一步:无产量观察井只接收压力结果,不调用依赖产量制度的曲线 DLL。 HMODULE hMod_solver = nTimeNumQ > 0 @@ -1176,9 +994,7 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( oWellResult.m_vecPressure = vvecPressure; oWellResult.m_vecLogLog = vvecLogLog; oWellResult.m_vecSemiLog = vvecSemiLog; - oWellResult.m_oLocation = QPointF( - pWellData->getX().getValue().toDouble(), - pWellData->getY().getValue().toDouble()); + oWellResult.m_oLocation = oWellInput.m_oLocation; m_vecPendingWellResults.append(oWellResult); } } @@ -1279,9 +1095,9 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( return false; } - // 第六步:复制与本次场数据对应的基础网格,并在最后才发布“快照就绪”。 - m_pPendingResultGrid = - pDataInstance->getUnstructuredGridCopy(); + // 第六步:基础网格在任务创建或局部建网阶段已经完成深拷贝。后台结果构造 + // 不再读取 DataManager 中可能被网格窗口替换的 VTK 指针。 + m_pPendingResultGrid = m_pInputSnapshot->m_pBaseGrid; if(m_pPendingResultGrid == nullptr || m_pPendingResultGrid->GetNumberOfCells() <= 0) { return false; @@ -1308,7 +1124,8 @@ bool nmCalculationDllPebiSolverTask::commitResult( { // 第一步:自动拟合只返回目标曲线,不允许提交完整成果;手工求解必须回到 // DataManager 所属线程执行,保证界面看不到逐项替换过程中的中间状态。 - if(!m_sAutoFitTargetWellCode.isEmpty() || + if(m_pInputSnapshot == nullptr || + m_pInputSnapshot->m_bAutoFitTargetOnly || !m_bPendingFullResultReady || pDataManager == nullptr || pDataManager != m_pDataManager || @@ -1321,7 +1138,6 @@ bool nmCalculationDllPebiSolverTask::commitResult( if(pAnalysisCase == nullptr || pAnalysisCase->getGridInputRevision() != m_nGridInputRevision || pAnalysisCase->getResultInputRevision() != m_nResultInputRevision || - !pAnalysisCase->isGridValid() || m_mapPendingTimeSteps.isEmpty() || m_pPendingResultGrid == nullptr || m_pPendingResultGrid->GetNumberOfCells() <= 0) { @@ -1345,14 +1161,27 @@ bool nmCalculationDllPebiSolverTask::commitResult( vecTargetWells.append(pWellData); } - // 第三步:最后一次登记版本。登记失败时旧成果仍未被修改;登记成功后当前 + // 第三步:任务后台生成了新网格时,先在当前主线程按同一输入版本提交。 + // 已有网格路径则再次确认其仍然有效。两条路径都不允许旧任务覆盖新编辑。 + if(m_pInputSnapshot->m_bGridResultNeedsCommit) { + if(!nmCalculationPebiGrid::getInstance()->commitSnapshotResult( + pDataManager, + m_pInputSnapshot->m_oGridInput, + m_pInputSnapshot->m_oGridResult)) { + return false; + } + } else if(!pAnalysisCase->isGridValid()) { + return false; + } + + // 第四步:最后一次登记版本。登记失败时旧成果仍未被修改;登记成功后当前 // 主线程事件不会被其他编辑操作插入,因此后续替换不存在可恢复失败分支。 if(!pAnalysisCase->markResultsAvailableIfCurrent( m_nGridInputRevision, m_nResultInputRevision)) { return false; } - // 第四步:版本检查和对象解析全部通过后,在当前主线程事件内整体替换。 + // 第五步:版本检查和对象解析全部通过后,在当前主线程事件内整体替换。 // 这些 setter 不发事件也不包含可恢复失败分支,外部只能在本函数返回后看到新成果。 pDataManager->clearWellLocations(); for(int nIndex = 0; nIndex < m_vecPendingWellResults.size(); ++nIndex) { @@ -1376,7 +1205,7 @@ bool nmCalculationDllPebiSolverTask::commitResult( m_dPendingScalarMax); pDataManager->setResultBaseGrid(m_pPendingResultGrid); - // 第五步:清除待提交标志,防止重复完成信号再次覆盖后续结果。 + // 第六步:清除待提交标志,防止重复完成信号再次覆盖后续结果。 m_bPendingFullResultReady = false; return true; } diff --git a/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp b/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp index 5dce28c8..643ef9f6 100644 --- a/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -37,7 +38,7 @@ namespace { const int CONST_PVT_POINT_COUNT = 200; // PEBI 网格生成器和其 DLL 都使用进程级共享状态,所有公开读写入口必须串行化。 -// 使用递归锁是因为 meshGenPebi() 会在持锁状态下调用 generateOutputPara()。 +// 保留递归锁以兼容可能在持锁网格入口中调用缓存查询的旧代码路径。 QMutex s_oPebiGridMutex(QMutex::Recursive); std::vector buildConstantPvtVector(double value) @@ -179,6 +180,7 @@ nmCalculationPebiGrid* nmCalculationPebiGrid::getInstance() nmCalculationPebiGrid::nmCalculationPebiGrid() : m_nPebiCount(-1), + m_nCachedGridInputRevision(0), m_pDataManager(nullptr) { // 默认值来自HX_NWTM_GRID_INPUT构造函数 @@ -199,51 +201,6 @@ nmCalculationPebiGrid::~nmCalculationPebiGrid() { } -bool nmCalculationPebiGrid::meshGenPebi(nmDataAnalyzeManager* pDataManager) -{ - QMutexLocker oLocker(&s_oPebiGridMutex); - - if(pDataManager == nullptr) { - return false; - } - - const nmDataNumericalAnalysisCase* pAnalysisCase = - pDataManager->getNumericalAnalysisCase(); - if(pAnalysisCase == nullptr) { - return false; - } - const quint64 nGridInputRevision = - pAnalysisCase->getGridInputRevision(); - - // 第一步:每次重划都先丢弃旧网格输出和求解器顺序。 - // 重划动作本身不是输入修改,因此这里不增加几何输入版本号。 - pDataManager->clearSolverWellOrder(); - - if(this->generateOutputPara(pDataManager) == false) { - return false; - } - - // 第二步:根据 DLL 输出生成 VTK 网格,转换失败时保持方案无有效网格。 - vtkSmartPointer pGrid = this->createPebiUnstructuredGrid(p1); - if(pGrid == nullptr || pGrid->GetNumberOfCells() <= 0) { - pDataManager->clearSolverWellOrder(); - return false; - } - - // 第三步:VTK 网格和 DLL 井顺序完整,并且后台划分期间输入未变化, - // 才提交网格并标记当前版本有效。 - nmDataAnalyzeManager* pCurDataManager = m_pDataManager; - Q_ASSERT(nullptr != pCurDataManager); - if(pCurDataManager == nullptr || - !pCurDataManager->markPebiGridBuiltIfCurrent(nGridInputRevision)) { - pDataManager->clearSolverWellOrder(); - return false; - } - pCurDataManager->setUnstructuredGrid(pGrid); - - return true; -} - void nmCalculationPebiGrid::setGridControl( double dGridControl, nmDataAnalyzeManager* pDataManager) @@ -279,39 +236,44 @@ void nmCalculationPebiGrid::clearGridData( p2 = HX_NWTM_GRID_OUTPUT2(); m_dGridControl = p0.GridControl; m_nPebiCount = -1; + m_nCachedGridInputRevision = 0; m_pDataManager = nullptr; } -bool nmCalculationPebiGrid::isGridCurrentFor( - const nmDataAnalyzeManager* pDataManager) const -{ - QMutexLocker oLocker(&s_oPebiGridMutex); - return pDataManager != nullptr && - m_pDataManager == pDataManager && - pDataManager->isPebiGridValid() && - !p1.PEBI_cell.p.empty(); -} - bool nmCalculationPebiGrid::copyCurrentGridFor( const nmDataAnalyzeManager* pDataManager, HX_NWTM_GRID_OUTPUT1& oGridOutput1, HX_NWTM_GRID_OUTPUT2& oGridOutput2, int& nPebiCount) const { - QMutexLocker oLocker(&s_oPebiGridMutex); + // 求解任务在主线程构造,不能为了复制缓存等待正在运行的网格 DLL。 + // 锁忙时返回 false,任务会在后台基于自己的值快照生成局部网格。 + if(!s_oPebiGridMutex.tryLock()) { + return false; + } // 第一步:在复制前同时校验所有权、输入版本和实际网格内容。 if(pDataManager == nullptr || m_pDataManager != pDataManager || !pDataManager->isPebiGridValid() || + pDataManager->getNumericalAnalysisCase() == nullptr || + m_nCachedGridInputRevision != + pDataManager->getNumericalAnalysisCase()->getGridInputRevision() || p1.PEBI_cell.p.empty()) { + s_oPebiGridMutex.unlock(); return false; } // 第二步:一次性复制两份 DLL 输出,保证求解期间使用同一版本的网格快照。 - oGridOutput1 = p1; - oGridOutput2 = p2; - nPebiCount = m_nPebiCount; + try { + oGridOutput1 = p1; + oGridOutput2 = p2; + nPebiCount = m_nPebiCount; + } catch(...) { + s_oPebiGridMutex.unlock(); + throw; + } + s_oPebiGridMutex.unlock(); return true; } @@ -407,11 +369,13 @@ void nmCalculationPebiGrid::logCurrentState() qDebug() << stateMsg; } -bool nmCalculationPebiGrid::meshGenPebiBoundary(HX_NWTM_GRID_INPUT& inputObj) +bool nmCalculationPebiGrid::meshGenPebiBoundary( + nmDataAnalyzeManager* pDataManager, + HX_NWTM_GRID_INPUT& inputObj) { // 1、从数据中心获取边界数据 - nmDataOutline* pOutlineData = m_pDataManager != nullptr - ? m_pDataManager->getOutlineData() : nullptr; + nmDataOutline* pOutlineData = pDataManager != nullptr + ? pDataManager->getOutlineData() : nullptr; if(pOutlineData == nullptr) { return false; @@ -448,7 +412,10 @@ bool nmCalculationPebiGrid::meshGenPebiBoundary(HX_NWTM_GRID_INPUT& inputObj) return true; } -bool nmCalculationPebiGrid::meshGenPebiWells(HX_NWTM_GRID_INPUT& inputObj) +bool nmCalculationPebiGrid::meshGenPebiWells( + nmDataAnalyzeManager* pDataManager, + HX_NWTM_GRID_INPUT& inputObj, + QVector& vecSolverWellOrder) { // 从数据中心获取井数据 @@ -458,7 +425,6 @@ bool nmCalculationPebiGrid::meshGenPebiWells(HX_NWTM_GRID_INPUT& inputObj) // return true; //} - nmDataAnalyzeManager* pDataManager = m_pDataManager; if(pDataManager == nullptr) { return false; } @@ -471,8 +437,8 @@ bool nmCalculationPebiGrid::meshGenPebiWells(HX_NWTM_GRID_INPUT& inputObj) // 斜井 inputObj.InclinedWell.clear(); - // 第一步:清空上一次网格产生的求解器顺序。用户勾选的包含井集合不会被修改。 - pDataManager->clearSolverWellOrder(); + // 第一步:求解器顺序只写入局部快照,后台成功前不修改分析方案。 + vecSolverWellOrder.clear(); QSet setEffectiveWellCodes; QVector vecEffectiveWells = @@ -500,10 +466,10 @@ bool nmCalculationPebiGrid::meshGenPebiWells(HX_NWTM_GRID_INPUT& inputObj) well[2] = pVerticalWell->getRadius().getValue().toDouble(); inputObj.VerticalWell.push_back(well); // 求解器顺序只保存 WellCode,井名不参与数组映射。 - pDataManager->appendSolverWell( - nmSolverWellRef(-1, - NM_WELL_MODEL::Vertical_Well, - pVerticalWell->getWellCode())); + vecSolverWellOrder.append(nmSolverWellRef( + -1, + NM_WELL_MODEL::Vertical_Well, + pVerticalWell->getWellCode())); } } @@ -524,10 +490,10 @@ bool nmCalculationPebiGrid::meshGenPebiWells(HX_NWTM_GRID_INPUT& inputObj) // FC直接取井对象中保存的裂缝导流能力,0表示无限导流。 crack[5] = pVerticalFracturedWell->getDfc().getValue().toDouble(); inputObj.FractureVerticalWell.push_back(crack); - pDataManager->appendSolverWell( - nmSolverWellRef(-1, - NM_WELL_MODEL::Vertical_Fractured_Well, - pVerticalFracturedWell->getWellCode())); + vecSolverWellOrder.append(nmSolverWellRef( + -1, + NM_WELL_MODEL::Vertical_Fractured_Well, + pVerticalFracturedWell->getWellCode())); } } } @@ -557,10 +523,10 @@ bool nmCalculationPebiGrid::meshGenPebiWells(HX_NWTM_GRID_INPUT& inputObj) } inputObj.MultistageFracturedHorizontalWell.push_back(vMultistageFracturedHorizontalWell); - pDataManager->appendSolverWell( - nmSolverWellRef(-1, - NM_WELL_MODEL::Horizontal_Fractured_Well, - pHorizontalFracturedWell->getWellCode())); + vecSolverWellOrder.append(nmSolverWellRef( + -1, + NM_WELL_MODEL::Horizontal_Fractured_Well, + pHorizontalFracturedWell->getWellCode())); } } @@ -642,11 +608,13 @@ bool nmCalculationPebiGrid::meshGenPebiWells(HX_NWTM_GRID_INPUT& inputObj) //return true; } -bool nmCalculationPebiGrid::meshGenPebiFault(HX_NWTM_GRID_INPUT & inputObj) +bool nmCalculationPebiGrid::meshGenPebiFault( + nmDataAnalyzeManager* pDataManager, + HX_NWTM_GRID_INPUT & inputObj) { // 从数据中心获取断层数据 - QVector vecDataFault = m_pDataManager != nullptr - ? m_pDataManager->getFaultDataList() : QVector(); + QVector vecDataFault = pDataManager != nullptr + ? pDataManager->getFaultDataList() : QVector(); inputObj.Fault.clear(); // 断层,没有宽度 @@ -675,11 +643,14 @@ bool nmCalculationPebiGrid::meshGenPebiFault(HX_NWTM_GRID_INPUT & inputObj) return true; } -bool nmCalculationPebiGrid::meshGenPebiCrack(HX_NWTM_GRID_INPUT &inputObj) +bool nmCalculationPebiGrid::meshGenPebiCrack( + nmDataAnalyzeManager* pDataManager, + HX_NWTM_GRID_INPUT &inputObj, + QVector& vecSolverWellOrder) { // 从数据中心获取裂缝几何数据 - QVector vecDataFracture = m_pDataManager != nullptr - ? m_pDataManager->getFractureDataList() : QVector(); + QVector vecDataFracture = pDataManager != nullptr + ? pDataManager->getFractureDataList() : QVector(); // 裂缝,将所有裂缝当一个 裂缝直井来处理 for(int i = 0; i < vecDataFracture.size(); i++) { @@ -711,12 +682,12 @@ bool nmCalculationPebiGrid::meshGenPebiCrack(HX_NWTM_GRID_INPUT &inputObj) //int iIndex = inputObj.VerticalWell.size() + inputObj.FractureVerticalWell.size(); // 添加到自定义井顺序数组 - m_pDataManager->insertSolverWell( - iIndex, - nmSolverWellRef(iIndex, - NM_WELL_MODEL::Unknow_Well, - QString(), - NM_SolverEntry_ManualFracture)); + vecSolverWellOrder.insert( + iIndex, + nmSolverWellRef(iIndex, + NM_WELL_MODEL::Unknow_Well, + QString(), + NM_SolverEntry_ManualFracture)); } } } @@ -954,451 +925,442 @@ vtkSmartPointer nmCalculationPebiGrid::createPebiUnstructur return pUnstructuredGrid; } -//bool nmCalculationPebiGrid::generateOutputPara() { -// HMODULE dll = LoadLibrary(L"HX_NWTM.dll"); -// -// if(!dll) { -// DWORD error = GetLastError(); -// printf("Failed to load DLL. Error code: %lu\n", error); -// return false; -// } -// -// // 定义函数指针类型 -// typedef void (*HX_NWTM_GRID_Func)(HX_NWTM_GRID_OUTPUT1&, HX_NWTM_GRID_OUTPUT2&, const HX_NWTM_GRID_INPUT&, std::string); -// //typedef void (*HX_NWTM_GRID_Func)(HX_NWTM_GRID_OUTPUT1&, HX_NWTM_GRID_OUTPUT2&, const HX_NWTM_GRID_INPUT&, std::string); -// -// // 获取函数地址 -// HX_NWTM_GRID_Func HX_NWTM_GRID = (HX_NWTM_GRID_Func)GetProcAddress(dll, "HX_NWTM_GRID"); -// -// if(!HX_NWTM_GRID) { -// DWORD error = GetLastError(); -// printf("Failed to resolve function. Error code: %lu\n", error); -// FreeLibrary(dll); -// return false; -// } -// -// try { -// // 执行网格生成逻辑 -// this->meshGenPebiBoundary(p0); -// this->meshGenPebiWells(p0); -// this->meshGenPebiFault(p0); -// this->meshGenPebiCrack(p0); -// -// nmDataAnalyzeManager* pDataManager = nmDataAnalyzeManager::getCurrentInstance(); -// QString licensePath = pDataManager->getLicensePath(); -// std::string licensePathStd = licensePath.toStdString(); -// -// // 调用DLL函数 -// HX_NWTM_GRID(p1, p2, p0, licensePathStd); -// -// } catch(const std::exception& e) { -// zxLogInstance::getInstance()->writeLogF(QString("C++ Exception: %1").arg(e.what())); -// logCurrentState(); -// // 记录输入参数信息 -// logInputParameters(p0); -// // 确保只释放一次 -// FreeLibrary(dll); -// return false; -// } catch(...) { -// // 结构化异常,需要修改项目属性,项目属性 → C/C++ → Code Generation → Enable C++ Exceptions → 选择 Yes with SEH Exceptions (/EHa) -// zxLogInstance::getInstance()->writeLogF("SEH Exception Occurred"); -// logCurrentState(); -// // 记录输入参数信息 -// logInputParameters(p0); -// // 确保只释放一次 -// FreeLibrary(dll); -// return false; -// } -// -// // 确保只释放一次 -// FreeLibrary(dll); -// return true; -//} - -bool nmCalculationPebiGrid::generateOutputPara(nmDataAnalyzeManager* pDataManager) +bool nmCalculationPebiGrid::captureInputSnapshot( + nmDataAnalyzeManager* pDataManager, + nmPebiGridInputSnapshot& oSnapshot) { - QMutexLocker oLocker(&s_oPebiGridMutex); + // 使用具名常量触发复制赋值,兼容 Qt 4.8 配套的 VS2010 运行库 ABI。 + const nmPebiGridInputSnapshot oEmptySnapshot; + oSnapshot = oEmptySnapshot; + if(pDataManager == nullptr) { + return false; + } - // 每次调用都绑定明确的数据管理器;未显式传入时只为旧调用点读取当前窗口。 - m_pDataManager = pDataManager != nullptr - ? pDataManager : nmDataAnalyzeManager::getCurrentInstance(); - if(m_pDataManager == nullptr) { + // DataManager 及井对象都由所属线程维护。快照只允许在该线程创建,后台任务 + // 随后只读取复制出的 STL/Qt 值类型,不能再次访问这些可变对象。 + if(QThread::currentThread() != pDataManager->thread()) { + qWarning() << "PEBI input snapshot must be captured on the DataManager thread."; return false; } - // GridControl 属于分析方案;切换窗口后不能沿用单例中上一成果的值。 - m_dGridControl = m_pDataManager->getPebiGridControl(); + const nmDataNumericalAnalysisCase* pAnalysisCase = + pDataManager->getNumericalAnalysisCase(); + if(pAnalysisCase == nullptr) { + return false; + } - HMODULE dll = LoadLibrary(L"HX_NWTM.dll"); - if(dll == nullptr) { - qWarning() << "Failed to load HX_NWTM.dll. Error code:" << GetLastError(); + // 第一步:先记录版本并复制网格控制参数。调用方必须在启动后台线程前完成本函数。 + oSnapshot.m_nGridInputRevision = + pAnalysisCase->getGridInputRevision(); + oSnapshot.m_oGridInput = HX_NWTM_GRID_INPUT(); + oSnapshot.m_oGridInput.GridControl = + pDataManager->getPebiGridControl(); + + // 第二步:把 Map 中的边界、有效井、断层和手工裂缝全部转成 DLL 值类型。 + if(!meshGenPebiBoundary(pDataManager, oSnapshot.m_oGridInput) || + !meshGenPebiWells(pDataManager, + oSnapshot.m_oGridInput, + oSnapshot.m_vecSolverWellOrder) || + !meshGenPebiFault(pDataManager, oSnapshot.m_oGridInput) || + !meshGenPebiCrack(pDataManager, + oSnapshot.m_oGridInput, + oSnapshot.m_vecSolverWellOrder)) { return false; } - // 定义函数指针类型 - typedef void (*HX_NWTM_GRID_Func)(HX_NWTM_GRID_OUTPUT1&, HX_NWTM_GRID_OUTPUT2&, const HX_NWTM_GRID_INPUT&, std::string); - typedef int (*GetIntValueFunc)(); + for(int nIndex = 0; + nIndex < oSnapshot.m_vecSolverWellOrder.size(); + ++nIndex) { + oSnapshot.m_vecSolverWellOrder[nIndex].m_nSolverIndex = nIndex; + } - // ===== 调用网格生成器 ===== - HX_NWTM_GRID_Func HX_NWTM_GRID = (HX_NWTM_GRID_Func)GetProcAddress(dll, "HX_NWTM_GRID"); - GetIntValueFunc getPebiCount = (GetIntValueFunc)GetProcAddress(dll, "getPEBInum"); + // 第三步:真实井必须与本次有效计算井一一对应;手工裂缝只占槽位,不参与集合比较。 + QSet setEffectiveWellCodes; + const QVector vecEffectiveWells = + pDataManager->getEffectiveCalculationWells(); + for(int nIndex = 0; nIndex < vecEffectiveWells.size(); ++nIndex) { + setEffectiveWellCodes.insert(vecEffectiveWells[nIndex].m_sWellCode); + } + + QSet setOrderedWellCodes; + bool bSolverOrderValid = !setEffectiveWellCodes.isEmpty(); + for(int nIndex = 0; + bSolverOrderValid && + nIndex < oSnapshot.m_vecSolverWellOrder.size(); + ++nIndex) { + const nmSolverWellRef& oWellRef = + oSnapshot.m_vecSolverWellOrder[nIndex]; + if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture) { + continue; + } - if(!HX_NWTM_GRID || !getPebiCount) { - DWORD error = GetLastError(); - printf("Failed to resolve function. Error code: %lu\n", error); - FreeLibrary(dll); + bSolverOrderValid = + oWellRef.m_eEntryKind == NM_SolverEntry_Well && + setEffectiveWellCodes.contains(oWellRef.m_sWellCode) && + !setOrderedWellCodes.contains(oWellRef.m_sWellCode); + if(bSolverOrderValid) { + setOrderedWellCodes.insert(oWellRef.m_sWellCode); + } + } + bSolverOrderValid = bSolverOrderValid && + setOrderedWellCodes == setEffectiveWellCodes; + if(!bSolverOrderValid) { + qWarning() << "PEBI solver order does not match effective wells."; return false; } - try { - // PEBI生成器是单例,输入输出结构会保留上一次状态;重新划分前先清空,避免拖动井后复用旧网格数据 - p0 = HX_NWTM_GRID_INPUT(); - p1 = HX_NWTM_GRID_OUTPUT1(); - p2 = HX_NWTM_GRID_OUTPUT2(); - m_nPebiCount = -1; - - // PEBI网格划分输入参数:GridControl来自界面 - p0.GridControl = m_dGridControl; - - // 第一步:构造全部网格输入,任一基础输入失败都不得继续调用 DLL。 - if(!this->meshGenPebiBoundary(p0) || - !this->meshGenPebiWells(p0) || - !this->meshGenPebiFault(p0) || - !this->meshGenPebiCrack(p0)) { - m_pDataManager->clearSolverWellOrder(); - FreeLibrary(dll); - return false; - } - - // 第二步:真实井顺序必须完整覆盖有效计算井。 - // 裂缝几何不完整、井型不支持等情况会在这里明确失败,禁止静默少算一口井。 - QSet setEffectiveWellCodes; - QVector vecEffectiveWells = - m_pDataManager->getEffectiveCalculationWells(); - for(int nIndex = 0; nIndex < vecEffectiveWells.size(); ++nIndex) { - setEffectiveWellCodes.insert(vecEffectiveWells[nIndex].m_sWellCode); - } - - QSet setOrderedWellCodes; - QVector vecBuiltSolverOrder = - m_pDataManager->getSolverWellOrder(); - bool bSolverOrderValid = !setEffectiveWellCodes.isEmpty(); - for(int nIndex = 0; - bSolverOrderValid && nIndex < vecBuiltSolverOrder.size(); - ++nIndex) { - const nmSolverWellRef& oWellRef = vecBuiltSolverOrder[nIndex]; - if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture) { - continue; - } - bSolverOrderValid = - oWellRef.m_eEntryKind == NM_SolverEntry_Well && - setEffectiveWellCodes.contains(oWellRef.m_sWellCode) && - !setOrderedWellCodes.contains(oWellRef.m_sWellCode); - if(bSolverOrderValid) { - setOrderedWellCodes.insert(oWellRef.m_sWellCode); - } - } - bSolverOrderValid = bSolverOrderValid && - setOrderedWellCodes == setEffectiveWellCodes; - if(!bSolverOrderValid) { - qWarning() << "PEBI solver order does not match effective wells."; - m_pDataManager->clearSolverWellOrder(); - FreeLibrary(dll); - return false; - } - - // ===== 导出当前相态 scene ===== - { - nmDataBinaryTools::NM_PEBI_SCENE scene; - nmDataAnalyzeManager* dm = m_pDataManager; - - // 1. 网格基础数据 - scene.version = 1; - scene.D = p0.D; - scene.GridControl = p0.GridControl; - scene.Boundary = p0.Boundary; - scene.VerticalWell = p0.VerticalWell; - scene.HorizontalWell = p0.HorizontalWell; - scene.FractureVerticalWell = p0.FractureVerticalWell; - scene.MultistageFracturedHorizontalWell = p0.MultistageFracturedHorizontalWell; - scene.InclinedWell = p0.InclinedWell; - scene.Fault = p0.Fault; - - // 2. 井顺序信息 - QVector vecSolverOrder = dm->getSolverWellOrder(); - scene.wellType.clear(); - scene.wellName.clear(); - scene.wellType.reserve(vecSolverOrder.size()); - scene.wellName.reserve(vecSolverOrder.size()); - - for(int nIndex = 0; nIndex < vecSolverOrder.size(); ++nIndex) { - const nmSolverWellRef& oWellRef = vecSolverOrder[nIndex]; - scene.wellType.push_back((int)oWellRef.m_eWellType); - - // ML 场景格式仍有显示名称字段,但身份映射已经由 WellCode 完成。 - nmDataWellBase* pWellData = - dm->findWellByCode(oWellRef.m_sWellCode); - scene.wellName.push_back(pWellData != nullptr - ? pWellData->getWellName() - : QString()); - } + // 第四步:场景和授权路径同样在捕获阶段读取,后台不再接触任何井对象。 + if(!buildPebiScene(pDataManager, + oSnapshot.m_oGridInput, + oSnapshot.m_vecSolverWellOrder, + oSnapshot.m_oScene)) { + return false; + } + oSnapshot.m_sLicensePath = pDataManager->getLicensePath(); - // ===== 3. 求解器参数 ===== - - // 3.1 求解器类型:与真实求解器使用同一个模型类型,不再根据相态推断。 - nmDataReservoir* pReservoirData = dm->getReservoirData(); - NM_SOLVER_MODEL_TYPE solverModelType = dm->getSolverModelType(); - scene.solverType = static_cast(solverModelType); - - // 3.2 Rate流量数据 - scene.Rate.t.resize(vecSolverOrder.size()); - scene.Rate.qo.resize(vecSolverOrder.size()); - scene.Rate.qg.resize(vecSolverOrder.size()); - scene.Rate.qw.resize(vecSolverOrder.size()); - - for(int wellIdx = 0; wellIdx < vecSolverOrder.size(); ++wellIdx) { - const nmSolverWellRef& oWellRef = vecSolverOrder[wellIdx]; - - // 手工裂缝和观察井都必须保留外层槽位,但不能提供源汇项。 - if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture || - dm->getCalculationWellMode( - oWellRef.m_sWellCode) == NM_CaseWell_Observation) { - scene.Rate.t[wellIdx].clear(); - scene.Rate.qo[wellIdx].clear(); - scene.Rate.qg[wellIdx].clear(); - scene.Rate.qw[wellIdx].clear(); - continue; - } - - // 从井数据中提取流量曲线 - nmDataWellBase* pWellData = - dm->findWellByCode(oWellRef.m_sWellCode); - - if(pWellData) { - QVector vecTimeQ = pWellData->getFlowPoints(); - - // 移除第一个点(0,0) - if(!vecTimeQ.isEmpty() && vecTimeQ[0].x() == 0 && vecTimeQ[0].y() == 0) { - vecTimeQ.remove(0); - } - - // 提取时间和流量 - std::vector timeData; - std::vector rateData; - - for(size_t i = 0; i < vecTimeQ.size(); ++i) { - const QPointF& pt = vecTimeQ[i]; - timeData.push_back(pt.x()); - rateData.push_back(pt.y()); - } - - scene.Rate.t[wellIdx] = timeData; - scene.Rate.qo[wellIdx].assign(rateData.size(), 0.0); - scene.Rate.qg[wellIdx].assign(rateData.size(), 0.0); - scene.Rate.qw[wellIdx].assign(rateData.size(), 0.0); - - // 单相模型只向对应相写入产注制度,避免一条曲线同时成为三相源汇。 - if(solverModelType == SMT_Gas_VariablePvt || - solverModelType == SMT_Gas_PseudoPressure) { - scene.Rate.qg[wellIdx] = rateData; - } else if(solverModelType == SMT_Water_ConstPvt || - solverModelType == SMT_Water_VariablePvt) { - scene.Rate.qw[wellIdx] = rateData; - } else { - scene.Rate.qo[wellIdx] = rateData; - } - } - } + oSnapshot.m_bValid = true; + return true; +} - // 油水两相算例按定产油量处理:导入流量传给qo,qg和qw使用等长零数组。 - if(solverModelType == SMT_Oil_Water_TwoPhase) { - for(size_t wellIdx = 0; wellIdx < scene.Rate.t.size(); ++wellIdx) { - scene.Rate.qg[wellIdx].assign(scene.Rate.t[wellIdx].size(), 0.0); - scene.Rate.qw[wellIdx].assign(scene.Rate.t[wellIdx].size(), 0.0); - } - } +bool nmCalculationPebiGrid::buildPebiScene( + nmDataAnalyzeManager* pDataManager, + const HX_NWTM_GRID_INPUT& oGridInput, + const QVector& vecSolverWellOrder, + nmDataBinaryTools::NM_PEBI_SCENE& oScene) +{ + if(pDataManager == nullptr) { + return false; + } - // 3.3 CS井筒参数 - scene.CS.C.resize(vecSolverOrder.size()); - scene.CS.S.resize(vecSolverOrder.size()); + // 第一步:复制网格基础数据和井显示信息。 + const nmDataBinaryTools::NM_PEBI_SCENE oEmptyScene; + oScene = oEmptyScene; + oScene.version = 1; + oScene.D = oGridInput.D; + oScene.GridControl = oGridInput.GridControl; + oScene.Boundary = oGridInput.Boundary; + oScene.VerticalWell = oGridInput.VerticalWell; + oScene.HorizontalWell = oGridInput.HorizontalWell; + oScene.FractureVerticalWell = oGridInput.FractureVerticalWell; + oScene.MultistageFracturedHorizontalWell = + oGridInput.MultistageFracturedHorizontalWell; + oScene.InclinedWell = oGridInput.InclinedWell; + oScene.Fault = oGridInput.Fault; + + oScene.wellType.clear(); + oScene.wellName.clear(); + oScene.wellType.reserve(vecSolverWellOrder.size()); + oScene.wellName.reserve(vecSolverWellOrder.size()); + for(int nIndex = 0; nIndex < vecSolverWellOrder.size(); ++nIndex) { + const nmSolverWellRef& oWellRef = vecSolverWellOrder[nIndex]; + oScene.wellType.push_back(static_cast(oWellRef.m_eWellType)); + + nmDataWellBase* pWellData = + pDataManager->findWellByCode(oWellRef.m_sWellCode); + oScene.wellName.push_back(pWellData != nullptr + ? pWellData->getWellName() + : QString()); + } - for(int wellIdx = 0; wellIdx < vecSolverOrder.size(); ++wellIdx) { - const nmSolverWellRef& oWellRef = vecSolverOrder[wellIdx]; + // 第二步:按求解器模型填充每个槽位的产量制度。 + const NM_SOLVER_MODEL_TYPE eSolverModelType = + pDataManager->getSolverModelType(); + oScene.solverType = static_cast(eSolverModelType); + oScene.Rate.t.resize(vecSolverWellOrder.size()); + oScene.Rate.qo.resize(vecSolverWellOrder.size()); + oScene.Rate.qg.resize(vecSolverWellOrder.size()); + oScene.Rate.qw.resize(vecSolverWellOrder.size()); + + for(int nWellIndex = 0; + nWellIndex < vecSolverWellOrder.size(); + ++nWellIndex) { + const nmSolverWellRef& oWellRef = + vecSolverWellOrder[nWellIndex]; + + // 手工裂缝和观察井保留外层槽位,但不提供源汇项。 + if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture || + pDataManager->getCalculationWellMode( + oWellRef.m_sWellCode) == NM_CaseWell_Observation) { + continue; + } - if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture) { - scene.CS.C[wellIdx] = 0.0; - scene.CS.S[wellIdx] = 0.0; - continue; - } + nmDataWellBase* pWellData = + pDataManager->findWellByCode(oWellRef.m_sWellCode); + if(pWellData == nullptr) { + continue; + } - nmDataWellBase* pWellData = - dm->findWellByCode(oWellRef.m_sWellCode); + QVector vecTimeQ = pWellData->getFlowPoints(); + if(!vecTimeQ.isEmpty() && + vecTimeQ[0].x() == 0.0 && + vecTimeQ[0].y() == 0.0) { + vecTimeQ.remove(0); + } - if(pWellData) { - scene.CS.C[wellIdx] = pWellData->getWellboreStorage().getValue().toDouble(); + std::vector vecTime; + std::vector vecRate; + vecTime.reserve(vecTimeQ.size()); + vecRate.reserve(vecTimeQ.size()); + for(int nPointIndex = 0; + nPointIndex < vecTimeQ.size(); + ++nPointIndex) { + vecTime.push_back(vecTimeQ[nPointIndex].x()); + vecRate.push_back(vecTimeQ[nPointIndex].y()); + } - // 获取第一段射孔的表皮系数 - if(pWellData->getPerforationCount() > 0) { - scene.CS.S[wellIdx] = pWellData->getPerforation(0)->getSkin().getValue().toDouble(); - } else { - scene.CS.S[wellIdx] = 0.0; - } - } - } + oScene.Rate.t[nWellIndex] = vecTime; + oScene.Rate.qo[nWellIndex].assign(vecRate.size(), 0.0); + oScene.Rate.qg[nWellIndex].assign(vecRate.size(), 0.0); + oScene.Rate.qw[nWellIndex].assign(vecRate.size(), 0.0); + + if(eSolverModelType == SMT_Gas_VariablePvt || + eSolverModelType == SMT_Gas_PseudoPressure) { + oScene.Rate.qg[nWellIndex] = vecRate; + } else if(eSolverModelType == SMT_Water_ConstPvt || + eSolverModelType == SMT_Water_VariablePvt) { + oScene.Rate.qw[nWellIndex] = vecRate; + } else { + oScene.Rate.qo[nWellIndex] = vecRate; + } + } - // 3.4 井流量段索引 - scene.wellFlowSectionIndex.resize(vecSolverOrder.size()); + // 油水两相当前按定产油量处理,气、水产量数组保持等长零值。 + if(eSolverModelType == SMT_Oil_Water_TwoPhase) { + for(size_t nWellIndex = 0; + nWellIndex < oScene.Rate.t.size(); + ++nWellIndex) { + oScene.Rate.qg[nWellIndex].assign( + oScene.Rate.t[nWellIndex].size(), 0.0); + oScene.Rate.qw[nWellIndex].assign( + oScene.Rate.t[nWellIndex].size(), 0.0); + } + } - for(int wellIdx = 0; wellIdx < vecSolverOrder.size(); ++wellIdx) { - const nmSolverWellRef& oWellRef = vecSolverOrder[wellIdx]; + // 第三步:复制井筒储集、表皮和流量段索引。 + oScene.CS.C.resize(vecSolverWellOrder.size()); + oScene.CS.S.resize(vecSolverWellOrder.size()); + oScene.wellFlowSectionIndex.resize(vecSolverWellOrder.size()); + + for(int nWellIndex = 0; + nWellIndex < vecSolverWellOrder.size(); + ++nWellIndex) { + const nmSolverWellRef& oWellRef = + vecSolverWellOrder[nWellIndex]; + if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture) { + oScene.CS.C[nWellIndex] = 0.0; + oScene.CS.S[nWellIndex] = 0.0; + oScene.wellFlowSectionIndex[nWellIndex] = 1; + continue; + } - if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture) { - scene.wellFlowSectionIndex[wellIdx] = 1; // 默认值 - continue; - } + nmDataWellBase* pWellData = + pDataManager->findWellByCode(oWellRef.m_sWellCode); + if(pWellData == nullptr) { + oScene.wellFlowSectionIndex[nWellIndex] = 1; + continue; + } - nmDataWellBase* pWellData = - dm->findWellByCode(oWellRef.m_sWellCode); + oScene.CS.C[nWellIndex] = + pWellData->getWellboreStorage().getValue().toDouble(); + oScene.CS.S[nWellIndex] = + pWellData->getPerforationCount() > 0 + ? pWellData->getPerforation(0)->getSkin() + .getValue().toDouble() + : 0.0; + oScene.wellFlowSectionIndex[nWellIndex] = + pWellData->getIndexF(); + } - if(pWellData) { - scene.wellFlowSectionIndex[wellIdx] = pWellData->getIndexF(); - } else { - scene.wellFlowSectionIndex[wellIdx] = 1; // 默认值 - } - } + // 第四步:复制 PVT、拟压力、储层和时间步参数。 + nmDataReservoir* pReservoirData = + pDataManager->getReservoirData(); + nmDataPvtParaForPebi* pPvtData = + pDataManager->getPebiPvtPara(); + fillScenePvtByModel(oScene, + eSolverModelType, + pPvtData, + pReservoirData); + fillScenePseudoPressureTable(oScene, + eSolverModelType, + pDataManager); + + if(pReservoirData != nullptr) { + oScene.Base.Pi = pReservoirData->getInitialPressure() + .getValue().toDouble(); + oScene.Base.Cti = pReservoirData->getCt() + .getValue().toDouble(); + oScene.Base.Cf = pReservoirData->getCf() + .getValue().toDouble(); + oScene.Base.Soi = pReservoirData->getSoi() + .getValue().toDouble(); + oScene.Base.Sgi = pReservoirData->getSgi() + .getValue().toDouble(); + oScene.Base.Swi = pReservoirData->getSwi() + .getValue().toDouble(); + oScene.Base.k_ref = nmCalculationUtils::milliDarcyToDarcy( + pReservoirData->getPermeability() + .getValue().toDouble()); + oScene.Base.phi_ref = pReservoirData->getPorosity() + .getValue().toDouble(); + oScene.Base.h_ref = pReservoirData->getThickness() + .getValue().toDouble(); + } - // 3.5 PVT数据:常数PVT和变化PVT采用与真实求解器相同的填充方式。 - nmDataPvtParaForPebi* pvt = dm->getPebiPvtPara(); - fillScenePvtByModel(scene, solverModelType, pvt, pReservoirData); - fillScenePseudoPressureTable(scene, solverModelType, dm); - - // 3.6 Base储层参数 - if(pReservoirData) { - scene.Base.Pi = pReservoirData->getInitialPressure().getValue().toDouble(); - scene.Base.Cti = pReservoirData->getCt().getValue().toDouble(); - scene.Base.Cf = pReservoirData->getCf().getValue().toDouble(); - scene.Base.Soi = pReservoirData->getSoi().getValue().toDouble(); - scene.Base.Sgi = pReservoirData->getSgi().getValue().toDouble(); - scene.Base.Swi = pReservoirData->getSwi().getValue().toDouble(); - - // 保存参考值(用于Python采样) - // 训练场景最终会直接填入PEBI输入结构,写文件前转换为求解器使用的D。 - scene.Base.k_ref = nmCalculationUtils::milliDarcyToDarcy( - pReservoirData->getPermeability().getValue().toDouble()); - scene.Base.phi_ref = pReservoirData->getPorosity().getValue().toDouble(); - scene.Base.h_ref = pReservoirData->getThickness().getValue().toDouble(); - } + nmDataTimeStepSetting* pTimeStepSetting = + pDataManager->getTimeStep(); + if(pTimeStepSetting != nullptr) { + oScene.Base.d = pTimeStepSetting->getTimeGrowthExponent() + .getValue().toDouble(); + oScene.Base.dt_Min = pTimeStepSetting->getMinDeltaTAttribute() + .getValue().toDouble(); + oScene.Base.dt_Max = pTimeStepSetting->getMaxDeltaTAttribute() + .getValue().toDouble(); + } - // 3.7 时间步长设置 - nmDataTimeStepSetting* pTimeStepSetting = dm->getTimeStep(); + return true; +} - if(pTimeStepSetting) { - scene.Base.d = pTimeStepSetting->getTimeGrowthExponent().getValue().toDouble(); - scene.Base.dt_Min = pTimeStepSetting->getMinDeltaTAttribute().getValue().toDouble(); - scene.Base.dt_Max = pTimeStepSetting->getMaxDeltaTAttribute().getValue().toDouble(); - } +bool nmCalculationPebiGrid::calculateSnapshot( + const nmPebiGridInputSnapshot& oSnapshot, + nmPebiGridResult& oResult, + bool bCreateUnstructuredGrid) +{ + QMutexLocker oLocker(&s_oPebiGridMutex); - // 4. 保存到文件 - QDir appDir(QCoreApplication::applicationDirPath()); + const nmPebiGridResult oEmptyResult; + oResult = oEmptyResult; + if(!oSnapshot.m_bValid) { + return false; + } - //QDir dataDir(appDir.filePath("../../3rd/Data")); - QDir dataDir(appDir.filePath("../../ML/nmWTAI-ML/data/temp")); + HMODULE hGridModule = LoadLibrary(L"HX_NWTM.dll"); + if(hGridModule == nullptr) { + qWarning() << "Failed to load HX_NWTM.dll. Error code:" + << GetLastError(); + return false; + } - if(!dataDir.exists()) { - dataDir.mkpath("."); // 确保 Data 目录存在 - } + typedef void (*HX_NWTM_GRID_Func)( + HX_NWTM_GRID_OUTPUT1&, + HX_NWTM_GRID_OUTPUT2&, + const HX_NWTM_GRID_INPUT&, + std::string); + typedef int (*GetIntValueFunc)(); + + HX_NWTM_GRID_Func pfnGenerateGrid = + reinterpret_cast( + GetProcAddress(hGridModule, "HX_NWTM_GRID")); + GetIntValueFunc pfnGetPebiCount = + reinterpret_cast( + GetProcAddress(hGridModule, "getPEBInum")); + if(pfnGenerateGrid == nullptr || pfnGetPebiCount == nullptr) { + qWarning() << "Failed to resolve HX_NWTM grid functions. Error code:" + << GetLastError(); + FreeLibrary(hGridModule); + return false; + } - QString typedFileName = QString("scene_%1.bin").arg(solverModelFileTag(solverModelType)); - QString typedOutPath = dataDir.filePath(typedFileName); - bool ok = nmDataBinaryTools::savePebiSceneBin(typedOutPath, scene); + try { + // 第一步:后台写出已经值化的场景,不再回查当前成果或井对象。 + QDir oAppDir(QCoreApplication::applicationDirPath()); + QDir oDataDir(oAppDir.filePath( + "../../ML/nmWTAI-ML/data/temp")); + if(!oDataDir.exists()) { + oDataDir.mkpath("."); + } - if(!ok) { - zxLogInstance::getInstance()->writeLogF("savePebiSceneBin failed: " + nmDataBinaryTools::getLastError()); - FreeLibrary(dll); + const NM_SOLVER_MODEL_TYPE eSolverModelType = + static_cast( + oSnapshot.m_oScene.solverType); + const QString sScenePath = oDataDir.filePath( + QString("scene_%1.bin") + .arg(solverModelFileTag(eSolverModelType))); + if(!nmDataBinaryTools::savePebiSceneBin( + sScenePath, oSnapshot.m_oScene)) { + zxLogInstance::getInstance()->writeLogF( + "savePebiSceneBin failed: " + + nmDataBinaryTools::getLastError()); + FreeLibrary(hGridModule); + return false; + } + zxLogInstance::getInstance()->writeLogF( + "scene exported: " + sScenePath); + + // 第二步:DLL 输出先落在局部结果,失败时不清空单例缓存和已有成果。 + pfnGenerateGrid(oResult.m_oGridOutput1, + oResult.m_oGridOutput2, + oSnapshot.m_oGridInput, + oSnapshot.m_sLicensePath.toStdString()); + oResult.m_nPebiCount = pfnGetPebiCount(); + + // 自动拟合只需要 DLL 数组,可跳过 VTK 构造;网格任务必须生成完整 VTK。 + if(bCreateUnstructuredGrid) { + oResult.m_pUnstructuredGrid = + createPebiUnstructuredGrid( + oResult.m_oGridOutput1); + if(oResult.m_pUnstructuredGrid == nullptr || + oResult.m_pUnstructuredGrid->GetNumberOfCells() <= 0) { + FreeLibrary(hGridModule); return false; } - - zxLogInstance::getInstance()->writeLogF("scene exported: " + typedOutPath); } - // ===== 导出所有井:loglog + rate + pressure 到 temp 目录 ===== - { - nmDataAnalyzeManager* dm = m_pDataManager; - - if(dm) { - QDir appDir(QCoreApplication::applicationDirPath()); - QDir tempDir(appDir.filePath("../../ML/nmWTAI-ML/data/temp")); - - if(!tempDir.exists()) { - tempDir.mkpath("."); - } - - QString outDir = tempDir.absolutePath(); - - int okCount = 0; - int failCount = 0; - - auto exportOne = [&](nmDataWellBase * w) { - if(!w) return; - - bool ok1 = w->exportHistoryLogLogToCsv(outDir); - bool ok2 = w->exportHistoryRateToCsv(outDir); - bool ok3 = w->exportHistoryPressureToCsv(outDir); - - if(ok1 && ok2 && ok3) okCount++; - else failCount++; - }; - - // 1) 直井 - QVector verticalWells = dm->getVerticalWellData(); - for(int i = 0; i < verticalWells.size(); ++i) { - exportOne(verticalWells[i]); - } - - // 2) 垂直裂缝井 - QVector vfWells = dm->getVerticalFracturedWellData(); - for(int i = 0; i < vfWells.size(); ++i) { - exportOne(vfWells[i]); - } - - // 3) 多段压裂水平井 - QVector hfWells = dm->getHorizontalFracturedWellData(); - for(int i = 0; i < hfWells.size(); ++i) { - exportOne(hfWells[i]); - } - - zxLogInstance::getInstance()->writeLogF( - QString("History exported to temp. ok=%1, fail=%2, dir=%3") - .arg(okCount).arg(failCount).arg(outDir) - ); - } - } + oResult.m_bSucceeded = true; + } catch(const std::exception& e) { + zxLogInstance::getInstance()->writeLogF( + QString("C++ Exception: %1").arg(e.what())); + logInputParameters(oSnapshot.m_oGridInput); + FreeLibrary(hGridModule); + return false; + } catch(...) { + zxLogInstance::getInstance()->writeLogF( + "SEH Exception Occurred"); + logInputParameters(oSnapshot.m_oGridInput); + FreeLibrary(hGridModule); + return false; + } - nmDataAnalyzeManager* pBoundDataManager = m_pDataManager; - QString licensePath = pBoundDataManager->getLicensePath(); - std::string licensePathStd = licensePath.toStdString(); + FreeLibrary(hGridModule); + return true; +} - HX_NWTM_GRID(p1, p2, p0, licensePathStd); +void nmCalculationPebiGrid::commitOutputCache( + nmDataAnalyzeManager* pDataManager, + const nmPebiGridInputSnapshot& oSnapshot, + const nmPebiGridResult& oResult) +{ + p0 = oSnapshot.m_oGridInput; + p1 = oResult.m_oGridOutput1; + p2 = oResult.m_oGridOutput2; + m_dGridControl = oSnapshot.m_oGridInput.GridControl; + m_nPebiCount = oResult.m_nPebiCount; + m_nCachedGridInputRevision = oSnapshot.m_nGridInputRevision; + m_pDataManager = pDataManager; +} - // getPEBInum的计数在HX_NWTM_GRID阶段生成,必须在卸载DLL前保存。 - m_nPebiCount = getPebiCount(); +bool nmCalculationPebiGrid::commitSnapshotResult( + nmDataAnalyzeManager* pDataManager, + const nmPebiGridInputSnapshot& oSnapshot, + const nmPebiGridResult& oResult) +{ + QMutexLocker oLocker(&s_oPebiGridMutex); - } catch(const std::exception& e) { - zxLogInstance::getInstance()->writeLogF(QString("C++ Exception: %1").arg(e.what())); - logCurrentState(); - logInputParameters(p0); - m_pDataManager->clearSolverWellOrder(); - FreeLibrary(dll); + if(pDataManager == nullptr || + !oSnapshot.m_bValid || + !oResult.m_bSucceeded || + oResult.m_pUnstructuredGrid == nullptr) { return false; - } catch(...) { - zxLogInstance::getInstance()->writeLogF("SEH Exception Occurred"); - logCurrentState(); - logInputParameters(p0); - m_pDataManager->clearSolverWellOrder(); - FreeLibrary(dll); + } + + // 第一步:DataManager 在主线程内按版本整体替换井顺序、有效状态和 VTK 网格。 + if(!pDataManager->commitPebiGridResult( + oSnapshot.m_nGridInputRevision, + oSnapshot.m_vecSolverWellOrder, + oResult.m_pUnstructuredGrid)) { return false; } - FreeLibrary(dll); + // 第二步:只有成果提交成功后才更新进程级 DLL 缓存,旧任务不能覆盖新网格。 + commitOutputCache(pDataManager, oSnapshot, oResult); return true; } diff --git a/Src/nmNum/nmCalculation/nmCalculationPebiGridTask.cpp b/Src/nmNum/nmCalculation/nmCalculationPebiGridTask.cpp index a2e06af5..b5697fbf 100644 --- a/Src/nmNum/nmCalculation/nmCalculationPebiGridTask.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationPebiGridTask.cpp @@ -8,11 +8,18 @@ nmCalculationPebiGridTask::nmCalculationPebiGridTask( QObject *parent) : QThread(parent), m_pDataManager(pDataManager), - m_bManagerUseActive(false) + m_bManagerUseActive(false), + m_bSnapshotValid(false) { if(m_pDataManager != nullptr) { m_pDataManager->beginBackgroundUse(); m_bManagerUseActive = true; + + // 第一步:构造函数运行在主线程,此处一次性复制全部 Map 和求解输入。 + // run() 启动后不得再读取 DataManager 中的井、断层或参数对象。 + m_bSnapshotValid = + nmCalculationPebiGrid::getInstance()->captureInputSnapshot( + m_pDataManager, m_oInputSnapshot); } } @@ -26,16 +33,30 @@ nmCalculationPebiGridTask::~nmCalculationPebiGridTask() void nmCalculationPebiGridTask::run() { - // 当前网格生成链路只保留PEBI. - // 生成网格. - bool success = nmCalculationPebiGrid::getInstance()->meshGenPebi( - m_pDataManager); + // 第二步:后台只消费不可变快照,所有 DLL 输出和 VTK 数据先保存在任务内部。 + const bool success = m_bSnapshotValid && + nmCalculationPebiGrid::getInstance()->calculateSnapshot( + m_oInputSnapshot, m_oResult); // 完成通知可能引起成果窗口销毁,发信号前先解除 DataManager 使用权。 releaseDataManagerUse(); // 无论成功或失败都通知窗口,使其可以结束本轮任务状态并处理后续待刷新请求。 emit pebiGridGenerated(success); } +bool nmCalculationPebiGridTask::commitResult( + nmDataAnalyzeManager* pDataManager) +{ + // 第三步:该方法由网格窗口的主线程槽调用。版本不一致时只丢弃局部结果, + // 绝不覆盖用户编辑后已经失效或重新生成的网格。 + if(!m_bSnapshotValid || !m_oResult.m_bSucceeded || + pDataManager == nullptr) { + return false; + } + + return nmCalculationPebiGrid::getInstance()->commitSnapshotResult( + pDataManager, m_oInputSnapshot, m_oResult); +} + void nmCalculationPebiGridTask::releaseDataManagerUse() { if(m_bManagerUseActive && m_pDataManager != nullptr) { diff --git a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp index 454a9819..11778e6f 100644 --- a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp +++ b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp @@ -3584,6 +3584,41 @@ QVector nmDataAnalyzeManager::getSolverWellOrder() const return m_oNumericalAnalysisCase.getSolverWellOrder(); } +void nmDataAnalyzeManager::setSolverWellOrder( + const QVector& vecSolverWellOrder) +{ + m_oNumericalAnalysisCase.setSolverWellOrder(vecSolverWellOrder); +} + +bool nmDataAnalyzeManager::commitPebiGridResult( + quint64 nGridInputRevision, + const QVector& vecSolverWellOrder, + vtkSmartPointer pGrid) +{ + // 第一步:空井顺序或空网格都不是可求解成果,不能破坏当前有效网格。 + if(vecSolverWellOrder.isEmpty() || pGrid == nullptr || + pGrid->GetNumberOfCells() <= 0) { + return false; + } + + // 第二步:先检查后台任务捕获的输入版本。该函数只在主线程调用,检查和 + // 后续写入之间不会处理界面事件,因此可以作为一次不可分割的成果提交。 + if(m_oNumericalAnalysisCase.getGridInputRevision() != + nGridInputRevision) { + return false; + } + + // 第三步:整体替换 DLL 槽位顺序,再登记同一输入版本并替换 VTK 网格。 + // setSolverWellOrder() 会统一重编号,避免逐井追加期间暴露半成品顺序。 + m_oNumericalAnalysisCase.setSolverWellOrder(vecSolverWellOrder); + if(!m_oNumericalAnalysisCase.markGridBuiltIfCurrent( + nGridInputRevision)) { + return false; + } + m_pVtkUnstructuredGrid = pGrid; + return true; +} + void nmDataAnalyzeManager::markPebiGridBuilt() { m_oNumericalAnalysisCase.markGridBuilt(); diff --git a/Src/nmNum/nmSubWnd/nmSubWndGrid.cpp b/Src/nmNum/nmSubWnd/nmSubWndGrid.cpp index 321a28ef..ea29e311 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndGrid.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndGrid.cpp @@ -49,6 +49,8 @@ #include #include #include +#include +#include #include "ZxRstWnd.h" @@ -60,6 +62,7 @@ nmSubWndGrid::nmSubWndGrid(QWidget *parent, iSubWndBaseFit(parent, sExt), m_mainLayout(nullptr), m_vtkWidget(nullptr), + m_pGridLoadingLabel(nullptr), m_pDebounceTimer(nullptr), m_pDataManager(pDataManager), m_pGridTask(nullptr), @@ -134,7 +137,7 @@ void nmSubWndGrid::initUI() { QWidget* mainWidget = new QWidget(this); // 设置 mainWidget 的布局和尺寸策略 - mainWidget->setLayout(new QVBoxLayout(mainWidget)); // 确保 mainWidget 有自己的布局 + mainWidget->setLayout(new QStackedLayout(mainWidget)); // 加载背景和网格共用稳定显示区域。 mainWidget->layout()->setContentsMargins(0, 0, 0, 0); // 移除边距 mainWidget->layout()->setSpacing(0); // 移除间距 mainWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // 扩展策略 @@ -142,25 +145,27 @@ void nmSubWndGrid::initUI() setCentralWidget(mainWidget); // m_mainLayout 应该直接是 mainWidget 的布局 - m_mainLayout = qobject_cast(mainWidget->layout()); - - // 当前不是加载进来的网格,都需要根据Map几何生成新的PEBI网格 - if (m_bGeneratePebiGrid) - { + m_mainLayout = qobject_cast(mainWidget->layout()); + + // 第一步:先建立轻量加载页。颜色与网格 Renderer 保持一致,后台任务尚未 + // 返回时也能立即绘制正常背景,不会显示空 QVTKWidget 的黑色原生窗口。 + m_pGridLoadingLabel = new QLabel(tr("Generating grid..."), mainWidget); + m_pGridLoadingLabel->setAlignment(Qt::AlignCenter); + m_pGridLoadingLabel->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Expanding); + m_pGridLoadingLabel->setStyleSheet( + "QLabel { color: white; font-size: 14px; " + "background: qlineargradient(x1:0, y1:0, x2:0, y2:1, " + "stop:0 rgb(0, 128, 255), stop:1 rgb(171, 209, 240)); }"); + m_mainLayout->addWidget(m_pGridLoadingLabel); + + // 第二步:新建成果在渲染窗口就绪后立即启动后台划分;加载成果则直接显示 + // DataManager 中已有的网格,不进入生成状态。VTK 页只在网格有效后创建。 + if(m_bGeneratePebiGrid) { generatePebiGrid(); + } else { + showGridResult(); } - - m_vtkWidget = new nmWxGridVTKContainerWidget(m_pDataManager.data(), this); - m_vtkWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // 扩展策略 - m_mainLayout->addWidget(m_vtkWidget); - - // 首次创建 - //if(generatePebiGrid()) { - // // nmWxGridVTKContainerWidget 的尺寸策略 - // m_vtkWidget = new nmWxGridVTKContainerWidget(this); - // m_vtkWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // 扩展策略 - // m_mainLayout->addWidget(m_vtkWidget); - //} } QWidget* nmSubWndGrid::initUiMainWx() @@ -192,8 +197,11 @@ bool nmSubWndGrid::generatePebiGrid() // 将对话框中的GridControl传给PEBI输入参数GridControl pPebiGridGeneratorInstance->setGridControl(m_dPebiGridControl, pDataManager); - // 生成PEBI非结构化网格对象 - return pPebiGridGeneratorInstance->meshGenPebi(pDataManager); + // 首次手工生成无需等待防抖窗口。此时 VTK 背景已经完成初始化,任务启动 + // 后界面可以立即绘制;连续 dataChanged 仍由 updateGrid() 合并处理。 + showGridLoading(); + onDebounceTimeout(); + return true; } void nmSubWndGrid::updateGrid() @@ -204,6 +212,8 @@ void nmSubWndGrid::updateGrid() return; } + showGridLoading(); + // dataChanged可能连续触发,使用当前窗口自己的定时器合并为一次网格重建 if(!m_pDebounceTimer) { m_pDebounceTimer = new QTimer(this); @@ -230,9 +240,9 @@ void nmSubWndGrid::onDebounceTimeout() // 第二步:把窗口参数写回所属分析方案,再启动绑定同一管理器的后台任务。 nmCalculationPebiGrid::getInstance()->setGridControl( m_dPebiGridControl, pDataManager); - m_pGridTask = new nmCalculationPebiGridTask(pDataManager, this); + m_pGridTask = new nmCalculationPebiGridTask(pDataManager, this); connect(m_pGridTask, SIGNAL(pebiGridGenerated(bool)), - this, SLOT(onPebiGridGenerated(bool))); + this, SLOT(onPebiGridGenerated(bool)), Qt::QueuedConnection); connect(m_pGridTask, SIGNAL(finished()), this, SLOT(onPebiGridTaskFinished())); connect(m_pGridTask, SIGNAL(finished()), @@ -242,21 +252,24 @@ void nmSubWndGrid::onDebounceTimeout() void nmSubWndGrid::onPebiGridGenerated(bool success) { - if (!success) { - return; - } - // 删除旧的 VTK 控件 - if (m_vtkWidget) { - m_mainLayout->removeWidget(m_vtkWidget); - delete m_vtkWidget; - m_vtkWidget = nullptr; - } + // 第一步:信号通过队列回到主线程后才允许写 DataManager。后台任务只保存 + // 局部结果;如果用户在划分期间修改了输入,commitResult() 会按版本拒绝。 + nmCalculationPebiGridTask* pCompletedTask = + qobject_cast(sender()); + nmDataAnalyzeManager* pDataManager = m_pDataManager.data(); + if(success && pCompletedTask != nullptr && pDataManager != nullptr) { + success = pCompletedTask->commitResult(pDataManager); + } else { + success = false; + } - // 创建新的 VTK 控件,并将其加入布局 - m_vtkWidget = new nmWxGridVTKContainerWidget(m_pDataManager.data(), this); - // 设置新创建的 nmWxGridVTKContainerWidget 的尺寸策略** - m_vtkWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // 扩展策略 - m_mainLayout->addWidget(m_vtkWidget); + // 第二步:提交成功后创建绑定最新网格的 VTK 页。失败或版本过期时,如果 + // 没有排队中的更新,则恢复旧网格或显示失败状态,不能永久停留在加载文字。 + if(success) { + showGridResult(); + } else if(!m_bGridUpdatePending) { + showGridFailure(); + } } void nmSubWndGrid::onPebiGridTaskFinished() @@ -272,3 +285,51 @@ void nmSubWndGrid::onPebiGridTaskFinished() updateGrid(); } } + +void nmSubWndGrid::showGridLoading() +{ + if(m_pGridLoadingLabel == nullptr || m_mainLayout == nullptr) { + return; + } + + m_pGridLoadingLabel->setText(tr("Generating grid...")); + m_mainLayout->setCurrentWidget(m_pGridLoadingLabel); +} + +void nmSubWndGrid::showGridResult() +{ + if(m_mainLayout == nullptr || m_pDataManager.isNull()) { + return; + } + + // 网格已经在主线程完整提交,此处一次创建新的 VTK 页,不会向用户暴露 + // 没有 RenderWindow 的中间状态。 + if(m_vtkWidget != nullptr) { + m_mainLayout->removeWidget(m_vtkWidget); + delete m_vtkWidget; + m_vtkWidget = nullptr; + } + + m_vtkWidget = new nmWxGridVTKContainerWidget( + m_pDataManager.data(), centralWidget()); + m_vtkWidget->setSizePolicy(QSizePolicy::Expanding, + QSizePolicy::Expanding); + m_mainLayout->addWidget(m_vtkWidget); + m_mainLayout->setCurrentWidget(m_vtkWidget); +} + +void nmSubWndGrid::showGridFailure() +{ + if(m_mainLayout == nullptr) { + return; + } + + // 更新失败但旧网格仍可显示时保留旧成果;首次生成失败时在正常背景上给出 + // 明确状态,避免用户把空白或黑屏误认为程序仍在计算。 + if(m_vtkWidget != nullptr) { + m_mainLayout->setCurrentWidget(m_vtkWidget); + } else if(m_pGridLoadingLabel != nullptr) { + m_pGridLoadingLabel->setText(tr("Grid generation failed")); + m_mainLayout->setCurrentWidget(m_pGridLoadingLabel); + } +} diff --git a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp index d426205b..7f82be75 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp @@ -1644,8 +1644,8 @@ void nmSubWndMain::solveAndAnalyze() // TODO:关联到父窗口 m_pProgressDlg = new QProgressDialog(tr("Calculating, please wait..."), QString(), 0, 100, getMainWindow()); m_pProgressDlg->setWindowTitle(tr("Solver Progress")); - // 求解任务仍需读取 DataManager 中的 Qt 容器。应用级模态会冻结所有成果的 - // 编辑入口,避免 Qt 4.8 下后台读与前台写同时发生数据竞争。 + // 后台只读取任务值快照;应用级模态仍用于阻止重复启动、切换成果等会让 + // 用户误判当前计算归属的操作,数据安全不再依赖该对话框冻结界面。 m_pProgressDlg->setWindowModality(Qt::ApplicationModal); // 某些系统下,对话框右上角的关闭按钮可能仍存 @@ -2521,20 +2521,9 @@ void nmSubWndMain::onWellSelected(const QString& sWellCode) void nmSubWndMain::onGenerateButtonClicked() { - //QString sDir = ZxBaseUtil::getCurWellDirOf("Nm/PreProcessing"); - if(nmDataAnalyzeManager::getCurrentInstance()->getGridType() == NM_Grid_PEBI) { - nmCalculationPebiGrid* pPebiGridGeneratorInstance = nmCalculationPebiGrid::getInstance(); - nmDataAnalyzeManager* pDataManager = - nmDataAnalyzeManager::getCurrentInstance(); - if(!pPebiGridGeneratorInstance->isGridCurrentFor(pDataManager)) { - // Map、包含井或几何输入变化后,旧网格即使非空也不能继续求解。 - if(!pPebiGridGeneratorInstance->meshGenPebi(pDataManager)) { - return; - } - } - - // 求解计算 + // 求解任务创建时会捕获完整输入。网格无效时,工作线程只基于值快照生成 + // 一次局部网格,求解成功后再和全部结果一起回主线程提交。 this->solveAndAnalyze(); } }