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++

#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);
}