Merge branch 'feature/Forecast-Sensitivity-Dialog-20261022' into develop

# Conflicts:
#	Bin/Config/Lang/cn/nmNum_cn.qm   resolved by feature/Forecast-Sensitivity-Dialog-20261022 version
develop
lvjunjie 2 days ago
commit 035892f3a7

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

@ -7388,6 +7388,22 @@ Average pressure in contour: %2 MPa</source>
</context>
<context>
<name>nmWxSensitive</name>
<message>
<source>Result plots</source>
<translation>结果图</translation>
</message>
<message>
<source>Loglog plot</source>
<translation>双对数图</translation>
</message>
<message>
<source>History plot</source>
<translation>历史曲线</translation>
</message>
<message>
<source>Histogram</source>
<translation>直方图</translation>
</message>
<message>
<source>Sensitivity</source>
<translation>敏感性分析</translation>
@ -8738,6 +8754,10 @@ Average pressure in contour: %2 MPa</source>
</context>
<context>
<name>nmWxForecast</name>
<message>
<source>Result plots</source>
<translation>结果图</translation>
</message>
<message>
<source>Forecast</source>
<translation>预测</translation>

@ -22,6 +22,7 @@ class nmCalculationDllPebiSolverTask;
class nmWxSensitive;
class nmWxForecast;
class iSubWndFitting;
class QDialog;
class QWidget;
class QObject;
@ -170,9 +171,11 @@ class NM_SUB_WND_EXPORT nmSubWndMain : public iSubWndBaseFit {
void onForecastTaskReady(nmWxForecast* dialog, nmCalculationDllPebiSolverTask* task);
void onAuxiliarySolverTaskFinished();
void onAuxiliarySolverTaskDestroyed();
/** @brief 在框架已有的敏感性子窗口叠加显示本分析的双对数结果。 */
/** @brief 在独立对话框中显示本分析的敏感性结果。 */
void onShowSensitiveResults();
// 预测结果复用所属分析内的 MDI 子窗口,模型变化时移除旧结果。
// 输入窗口菜单按需打开指定结果图。
void onShowSensitiveResult(int plotType);
// 预测结果复用所属分析的独立对话框。
void onShowForecastResult();
void onForecastDataChanged();
// 只读结果曲线共用框架数据表,不开放拖动编辑。
@ -293,7 +296,9 @@ class NM_SUB_WND_EXPORT nmSubWndMain : public iSubWndBaseFit {
bool initloglog(QVector<QVector<double >> & vvecLogPreData, QVector<iCurveDesc>& vecDescs,bool isHistoryData);
// 压力历史曲线数据
bool initPreHistory(QVector<QVector<double >> & vvecHistoryData, QVector<iCurveDesc>& vecDescs,bool isHistoryData);
// 在分析页面内同步敏感性压力/流量历史图。
// 创建本分析所属的非模态结果对话框。
QDialog* createResultDialog(const QString& name, const QString& title);
// 在独立对话框内同步敏感性压力/流量历史图。
void updateSensitiveHistoryPlot();
// 各结果图使用独立的 Show 按钮打开曲线显示表格。
void installSensitiveShow(QWidget* host);

@ -119,6 +119,9 @@ public:
/** @brief 返回数值编辑列的统一最小宽度。 */
static int editorMinimumWidth();
/** @brief 将单位别名及上标写法统一到框架单位表中的名称。 */
static QString resolveUnitSpelling(const QString& sUnit);
/** @brief 返回单位列的统一最小宽度。 */
static int unitMinimumWidth();

@ -37,6 +37,7 @@ public:
const QStringList& sampledParameters = QStringList());
~nmWxAutomaticFitting();
virtual QSize sizeHint() const;
void reloadSensitivitySettings(const QStringList& sampledParameters);
private slots:
void onSelectAll(bool checked);

@ -33,9 +33,11 @@ public:
signals:
void sigForecastTaskReady(nmWxForecast* dialog, nmCalculationDllPebiSolverTask* task);
void sigForecastCancelRequested();
void sigShowForecastResult();
public slots:
void reject();
void reloadInputData();
protected:
void closeEvent(QCloseEvent* event);
@ -59,7 +61,7 @@ protected:
void onCancelClicked();
void onForecastFinished();
void onModelChanged();
void saveDataToManager();
void updateResultButton();
// Table edit
void onPressureCellChanged(int row, int column);
@ -75,6 +77,7 @@ private:
void setupUI();
void setupConnections();
void loadDataFromManager();
void saveDataToManager();
void updateParametersForOption();
void updateUIFromData();
void updateDataFromUI();
@ -142,6 +145,7 @@ private:
QHBoxLayout *m_buttonLayout;
QPushButton *m_loadDataButton;
QPushButton *m_generateButton;
QPushButton *m_resultPlotButton;
QPushButton *m_cancelButton;
// Table

@ -77,6 +77,8 @@ signals:
void sigGenerateClicked(); // 点击生成按钮时发出的信号
// 敏感性批量任务交给所属主窗口,复用普通求解进度框。
void sigSensitivityTaskReady(nmWxSensitive* dialog, nmCalculationDllPebiSolverTask* task);
void sigShowSensitivityResult(int plotType);
void sigShowForecastResult();
void sigIncludeWells(); // 包含其他井后刷新参数栏的信号
/** @brief 用户切换结果井时发送快照中的稳定井 UUID。 */
void sigResultWellChanged(const QString& sWellInstanceId);
@ -229,6 +231,7 @@ private:
QPushButton* m_pForecastBtn;
QPointer<nmWxForecast> m_pForecastDialog; ///< 每个分析面板复用自己的预测输入窗口。
QPushButton* m_pSensitiveBtn;
QPointer<nmWxSensitive> m_pSensitiveDialog; ///< 每个分析面板复用自己的敏感性分析输入窗口。
QHBoxLayout* m_pGenerateLayout;
// 主布局

@ -34,12 +34,14 @@ class NM_SUB_WXS_EXPORT nmWxSensitivityHistogramOptions : public iDlgBase
public:
nmWxSensitivityHistogramOptions(const QSharedPointer<nmDataSensitive::BatchResult>& results,
int bins, QWidget* parent);
void setResults(const QSharedPointer<nmDataSensitive::BatchResult>& results);
signals:
void settingsChanged(int bins, int variable);
private slots:
void onApply();
void onAccept();
private:
QTableView* m_table;
QSpinBox* m_bins;
QComboBox* m_variable;
};
@ -50,6 +52,7 @@ class NM_SUB_WXS_EXPORT nmWxSensitivityScatter : public iDlgBase
public:
nmWxSensitivityScatter(const QSharedPointer<nmDataSensitive::BatchResult>& results,
double baseError, QWidget* parent = nullptr);
void setResults(const QSharedPointer<nmDataSensitive::BatchResult>& results, double baseError);
signals:
void filterChanged();
protected:
@ -83,6 +86,7 @@ public:
nmWxSensitivityShow(const QSharedPointer<nmDataSensitive::BatchResult>& results,
iGuiPlot* plot, QWidget* parent);
void done(int result) override;
void setResults(const QSharedPointer<nmDataSensitive::BatchResult>& results);
private slots:
void onApply();
@ -100,14 +104,17 @@ class NM_SUB_WXS_EXPORT nmWxSensitive : public iDlgBase
Q_OBJECT
public:
enum ResultPlot { LoglogResult, HistoryResult, HistogramResult };
explicit nmWxSensitive(QWidget *parent = 0, nmDataAnalyzeManager* manager = nullptr);
~nmWxSensitive();
// 主窗口接管普通求解进度框后,才隐藏参数窗口并启动批量任务。
void startSensitivityCalculation();
void reloadInputData();
signals:
void sigSensitivityTaskReady(nmWxSensitive* dialog, nmCalculationDllPebiSolverTask* task);
void sigSensitivityCancelRequested();
void sigShowSensitivityResult(int plotType);
public slots:
void reject() override;
@ -123,12 +130,14 @@ protected:
void onTreeItemClicked(QTreeWidgetItem *item, int column);
void onVariableParamChanged();
void onSamplingEditingFinished();
void onSamplingUnitChanged();
void onTableCellClicked(int row, int column);
void onTreeItemChanged(QTreeWidgetItem *item, int column);
void onPreviewClicked();
void onPreviewScrollAction(int action);
void onSensitivityFinished();
void onResultsChanged();
void onResultPlotTriggered(QAction* action);
void onModelResultChanged();
private:
@ -156,6 +165,8 @@ private:
QStackedWidget *detailStack;
QLabel *modelCountLabel;
QSpinBox *m_monteCarloCount;
QPushButton *m_resultPlotsButton;
QAction *m_histogramResultAction;
QPushButton *generateButton;
QPushButton *cancelButton;
QPushButton *previewButton;
@ -168,7 +179,7 @@ private:
QList<QLineEdit*> m_minSpins;
QList<QLineEdit*> m_maxSpins;
QList<QTableWidget*> m_valueTables;
QList<QLabel*> m_unitLabels;
QList<QComboBox*> m_unitCombos;
QList<QLabel*> m_numLabels, m_minLabels, m_maxLabels;
QList<QComboBox*> m_distributionCombos;
QList<QWidget*> m_distributionPanels;
@ -176,8 +187,10 @@ private:
// 跟踪当前选中的值索引
int m_selectedValueIndex;
QSharedPointer<nmSensitivityFitSettings> m_improveSettingsDraft;
bool m_loading;
nmDataAnalyzeManager* m_pDataManager;
const nmDataSensitive* m_sourceData; ///< 识别项目读取后数据对象的替换,不持有其所有权。
SensitivityRunsView* m_previewView;
nmCalculationDllPebiSolverTask* m_pSensitivityTask;
bool m_calculationRunning;

@ -4838,8 +4838,8 @@ bool nmDataAnalyzeManager::setCurrentResultWellInstanceId(
return true;
}
// 仅切换查看井,不通知快照变更,避免清理预测和敏感性分析的数据及窗口。
m_sCurrentResultWellInstanceId = sValidatedWellInstanceId;
emit sigPebiResultSnapshotChanged();
return true;
}

