diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index ca1b12f..c92d113 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 3d81ec7..32fae2b 100644 --- a/Bin/Config/Lang/cn/nmNum_cn.ts +++ b/Bin/Config/Lang/cn/nmNum_cn.ts @@ -2826,6 +2826,26 @@ Reason: %1 Solver Progress 计算进度 + + Preparing input snapshot... + 正在准备输入快照... + + + Stop + 停止 + + + Stopping; waiting for the current solver call to return... + 正在停止;等待当前求解器调用返回... + + + Updating result views... + 正在更新结果视图... + + + Committing results... + 正在提交计算结果... + Solver completed 求解完成 @@ -6942,6 +6962,61 @@ Average pressure in contour: %2 MPa The Kriging result contains an invalid value. Kriging 结果中包含无效数值。 + + Kriging calculation was cancelled. + Kriging 计算已取消。 + + + + nmCalculationDllPebiSolverTask + + Preparing input data... + 正在准备输入数据... + + + Checking grid cache... + 正在检查网格缓存... + + + Preparing result grid... + 正在准备结果网格... + + + Generating grid... + 正在生成网格... + + + Preparing model input... + 正在准备模型输入... + + + Waiting for solver... + 正在等待求解器... + + + Solving model... + 正在求解模型... + + + Processing well results... + 正在处理井结果... + + + Processing field results... + 正在处理场结果... + + + Capturing well data... + 正在捕获井数据... + + + Finalizing input snapshot... + 正在完成输入快照... + + + Capturing solver settings... + 正在捕获求解设置... + nmParameterField diff --git a/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h b/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h index a057971..dcb0b3b 100644 --- a/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h +++ b/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -18,6 +19,7 @@ class nmDataAnalyzeManager; class vtkDoubleArray; class vtkUnstructuredGrid; struct nmPebiSolverInputSnapshot; +struct nmPebiManualCaptureState; // 主窗口现在直接创建DLL求解线程任务,因此类需要导出,供nmSubWnd模块跨DLL使用。 class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { @@ -29,11 +31,13 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { * @param pDataManager 任务所属数据管理器,不转移所有权。 * @param sAutoFitTargetWellName 自动拟合目标井名;空值表示完整成果计算。 * @param parent Qt 父对象。 + * @param bDeferManualSnapshot 手工求解是否由界面事件循环分批捕获输入。 */ nmCalculationDllPebiSolverTask(QString sPostprocessingDir, nmDataAnalyzeManager* pDataManager = nullptr, const QString& sAutoFitTargetWellName = QString(), - QObject *parent = nullptr); + QObject *parent = nullptr, + bool bDeferManualSnapshot = false); ~nmCalculationDllPebiSolverTask(); /** @brief QThread 入口,记录本次 execute() 的成功状态并发送完成信号。 */ @@ -45,6 +49,16 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { int getSolveTimeMs() const; /** @brief 获取本次求解的PEBI网格数量。 */ int getPebiCount() const; + /** @brief 请求协作式停止;DLL 正在执行时会在返回后丢弃结果。 */ + void requestCancel(); + /** @brief 返回本次任务是否因手工停止而结束。 */ + bool wasCancelled() const; + /** + * @brief 在 DataManager 所属线程分批捕获手工求解输入。 + * @param nMaxWellCount 本次最多捕获的真实井数量。 + * @param bFinished 全部值快照完成时置为 true,随后才允许 start()。 + */ + bool captureManualInputStep(int nMaxWellCount, bool& bFinished); /** * @brief 在主线程一次性提交已经完整校验的求解结果。 @@ -65,8 +79,14 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { bool execute(); /** @brief 成对释放构造时登记的 DataManager 后台使用权。 */ void releaseDataManagerUse(); - /** @brief 在任务创建线程一次性捕获网格、井、储层和求解器设置。 */ + /** @brief 同步路径在任务创建线程一次性捕获全部求解输入。 */ bool captureInputSnapshot(const QString& sAutoFitTargetWellName); + /** @brief 手工求解在线程内完成场景、缓存和 VTK 基础网格准备。 */ + bool prepareManualInput(); + /** @brief 无锁读取原子停止标记。 */ + bool isCancelRequested() const; + /** @brief 仅为手工求解报告真实执行阶段。 */ + void reportStage(const QString& sStage, int nCurrent, int nTotal); /** @brief 构造 DLL 输入、执行求解并生成仍然有效的局部结果快照。 */ bool execPebiMode(); @@ -96,6 +116,8 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { nmDataAnalyzeManager* m_pDataManager; /** @brief 任务独占的完整值快照;析构时释放,后台禁止访问 DataManager。 */ nmPebiSolverInputSnapshot* m_pInputSnapshot; + /** @brief 仅在界面线程分批捕获期间持有井对象,线程启动前必须释放。 */ + nmPebiManualCaptureState* m_pManualCaptureState; bool m_bManagerUseActive; ///< 是否仍持有 DataManager 后台使用权。 bool m_bInputSnapshotValid; ///< 构造阶段是否已完成全部输入校验。 /** @brief 创建任务时捕获网格输入版本,防止回填过期网格上的结果。 */ @@ -117,12 +139,16 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { double m_dPendingScalarMin; ///< 全部场压力的最小值。 double m_dPendingScalarMax; ///< 全部场压力的最大值。 bool m_bPendingFullResultReady; ///< 是否已有可由主线程一次提交的完整结果。 + QAtomicInt m_nCancelRequested; ///< 跨线程协作式停止标记,1 表示请求停止。 + QAtomicInt m_nWasCancelled; ///< run() 已按停止请求丢弃本次局部结果。 private slots: //void slotTaskUpdateProgress(); signals: void sig_calculateDone(bool isSuccessed); + /** @brief 报告当前真实阶段;nTotal<=0 表示 DLL 等不可测阶段。 */ + void sigStageChanged(QString sStage, int nCurrent, int nTotal); //void sigTaskProgressUpdated(int progress); //void sigResSolverProgressUpdated(int progress); }; diff --git a/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h b/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h index ae47ff4..7425da8 100644 --- a/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h +++ b/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include "pch.h" #include "nmDataBinaryTools.h" @@ -14,6 +16,35 @@ #include class nmDataAnalyzeManager; +class nmDataWellBase; + +/** + * @brief 一口求解井在输入捕获时形成的值快照。 + * + * 井对象只允许在 DataManager 所属线程读取;后台准备场景和后处理曲线时只使用 + * 这里的值,避免多井循环反复扫描 DataManager 或跨线程访问井对象。 + */ +struct nmPebiWellInputSnapshot +{ + nmPebiWellInputSnapshot() + : m_nFlowSectionIndex(1), + m_dWellboreStorage(0.0), + m_dSkin(0.0), + m_bRateControlled(false), + m_bRealWell(false) + { + } + + QString m_sWellCode; + QString m_sWellName; + QVector m_vecFlowPoints; + QPointF m_oLocation; + int m_nFlowSectionIndex; + double m_dWellboreStorage; + double m_dSkin; + bool m_bRateControlled; + bool m_bRealWell; +}; /** * @brief 一次 PEBI 网格任务使用的不可变输入快照。 @@ -26,6 +57,7 @@ struct nmPebiGridInputSnapshot /** @brief 创建空快照;m_bValid 为 false,不能直接提交给网格 DLL。 */ nmPebiGridInputSnapshot() : m_nGridInputRevision(0), + m_bCaptured(false), m_bValid(false) { } @@ -33,9 +65,11 @@ struct nmPebiGridInputSnapshot HX_NWTM_GRID_INPUT m_oGridInput; ///< 边界、井、断层和裂缝等 DLL 输入。 nmDataBinaryTools::NM_PEBI_SCENE m_oScene; ///< 与本次网格输入一致的 ML 场景数据。 QVector m_vecSolverWellOrder; ///< DLL 数组槽位对应的稳定井编码顺序。 + QVector m_vecWellInputs; ///< 与 DLL 井槽位严格对齐的值快照。 QString m_sLicensePath; ///< 网格 DLL 授权文件路径副本。 quint64 m_nGridInputRevision; ///< 捕获时的几何输入版本。 - bool m_bValid; ///< 主线程是否已完成全部输入校验。 + bool m_bCaptured; ///< DataManager 值是否已在所属线程完整捕获。 + bool m_bValid; ///< 后台场景准备是否完成,可否提交给网格 DLL。 }; /** @@ -66,13 +100,29 @@ public: nmCalculationPebiGrid(); ~nmCalculationPebiGrid(); - /** @brief 在调用线程从 DataManager 复制一份完整且不含指针的网格输入。 */ + /** @brief 在调用线程从 DataManager 复制一份完整且不含对象指针的网格输入。 */ bool captureInputSnapshot(nmDataAnalyzeManager* pDataManager, - nmPebiGridInputSnapshot& oSnapshot); + nmPebiGridInputSnapshot& oSnapshot, + bool bDeferPreparation = false); + /** @brief 初始化手工求解的分批网格快照,只在 DataManager 所属线程调用。 */ + bool beginManualInputSnapshot(nmDataAnalyzeManager* pDataManager, + nmPebiGridInputSnapshot& oSnapshot); + /** @brief 把一口有效井的几何和值追加到手工求解快照。 */ + bool appendManualWellInputSnapshot(nmDataWellBase* pWellData, + int nWellMode, + nmPebiGridInputSnapshot& oSnapshot); + /** @brief 捕获非井输入并完成手工值快照;后台准备前 m_bValid 保持 false。 */ + bool finishManualInputSnapshot(nmDataAnalyzeManager* pDataManager, + const QSet& setEffectiveWellCodes, + nmPebiGridInputSnapshot& oSnapshot); + /** @brief 只使用已捕获的值在后台组装场景;取消时保持 m_bValid 为 false。 */ + bool prepareInputSnapshot(nmPebiGridInputSnapshot& oSnapshot, + const QAtomicInt* pCancelRequested = NULL); /** @brief 仅使用值快照调用网格 DLL,并把输出保存在局部结果中。 */ bool calculateSnapshot(const nmPebiGridInputSnapshot& oSnapshot, nmPebiGridResult& oResult, - bool bCreateUnstructuredGrid = true); + bool bCreateUnstructuredGrid = true, + const QAtomicInt* pCancelRequested = NULL); /** @brief 在主线程校验输入版本,并一次性提交井顺序、网格和单例缓存。 */ bool commitSnapshotResult(nmDataAnalyzeManager* pDataManager, const nmPebiGridInputSnapshot& oSnapshot, @@ -90,7 +140,8 @@ private: /** @brief 按有效计算井集合构造井几何并记录 DLL 井顺序。 */ bool meshGenPebiWells(nmDataAnalyzeManager* pDataManager, HX_NWTM_GRID_INPUT& inputObj, - QVector& vecSolverWellOrder); + QVector& vecSolverWellOrder, + const QSet& setEffectiveWellCodes); /** @brief 从 Map 构造断层输入。 */ bool meshGenPebiFault(nmDataAnalyzeManager* pDataManager, HX_NWTM_GRID_INPUT& inputObj); @@ -98,11 +149,9 @@ private: 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 从已捕获值构造与网格输入一致的 ML 场景,不访问 DataManager。 */ + bool buildPebiScene(nmPebiGridInputSnapshot& oSnapshot, + const QAtomicInt* pCancelRequested); /** @brief 更新单例中的 DLL 输入输出缓存,不改变网格有效版本。 */ void commitOutputCache(nmDataAnalyzeManager* pDataManager, const nmPebiGridInputSnapshot& oSnapshot, @@ -112,7 +161,9 @@ private: void genPebiVTK(const HX_NWTM_GRID_OUTPUT1& P1, QString vtkDir); // 根据网格输出结构体创建 vtkUnstructuredGrid 对象 - vtkSmartPointer createPebiUnstructuredGrid(const HX_NWTM_GRID_OUTPUT1& P1); + vtkSmartPointer createPebiUnstructuredGrid( + const HX_NWTM_GRID_OUTPUT1& P1, + const QAtomicInt* pCancelRequested = NULL); // 日志输出 void logCurrentState(); @@ -121,9 +172,14 @@ private: public: /** @brief 非阻塞复制当前有效网格;DLL忙碌或缓存不匹配时返回 false。 */ bool copyCurrentGridFor(const nmDataAnalyzeManager* pDataManager, + quint64 nGridInputRevision, HX_NWTM_GRID_OUTPUT1& oGridOutput1, HX_NWTM_GRID_OUTPUT2& oGridOutput2, - int& nPebiCount) const; + int& nPebiCount, + const QAtomicInt* pCancelRequested = NULL) const; + /** @brief 主线程快速判断缓存归属和版本,不复制大型 DLL 输出。 */ + bool isCurrentGridAvailableFor(const nmDataAnalyzeManager* pDataManager, + quint64 nGridInputRevision) const; public: /** @brief 在线程锁保护下返回最近一次网格输出 p1 的副本。 */ diff --git a/Include/nmNum/nmCalculation/nmCalculationUtils.h b/Include/nmNum/nmCalculation/nmCalculationUtils.h index a3a86dd..b097874 100644 --- a/Include/nmNum/nmCalculation/nmCalculationUtils.h +++ b/Include/nmNum/nmCalculation/nmCalculationUtils.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "nmCalculation_global.h" #include "nmCalculationDefine.h" #include @@ -43,7 +44,8 @@ class NMCALCULATION_EXPORT nmCalculationUtils { int model, const QString& licensePath, QVector& outputValues, - QString* errorMessage = 0); + QString* errorMessage = 0, + const QAtomicInt* pCancelRequested = 0); }; #endif // NMCALCULATIONUTILS_H diff --git a/Include/nmNum/nmSubWnd/nmSubWndMain.h b/Include/nmNum/nmSubWnd/nmSubWndMain.h index c6636cf..d4b9b2b 100644 --- a/Include/nmNum/nmSubWnd/nmSubWndMain.h +++ b/Include/nmNum/nmSubWnd/nmSubWndMain.h @@ -140,7 +140,14 @@ class NM_SUB_WND_EXPORT nmSubWndMain : public iSubWndBaseFit { void on_calculationFinished(NM_Calculation_Result); // DLL线程只返回成功/失败,这里转换回原来的计算结果处理流程。 void on_solverTaskFinished(bool isSuccessed); - void onProgressUpdated(int progress); // 新增槽函数,用于更新进度条 + /** @brief 进度框显示后再创建任务,避免输入捕获发生在首次绘制之前。 */ + void startSolverTask(); + /** @brief 每轮事件只捕获少量井输入,保持准备阶段界面和停止按钮可响应。 */ + void continueSolverInputCapture(); + /** @brief 使用后台任务报告的真实阶段更新进度框。 */ + void onSolverStageChanged(QString sStage, int nCurrent, int nTotal); + /** @brief 请求当前手工任务协作式停止。 */ + void onSolverCancelRequested(); /** @brief 按 WellCode 切换结果曲线,不改变主分析井和参数编辑井。 */ void onWellSelected(const QString& sWellCode); @@ -148,7 +155,6 @@ class NM_SUB_WND_EXPORT nmSubWndMain : public iSubWndBaseFit { // 点击左侧按钮求解生成操作 void onGenerateButtonClicked(); - void slotUpdateFakeProgress(); // 用于定时器触发 void onDialogCmdDestroyed(QObject*); private: @@ -257,9 +263,8 @@ private: QPointer m_pSolverDataManager; /** @brief 启动求解时捕获的拟合窗口,只把结果合并回该窗口。 */ QPointer m_pSolverFitting; - QTimer* m_pFakeProgressTimer; // 用于模拟进度的定时器 - int m_nVirtualProgress; // 记录当前的模拟进度值 - int m_nSlowDownCounter; // 记录定时器触发次数,用于控制减速节奏 + bool m_bSolverStartPending; ///< 进度框已显示,输入正分批捕获或任务等待启动。 + bool m_bSolverCancelRequested; ///< 已向当前任务发送停止请求,忽略后续阶段信号。 #ifdef QT_DEBUG // 示例:演示增加Dock并且追加至程序主窗口 diff --git a/Include/nmNum/nmSubWxs/nmWxPostprocessingAnimationWidget.h b/Include/nmNum/nmSubWxs/nmWxPostprocessingAnimationWidget.h index 862ee64..be4145f 100644 --- a/Include/nmNum/nmSubWxs/nmWxPostprocessingAnimationWidget.h +++ b/Include/nmNum/nmSubWxs/nmWxPostprocessingAnimationWidget.h @@ -1,4 +1,4 @@ -#ifndef NMWXPOSTPROCESSINGANIMATIONWIDGET_H +#ifndef NMWXPOSTPROCESSINGANIMATIONWIDGET_H #define NMWXPOSTPROCESSINGANIMATIONWIDGET_H #include "nmSubWxs_global.h" @@ -106,6 +106,9 @@ public: // 析构函数,用于清理资源,尽管 vtkSmartPointer 会自动管理大部分 VTK 对象的内存 ~nmWxPostprocessingAnimationWidget(); + /** @brief 复用现有 VTK 管道并切换到 DataManager 最新一次完整结果。 */ + void refreshResult(); + // 将 VTK 图像数据(vtkImageData)转换为 Qt 的 QImage 对象 QImage createQImage(vtkImageData* imageData); // 处理单通道图像,灰度图像 diff --git a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp index 912782c..78c584b 100644 --- a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp @@ -21,6 +21,7 @@ #include "nmDataPvtParaForPebi.h" #include +#include #include #include #include @@ -37,29 +38,6 @@ #include #include -/** - * @brief 一口求解井在任务启动时捕获的后处理输入。 - * - * DLL 模型输入已经保存在场景快照中;这里仅保留生成双对数、半对数和结果井位置 - * 仍需要的数据,后台线程不再回到 DataManager 查井对象。 - */ -struct nmPebiSolverWellInputSnapshot -{ - nmPebiSolverWellInputSnapshot() - : m_nFlowSectionIndex(1), - m_bRateControlled(false), - m_bRealWell(false) - { - } - - QString m_sWellCode; ///< 项目内稳定井编码。 - QVector m_vecFlowPoints; ///< 包含首个零点的原始产量制度。 - QPointF m_oLocation; ///< 捕获时的 Map 坐标。 - int m_nFlowSectionIndex; ///< 双对数预处理使用的流量段下标。 - bool m_bRateControlled; ///< 是否为有产量的主动生产/注入井。 - bool m_bRealWell; ///< false 表示仅占 DLL 槽位的手工裂缝。 -}; - /** * @brief 一次 PEBI 求解任务独占的完整不可变输入。 */ @@ -71,25 +49,112 @@ struct nmPebiSolverInputSnapshot m_nIluReuseSteps(1), m_bRequiresGridCalculation(false), m_bGridResultNeedsCommit(false), + m_bMayUseCachedGrid(false), m_bAutoFitTargetOnly(false) { } nmPebiGridInputSnapshot m_oGridInput; ///< 网格、场景、井顺序和授权路径值快照。 nmPebiGridResult m_oGridResult; ///< 已有网格副本或后台新生成的局部网格。 - QVector m_vecWellInputs; ///< 与 DLL 井槽位严格对齐。 QVector m_vecPropertyDataSets; ///< 属性插值值副本。 vtkSmartPointer m_pBaseGrid; ///< 完整成果使用的基础 VTK 网格。 + vtkSmartPointer m_pSourceBaseGrid; ///< 主线程捕获引用,后台只读并深拷贝。 int m_nSolverType; ///< PEBI 求解器实现类型。 int m_nOmpThreads; ///< CPU 加速求解线程数。 int m_nIluReuseSteps; ///< ILU 预条件复用步数。 bool m_bRequiresGridCalculation; ///< 后台是否需先用网格值快照计算一次网格。 bool m_bGridResultNeedsCommit; ///< 完整求解成功后是否需在主线程提交新网格。 + bool m_bMayUseCachedGrid; ///< 捕获时是否存在同版本网格缓存。 bool m_bAutoFitTargetOnly; ///< true 时只构造目标井临时曲线,不提交成果。 }; +/** + * @brief 手工求解在界面线程分批捕获期间使用的临时状态。 + * + * 这里只暂存 DataManager 拥有的井指针;最后一批完成后立即释放,QThread 启动后 + * 只能读取 nmPebiSolverInputSnapshot 中的值,不能跨线程访问这些对象。 + */ +struct nmPebiManualCaptureState +{ + enum CapturePhase + { + CapturePhase_Initialize = 0, + CapturePhase_Wells, + CapturePhase_FinalizeGrid, + CapturePhase_SolverSettings, + CapturePhase_GridCache + }; + + nmPebiManualCaptureState() + : m_ePhase(CapturePhase_Initialize), + m_nNextWellIndex(0) + { + } + + CapturePhase m_ePhase; + QVector m_vecOrderedWells; + QSet m_setEffectiveWellCodes; + QHash m_mapWellModes; + int m_nNextWellIndex; +}; + namespace { +bool isTaskCancellationRequested(const QAtomicInt* pCancelRequested) +{ + return pCancelRequested != NULL && + static_cast(*pCancelRequested) != 0; +} + +class nmSolverDllMutexLocker +{ +public: + nmSolverDllMutexLocker() + : m_pMutex(NULL), + m_bLocked(false) + { + } + + ~nmSolverDllMutexLocker() + { + unlock(); + } + + bool lock(QMutex* pMutex, const QAtomicInt* pCancelRequested) + { + if(pMutex == NULL || m_bLocked) { + return false; + } + + if(pCancelRequested == NULL) { + pMutex->lock(); + } else { + // 只让手工任务在等待全局 DLL 锁时可停止,互斥范围和锁顺序不变。 + while(!pMutex->tryLock(100)) { + if(isTaskCancellationRequested(pCancelRequested)) { + return false; + } + } + } + m_pMutex = pMutex; + m_bLocked = true; + return true; + } + + void unlock() + { + if(m_bLocked && m_pMutex != NULL) { + m_pMutex->unlock(); + m_pMutex = NULL; + m_bLocked = false; + } + } + +private: + QMutex* m_pMutex; + bool m_bLocked; +}; + bool isFiniteSolverNumber(double value) { #ifdef _MSC_VER @@ -104,7 +169,8 @@ bool applyPropertyInterpolation( HX_NWTM_MODEL_INPUT& modelInput, const QVector& dataSets, const QString& licensePath, - QString& errorMessage) + QString& errorMessage, + const QAtomicInt* pCancelRequested) { bool hasEnabledDataSet = false; for(int i = 0; i < dataSets.size(); ++i) { @@ -122,6 +188,10 @@ bool applyPropertyInterpolation( targetPoints.reserve(static_cast(modelInput.GRID.Trinodexy.size())); for(size_t cellIndex = 0; cellIndex < modelInput.GRID.Trinodexy.size(); ++cellIndex) { + if((cellIndex % 256) == 0 && + isTaskCancellationRequested(pCancelRequested)) { + return false; + } const dVec1& cellPosition = modelInput.GRID.Trinodexy[cellIndex]; if(cellPosition.size() < 2) { errorMessage = QString("Grid cell %1 has no valid center coordinate.") @@ -135,6 +205,9 @@ bool applyPropertyInterpolation( bool phiApplied = false; bool hApplied = false; for(int dataSetIndex = 0; dataSetIndex < dataSets.size(); ++dataSetIndex) { + if(isTaskCancellationRequested(pCancelRequested)) { + return false; + } const nmPropertyInterpolationDataSet& dataSet = dataSets[dataSetIndex]; if(!dataSet.useForCalculation) { continue; @@ -186,7 +259,8 @@ bool applyPropertyInterpolation( dataSet.model, licensePath, interpolationValues, - &calculationError)) { + &calculationError, + pCancelRequested)) { errorMessage = QString("Dataset '%1': %2") .arg(dataSet.name) .arg(calculationError); @@ -195,6 +269,10 @@ bool applyPropertyInterpolation( solverValues->resize(interpolationValues.size()); for(int valueIndex = 0; valueIndex < interpolationValues.size(); ++valueIndex) { + if((valueIndex % 256) == 0 && + isTaskCancellationRequested(pCancelRequested)) { + return false; + } // 属性插值数据层以mD保存;渗透率在写入PEBI数组时转换为D。 (*solverValues)[valueIndex] = dataSet.property == "k" ? nmCalculationUtils::milliDarcyToDarcy( @@ -214,11 +292,18 @@ bool buildModelInputFromSnapshot( const nmPebiSolverInputSnapshot& oSnapshot, const HX_NWTM_GRID_OUTPUT2& oGridOutput, HX_NWTM_MODEL_INPUT& oModelInput, - QString& sErrorMessage) + QString& sErrorMessage, + const QAtomicInt* pCancelRequested) { const nmDataBinaryTools::NM_PEBI_SCENE& oScene = oSnapshot.m_oGridInput.m_oScene; + if(isTaskCancellationRequested(pCancelRequested)) { + return false; + } oModelInput = HX_NWTM_MODEL_INPUT(oGridOutput); + if(isTaskCancellationRequested(pCancelRequested)) { + return false; + } // 第一步:恢复井槽位、产量制度和井筒参数。场景与网格快照在同一次捕获中 // 生成,因此这些外层数组天然使用同一个 DLL 下标顺序。 @@ -280,7 +365,8 @@ bool buildModelInputFromSnapshot( oModelInput, oSnapshot.m_vecPropertyDataSets, oSnapshot.m_oGridInput.m_sLicensePath, - sErrorMessage); + sErrorMessage, + pCancelRequested); } bool isReasonableLogLogValue(double value) @@ -333,13 +419,15 @@ nmCalculationDllPebiSolverTask::nmCalculationDllPebiSolverTask( QString sPostprocessingDir, nmDataAnalyzeManager* pDataManager, const QString& sAutoFitTargetWellName, - QObject *parent): + QObject *parent, + bool bDeferManualSnapshot): QThread(parent), m_sPostprocessingDir(sPostprocessingDir), m_pDataManager(pDataManager != nullptr ? pDataManager : nmDataAnalyzeManager::getCurrentInstance()), m_pInputSnapshot(new nmPebiSolverInputSnapshot()), + m_pManualCaptureState(nullptr), m_bManagerUseActive(false), m_bInputSnapshotValid(false), m_nGridInputRevision(0), @@ -349,7 +437,9 @@ nmCalculationDllPebiSolverTask::nmCalculationDllPebiSolverTask( m_nPebiCount(-1), m_dPendingScalarMin(0.0), m_dPendingScalarMax(0.0), - m_bPendingFullResultReady(false) + m_bPendingFullResultReady(false), + m_nCancelRequested(0), + m_nWasCancelled(0) { // 第一步:构造后到 run() 结束前阻止所属成果提前释放 DataManager。 if(m_pDataManager != nullptr) { @@ -357,10 +447,14 @@ nmCalculationDllPebiSolverTask::nmCalculationDllPebiSolverTask( m_bManagerUseActive = true; } - // 第二步:在创建线程内一次性冻结全部值输入。run() 启动后不得再读取 - // DataManager、井、储层、PVT 或属性插值容器。 - m_bInputSnapshotValid = - captureInputSnapshot(sAutoFitTargetWellName); + if(bDeferManualSnapshot && sAutoFitTargetWellName.isEmpty()) { + // 手工求解由界面事件循环分批冻结值输入;start() 必须等状态释放后再调用。 + m_pManualCaptureState = new nmPebiManualCaptureState(); + } else { + // 自动拟合以及旧调用路径继续一次性同步捕获,保持原来的评价行为。 + m_bInputSnapshotValid = + captureInputSnapshot(sAutoFitTargetWellName); + } } nmCalculationDllPebiSolverTask::~nmCalculationDllPebiSolverTask() @@ -371,6 +465,8 @@ nmCalculationDllPebiSolverTask::~nmCalculationDllPebiSolverTask() wait(); } releaseDataManagerUse(); + delete m_pManualCaptureState; + m_pManualCaptureState = nullptr; delete m_pInputSnapshot; m_pInputSnapshot = nullptr; } @@ -379,12 +475,247 @@ void nmCalculationDllPebiSolverTask::run() { m_nSolveTimeMs = -1; m_nPebiCount = -1; - m_lastRunSucceeded = this->execute(); + bool bSucceeded = this->execute(); + if(isCancelRequested()) { + // 停止只丢弃任务局部结果,主线程不会进入 commitResult()。 + m_nWasCancelled.fetchAndStoreOrdered(1); + m_bPendingFullResultReady = false; + bSucceeded = false; + } + m_lastRunSucceeded = bSucceeded; // 完成信号可能触发成果关闭;必须在发信号前结束 DataManager 使用期。 releaseDataManagerUse(); emit sig_calculateDone(m_lastRunSucceeded); } +void nmCalculationDllPebiSolverTask::requestCancel() +{ + m_nCancelRequested.fetchAndStoreOrdered(1); +} + +bool nmCalculationDllPebiSolverTask::wasCancelled() const +{ + return static_cast(m_nWasCancelled) != 0 || + isCancelRequested(); +} + +bool nmCalculationDllPebiSolverTask::captureManualInputStep( + int nMaxWellCount, + bool& bFinished) +{ + bFinished = false; + if(m_pManualCaptureState == nullptr || m_pInputSnapshot == nullptr || + m_pDataManager == nullptr || isRunning() || + QThread::currentThread() != m_pDataManager->thread()) { + return false; + } + if(isCancelRequested()) { + m_nWasCancelled.fetchAndStoreOrdered(1); + return false; + } + + nmPebiManualCaptureState* pState = m_pManualCaptureState; + nmCalculationPebiGrid* pGridService = + nmCalculationPebiGrid::getInstance(); + nmDataNumericalAnalysisCase* pAnalysisCase = + m_pDataManager->getNumericalAnalysisCase(); + if(pGridService == nullptr || pAnalysisCase == nullptr) { + return false; + } + + if(pState->m_ePhase != + nmPebiManualCaptureState::CapturePhase_Initialize && + (pAnalysisCase->getGridInputRevision() != m_nGridInputRevision || + pAnalysisCase->getResultInputRevision() != m_nResultInputRevision)) { + // 分批期间输入版本变化时整批作废,不能把不同时刻的值拼成一个快照。 + return false; + } + + if(pState->m_ePhase == + nmPebiManualCaptureState::CapturePhase_Initialize) { + m_nGridInputRevision = pAnalysisCase->getGridInputRevision(); + m_nResultInputRevision = pAnalysisCase->getResultInputRevision(); + m_sAutoFitTargetWellCode.clear(); + + if(!pGridService->beginManualInputSnapshot( + m_pDataManager, m_pInputSnapshot->m_oGridInput)) { + return false; + } + + const QVector vecEffectiveWells = + m_pDataManager->getEffectiveCalculationWells(); + for(int nIndex = 0; nIndex < vecEffectiveWells.size(); ++nIndex) { + const nmCalculationWellRef& oWellRef = vecEffectiveWells[nIndex]; + if(oWellRef.m_sWellCode.isEmpty() || + pState->m_setEffectiveWellCodes.contains( + oWellRef.m_sWellCode)) { + return false; + } + pState->m_setEffectiveWellCodes.insert(oWellRef.m_sWellCode); + pState->m_mapWellModes.insert( + oWellRef.m_sWellCode, + static_cast(oWellRef.m_eMode)); + } + if(pState->m_setEffectiveWellCodes.isEmpty()) { + return false; + } + + QSet setOrderedWellCodes; + const QVector vecVerticalWells = + m_pDataManager->getVerticalWellData(); + for(int nIndex = 0; nIndex < vecVerticalWells.size(); ++nIndex) { + nmDataVerticalWell* pWellData = vecVerticalWells[nIndex]; + if(pWellData != nullptr && + pState->m_setEffectiveWellCodes.contains( + pWellData->getWellCode())) { + pState->m_vecOrderedWells.append(pWellData); + setOrderedWellCodes.insert(pWellData->getWellCode()); + } + } + const QVector vecVerticalFracturedWells = + m_pDataManager->getVerticalFracturedWellData(); + for(int nIndex = 0; + nIndex < vecVerticalFracturedWells.size(); + ++nIndex) { + nmDataVerticalFracturedWell* pWellData = + vecVerticalFracturedWells[nIndex]; + if(pWellData != nullptr && + pState->m_setEffectiveWellCodes.contains( + pWellData->getWellCode())) { + pState->m_vecOrderedWells.append(pWellData); + setOrderedWellCodes.insert(pWellData->getWellCode()); + } + } + const QVector + vecHorizontalFracturedWells = + m_pDataManager->getHorizontalFracturedWellData(); + for(int nIndex = 0; + nIndex < vecHorizontalFracturedWells.size(); + ++nIndex) { + nmDataHorizontalFracturedWell* pWellData = + vecHorizontalFracturedWells[nIndex]; + if(pWellData != nullptr && + pState->m_setEffectiveWellCodes.contains( + pWellData->getWellCode())) { + pState->m_vecOrderedWells.append(pWellData); + setOrderedWellCodes.insert(pWellData->getWellCode()); + } + } + if(setOrderedWellCodes != pState->m_setEffectiveWellCodes || + pState->m_vecOrderedWells.size() != + pState->m_setEffectiveWellCodes.size()) { + return false; + } + + pState->m_ePhase = nmPebiManualCaptureState::CapturePhase_Wells; + reportStage(tr("Capturing well data..."), + 0, + pState->m_vecOrderedWells.size()); + return true; + } + + if(pState->m_ePhase == nmPebiManualCaptureState::CapturePhase_Wells) { + const int nBatchEnd = qMin( + pState->m_nNextWellIndex + qMax(1, nMaxWellCount), + pState->m_vecOrderedWells.size()); + while(pState->m_nNextWellIndex < nBatchEnd) { + if(isCancelRequested()) { + m_nWasCancelled.fetchAndStoreOrdered(1); + return false; + } + nmDataWellBase* pWellData = + pState->m_vecOrderedWells[pState->m_nNextWellIndex]; + if(pWellData == nullptr || + !pState->m_mapWellModes.contains(pWellData->getWellCode()) || + !pGridService->appendManualWellInputSnapshot( + pWellData, + pState->m_mapWellModes.value(pWellData->getWellCode()), + m_pInputSnapshot->m_oGridInput)) { + return false; + } + ++pState->m_nNextWellIndex; + } + + reportStage(tr("Capturing well data..."), + pState->m_nNextWellIndex, + pState->m_vecOrderedWells.size()); + if(pState->m_nNextWellIndex >= + pState->m_vecOrderedWells.size()) { + pState->m_ePhase = + nmPebiManualCaptureState::CapturePhase_FinalizeGrid; + } + return true; + } + + if(pState->m_ePhase == + nmPebiManualCaptureState::CapturePhase_FinalizeGrid) { + reportStage(tr("Finalizing input snapshot..."), 0, 0); + if(!pGridService->finishManualInputSnapshot( + m_pDataManager, + pState->m_setEffectiveWellCodes, + m_pInputSnapshot->m_oGridInput)) { + return false; + } + pState->m_ePhase = + nmPebiManualCaptureState::CapturePhase_SolverSettings; + return true; + } + + if(pState->m_ePhase == + nmPebiManualCaptureState::CapturePhase_SolverSettings) { + reportStage(tr("Capturing solver settings..."), 0, 0); + 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(); + pState->m_ePhase = + nmPebiManualCaptureState::CapturePhase_GridCache; + return true; + } + + reportStage(tr("Checking grid cache..."), 0, 0); + m_pInputSnapshot->m_bMayUseCachedGrid = + pGridService->isCurrentGridAvailableFor( + m_pDataManager, m_nGridInputRevision); + if(m_pInputSnapshot->m_bMayUseCachedGrid) { + m_pInputSnapshot->m_pSourceBaseGrid = + m_pDataManager->getUnstructuredGrid(); + if(m_pInputSnapshot->m_pSourceBaseGrid == nullptr || + m_pInputSnapshot->m_pSourceBaseGrid->GetNumberOfCells() <= 0) { + m_pInputSnapshot->m_bMayUseCachedGrid = false; + } + } + m_pInputSnapshot->m_bRequiresGridCalculation = + !m_pInputSnapshot->m_bMayUseCachedGrid; + + // 至此快照不再含 DataManager 对象指针,先释放临时状态再允许启动 QThread。 + delete m_pManualCaptureState; + m_pManualCaptureState = nullptr; + m_bInputSnapshotValid = true; + bFinished = true; + return true; +} + +bool nmCalculationDllPebiSolverTask::isCancelRequested() const +{ + return static_cast(m_nCancelRequested) != 0; +} + +void nmCalculationDllPebiSolverTask::reportStage( + const QString& sStage, + int nCurrent, + int nTotal) +{ + if(m_pInputSnapshot != nullptr && + !m_pInputSnapshot->m_bAutoFitTargetOnly) { + emit sigStageChanged(sStage, nCurrent, nTotal); + } +} + void nmCalculationDllPebiSolverTask::releaseDataManagerUse() { if(m_bManagerUseActive && m_pDataManager != nullptr) { @@ -431,13 +762,15 @@ bool nmCalculationDllPebiSolverTask::captureInputSnapshot( m_sAutoFitTargetWellCode.clear(); } - // 第二步:捕获网格输入和求解场景。该调用只复制值,不执行 DLL,也不导出 - // CSV,因此任务创建不会等待另一个网格 DLL 或进行无关磁盘写入。 + // 第二步:手工求解只捕获 DataManager 值,场景数组延后到工作线程组装; + // 自动拟合保持原来的完整同步快照时机和行为。 nmCalculationPebiGrid* pGridService = nmCalculationPebiGrid::getInstance(); if(pGridService == nullptr || !pGridService->captureInputSnapshot( - m_pDataManager, m_pInputSnapshot->m_oGridInput) || + m_pDataManager, + m_pInputSnapshot->m_oGridInput, + !m_pInputSnapshot->m_bAutoFitTargetOnly) || m_pInputSnapshot->m_oGridInput.m_nGridInputRevision != m_nGridInputRevision) { return false; @@ -445,52 +778,12 @@ bool nmCalculationDllPebiSolverTask::captureInputSnapshot( 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); + if(m_pInputSnapshot->m_oGridInput.m_vecWellInputs.size() != + vecWellOrder.size()) { + return false; } - // 第四步:复制属性插值及 DLL 求解配置。这些设置只影响模型求解结果, + // 第三步:复制属性插值及 DLL 求解配置。这些设置只影响模型求解结果, // 不应在工作线程中再次从 DataManager 查询。 m_pInputSnapshot->m_vecPropertyDataSets = m_pDataManager->getPropertyInterpolationDataSets(); @@ -501,34 +794,107 @@ bool nmCalculationDllPebiSolverTask::captureInputSnapshot( 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) { + // 第四步:主线程仅检查缓存并捕获基础网格的智能指针。生成后的基础网格 + // 不在原位修改,智能指针保证 DataManager 替换网格后旧对象仍存活;真正的 + // DLL 输出复制和 VTK DeepCopy 在手工任务线程执行。 + m_pInputSnapshot->m_bMayUseCachedGrid = + pGridService->isCurrentGridAvailableFor( + m_pDataManager, m_nGridInputRevision); + if(m_pInputSnapshot->m_bMayUseCachedGrid && + !m_pInputSnapshot->m_bAutoFitTargetOnly) { + m_pInputSnapshot->m_pSourceBaseGrid = + m_pDataManager->getUnstructuredGrid(); + if(m_pInputSnapshot->m_pSourceBaseGrid == nullptr || + m_pInputSnapshot->m_pSourceBaseGrid->GetNumberOfCells() <= 0) { + m_pInputSnapshot->m_bMayUseCachedGrid = false; + } + } + + if(m_pInputSnapshot->m_bAutoFitTargetOnly && + m_pInputSnapshot->m_bMayUseCachedGrid) { + HX_NWTM_GRID_OUTPUT1 oGridOutput1; + HX_NWTM_GRID_OUTPUT2 oGridOutput2; + int nPebiCount = -1; + if(pGridService->copyCurrentGridFor(m_pDataManager, + m_nGridInputRevision, + 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; + } else { + m_pInputSnapshot->m_bRequiresGridCalculation = true; + } + } else if(!m_pInputSnapshot->m_bMayUseCachedGrid) { + m_pInputSnapshot->m_bRequiresGridCalculation = true; + } + + return true; +} + +bool nmCalculationDllPebiSolverTask::prepareManualInput() +{ + if(m_pInputSnapshot == nullptr || + m_pInputSnapshot->m_bAutoFitTargetOnly) { + return true; + } + + reportStage(tr("Preparing input data..."), 0, 0); + nmCalculationPebiGrid* pGridService = + nmCalculationPebiGrid::getInstance(); + if(pGridService == nullptr || + !pGridService->prepareInputSnapshot( + m_pInputSnapshot->m_oGridInput, + &m_nCancelRequested) || + isCancelRequested()) { + return false; + } + + reportStage(tr("Checking grid cache..."), 0, 0); + if(m_pInputSnapshot->m_bMayUseCachedGrid) { + HX_NWTM_GRID_OUTPUT1 oGridOutput1; + HX_NWTM_GRID_OUTPUT2 oGridOutput2; + int nPebiCount = -1; + if(pGridService->copyCurrentGridFor( + m_pDataManager, + m_nGridInputRevision, + oGridOutput1, + oGridOutput2, + nPebiCount, + &m_nCancelRequested)) { + nmPebiGridResult& oGridResult = + m_pInputSnapshot->m_oGridResult; + oGridResult.m_oGridOutput1 = oGridOutput1; + oGridResult.m_oGridOutput2 = oGridOutput2; + oGridResult.m_nPebiCount = nPebiCount; + oGridResult.m_bSucceeded = true; + + reportStage(tr("Preparing result grid..."), 0, 0); + if(isCancelRequested()) { + return false; + } m_pInputSnapshot->m_pBaseGrid = - m_pDataManager->getUnstructuredGridCopy(); - if(m_pInputSnapshot->m_pBaseGrid == nullptr || - m_pInputSnapshot->m_pBaseGrid->GetNumberOfCells() <= 0) { + vtkSmartPointer::New(); + m_pInputSnapshot->m_pBaseGrid->DeepCopy( + m_pInputSnapshot->m_pSourceBaseGrid); + if(isCancelRequested()) { + m_pInputSnapshot->m_pBaseGrid = nullptr; + return false; + } + if(m_pInputSnapshot->m_pBaseGrid->GetNumberOfCells() <= 0) { return false; } + } else if(isCancelRequested()) { + return false; + } else { + // 捕获后缓存被其他网格任务替换时,仍按本任务快照在后台重建。 + m_pInputSnapshot->m_bRequiresGridCalculation = true; } - } else { - m_pInputSnapshot->m_bRequiresGridCalculation = true; } - return m_pInputSnapshot->m_vecWellInputs.size() == - vecWellOrder.size(); + return !isCancelRequested(); } bool nmCalculationDllPebiSolverTask::wasSuccessful() const @@ -563,16 +929,24 @@ QVector > nmCalculationDllPebiSolverTask::getAutoFitResultSemiLo bool nmCalculationDllPebiSolverTask::execute() { - return this->execPebiMode(); + return !isCancelRequested() && + prepareManualInput() && + !isCancelRequested() && + this->execPebiMode(); } bool nmCalculationDllPebiSolverTask::execPebiMode() { - if(!m_bInputSnapshotValid || m_pInputSnapshot == nullptr) { + if(!m_bInputSnapshotValid || m_pInputSnapshot == nullptr || + isCancelRequested()) { return false; } + const QAtomicInt* pCancelRequested = + m_pInputSnapshot->m_bAutoFitTargetOnly + ? NULL : &m_nCancelRequested; + // 第一步:清空本次任务自己的输出。旧成果仍保留在 DataManager 中,只有主线程 // 最终提交成功后才会被整体替换。 m_bPendingFullResultReady = false; @@ -589,10 +963,16 @@ bool nmCalculationDllPebiSolverTask::execPebiMode() // 自动拟合候选不创建 VTK;完整求解创建局部 VTK,完成后回主线程提交。 const bool bCreateUnstructuredGrid = !m_pInputSnapshot->m_bAutoFitTargetOnly; + reportStage(tr("Generating grid..."), 0, 0); if(!nmCalculationPebiGrid::getInstance()->calculateSnapshot( m_pInputSnapshot->m_oGridInput, oGridResult, - bCreateUnstructuredGrid)) { + bCreateUnstructuredGrid, + pCancelRequested)) { + return false; + } + + if(isCancelRequested()) { return false; } @@ -606,12 +986,17 @@ bool nmCalculationDllPebiSolverTask::execPebiMode() vtkSmartPointer::New(); m_pInputSnapshot->m_pBaseGrid->DeepCopy( oGridResult.m_pUnstructuredGrid); + if(isCancelRequested()) { + m_pInputSnapshot->m_pBaseGrid = nullptr; + return false; + } m_pInputSnapshot->m_bGridResultNeedsCommit = true; } } if(!oGridResult.m_bSucceeded || - oGridResult.m_oGridOutput1.PEBI_cell.p.empty()) { + oGridResult.m_oGridOutput1.PEBI_cell.p.empty() || + isCancelRequested()) { return false; } @@ -619,10 +1004,15 @@ bool nmCalculationDllPebiSolverTask::execPebiMode() // 自己的点集副本,因此放在工作线程不会阻塞或竞争界面数据。 HX_NWTM_MODEL_INPUT oModelInput; QString sInterpolationError; + reportStage(tr("Preparing model input..."), 0, 0); if(!buildModelInputFromSnapshot(*m_pInputSnapshot, oGridResult.m_oGridOutput2, oModelInput, - sInterpolationError)) { + sInterpolationError, + pCancelRequested)) { + if(isCancelRequested()) { + return false; + } const QString sLogMessage = QString("Property interpolation failed: %1") .arg(sInterpolationError); @@ -631,9 +1021,17 @@ bool nmCalculationDllPebiSolverTask::execPebiMode() return false; } + if(isCancelRequested()) { + return false; + } + // 第四步:建网、模型求解和 Kriging 共用 DLL 全局状态,整个 DLL 调用必须串行。 - QMutexLocker oDllLocker( - nmCalculationUtils::getHxNwtmDllMutex()); + reportStage(tr("Waiting for solver..."), 0, 0); + nmSolverDllMutexLocker oDllLocker; + if(!oDllLocker.lock(nmCalculationUtils::getHxNwtmDllMutex(), + pCancelRequested)) { + return false; + } HMODULE hModelModule = LoadLibrary(L"HX_NWTM.dll"); if(hModelModule == nullptr) { qWarning() << "Failed to load HX_NWTM.dll. Error code:" @@ -681,6 +1079,7 @@ bool nmCalculationDllPebiSolverTask::execPebiMode() pfnSetIluReuseSteps(m_pInputSnapshot->m_nIluReuseSteps); } + reportStage(tr("Solving model..."), 0, 0); pfnModel(oModelOutput, oModelInput, m_pInputSnapshot->m_oGridInput.m_sLicensePath @@ -704,6 +1103,11 @@ bool nmCalculationDllPebiSolverTask::execPebiMode() FreeLibrary(hModelModule); oDllLocker.unlock(); + // HX_NWTM_MODEL 没有停止入口;调用期间收到请求时,返回后直接丢弃输出。 + if(isCancelRequested()) { + return false; + } + // 第五步:把井曲线和场压力构造成任务局部结果。该函数同样只读取输入快照。 const bool bSucceeded = buildPebiModeResult( oModelOutput, @@ -767,8 +1171,8 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( // 井顺序与后处理输入都来自任务启动时的同一份快照。 const QVector& vecWellsOrder = m_pInputSnapshot->m_oGridInput.m_vecSolverWellOrder; - const QVector& vecWellInputs = - m_pInputSnapshot->m_vecWellInputs; + const QVector& vecWellInputs = + m_pInputSnapshot->m_oGridInput.m_vecWellInputs; // 第一步:真实井必须逐口具有与公共时间轴等长的井底压力。 // 观察井虽然没有源汇项,也必须由 DLL 返回压力,否则不能形成有效多井结果。 @@ -778,6 +1182,9 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( return false; } for(size_t nTimeIndex = 0; nTimeIndex < p1.t.size(); ++nTimeIndex) { + if((nTimeIndex % 256) == 0 && isCancelRequested()) { + return false; + } if(!isFiniteSolverNumber(p1.t[nTimeIndex])) { qWarning() << "PEBI returned a non-finite time at index:" << static_cast(nTimeIndex); @@ -785,6 +1192,9 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( } } for(int nIndex = 0; nIndex < vecWellsOrder.size(); ++nIndex) { + if(isCancelRequested()) { + return false; + } const nmSolverWellRef& oWellRef = vecWellsOrder[nIndex]; if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture) { continue; @@ -801,6 +1211,9 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( for(size_t nTimeIndex = 0; nTimeIndex < p1.pw[nIndex].size(); ++nTimeIndex) { + if((nTimeIndex % 256) == 0 && isCancelRequested()) { + return false; + } if(!isFiniteSolverNumber(p1.pw[nIndex][nTimeIndex])) { qWarning() << "PEBI returned a non-finite pressure for WellCode:" << oWellRef.m_sWellCode @@ -811,11 +1224,18 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( } // 遍历每口井,处理其数据 + reportStage(tr("Processing well results..."), + 0, + vecWellsOrder.size()); for(int wellIdx = 0; wellIdx < vecWellsOrder.size(); ++wellIdx) { const nmSolverWellRef& oWellRef = vecWellsOrder[wellIdx]; - const nmPebiSolverWellInputSnapshot& oWellInput = + const nmPebiWellInputSnapshot& oWellInput = vecWellInputs[wellIdx]; + if(isCancelRequested()) { + return false; + } + if(autoFitTargetOnly && oWellRef.m_sWellCode != m_sAutoFitTargetWellCode) { continue; @@ -823,6 +1243,9 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( // 手工裂缝不是结果井,只用于保持 DLL 下标与网格输入一致。 if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture) { + reportStage(tr("Processing well results..."), + wellIdx + 1, + vecWellsOrder.size()); continue; } @@ -831,6 +1254,9 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( QVector currentWellPressure; for(size_t i = 0; i < p1.pw[wellIdx].size(); ++i) { + if((i % 256) == 0 && isCancelRequested()) { + return false; + } currentWellTime.append(p1.t[i]); currentWellPressure.append(p1.pw[wellIdx][i]); } @@ -858,6 +1284,9 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( std::vector wellPressureDataForDll; for(size_t i = 0; i < p1.pw[wellIdx].size(); ++i) { + if((i % 256) == 0 && isCancelRequested()) { + return false; + } Point pt; pt.x = p1.t[i]; @@ -933,14 +1362,23 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( return false; } + if(isCancelRequested()) { + FreeLibrary(hMod_solver); + return false; + } + // 填充双对数曲线数据到局部变量 QVector logX, logY, logZ; // 检查结果是否为空,并跳过第一个点 if (logPreResultFromDll.size() > 1) { // 从索引 1 开始遍历,跳过索引 0 的第一个点 - for (size_t i = 1; i < logPreResultFromDll.size(); ++i) { - const auto& pt = logPreResultFromDll[i]; + for (size_t i = 1; i < logPreResultFromDll.size(); ++i) { + if((i % 256) == 0 && isCancelRequested()) { + FreeLibrary(hMod_solver); + return false; + } + const auto& pt = logPreResultFromDll[i]; if(!isValidLogLogPoint(pt)) { continue; } @@ -963,6 +1401,10 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( if (logPreResultFromDll.size() > 1) { // 半对数曲线也应该同步跳过第一个点 for (size_t i = 1; i < logPreResultFromDll.size(); ++i) { + if((i % 256) == 0 && isCancelRequested()) { + FreeLibrary(hMod_solver); + return false; + } const auto& pt = logPreResultFromDll[i]; if(!isValidSemiLogPoint(pt)) { continue; @@ -1002,6 +1444,10 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( oWellResult.m_oLocation = oWellInput.m_oLocation; m_vecPendingWellResults.append(oWellResult); } + + reportStage(tr("Processing well results..."), + wellIdx + 1, + vecWellsOrder.size()); } if(autoFitTargetOnly) { @@ -1026,6 +1472,9 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( size_t actualPlotCellsCount = 0; for(size_t i = 0; i < oGridOutput.PEBI_cell.isplot.size(); ++i) { + if((i % 256) == 0 && isCancelRequested()) { + return false; + } if(oGridOutput.PEBI_cell.isplot[i] == 1) { actualPlotCellsCount++; } @@ -1035,6 +1484,9 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( return false; } for(size_t nTimeIndex = 0; nTimeIndex < p1.p.size(); ++nTimeIndex) { + if(isCancelRequested()) { + return false; + } if(p1.p[nTimeIndex].size() < oGridOutput.PEBI_cell.isplot.size()) { qWarning() << "Incomplete field pressure returned for time index:" << static_cast(nTimeIndex); @@ -1043,6 +1495,9 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( for(size_t nCellIndex = 0; nCellIndex < oGridOutput.PEBI_cell.isplot.size(); ++nCellIndex) { + if((nCellIndex % 256) == 0 && isCancelRequested()) { + return false; + } if(oGridOutput.PEBI_cell.isplot[nCellIndex] == 1 && !isFiniteSolverNumber(p1.p[nTimeIndex][nCellIndex])) { qWarning() << "PEBI returned a non-finite field pressure at time/cell:" @@ -1060,7 +1515,13 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( double dMaxP = -DBL_MAX; // 为每个时间步生成完整的 VTK 压力数组。 + reportStage(tr("Processing field results..."), + 0, + static_cast(p1.p.size())); for(size_t timeIdx = 0; timeIdx < p1.p.size(); ++timeIdx) { + if(isCancelRequested()) { + return false; + } // 获取当前时间步的时间值 double currentTime = p1.t[timeIdx]; @@ -1077,6 +1538,9 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( // 遍历原始数据,并根据 isplot 填充到 pressureData for(size_t i = 0; i < oGridOutput.PEBI_cell.isplot.size(); ++i) { + if((i % 256) == 0 && isCancelRequested()) { + return false; + } if(oGridOutput.PEBI_cell.isplot[i] == 1) { // 只有当 isplot 为1时才考虑这个单元格 pressureData->SetValue(destIdx, p1.p[timeIdx][i]); @@ -1093,6 +1557,15 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( dMaxP = qMax(dMaxP, dCurrentTimeMax); m_mapPendingTimeSteps.insert(currentTime, pressureData); + + const int nCompleted = static_cast(timeIdx + 1); + const int nTotal = static_cast(p1.p.size()); + const int nReportStep = qMax(1, nTotal / 100); + if(nCompleted == nTotal || (nCompleted % nReportStep) == 0) { + reportStage(tr("Processing field results..."), + nCompleted, + nTotal); + } } // QMap 使用时间作为唯一键;重复时间会覆盖旧帧,因此必须检查数量一致。 @@ -1131,6 +1604,7 @@ bool nmCalculationDllPebiSolverTask::commitResult( // DataManager 所属线程执行,保证界面看不到逐项替换过程中的中间状态。 if(m_pInputSnapshot == nullptr || m_pInputSnapshot->m_bAutoFitTargetOnly || + wasCancelled() || isCancelRequested() || !m_bPendingFullResultReady || pDataManager == nullptr || pDataManager != m_pDataManager || diff --git a/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp b/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp index 527a3f5..4133be2 100644 --- a/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "nmCalculationUtils.h" #include "zxLogInstance.h" @@ -41,6 +42,72 @@ const int CONST_PVT_POINT_COUNT = 200; // 保留递归锁以兼容可能在持锁网格入口中调用缓存查询的旧代码路径。 QMutex s_oPebiGridMutex(QMutex::Recursive); +bool isCancellationRequested(const QAtomicInt* pCancelRequested) +{ + return pCancelRequested != NULL && + static_cast(*pCancelRequested) != 0; +} + +class nmInterruptibleMutexLocker +{ +public: + nmInterruptibleMutexLocker() + : m_pMutex(NULL), + m_bLocked(false) + { + } + + ~nmInterruptibleMutexLocker() + { + unlock(); + } + + bool lock(QMutex* pMutex, const QAtomicInt* pCancelRequested) + { + if(pMutex == NULL || m_bLocked) { + return false; + } + + if(pCancelRequested == NULL) { + pMutex->lock(); + } else { + // 等锁阶段不能无限阻塞;锁顺序仍保持“网格缓存锁 -> DLL 全局锁”。 + while(!pMutex->tryLock(100)) { + if(isCancellationRequested(pCancelRequested)) { + return false; + } + } + } + + m_pMutex = pMutex; + m_bLocked = true; + return true; + } + + bool tryLock(QMutex* pMutex) + { + if(pMutex == NULL || m_bLocked || !pMutex->tryLock()) { + return false; + } + m_pMutex = pMutex; + m_bLocked = true; + return true; + } + + void unlock() + { + if(m_bLocked && m_pMutex != NULL) { + m_pMutex->unlock(); + m_bLocked = false; + m_pMutex = NULL; + } + } + +private: + QMutex* m_pMutex; + bool m_bLocked; +}; + std::vector buildConstantPvtVector(double value) { return std::vector(CONST_PVT_POINT_COUNT, value); @@ -242,41 +309,56 @@ void nmCalculationPebiGrid::clearGridData( bool nmCalculationPebiGrid::copyCurrentGridFor( const nmDataAnalyzeManager* pDataManager, + quint64 nGridInputRevision, HX_NWTM_GRID_OUTPUT1& oGridOutput1, HX_NWTM_GRID_OUTPUT2& oGridOutput2, - int& nPebiCount) const + int& nPebiCount, + const QAtomicInt* pCancelRequested) const { - // 求解任务在主线程构造,不能为了复制缓存等待正在运行的网格 DLL。 - // 锁忙时返回 false,任务会在后台基于自己的值快照生成局部网格。 - if(!s_oPebiGridMutex.tryLock()) { + nmInterruptibleMutexLocker oGridLocker; + const bool bLocked = pCancelRequested == NULL + ? oGridLocker.tryLock(&s_oPebiGridMutex) + : oGridLocker.lock(&s_oPebiGridMutex, pCancelRequested); + if(!bLocked) { return false; } - // 第一步:在复制前同时校验所有权、输入版本和实际网格内容。 + // 后台只比较捕获时的所有权和版本值,不能再读取 DataManager 内部状态。 if(pDataManager == nullptr || m_pDataManager != pDataManager || - !pDataManager->isPebiGridValid() || - pDataManager->getNumericalAnalysisCase() == nullptr || - m_nCachedGridInputRevision != - pDataManager->getNumericalAnalysisCase()->getGridInputRevision() || + m_nCachedGridInputRevision != nGridInputRevision || p1.PEBI_cell.p.empty()) { - s_oPebiGridMutex.unlock(); return false; } - // 第二步:一次性复制两份 DLL 输出,保证求解期间使用同一版本的网格快照。 - try { - oGridOutput1 = p1; - oGridOutput2 = p2; - nPebiCount = m_nPebiCount; - } catch(...) { - s_oPebiGridMutex.unlock(); - throw; + // 一次性复制两份 DLL 输出,保证求解期间使用同一版本的网格快照。 + oGridOutput1 = p1; + if(isCancellationRequested(pCancelRequested)) { + return false; } - s_oPebiGridMutex.unlock(); + oGridOutput2 = p2; + nPebiCount = m_nPebiCount; return true; } +bool nmCalculationPebiGrid::isCurrentGridAvailableFor( + const nmDataAnalyzeManager* pDataManager, + quint64 nGridInputRevision) const +{ + // 该接口在主线程只做常量时间校验,不复制大型网格输出;锁忙时交给后台重建。 + if(!s_oPebiGridMutex.tryLock()) { + return false; + } + + const bool bAvailable = pDataManager != nullptr && + m_pDataManager == pDataManager && + pDataManager->isPebiGridValid() && + m_nCachedGridInputRevision == nGridInputRevision && + !p1.PEBI_cell.p.empty(); + s_oPebiGridMutex.unlock(); + return bAvailable; +} + void nmCalculationPebiGrid::logInputParameters(const HX_NWTM_GRID_INPUT& input) { QString logMsg = "Input Parameters:\n"; @@ -415,7 +497,8 @@ bool nmCalculationPebiGrid::meshGenPebiBoundary( bool nmCalculationPebiGrid::meshGenPebiWells( nmDataAnalyzeManager* pDataManager, HX_NWTM_GRID_INPUT& inputObj, - QVector& vecSolverWellOrder) + QVector& vecSolverWellOrder, + const QSet& setEffectiveWellCodes) { // 从数据中心获取井数据 @@ -440,13 +523,6 @@ bool nmCalculationPebiGrid::meshGenPebiWells( // 第一步:求解器顺序只写入局部快照,后台成功前不修改分析方案。 vecSolverWellOrder.clear(); - QSet setEffectiveWellCodes; - QVector vecEffectiveWells = - pDataManager->getEffectiveCalculationWells(); - for(int nIndex = 0; nIndex < vecEffectiveWells.size(); ++nIndex) { - setEffectiveWellCodes.insert(vecEffectiveWells[nIndex].m_sWellCode); - } - // 获取直井数据 QVector verticalWells = pDataManager->getVerticalWellData(); // 获取垂直裂缝井数据 @@ -816,7 +892,9 @@ int nmCalculationPebiGrid::getPebiCount() const return m_nPebiCount; } -vtkSmartPointer nmCalculationPebiGrid::createPebiUnstructuredGrid(const HX_NWTM_GRID_OUTPUT1& P1) +vtkSmartPointer nmCalculationPebiGrid::createPebiUnstructuredGrid( + const HX_NWTM_GRID_OUTPUT1& P1, + const QAtomicInt* pCancelRequested) { vtkSmartPointer pUnstructuredGrid = vtkSmartPointer::New(); vtkSmartPointer pPoints = vtkSmartPointer::New(); @@ -827,6 +905,9 @@ vtkSmartPointer nmCalculationPebiGrid::createPebiUnstructur // 1. 预处理单元数据,确定哪些单元是有效的,并收集这些单元引用的所有唯一点索引 // 这一步先不向 vtkPoints 添加点,而是收集需要添加的点的索引。 for(size_t i = 0; i < P1.PEBI_cell.pindex.size(); ++i) { + if((i % 256) == 0 && isCancellationRequested(pCancelRequested)) { + return nullptr; + } // 只处理 isplot 为 1 的单元 if(i >= P1.PEBI_cell.isplot.size() || P1.PEBI_cell.isplot[i] != 1) { continue; @@ -874,6 +955,9 @@ vtkSmartPointer nmCalculationPebiGrid::createPebiUnstructur // 3. 再次遍历单元数据,这次是根据新的 VTK 点索引来插入单元 for(size_t i = 0; i < P1.PEBI_cell.pindex.size(); ++i) { + if((i % 256) == 0 && isCancellationRequested(pCancelRequested)) { + return nullptr; + } // 再次检查 isplot 标志,确保只处理有效单元 if(i >= P1.PEBI_cell.isplot.size() || P1.PEBI_cell.isplot[i] != 1) { continue; @@ -925,9 +1009,265 @@ vtkSmartPointer nmCalculationPebiGrid::createPebiUnstructur return pUnstructuredGrid; } -bool nmCalculationPebiGrid::captureInputSnapshot( +bool nmCalculationPebiGrid::beginManualInputSnapshot( + nmDataAnalyzeManager* pDataManager, + nmPebiGridInputSnapshot& oSnapshot) +{ + const nmPebiGridInputSnapshot oEmptySnapshot; + oSnapshot = oEmptySnapshot; + if(pDataManager == nullptr || + QThread::currentThread() != pDataManager->thread()) { + qWarning() << "Manual PEBI input must be captured on the DataManager thread."; + return false; + } + + const nmDataNumericalAnalysisCase* pAnalysisCase = + pDataManager->getNumericalAnalysisCase(); + if(pAnalysisCase == nullptr) { + return false; + } + + oSnapshot.m_nGridInputRevision = + pAnalysisCase->getGridInputRevision(); + oSnapshot.m_oGridInput = HX_NWTM_GRID_INPUT(); + oSnapshot.m_oGridInput.GridControl = + pDataManager->getPebiGridControl(); + + // 边界只捕获一次;后续井批次只追加值,不重复访问已经完成的对象。 + return meshGenPebiBoundary(pDataManager, oSnapshot.m_oGridInput); +} + +bool nmCalculationPebiGrid::appendManualWellInputSnapshot( + nmDataWellBase* pWellData, + int nWellMode, + nmPebiGridInputSnapshot& oSnapshot) +{ + if(pWellData == nullptr || pWellData->getWellCode().isEmpty() || + oSnapshot.m_bCaptured) { + return false; + } + + nmSolverWellRef oWellRef; + nmDataHorizontalFracturedWell* pHorizontalFracturedWell = + dynamic_cast(pWellData); + nmDataVerticalFracturedWell* pVerticalFracturedWell = + dynamic_cast(pWellData); + nmDataVerticalWell* pVerticalWell = + dynamic_cast(pWellData); + + if(pHorizontalFracturedWell != nullptr) { + const QVector > vecFracPoints = + pHorizontalFracturedWell->getFracs(); + std::vector > vecFractures; + vecFractures.reserve(vecFracPoints.size()); + for(int nIndex = 0; nIndex < vecFracPoints.size(); ++nIndex) { + dVec1 oCrack(6); + oCrack[0] = vecFracPoints[nIndex].first.x(); + oCrack[1] = vecFracPoints[nIndex].first.y(); + oCrack[2] = vecFracPoints[nIndex].second.x(); + oCrack[3] = vecFracPoints[nIndex].second.y(); + oCrack[4] = pHorizontalFracturedWell->getWidth() + .getValue().toDouble(); + oCrack[5] = pHorizontalFracturedWell->getDfc() + .getValue().toDouble(); + vecFractures.push_back(oCrack); + } + oSnapshot.m_oGridInput.MultistageFracturedHorizontalWell + .push_back(vecFractures); + oWellRef = nmSolverWellRef( + -1, + NM_WELL_MODEL::Horizontal_Fractured_Well, + pWellData->getWellCode()); + } else if(pVerticalFracturedWell != nullptr) { + const QVector vecFracPoints = + pVerticalFracturedWell->getFracs(); + if(vecFracPoints.size() != 2) { + return false; + } + dVec1 oCrack(6); + oCrack[0] = vecFracPoints[0].x(); + oCrack[1] = vecFracPoints[0].y(); + oCrack[2] = vecFracPoints[1].x(); + oCrack[3] = vecFracPoints[1].y(); + oCrack[4] = pVerticalFracturedWell->getWidth() + .getValue().toDouble(); + oCrack[5] = pVerticalFracturedWell->getDfc() + .getValue().toDouble(); + oSnapshot.m_oGridInput.FractureVerticalWell.push_back(oCrack); + oWellRef = nmSolverWellRef( + -1, + NM_WELL_MODEL::Vertical_Fractured_Well, + pWellData->getWellCode()); + } else if(pVerticalWell != nullptr) { + dVec1 oWell(3); + oWell[0] = pVerticalWell->getX().getValue().toDouble(); + oWell[1] = pVerticalWell->getY().getValue().toDouble(); + oWell[2] = pVerticalWell->getRadius().getValue().toDouble(); + oSnapshot.m_oGridInput.VerticalWell.push_back(oWell); + oWellRef = nmSolverWellRef( + -1, + NM_WELL_MODEL::Vertical_Well, + pWellData->getWellCode()); + } else { + return false; + } + + nmPebiWellInputSnapshot oWellInput; + oWellInput.m_bRealWell = true; + oWellInput.m_sWellCode = pWellData->getWellCode(); + oWellInput.m_sWellName = pWellData->getWellName(); + oWellInput.m_vecFlowPoints = pWellData->getFlowPoints(); + oWellInput.m_nFlowSectionIndex = pWellData->getIndexF(); + oWellInput.m_oLocation = QPointF( + pWellData->getX().getValue().toDouble(), + pWellData->getY().getValue().toDouble()); + oWellInput.m_dWellboreStorage = + pWellData->getWellboreStorage().getValue().toDouble(); + oWellInput.m_dSkin = pWellData->getPerforationCount() > 0 + ? pWellData->getPerforation(0)->getSkin().getValue().toDouble() + : 0.0; + oWellInput.m_bRateControlled = + nWellMode == static_cast(NM_CaseWell_RateControlled); + if(oWellInput.m_bRateControlled && + oWellInput.m_vecFlowPoints.size() < 2) { + return false; + } + + oSnapshot.m_vecSolverWellOrder.append(oWellRef); + oSnapshot.m_vecWellInputs.append(oWellInput); + return true; +} + +bool nmCalculationPebiGrid::finishManualInputSnapshot( nmDataAnalyzeManager* pDataManager, + const QSet& setEffectiveWellCodes, nmPebiGridInputSnapshot& oSnapshot) +{ + if(pDataManager == nullptr || setEffectiveWellCodes.isEmpty() || + QThread::currentThread() != pDataManager->thread() || + oSnapshot.m_vecSolverWellOrder.size() != + oSnapshot.m_vecWellInputs.size()) { + return false; + } + + const nmDataNumericalAnalysisCase* pAnalysisCase = + pDataManager->getNumericalAnalysisCase(); + if(pAnalysisCase == nullptr || + pAnalysisCase->getGridInputRevision() != + oSnapshot.m_nGridInputRevision) { + return false; + } + + QHash mapWellInputs; + for(int nIndex = 0; nIndex < oSnapshot.m_vecWellInputs.size(); ++nIndex) { + const nmPebiWellInputSnapshot& oWellInput = + oSnapshot.m_vecWellInputs[nIndex]; + if(oWellInput.m_sWellCode.isEmpty() || + mapWellInputs.contains(oWellInput.m_sWellCode)) { + return false; + } + mapWellInputs.insert(oWellInput.m_sWellCode, oWellInput); + } + + if(!meshGenPebiFault(pDataManager, oSnapshot.m_oGridInput) || + !meshGenPebiCrack(pDataManager, + oSnapshot.m_oGridInput, + oSnapshot.m_vecSolverWellOrder)) { + return false; + } + + QVector vecOrderedWellInputs; + vecOrderedWellInputs.reserve(oSnapshot.m_vecSolverWellOrder.size()); + QSet setOrderedWellCodes; + for(int nIndex = 0; + nIndex < oSnapshot.m_vecSolverWellOrder.size(); + ++nIndex) { + nmSolverWellRef& oWellRef = oSnapshot.m_vecSolverWellOrder[nIndex]; + oWellRef.m_nSolverIndex = nIndex; + if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture) { + vecOrderedWellInputs.append(nmPebiWellInputSnapshot()); + continue; + } + if(oWellRef.m_eEntryKind != NM_SolverEntry_Well || + !setEffectiveWellCodes.contains(oWellRef.m_sWellCode) || + setOrderedWellCodes.contains(oWellRef.m_sWellCode) || + !mapWellInputs.contains(oWellRef.m_sWellCode)) { + return false; + } + setOrderedWellCodes.insert(oWellRef.m_sWellCode); + vecOrderedWellInputs.append(mapWellInputs.value(oWellRef.m_sWellCode)); + } + if(setOrderedWellCodes != setEffectiveWellCodes) { + return false; + } + oSnapshot.m_vecWellInputs = vecOrderedWellInputs; + + const nmDataBinaryTools::NM_PEBI_SCENE oEmptyScene; + oSnapshot.m_oScene = oEmptyScene; + const NM_SOLVER_MODEL_TYPE eSolverModelType = + pDataManager->getSolverModelType(); + oSnapshot.m_oScene.solverType = + static_cast(eSolverModelType); + + nmDataReservoir* pReservoirData = + pDataManager->getReservoirData(); + nmDataPvtParaForPebi* pPvtData = + pDataManager->getPebiPvtPara(); + fillScenePvtByModel(oSnapshot.m_oScene, + eSolverModelType, + pPvtData, + pReservoirData); + fillScenePseudoPressureTable(oSnapshot.m_oScene, + eSolverModelType, + pDataManager); + + if(pReservoirData != nullptr) { + oSnapshot.m_oScene.Base.Pi = pReservoirData->getInitialPressure() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.Cti = pReservoirData->getCt() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.Cf = pReservoirData->getCf() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.Soi = pReservoirData->getSoi() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.Sgi = pReservoirData->getSgi() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.Swi = pReservoirData->getSwi() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.k_ref = + nmCalculationUtils::milliDarcyToDarcy( + pReservoirData->getPermeability() + .getValue().toDouble()); + oSnapshot.m_oScene.Base.phi_ref = pReservoirData->getPorosity() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.h_ref = pReservoirData->getThickness() + .getValue().toDouble(); + } + + nmDataTimeStepSetting* pTimeStepSetting = + pDataManager->getTimeStep(); + if(pTimeStepSetting != nullptr) { + oSnapshot.m_oScene.Base.d = + pTimeStepSetting->getTimeGrowthExponent() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.dt_Min = + pTimeStepSetting->getMinDeltaTAttribute() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.dt_Max = + pTimeStepSetting->getMaxDeltaTAttribute() + .getValue().toDouble(); + } + + oSnapshot.m_sLicensePath = pDataManager->getLicensePath(); + oSnapshot.m_bCaptured = true; + oSnapshot.m_bValid = false; + return true; +} + +bool nmCalculationPebiGrid::captureInputSnapshot( + nmDataAnalyzeManager* pDataManager, + nmPebiGridInputSnapshot& oSnapshot, + bool bDeferPreparation) { // 使用具名常量触发复制赋值,兼容 Qt 4.8 配套的 VS2010 运行库 ABI。 const nmPebiGridInputSnapshot oEmptySnapshot; @@ -956,11 +1296,23 @@ bool nmCalculationPebiGrid::captureInputSnapshot( oSnapshot.m_oGridInput.GridControl = pDataManager->getPebiGridControl(); - // 第二步:把 Map 中的边界、有效井、断层和手工裂缝全部转成 DLL 值类型。 + // 第二步:有效井集合只计算一次,后续几何、角色和结果井快照共同复用。 + const QVector vecEffectiveWells = + pDataManager->getEffectiveCalculationWells(); + QSet setEffectiveWellCodes; + QHash mapWellModes; + for(int nIndex = 0; nIndex < vecEffectiveWells.size(); ++nIndex) { + setEffectiveWellCodes.insert(vecEffectiveWells[nIndex].m_sWellCode); + mapWellModes.insert(vecEffectiveWells[nIndex].m_sWellCode, + static_cast(vecEffectiveWells[nIndex].m_eMode)); + } + + // 第三步:把 Map 中的边界、有效井、断层和手工裂缝全部转成 DLL 值类型。 if(!meshGenPebiBoundary(pDataManager, oSnapshot.m_oGridInput) || !meshGenPebiWells(pDataManager, oSnapshot.m_oGridInput, - oSnapshot.m_vecSolverWellOrder) || + oSnapshot.m_vecSolverWellOrder, + setEffectiveWellCodes) || !meshGenPebiFault(pDataManager, oSnapshot.m_oGridInput) || !meshGenPebiCrack(pDataManager, oSnapshot.m_oGridInput, @@ -974,14 +1326,7 @@ bool nmCalculationPebiGrid::captureInputSnapshot( oSnapshot.m_vecSolverWellOrder[nIndex].m_nSolverIndex = nIndex; } - // 第三步:真实井必须与本次有效计算井一一对应;手工裂缝只占槽位,不参与集合比较。 - 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; @@ -1009,32 +1354,159 @@ bool nmCalculationPebiGrid::captureInputSnapshot( return false; } - // 第四步:场景和授权路径同样在捕获阶段读取,后台不再接触任何井对象。 - if(!buildPebiScene(pDataManager, - oSnapshot.m_oGridInput, - oSnapshot.m_vecSolverWellOrder, - oSnapshot.m_oScene)) { - return false; + // 第五步:建立 WellCode 索引并一次复制每口井,避免五十口井时反复线性查找。 + QHash mapWellsByCode; + const QVector vecAllWells = + pDataManager->getWellDataList(); + for(int nIndex = 0; nIndex < vecAllWells.size(); ++nIndex) { + nmDataWellBase* pWellData = vecAllWells[nIndex]; + if(pWellData != nullptr && !pWellData->getWellCode().isEmpty()) { + mapWellsByCode.insert(pWellData->getWellCode(), pWellData); + } } + + oSnapshot.m_vecWellInputs.clear(); + oSnapshot.m_vecWellInputs.reserve( + oSnapshot.m_vecSolverWellOrder.size()); + for(int nIndex = 0; + nIndex < oSnapshot.m_vecSolverWellOrder.size(); + ++nIndex) { + const nmSolverWellRef& oWellRef = + oSnapshot.m_vecSolverWellOrder[nIndex]; + nmPebiWellInputSnapshot oWellInput; + oWellInput.m_sWellCode = oWellRef.m_sWellCode; + + if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture) { + oSnapshot.m_vecWellInputs.append(oWellInput); + continue; + } + + nmDataWellBase* pWellData = + mapWellsByCode.value(oWellRef.m_sWellCode, nullptr); + if(pWellData == nullptr || !mapWellModes.contains(oWellRef.m_sWellCode)) { + qWarning() << "Solver well code is missing from Map:" + << oWellRef.m_sWellCode; + return false; + } + + oWellInput.m_bRealWell = true; + oWellInput.m_sWellName = pWellData->getWellName(); + oWellInput.m_vecFlowPoints = pWellData->getFlowPoints(); + oWellInput.m_nFlowSectionIndex = pWellData->getIndexF(); + oWellInput.m_oLocation = QPointF( + pWellData->getX().getValue().toDouble(), + pWellData->getY().getValue().toDouble()); + oWellInput.m_dWellboreStorage = + pWellData->getWellboreStorage().getValue().toDouble(); + oWellInput.m_dSkin = pWellData->getPerforationCount() > 0 + ? pWellData->getPerforation(0)->getSkin() + .getValue().toDouble() + : 0.0; + oWellInput.m_bRateControlled = + mapWellModes.value(oWellRef.m_sWellCode) == + static_cast(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; + } + oSnapshot.m_vecWellInputs.append(oWellInput); + } + + // 第六步:PVT、储层和时间步仍在主线程复制为值;较重的井制度和场景数组 + // 组装可由手工求解任务延后到后台执行。 + const nmDataBinaryTools::NM_PEBI_SCENE oEmptyScene; + oSnapshot.m_oScene = oEmptyScene; + const NM_SOLVER_MODEL_TYPE eSolverModelType = + pDataManager->getSolverModelType(); + oSnapshot.m_oScene.solverType = + static_cast(eSolverModelType); + + nmDataReservoir* pReservoirData = + pDataManager->getReservoirData(); + nmDataPvtParaForPebi* pPvtData = + pDataManager->getPebiPvtPara(); + fillScenePvtByModel(oSnapshot.m_oScene, + eSolverModelType, + pPvtData, + pReservoirData); + fillScenePseudoPressureTable(oSnapshot.m_oScene, + eSolverModelType, + pDataManager); + + if(pReservoirData != nullptr) { + oSnapshot.m_oScene.Base.Pi = pReservoirData->getInitialPressure() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.Cti = pReservoirData->getCt() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.Cf = pReservoirData->getCf() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.Soi = pReservoirData->getSoi() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.Sgi = pReservoirData->getSgi() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.Swi = pReservoirData->getSwi() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.k_ref = + nmCalculationUtils::milliDarcyToDarcy( + pReservoirData->getPermeability() + .getValue().toDouble()); + oSnapshot.m_oScene.Base.phi_ref = pReservoirData->getPorosity() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.h_ref = pReservoirData->getThickness() + .getValue().toDouble(); + } + + nmDataTimeStepSetting* pTimeStepSetting = + pDataManager->getTimeStep(); + if(pTimeStepSetting != nullptr) { + oSnapshot.m_oScene.Base.d = + pTimeStepSetting->getTimeGrowthExponent() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.dt_Min = + pTimeStepSetting->getMinDeltaTAttribute() + .getValue().toDouble(); + oSnapshot.m_oScene.Base.dt_Max = + pTimeStepSetting->getMaxDeltaTAttribute() + .getValue().toDouble(); + } + oSnapshot.m_sLicensePath = pDataManager->getLicensePath(); + oSnapshot.m_bCaptured = true; - oSnapshot.m_bValid = true; - return true; + return bDeferPreparation + ? true + : prepareInputSnapshot(oSnapshot); } -bool nmCalculationPebiGrid::buildPebiScene( - nmDataAnalyzeManager* pDataManager, - const HX_NWTM_GRID_INPUT& oGridInput, - const QVector& vecSolverWellOrder, - nmDataBinaryTools::NM_PEBI_SCENE& oScene) +bool nmCalculationPebiGrid::prepareInputSnapshot( + nmPebiGridInputSnapshot& oSnapshot, + const QAtomicInt* pCancelRequested) { - if(pDataManager == nullptr) { + oSnapshot.m_bValid = false; + if(!oSnapshot.m_bCaptured || + oSnapshot.m_vecWellInputs.size() != + oSnapshot.m_vecSolverWellOrder.size() || + isCancellationRequested(pCancelRequested)) { return false; } - // 第一步:复制网格基础数据和井显示信息。 - const nmDataBinaryTools::NM_PEBI_SCENE oEmptyScene; - oScene = oEmptyScene; + return buildPebiScene(oSnapshot, pCancelRequested); +} + +bool nmCalculationPebiGrid::buildPebiScene( + nmPebiGridInputSnapshot& oSnapshot, + const QAtomicInt* pCancelRequested) +{ + const HX_NWTM_GRID_INPUT& oGridInput = oSnapshot.m_oGridInput; + const QVector& vecSolverWellOrder = + oSnapshot.m_vecSolverWellOrder; + const QVector& vecWellInputs = + oSnapshot.m_vecWellInputs; + nmDataBinaryTools::NM_PEBI_SCENE& oScene = oSnapshot.m_oScene; + + // 第一步:复制网格基础数据。PVT、储层和时间步已在主线程值化。 oScene.version = 1; oScene.D = oGridInput.D; oScene.GridControl = oGridInput.GridControl; @@ -1052,20 +1524,17 @@ bool nmCalculationPebiGrid::buildPebiScene( oScene.wellType.reserve(vecSolverWellOrder.size()); oScene.wellName.reserve(vecSolverWellOrder.size()); for(int nIndex = 0; nIndex < vecSolverWellOrder.size(); ++nIndex) { + if(isCancellationRequested(pCancelRequested)) { + return false; + } 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()); + oScene.wellName.push_back(vecWellInputs[nIndex].m_sWellName); } // 第二步:按求解器模型填充每个槽位的产量制度。 const NM_SOLVER_MODEL_TYPE eSolverModelType = - pDataManager->getSolverModelType(); - oScene.solverType = static_cast(eSolverModelType); + static_cast(oScene.solverType); oScene.Rate.t.resize(vecSolverWellOrder.size()); oScene.Rate.qo.resize(vecSolverWellOrder.size()); oScene.Rate.qg.resize(vecSolverWellOrder.size()); @@ -1076,21 +1545,20 @@ bool nmCalculationPebiGrid::buildPebiScene( ++nWellIndex) { const nmSolverWellRef& oWellRef = vecSolverWellOrder[nWellIndex]; + const nmPebiWellInputSnapshot& oWellInput = + vecWellInputs[nWellIndex]; - // 手工裂缝和观察井保留外层槽位,但不提供源汇项。 - if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture || - pDataManager->getCalculationWellMode( - oWellRef.m_sWellCode) == NM_CaseWell_Observation) { - continue; + if(isCancellationRequested(pCancelRequested)) { + return false; } - nmDataWellBase* pWellData = - pDataManager->findWellByCode(oWellRef.m_sWellCode); - if(pWellData == nullptr) { + // 手工裂缝和观察井保留外层槽位,但不提供源汇项。 + if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture || + !oWellInput.m_bRateControlled) { continue; } - QVector vecTimeQ = pWellData->getFlowPoints(); + QVector vecTimeQ = oWellInput.m_vecFlowPoints; if(!vecTimeQ.isEmpty() && vecTimeQ[0].x() == 0.0 && vecTimeQ[0].y() == 0.0) { @@ -1104,6 +1572,10 @@ bool nmCalculationPebiGrid::buildPebiScene( for(int nPointIndex = 0; nPointIndex < vecTimeQ.size(); ++nPointIndex) { + if((nPointIndex % 256) == 0 && + isCancellationRequested(pCancelRequested)) { + return false; + } vecTime.push_back(vecTimeQ[nPointIndex].x()); vecRate.push_back(vecTimeQ[nPointIndex].y()); } @@ -1129,6 +1601,9 @@ bool nmCalculationPebiGrid::buildPebiScene( for(size_t nWellIndex = 0; nWellIndex < oScene.Rate.t.size(); ++nWellIndex) { + if(isCancellationRequested(pCancelRequested)) { + return false; + } oScene.Rate.qg[nWellIndex].assign( oScene.Rate.t[nWellIndex].size(), 0.0); oScene.Rate.qw[nWellIndex].assign( @@ -1146,6 +1621,11 @@ bool nmCalculationPebiGrid::buildPebiScene( ++nWellIndex) { const nmSolverWellRef& oWellRef = vecSolverWellOrder[nWellIndex]; + const nmPebiWellInputSnapshot& oWellInput = + vecWellInputs[nWellIndex]; + if(isCancellationRequested(pCancelRequested)) { + return false; + } if(oWellRef.m_eEntryKind == NM_SolverEntry_ManualFracture) { oScene.CS.C[nWellIndex] = 0.0; oScene.CS.S[nWellIndex] = 0.0; @@ -1153,89 +1633,39 @@ bool nmCalculationPebiGrid::buildPebiScene( continue; } - nmDataWellBase* pWellData = - pDataManager->findWellByCode(oWellRef.m_sWellCode); - if(pWellData == nullptr) { - oScene.wellFlowSectionIndex[nWellIndex] = 1; - continue; - } - - oScene.CS.C[nWellIndex] = - pWellData->getWellboreStorage().getValue().toDouble(); - oScene.CS.S[nWellIndex] = - pWellData->getPerforationCount() > 0 - ? pWellData->getPerforation(0)->getSkin() - .getValue().toDouble() - : 0.0; + oScene.CS.C[nWellIndex] = oWellInput.m_dWellboreStorage; + oScene.CS.S[nWellIndex] = oWellInput.m_dSkin; oScene.wellFlowSectionIndex[nWellIndex] = - pWellData->getIndexF(); - } - - // 第四步:复制 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(); - } - - 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(); + oWellInput.m_nFlowSectionIndex; } - return true; + oSnapshot.m_bValid = !isCancellationRequested(pCancelRequested); + return oSnapshot.m_bValid; } bool nmCalculationPebiGrid::calculateSnapshot( const nmPebiGridInputSnapshot& oSnapshot, nmPebiGridResult& oResult, - bool bCreateUnstructuredGrid) + bool bCreateUnstructuredGrid, + const QAtomicInt* pCancelRequested) { - QMutexLocker oLocker(&s_oPebiGridMutex); + nmInterruptibleMutexLocker oGridLocker; + if(!oGridLocker.lock(&s_oPebiGridMutex, pCancelRequested)) { + return false; + } const nmPebiGridResult oEmptyResult; oResult = oEmptyResult; - if(!oSnapshot.m_bValid) { + if(!oSnapshot.m_bValid || isCancellationRequested(pCancelRequested)) { return false; } // 建网、模型求解和 Kriging 共用 DLL 全局状态,加载至读取结果期间必须串行。 - QMutexLocker oDllLocker( - nmCalculationUtils::getHxNwtmDllMutex()); + nmInterruptibleMutexLocker oDllLocker; + if(!oDllLocker.lock(nmCalculationUtils::getHxNwtmDllMutex(), + pCancelRequested)) { + return false; + } HMODULE hGridModule = LoadLibrary(L"HX_NWTM.dll"); if(hGridModule == nullptr) { qWarning() << "Failed to load HX_NWTM.dll. Error code:" @@ -1289,6 +1719,11 @@ bool nmCalculationPebiGrid::calculateSnapshot( zxLogInstance::getInstance()->writeLogF( "scene exported: " + sScenePath); + if(isCancellationRequested(pCancelRequested)) { + FreeLibrary(hGridModule); + return false; + } + // 第二步:DLL 输出先落在局部结果,失败时不清空单例缓存和已有成果。 pfnGenerateGrid(oResult.m_oGridOutput1, oResult.m_oGridOutput2, @@ -1301,18 +1736,24 @@ bool nmCalculationPebiGrid::calculateSnapshot( hGridModule = nullptr; oDllLocker.unlock(); + // DLL 本身没有取消入口;若执行期间收到停止请求,返回后立即丢弃局部输出。 + if(isCancellationRequested(pCancelRequested)) { + return false; + } + // 自动拟合只需要 DLL 数组,可跳过 VTK 构造;网格任务必须生成完整 VTK。 if(bCreateUnstructuredGrid) { oResult.m_pUnstructuredGrid = createPebiUnstructuredGrid( - oResult.m_oGridOutput1); + oResult.m_oGridOutput1, + pCancelRequested); if(oResult.m_pUnstructuredGrid == nullptr || oResult.m_pUnstructuredGrid->GetNumberOfCells() <= 0) { return false; } } - oResult.m_bSucceeded = true; + oResult.m_bSucceeded = !isCancellationRequested(pCancelRequested); } catch(const std::exception& e) { zxLogInstance::getInstance()->writeLogF( QString("C++ Exception: %1").arg(e.what())); diff --git a/Src/nmNum/nmCalculation/nmCalculationUtils.cpp b/Src/nmNum/nmCalculation/nmCalculationUtils.cpp index 69de14a..ec0edb7 100644 --- a/Src/nmNum/nmCalculation/nmCalculationUtils.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationUtils.cpp @@ -17,6 +17,53 @@ namespace // HX_NWTM.dll 的配置和结果查询接口使用进程级共享状态,所有入口共用此锁。 QMutex s_oHxNwtmDllMutex; +bool isKrigingCancellationRequested(const QAtomicInt* pCancelRequested) +{ + return pCancelRequested != NULL && + static_cast(*pCancelRequested) != 0; +} + +class nmKrigingMutexLocker +{ +public: + nmKrigingMutexLocker() + : m_pMutex(NULL), + m_bLocked(false) + { + } + + ~nmKrigingMutexLocker() + { + if(m_bLocked && m_pMutex != NULL) { + m_pMutex->unlock(); + } + } + + bool lock(QMutex* pMutex, const QAtomicInt* pCancelRequested) + { + if(pMutex == NULL) { + return false; + } + if(pCancelRequested == NULL) { + pMutex->lock(); + } else { + // 只改变等待方式,不改变 DLL 全局锁的保护范围和互斥语义。 + while(!pMutex->tryLock(100)) { + if(isKrigingCancellationRequested(pCancelRequested)) { + return false; + } + } + } + m_pMutex = pMutex; + m_bLocked = true; + return true; + } + +private: + QMutex* m_pMutex; + bool m_bLocked; +}; + QString krigingText(const char* sourceText) { return QCoreApplication::translate("nmCalculationUtils", sourceText); @@ -213,11 +260,17 @@ bool nmCalculationUtils::calculateKriging( int model, const QString& licensePath, QVector& outputValues, - QString* errorMessage) + QString* errorMessage, + const QAtomicInt* pCancelRequested) { outputValues.clear(); setKrigingError(errorMessage, QString()); + if(isKrigingCancellationRequested(pCancelRequested)) { + setKrigingError(errorMessage, krigingText("Kriging calculation was cancelled.")); + return false; + } + if(targetPoints.isEmpty()) { setKrigingError(errorMessage, krigingText("No interpolation points are available.")); return false; @@ -247,6 +300,11 @@ bool nmCalculationUtils::calculateKriging( } for(int i = 0; i < targetPoints.size(); ++i) { + if((i % 256) == 0 && + isKrigingCancellationRequested(pCancelRequested)) { + setKrigingError(errorMessage, krigingText("Kriging calculation was cancelled.")); + return false; + } if(!isFiniteValue(targetPoints[i].x()) || !isFiniteValue(targetPoints[i].y())) { setKrigingError(errorMessage, krigingText("An interpolation point contains an invalid coordinate.")); @@ -255,6 +313,10 @@ bool nmCalculationUtils::calculateKriging( } for(int i = 0; i < measurementPoints.size(); ++i) { + if(isKrigingCancellationRequested(pCancelRequested)) { + setKrigingError(errorMessage, krigingText("Kriging calculation was cancelled.")); + return false; + } if(!isFiniteValue(measurementPoints[i].x()) || !isFiniteValue(measurementPoints[i].y()) || !isFiniteValue(measurementValues[i])) { @@ -278,7 +340,11 @@ bool nmCalculationUtils::calculateKriging( } // Kriging 与建网、模型求解来自同一个 DLL,不能在不同线程中并发进入。 - QMutexLocker oDllLocker(getHxNwtmDllMutex()); + nmKrigingMutexLocker oDllLocker; + if(!oDllLocker.lock(getHxNwtmDllMutex(), pCancelRequested)) { + setKrigingError(errorMessage, krigingText("Kriging calculation was cancelled.")); + return false; + } HMODULE dll = LoadLibrary(L"HX_NWTM.dll"); if(dll == NULL) { setKrigingError(errorMessage, krigingText("Failed to load HX_NWTM.dll.")); @@ -338,6 +404,13 @@ bool nmCalculationUtils::calculateKriging( calculationError = krigingText("Kriging calculation failed."); } + // DLL 无取消入口;调用期间收到停止请求时只丢弃返回值,不提交插值结果。 + if(calculationSucceeded && + isKrigingCancellationRequested(pCancelRequested)) { + calculationSucceeded = false; + calculationError = krigingText("Kriging calculation was cancelled."); + } + if(calculationSucceeded && output.v.size() != static_cast(targetPoints.size())) { calculationSucceeded = false; @@ -348,6 +421,12 @@ bool nmCalculationUtils::calculateKriging( if(calculationSucceeded) { outputValues.reserve(targetPoints.size()); for(size_t i = 0; i < output.v.size(); ++i) { + if((i % 256) == 0 && + isKrigingCancellationRequested(pCancelRequested)) { + calculationSucceeded = false; + calculationError = krigingText("Kriging calculation was cancelled."); + break; + } if(!isFiniteValue(output.v[i])) { calculationSucceeded = false; calculationError = krigingText( diff --git a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp index 7f82be7..36d0a35 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp @@ -67,6 +67,8 @@ #include #include +#include +#include #include "nmSingalCenter.h" @@ -95,6 +97,7 @@ namespace // 保留原来求解器单例的防重入语义:全局同一时间只允许一个DLL求解任务运行。 // QPointer会在QObject销毁后自动变空,避免保留已经释放的线程指针。 QPointer s_pRunningSolverTask; +QPointer s_pPendingSolverWindow; } nmSubWndMain::nmSubWndMain(QWidget *parent, QString sExt) : @@ -127,9 +130,8 @@ nmSubWndMain::nmSubWndMain(QWidget *parent, QString sExt) : m_pSolverTask = nullptr; m_pSolverDataManager = nullptr; m_pSolverFitting = nullptr; - m_pFakeProgressTimer = nullptr; - m_nVirtualProgress = 0; - m_nSlowDownCounter = 0; + m_bSolverStartPending = false; + m_bSolverCancelRequested = false; m_pPlotToolBar = nullptr; } @@ -171,11 +173,11 @@ nmSubWndMain::~nmSubWndMain() // } //} - if (m_pFakeProgressTimer) { - m_pFakeProgressTimer->stop(); - // 因为构造时传了 this,这里不 delete 也可以,但 disconnect 是安全的 - m_pFakeProgressTimer->disconnect(); - } + + m_bSolverStartPending = false; + if(s_pPendingSolverWindow == this) { + s_pPendingSolverWindow = nullptr; + } if (m_pProgressDlg) { delete m_pProgressDlg; @@ -185,8 +187,9 @@ nmSubWndMain::~nmSubWndMain() if (m_pSolverTask) { // 第一步:停止向正在析构的窗口投递完成回调。 disconnect(m_pSolverTask, SIGNAL(sig_calculateDone(bool)), this, SLOT(on_solverTaskFinished(bool))); + m_pSolverTask->requestCancel(); // 第二步:DataManager 会在成果窗口销毁后释放,而任务仍读取该对象。 - // Qt 4.8 下只能等待外部 DLL 正常返回,绝不能让裸指针越过管理器生命周期。 + // Qt 4.8 下 DLL 执行中只能等待其正常返回,绝不能强杀持锁线程。 if(m_pSolverTask->isRunning()) { m_pSolverTask->wait(); } @@ -1518,6 +1521,12 @@ void nmSubWndMain::generationMesh() void nmSubWndMain::solveAndAnalyze() { + if(!s_pPendingSolverWindow.isNull() || + !s_pRunningSolverTask.isNull()) { + QMessageBox::information(this, tr("solver error"), tr("task is running!")); + return; + } + // 强制清理旧的(以防万一上次没删掉) if (m_pProgressDlg != nullptr) { delete m_pProgressDlg; @@ -1556,11 +1565,20 @@ void nmSubWndMain::solveAndAnalyze() // 观察井可以完全不传产量,但不能在所有井都是观察井时启动无源汇计算。 QVector vecEffectiveWells = pDataManager->getEffectiveCalculationWells(); + QHash mapWellsByCode; + const QVector vecAllWells = + pDataManager->getWellDataList(); + for(int nIndex = 0; nIndex < vecAllWells.size(); ++nIndex) { + nmDataWellBase* pWellData = vecAllWells[nIndex]; + if(pWellData != nullptr) { + mapWellsByCode.insert(pWellData->getWellCode(), pWellData); + } + } bool bHasRateControlledWell = false; for(int nIndex = 0; nIndex < vecEffectiveWells.size(); ++nIndex) { const nmCalculationWellRef& oWellRef = vecEffectiveWells[nIndex]; nmDataWellBase* pWellData = - pDataManager->findWellByCode(oWellRef.m_sWellCode); + mapWellsByCode.value(oWellRef.m_sWellCode, nullptr); if(pWellData == nullptr) { QMessageBox::warning(this, tr("solver error"), tr("A selected well is missing from the map.")); @@ -1605,23 +1623,6 @@ void nmSubWndMain::solveAndAnalyze() } } - // 调用求解器 - QString sPostprocessingPath = ""; - - // 获取计算类型 - NM_Grid_Type gridType = pDataManager->getGridType(); - - // PEBI求解直接使用当前后处理目录 - if(gridType == NM_Grid_PEBI) { - //sPostprocessingPath = sPostprocessingPath + "/Pebi"; - } - - // 以前由旧的DLL求解器单例阻止重复启动;删掉中间层后在这里保留同样的保护。 - if(!s_pRunningSolverTask.isNull() && s_pRunningSolverTask->isRunning()) { - QMessageBox::information(this, tr("solver error"), tr("task is running!")); - return; - } - if(m_pSolverTask != nullptr) { // 上一次线程已结束但指针还未清空时,先交给Qt事件循环安全释放。 m_pSolverTask->deleteLater(); @@ -1631,48 +1632,184 @@ void nmSubWndMain::solveAndAnalyze() // 第一步:捕获本次求解的完整窗口上下文,完成回调不得再读取全局当前窗口。 m_pSolverDataManager = pDataManager; m_pSolverFitting = pSubWndFit; + m_bSolverCancelRequested = false; - // 第二步:直接创建真正执行 DLL 计算的 PEBI 线程任务。 - m_pSolverTask = new nmCalculationDllPebiSolverTask(sPostprocessingPath, - pDataManager); - s_pRunningSolverTask = m_pSolverTask; - connect(m_pSolverTask, SIGNAL(sig_calculateDone(bool)), this, SLOT(on_solverTaskFinished(bool))); - - // 1. 创建并配置进度对话框 + // 第二步:先显示真实进度框,再在下一轮事件中捕获输入并启动线程。 if (m_pProgressDlg == nullptr) { - // TODO:关联到父窗口 - m_pProgressDlg = new QProgressDialog(tr("Calculating, please wait..."), QString(), 0, 100, getMainWindow()); + m_pProgressDlg = new QProgressDialog( + tr("Preparing input snapshot..."), + tr("Stop"), + 0, + 0, + getMainWindow()); m_pProgressDlg->setWindowTitle(tr("Solver Progress")); // 后台只读取任务值快照;应用级模态仍用于阻止重复启动、切换成果等会让 // 用户误判当前计算归属的操作,数据安全不再依赖该对话框冻结界面。 m_pProgressDlg->setWindowModality(Qt::ApplicationModal); // 某些系统下,对话框右上角的关闭按钮可能仍存 - // 使用 WindowFlags 彻底禁用关闭按钮 - m_pProgressDlg->setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint); + // 使用 WindowFlags 彻底禁用关闭按钮 + m_pProgressDlg->setWindowFlags(Qt::Dialog | Qt::WindowTitleHint | Qt::CustomizeWindowHint); + m_pProgressDlg->setMinimumDuration(0); + m_pProgressDlg->setAutoClose(false); + m_pProgressDlg->setAutoReset(false); + connect(m_pProgressDlg, SIGNAL(canceled()), + this, SLOT(onSolverCancelRequested())); } - m_pProgressDlg->setValue(0); + m_pProgressDlg->setLabelText(tr("Preparing input snapshot...")); + m_pProgressDlg->setRange(0, 0); m_pProgressDlg->show(); m_pProgressDlg->raise(); // 提升层级到最前 m_pProgressDlg->activateWindow(); // 激活窗口焦点 - // 2. 初始化模拟进度逻辑 - // 每次计算启动前,务必清零 - m_nVirtualProgress = 0; - m_nSlowDownCounter = 0; + m_bSolverStartPending = true; + s_pPendingSolverWindow = this; + QTimer::singleShot(0, this, SLOT(startSolverTask())); +} + +void nmSubWndMain::startSolverTask() +{ + if(!m_bSolverStartPending) { + return; + } - // 创建定时器 (如果不存在) - if (m_pFakeProgressTimer == nullptr) { - m_pFakeProgressTimer = new QTimer(this); - connect(m_pFakeProgressTimer, SIGNAL(timeout()), this, SLOT(slotUpdateFakeProgress())); + if(m_pProgressDlg == nullptr || m_pSolverDataManager.isNull() || + m_pSolverFitting.isNull()) { + m_bSolverStartPending = false; + if(s_pPendingSolverWindow == this) { + s_pPendingSolverWindow = nullptr; + } + if(m_pProgressDlg != nullptr) { + m_pProgressDlg->deleteLater(); + m_pProgressDlg = nullptr; + } + m_pSolverDataManager = nullptr; + m_pSolverFitting = nullptr; + return; } - // 3. 启动计算并开启模拟进度 - m_pSolverTask->start(); - m_pFakeProgressTimer->start(500); // 每 500 毫秒增加一次 + // 先创建不自动捕获的任务,使停止按钮在第一批井输入开始前就有明确接收者。 + m_pSolverTask = new nmCalculationDllPebiSolverTask( + QString(), + m_pSolverDataManager.data(), + QString(), + nullptr, + true); + s_pRunningSolverTask = m_pSolverTask; + connect(m_pSolverTask, SIGNAL(sig_calculateDone(bool)), + this, SLOT(on_solverTaskFinished(bool))); + connect(m_pSolverTask, SIGNAL(sigStageChanged(QString,int,int)), + this, SLOT(onSolverStageChanged(QString,int,int))); + // 给 Windows 一次完成首帧绘制的机会;后续每个事件循环只捕获一口井。 + QTimer::singleShot(20, this, SLOT(continueSolverInputCapture())); +} + +void nmSubWndMain::continueSolverInputCapture() +{ + if(!m_bSolverStartPending || m_pSolverTask == nullptr) { + return; + } + + bool bFinished = false; + const bool bSucceeded = + m_pSolverTask->captureManualInputStep(1, bFinished); + if(!bSucceeded) { + nmCalculationDllPebiSolverTask* pTask = m_pSolverTask; + const bool bCancelled = pTask->wasCancelled(); + m_pSolverTask = nullptr; + if(pTask == s_pRunningSolverTask) { + s_pRunningSolverTask = nullptr; + } + if(s_pPendingSolverWindow == this) { + s_pPendingSolverWindow = nullptr; + } + m_bSolverStartPending = false; + disconnect(pTask, nullptr, this, nullptr); + pTask->deleteLater(); + + if(bCancelled) { + delete m_pProgressDlg; + m_pProgressDlg = nullptr; + } else { + on_calculationFinished(NM_Calculation_Result_Fail); + } + m_pSolverDataManager = nullptr; + m_pSolverFitting = nullptr; + m_bSolverCancelRequested = false; + return; + } + + if(bFinished) { + // 临时井对象指针已全部释放,只有完整值快照可以进入后台线程。 + m_bSolverStartPending = false; + if(s_pPendingSolverWindow == this) { + s_pPendingSolverWindow = nullptr; + } + m_pSolverTask->start(); + return; + } + + QTimer::singleShot(0, this, SLOT(continueSolverInputCapture())); +} + +void nmSubWndMain::onSolverStageChanged( + QString sStage, + int nCurrent, + int nTotal) +{ + if(sender() != m_pSolverTask || m_pProgressDlg == nullptr || + m_bSolverCancelRequested) { + return; + } + + m_pProgressDlg->setLabelText(sStage); + if(nTotal <= 0) { + m_pProgressDlg->setRange(0, 0); + } else { + m_pProgressDlg->setRange(0, nTotal); + m_pProgressDlg->setValue(qBound(0, nCurrent, nTotal)); + } +} + +void nmSubWndMain::onSolverCancelRequested() +{ + if(m_bSolverStartPending && m_pSolverTask == nullptr) { + // 尚未创建任务时可立即撤销,DataManager 也尚未登记后台使用权。 + m_bSolverStartPending = false; + if(s_pPendingSolverWindow == this) { + s_pPendingSolverWindow = nullptr; + } + // canceled() 由进度框自身发出,延迟释放避免在信号调用栈内销毁发送者。 + if(m_pProgressDlg != nullptr) { + m_pProgressDlg->deleteLater(); + } + m_pProgressDlg = nullptr; + m_pSolverDataManager = nullptr; + m_pSolverFitting = nullptr; + m_bSolverCancelRequested = false; + return; + } + + if(m_pSolverTask != nullptr) { + m_bSolverCancelRequested = true; + m_pSolverTask->requestCancel(); + if(m_pProgressDlg != nullptr) { + // DLL 没有取消导出;若已进入 DLL,只能等待其返回后丢弃结果。 + m_pProgressDlg->setLabelText( + tr("Stopping; waiting for the current solver call to return...")); + m_pProgressDlg->setRange(0, 0); + QPushButton* pCancelButton = + m_pProgressDlg->findChild(); + if(pCancelButton != nullptr) { + pCancelButton->setEnabled(false); + } + m_pProgressDlg->show(); + m_pProgressDlg->raise(); + } + } } void nmSubWndMain::triggerToolBarAction(int index) @@ -1799,104 +1936,68 @@ void nmSubWndMain::mergeAnaResultToFitting() QString errorMessage4; pSubWndFit->adjustFitSubPlotBy(tag, vecHistory, true, &errorMessage4); - // 更新结果基础网格,与当前实时生成的网格保持一致 - pInstance->setResultBaseGrid(pInstance->getUnstructuredGridCopy()); - QWidget* widget1 = pSubWndFit->getFitSubRstWxOf(FSRT_Nm3D, true, &errorMessage); - - // 创建 vtkWidget - nmWxPostprocessingAnimationWidget* vtkWidget = nullptr; - - if(pInstance->getGridType() == NM_Grid_PEBI) { - vtkWidget = new nmWxPostprocessingAnimationWidget(NULL, pInstance); - } - - if(widget1 == nullptr || vtkWidget == nullptr) { - delete vtkWidget; + if(widget1 == nullptr || pInstance->getGridType() != NM_Grid_PEBI) { return; } - // 先清理 - // 获取当前布局 - QLayout* layout1 = widget1->layout(); - - if(layout1) { - // 移除布局中的所有控件 - QLayoutItem* item; - - while((item = layout1->takeAt(0))) { - if(item->widget()) { - // 删除控件 - delete item->widget(); + nmWxPostprocessingAnimationWidget* vtkWidget = + widget1->findChild(); + if(vtkWidget != nullptr) { + // 复用既有渲染窗口和 VTK 管道,只切换本轮已整体提交的数据。 + vtkWidget->refreshResult(); + } else { + vtkWidget = new nmWxPostprocessingAnimationWidget(NULL, pInstance); + QLayout* layout1 = widget1->layout(); + if(layout1) { + QLayoutItem* item; + while((item = layout1->takeAt(0))) { + if(item->widget()) { + delete item->widget(); + } + delete item; } - - delete item; // 删除布局项 + delete layout1; } - // 删除布局 - delete layout1; + widget1->setLayout(nullptr); + QVBoxLayout* newLayout1 = new QVBoxLayout(widget1); + newLayout1->addWidget(vtkWidget); + widget1->setLayout(newLayout1); } - // 确保布局被移除 - widget1->setLayout(nullptr); - - // 创建一个垂直布局管理器 - QVBoxLayout* newLayout1 = new QVBoxLayout(widget1); - - // 将现有的控件添加到布局中 - newLayout1->addWidget(vtkWidget); - - // 设置 widget 的布局 - widget1->setLayout(newLayout1); - // 结果参数界面 QWidget* widget2 = pSubWndFit->getFitSubRstWxOf(FSRT_NmRst, true, &errorMessage); if(widget2 == nullptr) { return; } - // 与左侧共用属性面板类,结果区使用独立实例和只读参数列表。 - nmWxParaPropertyPebi* resultWidget = new nmWxParaPropertyPebi; - resultWidget->initUI(); - resultWidget->setDataManager(pInstance); - resultWidget->rebuildResultParas(); - - // 先清理 - // 获取当前布局 - QLayout* layout2 = widget2->layout(); - - if(layout2) { - // 移除布局中的所有控件 - QLayoutItem* item; - - while((item = layout2->takeAt(0))) { - if(item->widget()) { - // 删除控件 - delete item->widget(); + nmWxParaPropertyPebi* resultWidget = + widget2->findChild(); + if(resultWidget == nullptr) { + // 与左侧共用属性面板类,结果区使用独立实例和只读参数列表。 + resultWidget = new nmWxParaPropertyPebi; + resultWidget->initUI(); + + QLayout* layout2 = widget2->layout(); + if(layout2) { + QLayoutItem* item; + while((item = layout2->takeAt(0))) { + if(item->widget()) { + delete item->widget(); + } + delete item; } - - delete item; // 删除布局项 + delete layout2; } - - // 删除布局 - delete layout2; + widget2->setLayout(nullptr); + QVBoxLayout* newLayout2 = new QVBoxLayout(widget2); + newLayout2->addWidget(resultWidget); + widget2->setLayout(newLayout2); } - // 确保布局被移除 - widget2->setLayout(nullptr); - - // 创建一个垂直布局管理器 - QVBoxLayout* newLayout2 = new QVBoxLayout(widget2); - - // 将现有的控件添加到布局中 - newLayout2->addWidget(resultWidget); - - // 设置 widget 的布局 - widget2->setLayout(newLayout2); - - //nmWxResultParameters* resultWidget = new nmWxResultParameters; - //resultWidget->show(); - + resultWidget->setDataManager(pInstance); + resultWidget->rebuildResultParas(); } @@ -2055,7 +2156,8 @@ bool nmSubWndMain::onConfirmClosing() { // 正常关闭入口在求解结束前保持当前成果和 DataManager 存活;应用退出等绕过 // 该确认流程的路径仍由析构函数 wait() 兜底。 - if(m_pSolverTask != nullptr && m_pSolverTask->isRunning()) { + if(m_bSolverStartPending || + (m_pSolverTask != nullptr && m_pSolverTask->isRunning())) { QMessageBox::information(this, tr("solver error"), tr("task is running!")); return false; } @@ -2113,17 +2215,12 @@ void nmSubWndMain::geoLayering() void nmSubWndMain::on_calculationFinished(NM_Calculation_Result result) { - // 1. 立即停止模拟定时器(防止它在处理结果时继续触发) - if (m_pFakeProgressTimer) { - m_pFakeProgressTimer->stop(); - } - - // 2. 强制拉满进度条(给用户一个成功的反馈) + // 结果提交成功后仍保留进度框,直到曲线和结果页刷新全部完成。 if (m_pProgressDlg) { - m_pProgressDlg->setValue(100); + m_pProgressDlg->setLabelText(tr("Updating result views...")); + m_pProgressDlg->setRange(0, 0); } - // 3. 处理计算结果 if(result == NM_Calculation_Result_Success) { this->mergeAnaResultToFitting(); } else { @@ -2131,7 +2228,6 @@ void nmSubWndMain::on_calculationFinished(NM_Calculation_Result result) QMessageBox::warning(this, tr("solver error"), tr("solver failed!")); } - // 4. 直接销毁对象并置空 if (m_pProgressDlg != nullptr) { delete m_pProgressDlg; m_pProgressDlg = nullptr; @@ -2149,10 +2245,17 @@ void nmSubWndMain::on_solverTaskFinished(bool isSuccessed) int nPebiCount = pTask != nullptr ? pTask->getPebiCount() : -1; int nSolveTimeMs = pTask != nullptr ? pTask->getSolveTimeMs() : -1; + const bool bCancelled = pTask->wasCancelled(); // 后台线程只生成局部结果快照。回到主线程后先验证所属 DataManager 和 // 输入版本,再整体替换旧成果;提交失败与求解失败使用同一条错误路径。 - if(isSuccessed) { + if(isSuccessed && !bCancelled) { + if(m_pProgressDlg != nullptr) { + m_pProgressDlg->setLabelText(tr("Committing results...")); + m_pProgressDlg->setRange(0, 0); + } isSuccessed = pTask->commitResult(m_pSolverDataManager.data()); + } else if(bCancelled) { + isSuccessed = false; } m_pSolverTask = nullptr; if(pTask == s_pRunningSolverTask) { @@ -2161,13 +2264,21 @@ void nmSubWndMain::on_solverTaskFinished(bool isSuccessed) // 正常完成路径由主线程延迟释放任务;析构路径则会等待并直接释放。 pTask->deleteLater(); - // 第二步:在清空捕获上下文前完成结果合并,确保始终回写原拟合窗口。 - NM_Calculation_Result result = isSuccessed ? NM_Calculation_Result_Success : NM_Calculation_Result_Fail; - this->on_calculationFinished(result); + // 第二步:停止不属于失败,也不允许触发结果提交或覆盖旧成果。 + if(bCancelled) { + delete m_pProgressDlg; + m_pProgressDlg = nullptr; + } else { + NM_Calculation_Result result = isSuccessed + ? NM_Calculation_Result_Success + : NM_Calculation_Result_Fail; + this->on_calculationFinished(result); + } // 第三步:结果处理结束后释放非拥有型上下文引用。 m_pSolverDataManager = nullptr; m_pSolverFitting = nullptr; + m_bSolverCancelRequested = false; if(isSuccessed && nPebiCount >= 0 && nSolveTimeMs >= 0) { QMessageBox::information(this, tr("Solver completed"), @@ -2316,32 +2427,6 @@ nmGuiPlot * nmSubWndMain::getWxPlot() const return m_pWxPlot; } -void nmSubWndMain::onProgressUpdated(int progress) -{ - if (!m_pProgressDlg) { - return; - } - - // 1. 处理特殊结束信号 - if (progress >= 100 || progress == -1) { - // 停止模拟定时器,防止它继续累加 - if (m_pFakeProgressTimer && m_pFakeProgressTimer->isActive()) { - m_pFakeProgressTimer->stop(); - } - m_pProgressDlg->setValue(100); - return; - } - - // 2. 进度倒退保护 - // 如果模拟进度已经跑到了 50%,但某个真实信号传回来 40%, - // 为了防止进度条“倒着走”引起用户困惑,只有当前值更大时才更新。 - if (progress > m_pProgressDlg->value()) { - m_pProgressDlg->setValue(progress); - // 同步更新模拟变量,防止定时器下次触发时使用旧值 - m_nVirtualProgress = progress; - } -} - void nmSubWndMain::onWellSelected(const QString& sWellCode) { nmDataAnalyzeManager* pInstance = nmDataAnalyzeManager::getCurrentInstance(); @@ -2528,65 +2613,6 @@ void nmSubWndMain::onGenerateButtonClicked() } } -void nmSubWndMain::slotUpdateFakeProgress() -{ - // --- 1. 安全防御检查 --- - // 如果进度对话框已被销毁(例如计算提前结束或用户关闭),立即停止定时器并退出 - // 防止在 delete 对象后发生野指针访问崩溃 - if (m_pProgressDlg == nullptr) { - if (m_pFakeProgressTimer) { - m_pFakeProgressTimer->stop(); - } - return; - } - - // --- 2. 分段模拟进度逻辑 (0% - 99%) --- - // 采用“前快后慢”的心理学策略,50% 之后逐级增加阻力,为 DLL 计算留出缓冲时间 - if (m_nVirtualProgress < 99) { - - // 步进计数器,用于实现非匀速增长 - m_nSlowDownCounter++; - - if (m_nVirtualProgress < 50) { - // [第一阶段] 0% - 50%: 快速增长 - // 每次定时器触发(500ms)即增加 1%,给予用户“响应迅速”的反馈 - m_nVirtualProgress += 1; - } - else if (m_nVirtualProgress < 75) { - // [第二阶段] 50% - 75%: 第一次减速 - // 每触发 4 次(约 2秒)增加 1%,模拟进入核心计算阶段 - if (m_nSlowDownCounter % 4 == 0) { - m_nVirtualProgress += 1; - } - } - else if (m_nVirtualProgress < 90) { - // [第三阶段] 75% - 90%: 第二次减速 - // 每触发 8 次(约 4秒)增加 1%,应对较大规模的数据处理 - if (m_nSlowDownCounter % 8 == 0) { - m_nVirtualProgress += 1; - } - } - else { - // [第四阶段] 90% - 99%: 深度减速 (极慢) - // 每触发 15 次(约 7.5秒)增加 1%。此时进度条几乎“爬行”, - // 旨在确保在 DLL 返回完成信号前,进度条不会提前撞击 100% - if (m_nSlowDownCounter % 15 == 0) { - m_nVirtualProgress += 1; - } - } - - // 统一调用更新接口,同步 UI 显示 - this->onProgressUpdated(m_nVirtualProgress); - } - else { - // --- 3. 终点停靠 --- - // 到达 99% 时停止自动增长,死等来自 Solver 线程的真正的 Finished 信号 - if (m_pFakeProgressTimer) { - m_pFakeProgressTimer->stop(); - } - } -} - void nmSubWndMain::generateDFN() { nmWxDFN dfnDialog(nullptr); diff --git a/Src/nmNum/nmSubWxs/nmWxPostprocessingAnimationWidget.cpp b/Src/nmNum/nmSubWxs/nmWxPostprocessingAnimationWidget.cpp index 14466cd..6653d65 100644 --- a/Src/nmNum/nmSubWxs/nmWxPostprocessingAnimationWidget.cpp +++ b/Src/nmNum/nmSubWxs/nmWxPostprocessingAnimationWidget.cpp @@ -238,6 +238,66 @@ nmWxPostprocessingAnimationWidget::~nmWxPostprocessingAnimationWidget() } } +void nmWxPostprocessingAnimationWidget::refreshResult() +{ + nmDataAnalyzeManager* pDataManager = m_pDataManager.data(); + if(pDataManager == nullptr || m_pVtkWidget == nullptr || + m_thresholdFilter == nullptr || m_mapper == nullptr || + m_lookupTable == nullptr) { + return; + } + + // 结果提交发生在主线程;先停止旧动画和轮廓,再整体切换基础网格及时间步。 + on_stop(); + slotClearContour(); + clearWellActors(); + m_contourRepresentation = nullptr; + m_pointPlacer = nullptr; + m_polyDataForContour = nullptr; + + m_pCachedBaseGrid = pDataManager->getResultBaseGrid(); + m_thresholdFilter->SetInputData(m_pCachedBaseGrid); + m_nCurrentIndex = 0; + m_vecTimeStepKeys.clear(); + initTimeSteps(); + + if(m_pSlider != nullptr) { + m_pSlider->blockSignals(true); + m_pSlider->setMinimum(1); + m_pSlider->setMaximum(qMax(1, m_vecTimeStepKeys.size())); + m_pSlider->setValue(1); + m_pSlider->blockSignals(false); + } + if(m_pProgress != nullptr) { + const int nTotal = m_vecTimeStepKeys.size(); + m_pProgress->setText(nTotal > 0 + ? QString("%1%(%2/%3)").arg(100 / nTotal).arg(1).arg(nTotal) + : QString("0%(0/0)")); + } + + double scalarRange[2]; + pDataManager->getScalarRangeP(scalarRange); + m_mapper->SetScalarRange(scalarRange); + m_lookupTable->SetRange(scalarRange); + m_lookupTable->Build(); + m_thresholdFilter->ThresholdBetween(scalarRange[0], scalarRange[1]); + m_thresholdFilter->Modified(); + m_bFilteringEnabled = true; + m_bAboveMinEnabled = true; + m_dMinValue = scalarRange[0]; + m_bBelowMaxEnabled = true; + m_dMaxValue = scalarRange[1]; + + if(!m_vecTimeStepKeys.isEmpty() && m_pCachedBaseGrid != nullptr) { + loadDataForIndex(0); + } + initWellDrawing(); + if(m_renderer != nullptr) { + m_renderer->ResetCamera(); + } + m_pVtkWidget->GetRenderWindow()->Render(); +} + QImage nmWxPostprocessingAnimationWidget::createQImage1(int nWidth, int nHeight, vtkUnsignedCharArray* pScalars) { QImage qImage(nWidth, nHeight, QImage::Format_ARGB32);