diff --git a/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h b/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h index ed78153..1de9f71 100644 --- a/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h +++ b/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h @@ -18,6 +18,9 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { ~nmCalculationDllPebiSolverTask(); void run(); + // PSO 等待线程结束后通过该接口判断本次结果是否可以继续参与误差计算. + // 返回 false 时调用方会丢弃本次结果, 防止复用上一粒子留下的旧曲线. + bool wasSuccessful() const; private: bool execute(); @@ -25,13 +28,15 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { // 执行PEBI求解流程。 bool execPebiMode(); // 将PEBI求解结果保存回数据对象。 - bool savePebiModeResult(HX_NWTM_MODEL_OUTPUT& p1); + bool savePebiModeResult(HX_NWTM_MODEL_OUTPUT& p1, int modelType); void logHX_NWTM_MODEL_INPUT_Simplified(const HX_NWTM_MODEL_INPUT& p0); bool saveHX_NWTM_MODEL_INPUT_ToTxt(const HX_NWTM_MODEL_INPUT& p0, const QString& filePath); private: QString m_sPostprocessingDir; + // run() 在线程内保存 execute() 结果, 等待线程结束的调用方只读取该状态. + bool m_lastRunSucceeded; private slots: //void slotTaskUpdateProgress(); diff --git a/Include/nmNum/nmSubWxs/nmWxAutomaticFittingStart.h b/Include/nmNum/nmSubWxs/nmWxAutomaticFittingStart.h index 58fcced..eb37eeb 100644 --- a/Include/nmNum/nmSubWxs/nmWxAutomaticFittingStart.h +++ b/Include/nmNum/nmSubWxs/nmWxAutomaticFittingStart.h @@ -41,6 +41,7 @@ public: void setTargetLogLogData(const QVector >& data); void setBestLogLogData(const QVector >& data, int iteration, double fitness); + void setPseudoPressureMode(bool enabled); protected: virtual void paintEvent(QPaintEvent* event); @@ -64,6 +65,7 @@ private: QRectF m_logRange; int m_bestIteration; double m_bestFitness; + bool m_pseudoPressureMode; }; // 算法类型枚举 @@ -90,6 +92,7 @@ public: void setFittingParameters(int maxIterations, double targetError, const QString& wellName); void setSelectedParameters(const QStringList& parameterNames); void setTargetLogLogData(const QVector >& targetData); + void setPseudoPressureMode(bool enabled); // 拟合 void markFittingStarted(); diff --git a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp index 8ba99db..e8db3a5 100644 --- a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp @@ -77,6 +77,23 @@ bool isValidSemiLogPoint(const Point& pt) && isReasonableLogLogValue(pt.pointData[0]); } +typedef bool (*CalPseudoPressure)(double, double&, int); + +// mAlgPseudo.dll 是主界面拟压力算法所在模块. +// 此处直接解析 iAlgPseuCaller::calPS 的导出符号, 复用主界面的同一套转换实现. +// 函数参数依次为原始压力、输出拟压力和拟压力分区编号. +CalPseudoPressure getPseudoPressureConverter() +{ + // 静态保存模块和函数地址, 避免每个粒子、每个压力点重复加载和解析 DLL. + // configPsAbouts() 初始化的数据及模式也保存在同一个 mAlgPseudo.dll 模块中. + static HMODULE module = LoadLibraryW(L"mAlgPseudo.dll"); + static CalPseudoPressure converter = module + ? reinterpret_cast(GetProcAddress( + module, "?calPS@iAlgPseuCaller@@SA_NNAANH@Z")) + : nullptr; + return converter; +} + /// @brief 将常数PVT单值扩展为200元素数组(PEBI求解器要求数组输入) /// @param dValue 常数PVT值 /// @param nSize 数组大小,默认200 @@ -186,7 +203,8 @@ nmCalculationDllPebiSolverTask::nmCalculationDllPebiSolverTask( QString sPostprocessingDir, QObject *parent): QThread(parent), - m_sPostprocessingDir(sPostprocessingDir) + m_sPostprocessingDir(sPostprocessingDir), + m_lastRunSucceeded(false) { } @@ -200,8 +218,13 @@ nmCalculationDllPebiSolverTask::~nmCalculationDllPebiSolverTask() void nmCalculationDllPebiSolverTask::run() { - bool result = this->execute(); - emit sig_calculateDone(result); + m_lastRunSucceeded = this->execute(); + emit sig_calculateDone(m_lastRunSucceeded); +} + +bool nmCalculationDllPebiSolverTask::wasSuccessful() const +{ + return m_lastRunSucceeded; } bool nmCalculationDllPebiSolverTask::execute() @@ -244,7 +267,8 @@ bool nmCalculationDllPebiSolverTask::execPebiMode() HX_NWTM_MODEL_INPUT p0(pGridInstance->getGridOutput2()); nmDataReservoir* pReservoirData = pDataInstance->getReservoirData(); // 根据PVT参数获取结果选择求解器模型类型 - p0.T = static_cast(pDataInstance->getSolverModelType()); + const int modelType = static_cast(pDataInstance->getSolverModelType()); + p0.T = modelType; // 获取PEBI网格存入井的顺序 QVector> vecWellsOrder = pDataInstance->getCalculationWells(); @@ -513,7 +537,7 @@ bool nmCalculationDllPebiSolverTask::execPebiMode() } // 保存计算结果 - bool success = this->savePebiModeResult(p1); + bool success = this->savePebiModeResult(p1, modelType); FreeLibrary(dll); // 卸载 DLL return success; @@ -553,13 +577,26 @@ std::vector HX_logderivative(const std::vector& x, const std::ve return d; } -bool nmCalculationDllPebiSolverTask::savePebiModeResult(HX_NWTM_MODEL_OUTPUT& p1) +bool nmCalculationDllPebiSolverTask::savePebiModeResult( + HX_NWTM_MODEL_OUTPUT& p1, + int modelType) { nmDataAnalyzeManager* pDataInstance = nmDataAnalyzeManager::getCurrentInstance(); QVector> vvecPressure; QVector> vvecLogLog; QVector> vvecSemiLog; + // p1.pw 是求解器返回的原始井底压力, 后续仍按 MPa 保存到 vvecPressure. + // 只有气单相变化 PVT 模型的双对数和半对数计算需要改用拟压力. + const bool usePseudoPressure = (modelType == static_cast(SMT_Gas_VariablePvt)); + CalPseudoPressure calPseudoPressure = usePseudoPressure + ? getPseudoPressureConverter() + : nullptr; + + if(usePseudoPressure && calPseudoPressure == nullptr) { + qWarning() << "Failed to load the gas pseudo-pressure converter."; + return false; + } // 获取参与求解的井的顺序 QVector> vecWellsOrder = nmDataAnalyzeManager::getCurrentInstance()->getCalculationWells(); @@ -615,7 +652,19 @@ bool nmCalculationDllPebiSolverTask::savePebiModeResult(HX_NWTM_MODEL_OUTPUT& p1 pt.x = 0.0; // 或者进行错误处理 } - pt.y = p1.pw[wellIdx][i]; + // 使用局部副本转换, 不修改 p1.pw 中需要保存和显示的原始压力. + double pressureForLog = p1.pw[wellIdx][i]; + if(usePseudoPressure + && (!isFiniteSolverNumber(pressureForLog) + // -1 与主界面调用一致, 表示使用 configPsAbouts() 配置的当前模式. + || !calPseudoPressure(pressureForLog, pressureForLog, -1) + || !isFiniteSolverNumber(pressureForLog))) { + qWarning() << "Failed to convert gas pressure to pseudo-pressure:" + << p1.pw[wellIdx][i] << "well:" << sWellName; + return false; + } + + pt.y = pressureForLog; pt.z = 0.0; // 假设这里的 z 是 0 wellPressureDataForDll.push_back(pt); } @@ -648,7 +697,17 @@ bool nmCalculationDllPebiSolverTask::savePebiModeResult(HX_NWTM_MODEL_OUTPUT& p1 return false; } - preLogFun(wellPressureDataForDll, iSectionFlowIndex, timeQ.data(), q.data(), nTimeNumQ, logPreResultFromDll); + // 气井传入拟压力序列, 油井和水井仍传入原始压力序列. + // 计算失败必须向上返回, 避免 PSO 使用空曲线或上一粒子的旧曲线. + if(!preLogFun(wellPressureDataForDll, + iSectionFlowIndex, + timeQ.data(), + q.data(), + nTimeNumQ, + logPreResultFromDll)) { + FreeLibrary(hMod_solver); + return false; + } // 填充双对数曲线数据到局部变量 QVector logX, logY, logZ; @@ -695,6 +754,9 @@ bool nmCalculationDllPebiSolverTask::savePebiModeResult(HX_NWTM_MODEL_OUTPUT& p1 vvecSemiLog.append(semiLogY); FreeLibrary(hMod_solver); + } else { + qWarning() << "Failed to load singlePhaseSolverDll.dll."; + return false; } } diff --git a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp index 5db6c76..6e57f64 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp @@ -60,6 +60,7 @@ #include "nmWxGeoRefDlg.h" #include "iSubWndFitting.h" +#include "iAnalRun.h" #include @@ -1477,6 +1478,27 @@ void nmSubWndMain::solveAndAnalyze() } pTabWx->setCurrentWidget(pSubWndFit); + nmDataAnalyzeManager* pDataManager = nmDataAnalyzeManager::getCurrentInstance(); + if(pDataManager == nullptr) { + return; + } + + // 手动求解可能直接启动, 因此必须在创建求解线程前初始化拟压力转换器. + // pSubWndFit 保存的是当前拟合窗口克隆后的 PVT、拟压力及模型选项数据. + // configPsAbouts() 会据此初始化常规、三区或组分转换器并设置当前模式. + if(pDataManager->getSolverModelType() == SMT_Gas_VariablePvt) { + iAnalRun* pAnalRun = pSubWndFit->getAnalRun(); + if(pAnalRun == nullptr + || !pAnalRun->configPsAbouts(true, + pSubWndFit->getModelOption(), + false, + pSubWndFit->getAllWxPtr())) { + QMessageBox::warning(this, tr("solver error"), + tr("Gas pseudo-pressure data is unavailable or invalid.")); + return; + } + } + // 调用求解器 QString sPostprocessingPath = ""; diff --git a/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp b/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp index ebd2b49..0a38711 100644 --- a/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp +++ b/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp @@ -3,14 +3,116 @@ #include "nmWxAutomaticfittingStart.h" #include "nmWxParameterProperty.h" #include "nmDataAnalyzeManager.h" +#include "iSubWndFitting.h" +#include "mGui/mGuiAnal/iAnalRun.h" +#include "iGuiPlot.h" +#include "ZxObjCurve.h" +#include "mAlgDefines.h" + +#pragma comment(lib, "mGuiAnal.lib") + #include #include +#include +#include #ifdef Q_OS_WIN #include #define DEBUG_UI(msg) OutputDebugStringA(QString("[UI] %1\n").arg(msg).toLocal8Bit().data()) #endif +namespace { + +bool nmAutoFitUiIsFinite(double value) +{ +#ifdef Q_OS_WIN + return _finite(value) != 0; +#else + return std::isfinite(value); +#endif +} + +// 主界面气井双对数中的源曲线和导数曲线已经采用拟压力量纲. +// 自动拟合直接读取这两条显示曲线, 避免井对象中的原始压力历史数据与结果曲线量纲不一致. +// 两条曲线可能因无效点过滤而长度不同, 因此按时间坐标匹配共同有效点. +bool getMainHistoryLogLog(iSubWndFitting* fitting, + QVector >& data, + QString& errorMessage) +{ + data.clear(); + if(!fitting) { + errorMessage = QObject::tr("The current fitting window is unavailable."); + return false; + } + + QWidget* widget = fitting->getFitSubRstWxOf(FSRT_DoubleLog, false, &errorMessage); + iGuiPlot* plot = qobject_cast(widget); + if(!plot) { + errorMessage = QObject::tr("The main double-log plot is unavailable."); + return false; + } + + ZxObjCurve* sourceCurve = plot->getCurve(s_Souce_Curve); + ZxObjCurve* derivativeCurve = plot->getCurve(s_Deriv_Curve); + if(!sourceCurve || !derivativeCurve) { + errorMessage = QObject::tr("The main double-log history curves are unavailable."); + return false; + } + + QVector sourcePoints = sourceCurve->getAllValues(); + QVector derivativePoints = derivativeCurve->getAllValues(); + + data.resize(3); + int sourceIndex = 0; + int derivativeIndex = 0; + while(sourceIndex < sourcePoints.size() + && derivativeIndex < derivativePoints.size()) { + const QPointF sourcePoint = sourcePoints[sourceIndex]; + const QPointF derivativePoint = derivativePoints[derivativeIndex]; + const double sourceTime = sourcePoint.x(); + const double derivativeTime = derivativePoint.x(); + + if(!nmAutoFitUiIsFinite(sourceTime) || sourceTime <= 0.0) { + ++sourceIndex; + continue; + } + if(!nmAutoFitUiIsFinite(derivativeTime) || derivativeTime <= 0.0) { + ++derivativeIndex; + continue; + } + + const double timeTolerance = qMax(1.0, + qMax(qAbs(sourceTime), qAbs(derivativeTime))) * 1e-8; + if(qAbs(sourceTime - derivativeTime) <= timeTolerance) { + const double source = sourcePoint.y(); + const double derivative = derivativePoint.y(); + if(nmAutoFitUiIsFinite(source) + && nmAutoFitUiIsFinite(derivative) + && source > 0.0 && derivative > 0.0) { + data[0].append(sourceTime); + data[1].append(source); + data[2].append(derivative); + } + ++sourceIndex; + ++derivativeIndex; + } else if(sourceTime < derivativeTime) { + ++sourceIndex; + } else { + ++derivativeIndex; + } + } + + if(data[0].size() < 5) { + data.clear(); + errorMessage = QObject::tr("The main double-log history curves have too few valid points."); + return false; + } + + return true; +} + +} + // 设置某一行参数是否显示。隐藏时同步取消勾选并禁用,避免隐藏参数参与自动拟合。 void nmWxAutomaticFitting::setParameterRowVisible(QTableWidget* table, int row, bool visible) { @@ -556,16 +658,54 @@ void nmWxAutomaticFitting::onAccept() return; } + nmDataAnalyzeManager* pManager = nmDataAnalyzeManager::getCurrentInstance(); + if(!pManager) { + QMessageBox::warning(this, tr("Warning"), tr("Data manager is unavailable!")); + return; + } + // 从数据管理器获取最新的井对象 - nmDataWellBase* pTargetWell = nmDataAnalyzeManager::getCurrentInstance()->findWellByName(selectedWellName); + nmDataWellBase* pTargetWell = pManager->findWellByName(selectedWellName); if(!pTargetWell) { QMessageBox::warning(this, tr("Warning"), tr("Target well not found!")); return; } - // 获取目标双对数历史数据 - QVector > targetLogLogData = pTargetWell->getHistoryLogLog(); + QVector > targetLogLogData; + const bool usePseudoPressure = (pManager->getSolverModelType() == SMT_Gas_VariablePvt); + if(usePseudoPressure) { + // 目标曲线来自当前主界面, 必须确认界面显示的井就是用户选择的拟合井. + nmDataWellBase* pCurrentWell = pManager->getCurWellData(); + if(!pCurrentWell || pCurrentWell->getWellName() != selectedWellName) { + QMessageBox::warning(this, tr("Warning"), + tr("Please display the selected gas well in the main result view before fitting.")); + return; + } + + iSubWndFitting* pFitting = nmDataAnalyzeManager::getCurrentFitting(); + iAnalRun* pAnalRun = pFitting ? pFitting->getAnalRun() : nullptr; + // 自动拟合不依赖手动求解流程, 启动 PSO 前需要单独初始化同一拟压力转换器. + // 传入拟合窗口自己的页面数据, 保证目标曲线和每个粒子的结果使用同一 PVT 与模式. + if(!pAnalRun + || !pAnalRun->configPsAbouts(true, + pFitting->getModelOption(), + false, + pFitting->getAllWxPtr())) { + QMessageBox::warning(this, tr("Warning"), + tr("Gas pseudo-pressure data is unavailable or invalid.")); + return; + } + + QString curveError; + if(!getMainHistoryLogLog(pFitting, targetLogLogData, curveError)) { + QMessageBox::warning(this, tr("Warning"), curveError); + return; + } + } else { + // 油井和水井不使用拟压力, 继续读取井对象中原有的双对数历史数据. + targetLogLogData = pTargetWell->getHistoryLogLog(); + } if(targetLogLogData.isEmpty() || targetLogLogData.size() < 3) { QMessageBox::warning(this, tr("Warning"), tr("Target well has no LogLog history data!")); @@ -864,6 +1004,8 @@ void nmWxAutomaticFitting::startAutoFitting(const QVector>& targ m_progressMonitor = new nmWxAutomaticfittingStart(this); m_progressMonitor->setAutoFitter(m_autoFitterPSO); + m_progressMonitor->setPseudoPressureMode( + nmDataAnalyzeManager::getCurrentInstance()->getSolverModelType() == SMT_Gas_VariablePvt); m_progressMonitor->setTargetLogLogData(targetData); connect(m_autoFitterPSO, SIGNAL(fittingFinished(bool, QString)), diff --git a/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp b/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp index fc13e4e..6e0bd2b 100644 --- a/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp +++ b/Src/nmNum/nmSubWxs/nmWxAutomaticFittingStart.cpp @@ -31,11 +31,19 @@ nmAutomaticFittingCurveChart::nmAutomaticFittingCurveChart(QWidget* parent) , m_logRange(0.0, 0.0, 1.0, 1.0) , m_bestIteration(0) , m_bestFitness(0.0) + , m_pseudoPressureMode(false) { setMinimumHeight(220); setMinimumWidth(420); } +void nmAutomaticFittingCurveChart::setPseudoPressureMode(bool enabled) +{ + // 该状态只控制纵轴标题, 不改变目标曲线、最优曲线或误差数据. + m_pseudoPressureMode = enabled; + update(); +} + void nmAutomaticFittingCurveChart::setTargetLogLogData(const QVector >& data) { m_targetPressure = extractCurve(data, 1); @@ -293,9 +301,13 @@ void nmAutomaticFittingCurveChart::drawAxes(QPainter& painter) painter.drawText(QPointF(m_chartRect.center().x() - fm.width(xTitle) / 2.0, height() - 8), xTitle); painter.save(); - painter.translate(13, m_chartRect.center().y() + 46); + const QString yTitle = m_pseudoPressureMode + // 气井目标和结果均为拟压力时显示对应量纲. + ? tr("m(p) (MPa^2/mPa.s)") + : tr("Pressure (MPa)"); + painter.translate(13, m_chartRect.center().y()); painter.rotate(-90); - painter.drawText(QPointF(0, 0), tr("Pressure (MPa)")); + painter.drawText(QPointF(-fm.width(yTitle) / 2.0, 0), yTitle); painter.restore(); painter.restore(); @@ -664,6 +676,13 @@ void nmWxAutomaticfittingStart::setTargetLogLogData(const QVectorsetPseudoPressureMode(enabled); + } +} + void nmWxAutomaticfittingStart::onFittingProgress(int iteration, double fitness) { // 确保iteration在合理范围内