#include "nmWxAutomaticFitting.h" #include "nmCalculationAutoFitPSO.h" #include "nmCalculationAutoFitLM.h" #include "nmWxAutomaticfittingStart.h" #include "nmWxParameterProperty.h" #include "nmAttrRegistry.h" #include "nmDataAnalyzeManager.h" #include "iSubWndFitting.h" #include "iSysParaHelper.h" #include "iParameter.h" #include "mModuleDefines.h" #include "mGui/mGuiAnal/iAnalRun.h" #include "mAlgDefines.h" #pragma comment(lib, "mGuiAnal.lib") #include #include #include #include #include #include #ifdef Q_OS_WIN #include #define DEBUG_UI(msg) OutputDebugStringA(QString("[UI] %1\n").arg(msg).toLocal8Bit().data()) #endif 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 return _finite(value) != 0; #else return std::isfinite(value); #endif } // 物理边界可能由多个浮点配置量计算得到,界面十进制文本再转回 double 后会有 // 末位舍入差。这里只放宽约 8 个机器精度,不放宽实际物理范围。 bool nmAutoFitUiNearlyEqual(double left, double right) { const double scale = qMax(std::fabs(left), std::fabs(right)); return std::fabs(left - right) <= DBL_EPSILON * 8.0 * scale; } // 从系统参数表读取物理边界,所有自动拟合参数统一以参数表配置为准。 bool nmAutoFitReadPhysicalRange(const char* parameterName, double& minValue, double& maxValue) { iSysParaHelper* paraHelper = _paraHelper; if(!paraHelper) { return false; } iParameter* parameter = paraHelper->getPara(QString::fromLatin1(parameterName), s_Nm_Serie); if(!parameter || !nmAutoFitUiIsFinite(parameter->m_dMin) || !nmAutoFitUiIsFinite(parameter->m_dMax) || parameter->m_dMin >= parameter->m_dMax) { return false; } minValue = parameter->m_dMin; maxValue = parameter->m_dMax; return true; } } // 获取可拟合参数的系统物理边界,索引 4 不再作为拟合参数。 bool nmWxAutomaticFitting::getPhysicalParameterRange(int parameterIndex, double& minValue, double& maxValue) { // 参数行按实际类型查询边界,不依赖它位于哪口井的分组。 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) { // 自动范围写入时不触发尚未完成的行校验。 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, // 上界按初值加固定宽度,其余正值参数使用倍率范围;每次打开窗口和拟合完成后复用。 void nmWxAutomaticFitting::updateRangeForParameter(int parameterIndex, double centerValue) { if(!m_parameterTable || parameterIndex < 0 || parameterIndex >= m_parameterItems.size() || !nmAutoFitUiIsFinite(centerValue)) { return; } double physicalMin = 0.0; double physicalMax = 0.0; if(!getPhysicalParameterRange(parameterIndex, physicalMin, physicalMax)) { return; } double reference = centerValue; const int type = automaticFittingData.getModelParameters()[parameterIndex].type; const bool positiveParameter = type != 1; // 正值参数初值为 0 时不再借用旧的界面范围,直接从物理边界选取搜索尺度。 if(positiveParameter && reference <= 0.0) { if(physicalMin > 0.0) { reference = physicalMin; } else { reference = qMax(physicalMax * 0.01, 1.0e-12); } } if(physicalMax < physicalMin) { return; } reference = qBound(physicalMin, reference, physicalMax); const double boundedCenterValue = qBound(physicalMin, centerValue, physicalMax); double newMin = physicalMin; double newMax = physicalMax; if(type == 1) { const double skinHalfRange = 10.0; newMin = physicalMin; newMax = qMin(physicalMax, reference + skinHalfRange); } else if(reference > 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(type == 4) { // Dfc=0 表示无限导流,不存在以零为中心的连续倍率范围。 newMin = physicalMin; newMax = physicalMax; } // 任何自动范围都必须包含本次使用的中心值,并且不能越过物理边界。 newMin = qMin(newMin, boundedCenterValue); newMax = qMax(newMax, boundedCenterValue); newMin = qMax(newMin, physicalMin); newMax = qMin(newMax, physicalMax); if(newMax >= newMin) { setParameterRange(parameterIndex, newMin, newMax); } } // 校验参数范围;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 >= m_parameterItems.size()) { errorMessage = tr("The parameter row is invalid."); return false; } const int firstParameterIndex = parameterIndex < 0 ? 0 : parameterIndex; const int lastParameterIndex = parameterIndex < 0 ? m_parameterItems.size() : parameterIndex + 1; for(int currentParameterIndex = firstParameterIndex; currentParameterIndex < lastParameterIndex; ++currentParameterIndex) { 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 = 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(parameter.name()); return false; } double physicalMin = 0.0; double physicalMax = 0.0; if(!getPhysicalParameterRange(currentParameterIndex, physicalMin, physicalMax) || !nmAutoFitUiIsFinite(physicalMin) || !nmAutoFitUiIsFinite(physicalMax) || physicalMax < physicalMin) { errorMessage = tr("The physical range of %1 is invalid.") .arg(parameter.name()); return false; } if((minValue < physicalMin && !nmAutoFitUiNearlyEqual(minValue, physicalMin)) || (minValue > physicalMax && !nmAutoFitUiNearlyEqual(minValue, physicalMax)) || (initialValue < physicalMin && !nmAutoFitUiNearlyEqual(initialValue, physicalMin)) || (initialValue > physicalMax && !nmAutoFitUiNearlyEqual(initialValue, physicalMax)) || (maxValue < physicalMin && !nmAutoFitUiNearlyEqual(maxValue, physicalMin)) || (maxValue > physicalMax && !nmAutoFitUiNearlyEqual(maxValue, physicalMax))) { errorMessage = tr("The values of %1 exceed the physical range [%2, %3].") .arg(parameter.name()) .arg(QString::number(physicalMin, 'g', 10)) .arg(QString::number(physicalMax, 'g', 10)); return false; } if(minValue > maxValue || minValue > initialValue || initialValue > maxValue) { errorMessage = tr("The values of %1 must satisfy: minimum <= initial value <= maximum.") .arg(parameter.name()); return false; } // 底层用 Dfc=0 表示无限导流,这是离散模型选项,不属于有限导流拟合域。 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(parameter.name()); return false; } } return true; } nmWxAutomaticFitting::nmWxAutomaticFitting(QWidget *parent, nmSensitivityFitSettings* settings, const QStringList& sampledParameters) : iDlgBase(parent) , m_sensitivitySettings(settings) , m_sampledParameters(sampledParameters) , m_autoFitterPSO(nullptr) , m_autoFitterLM(nullptr) , m_progressDialog(nullptr) , m_progressTimer(nullptr) , m_progressMonitor(nullptr) // 构造期间先禁止即时校验,避免初始值写入和范围生成之间出现短暂的不一致。 , m_updatingParameterRanges(true) { DEBUG_UI(QString("AutoFitting Constructor: this=0x%1").arg((quintptr)this, 0, 16)); // 获取数据 nmDataAnalyzeManager* pManager = nmDataAnalyzeManager::getCurrentInstance(); reservoirData = pManager->getReservoirDataCopy(); automaticFittingData = pManager->getAutomaticFittingDataCopy(); 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(); // 敏感性入口使用“确定”保存设置,普通入口仍直接开始拟合。 if(m_sensitivitySettings) m_okBtn->setText(tr("OK")); setWindowTitle(tr("Automatic fitting")); setModal(true); // 初始尺寸随所在屏幕变化,最小尺寸交给实际控件布局决定。 resize(sizeHint()); initializeModelParameters(); onAlgorithmChanged(m_algorithmCombo->currentIndex()); m_updatingParameterRanges = false; DEBUG_UI("AutoFitting Constructor completed"); } QSize nmWxAutomaticFitting::sizeHint() const { // 沿用项目的可用屏幕尺寸计算方式,优先取父窗口所在屏幕,支持多显示器。 QDesktopWidget* desktop = QApplication::desktop(); if(desktop == nullptr) return iDlgBase::sizeHint(); const QRect available = desktop->availableGeometry(parentWidget() ? parentWidget()->window() : this); return QSize(available.width() * 45 / 100, available.height() * 55 / 100); } nmWxAutomaticFitting::~nmWxAutomaticFitting() { DEBUG_UI(QString("AutoFitting Destructor: this=0x%1").arg((quintptr)this, 0, 16)); DEBUG_UI("AutoFitting destructor - starting cleanup"); // 停止定时器 if (m_progressTimer) { m_progressTimer->stop(); } // 断开所有连接,避免悬空指针 if (m_autoFitterPSO) { disconnect(m_autoFitterPSO, nullptr, this, nullptr); } if (m_autoFitterLM) { disconnect(m_autoFitterLM, nullptr, this, nullptr); } DEBUG_UI("AutoFitting destructor - completed"); } void nmWxAutomaticFitting::setupUI() { // 用浅底色区分白色设置区和参数区,避免所有内容铺在同一平面。 setObjectName("autoFittingSettings"); setFont(QFont("Microsoft YaHei", 9)); setStyleSheet( "#autoFittingSettings { background: #f3f5f8; color: #273449; }" "#autoFittingSettings QFrame#fittingControls, #autoFittingSettings QFrame#fittingParameters { background: #ffffff; border: 1px solid #dce2ea; border-radius: 7px; }" "#autoFittingSettings QLabel { color: #667487; background: transparent; border: none; }" "#autoFittingSettings QLabel#parameterHeading { color: #273449; font-weight: bold; }" "#autoFittingSettings QLineEdit, #autoFittingSettings QComboBox { background: #ffffff; color: #273449; border: 1px solid #cfd7e2; border-radius: 4px; padding: 5px 8px; min-height: 20px; selection-background-color: #dceafb; selection-color: #273449; }" "#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 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 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; }" "#autoFittingSettings QPushButton#startFitting { background: #3978c6; color: #ffffff; border-color: #3978c6; }" "#autoFittingSettings QPushButton#startFitting:hover { background: #306bb5; }" "#autoFittingSettings QPushButton#startFitting:pressed { background: #285b9c; }"); m_mainLayout = new QVBoxLayout(this); m_mainLayout->setContentsMargins(16, 16, 16, 14); m_mainLayout->setSpacing(12); setupControlPanel(); QFrame* controlsPanel = new QFrame(this); controlsPanel->setObjectName("fittingControls"); m_controlLayout->setContentsMargins(16, 14, 16, 14); controlsPanel->setLayout(m_controlLayout); m_mainLayout->addWidget(controlsPanel); setupParameterTable(); QFrame* tablePanel = new QFrame(this); tablePanel->setObjectName("fittingParameters"); QVBoxLayout* tableLayout = new QVBoxLayout(tablePanel); tableLayout->setContentsMargins(12, 12, 12, 8); tableLayout->setSpacing(10); QLabel* tableHeading = new QLabel(tr("Fitting parameters"), tablePanel); tableHeading->setObjectName("parameterHeading"); tableLayout->addWidget(tableHeading); tableLayout->addWidget(m_parameterTable, 1); m_mainLayout->addWidget(tablePanel, 1); setupButtons(); setLayout(m_mainLayout); } void nmWxAutomaticFitting::setupParameterTable() { // 用可折叠分组直接展示各井参数,数值列沿用原来的编辑方式。 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); // 算法选择 QLabel* algorithmLabel = new QLabel(tr("Algorithm:")); m_algorithmCombo = new QComboBox(); m_algorithmCombo->addItem(tr("PSO (Particle Swarm)")); m_algorithmCombo->addItem(tr("LM")); // 从敏感性分析打开时仅使用 LM,普通自动拟合仍可选择算法。 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))); // 迭代次数 m_iterationLabel = new QLabel(); m_iterationEdit = new QLineEdit(QString::number(automaticFittingData.getIterationCount().getValue().toInt())); // 误差上限 QLabel* errorLabel = new QLabel(tr("Target error:")); m_errorLimitEdit = new QLineEdit(QString::number(automaticFittingData.getErrorTolerance().getValue().toDouble())); 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;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_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_okBtn->setMinimumWidth(80); m_cancelBtn->setMinimumWidth(80); buttonLayout->addWidget(m_selectAllCheckBox); buttonLayout->addStretch(); buttonLayout->addWidget(m_cancelBtn); buttonLayout->addWidget(m_okBtn); m_mainLayout->addLayout(buttonLayout); // 连接信号槽 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::onSelectAll(bool checked) { // 批量切换可用参数,完成后统一同步全选框,避免逐项更新时改变点击状态。 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::updateSelectAllState() { // 仅统计可用参数;单项勾选和算法切换后,全选框都与当前列表保持一致。 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(QTreeWidgetItem* item, int column) { // 勾选时同步全选框,数值编辑仍沿用原来的范围校验。 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() { QString validationError; if(!validateParameterTable(validationError)) { QMessageBox::warning(this, tr("Invalid parameter range"), validationError); return; } // 校验通过后再保存参数设置,非法输入不会被静默裁剪。 saveModelParameters(); setAutomaticFittingValue(); if(m_sensitivitySettings) { // 仅提交给敏感性窗口;取消、关闭及普通拟合均不经过此分支。 m_sensitivitySettings->parameters = automaticFittingData; m_sensitivitySettings->targetWell = m_targetWellName; m_sensitivitySettings->initialValues = primaryInitialValues(); m_sensitivitySettings->confirmed = true; accept(); return; } // 更新完成后,通知参数界面刷新 nmWxParameterProperty::notifyUpdateTable(); // 获取目标井的双对数历史数据 QString selectedWellName = m_targetWellName; if(selectedWellName.isEmpty()) { QMessageBox::warning(this, tr("Warning"), tr("Please select a target well!")); return; } nmDataAnalyzeManager* pManager = nmDataAnalyzeManager::getCurrentInstance(); if(!pManager) { QMessageBox::warning(this, tr("Warning"), tr("Data manager is unavailable!")); return; } // 从数据管理器获取最新的井对象 nmDataWellBase* pTargetWell = pManager->findWellByName(selectedWellName); if(!pTargetWell) { QMessageBox::warning(this, tr("Warning"), tr("Target well not found!")); return; } const bool usePseudoPressure = (pManager->getSolverModelType() == SMT_Gas_VariablePvt); if(usePseudoPressure) { iSubWndFitting* pFitting = nmDataAnalyzeManager::getCurrentFitting(); iAnalRun* pAnalRun = pFitting ? pFitting->getAnalRun() : nullptr; // 气井求解结果的后处理仍需初始化当前分析的拟压力转换器。 if(!pAnalRun || !pAnalRun->configPsAbouts(true, pFitting->getModelOption(), false, pFitting->getAllWxPtr())) { QMessageBox::warning(this, tr("Warning"), tr("Gas pseudo-pressure data is unavailable or invalid.")); return; } } // 开始拟合前重算气井目标,避免旧缓存或已修改的拟压力配置造成量纲不一致。 if(usePseudoPressure && !pManager->updateWellHistoryData(pTargetWell)) { QMessageBox::warning(this, tr("Warning"), tr("Gas pseudo-pressure data is unavailable or invalid.")); return; } // 油、气、水井统一读取所选目标井的历史曲线,不依赖主界面显示的结果井。 QVector > targetLogLogData = pTargetWell->getHistoryLogLog(); if(targetLogLogData.isEmpty() || targetLogLogData.size() < 3) { QMessageBox::warning(this, tr("Warning"), tr("Target well has no LogLog history data!")); return; } // 检查双对数数据的一致性 if(targetLogLogData[0].size() != targetLogLogData[1].size() || targetLogLogData[0].size() != targetLogLogData[2].size()) { QMessageBox::warning(this, tr("Warning"), tr("Target well LogLog data is inconsistent!")); return; } // 检查结果双对数数据 QVector > rstLogLogData = pTargetWell->getResultLogLog(); if(rstLogLogData.isEmpty() || rstLogLogData.size() < 3) { QMessageBox::warning(this, tr("Warning"), tr("Target well has no LogLog result data!")); return; } // 检查是否有参数被选中 bool hasSelectedParams = automaticFittingData.hasSelectedParameters(); if(!hasSelectedParams) { QMessageBox::warning(this, tr("Warning"), tr("Please select at least one parameter for optimization!")); return; } const QStringList selectedParameterNames = selectedParameterLabels(false); // 启动自动拟合 - 传递双对数历史数据 startAutoFitting(targetLogLogData, selectedParameterNames, selectedWellName); } void nmWxAutomaticFitting::onReject() { reject(); } void nmWxAutomaticFitting::setAutomaticFittingValue() { // 单井旧字段继续供 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(initial[0]); // 渗透率 reservoirData.getPorosity().setValue(initial[3]); // 孔隙度 // 更新储层数据(全局) nmDataAnalyzeManager::getCurrentInstance()->updateReservoirData(reservoirData); // 保存自动拟合数据 nmDataAutomaticFitting psoSettings = automaticFittingData; psoSettings.getModelParameters().clear(); nmDataAnalyzeManager::getCurrentInstance()->updateAutomaticFittingData(psoSettings); // 只更新目标井的参数 QString selectedWellName = m_targetWellName; if(!selectedWellName.isEmpty()) { double newSkinValue = initial[1]; double newWellboreStorageValue = initial[2]; double newFractureConductivityValue = initial[4]; double newFractureHalfLengthValue = initial[5]; // 直接从数据管理器获取目标井 nmDataAnalyzeManager* manager = nmDataAnalyzeManager::getCurrentInstance(); nmDataWellBase* pTargetWell = manager->findWellByName(selectedWellName); if(pTargetWell) { // 先修改井参数副本,最后写回原对象中的现有属性,保持射孔对象地址不变。 nmDataPerforation* perf = pTargetWell->getPerforation(0); if(perf) { nmDataAttribute skinAttr = perf->getSkin(); skinAttr.setValue(newSkinValue); perf->getSkin().setValue(skinAttr.getValue()); } // 更新井筒储集系数 nmDataAttribute wellboreAttr = pTargetWell->getWellboreStorage(); wellboreAttr.setValue(newWellboreStorageValue); pTargetWell->getWellboreStorage().setValue(wellboreAttr.getValue()); NM_WELL_MODEL wellType = pTargetWell->getWellType(); // Dfc 属于压裂井对象;只修改这一项,裂缝位置、长度和段数保持原值。 if(wellType == NM_WELL_MODEL::Vertical_Fractured_Well) { nmDataVerticalFracturedWell* fracturedWell = dynamic_cast(pTargetWell); if(fracturedWell) { nmDataAttribute dfc = fracturedWell->getDfc(); dfc.setValue(newFractureConductivityValue); fracturedWell->getDfc().setValue(dfc.getValue()); nmDataAttribute halfLength = fracturedWell->getFractureHalfLength(); halfLength.setValue(newFractureHalfLengthValue); fracturedWell->getFractureHalfLength().setValue(halfLength.getValue()); } } else if(wellType == NM_WELL_MODEL::Horizontal_Fractured_Well) { nmDataHorizontalFracturedWell* fracturedWell = dynamic_cast(pTargetWell); if(fracturedWell) { nmDataAttribute dfc = fracturedWell->getDfc(); dfc.setValue(newFractureConductivityValue); fracturedWell->getDfc().setValue(dfc.getValue()); nmDataAttribute halfLength = fracturedWell->getFractureHalfLength(); halfLength.setValue(newFractureHalfLengthValue); fracturedWell->getFractureHalfLength().setValue(halfLength.getValue()); } } // 全部井参数落地后统一刷新一次右侧参数面板。 if(manager->getAttrRegistry()) { manager->getAttrRegistry()->refreshAll(); } } } } void nmWxAutomaticFitting::startAutoFitting(const QVector>& targetData, const QStringList& selectedParams, const QString& targetWellName) { DEBUG_UI("=== START AUTO FITTING ==="); // 先清理之前的实例 cleanupFitting(); const bool useLM = m_algorithmCombo && m_algorithmCombo->currentIndex() == 1; if(useLM) { DEBUG_UI("Creating LM auto fitter"); m_autoFitterLM = new nmCalculationAutoFitLM(this); m_autoFitterLM->setTargetLogLogData(targetData); m_autoFitterLM->setTargetWellName(targetWellName); } else { DEBUG_UI("Creating PSO auto fitter"); m_autoFitterPSO = new nmCalculationAutoFitPSO(this); m_autoFitterPSO->setTargetLogLogData(targetData); m_autoFitterPSO->setPSOTargetWellName(targetWellName); } m_progressMonitor = new nmWxAutomaticfittingStart(this); if(m_autoFitterLM) { m_progressMonitor->setAutoFitter(m_autoFitterLM); } else { m_progressMonitor->setAutoFitter(m_autoFitterPSO); } m_progressMonitor->setPseudoPressureMode( nmDataAnalyzeManager::getCurrentInstance()->getSolverModelType() == SMT_Gas_VariablePvt); m_progressMonitor->setTargetLogLogData(targetData); if(m_autoFitterLM) { connect(m_autoFitterLM, SIGNAL(fittingFinished(bool, QString)), this, SLOT(onFittingFinished(bool, QString))); } else { connect(m_autoFitterPSO, SIGNAL(fittingFinished(bool, QString)), this, SLOT(onFittingFinished(bool, QString))); } int maxIterations = m_iterationEdit->text().toInt(); double targetError = m_errorLimitEdit->text().toDouble(); QString wellName = m_targetWellName; m_progressMonitor->setFittingParameters(maxIterations, targetError, wellName); // 名称和单位均按实际优化向量的顺序展示。 m_progressMonitor->setSelectedParameters(selectedParams, selectedParameterLabels(true)); m_progressMonitor->show(); QTimer::singleShot(100, this, SLOT(runAutoFitting())); } void nmWxAutomaticFitting::runAutoFitting() { if (m_progressMonitor) { m_progressMonitor->markFittingStarted(); } if(m_autoFitterPSO) { m_autoFitterPSO->startAutoFitting(); } else if(m_autoFitterLM) { m_autoFitterLM->startAutoFitting(); } } void nmWxAutomaticFitting::onFittingProgress(int iteration, double fitness) { } void nmWxAutomaticFitting::onFittingFinished(bool success, const QString& message) { // 立即断开信号连接,防止重复调用 if (m_autoFitterPSO) { disconnect(m_autoFitterPSO, SIGNAL(fittingFinished(bool, QString)), this, SLOT(onFittingFinished(bool, QString))); } if (m_autoFitterLM) { disconnect(m_autoFitterLM, SIGNAL(fittingFinished(bool, QString)), this, SLOT(onFittingFinished(bool, QString))); } // 拟合候选值通过参数副本写回,结束时统一把最终值同步到右侧参数面板。 nmDataAnalyzeManager* manager = nmDataAnalyzeManager::getCurrentInstance(); if(manager && manager->getAttrRegistry()) { manager->getAttrRegistry()->refreshAll(); } // 失败状态不代表参数未写回;初值与范围始终以模型实际保留的数据为准。 refreshParametersFromModel(); if(success) { QString resultInfo; if(m_autoFitterPSO || m_autoFitterLM) { double bestFitness = m_autoFitterLM ? m_autoFitterLM->getBestFitness() : m_autoFitterPSO->getBestFitness(); const QString algorithmName = m_autoFitterLM ? "LM" : "PSO"; // 检查是否是用户停止的情况 if(message.contains("stopped by user", Qt::CaseInsensitive)) { resultInfo = tr("%1 Optimization stopped by user:\n").arg(algorithmName); resultInfo += tr("Best Error: %1\n").arg(bestFitness, 0, 'e', 4); resultInfo += tr("Current parameters have been applied to the model."); QMessageBox::information(this, tr("Optimization Stopped"), resultInfo); } else { resultInfo = tr("%1 Optimization completed:\n").arg(algorithmName); resultInfo += tr("Best Error: %1\n").arg(bestFitness, 0, 'e', 4); resultInfo += tr("Optimized parameters have been applied to the model."); QMessageBox::information(this, tr("Optimization Completed"), resultInfo); } } } else { // 只有真正失败的情况才显示警告 QMessageBox::warning(this, tr("Optimization Failed"), message); } } void nmWxAutomaticFitting::onStopFitting() { if(m_autoFitterPSO && m_autoFitterPSO->isRunning()) { m_autoFitterPSO->stopFitting(); } else if(m_autoFitterLM && m_autoFitterLM->isRunning()) { m_autoFitterLM->stopFitting(); } } void nmWxAutomaticFitting::cleanupFitting() { DEBUG_UI("=== CLEANUP FITTING START ==="); if (m_progressTimer) { m_progressTimer->stop(); delete m_progressTimer; m_progressTimer = nullptr; DEBUG_UI("Progress timer cleaned up"); } // 断开所有信号连接,防止后续回调 if (m_autoFitterPSO) { DEBUG_UI("Stopping and disconnecting PSO fitter"); // 断开所有信号连接 disconnect(m_autoFitterPSO, nullptr, nullptr, nullptr); // 如果还在运行,停止它 if (m_autoFitterPSO->isRunning()) { m_autoFitterPSO->stopFitting(); // 等待停止完成 int waitCount = 0; while (m_autoFitterPSO->isRunning() && waitCount < 50) { QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 100); waitCount++; } // 如果仍在运行则强制停止 if (m_autoFitterPSO->isRunning()) { DEBUG_UI("Force stopping PSO - timeout reached"); } } delete m_autoFitterPSO; m_autoFitterPSO = nullptr; DEBUG_UI("PSO fitter cleaned up"); } if (m_autoFitterLM) { DEBUG_UI("Stopping and disconnecting LM fitter"); disconnect(m_autoFitterLM, nullptr, nullptr, nullptr); if (m_autoFitterLM->isRunning()) { m_autoFitterLM->stopFitting(); int waitCount = 0; while (m_autoFitterLM->isRunning() && waitCount < 50) { QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 100); waitCount++; } } delete m_autoFitterLM; m_autoFitterLM = nullptr; DEBUG_UI("LM fitter cleaned up"); } // 清理进度监控 if (m_progressMonitor) { // 先断开进度监控的信号连接 disconnect(m_progressMonitor, nullptr, nullptr, nullptr); m_progressMonitor->hide(); delete m_progressMonitor; m_progressMonitor = nullptr; DEBUG_UI("Progress monitor cleaned up"); } if (m_progressDialog) { m_progressDialog->hide(); delete m_progressDialog; m_progressDialog = nullptr; DEBUG_UI("Progress dialog cleaned up"); } DEBUG_UI("=== CLEANUP FITTING END ==="); } void nmWxAutomaticFitting::refreshParametersFromModel() { // 拟合结束后按各井实际落地的值刷新初值和范围,保留参数勾选。 nmDataAnalyzeManager* manager = nmDataAnalyzeManager::getCurrentInstance(); saveModelParameters(); reservoirData = manager->getReservoirDataCopy(); initializeModelParameters(false); onAlgorithmChanged(m_algorithmCombo->currentIndex()); saveModelParameters(); setAutomaticFittingValue(); 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; }