feat(nmNum): 实现单井单相分段产量预测及主界面结果展示

feature/Sensitivity-20261012
lvjunjie 3 days ago
parent cc9f00eaac
commit 280c995d0f

@ -78,6 +78,11 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread {
/** @brief 返回自动拟合目标井的原始压力结果副本。 */
QVector<QVector<double> > getAutoFitResultPressure() const;
/** @brief 启动前设置单井单相预测制度;单位为 MPa、hr、m^3/D,仅修改任务副本。 */
bool configureRateForecast(double initialPressure, const QVector<double>& durations,
const QVector<double>& rates);
/** @brief 线程结束后读取预测时间(hr)和井底压力(MPa),不生成试井诊断曲线。 */
QVector<QVector<double> > getRateForecastPressure() const;
/** @brief 返回自动拟合目标井的双对数结果副本。 */
QVector<QVector<double> > getAutoFitResultLogLog() const;
/** @brief 返回自动拟合目标井的半对数结果副本。 */

@ -91,6 +91,8 @@ class NM_SUB_WND_EXPORT nmSubWndMain : public iSubWndBaseFit {
protected:
bool eventFilter(QObject* watched, QEvent* event);
// 绑定图形信号
virtual void bindChartSignals(iGuiPlot* pWxPlot);
@ -168,6 +170,13 @@ class NM_SUB_WND_EXPORT nmSubWndMain : public iSubWndBaseFit {
void onSensitivityTaskDestroyed();
/** @brief 在框架已有的敏感性子窗口叠加显示本分析的双对数结果。 */
void onShowSensitiveResults();
// 预测结果复用所属分析内的 MDI 子窗口,模型变化时移除旧结果。
void onShowForecastResult(const QString& wellName, const QVector<double>& times,
const QVector<double>& pressures, const QVector<double>& durations,
const QVector<double>& rates);
void clearForecastResult();
void syncForecastTimeAxis(double minimum, double maximum, bool recalcTicks, bool adjustZoom);
void fitForecastViews();
void onSensitiveFilterChanged();
void onSensitiveShowClicked();
/** @brief 刷新主界面的误差直方图,修改分箱数时复用当前批次结果。 */

@ -13,17 +13,30 @@
#include <QStackedWidget>
#include <QTableWidget>
#include <QComboBox>
#include <QPointer>
#include "nmDataForecast.h"
#include "nmGUIComponentLineEdit.h"
#include "nmDataAnalyzeManager.h"
class nmCalculationDllPebiSolverTask;
class QProgressDialog;
class nmWxForecast : public iDlgBase
{
Q_OBJECT
public:
explicit nmWxForecast(QWidget *parent = 0);
explicit nmWxForecast(QWidget *parent = 0, nmDataAnalyzeManager* manager = 0);
~nmWxForecast();
signals:
// 计算完成后由所属分析主界面承载结果,输入窗口不持有结果窗口。
void sigForecastReady(const QString& wellName, const QVector<double>& times,
const QVector<double>& pressures, const QVector<double>& durations,
const QVector<double>& rates);
public slots:
void reject();
private slots:
// Forecast option slots
void onConstantPressureSelected();
@ -41,6 +54,8 @@ public:
void onLoadDataClicked();
void onGenerateClicked();
void onCancelClicked();
void onForecastFinished();
void onModelChanged();
// Table edit
void onPressureCellChanged(int row, int column);
@ -60,6 +75,7 @@ private:
void updateUIFromData();
void updateDataFromUI();
void saveDataToManager();
bool readRateSchedule(QVector<double>& durations, QVector<double>& rates, QString& error) const;
QWidget* buildExtEmptyPage(const QString& text = QString());
QWidget* buildExtRampPage();
@ -71,6 +87,13 @@ private:
private:
nmDataForecast m_forecastData;
QPointer<nmDataAnalyzeManager> m_manager; ///< 固定所属分析窗口,不跟随全局当前窗口。
nmCalculationDllPebiSolverTask* m_task;
QProgressDialog* m_progress;
QString m_sourceSnapshotId;
QString m_wellName;
QVector<double> m_durations; ///< 本次计算的分段时长(hr)。
QVector<double> m_rates; ///< 本次计算的分段产量(m^3/D)。
// Main layout
QVBoxLayout *m_mainLayout;

@ -70,6 +70,10 @@ public:
void activateCurrentInstance();
signals:
// 将预测结果转交给本面板所属的分析主界面。
void sigForecastReady(const QString& wellName, const QVector<double>& times,
const QVector<double>& pressures, const QVector<double>& durations,
const QVector<double>& rates);
void sigGenerateClicked(); // 点击生成按钮时发出的信号
// 敏感性批量任务交给所属主窗口,复用普通求解进度框。
void sigSensitivityTaskReady(nmWxSensitive* dialog, nmCalculationDllPebiSolverTask* task);

@ -90,6 +90,7 @@ struct nmPebiSolverInputSnapshot
m_bGridResultNeedsCommit(false),
m_bMayUseCachedGrid(false),
m_bAutoFitTargetOnly(false),
m_bRateForecast(false),
m_bResultMetadataCaptured(false)
{
}
@ -114,6 +115,7 @@ struct nmPebiSolverInputSnapshot
bool m_bGridResultNeedsCommit; ///< 完整求解成功后是否需在主线程提交新网格。
bool m_bMayUseCachedGrid; ///< 捕获时是否存在同版本网格缓存。
bool m_bAutoFitTargetOnly; ///< true 时只构造目标井临时曲线,不提交成果。
bool m_bRateForecast; ///< 产量预测只返回原始压力,不计算双对数和半对数。
bool m_bResultMetadataCaptured; ///< 正式结果元数据是否已在主线程完整冻结。
};
@ -1795,6 +1797,90 @@ bool nmCalculationDllPebiSolverTask::captureInputSnapshot(
return true;
}
bool nmCalculationDllPebiSolverTask::configureRateForecast(
double initialPressure, const QVector<double>& durations,
const QVector<double>& rates)
{
// 使用已有模型的任务副本,从零时刻重新施加制度,绝不改写正式模型及历史数据。
if(isRunning() || !m_bInputSnapshotValid || m_pInputSnapshot == NULL ||
!m_pInputSnapshot->m_bAutoFitTargetOnly) {
return fail(tr("Forecast input is unavailable."));
}
m_bInputSnapshotValid = false;
if(m_pDataManager == NULL || !m_pDataManager->isPebiResultSnapshotCurrent() ||
hasInputChanged(m_pDataManager)) {
return fail(tr("Generate the current model before forecasting."));
}
nmPebiGridInputSnapshot& grid = m_pInputSnapshot->m_oGridInput;
nmDataBinaryTools::NM_PEBI_SCENE& scene = grid.m_oScene;
const int model = scene.solverType;
if(model < SMT_Oil_ConstPvt || model > SMT_Gas_PseudoPressure) {
return fail(tr("Rate forecast currently supports single-phase models only."));
}
int target = -1;
int wellCount = 0;
for(int i = 0; i < grid.m_vecWellInputs.size(); ++i) {
const nmPebiWellInputSnapshot& well = grid.m_vecWellInputs[i];
if(!well.m_bRealWell) continue;
++wellCount;
if(well.m_sWellCode == m_sAutoFitTargetWellCode && well.m_bRateControlled) target = i;
}
if(wellCount != 1 || target < 0) {
return fail(tr("Rate forecast currently requires one rate-controlled well."));
}
if(!isFiniteSolverNumber(initialPressure) || initialPressure <= 0.0 ||
durations.isEmpty() || durations.size() != rates.size()) {
return fail(tr("Enter a positive initial pressure and a complete rate schedule."));
}
double totalTime = 0.0;
double cumulative = 0.0;
for(int i = 0; i < durations.size(); ++i) {
const double nextTime = totalTime + durations[i];
cumulative += rates[i] * durations[i] / 24.0;
if(!isFiniteSolverNumber(durations[i]) || durations[i] <= 0.0 ||
!isFiniteSolverNumber(rates[i]) || rates[i] < 0.0 ||
!isFiniteSolverNumber(nextTime) || nextTime <= totalTime ||
!isFiniteSolverNumber(cumulative)) {
return fail(tr("Each duration must be positive and each production rate non-negative."));
}
totalTime = nextTime;
}
// 当前 DLL 除油单相常量 PVT 外,单段制度会抛出 vector<T> too long。
// 拆为两个同产量的等长段,仅满足 DLL 输入要求,不改变用户的生产制度。
QVector<double> solverDurations = durations;
QVector<double> solverRates = rates;
if(durations.size() == 1 && model != SMT_Oil_ConstPvt) {
solverDurations[0] = durations[0] / 2.0;
solverDurations.append(durations[0] - solverDurations[0]);
solverRates.append(rates[0]);
}
// 外层数组保持网格槽位顺序;手工裂缝不施加流量,单相流量只填入对应相。
const int slotCount = grid.m_vecWellInputs.size();
scene.Rate.t.assign(slotCount, std::vector<double>());
scene.Rate.qo.assign(slotCount, std::vector<double>());
scene.Rate.qg.assign(slotCount, std::vector<double>());
scene.Rate.qw.assign(slotCount, std::vector<double>());
scene.Rate.t[target].assign(solverDurations.constBegin(), solverDurations.constEnd());
scene.Rate.qo[target].assign(solverRates.size(), 0.0);
scene.Rate.qg[target].assign(solverRates.size(), 0.0);
scene.Rate.qw[target].assign(solverRates.size(), 0.0);
std::vector<double>& phaseRates = model <= SMT_Oil_VariablePvt ? scene.Rate.qo[target]
: model <= SMT_Water_VariablePvt ? scene.Rate.qw[target] : scene.Rate.qg[target];
phaseRates.assign(solverRates.constBegin(), solverRates.constEnd());
scene.Base.Pi = initialPressure;
m_pInputSnapshot->m_bRateForecast = true;
m_bInputSnapshotValid = true;
return true;
}
QVector<QVector<double> > nmCalculationDllPebiSolverTask::getRateForecastPressure() const
{
// 沿用目标井临时结果容器,预测结果不进入正式成果提交路径。
return m_pInputSnapshot != NULL && m_pInputSnapshot->m_bRateForecast
? m_autoFitResultPressure : QVector<QVector<double> >();
}
bool nmCalculationDllPebiSolverTask::prepareManualInput()
{
if(m_pInputSnapshot == nullptr) {
@ -1911,7 +1997,8 @@ bool nmCalculationDllPebiSolverTask::execPebiMode()
}
const QAtomicInt* pCancelRequested =
m_pInputSnapshot->m_bAutoFitTargetOnly && m_pSensitivitySampling.isNull()
m_pInputSnapshot->m_bAutoFitTargetOnly && !m_pInputSnapshot->m_bRateForecast &&
m_pSensitivitySampling.isNull()
? NULL : &m_nCancelRequested;
// 第一步:清空本次任务自己的输出。旧成果仍保留在 DataManager 中,只有主线程
@ -2166,6 +2253,43 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult(
QVector<QVector<double>> vvecPressure;
QVector<QVector<double>> vvecLogLog;
QVector<QVector<double>> vvecSemiLog;
if(m_pInputSnapshot->m_bRateForecast) {
// 预测允许单段产量和全程关井,不依赖历史流动段或试井诊断曲线。
const nmPebiGridInputSnapshot& grid = m_pInputSnapshot->m_oGridInput;
int target = -1;
for(int i = 0; i < grid.m_vecWellInputs.size(); ++i) {
if(grid.m_vecWellInputs[i].m_bRealWell &&
grid.m_vecWellInputs[i].m_sWellCode == m_sAutoFitTargetWellCode) target = i;
}
if(target < 0 || target >= static_cast<int>(p1.pw.size()) || p1.t.empty() ||
p1.pw[target].size() != p1.t.size()) {
return fail(tr("The solver returned incomplete forecast pressure data."));
}
double endTime = 0.0;
for(size_t i = 0; i < grid.m_oScene.Rate.t[target].size(); ++i)
endTime += grid.m_oScene.Rate.t[target][i];
const double tolerance = qMax(1.0, endTime) * 1.0e-8;
QVector<QVector<double> > pressure(2);
if(p1.t[0] > 0.0) {
pressure[0].append(0.0);
pressure[1].append(grid.m_oScene.Base.Pi);
}
for(size_t i = 0; i < p1.t.size(); ++i) {
if(isCancelRequested()) return false;
if(!isFiniteSolverNumber(p1.t[i]) || p1.t[i] < 0.0 ||
p1.t[i] > endTime + tolerance || (i > 0 && p1.t[i] <= p1.t[i - 1]) ||
!isFiniteSolverNumber(p1.pw[target][i]) || p1.pw[target][i] <= 0.0) {
return fail(tr("Invalid forecast pressure or time. Check the rate schedule and model."));
}
pressure[0].append(p1.t[i]);
pressure[1].append(p1.pw[target][i]);
}
if(qAbs(p1.t.back() - endTime) > tolerance) {
return fail(tr("The solver did not reach the end of the forecast schedule."));
}
m_autoFitResultPressure = pressure;
return true;
}
// p1.pw 是求解器返回的原始井底压力, 后续仍按 MPa 保存到 vvecPressure.
// 只有气单相变化 PVT 模型的双对数和半对数计算需要改用拟压力.
const bool usePseudoPressure = (modelType == static_cast<int>(SMT_Gas_VariablePvt));

@ -1901,6 +1901,166 @@ void nmSubWndMain::onSensitivityTaskDestroyed()
m_pProgressDlg->hide();
}
void nmSubWndMain::onShowForecastResult(const QString& wellName, const QVector<double>& times,
const QVector<double>& pressures, const QVector<double>& durations,
const QVector<double>& rates)
{
// 使用面板绑定的分析页面,切换活动分析不会改变预测结果的归属。
iSubWndFitting* fitting = m_pResultRefreshFitting.data();
nmDataAnalyzeManager* manager = m_pResultRefreshDataManager.data();
if(!fitting || !fitting->m_pMdiArea || !manager ||
nmDataAnalyzeManager::findManagerByFitting(fitting) != manager ||
!manager->isPebiResultSnapshotCurrent()) return;
ZxMdiArea* area = fitting->m_pMdiArea;
QMdiSubWindow* window = area->findChild<QMdiSubWindow*>("nmForecastResult");
const bool created = window == nullptr;
if(created) {
// 结果页面由 MDI 承载,关闭预测输入对话框不销毁结果。
QWidget* page = new QWidget;
QVBoxLayout* layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
QSplitter* charts = new QSplitter(Qt::Vertical, page);
charts->setObjectName("nmForecastCharts");
charts->setChildrenCollapsible(false);
layout->addWidget(charts);
// 布局完成后统一适配画布,避免小窗口下两个绘图区缩放比例不同。
charts->installEventFilter(this);
connect(charts, SIGNAL(splitterMoved(int,int)), this, SLOT(fitForecastViews()), Qt::QueuedConnection);
for(int i = 0; i < 2; ++i) {
iGuiPlot* plot = new iGuiPlot(true, charts);
plot->setObjectName(i == 0 ? "nmForecastRate" : "nmForecastPressure");
plot->initUI(i == 0 ? tr("Production rate") : tr("Bottomhole pressure"));
plot->m_pPlotView->setSimpleMode(true);
plot->setDropEnabled(false);
plot->setAxisX(tr("Time"), "hr");
plot->setAxisY(i == 0 ? tr("Production rate") : tr("Bottomhole pressure"), i == 0 ? "m^3/D" : "MPa");
plot->setAxisY(i == 0 ? tr("Cumulative production") : QString(), i == 0 ? "m^3" : "", true);
for(int axis = 0; axis < 3; ++axis) plot->setAxisXYLog(false, axis);
plot->getAxisOf(2)->setVisible(true);
// 下图右侧保留空轴位,确保两张图的时间刻度在水平方向对齐。
if(i == 1) {
plot->getAxisOf(2)->setColor(Qt::transparent);
}
plot->m_pPlot->setBKColor(Qt::white);
plot->m_pPlot->setLegendVisible(false);
charts->addWidget(plot);
charts->setStretchFactor(i, 1);
connect(plot->getAxisOf(0), SIGNAL(sigRangeChanged(double,double,bool,bool)),
this, SLOT(syncForecastTimeAxis(double,double,bool,bool)));
}
window = area->addSubWindow(page);
window->setObjectName("nmForecastResult");
window->setAttribute(Qt::WA_DeleteOnClose, false);
}
window->setWindowTitle(tr("Forecast - %1").arg(wellName));
// 压力直接取求解器结果;阶梯产量及累计量按本次输入制度精确构造。
QVector<double> rateTimes, rateValues, cumulativeTimes, cumulativeValues;
double time = 0.0, cumulative = 0.0;
cumulativeTimes.append(0.0);
cumulativeValues.append(0.0);
for(int i = 0; i < durations.size(); ++i) {
rateTimes << time << time + durations[i];
rateValues << rates[i] << rates[i];
time += durations[i];
cumulative += rates[i] * durations[i] / 24.0;
cumulativeTimes.append(time);
cumulativeValues.append(cumulative);
}
const QStringList names = QStringList() << tr("Bottomhole pressure") << tr("Production rate") << tr("Cumulative production");
QVector<iCurveDesc> curves;
for(int i = 0; i < 3; ++i) {
iCurveDesc curve;
curve.m_sCurveName = names[i];
curve.m_oCurveType = POT_CurveDiscrete;
curve.m_bLineVisible = true;
curve.m_bPointVisible = false;
curve.m_bUseY2 = i == 2;
curve.m_oPen = QPen(i == 0 ? Qt::red : i == 1 ? Qt::darkGreen : Qt::gray, 0.35);
curve.m_vecX = i == 0 ? times : i == 1 ? rateTimes : cumulativeTimes;
curve.m_vecY = i == 0 ? pressures : i == 1 ? rateValues : cumulativeValues;
curves.append(curve);
}
iGuiPlot* plots[] = {window->findChild<iGuiPlot*>("nmForecastRate"),
window->findChild<iGuiPlot*>("nmForecastPressure")};
for(int i = 0; i < 2; ++i) {
iGuiPlot* plot = plots[i];
// 重复生成只替换曲线,清理旧图元和撤销记录,保留上下图窗口。
for(int j = plot->m_pPlot->getObjCount() - 1; j >= 0; --j)
if(qobject_cast<ZxObjCurve*>(plot->m_pPlot->getObjByIndex(j))) plot->m_pPlot->removeObjByIndex(j);
plot->clearUndoStack();
plot->setCurveData(i == 0 ? curves.mid(1, 2) : curves.mid(0, 1));
plot->runUpdate();
plot->setReadOnly(true);
plot->freshAxisScales(true, true);
// 自动定标会压缩没有曲线的右轴,下图需要保留这块空白以对齐上图。
plot->getAxisOf(2)->setMinimizeAxis(false);
// 两张图都以完整预测时长为基准,缩放复原时也回到同一时间范围。
VecDouble ticks;
for(int tick = 0; tick <= 5; ++tick) ticks.append(time * tick / 5.0);
plot->setAxisScaleX(0.0, time, ticks);
// 时间刻度及标题只在下图显示,上图仍保留横轴数据与同步关系。
plot->getAxisOf(0)->setMinimizeAxis(i == 0);
}
const bool restore = window->isHidden();
window->widget()->show();
window->show();
// 隐藏的分析页交给框架显示事件布局,避免此时重排把结果重新隐藏。
if((created || restore) && area->isVisible()) area->relayoutSubWnds();
area->setActiveSubWindow(window);
QTimer::singleShot(0, this, SLOT(fitForecastViews()));
}
bool nmSubWndMain::eventFilter(QObject* watched, QEvent* event)
{
// 子窗口缩放、重新显示均在内部控件完成尺寸更新后再适配。
if(watched->objectName() == "nmForecastCharts" &&
(event->type() == QEvent::Resize || event->type() == QEvent::Show))
QTimer::singleShot(0, this, SLOT(fitForecastViews()));
return iSubWndBaseFit::eventFilter(watched, event);
}
void nmSubWndMain::fitForecastViews()
{
// 只适配图形画布的显示尺寸,不改变用户选定的时间和压力范围。
iSubWndFitting* fitting = m_pResultRefreshFitting.data();
if(!fitting || !fitting->m_pMdiArea) return;
QMdiSubWindow* window = fitting->m_pMdiArea->findChild<QMdiSubWindow*>("nmForecastResult");
if(!window) return;
foreach(iGuiPlot* plot, window->findChildren<iGuiPlot*>())
plot->m_pPlotView->setZoomMode(ZxRenderView::eZoomFitWindow, true);
}
void nmSubWndMain::syncForecastTimeAxis(double minimum, double maximum, bool recalcTicks, bool adjustZoom)
{
// 只同步本分析预测图的横轴;阻断回传,避免两张图相互触发范围更新。
iSubWndFitting* fitting = m_pResultRefreshFitting.data();
if(!fitting || !fitting->m_pMdiArea) return;
QMdiSubWindow* window = fitting->m_pMdiArea->findChild<QMdiSubWindow*>("nmForecastResult");
if(!window) return;
iGuiPlot* rate = window->findChild<iGuiPlot*>("nmForecastRate");
iGuiPlot* pressure = window->findChild<iGuiPlot*>("nmForecastPressure");
if(!rate || !pressure) return;
iGuiPlot* target = sender() == rate->getAxisOf(0) ? pressure : rate;
ZxSubAxisBase* axis = target->getAxisOf(0);
const bool blocked = axis->blockSignals(true);
axis->setRangeMinMax(minimum, maximum, recalcTicks, adjustZoom);
axis->blockSignals(blocked);
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<QMdiSubWindow*>("nmForecastResult");
if(window) {
fitting->m_pMdiArea->removeSubWindow(window);
delete window;
}
}
void nmSubWndMain::onShowSensitiveResults()
{
// 使用固定所属分析的窗口与数据,不能把结果画到当前活动的其他分析中。

@ -481,6 +481,8 @@ void nmSubWndUtils::setupNumericalDockWidgets(nmDataAnalyzeManager* pDataManager
// 数值设计面板只发出操作请求,具体处理仍由主窗口完成。
connect(pAnalWx, SIGNAL(sigGenerateClicked()),
pSubWndMain, SLOT(onGenerateButtonClicked()));
connect(pAnalWx, SIGNAL(sigForecastReady(QString,QVector<double>,QVector<double>,QVector<double>,QVector<double>)),
pSubWndMain, SLOT(onShowForecastResult(QString,QVector<double>,QVector<double>,QVector<double>,QVector<double>)));
connect(pAnalWx, SIGNAL(sigSensitivityTaskReady(nmWxSensitive*,nmCalculationDllPebiSolverTask*)),
pSubWndMain, SLOT(onSensitivityTaskReady(nmWxSensitive*,nmCalculationDllPebiSolverTask*)));
connect(pAnalWx, SIGNAL(sigResultWellChanged(QString)),
@ -488,6 +490,11 @@ void nmSubWndUtils::setupNumericalDockWidgets(nmDataAnalyzeManager* pDataManager
// 手工求解和自动拟合都发布同一种正式快照,统一复用主窗口结果刷新逻辑。
pSubWndMain->bindPebiResultRefreshContext(pDataManager, pSubWndFitting);
// 输入窗口关闭后,主界面仍负责清理因模型变化而失效的预测。
connect(pDataManager, SIGNAL(sigPebiResultSnapshotChanged()),
pSubWndMain, SLOT(clearForecastResult()), Qt::UniqueConnection);
connect(pDataManager, SIGNAL(sigPebiResultStateChanged()),
pSubWndMain, SLOT(clearForecastResult()), Qt::UniqueConnection);
connect(pDataManager, SIGNAL(sigSensitiveResultsChanged()),
pSubWndMain, SLOT(onShowSensitiveResults()),
static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));

@ -4,9 +4,17 @@
#include <QSpacerItem>
#include <QSizePolicy>
#include <QLabel>
#include <QMessageBox>
#include <QProgressDialog>
#include <QtCore/qnumeric.h>
#include "nmCalculationDllPebiSolverTask.h"
#include "nmPebiResultSnapshot.h"
nmWxForecast::nmWxForecast(QWidget *parent)
nmWxForecast::nmWxForecast(QWidget *parent, nmDataAnalyzeManager* manager)
: iDlgBase(parent)
, m_manager(manager ? manager : nmDataAnalyzeManager::getCurrentInstance())
, m_task(NULL)
, m_progress(NULL)
, m_initialPressureComponent(nullptr)
, m_abandonmentRateComponent(nullptr)
, m_maximumRateComponent(nullptr)
@ -26,6 +34,7 @@ nmWxForecast::nmWxForecast(QWidget *parent)
{
setWindowTitle("Forecast");
setFixedSize(520, 560);
setAttribute(Qt::WA_DeleteOnClose);
PressureRow tempDefaults;
m_currentDurationUnit = tempDefaults.duration.getUnit();
@ -43,44 +52,41 @@ nmWxForecast::nmWxForecast(QWidget *parent)
// 根据加载的数据更新界面
updateParametersForOption();
if(m_manager) {
connect(m_manager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(onModelChanged()));
connect(m_manager, SIGNAL(sigPebiResultStateChanged()), this, SLOT(onModelChanged()));
connect(m_manager, SIGNAL(destroyed()), this, SLOT(close()));
}
}
nmWxForecast::~nmWxForecast()
{
// DLL 内部不能强行中断,析构前等待任务退出,避免悬空回调。
if(m_task) {
m_task->requestCancel();
m_task->wait();
delete m_task;
}
}
void nmWxForecast::loadDataFromManager()
{
// 从数据中心获取预测数据的副本
nmDataAnalyzeManager* pManager = nmDataAnalyzeManager::getCurrentInstance();
if(pManager) {
m_forecastData = pManager->getForecastDataCopy();
// 根据数据设置单选按钮
nmDataForecast::ForecastOption option = m_forecastData.getForecastOption();
switch(option) {
case nmDataForecast::ConstantPressureOption:
m_constantPressureRadio->setChecked(true);
break;
case nmDataForecast::DecliningPressureOption:
m_decliningPressureRadio->setChecked(true);
break;
case nmDataForecast::MultiplePressuresOption:
m_multiplePressuresRadio->setChecked(true);
break;
case nmDataForecast::MultipleRatesOption:
m_multipleRatesRadio->setChecked(true);
break;
// 当前先开放分段产量,初始压力默认取已生成模型,输入只保留在内存中。
if(m_manager) m_forecastData = m_manager->getForecastDataCopy();
nmDataAttribute& pressure = m_forecastData.getMultipleRatesData().getInitialPressure();
if(m_manager && m_forecastData.getForecastOption() != nmDataForecast::MultipleRatesOption) {
const QSharedPointer<const nmPebiResultSnapshot> solved = m_manager->getPebiResultSnapshot();
if(!solved.isNull()) {
pressure.setValue(pressure.convertValueByUnitType(
solved->getReservoirParameters().m_dInitialPressure, "MPa", pressure.getUnit()));
}
} else {
// 如果数据中心不存在,使用默认数据
m_constantPressureRadio->setChecked(true);
m_forecastData.setForecastOption(nmDataForecast::ConstantPressureOption);
}
// 本页使用绝对压力,避免通用单位控件把表压当作绝压换算。
pressure.setListUnitSelections(QStringList() << "MPa" << "psia" << "kPa" << "bara");
m_forecastData.setForecastOption(nmDataForecast::MultipleRatesOption);
m_multipleRatesRadio->blockSignals(true);
m_multipleRatesRadio->setChecked(true);
m_multipleRatesRadio->blockSignals(false);
}
void nmWxForecast::saveDataToManager()
@ -89,7 +95,7 @@ void nmWxForecast::saveDataToManager()
updateDataFromUI();
// 保存到数据中心
nmDataAnalyzeManager* pManager = nmDataAnalyzeManager::getCurrentInstance();
nmDataAnalyzeManager* pManager = m_manager.data();
if(pManager) {
pManager->updateForecastData(m_forecastData);
@ -108,6 +114,10 @@ void nmWxForecast::setupUI()
m_decliningPressureRadio = new QRadioButton("Declining pressure (ramp)", this);
m_multiplePressuresRadio = new QRadioButton("Multiple pressures", this);
m_multipleRatesRadio = new QRadioButton("Multiple rates", this);
// 求解器目前仅接通产量控制;压力控制保留入口但不允许生成。
m_constantPressureRadio->setEnabled(false);
m_decliningPressureRadio->setEnabled(false);
m_multiplePressuresRadio->setEnabled(false);
m_forecastButtonGroup = new QButtonGroup(this);
m_forecastButtonGroup->addButton(m_constantPressureRadio, 0);
@ -390,7 +400,7 @@ QWidget* nmWxForecast::buildExtMultiRatePage()
// 创建表格
m_rateTableWidget = new QTableWidget(0, 2);
// 根据截图设置表头
m_rateTableWidget->setHorizontalHeaderLabels(QStringList() << "Duration" << "Qo");
m_rateTableWidget->setHorizontalHeaderLabels(QStringList() << "Duration" << "Rate");
m_rateTableWidget->verticalHeader()->setVisible(false);
m_rateTableWidget->horizontalHeader()->setResizeMode(QHeaderView::Stretch);
@ -799,7 +809,7 @@ void nmWxForecast::updateUIFromData()
QString basePresUnit = rowData.pressure.getUnit();
double displayPresValue = tempDefaults.pressure.convertValueByUnitType(basePresValue, basePresUnit, m_currentPressureUnit);
QTableWidgetItem* durationItem = new QTableWidgetItem(QString::number(displayDurValue));
QTableWidgetItem* durationItem = new QTableWidgetItem(QString::number(displayDurValue, 'g', 15));
durationItem->setTextAlignment(Qt::AlignCenter);
m_pressureTableWidget->setItem(tableRow, 0, durationItem);
@ -855,11 +865,11 @@ void nmWxForecast::updateUIFromData()
QString baseRateUnit = rowData.q0.getUnit();
double displayRateValue = tempDefaults.q0.convertValueByUnitType(baseRateValue, baseRateUnit, m_currentRateUnit);
QTableWidgetItem* durationItem = new QTableWidgetItem(QString::number(displayDurValue));
QTableWidgetItem* durationItem = new QTableWidgetItem(QString::number(displayDurValue, 'g', 15));
durationItem->setTextAlignment(Qt::AlignCenter);
m_rateTableWidget->setItem(tableRow, 0, durationItem);
QTableWidgetItem* rateItem = new QTableWidgetItem(QString::number(displayRateValue));
QTableWidgetItem* rateItem = new QTableWidgetItem(QString::number(displayRateValue, 'g', 15));
rateItem->setTextAlignment(Qt::AlignCenter);
m_rateTableWidget->setItem(tableRow, 1, rateItem);
}
@ -992,22 +1002,138 @@ void nmWxForecast::onLoadDataClicked()
void nmWxForecast::onGenerateClicked()
{
// 确保最新的界面数据写回到模型
updateDataFromUI();
// 保存数据到数据中心
// 先校验原始输入,避免空单元格被旧的数据同步逻辑补成默认值。
if(m_task) return;
if(!m_manager || !m_manager->isPebiResultSnapshotCurrent()) {
QMessageBox::warning(this, tr("Forecast"), tr("Generate the current model before forecasting."));
return;
}
const QSharedPointer<const nmPebiResultSnapshot> solved = m_manager->getPebiResultSnapshot();
if(solved->getWellCount() != 1 || solved->getSolverSettings().m_nSolverModelType < 1 ||
solved->getSolverSettings().m_nSolverModelType > 6) {
QMessageBox::warning(this, tr("Forecast"), tr("Rate forecast currently supports single-well, single-phase models only."));
return;
}
QString error;
QVector<double> durations, rates;
if(!readRateSchedule(durations, rates, error)) {
QMessageBox::warning(this, tr("Forecast"), error);
return;
}
nmDataAttribute& pressure = m_forecastData.getMultipleRatesData().getInitialPressure();
if(!m_initialPressureComponent->updateValueToOrigin()) {
QMessageBox::warning(this, tr("Forecast"), tr("Enter a positive initial pressure."));
return;
}
const double pi = pressure.convertValueByUnitType(pressure.getValue().toDouble(), pressure.getUnit(), "MPa");
m_wellName = solved->getWellAt(0)->m_sWellName;
m_task = new nmCalculationDllPebiSolverTask(QString(), m_manager, m_wellName, this);
if(!m_task->configureRateForecast(pi, durations, rates)) {
error = m_task->getFailureReason();
delete m_task;
m_task = NULL;
QMessageBox::warning(this, tr("Forecast"), error);
return;
}
saveDataToManager();
// TODO: 使用数据中心的数据进行预测计算
accept();
m_durations = durations;
m_rates = rates;
m_sourceSnapshotId = solved->getSnapshotId();
m_parametersGroup->setEnabled(false);
m_generateButton->setEnabled(false);
m_progress = new QProgressDialog(tr("Calculating forecast..."), tr("Cancel"), 0, 0, this);
m_progress->setWindowTitle(tr("Forecast"));
m_progress->setWindowModality(Qt::ApplicationModal);
m_progress->setMinimumDuration(0);
m_progress->setAutoClose(false);
connect(m_progress, SIGNAL(canceled()), this, SLOT(onCancelClicked()));
connect(m_task, SIGNAL(finished()), this, SLOT(onForecastFinished()));
m_progress->show();
m_task->start();
}
void nmWxForecast::onCancelClicked()
{
// 关闭按钮和进度框共用协作式停止,不在 DLL 内部强制终止线程。
reject();
}
void nmWxForecast::reject()
{
// 正在求解时保留窗口,等线程结束后恢复输入,避免关闭操作阻塞界面。
if(m_task) {
m_task->requestCancel();
m_progress->setLabelText(tr("Stopping after the current solver call finishes..."));
m_progress->setCancelButton(NULL);
m_progress->show();
return;
}
iDlgBase::reject();
}
bool nmWxForecast::readRateSchedule(QVector<double>& durations, QVector<double>& rates, QString& error) const
{
// 求解器统一使用小时及标准体积产量,完整空行跳过,半空行必须补全。
durations.clear();
rates.clear();
FlowRow units;
for(int row = 1; row < m_rateTableWidget->rowCount(); ++row) {
const QTableWidgetItem* durationItem = m_rateTableWidget->item(row, 0);
const QTableWidgetItem* rateItem = m_rateTableWidget->item(row, 1);
const QString durationText = durationItem ? durationItem->text().trimmed() : QString();
const QString rateText = rateItem ? rateItem->text().trimmed() : QString();
if(durationText.isEmpty() && rateText.isEmpty()) continue;
bool durationOk = false, rateOk = false;
double duration = durationText.toDouble(&durationOk);
double rate = rateText.toDouble(&rateOk);
duration = units.duration.convertValueByUnitType(duration, m_currentDurationUnit, "hr");
rate = units.q0.convertValueByUnitType(rate, m_currentRateUnit, "m^3/D");
if(!durationOk || !rateOk || !qIsFinite(duration) || duration <= 0.0 ||
!qIsFinite(rate) || rate < 0.0) {
error = tr("Row %1: enter a positive duration and a non-negative production rate (0 for shut-in).").arg(row);
return false;
}
durations.append(duration);
rates.append(rate);
}
if(durations.isEmpty()) {
error = tr("Enter at least one duration and production rate.");
return false;
}
return true;
}
void nmWxForecast::onForecastFinished()
{
// finished 后才读取及销毁任务;输入变化或模型重新生成时丢弃本次预测。
m_task->wait();
const bool cancelled = m_task->wasCancelled() || m_progress->wasCanceled();
const bool current = m_manager && m_manager->isPebiResultSnapshotCurrent() &&
!m_task->hasInputChanged(m_manager) &&
m_manager->getPebiResultSnapshot()->getSnapshotId() == m_sourceSnapshotId;
const bool success = m_task->wasSuccessful();
const QString error = m_task->getFailureReason();
const QVector<QVector<double> > pressure = m_task->getRateForecastPressure();
delete m_task;
m_task = NULL;
delete m_progress;
m_progress = NULL;
m_parametersGroup->setEnabled(true);
m_generateButton->setEnabled(true);
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);
}
void nmWxForecast::onModelChanged()
{
// 模型或输入改变时停止本次计算;已显示的结果由所属主界面清理。
if(m_task) m_task->requestCancel();
}
void nmWxForecast::onPressureCellChanged(int row, int column)
{
Q_UNUSED(column);
@ -1131,10 +1257,12 @@ void nmWxForecast::onSharedDurationUnitChanged(const QString& unit)
QTableWidgetItem* dataItem = m_rateTableWidget->item(i, 0); // 第0列 (Duration)
if(dataItem && !dataItem->text().isEmpty()) {
double oldDisplayValue = dataItem->text().toDouble();
bool ok = false;
double oldDisplayValue = dataItem->text().toDouble(&ok);
if(!ok || !qIsFinite(oldDisplayValue)) continue;
// 转换值
double newDisplayValue = tempDefaults.duration.convertValueByUnitType(oldDisplayValue, oldUnit, unit);
dataItem->setText(QString::number(newDisplayValue));
dataItem->setText(QString::number(newDisplayValue, 'g', 15));
}
}
m_rateTableWidget->blockSignals(false);
@ -1170,10 +1298,12 @@ void nmWxForecast::onRateUnitChanged(const QString& unit)
QTableWidgetItem* dataItem = m_rateTableWidget->item(i, 1); // 第1列
if(dataItem && !dataItem->text().isEmpty()) {
double oldDisplayValue = dataItem->text().toDouble();
bool ok = false;
double oldDisplayValue = dataItem->text().toDouble(&ok);
if(!ok || !qIsFinite(oldDisplayValue)) continue;
// 使用 q0 (FlowRate) 进行单位转换
double newDisplayValue = tempDefaults.q0.convertValueByUnitType(oldDisplayValue, oldUnit, unit);
dataItem->setText(QString::number(newDisplayValue));
dataItem->setText(QString::number(newDisplayValue, 'g', 15));
}
}

@ -1445,7 +1445,12 @@ void nmWxNumericalDesign::onAutomaticFittingClicked()
void nmWxNumericalDesign::onForecastClicked()
{
nmWxForecast* dlg = new nmWxForecast();
// 预测固定绑定当前分析窗口,并要求先生成基础模型。
updateSensitiveButtonEnabled();
if(!m_pForecastBtn->isEnabled()) return;
nmWxForecast* dlg = new nmWxForecast(this, m_pDataManager);
connect(dlg, SIGNAL(sigForecastReady(QString,QVector<double>,QVector<double>,QVector<double>,QVector<double>)),
this, SIGNAL(sigForecastReady(QString,QVector<double>,QVector<double>,QVector<double>,QVector<double>)));
dlg->show();
}
@ -1457,6 +1462,8 @@ void nmWxNumericalDesign::updateSensitiveButtonEnabled()
const bool available = !solved.isNull();
m_pSensitiveBtn->setEnabled(available);
m_pAutomaticfittingBtn->setEnabled(available);
m_pForecastBtn->setEnabled(available);
m_pForecastBtn->setToolTip(available ? QString() : tr("Generate the model to enable forecast."));
m_pSensitiveBtn->setToolTip(available ? QString() : tr("Generate the model to enable sensitivity analysis."));
}

Loading…
Cancel
Save