feat(nmNum): 支持多井预测并完善结果窗口布局

- 替换当前井预测流量,复用普通多井求解并显示完整结果。
- 预测窗口清除及切换井关闭敏感性窗口后自动重新布局。
- 更新多井预测相关中文提示。
feature/Sensitivity-20261012
lvjunjie 2 days ago
parent 9ba4f2e0cc
commit 8ba378759d

@ -8421,8 +8421,8 @@ Average pressure in contour: %2 MPa</source>
<translation>产量预测目前仅支持单相模型。</translation>
</message>
<message>
<source>Rate forecast currently requires one rate-controlled well.</source>
<translation>产量预测目前要求模型中仅有一口井,且该井采用产量控制。</translation>
<source>The forecast well must be rate-controlled.</source>
<translation>当前预测井必须采用产量控制。</translation>
</message>
<message>
<source>Enter a positive initial pressure and a complete rate schedule.</source>
@ -8440,10 +8440,6 @@ Average pressure in contour: %2 MPa</source>
<source>Invalid forecast pressure or time. Check the rate schedule and model.</source>
<translation>预测压力或时间无效,请检查分段产量制度和模型。</translation>
</message>
<message>
<source>The solver did not reach the end of the forecast schedule.</source>
<translation>求解器未计算到预测制度的结束时刻。</translation>
</message>
</context>
<context>
<name>nmParameterField</name>
@ -8796,8 +8792,12 @@ Average pressure in contour: %2 MPa</source>
<translation>请先生成当前模型,再进行预测。</translation>
</message>
<message>
<source>Rate forecast currently supports single-well, single-phase models only.</source>
<translation>产量预测目前仅支持单井单相模型。</translation>
<source>Rate forecast currently supports single-phase models only.</source>
<translation>产量预测目前仅支持单相模型。</translation>
</message>
<message>
<source>The target well is unavailable.</source>
<translation>目标井不可用。</translation>
</message>
<message>
<source>Enter a positive initial pressure.</source>

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

@ -1812,15 +1812,13 @@ bool nmCalculationDllPebiSolverTask::configureRateForecast(
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(target < 0) {
return fail(tr("The forecast well must be rate-controlled."));
}
if(!isFiniteSolverNumber(initialPressure) || initialPressure <= 0.0 ||
durations.isEmpty() || durations.size() != rates.size()) {
@ -1849,12 +1847,7 @@ bool nmCalculationDllPebiSolverTask::configureRateForecast(
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);
@ -2252,28 +2245,22 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult(
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);
}
// 与普通多井求解一致,保留 DLL 返回的完整时间轴,不按当前井制度截断结果。
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]) ||
(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;
}

@ -342,6 +342,25 @@ void nmSubWndMain::onPebiResultSnapshotChanged()
const QString sSnapshotId = pSnapshot->getSnapshotId();
if(sSnapshotId == m_sObservedPebiResultSnapshotId) {
// 切换结果井时关闭上一口井的敏感性窗口及附属表格,保留批次数据供再次查看。
ZxMdiArea* area = pSubWndFitting->m_pMdiArea;
if(area) {
iGuiPlot* loglog = pSubWndFitting->getPlotByTag(s_Tag_ChartSensitivity);
bool closed = false;
foreach(QMdiSubWindow* window, area->subWindowList()) {
if(window->objectName() != "nmSensitivityHistory" &&
window->objectName() != "nmSensitivityHistogram" &&
!(loglog && window->isAncestorOf(loglog))) continue;
foreach(iGuiPlot* plot, window->findChildren<iGuiPlot*>())
clearResultCurveDataTables(plot);
foreach(QDialog* dialog, window->findChildren<QDialog*>()) {
if(dialog->isWindow()) dialog->close();
}
window->close();
closed = true;
}
if(closed && area->isVisible()) area->relayoutSubWnds();
}
// 同一快照内切换结果井仍由 onWellSelected() 处理,不重复刷新全部结果页面。
return;
}
@ -2051,10 +2070,11 @@ void nmSubWndMain::onShowForecastResult(const QString& wellName, const QVector<d
}
// 自动定标会压缩没有曲线的右轴,下图需要保留这块空白以对齐上图。
plot->getAxisOf(2)->setMinimizeAxis(false);
// 两张图共用时间范围,零时刻左侧留白,避免起始竖线与纵轴重叠。
// 两张图共用包含完整 DLL 结果的时间范围,多井计算可能长于当前井输入制度。
const double endTime = qMax(time, times.last());
VecDouble ticks;
for(int tick = 0; tick <= 5; ++tick) ticks.append(time * tick / 5.0);
plot->setAxisScaleX(-time * 0.05, time, ticks);
for(int tick = 0; tick <= 5; ++tick) ticks.append(endTime * tick / 5.0);
plot->setAxisScaleX(-endTime * 0.05, endTime, ticks);
// 时间刻度及标题只在下图显示,上图仍保留横轴数据与同步关系。
plot->getAxisOf(0)->setMinimizeAxis(i == 0);
}
@ -2135,6 +2155,8 @@ void nmSubWndMain::clearForecastResult()
if(window) {
fitting->m_pMdiArea->removeSubWindow(window);
delete window;
// 直接移除窗口不会经过框架的关闭流程,清场后按当前布局重排其余窗口。
if(fitting->m_pMdiArea->isVisible()) fitting->m_pMdiArea->relayoutSubWnds();
}
}

@ -1010,9 +1010,9 @@ void nmWxForecast::onGenerateClicked()
return;
}
const QSharedPointer<const nmPebiResultSnapshot> solved = m_manager->getPebiResultSnapshot();
if(solved->getWellCount() != 1 || solved->getSolverSettings().m_nSolverModelType < 1 ||
if(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."));
QMessageBox::warning(this, tr("Forecast"), tr("Rate forecast currently supports single-phase models only."));
return;
}
QString error;
@ -1027,7 +1027,13 @@ void nmWxForecast::onGenerateClicked()
return;
}
const double pi = pressure.convertValueByUnitType(pressure.getValue().toDouble(), pressure.getUnit(), "MPa");
m_wellName = solved->getWellAt(0)->m_sWellName;
// 与主界面的结果井选择保持一致,预测制度只替换当前井,其他井共同参与求解。
const nmPebiResultWellSnapshot* well = solved->findWell(m_manager->getCurrentResultWellInstanceId());
if(!well) {
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);
if(!m_task->configureRateForecast(pi, durations, rates)) {
error = m_task->getFailureReason();

Loading…
Cancel
Save