You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nmWTAI-Platform/Src/nmNum/nmSubWxs/nmWxSensitive.cpp

2961 lines
141 KiB
C++

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#include "nmWxSensitive.h"
#include "nmDataAnalyzeManager.h"
#include "nmWxAutomaticFitting.h"
#include "nmParameterField.h"
#include "ZxBaseUtil.h"
#include <QGroupBox>
#include "nmDataSensitive.h"
#include "nmDataReservoir.h"
#include "nmDataWellBase.h"
#include "nmDataPerforation.h"
#include "iSubWndFitting.h"
#include "mGui/mGuiAnal/iAnalRun.h"
#pragma comment(lib, "mGuiAnal.lib")
#include "nmCalculationDllPebiSolverTask.h"
#include "iUnitHelper.h"
#include "iUnitGroup.h"
#include "iSysParaHelper.h"
#include "iParameter.h"
#include "mModuleDefines.h"
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QComboBox>
#include <QLabel>
#include <QPushButton>
#include <QStackedWidget>
#include <QCheckBox>
#include <QSpinBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QApplication>
#include <QDesktopWidget>
#include <QTableView>
#include <QAbstractTableModel>
#include <QMap>
#include <QPointer>
#include "iGuiPlot.h"
#include "ZxPlot.h"
#include "ZxObjCurve.h"
#include "ZxSubAxisX.h"
#include "ZxSubAxisY.h"
#include "tCurvePlotView.h"
#include "iCurveDesc.h"
#include <QMenu>
#include <QAction>
#include <QHelpEvent>
#include <QToolTip>
#include <QTableWidget>
#include <QAbstractScrollArea>
#include <QScrollBar>
#include <QWheelEvent>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QHeaderView>
#include <QStyleOptionButton>
#include <QStyleOptionHeader>
#include <QSplitter>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QGroupBox>
#include <QList>
#include <QVariant>
#include <QString>
#include <QStringList>
#include <QAbstractSpinBox>
#include <QMessageBox>
#include <QWidget>
#include <QPainter>
#include <QPixmap>
#include <QIcon>
#include <QStyledItemDelegate>
#include <QLineEdit>
#include <QCloseEvent>
#include <QScopedPointer>
#include <new>
#include <math.h>
#include <float.h>
#include <limits.h>
namespace
{
const char* distributionFields[] = {"mean", "stdDev", "peak", "minimum", "maximum"};
QString formatSamplingValue(double value, int digits)
{
// 仅对显示值取有效数字,较大或较小的数自动使用科学计数法。
return value == 0.0 ? QString("0") : QString::number(value, 'g', digits);
}
void setSamplingEditValue(QLineEdit* edit, double value)
{
// 显示文本与原始数值分开保存,打开或保存窗口不会丢失采样精度。
const QString text = formatSamplingValue(value, 6);
edit->setProperty("samplingValue", value);
edit->setProperty("samplingText", text);
edit->setText(text);
}
double samplingEditValue(const QLineEdit* edit, bool* ok)
{
// 未修改的格式化文本读取原始值;用户的新输入按实际文本解析。
const QVariant value = edit->property("samplingValue");
if (value.isValid() && edit->text() == edit->property("samplingText").toString())
return value.toDouble(ok);
return edit->text().trimmed().toDouble(ok);
}
QString samplingBaseUnit(const QString& name)
{
// 与现有 PEBI 数据层的业务基准单位保持一致。
if (name == "Pi") return "MPa";
if (name == "k") return "mD";
if (name == "h") return "m";
if (name == "C") return "m^3/MPa";
return QString();
}
bool convertSamplingValue(const QString& name, double value,
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);
else return false;
return qIsFinite(converted);
}
bool isSupportedSamplingVariable(const QString& name)
{
return name == "Skin" || name == "C" || name == "Pi" ||
name == "k" || name == "h" || name == QString(QChar(0x03c6));
}
bool samplingPhysicalRange(const QString& name, double& minimum, double& maximum)
{
// 与自动拟合共用 NM 系统参数表,数值均为业务基准单位。
QString parameterName;
if (name == "Skin") parameterName = "Result_W_Skin";
else if (name == "C") parameterName = "Result_W_C";
else if (name == "Pi") parameterName = "Result_Pi";
else if (name == "k") parameterName = "Result_K";
else if (name == "h") parameterName = "Result_h";
else if (name == QString(QChar(0x03c6))) parameterName = "Result_phi";
else return false;
iSysParaHelper* helper = _paraHelper;
iParameter* parameter = helper ? helper->getPara(parameterName, s_Nm_Serie) : nullptr;
if (!parameter || !qIsFinite(parameter->m_dMin) || !qIsFinite(parameter->m_dMax) ||
parameter->m_dMin >= parameter->m_dMax) return false;
minimum = name == "Skin" ? 0.0 : parameter->m_dMin;
maximum = parameter->m_dMax;
return minimum < maximum;
}
bool nearlyEqualSamplingValue(double left, double right)
{
// 沿用自动拟合的八个机器精度容差,只处理边界的浮点舍入差。
return qAbs(left - right) <= DBL_EPSILON * 8.0 * qMax(qAbs(left), qAbs(right));
}
bool validSamplingValue(const QString& name, double& value)
{
// 所有采样值都受系统物理范围约束,边界末位误差归一到精确边界。
if (!qIsFinite(value)) return false;
double minimum = 0.0, maximum = 0.0;
if (!samplingPhysicalRange(name, minimum, maximum)) return false;
if (value < minimum && !nearlyEqualSamplingValue(value, minimum)) return false;
if (value > maximum && !nearlyEqualSamplingValue(value, maximum)) return false;
value = qBound(minimum, value, maximum);
return true;
}
}
namespace {
bool hasSensitivityError(const nmDataSensitive::RunResult& run)
{
// 统计只使用成功且误差有效的运行,与曲线显示勾选无关。
return run.m_bSucceeded && !run.m_bCancelled && qIsFinite(run.m_dPressureRmse) && run.m_dPressureRmse >= 0.0;
}
QString sensitivityRunParameters(const nmDataSensitive::BatchResult& results, const nmDataSensitive::RunResult& run)
{
// 表格和散点提示共用冻结参数的单位换算。
QStringList descriptions;
for(int i = 0; i < results.m_variables.size() && i < run.m_vecParameters.size(); ++i) {
const nmDataSensitive::VariableSampling& variable = results.m_variables[i];
if(!variable.m_enabled) continue;
double value = run.m_vecParameters[i];
const QString unit = variable.m_modelValue.getUnit();
convertSamplingValue(variable.m_varName, value, samplingBaseUnit(variable.m_varName), unit, value);
descriptions << variable.displayName() + " = " + formatSamplingValue(value, 3)
+ (unit.isEmpty() ? QString() : " " + unit);
}
return descriptions.join(" + ");
}
}
// ========= 已完成批次的连续结果列表 =========
class SensitivityResultsModel : public QAbstractTableModel
{
public:
SensitivityResultsModel(const QSharedPointer<nmDataSensitive::BatchResult>& results,
iGuiPlot* plot, QObject* parent, bool statistics = false)
: QAbstractTableModel(parent), m_results(results), m_plot(plot), m_statistics(statistics) {}
~SensitivityResultsModel()
{
// 直接销毁 Show 窗口时也恢复原色。
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
{
// 双对数图的一组包含压力与导数,历史压力图只包含压力。
QList<ZxObjCurve*> curves;
if(m_plot.isNull()) return curves;
const quint64 number = m_results->m_runs[row].m_nRunIndex + 1;
ZxObjCurve* pressure = m_plot->getCurve(nmWxSensitive::tr("Run %1 - Pressure").arg(number));
ZxObjCurve* derivative = m_plot->getCurve(nmWxSensitive::tr("Run %1 - Derivative").arg(number));
if(pressure) curves.append(pressure);
if(derivative) curves.append(derivative);
return curves;
}
bool isRunVisible(int row) const
{
// 从所属图读取已应用状态,两个 Show 表格互不共享勾选结果。
const QList<ZxObjCurve*> curves = runCurves(row);
for(int i = 0; i < curves.size(); ++i)
if(curves[i]->isVisible()) return true;
return false;
}
int rowCount(const QModelIndex& parent = QModelIndex()) const override
{
// QTableView 按需读取行,结果列表连续滚动,不一次性创建全部表格项。
return parent.isValid() || m_results.isNull() ? 0 : int(m_results->m_runs.size());
}
int columnCount(const QModelIndex& parent = QModelIndex()) const override
{
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
{
// 表格仅保留显示选择、运行编号与冻结参数。
if(orientation != Qt::Horizontal) return QVariant();
if(!m_statistics && section == 0 && role == Qt::CheckStateRole) {
// 全选状态只统计确实有曲线、允许勾选的运行。
int total = 0, checked = 0;
for(int row = 0; row < rowCount(); ++row) {
const QModelIndex cell = index(row, 0);
if(!(flags(cell) & Qt::ItemIsUserCheckable)) continue;
++total;
if(data(cell, Qt::CheckStateRole).toInt() == Qt::Checked) ++checked;
}
if(!total) return QVariant();
return checked == total ? Qt::Checked : checked == 0 ? Qt::Unchecked : Qt::PartiallyChecked;
}
if(role != Qt::DisplayRole) return QVariant();
if(m_statistics) {
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() + "]");
}
return (QStringList() << "#" << nmWxSensitive::tr("Realization")
<< nmWxSensitive::tr("Goodness of fit [MPa]")).value(section);
}
const QStringList titles = QStringList() << nmWxSensitive::tr("Show") << "#"
<< nmWxSensitive::tr("Parameters");
return titles.value(section);
}
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override
{
// 参数显示使用冻结配置中的单位和三位有效数字,不修改原始采样值。
if(!index.isValid() || index.row() >= rowCount()) return QVariant();
const nmDataSensitive::RunResult& run = m_results->m_runs[index.row()];
if(m_statistics) {
if(role == Qt::ToolTipRole && !run.m_sFailureReason.isEmpty()) return run.m_sFailureReason;
if(role != Qt::DisplayRole && role != Qt::ToolTipRole) return QVariant();
if(index.column() == 0) return QString::number(run.m_nRunIndex + 1);
if(index.column() == 1) return sensitivityRunParameters(*m_results, run);
double value;
return m_results->statisticValue(run, index.column() - 3, value) ? QString::number(value, 'g', 6)
: run.m_bCancelled ? nmWxSensitive::tr("Cancelled")
: !run.m_bSucceeded ? nmWxSensitive::tr("Failed") : nmWxSensitive::tr("N/A");
}
if(role == Qt::CheckStateRole && index.column() == 0)
return run.m_bSucceeded && !run.m_bCancelled &&
m_pendingVisibility.value(index.row(), isRunVisible(index.row())) ? Qt::Checked : Qt::Unchecked;
if(role == Qt::TextAlignmentRole && index.column() == 1)
return int(Qt::AlignRight | Qt::AlignVCenter);
if(role == Qt::ToolTipRole && !run.m_sFailureReason.isEmpty()) return run.m_sFailureReason;
if(role != Qt::DisplayRole && role != Qt::ToolTipRole) return QVariant();
if(index.column() == 1) return QString::number(run.m_nRunIndex + 1);
if(index.column() != 2) return QVariant();
return sensitivityRunParameters(*m_results, run);
}
Qt::ItemFlags flags(const QModelIndex& index) const override
{
// 失败和取消组只能查看,不能勾选不存在的计算曲线。
if(!index.isValid()) return Qt::NoItemFlags;
Qt::ItemFlags result = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
const nmDataSensitive::RunResult& run = m_results->m_runs[index.row()];
if(!m_statistics && index.column() == 0 && run.m_bSucceeded && !run.m_bCancelled && !runCurves(index.row()).isEmpty())
result |= Qt::ItemIsUserCheckable;
return result;
}
bool setData(const QModelIndex& index, const QVariant& value, int role) override
{
// 勾选只记在本窗口,点击 Apply 或 OK 后才改变所属图的显示。
if(m_statistics || !index.isValid() || index.column() != 0 || role != Qt::CheckStateRole) return false;
const nmDataSensitive::RunResult& run = m_results->m_runs[index.row()];
if(!run.m_bSucceeded || run.m_bCancelled || runCurves(index.row()).isEmpty()) return false;
const bool visible = value.toInt() == Qt::Checked;
if(visible == isRunVisible(index.row())) m_pendingVisibility.remove(index.row());
else m_pendingVisibility.insert(index.row(), visible);
emit dataChanged(index, index);
emit headerDataChanged(Qt::Horizontal, 0, 0);
return true;
}
void setAllVisible(bool visible)
{
// 全选与全隐藏也只改变待应用选择,不立即刷新曲线。
if(m_results.isNull() || m_results->m_runs.empty()) return;
for(int i = 0; i < rowCount(); ++i) {
const nmDataSensitive::RunResult& run = m_results->m_runs[i];
if(!run.m_bSucceeded || run.m_bCancelled || runCurves(i).isEmpty()) continue;
if(visible == isRunVisible(i)) m_pendingVisibility.remove(i);
else m_pendingVisibility.insert(i, visible);
}
emit dataChanged(index(0, 0), index(rowCount() - 1, 0));
emit headerDataChanged(Qt::Horizontal, 0, 0);
}
void applyVisibility()
{
// Apply 仅更新所属图的采样曲线,保留实测、基准和流量参考线。
QMap<int, bool>::const_iterator it = m_pendingVisibility.constBegin();
for(; it != m_pendingVisibility.constEnd(); ++it) {
const QList<ZxObjCurve*> curves = runCurves(it.key());
for(int i = 0; i < curves.size(); ++i) curves[i]->setVisible(it.value());
}
m_pendingVisibility.clear();
if(!m_plot.isNull()) {
m_plot->clearUndoStack();
m_plot->m_pPlot->update();
}
}
void highlightRun(int row)
{
// 绘图窗口已销毁时,残留图元的坐标轴也已失效,不能再修改样式。
if(m_plot.isNull()) { m_highlightStyles.clear(); return; }
// 高亮不改曲线数据或可见性,先恢复原色再淡化其他采样与基准曲线。
for(int i = 0; i < m_highlightStyles.size(); ++i) {
const HighlightStyle& style = m_highlightStyles[i];
if(style.curve.isNull()) continue;
style.curve->setPen(style.pen);
style.curve->setDot(style.dot);
}
m_highlightStyles.clear();
if(row >= 0 && row < rowCount()) {
QList<ZxObjCurve*> dimmed;
for(int other = 0; other < rowCount(); ++other) {
if(other == row) continue;
dimmed.append(runCurves(other));
}
// 双对数的基准压力、导数和历史图的基准压力共用样式恢复逻辑。
if(ZxObjCurve* base = m_plot->getCurve(nmWxSensitive::tr("Base model - Pressure"))) dimmed.append(base);
if(ZxObjCurve* base = m_plot->getCurve(nmWxSensitive::tr("Base model - Derivative"))) dimmed.append(base);
for(int i = 0; i < dimmed.size(); ++i) {
HighlightStyle style;
style.curve = dimmed[i];
style.pen = dimmed[i]->getPen();
style.dot = dimmed[i]->getDot();
m_highlightStyles.append(style);
dimmed[i]->setColor(QColor(210, 210, 210));
dimmed[i]->setDotColor(QColor(210, 210, 210));
}
// 重合曲线也能看清选中组,压力和导数一同放到上层。
const QList<ZxObjCurve*> selected = runCurves(row);
for(int i = 0; i < selected.size(); ++i) m_plot->m_pPlot->bringToFront(selected[i]);
}
if(!m_plot.isNull()) m_plot->m_pPlot->update();
}
private:
struct HighlightStyle {
QPointer<ZxObjCurve> curve;
QPen pen;
ZxDot dot;
};
QVector<HighlightStyle> m_highlightStyles;
QSharedPointer<nmDataSensitive::BatchResult> m_results;
QMap<int, bool> m_pendingVisibility;
QPointer<iGuiPlot> m_plot;
bool m_statistics;
};
class SensitivityShowHeader : public QHeaderView
{
public:
explicit SensitivityShowHeader(QWidget* parent) : QHeaderView(Qt::Horizontal, parent) {}
protected:
QSize sectionSizeFromContents(int section) const override
{
// Show 列额外留出原生复选框的宽度。
QSize size = QHeaderView::sectionSizeFromContents(section);
if(section == 0) size.rwidth() += style()->pixelMetric(QStyle::PM_IndicatorWidth) + 8;
return size;
}
void paintSection(QPainter* painter, const QRect& rect, int section) const override
{
// 仅替换第一列表头,其他列沿用原来的标题与样式。
if(section != 0) { QHeaderView::paintSection(painter, rect, section); return; }
QStyleOptionHeader header;
initStyleOption(&header);
header.rect = rect;
header.text.clear();
painter->save();
style()->drawControl(QStyle::CE_Header, &header, painter, this);
painter->restore();
QStyleOptionButton check;
check.initFrom(this);
check.rect = rect.adjusted(4, 0, -2, 0);
check.text = model()->headerData(0, Qt::Horizontal).toString();
const QVariant state = model()->headerData(0, Qt::Horizontal, Qt::CheckStateRole);
if(!state.isValid()) check.state &= ~QStyle::State_Enabled;
check.state |= state.toInt() == Qt::Checked ? QStyle::State_On
: state.toInt() == Qt::PartiallyChecked ? QStyle::State_NoChange : QStyle::State_Off;
painter->save();
style()->drawControl(QStyle::CE_CheckBox, &check, painter, this);
painter->restore();
}
void mousePressEvent(QMouseEvent* event) override
{
// 部分选中时点击全选;全选时点击取消,全程沿用待应用选择。
if(event->button() == Qt::LeftButton && logicalIndexAt(event->pos()) == 0) {
const QVariant state = model()->headerData(0, Qt::Horizontal, Qt::CheckStateRole);
if(state.isValid()) static_cast<SensitivityResultsModel*>(model())->setAllVisible(state.toInt() != Qt::Checked);
return;
}
QHeaderView::mousePressEvent(event);
}
};
nmWxSensitivityShow::nmWxSensitivityShow(
const QSharedPointer<nmDataSensitive::BatchResult>& results, iGuiPlot* plot, QWidget* parent)
: iDlgBase(parent)
{
// 沿用原 Results 的白色表格及按钮布局,勾选在 Apply 或 OK 后生效。
setAttribute(Qt::WA_DeleteOnClose, false);
setObjectName("nmSensitivityShowDialog");
resize(700, 380);
QVBoxLayout* layout = new QVBoxLayout(this);
QTableView* table = m_table = new QTableView(this);
table->setStyleSheet("QTableView { background-color: white; }");
m_model = new SensitivityResultsModel(results, plot, this);
table->setHorizontalHeader(new SensitivityShowHeader(table));
table->setModel(m_model);
table->setSelectionBehavior(QAbstractItemView::SelectRows);
table->setSelectionMode(QAbstractItemView::SingleSelection);
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
table->setWordWrap(false);
table->verticalHeader()->setVisible(false);
table->horizontalHeader()->setResizeMode(0, QHeaderView::ResizeToContents);
table->horizontalHeader()->setResizeMode(1, QHeaderView::ResizeToContents);
table->horizontalHeader()->setResizeMode(2, QHeaderView::Stretch);
layout->addWidget(table);
QHBoxLayout* buttons = new QHBoxLayout();
m_highlight = new QPushButton(tr("Highlight"), this);
m_highlight->setCheckable(true);
m_highlight->setAutoDefault(false);
m_highlight->setToolTip(tr("Highlight the selected run; click again to restore colors."));
// 明确按钮顺序,避免系统的标准按钮布局把 Apply 排到最后。
QPushButton* apply = new QPushButton(tr("Apply"), this);
QPushButton* ok = new QPushButton(tr("OK"), this);
QPushButton* cancel = new QPushButton(tr("Cancel"), this);
apply->setAutoDefault(false);
cancel->setAutoDefault(false);
ok->setDefault(true);
buttons->addWidget(m_highlight);
buttons->addStretch();
buttons->addWidget(apply);
buttons->addWidget(ok);
buttons->addWidget(cancel);
layout->addLayout(buttons);
connect(m_highlight, SIGNAL(toggled(bool)), this, SLOT(updateHighlight()));
connect(table->selectionModel(), SIGNAL(currentRowChanged(QModelIndex,QModelIndex)), this, SLOT(updateHighlight()));
connect(m_model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), this, SLOT(updateHighlight()));
connect(apply, SIGNAL(clicked()), this, SLOT(onApply()));
connect(ok, SIGNAL(clicked()), this, SLOT(onAccept()));
connect(cancel, SIGNAL(clicked()), this, SLOT(reject()));
updateHighlight();
}
void nmWxSensitivityShow::done(int result)
{
// 确认、取消及关闭都结束临时高亮,不改变已应用的显示选择。
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()
{
// 应用选择,保留窗口以便继续调整。
m_model->applyVisibility();
}
void nmWxSensitivityShow::onAccept()
{
// 确认时提交勾选,取消则直接丢弃未应用的选择。
onApply();
accept();
}
void nmWxSensitivityShow::updateHighlight()
{
// 只有当前选中且勾选显示的有效运行可以高亮,切换行时同步跟随。
const QModelIndex cell = m_model->index(m_table->currentIndex().row(), 0);
const bool enabled = (m_model->flags(cell) & Qt::ItemIsUserCheckable)
&& m_model->data(cell, Qt::CheckStateRole).toInt() == Qt::Checked;
m_highlight->setEnabled(enabled);
if(!enabled) {
m_highlight->blockSignals(true);
m_highlight->setChecked(false);
m_highlight->blockSignals(false);
}
m_model->highlightRun(enabled && m_highlight->isChecked() ? cell.row() : -1);
}
nmWxSensitivityHistogramOptions::nmWxSensitivityHistogramOptions(
const QSharedPointer<nmDataSensitive::BatchResult>& results, int bins, QWidget* parent)
: iDlgBase(parent)
{
// 复用运行表格;第四种在误差后追加拟合参数,选择变量后按原有 Apply/OK 流程生效。
setAttribute(Qt::WA_DeleteOnClose, false);
setObjectName("nmHistogramOptions");
setWindowTitle(tr("Histogram options"));
resize(720, 400);
QVBoxLayout* layout = new QVBoxLayout(this);
QHBoxLayout* options = new QHBoxLayout;
options->addWidget(new QLabel(tr("Number of bins:"), this));
m_bins = new QSpinBox(this);
m_bins->setRange(1, 1000);
m_bins->setValue(bins);
options->addWidget(m_bins);
options->addStretch();
options->addWidget(new QLabel(tr("Variable:"), this));
m_variable = new QComboBox(this);
m_variable->addItem(tr("Goodness of fit"), -1);
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));
options->addWidget(m_variable);
layout->addLayout(options);
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);
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
table->setWordWrap(false);
table->verticalHeader()->hide();
table->horizontalHeader()->setResizeMode(0, QHeaderView::ResizeToContents);
table->horizontalHeader()->setResizeMode(1, QHeaderView::Stretch);
for(int i = 2; i < table->model()->columnCount(); ++i)
table->horizontalHeader()->setResizeMode(i, QHeaderView::ResizeToContents);
layout->addWidget(table);
QHBoxLayout* buttons = new QHBoxLayout;
buttons->addStretch();
QPushButton* apply = new QPushButton(tr("Apply"), this);
QPushButton* ok = new QPushButton(tr("OK"), this);
QPushButton* cancel = new QPushButton(tr("Cancel"), this);
buttons->addWidget(apply);
buttons->addWidget(ok);
buttons->addWidget(cancel);
layout->addLayout(buttons);
connect(apply, SIGNAL(clicked()), this, SLOT(onApply()));
connect(ok, SIGNAL(clicked()), this, SLOT(onAccept()));
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()
{
// 分箱数与横轴变量一起提交,取消不会撤销已经应用的设置。
emit settingsChanged(m_bins->value(), m_variable->itemData(m_variable->currentIndex()).toInt());
}
void nmWxSensitivityHistogramOptions::onAccept()
{
// 与 Show 表格保持相同的确认行为。
onApply();
accept();
}
nmWxSensitivityScatter::nmWxSensitivityScatter(
const QSharedPointer<nmDataSensitive::BatchResult>& results, double baseError, QWidget* parent)
: iDlgBase(parent), m_results(results), m_baseError(baseError),
m_defaultThreshold(0.0), m_draggingThreshold(false)
{
// 散点与基准均来自同一批次快照,不读取后来编辑的模型参数。
setObjectName("nmSensitivityScatterPlot");
setAttribute(Qt::WA_DeleteOnClose, false);
setWindowTitle(tr("Scatter plot"));
resize(850, 550);
QVBoxLayout* layout = new QVBoxLayout(this);
layout->setContentsMargins(8, 8, 8, 8);
QHBoxLayout* options = new QHBoxLayout;
options->addWidget(new QLabel(tr("X Axis:"), this));
m_parameter = new QComboBox(this);
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);
options->addWidget(m_parameter, 1);
options->addSpacing(20);
options->addWidget(new QLabel(tr("Y Axis:"), this));
m_ordinate = new QComboBox(this);
m_ordinate->addItem(tr("Goodness of fit"), -1);
for(int i = 0; i < results->m_fitParameters.size(); ++i)
m_ordinate->addItem(results->m_fitParameters[i].getName(), i);
options->addWidget(m_ordinate, 1);
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;
layout->addLayout(options);
m_plot = new iGuiPlot(true, this);
m_plot->initUI(tr("Sensitivity scatter plot"));
m_plot->setDropEnabled(false);
m_plot->setAxisY(tr("Goodness of fit"), "MPa");
m_plot->setAxisXYLog(false, 0);
m_plot->setAxisXYLog(false, 1);
m_plot->getAxisOf(2)->setVisible(false);
// 给纵轴标题与刻度留出间距。
m_plot->getAxisOf(1)->setSpan(20.0);
m_plot->m_pPlot->setBKColor(Qt::white);
m_plot->m_pPlot->setLegendVisible(false);
layout->addWidget(m_plot);
// 两项固定图例不使用框架的分组标题,覆盖在绘图区左上角。
m_legend = new QLabel(m_plot->m_pPlotView->viewport());
m_legend->setStyleSheet("QLabel { background: white; color: black; border: 1px solid #cccccc; }");
m_legend->setAttribute(Qt::WA_TransparentForMouseEvents);
m_resultsInPlot = new QAction(tr("Results in plot"), this);
m_resultsInPlot->setCheckable(true);
connect(m_resultsInPlot, SIGNAL(toggled(bool)), this, SLOT(updateLegend()));
updateLegend();
m_plot->m_pPlotView->viewport()->installEventFilter(this);
connect(m_parameter, SIGNAL(currentIndexChanged(int)), this, SLOT(updateParameter()));
connect(m_ordinate, SIGNAL(currentIndexChanged(int)), this, SLOT(updateParameter()));
connect(m_plot->m_pPlotView, SIGNAL(sigAdjustContextMenu(QMenu*)), this, SLOT(adjustContextMenu(QMenu*)));
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()
{
// 两轴切换都从本批结果重建坐标;拟合参数轴不使用误差阈值。
m_draggingThreshold = false;
const int ordinate = m_ordinate->itemData(m_ordinate->currentIndex()).toInt();
const bool errorAxis = ordinate == -1;
const double modelY = errorAxis ? m_baseError : m_results->m_fitParameters[ordinate].getValue().toDouble();
m_plot->setAxisY(m_ordinate->currentText(), errorAxis ? "MPa" : m_results->m_fitParameters[ordinate].getUnit());
m_points.clear();
m_runRows.clear();
if(m_parameter->currentIndex() < 0) {
m_plot->setToolTip(tr("No sampled parameters"));
return;
}
const int index = m_parameter->itemData(m_parameter->currentIndex()).toInt();
const nmDataSensitive::VariableSampling& variable = m_results->m_variables[index];
const QString unit = variable.m_modelValue.getUnit();
m_plot->setAxisX(variable.displayName(), unit);
double modelValue = variable.m_modelValue.getValue().toDouble();
const bool hasModel = convertSamplingValue(variable.m_varName, modelValue,
samplingBaseUnit(variable.m_varName), unit, modelValue) && qIsFinite(modelY) && (!errorAxis || modelY >= 0.0);
for(size_t i = 0; i < m_results->m_runs.size(); ++i) {
const nmDataSensitive::RunResult& run = m_results->m_runs[i];
double ordinateValue;
if(!m_results->statisticValue(run, ordinate, ordinateValue) || index >= run.m_vecParameters.size()) continue;
double value = run.m_vecParameters[index];
if(!convertSamplingValue(variable.m_varName, value, samplingBaseUnit(variable.m_varName), unit, value)) continue;
m_points.append(QPointF(value, ordinateValue));
m_runRows.append(int(i));
}
QVector<iCurveDesc> curves;
for(int i = 0; i < 4; ++i) {
if(!errorAxis && (i == 1 || i == 3)) continue;
iCurveDesc curve;
curve.m_sCurveName = i == 0 ? tr("sensitivity data") : i == 1 ? tr("Below threshold")
: i == 2 ? tr("reference") : tr("Threshold");
curve.m_oCurveType = POT_CurveDiscrete;
curve.m_bLineVisible = false;
curve.m_bPointVisible = true;
curve.m_oDot = ZxDot(DTS_Square, i == 2 ? Qt::blue : Qt::black, 1.0, i != 0);
if(i < 2) for(int j = 0; j < m_points.size(); ++j) {
if(i == 1 && m_points[j].y() >= m_threshold) continue;
curve.m_vecX.append(m_points[j].x());
curve.m_vecY.append(m_points[j].y());
}
if(i == 2 && hasModel) {
curve.m_vecX.append(modelValue);
curve.m_vecY.append(modelY);
}
if(i == 3) {
curve.m_bLineVisible = true;
curve.m_bPointVisible = false;
curve.m_oPen = QPen(Qt::red, 0.35);
curve.m_vecX << 0.0 << 1.0;
curve.m_vecY << m_threshold << m_threshold;
}
curves.append(curve);
}
// 固定图层名称,参数切换不叠加上一参数的点,也不让旧阈值影响自动范围。
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(curves);
m_plot->runUpdate();
m_plot->setReadOnly(true);
double left = hasModel ? modelValue : m_points.isEmpty() ? 0.0 : m_points.first().x();
double right = left;
double bottom = hasModel ? modelY : m_points.isEmpty() ? 0.0 : m_points.first().y();
double top = bottom;
for(int i = 0; i < m_points.size(); ++i) {
left = qMin(left, m_points[i].x());
right = qMax(right, m_points[i].x());
bottom = qMin(bottom, m_points[i].y());
top = qMax(top, m_points[i].y());
}
const double padding = qMax(1e-6, (right > left ? right - left : qMax(1.0, qAbs(left))) * 0.05);
left -= padding;
right += padding;
if(errorAxis) {
bottom = 0.0;
top = qMax(1e-6, qMax(top, m_threshold) * 1.1);
} else {
const double margin = qMax(1e-6, (top > bottom ? top - bottom : qMax(1.0, qAbs(top))) * 0.05);
bottom -= margin;
top += margin;
}
VecDouble ticksX, ticksY;
for(int i = 0; i <= 5; ++i) {
ticksX.append(left + i * (right - left) / 5.0);
ticksY.append(bottom + i * (top - bottom) / 5.0);
}
m_plot->setAxisScaleX(left, right, ticksX);
m_plot->setAxisScaleY(bottom, top, ticksY);
updateLegend();
if(errorAxis) updateThreshold();
}
void nmWxSensitivityScatter::updateThreshold()
{
// 阈值以下的点填黑,并同步图例中的阈值显示。
if(m_parameter->currentIndex() < 0 || m_ordinate->currentIndex() != 0) return;
// 拖动时只更新图内反馈,松开后再保存阈值并刷新结果面板。
if(!m_draggingThreshold && m_results->m_dFilterThreshold != m_threshold) {
m_results->m_dFilterThreshold = m_threshold;
emit filterChanged();
}
updateLegend();
QVector<QPointF> accepted;
for(int i = 0; i < m_points.size(); ++i) if(m_points[i].y() < m_threshold) {
accepted.append(m_points[i]);
}
if(m_threshold > m_plot->getAxisOf(1)->getRangeMax()) {
VecDouble ticks;
const double top = m_threshold * 1.1;
for(int i = 0; i <= 5; ++i) ticks.append(i * top / 5.0);
m_plot->setAxisScaleY(0.0, top, ticks);
}
QVector<QPointF> line;
line << QPointF(m_plot->getAxisOf(0)->getRangeMin(), m_threshold)
<< QPointF(m_plot->getAxisOf(0)->getRangeMax(), m_threshold);
QVector<iCurveDesc> curves = m_plot->getCurveData();
// 原位更新图元及导出数据,不触发框架重新缩放,拖动时坐标轴保持稳定。
for(int i = 0; i < curves.size(); ++i) {
const QString name = curves[i].m_sCurveName;
if(name != tr("Below threshold") && name != tr("Threshold")) continue;
const QVector<QPointF>& points = name == tr("Threshold") ? line : accepted;
curves[i].m_vecX.clear();
curves[i].m_vecY.clear();
for(int j = 0; j < points.size(); ++j) {
curves[i].m_vecX.append(points[j].x());
curves[i].m_vecY.append(points[j].y());
}
ZxObjCurve* curve = m_plot->getCurve(name);
if(curve) curve->setAllValues(points);
else if(!points.isEmpty()) {
curve = m_plot->appendSeriesD(curves[i].m_vecX, curves[i].m_vecY, name);
if(curve) curve->setCurvePty(curves[i].m_oPen, curves[i].m_oDot,
curves[i].m_bLineVisible, curves[i].m_bPointVisible);
}
}
m_plot->setCurveData(curves);
// 基准点始终置顶,包括首次出现阈值以下的点时。
ZxObjCurve* reference = m_plot->getCurve(tr("reference"));
if(reference) m_plot->m_pPlot->bringToFront(reference);
m_plot->m_pPlot->update();
}
void nmWxSensitivityScatter::resetThreshold()
{
// 有基准误差时恢复至基准值,无实测数据时使用本批最大有效误差。
m_threshold = m_defaultThreshold;
updateThreshold();
}
void nmWxSensitivityScatter::adjustContextMenu(QMenu* menu)
{
// 复用绘图右键菜单,保留框架的缩放、复制与导出入口。
if(m_ordinate->currentIndex() != 0) return;
menu->addAction(m_resultsInPlot);
menu->addAction(tr("Reset line"), this, SLOT(resetThreshold()));
}
void nmWxSensitivityScatter::updateLegend()
{
// 勾选后在原有两项图例下追加阈值,拖线和重置时实时更新。
QString text = "<table cellspacing='2'><tr><td>&#9633;</td><td>" + tr("sensitivity data")
+ "</td></tr><tr><td style='color:blue'>&#9632;</td><td>" + tr("reference") + "</td></tr></table>";
if(m_ordinate->currentIndex() == 0 && m_resultsInPlot->isChecked())
text += "<div>" + tr("Threshold: %1 MPa").arg(m_threshold, 0, 'g', 6) + "</div>";
m_legend->setText(text);
m_legend->adjustSize();
}
bool nmWxSensitivityScatter::eventFilter(QObject* object, QEvent* event)
{
if(object != m_plot->m_pPlotView->viewport()) return iDlgBase::eventFilter(object, event);
tCurvePlotView* view = m_plot->m_pPlotView;
ZxSubAxisBase* axisY = m_plot->getAxisOf(1);
// 位置跟随实际绘图区,窗口缩放后仍贴在坐标轴内侧。
if(event->type() == QEvent::Paint)
m_legend->move(view->mapFromScene(m_plot->m_pPlot->getInnerRectF().topLeft()).toPoint() + QPoint(8, 8));
// 只接管阈值线附近的左键拖动,其余缩放、右键及导出仍由框架处理。
if(m_ordinate->currentIndex() == 0 && (event->type() == QEvent::MouseButtonPress ||
event->type() == QEvent::MouseMove || event->type() == QEvent::MouseButtonRelease)) {
QMouseEvent* mouse = static_cast<QMouseEvent*>(event);
const QPointF scene = view->mapToScene(QPointF(mouse->pos()));
const double lineY = view->mapFromScene(QPointF(scene.x(), axisY->getPosForValue(m_threshold))).y();
if(event->type() == QEvent::MouseButtonPress && mouse->button() == Qt::LeftButton &&
m_plot->m_pPlot->getInnerRectF().contains(scene) && qAbs(mouse->pos().y() - lineY) <= 6.0) m_draggingThreshold = true;
if(m_draggingThreshold) {
m_threshold = qBound(0.0, axisY->getValueForPos(scene.y()), axisY->getRangeMax());
if(event->type() == QEvent::MouseButtonRelease) m_draggingThreshold = false;
updateThreshold();
return true;
}
}
if(event->type() == QEvent::ToolTip) {
// 点提示显示对应运行的完整参数组合;不以排序后的点号代替运行编号。
QHelpEvent* help = static_cast<QHelpEvent*>(event);
for(int i = 0; i < m_points.size(); ++i) {
const QPointF position = view->mapFromScene(QPointF(m_plot->getAxisOf(0)->getPosForValue(m_points[i].x()), axisY->getPosForValue(m_points[i].y())));
if(qAbs(position.x() - help->pos().x()) > 6.0 || qAbs(position.y() - help->pos().y()) > 6.0) continue;
const nmDataSensitive::RunResult& run = m_results->m_runs[m_runRows[i]];
const int ordinate = m_ordinate->itemData(m_ordinate->currentIndex()).toInt();
const QString unit = ordinate < 0 ? "MPa" : m_results->m_fitParameters[ordinate].getUnit();
QToolTip::showText(help->globalPos(), tr("Run %1\n%2").arg(run.m_nRunIndex + 1)
.arg(sensitivityRunParameters(*m_results, run)) + "\n" + m_ordinate->currentText()
+ ": " + QString::number(m_points[i].y(), 'g', 6) + " " + unit, view->viewport());
return true;
}
QToolTip::hideText();
event->ignore();
return true;
}
return iDlgBase::eventFilter(object, event);
}
class SensitivityDistributionPlot : public QWidget
{
public:
explicit SensitivityDistributionPlot(QWidget* parent) : QWidget(parent), m_valid(false)
{
// 分布图复用参数分位数计算,避免绘图与实际采样使用两套分布定义。
setMinimumSize(340, 300);
}
void setVariable(const nmDataSensitive::VariableSampling& variable, bool valid)
{
m_variable = variable;
m_valid = valid;
update();
}
protected:
void paintEvent(QPaintEvent*) override
{
// 蓝线为累积概率,红色虚线为按峰值归一化的概率密度,与 Saphir 的显示一致。
QPainter painter(this);
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;
bool valid = m_valid;
for(int i = 0; valid && i < x.size(); ++i) {
// 两端加密以平滑显示尾部,有界分布的横轴保留精确上下限。
const double probability = (1.0 - std::cos(3.141592653589793 * i / 200.0)) * 0.5;
cumulativeValues[i] = bounded ? probability : 0.0001 + 0.9998 * probability;
// Skin 保留原分布绘图,其余参数显示与实际采样一致的受限分布。
valid = m_variable.distributionQuantile(qBound(1e-10, cumulativeValues[i], 1.0 - 1e-10), x[i],
m_variable.m_varName != "Skin");
if(bounded && i == 0) x[i] = m_variable.m_distributionMin;
if(bounded && i == 200) x[i] = m_variable.m_distributionMax;
valid = valid && convertSamplingValue(m_variable.m_varName, x[i], samplingBaseUnit(m_variable.m_varName),
m_variable.m_modelValue.getUnit(), x[i]);
}
if(!valid || x.last() <= x.first()) {
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);
// 横轴采用 1、2、5 的整齐步长,按分布范围自适应,不限制实际采样值。
const double roughStep = (x.last() - x.first()) / 5.0;
const double scale = std::pow(10.0, std::floor(std::log10(roughStep)));
const double fraction = roughStep / scale;
const double step = (fraction <= 1.0 ? 1.0 : fraction <= 2.0 ? 2.0 : fraction <= 5.0 ? 5.0 : 10.0) * scale;
double axisMin = std::floor(x.first() / step) * step;
double axisMax = std::ceil(x.last() / step) * step;
// 除 Skin 外,绘图范围与系统允许范围取交集;只裁剪显示,不改变分布概率。
if(m_variable.m_varName != "Skin") {
double minimum = 0.0, maximum = 0.0;
const QString unit = m_variable.m_modelValue.getUnit();
if(samplingPhysicalRange(m_variable.m_varName, minimum, maximum) &&
convertSamplingValue(m_variable.m_varName, minimum, samplingBaseUnit(m_variable.m_varName), unit, minimum) &&
convertSamplingValue(m_variable.m_varName, maximum, samplingBaseUnit(m_variable.m_varName), unit, maximum)) {
axisMin = qMax(axisMin, minimum);
axisMax = qMin(axisMax, maximum);
}
}
if(axisMax <= axisMin) {
painter.drawText(plot, Qt::AlignCenter, nmWxSensitive::tr("Distribution outside the allowed range"));
return;
}
// 曲线保留真实边界,坐标刻度向外取整;压力下限 0.1 等情形也能显示 0 刻度。
const double curveMin = axisMin, curveMax = axisMax;
axisMin = std::floor(curveMin / step) * step;
axisMax = std::ceil(curveMax / step) * step;
QVector<double> density(201);
double peak = 0.0;
for(int i = 0; i < x.size(); ++i) {
const int a = qMax(0, i - 1), b = qMin(200, i + 1);
density[i] = x[b] > x[a] ? (cumulativeValues[b] - cumulativeValues[a]) / (x[b] - x[a]) : 0.0;
if(m_variable.m_distribution == nmDataSensitive::VariableSampling::TRIANGULAR &&
((i == 0 && m_variable.m_peak > m_variable.m_distributionMin) ||
(i == 200 && m_variable.m_peak < m_variable.m_distributionMax))) density[i] = 0.0;
peak = qMax(peak, density[i]);
}
// 蓝色点状主网格配合黑色主、次刻度,概率轴按 0.1 标注。
const double tickStart = std::floor(axisMin / step) * step;
const int intervals = int(std::ceil((axisMax - tickStart) / step));
// 仅容忍刻度计算的浮点舍入误差。
const double tickTolerance = DBL_EPSILON * 8.0 * qMax(qAbs(axisMin), qAbs(axisMax));
for(int i = 0; i <= intervals * 5; ++i) {
const double value = tickStart + i * step / 5.0;
if(value < axisMin - tickTolerance || value > axisMax + tickTolerance) continue;
const double px = dataArea.left() + (value - axisMin) / (axisMax - axisMin) * dataArea.width();
if(i % 5 == 0) {
painter.setPen(QPen(QColor(100, 120, 255), 1, Qt::DotLine));
painter.drawLine(QPointF(px, plot.top()), QPointF(px, plot.bottom()));
painter.setPen(Qt::black);
painter.drawText(QRectF(px - 36, plot.bottom() + 8, 72, 20), Qt::AlignCenter,
formatSamplingValue(value, 4));
}
painter.setPen(Qt::black);
painter.drawLine(QPointF(px, plot.bottom()), QPointF(px, plot.bottom() + (i % 5 == 0 ? 7 : 4)));
}
for(int i = 0; i <= 50; ++i) {
const double ratio = i / 50.0;
const double py = dataArea.bottom() - ratio * dataArea.height();
if(i % 10 == 0) {
painter.setPen(QPen(QColor(100, 120, 255), 1, Qt::DotLine));
painter.drawLine(QPointF(plot.left(), py), QPointF(plot.right(), py));
}
painter.setPen(Qt::black);
painter.drawLine(QPointF(plot.left(), py), QPointF(plot.left() - (i % 5 == 0 ? 7 : 4), py));
if(i % 5 == 0)
painter.drawText(QRectF(28, py - 10, plot.left() - 40, 20), Qt::AlignRight | Qt::AlignVCenter,
QString::number(ratio));
}
QPolygonF cumulative, probability;
for(int i = 0; i < x.size(); ++i) {
const double px = dataArea.left() + (x[i] - axisMin) / (axisMax - axisMin) * dataArea.width();
cumulative << QPointF(px, dataArea.bottom() - cumulativeValues[i] * dataArea.height());
probability << QPointF(px, dataArea.bottom() - (peak > 0.0 ? density[i] / peak : 0.0) * dataArea.height());
}
// 曲线也按允许的横轴区间裁剪,避免负值部分画进坐标留白或标签区域。
painter.save();
const double clipLeft = dataArea.left() + (curveMin - axisMin) / (axisMax - axisMin) * dataArea.width();
const double clipWidth = (curveMax - curveMin) / (axisMax - axisMin) * dataArea.width();
painter.setClipRect(QRectF(clipLeft, plot.top(), clipWidth, plot.height()));
painter.setPen(QPen(Qt::blue, 1.5));
painter.drawPolyline(cumulative);
painter.setPen(QPen(Qt::red, 1, Qt::DashLine));
painter.drawPolyline(probability);
painter.restore();
painter.setPen(Qt::black);
painter.drawRect(plot);
painter.save();
painter.translate(17, plot.center().y());
painter.rotate(-90);
painter.drawText(QRectF(-plot.height() / 2, -10, plot.height(), 20), Qt::AlignCenter,
nmWxSensitive::tr("Probability distribution function"));
painter.restore();
painter.drawText(QRectF(plot.left(), height() - 22, plot.width(), 20), Qt::AlignCenter,
m_variable.m_varName + " [" + m_variable.m_modelValue.getUnit() + "]");
}
private:
nmDataSensitive::VariableSampling m_variable;
bool m_valid;
};
// ========= 右对齐委托:保证显示和编辑时都是右对齐 =========
class RightAlignDelegate : public QStyledItemDelegate
{
public:
explicit RightAlignDelegate(QObject *parent = nullptr)
: QStyledItemDelegate(parent)
{}
void initStyleOption(QStyleOptionViewItem *option,
const QModelIndex &index) const override
{
QStyledItemDelegate::initStyleOption(option, index);
option->displayAlignment = Qt::AlignRight | Qt::AlignVCenter;
}
QWidget *createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override
{
QWidget *editor = QStyledItemDelegate::createEditor(parent, option, index);
if (QLineEdit *line = qobject_cast<QLineEdit*>(editor)) {
line->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
}
return editor;
}
void setEditorData(QWidget *editor, const QModelIndex &index) const override
{
QLineEdit* line = qobject_cast<QLineEdit*>(editor);
// 编辑时沿用三位有效数字显示,避免重新暴露浮点尾数。
if (line) line->setText(index.data(Qt::UserRole).isValid()
? formatSamplingValue(index.data(Qt::UserRole).toDouble(), 3)
: index.data(Qt::DisplayRole).toString());
}
void setModelData(QWidget *editor, QAbstractItemModel *model,
const QModelIndex &index) const override
{
QLineEdit* line = qobject_cast<QLineEdit*>(editor);
if (!line) return;
bool ok = false;
double value = line->text().trimmed().toDouble(&ok);
// 编辑后文本未改变时保留完整采样值,不把显示舍入当成用户修改。
const QVariant original = index.data(Qt::UserRole);
if (original.isValid() && line->text() == formatSamplingValue(original.toDouble(), 3))
value = original.toDouble(&ok);
// 非法输入保留原文,由 Generate 或预览统一定位并提示。
model->setData(index, ok && qIsFinite(value) ? QVariant(value) : QVariant(), Qt::UserRole);
model->setData(index, ok && qIsFinite(value)
? formatSamplingValue(value, 3) : line->text(), Qt::DisplayRole);
}
};
// 表格仅承载可见行,完整组合编号使用六十四位整数,滚动过程中按编号取数。
class SensitivityRunsView : public QAbstractScrollArea
{
public:
SensitivityRunsView(const nmDataSensitive& data, const QIcon& arrow, QWidget* parent)
: QAbstractScrollArea(parent), m_data(data), m_arrow(arrow),
m_table(nullptr),
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);
m_table = new QTableWidget(viewport());
m_table->setFrameShape(QFrame::NoFrame);
m_table->setColumnCount(3);
m_table->setHorizontalHeaderLabels(QStringList() << "" << "#" << nmWxSensitive::tr("Description"));
m_table->verticalHeader()->setVisible(false);
m_table->verticalHeader()->setDefaultSectionSize(24);
m_table->horizontalHeader()->setResizeMode(0, QHeaderView::Fixed);
m_table->horizontalHeader()->setResizeMode(1, QHeaderView::Fixed);
m_table->horizontalHeader()->setResizeMode(2, QHeaderView::Interactive);
m_table->horizontalHeader()->setStretchLastSection(true);
m_table->horizontalHeader()->setHighlightSections(false);
m_table->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
m_table->setColumnWidth(0, 20);
m_table->setColumnWidth(1, m_table->fontMetrics().width(QString::number(m_total)) + 16);
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_table->setSelectionMode(QAbstractItemView::SingleSelection);
m_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_table->setWordWrap(false);
m_table->setStyleSheet("QTableWidget { background-color: white; }");
int descriptionWidth = 0;
for (int i = 0; i < m_data.getVariableCount(); ++i) {
nmDataSensitive::VariableSampling& variable = m_data.getVariable(i);
if (!variable.getEnabled()) continue;
m_indices.append(i);
int width = 0;
const QVector<double> samples = variable.samplingValues();
for (int row = 0; row < samples.size(); ++row)
width = qMax(width, m_table->fontMetrics().width(parameterDescription(i, samples[row])));
if (m_data.getCalculationType() == nmDataSensitive::CALC_DETERMINISTIC)
descriptionWidth = qMax(descriptionWidth, width);
else descriptionWidth += width + m_table->fontMetrics().width(" + ");
}
// 长组合可横向滚动并通过提示查看完整文本,保持各行高度一致。
m_table->setColumnWidth(2, qMax(200, descriptionWidth + 16));
QVBoxLayout* layout = new QVBoxLayout(viewport());
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(m_table);
m_table->installEventFilter(this);
m_table->viewport()->installEventFilter(this);
verticalScrollBar()->installEventFilter(this);
}
void scrollAction(int action)
{
// 超大列表的滚动条位置按比例映射,单步、整屏移动仍保持精确行号。
switch (action) {
case QAbstractSlider::SliderSingleStepAdd: scrollRows(1); break;
case QAbstractSlider::SliderSingleStepSub: scrollRows(-1); break;
case QAbstractSlider::SliderPageStepAdd: scrollRows(m_visibleRows); break;
case QAbstractSlider::SliderPageStepSub: scrollRows(-m_visibleRows); break;
case QAbstractSlider::SliderToMinimum: m_first = 0; refreshRows(); break;
case QAbstractSlider::SliderToMaximum: m_first = maximumFirst(); refreshRows(); break;
case QAbstractSlider::SliderMove:
m_first = firstFromPosition(verticalScrollBar()->sliderPosition());
refreshRows();
break;
default: break;
}
}
protected:
void resizeEvent(QResizeEvent* event) override
{
// 窗口变化时仅调整可见行数量,绝不按总次数分配表格项。
QAbstractScrollArea::resizeEvent(event);
refreshRows();
}
void scrollContentsBy(int, int) override
{
// 处理拖动滚动条或系统直接改变位置的情况。
if (m_updating) return;
m_first = firstFromPosition(verticalScrollBar()->value());
refreshRows();
}
bool eventFilter(QObject* object, QEvent* event) override
{
// 滚轮、键盘和鼠标均操作完整运行列表,而不是可见行缓存。
if (event->type() == QEvent::Wheel) {
QWheelEvent* wheel = static_cast<QWheelEvent*>(event);
if (wheel->orientation() == Qt::Vertical) {
m_wheelDelta += wheel->delta();
const int steps = m_wheelDelta / 120;
m_wheelDelta %= 120;
scrollRows(-qint64(steps) * QApplication::wheelScrollLines());
wheel->accept();
return true;
}
} else if (event->type() == QEvent::KeyPress) {
QKeyEvent* key = static_cast<QKeyEvent*>(event);
qint64 movement = 0;
switch (key->key()) {
case Qt::Key_Up: movement = -1; break;
case Qt::Key_Down: movement = 1; break;
case Qt::Key_PageUp: movement = -m_visibleRows; break;
case Qt::Key_PageDown: movement = m_visibleRows; break;
case Qt::Key_Home: m_selected = 0; break;
case Qt::Key_End: m_selected = m_total - 1; break;
default: return QAbstractScrollArea::eventFilter(object, event);
}
if (movement < 0) m_selected -= qMin(m_selected, quint64(-movement));
else if (movement > 0) m_selected += qMin(m_total - 1 - m_selected, quint64(movement));
if (m_selected < m_first) m_first = m_selected;
else if (m_selected - m_first >= quint64(m_visibleRows))
m_first = m_selected - m_visibleRows + 1;
refreshRows();
key->accept();
return true;
} else if (object == m_table->viewport() &&
(event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease)) {
QMouseEvent* mouse = static_cast<QMouseEvent*>(event);
const int row = m_table->rowAt(mouse->pos().y());
if (mouse->button() == Qt::LeftButton && row >= 0) {
m_selected = m_first + row;
refreshRows();
}
} else if (object == m_table->viewport() && event->type() == QEvent::Resize) {
refreshRows();
}
return QAbstractScrollArea::eventFilter(object, event);
}
private:
quint64 maximumFirst() const
{
return m_total > quint64(m_visibleRows) ? m_total - m_visibleRows : 0;
}
quint64 firstFromPosition(int position) const
{
// 两端准确对应首尾,中间位置不把六十四位总数截断为 int。
const int maximum = verticalScrollBar()->maximum();
if (position <= 0 || maximum == 0) return 0;
if (position >= maximum) return maximumFirst();
return qMin(maximumFirst(), quint64((long double)position * maximumFirst() / maximum));
}
void scrollRows(qint64 movement)
{
// 用差值限制移动距离,避免无符号行号在首尾发生溢出。
if (movement < 0) m_first -= qMin(m_first, quint64(-movement));
else m_first += qMin(maximumFirst() - m_first, quint64(movement));
refreshRows();
}
QString parameterDescription(int index, double value)
{
// 描述沿用采样表的三位有效数字和当前显示单位。
nmDataSensitive::VariableSampling& variable = m_data.getVariable(index);
const QString unit = variable.getModelValue().getUnit();
convertSamplingValue(variable.getVarName(), value, samplingBaseUnit(variable.getVarName()), unit, value);
return variable.displayName() + " = " + formatSamplingValue(value, 3)
+ (unit.isEmpty() ? QString() : " " + unit);
}
void refreshRows()
{
if (m_updating || !m_table || !viewport()->layout()) return;
// 始终只生成视口内的行,所有组合共用一根连续滚动条。
m_updating = true;
viewport()->layout()->activate();
m_visibleRows = qMax(1, m_table->viewport()->height() / 24);
setVerticalScrollBarPolicy(m_total > quint64(m_visibleRows)
? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
m_first = qMin(m_first, maximumFirst());
QScrollBar* bar = verticalScrollBar();
const quint64 last = maximumFirst();
const int maximum = int(qMin(last, quint64(INT_MAX)));
const bool blocked = bar->blockSignals(true);
bar->setRange(0, maximum);
bar->setSingleStep(1);
bar->setPageStep(last ? qMax(1, int((long double)m_visibleRows * maximum / last)) : m_visibleRows);
int position = last ? int((long double)m_first * maximum / last) : 0;
// 比例舍入后仍保留首尾箭头的可用状态。
if (last > quint64(INT_MAX) && m_first > 0 && m_first < last)
position = qBound(1, position, maximum - 1);
bar->setValue(position);
bar->blockSignals(blocked);
const int rows = int(qMin(quint64(m_visibleRows), m_total - m_first));
m_table->setRowCount(rows);
for (int row = 0; row < rows; ++row) {
const quint64 run = m_first + row;
QVector<double> values;
if (!m_data.getRunValues(run, values)) break;
int changingIndex = -1;
if (m_data.getCalculationType() == nmDataSensitive::CALC_DETERMINISTIC) {
quint64 remaining = run;
for (int i = 0; i < m_indices.size(); ++i) {
const quint64 count = m_data.getVariable(m_indices[i]).getNumber();
if (remaining < count) { changingIndex = m_indices[i]; break; }
remaining -= count;
}
}
QStringList descriptions;
for (int i = 0; i < m_indices.size(); ++i) {
const int index = m_indices[i];
if (changingIndex < 0 || index == changingIndex)
descriptions << parameterDescription(index, values[index]);
}
QTableWidgetItem* arrow = new QTableWidgetItem();
arrow->setIcon(run == m_selected ? m_arrow : QIcon());
m_table->setItem(row, 0, arrow);
QTableWidgetItem* number = new QTableWidgetItem(QString::number(run + 1));
number->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
m_table->setItem(row, 1, number);
QTableWidgetItem* description = new QTableWidgetItem(descriptions.join(" + "));
description->setToolTip(description->text());
m_table->setItem(row, 2, description);
}
if (m_selected >= m_first && m_selected - m_first < quint64(rows)) {
m_table->setCurrentCell(int(m_selected - m_first), 1);
m_table->selectRow(int(m_selected - m_first));
} else {
m_table->setCurrentIndex(QModelIndex());
m_table->clearSelection();
}
m_updating = false;
// 横向滚动条显隐可能改变视口高度,补齐最终可见行数量。
if (m_visibleRows != qMax(1, m_table->viewport()->height() / 24)) refreshRows();
}
nmDataSensitive m_data;
QIcon m_arrow;
QTableWidget* m_table;
QList<int> m_indices;
quint64 m_total;
quint64 m_first;
quint64 m_selected;
int m_visibleRows;
bool m_updating;
int m_wheelDelta;
};
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();
setupUI();
createConnections();
populateTreeWidget();
createDetailPages();
loadDataToUI();
m_loading = false;
onTreeItemChanged(nullptr, 0);
// 结果发布或清除后,同步本窗口持有的批次数据。
if(m_pDataManager) {
connect(m_pDataManager, SIGNAL(sigSensitiveResultsChanged()), this, SLOT(onResultsChanged()));
connect(m_pDataManager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(onModelResultChanged()));
}
onResultsChanged();
}
nmWxSensitive::~nmWxSensitive()
{
// 父窗口强制销毁时先请求停止并等 DLL 返回,线程结束前不释放计算副本。
if(m_pSensitivityTask) {
disconnect(m_pSensitivityTask, nullptr, this, nullptr);
m_pSensitivityTask->requestCancel();
m_pSensitivityTask->wait();
delete m_pSensitivityTask;
}
}
void nmWxSensitive::reject()
{
// Esc 与取消按钮使用相同的停止路径,保留已完成组供结束后查看。
if(m_calculationRunning) onCancelClicked();
else {
foreach(QDialog* dialog, findChildren<QDialog*>())
if(dialog->isWindow()) dialog->close();
iDlgBase::reject();
}
}
void nmWxSensitive::closeEvent(QCloseEvent* event)
{
if(m_calculationRunning) {
onCancelClicked();
event->ignore();
return;
}
iDlgBase::closeEvent(event);
}
void nmWxSensitive::refreshModelValues()
{
// 每次打开或模型更新时读取当前参数,沿用已有单位和物理范围限制。
nmDataReservoir* reservoir = m_pDataManager ? m_pDataManager->getReservoirData() : nullptr;
const QVector<nmCalculationWellRef> wells = m_pDataManager
? m_pDataManager->getEffectiveCalculationWells() : QVector<nmCalculationWellRef>();
// 旧版主井配置补上井编码;已不参与模型的井不再生成样本。
for(int i = 0; i < m_sensitiveData.getVariableCount(); ++i) {
nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i);
if(variable.m_varName == "Skin" || variable.m_varName == "C") {
if(variable.m_wellCode.isEmpty() && m_pDataManager)
variable.m_wellCode = m_pDataManager->getPrimaryWellCode();
if(!m_pDataManager || !m_pDataManager->isWellSelectedForCalculation(variable.m_wellCode))
variable.m_enabled = false;
}
}
bool modelChanged = false;
const QStringList names = QStringList() << "Skin" << "C" << "Pi" << "k" << "h" << QString(QChar(0x03c6));
for (int entry = 0; entry < wells.size() * 2 + 4; ++entry) {
const bool wellParameter = entry < wells.size() * 2;
const int n = wellParameter ? entry % 2 : entry - wells.size() * 2 + 2;
const QString wellCode = wellParameter ? wells[entry / 2].m_sWellCode : QString();
nmDataWellBase* well = wellParameter ? m_pDataManager->findWellByCode(wellCode) : nullptr;
nmDataAttribute* attribute = nullptr;
const QString name = names[n];
if (well && name == "Skin" && well->getPerforationCount() > 0)
attribute = &well->getPerforation(0)->getSkin();
else if (well && name == "C") attribute = &well->getWellboreStorage();
else if (reservoir && name == "Pi") attribute = &reservoir->getInitialPressure();
else if (reservoir && name == "k") attribute = &reservoir->getPermeability();
else if (reservoir && name == "h") attribute = &reservoir->getThickness();
else if (reservoir && name == QString(QChar(0x03c6))) attribute = &reservoir->getPorosity();
int index = -1;
for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i)
if (m_sensitiveData.getVariable(i).getVarName() == name &&
m_sensitiveData.getVariable(i).m_wellCode == wellCode) { index = i; break; }
const bool fresh = index < 0;
if (fresh) {
nmDataSensitive::VariableSampling variable;
variable.setVarName(name);
variable.m_wellCode = wellCode;
variable.setNumber(5);
m_sensitiveData.getVariables().append(variable);
index = m_sensitiveData.getVariableCount() - 1;
}
nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(index);
variable.setVarGroup(well ? well->getWellName() : "Reservoir");
double physicalMin = 0.0, physicalMax = 0.0;
if (!attribute || !samplingPhysicalRange(name, physicalMin, physicalMax)) {
variable.setEnabled(false);
variable.getModelValue().setValue(QVariant());
continue;
}
const QString baseUnit = samplingBaseUnit(name);
// 已保存的采样显示单位独立于模型单位,重开及读取项目时继续沿用。
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))
displayUnit = baseUnit;
double minimum = variable.getMinValue().getValue().toDouble();
double maximum = variable.getMaxValue().getValue().toDouble();
QVector<double> values = variable.getValues();
double previousModel = variable.getModelValue().getValue().toDouble();
bool migrated = !fresh && (m_sensitiveData.usesBaseUnits() ||
variable.getMode() == nmDataSensitive::VariableSampling::MODE_MANUAL || !values.isEmpty());
if (migrated && !m_sensitiveData.usesBaseUnits()) {
const QString oldUnit = variable.getModelValue().getUnit();
convertSamplingValue(name, previousModel, oldUnit, baseUnit, previousModel);
migrated = convertSamplingValue(name, minimum, oldUnit, baseUnit, minimum) &&
convertSamplingValue(name, maximum, oldUnit, baseUnit, maximum);
for (int i = 0; migrated && i < values.size(); ++i)
migrated = convertSamplingValue(name, values[i], oldUnit, baseUnit, values[i]);
}
const bool resetRanges = fresh || !nearlyEqualSamplingValue(previousModel, attribute->getValue().toDouble());
modelChanged = modelChanged || resetRanges;
// 识别 Skin、Pi 的自动默认范围,按当前模型更新,保留用户自定义区间。
const bool previousDefault = variable.getMode() == nmDataSensitive::VariableSampling::MODE_AUTOMATIC &&
((nearlyEqualSamplingValue(minimum, qMin(previousModel / 2.0, previousModel * 2.0)) &&
nearlyEqualSamplingValue(maximum, qMax(previousModel / 2.0, previousModel * 2.0))) ||
(name == "Skin" && previousModel != 0.0 &&
nearlyEqualSamplingValue(minimum, qMax(physicalMin, qMin(previousModel * 0.9, previousModel * 1.1))) &&
nearlyEqualSamplingValue(maximum, qMin(physicalMax, qMax(previousModel * 0.9, previousModel * 1.1)))) ||
(name == "Skin" && previousModel == 0.0 && (minimum == -1.0 || minimum == 0.0) && maximum == 1.0) ||
(name == "Pi" && variable.getModelValue().getMinValue().isValid() &&
variable.getModelValue().getMaxValue().isValid() &&
nearlyEqualSamplingValue(minimum, variable.getModelValue().getMinValue().toDouble()) &&
nearlyEqualSamplingValue(maximum, variable.getModelValue().getMaxValue().toDouble())));
// 当前模型值变化时重新建立范围,采样数量、模式及手动值保持原设置。
if (resetRanges || !migrated || ((name == "Skin" || name == "Pi") && previousDefault)) {
const double model = attribute->getValue().toDouble();
minimum = qMin(model / 2.0, model * 2.0);
maximum = qMax(model / 2.0, model * 2.0);
if (name == "Skin") {
minimum = model == 0.0 ? -1.0 : qMin(model * 0.9, model * 1.1);
maximum = model == 0.0 ? 1.0 : qMax(model * 0.9, model * 1.1);
}
if (name == "Pi") { minimum = physicalMin; maximum = physicalMax; }
}
if (!migrated) {
values.clear();
variable.setMode(nmDataSensitive::VariableSampling::MODE_AUTOMATIC);
variable.setLog(false);
variable.setNumber(5);
}
// 初始范围和已保存范围都截断到硬限制,手动采样值留给提交校验。
minimum = qBound(physicalMin, minimum, physicalMax);
maximum = qBound(physicalMin, maximum, physicalMax);
variable.setModelValue(*attribute);
variable.getModelValue().setUnit(displayUnit);
variable.getModelValue().setValueRange(physicalMin, physicalMax);
variable.setMinValue(variable.getModelValue());
variable.setMaxValue(variable.getModelValue());
variable.getMinValue().setValue(minimum);
variable.getMaxValue().setValue(maximum);
variable.setValues(values);
if(!variable.m_distributionInitialized || resetRanges) {
// Saphir 默认正态均值为模型值、标准差为其三分之一;Skin=0 时使用 5/3。
const double model = attribute->getValue().toDouble();
variable.m_mean = model;
variable.m_stdDev = model == 0.0 ? 5.0 / 3.0 : std::fabs(model) / 3.0;
variable.m_distributionMin = minimum;
variable.m_distributionMax = maximum;
variable.m_peak = qBound(variable.m_distributionMin, model * 1.01, variable.m_distributionMax);
variable.m_distributionInitialized = true;
}
// Saphir 在重新打开对话框时同步无效 Log 范围的实际状态。
if (variable.getMode() == nmDataSensitive::VariableSampling::MODE_AUTOMATIC &&
(minimum <= 0.0 || maximum <= 0.0)) variable.setLog(false);
}
for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i)
if (!isSupportedSamplingVariable(m_sensitiveData.getVariable(i).getVarName()))
m_sensitiveData.getVariable(i).setEnabled(false);
m_sensitiveData.setUsesBaseUnits(true);
if (modelChanged) m_sensitiveData.clearMonteCarloSamples();
}
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();
// 井列表可能改变,重建参数页,保留外层输入窗口及底部菜单。
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;
onTreeItemChanged(nullptr, 0);
onResultsChanged();
}
void nmWxSensitive::loadDataToUI(bool refreshRangesOnly)
{
// 加载过程中阻止联动刷新,先恢复设置,再一次生成各参数表格。
if (!refreshRangesOnly)
calculationTypeCombo->setCurrentIndex(m_sensitiveData.isImproveEnabled() ? 3 : int(m_sensitiveData.getCalculationType()));
m_monteCarloCount->setValue(m_sensitiveData.getMonteCarloCount());
for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) {
nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i);
const QString name = variable.getVarName();
const int idx = findVariableIndex(variable);
if (idx < 0) continue;
const bool available = isSupportedSamplingVariable(name) &&
variable.getModelValue().getValue().isValid();
for (int group = 0; group < variablesTree->topLevelItemCount(); ++group) {
QTreeWidgetItem* parent = variablesTree->topLevelItem(group);
for (int child = 0; child < parent->childCount(); ++child) {
QTreeWidgetItem* item = parent->child(child);
if (item->text(0) != name || item->data(0, Qt::UserRole + 1).toString() != variable.m_wellCode) continue;
item->setDisabled(!available);
if (!refreshRangesOnly)
item->setCheckState(0, available && variable.getEnabled() ? Qt::Checked : Qt::Unchecked);
}
}
detailStack->widget(idx + 1)->setEnabled(available);
// 求解后的刷新只更新模型值和范围,不覆盖窗口中尚未提交的采样设置。
if (!refreshRangesOnly) {
m_modeCombos[idx]->setCurrentIndex(int(variable.getMode()));
m_logChecks[idx]->setChecked(variable.getLog());
m_numSpins[idx]->setValue(qMax(1, variable.getNumber()));
}
// 已打开窗口沿用原显示单位,避免手动表格的数值被换成另一种单位解释。
const QString unit = nmParameterField::resolveUnitSpelling(
refreshRangesOnly ? m_unitCombos[idx]->currentText() : variable.getModelValue().getUnit());
variable.getModelValue().setUnit(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();
convertSamplingValue(name, model, samplingBaseUnit(name), unit, model);
convertSamplingValue(name, minimum, samplingBaseUnit(name), unit, minimum);
convertSamplingValue(name, maximum, samplingBaseUnit(name), unit, maximum);
setSamplingEditValue(m_modelSpins[idx], model);
setSamplingEditValue(m_minSpins[idx], minimum);
setSamplingEditValue(m_maxSpins[idx], maximum);
m_distributionCombos[idx]->setCurrentIndex(int(variable.m_distribution));
const double distributionValues[] = {variable.m_mean, variable.m_stdDev, variable.m_peak,
variable.m_distributionMin, variable.m_distributionMax};
for(int field = 0; field < 5; ++field) {
double display = distributionValues[field];
convertSamplingValue(name, display, samplingBaseUnit(name), unit, display, field == 1);
setSamplingEditValue(m_distributionPanels[idx]->findChild<QLineEdit*>(distributionFields[field]), display);
}
double displayMin = variable.getModelValue().getMinValue().toDouble();
double displayMax = variable.getModelValue().getMaxValue().toDouble();
convertSamplingValue(name, displayMin, samplingBaseUnit(name), unit, displayMin);
convertSamplingValue(name, displayMax, samplingBaseUnit(name), unit, displayMax);
const QString rangeHint = tr("Allowed range: [%1, %2] %3")
.arg(formatSamplingValue(displayMin, 6)).arg(formatSamplingValue(displayMax, 6)).arg(unit);
m_minSpins[idx]->setToolTip(rangeHint);
m_maxSpins[idx]->setToolTip(rangeHint);
if (!refreshRangesOnly && variable.getMode() == nmDataSensitive::VariableSampling::MODE_MANUAL) {
QTableWidget* table = m_valueTables[idx];
table->setRowCount(variable.getNumber());
const QVector<double>& values = variable.getValues();
for (int row = 0; row < values.size() && row < table->rowCount(); ++row) {
double display = values[row];
convertSamplingValue(name, display, samplingBaseUnit(name), unit, display);
QTableWidgetItem* item = new QTableWidgetItem(formatSamplingValue(display, 3));
item->setData(Qt::UserRole, display);
table->setItem(row, 1, item);
}
}
updateValuesForPage(idx);
}
}
int nmWxSensitive::findVariableIndex(const nmDataSensitive::VariableSampling& variable)
{
// 页面由树节点创建,通过井编码与参数名定位,不依赖固定页面次序。
for(int group = 0; group < variablesTree->topLevelItemCount(); ++group) {
QTreeWidgetItem* parent = variablesTree->topLevelItem(group);
for(int child = 0; child < parent->childCount(); ++child) {
QTreeWidgetItem* item = parent->child(child);
if(item->text(0) == variable.m_varName &&
item->data(0, Qt::UserRole + 1).toString() == variable.m_wellCode)
return item->data(0, Qt::UserRole).toInt() - 1;
}
}
return -1;
}
void nmWxSensitive::updateModelCount()
{
// 勾选、数量和方法修改后立即更新总次数,不等待 Generate。
if (m_loading) return;
m_sensitiveData.setCalculationType(
static_cast<nmDataSensitive::CalculationType>(calculationTypeCombo->itemData(calculationTypeCombo->currentIndex()).toInt()));
m_sensitiveData.setImproveEnabled(calculationTypeCombo->currentIndex() == 3);
for (int group = 0; group < variablesTree->topLevelItemCount(); ++group) {
QTreeWidgetItem* parent = variablesTree->topLevelItem(group);
for (int child = 0; child < parent->childCount(); ++child) {
QTreeWidgetItem* item = parent->child(child);
for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) {
nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i);
if (variable.getVarName() != item->text(0) ||
variable.m_wellCode != item->data(0, Qt::UserRole + 1).toString()) continue;
variable.setEnabled(!item->isDisabled() && item->checkState(0) == Qt::Checked);
const int idx = findVariableIndex(variable);
if (idx >= 0) variable.setNumber(m_numSpins[idx]->value());
}
}
}
bool overflow = false;
m_sensitiveData.setMonteCarloCount(m_monteCarloCount->value());
const quint64 count = m_sensitiveData.calculateModelCount(&overflow);
m_sensitiveData.setTotalModelCount(count);
modelCountLabel->setText(overflow ? tr("Too many models") : QString::number(count));
previewButton->setEnabled(!overflow && count > 0);
const bool monteCarlo = m_sensitiveData.getCalculationType() == nmDataSensitive::CALC_MONTE_CARLO;
modelCountLabel->setVisible(!monteCarlo);
m_monteCarloCount->setVisible(monteCarlo);
// 第四种确认设置且至少选择一个拟合参数后才开放生成,空选择直接禁用。
bool hasFittingParameters = false;
if(!m_sensitiveData.getImproveSettings().isNull()) {
nmSensitivityFitSettings settings = *m_sensitiveData.getImproveSettings();
QVector<nmFitParameter>& parameters = settings.parameters.getModelParameters();
// 新勾选的采样只取消同井同参数的拟合,其他井的同名参数仍可参与。
for(int i = 0; i < parameters.size(); ++i) {
if(!parameters[i].wellCode.isEmpty() &&
!m_pDataManager->isWellSelectedForCalculation(parameters[i].wellCode))
parameters[i].selected = false;
for(int v = 0; v < m_sensitiveData.getVariableCount(); ++v) {
const nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(v);
if(variable.m_enabled && variable.parameterKey() == parameters[i].samplingKey())
parameters[i].selected = false;
}
}
hasFittingParameters = settings.confirmed && settings.parameters.hasSelectedParameters();
m_sensitiveData.setImproveSettings(QSharedPointer<const nmSensitivityFitSettings>(new nmSensitivityFitSettings(settings)));
}
generateButton->setEnabled(m_pDataManager && !overflow && count > 0 && !m_calculationRunning &&
(calculationTypeCombo->currentIndex() != 3 || hasFittingParameters));
}
bool nmWxSensitive::saveDataFromUI()
{
// 校验选中参数后再提交副本,失败不改写数据中心已有配置。
updateModelCount();
if (m_sensitiveData.getTotalModelCount() == 0) return false;
nmDataSensitive candidate(m_sensitiveData);
for (int i = 0; i < candidate.getVariableCount(); ++i) {
nmDataSensitive::VariableSampling& variable = candidate.getVariable(i);
const QString name = variable.getVarName();
const int idx = findVariableIndex(variable);
if (idx < 0 || !isSupportedSamplingVariable(name)) continue;
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;
valid = readDistributionPage(idx, variable) && variable.distributionQuantile(0.5, median);
if(!valid && variable.getEnabled()) {
QMessageBox::warning(this, tr("Sensitivity"), tr("%1: invalid distribution parameters.").arg(name));
detailStack->setCurrentIndex(idx + 1);
return false;
}
// 均值描述原分布,可以在截断区间之外;显式上下限及众数仍需符合系统范围。
if(valid && variable.m_distribution >= nmDataSensitive::VariableSampling::TRUNCATED_NORMAL)
valid = validSamplingValue(name, variable.m_distributionMin) &&
validSamplingValue(name, variable.m_distributionMax);
if(valid && variable.m_distribution == nmDataSensitive::VariableSampling::TRIANGULAR)
valid = validSamplingValue(name, variable.m_peak);
} else {
variable.setMode(static_cast<nmDataSensitive::VariableSampling::Mode>(m_modeCombos[idx]->currentIndex()));
variable.setLog(m_logChecks[idx]->isChecked());
if (variable.getMode() == nmDataSensitive::VariableSampling::MODE_AUTOMATIC) {
bool minOk = false, maxOk = false;
double minimum = samplingEditValue(m_minSpins[idx], &minOk);
double maximum = samplingEditValue(m_maxSpins[idx], &maxOk);
valid = minOk && maxOk &&
convertSamplingValue(name, minimum, unit, baseUnit, minimum) &&
convertSamplingValue(name, maximum, unit, baseUnit, maximum) &&
minimum <= maximum && validSamplingValue(name, minimum) && validSamplingValue(name, maximum);
if (valid) {
variable.getMinValue().setValue(minimum);
variable.getMaxValue().setValue(maximum);
// Number=1 使用模型值,也必须在同一物理范围内。
QVector<double> values = variable.samplingValues();
for (int row = 0; valid && row < values.size(); ++row)
valid = validSamplingValue(name, values[row]);
if (valid) variable.setValues(values);
}
} else {
QVector<double> values;
QTableWidget* table = m_valueTables[idx];
for (int row = 0; row < table->rowCount(); ++row) {
QTableWidgetItem* item = table->item(row, 1);
bool ok = false;
double value = 0.0;
if (item) value = item->data(Qt::UserRole).isValid()
? item->data(Qt::UserRole).toDouble(&ok) : item->text().trimmed().toDouble(&ok);
if (!ok || !convertSamplingValue(name, value, unit, baseUnit, value) ||
!validSamplingValue(name, value)) { valid = false; break; }
values.append(value);
}
valid = valid && values.size() == variable.getNumber();
if (valid) variable.setValues(values);
}
}
if (!valid && variable.getEnabled()) {
double lower = 0.0, upper = 0.0;
const bool hasRange = samplingPhysicalRange(name, lower, upper) &&
convertSamplingValue(name, lower, baseUnit, unit, lower) &&
convertSamplingValue(name, upper, baseUnit, unit, upper);
QMessageBox::warning(this, tr("Sensitivity"),
hasRange ? tr("%1: invalid sampling values. Allowed range: [%2, %3] %4. Check the sampling settings.")
.arg(name).arg(formatSamplingValue(lower, 6)).arg(formatSamplingValue(upper, 6)).arg(unit)
: tr("%1: the physical range is unavailable.").arg(name));
detailStack->setCurrentIndex(idx + 1);
return false;
}
}
m_sensitiveData = candidate;
return true;
}
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(1120, 660).boundedTo(available));
// 沿用储层参数窗口的分组框、内容边距和底部按钮间距。
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(12, 10, 12, 0);
mainLayout->setSpacing(8);
// ===== 顶部:Calculation type =====
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);
calculationTypeCombo->addItem(tr("Monte-Carlo"), nmDataSensitive::CALC_MONTE_CARLO);
// 第四种的采样界面和预览直接复用 Monte-Carlo,计算逻辑后续接入。
calculationTypeCombo->addItem(tr("Monte-Carlo + Improve"), nmDataSensitive::CALC_MONTE_CARLO);
m_improveSettingsButton = new QPushButton(tr("Improve Settings"), this);
// 按钮与下拉框等高,显示或隐藏时不撑高顶部布局。
m_improveSettingsButton->setFixedHeight(calculationTypeCombo->sizeHint().height());
m_improveSettingsButton->hide();
calcTypeLayout->addWidget(calculationTypeCombo);
calcTypeLayout->addWidget(m_improveSettingsButton);
calcTypeLayout->addStretch();
mainLayout->addWidget(calcTypeGroup);
// ===== 中间:左 Variables 面板 + 右 detailStack =====
QSplitter *centerSplitter = new QSplitter(Qt::Horizontal, this);
centerSplitter->setChildrenCollapsible(false); // 不允许把某一侧完全折叠
centerSplitter->setHandleWidth(4); // 中间拖动条宽度
// 左侧参数树使用标准分组框,样式交给主软件统一处理。
QGroupBox *variablesPanel = new QGroupBox(tr("Variables"), this);
variablesPanel->setMinimumWidth(200);
QVBoxLayout *variablesPanelLayout = new QVBoxLayout(variablesPanel);
variablesPanelLayout->setContentsMargins(10, 10, 10, 10);
// Tree 区域
variablesTree = new QTreeWidget(variablesPanel);
variablesTree->setHeaderHidden(true);
variablesTree->setFrameShape(QFrame::NoFrame); // 外层已经有 frame
variablesTree->setMinimumHeight(350);
variablesPanelLayout->addWidget(variablesTree, 1);
// 把左侧 panel 加进 splitter
centerSplitter->addWidget(variablesPanel);
// --- 右侧:detailStack(后面每个 page 自己做标题栏)---
detailStack = new QStackedWidget(this);
detailStack->setContentsMargins(0, 0, 0, 0);
// index 0:默认提示页
QWidget *blankPage = new QWidget(this);
QVBoxLayout *blankLayout = new QVBoxLayout(blankPage);
blankLayout->setContentsMargins(0, 0, 0, 0);
QLabel *blankLabel = new QLabel(tr("Select a variable on the left."), blankPage);
blankLayout->addStretch();
blankLayout->addWidget(blankLabel, 0, Qt::AlignHCenter);
blankLayout->addStretch();
detailStack->addWidget(blankPage);
// 把右侧 stack 加进 splitter
centerSplitter->addWidget(detailStack);
// 左边宽度基本固定,右边自适应
centerSplitter->setStretchFactor(0, 0);
centerSplitter->setStretchFactor(1, 1);
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);
m_monteCarloCount = new QSpinBox(this);
m_monteCarloCount->setObjectName("sensitivityMonteCarloCount");
m_monteCarloCount->setRange(1, 100000);
m_monteCarloCount->setValue(m_sensitiveData.getMonteCarloCount());
m_monteCarloCount->setButtonSymbols(QAbstractSpinBox::NoButtons);
m_monteCarloCount->setVisible(false);
bottomLayout->addWidget(totalLabel);
bottomLayout->addWidget(modelCountLabel);
bottomLayout->addWidget(m_monteCarloCount);
previewButton = new QPushButton(this);
previewButton->setIcon(QIcon(QCoreApplication::applicationDirPath().section('/', 0, -2)
+ "/Res/Icon/Eyes.png"));
previewButton->setFixedSize(28, 24);
previewButton->setToolTip(tr("Sensitivity runs"));
bottomLayout->addWidget(previewButton);
bottomLayout->addStretch();
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);
mainLayout->addLayout(bottomLayout);
}
void nmWxSensitive::createConnections()
{
// 复用已有拟合窗口作为第四种方法的设置入口。
connect(m_improveSettingsButton, SIGNAL(clicked()), this, SLOT(onImproveSettingsClicked()));
connect(m_monteCarloCount, SIGNAL(valueChanged(int)), this, SLOT(onVariableParamChanged()));
connect(generateButton, SIGNAL(clicked()),
this, SLOT(onGenerateClicked()));
connect(cancelButton, SIGNAL(clicked()),
this, SLOT(onCancelClicked()));
connect(previewButton, SIGNAL(clicked()), this, SLOT(onPreviewClicked()));
connect(calculationTypeCombo, SIGNAL(currentIndexChanged(int)),
this, SLOT(onCalculationTypeChanged(int)));
connect(variablesTree, SIGNAL(itemClicked(QTreeWidgetItem*,int)),
this, SLOT(onTreeItemClicked(QTreeWidgetItem*,int)));
connect(variablesTree, SIGNAL(itemChanged(QTreeWidgetItem*,int)),
this, SLOT(onTreeItemChanged(QTreeWidgetItem*,int)));
}
void nmWxSensitive::populateTreeWidget()
{
variablesTree->clear();
// 只列出实际参与模型的井,每口井复用现有的参数及采样页。
const QVector<nmCalculationWellRef> wells = m_pDataManager
? m_pDataManager->getEffectiveCalculationWells() : QVector<nmCalculationWellRef>();
int i;
for(int w = 0; w < wells.size(); ++w) {
nmDataWellBase* well = m_pDataManager->findWellByCode(wells[w].m_sWellCode);
if(!well) continue;
QTreeWidgetItem* testedWellItem = new QTreeWidgetItem(variablesTree);
// 单井和多井统一使用实际井名作为参数分组标题。
testedWellItem->setText(0, well->getWellName());
testedWellItem->setCheckState(0, Qt::Unchecked);
QStringList children;
children << "Zw" << "Hw" << "Lw" << "Skin" << "C";
for(i = 0; i < children.size(); ++i) {
QTreeWidgetItem* child = new QTreeWidgetItem(testedWellItem);
child->setText(0, children[i]);
child->setData(0, Qt::UserRole + 1, well->getWellCode());
child->setCheckState(0, Qt::Unchecked);
child->setDisabled(!isSupportedSamplingVariable(children[i]));
child->setHidden(!isSupportedSamplingVariable(children[i]));
}
}
// ===== Reservoir =====
QTreeWidgetItem *reservoirItem = new QTreeWidgetItem(variablesTree);
reservoirItem->setText(0, tr("Reservoir"));
reservoirItem->setCheckState(0, Qt::Unchecked);
reservoirItem->setFlags(reservoirItem->flags() | Qt::ItemIsUserCheckable);
QStringList reservoirChildren;
reservoirChildren << "Pi" << "k" << "h" << QString(QChar(0x03c6)) << "ntg" << "kz/kr";
for (i = 0; i < reservoirChildren.size(); ++i) {
QTreeWidgetItem *childItem = new QTreeWidgetItem(reservoirItem);
childItem->setText(0, reservoirChildren.at(i));
childItem->setCheckState(0, Qt::Unchecked);
childItem->setFlags(childItem->flags() | Qt::ItemIsUserCheckable);
childItem->setDisabled(!isSupportedSamplingVariable(reservoirChildren.at(i)));
childItem->setHidden(!isSupportedSamplingVariable(reservoirChildren.at(i)));
}
// ===== Pvt =====
QTreeWidgetItem *pvtItem = new QTreeWidgetItem(variablesTree);
pvtItem->setText(0, tr("Pvt"));
pvtItem->setCheckState(0, Qt::Unchecked);
pvtItem->setFlags(pvtItem->flags() | Qt::ItemIsUserCheckable);
QTreeWidgetItem *compressibilityItem = new QTreeWidgetItem(pvtItem);
compressibilityItem->setText(0, tr("Total compressibility"));
compressibilityItem->setCheckState(0, Qt::Unchecked);
compressibilityItem->setFlags(compressibilityItem->flags() | Qt::ItemIsUserCheckable);
compressibilityItem->setDisabled(true);
pvtItem->setHidden(true);
variablesTree->expandAll();
}
void nmWxSensitive::createDetailPages()
{
int i, j;
// 依次为每个"叶子变量"创建一个页面,并把 index 存到 item->data 里
for (i = 0; i < variablesTree->topLevelItemCount(); ++i) {
QTreeWidgetItem *topItem = variablesTree->topLevelItem(i);
for (j = 0; j < topItem->childCount(); ++j) {
QTreeWidgetItem *childItem = topItem->child(j);
if (childItem == nullptr) {
continue;
}
QString varName = childItem->text(0);
QWidget *page = createVariablePage(varName);
int pageIndex = detailStack->addWidget(page); // index 从 1 开始
childItem->setData(0, Qt::UserRole, pageIndex);
}
}
}
QIcon nmWxSensitive::createTriangleIcon()
{
QPixmap trianglePixmap(16, 16);
trianglePixmap.fill(Qt::transparent);
QPainter painter(&trianglePixmap);
painter.setRenderHint(QPainter::Antialiasing, true);
// 绘制小三角形(指向右的箭头)
QPolygon triangle;
triangle << QPoint(4, 6) << QPoint(4, 10) << QPoint(10, 8);
painter.setBrush(QBrush(QColor(0, 0, 0))); // 黑色三角形
painter.setPen(QPen(QColor(0, 0, 0), 1));
painter.drawPolygon(triangle);
return QIcon(trianglePixmap);
}
QWidget *nmWxSensitive::createVariablePage(const QString &varName)
{
QWidget *page = new QWidget(this);
QHBoxLayout *pageLayout = new QHBoxLayout(page);
pageLayout->setContentsMargins(0, 0, 0, 0);
pageLayout->setSpacing(0);
// 采样页与左侧参数树统一使用标准分组框。
QGroupBox *samplingPanel = new QGroupBox(tr("Sampling %1").arg(varName), page);
QVBoxLayout *panelLayout = new QVBoxLayout(samplingPanel);
panelLayout->setContentsMargins(10, 10, 10, 10);
panelLayout->setSpacing(8);
// ---- 内容区:左参数 + 右表格 ----
QWidget *content = new QWidget(samplingPanel);
QHBoxLayout *contentLayout = new QHBoxLayout(content);
contentLayout->setContentsMargins(0, 0, 0, 0);
contentLayout->setSpacing(12);
// 左:参数区(GridLayout)
QGridLayout *paramLayout = new QGridLayout();
paramLayout->setVerticalSpacing(8);
paramLayout->setHorizontalSpacing(8);
int row = 0;
// Mode + Log?
QLabel *modeLabel = new QLabel(tr("Mode"), content);
QComboBox *modeCombo = new QComboBox(content);
modeCombo->addItem(tr("Automatic")); // 0
modeCombo->addItem(tr("Manual")); // 1
QCheckBox *logCheck = new QCheckBox(tr("Log?"), content);
QWidget *modeRowWidget = new QWidget(content);
QHBoxLayout *modeRowLayout = new QHBoxLayout(modeRowWidget);
modeRowLayout->setContentsMargins(0, 0, 0, 0);
modeRowLayout->addWidget(modeCombo);
QComboBox* distributionCombo = new QComboBox(content);
distributionCombo->setObjectName("sensitivityDistribution_" + varName);
distributionCombo->addItems(QStringList() << tr("Normal") << tr("Lognormal")
<< tr("Truncated normal") << tr("Truncated lognormal") << tr("Uniform") << tr("Triangular"));
modeRowLayout->addWidget(distributionCombo);
modeRowLayout->addWidget(logCheck);
modeRowLayout->addStretch();
paramLayout->addWidget(modeLabel, row, 0);
paramLayout->addWidget(modeRowWidget, row, 1);
++row;
// Number
QLabel *numLabel = new QLabel(tr("Number"), content);
QSpinBox *numSpin = new QSpinBox(content);
numSpin->setRange(1, 1000);
//numSpin->setValue(1);
numSpin->setButtonSymbols(QAbstractSpinBox::NoButtons);
paramLayout->addWidget(numLabel, row, 0);
paramLayout->addWidget(numSpin, row, 1);
++row;
// Model value + 单位
QLabel *modelLabel = new QLabel(tr("Model value:"), content);
QLineEdit *modelSpin = new QLineEdit(content);
modelSpin->setObjectName("sensitivityModel_" + varName);
// 基准值来自真实模型,只作参照;输入框支持科学计数法和完整精度。
modelSpin->setReadOnly(true);
// 当前参数整页共用一个单位,下拉选项和换算均复用框架单位组。
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(unitCombo);
modelRowLayout->addStretch();
paramLayout->addWidget(modelLabel, row, 0);
paramLayout->addWidget(modelRowWidget, row, 1);
++row;
// Monte-Carlo 使用独立分布字段,切回确定性方法时保留原范围与手动表格。
QWidget* distributionPanel = new QWidget(content);
QGridLayout* distributionLayout = new QGridLayout(distributionPanel);
distributionLayout->setContentsMargins(0, 0, 0, 0);
distributionLayout->setHorizontalSpacing(8);
distributionLayout->setVerticalSpacing(0);
const int fieldHeight = modelSpin->sizeHint().height();
// Mean 与三角分布的 Mode 共用一行,其余字段保持固定位置。
distributionPanel->setFixedHeight(fieldHeight * 4 + 6 * 3);
const QStringList distributionLabels = QStringList() << tr("Mean:") << tr("Std. dev.:")
<< tr("Mode:") << tr("Minimum:") << tr("Maximum:");
for(int field = 0; field < 5; ++field) {
QLineEdit* edit = new QLineEdit(distributionPanel);
edit->setObjectName(distributionFields[field]);
edit->setFixedSize(qMax(150, modelSpin->sizeHint().width()), fieldHeight);
QLabel* label = new QLabel(distributionLabels[field], distributionPanel);
label->setObjectName(QString(distributionFields[field]) + "Label");
label->setFixedHeight(fieldHeight);
const int fieldRow = field == 2 ? 0 : field > 2 ? field - 1 : field;
distributionLayout->setRowMinimumHeight(fieldRow, fieldHeight + (fieldRow < 3 ? 6 : 0));
distributionLayout->addWidget(label, fieldRow, 0, Qt::AlignTop);
distributionLayout->addWidget(edit, fieldRow, 1, Qt::AlignTop);
connect(edit, SIGNAL(textChanged(QString)), this, SLOT(onVariableParamChanged()));
}
distributionLayout->setColumnStretch(2, 1);
paramLayout->addWidget(distributionPanel, row++, 0, 1, 2);
// Minimum
QLabel *minLabel = new QLabel(tr("Minimum:"), content);
QLineEdit *minSpin = new QLineEdit(content);
minSpin->setObjectName("sensitivityMinimum_" + varName);
paramLayout->addWidget(minLabel, row, 0);
paramLayout->addWidget(minSpin, row, 1);
++row;
// Maximum
QLabel *maxLabel = new QLabel(tr("Maximum:"), content);
QLineEdit *maxSpin = new QLineEdit(content);
maxSpin->setObjectName("sensitivityMaximum_" + varName);
paramLayout->addWidget(maxLabel, row, 0);
paramLayout->addWidget(maxSpin, row, 1);
++row;
// 基本参数与分布参数使用相同标签列宽、列间距和输入宽度,保证上下对齐。
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);
modelSpin->setFixedWidth(editWidth);
minSpin->setFixedWidth(editWidth);
maxSpin->setFixedWidth(editWidth);
modeCombo->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
numSpin->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
modelSpin->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
minSpin->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
maxSpin->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
QWidget *paramWidget = new QWidget(content);
QVBoxLayout *leftVBox = new QVBoxLayout(paramWidget);
leftVBox->setContentsMargins(0, 0, 0, 0);
leftVBox->addLayout(paramLayout);
leftVBox->addStretch();
// 右:Values 表格(双列:箭头 + 值)
QTableWidget *valuesTable = new QTableWidget(content);
valuesTable->setObjectName("sensitivityValues_" + varName);
valuesTable->setColumnCount(2);
QStringList headers;
headers << "" << tr("Values");
valuesTable->setHorizontalHeaderLabels(headers);
// 沿用编辑井的行号、隔行底色和网格样式,保留采样行的箭头标记。
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->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);
valuesTable->setSelectionMode(QAbstractItemView::NoSelection);
valuesTable->setFocusPolicy(Qt::NoFocus);
valuesTable->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
valuesTable->setShowGrid(true);
valuesTable->setItemDelegateForColumn(1, new RightAlignDelegate(valuesTable));
// 与编辑井一样使用可拖动分栏,表格和分布图共用右侧空间。
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(samplingSplitter);
QVBoxLayout *tableLayout = new QVBoxLayout(tableContainer);
tableLayout->setContentsMargins(0, 0, 0, 0);
tableLayout->setSpacing(0);
tableLayout->addWidget(valuesTable);
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);
page->setEnabled(isSupportedSamplingVariable(varName));
// ===== 保存控件指针 =====
m_modeCombos.append(modeCombo);
m_logChecks.append(logCheck);
m_numSpins.append(numSpin);
m_modelSpins.append(modelSpin);
m_minSpins.append(minSpin);
m_maxSpins.append(maxSpin);
m_valueTables.append(valuesTable);
m_unitCombos.append(unitCombo);
connect(unitCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onSamplingUnitChanged()));
m_numLabels.append(numLabel);
m_minLabels.append(minLabel);
m_maxLabels.append(maxLabel);
m_distributionCombos.append(distributionCombo);
m_distributionPanels.append(distributionPanel);
m_distributionPlots.append(distributionPlot);
connect(distributionCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onVariableParamChanged()));
connect(numSpin, SIGNAL(valueChanged(int)),
this, SLOT(onVariableParamChanged()));
connect(minSpin, SIGNAL(textChanged(QString)),
this, SLOT(onVariableParamChanged()));
connect(maxSpin, SIGNAL(textChanged(QString)),
this, SLOT(onVariableParamChanged()));
connect(minSpin, SIGNAL(editingFinished()),
this, SLOT(onSamplingEditingFinished()));
connect(maxSpin, SIGNAL(editingFinished()),
this, SLOT(onSamplingEditingFinished()));
connect(modeCombo, SIGNAL(currentIndexChanged(int)),
this, SLOT(onVariableParamChanged()));
connect(logCheck, SIGNAL(toggled(bool)),
this, SLOT(onVariableParamChanged()));
connect(valuesTable, SIGNAL(cellClicked(int,int)),
this, SLOT(onTableCellClicked(int,int)));
return page;
}
void nmWxSensitive::updateValuesForPage(int idx)
{
if (idx < 0 || idx >= m_numSpins.size()) return;
const bool monteCarlo = calculationTypeCombo->itemData(calculationTypeCombo->currentIndex()).toInt() == nmDataSensitive::CALC_MONTE_CARLO;
m_modeCombos[idx]->setVisible(!monteCarlo);
m_logChecks[idx]->setVisible(!monteCarlo);
m_numLabels[idx]->setVisible(!monteCarlo);
m_numSpins[idx]->setVisible(!monteCarlo);
m_minLabels[idx]->setVisible(!monteCarlo);
m_maxLabels[idx]->setVisible(!monteCarlo);
m_minSpins[idx]->setVisible(!monteCarlo);
m_maxSpins[idx]->setVisible(!monteCarlo);
m_valueTables[idx]->setVisible(!monteCarlo);
m_distributionCombos[idx]->setVisible(monteCarlo);
m_distributionPanels[idx]->setVisible(monteCarlo);
m_distributionPlots[idx]->setVisible(monteCarlo);
// 表格采用较窄的默认宽度;分布图让参数区仅占所需宽度,避免沿用表格分栏。
// 只在布局类型切换时调整,编辑参数时保留用户拖动后的宽度。
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];
const bool automatic = m_modeCombos[idx]->currentIndex() == 0;
m_minSpins[idx]->setEnabled(automatic);
m_maxSpins[idx]->setEnabled(automatic);
m_logChecks[idx]->setEnabled(automatic);
const int number = m_numSpins[idx]->value();
table->setRowCount(number);
nmDataSensitive::VariableSampling variable;
for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) {
if (findVariableIndex(m_sensitiveData.getVariable(i)) == idx) {
variable = m_sensitiveData.getVariable(i);
break;
}
}
QVector<double> samples;
const QString name = variable.getVarName();
const QString unit = m_unitCombos[idx]->currentText();
if (automatic) {
bool minOk = false, maxOk = false;
double minimum = samplingEditValue(m_minSpins[idx], &minOk);
double maximum = samplingEditValue(m_maxSpins[idx], &maxOk);
if (minOk && maxOk && minimum <= maximum &&
convertSamplingValue(name, minimum, unit, samplingBaseUnit(name), minimum) &&
convertSamplingValue(name, maximum, unit, samplingBaseUnit(name), maximum) &&
validSamplingValue(name, minimum) && validSamplingValue(name, maximum)) {
variable.setMode(nmDataSensitive::VariableSampling::MODE_AUTOMATIC);
variable.setNumber(number);
variable.setLog(m_logChecks[idx]->isChecked());
variable.getMinValue().setValue(minimum);
variable.getMaxValue().setValue(maximum);
samples = variable.samplingValues();
}
}
for (int row = 0; row < number; ++row) {
QTableWidgetItem* arrow = table->item(row, 0);
if (!arrow) { arrow = new QTableWidgetItem(); table->setItem(row, 0, arrow); }
arrow->setFlags(Qt::ItemIsEnabled);
arrow->setIcon(row == m_selectedValueIndex ? createTriangleIcon() : QIcon());
QTableWidgetItem* item = table->item(row, 1);
if (!item) { item = new QTableWidgetItem(); table->setItem(row, 1, item); }
item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
item->setFlags(automatic ? Qt::ItemIsEnabled : Qt::ItemIsEnabled | Qt::ItemIsEditable);
if (automatic) {
double display = row < samples.size() ? samples[row] : 0.0;
const bool valid = row < samples.size() &&
convertSamplingValue(name, display, samplingBaseUnit(name), unit, display);
item->setData(Qt::UserRole, valid ? QVariant(display) : QVariant());
item->setText(valid ? formatSamplingValue(display, 3) : QString());
}
}
}
bool nmWxSensitive::readDistributionPage(int idx, nmDataSensitive::VariableSampling& variable) const
{
// 字段转换沿用现有参数单位,仅校验当前分布实际使用的字段。
variable.m_distribution = static_cast<nmDataSensitive::VariableSampling::Distribution>(
m_distributionCombos[idx]->currentIndex());
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;
for(int field = 0; field < 5; ++field) {
const bool used = field < 2 ? distribution < nmDataSensitive::VariableSampling::UNIFORM
: field == 2 ? distribution == nmDataSensitive::VariableSampling::TRIANGULAR
: distribution >= nmDataSensitive::VariableSampling::TRUNCATED_NORMAL;
if(!used) continue;
bool ok = false;
double value = samplingEditValue(m_distributionPanels[idx]->findChild<QLineEdit*>(distributionFields[field]), &ok);
if(!ok || !qIsFinite(value) || !convertSamplingValue(variable.m_varName, value,
m_unitCombos[idx]->currentText(), samplingBaseUnit(variable.m_varName), *fields[field], field == 1)) return false;
}
return true;
}
void nmWxSensitive::updateDistributionPage(int idx)
{
// 只隐藏所选分布不用的字段,保留各行位置;编辑时更新曲线,不提前抽样。
const int distribution = m_distributionCombos[idx]->currentIndex();
for(int field = 0; field < 5; ++field) {
QWidget* edit = m_distributionPanels[idx]->findChild<QLineEdit*>(distributionFields[field]);
const bool used = field < 2 ? distribution < nmDataSensitive::VariableSampling::UNIFORM
: field == 2 ? distribution == nmDataSensitive::VariableSampling::TRIANGULAR
: distribution >= nmDataSensitive::VariableSampling::TRUNCATED_NORMAL;
edit->setVisible(used);
m_distributionPanels[idx]->findChild<QLabel*>(QString(distributionFields[field]) + "Label")->setVisible(used);
}
for(int i = 0; i < m_sensitiveData.getVariableCount(); ++i) {
nmDataSensitive::VariableSampling variable = m_sensitiveData.getVariable(i);
if(findVariableIndex(variable) != idx) continue;
const bool valid = readDistributionPage(idx, variable);
m_distributionPlots[idx]->setVariable(variable, valid);
break;
}
}
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()
{
// 输入结束后截断越界范围,再按六位有效数字显示,计算仍使用原始精度。
QLineEdit* edit = qobject_cast<QLineEdit*>(sender());
if (m_loading || !edit) return;
bool ok = false;
double value = samplingEditValue(edit, &ok);
if (!ok || !qIsFinite(value)) return;
for (int idx = 0; idx < m_minSpins.size(); ++idx) {
if (edit != m_minSpins[idx] && edit != m_maxSpins[idx]) continue;
for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) {
const nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i);
const QString name = variable.getVarName();
if (findVariableIndex(variable) != idx) continue;
double lower = 0.0, upper = 0.0;
const QString unit = m_unitCombos[idx]->currentText();
const QString baseUnit = samplingBaseUnit(name);
if (!samplingPhysicalRange(name, lower, upper) ||
!convertSamplingValue(name, value, unit, baseUnit, value)) return;
value = qBound(lower, value, upper);
if (!convertSamplingValue(name, value, baseUnit, unit, value)) return;
setSamplingEditValue(edit, value);
updateValuesForPage(idx);
return;
}
}
}
void nmWxSensitive::onVariableParamChanged()
{
// 数据加载时不响应控件逐项赋值,编辑时只刷新所属参数页。
if (m_loading) return;
// 分布设置或次数改变后,生成必须重新抽样,不能使用旧预览。
m_sensitiveData.clearMonteCarloSamples();
for (int i = 0; i < m_numSpins.size(); ++i) {
if (sender() == m_numSpins[i] || sender() == m_minSpins[i] ||
sender() == m_maxSpins[i] || sender() == m_modeCombos[i] ||
sender() == m_logChecks[i] || sender() == m_distributionCombos[i] ||
sender()->parent() == m_distributionPanels[i]) {
m_selectedValueIndex = qMin(m_selectedValueIndex, m_numSpins[i]->value() - 1);
updateValuesForPage(i);
break;
}
}
updateModelCount();
}
void nmWxSensitive::onTableCellClicked(int row, int column)
{
Q_UNUSED(column);
// 选中标记不改写表格内容,避免自动重算或丢失手动输入。
m_selectedValueIndex = row;
QTableWidget* table = qobject_cast<QTableWidget*>(sender());
if (!table) return;
for (int i = 0; i < table->rowCount(); ++i)
if (table->item(i, 0))
table->item(i, 0)->setIcon(i == row ? createTriangleIcon() : QIcon());
}
void nmWxSensitive::onTreeItemChanged(QTreeWidgetItem* item, int column)
{
Q_UNUSED(column);
if (m_loading) return;
// 勾选变量改变会改变整批组合;初始化树状态时保留已有样本。
if(item) m_sensitiveData.clearMonteCarloSamples();
// 分组选择与子项选择双向同步,禁用的参数不参与全选和总次数。
m_loading = true;
variablesTree->blockSignals(true);
if (item && item->childCount() > 0) {
const Qt::CheckState state = item->checkState(0);
for (int i = 0; i < item->childCount(); ++i)
if (!item->child(i)->isDisabled()) item->child(i)->setCheckState(0, state);
}
for (int group = 0; group < variablesTree->topLevelItemCount(); ++group) {
QTreeWidgetItem* parent = variablesTree->topLevelItem(group);
int available = 0, checked = 0;
for (int i = 0; i < parent->childCount(); ++i) {
if (parent->child(i)->isDisabled()) continue;
++available;
if (parent->child(i)->checkState(0) == Qt::Checked) ++checked;
}
parent->setCheckState(0, checked == 0 ? Qt::Unchecked
: checked == available ? Qt::Checked : Qt::PartiallyChecked);
parent->setDisabled(available == 0);
}
variablesTree->blockSignals(false);
m_loading = false;
updateModelCount();
}
void nmWxSensitive::onGenerateClicked()
{
if(m_calculationRunning || !m_pDataManager || !saveDataFromUI()) return;
nmDataWellBase* well = m_pDataManager->findWellByCode(m_pDataManager->getPrimaryWellCode());
if(!well || well->getWellName().isEmpty()) {
QMessageBox::warning(this, tr("Sensitivity"), tr("The target well is unavailable."));
return;
}
// 气井转换器使用本窗口所属分析的配置,不能随当前活动窗口切换。
if(m_pDataManager->getSolverModelType() == SMT_Gas_VariablePvt) {
iSubWndFitting* fitting = nullptr;
QMap<iSubWndFitting*, nmDataAnalyzeManager*>::const_iterator it =
nmDataAnalyzeManager::s_mapDataAnalManager.constBegin();
for(; it != nmDataAnalyzeManager::s_mapDataAnalManager.constEnd(); ++it) {
if(it.value() == m_pDataManager) { fitting = it.key(); break; }
}
iAnalRun* analRun = fitting ? fitting->getAnalRun() : nullptr;
// 第四种可选择其他井拟合,其历史曲线也要使用本次的拟压力配置。
nmDataWellBase* fitWell = m_sensitiveData.isImproveEnabled() && !m_sensitiveData.getImproveSettings().isNull()
? m_pDataManager->findWellByName(m_sensitiveData.getImproveSettings()->targetWell) : nullptr;
if(!analRun || !analRun->configPsAbouts(true, fitting->getModelOption(),
false, fitting->getAllWxPtr()) ||
!m_pDataManager->updateWellHistoryData(well) ||
(fitWell && fitWell != well && !m_pDataManager->updateWellHistoryData(fitWell))) {
QMessageBox::warning(this, tr("Sensitivity"),
tr("Gas pseudo-pressure data is unavailable or invalid."));
return;
}
}
QString error;
QScopedPointer<nmCalculationDllPebiSolverTask> task;
try {
// 优先使用预览的整批样本;未预览或设置改变后才重新采样。
if(m_sensitiveData.getCalculationType() == nmDataSensitive::CALC_MONTE_CARLO) {
QVector<double> values;
if(!m_sensitiveData.getRunValues(0, values) && !m_sensitiveData.resampleMonteCarlo(&error)) {
QMessageBox::warning(this, tr("Sensitivity"), error);
return;
}
}
// 启动时读取当前模型,后台各组计算只修改任务自己的参数副本。
nmCalculationDllPebiSolverTask source(QString(), m_pDataManager, well->getWellName());
// 第四种直接交给后台任务逐组拟合,仍使用主窗口原有进度框。
task.reset(source.createSensitivityBatch(m_sensitiveData, &error,
m_sensitiveData.isImproveEnabled() ? m_sensitiveData.getImproveSettings().data() : nullptr));
} catch(const std::bad_alloc&) {
error = tr("Insufficient memory to prepare solver input.");
}
if(!task) {
QMessageBox::warning(this, tr("Sensitivity"), error);
return;
}
m_pDataManager->updateSensitiveData(m_sensitiveData);
m_sourceData = m_pDataManager->getSensitiveData();
// 新任务准备成功后才替换上次临时结果,准备失败仍可保留旧结果。
delete m_pSensitivityTask;
m_pSensitivityTask = task.take();
m_pSensitivityTask->setParent(this);
// 进度框和停止按钮由主窗口统一管理;接管失败时不启动本次任务。
emit sigSensitivityTaskReady(this, m_pSensitivityTask);
if(!m_calculationRunning) {
delete m_pSensitivityTask;
m_pSensitivityTask = nullptr;
}
}
void nmWxSensitive::startSensitivityCalculation()
{
// 读取和清理必须等 QThread 真正结束,不能由 run() 内的提前通知触发。
connect(m_pSensitivityTask, SIGNAL(finished()),
this, SLOT(onSensitivityFinished()), Qt::QueuedConnection);
setCalculationRunning(true);
// 计算期间隐藏输入窗口,任务结束后继续复用。
hide();
m_pSensitivityTask->start();
}
void nmWxSensitive::onCancelClicked()
{
if(m_calculationRunning && m_pSensitivityTask) {
// 关闭或 Esc 也走主窗口现有停止路径,保持进度框的等待状态一致。
emit sigSensitivityCancelRequested();
return;
}
reject();
}
void nmWxSensitive::setCalculationRunning(bool running)
{
// 计算过程中固定采样输入,仅保留停止操作;结束后恢复编辑与再次生成。
m_calculationRunning = running;
calculationTypeCombo->setEnabled(!running);
variablesTree->setEnabled(!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);
} else {
updateModelCount();
}
}
void nmWxSensitive::onSensitivityFinished()
{
if(sender() != m_pSensitivityTask) return;
m_pSensitivityTask->wait();
// 数据层接管曲线后释放计算任务及网格输入,结果窗口不依赖任务存活。
QSharedPointer<nmDataSensitive::BatchResult> batch = m_pSensitivityTask->getSensitivityBatchResult();
delete m_pSensitivityTask;
m_pSensitivityTask = nullptr;
setCalculationRunning(false);
m_sensitiveData.setResults(batch);
m_pDataManager->publishSensitiveResults(m_sensitiveData);
const std::vector<nmDataSensitive::RunResult>& results = batch->m_runs;
QStringList errors;
for(size_t i = 0; i < results.size() && errors.size() < 5; ++i) {
const nmDataSensitive::RunResult& result = results[i];
if(!result.m_bSucceeded && !result.m_bCancelled)
errors.append(tr("Run %1: %2").arg(result.m_nRunIndex + 1).arg(result.m_sFailureReason));
}
const QString fatalError = batch->m_sFailureReason;
// 实际计算错误仍需提示原因,取消组不作为错误报告。
if(fatalError.isEmpty() && errors.isEmpty()) return;
QString message = fatalError;
if(!errors.isEmpty()) {
if(!message.isEmpty()) message += "\n\n";
message += errors.join("\n");
}
QWidget* ownerWindow = parentWidget() ? parentWidget()->window() : nullptr;
QMessageBox::warning(ownerWindow, tr("Sensitivity"), message);
}
void nmWxSensitive::onResultsChanged()
{
// 同步当前批次,新模型清除旧结果时也清除本地引用。
QSharedPointer<nmDataSensitive::BatchResult> results;
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)
{
m_improveSettingsButton->setVisible(index == 3);
// 复用现有参数页,切换方法只改变可见控件,不覆盖另一种方法的设置。
if(m_loading) return;
for(int i = 0; i < m_numSpins.size(); ++i) updateValuesForPage(i);
updateModelCount();
}
void nmWxSensitive::onImproveSettingsClicked()
{
// 在副本上编辑,只有确定才替换设置;取消保留上次确认的内容。
nmSensitivityFitSettings settings;
if(!m_sensitiveData.getImproveSettings().isNull()) settings = *m_sensitiveData.getImproveSettings();
// 将分析窗口当前勾选的采样参数传给拟合窗口,用于禁用重叠选项。
updateModelCount();
QStringList sampledParameters;
foreach(const nmDataSensitive::VariableSampling& variable, m_sensitiveData.getVariables())
if(variable.m_enabled) sampledParameters.append(variable.parameterKey());
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();
}
}
void nmWxSensitive::onTreeItemClicked(QTreeWidgetItem* item, int column)
{
Q_UNUSED(column);
if (item->childCount() > 0) { detailStack->setCurrentIndex(0); return; }
const int pageIndex = item->data(0, Qt::UserRole).toInt();
if (pageIndex <= 0) return;
detailStack->setCurrentIndex(pageIndex);
// 页面切换只更新箭头,保留用户正在编辑的采样表。
m_selectedValueIndex = 0;
QTableWidget* table = m_valueTables[pageIndex - 1];
for (int row = 0; row < table->rowCount(); ++row)
if (table->item(row, 0))
table->item(row, 0)->setIcon(row == 0 ? createTriangleIcon() : QIcon());
}
void nmWxSensitive::onPreviewClicked()
{
if (!saveDataFromUI()) return;
if(m_sensitiveData.getCalculationType() == nmDataSensitive::CALC_MONTE_CARLO) {
QString error;
if(!m_sensitiveData.resampleMonteCarlo(&error)) {
QMessageBox::warning(this, tr("Sensitivity"), error);
return;
}
// 样本已在允许范围内生成,直接显示预览。
}
// Saphir 风格的连续运行列表,组合数再大也不显示分页控件。
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();
}
void nmWxSensitive::onPreviewScrollAction(int action)
{
// 将原生滚动条的单步、整屏和拖动操作转交连续列表。
if (m_previewView) m_previewView->scrollAction(action);
}