diff --git a/Include/nmNum/nmData/nmDataSensitive.h b/Include/nmNum/nmData/nmDataSensitive.h index 9c659615..663a32ed 100644 --- a/Include/nmNum/nmData/nmDataSensitive.h +++ b/Include/nmNum/nmData/nmDataSensitive.h @@ -87,6 +87,8 @@ public: // values QVector& getValues(); void setValues(const QVector& vals); + // 自动采样始终保留原始精度;手动模式直接返回用户指定值。 + QVector samplingValues() const; }; public: @@ -104,8 +106,13 @@ public: void setCalculationType(CalculationType type); // === 总模型数 === - int getTotalModelCount() const; - void setTotalModelCount(int count); + quint64 getTotalModelCount() const; + void setTotalModelCount(quint64 count); + quint64 calculateModelCount(bool* overflow = nullptr) const; + // 返回与变量列表对应的基准单位值,未改变的参数保持模型值。 + bool getRunValues(quint64 runIndex, QVector& values) const; + bool usesBaseUnits() const; + void setUsesBaseUnits(bool enabled); // === 变量列表 === QList& getVariables(); @@ -116,7 +123,8 @@ public: private: CalculationType m_calcType; - int m_totalModelCount; + quint64 m_totalModelCount; + bool m_usesBaseUnits; QList m_variables; }; diff --git a/Include/nmNum/nmSubWxs/nmWxSensitive.h b/Include/nmNum/nmSubWxs/nmWxSensitive.h index 220c8004..ffdc4f34 100644 --- a/Include/nmNum/nmSubWxs/nmWxSensitive.h +++ b/Include/nmNum/nmSubWxs/nmWxSensitive.h @@ -13,7 +13,8 @@ class QPushButton; class QStackedWidget; class QCheckBox; class QSpinBox; -class QDoubleSpinBox; +class QLineEdit; +class nmDataAnalyzeManager; class QTableWidget; class QLabel; class QIcon; @@ -23,7 +24,7 @@ class NM_SUB_WXS_EXPORT nmWxSensitive : public iDlgBase Q_OBJECT public: - explicit nmWxSensitive(QWidget *parent = 0); + explicit nmWxSensitive(QWidget *parent = 0, nmDataAnalyzeManager* manager = nullptr); ~nmWxSensitive(); private slots: @@ -32,7 +33,13 @@ public: void onCalculationTypeChanged(int index); void onTreeItemClicked(QTreeWidgetItem *item, int column); void onVariableParamChanged(); + void onSamplingEditingFinished(); void onTableCellClicked(int row, int column); + void onTreeItemChanged(QTreeWidgetItem *item, int column); + void onPreviewClicked(); + void onPreviewPrevious(); + void onPreviewNext(); + void onPreviewJump(); private: void setupUI(); @@ -44,7 +51,10 @@ private: QIcon createTriangleIcon(); // 创建三角箭头图标 void loadDataToUI(); // 从数据加载到界面 - void saveDataFromUI(); // 从界面保存到数据 + bool saveDataFromUI(); // 校验并从界面保存到数据 + void refreshModelValues(); + void updateModelCount(); + void refreshPreviewPage(); int findVariableIndex(const QString& varName); // 查找变量索引 // UI 控件 @@ -54,19 +64,26 @@ private: QLabel *modelCountLabel; QPushButton *generateButton; QPushButton *cancelButton; + QPushButton *previewButton; // 各个变量页面的控件列表 QList m_modeCombos; QList m_logChecks; QList m_numSpins; - QList m_modelSpins; - QList m_minSpins; - QList m_maxSpins; + QList m_modelSpins; + QList m_minSpins; + QList m_maxSpins; QList m_valueTables; QList m_unitLabels; // 跟踪当前选中的值索引 int m_selectedValueIndex; + bool m_loading; + nmDataAnalyzeManager* m_pDataManager; + QTableWidget* m_previewTable; + QLineEdit* m_previewPageEdit; + QLabel* m_previewPageLabel; + quint64 m_previewPage; // 敏感分析数据 nmDataSensitive m_sensitiveData; diff --git a/Src/nmNum/nmData/nmDataSensitive.cpp b/Src/nmNum/nmData/nmDataSensitive.cpp index 2117e831..f3b0553a 100644 --- a/Src/nmNum/nmData/nmDataSensitive.cpp +++ b/Src/nmNum/nmData/nmDataSensitive.cpp @@ -1,4 +1,6 @@ #include "nmDataSensitive.h" +#include +#include nmDataSensitive::VariableSampling::VariableSampling() : m_varGroup("") @@ -204,10 +206,33 @@ void nmDataSensitive::VariableSampling::setValues(const QVector& vals) m_values = vals; } +QVector nmDataSensitive::VariableSampling::samplingValues() const +{ + // 表格显示精度不参与采样,包含零或负数的对数范围按线性处理。 + if (m_mode == MODE_MANUAL) return m_values; + QVector values; + if (m_number <= 0) return values; + const double minimum = m_minValue.getValue().toDouble(); + const double maximum = m_maxValue.getValue().toDouble(); + const bool logarithmic = m_log && minimum > 0.0 && maximum > 0.0; + for (int i = 0; i < m_number; ++i) { + const double t = m_number == 1 ? 0.0 : double(i) / (m_number - 1); + double value = logarithmic + ? std::exp((1.0 - t) * std::log(minimum) + t * std::log(maximum)) + : (1.0 - t) * minimum + t * maximum; + if (m_number == 1) value = m_modelValue.getValue().toDouble(); + else if (i == 0) value = minimum; + else if (i == m_number - 1) value = maximum; + values.append(value); + } + return values; +} + nmDataSensitive::nmDataSensitive() : m_calcType(CALC_DETERMINISTIC) , m_totalModelCount(0) + , m_usesBaseUnits(false) , m_variables() { } @@ -224,6 +249,7 @@ nmDataSensitive& nmDataSensitive::operator=(const nmDataSensitive& other) if (this != &other) { m_calcType = other.m_calcType; m_totalModelCount = other.m_totalModelCount; + m_usesBaseUnits = other.m_usesBaseUnits; m_variables = other.m_variables; } return *this; @@ -238,6 +264,8 @@ rapidjson::Value nmDataSensitive::ToJsonValue( rapidjson::Document::AllocatorType& allocator) const { rapidjson::Value root(rapidjson::kObjectType); + // 旧配置中的采样值是显示单位,新配置统一保存业务基准单位。 + root.AddMember("UsesBaseUnits", m_usesBaseUnits, allocator); // 顶部 Calculation type root.AddMember("CalculationType", @@ -262,6 +290,8 @@ rapidjson::Value nmDataSensitive::ToJsonValue( // 从 RapidJSON Value 反序列化数据到 nmDataSensitive void nmDataSensitive::FromJsonValue(const rapidjson::Value& jsonValue) { + m_usesBaseUnits = jsonValue.HasMember("UsesBaseUnits") && + jsonValue["UsesBaseUnits"].IsBool() && jsonValue["UsesBaseUnits"].GetBool(); if (jsonValue.HasMember("CalculationType") && jsonValue["CalculationType"].IsInt()) { int t = jsonValue["CalculationType"].GetInt(); if (t == CALC_DETERMINISTIC_MULTIVARIATE) @@ -270,8 +300,8 @@ void nmDataSensitive::FromJsonValue(const rapidjson::Value& jsonValue) m_calcType = CALC_DETERMINISTIC; } - if (jsonValue.HasMember("TotalModelCount") && jsonValue["TotalModelCount"].IsInt()) { - m_totalModelCount = jsonValue["TotalModelCount"].GetInt(); + if (jsonValue.HasMember("TotalModelCount") && jsonValue["TotalModelCount"].IsUint64()) { + m_totalModelCount = jsonValue["TotalModelCount"].GetUint64(); } m_variables.clear(); @@ -300,16 +330,87 @@ void nmDataSensitive::setCalculationType(CalculationType type) } // === 总模型数 === -int nmDataSensitive::getTotalModelCount() const +quint64 nmDataSensitive::getTotalModelCount() const { return m_totalModelCount; } -void nmDataSensitive::setTotalModelCount(int count) +void nmDataSensitive::setTotalModelCount(quint64 count) { m_totalModelCount = count; } +quint64 nmDataSensitive::calculateModelCount(bool* overflow) const +{ + // 未选择变量时为零;两种确定性方法分别累计和、积。 + if (overflow) *overflow = false; + quint64 count = 0; + bool selected = false; + for (int i = 0; i < m_variables.size(); ++i) { + const VariableSampling& variable = m_variables[i]; + if (!variable.m_enabled) continue; + const quint64 number = variable.m_number > 0 ? variable.m_number : 0; + if (!number) return 0; + const quint64 limit = (std::numeric_limits::max)(); + if (m_calcType == CALC_DETERMINISTIC) { + if (count > limit - number) { if (overflow) *overflow = true; return 0; } + count += number; + } else { + if (!selected) count = 1; + if (count > limit / number) { if (overflow) *overflow = true; return 0; } + count *= number; + } + selected = true; + } + return count; +} + +bool nmDataSensitive::getRunValues(quint64 runIndex, QVector& values) const +{ + // 按编号解码组合,不预先展开笛卡尔积,也不删除重复的基准组合。 + bool overflow = false; + const quint64 count = calculateModelCount(&overflow); + if (overflow || runIndex >= count) return false; + values.clear(); + for (int i = 0; i < m_variables.size(); ++i) + values.append(m_variables[i].m_modelValue.getValue().toDouble()); + if (m_calcType == CALC_DETERMINISTIC) { + for (int i = 0; i < m_variables.size(); ++i) { + const VariableSampling& variable = m_variables[i]; + if (!variable.m_enabled) continue; + const QVector samples = variable.samplingValues(); + if (samples.size() != variable.m_number) return false; + if (runIndex < quint64(samples.size())) { + values[i] = samples[int(runIndex)]; + return true; + } + runIndex -= samples.size(); + } + } else { + for (int i = m_variables.size() - 1; i >= 0; --i) { + const VariableSampling& variable = m_variables[i]; + if (!variable.m_enabled) continue; + const QVector samples = variable.samplingValues(); + if (samples.isEmpty() || samples.size() != variable.m_number) return false; + values[i] = samples[int(runIndex % samples.size())]; + runIndex /= samples.size(); + } + return true; + } + return false; +} + +bool nmDataSensitive::usesBaseUnits() const +{ + return m_usesBaseUnits; +} + +void nmDataSensitive::setUsesBaseUnits(bool enabled) +{ + // 单位标记随配置保存,避免再次打开时重复转换。 + m_usesBaseUnits = enabled; +} + // === 变量列表 === QList& nmDataSensitive::getVariables() { diff --git a/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp b/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp index f3167fe4..3b17d8bf 100644 --- a/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp +++ b/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp @@ -1444,7 +1444,7 @@ void nmWxNumericalDesign::onForecastClicked() void nmWxNumericalDesign::onSensitiveClicked() { - nmWxSensitive* dlg = new nmWxSensitive(); + nmWxSensitive* dlg = new nmWxSensitive(this, m_pDataManager); dlg->show(); } diff --git a/Src/nmNum/nmSubWxs/nmWxSensitive.cpp b/Src/nmNum/nmSubWxs/nmWxSensitive.cpp index c09a1fb2..98d74386 100644 --- a/Src/nmNum/nmSubWxs/nmWxSensitive.cpp +++ b/Src/nmNum/nmSubWxs/nmWxSensitive.cpp @@ -2,6 +2,14 @@ #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 #include @@ -11,7 +19,10 @@ #include #include #include -#include +#include +#include +#include +#include #include #include #include @@ -38,6 +49,111 @@ #include #include +#include + +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 @@ -64,104 +180,220 @@ public: } return editor; } + + void setEditorData(QWidget *editor, const QModelIndex &index) const override + { + QLineEdit* line = qobject_cast(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(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) - : iDlgBase(parent), m_selectedValueIndex(0) // 初始化选中的值索引为0 +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) { - m_sensitiveData = nmDataAnalyzeManager::getCurrentInstance()->getSensitiveDataCopy(); + // 显式绑定所属分析窗口,后续切换窗口不会改变配置保存目标。 + if (m_pDataManager) m_sensitiveData = m_pDataManager->getSensitiveDataCopy(); + refreshModelValues(); setupUI(); createConnections(); populateTreeWidget(); createDetailPages(); loadDataToUI(); + m_loading = false; + onTreeItemChanged(nullptr, 0); } nmWxSensitive::~nmWxSensitive() { } -void nmWxSensitive::loadDataToUI() +void nmWxSensitive::refreshModelValues() { - // 1. 加载 Calculation Type - calculationTypeCombo->setCurrentIndex( - static_cast(m_sensitiveData.getCalculationType()) - ); + // 仅将本期六个参数绑定到主井和储层,旧采样范围按显示单位迁移一次。 + 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 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); +} - // 2. 加载总模型数 - modelCountLabel->setText(QString::number(m_sensitiveData.getTotalModelCount())); - - // 3. 加载每个变量的数据 - for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) { - nmDataSensitive::VariableSampling& var = m_sensitiveData.getVariable(i); - QString varName = var.getVarName(); - bool enabled = var.getEnabled(); - int mode = static_cast(var.getMode()); - bool log = var.getLog(); - int number = var.getNumber(); - - // 提前获取值,避免const问题 - double modelValue = var.getModelValue().getValue().toDouble(); - double minValue = var.getMinValue().getValue().toDouble(); - double maxValue = var.getMaxValue().getValue().toDouble(); - QString unit = var.getModelValue().getUnit(); // ⭐ 获取单位 - QVector values = var.getValues(); - - // 在树中找到对应的项并设置勾选状态 - for (int topIdx = 0; topIdx < variablesTree->topLevelItemCount(); ++topIdx) { - QTreeWidgetItem *topItem = variablesTree->topLevelItem(topIdx); - for (int childIdx = 0; childIdx < topItem->childCount(); ++childIdx) { - QTreeWidgetItem *childItem = topItem->child(childIdx); - if (childItem == nullptr) { - continue; - } - if (childItem->text(0) == varName) { - childItem->setCheckState(0, enabled ? Qt::Checked : Qt::Unchecked); - break; - } - } - } - - // 找到对应的控件索引 - int ctrlIdx = findVariableIndex(varName); - if (ctrlIdx < 0) continue; - - // 加载所有控件数据(从数据中心) - m_modeCombos[ctrlIdx]->setCurrentIndex(mode); - m_logChecks[ctrlIdx]->setChecked(log); - m_numSpins[ctrlIdx]->setValue(number); - - m_modelSpins[ctrlIdx]->setValue(modelValue); - m_minSpins[ctrlIdx]->setValue(minValue); - m_maxSpins[ctrlIdx]->setValue(maxValue); - - // 设置单位标签 - m_unitLabels[ctrlIdx]->setText(unit); - - // 如果是手动模式且有保存的values,加载到表格 - if (mode == nmDataSensitive::VariableSampling::MODE_MANUAL && values.size() > 0) { - QTableWidget *table = m_valueTables[ctrlIdx]; - table->setRowCount(values.size()); - for (int row = 0; row < values.size(); ++row) { - // 创建箭头项 - QTableWidgetItem *arrowItem = new QTableWidgetItem(); - arrowItem->setFlags(Qt::ItemIsEnabled); - arrowItem->setTextAlignment(Qt::AlignCenter); - bool isSelected = (row == m_selectedValueIndex) && (m_selectedValueIndex < values.size()); - arrowItem->setIcon(isSelected ? createTriangleIcon() : QIcon()); - table->setItem(row, 0, arrowItem); - - // 创建值项 - QTableWidgetItem *valueItem = new QTableWidgetItem(QString::number(values[row], 'f', 1)); - valueItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); - table->setItem(row, 1, valueItem); - } - } else { - // 自动模式:调用 updateValuesForPage 计算并填充值 - updateValuesForPage(ctrlIdx); - } - } +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& 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) @@ -169,79 +401,117 @@ int nmWxSensitive::findVariableIndex(const QString& varName) // 根据创建顺序查找变量索引 QStringList allVars; allVars << "Zw" << "Hw" << "Lw" << "Skin" << "C" // Tested Well - << "Pi" << "k" << "h" << "φ" << "ntg" << "kz/kr" // Reservoir + << "Pi" << "k" << "h" << QString(QChar(0x03c6)) << "ntg" << "kz/kr" // Reservoir << "Total compressibility"; // Pvt return allVars.indexOf(varName); } -void nmWxSensitive::saveDataFromUI() +void nmWxSensitive::updateModelCount() { - // 1. 保存 Calculation Type - m_sensitiveData.setCalculationType( - static_cast( - calculationTypeCombo->currentIndex() - ) - ); - - // 2. 计算并保存总模型数 - int totalModels = 1; - for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) { - nmDataSensitive::VariableSampling& var = m_sensitiveData.getVariable(i); - - // 检查树中的勾选状态 - bool enabled = false; - QString varName = var.getVarName(); - for (int topIdx = 0; topIdx < variablesTree->topLevelItemCount(); ++topIdx) { - QTreeWidgetItem *topItem = variablesTree->topLevelItem(topIdx); - for (int childIdx = 0; childIdx < topItem->childCount(); ++childIdx) { - QTreeWidgetItem *childItem = topItem->child(childIdx); - if (childItem == nullptr) { - continue; - } - if (childItem->text(0) == varName) { - enabled = (childItem->checkState(0) == Qt::Checked); - break; - } - } - } - - var.setEnabled(enabled); - - // 找到对应的控件索引 - int ctrlIdx = findVariableIndex(varName); - if (ctrlIdx < 0) continue; - - // 保存控件数据 - var.setMode(static_cast( - m_modeCombos[ctrlIdx]->currentIndex()) - ); - var.setLog(m_logChecks[ctrlIdx]->isChecked()); - var.setNumber(m_numSpins[ctrlIdx]->value()); - - var.getModelValue().setValue(m_modelSpins[ctrlIdx]->value()); - var.getMinValue().setValue(m_minSpins[ctrlIdx]->value()); - var.getMaxValue().setValue(m_maxSpins[ctrlIdx]->value()); - - // 从表格保存 values - QTableWidget *table = m_valueTables[ctrlIdx]; - QVector values; - for (int row = 0; row < table->rowCount(); ++row) { - QTableWidgetItem *item = table->item(row, 1); - if (item && !item->text().isEmpty()) { - values.append(item->text().toDouble()); - } - } - var.setValues(values); - - // 计算总模型数 - if (enabled) { - totalModels *= var.getNumber(); - } - } + // 勾选、数量和方法修改后立即更新总次数,不等待 Generate。 + if (m_loading) return; + m_sensitiveData.setCalculationType( + static_cast(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); +} - m_sensitiveData.setTotalModelCount(totalModels); - modelCountLabel->setText(QString::number(totalModels)); +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(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 values = variable.samplingValues(); + for (int row = 0; valid && row < values.size(); ++row) + valid = validSamplingValue(name, values[row]); + if (valid) variable.setValues(values); + } + } else { + QVector 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 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() @@ -355,6 +625,12 @@ void nmWxSensitive::setupUI() 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); @@ -373,12 +649,15 @@ void nmWxSensitive::createConnections() 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() @@ -400,6 +679,7 @@ void nmWxSensitive::populateTreeWidget() childItem->setText(0, testedWellChildren.at(i)); childItem->setCheckState(0, Qt::Unchecked); childItem->setFlags(childItem->flags() | Qt::ItemIsUserCheckable); + childItem->setDisabled(!isSupportedSamplingVariable(testedWellChildren.at(i))); } // ===== Reservoir ===== @@ -409,13 +689,14 @@ void nmWxSensitive::populateTreeWidget() reservoirItem->setFlags(reservoirItem->flags() | Qt::ItemIsUserCheckable); QStringList reservoirChildren; - reservoirChildren << "Pi" << "k" << "h" << "φ" << "ntg" << "kz/kr"; + 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 ===== @@ -428,6 +709,7 @@ void nmWxSensitive::populateTreeWidget() compressibilityItem->setText(0, tr("Total compressibility")); compressibilityItem->setCheckState(0, Qt::Unchecked); compressibilityItem->setFlags(compressibilityItem->flags() | Qt::ItemIsUserCheckable); + compressibilityItem->setDisabled(true); variablesTree->expandAll(); } @@ -560,11 +842,10 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) // Model value + 单位 QLabel *modelLabel = new QLabel(tr("Model value:"), content); - QDoubleSpinBox *modelSpin = new QDoubleSpinBox(content); - modelSpin->setDecimals(4); - modelSpin->setRange(-1e9, 1e9); - //modelSpin->setValue(0.0); - modelSpin->setButtonSymbols(QAbstractSpinBox::NoButtons); + QLineEdit *modelSpin = new QLineEdit(content); + modelSpin->setObjectName("sensitivityModel_" + varName); + // 基准值来自真实模型,只作参照;输入框支持科学计数法和完整精度。 + modelSpin->setReadOnly(true); QLabel *unitLabel = new QLabel(tr(""), content); // ⭐ 单位也从数据加载 @@ -581,11 +862,8 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) // Minimum QLabel *minLabel = new QLabel(tr("Minimum:"), content); - QDoubleSpinBox *minSpin = new QDoubleSpinBox(content); - minSpin->setDecimals(4); - minSpin->setRange(-1e9, 1e9); - //minSpin->setValue(0.0); - minSpin->setButtonSymbols(QAbstractSpinBox::NoButtons); + QLineEdit *minSpin = new QLineEdit(content); + minSpin->setObjectName("sensitivityMinimum_" + varName); paramLayout->addWidget(minLabel, row, 0); paramLayout->addWidget(minSpin, row, 1); @@ -593,11 +871,8 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) // Maximum QLabel *maxLabel = new QLabel(tr("Maximum:"), content); - QDoubleSpinBox *maxSpin = new QDoubleSpinBox(content); - maxSpin->setDecimals(4); - maxSpin->setRange(-1e9, 1e9); - //maxSpin->setValue(0.0); - maxSpin->setButtonSymbols(QAbstractSpinBox::NoButtons); + QLineEdit *maxSpin = new QLineEdit(content); + maxSpin->setObjectName("sensitivityMaximum_" + varName); paramLayout->addWidget(maxLabel, row, 0); paramLayout->addWidget(maxSpin, row, 1); @@ -605,7 +880,7 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) // 统一编辑框宽度 int fullWidth = modelSpin->sizeHint().width(); - int editWidth = fullWidth * 3 / 4; + int editWidth = qMax(150, fullWidth); modeCombo->setFixedWidth(editWidth); numSpin->setFixedWidth(editWidth); @@ -627,6 +902,7 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) // 右:Values 表格(双列:箭头 + 值) QTableWidget *valuesTable = new QTableWidget(content); + valuesTable->setObjectName("sensitivityValues_" + varName); valuesTable->setColumnCount(2); QStringList headers; headers << "" << tr("Values"); @@ -683,6 +959,7 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) panelLayout->addWidget(content); pageLayout->addWidget(samplingPanel); + page->setEnabled(isSupportedSamplingVariable(varName)); // ===== 保存控件指针 ===== m_modeCombos.append(modeCombo); @@ -696,10 +973,14 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) connect(numSpin, SIGNAL(valueChanged(int)), this, SLOT(onVariableParamChanged())); - connect(minSpin, SIGNAL(valueChanged(double)), + connect(minSpin, SIGNAL(textChanged(QString)), this, SLOT(onVariableParamChanged())); - connect(maxSpin, SIGNAL(valueChanged(double)), + 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)), @@ -713,151 +994,150 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName) void nmWxSensitive::updateValuesForPage(int idx) { - if (idx < 0 || idx >= m_numSpins.size()) - return; - - QSpinBox *numSpin = m_numSpins.at(idx); - QDoubleSpinBox *minSpin = m_minSpins.at(idx); - QDoubleSpinBox *maxSpin = m_maxSpins.at(idx); - QDoubleSpinBox *modelSpin = m_modelSpins.at(idx); - QComboBox *modeCombo = m_modeCombos.at(idx); - QCheckBox *logCheck = m_logChecks.at(idx); - QTableWidget *table = m_valueTables.at(idx); - - int n = numSpin->value(); - if (n <= 0) - n = 1; - - table->setRowCount(n); - - // 为每行创建箭头项,根据当前选中索引决定是否显示箭头 - for (int i = 0; i < n; ++i) { - QTableWidgetItem *arrowItem = table->item(i, 0); - if (!arrowItem) { - arrowItem = new QTableWidgetItem(); - arrowItem->setFlags(Qt::ItemIsEnabled); // 不可编辑 - arrowItem->setTextAlignment(Qt::AlignCenter); - table->setItem(i, 0, arrowItem); + 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; } - - // 根据是否为当前选中行设置箭头图标 - bool isSelected = (i == m_selectedValueIndex); - arrowItem->setIcon(isSelected ? createTriangleIcon() : QIcon()); - arrowItem->setText(""); } - - if (modeCombo->currentIndex() != 0) { - // Manual 模式:只控制行数,不自动填充数值 - for (int i = 0; i < n; ++i) { - QTableWidgetItem *valueItem = table->item(i, 1); - if (!valueItem) { - valueItem = new QTableWidgetItem(""); - valueItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); - table->setItem(i, 1, valueItem); - } + QVector 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(); } - return; } - - double minVal = minSpin->value(); - double maxVal = maxSpin->value(); - - if (n == 1) { - double v = modelSpin->value(); - QTableWidgetItem *item = new QTableWidgetItem(QString::number(v, 'f', 1)); + 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); - table->setItem(0, 1, item); - return; + 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 logMode = logCheck->isChecked(); - int i; - - if (logMode && minVal > 0.0 && maxVal > 0.0) { - double logMin = log10(minVal); - double logMax = log10(maxVal); - for (i = 0; i < n; ++i) { - double t = double(i) / double(n - 1); - double v = pow(10.0, logMin + (logMax - logMin) * t); - QTableWidgetItem *item = new QTableWidgetItem(QString::number(v, 'f', 1)); - item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); - table->setItem(i, 1, item); - } - } else { // 线性 - for (i = 0; i < n; ++i) { - double t = double(i) / double(n - 1); - double v = minVal + (maxVal - minVal) * t; - QTableWidgetItem *item = new QTableWidgetItem(QString::number(v, 'f', 1)); - item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); - table->setItem(i, 1, item); +void nmWxSensitive::onSamplingEditingFinished() +{ + // 输入结束后截断越界范围,再按六位有效数字显示,计算仍使用原始精度。 + QLineEdit* edit = qobject_cast(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() { - QObject *s = sender(); - int i, idx = -1; - - for (i = 0; i < m_numSpins.size(); ++i) { - if (s == m_numSpins.at(i) || - s == m_minSpins.at(i) || - s == m_maxSpins.at(i) || - s == m_modeCombos.at(i) || - s == m_logChecks.at(i)) { - idx = i; + // 数据加载时不响应控件逐项赋值,编辑时只刷新所属参数页。 + 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; } } - - if (idx >= 0) { - // 重置选中索引,确保在有效范围内 - int newCount = m_numSpins.at(idx)->value(); - if (m_selectedValueIndex >= newCount) { - m_selectedValueIndex = qMax(0, newCount - 1); - } - updateValuesForPage(idx); - } + updateModelCount(); } -// 处理表格单元格点击事件 void nmWxSensitive::onTableCellClicked(int row, int column) { Q_UNUSED(column); - - QObject *s = sender(); - QTableWidget *table = qobject_cast(s); - if (!table) - return; - - // 更新选中的值索引 + // 选中标记不改写表格内容,避免自动重算或丢失手动输入。 m_selectedValueIndex = row; + QTableWidget* table = qobject_cast(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()); +} - // 重新更新表格以反映新的选中状态 - // 找到这个表格对应的页面索引 - int pageIdx = -1; - for (int i = 0; i < m_valueTables.size(); ++i) { - if (m_valueTables.at(i) == table) { - pageIdx = i; - break; - } +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); } - - if (pageIdx >= 0) { - updateValuesForPage(pageIdx); + 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() { - // 保存界面数据到 m_sensitiveData - saveDataFromUI(); - - // 更新数据到数据中心 - nmDataAnalyzeManager::getCurrentInstance()->updateSensitiveData(m_sensitiveData); - - // 关闭对话框 - accept(); + // 本阶段只保存经过校验的采样配置,实际批量求解将在下一阶段接入。 + if (!m_pDataManager || !saveDataFromUI()) return; + m_pDataManager->updateSensitiveData(m_sensitiveData); + accept(); } void nmWxSensitive::onCancelClicked() @@ -867,36 +1147,127 @@ void nmWxSensitive::onCancelClicked() void nmWxSensitive::onCalculationTypeChanged(int index) { - if (index == 0) { - // Deterministic - } else { - // Probabilistic - } + Q_UNUSED(index); + // 两种确定性方法共用参数设置,仅切换总次数及组合规则。 + updateModelCount(); } -void nmWxSensitive::onTreeItemClicked(QTreeWidgetItem *item, int column) +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()); +} - if (item->childCount() == 0) { - QVariant v = item->data(0, Qt::UserRole); - if (v.isValid()) { - int pageIndex = v.toInt(); - detailStack->setCurrentIndex(pageIndex); - - // 切换页面时重置选中索引为0 - m_selectedValueIndex = 0; - - // 找到对应的页面索引并更新表格 - int tableIdx = pageIndex - 1; // 减去默认页面的索引 - if (tableIdx >= 0 && tableIdx < m_valueTables.size()) { - updateValuesForPage(tableIdx); - } +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; +} - return; +void nmWxSensitive::refreshPreviewPage() +{ + // 参数列与计算数据共用 getRunValues,单变量运行的其他参数显示基准值。 + if (!m_previewTable) return; + QStringList headers; + headers << tr("Run"); + QList 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 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)); +} - // 父节点等:回到默认页 - detailStack->setCurrentIndex(0); +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(); }