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

2490 lines
113 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 "nmDataSensitive.h"
#include "nmDataReservoir.h"
#include "nmDataWellBase.h"
#include "nmDataPerforation.h"
#include "nmPebiResultSnapshot.h"
#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 <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)
{
// 优先复用项目单位组;旧配置的 psi 按绝对压力单位迁移。
if (from == "psi") from = "psia";
if (to == "psi") to = "psia";
if (from == "dimensionless") from.clear();
if (to == "dimensionless") to.clear();
if (from == to || (from.isEmpty() && to.isEmpty())) {
converted = value;
return qIsFinite(converted);
}
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 (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.m_varName + " = " + 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) {}
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;
}
QVariant headerData(int section, Qt::Orientation orientation, int role) const override
{
// 表格仅保留显示选择、运行编号与冻结参数。
if(orientation != Qt::Horizontal || role != Qt::DisplayRole) return QVariant();
if(m_statistics) 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);
return hasSensitivityError(run) ? QString::number(run.m_dPressureRmse, '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);
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));
}
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();
}
}
private:
QSharedPointer<nmDataSensitive::BatchResult> m_results;
QMap<int, bool> m_pendingVisibility;
QPointer<iGuiPlot> m_plot;
bool m_statistics;
};
nmWxSensitivityShow::nmWxSensitivityShow(
const QSharedPointer<nmDataSensitive::BatchResult>& results, iGuiPlot* plot, QWidget* parent)
: iDlgBase(parent)
{
// 沿用原 Results 的白色表格及按钮布局,勾选在 Apply 或 OK 后生效。
setAttribute(Qt::WA_DeleteOnClose);
setObjectName("nmSensitivityShowDialog");
resize(700, 380);
QVBoxLayout* layout = new QVBoxLayout(this);
QTableView* table = new QTableView(this);
table->setStyleSheet("QTableView { background-color: white; }");
m_model = new SensitivityResultsModel(results, plot, this);
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();
QPushButton* showAll = new QPushButton(tr("Show all"), this);
QPushButton* hideAll = new QPushButton(tr("Hide all"), this);
// 明确按钮顺序,避免系统的标准按钮布局把 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(showAll);
buttons->addWidget(hideAll);
buttons->addStretch();
buttons->addWidget(apply);
buttons->addWidget(ok);
buttons->addWidget(cancel);
layout->addLayout(buttons);
connect(showAll, SIGNAL(clicked()), this, SLOT(onShowAll()));
connect(hideAll, SIGNAL(clicked()), this, SLOT(onHideAll()));
connect(apply, SIGNAL(clicked()), this, SLOT(onApply()));
connect(ok, SIGNAL(clicked()), this, SLOT(onAccept()));
connect(cancel, SIGNAL(clicked()), this, SLOT(reject()));
}
void nmWxSensitivityShow::onApply()
{
// 应用选择,保留窗口以便继续调整。
m_model->applyVisibility();
}
void nmWxSensitivityShow::onAccept()
{
// 确认时提交勾选,取消则直接丢弃未应用的选择。
onApply();
accept();
}
void nmWxSensitivityShow::onShowAll()
{
// 只修改待应用的采样组选择。
m_model->setAllVisible(true);
}
void nmWxSensitivityShow::onHideAll()
{
// 参考曲线不属于采样组,保持原来的显示方式。
m_model->setAllVisible(false);
}
nmWxSensitivityHistogramOptions::nmWxSensitivityHistogramOptions(
const QSharedPointer<nmDataSensitive::BatchResult>& results, int bins, QWidget* parent)
: iDlgBase(parent)
{
// 复用运行表格的参数展示,普通 Monte-Carlo 的统计变量只有误差。
setAttribute(Qt::WA_DeleteOnClose);
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));
QComboBox* variable = new QComboBox(this);
variable->addItem(tr("Goodness of fit"));
options->addWidget(variable);
layout->addLayout(options);
QTableView* 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);
table->horizontalHeader()->setResizeMode(2, 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::onApply()
{
// 只提交分箱数,取消不会撤销已经应用的设置。
emit binsChanged(m_bins->value());
}
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);
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].m_varName, i);
options->addWidget(m_parameter, 1);
options->addSpacing(20);
options->addWidget(new QLabel(tr("Y Axis:"), this));
QComboBox* ordinate = new QComboBox(this);
ordinate->addItem(tr("Goodness of fit"));
options->addWidget(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_plot->m_pPlotView, SIGNAL(sigAdjustContextMenu(QMenu*)), this, SLOT(adjustContextMenu(QMenu*)));
updateParameter();
}
void nmWxSensitivityScatter::updateParameter()
{
// 切换横轴时重建参数坐标,阈值在各参数之间共用。
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.m_varName, unit);
double modelValue = variable.m_modelValue.getValue().toDouble();
const bool hasModel = convertSamplingValue(variable.m_varName, modelValue,
samplingBaseUnit(variable.m_varName), unit, modelValue) && m_baseError >= 0.0 && qIsFinite(m_baseError);
for(size_t i = 0; i < m_results->m_runs.size(); ++i) {
const nmDataSensitive::RunResult& run = m_results->m_runs[i];
if(!hasSensitivityError(run) || 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, run.m_dPressureRmse));
m_runRows.append(int(i));
}
QVector<iCurveDesc> curves;
for(int i = 0; i < 4; ++i) {
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(m_baseError);
}
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, top = hasModel ? m_baseError : 0.0;
for(int i = 0; i < m_points.size(); ++i) {
left = qMin(left, m_points[i].x());
right = qMax(right, m_points[i].x());
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;
top = qMax(1e-6, qMax(top, m_threshold) * 1.1);
VecDouble ticksX, ticksY;
for(int i = 0; i <= 5; ++i) {
ticksX.append(left + i * (right - left) / 5.0);
ticksY.append(i * top / 5.0);
}
m_plot->setAxisScaleX(left, right, ticksX);
m_plot->setAxisScaleY(0.0, top, ticksY);
updateThreshold();
}
void nmWxSensitivityScatter::updateThreshold()
{
// 阈值以下的点填黑,并同步图例中的阈值显示。
if(m_parameter->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)
{
// 复用绘图右键菜单,保留框架的缩放、复制与导出入口。
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_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(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]];
QToolTip::showText(help->globalPos(), tr("Run %1\n%2\nGoodness of fit: %3 MPa")
.arg(run.m_nRunIndex + 1).arg(sensitivityRunParameters(*m_results, run))
.arg(run.m_dPressureRmse, 0, 'g', 6), 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(), palette().color(QPalette::Window));
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)
{
// 使用现有表格控件绘制表头、行选中和单元格,滚动条控制完整列表。
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.getVarName() + " = " + 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_previewView(nullptr), m_pSensitivityTask(nullptr), m_calculationRunning(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 iDlgBase::reject();
}
void nmWxSensitive::closeEvent(QCloseEvent* event)
{
if(m_calculationRunning) {
onCancelClicked();
event->ignore();
return;
}
iDlgBase::closeEvent(event);
}
void nmWxSensitive::refreshModelValues()
{
// 六个参数以最后成功求解的快照为准,编辑中的模型只提供属性类型和显示单位。
const QSharedPointer<const nmPebiResultSnapshot> snapshot = m_pDataManager
? m_pDataManager->getPebiResultSnapshot() : QSharedPointer<const nmPebiResultSnapshot>();
const bool resetRanges = !snapshot.isNull() &&
snapshot->getSnapshotId() != m_sensitiveData.getModelSnapshotId();
const QString wellCode = !snapshot.isNull() ? snapshot->getSensitivityWellCode() : QString();
nmDataWellBase* well = m_pDataManager ? m_pDataManager->findWellByCode(wellCode) : nullptr;
nmDataReservoir* reservoir = m_pDataManager ? m_pDataManager->getReservoirData() : nullptr;
const nmPebiResultWellSnapshot* solvedWell = nullptr;
for(int i = 0; !snapshot.isNull() && i < snapshot->getWellCount(); ++i) {
if(snapshot->getWellAt(i)->m_sWellCode == wellCode) {
solvedWell = snapshot->getWellAt(i);
break;
}
}
const QStringList names = QStringList() << "Skin" << "C" << "Pi" << "k" << "h" << QString(QChar(0x03c6));
for (int n = 0; n < names.size(); ++n) {
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) { index = i; break; }
const bool fresh = index < 0;
if (fresh) {
nmDataSensitive::VariableSampling variable;
variable.setVarName(name);
variable.setVarGroup(n < 2 ? "Tested Well" : "Reservoir");
variable.setNumber(5);
m_sensitiveData.getVariables().append(variable);
index = m_sensitiveData.getVariableCount() - 1;
}
nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(index);
// 在局部属性上替换数值,不能把未求解的编辑值带入 Model value 或默认范围。
nmDataAttribute solvedAttribute;
if (!snapshot.isNull()) {
if (attribute) solvedAttribute = *attribute;
else solvedAttribute.setUnit(samplingBaseUnit(name));
attribute = &solvedAttribute;
const nmPebiResultReservoirParameters& solvedReservoir = snapshot->getReservoirParameters();
if (name == "Skin" || name == "C") {
if (!solvedWell || (name == "Skin" && !solvedWell->m_bHasSkin)) attribute = nullptr;
else solvedAttribute.setValue(name == "Skin" ? solvedWell->m_dSkin : solvedWell->m_dWellboreStorage);
} else if (name == "Pi") solvedAttribute.setValue(solvedReservoir.m_dInitialPressure);
else if (name == "k") solvedAttribute.setValue(solvedReservoir.m_dPermeability);
else if (name == "h") solvedAttribute.setValue(solvedReservoir.m_dThickness);
else if (name == QString(QChar(0x03c6))) solvedAttribute.setValue(solvedReservoir.m_dPorosity);
}
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 = 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]);
}
// 识别 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 (!snapshot.isNull()) m_sensitiveData.setModelSnapshotId(snapshot->getSnapshotId());
}
void nmWxSensitive::onModelResultChanged()
{
// 查看井切换也会发送快照通知;只有新求解快照才刷新已打开窗口。
const QSharedPointer<const nmPebiResultSnapshot> snapshot = m_pDataManager->getPebiResultSnapshot();
if (snapshot.isNull() || snapshot->getSnapshotId() == m_sensitiveData.getModelSnapshotId()) return;
m_loading = true;
refreshModelValues();
loadDataToUI(true);
m_loading = false;
updateModelCount();
}
void nmWxSensitive::loadDataToUI(bool refreshRangesOnly)
{
// 加载过程中阻止联动刷新,先恢复设置,再一次生成各参数表格。
if (!refreshRangesOnly)
calculationTypeCombo->setCurrentIndex(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(name);
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) 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 = refreshRangesOnly ? m_unitLabels[idx]->text() : variable.getModelValue().getUnit();
variable.getModelValue().setUnit(unit);
m_unitLabels[idx]->setText(unit);
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);
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 QString& varName)
{
// 根据创建顺序查找变量索引
QStringList allVars;
allVars << "Zw" << "Hw" << "Lw" << "Skin" << "C" // Tested Well
<< "Pi" << "k" << "h" << QString(QChar(0x03c6)) << "ntg" << "kz/kr" // Reservoir
<< "Total compressibility"; // Pvt
return allVars.indexOf(varName);
}
void nmWxSensitive::updateModelCount()
{
// 勾选、数量和方法修改后立即更新总次数,不等待 Generate。
if (m_loading) return;
m_sensitiveData.setCalculationType(
static_cast<nmDataSensitive::CalculationType>(calculationTypeCombo->currentIndex()));
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)) continue;
variable.setEnabled(!item->isDisabled() && item->checkState(0) == Qt::Checked);
const int idx = findVariableIndex(variable.getVarName());
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);
generateButton->setEnabled(m_pDataManager && !overflow && count > 0 && !m_calculationRunning);
}
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(name);
if (idx < 0 || !isSupportedSamplingVariable(name)) continue;
const QString unit = m_unitLabels[idx]->text();
const QString baseUnit = samplingBaseUnit(name);
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);
// 默认放大窗口,同时为屏幕边缘和窗口标题栏留出空间。
const QSize available = QApplication::desktop()->availableGeometry(this).size() - QSize(60, 80);
resize(QSize(1250, 760).boundedTo(available));
QVBoxLayout *mainLayout = new QVBoxLayout(this);
// ===== 顶部:Calculation type =====
QHBoxLayout *calcTypeLayout = new QHBoxLayout();
QLabel *calcTypeLabel = new QLabel(tr("Calculation type:"), this);
calculationTypeCombo = new QComboBox(this);
calculationTypeCombo->addItem(tr("Deterministic"));
calculationTypeCombo->addItem(tr("Deterministic multivariate"));
calculationTypeCombo->addItem(tr("Monte-Carlo"));
//calculationTypeCombo->addItem(tr("Monte-Carlo + Improve"));
calcTypeLayout->addWidget(calcTypeLabel);
calcTypeLayout->addWidget(calculationTypeCombo);
calcTypeLayout->addStretch();
mainLayout->addLayout(calcTypeLayout);
// ===== 中间:左 Variables 面板 + 右 detailStack =====
QSplitter *centerSplitter = new QSplitter(Qt::Horizontal, this);
centerSplitter->setChildrenCollapsible(false); // 不允许把某一侧完全折叠
centerSplitter->setHandleWidth(4); // 中间拖动条宽度
centerSplitter->setStyleSheet(
"QSplitter::handle:horizontal {"
" background-color: #C0C0C0;"
"}"
"QSplitter::handle:horizontal:hover {"
" background-color: #A0A0A0;"
"}"
);
// --- 左侧:Variables 面板(QFrame + 标题栏 + Tree)---
QFrame *variablesPanel = new QFrame(this);
variablesPanel->setFrameShape(QFrame::StyledPanel);
variablesPanel->setFrameShadow(QFrame::Raised);
variablesPanel->setMinimumWidth(230);
QVBoxLayout *variablesPanelLayout = new QVBoxLayout(variablesPanel);
variablesPanelLayout->setContentsMargins(0, 0, 0, 0);
variablesPanelLayout->setSpacing(0);
// 标题栏
QWidget *varHeader = new QWidget(variablesPanel);
varHeader->setFixedHeight(24);
varHeader->setAutoFillBackground(true);
{
QPalette pal = varHeader->palette();
pal.setColor(QPalette::Window,
palette().color(QPalette::Midlight));
varHeader->setPalette(pal);
}
QHBoxLayout *varHeaderLayout = new QHBoxLayout(varHeader);
varHeaderLayout->setContentsMargins(6, 0, 6, 0);
varHeaderLayout->setSpacing(0);
QLabel *varTitle = new QLabel(tr("Variables"), varHeader);
varTitle->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
QFont f = varTitle->font();
f.setBold(true);
varTitle->setFont(f);
varHeaderLayout->addWidget(varTitle);
varHeaderLayout->addStretch();
// Tree 区域
variablesTree = new QTreeWidget(variablesPanel);
variablesTree->setHeaderHidden(true);
variablesTree->setFrameShape(QFrame::NoFrame); // 外层已经有 frame
variablesTree->setMinimumHeight(350);
variablesPanelLayout->addWidget(varHeader);
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>() << 260 << 960);
mainLayout->addWidget(centerSplitter, 1);
// ===== 底部:Total number of models + 按钮 =====
QHBoxLayout *bottomLayout = new QHBoxLayout();
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);
bottomLayout->addWidget(generateButton);
bottomLayout->addWidget(cancelButton);
mainLayout->addLayout(bottomLayout);
}
void nmWxSensitive::createConnections()
{
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();
// ===== Tested Well =====
QTreeWidgetItem *testedWellItem = new QTreeWidgetItem(variablesTree);
testedWellItem->setText(0, tr("Tested Well"));
testedWellItem->setCheckState(0, Qt::Unchecked);
testedWellItem->setFlags(testedWellItem->flags() | Qt::ItemIsUserCheckable);
QStringList testedWellChildren;
testedWellChildren << "Zw" << "Hw" << "Lw" << "Skin" << "C";
int i;
for (i = 0; i < testedWellChildren.size(); ++i) {
QTreeWidgetItem *childItem = new QTreeWidgetItem(testedWellItem);
childItem->setText(0, testedWellChildren.at(i));
childItem->setCheckState(0, Qt::Unchecked);
childItem->setFlags(childItem->flags() | Qt::ItemIsUserCheckable);
childItem->setDisabled(!isSupportedSamplingVariable(testedWellChildren.at(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)));
}
// ===== 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);
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);
// ===== 整个 Sampling 面板:QFrame + 标题栏 + 内容 =====
QFrame *samplingPanel = new QFrame(page);
samplingPanel->setFrameShape(QFrame::StyledPanel);
samplingPanel->setFrameShadow(QFrame::Raised);
QVBoxLayout *panelLayout = new QVBoxLayout(samplingPanel);
panelLayout->setContentsMargins(0, 0, 0, 0);
panelLayout->setSpacing(0);
// ---- 标题栏:Sampling Xxx ----
QWidget *header = new QWidget(samplingPanel);
header->setFixedHeight(24);
header->setAutoFillBackground(true);
{
QPalette pal = header->palette();
pal.setColor(QPalette::Window,
palette().color(QPalette::Midlight));
header->setPalette(pal);
}
QHBoxLayout *headerLayout = new QHBoxLayout(header);
headerLayout->setContentsMargins(6, 0, 6, 0);
headerLayout->setSpacing(0);
QLabel *title = new QLabel(tr("Sampling %1").arg(varName), header);
QFont tf = title->font();
tf.setBold(true);
title->setFont(tf);
headerLayout->addWidget(title);
headerLayout->addStretch();
panelLayout->addWidget(header);
// ---- 内容区:左参数 + 右表格 ----
QWidget *content = new QWidget(samplingPanel);
QHBoxLayout *contentLayout = new QHBoxLayout(content);
contentLayout->setContentsMargins(6, 0, 6, 6);
contentLayout->setSpacing(12);
// 左:参数区(GridLayout)
QGridLayout *paramLayout = new QGridLayout();
paramLayout->setVerticalSpacing(6);
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"));
distributionCombo->setFixedWidth(180);
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;
// 模式、数量与下方采样参数之间留出约三行空白。
int gapRow = row;
paramLayout->setRowMinimumHeight(gapRow, numSpin->sizeHint().height() * 3);
++row;
// Model value + 单位
QLabel *modelLabel = new QLabel(tr("Model value:"), content);
QLineEdit *modelSpin = new QLineEdit(content);
modelSpin->setObjectName("sensitivityModel_" + varName);
// 基准值来自真实模型,只作参照;输入框支持科学计数法和完整精度。
modelSpin->setReadOnly(true);
QLabel *unitLabel = new QLabel(tr(""), content); // ⭐ 单位也从数据加载
QWidget *modelRowWidget = new QWidget(content);
QHBoxLayout *modelRowLayout = new QHBoxLayout(modelRowWidget);
modelRowLayout->setContentsMargins(0, 0, 0, 0);
modelRowLayout->addWidget(modelSpin);
modelRowLayout->addWidget(unitLabel);
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(6);
distributionLayout->setVerticalSpacing(0);
distributionLayout->setColumnMinimumWidth(0, modelLabel->sizeHint().width());
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;
// 统一编辑框宽度
int fullWidth = modelSpin->sizeHint().width();
int editWidth = qMax(150, fullWidth);
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, 6, 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(false);
QHeaderView *hh = valuesTable->horizontalHeader();
hh->setResizeMode(QHeaderView::Fixed);
hh->setHighlightSections(false);
// 箭头列固定,数值列在下方设置为填满加宽后的表格。
valuesTable->setColumnWidth(0, 15);
valuesTable->setStyleSheet(
"QHeaderView::section {"
" border-top: 0px;"
" border-left: 0px;"
" border-right: 1px solid lightgray;"
" border-bottom: 1px solid lightgray;"
"}"
"QTableCornerButton::section {"
" border-top: 0px;"
" border-left: 0px;"
" border-right: 1px solid lightgray;"
" border-bottom: 1px solid lightgray;"
"}"
);
valuesTable->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));
// 初始空表的列宽尚未稳定,直接固定容器宽度,不从未显示的表头反算。
const int tableWidth = 190;
valuesTable->setFixedWidth(tableWidth);
QWidget *tableContainer = new QWidget(content);
QVBoxLayout *tableLayout = new QVBoxLayout(tableContainer);
tableLayout->setContentsMargins(0, 0, 0, 0);
tableLayout->setSpacing(0);
tableLayout->addWidget(valuesTable);
tableContainer->setFixedWidth(tableWidth);
contentLayout->addWidget(paramWidget, 1);
contentLayout->addStretch();
contentLayout->addWidget(tableContainer, 0, Qt::AlignRight);
SensitivityDistributionPlot* distributionPlot = new SensitivityDistributionPlot(content);
contentLayout->addWidget(distributionPlot, 2);
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_unitLabels.append(unitLabel);
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->currentIndex() == 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]->parentWidget()->setVisible(!monteCarlo);
m_distributionCombos[idx]->setVisible(monteCarlo);
m_distributionPanels[idx]->setVisible(monteCarlo);
m_distributionPlots[idx]->setVisible(monteCarlo);
// Monte-Carlo 参数区只占所需宽度,窗口增宽时优先扩展分布图。
QHBoxLayout* contentLayout = static_cast<QHBoxLayout*>(m_distributionPlots[idx]->parentWidget()->layout());
contentLayout->setStretch(0, monteCarlo ? 0 : 1);
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).getVarName()) == idx) {
variable = m_sensitiveData.getVariable(i);
break;
}
}
QVector<double> samples;
const QString name = variable.getVarName();
const QString unit = m_unitLabels[idx]->text();
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_unitLabels[idx]->text());
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_unitLabels[idx]->text(), samplingBaseUnit(variable.m_varName), *fields[field])) 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.m_varName) != idx) continue;
const bool valid = readDistributionPage(idx, variable);
m_distributionPlots[idx]->setVariable(variable, valid);
break;
}
}
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 QString name = m_sensitiveData.getVariable(i).getVarName();
if (findVariableIndex(name) != idx) continue;
double lower = 0.0, upper = 0.0;
const QString unit = m_unitLabels[idx]->text();
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;
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, QString(), nullptr, false, true);
task.reset(source.createSensitivityBatch(m_sensitiveData, &error));
} 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);
// 新任务准备成功后才替换上次临时结果,准备失败仍可保留旧结果。
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);
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);
}
void nmWxSensitive::onCalculationTypeChanged(int index)
{
Q_UNUSED(index);
// 复用现有参数页,切换方法只改变可见控件,不覆盖另一种方法的设置。
if(m_loading) return;
for(int i = 0; i < m_numSpins.size(); ++i) updateValuesForPage(i);
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(this);
dialog.setWindowTitle(tr("Sensitivity Runs"));
dialog.setWindowFlags(dialog.windowFlags() & ~Qt::WindowContextHelpButtonHint);
dialog.setMinimumSize(420, 360);
dialog.resize(470, 650);
QVBoxLayout* layout = new QVBoxLayout(&dialog);
layout->setContentsMargins(16, 16, 16, 12);
layout->setSpacing(8);
m_previewView = new SensitivityRunsView(m_sensitiveData, createTriangleIcon(), &dialog);
layout->addWidget(m_previewView);
QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Close, Qt::Horizontal, &dialog);
buttons->setCenterButtons(true);
buttons->button(QDialogButtonBox::Close)->setMinimumWidth(110);
layout->addWidget(buttons);
connect(buttons, SIGNAL(rejected()), &dialog, SLOT(reject()));
connect(m_previewView->verticalScrollBar(), SIGNAL(actionTriggered(int)),
this, SLOT(onPreviewScrollAction(int)));
dialog.exec();
m_previewView = nullptr;
}
void nmWxSensitive::onPreviewScrollAction(int action)
{
// 将原生滚动条的单步、整屏和拖动操作转交连续列表。
if (m_previewView) m_previewView->scrollAction(action);
}