@ -78,7 +78,7 @@
#include <QPushButton>
#include <QLabel>
#include <cmath>
#include <QMdiSubWindow>
#include <QDialog>
#include "ZxMdiArea.h"
#include "nmSingalCenter.h"
@ -130,6 +130,32 @@ void clearResultCurveDataTables(iGuiPlot* plot)
}
}
// 结果替换时保留窗口,清理旧批次引用、曲线与框架表格,随后按需重绘。
void resetResultDialog(QDialog* window)
{
if(!window) return;
// 先恢复最小化状态再隐藏,避免隐藏后调整尺寸使系统重新显示空白窗口。
if(window->isMinimized())
window->setWindowState(window->windowState() & ~Qt::WindowMinimized);
window->hide();
foreach(iGuiPlot* plot, window->findChildren<iGuiPlot*>()) clearResultCurveDataTables(plot);
foreach(nmWxSensitivityShow* dialog, window->findChildren<nmWxSensitivityShow*>())
dialog->setResults(QSharedPointer<nmDataSensitive::BatchResult>());
foreach(nmWxSensitivityHistogramOptions* dialog, window->findChildren<nmWxSensitivityHistogramOptions*>())
dialog->setResults(QSharedPointer<nmDataSensitive::BatchResult>());
foreach(nmWxSensitivityScatter* dialog, window->findChildren<nmWxSensitivityScatter*>())
dialog->setResults(QSharedPointer<nmDataSensitive::BatchResult>(), -1.0);
foreach(iGuiPlot* plot, window->findChildren<iGuiPlot*>()) {
clearResultCurveDataTables(plot);
for(int i = plot->m_pPlot->getObjCount() - 1; i >= 0; --i)
if(qobject_cast<ZxObjCurve*>(plot->m_pPlot->getObjByIndex(i))) plot->m_pPlot->removeObjByIndex(i);
plot->clearUndoStack();
plot->setCurveData(QVector<iCurveDesc>());
plot->runUpdate();
}
window->setProperty("nmResultDirty", true);
}
QSet<QString> hiddenSensitiveCurves(iGuiPlot* plot)
{
// 刷新同一批次或重新打开窗口时保留本图的显示选择。
@ -334,6 +360,11 @@ void nmSubWndMain::onPebiResultSnapshotChanged()
return;
}
// 项目读取或结果井切换后清理旧图,保留数据,等待设置窗口请求显示。
const char* names[] = { "nmForecastResult", "nmSensitivityLoglog", "nmSensitivityHistory", "nmSensitivityHistogram" };
for(int i = 0; i < 4; ++i)
resetResultDialog(pSubWndFitting->findChild<QDialog*>(names[i]));
QSharedPointer<const nmPebiResultSnapshot> pSnapshot =
pDataManager->getPebiResultSnapshot();
if(pSnapshot.isNull()) {
@ -343,25 +374,6 @@ 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;
}
@ -1961,15 +1973,29 @@ void nmSubWndMain::onForecastDataChanged()
m_pResultRefreshFitting->setModified(true);
}
QDialog* nmSubWndMain::createResultDialog(const QString& name, const QString& title)
{
// 关闭只隐藏,窗口及绘图控件由所属分析统一销毁。
QDialog* dialog = new QDialog(m_pResultRefreshFitting.data(),
Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint);
dialog->setObjectName(name);
dialog->setWindowTitle(title);
dialog->setAttribute(Qt::WA_DeleteOnClose, false);
dialog->resize(900, 650);
QVBoxLayout* layout = new QVBoxLayout(dialog);
layout->setContentsMargins(0, 0, 0, 0);
dialog->installEventFilter(this);
return dialog;
}
void nmSubWndMain::onShowForecastResult()
{
// 使用面板绑定的分析页面,切换活动分析不会改变预测结果的归属。
iSubWndFitting* fitting = m_pResultRefreshFitting.data();
nmDataAnalyzeManager* manager = m_pResultRefreshDataManager.data();
if(!fitting || !fitting->m_pMdiArea || !manager ||
if(!fitting || !manager ||
nmDataAnalyzeManager::findManagerByFitting(fitting) != manager) return;
ZxMdiArea* area = fitting->m_pMdiArea;
QMdiSubWindow* window = area->findChild<QMdiSubWindow*>("nmForecastResult");
QDialog* window = fitting->findChild<QDialog*>("nmForecastResult");
const QSharedPointer<nmDataForecast::Result> result = manager->getForecastData()
? manager->getForecastData()->getResult() : QSharedPointer<nmDataForecast::Result>();
const QSharedPointer<const nmPebiResultSnapshot> snapshot = manager->getPebiResultSnapshot();
@ -1977,12 +2003,7 @@ void nmSubWndMain::onShowForecastResult()
result->snapshotId != snapshot->getSnapshotId() ||
result->wellInstanceId != manager->getCurrentResultWellInstanceId()) {
// 基础结果或结果井切换后清除旧窗口;模型参数编辑不影响上次预测的显示。
if(window) {
foreach(iGuiPlot* plot, window->findChildren<iGuiPlot*>()) clearResultCurveDataTables(plot);
area->removeSubWindow(window);
delete window;
if(area->isVisible()) area->relayoutSubWnds();
}
resetResultDialog(window);
return;
}
const QVector<double>& times = result->times;
@ -1991,10 +2012,9 @@ void nmSubWndMain::onShowForecastResult()
const QVector<double>& rates = result->rates;
const bool created = window == nullptr;
if(created) {
// 结果页面由 MDI 承载,关闭预测输入对话框不销毁结果。
QWidget* page = new QWidget;
QVBoxLayout* layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
window = createResultDialog("nmForecastResult", nmWxForecast::tr("Forecast"));
QWidget* page = window;
QVBoxLayout* layout = qobject_cast<QVBoxLayout*>(window->layout());
QSplitter* charts = new QSplitter(Qt::Vertical, page);
charts->setObjectName("nmForecastCharts");
charts->setChildrenCollapsible(false);
@ -2029,9 +2049,6 @@ void nmSubWndMain::onShowForecastResult()
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(nmWxForecast::tr("Forecast"));
@ -2103,12 +2120,11 @@ void nmSubWndMain::onShowForecastResult()
// 时间刻度及标题只在下图显示,上图仍保留横轴数据与同步关系。
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);
// 再次生成时恢复已最小化的结果窗口,正常或最大化状态保持不变。
if(window->isMinimized()) window->showNormal();
else window->show();
window->raise();
window->activateWindow();
QTimer::singleShot(0, this, SLOT(fitForecastViews()));
}
@ -2135,7 +2151,25 @@ void nmSubWndMain::onReadonlyCurveDoubleClicked(
bool nmSubWndMain::eventFilter(QObject* watched, QEvent* event)
{
// 子窗口缩放、重新显示均在内部控件完成尺寸更新后再适配。
// 最小化时先恢复窗口状态,再交给默认关闭流程隐藏,避免复用时丢失正常尺寸。
QDialog* dialog = qobject_cast<QDialog*>(watched);
if(dialog && event->type() == QEvent::Close && dialog->isMinimized())
dialog->setWindowState(dialog->windowState() & ~Qt::WindowMinimized);
// 关闭结果对话框时同步关闭其曲线数据表、Show 和统计设置窗口。
if((event->type() == QEvent::Close || event->type() == QEvent::Hide) &&
qobject_cast<QDialog*>(watched)) {
foreach(iGuiPlot* plot, watched->findChildren<iGuiPlot*>())
clearResultCurveDataTables(plot);
foreach(QDialog* dialog, watched->findChildren<QDialog*>())
if(dialog->isWindow()) dialog->close();
}
// 独立窗口不再经过 MDI 的画布适配,显示和缩放时适配自己的绘图区。
if(dialog && (event->type() == QEvent::Resize || event->type() == QEvent::Show)) {
dialog->layout()->activate();
foreach(iGuiPlot* plot, dialog->findChildren<iGuiPlot*>())
plot->m_pPlotView->setZoomMode(ZxRenderView::eZoomFitWindow, true);
}
// 对话框缩放、重新显示均在内部控件完成尺寸更新后再适配。
if(watched->objectName() == "nmForecastCharts" &&
(event->type() == QEvent::Resize || event->type() == QEvent::Show))
QTimer::singleShot(0, this, SLOT(fitForecastViews()));
@ -2146,8 +2180,8 @@ void nmSubWndMain::fitForecastViews()
{
// 只适配图形画布的显示尺寸,不改变用户选定的时间和压力范围。
iSubWndFitting* fitting = m_pResultRefreshFitting.data();
if(!fitting || !fitting->m_pMdiArea) return;
QMdiSubWindow* window = fitting->m_pMdiArea->findChild<QMdiSubWindow*>("nmForecastResult");
if(!fitting) return;
QDialog* window = fitting->findChild<QDialog*>("nmForecastResult");
if(!window) return;
foreach(iGuiPlot* plot, window->findChildren<iGuiPlot*>())
plot->m_pPlotView->setZoomMode(ZxRenderView::eZoomFitWindow, true);
@ -2157,8 +2191,8 @@ void nmSubWndMain::syncForecastTimeAxis(double minimum, double maximum, bool rec
{
// 只同步本分析预测图的横轴;阻断回传,避免两张图相互触发范围更新。
iSubWndFitting* fitting = m_pResultRefreshFitting.data();
if(!fitting || !fitting->m_pMdiArea) return;
QMdiSubWindow* window = fitting->m_pMdiArea->findChild<QMdiSubWindow*>("nmForecastResult");
if(!fitting) return;
QDialog* window = fitting->findChild<QDialog*>("nmForecastResult");
if(!window) return;
iGuiPlot* rate = window->findChild<iGuiPlot*>("nmForecastRate");
iGuiPlot* pressure = window->findChild<iGuiPlot*>("nmForecastPressure");
@ -2173,48 +2207,62 @@ void nmSubWndMain::syncForecastTimeAxis(double minimum, double maximum, bool rec
void nmSubWndMain::onShowSensitiveResults()
{
// 使用固定所属分析的窗口与数据,不能把结果画到当前活动的其他分析中。
nmDataAnalyzeManager* manager = m_pResultRefreshDataManager.data();
iSubWndFitting* fitting = m_pResultRefreshFitting.data();
if(!manager || !fitting || !manager->getSensitiveData()) return;
// 新批次或清空结果时同步移除上一批的上下限分组。
if(!manager || !fitting) return;
// 新批次先清理已有窗口的旧数据,默认只重绘并显示双对数图。
const char* names[] = { "nmSensitivityLoglog", "nmSensitivityHistory", "nmSensitivityHistogram" };
for(int i = 0; i < 3; ++i) {
QDialog* window = fitting->findChild<QDialog*>(names[i]);
resetResultDialog(window);
}
onSensitiveFilterChanged();
updateSensitiveHistoryPlot();
updateSensitiveHistogramPlot();
QSharedPointer<nmDataSensitive::BatchResult> result = manager->getSensitiveData()->getResults();
iGuiPlot* plot = fitting->getPlotByTag(s_Tag_ChartSensitivity);
if(plot) clearResultCurveDataTables(plot);
if(result.isNull()) {
// 新模型已丢弃旧批次,同时清除图元与撤销记录,避免窗口恢复或保存后残留旧曲线。
if(plot) {
for(int i = plot->m_pPlot->getObjCount() - 1; i >= 0; --i) {
if(qobject_cast<ZxObjCurve*>(plot->m_pPlot->getObjByIndex(i)))
plot->m_pPlot->removeObjByIndex(i);
}
plot->clearUndoStack();
plot->setCurveData(QVector<iCurveDesc>());
plot->runUpdate();
QMdiSubWindow* subWindow = nullptr;
for(QWidget* parent = plot->parentWidget(); parent && !subWindow; parent = parent->parentWidget())
subWindow = qobject_cast<QMdiSubWindow*>(parent);
if(subWindow) subWindow->close();
else plot->hide();
}
if(sender() == manager) fitting->setModified(true);
onShowSensitiveResult(nmWxSensitive::LoglogResult);
if(sender() == manager) fitting->setModified(true);
}
void nmSubWndMain::onShowSensitiveResult(int plotType)
{
// 仅从数据中心读取已有批次,已打开的图直接恢复并激活,不重复绘制。
nmDataAnalyzeManager* manager = m_pResultRefreshDataManager.data();
iSubWndFitting* fitting = m_pResultRefreshFitting.data();
if(!manager || !fitting || !manager->getSensitiveData()) return;
const QSharedPointer<nmDataSensitive::BatchResult> result = manager->getSensitiveData()->getResults();
if(result.isNull() || plotType < nmWxSensitive::LoglogResult ||
plotType > nmWxSensitive::HistogramResult) return;
if(plotType == nmWxSensitive::HistogramResult && result->m_calcType != nmDataSensitive::CALC_MONTE_CARLO) return;
const char* names[] = { "nmSensitivityLoglog", "nmSensitivityHistory", "nmSensitivityHistogram" };
QDialog* window = fitting->findChild<QDialog*>(names[plotType]);
if(window && !window->property("nmResultDirty").toBool()) {
if(window->isMinimized()) window->showNormal();
else window->show();
window->raise();
window->activateWindow();
return;
}
if(plotType == nmWxSensitive::HistoryResult) {
updateSensitiveHistoryPlot();
return;
}
const bool created = plot == nullptr;
// 通用取窗接口未实现敏感性类型,直接复用框架已有的内部 MDI 创建函数。
if(created) plot = fitting->initSensitivityPlot();
if(!plot) {
QMessageBox::warning(fitting, nmWxSensitive::tr("Sensitivity"),
tr("Failed to create sensitivity plot."));
if(plotType == nmWxSensitive::HistogramResult) {
updateSensitiveHistogramPlot();
return;
}
// 与预测一致,直接在结果对话框内创建框架绘图控件,不经过主界面 MDI。
if(!window) {
window = createResultDialog("nmSensitivityLoglog", tr("Sensitivity loglog plot"));
iGuiPlot* plot = new iGuiPlot(true, window);
plot->initUI(tr("Sensitivity loglog plot"));
plot->m_pPlotView->setSimpleMode(true);
plot->setAxisX(tr("Time"), "hr");
plot->setAxisY(tr("Pressure"), "MPa");
plot->m_pPlot->setBKColor(Qt::white);
window->layout()->addWidget(plot);
}
iGuiPlot* plot = window->findChild<iGuiPlot*>();
connect(plot, SIGNAL(sigDoubleClicked(ZxPlot*,ZxObjBase*,const QPointF&,bool&)),
this, SLOT(onReadonlyCurveDoubleClicked(ZxPlot*,ZxObjBase*,const QPointF&,bool&)), Qt::UniqueConnection);
installSensitiveShow(plot);
const QSet<QString> hidden = sender() == manager ? QSet<QString>() : hiddenSensitiveCurves(plot);
QVector<iCurveDesc> curves;
QVector<QVector<double> > history = result->m_vecHistoryLogLog;
if(history.size() == 3 && history[0].size() == history[1].size() &&
@ -2268,78 +2316,44 @@ void nmSubWndMain::onShowSensitiveResults()
plot->setAxisXYLog(true, 1);
if(!result->m_sAxisXName.isEmpty()) plot->setAxisX(result->m_sAxisXName, result->m_sAxisXUnit);
if(!result->m_sAxisYName.isEmpty()) plot->setAxisY(result->m_sAxisYName, result->m_sAxisYUnit);
// 框架刷新只创建或更新传入曲线,不删除缺席曲线,先移除已隐藏或上一批残留的图元。
QSet<QString> visibleNames;
for(int i = 0; i < curves.size(); ++i) visibleNames.insert(curves[i].m_sCurveName);
bool removed = false;
for(int i = plot->m_pPlot->getObjCount() - 1; i >= 0; --i) {
ZxObjCurve* curve = qobject_cast<ZxObjCurve*>(plot->m_pPlot->getObjByIndex(i));
if(curve && !visibleNames.contains(curve->getName())) {
plot->m_pPlot->removeObjByIndex(i);
removed = true;
}
}
// 清理删除曲线相关的撤销记录,避免后续操作恢复已经隐藏的图元。
if(removed) plot->clearUndoStack();
plot->setCurveData(curves);
plot->runUpdate();
for(int i = 0; i < curves.size(); ++i)
if(ZxObjCurve* curve = plot->getCurve(curves[i].m_sCurveName))
curve->setVisible(!hidden.contains(curve->getName()));
plot->setReadOnly(true);
plot->freshAxisScales(true, false);
// 只有新建或恢复窗口时才重新调整内部布局。
QMdiSubWindow* subWindow = nullptr;
for(QWidget* parent = plot->parentWidget(); parent && !subWindow; parent = parent->parentWidget())
subWindow = qobject_cast<QMdiSubWindow*>(parent);
const bool restore = subWindow && subWindow->isHidden();
plot->show();
if(subWindow) {
// 敏感性子窗口标题使用数值模块译文,与历史图、直方图保持一致。
subWindow->setWindowTitle(tr("Sensitivity loglog plot"));
subWindow->show();
if(created || restore) {
ZxMdiArea* area = qobject_cast<ZxMdiArea*>(subWindow->mdiArea());
fitting->layoutSubWnds(area ? area->getLayoutMode() : WLM_Cell);
}
}
if(sender() == manager) fitting->setModified(true);
window->setProperty("nmResultDirty", false);
if(window->isMinimized()) window->showNormal();
else window->show();
window->raise();
window->activateWindow();
}
void nmSubWndMain::updateSensitiveHistoryPlot()
{
// 复用框架的上下压力/流量控件,作为同一分析页面中的独立 MDI 子窗口。
// 复用框架的上下压力/流量控件,在本分析的独立对话框展示。
iSubWndFitting* fitting = m_pResultRefreshFitting.data();
nmDataAnalyzeManager* manager = m_pResultRefreshDataManager.data();
if(!fitting || !fitting->m_pMdiArea || !manager || !manager->getSensitiveData()) return;
ZxMdiArea* area = fitting->m_pMdiArea;
QMdiSubWindow* window = area->findChild<QMdiSubWindow*>("nmSensitivityHistory");
if(!fitting || !manager || !manager->getSensitiveData()) return;
QDialog* window = fitting->findChild<QDialog*>("nmSensitivityHistory");
const QSharedPointer<nmDataSensitive::BatchResult> batch = manager->getSensitiveData()->getResults();
if(batch.isNull()) {
// 新模型丢弃旧批次时一并销毁窗口,曲线和撤销记录不再残留。
if(window) {
area->removeSubWindow(window);
delete window;
}
// 新模型丢弃旧批次时清空并隐藏窗口,曲线和撤销记录不再残留。
resetResultDialog(window);
return;
}
const bool created = window == nullptr;
if(created) {
// PF 继承对话框,用普通页面承载,关闭子窗口时不进入其对话框关闭流程。
QWidget* page = new QWidget;
QVBoxLayout* layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
// PF 作为对话框内的控件使用,保留框架的上下曲线布局。
window = createResultDialog("nmSensitivityHistory", tr("Sensitivity history plot"));
QWidget* page = window;
QVBoxLayout* layout = qobject_cast<QVBoxLayout*>(window->layout());
iGuiPlotPF* history = new iGuiPlotPF(s_Tag_HIST_PF, true, true, page);
history->setWindowFlags(Qt::Widget);
history->initUI(tr("Sensitivity history plot"));
history->setTableHelperVisible(false);
layout->addWidget(history);
window = area->addSubWindow(page);
window->setObjectName("nmSensitivityHistory");
window->setWindowTitle(tr("Sensitivity history plot"));
window->setAttribute(Qt::WA_DeleteOnClose, false);
}
installSensitiveShow(window->widget());
installSensitiveShow(window);
iGuiPlotPF* history = window->findChild<iGuiPlotPF*>();
QVector<iCurveDesc> curves;
// 基准压力、实测压力和流量均取成功求解快照,未求解的参数或制度编辑不参与绘图。
@ -2408,13 +2422,14 @@ void nmSubWndMain::updateSensitiveHistoryPlot()
plot->setReadOnly(true);
plot->freshAxisScales(true, true);
}
const bool restore = window->isHidden();
history->show();
window->widget()->show();
window->show();
window->setProperty("nmResultDirty", false);
if(window->isMinimized()) window->showNormal();
else window->show();
// 历史图只显示曲线,隐藏框架默认的空数据表侧栏。
history->m_pTableHelper->hide();
if(created || restore) area->relayoutSubWnds();
window->raise();
window->activateWindow();
}
void nmSubWndMain::installSensitiveShow(QWidget* host)
@ -2455,7 +2470,9 @@ void nmSubWndMain::onSensitiveShowClicked()
// 发布新批次或清除旧模型后关闭旧表格,不能再操作上一批曲线。
connect(manager, SIGNAL(sigSensitiveResultsChanged()), dialog, SLOT(reject()));
}
dialog->show();
dialog->setResults(batch);
if(dialog->isMinimized()) dialog->showNormal();
else dialog->show();
dialog->raise();
dialog->activateWindow();
}
@ -2464,23 +2481,19 @@ void nmSubWndMain::updateSensitiveHistogramPlot()
{
iSubWndFitting* fitting = m_pResultRefreshFitting.data();
nmDataAnalyzeManager* manager = m_pResultRefreshDataManager.data();
if(!fitting || !fitting->m_pMdiArea || !manager || !manager->getSensitiveData()) return;
ZxMdiArea* area = fitting->m_pMdiArea;
QMdiSubWindow* window = area->findChild<QMdiSubWindow*>("nmSensitivityHistogram");
if(!fitting || !manager || !manager->getSensitiveData()) return;
QDialog* window = fitting->findChild<QDialog*>("nmSensitivityHistogram");
const QSharedPointer<nmDataSensitive::BatchResult> batch = manager->getSensitiveData()->getResults();
if(batch.isNull() || batch->m_calcType != nmDataSensitive::CALC_MONTE_CARLO) {
// 新模型或确定性批次不保留上一批 Monte-Carlo 的统计图。
if(window) {
area->removeSubWindow(window);
delete window;
}
resetResultDialog(window);
return;
}
const bool created = window == nullptr;
if(created) {
QWidget* page = new QWidget;
QVBoxLayout* layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
window = createResultDialog("nmSensitivityHistogram", tr("Sensitivity histogram plot"));
QWidget* page = window;
QVBoxLayout* layout = qobject_cast<QVBoxLayout*>(window->layout());
QHBoxLayout* options = new QHBoxLayout;
QPushButton* scatter = new QPushButton(tr("Scatter plot"), page);
scatter->setObjectName("nmScatterButton");
@ -2502,14 +2515,10 @@ void nmSubWndMain::updateSensitiveHistogramPlot()
plot->getAxisOf(i)->setVisible(true);
}
layout->addWidget(plot);
window = area->addSubWindow(page);
window->setObjectName("nmSensitivityHistogram");
window->setWindowTitle(tr("Sensitivity histogram plot"));
window->setAttribute(Qt::WA_DeleteOnClose, false);
connect(scatter, SIGNAL(clicked()), this, SLOT(onSensitiveScatter()));
connect(settings, SIGNAL(clicked()), this, SLOT(onSensitiveHistogramOptions()));
}
iGuiPlot* plot = window->widget()->findChild<iGuiPlot*>();
iGuiPlot* plot = window->findChild<iGuiPlot*>();
const int binCount = batch->m_nHistogramBins;
// 拟合参数与误差共用分箱绘制,轴名称和单位随本批选择同步切换。
const int variable = batch->m_nHistogramVariable;
@ -2544,6 +2553,7 @@ void nmSubWndMain::updateSensitiveHistogramPlot()
const int index = qMin(binCount - 1, int((values[i] - minimum) / width));
++counts[index];
}
clearResultCurveDataTables(plot);
for(int i = plot->m_pPlot->getObjCount() - 1; i >= 0; --i)
if(qobject_cast<ZxObjCurve*>(plot->m_pPlot->getObjByIndex(i))) plot->m_pPlot->removeObjByIndex(i);
plot->clearUndoStack();
@ -2600,11 +2610,12 @@ void nmSubWndMain::updateSensitiveHistogramPlot()
plot->setAxisScaleX(minimum, maximum, ticksX);
plot->setAxisScaleY(0.0, top, ticksY);
plot->setAxisScaleY2(0.0, 110.0, ticksY2);
const bool restore = window->isHidden();
plot->show();
window->widget()->show();
window->show();
if(created || restore) area->relayoutSubWnds();
window->setProperty("nmResultDirty", false);
if(window->isMinimized()) window->showNormal();
else window->show();
window->raise();
window->activateWindow();
}
void nmSubWndMain::onSensitiveHistogramOptions()
@ -2612,17 +2623,19 @@ void nmSubWndMain::onSensitiveHistogramOptions()
// 设置窗口复用当前批次,分箱数在 Apply 或 OK 后才应用到直方图。
iSubWndFitting* fitting = m_pResultRefreshFitting.data();
nmDataAnalyzeManager* manager = m_pResultRefreshDataManager.data();
if(!fitting || !fitting->m_pMdiArea || !manager || !manager->getSensitiveData()) return;
QMdiSubWindow* window = fitting->m_pMdiArea->findChild<QMdiSubWindow*>("nmSensitivityHistogram");
if(!fitting || !manager || !manager->getSensitiveData()) return;
QDialog* window = fitting->findChild<QDialog*>("nmSensitivityHistogram");
const QSharedPointer<nmDataSensitive::BatchResult> batch = manager->getSensitiveData()->getResults();
if(!window || batch.isNull()) return;
nmWxSensitivityHistogramOptions* dialog = window->findChild<nmWxSensitivityHistogramOptions*>();
if(!dialog) {
dialog = new nmWxSensitivityHistogramOptions(batch, batch->m_nHistogramBins, window->widget());
dialog = new nmWxSensitivityHistogramOptions(batch, batch->m_nHistogramBins, window);
connect(dialog, SIGNAL(settingsChanged(int,int)), this, SLOT(onSensitiveHistogramSettingsChanged(int,int)));
connect(manager, SIGNAL(sigSensitiveResultsChanged()), dialog, SLOT(reject()));
}
dialog->show();
dialog->setResults(batch);
if(dialog->isMinimized()) dialog->showNormal();
else dialog->show();
dialog->raise();
dialog->activateWindow();
}
@ -2637,8 +2650,8 @@ void nmSubWndMain::onSensitiveHistogramSettingsChanged(int bins, int variable)
batch->m_nHistogramVariable = variable;
batch->m_nHistogramBins = bins;
iSubWndFitting* fitting = m_pResultRefreshFitting.data();
if(!fitting || !fitting->m_pMdiArea) return;
QMdiSubWindow* window = fitting->m_pMdiArea->findChild<QMdiSubWindow*>("nmSensitivityHistogram");
if(!fitting) return;
QDialog* window = fitting->findChild<QDialog*>("nmSensitivityHistogram");
if(!window) return;
fitting->setModified(true);
updateSensitiveHistogramPlot();
@ -2661,30 +2674,32 @@ void nmSubWndMain::onSensitiveScatter()
// 与 Show 一样打开独立弹窗,重复点击时激活已有窗口。
iSubWndFitting* fitting = m_pResultRefreshFitting.data();
nmDataAnalyzeManager* manager = m_pResultRefreshDataManager.data();
if(!fitting || !fitting->m_pMdiArea || !manager || !manager->getSensitiveData()) return;
if(!fitting || !manager || !manager->getSensitiveData()) return;
const QSharedPointer<nmDataSensitive::BatchResult> batch = manager->getSensitiveData()->getResults();
if(batch.isNull() || batch->m_calcType != nmDataSensitive::CALC_MONTE_CARLO) return;
QMdiSubWindow* window = fitting->m_pMdiArea->findChild<QMdiSubWindow*>("nmSensitivityHistogram");
QDialog* window = fitting->findChild<QDialog*>("nmSensitivityHistogram");
if(!window) return;
nmWxSensitivityScatter* dialog = window->findChild<nmWxSensitivityScatter*>();
double baseError = -1.0;
const QSharedPointer<const nmPebiResultSnapshot> snapshot = manager->getPebiResultSnapshot();
for(int i = 0; !snapshot.isNull() && i < snapshot->getWellCount(); ++i) {
const nmPebiResultWellSnapshot* well = snapshot->getWellAt(i);
if(well->m_sWellCode != batch->m_sWellCode) continue;
baseError = nmCalculationDllPebiSolverTask::calculatePressureRmse(
well->m_oCurves.m_vecHistoryPressure, well->m_oCurves.m_vecResultPressure);
break;
}
if(!dialog) {
double baseError = -1.0;
const QSharedPointer<const nmPebiResultSnapshot> snapshot = manager->getPebiResultSnapshot();
for(int i = 0; !snapshot.isNull() && i < snapshot->getWellCount(); ++i) {
const nmPebiResultWellSnapshot* well = snapshot->getWellAt(i);
if(well->m_sWellCode != batch->m_sWellCode) continue;
baseError = nmCalculationDllPebiSolverTask::calculatePressureRmse(
well->m_oCurves.m_vecHistoryPressure, well->m_oCurves.m_vecResultPressure);
break;
}
dialog = new nmWxSensitivityScatter(batch, baseError, window->widget());
dialog = new nmWxSensitivityScatter(batch, baseError, window);
connect(dialog, SIGNAL(filterChanged()), this, SLOT(onSensitiveFilterChanged()));
// 构造时已应用初始阈值,连接完成后补一次结果面板同步。
onSensitiveFilterChanged();
// 新结果或新模型生成后关闭弹窗,避免保留旧批次的点和阈值。
connect(manager, SIGNAL(sigSensitiveResultsChanged()), dialog, SLOT(reject()));
}
dialog->show();
dialog->setResults(batch, baseError);
if(dialog->isMinimized()) dialog->showNormal();
else dialog->show();
dialog->raise();
dialog->activateWindow();
}

@ -485,18 +485,21 @@ void nmSubWndUtils::setupNumericalDockWidgets(nmDataAnalyzeManager* pDataManager
pSubWndMain, SLOT(onForecastTaskReady(nmWxForecast*,nmCalculationDllPebiSolverTask*)));
connect(pAnalWx, SIGNAL(sigSensitivityTaskReady(nmWxSensitive*,nmCalculationDllPebiSolverTask*)),
pSubWndMain, SLOT(onSensitivityTaskReady(nmWxSensitive*,nmCalculationDllPebiSolverTask*)));
connect(pAnalWx, SIGNAL(sigShowForecastResult()),
pSubWndMain, SLOT(onShowForecastResult()));
connect(pAnalWx, SIGNAL(sigShowSensitivityResult(int)),
pSubWndMain, SLOT(onShowSensitiveResult(int)));
connect(pAnalWx, SIGNAL(sigResultWellChanged(QString)),
pSubWndMain, SLOT(onWellSelected(QString)));
// 手工求解和自动拟合都发布同一种正式快照,统一复用主窗口结果刷新逻辑。
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(onShowForecastResult()), Qt::UniqueConnection);
connect(pDataManager, SIGNAL(sigSensitiveResultsChanged()),
pSubWndMain, SLOT(onShowSensitiveResults()),
static_cast<Qt::ConnectionType>(Qt::QueuedConnection | Qt::UniqueConnection));
@ -508,10 +511,7 @@ void nmSubWndUtils::setupNumericalDockWidgets(nmDataAnalyzeManager* pDataManager
pWxDockNm1->setWidget(pAnalWx);
pWxDockNm2->setWidget(pParaWx);
// 成果加载和首次创建面板共用入口,有已保存的敏感性结果时恢复同一子窗口。
if(pDataManager->getSensitiveData() && !pDataManager->getSensitiveData()->getResults().isNull())
pSubWndMain->onShowSensitiveResults();
pSubWndMain->onShowForecastResult();
}
bool nmSubWndUtils::fillNmDockWxs(iSubWnd* pSubWnd)

@ -221,7 +221,7 @@ void resolveParameterUnitSpelling(iParameter* pParameter)
}
// 参数XML和单位XML可能使用不同排版写法,绑定前切到当前单位表可识别的单位名。
QString sResolvedUnit = resolveAvailableUnitSpelling(pParameter->m_sUnit);
QString sResolvedUnit = nmParameterField::resolveUnitSpelling(pParameter->m_sUnit);
if (sResolvedUnit != pParameter->m_sUnit)
{
pParameter->m_sUnit = sResolvedUnit;
@ -341,6 +341,12 @@ int nmParameterField::editorMinimumWidth()
return 140;
}
QString nmParameterField::resolveUnitSpelling(const QString& sUnit)
{
// 参数编辑与采样界面共用名称解析,避免普通指数与上标写法匹配到不同单位。
return resolveAvailableUnitSpelling(sUnit);
}
int nmParameterField::unitMinimumWidth()
{
return 86;

@ -329,6 +329,26 @@ nmWxAutomaticFitting::nmWxAutomaticFitting(QWidget *parent, nmSensitivityFitSett
DEBUG_UI("AutoFitting Constructor completed");
}
void nmWxAutomaticFitting::reloadSensitivitySettings(const QStringList& sampledParameters)
{
// 敏感性设置复用原对话框,每次打开恢复已确认配置和本次禁用的采样参数。
m_sampledParameters = sampledParameters;
nmDataAnalyzeManager* manager = nmDataAnalyzeManager::getCurrentInstance();
reservoirData = manager->getReservoirDataCopy();
automaticFittingData = m_sensitivitySettings->confirmed
? m_sensitivitySettings->parameters : manager->getAutomaticFittingDataCopy();
if(!m_sensitivitySettings->confirmed) automaticFittingData.getModelParameters().clear();
nmDataWellBase* well = manager->findWellByCode(manager->getPrimaryWellCode());
m_targetWellName = well ? well->getWellName() : QString();
m_targetWellCode = well ? well->getWellCode() : QString();
m_updatingParameterRanges = true;
initializeModelParameters();
m_iterationEdit->setText(QString::number(automaticFittingData.getIterationCount().getValue().toInt()));
m_errorLimitEdit->setText(QString::number(automaticFittingData.getErrorTolerance().getValue().toDouble()));
onAlgorithmChanged(1);
m_updatingParameterRanges = false;
}
QSize nmWxAutomaticFitting::sizeHint() const
{
// 与改版前设置窗口保持相同推荐尺寸,仍允许手动放大。

@ -181,7 +181,8 @@ void nmAutomaticFittingCurveChart::paintEvent(QPaintEvent* event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
painter.fillRect(rect(), QColor(240, 240, 240));
// 图表内外统一使用白色背景。
painter.fillRect(rect(), Qt::white);
const int marginLeft = 62;
const int marginTop = 18;
@ -191,7 +192,7 @@ void nmAutomaticFittingCurveChart::paintEvent(QPaintEvent* event)
qMax(10, width() - marginLeft - marginRight),
qMax(10, height() - marginTop - marginBottom));
painter.fillRect(m_chartRect, QColor(252, 252, 252));
painter.fillRect(m_chartRect, Qt::white);
drawGrid(painter);
drawAxes(painter);
drawCurveLine(painter, m_bestPressure, QColor(250, 74, 74));

@ -4,13 +4,14 @@
#include <QSpacerItem>
#include <QSizePolicy>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QCloseEvent>
#include <QLineEdit>
#include <QtCore/qnumeric.h>
#include "nmCalculationDllPebiSolverTask.h"
#include "nmGUIComponentLineEdit.h"
#include "nmPebiResultSnapshot.h"
#include "ZxBaseUtil.h"
nmWxForecast::nmWxForecast(QWidget *parent, nmDataAnalyzeManager* manager)
: iDlgBase(parent)
@ -35,7 +36,8 @@ nmWxForecast::nmWxForecast(QWidget *parent, nmDataAnalyzeManager* manager)
, m_extEmpty(nullptr)
{
setWindowTitle(tr("Forecast"));
setFixedSize(520, 560);
resize(560, 560);
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
// 关闭时只隐藏,所属设计面板复用同一个输入窗口。
setAttribute(Qt::WA_DeleteOnClose, false);
@ -55,24 +57,21 @@ 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<QLineEdit*> edits = m_initialPressureComponent->findChildren<QLineEdit*>();
foreach(QLineEdit* edit, edits)
connect(edit, SIGNAL(textEdited(QString)), this, SLOT(saveDataToManager()));
// 编辑内容保留在窗口中,点击生成后再提交到数据中心。
const QList<QComboBox*> units = m_initialPressureComponent->findChildren<QComboBox*>();
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(sigPebiResultSnapshotChanged()), this, SLOT(reloadInputData()));
connect(m_manager, SIGNAL(sigPebiResultStateChanged()), this, SLOT(onModelChanged()));
connect(m_manager, SIGNAL(sigForecastResultChanged()), this, SLOT(updateResultButton()));
connect(m_manager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(updateResultButton()));
connect(m_manager, SIGNAL(destroyed()), this, SLOT(close()));
}
updateResultButton();
}
nmWxForecast::~nmWxForecast()
@ -109,9 +108,17 @@ void nmWxForecast::loadDataFromManager()
m_multipleRatesRadio->blockSignals(false);
}
void nmWxForecast::reloadInputData()
{
// 保留输入窗口,模型或项目读取通知后同步数据中心配置。
if(m_calculationRunning) return;
loadDataFromManager();
updateParametersForOption();
}
void nmWxForecast::saveDataToManager()
{
// 先从界面更新数据
// 生成前统一收集输入,编辑和关闭窗口时不提交到数据中心。
updateDataFromUI();
const QList<QComboBox*> units = m_initialPressureComponent->findChildren<QComboBox*>();
@ -128,11 +135,16 @@ void nmWxForecast::saveDataToManager()
void nmWxForecast::setupUI()
{
// 与储层参数窗口统一边距,表格随窗口大小扩展。
m_mainLayout = new QVBoxLayout(this);
m_mainLayout->setContentsMargins(12, 10, 12, 0);
m_mainLayout->setSpacing(8);
// ========== Forecast options ==========
m_forecastOptionsGroup = new QGroupBox(tr("Forecast options"), this);
m_forecastOptionsLayout = new QVBoxLayout(m_forecastOptionsGroup);
m_forecastOptionsLayout->setContentsMargins(10, 10, 10, 10);
m_forecastOptionsLayout->setSpacing(8);
m_constantPressureRadio = new QRadioButton(tr("Constant pressure"), this);
m_decliningPressureRadio = new QRadioButton(tr("Declining pressure (ramp)"), this);
@ -154,10 +166,14 @@ void nmWxForecast::setupUI()
m_usePastHistoryCheck->setEnabled(false);
m_surfacePressuresCheck->setEnabled(false);
m_forecastOptionsLayout->addWidget(m_constantPressureRadio);
m_forecastOptionsLayout->addWidget(m_decliningPressureRadio);
m_forecastOptionsLayout->addWidget(m_multiplePressuresRadio);
m_forecastOptionsLayout->addWidget(m_multipleRatesRadio);
QGridLayout* optionsLayout = new QGridLayout();
optionsLayout->setHorizontalSpacing(16);
optionsLayout->setVerticalSpacing(8);
optionsLayout->addWidget(m_constantPressureRadio, 0, 0);
optionsLayout->addWidget(m_decliningPressureRadio, 0, 1);
optionsLayout->addWidget(m_multiplePressuresRadio, 1, 0);
optionsLayout->addWidget(m_multipleRatesRadio, 1, 1);
m_forecastOptionsLayout->addLayout(optionsLayout);
QHBoxLayout *checkBoxLayout = new QHBoxLayout();
checkBoxLayout->addWidget(m_usePastHistoryCheck);
@ -168,11 +184,12 @@ void nmWxForecast::setupUI()
// ========== Parameters ==========
m_parametersGroup = new QGroupBox(tr("Parameters"), this);
QVBoxLayout *parametersOuterLayout = new QVBoxLayout(m_parametersGroup);
parametersOuterLayout->setContentsMargins(6, 6, 6, 6);
parametersOuterLayout->setContentsMargins(10, 10, 10, 10);
// 基础区
QWidget *baseWidget = new QWidget(this);
m_parametersLayout = new QGridLayout(baseWidget);
m_parametersLayout->setContentsMargins(0, 0, 0, 0);
m_parametersLayout->setHorizontalSpacing(8);
m_parametersLayout->setVerticalSpacing(8);
m_parametersLayout->setColumnStretch(0, 0); // 复选框列
@ -180,11 +197,6 @@ void nmWxForecast::setupUI()
int row = 0;
// 1. 创建一个“占位符” (strut),用于永久撑开第0列
QWidget* column0Strut = new QWidget(this);
// 2. 将占位符添加到 (row 0, col 0)
m_parametersLayout->addWidget(column0Strut, row, 0);
// (row 0) Initial pressure
ConstantPressure& constantPressureData = m_forecastData.getConstantPressureData();
m_initialPressureComponent = new nmGUIComponentLineEdit(
@ -192,7 +204,23 @@ void nmWxForecast::setupUI()
true,
this
);
m_parametersLayout->addWidget(m_initialPressureComponent, row, 1);
// 标签按内容占宽,数值和单位按储层参数页的比例铺满剩余空间。
QLabel* pressureLabel = m_initialPressureComponent->findChild<QLabel*>();
// 先解除通用控件的 200 像素标签限制,再由布局按实际文字宽度分配。
pressureLabel->setMinimumWidth(0);
pressureLabel->setMaximumWidth(QWIDGETSIZE_MAX);
pressureLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
QLineEdit* pressureEdit = m_initialPressureComponent->findChild<QLineEdit*>();
QComboBox* pressureUnit = m_initialPressureComponent->findChild<QComboBox*>();
pressureEdit->setMinimumWidth(140);
pressureEdit->setMaximumWidth(QWIDGETSIZE_MAX);
pressureUnit->setMinimumWidth(86);
pressureUnit->setMaximumWidth(QWIDGETSIZE_MAX);
QHBoxLayout* pressureLayout = static_cast<QHBoxLayout*>(m_initialPressureComponent->layout());
delete pressureLayout->takeAt(1); // 移除标签后的弹性留白。
pressureLayout->setStretchFactor(pressureEdit, 8);
pressureLayout->setStretchFactor(pressureUnit, 5);
m_parametersLayout->addWidget(m_initialPressureComponent, row, 0, 1, 2);
row++;
// (row 1) Abandonment rate
@ -227,10 +255,6 @@ void nmWxForecast::setupUI()
m_parametersLayout->addWidget(m_forecastDurationComponent, row, 1);
row++;
// 设置占位符的固定宽度,使其等于复选框的宽度
int chkBoxWidth = m_abandonmentRateCheck->minimumSizeHint().width();
column0Strut->setFixedWidth(chkBoxWidth);
// (row 4) Producing pressure
m_producingPressureComponent = new nmGUIComponentLineEdit(&m_forecastData.getConstantPressureData().getProducingPressure(), true, this);
m_parametersLayout->addWidget(m_producingPressureComponent, row, 1);
@ -261,34 +285,42 @@ void nmWxForecast::setupUI()
m_extStackedWidget->addWidget(m_extMultiP); // 页面 2
m_extStackedWidget->addWidget(m_extMultiR); // 页面 3
// --- 4. 将 QStackedWidget 添加到布局的固定位置 (row 5) ---
m_parametersLayout->addWidget(m_extStackedWidget, row, 1);
row++; // row 变为 6
parametersOuterLayout->addWidget(baseWidget);
//parametersOuterLayout->addWidget(m_extStackedWidget);
// 制度表格单独分组,占用参数区下方的可用空间。
QGroupBox* scheduleGroup = new QGroupBox(tr("Multiple rates"), this);
QVBoxLayout* scheduleLayout = new QVBoxLayout(scheduleGroup);
scheduleLayout->setContentsMargins(10, 10, 10, 10);
scheduleLayout->addWidget(m_extStackedWidget);
// ========== Buttons ==========
m_buttonLayout = new QHBoxLayout();
m_buttonLayout->setContentsMargins(4, 5, 4, 5);
m_buttonLayout->setSpacing(8);
m_loadDataButton = new QPushButton(tr("Load data"), this);
m_generateButton = new QPushButton(tr("Generate"), this);
// 只有一张预测结果图,直接打开已有结果,不重新求解。
m_resultPlotButton = new QPushButton(tr("Result plots"), this);
m_resultPlotButton->setObjectName("nmForecastResultPlots");
m_cancelButton = new QPushButton(tr("Cancel"), this);
m_loadDataButton->setEnabled(false);
m_generateButton->setIcon(zxLoadIcon("OK"));
m_cancelButton->setIcon(zxLoadIcon("Cancel"));
m_buttonLayout->addWidget(m_loadDataButton);
m_buttonLayout->addStretch();
m_buttonLayout->addWidget(m_resultPlotButton);
m_buttonLayout->addWidget(m_generateButton);
m_buttonLayout->addWidget(m_cancelButton);
// ========== Main ==========
m_mainLayout->addWidget(m_forecastOptionsGroup);
m_mainLayout->addWidget(m_parametersGroup);
m_mainLayout->addStretch();
m_mainLayout->addWidget(scheduleGroup, 1);
m_mainLayout->addLayout(m_buttonLayout);
// 收集所有组件
QList<nmGUIComponentLineEdit*> allComponents;
allComponents.append(m_initialPressureComponent);
allComponents.append(m_abandonmentRateComponent);
allComponents.append(m_maximumRateComponent);
allComponents.append(m_forecastDurationComponent);
@ -524,6 +556,7 @@ void nmWxForecast::setupConnections()
// Buttons
connect(m_loadDataButton, SIGNAL(clicked()), this, SLOT(onLoadDataClicked()));
connect(m_generateButton, SIGNAL(clicked()), this, SLOT(onGenerateClicked()));
connect(m_resultPlotButton, SIGNAL(clicked()), this, SIGNAL(sigShowForecastResult()));
connect(m_cancelButton, SIGNAL(clicked()), this, SLOT(onCancelClicked()));
// Pressure Table
@ -634,6 +667,10 @@ void nmWxForecast::updateParametersForOption()
const nmDataForecast::ForecastOption option = m_forecastData.getForecastOption();
// 隐藏不适用的整行,避免多段产量模式留下空白占位。
m_abandonmentStack->setVisible(option != nmDataForecast::MultipleRatesOption);
m_maximumStack->setVisible(option != nmDataForecast::MultipleRatesOption);
// 默认显示所有基本组件
if (m_abandonmentRateCheck) m_abandonmentRateCheck->setVisible(true);
if (m_abandonmentStack) m_abandonmentStack->setCurrentIndex(0);
@ -1182,6 +1219,18 @@ void nmWxForecast::onForecastFinished()
m_forecastData.setResult(QSharedPointer<nmDataForecast::Result>());
}
void nmWxForecast::updateResultButton()
{
// 与结果绘图入口保持相同的有效性条件,读取项目后即可查看已保存的结果。
const QSharedPointer<nmDataForecast::Result> result = m_manager && m_manager->getForecastData()
? m_manager->getForecastData()->getResult() : QSharedPointer<nmDataForecast::Result>();
const QSharedPointer<const nmPebiResultSnapshot> snapshot = m_manager
? m_manager->getPebiResultSnapshot() : QSharedPointer<const nmPebiResultSnapshot>();
m_resultPlotButton->setEnabled(!result.isNull() && !snapshot.isNull() &&
result->snapshotId == snapshot->getSnapshotId() &&
result->wellInstanceId == m_manager->getCurrentResultWellInstanceId());
}
void nmWxForecast::onModelChanged()
{
// 模型或输入改变时停止本次计算;已显示的结果由所属主界面清理。

@ -200,9 +200,10 @@ nmWxNumericalDesign::nmWxNumericalDesign(
m_pApplyUnconsolidationIconButton->setVisible(bDebugOptionsVisible);
m_pUseDFNCheck->setVisible(bDebugOptionsVisible);
m_pNumericalSettingsGroup->setVisible(bDebugOptionsVisible);
// 暂时隐藏预测和敏感性分析入口。
m_pForecastBtn->setVisible(false);
m_pSensitiveBtn->setVisible(false);
// 预测配置界面在 Debug 和 Release 下均显示。
m_pForecastBtn->setVisible(true);
// 敏感性分析配置界面在 Debug 和 Release 下均显示。
m_pSensitiveBtn->setVisible(true);
}
nmWxNumericalDesign::~nmWxNumericalDesign()
@ -1450,6 +1451,8 @@ void nmWxNumericalDesign::onForecastClicked()
// 关闭和生成后均保留输入窗口,只在首次使用时创建并连接任务信号。
if(m_pForecastDialog.isNull()) {
m_pForecastDialog = new nmWxForecast(this, m_pDataManager);
// 设置窗口只请求查看,绘图仍交给所属主窗口。
connect(m_pForecastDialog, SIGNAL(sigShowForecastResult()), this, SIGNAL(sigShowForecastResult()));
connect(m_pForecastDialog, SIGNAL(sigForecastTaskReady(nmWxForecast*,nmCalculationDllPebiSolverTask*)),
this, SIGNAL(sigForecastTaskReady(nmWxForecast*,nmCalculationDllPebiSolverTask*)));
}
@ -1476,11 +1479,17 @@ void nmWxNumericalDesign::onSensitiveClicked()
// 没有模型时直接阻止进入,正常交互由按钮置灰表达,不再等生成分析时弹框。
updateSensitiveButtonEnabled();
if(!m_pSensitiveBtn->isEnabled()) return;
nmWxSensitive* dlg = new nmWxSensitive(this, m_pDataManager);
// 按数值生成按钮的现有路径,将任务请求转发给所属主窗口。
connect(dlg, SIGNAL(sigSensitivityTaskReady(nmWxSensitive*,nmCalculationDllPebiSolverTask*)),
this, SIGNAL(sigSensitivityTaskReady(nmWxSensitive*,nmCalculationDllPebiSolverTask*)));
dlg->show();
// 每个分析只创建一个输入窗口,关闭后隐藏,重新打开直接激活。
if(m_pSensitiveDialog.isNull()) {
m_pSensitiveDialog = new nmWxSensitive(this, m_pDataManager);
connect(m_pSensitiveDialog, SIGNAL(sigShowSensitivityResult(int)), this, SIGNAL(sigShowSensitivityResult(int)));
connect(m_pSensitiveDialog, SIGNAL(sigSensitivityTaskReady(nmWxSensitive*,nmCalculationDllPebiSolverTask*)),
this, SIGNAL(sigSensitivityTaskReady(nmWxSensitive*,nmCalculationDllPebiSolverTask*)));
}
if(m_pSensitiveDialog->isMinimized()) m_pSensitiveDialog->showNormal();
else m_pSensitiveDialog->show();
m_pSensitiveDialog->raise();
m_pSensitiveDialog->activateWindow();
}
// 实现通知方法

@ -2,6 +2,9 @@
#include "nmDataAnalyzeManager.h"
#include "nmWxAutomaticFitting.h"
#include "nmParameterField.h"
#include "ZxBaseUtil.h"
#include <QGroupBox>
#include "nmDataSensitive.h"
#include "nmDataReservoir.h"
#include "nmDataWellBase.h"
@ -122,21 +125,33 @@ QString samplingBaseUnit(const QString& name)
}
bool convertSamplingValue(const QString& name, double value,
QString from, QString to, double& converted)
{
// 优先复用项目单位组;旧配置的 psi 按绝对压力单位迁移。
if (from == "psi") from = "psia";
if (to == "psi") to = "psia";
QString from, QString to, double& converted, bool difference = false)
{
// 标准差是差值,单位换算只应用比例,不带入表压和绝压之间的偏移。
if (difference) {
double zero = 0.0;
if (!convertSamplingValue(name, 0.0, from, to, zero) ||
!convertSamplingValue(name, value, from, to, converted)) return false;
converted -= zero;
return qIsFinite(converted);
}
// 优先使用框架单位组中的原始名称,兼容下拉框内的全部单位。
if (from == "dimensionless") from.clear();
if (to == "dimensionless") to.clear();
if (from == to || (from.isEmpty() && to.isEmpty())) {
converted = value;
return qIsFinite(converted);
}
// 与井参数编辑共用名称解析,使 m^3/MPa 等旧写法也能参与框架换算。
from = nmParameterField::resolveUnitSpelling(from);
to = nmParameterField::resolveUnitSpelling(to);
iUnitGroup* group = iUnitHelper::getUnitGroupByUnit(from);
int digit = 0;
if (group && group->indexOf(to) >= 0 &&
group->convert(from, value, to, converted, digit)) return qIsFinite(converted);
// 旧配置未被框架识别时,沿用业务层的绝对压力单位换算。
if (from == "psi") from = "psia";
if (to == "psi") to = "psia";
if (name == "h") converted = nmDataAttribute::convertLengthUnit(value, from, to);
else if (name == "Pi") converted = nmDataAttribute::convertPressureUnit(value, from, to);
else if (name == "k") converted = nmDataAttribute::convertPermeabilityUnit(value, from, to);
@ -227,6 +242,18 @@ public:
highlightRun(-1);
}
QSharedPointer<nmDataSensitive::BatchResult> results() const { return m_results; }
void setResults(const QSharedPointer<nmDataSensitive::BatchResult>& results)
{
// 重置行数与列数,旧批次的选择和高亮不能带到新结果。
highlightRun(-1);
beginResetModel();
m_pendingVisibility.clear();
m_results = results;
endResetModel();
}
QList<ZxObjCurve*> runCurves(int row) const
{
// 双对数图的一组包含压力与导数,历史压力图只包含压力。
@ -257,7 +284,7 @@ public:
int columnCount(const QModelIndex& parent = QModelIndex()) const override
{
return parent.isValid() ? 0 : 3 + (m_statistics ? m_results->m_fitParameters.size() : 0);
return parent.isValid() ? 0 : 3 + (m_statistics && !m_results.isNull() ? m_results->m_fitParameters.size() : 0);
}
QVariant headerData(int section, Qt::Orientation orientation, int role) const override
@ -278,7 +305,7 @@ public:
}
if(role != Qt::DisplayRole) return QVariant();
if(m_statistics) {
if(section >= 3 && section - 3 < m_results->m_fitParameters.size()) {
if(!m_results.isNull() && section >= 3 && section - 3 < m_results->m_fitParameters.size()) {
const nmDataAttribute& parameter = m_results->m_fitParameters[section - 3];
return parameter.getName() + (parameter.getUnit().isEmpty() ? QString()
: " [" + parameter.getUnit() + "]");
@ -474,7 +501,7 @@ nmWxSensitivityShow::nmWxSensitivityShow(
: iDlgBase(parent)
{
// 沿用原 Results 的白色表格及按钮布局,勾选在 Apply 或 OK 后生效。
setAttribute(Qt::WA_DeleteOnClose);
setAttribute(Qt::WA_DeleteOnClose, false);
setObjectName("nmSensitivityShowDialog");
resize(700, 380);
QVBoxLayout* layout = new QVBoxLayout(this);
@ -522,10 +549,20 @@ nmWxSensitivityShow::nmWxSensitivityShow(
void nmWxSensitivityShow::done(int result)
{
// 确认、取消及关闭都结束临时高亮,不改变已应用的显示选择。
m_model->highlightRun(-1);
setResults(m_model->results());
iDlgBase::done(result);
}
void nmWxSensitivityShow::setResults(const QSharedPointer<nmDataSensitive::BatchResult>& results)
{
// 保留窗口和模型对象,重新打开时取消未应用的勾选和临时高亮。
m_highlight->blockSignals(true);
m_highlight->setChecked(false);
m_highlight->blockSignals(false);
m_model->setResults(results);
updateHighlight();
}
void nmWxSensitivityShow::onApply()
{
// 应用选择,保留窗口以便继续调整。
@ -559,7 +596,7 @@ nmWxSensitivityHistogramOptions::nmWxSensitivityHistogramOptions(
: iDlgBase(parent)
{
// 复用运行表格;第四种在误差后追加拟合参数,选择变量后按原有 Apply/OK 流程生效。
setAttribute(Qt::WA_DeleteOnClose);
setAttribute(Qt::WA_DeleteOnClose, false);
setObjectName("nmHistogramOptions");
setWindowTitle(tr("Histogram options"));
resize(720, 400);
@ -579,7 +616,7 @@ nmWxSensitivityHistogramOptions::nmWxSensitivityHistogramOptions(
m_variable->setCurrentIndex(m_variable->findData(results->m_nHistogramVariable));
options->addWidget(m_variable);
layout->addLayout(options);
QTableView* table = new QTableView(this);
QTableView* table = m_table = new QTableView(this);
table->setStyleSheet("QTableView { background-color: white; }");
table->setModel(new SensitivityResultsModel(results, nullptr, table, true));
table->setSelectionBehavior(QAbstractItemView::SelectRows);
@ -605,6 +642,22 @@ nmWxSensitivityHistogramOptions::nmWxSensitivityHistogramOptions(
connect(cancel, SIGNAL(clicked()), this, SLOT(reject()));
}
void nmWxSensitivityHistogramOptions::setResults(const QSharedPointer<nmDataSensitive::BatchResult>& results)
{
// 复用表格模型,同时恢复当前批次已应用的分箱和统计变量。
static_cast<SensitivityResultsModel*>(m_table->model())->setResults(results);
m_variable->clear();
m_variable->addItem(tr("Goodness of fit"), -1);
if(!results.isNull()) {
for(int i = 0; i < results->m_fitParameters.size(); ++i)
m_variable->addItem(results->m_fitParameters[i].getName(), i);
m_variable->setCurrentIndex(m_variable->findData(results->m_nHistogramVariable));
m_bins->setValue(results->m_nHistogramBins);
}
for(int i = 2; i < m_table->model()->columnCount(); ++i)
m_table->horizontalHeader()->setResizeMode(i, QHeaderView::ResizeToContents);
}
void nmWxSensitivityHistogramOptions::onApply()
{
// 分箱数与横轴变量一起提交,取消不会撤销已经应用的设置。
@ -625,7 +678,7 @@ nmWxSensitivityScatter::nmWxSensitivityScatter(
{
// 散点与基准均来自同一批次快照,不读取后来编辑的模型参数。
setObjectName("nmSensitivityScatterPlot");
setAttribute(Qt::WA_DeleteOnClose);
setAttribute(Qt::WA_DeleteOnClose, false);
setWindowTitle(tr("Scatter plot"));
resize(850, 550);
QVBoxLayout* layout = new QVBoxLayout(this);
@ -675,6 +728,42 @@ nmWxSensitivityScatter::nmWxSensitivityScatter(
updateParameter();
}
void nmWxSensitivityScatter::setResults(const QSharedPointer<nmDataSensitive::BatchResult>& results, double baseError)
{
// 同一批次保留坐标选择,新批次先清空旧图元与统计引用。
if(m_results == results && !results.isNull()) return;
m_results = results;
m_baseError = baseError;
m_draggingThreshold = false;
m_points.clear();
m_runRows.clear();
for(int i = m_plot->m_pPlot->getObjCount() - 1; i >= 0; --i)
if(qobject_cast<ZxObjCurve*>(m_plot->m_pPlot->getObjByIndex(i))) m_plot->m_pPlot->removeObjByIndex(i);
m_plot->clearUndoStack();
m_plot->setCurveData(QVector<iCurveDesc>());
m_plot->runUpdate();
m_parameter->blockSignals(true);
m_ordinate->blockSignals(true);
m_parameter->clear();
m_ordinate->clear();
m_ordinate->addItem(tr("Goodness of fit"), -1);
m_defaultThreshold = 0.0;
if(!results.isNull()) {
for(int i = 0; i < results->m_variables.size(); ++i)
if(results->m_variables[i].m_enabled) m_parameter->addItem(results->m_variables[i].displayName(), i);
for(int i = 0; i < results->m_fitParameters.size(); ++i)
m_ordinate->addItem(results->m_fitParameters[i].getName(), i);
for(size_t i = 0; i < results->m_runs.size(); ++i)
if(hasSensitivityError(results->m_runs[i])) m_defaultThreshold = qMax(m_defaultThreshold, results->m_runs[i].m_dPressureRmse);
if(qIsFinite(baseError) && baseError >= 0.0) m_defaultThreshold = baseError;
m_threshold = results->m_dFilterThreshold >= 0.0 ? results->m_dFilterThreshold : m_defaultThreshold;
}
m_parameter->blockSignals(false);
m_ordinate->blockSignals(false);
if(!results.isNull()) updateParameter();
else m_legend->clear();
}
void nmWxSensitivityScatter::updateParameter()
{
// 两轴切换都从本批结果重建坐标;拟合参数轴不使用误差阈值。
@ -910,7 +999,7 @@ protected:
{
// 蓝线为累积概率,红色虚线为按峰值归一化的概率密度,与 Saphir 的显示一致。
QPainter painter(this);
painter.fillRect(rect(), palette().color(QPalette::Window));
painter.fillRect(rect(), Qt::white);
painter.setRenderHint(QPainter::Antialiasing);
QVector<double> x(201), cumulativeValues(201);
const bool bounded = m_variable.m_distribution >= nmDataSensitive::VariableSampling::TRUNCATED_NORMAL;
@ -931,7 +1020,7 @@ protected:
painter.drawText(rect(), Qt::AlignCenter, nmWxSensitive::tr("Invalid distribution parameters"));
return;
}
// 坐标外侧沿用窗口底色,绘图区留白,标题放在纵轴左侧。
// 坐标内外统一使用白底,标题放在纵轴左侧。
const QRectF plot(76, 12, width() - 96, height() - 66);
painter.fillRect(plot, Qt::white);
const QRectF dataArea = plot.adjusted(12, 16, -12, -16);
@ -1098,6 +1187,8 @@ public:
m_total(data.getTotalModelCount()), m_first(0), m_selected(0),
m_visibleRows(1), m_updating(false), m_wheelDelta(0)
{
// 预览只持有采样配置,不保留计算结果批次。
m_data.setResults(QSharedPointer<nmDataSensitive::BatchResult>());
// 使用现有表格控件绘制表头、行选中和单元格,滚动条控制完整列表。
setFrameShape(QFrame::StyledPanel);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
@ -1339,8 +1430,11 @@ private:
nmWxSensitive::nmWxSensitive(QWidget *parent, nmDataAnalyzeManager* manager)
: iDlgBase(parent), m_selectedValueIndex(0), m_loading(true),
m_pDataManager(manager ? manager : nmDataAnalyzeManager::getCurrentInstance()),
m_sourceData(m_pDataManager ? m_pDataManager->getSensitiveData() : nullptr),
m_previewView(nullptr), m_pSensitivityTask(nullptr), m_calculationRunning(false)
{
// 关闭只隐藏,由所属分析统一销毁,重新打开保留当前输入。
setAttribute(Qt::WA_DeleteOnClose, false);
// 显式绑定所属分析窗口,后续切换窗口不会改变配置保存目标。
if (m_pDataManager) m_sensitiveData = m_pDataManager->getSensitiveDataCopy();
refreshModelValues();
@ -1375,7 +1469,11 @@ void nmWxSensitive::reject()
{
// Esc 与取消按钮使用相同的停止路径,保留已完成组供结束后查看。
if(m_calculationRunning) onCancelClicked();
else iDlgBase::reject();
else {
foreach(QDialog* dialog, findChildren<QDialog*>())
if(dialog->isWindow()) dialog->close();
iDlgBase::reject();
}
}
void nmWxSensitive::closeEvent(QCloseEvent* event)
@ -1443,7 +1541,9 @@ void nmWxSensitive::refreshModelValues()
continue;
}
const QString baseUnit = samplingBaseUnit(name);
QString displayUnit = attribute->getUnit();
// 已保存的采样显示单位独立于模型单位,重开及读取项目时继续沿用。
QString displayUnit = !fresh && m_sensitiveData.usesBaseUnits()
? variable.getModelValue().getUnit() : attribute->getUnit();
if (displayUnit == "dimensionless") displayUnit.clear();
double converted = 0.0;
if (!convertSamplingValue(name, attribute->getValue().toDouble(), baseUnit, displayUnit, converted))
@ -1527,12 +1627,35 @@ void nmWxSensitive::refreshModelValues()
void nmWxSensitive::onModelResultChanged()
{
// 通知后按当前模型刷新数值;未改变的参数保留用户范围。
// 项目读取替换数据对象时重新加载;普通模型刷新保留当前未提交的采样输入。
reloadInputData();
}
void nmWxSensitive::reloadInputData()
{
if(m_calculationRunning) return;
m_loading = true;
if(m_pDataManager && m_sourceData != m_pDataManager->getSensitiveData())
m_sensitiveData = m_pDataManager->getSensitiveDataCopy();
m_sourceData = m_pDataManager ? m_pDataManager->getSensitiveData() : nullptr;
foreach(QDialog* dialog, findChildren<QDialog*>())
if(dialog->isWindow()) dialog->close();
delete m_previewView;
m_previewView = nullptr;
refreshModelValues();
loadDataToUI(true);
// 井列表可能改变,重建参数页,保留外层输入窗口及底部菜单。
while(detailStack->count() > 1) delete detailStack->widget(1);
m_modeCombos.clear(); m_logChecks.clear(); m_numSpins.clear();
m_modelSpins.clear(); m_minSpins.clear(); m_maxSpins.clear();
m_valueTables.clear(); m_unitCombos.clear();
m_numLabels.clear(); m_minLabels.clear(); m_maxLabels.clear();
m_distributionCombos.clear(); m_distributionPanels.clear(); m_distributionPlots.clear();
populateTreeWidget();
createDetailPages();
loadDataToUI();
m_loading = false;
updateModelCount();
onTreeItemChanged(nullptr, 0);
onResultsChanged();
}
void nmWxSensitive::loadDataToUI(bool refreshRangesOnly)
@ -1566,9 +1689,17 @@ void nmWxSensitive::loadDataToUI(bool refreshRangesOnly)
m_numSpins[idx]->setValue(qMax(1, variable.getNumber()));
}
// 已打开窗口沿用原显示单位,避免手动表格的数值被换成另一种单位解释。
const QString unit = refreshRangesOnly ? m_unitLabels[idx]->text() : variable.getModelValue().getUnit();
const QString unit = nmParameterField::resolveUnitSpelling(
refreshRangesOnly ? m_unitCombos[idx]->currentText() : variable.getModelValue().getUnit());
variable.getModelValue().setUnit(unit);
m_unitLabels[idx]->setText(unit);
QComboBox* unitCombo = m_unitCombos[idx];
unitCombo->clear();
iUnitGroup* unitGroup = iUnitHelper::getUnitGroupByUnit(
nmParameterField::resolveUnitSpelling(samplingBaseUnit(name)));
if (unitGroup) unitCombo->addItems(unitGroup->getAllUnitNames());
if (unitCombo->findText(unit) < 0) unitCombo->addItem(unit);
unitCombo->setCurrentIndex(unitCombo->findText(unit));
unitCombo->setVisible(!unit.isEmpty());
double model = variable.getModelValue().getValue().toDouble();
double minimum = variable.getMinValue().getValue().toDouble();
double maximum = variable.getMaxValue().getValue().toDouble();
@ -1583,7 +1714,7 @@ void nmWxSensitive::loadDataToUI(bool refreshRangesOnly)
variable.m_distributionMin, variable.m_distributionMax};
for(int field = 0; field < 5; ++field) {
double display = distributionValues[field];
convertSamplingValue(name, display, samplingBaseUnit(name), unit, display);
convertSamplingValue(name, display, samplingBaseUnit(name), unit, display, field == 1);
setSamplingEditValue(m_distributionPanels[idx]->findChild<QLineEdit*>(distributionFields[field]), display);
}
double displayMin = variable.getModelValue().getMinValue().toDouble();
@ -1689,8 +1820,9 @@ bool nmWxSensitive::saveDataFromUI()
const QString name = variable.getVarName();
const int idx = findVariableIndex(variable);
if (idx < 0 || !isSupportedSamplingVariable(name)) continue;
const QString unit = m_unitLabels[idx]->text();
const QString unit = m_unitCombos[idx]->currentText();
const QString baseUnit = samplingBaseUnit(name);
variable.getModelValue().setUnit(unit);
bool valid = true;
if(candidate.getCalculationType() == nmDataSensitive::CALC_MONTE_CARLO) {
double median = 0.0;
@ -1764,15 +1896,21 @@ void nmWxSensitive::setupUI()
{
setWindowTitle(tr("Sensitivity"));
setMinimumSize(900, 550);
// 默认放大窗口,同时为屏幕边缘和窗口标题栏留出空间。
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
// 适当加宽窗口,为整页单位下拉框和分布图保留空间。
const QSize available = QApplication::desktop()->availableGeometry(this).size() - QSize(60, 80);
resize(QSize(1250, 760).boundedTo(available));
resize(QSize(1120, 660).boundedTo(available));
// 沿用储层参数窗口的分组框、内容边距和底部按钮间距。
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(12, 10, 12, 0);
mainLayout->setSpacing(8);
// ===== 顶部:Calculation type =====
QHBoxLayout *calcTypeLayout = new QHBoxLayout();
QLabel *calcTypeLabel = new QLabel(tr("Calculation type:"), this);
QGroupBox* calcTypeGroup = new QGroupBox(tr("Calculation type:"), this);
QHBoxLayout *calcTypeLayout = new QHBoxLayout(calcTypeGroup);
calcTypeLayout->setContentsMargins(10, 10, 10, 10);
calcTypeLayout->setSpacing(8);
calculationTypeCombo = new QComboBox(this);
calculationTypeCombo->addItem(tr("Deterministic"), nmDataSensitive::CALC_DETERMINISTIC);
calculationTypeCombo->addItem(tr("Deterministic multivariate"), nmDataSensitive::CALC_DETERMINISTIC_MULTIVARIATE);
@ -1784,58 +1922,20 @@ void nmWxSensitive::setupUI()
m_improveSettingsButton->setFixedHeight(calculationTypeCombo->sizeHint().height());
m_improveSettingsButton->hide();
calcTypeLayout->addWidget(calcTypeLabel);
calcTypeLayout->addWidget(calculationTypeCombo);
calcTypeLayout->addWidget(m_improveSettingsButton);
calcTypeLayout->addStretch();
mainLayout->addLayout(calcTypeLayout);
mainLayout->addWidget(calcTypeGroup);
// ===== 中间:左 Variables 面板 + 右 detailStack =====
QSplitter *centerSplitter = new QSplitter(Qt::Horizontal, this);
centerSplitter->setChildrenCollapsible(false); // 不允许把某一侧完全折叠
centerSplitter->setHandleWidth(4); // 中间拖动条宽度
centerSplitter->setStyleSheet(
"QSplitter::handle:horizontal {"
" background-color: #C0C0C0;"
"}"
"QSplitter::handle:horizontal:hover {"
" background-color: #A0A0A0;"
"}"
);
// --- 左侧:Variables 面板(QFrame + 标题栏 + Tree)---
QFrame *variablesPanel = new QFrame(this);
variablesPanel->setFrameShape(QFrame::StyledPanel);
variablesPanel->setFrameShadow(QFrame::Raised);
variablesPanel->setMinimumWidth(230);
// 左侧参数树使用标准分组框,样式交给主软件统一处理。
QGroupBox *variablesPanel = new QGroupBox(tr("Variables"), this);
variablesPanel->setMinimumWidth(200);
QVBoxLayout *variablesPanelLayout = new QVBoxLayout(variablesPanel);
variablesPanelLayout->setContentsMargins(0, 0, 0, 0);
variablesPanelLayout->setSpacing(0);
// 标题栏
QWidget *varHeader = new QWidget(variablesPanel);
varHeader->setFixedHeight(24);
varHeader->setAutoFillBackground(true);
{
QPalette pal = varHeader->palette();
pal.setColor(QPalette::Window,
palette().color(QPalette::Midlight));
varHeader->setPalette(pal);
}
QHBoxLayout *varHeaderLayout = new QHBoxLayout(varHeader);
varHeaderLayout->setContentsMargins(6, 0, 6, 0);
varHeaderLayout->setSpacing(0);
QLabel *varTitle = new QLabel(tr("Variables"), varHeader);
varTitle->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
QFont f = varTitle->font();
f.setBold(true);
varTitle->setFont(f);
varHeaderLayout->addWidget(varTitle);
varHeaderLayout->addStretch();
variablesPanelLayout->setContentsMargins(10, 10, 10, 10);
// Tree 区域
variablesTree = new QTreeWidget(variablesPanel);
@ -1843,7 +1943,6 @@ void nmWxSensitive::setupUI()
variablesTree->setFrameShape(QFrame::NoFrame); // 外层已经有 frame
variablesTree->setMinimumHeight(350);
variablesPanelLayout->addWidget(varHeader);
variablesPanelLayout->addWidget(variablesTree, 1);
// 把左侧 panel 加进 splitter
@ -1869,12 +1968,14 @@ void nmWxSensitive::setupUI()
// 左边宽度基本固定,右边自适应
centerSplitter->setStretchFactor(0, 0);
centerSplitter->setStretchFactor(1, 1);
centerSplitter->setSizes(QList<int>() << 260 << 960);
centerSplitter->setSizes(QList<int>() << 220 << 860);
mainLayout->addWidget(centerSplitter, 1);
// ===== 底部:Total number of models + 按钮 =====
QHBoxLayout *bottomLayout = new QHBoxLayout();
bottomLayout->setContentsMargins(4, 5, 4, 5);
bottomLayout->setSpacing(8);
QLabel *totalLabel = new QLabel(tr("Total number of models:"), this);
modelCountLabel = new QLabel(tr("0"), this);
@ -1898,7 +1999,20 @@ void nmWxSensitive::setupUI()
generateButton = new QPushButton(tr("Generate"), this);
cancelButton = new QPushButton(tr("Cancel"), this);
generateButton->setIcon(zxLoadIcon("OK"));
cancelButton->setIcon(zxLoadIcon("Cancel"));
// 查看已有计算结果,不改变采样配置,也不重新求解。
m_resultPlotsButton = new QPushButton(tr("Result plots"), this);
m_resultPlotsButton->setObjectName("nmSensitivityResultPlots");
QMenu* resultMenu = new QMenu(m_resultPlotsButton);
resultMenu->addAction(tr("Loglog plot"))->setData(LoglogResult);
resultMenu->addAction(tr("History plot"))->setData(HistoryResult);
m_histogramResultAction = resultMenu->addAction(tr("Histogram"));
m_histogramResultAction->setData(HistogramResult);
m_resultPlotsButton->setMenu(resultMenu);
connect(resultMenu, SIGNAL(triggered(QAction*)), this, SLOT(onResultPlotTriggered(QAction*)));
bottomLayout->addWidget(m_resultPlotsButton);
bottomLayout->addWidget(generateButton);
bottomLayout->addWidget(cancelButton);
@ -2032,48 +2146,22 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
pageLayout->setContentsMargins(0, 0, 0, 0);
pageLayout->setSpacing(0);
// ===== 整个 Sampling 面板:QFrame + 标题栏 + 内容 =====
QFrame *samplingPanel = new QFrame(page);
samplingPanel->setFrameShape(QFrame::StyledPanel);
samplingPanel->setFrameShadow(QFrame::Raised);
// 采样页与左侧参数树统一使用标准分组框。
QGroupBox *samplingPanel = new QGroupBox(tr("Sampling %1").arg(varName), page);
QVBoxLayout *panelLayout = new QVBoxLayout(samplingPanel);
panelLayout->setContentsMargins(0, 0, 0, 0);
panelLayout->setSpacing(0);
// ---- 标题栏:Sampling Xxx ----
QWidget *header = new QWidget(samplingPanel);
header->setFixedHeight(24);
header->setAutoFillBackground(true);
{
QPalette pal = header->palette();
pal.setColor(QPalette::Window,
palette().color(QPalette::Midlight));
header->setPalette(pal);
}
QHBoxLayout *headerLayout = new QHBoxLayout(header);
headerLayout->setContentsMargins(6, 0, 6, 0);
headerLayout->setSpacing(0);
QLabel *title = new QLabel(tr("Sampling %1").arg(varName), header);
QFont tf = title->font();
tf.setBold(true);
title->setFont(tf);
headerLayout->addWidget(title);
headerLayout->addStretch();
panelLayout->addWidget(header);
panelLayout->setContentsMargins(10, 10, 10, 10);
panelLayout->setSpacing(8);
// ---- 内容区:左参数 + 右表格 ----
QWidget *content = new QWidget(samplingPanel);
QHBoxLayout *contentLayout = new QHBoxLayout(content);
contentLayout->setContentsMargins(6, 0, 6, 6);
contentLayout->setContentsMargins(0, 0, 0, 0);
contentLayout->setSpacing(12);
// 左:参数区(GridLayout)
QGridLayout *paramLayout = new QGridLayout();
paramLayout->setVerticalSpacing(6);
paramLayout->setVerticalSpacing(8);
paramLayout->setHorizontalSpacing(8);
int row = 0;
// Mode + Log?
@ -2091,7 +2179,6 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
distributionCombo->setObjectName("sensitivityDistribution_" + varName);
distributionCombo->addItems(QStringList() << tr("Normal") << tr("Lognormal")
<< tr("Truncated normal") << tr("Truncated lognormal") << tr("Uniform") << tr("Triangular"));
distributionCombo->setFixedWidth(180);
modeRowLayout->addWidget(distributionCombo);
modeRowLayout->addWidget(logCheck);
modeRowLayout->addStretch();
@ -2111,11 +2198,6 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
paramLayout->addWidget(numSpin, row, 1);
++row;
// 模式、数量与下方采样参数之间留出约三行空白。
int gapRow = row;
paramLayout->setRowMinimumHeight(gapRow, numSpin->sizeHint().height() * 3);
++row;
// Model value + 单位
QLabel *modelLabel = new QLabel(tr("Model value:"), content);
QLineEdit *modelSpin = new QLineEdit(content);
@ -2123,13 +2205,18 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
// 基准值来自真实模型,只作参照;输入框支持科学计数法和完整精度。
modelSpin->setReadOnly(true);
QLabel *unitLabel = new QLabel(tr(""), content); // ⭐ 单位也从数据加载
// 当前参数整页共用一个单位,下拉选项和换算均复用框架单位组。
QComboBox *unitCombo = new QComboBox(content);
unitCombo->setObjectName("sensitivityUnit_" + varName);
unitCombo->setMinimumWidth(86);
unitCombo->setMaximumWidth(120);
unitCombo->setFixedHeight(25);
QWidget *modelRowWidget = new QWidget(content);
QHBoxLayout *modelRowLayout = new QHBoxLayout(modelRowWidget);
modelRowLayout->setContentsMargins(0, 0, 0, 0);
modelRowLayout->addWidget(modelSpin);
modelRowLayout->addWidget(unitLabel);
modelRowLayout->addWidget(unitCombo);
modelRowLayout->addStretch();
paramLayout->addWidget(modelLabel, row, 0);
@ -2140,9 +2227,8 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
QWidget* distributionPanel = new QWidget(content);
QGridLayout* distributionLayout = new QGridLayout(distributionPanel);
distributionLayout->setContentsMargins(0, 0, 0, 0);
distributionLayout->setHorizontalSpacing(6);
distributionLayout->setHorizontalSpacing(8);
distributionLayout->setVerticalSpacing(0);
distributionLayout->setColumnMinimumWidth(0, modelLabel->sizeHint().width());
const int fieldHeight = modelSpin->sizeHint().height();
// Mean 与三角分布的 Mode 共用一行,其余字段保持固定位置。
distributionPanel->setFixedHeight(fieldHeight * 4 + 6 * 3);
@ -2182,9 +2268,17 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
paramLayout->addWidget(maxSpin, row, 1);
++row;
// 统一编辑框宽度
int fullWidth = modelSpin->sizeHint().width();
int editWidth = qMax(150, fullWidth);
// 基本参数与分布参数使用相同标签列宽、列间距和输入宽度,保证上下对齐。
QList<QLabel*> labels;
labels << modeLabel << numLabel << modelLabel << minLabel << maxLabel;
labels.append(distributionPanel->findChildren<QLabel*>());
int labelWidth = 0;
foreach(QLabel* label, labels) labelWidth = qMax(labelWidth, label->sizeHint().width());
foreach(QLabel* label, labels) label->setFixedWidth(labelWidth);
const int editWidth = qMax(180, modelSpin->sizeHint().width());
distributionCombo->setFixedWidth(editWidth);
foreach(QLineEdit* edit, distributionPanel->findChildren<QLineEdit*>())
edit->setFixedWidth(editWidth);
modeCombo->setFixedWidth(editWidth);
numSpin->setFixedWidth(editWidth);
@ -2200,7 +2294,7 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
QWidget *paramWidget = new QWidget(content);
QVBoxLayout *leftVBox = new QVBoxLayout(paramWidget);
leftVBox->setContentsMargins(0, 6, 0, 0);
leftVBox->setContentsMargins(0, 0, 0, 0);
leftVBox->addLayout(paramLayout);
leftVBox->addStretch();
@ -2212,27 +2306,40 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
headers << "" << tr("Values");
valuesTable->setHorizontalHeaderLabels(headers);
valuesTable->verticalHeader()->setVisible(false);
// 沿用编辑井的行号、隔行底色和网格样式,保留采样行的箭头标记。
valuesTable->verticalHeader()->setVisible(true);
valuesTable->verticalHeader()->setMovable(false);
valuesTable->verticalHeader()->setDefaultAlignment(Qt::AlignCenter);
valuesTable->verticalHeader()->setMinimumWidth(18);
QHeaderView *hh = valuesTable->horizontalHeader();
hh->setResizeMode(QHeaderView::Fixed);
hh->setHighlightSections(false);
// 箭头列固定,数值列在下方设置为填满加宽后的表格。
valuesTable->setColumnWidth(0, 15);
valuesTable->setStyleSheet(
"QHeaderView::section {"
" border-top: 0px;"
" border-left: 0px;"
" border-right: 1px solid lightgray;"
" border-bottom: 1px solid lightgray;"
"}"
"QTableCornerButton::section {"
" border-top: 0px;"
" border-left: 0px;"
" border-right: 1px solid lightgray;"
" border-bottom: 1px solid lightgray;"
"}"
);
valuesTable->setAlternatingRowColors(true);
valuesTable->setStyleSheet(
"QTableWidget {"
" background-color: palette(base);"
" alternate-background-color: rgb(232, 232, 232);"
" gridline-color: palette(midlight);"
" border: 1px solid palette(mid);"
"}"
"QHeaderView {"
" background-color: palette(button);"
" color: palette(button-text);"
"}"
"QHeaderView::section {"
" background-color: palette(button);"
" color: palette(button-text);"
" border: none;"
" border-right: 1px solid palette(mid);"
" border-bottom: 1px solid palette(mid);"
"}"
"QTableCornerButton::section {"
" background-color: palette(base);"
" border: none;"
" border-right: 1px solid palette(mid);"
" border-bottom: 1px solid palette(mid);"
"}");
valuesTable->horizontalHeader()->setStretchLastSection(true);
valuesTable->setEditTriggers(QAbstractItemView::DoubleClicked);
valuesTable->setSelectionBehavior(QAbstractItemView::SelectRows);
@ -2243,22 +2350,27 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
valuesTable->setItemDelegateForColumn(1, new RightAlignDelegate(valuesTable));
// 初始空表的列宽尚未稳定,直接固定容器宽度,不从未显示的表头反算。
const int tableWidth = 190;
valuesTable->setFixedWidth(tableWidth);
// 与编辑井一样使用可拖动分栏,表格和分布图共用右侧空间。
QSplitter* samplingSplitter = new QSplitter(Qt::Horizontal, content);
samplingSplitter->setChildrenCollapsible(false);
samplingSplitter->setHandleWidth(6);
// 分栏仍可拖动,但不绘制参数与图表之间的灰色竖线。
samplingSplitter->setStyleSheet("QSplitter::handle { background: transparent; border: none; }");
samplingSplitter->addWidget(paramWidget);
QWidget *tableContainer = new QWidget(content);
QWidget *tableContainer = new QWidget(samplingSplitter);
QVBoxLayout *tableLayout = new QVBoxLayout(tableContainer);
tableLayout->setContentsMargins(0, 0, 0, 0);
tableLayout->setSpacing(0);
tableLayout->addWidget(valuesTable);
tableContainer->setFixedWidth(tableWidth);
contentLayout->addWidget(paramWidget, 1);
contentLayout->addStretch();
contentLayout->addWidget(tableContainer, 0, Qt::AlignRight);
SensitivityDistributionPlot* distributionPlot = new SensitivityDistributionPlot(content);
contentLayout->addWidget(distributionPlot, 2);
valuesTable->setMinimumWidth(190);
SensitivityDistributionPlot* distributionPlot = new SensitivityDistributionPlot(tableContainer);
tableLayout->addWidget(distributionPlot);
samplingSplitter->addWidget(tableContainer);
samplingSplitter->setStretchFactor(0, 1);
samplingSplitter->setStretchFactor(1, 0);
samplingSplitter->setSizes(QList<int>() << 630 << 210);
contentLayout->addWidget(samplingSplitter);
panelLayout->addWidget(content);
pageLayout->addWidget(samplingPanel);
@ -2272,7 +2384,8 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
m_minSpins.append(minSpin);
m_maxSpins.append(maxSpin);
m_valueTables.append(valuesTable);
m_unitLabels.append(unitLabel);
m_unitCombos.append(unitCombo);
connect(unitCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onSamplingUnitChanged()));
m_numLabels.append(numLabel);
m_minLabels.append(minLabel);
m_maxLabels.append(maxLabel);
@ -2314,13 +2427,23 @@ void nmWxSensitive::updateValuesForPage(int idx)
m_maxLabels[idx]->setVisible(!monteCarlo);
m_minSpins[idx]->setVisible(!monteCarlo);
m_maxSpins[idx]->setVisible(!monteCarlo);
m_valueTables[idx]->parentWidget()->setVisible(!monteCarlo);
m_valueTables[idx]->setVisible(!monteCarlo);
m_distributionCombos[idx]->setVisible(monteCarlo);
m_distributionPanels[idx]->setVisible(monteCarlo);
m_distributionPlots[idx]->setVisible(monteCarlo);
// Monte-Carlo 参数区只占所需宽度,窗口增宽时优先扩展分布图。
QHBoxLayout* contentLayout = static_cast<QHBoxLayout*>(m_distributionPlots[idx]->parentWidget()->layout());
contentLayout->setStretch(0, monteCarlo ? 0 : 1);
// 表格采用较窄的默认宽度;分布图让参数区仅占所需宽度,避免沿用表格分栏。
// 只在布局类型切换时调整,编辑参数时保留用户拖动后的宽度。
QSplitter* splitter = static_cast<QSplitter*>(m_valueTables[idx]->parentWidget()->parentWidget());
const QVariant previousLayout = splitter->property("monteCarloLayout");
if (!previousLayout.isValid() || previousLayout.toBool() != monteCarlo) {
splitter->setProperty("monteCarloLayout", monteCarlo);
splitter->setStretchFactor(0, monteCarlo ? 0 : 1);
splitter->setStretchFactor(1, monteCarlo ? 1 : 0);
const int availableWidth = qMax(840, splitter->width() - splitter->handleWidth());
const int parameterWidth = monteCarlo ? splitter->widget(0)->minimumSizeHint().width()
: availableWidth - 210;
splitter->setSizes(QList<int>() << parameterWidth << availableWidth - parameterWidth);
}
if(monteCarlo) { updateDistributionPage(idx); return; }
// 自动模式由范围生成只读表格;手动模式保留每行值,不因选中行而重算。
QTableWidget* table = m_valueTables[idx];
@ -2339,7 +2462,7 @@ void nmWxSensitive::updateValuesForPage(int idx)
}
QVector<double> samples;
const QString name = variable.getVarName();
const QString unit = m_unitLabels[idx]->text();
const QString unit = m_unitCombos[idx]->currentText();
if (automatic) {
bool minOk = false, maxOk = false;
double minimum = samplingEditValue(m_minSpins[idx], &minOk);
@ -2380,7 +2503,7 @@ bool nmWxSensitive::readDistributionPage(int idx, nmDataSensitive::VariableSampl
// 字段转换沿用现有参数单位,仅校验当前分布实际使用的字段。
variable.m_distribution = static_cast<nmDataSensitive::VariableSampling::Distribution>(
m_distributionCombos[idx]->currentIndex());
variable.m_modelValue.setUnit(m_unitLabels[idx]->text());
variable.m_modelValue.setUnit(m_unitCombos[idx]->currentText());
double* fields[] = {&variable.m_mean, &variable.m_stdDev, &variable.m_peak,
&variable.m_distributionMin, &variable.m_distributionMax};
const int distribution = variable.m_distribution;
@ -2392,7 +2515,7 @@ bool nmWxSensitive::readDistributionPage(int idx, nmDataSensitive::VariableSampl
bool ok = false;
double value = samplingEditValue(m_distributionPanels[idx]->findChild<QLineEdit*>(distributionFields[field]), &ok);
if(!ok || !qIsFinite(value) || !convertSamplingValue(variable.m_varName, value,
m_unitLabels[idx]->text(), samplingBaseUnit(variable.m_varName), *fields[field])) return false;
m_unitCombos[idx]->currentText(), samplingBaseUnit(variable.m_varName), *fields[field], field == 1)) return false;
}
return true;
}
@ -2418,6 +2541,81 @@ void nmWxSensitive::updateDistributionPage(int idx)
}
}
void nmWxSensitive::onSamplingUnitChanged()
{
if (m_loading) return;
QComboBox* combo = qobject_cast<QComboBox*>(sender());
const int idx = m_unitCombos.indexOf(combo);
if (idx < 0) return;
for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) {
nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i);
if (findVariableIndex(variable) != idx) continue;
const QString from = variable.getModelValue().getUnit();
const QString to = combo->currentText();
if (from == to) return;
// 先换算当前界面草稿,全部成功后再更新,避免覆盖未提交的手动输入。
QList<QLineEdit*> edits;
edits << m_modelSpins[idx] << m_minSpins[idx] << m_maxSpins[idx];
for (int field = 0; field < 5; ++field)
edits << m_distributionPanels[idx]->findChild<QLineEdit*>(distributionFields[field]);
QVector<double> values(edits.size());
bool valid = true;
for (int field = 0; valid && field < edits.size(); ++field) {
bool ok = false;
const double value = samplingEditValue(edits[field], &ok);
valid = ok && convertSamplingValue(variable.m_varName, value, from, to, values[field], field == 4);
}
QTableWidget* table = m_valueTables[idx];
QVector<double> tableValues(table->rowCount());
for (int row = 0; valid && row < table->rowCount(); ++row) {
QTableWidgetItem* item = table->item(row, 1);
if (!item || item->text().isEmpty()) continue;
bool ok = false;
const double value = item->data(Qt::UserRole).isValid()
? item->data(Qt::UserRole).toDouble(&ok) : item->text().trimmed().toDouble(&ok);
valid = ok && convertSamplingValue(variable.m_varName, value, from, to, tableValues[row]);
}
if (!valid) {
combo->blockSignals(true);
combo->setCurrentIndex(combo->findText(from));
combo->blockSignals(false);
double lower = variable.getModelValue().getMinValue().toDouble();
double upper = variable.getModelValue().getMaxValue().toDouble();
convertSamplingValue(variable.m_varName, lower, samplingBaseUnit(variable.m_varName), from, lower);
convertSamplingValue(variable.m_varName, upper, samplingBaseUnit(variable.m_varName), from, upper);
QMessageBox::warning(this, tr("Sensitivity"),
tr("%1: invalid sampling values. Allowed range: [%2, %3] %4. Check the sampling settings.")
.arg(variable.m_varName).arg(formatSamplingValue(lower, 6))
.arg(formatSamplingValue(upper, 6)).arg(from));
return;
}
// 仅更新显示单位和显示值,基准采样数据及已生成的随机样本保持不变。
m_loading = true;
variable.getModelValue().setUnit(to);
for (int field = 0; field < edits.size(); ++field)
setSamplingEditValue(edits[field], values[field]);
for (int row = 0; row < table->rowCount(); ++row) {
QTableWidgetItem* item = table->item(row, 1);
if (!item || item->text().isEmpty()) continue;
item->setData(Qt::UserRole, tableValues[row]);
item->setText(formatSamplingValue(tableValues[row], 3));
}
double lower = variable.getModelValue().getMinValue().toDouble();
double upper = variable.getModelValue().getMaxValue().toDouble();
convertSamplingValue(variable.m_varName, lower, samplingBaseUnit(variable.m_varName), to, lower);
convertSamplingValue(variable.m_varName, upper, samplingBaseUnit(variable.m_varName), to, upper);
const QString rangeHint = tr("Allowed range: [%1, %2] %3")
.arg(formatSamplingValue(lower, 6)).arg(formatSamplingValue(upper, 6)).arg(to);
m_minSpins[idx]->setToolTip(rangeHint);
m_maxSpins[idx]->setToolTip(rangeHint);
updateValuesForPage(idx);
m_loading = false;
return;
}
}
void nmWxSensitive::onSamplingEditingFinished()
{
// 输入结束后截断越界范围,再按六位有效数字显示,计算仍使用原始精度。
@ -2433,7 +2631,7 @@ void nmWxSensitive::onSamplingEditingFinished()
const QString name = variable.getVarName();
if (findVariableIndex(variable) != idx) continue;
double lower = 0.0, upper = 0.0;
const QString unit = m_unitLabels[idx]->text();
const QString unit = m_unitCombos[idx]->currentText();
const QString baseUnit = samplingBaseUnit(name);
if (!samplingPhysicalRange(name, lower, upper) ||
!convertSamplingValue(name, value, unit, baseUnit, value)) return;
@ -2564,6 +2762,7 @@ void nmWxSensitive::onGenerateClicked()
}
m_pDataManager->updateSensitiveData(m_sensitiveData);
m_sourceData = m_pDataManager->getSensitiveData();
// 新任务准备成功后才替换上次临时结果,准备失败仍可保留旧结果。
delete m_pSensitivityTask;
m_pSensitivityTask = task.take();
@ -2582,7 +2781,7 @@ void nmWxSensitive::startSensitivityCalculation()
connect(m_pSensitivityTask, SIGNAL(finished()),
this, SLOT(onSensitivityFinished()), Qt::QueuedConnection);
setCalculationRunning(true);
// 仅关闭参数界面;控制器及任务仍存活,结束后继续保留临时计算结果。
// 计算期间隐藏输入窗口,任务结束后继续复用。
hide();
m_pSensitivityTask->start();
}
@ -2606,6 +2805,7 @@ void nmWxSensitive::setCalculationRunning(bool running)
detailStack->setEnabled(!running);
m_monteCarloCount->setEnabled(!running);
previewButton->setEnabled(!running);
m_resultPlotsButton->setEnabled(!running && !m_sensitiveData.getResults().isNull());
cancelButton->setEnabled(!running);
if(running) {
generateButton->setEnabled(false);
@ -2651,6 +2851,16 @@ void nmWxSensitive::onResultsChanged()
if(m_pDataManager && m_pDataManager->getSensitiveData())
results = m_pDataManager->getSensitiveData()->getResults();
m_sensitiveData.setResults(results);
m_resultPlotsButton->setEnabled(!m_calculationRunning && !results.isNull());
m_histogramResultAction->setEnabled(!results.isNull() &&
results->m_calcType == nmDataSensitive::CALC_MONTE_CARLO);
}
void nmWxSensitive::onResultPlotTriggered(QAction* action)
{
// 菜单按已生成批次开放,切换输入计算方法不改变旧结果可查看的图形。
if(!action || m_calculationRunning || m_sensitiveData.getResults().isNull()) return;
emit sigShowSensitivityResult(action->data().toInt());
}
void nmWxSensitive::onCalculationTypeChanged(int index)
@ -2672,9 +2882,16 @@ void nmWxSensitive::onImproveSettingsClicked()
QStringList sampledParameters;
foreach(const nmDataSensitive::VariableSampling& variable, m_sensitiveData.getVariables())
if(variable.m_enabled) sampledParameters.append(variable.parameterKey());
nmWxAutomaticFitting dialog(this, &settings, sampledParameters);
if(dialog.exec() == QDialog::Accepted) {
m_sensitiveData.setImproveSettings(QSharedPointer<const nmSensitivityFitSettings>(new nmSensitivityFitSettings(settings)));
if(m_improveSettingsDraft.isNull()) m_improveSettingsDraft = QSharedPointer<nmSensitivityFitSettings>(new nmSensitivityFitSettings);
*m_improveSettingsDraft = settings;
nmWxAutomaticFitting* dialog = findChild<nmWxAutomaticFitting*>("nmSensitivityImproveSettings");
if(!dialog) {
dialog = new nmWxAutomaticFitting(this, m_improveSettingsDraft.data(), sampledParameters);
dialog->setObjectName("nmSensitivityImproveSettings");
dialog->setAttribute(Qt::WA_DeleteOnClose, false);
} else dialog->reloadSensitivitySettings(sampledParameters);
if(dialog->exec() == QDialog::Accepted) {
m_sensitiveData.setImproveSettings(QSharedPointer<const nmSensitivityFitSettings>(new nmSensitivityFitSettings(*m_improveSettingsDraft)));
updateModelCount();
}
}
@ -2706,25 +2923,31 @@ void nmWxSensitive::onPreviewClicked()
// 样本已在允许范围内生成,直接显示预览。
}
// Saphir 风格的连续运行列表,组合数再大也不显示分页控件。
QDialog dialog(this);
dialog.setWindowTitle(tr("Sensitivity Runs"));
dialog.setWindowFlags(dialog.windowFlags() & ~Qt::WindowContextHelpButtonHint);
dialog.setMinimumSize(420, 360);
dialog.resize(470, 650);
QVBoxLayout* layout = new QVBoxLayout(&dialog);
layout->setContentsMargins(16, 16, 16, 12);
layout->setSpacing(8);
m_previewView = new SensitivityRunsView(m_sensitiveData, createTriangleIcon(), &dialog);
layout->addWidget(m_previewView);
QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Close, Qt::Horizontal, &dialog);
buttons->setCenterButtons(true);
buttons->button(QDialogButtonBox::Close)->setMinimumWidth(110);
layout->addWidget(buttons);
connect(buttons, SIGNAL(rejected()), &dialog, SLOT(reject()));
QDialog* dialog = findChild<QDialog*>("nmSensitivityPreview");
if(!dialog) {
dialog = new QDialog(this);
dialog->setObjectName("nmSensitivityPreview");
dialog->setAttribute(Qt::WA_DeleteOnClose, false);
dialog->setWindowTitle(tr("Sensitivity Runs"));
dialog->setWindowFlags(dialog->windowFlags() & ~Qt::WindowContextHelpButtonHint);
dialog->setMinimumSize(420, 360);
dialog->resize(470, 650);
QVBoxLayout* layout = new QVBoxLayout(dialog);
layout->setContentsMargins(16, 16, 16, 12);
layout->setSpacing(8);
QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Close, Qt::Horizontal, dialog);
buttons->setCenterButtons(true);
buttons->button(QDialogButtonBox::Close)->setMinimumWidth(110);
layout->addWidget(buttons);
connect(buttons, SIGNAL(rejected()), dialog, SLOT(reject()));
}
// 预览窗口保持不变,运行列表按本次采样重建,避免保留旧行数。
delete m_previewView;
m_previewView = new SensitivityRunsView(m_sensitiveData, createTriangleIcon(), dialog);
qobject_cast<QVBoxLayout*>(dialog->layout())->insertWidget(0, m_previewView);
connect(m_previewView->verticalScrollBar(), SIGNAL(actionTriggered(int)),
this, SLOT(onPreviewScrollAction(int)));
dialog.exec();
m_previewView = nullptr;
dialog->exec();
}
void nmWxSensitive::onPreviewScrollAction(int action)

Loading…
Cancel
Save