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

1274 lines
53 KiB
C++

#include "nmWxSensitive.h"
#include "nmDataAnalyzeManager.h"
#include "nmDataSensitive.h"
#include "nmDataReservoir.h"
#include "nmDataWellBase.h"
#include "nmDataPerforation.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 <QStyle>
#include <QTableWidget>
#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 <math.h>
#include <float.h>
namespace
{
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;
}
}
// ========= 右对齐委托:保证显示和编辑时都是右对齐 =========
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);
}
};
nmWxSensitive::nmWxSensitive(QWidget *parent, nmDataAnalyzeManager* manager)
: iDlgBase(parent), m_selectedValueIndex(0), m_loading(true),
m_pDataManager(manager ? manager : nmDataAnalyzeManager::getCurrentInstance()),
m_previewTable(nullptr), m_previewPageEdit(nullptr), m_previewPageLabel(nullptr),
m_previewPage(0)
{
// 显式绑定所属分析窗口,后续切换窗口不会改变配置保存目标。
if (m_pDataManager) m_sensitiveData = m_pDataManager->getSensitiveDataCopy();
refreshModelValues();
setupUI();
createConnections();
populateTreeWidget();
createDetailPages();
loadDataToUI();
m_loading = false;
onTreeItemChanged(nullptr, 0);
}
nmWxSensitive::~nmWxSensitive()
{
}
void nmWxSensitive::refreshModelValues()
{
// 仅将本期六个参数绑定到主井和储层,旧采样范围按显示单位迁移一次。
nmDataWellBase* well = m_pDataManager
? m_pDataManager->findWellByCode(m_pDataManager->getPrimaryWellCode()) : nullptr;
nmDataReservoir* reservoir = m_pDataManager ? m_pDataManager->getReservoirData() : nullptr;
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);
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 (!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);
// 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);
}
void nmWxSensitive::loadDataToUI()
{
// 加载过程中阻止联动刷新,先恢复设置,再一次生成各参数表格。
calculationTypeCombo->setCurrentIndex(int(m_sensitiveData.getCalculationType()));
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);
item->setCheckState(0, available && variable.getEnabled() ? Qt::Checked : Qt::Unchecked);
}
}
detailStack->widget(idx + 1)->setEnabled(available);
m_modeCombos[idx]->setCurrentIndex(int(variable.getMode()));
m_logChecks[idx]->setChecked(variable.getLog());
m_numSpins[idx]->setValue(qMax(1, variable.getNumber()));
const QString unit = variable.getModelValue().getUnit();
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);
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 (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;
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);
generateButton->setEnabled(m_pDataManager && !overflow && count > 0);
}
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;
variable.setMode(static_cast<nmDataSensitive::VariableSampling::Mode>(m_modeCombos[idx]->currentIndex()));
variable.setLog(m_logChecks[idx]->isChecked());
const QString unit = m_unitLabels[idx]->text();
const QString baseUnit = samplingBaseUnit(name);
bool valid = true;
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 range and every manual row.")
.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;
}
if (!variable.getEnabled()) continue;
// 本期处理常量参数;属性场开启时常量会被插值覆盖,因此不能静默运行。
if (m_pDataManager && (name == "k" || name == "h" || name == QString(QChar(0x03c6)))) {
const QString property = name == QString(QChar(0x03c6)) ? "phi" : name;
const QVector<nmPropertyInterpolationDataSet> dataSets = m_pDataManager->getPropertyInterpolationDataSets();
for (int field = 0; field < dataSets.size(); ++field) {
if (dataSets[field].useForCalculation && dataSets[field].property == property &&
!dataSets[field].points.isEmpty()) {
QMessageBox::warning(this, tr("Sensitivity"),
tr("%1 uses a non-uniform field. Disable that field before sampling a constant value.").arg(name));
return false;
}
}
}
}
m_sensitiveData = candidate;
return true;
}
void nmWxSensitive::setupUI()
{
setWindowTitle(tr("Sensitivity"));
setMinimumSize(900, 550);
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);
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);
bottomLayout->addWidget(totalLabel);
bottomLayout->addWidget(modelCountLabel);
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(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);
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;
// 空行(Number 和 Model value 之间)
int gapRow = row;
paramLayout->setRowMinimumHeight(gapRow, 12);
++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;
// 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);
valuesTable->setColumnWidth(0, 15);
valuesTable->setColumnWidth(1, 130);
QHeaderView *hh = valuesTable->horizontalHeader();
hh->setResizeMode(QHeaderView::Fixed);
hh->setHighlightSections(false);
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(false);
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));
int tableWidth = valuesTable->frameWidth() * 2
+ valuesTable->verticalHeader()->sizeHint().width()
+ valuesTable->columnWidth(0)
+ valuesTable->columnWidth(1);
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);
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);
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;
// 自动模式由范围生成只读表格;手动模式保留每行值,不因选中行而重算。
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());
}
}
}
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;
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]) {
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;
// 分组选择与子项选择双向同步,禁用的参数不参与全选和总次数。
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_pDataManager || !saveDataFromUI()) return;
m_pDataManager->updateSensitiveData(m_sensitiveData);
accept();
}
void nmWxSensitive::onCancelClicked()
{
reject();
}
void nmWxSensitive::onCalculationTypeChanged(int index)
{
Q_UNUSED(index);
// 两种确定性方法共用参数设置,仅切换总次数及组合规则。
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;
// 按页解码编号,组合数再大也只创建当前页的五十行。
QDialog dialog(this);
dialog.setWindowTitle(tr("Sensitivity runs"));
dialog.resize(850, 500);
QVBoxLayout* layout = new QVBoxLayout(&dialog);
m_previewTable = new QTableWidget(&dialog);
m_previewTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
layout->addWidget(m_previewTable);
QHBoxLayout* navigation = new QHBoxLayout();
QPushButton* previous = new QPushButton(&dialog);
previous->setIcon(style()->standardIcon(QStyle::SP_ArrowLeft));
previous->setToolTip(tr("Previous page"));
QPushButton* next = new QPushButton(&dialog);
next->setIcon(style()->standardIcon(QStyle::SP_ArrowRight));
next->setToolTip(tr("Next page"));
m_previewPageEdit = new QLineEdit(&dialog);
m_previewPageEdit->setFixedWidth(180);
m_previewPageLabel = new QLabel(&dialog);
navigation->addWidget(previous);
navigation->addWidget(m_previewPageEdit);
navigation->addWidget(m_previewPageLabel);
navigation->addWidget(next);
navigation->addStretch();
layout->addLayout(navigation);
QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Close, Qt::Horizontal, &dialog);
layout->addWidget(buttons);
connect(buttons, SIGNAL(rejected()), &dialog, SLOT(reject()));
connect(previous, SIGNAL(clicked()), this, SLOT(onPreviewPrevious()));
connect(next, SIGNAL(clicked()), this, SLOT(onPreviewNext()));
connect(m_previewPageEdit, SIGNAL(returnPressed()), this, SLOT(onPreviewJump()));
m_previewPage = 0;
refreshPreviewPage();
dialog.exec();
m_previewTable = nullptr;
m_previewPageEdit = nullptr;
m_previewPageLabel = nullptr;
}
void nmWxSensitive::refreshPreviewPage()
{
// 参数列与计算数据共用 getRunValues,单变量运行的其他参数显示基准值。
if (!m_previewTable) return;
QStringList headers;
headers << tr("Run");
QList<int> selected;
for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) {
nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i);
if (!variable.getEnabled()) continue;
selected.append(i);
const QString unit = variable.getModelValue().getUnit();
headers << variable.getVarName() + (unit.isEmpty() ? QString() : " (" + unit + ")");
}
m_previewTable->clear();
m_previewTable->setColumnCount(headers.size());
m_previewTable->setHorizontalHeaderLabels(headers);
const quint64 total = m_sensitiveData.getTotalModelCount();
const quint64 first = m_previewPage * 50;
const int rows = int(qMin(quint64(50), total - first));
m_previewTable->setRowCount(rows);
for (int row = 0; row < rows; ++row) {
QVector<double> values;
if (!m_sensitiveData.getRunValues(first + row, values)) break;
m_previewTable->setItem(row, 0, new QTableWidgetItem(QString::number(first + row + 1)));
for (int column = 0; column < selected.size(); ++column) {
const int index = selected[column];
nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(index);
double display = values[index];
convertSamplingValue(variable.getVarName(), display, samplingBaseUnit(variable.getVarName()),
variable.getModelValue().getUnit(), display);
QTableWidgetItem* item = new QTableWidgetItem(formatSamplingValue(display, 3));
item->setToolTip(formatSamplingValue(display, 6));
item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
m_previewTable->setItem(row, column + 1, item);
}
}
m_previewTable->resizeColumnsToContents();
m_previewPageEdit->setText(QString::number(m_previewPage + 1));
m_previewPageLabel->setText(tr("/ %1 pages; %2 runs").arg((total - 1) / 50 + 1).arg(total));
}
void nmWxSensitive::onPreviewPrevious()
{
if (m_previewPage > 0) --m_previewPage;
refreshPreviewPage();
}
void nmWxSensitive::onPreviewNext()
{
if ((m_previewPage + 1) * 50 < m_sensitiveData.getTotalModelCount()) ++m_previewPage;
refreshPreviewPage();
}
void nmWxSensitive::onPreviewJump()
{
// 允许直接跳转到大批量组合中的指定页,页码使用六十四位整数。
bool ok = false;
const quint64 page = m_previewPageEdit->text().trimmed().toULongLong(&ok);
const quint64 pages = (m_sensitiveData.getTotalModelCount() - 1) / 50 + 1;
if (ok && page > 0 && page <= pages) m_previewPage = page - 1;
refreshPreviewPage();
}