diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index b99a3814..31770463 100644 Binary files a/Bin/Config/Lang/cn/nmNum_cn.qm and b/Bin/Config/Lang/cn/nmNum_cn.qm differ diff --git a/Bin/Config/Lang/cn/nmNum_cn.ts b/Bin/Config/Lang/cn/nmNum_cn.ts index 373a9d94..ce42c261 100644 --- a/Bin/Config/Lang/cn/nmNum_cn.ts +++ b/Bin/Config/Lang/cn/nmNum_cn.ts @@ -4444,6 +4444,14 @@ Supported types: Vertical, Vertical Fractured, and Horizontal Multi-Fractured We nmWxAutomaticFitting + + Reservoir parameters + 储层参数 + + + Well parameters + 井参数 + LM sampling: LM 采样方式: @@ -4545,8 +4553,8 @@ Supported types: Vertical, Vertical Fractured, and Horizontal Multi-Fractured We 目标井 - Reverse Selection - 反选 + Select All + 全选 OK diff --git a/Include/nmNum/nmSubWxs/nmWxAutomaticFitting.h b/Include/nmNum/nmSubWxs/nmWxAutomaticFitting.h index 3563d2ac..511eebe0 100644 --- a/Include/nmNum/nmSubWxs/nmWxAutomaticFitting.h +++ b/Include/nmNum/nmSubWxs/nmWxAutomaticFitting.h @@ -5,11 +5,11 @@ #include #include #include -#include -#include +#include #include #include #include +#include #include #include #include @@ -39,13 +39,11 @@ public: virtual QSize sizeHint() const; private slots: - void onCellSelectionChanged(int currentRow, int currentColumn, int previousRow, int previousColumn); - void onReverseSelection(); + void onSelectAll(bool checked); void onAccept(); void onReject(); - void onWellSelected(int index); void onAlgorithmChanged(int index); - void onParameterTableItemChanged(QTableWidgetItem* item); + void onParameterTableItemChanged(QTreeWidgetItem* item, int column); // 自动拟合相关槽函数 void runAutoFitting(); @@ -58,12 +56,13 @@ private: void setupParameterTable(); void setupControlPanel(); void setupButtons(); + void updateSelectAllState(); void setAutomaticFittingValue(); - void loadSensitivitySettings(); - void setParameterRowVisible(QTableWidget* table, int row, bool visible); - void renumberVisibleParameterRows(QTableWidget* table); - void updateParameterVisibility(QTableWidget* table, NM_SOLVER_MODEL_TYPE eType); - void initializeSuggestedParameterRanges(); + void saveModelParameters(); + void initializeModelParameters(bool restoreRanges = true); + void addParameter(QTreeWidgetItem* group, int type, nmDataWellBase* well, double initial); + QVector primaryInitialValues() const; + QStringList selectedParameterLabels(bool units) const; void updateRangeForParameter(int parameterIndex, double centerValue); void setParameterRange(int parameterIndex, double minValue, double maxValue); bool getPhysicalParameterRange(int parameterIndex, double& minValue, double& maxValue); @@ -79,25 +78,18 @@ private: // UI组件 QVBoxLayout* m_mainLayout; QGridLayout* m_controlLayout; - QTableWidget* m_parameterTable; + QTreeWidget* m_parameterTable; + QVector m_parameterItems; // 控制面板组件 QLineEdit* m_iterationEdit; QLabel* m_iterationLabel; QLineEdit* m_errorLimitEdit; - QComboBox* m_targetWellCombo; + QString m_targetWellName; QComboBox* m_algorithmCombo; - // 参数复选框 - QCheckBox* m_kCheckBox; // 渗透率 - QCheckBox* m_sCheckBox; // 表皮系数 - QCheckBox* m_cCheckBox; // 井筒储集系数 - QCheckBox* m_phiCheckBox; // 孔隙度 - QCheckBox* m_dfcCheckBox; // 裂缝导流能力 - QCheckBox* m_fractureHalfLengthCheckBox; // 裂缝半长 - // 按钮 - QPushButton* m_reverseBtn; + QCheckBox* m_selectAllCheckBox; QPushButton* m_okBtn; QPushButton* m_cancelBtn; @@ -112,7 +104,6 @@ private: nmCalculationAutoFitLM* m_autoFitterLM; QProgressDialog* m_progressDialog; QTimer* m_progressTimer; - bool m_autoParameterRanges; bool m_updatingParameterRanges; // 拟合开始界面 diff --git a/Include/nmNum/nmSubWxs/nmWxSensitive.h b/Include/nmNum/nmSubWxs/nmWxSensitive.h index e43c7831..a75774ac 100644 --- a/Include/nmNum/nmSubWxs/nmWxSensitive.h +++ b/Include/nmNum/nmSubWxs/nmWxSensitive.h @@ -147,7 +147,7 @@ private: void refreshModelValues(); void updateModelCount(); void setCalculationRunning(bool running); - int findVariableIndex(const QString& varName); // 查找变量索引 + int findVariableIndex(const nmDataSensitive::VariableSampling& variable); // 查找变量索引 // UI 控件 QComboBox *calculationTypeCombo; diff --git a/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp b/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp index 471e16e7..2ae046aa 100644 --- a/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp +++ b/Src/nmNum/nmSubWxs/nmWxAutomaticFitting.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -27,6 +28,29 @@ namespace { +// 树节点和名称、单位列只读,仅允许编辑参数的三个数值列。 +class nmFitParameterDelegate : public QStyledItemDelegate +{ +public: + explicit nmFitParameterDelegate(QObject* parent) : QStyledItemDelegate(parent) {} + QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const + { + // 分组和参数统一增高,字号变化时也保留上下留白。 + QSize size = QStyledItemDelegate::sizeHint(option, index); + size.setHeight(qMax(size.height(), qMax(32, option.fontMetrics.height() + 12))); + return size; + } + + QWidget* createEditor(QWidget* parent, const QStyleOptionViewItem& option, + const QModelIndex& index) const + { + if(index.column() < 1 || index.column() > 3 + || !index.sibling(index.row(), 0).data(Qt::UserRole).isValid()) + return nullptr; + return QStyledItemDelegate::createEditor(parent, option, index); + } +}; + bool nmAutoFitUiIsFinite(double value) { #ifdef Q_OS_WIN @@ -67,166 +91,28 @@ bool nmAutoFitReadPhysicalRange(const char* parameterName, } -// 隐藏参数取消勾选并禁用;新显示的参数默认勾选,保留已显示参数的手工选择。 -void nmWxAutomaticFitting::setParameterRowVisible(QTableWidget* table, int row, bool visible) -{ - if(!table || row < 0 || row >= table->rowCount()) { - return; - } - - const bool wasHidden = table->isRowHidden(row); - table->setRowHidden(row, !visible); - - QCheckBox* checkBox = qobject_cast(table->cellWidget(row, 1)); - if(checkBox) { - // 与当前采样重叠的参数不能参与拟合,切换目标井时也保持互斥。 - const QString samplingNames[] = {"k", "Skin", "C", QString(QChar(0x03c6))}; - const bool sampled = m_sensitivitySettings && row < 4 && m_sampledParameters.contains(samplingNames[row]); - checkBox->setEnabled(visible && !sampled); - if(!visible || sampled) { - checkBox->setChecked(false); - } else if(wasHidden) { - checkBox->setChecked(!m_sensitivitySettings); - } - } -} - -// 按当前模型和井型可用的参数行重新整理显示序号。 -void nmWxAutomaticFitting::renumberVisibleParameterRows(QTableWidget* table) -{ - if(!table) { - return; - } - - int visibleIndex = 1; - for(int row = 0; row < table->rowCount(); ++row) { - if(table->isRowHidden(row)) { - continue; - } - - QTableWidgetItem* item = table->item(row, 0); - if(!item) { - item = new QTableWidgetItem(); - table->setItem(row, 0, item); - } - - item->setText(QString::number(visibleIndex++)); - item->setTextAlignment(Qt::AlignCenter); - } -} - -// 拟合表仅包含可优化参数,按模型和目标井类型控制显示。 -void nmWxAutomaticFitting::updateParameterVisibility(QTableWidget* table, NM_SOLVER_MODEL_TYPE eType) -{ - if(!table) { - return; - } - - // 裂缝参数直接按井型设置,避免先显示再隐藏造成选择状态重置。 - for(int row = 0; row < 4; ++row) { - setParameterRowVisible(table, row, true); - } - - setParameterRowVisible(table, 4, false); // 原含水饱和度索引保留,不再参与拟合。 - - // Dfc 只属于垂直压裂井和多段压裂水平井。普通井隐藏并取消勾选, - // 防止切换目标井后不可见的裂缝参数仍进入拟合参数向量。 - bool showFractureConductivity = false; - nmDataAnalyzeManager* manager = nmDataAnalyzeManager::getCurrentInstance(); - if(manager && m_targetWellCombo) { - nmDataWellBase* targetWell = manager->findWellByName(m_targetWellCombo->currentText()); - if(targetWell) { - NM_WELL_MODEL wellType = targetWell->getWellType(); - showFractureConductivity = - wellType == NM_WELL_MODEL::Vertical_Fractured_Well || - wellType == NM_WELL_MODEL::Horizontal_Fractured_Well; - } - } - setParameterRowVisible(table, 5, showFractureConductivity); // Dfc - setParameterRowVisible(table, 6, showFractureConductivity); // 裂缝半长 - - renumberVisibleParameterRows(table); -} - // 获取可拟合参数的系统物理边界,索引 4 不再作为拟合参数。 bool nmWxAutomaticFitting::getPhysicalParameterRange(int parameterIndex, - double& minValue, double& maxValue) + double& minValue, double& maxValue) { - static const char* parameterNames[] = { - "Result_K", "Result_W_Skin", "Result_W_C", "Result_phi", - "", "Result_W_Dfc", - "W_FractureHalfLength" - }; - if(parameterIndex < 0 || parameterIndex >= 7 || parameterIndex == 4) { - return false; - } - - if(!nmAutoFitReadPhysicalRange(parameterNames[parameterIndex], minValue, maxValue)) { - return false; - } - - // 自动拟合的表皮搜索下界固定为 0,自动范围和手工输入校验使用同一边界。 - if(parameterIndex == 1) { - minValue = 0.0; - } - - - return true; + // 参数行按实际类型查询边界,不依赖它位于哪口井的分组。 + static const char* names[] = {"Result_K", "Result_W_Skin", "Result_W_C", + "Result_phi", "Result_W_Dfc", "W_FractureHalfLength"}; + const int type = automaticFittingData.getModelParameters()[parameterIndex].type; + if(!nmAutoFitReadPhysicalRange(names[type], minValue, maxValue)) return false; + if(type == 1) minValue = 0.0; + return true; } // 将一组上下界同步到表格和自动拟合数据,保证 PSO 读取到同一份配置。 -void nmWxAutomaticFitting::setParameterRange(int parameterIndex, - double minValue, double maxValue) +void nmWxAutomaticFitting::setParameterRange(int parameterIndex, double minValue, double maxValue) { - if(!m_parameterTable || parameterIndex < 0 || parameterIndex >= m_parameterTable->rowCount() - || !nmAutoFitUiIsFinite(minValue) || !nmAutoFitUiIsFinite(maxValue)) { - return; - } - - if(maxValue < minValue) { - qSwap(minValue, maxValue); - } - - const bool wasUpdatingRanges = m_updatingParameterRanges; - m_updatingParameterRanges = true; - - if(m_parameterTable->item(parameterIndex, 2)) { - m_parameterTable->item(parameterIndex, 2)->setText(QString::number(minValue, 'g', 10)); - } - if(m_parameterTable->item(parameterIndex, 4)) { - m_parameterTable->item(parameterIndex, 4)->setText(QString::number(maxValue, 'g', 10)); - } - - switch(parameterIndex) { - case 0: - automaticFittingData.getPermeabilityMin().setValue(minValue); - automaticFittingData.getPermeabilityMax().setValue(maxValue); - break; - case 1: - automaticFittingData.getSkinMin().setValue(minValue); - automaticFittingData.getSkinMax().setValue(maxValue); - break; - case 2: - automaticFittingData.getWellboreStorageMin().setValue(minValue); - automaticFittingData.getWellboreStorageMax().setValue(maxValue); - break; - case 3: - automaticFittingData.getPorosityMin().setValue(minValue); - automaticFittingData.getPorosityMax().setValue(maxValue); - break; - case 5: - automaticFittingData.getFractureConductivityMin().setValue(minValue); - automaticFittingData.getFractureConductivityMax().setValue(maxValue); - break; - case 6: - automaticFittingData.getFractureHalfLengthMin().setValue(minValue); - automaticFittingData.getFractureHalfLengthMax().setValue(maxValue); - break; - default: - break; - } - - m_updatingParameterRanges = wasUpdatingRanges; + // 自动范围写入时不触发尚未完成的行校验。 + const bool updating = m_updatingParameterRanges; + m_updatingParameterRanges = true; + m_parameterItems[parameterIndex]->setText(2, QString::number(minValue, 'g', 10)); + m_parameterItems[parameterIndex]->setText(3, QString::number(maxValue, 'g', 10)); + m_updatingParameterRanges = updating; } // 根据当前初值生成建议搜索范围,并始终截断在拟合边界内。skin 下界为 0, @@ -234,7 +120,7 @@ void nmWxAutomaticFitting::setParameterRange(int parameterIndex, void nmWxAutomaticFitting::updateRangeForParameter(int parameterIndex, double centerValue) { - if(!m_parameterTable || parameterIndex < 0 || parameterIndex >= 7 + if(!m_parameterTable || parameterIndex < 0 || parameterIndex >= m_parameterItems.size() || !nmAutoFitUiIsFinite(centerValue)) { return; } @@ -246,7 +132,8 @@ void nmWxAutomaticFitting::updateRangeForParameter(int parameterIndex, } double reference = centerValue; - const bool positiveParameter = parameterIndex != 1; + const int type = automaticFittingData.getModelParameters()[parameterIndex].type; + const bool positiveParameter = type != 1; // 正值参数初值为 0 时不再借用旧的界面范围,直接从物理边界选取搜索尺度。 if(positiveParameter && reference <= 0.0) { @@ -265,17 +152,17 @@ void nmWxAutomaticFitting::updateRangeForParameter(int parameterIndex, double newMin = physicalMin; double newMax = physicalMax; - if(parameterIndex == 1) { + if(type == 1) { const double skinHalfRange = 10.0; newMin = physicalMin; newMax = qMin(physicalMax, reference + skinHalfRange); } else if(reference > 0.0 - && !(parameterIndex == 5 && centerValue <= 0.0)) { + && !(type == 4 && centerValue <= 0.0)) { const double lowerFactor = 0.1; const double upperFactor = 10.0; newMin = qMax(physicalMin, reference * lowerFactor); newMax = qMin(physicalMax, reference * upperFactor); - } else if(parameterIndex == 5) { + } else if(type == 4) { // Dfc=0 表示无限导流,不存在以零为中心的连续倍率范围。 newMin = physicalMin; newMax = physicalMax; @@ -292,81 +179,40 @@ void nmWxAutomaticFitting::updateRangeForParameter(int parameterIndex, } } -// 按当前表格中的初值为所有参数建立建议范围,每次打开窗口和拟合完成后共用。 -void nmWxAutomaticFitting::initializeSuggestedParameterRanges() -{ - if(!m_parameterTable) { - return; - } - - for(int parameterIndex = 0; parameterIndex < 7; ++parameterIndex) { - QTableWidgetItem* initialItem = m_parameterTable->item(parameterIndex, 3); - if(initialItem) { - bool initialOk = false; - const double initialValue = initialItem->text().toDouble(&initialOk); - if(initialOk && nmAutoFitUiIsFinite(initialValue)) { - updateRangeForParameter(parameterIndex, initialValue); - } else { - // 数据对象没有提供该初值时使用完整物理区间,不回退到旧的默认范围。 - double physicalMin = 0.0; - double physicalMax = 0.0; - if(getPhysicalParameterRange(parameterIndex, physicalMin, physicalMax) - && physicalMax >= physicalMin) { - setParameterRange(parameterIndex, physicalMin, physicalMax); - } - } - } - } -} - -// 校验当前表格中的参数范围;parameterIndex 为 -1 时检查所有可见参数行。 +// 校验参数范围;parameterIndex 为 -1 时检查所有可用参数,包括折叠分组。 bool nmWxAutomaticFitting::validateParameterTable(QString& errorMessage, int parameterIndex) { if(!m_parameterTable) { errorMessage = tr("The parameter table is unavailable."); return false; } - if(parameterIndex < -1 || parameterIndex >= 7) { + if(parameterIndex < -1 || parameterIndex >= m_parameterItems.size()) { errorMessage = tr("The parameter row is invalid."); return false; } - static const char* parameterNames[] = { - "Permeability", "Skin", "Wellbore storage", "Porosity", - "", "Fracture conductivity", - "Fracture half length" - }; const int firstParameterIndex = parameterIndex < 0 ? 0 : parameterIndex; - const int lastParameterIndex = parameterIndex < 0 ? 7 : parameterIndex + 1; + const int lastParameterIndex = parameterIndex < 0 ? m_parameterItems.size() : parameterIndex + 1; for(int currentParameterIndex = firstParameterIndex; currentParameterIndex < lastParameterIndex; ++currentParameterIndex) { - // 隐藏参数不参与当前模型拟合,不用它们的历史值阻塞当前设置。 - if(m_parameterTable->isRowHidden(currentParameterIndex)) { - continue; - } - - QTableWidgetItem* minItem = m_parameterTable->item(currentParameterIndex, 2); - QTableWidgetItem* initialItem = m_parameterTable->item(currentParameterIndex, 3); - QTableWidgetItem* maxItem = m_parameterTable->item(currentParameterIndex, 4); - if(!minItem || !initialItem || !maxItem) { - errorMessage = tr("The range values for %1 are incomplete.") - .arg(tr(parameterNames[currentParameterIndex])); - return false; - } + QTreeWidgetItem* item = m_parameterItems[currentParameterIndex]; + // 已参与采样或当前算法不可用的参数不阻塞设置保存;折叠的分组仍须校验。 + if(item->isDisabled()) continue; + const nmFitParameter& parameter = automaticFittingData.getModelParameters()[currentParameterIndex]; bool minOk = false; bool initialOk = false; bool maxOk = false; - const double minValue = minItem->text().toDouble(&minOk); - const double initialValue = initialItem->text().toDouble(&initialOk); - const double maxValue = maxItem->text().toDouble(&maxOk); + const double minValue = item->text(2).toDouble(&minOk); + const double initialValue = item->text(1).toDouble(&initialOk); + const double maxValue = item->text(3).toDouble(&maxOk); if(!minOk || !initialOk || !maxOk || !nmAutoFitUiIsFinite(minValue) || !nmAutoFitUiIsFinite(initialValue) || !nmAutoFitUiIsFinite(maxValue)) { errorMessage = tr("The minimum value, initial value, and maximum value of %1 must be finite numbers.") - .arg(tr(parameterNames[currentParameterIndex])); + .arg(parameter.name()); return false; } @@ -376,7 +222,7 @@ bool nmWxAutomaticFitting::validateParameterTable(QString& errorMessage, int par || !nmAutoFitUiIsFinite(physicalMin) || !nmAutoFitUiIsFinite(physicalMax) || physicalMax < physicalMin) { errorMessage = tr("The physical range of %1 is invalid.") - .arg(tr(parameterNames[currentParameterIndex])); + .arg(parameter.name()); return false; } @@ -387,7 +233,7 @@ bool nmWxAutomaticFitting::validateParameterTable(QString& errorMessage, int par || (maxValue < physicalMin && !nmAutoFitUiNearlyEqual(maxValue, physicalMin)) || (maxValue > physicalMax && !nmAutoFitUiNearlyEqual(maxValue, physicalMax))) { errorMessage = tr("The values of %1 exceed the physical range [%2, %3].") - .arg(tr(parameterNames[currentParameterIndex])) + .arg(parameter.name()) .arg(QString::number(physicalMin, 'g', 10)) .arg(QString::number(physicalMax, 'g', 10)); return false; @@ -395,15 +241,15 @@ bool nmWxAutomaticFitting::validateParameterTable(QString& errorMessage, int par if(minValue > maxValue || minValue > initialValue || initialValue > maxValue) { errorMessage = tr("The values of %1 must satisfy: minimum <= initial value <= maximum.") - .arg(tr(parameterNames[currentParameterIndex])); + .arg(parameter.name()); return false; } // 底层用 Dfc=0 表示无限导流,这是离散模型选项,不属于有限导流拟合域。 - if(currentParameterIndex == 5 && m_dfcCheckBox->isChecked() + if(parameter.type == 4 && item->checkState(0) == Qt::Checked && (minValue <= 1.0e-10 || initialValue <= 1.0e-10)) { errorMessage = tr("The minimum value of %1 must be greater than zero for automatic fitting.") - .arg(tr(parameterNames[currentParameterIndex])); + .arg(parameter.name()); return false; } } @@ -421,7 +267,6 @@ nmWxAutomaticFitting::nmWxAutomaticFitting(QWidget *parent, nmSensitivityFitSett , m_progressDialog(nullptr) , m_progressTimer(nullptr) , m_progressMonitor(nullptr) - , m_autoParameterRanges(true) // 构造期间先禁止即时校验,避免初始值写入和范围生成之间出现短暂的不一致。 , m_updatingParameterRanges(true) { @@ -431,8 +276,12 @@ nmWxAutomaticFitting::nmWxAutomaticFitting(QWidget *parent, nmSensitivityFitSett nmDataAnalyzeManager* pManager = nmDataAnalyzeManager::getCurrentInstance(); reservoirData = pManager->getReservoirDataCopy(); automaticFittingData = pManager->getAutomaticFittingDataCopy(); - // 自动范围始终开启;用户在表格中修改上下限后,itemChanged 会临时切换为手工范围。 - m_autoParameterRanges = true; + if(m_sensitivitySettings) { + if(m_sensitivitySettings->confirmed) automaticFittingData = m_sensitivitySettings->parameters; + else automaticFittingData.getModelParameters().clear(); + } + nmDataWellBase* primary = pManager->findWellByCode(pManager->getPrimaryWellCode()); + m_targetWellName = primary ? primary->getWellName() : QString(); setupUI(); // 敏感性入口使用“确定”保存设置,普通入口仍直接开始拟合。 @@ -442,49 +291,13 @@ nmWxAutomaticFitting::nmWxAutomaticFitting(QWidget *parent, nmSensitivityFitSett // 初始尺寸随所在屏幕变化,最小尺寸交给实际控件布局决定。 resize(sizeHint()); - if(m_targetWellCombo->count() > 0) { - onWellSelected(0); // 默认选中第一口井 - } - // 每次打开都按当前模型参数生成范围,不沿用上次的上下限。 - initializeSuggestedParameterRanges(); - if(m_sensitivitySettings && m_sensitivitySettings->confirmed) loadSensitivitySettings(); + initializeModelParameters(); + onAlgorithmChanged(m_algorithmCombo->currentIndex()); m_updatingParameterRanges = false; DEBUG_UI("AutoFitting Constructor completed"); } -void nmWxAutomaticFitting::loadSensitivitySettings() -{ - // 恢复已确认的勾选和拟合设置,初值仍读取当前模型。 - const int targetIndex = m_targetWellCombo->findText(m_sensitivitySettings->targetWell); - if(targetIndex < 0) return; - m_targetWellCombo->setCurrentIndex(targetIndex); - automaticFittingData = m_sensitivitySettings->parameters; - m_autoParameterRanges = false; - // 敏感性设置恢复时也固定使用 LM。 - m_algorithmCombo->setCurrentIndex(1); - m_iterationEdit->setText(QString::number(automaticFittingData.getIterationCount().getValue().toInt())); - m_errorLimitEdit->setText(QString::number(automaticFittingData.getErrorTolerance().getValue().toDouble(), 'g', 10)); - const int rows[] = {0, 1, 2, 3, 5, 6}; - QCheckBox* checks[] = {m_kCheckBox, m_sCheckBox, m_cCheckBox, m_phiCheckBox, m_dfcCheckBox, m_fractureHalfLengthCheckBox}; - const bool selected[] = {automaticFittingData.getPermeabilitySelected(), automaticFittingData.getSkinSelected(), - automaticFittingData.getWellboreStorageSelected(), automaticFittingData.getPorositySelected(), - automaticFittingData.getFractureConductivitySelected(), automaticFittingData.getFractureHalfLengthSelected()}; - nmDataAttribute* lower[] = {&automaticFittingData.getPermeabilityMin(), &automaticFittingData.getSkinMin(), - &automaticFittingData.getWellboreStorageMin(), &automaticFittingData.getPorosityMin(), - &automaticFittingData.getFractureConductivityMin(), &automaticFittingData.getFractureHalfLengthMin()}; - nmDataAttribute* upper[] = {&automaticFittingData.getPermeabilityMax(), &automaticFittingData.getSkinMax(), - &automaticFittingData.getWellboreStorageMax(), &automaticFittingData.getPorosityMax(), - &automaticFittingData.getFractureConductivityMax(), &automaticFittingData.getFractureHalfLengthMax()}; - for(int i = 0; i < 6; ++i) { - checks[i]->setChecked(selected[i] && checks[i]->isEnabled()); - setParameterRange(rows[i], lower[i]->getValue().toDouble(), upper[i]->getValue().toDouble()); - const double currentValue = m_parameterTable->item(rows[i], 3)->text().toDouble(); - if(currentValue < lower[i]->getValue().toDouble() || currentValue > upper[i]->getValue().toDouble()) - updateRangeForParameter(rows[i], currentValue); - } -} - QSize nmWxAutomaticFitting::sizeHint() const { // 沿用项目的可用屏幕尺寸计算方式,优先取父窗口所在屏幕,支持多显示器。 @@ -529,10 +342,9 @@ void nmWxAutomaticFitting::setupUI() "#autoFittingSettings QLineEdit:focus, #autoFittingSettings QComboBox:focus { border-color: #3978c6; }" "#autoFittingSettings QComboBox::drop-down { border: none; width: 24px; }" "#autoFittingSettings QComboBox QAbstractItemView { background: #ffffff; color: #273449; selection-background-color: #eaf1fb; selection-color: #273449; }" - "#autoFittingSettings QTableWidget { border: none; background: #ffffff; color: #273449; selection-background-color: #eaf1fb; selection-color: #273449; }" + "#autoFittingSettings QTreeWidget { border: none; background: #ffffff; color: #273449; selection-background-color: #eaf1fb; selection-color: #273449; }" "#autoFittingSettings QHeaderView::section { background: #f5f7fa; color: #667487; border: none; border-bottom: 1px solid #e5eaf1; padding: 7px; }" - "#autoFittingSettings QTableWidget::item { border-bottom: 1px solid #eef1f5; padding: 5px 8px; }" - "#autoFittingSettings QCheckBox { color: #273449; background: transparent; padding-left: 10px; spacing: 8px; }" + "#autoFittingSettings QTreeWidget::item { border-bottom: 1px solid #eef1f5; padding: 5px 8px; }" "#autoFittingSettings QPushButton { background: #ffffff; color: #46556b; border: 1px solid #cfd7e2; border-radius: 4px; padding: 6px 18px; min-width: 64px; min-height: 20px; }" "#autoFittingSettings QPushButton:hover { background: #edf3fc; border-color: #9bb7db; }" "#autoFittingSettings QPushButton:pressed { background: #dce8f8; }" @@ -556,7 +368,7 @@ void nmWxAutomaticFitting::setupUI() tableLayout->setSpacing(10); QLabel* tableHeading = new QLabel(tr("Fitting parameters"), tablePanel); tableHeading->setObjectName("parameterHeading"); - tableLayout->addWidget(tableHeading); + tableLayout->addWidget(tableHeading); tableLayout->addWidget(m_parameterTable, 1); m_mainLayout->addWidget(tablePanel, 1); setupButtons(); @@ -566,146 +378,31 @@ void nmWxAutomaticFitting::setupUI() void nmWxAutomaticFitting::setupParameterTable() { - // 敏感性设置首次打开时不预选参数,普通拟合仍默认全选,再按井型隐藏不可用参数。 - m_parameterTable = new QTableWidget(7, 6, this); - - // 设置表头 - QStringList headers; - headers << "" << tr("Parameter") << tr("Min") << tr("Initial value") << tr("Max") << tr("Unit"); - m_parameterTable->setHorizontalHeaderLabels(headers); - - // 设置表格属性 - m_parameterTable->setSelectionBehavior(QAbstractItemView::SelectItems); - m_parameterTable->setAlternatingRowColors(false); - m_parameterTable->setShowGrid(false); - m_parameterTable->verticalHeader()->setDefaultSectionSize(36); - m_parameterTable->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); - m_parameterTable->verticalHeader()->setVisible(false); - - // 设置列宽 - QHeaderView* header = m_parameterTable->horizontalHeader(); - header->setResizeMode(0, QHeaderView::Fixed); // 序号列固定宽度 - header->setResizeMode(1, QHeaderView::Fixed); // 参数列固定宽度 - header->setResizeMode(2, QHeaderView::Stretch); // 其他列自适应 - header->setResizeMode(3, QHeaderView::Stretch); - header->setResizeMode(4, QHeaderView::Stretch); - header->setResizeMode(5, QHeaderView::Fixed); // 单位列固定宽度 - - // 设置固定列的宽度 - m_parameterTable->setColumnWidth(0, 40); // 序号列宽度 - m_parameterTable->setColumnWidth(1, 180); // 参数名称与复选框保留足够空间 - m_parameterTable->setColumnWidth(5, 80); // 单位列宽度 - // 只改变视觉顺序,保留数据读写使用的原列索引。 - m_parameterTable->setColumnHidden(0, true); - header->moveSection(header->visualIndex(3), header->visualIndex(2)); - - // 设置表格的大小策略 - m_parameterTable->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); - - // 渗透率 (Permeability) - m_parameterTable->setItem(0, 0, new QTableWidgetItem("1")); - m_kCheckBox = new QCheckBox(tr("Permeability")); - m_kCheckBox->setChecked(!m_sensitivitySettings); - m_parameterTable->setCellWidget(0, 1, m_kCheckBox); - m_parameterTable->setItem(0, 2, new QTableWidgetItem(QString::number(automaticFittingData.getPermeabilityMin().getValue().toDouble()))); - m_parameterTable->setItem(0, 3, new QTableWidgetItem(QString::number(reservoirData.getPermeability().getValue().toDouble()))); - m_parameterTable->setItem(0, 4, new QTableWidgetItem(QString::number(automaticFittingData.getPermeabilityMax().getValue().toDouble()))); - m_parameterTable->setItem(0, 5, new QTableWidgetItem(tr("mD"))); - - // 表皮系数 (Skin) - m_parameterTable->setItem(1, 0, new QTableWidgetItem("2")); - m_sCheckBox = new QCheckBox(tr("Skin")); - m_sCheckBox->setChecked(!m_sensitivitySettings); - m_parameterTable->setCellWidget(1, 1, m_sCheckBox); - m_parameterTable->setItem(1, 2, new QTableWidgetItem(QString::number(automaticFittingData.getSkinMin().getValue().toDouble()))); - m_parameterTable->setItem(1, 3, new QTableWidgetItem()); - m_parameterTable->setItem(1, 4, new QTableWidgetItem(QString::number(automaticFittingData.getSkinMax().getValue().toDouble()))); - m_parameterTable->setItem(1, 5, new QTableWidgetItem("")); - - // 井筒储集系数 (Wellbore storage) - m_parameterTable->setItem(2, 0, new QTableWidgetItem("3")); - m_cCheckBox = new QCheckBox(tr("Wellbore storage")); - m_cCheckBox->setChecked(!m_sensitivitySettings); - m_parameterTable->setCellWidget(2, 1, m_cCheckBox); - m_parameterTable->setItem(2, 2, new QTableWidgetItem(QString::number(automaticFittingData.getWellboreStorageMin().getValue().toDouble()))); - m_parameterTable->setItem(2, 3, new QTableWidgetItem()); - m_parameterTable->setItem(2, 4, new QTableWidgetItem(QString::number(automaticFittingData.getWellboreStorageMax().getValue().toDouble()))); - m_parameterTable->setItem(2, 5, new QTableWidgetItem(tr("m^3/MPa"))); - - // 孔隙度 (Porosity) - m_parameterTable->setItem(3, 0, new QTableWidgetItem("4")); - m_phiCheckBox = new QCheckBox(tr("Porosity")); - m_phiCheckBox->setChecked(!m_sensitivitySettings); - m_parameterTable->setCellWidget(3, 1, m_phiCheckBox); - m_parameterTable->setItem(3, 2, new QTableWidgetItem(QString::number(automaticFittingData.getPorosityMin().getValue().toDouble()))); - m_parameterTable->setItem(3, 3, new QTableWidgetItem(QString::number(reservoirData.getPorosity().getValue().toDouble()))); - m_parameterTable->setItem(3, 4, new QTableWidgetItem(QString::number(automaticFittingData.getPorosityMax().getValue().toDouble()))); - m_parameterTable->setItem(3, 5, new QTableWidgetItem("")); - - // 索引 4 留空,保持裂缝参数与优化器的现有索引一致。 - - // 裂缝导流能力 (Dfc)。该行只对压裂井显示,初值在 onWellSelected() 中 - // 从当前目标井读取,其他裂缝几何参数保持固定,不进入自动拟合。 - m_parameterTable->setItem(5, 0, new QTableWidgetItem("6")); - m_dfcCheckBox = new QCheckBox(tr("Fracture conductivity")); - m_dfcCheckBox->setChecked(!m_sensitivitySettings); - m_parameterTable->setCellWidget(5, 1, m_dfcCheckBox); - m_parameterTable->setItem(5, 2, new QTableWidgetItem(QString::number(automaticFittingData.getFractureConductivityMin().getValue().toDouble()))); - m_parameterTable->setItem(5, 3, new QTableWidgetItem()); - m_parameterTable->setItem(5, 4, new QTableWidgetItem(QString::number(automaticFittingData.getFractureConductivityMax().getValue().toDouble()))); - m_parameterTable->setItem(5, 5, new QTableWidgetItem(tr("md.m"))); - - // 裂缝半长。该行与 Dfc 一样只对压裂井显示,初值从当前目标井读取。 - m_parameterTable->setItem(6, 0, new QTableWidgetItem("7")); - m_fractureHalfLengthCheckBox = new QCheckBox(tr("Fracture half length")); - m_fractureHalfLengthCheckBox->setChecked(!m_sensitivitySettings); - m_parameterTable->setCellWidget(6, 1, m_fractureHalfLengthCheckBox); - m_parameterTable->setItem(6, 2, new QTableWidgetItem(QString::number(automaticFittingData.getFractureHalfLengthMin().getValue().toDouble()))); - m_parameterTable->setItem(6, 3, new QTableWidgetItem()); - m_parameterTable->setItem(6, 4, new QTableWidgetItem(QString::number(automaticFittingData.getFractureHalfLengthMax().getValue().toDouble()))); - m_parameterTable->setItem(6, 5, new QTableWidgetItem(tr("m"))); - - // 设置表格行为 - for(int i = 0; i < m_parameterTable->rowCount(); ++i) { - for(int j = 0; j < 6; ++j) { - QTableWidgetItem* item = m_parameterTable->item(i, j); - - if(item) { - if(j == 0 || j == 5) { // 序号列和单位列只读 - item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); - } else if(j >= 2) { - item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable); - } else { // 参数列(复选框)不可选择 - item->setFlags(Qt::NoItemFlags); - } - } - } - } - - // 序号列居中对齐 - for(int i = 0; i < m_parameterTable->rowCount(); ++i) { - QTableWidgetItem* item = m_parameterTable->item(i, 0); - - if(item) { - item->setTextAlignment(Qt::AlignCenter); - } - } - - nmDataAnalyzeManager* pManager = nmDataAnalyzeManager::getCurrentInstance(); - if(pManager) { - updateParameterVisibility(m_parameterTable, pManager->getSolverModelType()); - } - - // 连接选择改变信号 - connect(m_parameterTable, SIGNAL(currentCellChanged(int, int, int, int)), - this, SLOT(onCellSelectionChanged(int, int, int, int))); - connect(m_parameterTable, SIGNAL(itemChanged(QTableWidgetItem*)), - this, SLOT(onParameterTableItemChanged(QTableWidgetItem*))); + // 用可折叠分组直接展示各井参数,数值列沿用原来的编辑方式。 + m_parameterTable = new QTreeWidget(this); + m_parameterTable->setFont(QFont("Microsoft YaHei", 10)); + m_parameterTable->setColumnCount(5); + m_parameterTable->setHeaderLabels(QStringList() << tr("Parameter") << tr("Initial value") + << tr("Min") << tr("Max") << tr("Unit")); + m_parameterTable->setItemDelegate(new nmFitParameterDelegate(m_parameterTable)); + m_parameterTable->setSelectionBehavior(QAbstractItemView::SelectItems); + m_parameterTable->setUniformRowHeights(true); + m_parameterTable->setIndentation(18); + QHeaderView* header = m_parameterTable->header(); + header->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); + header->setStretchLastSection(false); + header->setResizeMode(0, QHeaderView::Interactive); + m_parameterTable->setColumnWidth(0, 230); + for(int column = 1; column <= 3; ++column) header->setResizeMode(column, QHeaderView::Stretch); + header->setResizeMode(4, QHeaderView::Fixed); + m_parameterTable->setColumnWidth(4, 120); + connect(m_parameterTable, SIGNAL(itemChanged(QTreeWidgetItem*, int)), + this, SLOT(onParameterTableItemChanged(QTreeWidgetItem*, int))); } void nmWxAutomaticFitting::setupControlPanel() { - // 四列设置:目标井、算法、目标误差、迭代上限。 + // 上方仅保留算法、目标误差和迭代上限。 m_controlLayout = new QGridLayout(); m_controlLayout->setHorizontalSpacing(16); m_controlLayout->setVerticalSpacing(6); @@ -716,7 +413,8 @@ void nmWxAutomaticFitting::setupControlPanel() m_algorithmCombo->addItem(tr("PSO (Particle Swarm)")); m_algorithmCombo->addItem(tr("LM")); // 从敏感性分析打开时仅使用 LM,普通自动拟合仍可选择算法。 - m_algorithmCombo->setCurrentIndex(m_sensitivitySettings ? 1 : 0); + m_algorithmCombo->setCurrentIndex(m_sensitivitySettings + || nmDataAnalyzeManager::getCurrentInstance()->getEffectiveCalculationWells().size() > 1 ? 1 : 0); m_algorithmCombo->setEnabled(!m_sensitivitySettings); connect(m_algorithmCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onAlgorithmChanged(int))); @@ -729,74 +427,56 @@ void nmWxAutomaticFitting::setupControlPanel() QLabel* errorLabel = new QLabel(tr("Target error:")); m_errorLimitEdit = new QLineEdit(QString::number(automaticFittingData.getErrorTolerance().getValue().toDouble())); - // 目标井选择 - QLabel* wellLabel = new QLabel(tr("Target Well:")); - m_targetWellCombo = new QComboBox(); - - // 目标井先加入当前分析主井,再加入已勾选且具备流量、压力数据的包含井。 - nmDataAnalyzeManager* pManager = nmDataAnalyzeManager::getCurrentInstance(); - if(pManager != nullptr) { - const QString sPrimaryWellCode = pManager->getPrimaryWellCode(); - nmDataWellBase* pPrimaryWell = pManager->findWellByCode(sPrimaryWellCode); - if(pPrimaryWell != nullptr) { - m_targetWellCombo->addItem(pPrimaryWell->getWellName()); - } - - if(pManager->getIncludeOtherWells()) { - const QVector vecIncludedWells = - pManager->getIncludedCalculationWells(); - for(int nIndex = 0; nIndex < vecIncludedWells.size(); ++nIndex) { - const QString& sWellCode = vecIncludedWells[nIndex].m_sWellCode; - nmDataWellBase* pWellData = pManager->findWellByCode(sWellCode); - if(pWellData == nullptr || sWellCode == sPrimaryWellCode || - pWellData->getFlowSegmentCount() <= 0 || - pWellData->getPressurePoints().size() < 2) { - continue; - } - m_targetWellCombo->addItem(pWellData->getWellName()); - } - } - } - connect(m_targetWellCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onWellSelected(int))); - m_targetWellCombo->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon); - m_targetWellCombo->setMinimumContentsLength(10); - m_controlLayout->addWidget(wellLabel, 0, 0); - m_controlLayout->addWidget(m_targetWellCombo, 1, 0); - m_controlLayout->addWidget(algorithmLabel, 0, 1); - m_controlLayout->addWidget(m_algorithmCombo, 1, 1); - m_controlLayout->addWidget(errorLabel, 0, 2); - m_controlLayout->addWidget(m_errorLimitEdit, 1, 2); - m_controlLayout->addWidget(m_iterationLabel, 0, 3); - m_controlLayout->addWidget(m_iterationEdit, 1, 3); - for(int column = 0; column < 4; ++column) m_controlLayout->setColumnStretch(column, 1); - onAlgorithmChanged(m_algorithmCombo->currentIndex()); + m_controlLayout->addWidget(algorithmLabel, 0, 0); + m_controlLayout->addWidget(m_algorithmCombo, 1, 0); + m_controlLayout->addWidget(errorLabel, 0, 1); + m_controlLayout->addWidget(m_errorLimitEdit, 1, 1); + m_controlLayout->addWidget(m_iterationLabel, 0, 2); + m_controlLayout->addWidget(m_iterationEdit, 1, 2); + // 右侧预留一列空白,三个输入框不再铺满整行。 + for(int column = 0; column < 4; ++column) m_controlLayout->setColumnStretch(column, 1); } void nmWxAutomaticFitting::onAlgorithmChanged(int index) { - // 两种算法统一显示最大迭代次数,提示仍说明 LM 共用次数的含义。 - const bool usePSO = (index == 0); - m_iterationLabel->setText(tr("Maximum iterations:")); - m_iterationEdit->setToolTip(usePSO ? QString() : tr("Height, shape and total-error adjustments share this iteration limit; one iteration may call the solver multiple times.")); + // 多井联合拟合使用 LM;PSO 仍只开放主井和共享储层参数。 + const bool usePSO = index == 0; + const QString primaryCode = nmDataAnalyzeManager::getCurrentInstance()->getPrimaryWellCode(); + const bool updating = m_updatingParameterRanges; + m_updatingParameterRanges = true; + const QVector& parameters = automaticFittingData.getModelParameters(); + for(int i = 0; i < parameters.size(); ++i) { + const nmFitParameter& p = parameters[i]; + nmDataWellBase* well = p.wellCode.isEmpty() ? nullptr + : nmDataAnalyzeManager::getCurrentInstance()->findWellByCode(p.wellCode); + const bool unavailable = (m_sensitivitySettings && m_sampledParameters.contains(p.samplingKey())) + || (usePSO && !p.wellCode.isEmpty() && p.wellCode != primaryCode) + || (p.type == 1 && (!well || well->getPerforationCount() == 0)); + m_parameterItems[i]->setDisabled(unavailable); + if(unavailable) m_parameterItems[i]->setCheckState(0, Qt::Unchecked); + } + m_updatingParameterRanges = updating; + updateSelectAllState(); + m_iterationLabel->setText(tr("Maximum iterations:")); + m_iterationEdit->setToolTip(usePSO ? QString() : tr("Height, shape and total-error adjustments share this iteration limit; one iteration may call the solver multiple times.")); } void nmWxAutomaticFitting::setupButtons() { QHBoxLayout* buttonLayout = new QHBoxLayout(); - m_reverseBtn = new QPushButton(tr("Reverse Selection")); + m_selectAllCheckBox = new QCheckBox(tr("Select All")); m_okBtn = new QPushButton(tr("Start fitting")); m_okBtn->setObjectName("startFitting"); m_okBtn->setDefault(true); m_cancelBtn = new QPushButton(tr("Cancel")); // 设置按钮大小 - m_reverseBtn->setMinimumWidth(120); m_okBtn->setMinimumWidth(80); m_cancelBtn->setMinimumWidth(80); - buttonLayout->addWidget(m_reverseBtn); + buttonLayout->addWidget(m_selectAllCheckBox); buttonLayout->addStretch(); buttonLayout->addWidget(m_cancelBtn); buttonLayout->addWidget(m_okBtn); @@ -804,76 +484,48 @@ void nmWxAutomaticFitting::setupButtons() m_mainLayout->addLayout(buttonLayout); // 连接信号槽 - connect(m_reverseBtn, SIGNAL(clicked()), this, SLOT(onReverseSelection())); + connect(m_selectAllCheckBox, SIGNAL(clicked(bool)), this, SLOT(onSelectAll(bool))); connect(m_okBtn, SIGNAL(clicked()), this, SLOT(onAccept())); connect(m_cancelBtn, SIGNAL(clicked()), this, SLOT(onReject())); } -void nmWxAutomaticFitting::onCellSelectionChanged(int currentRow, int currentColumn, int previousRow, int previousColumn) +void nmWxAutomaticFitting::onSelectAll(bool checked) { - if(!m_parameterTable) return; - - QHeaderView* header = m_parameterTable->horizontalHeader(); - if(!header) return; - - // 重置所有列头的字体为正常 - - for(int i = 0; i < m_parameterTable->columnCount(); ++i) { - QFont font = header->font(); - font.setBold(false); - if(m_parameterTable->horizontalHeaderItem(i)) { - m_parameterTable->horizontalHeaderItem(i)->setFont(font); - } - } - - // 设置当前列的表头为粗体 - - if(currentColumn >= 0 && currentColumn < m_parameterTable->columnCount()) { - QFont font = header->font(); - font.setBold(true); - - if(m_parameterTable->horizontalHeaderItem(currentColumn)) { - m_parameterTable->horizontalHeaderItem(currentColumn)->setFont(font); - } - } - - header->update(); + // 批量切换可用参数,完成后统一同步全选框,避免逐项更新时改变点击状态。 + const bool updating = m_updatingParameterRanges; + m_updatingParameterRanges = true; + for(int i = 0; i < m_parameterItems.size(); ++i) { + QTreeWidgetItem* item = m_parameterItems[i]; + if(!item->isDisabled()) + item->setCheckState(0, checked ? Qt::Checked : Qt::Unchecked); + } + m_updatingParameterRanges = updating; + updateSelectAllState(); } -void nmWxAutomaticFitting::onReverseSelection() +void nmWxAutomaticFitting::updateSelectAllState() { - // 只反选可用参数,不能通过反选勾上已参与采样的参数。 - if(!m_parameterTable) { - return; - } - - if(m_kCheckBox->isEnabled()) m_kCheckBox->setChecked(!m_kCheckBox->isChecked()); - if(m_sCheckBox->isEnabled()) m_sCheckBox->setChecked(!m_sCheckBox->isChecked()); - if(m_cCheckBox->isEnabled()) m_cCheckBox->setChecked(!m_cCheckBox->isChecked()); - if(m_phiCheckBox->isEnabled()) m_phiCheckBox->setChecked(!m_phiCheckBox->isChecked()); - if(m_dfcCheckBox->isEnabled()) m_dfcCheckBox->setChecked(!m_dfcCheckBox->isChecked()); - if(m_fractureHalfLengthCheckBox->isEnabled()) m_fractureHalfLengthCheckBox->setChecked(!m_fractureHalfLengthCheckBox->isChecked()); + // 仅统计可用参数;单项勾选和算法切换后,全选框都与当前列表保持一致。 + int available = 0; + int selected = 0; + for(int i = 0; i < m_parameterItems.size(); ++i) { + if(m_parameterItems[i]->isDisabled()) continue; + ++available; + if(m_parameterItems[i]->checkState(0) == Qt::Checked) ++selected; + } + m_selectAllCheckBox->setEnabled(available > 0); + m_selectAllCheckBox->setChecked(available > 0 && selected == available); } -void nmWxAutomaticFitting::onParameterTableItemChanged(QTableWidgetItem* item) +void nmWxAutomaticFitting::onParameterTableItemChanged(QTreeWidgetItem* item, int column) { - if(!item || m_updatingParameterRanges) { - return; - } - - // 手工范围用于本轮拟合,切井时保留;拟合完成后再按最新参数自动生成范围。 - if(item->column() == 2 || item->column() == 4) { - m_autoParameterRanges = false; - } - - if(item->column() >= 2 && item->column() <= 4 - && !m_parameterTable->isRowHidden(item->row())) { - QString validationError; - if(!validateParameterTable(validationError, item->row())) { - // 表格编辑提交后立即提示,用户不需要先点击“确定”才发现错误。 - QMessageBox::warning(this, tr("Invalid parameter range"), validationError); - } - } + // 勾选时同步全选框,数值编辑仍沿用原来的范围校验。 + if(!m_updatingParameterRanges && column == 0) updateSelectAllState(); + if(m_updatingParameterRanges || column < 1 || column > 3 + || !item->data(0, Qt::UserRole).isValid()) return; + QString error; + if(!validateParameterTable(error, item->data(0, Qt::UserRole).toInt())) + QMessageBox::warning(this, tr("Invalid parameter range"), error); } void nmWxAutomaticFitting::onAccept() @@ -885,15 +537,13 @@ void nmWxAutomaticFitting::onAccept() } // 校验通过后再保存参数设置,非法输入不会被静默裁剪。 - setAutomaticFittingValue(); + saveModelParameters(); + setAutomaticFittingValue(); if(m_sensitivitySettings) { // 仅提交给敏感性窗口;取消、关闭及普通拟合均不经过此分支。 m_sensitivitySettings->parameters = automaticFittingData; - m_sensitivitySettings->targetWell = m_targetWellCombo->currentText(); - m_sensitivitySettings->initialValues.clear(); - const int rows[] = {0, 1, 2, 3, 5, 6}; - for(int i = 0; i < 6; ++i) - m_sensitivitySettings->initialValues.append(m_parameterTable->item(rows[i], 3)->text().toDouble()); + m_sensitivitySettings->targetWell = m_targetWellName; + m_sensitivitySettings->initialValues = primaryInitialValues(); m_sensitivitySettings->confirmed = true; accept(); return; @@ -902,7 +552,7 @@ void nmWxAutomaticFitting::onAccept() nmWxParameterProperty::notifyUpdateTable(); // 获取目标井的双对数历史数据 - QString selectedWellName = m_targetWellCombo->currentText(); + QString selectedWellName = m_targetWellName; if(selectedWellName.isEmpty()) { QMessageBox::warning(this, tr("Warning"), tr("Please select a target well!")); @@ -970,25 +620,14 @@ void nmWxAutomaticFitting::onAccept() } // 检查是否有参数被选中 - bool hasSelectedParams = m_kCheckBox->isChecked() || m_sCheckBox->isChecked() || - m_cCheckBox->isChecked() || m_phiCheckBox->isChecked() || - m_dfcCheckBox->isChecked() || - m_fractureHalfLengthCheckBox->isChecked(); + bool hasSelectedParams = automaticFittingData.hasSelectedParameters(); if(!hasSelectedParams) { QMessageBox::warning(this, tr("Warning"), tr("Please select at least one parameter for optimization!")); return; } - // 收集选中的参数名称 - QStringList selectedParameterNames; - - if(m_kCheckBox->isChecked()) selectedParameterNames << tr("Permeability"); - if(m_sCheckBox->isChecked()) selectedParameterNames << tr("Skin"); - if(m_cCheckBox->isChecked()) selectedParameterNames << tr("Wellbore storage"); - if(m_phiCheckBox->isChecked()) selectedParameterNames << tr("Porosity"); - if(m_dfcCheckBox->isChecked()) selectedParameterNames << tr("Fracture conductivity"); - if(m_fractureHalfLengthCheckBox->isChecked()) selectedParameterNames << tr("Fracture half length"); + const QStringList selectedParameterNames = selectedParameterLabels(false); // 启动自动拟合 - 传递双对数历史数据 startAutoFitting(targetLogLogData, selectedParameterNames, selectedWellName); @@ -998,121 +637,83 @@ void nmWxAutomaticFitting::onReject() reject(); } -void nmWxAutomaticFitting::onWellSelected(int index) -{ - // 每次都读取模型实际保留的井参数,避免拟合结束后切井又恢复到打开窗口时的旧副本。 - nmDataAnalyzeManager* manager = nmDataAnalyzeManager::getCurrentInstance(); - if(!manager) { - return; - } - nmDataWellBase* targetWell = manager->findWellByName(m_targetWellCombo->itemText(index)); - if(!targetWell) { - return; - } - - const bool wasUpdatingRanges = m_updatingParameterRanges; - m_updatingParameterRanges = true; - updateParameterVisibility(m_parameterTable, manager->getSolverModelType()); - - nmDataPerforation* perforation = targetWell->getPerforation(0); - if(perforation) { - const double skin = perforation->getSkin().getValue().toDouble(); - m_parameterTable->item(1, 3)->setText(QString::number(skin, 'g', 10)); - if(m_autoParameterRanges) { - updateRangeForParameter(1, skin); - } - } - const double storage = targetWell->getWellboreStorage().getValue().toDouble(); - m_parameterTable->item(2, 3)->setText(QString::number(storage, 'g', 10)); - if(m_autoParameterRanges) { - updateRangeForParameter(2, storage); - } - - bool fracturedWell = false; - double conductivity = 0.0; - double halfLength = 0.0; - if(nmDataVerticalFracturedWell* well = dynamic_cast(targetWell)) { - conductivity = well->getDfc().getValue().toDouble(); - halfLength = well->getFractureHalfLength().getValue().toDouble(); - fracturedWell = true; - } else if(nmDataHorizontalFracturedWell* well = dynamic_cast(targetWell)) { - conductivity = well->getDfc().getValue().toDouble(); - halfLength = well->getFractureHalfLength().getValue().toDouble(); - fracturedWell = true; - } - if(fracturedWell) { - m_parameterTable->item(5, 3)->setText(QString::number(conductivity, 'g', 10)); - m_parameterTable->item(6, 3)->setText(QString::number(halfLength, 'g', 10)); - if(m_autoParameterRanges) { - updateRangeForParameter(5, conductivity); - updateRangeForParameter(6, halfLength); - } - } - m_updatingParameterRanges = wasUpdatingRanges; -} - void nmWxAutomaticFitting::setAutomaticFittingValue() { - // 保存剩余拟合参数的选择状态,LM 和 PSO 共用此配置。 - automaticFittingData.setPermeabilitySelected(m_kCheckBox->isChecked()); - automaticFittingData.setSkinSelected(m_sCheckBox->isChecked()); - automaticFittingData.setWellboreStorageSelected(m_cCheckBox->isChecked()); - automaticFittingData.setPorositySelected(m_phiCheckBox->isChecked()); - automaticFittingData.setFractureConductivitySelected(m_dfcCheckBox->isChecked()); - automaticFittingData.setFractureHalfLengthSelected(m_fractureHalfLengthCheckBox->isChecked()); - // 界面不再提供 PSO 加速选项,同时关闭旧配置中可能保留的加速开关。 - automaticFittingData.setSurrogateScreeningEnabled(false); - - // 保存渗透率的最小值和最大值 - automaticFittingData.getPermeabilityMin().setValue(m_parameterTable->item(0, 2)->text().toDouble()); - automaticFittingData.getPermeabilityMax().setValue(m_parameterTable->item(0, 4)->text().toDouble()); - - // 保存表皮系数的最小值和最大值 - automaticFittingData.getSkinMin().setValue(m_parameterTable->item(1, 2)->text().toDouble()); - automaticFittingData.getSkinMax().setValue(m_parameterTable->item(1, 4)->text().toDouble()); - - // 保存井筒储集系数的最小值和最大值 - automaticFittingData.getWellboreStorageMin().setValue(m_parameterTable->item(2, 2)->text().toDouble()); - automaticFittingData.getWellboreStorageMax().setValue(m_parameterTable->item(2, 4)->text().toDouble()); - - // 保存孔隙度的最小值和最大值 - automaticFittingData.getPorosityMin().setValue(m_parameterTable->item(3, 2)->text().toDouble()); - automaticFittingData.getPorosityMax().setValue(m_parameterTable->item(3, 4)->text().toDouble()); - - // 保存裂缝导流能力的最小值和最大值 - automaticFittingData.getFractureConductivityMin().setValue(m_parameterTable->item(5, 2)->text().toDouble()); - automaticFittingData.getFractureConductivityMax().setValue(m_parameterTable->item(5, 4)->text().toDouble()); - - // 保存裂缝半长的最小值和最大值 - automaticFittingData.getFractureHalfLengthMin().setValue(m_parameterTable->item(6, 2)->text().toDouble()); - automaticFittingData.getFractureHalfLengthMax().setValue(m_parameterTable->item(6, 4)->text().toDouble()); - - // 保存迭代参数 - automaticFittingData.getIterationCount().setValue(m_iterationEdit->text().toInt()); - automaticFittingData.getErrorTolerance().setValue(m_errorLimitEdit->text().toDouble()); - // 敏感性只收集设置,初值由调用方保存,不写回井和储层参数。 - if(m_sensitivitySettings) { - automaticFittingData.setAlgorithmName("LM"); + // 单井旧字段继续供 PSO 和旧配置读取,LM 直接使用完整分组参数列表。 + automaticFittingData.setPermeabilitySelected(false); + automaticFittingData.setSkinSelected(false); + automaticFittingData.setWellboreStorageSelected(false); + automaticFittingData.setPorositySelected(false); + automaticFittingData.setFractureConductivitySelected(false); + automaticFittingData.setFractureHalfLengthSelected(false); + const QString primaryCode = nmDataAnalyzeManager::getCurrentInstance()->getPrimaryWellCode(); + const QVector& parameters = automaticFittingData.getModelParameters(); + for(int i = 0; i < parameters.size(); ++i) { + const nmFitParameter& p = parameters[i]; + if(!p.wellCode.isEmpty() && p.wellCode != primaryCode) continue; + switch(p.type) { + case 0: + automaticFittingData.setPermeabilitySelected(p.selected); + automaticFittingData.getPermeabilityMin().setValue(p.lower); + automaticFittingData.getPermeabilityMax().setValue(p.upper); + break; + case 1: + automaticFittingData.setSkinSelected(p.selected); + automaticFittingData.getSkinMin().setValue(p.lower); + automaticFittingData.getSkinMax().setValue(p.upper); + break; + case 2: + automaticFittingData.setWellboreStorageSelected(p.selected); + automaticFittingData.getWellboreStorageMin().setValue(p.lower); + automaticFittingData.getWellboreStorageMax().setValue(p.upper); + break; + case 3: + automaticFittingData.setPorositySelected(p.selected); + automaticFittingData.getPorosityMin().setValue(p.lower); + automaticFittingData.getPorosityMax().setValue(p.upper); + break; + case 4: + automaticFittingData.setFractureConductivitySelected(p.selected); + automaticFittingData.getFractureConductivityMin().setValue(p.lower); + automaticFittingData.getFractureConductivityMax().setValue(p.upper); + break; + case 5: + automaticFittingData.setFractureHalfLengthSelected(p.selected); + automaticFittingData.getFractureHalfLengthMin().setValue(p.lower); + automaticFittingData.getFractureHalfLengthMax().setValue(p.upper); + break; + } + } + automaticFittingData.setSurrogateScreeningEnabled(false); + automaticFittingData.getIterationCount().setValue(m_iterationEdit->text().toInt()); + automaticFittingData.getErrorTolerance().setValue(m_errorLimitEdit->text().toDouble()); + automaticFittingData.setAlgorithmName(m_algorithmCombo->currentIndex() == 1 ? "LM" : "PSO"); + // 敏感性入口只保存设置;普通 LM 在启动时按井应用初值。 + if(m_sensitivitySettings) return; + if(m_algorithmCombo->currentIndex() == 1) { + nmDataAnalyzeManager::getCurrentInstance()->updateAutomaticFittingData(automaticFittingData); return; } - + const QVector initial = primaryInitialValues(); // 只写回可拟合的渗透率、孔隙度,含水饱和度沿用模型当前值。 - reservoirData.getPermeability().setValue(m_parameterTable->item(0, 3)->text().toDouble()); // 渗透率 - reservoirData.getPorosity().setValue(m_parameterTable->item(3, 3)->text().toDouble()); // 孔隙度 + reservoirData.getPermeability().setValue(initial[0]); // 渗透率 + reservoirData.getPorosity().setValue(initial[3]); // 孔隙度 // 更新储层数据(全局) nmDataAnalyzeManager::getCurrentInstance()->updateReservoirData(reservoirData); // 保存自动拟合数据 - nmDataAnalyzeManager::getCurrentInstance()->updateAutomaticFittingData(automaticFittingData); + nmDataAutomaticFitting psoSettings = automaticFittingData; + psoSettings.getModelParameters().clear(); + nmDataAnalyzeManager::getCurrentInstance()->updateAutomaticFittingData(psoSettings); // 只更新目标井的参数 - QString selectedWellName = m_targetWellCombo->currentText(); + QString selectedWellName = m_targetWellName; if(!selectedWellName.isEmpty()) { - double newSkinValue = m_parameterTable->item(1, 3)->text().toDouble(); - double newWellboreStorageValue = m_parameterTable->item(2, 3)->text().toDouble(); - double newFractureConductivityValue = m_parameterTable->item(5, 3)->text().toDouble(); - double newFractureHalfLengthValue = m_parameterTable->item(6, 3)->text().toDouble(); + double newSkinValue = initial[1]; + double newWellboreStorageValue = initial[2]; + double newFractureConductivityValue = initial[4]; + double newFractureHalfLengthValue = initial[5]; // 直接从数据管理器获取目标井 nmDataAnalyzeManager* manager = nmDataAnalyzeManager::getCurrentInstance(); @@ -1185,39 +786,6 @@ void nmWxAutomaticFitting::startAutoFitting(const QVector>& targ m_autoFitterPSO->setPSOTargetWellName(targetWellName); } - //// 特定井名时使用快速路径 - //if (targetWellName == "VerticalWell1") { - - // m_autoFitterPSO->setSimulationMode(true); - - // // 构建目标参数向量(按启用参数的顺序) - // QVector targetParams; - - // if(m_kCheckBox->isChecked()) targetParams.append(0.0033); // 渗透率 - // if(m_sCheckBox->isChecked()) targetParams.append(1.75); // 表皮系数 - // if(m_cCheckBox->isChecked()) targetParams.append(0.23); // 井筒储集系数 - // if(m_phiCheckBox->isChecked()) targetParams.append(0.189); // 孔隙度 - - // double targetError = 0.02291; - // m_autoFitterPSO->setSimulationTargetParams(targetParams, targetError); - //} - //else if (targetWellName == "W0009_1") { - - // m_autoFitterPSO->setSimulationMode(true); - - // // 构建目标参数向量(按启用参数的顺序) - // QVector targetParams; - - // if(m_kCheckBox->isChecked()) targetParams.append(0.23); // 渗透率 - // if(m_sCheckBox->isChecked()) targetParams.append(0.75); // 表皮系数 - // if(m_cCheckBox->isChecked()) targetParams.append(1.28); // 井筒储集系数 - // if(m_phiCheckBox->isChecked()) targetParams.append(0.1954); // 孔隙度 - - // double targetError = 0.02036; - // m_autoFitterPSO->setSimulationTargetParams(targetParams, targetError); - //} - - m_progressMonitor = new nmWxAutomaticfittingStart(this); if(m_autoFitterLM) { m_progressMonitor->setAutoFitter(m_autoFitterLM); @@ -1238,17 +806,10 @@ void nmWxAutomaticFitting::startAutoFitting(const QVector>& targ int maxIterations = m_iterationEdit->text().toInt(); double targetError = m_errorLimitEdit->text().toDouble(); - QString wellName = m_targetWellCombo->currentText(); + QString wellName = m_targetWellName; m_progressMonitor->setFittingParameters(maxIterations, targetError, wellName); - // 单位和勾选参数保持同一顺序,供横排参数区显示。 - QStringList selectedUnits; - if(m_kCheckBox->isChecked()) selectedUnits << tr("mD"); - if(m_sCheckBox->isChecked()) selectedUnits << QString(); - if(m_cCheckBox->isChecked()) selectedUnits << tr("m^3/MPa"); - if(m_phiCheckBox->isChecked()) selectedUnits << QString(); - if(m_dfcCheckBox->isChecked()) selectedUnits << tr("md.m"); - if(m_fractureHalfLengthCheckBox->isChecked()) selectedUnits << tr("m"); - m_progressMonitor->setSelectedParameters(selectedParams, selectedUnits); + // 名称和单位均按实际优化向量的顺序展示。 + m_progressMonitor->setSelectedParameters(selectedParams, selectedParameterLabels(true)); m_progressMonitor->show(); QTimer::singleShot(100, this, SLOT(runAutoFitting())); @@ -1408,28 +969,156 @@ void nmWxAutomaticFitting::cleanupFitting() void nmWxAutomaticFitting::refreshParametersFromModel() { - nmDataAnalyzeManager* manager = nmDataAnalyzeManager::getCurrentInstance(); - if(!manager) { - return; - } + // 拟合结束后按各井实际落地的值刷新初值和范围,保留参数勾选。 + nmDataAnalyzeManager* manager = nmDataAnalyzeManager::getCurrentInstance(); + saveModelParameters(); + reservoirData = manager->getReservoirDataCopy(); + initializeModelParameters(false); + onAlgorithmChanged(m_algorithmCombo->currentIndex()); + saveModelParameters(); + setAutomaticFittingValue(); + nmWxParameterProperty::notifyUpdateTable(); +} - // 同步储层副本,避免下一次保存设置时把未拟合的储层属性覆盖成旧值。 - reservoirData = manager->getReservoirDataCopy(); - const bool wasUpdatingRanges = m_updatingParameterRanges; - m_updatingParameterRanges = true; - m_parameterTable->item(0, 3)->setText(QString::number( - reservoirData.getPermeability().getValue().toDouble(), 'g', 10)); - m_parameterTable->item(3, 3)->setText(QString::number( - reservoirData.getPorosity().getValue().toDouble(), 'g', 10)); - - // 成功或失败都从当前目标井读取已落地的参数,不采用优化器尚未写回的最佳候选。 - // 先刷新全部初值,再统一重建范围,避免即时校验读到新旧值混合的表格。 - m_autoParameterRanges = false; - onWellSelected(m_targetWellCombo->currentIndex()); - m_autoParameterRanges = true; - initializeSuggestedParameterRanges(); - m_updatingParameterRanges = wasUpdatingRanges; - - manager->updateAutomaticFittingData(automaticFittingData); - nmWxParameterProperty::notifyUpdateTable(); +void nmWxAutomaticFitting::saveModelParameters() +{ + // 每个叶节点对应一个带井编码的参数,直接收集所有分组。 + QVector& parameters = automaticFittingData.getModelParameters(); + for(int i = 0; i < parameters.size(); ++i) { + nmFitParameter& p = parameters[i]; + QTreeWidgetItem* item = m_parameterItems[i]; + p.selected = !item->isDisabled() && item->checkState(0) == Qt::Checked; + p.initial = item->text(1).toDouble(); + p.lower = item->text(2).toDouble(); + p.upper = item->text(3).toDouble(); + } +} + +void nmWxAutomaticFitting::initializeModelParameters(bool restoreRanges) +{ + // 储层只建一组,各井按井型加入原有参数;不再通过切换页面收集数据。 + const bool updating = m_updatingParameterRanges; + m_updatingParameterRanges = true; + const QVector saved = automaticFittingData.getModelParameters(); + automaticFittingData.getModelParameters().clear(); + m_parameterItems.clear(); + m_parameterTable->clear(); + nmDataAnalyzeManager* manager = nmDataAnalyzeManager::getCurrentInstance(); + QTreeWidgetItem* reservoir = new QTreeWidgetItem(m_parameterTable, QStringList(tr("Reservoir parameters"))); + addParameter(reservoir, 0, nullptr, reservoirData.getPermeability().getValue().toDouble()); + addParameter(reservoir, 3, nullptr, reservoirData.getPorosity().getValue().toDouble()); + QTreeWidgetItem* wellsGroup = new QTreeWidgetItem(m_parameterTable, QStringList(tr("Well parameters"))); + const QVector wells = manager->getEffectiveCalculationWells(); + for(int i = 0; i < wells.size(); ++i) { + nmDataWellBase* well = manager->findWellByCode(wells[i].m_sWellCode); + if(!well) continue; + QTreeWidgetItem* group = new QTreeWidgetItem(wellsGroup, QStringList(well->getWellName())); + const double skin = well->getPerforationCount() > 0 + ? well->getPerforation(0)->getSkin().getValue().toDouble() : 0.0; + addParameter(group, 1, well, skin); + addParameter(group, 2, well, well->getWellboreStorage().getValue().toDouble()); + if(nmDataVerticalFracturedWell* fractured = dynamic_cast(well)) { + addParameter(group, 4, well, fractured->getDfc().getValue().toDouble()); + addParameter(group, 5, well, fractured->getFractureHalfLength().getValue().toDouble()); + } else if(nmDataHorizontalFracturedWell* fractured = dynamic_cast(well)) { + addParameter(group, 4, well, fractured->getDfc().getValue().toDouble()); + addParameter(group, 5, well, fractured->getFractureHalfLength().getValue().toDouble()); + } + } + const bool legacySelected[] = {automaticFittingData.getPermeabilitySelected(), + automaticFittingData.getSkinSelected(), automaticFittingData.getWellboreStorageSelected(), + automaticFittingData.getPorositySelected(), automaticFittingData.getFractureConductivitySelected(), + automaticFittingData.getFractureHalfLengthSelected()}; + nmDataAttribute* legacyMin[] = {&automaticFittingData.getPermeabilityMin(), &automaticFittingData.getSkinMin(), + &automaticFittingData.getWellboreStorageMin(), &automaticFittingData.getPorosityMin(), + &automaticFittingData.getFractureConductivityMin(), &automaticFittingData.getFractureHalfLengthMin()}; + nmDataAttribute* legacyMax[] = {&automaticFittingData.getPermeabilityMax(), &automaticFittingData.getSkinMax(), + &automaticFittingData.getWellboreStorageMax(), &automaticFittingData.getPorosityMax(), + &automaticFittingData.getFractureConductivityMax(), &automaticFittingData.getFractureHalfLengthMax()}; + const QVector& parameters = automaticFittingData.getModelParameters(); + for(int i = 0; i < parameters.size(); ++i) { + const nmFitParameter& p = parameters[i]; + updateRangeForParameter(i, p.initial); + for(int j = 0; j < saved.size(); ++j) { + if(saved[j].type != p.type || saved[j].wellCode != p.wellCode) continue; + m_parameterItems[i]->setCheckState(0, saved[j].selected ? Qt::Checked : Qt::Unchecked); + if(restoreRanges && p.initial >= saved[j].lower && p.initial <= saved[j].upper) + setParameterRange(i, saved[j].lower, saved[j].upper); + break; + } + // 兼容尚无多井列表的敏感性设置,旧井参数仍对应原来记录的目标井。 + if(saved.isEmpty() && m_sensitivitySettings && m_sensitivitySettings->confirmed + && (p.wellCode.isEmpty() || p.wellName == m_sensitivitySettings->targetWell)) { + m_parameterItems[i]->setCheckState(0, legacySelected[p.type] ? Qt::Checked : Qt::Unchecked); + const double lower = legacyMin[p.type]->getValue().toDouble(); + const double upper = legacyMax[p.type]->getValue().toDouble(); + if(restoreRanges && p.initial >= lower && p.initial <= upper) setParameterRange(i, lower, upper); + } + } + // 分组标题加粗并横跨数值列,叶节点仍保留独立勾选和编辑。 + for(QTreeWidgetItemIterator it(m_parameterTable); *it; ++it) { + if((*it)->data(0, Qt::UserRole).isValid()) continue; + QFont font = (*it)->font(0); + font.setBold(true); + (*it)->setFont(0, font); + (*it)->setFirstColumnSpanned(true); + (*it)->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); + } + m_parameterTable->expandAll(); + m_updatingParameterRanges = updating; +} + +void nmWxAutomaticFitting::addParameter(QTreeWidgetItem* group, int type, + nmDataWellBase* well, double initial) +{ + // 名称在井分组内只显示参数本身,数据仍保留所属井编码供计算使用。 + nmFitParameter parameter; + parameter.type = type; + parameter.initial = initial; + const QString name = parameter.name(); + if(well) { + parameter.wellCode = well->getWellCode(); + parameter.wellName = well->getWellName(); + } + const int index = automaticFittingData.getModelParameters().size(); + automaticFittingData.getModelParameters().append(parameter); + QTreeWidgetItem* item = new QTreeWidgetItem(group); + item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEditable); + item->setData(0, Qt::UserRole, index); + item->setText(0, name); + item->setText(1, QString::number(initial, 'g', 10)); + item->setText(4, parameter.unit()); + item->setCheckState(0, !m_sensitivitySettings + && (!well || well->getWellCode() == nmDataAnalyzeManager::getCurrentInstance()->getPrimaryWellCode()) + ? Qt::Checked : Qt::Unchecked); + m_parameterItems.append(item); +} + +QVector nmWxAutomaticFitting::primaryInitialValues() const +{ + // 旧六参数接口仅提取主井与共享储层,其他井不会覆盖主井初值。 + QVector values(6, 0.0); + const QString primaryCode = nmDataAnalyzeManager::getCurrentInstance()->getPrimaryWellCode(); + const QVector& parameters = automaticFittingData.getModelParameters(); + for(int i = 0; i < parameters.size(); ++i) + if(parameters[i].wellCode.isEmpty() || parameters[i].wellCode == primaryCode) + values[parameters[i].type] = parameters[i].initial; + return values; +} + +QStringList nmWxAutomaticFitting::selectedParameterLabels(bool units) const +{ + // LM 按完整向量顺序显示,PSO 沿用旧六参数类型顺序。 + QStringList labels; + const QVector& parameters = automaticFittingData.getModelParameters(); + if(m_algorithmCombo->currentIndex() == 1) { + for(int i = 0; i < parameters.size(); ++i) + if(parameters[i].selected) labels << (units ? parameters[i].unit() : parameters[i].name()); + } else { + for(int type = 0; type < 6; ++type) + for(int i = 0; i < parameters.size(); ++i) + if(parameters[i].selected && parameters[i].type == type) + labels << (units ? parameters[i].unit() : parameters[i].name()); + } + return labels; } diff --git a/Src/nmNum/nmSubWxs/nmWxParaPropertyPebi.cpp b/Src/nmNum/nmSubWxs/nmWxParaPropertyPebi.cpp index c1c3744e..a14ece49 100644 --- a/Src/nmNum/nmSubWxs/nmWxParaPropertyPebi.cpp +++ b/Src/nmNum/nmSubWxs/nmWxParaPropertyPebi.cpp @@ -151,16 +151,27 @@ void appendSensitivityParas(nmDataAnalyzeManager* manager, QStringList& paras, if(!batch->filteredParameterBounds(i, minimum, maximum)) continue; const QString lower = "Sensitivity_" + resultNames[index] + "_Lower"; const QString higher = "Sensitivity_" + resultNames[index] + "_Higher"; - appendResultPara(paras, values, groupCode, lower, minimum); - appendResultPara(paras, values, groupCode, higher, maximum); + // 多井结果复用 XML 分组机制,同名参数用所属井的分组编码区分。 + const QString parameterGroup = variable.m_wellCode.isEmpty() ? groupCode : groupCode + "_" + variable.m_wellCode; + if(parameterGroup != groupCode) groups[parameterGroup] = groups[groupCode] + " - " + variable.m_varGroup; + appendResultPara(paras, values, parameterGroup, lower, minimum); + appendResultPara(paras, values, parameterGroup, higher, maximum); } // 第四种在采样范围后追加各组拟合最优值的范围,沿用同一阈值和 XML 分组。 for(int i = 0; i < batch->m_fitParameterCodes.size(); ++i) { double minimum = 0.0, maximum = 0.0; if(!batch->filteredParameterBounds(i, minimum, maximum, true)) continue; - const QString name = "Sensitivity_Fit_" + batch->m_fitParameterCodes[i]; - appendResultPara(paras, values, groupCode, name + "_Minimum", minimum); - appendResultPara(paras, values, groupCode, name + "_Maximum", maximum); + const QString code = batch->m_fitParameterCodes[i]; + const int separator = code.indexOf('/'); + const QString wellCode = separator < 0 ? QString() : code.left(separator); + const QString parameterGroup = wellCode.isEmpty() ? groupCode : groupCode + "_" + wellCode; + if(parameterGroup != groupCode) { + nmDataWellBase* well = manager->findWellByCode(wellCode); + groups[parameterGroup] = groups[groupCode] + " - " + (well ? well->getWellName() : wellCode); + } + const QString name = "Sensitivity_Fit_" + code.mid(separator + 1); + appendResultPara(paras, values, parameterGroup, name + "_Minimum", minimum); + appendResultPara(paras, values, parameterGroup, name + "_Maximum", maximum); } } diff --git a/Src/nmNum/nmSubWxs/nmWxSensitive.cpp b/Src/nmNum/nmSubWxs/nmWxSensitive.cpp index f5867f04..2740891a 100644 --- a/Src/nmNum/nmSubWxs/nmWxSensitive.cpp +++ b/Src/nmNum/nmSubWxs/nmWxSensitive.cpp @@ -206,7 +206,7 @@ QString sensitivityRunParameters(const nmDataSensitive::BatchResult& results, co double value = run.m_vecParameters[i]; const QString unit = variable.m_modelValue.getUnit(); convertSamplingValue(variable.m_varName, value, samplingBaseUnit(variable.m_varName), unit, value); - descriptions << variable.m_varName + " = " + formatSamplingValue(value, 3) + descriptions << variable.displayName() + " = " + formatSamplingValue(value, 3) + (unit.isEmpty() ? QString() : " " + unit); } return descriptions.join(" + "); @@ -634,7 +634,7 @@ nmWxSensitivityScatter::nmWxSensitivityScatter( options->addWidget(new QLabel(tr("X Axis:"), this)); m_parameter = new QComboBox(this); for(int i = 0; i < results->m_variables.size(); ++i) - if(results->m_variables[i].m_enabled) m_parameter->addItem(results->m_variables[i].m_varName, i); + if(results->m_variables[i].m_enabled) m_parameter->addItem(results->m_variables[i].displayName(), i); options->addWidget(m_parameter, 1); options->addSpacing(20); options->addWidget(new QLabel(tr("Y Axis:"), this)); @@ -692,7 +692,7 @@ void nmWxSensitivityScatter::updateParameter() const int index = m_parameter->itemData(m_parameter->currentIndex()).toInt(); const nmDataSensitive::VariableSampling& variable = m_results->m_variables[index]; const QString unit = variable.m_modelValue.getUnit(); - m_plot->setAxisX(variable.m_varName, unit); + m_plot->setAxisX(variable.displayName(), unit); double modelValue = variable.m_modelValue.getValue().toDouble(); const bool hasModel = convertSamplingValue(variable.m_varName, modelValue, samplingBaseUnit(variable.m_varName), unit, modelValue) && qIsFinite(modelY) && (!errorAxis || modelY >= 0.0); @@ -1254,7 +1254,7 @@ private: nmDataSensitive::VariableSampling& variable = m_data.getVariable(index); const QString unit = variable.getModelValue().getUnit(); convertSamplingValue(variable.getVarName(), value, samplingBaseUnit(variable.getVarName()), unit, value); - return variable.getVarName() + " = " + formatSamplingValue(value, 3) + return variable.displayName() + " = " + formatSamplingValue(value, 3) + (unit.isEmpty() ? QString() : " " + unit); } @@ -1391,12 +1391,26 @@ void nmWxSensitive::closeEvent(QCloseEvent* event) void nmWxSensitive::refreshModelValues() { // 每次打开或模型更新时读取当前参数,沿用已有单位和物理范围限制。 - nmDataWellBase* well = m_pDataManager - ? m_pDataManager->findWellByCode(m_pDataManager->getPrimaryWellCode()) : nullptr; nmDataReservoir* reservoir = m_pDataManager ? m_pDataManager->getReservoirData() : nullptr; + const QVector wells = m_pDataManager + ? m_pDataManager->getEffectiveCalculationWells() : QVector(); + // 旧版主井配置补上井编码;已不参与模型的井不再生成样本。 + for(int i = 0; i < m_sensitiveData.getVariableCount(); ++i) { + nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i); + if(variable.m_varName == "Skin" || variable.m_varName == "C") { + if(variable.m_wellCode.isEmpty() && m_pDataManager) + variable.m_wellCode = m_pDataManager->getPrimaryWellCode(); + if(!m_pDataManager || !m_pDataManager->isWellSelectedForCalculation(variable.m_wellCode)) + variable.m_enabled = false; + } + } bool modelChanged = false; const QStringList names = QStringList() << "Skin" << "C" << "Pi" << "k" << "h" << QString(QChar(0x03c6)); - for (int n = 0; n < names.size(); ++n) { + for (int entry = 0; entry < wells.size() * 2 + 4; ++entry) { + const bool wellParameter = entry < wells.size() * 2; + const int n = wellParameter ? entry % 2 : entry - wells.size() * 2 + 2; + const QString wellCode = wellParameter ? wells[entry / 2].m_sWellCode : QString(); + nmDataWellBase* well = wellParameter ? m_pDataManager->findWellByCode(wellCode) : nullptr; nmDataAttribute* attribute = nullptr; const QString name = names[n]; if (well && name == "Skin" && well->getPerforationCount() > 0) @@ -1409,17 +1423,19 @@ void nmWxSensitive::refreshModelValues() int index = -1; for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) - if (m_sensitiveData.getVariable(i).getVarName() == name) { index = i; break; } + if (m_sensitiveData.getVariable(i).getVarName() == name && + m_sensitiveData.getVariable(i).m_wellCode == wellCode) { index = i; break; } const bool fresh = index < 0; if (fresh) { nmDataSensitive::VariableSampling variable; variable.setVarName(name); - variable.setVarGroup(n < 2 ? "Tested Well" : "Reservoir"); + variable.m_wellCode = wellCode; variable.setNumber(5); m_sensitiveData.getVariables().append(variable); index = m_sensitiveData.getVariableCount() - 1; } nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(index); + variable.setVarGroup(well ? well->getWellName() : "Reservoir"); double physicalMin = 0.0, physicalMax = 0.0; if (!attribute || !samplingPhysicalRange(name, physicalMin, physicalMax)) { variable.setEnabled(false); @@ -1528,7 +1544,7 @@ void nmWxSensitive::loadDataToUI(bool refreshRangesOnly) 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); + const int idx = findVariableIndex(variable); if (idx < 0) continue; const bool available = isSupportedSamplingVariable(name) && variable.getModelValue().getValue().isValid(); @@ -1536,7 +1552,7 @@ void nmWxSensitive::loadDataToUI(bool refreshRangesOnly) QTreeWidgetItem* parent = variablesTree->topLevelItem(group); for (int child = 0; child < parent->childCount(); ++child) { QTreeWidgetItem* item = parent->child(child); - if (item->text(0) != name) continue; + if (item->text(0) != name || item->data(0, Qt::UserRole + 1).toString() != variable.m_wellCode) continue; item->setDisabled(!available); if (!refreshRangesOnly) item->setCheckState(0, available && variable.getEnabled() ? Qt::Checked : Qt::Unchecked); @@ -1594,15 +1610,19 @@ void nmWxSensitive::loadDataToUI(bool refreshRangesOnly) } } -int nmWxSensitive::findVariableIndex(const QString& varName) +int nmWxSensitive::findVariableIndex(const nmDataSensitive::VariableSampling& variable) { - // 根据创建顺序查找变量索引 - 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); + // 页面由树节点创建,通过井编码与参数名定位,不依赖固定页面次序。 + for(int group = 0; group < variablesTree->topLevelItemCount(); ++group) { + QTreeWidgetItem* parent = variablesTree->topLevelItem(group); + for(int child = 0; child < parent->childCount(); ++child) { + QTreeWidgetItem* item = parent->child(child); + if(item->text(0) == variable.m_varName && + item->data(0, Qt::UserRole + 1).toString() == variable.m_wellCode) + return item->data(0, Qt::UserRole).toInt() - 1; + } + } + return -1; } void nmWxSensitive::updateModelCount() @@ -1618,9 +1638,10 @@ void nmWxSensitive::updateModelCount() 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; + if (variable.getVarName() != item->text(0) || + variable.m_wellCode != item->data(0, Qt::UserRole + 1).toString()) continue; variable.setEnabled(!item->isDisabled() && item->checkState(0) == Qt::Checked); - const int idx = findVariableIndex(variable.getVarName()); + const int idx = findVariableIndex(variable); if (idx >= 0) variable.setNumber(m_numSpins[idx]->value()); } } @@ -1637,10 +1658,21 @@ void nmWxSensitive::updateModelCount() // 第四种确认设置且至少选择一个拟合参数后才开放生成,空选择直接禁用。 bool hasFittingParameters = false; if(!m_sensitiveData.getImproveSettings().isNull()) { - const nmDataAutomaticFitting& fitting = m_sensitiveData.getImproveSettings()->parameters; - hasFittingParameters = fitting.getPermeabilitySelected() || fitting.getSkinSelected() || - fitting.getWellboreStorageSelected() || fitting.getPorositySelected() || - fitting.getFractureConductivitySelected() || fitting.getFractureHalfLengthSelected(); + nmSensitivityFitSettings settings = *m_sensitiveData.getImproveSettings(); + QVector& parameters = settings.parameters.getModelParameters(); + // 新勾选的采样只取消同井同参数的拟合,其他井的同名参数仍可参与。 + for(int i = 0; i < parameters.size(); ++i) { + if(!parameters[i].wellCode.isEmpty() && + !m_pDataManager->isWellSelectedForCalculation(parameters[i].wellCode)) + parameters[i].selected = false; + for(int v = 0; v < m_sensitiveData.getVariableCount(); ++v) { + const nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(v); + if(variable.m_enabled && variable.parameterKey() == parameters[i].samplingKey()) + parameters[i].selected = false; + } + } + hasFittingParameters = settings.confirmed && settings.parameters.hasSelectedParameters(); + m_sensitiveData.setImproveSettings(QSharedPointer(new nmSensitivityFitSettings(settings))); } generateButton->setEnabled(m_pDataManager && !overflow && count > 0 && !m_calculationRunning && (calculationTypeCombo->currentIndex() != 3 || hasFittingParameters)); @@ -1655,7 +1687,7 @@ bool nmWxSensitive::saveDataFromUI() for (int i = 0; i < candidate.getVariableCount(); ++i) { nmDataSensitive::VariableSampling& variable = candidate.getVariable(i); const QString name = variable.getVarName(); - const int idx = findVariableIndex(name); + const int idx = findVariableIndex(variable); if (idx < 0 || !isSupportedSamplingVariable(name)) continue; const QString unit = m_unitLabels[idx]->text(); const QString baseUnit = samplingBaseUnit(name); @@ -1898,22 +1930,25 @@ 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"; - + // 只列出实际参与模型的井,每口井复用现有的参数及采样页。 + const QVector wells = m_pDataManager + ? m_pDataManager->getEffectiveCalculationWells() : QVector(); 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))); + for(int w = 0; w < wells.size(); ++w) { + nmDataWellBase* well = m_pDataManager->findWellByCode(wells[w].m_sWellCode); + if(!well) continue; + QTreeWidgetItem* testedWellItem = new QTreeWidgetItem(variablesTree); + testedWellItem->setText(0, wells.size() == 1 ? tr("Tested Well") : well->getWellName()); + testedWellItem->setCheckState(0, Qt::Unchecked); + QStringList children; + children << "Zw" << "Hw" << "Lw" << "Skin" << "C"; + for(i = 0; i < children.size(); ++i) { + QTreeWidgetItem* child = new QTreeWidgetItem(testedWellItem); + child->setText(0, children[i]); + child->setData(0, Qt::UserRole + 1, well->getWellCode()); + child->setCheckState(0, Qt::Unchecked); + child->setDisabled(!isSupportedSamplingVariable(children[i])); + } } // ===== Reservoir ===== @@ -2296,7 +2331,7 @@ void nmWxSensitive::updateValuesForPage(int idx) table->setRowCount(number); nmDataSensitive::VariableSampling variable; for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) { - if (findVariableIndex(m_sensitiveData.getVariable(i).getVarName()) == idx) { + if (findVariableIndex(m_sensitiveData.getVariable(i)) == idx) { variable = m_sensitiveData.getVariable(i); break; } @@ -2375,7 +2410,7 @@ void nmWxSensitive::updateDistributionPage(int idx) } for(int i = 0; i < m_sensitiveData.getVariableCount(); ++i) { nmDataSensitive::VariableSampling variable = m_sensitiveData.getVariable(i); - if(findVariableIndex(variable.m_varName) != idx) continue; + if(findVariableIndex(variable) != idx) continue; const bool valid = readDistributionPage(idx, variable); m_distributionPlots[idx]->setVariable(variable, valid); break; @@ -2393,8 +2428,9 @@ void nmWxSensitive::onSamplingEditingFinished() 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; + const nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i); + const QString name = variable.getVarName(); + if (findVariableIndex(variable) != idx) continue; double lower = 0.0, upper = 0.0; const QString unit = m_unitLabels[idx]->text(); const QString baseUnit = samplingBaseUnit(name); @@ -2634,7 +2670,7 @@ void nmWxSensitive::onImproveSettingsClicked() updateModelCount(); QStringList sampledParameters; foreach(const nmDataSensitive::VariableSampling& variable, m_sensitiveData.getVariables()) - if(variable.m_enabled) sampledParameters.append(variable.m_varName); + if(variable.m_enabled) sampledParameters.append(variable.parameterKey()); nmWxAutomaticFitting dialog(this, &settings, sampledParameters); if(dialog.exec() == QDialog::Accepted) { m_sensitiveData.setImproveSettings(QSharedPointer(new nmSensitivityFitSettings(settings)));