|
|
#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 <QApplication>
|
|
|
#include <QDesktopWidget>
|
|
|
#include <QHeaderView>
|
|
|
#include <QStyledItemDelegate>
|
|
|
#include <cmath>
|
|
|
#include <float.h>
|
|
|
|
|
|
#ifdef Q_OS_WIN
|
|
|
#include <windows.h>
|
|
|
#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<nmFitParameter>& 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<QVector<double> > 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<QVector<double> > 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<nmFitParameter>& 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<double> 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<nmDataVerticalFracturedWell*>(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<nmDataHorizontalFracturedWell*>(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<QVector<double>>& 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<nmFitParameter>& 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<nmFitParameter> 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<nmCalculationWellRef> 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<nmDataVerticalFracturedWell*>(well)) {
|
|
|
addParameter(group, 4, well, fractured->getDfc().getValue().toDouble());
|
|
|
addParameter(group, 5, well, fractured->getFractureHalfLength().getValue().toDouble());
|
|
|
} else if(nmDataHorizontalFracturedWell* fractured = dynamic_cast<nmDataHorizontalFracturedWell*>(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<nmFitParameter>& 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<double> nmWxAutomaticFitting::primaryInitialValues() const
|
|
|
{
|
|
|
// 旧六参数接口仅提取主井与共享储层,其他井不会覆盖主井初值。
|
|
|
QVector<double> values(6, 0.0);
|
|
|
const QString primaryCode = nmDataAnalyzeManager::getCurrentInstance()->getPrimaryWellCode();
|
|
|
const QVector<nmFitParameter>& 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<nmFitParameter>& 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;
|
|
|
}
|