From 8e0f1a98517d32de49f181ceb9582a7f9205aa5a Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Thu, 22 Oct 2026 16:45:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(nmNum):=20=E5=A4=8D=E7=94=A8=E9=A2=84?= =?UTF-8?q?=E6=B5=8B=E7=AA=97=E5=8F=A3=E5=B9=B6=E6=94=AF=E6=8C=81=E9=A2=84?= =?UTF-8?q?=E6=B5=8B=E6=95=B0=E6=8D=AE=E4=BF=9D=E5=AD=98=E8=AF=BB=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 复用预测输入窗口,保存输入参数及单位。 - 保存预测结果,读取项目时恢复曲线。 - 简化预测窗口标题,修改模型参数时保留已有预测。 --- Include/nmNum/nmData/nmDataAnalyzeManager.h | 4 + Include/nmNum/nmData/nmDataForecast.h | 17 ++++ Include/nmNum/nmSubWnd/nmSubWndMain.h | 6 +- Include/nmNum/nmSubWxs/nmWxForecast.h | 8 +- Include/nmNum/nmSubWxs/nmWxNumericalDesign.h | 6 +- Src/nmNum/nmData/nmDataAnalyzeManager.cpp | 45 ++++++++++- Src/nmNum/nmData/nmDataForecast.cpp | 56 ++++++++++++- Src/nmNum/nmSubWnd/nmSubWndMain.cpp | 51 +++++++----- Src/nmNum/nmSubWnd/nmSubWndUtils.cpp | 13 +-- Src/nmNum/nmSubWxs/nmWxForecast.cpp | 85 ++++++++++++++------ Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp | 15 ++-- 11 files changed, 232 insertions(+), 74 deletions(-) diff --git a/Include/nmNum/nmData/nmDataAnalyzeManager.h b/Include/nmNum/nmData/nmDataAnalyzeManager.h index 29e71efb..57d3cd56 100644 --- a/Include/nmNum/nmData/nmDataAnalyzeManager.h +++ b/Include/nmNum/nmData/nmDataAnalyzeManager.h @@ -418,6 +418,7 @@ public: nmDataForecast* getForecastData() const; nmDataForecast getForecastDataCopy() const; void updateForecastData(const nmDataForecast& newData); + void publishForecastResult(const nmDataForecast& completedData); // 敏感性分析相关 nmDataSensitive* createSensitiveData(); @@ -782,6 +783,8 @@ public: void syncGeometryAttrs(); signals: + void sigForecastResultChanged(); + void sigForecastDataChanged(); void dataChanged(); void sigSensitiveResultsChanged(); /// @brief 井数据准备完成信号 @@ -802,6 +805,7 @@ public: void sigPebiResultStateChanged(); private slots: + void clearInvalidForecastResult(); /** @brief 根据参数面板实际编辑的属性类型,使对应 PEBI 输入版本失效。 */ void onRegisteredAttributeEdited(QString sName, QVariant oValue); diff --git a/Include/nmNum/nmData/nmDataForecast.h b/Include/nmNum/nmData/nmDataForecast.h index 894f763a..6866676b 100644 --- a/Include/nmNum/nmData/nmDataForecast.h +++ b/Include/nmNum/nmData/nmDataForecast.h @@ -2,6 +2,7 @@ #define NMDATAFORECAST_H #include +#include #include "nmData_global.h" #include "nmDataBase.h" @@ -149,6 +150,9 @@ struct NM_DATA_EXPORT MultiplePressures { struct NM_DATA_EXPORT MultipleRates { nmDataAttribute initialPressure; + QString pressureUnit; + QString durationUnit; + QString rateUnit; MultipleRates(); @@ -192,10 +196,23 @@ public: void setMultipleRatesData(const MultipleRates& mr); MultipleRates& getMultipleRatesData(); + // 已生成结果保留独立制度,后续编辑输入不改变原曲线。 + struct Result { + QString snapshotId; + QString wellInstanceId; + QVector times; ///< hr + QVector pressures; ///< MPa + QVector durations; ///< hr + QVector rates; ///< m^3/D + }; + QSharedPointer getResult() const { return m_result; } + void setResult(const QSharedPointer& result) { m_result = result; } + virtual rapidjson::Value ToJsonValue(rapidjson::Document::AllocatorType& allocator) const override; virtual void FromJsonValue(const rapidjson::Value& jsonValue) override; private: + QSharedPointer m_result; ForecastOption currentOption; ConstantPressure constantPressureData; DecliningPressure decliningPressureData; diff --git a/Include/nmNum/nmSubWnd/nmSubWndMain.h b/Include/nmNum/nmSubWnd/nmSubWndMain.h index b66a3399..e90f2b6c 100644 --- a/Include/nmNum/nmSubWnd/nmSubWndMain.h +++ b/Include/nmNum/nmSubWnd/nmSubWndMain.h @@ -173,10 +173,8 @@ class NM_SUB_WND_EXPORT nmSubWndMain : public iSubWndBaseFit { /** @brief 在框架已有的敏感性子窗口叠加显示本分析的双对数结果。 */ void onShowSensitiveResults(); // 预测结果复用所属分析内的 MDI 子窗口,模型变化时移除旧结果。 - void onShowForecastResult(const QString& wellName, const QVector& times, - const QVector& pressures, const QVector& durations, - const QVector& rates); - void clearForecastResult(); + void onShowForecastResult(); + void onForecastDataChanged(); // 只读结果曲线共用框架数据表,不开放拖动编辑。 void onReadonlyCurveDoubleClicked(ZxPlot* plot, ZxObjBase* object, const QPointF& point, bool& processed); diff --git a/Include/nmNum/nmSubWxs/nmWxForecast.h b/Include/nmNum/nmSubWxs/nmWxForecast.h index c61b80e4..2ab3d0de 100644 --- a/Include/nmNum/nmSubWxs/nmWxForecast.h +++ b/Include/nmNum/nmSubWxs/nmWxForecast.h @@ -33,10 +33,6 @@ public: signals: void sigForecastTaskReady(nmWxForecast* dialog, nmCalculationDllPebiSolverTask* task); void sigForecastCancelRequested(); - // 计算完成后由所属分析主界面承载结果,输入窗口不持有结果窗口。 - void sigForecastReady(const QString& wellName, const QVector& times, - const QVector& pressures, const QVector& durations, - const QVector& rates); public slots: void reject(); @@ -63,6 +59,7 @@ protected: void onCancelClicked(); void onForecastFinished(); void onModelChanged(); + void saveDataToManager(); // Table edit void onPressureCellChanged(int row, int column); @@ -81,7 +78,6 @@ private: void updateParametersForOption(); void updateUIFromData(); void updateDataFromUI(); - void saveDataToManager(); bool readRateSchedule(QVector& durations, QVector& rates, QString& error) const; QWidget* buildExtEmptyPage(const QString& text = QString()); @@ -98,7 +94,7 @@ private: nmCalculationDllPebiSolverTask* m_task; bool m_calculationRunning; QString m_sourceSnapshotId; - QString m_wellName; + QString m_sourceWellInstanceId; QVector m_durations; ///< 本次计算的分段时长(hr)。 QVector m_rates; ///< 本次计算的分段产量(m^3/D)。 diff --git a/Include/nmNum/nmSubWxs/nmWxNumericalDesign.h b/Include/nmNum/nmSubWxs/nmWxNumericalDesign.h index da4e3f7d..a13fd4d0 100644 --- a/Include/nmNum/nmSubWxs/nmWxNumericalDesign.h +++ b/Include/nmNum/nmSubWxs/nmWxNumericalDesign.h @@ -5,6 +5,7 @@ #include "iDlgBase.h" #include "nmParameterField.h" #include +#include // 前向声明 class QGroupBox; @@ -73,10 +74,6 @@ public: signals: // 预测任务交给所属主窗口,复用普通求解进度框。 void sigForecastTaskReady(nmWxForecast* dialog, nmCalculationDllPebiSolverTask* task); - // 将预测结果转交给本面板所属的分析主界面。 - void sigForecastReady(const QString& wellName, const QVector& times, - const QVector& pressures, const QVector& durations, - const QVector& rates); void sigGenerateClicked(); // 点击生成按钮时发出的信号 // 敏感性批量任务交给所属主窗口,复用普通求解进度框。 void sigSensitivityTaskReady(nmWxSensitive* dialog, nmCalculationDllPebiSolverTask* task); @@ -230,6 +227,7 @@ private: QPushButton* m_pGenerateButton; QPushButton* m_pAutomaticfittingBtn; QPushButton* m_pForecastBtn; + QPointer m_pForecastDialog; ///< 每个分析面板复用自己的预测输入窗口。 QPushButton* m_pSensitiveBtn; QHBoxLayout* m_pGenerateLayout; diff --git a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp index 99cefc52..f2358f35 100644 --- a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp +++ b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp @@ -1358,6 +1358,8 @@ ZX_DEFINE_DYNAMIC(DataAnalyzeManager, nmDataAnalyzeManager) //m_pSkinVsRateData = nullptr; //m_pFlowSegmentData = nullptr; m_pForecastData = nullptr; + // 基础结果或结果井切换时清理预测结果,修改模型参数时保留上次预测。 + connect(this, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(clearInvalidForecastResult())); m_pSensitiveData = nullptr; // 属性注册表 @@ -2884,7 +2886,33 @@ void nmDataAnalyzeManager::updateForecastData(const nmDataForecast& newData) m_pForecastData = new nmDataForecast; } - *m_pForecastData = newData; // 调用赋值运算符 + // 参数更新不覆盖上一次生成的结果。 + const QSharedPointer result = m_pForecastData->getResult(); + *m_pForecastData = newData; + m_pForecastData->setResult(result); + emit sigForecastDataChanged(); +} + +void nmDataAnalyzeManager::publishForecastResult(const nmDataForecast& completedData) +{ + // 与敏感性分析一致,通过数据中心发布结果并通知所属主窗口。 + if(!m_pForecastData) m_pForecastData = new nmDataForecast; + m_pForecastData->setResult(completedData.getResult()); + emit sigForecastDataChanged(); + emit sigForecastResultChanged(); +} + +void nmDataAnalyzeManager::clearInvalidForecastResult() +{ + // 项目读取也会发出快照通知;只有来源不匹配时才丢弃已保存结果。 + if(!m_pForecastData || m_pForecastData->getResult().isNull()) return; + const QSharedPointer result = m_pForecastData->getResult(); + if(!m_pPebiResultSnapshot.isNull() && + result->snapshotId == m_pPebiResultSnapshot->getSnapshotId() && + result->wellInstanceId == getCurrentResultWellInstanceId()) return; + m_pForecastData->setResult(QSharedPointer()); + emit sigForecastDataChanged(); + emit sigForecastResultChanged(); } nmDataSensitive* nmDataAnalyzeManager::createSensitiveData() @@ -5921,6 +5949,14 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) m_pAutomaticFittingData->FromJsonValue(doc["AutomaticFitting"]); } + // 预测参数和结果随项目保存,旧项目没有此字段时使用默认值。 + delete m_pForecastData; + m_pForecastData = NULL; + if(doc.HasMember("Forecast") && doc["Forecast"].IsObject()) { + m_pForecastData = new nmDataForecast; + m_pForecastData->FromJsonValue(doc["Forecast"]); + } + // 解析 "Sensitive" 对象 if(doc.HasMember("Sensitive") && doc["Sensitive"].IsObject()) { m_pSensitiveData = new nmDataSensitive; @@ -6375,6 +6411,12 @@ bool nmDataAnalyzeManager::WriteProjectData( doc.AddMember("MixResult", mixResultsJson, allocator); } + // 复用预测数据已有的 JSON 序列化入口。 + if(m_pForecastData) { + rapidjson::Value forecastJson = m_pForecastData->ToJsonValue(allocator); + doc.AddMember("Forecast", forecastJson, allocator); + } + // 序列化 "Sensitive" if(m_pSensitiveData) { rapidjson::Value sensitiveJson(rapidjson::kObjectType); @@ -7213,6 +7255,7 @@ void nmDataAnalyzeManager::swapLoadedProjectState( qSwap(m_reservoirData, oLoadedManager.m_reservoirData); qSwap(m_pGeoRefData, oLoadedManager.m_pGeoRefData); qSwap(m_pSensitiveData, oLoadedManager.m_pSensitiveData); + qSwap(m_pForecastData, oLoadedManager.m_pForecastData); qSwap(m_pebiPvtPara, oLoadedManager.m_pebiPvtPara); qSwap(m_pMixedResults, oLoadedManager.m_pMixedResults); qSwap(m_pTimeStep, oLoadedManager.m_pTimeStep); diff --git a/Src/nmNum/nmData/nmDataForecast.cpp b/Src/nmNum/nmData/nmDataForecast.cpp index 4c97cb30..26540d33 100644 --- a/Src/nmNum/nmData/nmDataForecast.cpp +++ b/Src/nmNum/nmData/nmDataForecast.cpp @@ -1,4 +1,5 @@ #include "nmDataForecast.h" +#include PressureRow::PressureRow() { const QStringList timeUnits = (QStringList() @@ -334,6 +335,7 @@ nmDataForecast::nmDataForecast(const nmDataForecast& other) { nmDataForecast& nmDataForecast::operator=(const nmDataForecast& other) { if (this != &other) { + m_result = other.m_result; currentOption = other.currentOption; constantPressureData = other.constantPressureData; decliningPressureData = other.decliningPressureData; @@ -408,6 +410,9 @@ rapidjson::Value nmDataForecast::ToJsonValue(rapidjson::Document::AllocatorType& rapidjson::Value mr(rapidjson::kObjectType); mr.AddMember("InitialPressure", multipleRatesData.initialPressure.ToJsonValue(allocator), allocator); + mr.AddMember("PressureUnit", rapidjson::Value(multipleRatesData.pressureUnit.toUtf8().constData(), allocator).Move(), allocator); + mr.AddMember("DurationUnit", rapidjson::Value(multipleRatesData.durationUnit.toUtf8().constData(), allocator).Move(), allocator); + mr.AddMember("RateUnit", rapidjson::Value(multipleRatesData.rateUnit.toUtf8().constData(), allocator).Move(), allocator); // FlowTable: rapidjson::Value arr(rapidjson::kArrayType); @@ -424,6 +429,21 @@ rapidjson::Value nmDataForecast::ToJsonValue(rapidjson::Document::AllocatorType& root.AddMember("MultipleRatesData", mr, allocator); } + // 只保存原始曲线数值,读取后复用主界面的绘图流程。 + if(!m_result.isNull()) { + rapidjson::Value result(rapidjson::kObjectType); + result.AddMember("SnapshotId", rapidjson::Value(m_result->snapshotId.toUtf8().constData(), allocator).Move(), allocator); + result.AddMember("WellInstanceId", rapidjson::Value(m_result->wellInstanceId.toUtf8().constData(), allocator).Move(), allocator); + const char* names[] = { "Times", "Pressures", "Durations", "Rates" }; + const QVector* values[] = { &m_result->times, &m_result->pressures, &m_result->durations, &m_result->rates }; + for(int column = 0; column < 4; ++column) { + rapidjson::Value array(rapidjson::kArrayType); + for(int i = 0; i < values[column]->size(); ++i) + array.PushBack(values[column]->at(i), allocator); + result.AddMember(rapidjson::Value(names[column], allocator).Move(), array, allocator); + } + root.AddMember("Result", result, allocator); + } return root; } @@ -432,7 +452,9 @@ void nmDataForecast::FromJsonValue(const rapidjson::Value& jsonValue) if (!jsonValue.IsObject()) return; - if (jsonValue.HasMember("ForecastOption")) + // 重复读取或旧项目没有结果时,不沿用上一份预测。 + m_result.clear(); + if (jsonValue.HasMember("ForecastOption") && jsonValue["ForecastOption"].IsInt()) currentOption = static_cast(jsonValue["ForecastOption"].GetInt()); if (currentOption == ConstantPressureOption && jsonValue.HasMember("ConstantPressureData")) @@ -500,6 +522,12 @@ void nmDataForecast::FromJsonValue(const rapidjson::Value& jsonValue) if (mr.IsObject()) { if (mr.HasMember("InitialPressure")) multipleRatesData.initialPressure.FromJsonValue(mr["InitialPressure"]); + if (mr.HasMember("PressureUnit") && mr["PressureUnit"].IsString()) + multipleRatesData.pressureUnit = QString::fromUtf8(mr["PressureUnit"].GetString()); + if (mr.HasMember("DurationUnit") && mr["DurationUnit"].IsString()) + multipleRatesData.durationUnit = QString::fromUtf8(mr["DurationUnit"].GetString()); + if (mr.HasMember("RateUnit") && mr["RateUnit"].IsString()) + multipleRatesData.rateUnit = QString::fromUtf8(mr["RateUnit"].GetString()); multipleRatesData.flowTable.clear(); if (mr.HasMember("FlowTable") && mr["FlowTable"].IsArray()) @@ -519,6 +547,32 @@ void nmDataForecast::FromJsonValue(const rapidjson::Value& jsonValue) } } } + // 残缺或无效结果不恢复,输入参数仍可正常使用。 + if(jsonValue.HasMember("Result") && jsonValue["Result"].IsObject()) { + const rapidjson::Value& json = jsonValue["Result"]; + if(!json.HasMember("SnapshotId") || !json["SnapshotId"].IsString() || + !json.HasMember("WellInstanceId") || !json["WellInstanceId"].IsString()) return; + QSharedPointer result(new Result); + result->snapshotId = QString::fromUtf8(json["SnapshotId"].GetString()); + result->wellInstanceId = QString::fromUtf8(json["WellInstanceId"].GetString()); + if(result->snapshotId.isEmpty() || result->wellInstanceId.isEmpty()) return; + const char* names[] = { "Times", "Pressures", "Durations", "Rates" }; + QVector* values[] = { &result->times, &result->pressures, &result->durations, &result->rates }; + for(int column = 0; column < 4; ++column) { + if(!json.HasMember(names[column]) || !json[names[column]].IsArray()) return; + const rapidjson::Value& array = json[names[column]]; + if(array.Empty()) return; + for(rapidjson::SizeType i = 0; i < array.Size(); ++i) { + if(!array[i].IsNumber() || !qIsFinite(array[i].GetDouble())) return; + const double value = array[i].GetDouble(); + if((column == 0 && (value < 0.0 || (i > 0 && value < values[column]->last()))) || + (column == 2 && value <= 0.0) || (column == 3 && value < 0.0)) return; + values[column]->append(value); + } + } + if(result->times.size() != result->pressures.size() || result->durations.size() != result->rates.size()) return; + m_result = result; + } } diff --git a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp index 889936ae..54a07f3f 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp @@ -51,6 +51,7 @@ #include "nmDataAnalyzeManager.h" #include "nmPebiResultSnapshot.h" +#include "nmDataForecast.h" #include "nmDataDemo.h" #include "nmDataLogFile.h" #include "nmDataMeasure.h" @@ -1953,18 +1954,41 @@ void nmSubWndMain::onAuxiliarySolverTaskDestroyed() m_pProgressDlg->hide(); } -void nmSubWndMain::onShowForecastResult(const QString& wellName, const QVector& times, - const QVector& pressures, const QVector& durations, - const QVector& rates) +void nmSubWndMain::onForecastDataChanged() +{ + // 输入编辑和生成结果都需要参与框架的项目未保存提示。 + if(sender() == m_pResultRefreshDataManager.data() && m_pResultRefreshFitting) + m_pResultRefreshFitting->setModified(true); +} + +void nmSubWndMain::onShowForecastResult() { // 使用面板绑定的分析页面,切换活动分析不会改变预测结果的归属。 iSubWndFitting* fitting = m_pResultRefreshFitting.data(); nmDataAnalyzeManager* manager = m_pResultRefreshDataManager.data(); if(!fitting || !fitting->m_pMdiArea || !manager || - nmDataAnalyzeManager::findManagerByFitting(fitting) != manager || - !manager->isPebiResultSnapshotCurrent()) return; + nmDataAnalyzeManager::findManagerByFitting(fitting) != manager) return; ZxMdiArea* area = fitting->m_pMdiArea; QMdiSubWindow* window = area->findChild("nmForecastResult"); + const QSharedPointer result = manager->getForecastData() + ? manager->getForecastData()->getResult() : QSharedPointer(); + const QSharedPointer snapshot = manager->getPebiResultSnapshot(); + if(result.isNull() || snapshot.isNull() || + result->snapshotId != snapshot->getSnapshotId() || + result->wellInstanceId != manager->getCurrentResultWellInstanceId()) { + // 基础结果或结果井切换后清除旧窗口;模型参数编辑不影响上次预测的显示。 + if(window) { + foreach(iGuiPlot* plot, window->findChildren()) clearResultCurveDataTables(plot); + area->removeSubWindow(window); + delete window; + if(area->isVisible()) area->relayoutSubWnds(); + } + return; + } + const QVector& times = result->times; + const QVector& pressures = result->pressures; + const QVector& durations = result->durations; + const QVector& rates = result->rates; const bool created = window == nullptr; if(created) { // 结果页面由 MDI 承载,关闭预测输入对话框不销毁结果。 @@ -2009,7 +2033,8 @@ void nmSubWndMain::onShowForecastResult(const QString& wellName, const QVectorsetObjectName("nmForecastResult"); window->setAttribute(Qt::WA_DeleteOnClose, false); } - window->setWindowTitle(tr("Forecast - %1").arg(wellName)); + // 与预测输入窗口共用标题译文,不附加井名。 + window->setWindowTitle(nmWxForecast::tr("Forecast")); // 压力直接取求解器结果;阶梯产量及累计量按本次输入制度精确构造。 QVector rateTimes, rateValues, cumulativeTimes, cumulativeValues; @@ -2146,20 +2171,6 @@ void nmSubWndMain::syncForecastTimeAxis(double minimum, double maximum, bool rec target->m_pPlot->update(); } -void nmSubWndMain::clearForecastResult() -{ - // 由数据管理器通知主界面清场,不依赖预测输入对话框是否仍然打开。 - iSubWndFitting* fitting = m_pResultRefreshFitting.data(); - if(sender() != m_pResultRefreshDataManager.data() || !fitting || !fitting->m_pMdiArea) return; - QMdiSubWindow* window = fitting->m_pMdiArea->findChild("nmForecastResult"); - if(window) { - fitting->m_pMdiArea->removeSubWindow(window); - delete window; - // 直接移除窗口不会经过框架的关闭流程,清场后按当前布局重排其余窗口。 - if(fitting->m_pMdiArea->isVisible()) fitting->m_pMdiArea->relayoutSubWnds(); - } -} - void nmSubWndMain::onShowSensitiveResults() { // 使用固定所属分析的窗口与数据,不能把结果画到当前活动的其他分析中。 diff --git a/Src/nmNum/nmSubWnd/nmSubWndUtils.cpp b/Src/nmNum/nmSubWnd/nmSubWndUtils.cpp index 7b13e9be..faa7ce95 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndUtils.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndUtils.cpp @@ -483,8 +483,6 @@ void nmSubWndUtils::setupNumericalDockWidgets(nmDataAnalyzeManager* pDataManager pSubWndMain, SLOT(onGenerateButtonClicked())); connect(pAnalWx, SIGNAL(sigForecastTaskReady(nmWxForecast*,nmCalculationDllPebiSolverTask*)), pSubWndMain, SLOT(onForecastTaskReady(nmWxForecast*,nmCalculationDllPebiSolverTask*))); - connect(pAnalWx, SIGNAL(sigForecastReady(QString,QVector,QVector,QVector,QVector)), - pSubWndMain, SLOT(onShowForecastResult(QString,QVector,QVector,QVector,QVector))); connect(pAnalWx, SIGNAL(sigSensitivityTaskReady(nmWxSensitive*,nmCalculationDllPebiSolverTask*)), pSubWndMain, SLOT(onSensitivityTaskReady(nmWxSensitive*,nmCalculationDllPebiSolverTask*))); connect(pAnalWx, SIGNAL(sigResultWellChanged(QString)), @@ -492,11 +490,13 @@ void nmSubWndUtils::setupNumericalDockWidgets(nmDataAnalyzeManager* pDataManager // 手工求解和自动拟合都发布同一种正式快照,统一复用主窗口结果刷新逻辑。 pSubWndMain->bindPebiResultRefreshContext(pDataManager, pSubWndFitting); - // 输入窗口关闭后,主界面仍负责清理因模型变化而失效的预测。 + // 预测发布和失效统一由数据管理器通知,加载时也复用同一绘图入口。 + connect(pDataManager, SIGNAL(sigForecastDataChanged()), + pSubWndMain, SLOT(onForecastDataChanged()), Qt::UniqueConnection); + connect(pDataManager, SIGNAL(sigForecastResultChanged()), + pSubWndMain, SLOT(onShowForecastResult()), Qt::UniqueConnection); connect(pDataManager, SIGNAL(sigPebiResultSnapshotChanged()), - pSubWndMain, SLOT(clearForecastResult()), Qt::UniqueConnection); - connect(pDataManager, SIGNAL(sigPebiResultStateChanged()), - pSubWndMain, SLOT(clearForecastResult()), Qt::UniqueConnection); + pSubWndMain, SLOT(onShowForecastResult()), Qt::UniqueConnection); connect(pDataManager, SIGNAL(sigSensitiveResultsChanged()), pSubWndMain, SLOT(onShowSensitiveResults()), static_cast(Qt::QueuedConnection | Qt::UniqueConnection)); @@ -511,6 +511,7 @@ void nmSubWndUtils::setupNumericalDockWidgets(nmDataAnalyzeManager* pDataManager // 成果加载和首次创建面板共用入口,有已保存的敏感性结果时恢复同一子窗口。 if(pDataManager->getSensitiveData() && !pDataManager->getSensitiveData()->getResults().isNull()) pSubWndMain->onShowSensitiveResults(); + pSubWndMain->onShowForecastResult(); } bool nmSubWndUtils::fillNmDockWxs(iSubWnd* pSubWnd) diff --git a/Src/nmNum/nmSubWxs/nmWxForecast.cpp b/Src/nmNum/nmSubWxs/nmWxForecast.cpp index 0332098d..b6501a9e 100644 --- a/Src/nmNum/nmSubWxs/nmWxForecast.cpp +++ b/Src/nmNum/nmSubWxs/nmWxForecast.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "nmCalculationDllPebiSolverTask.h" #include "nmGUIComponentLineEdit.h" @@ -35,7 +36,8 @@ nmWxForecast::nmWxForecast(QWidget *parent, nmDataAnalyzeManager* manager) { setWindowTitle(tr("Forecast")); setFixedSize(520, 560); - setAttribute(Qt::WA_DeleteOnClose); + // 关闭时只隐藏,所属设计面板复用同一个输入窗口。 + setAttribute(Qt::WA_DeleteOnClose, false); PressureRow tempDefaults; m_currentDurationUnit = tempDefaults.duration.getUnit(); @@ -53,6 +55,19 @@ nmWxForecast::nmWxForecast(QWidget *parent, nmDataAnalyzeManager* manager) // 根据加载的数据更新界面 updateParametersForOption(); + // 初始化完成后再同步编辑,避免加载项目时被标记为修改。 + connect(m_rateTableWidget, SIGNAL(cellChanged(int,int)), this, SLOT(saveDataToManager())); + connect(m_rDurationUnitCombo, SIGNAL(currentIndexChanged(QString)), this, SLOT(saveDataToManager())); + connect(m_rRateUnitCombo, SIGNAL(currentIndexChanged(QString)), this, SLOT(saveDataToManager())); + const QList edits = m_initialPressureComponent->findChildren(); + foreach(QLineEdit* edit, edits) + connect(edit, SIGNAL(textEdited(QString)), this, SLOT(saveDataToManager())); + const QList units = m_initialPressureComponent->findChildren(); + foreach(QComboBox* unit, units) { + const int index = unit->findData(m_forecastData.getMultipleRatesData().pressureUnit); + if(index >= 0) unit->setCurrentIndex(index); + connect(unit, SIGNAL(currentIndexChanged(QString)), this, SLOT(saveDataToManager())); + } if(m_manager) { connect(m_manager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(onModelChanged())); connect(m_manager, SIGNAL(sigPebiResultStateChanged()), this, SLOT(onModelChanged())); @@ -72,8 +87,12 @@ nmWxForecast::~nmWxForecast() void nmWxForecast::loadDataFromManager() { - // 当前先开放分段产量,初始压力默认取已生成模型,输入只保留在内存中。 + // 优先恢复已保存的输入,首次打开时初始压力取已生成模型。 if(m_manager) m_forecastData = m_manager->getForecastDataCopy(); + m_forecastData.setResult(QSharedPointer()); + const MultipleRates& data = m_forecastData.getMultipleRatesData(); + if(m_rDurationUnitCombo->findText(data.durationUnit) >= 0) m_currentDurationUnit = data.durationUnit; + if(m_rRateUnitCombo->findText(data.rateUnit) >= 0) m_currentRateUnit = data.rateUnit; nmDataAttribute& pressure = m_forecastData.getMultipleRatesData().getInitialPressure(); if(m_manager && m_forecastData.getForecastOption() != nmDataForecast::MultipleRatesOption) { const QSharedPointer solved = m_manager->getPebiResultSnapshot(); @@ -95,6 +114,10 @@ void nmWxForecast::saveDataToManager() // 先从界面更新数据 updateDataFromUI(); + const QList units = m_initialPressureComponent->findChildren(); + if(!units.isEmpty()) + m_forecastData.getMultipleRatesData().pressureUnit = units.first()->itemData(units.first()->currentIndex()).toString(); + // 保存到数据中心 nmDataAnalyzeManager* pManager = m_manager.data(); @@ -842,7 +865,7 @@ void nmWxForecast::updateUIFromData() FlowRow tempDefaults; // 用于获取单位列表和默认值 int dataSize = tableData.size(); - int displayRowCount = (dataSize == 0) ? 1 : dataSize; + int displayRowCount = (dataSize == 0 && (!m_manager || !m_manager->getForecastData())) ? 1 : dataSize; // 设置行数: 1(单位) + N(数据) + 1(空白) m_rateTableWidget->setRowCount(1 + displayRowCount + 1); @@ -866,11 +889,14 @@ void nmWxForecast::updateUIFromData() QString baseRateUnit = rowData.q0.getUnit(); double displayRateValue = tempDefaults.q0.convertValueByUnitType(baseRateValue, baseRateUnit, m_currentRateUnit); - QTableWidgetItem* durationItem = new QTableWidgetItem(QString::number(displayDurValue, 'g', 15)); + // 同单位直接恢复原输入文本,半空行不能补成默认数值。 + QTableWidgetItem* durationItem = new QTableWidgetItem(baseDurUnit == m_currentDurationUnit && rowData.duration.getValue().type() == QVariant::String + ? rowData.duration.getValue().toString() : QString::number(displayDurValue, 'g', 15)); durationItem->setTextAlignment(Qt::AlignCenter); m_rateTableWidget->setItem(tableRow, 0, durationItem); - QTableWidgetItem* rateItem = new QTableWidgetItem(QString::number(displayRateValue, 'g', 15)); + QTableWidgetItem* rateItem = new QTableWidgetItem(baseRateUnit == m_currentRateUnit && rowData.q0.getValue().type() == QVariant::String + ? rowData.q0.getValue().toString() : QString::number(displayRateValue, 'g', 15)); rateItem->setTextAlignment(Qt::AlignCenter); m_rateTableWidget->setItem(tableRow, 1, rateItem); } @@ -959,10 +985,9 @@ void nmWxForecast::updateDataFromUI() QVector& tableData = m_forecastData.getMultipleRatesData().getFlowTable(); tableData.clear(); - FlowRow tempDefaults; - // 获取数据模型的基础单位 - QString baseDurUnit = tempDefaults.duration.getUnit(); - QString baseRateUnit = tempDefaults.q0.getUnit(); + // 保留输入单位和未填完的单元格,只有生成时才校验并换算给求解器。 + m_forecastData.getMultipleRatesData().durationUnit = m_currentDurationUnit; + m_forecastData.getMultipleRatesData().rateUnit = m_currentRateUnit; // 循环到 rowCount() - 1 来跳过最后那个空白行 for(int i = 1; i < m_rateTableWidget->rowCount() - 1; ++i) { @@ -977,19 +1002,11 @@ void nmWxForecast::updateDataFromUI() continue; } - FlowRow newRow; // newRow 将使用基础单位 - - if(!durationEmpty) { - double displayVal = durationItem->text().toDouble(); - // 转换值:从 "当前显示的单位" 转换回 "数据的基础单位" - double storageVal = tempDefaults.duration.convertValueByUnitType(displayVal, m_currentDurationUnit, baseDurUnit); - newRow.duration.setValue(storageVal); - } - if(!rateEmpty) { - double displayVal = rateItem->text().toDouble(); - double storageVal = tempDefaults.q0.convertValueByUnitType(displayVal, m_currentRateUnit, baseRateUnit); - newRow.q0.setValue(storageVal); - } + FlowRow newRow; + newRow.duration.setUnit(m_currentDurationUnit); + newRow.q0.setUnit(m_currentRateUnit); + newRow.duration.setValue(durationItem ? durationItem->text().trimmed() : QString()); + newRow.q0.setValue(rateItem ? rateItem->text().trimmed() : QString()); tableData.append(newRow); } } @@ -1033,8 +1050,7 @@ void nmWxForecast::onGenerateClicked() QMessageBox::warning(this, tr("Forecast"), tr("The target well is unavailable.")); return; } - m_wellName = well->m_sWellName; - m_task = new nmCalculationDllPebiSolverTask(QString(), m_manager, m_wellName, this); + m_task = new nmCalculationDllPebiSolverTask(QString(), m_manager, well->m_sWellName, this); if(!m_task->configureRateForecast(pi, durations, rates)) { error = m_task->getFailureReason(); delete m_task; @@ -1046,6 +1062,7 @@ void nmWxForecast::onGenerateClicked() m_durations = durations; m_rates = rates; m_sourceSnapshotId = solved->getSnapshotId(); + m_sourceWellInstanceId = well->m_sWellInstanceId; // 与敏感性分析一样,主窗口接管成功后才启动,忙碌时释放未启动任务。 emit sigForecastTaskReady(this, m_task); if(!m_calculationRunning) { @@ -1132,7 +1149,8 @@ void nmWxForecast::onForecastFinished() const bool cancelled = m_task->wasCancelled(); const bool current = m_manager && m_manager->isPebiResultSnapshotCurrent() && !m_task->hasInputChanged(m_manager) && - m_manager->getPebiResultSnapshot()->getSnapshotId() == m_sourceSnapshotId; + m_manager->getPebiResultSnapshot()->getSnapshotId() == m_sourceSnapshotId && + m_manager->getCurrentResultWellInstanceId() == m_sourceWellInstanceId; const bool success = m_task->wasSuccessful(); const QString error = m_task->getFailureReason(); const QVector > pressure = m_task->getRateForecastPressure(); @@ -1141,12 +1159,27 @@ void nmWxForecast::onForecastFinished() m_calculationRunning = false; m_parametersGroup->setEnabled(true); m_generateButton->setEnabled(true); + // 取消或失败后恢复同一份输入,方便直接调整后重算。 + if(cancelled || !current || !success) { + show(); + raise(); + activateWindow(); + } if(cancelled) return; if(!current || !success) { QMessageBox::warning(this, tr("Forecast"), current ? error : tr("The model changed. Generate the forecast again.")); return; } - emit sigForecastReady(m_wellName, pressure[0], pressure[1], m_durations, m_rates); + QSharedPointer result(new nmDataForecast::Result); + result->snapshotId = m_sourceSnapshotId; + result->wellInstanceId = m_sourceWellInstanceId; + result->times = pressure[0]; + result->pressures = pressure[1]; + result->durations = m_durations; + result->rates = m_rates; + m_forecastData.setResult(result); + m_manager->publishForecastResult(m_forecastData); + m_forecastData.setResult(QSharedPointer()); } void nmWxForecast::onModelChanged() diff --git a/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp b/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp index cfbcc73f..386e2740 100644 --- a/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp +++ b/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp @@ -1448,12 +1448,15 @@ void nmWxNumericalDesign::onForecastClicked() // 预测固定绑定当前分析窗口,并要求先生成基础模型。 updateSensitiveButtonEnabled(); if(!m_pForecastBtn->isEnabled()) return; - nmWxForecast* dlg = new nmWxForecast(this, m_pDataManager); - connect(dlg, SIGNAL(sigForecastTaskReady(nmWxForecast*,nmCalculationDllPebiSolverTask*)), - this, SIGNAL(sigForecastTaskReady(nmWxForecast*,nmCalculationDllPebiSolverTask*))); - connect(dlg, SIGNAL(sigForecastReady(QString,QVector,QVector,QVector,QVector)), - this, SIGNAL(sigForecastReady(QString,QVector,QVector,QVector,QVector))); - dlg->show(); + // 关闭和生成后均保留输入窗口,只在首次使用时创建并连接任务信号。 + if(m_pForecastDialog.isNull()) { + m_pForecastDialog = new nmWxForecast(this, m_pDataManager); + connect(m_pForecastDialog, SIGNAL(sigForecastTaskReady(nmWxForecast*,nmCalculationDllPebiSolverTask*)), + this, SIGNAL(sigForecastTaskReady(nmWxForecast*,nmCalculationDllPebiSolverTask*))); + } + m_pForecastDialog->show(); + m_pForecastDialog->raise(); + m_pForecastDialog->activateWindow(); } void nmWxNumericalDesign::updateSensitiveButtonEnabled()