fix(numerical): 修复多井成果恢复与异步求解安全问题

- 修复包含井、自动观察井及求解井顺序的保存加载问题
- 增加后台任务生命周期保护,避免关闭成果时访问已释放数据
- 将求解结果改为局部生成并一次性提交,防止失败或过期结果污染成果
- 修复 T5 拟压力成果串用及 Map 新井状态未及时刷新的问题
- 合入自动拟合的单位、目标首点、时间轴端点和空流量修复
feature/refactor-numerical-model-panel-20260824
lh 2 weeks ago
parent 2020638d28
commit 9bf9e41faf

@ -4,14 +4,19 @@
#include <QThread>
#include <QString>
#include <QVector>
#include <QMap>
#include <QPointF>
#include <iostream>
#include <vector>
#include <Windows.h>
#include <vtkSmartPointer.h>
#include "nmCalculationDefine.h"
#include "nmCalculation_global.h"
#include "pch.h"
class nmDataAnalyzeManager;
class vtkDoubleArray;
class vtkUnstructuredGrid;
// 主窗口现在直接创建DLL求解线程任务因此类需要导出供nmSubWnd模块跨DLL使用。
class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread {
@ -38,6 +43,13 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread {
/** @brief 获取本次求解的PEBI网格数量。 */
int getPebiCount() const;
/**
* @brief 线
* @param pDataManager
* @return true false
*/
bool commitResult(nmDataAnalyzeManager* pDataManager);
/**
* @brief WellCode
* @note
@ -53,13 +65,15 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread {
private:
/** @brief 执行当前唯一的 PEBI 求解模式。 */
bool execute();
/** @brief 成对释放构造时登记的 DataManager 后台使用权。 */
void releaseDataManagerUse();
/** @brief 构造 DLL 输入、执行求解并提交仍然有效的结果。 */
/** @brief 构造 DLL 输入、执行求解并生成仍然有效的局部结果快照。 */
bool execPebiMode();
/** @brief 按求解器井顺序将压力、双对数和半对数结果映射回井对象。 */
bool savePebiModeResult(HX_NWTM_MODEL_OUTPUT& p1,
int nModelType,
const HX_NWTM_GRID_OUTPUT1& oGridOutput);
/** @brief 按求解器井顺序组装压力、曲线和场结果快照,不修改 DataManager。 */
bool buildPebiModeResult(HX_NWTM_MODEL_OUTPUT& p1,
int nModelType,
const HX_NWTM_GRID_OUTPUT1& oGridOutput);
/** @brief 输出精简版 DLL 输入日志,用于定位求解异常。 */
void logHX_NWTM_MODEL_INPUT_Simplified(const HX_NWTM_MODEL_INPUT& p0);
@ -67,9 +81,20 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread {
bool saveHX_NWTM_MODEL_INPUT_ToTxt(const HX_NWTM_MODEL_INPUT& p0, const QString& filePath);
private:
/** @brief 一口真实井在本次求解中完整生成的待提交结果。 */
struct nmPebiWellResultSnapshot
{
QString m_sWellCode; ///< 稳定井编码。
QVector<QVector<double> > m_vecPressure; ///< 时间和井底压力。
QVector<QVector<double> > m_vecLogLog; ///< 时间、压差及导数。
QVector<QVector<double> > m_vecSemiLog; ///< 时间和半对数压力。
QPointF m_oLocation; ///< 结果网格中的井坐标。
};
QString m_sPostprocessingDir; ///< 本次求解使用的后处理输出目录。
/** @brief 创建任务时捕获所属数据中心,工作线程不得读取全局当前窗口。 */
nmDataAnalyzeManager* m_pDataManager;
bool m_bManagerUseActive; ///< 是否仍持有 DataManager 后台使用权。
/** @brief 创建任务时捕获网格输入版本,防止回填过期网格上的结果。 */
quint64 m_nGridInputRevision;
/** @brief 创建任务时捕获求解输入版本,防止角色或参数变化后回填旧结果。 */
@ -83,6 +108,12 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread {
QVector<QVector<double> > m_autoFitResultPressure; ///< 目标井原始压力结果。
QVector<QVector<double> > m_autoFitResultLogLog; ///< 目标井双对数结果。
QVector<QVector<double> > m_autoFitResultSemiLog; ///< 目标井半对数结果。
QVector<nmPebiWellResultSnapshot> m_vecPendingWellResults; ///< 完整求解的井结果快照。
QMap<double, vtkSmartPointer<vtkDoubleArray> > m_mapPendingTimeSteps; ///< 场压力时间步快照。
vtkSmartPointer<vtkUnstructuredGrid> m_pPendingResultGrid; ///< 本次结果绑定的基础网格副本。
double m_dPendingScalarMin; ///< 全部场压力的最小值。
double m_dPendingScalarMax; ///< 全部场压力的最大值。
bool m_bPendingFullResultReady; ///< 是否已有可由主线程一次提交的完整结果。
private slots:
//void slotTaskUpdateProgress();

@ -15,6 +15,8 @@ public:
/** @brief 创建绑定到指定分析窗口的后台网格生成任务。 */
explicit nmCalculationPebiGridTask(nmDataAnalyzeManager* pDataManager,
QObject *parent = 0);
/** @brief 等待线程结束,并释放 DataManager 后台使用权。 */
~nmCalculationPebiGridTask();
signals:
/** @brief 无论成功或失败均通知主线程结束本轮网格任务。 */
@ -25,8 +27,12 @@ protected:
void run() override;
private:
/** @brief 成对释放构造时登记的 DataManager 后台使用权。 */
void releaseDataManagerUse();
/** @brief 启动任务时捕获所属数据管理器,线程内不再读取全局当前窗口。 */
nmDataAnalyzeManager* m_pDataManager;
bool m_bManagerUseActive; ///< 是否仍持有 DataManager 后台使用权。
};
#endif // NMCALCULATIONPEBIGRIDTASK_H

@ -20,6 +20,8 @@
#include <QPair>
#include <QString>
#include <QList>
#include <QMutex>
#include <QWaitCondition>
#include <vector>
#include <vtkUnstructuredGrid.h>
@ -447,6 +449,11 @@ public:
nmDataNumericalAnalysisCase* getNumericalAnalysisCase();
/** @brief 返回只读数值分析方案,管理器保留对象所有权。 */
const nmDataNumericalAnalysisCase* getNumericalAnalysisCase() const;
/** @brief 后台任务开始使用本管理器;与 endBackgroundUse() 必须成对调用。 */
void beginBackgroundUse();
/** @brief 后台任务结束使用本管理器;最后一个任务退出时唤醒析构等待。 */
void endBackgroundUse();
/** @brief 设置主分析井 WellCode空编码用于清空主井。 */
void setPrimaryWellCode(const QString& sWellCode);
/** @brief 返回当前主分析井 WellCode。 */
@ -730,6 +737,14 @@ public:
void sigGeometryListChanged();
private:
/** @brief 创建本数据管理器的成果窗口;只用于读取本成果上下文,不拥有对象。 */
iSubWndFitting* m_pOwnerFitting;
/** @brief 保护后台使用计数和析构等待条件。 */
QMutex m_oBackgroundUseMutex;
/** @brief DataManager 析构时等待全部后台任务停止访问。 */
QWaitCondition m_oNoBackgroundUseCondition;
int m_nBackgroundUseCount; ///< 当前持有 DataManager 使用权的后台任务数量。
// 存储井的数据
QVector<nmDataWellBase*> m_vWellData;

@ -62,10 +62,10 @@ static const unsigned int kFixedPsoSeed = 1792008679u; // 当前固定
// ===== 代理模型训练域约束 =====
//
// 这些范围必须与 ML/nmWTAI-ML/configs/data_gen_family_random_v2_q.yaml 中的
// 数据生成范围保持一致。超出范围时C++ 侧会强制退回真实求解器,避免代理模型
// 在训练域外给出不可靠排序
static const double kSurrogateKMin = 1.0e-3; // 代理模型训练域:渗透率下界
static const double kSurrogateKMax = 10.0; // 代理模型训练域:渗透率上界
// 数据生成范围保持一致。PSO 中渗透率使用 mD因此将训练配置中的 Darcy 范围
// 乘以 1000 后再门控;超出范围时强制退回真实求解器
static const double kSurrogateKMin = 1.0; // 代理模型训练域:渗透率下界,单位 mD
static const double kSurrogateKMax = 10000.0; // 代理模型训练域:渗透率上界,单位 mD
static const double kSurrogateSkinMin = -10.0; // 代理模型训练域skin 下界。
static const double kSurrogateSkinMax = 10.0; // 代理模型训练域skin 上界。
static const double kSurrogateWellboreCMin = 1.0e-4; // 代理模型训练域:井筒储集下界。
@ -203,8 +203,8 @@ static bool getSurrogateParameterDomain(NM_SOLVER_MODEL_TYPE solverModelType, Su
return true;
case SMT_Gas_VariablePvt:
domain->kMin = 4.5e-6;
domain->kMax = 6.2e-6;
domain->kMin = 4.5e-3;
domain->kMax = 6.2e-3;
domain->skinMin = 0.0;
domain->skinMax = 4.5;
domain->wellboreCMin = 5.0e-5;
@ -6577,15 +6577,17 @@ double nmCalculationAutoFitPSO::calculateLogLogCurveError(
// 无法比较的采样行先跳过;有限但非正的导数无法进入双对数空间,
// 当前数据又没有逐点有效掩码,因此遇到这种导数时判本次评价无效。
auto prepareCurve = [valueFloor](const QVector<QVector<double> >& data,
int firstIndex,
QVector<QPointF>* pressure,
QVector<QPointF>* derivative) -> bool {
if(!pressure || !derivative || data.size() < 3 ||
data[0].size() != data[1].size() ||
data[0].size() != data[2].size()) {
data[0].size() != data[2].size() ||
firstIndex < 0 || firstIndex >= data[0].size()) {
return false;
}
for(int i = 0; i < data[0].size(); ++i) {
for(int i = firstIndex; i < data[0].size(); ++i) {
if(!isFiniteNumber(data[0][i]) ||
!isFiniteNumber(data[1][i]) ||
!isFiniteNumber(data[2][i]) ||
@ -6633,8 +6635,9 @@ double nmCalculationAutoFitPSO::calculateLogLogCurveError(
QVector<QPointF> targetDerivative;
QVector<QPointF> resultPressure;
QVector<QPointF> resultDerivative;
if(!prepareCurve(target, &targetPressure, &targetDerivative) ||
!prepareCurve(result, &resultPressure, &resultDerivative)) {
// 模拟结果生成时已经跳过 DLL 首点,因此目标曲线也从第二点开始比较。
if(!prepareCurve(target, 1, &targetPressure, &targetDerivative) ||
!prepareCurve(result, 0, &resultPressure, &resultDerivative)) {
return invalidLoss;
}
@ -6730,7 +6733,14 @@ double nmCalculationAutoFitPSO::calculateLogLogCurveError(
(targetLogMaxX - targetLogMinX) /
(numPoints - 1);
commonLogX[i] = logX;
commonX[i] = qExp(logX);
// 首尾直接使用原始端点,避免 exp(log(t)) 的舍入误差越过严格插值边界。
if(i == 0) {
commonX[i] = targetMinX;
} else if(i == numPoints - 1) {
commonX[i] = targetMaxX;
} else {
commonX[i] = qExp(logX);
}
if(!interpolateLogValue(
targetPressure, commonX[i],
@ -7026,10 +7036,13 @@ double nmCalculationAutoFitPSO::calculateLogLogCurveError(
return false;
}
double shiftedX = qExp(
qBound(resultLogMinX,
shiftedLogX,
resultLogMaxX));
// 从对数时间还原后再限制到原始端点,避免舍入误差越过插值边界。
double shiftedX = qBound(
resultMinX,
qExp(qBound(resultLogMinX,
shiftedLogX,
resultLogMaxX)),
resultMaxX);
double resultLogPressure = 0.0;
double resultLogDerivative = 0.0;
if(!interpolateLogValue(

@ -21,6 +21,7 @@
#include "nmDataPvtParaForPebi.h"
#include <QDebug>
#include <QSet>
#include <iostream>
#include <fstream>
#include <sstream>
@ -317,13 +318,23 @@ nmCalculationDllPebiSolverTask::nmCalculationDllPebiSolverTask(
m_pDataManager(pDataManager != nullptr
? pDataManager
: nmDataAnalyzeManager::getCurrentInstance()),
m_bManagerUseActive(false),
m_nGridInputRevision(0),
m_nResultInputRevision(0),
m_lastRunSucceeded(false),
m_nSolveTimeMs(-1),
m_nPebiCount(-1)
m_nPebiCount(-1),
m_dPendingScalarMin(0.0),
m_dPendingScalarMax(0.0),
m_bPendingFullResultReady(false)
{
// 任务创建时冻结两个输入版本;非模态求解期间界面仍可编辑数据。
// 第一步:构造后到 run() 结束前阻止所属成果提前释放 DataManager。
if(m_pDataManager != nullptr) {
m_pDataManager->beginBackgroundUse();
m_bManagerUseActive = true;
}
// 第二步:任务创建时冻结两个输入版本,提交前还会再次核对。
if(m_pDataManager != nullptr &&
m_pDataManager->getNumericalAnalysisCase() != nullptr) {
m_nGridInputRevision =
@ -335,10 +346,12 @@ nmCalculationDllPebiSolverTask::nmCalculationDllPebiSolverTask(
nmCalculationDllPebiSolverTask::~nmCalculationDllPebiSolverTask()
{
// Qt 4.8 没有可靠的外部 DLL 协作取消接口。强制 terminate() 可能让 DLL、
// VTK 或 STL 对象停在持锁/半析构状态,因此析构只能等待 run() 正常退出。
if(isRunning()) {
terminate();
wait(3000);
wait();
}
releaseDataManagerUse();
}
void nmCalculationDllPebiSolverTask::run()
@ -346,9 +359,19 @@ void nmCalculationDllPebiSolverTask::run()
m_nSolveTimeMs = -1;
m_nPebiCount = -1;
m_lastRunSucceeded = this->execute();
// 完成信号可能触发成果关闭;必须在发信号前结束 DataManager 使用期。
releaseDataManagerUse();
emit sig_calculateDone(m_lastRunSucceeded);
}
void nmCalculationDllPebiSolverTask::releaseDataManagerUse()
{
if(m_bManagerUseActive && m_pDataManager != nullptr) {
m_pDataManager->endBackgroundUse();
m_bManagerUseActive = false;
}
}
bool nmCalculationDllPebiSolverTask::wasSuccessful() const
{
return m_lastRunSucceeded;
@ -409,10 +432,12 @@ bool nmCalculationDllPebiSolverTask::execPebiMode()
pAnalysisCase->getResultInputRevision() != m_nResultInputRevision) {
return false;
}
if(m_sAutoFitTargetWellCode.isEmpty()) {
// 完整重算开始后立即废弃上一轮有效标记;失败任务不能继续保存旧结果。
pDataInstance->discardPebiResults();
}
// 旧结果在新结果完整生成并通过主线程版本校验前保持不变。失败任务只丢弃
// 自己的局部快照,不能提前清空上一轮仍然完整的成果。
m_bPendingFullResultReady = false;
m_vecPendingWellResults.clear();
m_mapPendingTimeSteps.clear();
m_pPendingResultGrid = nullptr;
// 载入Pebi动态库
HMODULE dll = LoadLibrary(L"HX_NWTM.dll");
@ -849,10 +874,10 @@ bool nmCalculationDllPebiSolverTask::execPebiMode()
return false;
}
// 保存计算结果
bool success = this->savePebiModeResult(p1,
modelType,
oGridOutput1);
// 只构造任务局部结果;完整求解由主线程在完成回调中一次性提交。
bool success = this->buildPebiModeResult(p1,
modelType,
oGridOutput1);
FreeLibrary(dll); // 卸载 DLL
return success;
@ -892,7 +917,7 @@ std::vector<double> HX_logderivative(const std::vector<double>& x, const std::ve
return d;
}
bool nmCalculationDllPebiSolverTask::savePebiModeResult(
bool nmCalculationDllPebiSolverTask::buildPebiModeResult(
HX_NWTM_MODEL_OUTPUT& p1,
int modelType,
const HX_NWTM_GRID_OUTPUT1& oGridOutput)
@ -964,11 +989,6 @@ bool nmCalculationDllPebiSolverTask::savePebiModeResult(
}
}
// 粒子评价不修改全局井位置,完整求解仍按原流程重建映射.
if(!autoFitTargetOnly) {
pDataInstance->clearWellLocations();
}
// 遍历每口井,处理其数据
for(int wellIdx = 0; wellIdx < vecWellsOrder.size(); ++wellIdx) {
const nmSolverWellRef& oWellRef = vecWellsOrder[wellIdx];
@ -1039,7 +1059,8 @@ bool nmCalculationDllPebiSolverTask::savePebiModeResult(
// 准备流量段数据
QVector<QPointF> vecTimeQ = pWellData->getFlowPoints();
int nTimeNumQ = vecTimeQ.size() - 1; // 移除第一个 0 点
// 防御空流量数据,避免 -1 转成 std::vector 的巨大无符号长度。
int nTimeNumQ = qMax(0, vecTimeQ.size() - 1); // 移除第一个 0 点
std::vector<double> timeQ(nTimeNumQ);
std::vector<double> q(nTimeNumQ);
@ -1052,9 +1073,22 @@ bool nmCalculationDllPebiSolverTask::savePebiModeResult(
std::vector<Point> logPreResultFromDll; // 存储 DLL 的计算结果
int iSectionFlowIndex = pWellData->getIndexF();
HMODULE hMod_solver = LoadLibrary(L"singlePhaseSolverDll.dll");
if(hMod_solver) {
// 第一步:无产量观察井只接收压力结果,不调用依赖产量制度的曲线 DLL。
HMODULE hMod_solver = nTimeNumQ > 0
? LoadLibrary(L"singlePhaseSolverDll.dll") : nullptr;
if(nTimeNumQ <= 0) {
// 第二步:保持固定的数据列结构,便于结果保存和后续读取;
// 各列为空明确表示该观察井没有可展示的双对数、半对数结果。
vvecLogLog.clear();
vvecLogLog.append(QVector<double>());
vvecLogLog.append(QVector<double>());
vvecLogLog.append(QVector<double>());
vvecSemiLog.clear();
vvecSemiLog.append(QVector<double>());
vvecSemiLog.append(QVector<double>());
} else if(hMod_solver) {
typedef bool (*PreLog)(const std::vector<Point>&, const int&, double*, double*, int, std::vector<Point>&);
PreLog preLogFun = (PreLog)GetProcAddress(hMod_solver, "logLogPre");
@ -1135,14 +1169,17 @@ bool nmCalculationDllPebiSolverTask::savePebiModeResult(
autoFitTargetFound = true;
break;
} else {
// 所有真实井都保存压力;观察井的双对数和半对数保持为空。
pWellData->setResultPressure(vvecPressure);
pWellData->setResultLogLog(vvecLogLog);
pWellData->setResultSemiLog(vvecSemiLog);
QPointF ptWellCoords(pWellData->getX().getValue().toDouble(), pWellData->getY().getValue().toDouble());
pDataInstance->addWellLocation(oWellRef.m_sWellCode,
ptWellCoords);
// 所有真实井先写入任务局部快照;观察井的双对数和半对数保持为空。
// 任意后续步骤失败时,整个快照会被丢弃,旧成果不会被局部覆盖。
nmPebiWellResultSnapshot oWellResult;
oWellResult.m_sWellCode = oWellRef.m_sWellCode;
oWellResult.m_vecPressure = vvecPressure;
oWellResult.m_vecLogLog = vvecLogLog;
oWellResult.m_vecSemiLog = vvecSemiLog;
oWellResult.m_oLocation = QPointF(
pWellData->getX().getValue().toDouble(),
pWellData->getY().getValue().toDouble());
m_vecPendingWellResults.append(oWellResult);
}
}
@ -1195,14 +1232,13 @@ bool nmCalculationDllPebiSolverTask::savePebiModeResult(
}
}
// 新结果提交前清空上一轮时间键,防止较短的新时间轴残留旧帧。
pDataInstance->clearTimeSteps();
// 在保存时间步数据前,先计算标量范围
// 第五步:在局部时间步映射中构造场压力,同时计算全局标量范围。
// 此处不触碰 DataManager较短的新时间轴会在主线程整体替换时自然清除旧帧。
m_mapPendingTimeSteps.clear();
double dMinP = DBL_MAX;
double dMaxP = -DBL_MAX;
// 4. 为每个时间步保存压力场到 nmDataAnalyzeManager 的 QMap 中
// 为每个时间步生成完整的 VTK 压力数组。
for(size_t timeIdx = 0; timeIdx < p1.p.size(); ++timeIdx) {
// 获取当前时间步的时间值
double currentTime = p1.t[timeIdx];
@ -1235,29 +1271,113 @@ bool nmCalculationDllPebiSolverTask::savePebiModeResult(
dMinP = qMin(dMinP, dCurrentTimeMin);
dMaxP = qMax(dMaxP, dCurrentTimeMax);
// 将时间与对应的压力数据存储到 nmDataAnalyzeManager
pDataInstance->addTimeStep(currentTime, pressureData);
m_mapPendingTimeSteps.insert(currentTime, pressureData);
}
// 保存全局范围到管理器
pDataInstance->setScalarRangeP(dMinP, dMaxP);
if(pDataInstance->getTimeStepKeys().size() !=
static_cast<int>(p1.t.size())) {
// QMap 使用时间作为唯一键;重复时间会覆盖旧帧,因此必须检查数量一致。
if(m_mapPendingTimeSteps.size() != static_cast<int>(p1.t.size())) {
return false;
}
vtkSmartPointer<vtkUnstructuredGrid> pResultGrid =
// 第六步:复制与本次场数据对应的基础网格,并在最后才发布“快照就绪”。
m_pPendingResultGrid =
pDataInstance->getUnstructuredGridCopy();
if(pResultGrid == nullptr || pResultGrid->GetNumberOfCells() <= 0) {
if(m_pPendingResultGrid == nullptr ||
m_pPendingResultGrid->GetNumberOfCells() <= 0) {
return false;
}
int nExpectedWellResultCount = 0;
for(int nIndex = 0; nIndex < vecWellsOrder.size(); ++nIndex) {
if(vecWellsOrder[nIndex].m_eEntryKind == NM_SolverEntry_Well) {
++nExpectedWellResultCount;
}
}
if(m_vecPendingWellResults.size() != nExpectedWellResultCount) {
return false;
}
m_dPendingScalarMin = dMinP;
m_dPendingScalarMax = dMaxP;
m_bPendingFullResultReady = true;
return true;
}
bool nmCalculationDllPebiSolverTask::commitResult(
nmDataAnalyzeManager* pDataManager)
{
// 第一步:自动拟合只返回目标曲线,不允许提交完整成果;手工求解必须回到
// DataManager 所属线程执行,保证界面看不到逐项替换过程中的中间状态。
if(!m_sAutoFitTargetWellCode.isEmpty() ||
!m_bPendingFullResultReady ||
pDataManager == nullptr ||
pDataManager != m_pDataManager ||
QThread::currentThread() != pDataManager->thread()) {
return false;
}
nmDataNumericalAnalysisCase* pAnalysisCase =
pDataManager->getNumericalAnalysisCase();
if(pAnalysisCase == nullptr ||
pAnalysisCase->getGridInputRevision() != m_nGridInputRevision ||
pAnalysisCase->getResultInputRevision() != m_nResultInputRevision ||
!pAnalysisCase->isGridValid() ||
m_mapPendingTimeSteps.isEmpty() ||
m_pPendingResultGrid == nullptr ||
m_pPendingResultGrid->GetNumberOfCells() <= 0) {
return false;
}
pDataInstance->setResultBaseGrid(pResultGrid);
if(!pDataInstance->getNumericalAnalysisCase()->markResultsAvailableIfCurrent(
// 第二步:在改动旧成果前一次性解析全部目标井,并检查 WellCode 不重复。
QVector<nmDataWellBase*> vecTargetWells;
QSet<QString> setWellCodes;
vecTargetWells.reserve(m_vecPendingWellResults.size());
for(int nIndex = 0; nIndex < m_vecPendingWellResults.size(); ++nIndex) {
const nmPebiWellResultSnapshot& oWellResult =
m_vecPendingWellResults[nIndex];
nmDataWellBase* pWellData =
pDataManager->findWellByCode(oWellResult.m_sWellCode);
if(pWellData == nullptr ||
setWellCodes.contains(oWellResult.m_sWellCode)) {
return false;
}
setWellCodes.insert(oWellResult.m_sWellCode);
vecTargetWells.append(pWellData);
}
// 第三步:最后一次登记版本。登记失败时旧成果仍未被修改;登记成功后当前
// 主线程事件不会被其他编辑操作插入,因此后续替换不存在可恢复失败分支。
if(!pAnalysisCase->markResultsAvailableIfCurrent(
m_nGridInputRevision, m_nResultInputRevision)) {
return false;
}
// 第四步:版本检查和对象解析全部通过后,在当前主线程事件内整体替换。
// 这些 setter 不发事件也不包含可恢复失败分支,外部只能在本函数返回后看到新成果。
pDataManager->clearWellLocations();
for(int nIndex = 0; nIndex < m_vecPendingWellResults.size(); ++nIndex) {
const nmPebiWellResultSnapshot& oWellResult =
m_vecPendingWellResults[nIndex];
nmDataWellBase* pWellData = vecTargetWells[nIndex];
pWellData->setResultPressure(oWellResult.m_vecPressure);
pWellData->setResultLogLog(oWellResult.m_vecLogLog);
pWellData->setResultSemiLog(oWellResult.m_vecSemiLog);
pDataManager->addWellLocation(oWellResult.m_sWellCode,
oWellResult.m_oLocation);
}
pDataManager->clearTimeSteps();
QMap<double, vtkSmartPointer<vtkDoubleArray> >::const_iterator oTimeIt =
m_mapPendingTimeSteps.constBegin();
for(; oTimeIt != m_mapPendingTimeSteps.constEnd(); ++oTimeIt) {
pDataManager->addTimeStep(oTimeIt.key(), oTimeIt.value());
}
pDataManager->setScalarRangeP(m_dPendingScalarMin,
m_dPendingScalarMax);
pDataManager->setResultBaseGrid(m_pPendingResultGrid);
// 第五步:清除待提交标志,防止重复完成信号再次覆盖后续结果。
m_bPendingFullResultReady = false;
return true;
}

@ -1,13 +1,27 @@
#include "nmCalculationPebiGridTask.h"
#include "nmCalculationPebiGrid.h"
#include "nmDataAnalyzeManager.h"
nmCalculationPebiGridTask::nmCalculationPebiGridTask(
nmDataAnalyzeManager* pDataManager,
QObject *parent)
: QThread(parent),
m_pDataManager(pDataManager)
m_pDataManager(pDataManager),
m_bManagerUseActive(false)
{
if(m_pDataManager != nullptr) {
m_pDataManager->beginBackgroundUse();
m_bManagerUseActive = true;
}
}
nmCalculationPebiGridTask::~nmCalculationPebiGridTask()
{
if(isRunning()) {
wait();
}
releaseDataManagerUse();
}
void nmCalculationPebiGridTask::run()
@ -16,6 +30,16 @@ void nmCalculationPebiGridTask::run()
// 生成网格.
bool success = nmCalculationPebiGrid::getInstance()->meshGenPebi(
m_pDataManager);
// 完成通知可能引起成果窗口销毁,发信号前先解除 DataManager 使用权。
releaseDataManagerUse();
// 无论成功或失败都通知窗口,使其可以结束本轮任务状态并处理后续待刷新请求。
emit pebiGridGenerated(success);
}
void nmCalculationPebiGridTask::releaseDataManagerUse()
{
if(m_bManagerUseActive && m_pDataManager != nullptr) {
m_pDataManager->endBackgroundUse();
m_bManagerUseActive = false;
}
}

@ -508,6 +508,8 @@ ZX_DEFINE_DYNAMIC(DataAnalyzeManager, nmDataAnalyzeManager)
nmDataAnalyzeManager::nmDataAnalyzeManager(): ZxDataObjectBin(0)
{
m_pOwnerFitting = nullptr;
m_nBackgroundUseCount = 0;
m_reservoirData = nullptr;
m_outlineData = nullptr;
m_pMeasuringScaleData = nullptr;
@ -571,6 +573,15 @@ ZX_DEFINE_DYNAMIC(DataAnalyzeManager, nmDataAnalyzeManager)
// 初始化静态成员
nmDataAnalyzeManager::~nmDataAnalyzeManager()
{
// 第一步:任何窗口销毁顺序下,都必须先等后台网格/求解任务停止访问。
// wait() 会暂时释放互斥量,因此任务仍能调用 endBackgroundUse() 正常退出。
{
QMutexLocker oLocker(&m_oBackgroundUseMutex);
while(m_nBackgroundUseCount > 0) {
m_oNoBackgroundUseCondition.wait(&m_oBackgroundUseMutex);
}
}
// 图元只引用数据数据统一由DataManager释放。
// 关闭成果或流动段分析后,清理该分析窗口对应的全部数据。
// 这两个成员是借用引用或容器内对象的别名不单独delete。
@ -635,6 +646,9 @@ nmDataAnalyzeManager* nmDataAnalyzeManager::getInstanceByFitting(iSubWndFitting*
}
nmDataAnalyzeManager* pInstance = new nmDataAnalyzeManager();
// DataManager 与创建它的成果窗口一一绑定。后续后台任务读取 PVT、拟压力等
// 上下文时必须使用该窗口,不能随界面当前页签切换到另一份成果。
pInstance->m_pOwnerFitting = pSubWndF;
s_mapDataAnalManager[pSubWndF] = pInstance;
return pInstance;
}
@ -663,6 +677,26 @@ nmDataAnalyzeManager* nmDataAnalyzeManager::getCurrentInstance()
}
}
void nmDataAnalyzeManager::beginBackgroundUse()
{
QMutexLocker oLocker(&m_oBackgroundUseMutex);
++m_nBackgroundUseCount;
}
void nmDataAnalyzeManager::endBackgroundUse()
{
QMutexLocker oLocker(&m_oBackgroundUseMutex);
if(m_nBackgroundUseCount <= 0) {
Q_ASSERT(false);
return;
}
--m_nBackgroundUseCount;
if(m_nBackgroundUseCount == 0) {
m_oNoBackgroundUseCondition.wakeAll();
}
}
nmDataWellBase *nmDataAnalyzeManager::createWell(NM_WELL_MODEL eWellType)
{
// 根据当前Fitting窗口来获取对应的井相关数据
@ -3111,7 +3145,8 @@ bool nmDataAnalyzeManager::getPebiPseudoPressureTable(std::vector<double>& press
pseudoPressure.clear();
nmDataAnalyzeContextProvider* context = nmDataAnalyzeContext::provider();
iSubWndFitting* fitting = nmDataAnalyzeManager::getCurrentFitting();
// 使用本 DataManager 创建时绑定的成果窗口;切换当前页签不能改变数据来源。
iSubWndFitting* fitting = m_pOwnerFitting;
VVecDouble pseudoResult;
if(context == nullptr
|| fitting == nullptr
@ -4086,8 +4121,9 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath)
oWellJson["WellCode"].GetString());
const NM_CASE_WELL_MODE eMode = static_cast<NM_CASE_WELL_MODE>(
oWellJson["Mode"].GetInt());
const bool bModeValid = eMode == NM_CaseWell_RateControlled ||
eMode == NM_CaseWell_Observation;
// Include Other Wells 只保存主动生产/注入井;无产量观察井由 Map
// 自动推导,不允许以观察角色混入这个持久化列表。
const bool bModeValid = eMode == NM_CaseWell_RateControlled;
if(sWellCode == sSavedPrimaryWellCode ||
!mapSavedWellTypes.contains(sWellCode) ||
setIncludedWellCodes.contains(sWellCode) ||
@ -4110,7 +4146,9 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath)
return false;
}
// 第四步:求解器顺序只允许有效真实井或显式手工裂缝占位。
// 第四步:此时井的历史流量尚未恢复,无法判断一口 Map 井最终属于
// 主动井还是自动观察井。因此这里只校验真实井存在、井型一致且不重复;
// 历史数据加载完成后,再与最终有效计算井集合做严格一致性校验。
QSet<QString> setOrderedWellCodes;
const rapidjson::Value& vecOrderJson = oCaseJson["SolverWellOrder"];
for(rapidjson::SizeType nIndex = 0;
@ -4137,7 +4175,7 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath)
oOrderJson["EntryKind"].GetInt());
if(eEntryKind == NM_SolverEntry_Well) {
if(!setEffectiveWellCodes.contains(sWellCode) ||
if(!mapSavedWellTypes.contains(sWellCode) ||
setOrderedWellCodes.contains(sWellCode) ||
mapSavedWellTypes.value(sWellCode, NM_WELL_MODEL::Unknow_Well) != eWellType) {
qWarning() << "SolverWellOrder contains an invalid real well:"
@ -4158,11 +4196,17 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath)
vecSavedSolverOrder.append(nmSolverWellRef(
static_cast<int>(nIndex), eWellType, sWellCode, eEntryKind));
}
if(!vecSavedSolverOrder.isEmpty() &&
setOrderedWellCodes != setEffectiveWellCodes) {
qWarning() << "SolverWellOrder does not contain every effective well:"
<< filePath;
return false;
if(!vecSavedSolverOrder.isEmpty()) {
// 主井和已启用的显式包含井无需依赖历史数据即可确定,必须已经在顺序中。
// 顺序中的其他 Map 井可能是稍后才能识别的无产量观察井,当前不能拒绝。
QSet<QString>::const_iterator oEffectiveIt = setEffectiveWellCodes.constBegin();
for(; oEffectiveIt != setEffectiveWellCodes.constEnd(); ++oEffectiveIt) {
if(!setOrderedWellCodes.contains(*oEffectiveIt)) {
qWarning() << "SolverWellOrder does not contain a configured well:"
<< *oEffectiveIt;
return false;
}
}
}
// 清空现有数据,确保从头加载新数据
@ -4536,7 +4580,9 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath)
return false;
}
// 井对象全部加载完成后,严格按 WellCode 恢复数值分析方案。
// 井对象全部加载完成后,先按 WellCode 原样恢复已经通过结构校验的分析方案。
// 这里不能调用会检查流量点的公开 setter因为历史流量要到 loadNmResult()
// 的二进制恢复阶段才可用;业务有效性在历史数据恢复后统一确认。
nmDataWellBase* pPrimaryWell = findWellByCode(sSavedPrimaryWellCode);
if(pPrimaryWell == nullptr) {
qWarning() << "PrimaryWellCode is missing from numerical project:"
@ -4547,10 +4593,11 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath)
m_oNumericalAnalysisCase.setPrimaryWellCode(sSavedPrimaryWellCode);
m_oNumericalAnalysisCase.setPrimaryWellMode(eSavedPrimaryWellMode);
m_oNumericalAnalysisCase.setPebiGridControl(dSavedPebiGridControl);
setIncludedCalculationWells(vecSavedIncludedWells);
m_oNumericalAnalysisCase.setIncludedWells(vecSavedIncludedWells);
m_oNumericalAnalysisCase.setIncludeOtherWells(bSavedIncludeOtherWells);
m_oNumericalAnalysisCase.setSolverWellOrder(vecSavedSolverOrder);
setCurrentResultWellCode(sSavedCurrentResultWellCode.isEmpty()
m_oNumericalAnalysisCase.setCurrentResultWellCode(
sSavedCurrentResultWellCode.isEmpty()
? sSavedPrimaryWellCode : sSavedCurrentResultWellCode);
@ -5212,8 +5259,24 @@ bool nmDataAnalyzeManager::loadNmResult(QString sLoadAnalDir)
// 第六步:二进制数据完整后,再生成各井用于曲线显示的压力、流量点。
this->loadWellPreAndFlow();
// 主动井必须能够从项目井目录恢复有效产量制度;观察井允许没有流量点。
// 第七步:历史数据恢复后再执行依赖流量的业务校验。
// IncludedWells 即使当前总开关关闭,也只能保存有有效产量制度的井。
QVector<nmCalculationWellRef> vecIncludedWells =
getIncludedCalculationWells();
for(int nIndex = 0; nIndex < vecIncludedWells.size(); ++nIndex) {
const nmCalculationWellRef& oWellRef = vecIncludedWells[nIndex];
nmDataWellBase* pWellData = findWellByCode(oWellRef.m_sWellCode);
if(!isSupportedNumericalWell(pWellData) ||
pWellData->getFlowPoints().size() < 2) {
qWarning() << "Failed to restore included rate well:"
<< oWellRef.m_sWellCode;
return false;
}
}
// 主动井必须能够恢复有效产量制度;自动观察井允许没有流量点。
QVector<nmCalculationWellRef> vecEffectiveWells = getEffectiveCalculationWells();
QSet<QString> setEffectiveWellCodes;
for(int nIndex = 0; nIndex < vecEffectiveWells.size(); ++nIndex) {
const nmCalculationWellRef& oWellRef = vecEffectiveWells[nIndex];
nmDataWellBase* pWellData = findWellByCode(oWellRef.m_sWellCode);
@ -5224,6 +5287,32 @@ bool nmDataAnalyzeManager::loadNmResult(QString sLoadAnalDir)
<< oWellRef.m_sWellCode;
return false;
}
setEffectiveWellCodes.insert(oWellRef.m_sWellCode);
}
// 保存的求解器槽位允许夹有手工裂缝,但其中全部真实井必须与最终有效井
// 完全一致。该校验放在这里,才能正确识别 Map 中自动加入的无产量观察井。
QVector<nmSolverWellRef> vecLoadedSolverOrder = getSolverWellOrder();
if(!vecLoadedSolverOrder.isEmpty()) {
QSet<QString> setOrderedWellCodes;
for(int nIndex = 0; nIndex < vecLoadedSolverOrder.size(); ++nIndex) {
const nmSolverWellRef& oWellRef = vecLoadedSolverOrder[nIndex];
if(oWellRef.m_eEntryKind == NM_SolverEntry_Well) {
setOrderedWellCodes.insert(oWellRef.m_sWellCode);
}
}
if(setOrderedWellCodes != setEffectiveWellCodes) {
qWarning() << "Loaded SolverWellOrder differs from effective wells:"
<< sLoadAnalDir;
return false;
}
}
// 结果井属于显示状态,但也必须在流量恢复后满足当前结果井规则。
if(!isWellAvailableForResult(getCurrentResultWellCode())) {
qWarning() << "Loaded result WellCode is not available:"
<< getCurrentResultWellCode();
return false;
}
if(bHasCurrentGridFile && bHasCompleteResultFiles) {

@ -183,14 +183,18 @@ nmSubWndMain::~nmSubWndMain()
}
if (m_pSolverTask) {
// 窗口销毁时只解除本窗口的回调;若线程仍在运行,让线程自己结束后释放
// 第一步:停止向正在析构的窗口投递完成回调
disconnect(m_pSolverTask, SIGNAL(sig_calculateDone(bool)), this, SLOT(on_solverTaskFinished(bool)));
if (m_pSolverTask == s_pRunningSolverTask && !m_pSolverTask->isRunning()) {
s_pRunningSolverTask = nullptr;
// 第二步DataManager 会在成果窗口销毁后释放,而任务仍读取该对象。
// Qt 4.8 下只能等待外部 DLL 正常返回,绝不能让裸指针越过管理器生命周期。
if(m_pSolverTask->isRunning()) {
m_pSolverTask->wait();
}
if (!m_pSolverTask->isRunning()) {
m_pSolverTask->deleteLater();
if (m_pSolverTask == s_pRunningSolverTask) {
s_pRunningSolverTask = nullptr;
}
// 第三步:线程已经停止,当前窗口仍是任务的明确所有者,可直接释放。
delete m_pSolverTask;
m_pSolverTask = nullptr;
}
@ -1633,7 +1637,6 @@ void nmSubWndMain::solveAndAnalyze()
pDataManager);
s_pRunningSolverTask = m_pSolverTask;
connect(m_pSolverTask, SIGNAL(sig_calculateDone(bool)), this, SLOT(on_solverTaskFinished(bool)));
connect(m_pSolverTask, SIGNAL(finished()), m_pSolverTask, SLOT(deleteLater()));
// 1. 创建并配置进度对话框
if (m_pProgressDlg == nullptr)
@ -1641,7 +1644,9 @@ void nmSubWndMain::solveAndAnalyze()
// TODO:关联到父窗口
m_pProgressDlg = new QProgressDialog(tr("Calculating, please wait..."), QString(), 0, 100, getMainWindow());
m_pProgressDlg->setWindowTitle(tr("Solver Progress"));
m_pProgressDlg->setWindowModality(Qt::NonModal); // 模态,锁定父窗口
// 求解任务仍需读取 DataManager 中的 Qt 容器。应用级模态会冻结所有成果的
// 编辑入口,避免 Qt 4.8 下后台读与前台写同时发生数据竞争。
m_pProgressDlg->setWindowModality(Qt::ApplicationModal);
// 某些系统下,对话框右上角的关闭按钮可能仍存
// 使用 WindowFlags 彻底禁用关闭按钮
@ -2048,6 +2053,13 @@ bool nmSubWndMain::initPreHistory(QVector<QVector<double> > &vvecHistoryData, QV
bool nmSubWndMain::onConfirmClosing()
{
// 正常关闭入口在求解结束前保持当前成果和 DataManager 存活;应用退出等绕过
// 该确认流程的路径仍由析构函数 wait() 兜底。
if(m_pSolverTask != nullptr && m_pSolverTask->isRunning()) {
QMessageBox::information(this, tr("solver error"), tr("task is running!"));
return false;
}
return iSubWndBaseAF::onConfirmClosing();
}
@ -2137,10 +2149,17 @@ void nmSubWndMain::on_solverTaskFinished(bool isSuccessed)
int nPebiCount = pTask != nullptr ? pTask->getPebiCount() : -1;
int nSolveTimeMs = pTask != nullptr ? pTask->getSolveTimeMs() : -1;
// 后台线程只生成局部结果快照。回到主线程后先验证所属 DataManager 和
// 输入版本,再整体替换旧成果;提交失败与求解失败使用同一条错误路径。
if(isSuccessed) {
isSuccessed = pTask->commitResult(m_pSolverDataManager.data());
}
m_pSolverTask = nullptr;
if(pTask == s_pRunningSolverTask) {
s_pRunningSolverTask = nullptr;
}
// 正常完成路径由主线程延迟释放任务;析构路径则会等待并直接释放。
pTask->deleteLater();
// 第二步:在清空捕获上下文前完成结果合并,确保始终回写原拟合窗口。
NM_Calculation_Result result = isSuccessed ? NM_Calculation_Result_Success : NM_Calculation_Result_Fail;

@ -821,6 +821,12 @@ void nmWxNumericalDesign::setupConnections()
if(m_pDataManager != nullptr) {
connect(m_pDataManager, SIGNAL(dataChanged()),
this, SLOT(onWellCollectionChanged()), Qt::UniqueConnection);
// createWell() 会在井对象登记时先发送 dataChanged(),此时 WellCode、
// 坐标和历史流量可能尚未填充。sigWellAdded 表示井已完整建立,必须再次
// 刷新 Include Other Wells才能正确识别刚加入 Map 的有产量井。
connect(m_pDataManager,
SIGNAL(sigWellAdded(QString,QString,QStringList)),
this, SLOT(onWellCollectionChanged()), Qt::UniqueConnection);
}
connect(m_pResetDiagnosticButton, SIGNAL(clicked()), this, SLOT(onResetFromDiagnosticClicked())); // "从诊断复位"按钮连接
connect(m_pResetAnalyticalButton, SIGNAL(clicked()), this, SLOT(onResetFromAnalyticalClicked())); // "从解析复位"按钮连接

Loading…
Cancel
Save