You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nmWTAI-Platform/Src/nmNum/nmSubWxs/nmWxSensitive.cpp

1872 lines
81 KiB
C++

#include "nmWxSensitive.h"
#include "nmDataAnalyzeManager.h"
#include "nmDataSensitive.h"
#include "nmDataReservoir.h"
#include "nmDataWellBase.h"
#include "nmDataPerforation.h"
#include "nmPebiResultSnapshot.h"
#include "nmCalculationDllPebiSolverTask.h"
#include "iSubWndFitting.h"
#include "mGui/mGuiAnal/iAnalRun.h"
#include "iUnitHelper.h"
#include "iUnitGroup.h"
#include "iSysParaHelper.h"
#include "iParameter.h"
#include "mModuleDefines.h"
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QComboBox>
#include <QLabel>
#include <QPushButton>
#include <QStackedWidget>
#include <QCheckBox>
#include <QSpinBox>
#include <QDialog>
#include <QDialogButtonBox>
#include <QApplication>
#include <QTableWidget>
#include <QTableView>
#include <QAbstractTableModel>
#include <QMap>
#include <QAbstractScrollArea>
#include <QScrollBar>
#include <QWheelEvent>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QHeaderView>
#include <QSplitter>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QGroupBox>
#include <QList>
#include <QVariant>
#include <QString>
#include <QStringList>
#include <QAbstractSpinBox>
#include <QMessageBox>
#include <QWidget>
#include <QPainter>
#include <QPixmap>
#include <QIcon>
#include <QStyledItemDelegate>
#include <QLineEdit>
#include <QCloseEvent>
#include <QScopedPointer>
#include <new>
#pragma comment(lib, "mGuiAnal.lib")
#include <math.h>
#include <float.h>
#include <limits.h>
namespace
{
QString formatSamplingValue(double value, int digits)
{
// 仅对显示值取有效数字,较大或较小的数自动使用科学计数法。
return value == 0.0 ? QString("0") : QString::number(value, 'g', digits);
}
void setSamplingEditValue(QLineEdit* edit, double value)
{
// 显示文本与原始数值分开保存,打开或保存窗口不会丢失采样精度。
const QString text = formatSamplingValue(value, 6);
edit->setProperty("samplingValue", value);
edit->setProperty("samplingText", text);
edit->setText(text);
}
double samplingEditValue(const QLineEdit* edit, bool* ok)
{
// 未修改的格式化文本读取原始值;用户的新输入按实际文本解析。
const QVariant value = edit->property("samplingValue");
if (value.isValid() && edit->text() == edit->property("samplingText").toString())
return value.toDouble(ok);
return edit->text().trimmed().toDouble(ok);
}
QString samplingBaseUnit(const QString& name)
{
// 与现有 PEBI 数据层的业务基准单位保持一致。
if (name == "Pi") return "MPa";
if (name == "k") return "mD";
if (name == "h") return "m";
if (name == "C") return "m^3/MPa";
return QString();
}
bool convertSamplingValue(const QString& name, double value,
QString from, QString to, double& converted)
{
// 优先复用项目单位组;旧配置的 psi 按绝对压力单位迁移。
if (from == "psi") from = "psia";
if (to == "psi") to = "psia";
if (from == "dimensionless") from.clear();
if (to == "dimensionless") to.clear();
if (from == to || (from.isEmpty() && to.isEmpty())) {
converted = value;
return qIsFinite(converted);
}
iUnitGroup* group = iUnitHelper::getUnitGroupByUnit(from);
int digit = 0;
if (group && group->indexOf(to) >= 0 &&
group->convert(from, value, to, converted, digit)) return qIsFinite(converted);
if (name == "h") converted = nmDataAttribute::convertLengthUnit(value, from, to);
else if (name == "Pi") converted = nmDataAttribute::convertPressureUnit(value, from, to);
else if (name == "k") converted = nmDataAttribute::convertPermeabilityUnit(value, from, to);
else return false;
return qIsFinite(converted);
}
bool isSupportedSamplingVariable(const QString& name)
{
return name == "Skin" || name == "C" || name == "Pi" ||
name == "k" || name == "h" || name == QString(QChar(0x03c6));
}
bool samplingPhysicalRange(const QString& name, double& minimum, double& maximum)
{
// 与自动拟合共用 NM 系统参数表,数值均为业务基准单位。
QString parameterName;
if (name == "Skin") parameterName = "Result_W_Skin";
else if (name == "C") parameterName = "Result_W_C";
else if (name == "Pi") parameterName = "Result_Pi";
else if (name == "k") parameterName = "Result_K";
else if (name == "h") parameterName = "Result_h";
else if (name == QString(QChar(0x03c6))) parameterName = "Result_phi";
else return false;
iSysParaHelper* helper = _paraHelper;
iParameter* parameter = helper ? helper->getPara(parameterName, s_Nm_Serie) : nullptr;
if (!parameter || !qIsFinite(parameter->m_dMin) || !qIsFinite(parameter->m_dMax) ||
parameter->m_dMin >= parameter->m_dMax) return false;
minimum = name == "Skin" ? 0.0 : parameter->m_dMin;
maximum = parameter->m_dMax;
return minimum < maximum;
}
bool nearlyEqualSamplingValue(double left, double right)
{
// 沿用自动拟合的八个机器精度容差,只处理边界的浮点舍入差。
return qAbs(left - right) <= DBL_EPSILON * 8.0 * qMax(qAbs(left), qAbs(right));
}
bool validSamplingValue(const QString& name, double& value)
{
// 所有采样值都受系统物理范围约束,边界末位误差归一到精确边界。
if (!qIsFinite(value)) return false;
double minimum = 0.0, maximum = 0.0;
if (!samplingPhysicalRange(name, minimum, maximum)) return false;
if (value < minimum && !nearlyEqualSamplingValue(value, minimum)) return false;
if (value > maximum && !nearlyEqualSamplingValue(value, maximum)) return false;
value = qBound(minimum, value, maximum);
return true;
}
}
// ========= 已完成批次的连续结果列表 =========
class SensitivityResultsModel : public QAbstractTableModel
{
public:
explicit SensitivityResultsModel(QObject* parent) : QAbstractTableModel(parent) {}
void setResults(const QSharedPointer<nmDataSensitive::BatchResult>& results)
{
// 同一批次同步已应用的选择,并保留本窗口尚未应用的勾选。
if(m_results == results) {
if(rowCount() > 0) emit dataChanged(index(0, 0), index(rowCount() - 1, 0));
return;
}
beginResetModel();
m_results = results;
m_pendingVisibility.clear();
endResetModel();
}
int rowCount(const QModelIndex& parent = QModelIndex()) const override
{
// QTableView 按需读取行,结果列表连续滚动,不一次性创建全部表格项。
return parent.isValid() || m_results.isNull() ? 0 : int(m_results->m_runs.size());
}
int columnCount(const QModelIndex& parent = QModelIndex()) const override
{
return parent.isValid() ? 0 : 3;
}
QVariant headerData(int section, Qt::Orientation orientation, int role) const override
{
// 表格仅保留显示选择、运行编号与冻结参数。
if(orientation != Qt::Horizontal || role != Qt::DisplayRole) return QVariant();
const QStringList titles = QStringList() << nmWxSensitive::tr("Show") << "#"
<< nmWxSensitive::tr("Parameters");
return titles.value(section);
}
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override
{
// 参数显示使用冻结配置中的单位和三位有效数字,不修改原始采样值。
if(!index.isValid() || index.row() >= rowCount()) return QVariant();
const nmDataSensitive::RunResult& run = m_results->m_runs[index.row()];
if(role == Qt::CheckStateRole && index.column() == 0)
return run.m_bSucceeded && !run.m_bCancelled &&
m_pendingVisibility.value(index.row(), run.m_bVisible) ? Qt::Checked : Qt::Unchecked;
if(role == Qt::TextAlignmentRole && index.column() == 1)
return int(Qt::AlignRight | Qt::AlignVCenter);
if(role == Qt::ToolTipRole && !run.m_sFailureReason.isEmpty()) return run.m_sFailureReason;
if(role != Qt::DisplayRole && role != Qt::ToolTipRole) return QVariant();
if(index.column() == 1) return QString::number(run.m_nRunIndex + 1);
if(index.column() != 2) return QVariant();
QStringList descriptions;
for(int i = 0; i < m_results->m_variables.size() && i < run.m_vecParameters.size(); ++i) {
const nmDataSensitive::VariableSampling& variable = m_results->m_variables[i];
if(!variable.m_enabled) continue;
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)
+ (unit.isEmpty() ? QString() : " " + unit);
}
return descriptions.join(" + ");
}
Qt::ItemFlags flags(const QModelIndex& index) const override
{
// 失败和取消组只能查看,不能勾选不存在的计算曲线。
if(!index.isValid()) return Qt::NoItemFlags;
Qt::ItemFlags result = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
const nmDataSensitive::RunResult& run = m_results->m_runs[index.row()];
if(index.column() == 0 && run.m_bSucceeded && !run.m_bCancelled)
result |= Qt::ItemIsUserCheckable;
return result;
}
bool setData(const QModelIndex& index, const QVariant& value, int role) override
{
// 勾选只记在本窗口,点击 Apply 或 OK 后才写入绘图使用的结果。
if(!index.isValid() || index.column() != 0 || role != Qt::CheckStateRole) return false;
const nmDataSensitive::RunResult& run = m_results->m_runs[index.row()];
if(!run.m_bSucceeded || run.m_bCancelled) return false;
const bool visible = value.toInt() == Qt::Checked;
if(visible == run.m_bVisible) m_pendingVisibility.remove(index.row());
else m_pendingVisibility.insert(index.row(), visible);
emit dataChanged(index, index);
return true;
}
void setAllVisible(bool visible)
{
// 全选与全隐藏也只改变待应用选择,不立即刷新曲线。
if(m_results.isNull() || m_results->m_runs.empty()) return;
for(int i = 0; i < rowCount(); ++i) {
const nmDataSensitive::RunResult& run = m_results->m_runs[i];
if(!run.m_bSucceeded || run.m_bCancelled) continue;
if(visible == run.m_bVisible) m_pendingVisibility.remove(i);
else m_pendingVisibility.insert(i, visible);
}
emit dataChanged(index(0, 0), index(rowCount() - 1, 0));
}
bool applyVisibility()
{
// 只提交本窗口改过的组,其他结果列表已应用的选择保持不变。
bool changed = false;
QMap<int, bool>::const_iterator it = m_pendingVisibility.constBegin();
for(; it != m_pendingVisibility.constEnd(); ++it) {
nmDataSensitive::RunResult& run = m_results->m_runs[it.key()];
if(run.m_bVisible != it.value()) {
run.m_bVisible = it.value();
changed = true;
}
}
m_pendingVisibility.clear();
return changed;
}
private:
QSharedPointer<nmDataSensitive::BatchResult> m_results;
QMap<int, bool> m_pendingVisibility;
};
// ========= 右对齐委托:保证显示和编辑时都是右对齐 =========
class RightAlignDelegate : public QStyledItemDelegate
{
public:
explicit RightAlignDelegate(QObject *parent = nullptr)
: QStyledItemDelegate(parent)
{}
void initStyleOption(QStyleOptionViewItem *option,
const QModelIndex &index) const override
{
QStyledItemDelegate::initStyleOption(option, index);
option->displayAlignment = Qt::AlignRight | Qt::AlignVCenter;
}
QWidget *createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override
{
QWidget *editor = QStyledItemDelegate::createEditor(parent, option, index);
if (QLineEdit *line = qobject_cast<QLineEdit*>(editor)) {
line->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
}
return editor;
}
void setEditorData(QWidget *editor, const QModelIndex &index) const override
{
QLineEdit* line = qobject_cast<QLineEdit*>(editor);
// 编辑时沿用三位有效数字显示,避免重新暴露浮点尾数。
if (line) line->setText(index.data(Qt::UserRole).isValid()
? formatSamplingValue(index.data(Qt::UserRole).toDouble(), 3)
: index.data(Qt::DisplayRole).toString());
}
void setModelData(QWidget *editor, QAbstractItemModel *model,
const QModelIndex &index) const override
{
QLineEdit* line = qobject_cast<QLineEdit*>(editor);
if (!line) return;
bool ok = false;
double value = line->text().trimmed().toDouble(&ok);
// 编辑后文本未改变时保留完整采样值,不把显示舍入当成用户修改。
const QVariant original = index.data(Qt::UserRole);
if (original.isValid() && line->text() == formatSamplingValue(original.toDouble(), 3))
value = original.toDouble(&ok);
// 非法输入保留原文,由 Generate 或预览统一定位并提示。
model->setData(index, ok && qIsFinite(value) ? QVariant(value) : QVariant(), Qt::UserRole);
model->setData(index, ok && qIsFinite(value)
? formatSamplingValue(value, 3) : line->text(), Qt::DisplayRole);
}
};
// 表格仅承载可见行,完整组合编号使用六十四位整数,滚动过程中按编号取数。
class SensitivityRunsView : public QAbstractScrollArea
{
public:
SensitivityRunsView(const nmDataSensitive& data, const QIcon& arrow, QWidget* parent)
: QAbstractScrollArea(parent), m_data(data), m_arrow(arrow),
m_table(nullptr),
m_total(data.getTotalModelCount()), m_first(0), m_selected(0),
m_visibleRows(1), m_updating(false), m_wheelDelta(0)
{
// 使用现有表格控件绘制表头、行选中和单元格,滚动条控制完整列表。
setFrameShape(QFrame::StyledPanel);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_table = new QTableWidget(viewport());
m_table->setFrameShape(QFrame::NoFrame);
m_table->setColumnCount(3);
m_table->setHorizontalHeaderLabels(QStringList() << "" << "#" << nmWxSensitive::tr("Description"));
m_table->verticalHeader()->setVisible(false);
m_table->verticalHeader()->setDefaultSectionSize(24);
m_table->horizontalHeader()->setResizeMode(0, QHeaderView::Fixed);
m_table->horizontalHeader()->setResizeMode(1, QHeaderView::Fixed);
m_table->horizontalHeader()->setResizeMode(2, QHeaderView::Interactive);
m_table->horizontalHeader()->setStretchLastSection(true);
m_table->horizontalHeader()->setHighlightSections(false);
m_table->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
m_table->setColumnWidth(0, 20);
m_table->setColumnWidth(1, m_table->fontMetrics().width(QString::number(m_total)) + 16);
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_table->setSelectionMode(QAbstractItemView::SingleSelection);
m_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_table->setWordWrap(false);
m_table->setStyleSheet("QTableWidget { background-color: white; }");
int descriptionWidth = 0;
for (int i = 0; i < m_data.getVariableCount(); ++i) {
nmDataSensitive::VariableSampling& variable = m_data.getVariable(i);
if (!variable.getEnabled()) continue;
m_indices.append(i);
int width = 0;
const QVector<double> samples = variable.samplingValues();
for (int row = 0; row < samples.size(); ++row)
width = qMax(width, m_table->fontMetrics().width(parameterDescription(i, samples[row])));
if (m_data.getCalculationType() == nmDataSensitive::CALC_DETERMINISTIC)
descriptionWidth = qMax(descriptionWidth, width);
else descriptionWidth += width + m_table->fontMetrics().width(" + ");
}
// 长组合可横向滚动并通过提示查看完整文本,保持各行高度一致。
m_table->setColumnWidth(2, qMax(200, descriptionWidth + 16));
QVBoxLayout* layout = new QVBoxLayout(viewport());
layout->setContentsMargins(0, 0, 0, 0);
layout->addWidget(m_table);
m_table->installEventFilter(this);
m_table->viewport()->installEventFilter(this);
verticalScrollBar()->installEventFilter(this);
}
void scrollAction(int action)
{
// 超大列表的滚动条位置按比例映射,单步、整屏移动仍保持精确行号。
switch (action) {
case QAbstractSlider::SliderSingleStepAdd: scrollRows(1); break;
case QAbstractSlider::SliderSingleStepSub: scrollRows(-1); break;
case QAbstractSlider::SliderPageStepAdd: scrollRows(m_visibleRows); break;
case QAbstractSlider::SliderPageStepSub: scrollRows(-m_visibleRows); break;
case QAbstractSlider::SliderToMinimum: m_first = 0; refreshRows(); break;
case QAbstractSlider::SliderToMaximum: m_first = maximumFirst(); refreshRows(); break;
case QAbstractSlider::SliderMove:
m_first = firstFromPosition(verticalScrollBar()->sliderPosition());
refreshRows();
break;
default: break;
}
}
protected:
void resizeEvent(QResizeEvent* event) override
{
// 窗口变化时仅调整可见行数量,绝不按总次数分配表格项。
QAbstractScrollArea::resizeEvent(event);
refreshRows();
}
void scrollContentsBy(int, int) override
{
// 处理拖动滚动条或系统直接改变位置的情况。
if (m_updating) return;
m_first = firstFromPosition(verticalScrollBar()->value());
refreshRows();
}
bool eventFilter(QObject* object, QEvent* event) override
{
// 滚轮、键盘和鼠标均操作完整运行列表,而不是可见行缓存。
if (event->type() == QEvent::Wheel) {
QWheelEvent* wheel = static_cast<QWheelEvent*>(event);
if (wheel->orientation() == Qt::Vertical) {
m_wheelDelta += wheel->delta();
const int steps = m_wheelDelta / 120;
m_wheelDelta %= 120;
scrollRows(-qint64(steps) * QApplication::wheelScrollLines());
wheel->accept();
return true;
}
} else if (event->type() == QEvent::KeyPress) {
QKeyEvent* key = static_cast<QKeyEvent*>(event);
qint64 movement = 0;
switch (key->key()) {
case Qt::Key_Up: movement = -1; break;
case Qt::Key_Down: movement = 1; break;
case Qt::Key_PageUp: movement = -m_visibleRows; break;
case Qt::Key_PageDown: movement = m_visibleRows; break;
case Qt::Key_Home: m_selected = 0; break;
case Qt::Key_End: m_selected = m_total - 1; break;
default: return QAbstractScrollArea::eventFilter(object, event);
}
if (movement < 0) m_selected -= qMin(m_selected, quint64(-movement));
else if (movement > 0) m_selected += qMin(m_total - 1 - m_selected, quint64(movement));
if (m_selected < m_first) m_first = m_selected;
else if (m_selected - m_first >= quint64(m_visibleRows))
m_first = m_selected - m_visibleRows + 1;
refreshRows();
key->accept();
return true;
} else if (object == m_table->viewport() &&
(event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease)) {
QMouseEvent* mouse = static_cast<QMouseEvent*>(event);
const int row = m_table->rowAt(mouse->pos().y());
if (mouse->button() == Qt::LeftButton && row >= 0) {
m_selected = m_first + row;
refreshRows();
}
} else if (object == m_table->viewport() && event->type() == QEvent::Resize) {
refreshRows();
}
return QAbstractScrollArea::eventFilter(object, event);
}
private:
quint64 maximumFirst() const
{
return m_total > quint64(m_visibleRows) ? m_total - m_visibleRows : 0;
}
quint64 firstFromPosition(int position) const
{
// 两端准确对应首尾,中间位置不把六十四位总数截断为 int。
const int maximum = verticalScrollBar()->maximum();
if (position <= 0 || maximum == 0) return 0;
if (position >= maximum) return maximumFirst();
return qMin(maximumFirst(), quint64((long double)position * maximumFirst() / maximum));
}
void scrollRows(qint64 movement)
{
// 用差值限制移动距离,避免无符号行号在首尾发生溢出。
if (movement < 0) m_first -= qMin(m_first, quint64(-movement));
else m_first += qMin(maximumFirst() - m_first, quint64(movement));
refreshRows();
}
QString parameterDescription(int index, double value)
{
// 描述沿用采样表的三位有效数字和当前显示单位。
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)
+ (unit.isEmpty() ? QString() : " " + unit);
}
void refreshRows()
{
if (m_updating || !m_table || !viewport()->layout()) return;
// 始终只生成视口内的行,所有组合共用一根连续滚动条。
m_updating = true;
viewport()->layout()->activate();
m_visibleRows = qMax(1, m_table->viewport()->height() / 24);
setVerticalScrollBarPolicy(m_total > quint64(m_visibleRows)
? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
m_first = qMin(m_first, maximumFirst());
QScrollBar* bar = verticalScrollBar();
const quint64 last = maximumFirst();
const int maximum = int(qMin(last, quint64(INT_MAX)));
const bool blocked = bar->blockSignals(true);
bar->setRange(0, maximum);
bar->setSingleStep(1);
bar->setPageStep(last ? qMax(1, int((long double)m_visibleRows * maximum / last)) : m_visibleRows);
int position = last ? int((long double)m_first * maximum / last) : 0;
// 比例舍入后仍保留首尾箭头的可用状态。
if (last > quint64(INT_MAX) && m_first > 0 && m_first < last)
position = qBound(1, position, maximum - 1);
bar->setValue(position);
bar->blockSignals(blocked);
const int rows = int(qMin(quint64(m_visibleRows), m_total - m_first));
m_table->setRowCount(rows);
for (int row = 0; row < rows; ++row) {
const quint64 run = m_first + row;
QVector<double> values;
if (!m_data.getRunValues(run, values)) break;
int changingIndex = -1;
if (m_data.getCalculationType() == nmDataSensitive::CALC_DETERMINISTIC) {
quint64 remaining = run;
for (int i = 0; i < m_indices.size(); ++i) {
const quint64 count = m_data.getVariable(m_indices[i]).getNumber();
if (remaining < count) { changingIndex = m_indices[i]; break; }
remaining -= count;
}
}
QStringList descriptions;
for (int i = 0; i < m_indices.size(); ++i) {
const int index = m_indices[i];
if (changingIndex < 0 || index == changingIndex)
descriptions << parameterDescription(index, values[index]);
}
QTableWidgetItem* arrow = new QTableWidgetItem();
arrow->setIcon(run == m_selected ? m_arrow : QIcon());
m_table->setItem(row, 0, arrow);
QTableWidgetItem* number = new QTableWidgetItem(QString::number(run + 1));
number->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
m_table->setItem(row, 1, number);
QTableWidgetItem* description = new QTableWidgetItem(descriptions.join(" + "));
description->setToolTip(description->text());
m_table->setItem(row, 2, description);
}
if (m_selected >= m_first && m_selected - m_first < quint64(rows)) {
m_table->setCurrentCell(int(m_selected - m_first), 1);
m_table->selectRow(int(m_selected - m_first));
} else {
m_table->setCurrentIndex(QModelIndex());
m_table->clearSelection();
}
m_updating = false;
// 横向滚动条显隐可能改变视口高度,补齐最终可见行数量。
if (m_visibleRows != qMax(1, m_table->viewport()->height() / 24)) refreshRows();
}
nmDataSensitive m_data;
QIcon m_arrow;
QTableWidget* m_table;
QList<int> m_indices;
quint64 m_total;
quint64 m_first;
quint64 m_selected;
int m_visibleRows;
bool m_updating;
int m_wheelDelta;
};
nmWxSensitive::nmWxSensitive(QWidget *parent, nmDataAnalyzeManager* manager)
: iDlgBase(parent), m_selectedValueIndex(0), m_loading(true),
m_pDataManager(manager ? manager : nmDataAnalyzeManager::getCurrentInstance()),
m_previewView(nullptr), m_pSensitivityTask(nullptr), m_calculationRunning(false)
{
// 显式绑定所属分析窗口,后续切换窗口不会改变配置保存目标。
if (m_pDataManager) m_sensitiveData = m_pDataManager->getSensitiveDataCopy();
refreshModelValues();
setupUI();
createConnections();
populateTreeWidget();
createDetailPages();
loadDataToUI();
m_loading = false;
onTreeItemChanged(nullptr, 0);
// 已有批次可直接重新打开,结果发布后同步入口与列表。
if(m_pDataManager) {
connect(m_pDataManager, SIGNAL(sigSensitiveResultsChanged()), this, SLOT(onResultsChanged()));
connect(m_pDataManager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(onModelResultChanged()));
}
onResultsChanged();
}
nmWxSensitive::~nmWxSensitive()
{
// 父窗口强制销毁时先请求停止并等 DLL 返回,线程结束前不释放计算副本。
if(m_pSensitivityTask) {
disconnect(m_pSensitivityTask, nullptr, this, nullptr);
m_pSensitivityTask->requestCancel();
m_pSensitivityTask->wait();
delete m_pSensitivityTask;
}
}
void nmWxSensitive::reject()
{
// Esc 与取消按钮使用相同的停止路径,保留已完成组供结束后查看。
if(m_calculationRunning) onCancelClicked();
else iDlgBase::reject();
}
void nmWxSensitive::closeEvent(QCloseEvent* event)
{
if(m_calculationRunning) {
onCancelClicked();
event->ignore();
return;
}
iDlgBase::closeEvent(event);
}
void nmWxSensitive::refreshModelValues()
{
// 六个参数以最后成功求解的快照为准,编辑中的模型只提供属性类型和显示单位。
const QSharedPointer<const nmPebiResultSnapshot> snapshot = m_pDataManager
? m_pDataManager->getPebiResultSnapshot() : QSharedPointer<const nmPebiResultSnapshot>();
const bool resetRanges = !snapshot.isNull() &&
snapshot->getSnapshotId() != m_sensitiveData.getModelSnapshotId();
nmDataWellBase* well = m_pDataManager
? m_pDataManager->findWellByCode(m_pDataManager->getPrimaryWellCode()) : nullptr;
nmDataReservoir* reservoir = m_pDataManager ? m_pDataManager->getReservoirData() : nullptr;
const nmPebiResultWellSnapshot* solvedWell = !snapshot.isNull() && well
? snapshot->findWell(well->getWellInstanceId()) : nullptr;
const QStringList names = QStringList() << "Skin" << "C" << "Pi" << "k" << "h" << QString(QChar(0x03c6));
for (int n = 0; n < names.size(); ++n) {
nmDataAttribute* attribute = nullptr;
const QString name = names[n];
if (well && name == "Skin" && well->getPerforationCount() > 0)
attribute = &well->getPerforation(0)->getSkin();
else if (well && name == "C") attribute = &well->getWellboreStorage();
else if (reservoir && name == "Pi") attribute = &reservoir->getInitialPressure();
else if (reservoir && name == "k") attribute = &reservoir->getPermeability();
else if (reservoir && name == "h") attribute = &reservoir->getThickness();
else if (reservoir && name == QString(QChar(0x03c6))) attribute = &reservoir->getPorosity();
int index = -1;
for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i)
if (m_sensitiveData.getVariable(i).getVarName() == name) { index = i; break; }
const bool fresh = index < 0;
if (fresh) {
nmDataSensitive::VariableSampling variable;
variable.setVarName(name);
variable.setVarGroup(n < 2 ? "Tested Well" : "Reservoir");
variable.setNumber(5);
m_sensitiveData.getVariables().append(variable);
index = m_sensitiveData.getVariableCount() - 1;
}
nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(index);
// 在局部属性上替换数值,不能把未求解的编辑值带入 Model value 或默认范围。
nmDataAttribute solvedAttribute;
if (attribute && !snapshot.isNull()) {
solvedAttribute = *attribute;
const nmPebiResultReservoirParameters& solvedReservoir = snapshot->getReservoirParameters();
if (name == "Skin" || name == "C") {
if (!solvedWell || (name == "Skin" && !solvedWell->m_bHasSkin)) attribute = nullptr;
else solvedAttribute.setValue(name == "Skin" ? solvedWell->m_dSkin : solvedWell->m_dWellboreStorage);
} else if (name == "Pi") solvedAttribute.setValue(solvedReservoir.m_dInitialPressure);
else if (name == "k") solvedAttribute.setValue(solvedReservoir.m_dPermeability);
else if (name == "h") solvedAttribute.setValue(solvedReservoir.m_dThickness);
else if (name == QString(QChar(0x03c6))) solvedAttribute.setValue(solvedReservoir.m_dPorosity);
if (attribute) attribute = &solvedAttribute;
}
double physicalMin = 0.0, physicalMax = 0.0;
if (!attribute || !samplingPhysicalRange(name, physicalMin, physicalMax)) {
variable.setEnabled(false);
variable.getModelValue().setValue(QVariant());
continue;
}
const QString baseUnit = samplingBaseUnit(name);
QString displayUnit = attribute->getUnit();
if (displayUnit == "dimensionless") displayUnit.clear();
double converted = 0.0;
if (!convertSamplingValue(name, attribute->getValue().toDouble(), baseUnit, displayUnit, converted))
displayUnit = baseUnit;
double minimum = variable.getMinValue().getValue().toDouble();
double maximum = variable.getMaxValue().getValue().toDouble();
QVector<double> values = variable.getValues();
double previousModel = variable.getModelValue().getValue().toDouble();
bool migrated = !fresh && (m_sensitiveData.usesBaseUnits() ||
variable.getMode() == nmDataSensitive::VariableSampling::MODE_MANUAL || !values.isEmpty());
if (migrated && !m_sensitiveData.usesBaseUnits()) {
const QString oldUnit = variable.getModelValue().getUnit();
convertSamplingValue(name, previousModel, oldUnit, baseUnit, previousModel);
migrated = convertSamplingValue(name, minimum, oldUnit, baseUnit, minimum) &&
convertSamplingValue(name, maximum, oldUnit, baseUnit, maximum);
for (int i = 0; migrated && i < values.size(); ++i)
migrated = convertSamplingValue(name, values[i], oldUnit, baseUnit, values[i]);
}
// 识别 Skin、Pi 的自动默认范围,按当前模型更新,保留用户自定义区间。
const bool previousDefault = variable.getMode() == nmDataSensitive::VariableSampling::MODE_AUTOMATIC &&
((nearlyEqualSamplingValue(minimum, qMin(previousModel / 2.0, previousModel * 2.0)) &&
nearlyEqualSamplingValue(maximum, qMax(previousModel / 2.0, previousModel * 2.0))) ||
(name == "Skin" && previousModel != 0.0 &&
nearlyEqualSamplingValue(minimum, qMax(physicalMin, qMin(previousModel * 0.9, previousModel * 1.1))) &&
nearlyEqualSamplingValue(maximum, qMin(physicalMax, qMax(previousModel * 0.9, previousModel * 1.1)))) ||
(name == "Skin" && previousModel == 0.0 && (minimum == -1.0 || minimum == 0.0) && maximum == 1.0) ||
(name == "Pi" && variable.getModelValue().getMinValue().isValid() &&
variable.getModelValue().getMaxValue().isValid() &&
nearlyEqualSamplingValue(minimum, variable.getModelValue().getMinValue().toDouble()) &&
nearlyEqualSamplingValue(maximum, variable.getModelValue().getMaxValue().toDouble())));
// 新的正式求解结果重新建立范围,采样数量、模式及手动值保持原设置。
if (resetRanges || !migrated || ((name == "Skin" || name == "Pi") && previousDefault)) {
const double model = attribute->getValue().toDouble();
minimum = qMin(model / 2.0, model * 2.0);
maximum = qMax(model / 2.0, model * 2.0);
if (name == "Skin") {
minimum = model == 0.0 ? -1.0 : qMin(model * 0.9, model * 1.1);
maximum = model == 0.0 ? 1.0 : qMax(model * 0.9, model * 1.1);
}
if (name == "Pi") { minimum = physicalMin; maximum = physicalMax; }
}
if (!migrated) {
values.clear();
variable.setMode(nmDataSensitive::VariableSampling::MODE_AUTOMATIC);
variable.setLog(false);
variable.setNumber(5);
}
// 初始范围和已保存范围都截断到硬限制,手动采样值留给提交校验。
minimum = qBound(physicalMin, minimum, physicalMax);
maximum = qBound(physicalMin, maximum, physicalMax);
variable.setModelValue(*attribute);
variable.getModelValue().setUnit(displayUnit);
variable.getModelValue().setValueRange(physicalMin, physicalMax);
variable.setMinValue(variable.getModelValue());
variable.setMaxValue(variable.getModelValue());
variable.getMinValue().setValue(minimum);
variable.getMaxValue().setValue(maximum);
variable.setValues(values);
// Saphir 在重新打开对话框时同步无效 Log 范围的实际状态。
if (variable.getMode() == nmDataSensitive::VariableSampling::MODE_AUTOMATIC &&
(minimum <= 0.0 || maximum <= 0.0)) variable.setLog(false);
}
for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i)
if (!isSupportedSamplingVariable(m_sensitiveData.getVariable(i).getVarName()))
m_sensitiveData.getVariable(i).setEnabled(false);
m_sensitiveData.setUsesBaseUnits(true);
if (!snapshot.isNull()) m_sensitiveData.setModelSnapshotId(snapshot->getSnapshotId());
}
void nmWxSensitive::onModelResultChanged()
{
// 查看井切换也会发送快照通知;只有新求解快照才刷新已打开窗口。
const QSharedPointer<const nmPebiResultSnapshot> snapshot = m_pDataManager->getPebiResultSnapshot();
if (snapshot.isNull() || snapshot->getSnapshotId() == m_sensitiveData.getModelSnapshotId()) return;
m_loading = true;
refreshModelValues();
loadDataToUI(true);
m_loading = false;
updateModelCount();
}
void nmWxSensitive::loadDataToUI(bool refreshRangesOnly)
{
// 加载过程中阻止联动刷新,先恢复设置,再一次生成各参数表格。
if (!refreshRangesOnly)
calculationTypeCombo->setCurrentIndex(int(m_sensitiveData.getCalculationType()));
for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) {
nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i);
const QString name = variable.getVarName();
const int idx = findVariableIndex(name);
if (idx < 0) continue;
const bool available = isSupportedSamplingVariable(name) &&
variable.getModelValue().getValue().isValid();
for (int group = 0; group < variablesTree->topLevelItemCount(); ++group) {
QTreeWidgetItem* parent = variablesTree->topLevelItem(group);
for (int child = 0; child < parent->childCount(); ++child) {
QTreeWidgetItem* item = parent->child(child);
if (item->text(0) != name) continue;
item->setDisabled(!available);
if (!refreshRangesOnly)
item->setCheckState(0, available && variable.getEnabled() ? Qt::Checked : Qt::Unchecked);
}
}
detailStack->widget(idx + 1)->setEnabled(available);
// 求解后的刷新只更新模型值和范围,不覆盖窗口中尚未提交的采样设置。
if (!refreshRangesOnly) {
m_modeCombos[idx]->setCurrentIndex(int(variable.getMode()));
m_logChecks[idx]->setChecked(variable.getLog());
m_numSpins[idx]->setValue(qMax(1, variable.getNumber()));
}
// 已打开窗口沿用原显示单位,避免手动表格的数值被换成另一种单位解释。
const QString unit = refreshRangesOnly ? m_unitLabels[idx]->text() : variable.getModelValue().getUnit();
variable.getModelValue().setUnit(unit);
m_unitLabels[idx]->setText(unit);
double model = variable.getModelValue().getValue().toDouble();
double minimum = variable.getMinValue().getValue().toDouble();
double maximum = variable.getMaxValue().getValue().toDouble();
convertSamplingValue(name, model, samplingBaseUnit(name), unit, model);
convertSamplingValue(name, minimum, samplingBaseUnit(name), unit, minimum);
convertSamplingValue(name, maximum, samplingBaseUnit(name), unit, maximum);
setSamplingEditValue(m_modelSpins[idx], model);
setSamplingEditValue(m_minSpins[idx], minimum);
setSamplingEditValue(m_maxSpins[idx], maximum);
double displayMin = variable.getModelValue().getMinValue().toDouble();
double displayMax = variable.getModelValue().getMaxValue().toDouble();
convertSamplingValue(name, displayMin, samplingBaseUnit(name), unit, displayMin);
convertSamplingValue(name, displayMax, samplingBaseUnit(name), unit, displayMax);
const QString rangeHint = tr("Allowed range: [%1, %2] %3")
.arg(formatSamplingValue(displayMin, 6)).arg(formatSamplingValue(displayMax, 6)).arg(unit);
m_minSpins[idx]->setToolTip(rangeHint);
m_maxSpins[idx]->setToolTip(rangeHint);
if (!refreshRangesOnly && variable.getMode() == nmDataSensitive::VariableSampling::MODE_MANUAL) {
QTableWidget* table = m_valueTables[idx];
table->setRowCount(variable.getNumber());
const QVector<double>& values = variable.getValues();
for (int row = 0; row < values.size() && row < table->rowCount(); ++row) {
double display = values[row];
convertSamplingValue(name, display, samplingBaseUnit(name), unit, display);
QTableWidgetItem* item = new QTableWidgetItem(formatSamplingValue(display, 3));
item->setData(Qt::UserRole, display);
table->setItem(row, 1, item);
}
}
updateValuesForPage(idx);
}
}
int nmWxSensitive::findVariableIndex(const QString& varName)
{
// 根据创建顺序查找变量索引
QStringList allVars;
allVars << "Zw" << "Hw" << "Lw" << "Skin" << "C" // Tested Well
<< "Pi" << "k" << "h" << QString(QChar(0x03c6)) << "ntg" << "kz/kr" // Reservoir
<< "Total compressibility"; // Pvt
return allVars.indexOf(varName);
}
void nmWxSensitive::updateModelCount()
{
// 勾选、数量和方法修改后立即更新总次数,不等待 Generate。
if (m_loading) return;
m_sensitiveData.setCalculationType(
static_cast<nmDataSensitive::CalculationType>(calculationTypeCombo->currentIndex()));
for (int group = 0; group < variablesTree->topLevelItemCount(); ++group) {
QTreeWidgetItem* parent = variablesTree->topLevelItem(group);
for (int child = 0; child < parent->childCount(); ++child) {
QTreeWidgetItem* item = parent->child(child);
for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) {
nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i);
if (variable.getVarName() != item->text(0)) continue;
variable.setEnabled(!item->isDisabled() && item->checkState(0) == Qt::Checked);
const int idx = findVariableIndex(variable.getVarName());
if (idx >= 0) variable.setNumber(m_numSpins[idx]->value());
}
}
}
bool overflow = false;
const quint64 count = m_sensitiveData.calculateModelCount(&overflow);
m_sensitiveData.setTotalModelCount(count);
modelCountLabel->setText(overflow ? tr("Too many models") : QString::number(count));
previewButton->setEnabled(!overflow && count > 0);
generateButton->setEnabled(m_pDataManager && !overflow && count > 0 && !m_calculationRunning);
}
bool nmWxSensitive::saveDataFromUI()
{
// 校验选中参数后再提交副本,失败不改写数据中心已有配置。
updateModelCount();
if (m_sensitiveData.getTotalModelCount() == 0) return false;
nmDataSensitive candidate(m_sensitiveData);
for (int i = 0; i < candidate.getVariableCount(); ++i) {
nmDataSensitive::VariableSampling& variable = candidate.getVariable(i);
const QString name = variable.getVarName();
const int idx = findVariableIndex(name);
if (idx < 0 || !isSupportedSamplingVariable(name)) continue;
variable.setMode(static_cast<nmDataSensitive::VariableSampling::Mode>(m_modeCombos[idx]->currentIndex()));
variable.setLog(m_logChecks[idx]->isChecked());
const QString unit = m_unitLabels[idx]->text();
const QString baseUnit = samplingBaseUnit(name);
bool valid = true;
if (variable.getMode() == nmDataSensitive::VariableSampling::MODE_AUTOMATIC) {
bool minOk = false, maxOk = false;
double minimum = samplingEditValue(m_minSpins[idx], &minOk);
double maximum = samplingEditValue(m_maxSpins[idx], &maxOk);
valid = minOk && maxOk &&
convertSamplingValue(name, minimum, unit, baseUnit, minimum) &&
convertSamplingValue(name, maximum, unit, baseUnit, maximum) &&
minimum <= maximum && validSamplingValue(name, minimum) && validSamplingValue(name, maximum);
if (valid) {
variable.getMinValue().setValue(minimum);
variable.getMaxValue().setValue(maximum);
// Number=1 使用模型值,也必须在同一物理范围内。
QVector<double> values = variable.samplingValues();
for (int row = 0; valid && row < values.size(); ++row)
valid = validSamplingValue(name, values[row]);
if (valid) variable.setValues(values);
}
} else {
QVector<double> values;
QTableWidget* table = m_valueTables[idx];
for (int row = 0; row < table->rowCount(); ++row) {
QTableWidgetItem* item = table->item(row, 1);
bool ok = false;
double value = 0.0;
if (item) value = item->data(Qt::UserRole).isValid()
? item->data(Qt::UserRole).toDouble(&ok) : item->text().trimmed().toDouble(&ok);
if (!ok || !convertSamplingValue(name, value, unit, baseUnit, value) ||
!validSamplingValue(name, value)) { valid = false; break; }
values.append(value);
}
valid = valid && values.size() == variable.getNumber();
if (valid) variable.setValues(values);
}
if (!valid && variable.getEnabled()) {
double lower = 0.0, upper = 0.0;
const bool hasRange = samplingPhysicalRange(name, lower, upper) &&
convertSamplingValue(name, lower, baseUnit, unit, lower) &&
convertSamplingValue(name, upper, baseUnit, unit, upper);
QMessageBox::warning(this, tr("Sensitivity"),
hasRange ? tr("%1: invalid sampling values. Allowed range: [%2, %3] %4. Check the range and every manual row.")
.arg(name).arg(formatSamplingValue(lower, 6)).arg(formatSamplingValue(upper, 6)).arg(unit)
: tr("%1: the physical range is unavailable.").arg(name));
detailStack->setCurrentIndex(idx + 1);
return false;
}
if (!variable.getEnabled()) continue;
// 本期处理常量参数;属性场开启时常量会被插值覆盖,因此不能静默运行。
if (m_pDataManager && (name == "k" || name == "h" || name == QString(QChar(0x03c6)))) {
const QString property = name == QString(QChar(0x03c6)) ? "phi" : name;
const QVector<nmPropertyInterpolationDataSet> dataSets = m_pDataManager->getPropertyInterpolationDataSets();
for (int field = 0; field < dataSets.size(); ++field) {
if (dataSets[field].useForCalculation && dataSets[field].property == property &&
!dataSets[field].points.isEmpty()) {
QMessageBox::warning(this, tr("Sensitivity"),
tr("%1 uses a non-uniform field. Disable that field before sampling a constant value.").arg(name));
return false;
}
}
}
}
m_sensitiveData = candidate;
return true;
}
void nmWxSensitive::setupUI()
{
setWindowTitle(tr("Sensitivity"));
setMinimumSize(900, 550);
QVBoxLayout *mainLayout = new QVBoxLayout(this);
// ===== 顶部:Calculation type =====
QHBoxLayout *calcTypeLayout = new QHBoxLayout();
QLabel *calcTypeLabel = new QLabel(tr("Calculation type:"), this);
calculationTypeCombo = new QComboBox(this);
calculationTypeCombo->addItem(tr("Deterministic"));
calculationTypeCombo->addItem(tr("Deterministic multivariate"));
//calculationTypeCombo->addItem(tr("Monte-Carlo"));
//calculationTypeCombo->addItem(tr("Monte-Carlo + Improve"));
calcTypeLayout->addWidget(calcTypeLabel);
calcTypeLayout->addWidget(calculationTypeCombo);
calcTypeLayout->addStretch();
mainLayout->addLayout(calcTypeLayout);
// ===== 中间:左 Variables 面板 + 右 detailStack =====
QSplitter *centerSplitter = new QSplitter(Qt::Horizontal, this);
centerSplitter->setChildrenCollapsible(false); // 不允许把某一侧完全折叠
centerSplitter->setHandleWidth(4); // 中间拖动条宽度
centerSplitter->setStyleSheet(
"QSplitter::handle:horizontal {"
" background-color: #C0C0C0;"
"}"
"QSplitter::handle:horizontal:hover {"
" background-color: #A0A0A0;"
"}"
);
// --- 左侧:Variables 面板(QFrame + 标题栏 + Tree)---
QFrame *variablesPanel = new QFrame(this);
variablesPanel->setFrameShape(QFrame::StyledPanel);
variablesPanel->setFrameShadow(QFrame::Raised);
variablesPanel->setMinimumWidth(230);
QVBoxLayout *variablesPanelLayout = new QVBoxLayout(variablesPanel);
variablesPanelLayout->setContentsMargins(0, 0, 0, 0);
variablesPanelLayout->setSpacing(0);
// 标题栏
QWidget *varHeader = new QWidget(variablesPanel);
varHeader->setFixedHeight(24);
varHeader->setAutoFillBackground(true);
{
QPalette pal = varHeader->palette();
pal.setColor(QPalette::Window,
palette().color(QPalette::Midlight));
varHeader->setPalette(pal);
}
QHBoxLayout *varHeaderLayout = new QHBoxLayout(varHeader);
varHeaderLayout->setContentsMargins(6, 0, 6, 0);
varHeaderLayout->setSpacing(0);
QLabel *varTitle = new QLabel(tr("Variables"), varHeader);
varTitle->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
QFont f = varTitle->font();
f.setBold(true);
varTitle->setFont(f);
varHeaderLayout->addWidget(varTitle);
varHeaderLayout->addStretch();
// Tree 区域
variablesTree = new QTreeWidget(variablesPanel);
variablesTree->setHeaderHidden(true);
variablesTree->setFrameShape(QFrame::NoFrame); // 外层已经有 frame
variablesTree->setMinimumHeight(350);
variablesPanelLayout->addWidget(varHeader);
variablesPanelLayout->addWidget(variablesTree, 1);
// 把左侧 panel 加进 splitter
centerSplitter->addWidget(variablesPanel);
// --- 右侧:detailStack(后面每个 page 自己做标题栏)---
detailStack = new QStackedWidget(this);
detailStack->setContentsMargins(0, 0, 0, 0);
// index 0:默认提示页
QWidget *blankPage = new QWidget(this);
QVBoxLayout *blankLayout = new QVBoxLayout(blankPage);
blankLayout->setContentsMargins(0, 0, 0, 0);
QLabel *blankLabel = new QLabel(tr("Select a variable on the left."), blankPage);
blankLayout->addStretch();
blankLayout->addWidget(blankLabel, 0, Qt::AlignHCenter);
blankLayout->addStretch();
detailStack->addWidget(blankPage);
// 把右侧 stack 加进 splitter
centerSplitter->addWidget(detailStack);
// 左边宽度基本固定,右边自适应
centerSplitter->setStretchFactor(0, 0);
centerSplitter->setStretchFactor(1, 1);
mainLayout->addWidget(centerSplitter, 1);
// ===== 底部:Total number of models + 按钮 =====
QHBoxLayout *bottomLayout = new QHBoxLayout();
QLabel *totalLabel = new QLabel(tr("Total number of models:"), this);
modelCountLabel = new QLabel(tr("0"), this);
bottomLayout->addWidget(totalLabel);
bottomLayout->addWidget(modelCountLabel);
previewButton = new QPushButton(this);
previewButton->setIcon(QIcon(QCoreApplication::applicationDirPath().section('/', 0, -2)
+ "/Res/Icon/Eyes.png"));
previewButton->setFixedSize(28, 24);
previewButton->setToolTip(tr("Sensitivity runs"));
bottomLayout->addWidget(previewButton);
bottomLayout->addStretch();
resultsButton = new QPushButton(tr("Results"), this);
generateButton = new QPushButton(tr("Generate"), this);
cancelButton = new QPushButton(tr("Cancel"), this);
bottomLayout->addWidget(resultsButton);
bottomLayout->addWidget(generateButton);
bottomLayout->addWidget(cancelButton);
mainLayout->addLayout(bottomLayout);
}
void nmWxSensitive::createConnections()
{
connect(generateButton, SIGNAL(clicked()),
this, SLOT(onGenerateClicked()));
connect(cancelButton, SIGNAL(clicked()),
this, SLOT(onCancelClicked()));
connect(previewButton, SIGNAL(clicked()), this, SLOT(onPreviewClicked()));
connect(resultsButton, SIGNAL(clicked()), this, SLOT(onResultsClicked()));
connect(calculationTypeCombo, SIGNAL(currentIndexChanged(int)),
this, SLOT(onCalculationTypeChanged(int)));
connect(variablesTree, SIGNAL(itemClicked(QTreeWidgetItem*,int)),
this, SLOT(onTreeItemClicked(QTreeWidgetItem*,int)));
connect(variablesTree, SIGNAL(itemChanged(QTreeWidgetItem*,int)),
this, SLOT(onTreeItemChanged(QTreeWidgetItem*,int)));
}
void nmWxSensitive::populateTreeWidget()
{
variablesTree->clear();
// ===== Tested Well =====
QTreeWidgetItem *testedWellItem = new QTreeWidgetItem(variablesTree);
testedWellItem->setText(0, tr("Tested Well"));
testedWellItem->setCheckState(0, Qt::Unchecked);
testedWellItem->setFlags(testedWellItem->flags() | Qt::ItemIsUserCheckable);
QStringList testedWellChildren;
testedWellChildren << "Zw" << "Hw" << "Lw" << "Skin" << "C";
int i;
for (i = 0; i < testedWellChildren.size(); ++i) {
QTreeWidgetItem *childItem = new QTreeWidgetItem(testedWellItem);
childItem->setText(0, testedWellChildren.at(i));
childItem->setCheckState(0, Qt::Unchecked);
childItem->setFlags(childItem->flags() | Qt::ItemIsUserCheckable);
childItem->setDisabled(!isSupportedSamplingVariable(testedWellChildren.at(i)));
}
// ===== Reservoir =====
QTreeWidgetItem *reservoirItem = new QTreeWidgetItem(variablesTree);
reservoirItem->setText(0, tr("Reservoir"));
reservoirItem->setCheckState(0, Qt::Unchecked);
reservoirItem->setFlags(reservoirItem->flags() | Qt::ItemIsUserCheckable);
QStringList reservoirChildren;
reservoirChildren << "Pi" << "k" << "h" << QString(QChar(0x03c6)) << "ntg" << "kz/kr";
for (i = 0; i < reservoirChildren.size(); ++i) {
QTreeWidgetItem *childItem = new QTreeWidgetItem(reservoirItem);
childItem->setText(0, reservoirChildren.at(i));
childItem->setCheckState(0, Qt::Unchecked);
childItem->setFlags(childItem->flags() | Qt::ItemIsUserCheckable);
childItem->setDisabled(!isSupportedSamplingVariable(reservoirChildren.at(i)));
}
// ===== Pvt =====
QTreeWidgetItem *pvtItem = new QTreeWidgetItem(variablesTree);
pvtItem->setText(0, tr("Pvt"));
pvtItem->setCheckState(0, Qt::Unchecked);
pvtItem->setFlags(pvtItem->flags() | Qt::ItemIsUserCheckable);
QTreeWidgetItem *compressibilityItem = new QTreeWidgetItem(pvtItem);
compressibilityItem->setText(0, tr("Total compressibility"));
compressibilityItem->setCheckState(0, Qt::Unchecked);
compressibilityItem->setFlags(compressibilityItem->flags() | Qt::ItemIsUserCheckable);
compressibilityItem->setDisabled(true);
variablesTree->expandAll();
}
void nmWxSensitive::createDetailPages()
{
int i, j;
// 依次为每个"叶子变量"创建一个页面,并把 index 存到 item->data 里
for (i = 0; i < variablesTree->topLevelItemCount(); ++i) {
QTreeWidgetItem *topItem = variablesTree->topLevelItem(i);
for (j = 0; j < topItem->childCount(); ++j) {
QTreeWidgetItem *childItem = topItem->child(j);
if (childItem == nullptr) {
continue;
}
QString varName = childItem->text(0);
QWidget *page = createVariablePage(varName);
int pageIndex = detailStack->addWidget(page); // index 从 1 开始
childItem->setData(0, Qt::UserRole, pageIndex);
}
}
}
QIcon nmWxSensitive::createTriangleIcon()
{
QPixmap trianglePixmap(16, 16);
trianglePixmap.fill(Qt::transparent);
QPainter painter(&trianglePixmap);
painter.setRenderHint(QPainter::Antialiasing, true);
// 绘制小三角形(指向右的箭头)
QPolygon triangle;
triangle << QPoint(4, 6) << QPoint(4, 10) << QPoint(10, 8);
painter.setBrush(QBrush(QColor(0, 0, 0))); // 黑色三角形
painter.setPen(QPen(QColor(0, 0, 0), 1));
painter.drawPolygon(triangle);
return QIcon(trianglePixmap);
}
QWidget *nmWxSensitive::createVariablePage(const QString &varName)
{
QWidget *page = new QWidget(this);
QHBoxLayout *pageLayout = new QHBoxLayout(page);
pageLayout->setContentsMargins(0, 0, 0, 0);
pageLayout->setSpacing(0);
// ===== 整个 Sampling 面板:QFrame + 标题栏 + 内容 =====
QFrame *samplingPanel = new QFrame(page);
samplingPanel->setFrameShape(QFrame::StyledPanel);
samplingPanel->setFrameShadow(QFrame::Raised);
QVBoxLayout *panelLayout = new QVBoxLayout(samplingPanel);
panelLayout->setContentsMargins(0, 0, 0, 0);
panelLayout->setSpacing(0);
// ---- 标题栏:Sampling Xxx ----
QWidget *header = new QWidget(samplingPanel);
header->setFixedHeight(24);
header->setAutoFillBackground(true);
{
QPalette pal = header->palette();
pal.setColor(QPalette::Window,
palette().color(QPalette::Midlight));
header->setPalette(pal);
}
QHBoxLayout *headerLayout = new QHBoxLayout(header);
headerLayout->setContentsMargins(6, 0, 6, 0);
headerLayout->setSpacing(0);
QLabel *title = new QLabel(tr("Sampling %1").arg(varName), header);
QFont tf = title->font();
tf.setBold(true);
title->setFont(tf);
headerLayout->addWidget(title);
headerLayout->addStretch();
panelLayout->addWidget(header);
// ---- 内容区:左参数 + 右表格 ----
QWidget *content = new QWidget(samplingPanel);
QHBoxLayout *contentLayout = new QHBoxLayout(content);
contentLayout->setContentsMargins(6, 0, 6, 6);
contentLayout->setSpacing(12);
// 左:参数区(GridLayout)
QGridLayout *paramLayout = new QGridLayout();
paramLayout->setVerticalSpacing(6);
int row = 0;
// Mode + Log?
QLabel *modeLabel = new QLabel(tr("Mode"), content);
QComboBox *modeCombo = new QComboBox(content);
modeCombo->addItem(tr("Automatic")); // 0
modeCombo->addItem(tr("Manual")); // 1
QCheckBox *logCheck = new QCheckBox(tr("Log?"), content);
QWidget *modeRowWidget = new QWidget(content);
QHBoxLayout *modeRowLayout = new QHBoxLayout(modeRowWidget);
modeRowLayout->setContentsMargins(0, 0, 0, 0);
modeRowLayout->addWidget(modeCombo);
modeRowLayout->addWidget(logCheck);
modeRowLayout->addStretch();
paramLayout->addWidget(modeLabel, row, 0);
paramLayout->addWidget(modeRowWidget, row, 1);
++row;
// Number
QLabel *numLabel = new QLabel(tr("Number"), content);
QSpinBox *numSpin = new QSpinBox(content);
numSpin->setRange(1, 1000);
//numSpin->setValue(1);
numSpin->setButtonSymbols(QAbstractSpinBox::NoButtons);
paramLayout->addWidget(numLabel, row, 0);
paramLayout->addWidget(numSpin, row, 1);
++row;
// 空行(Number 和 Model value 之间)
int gapRow = row;
paramLayout->setRowMinimumHeight(gapRow, 12);
++row;
// Model value + 单位
QLabel *modelLabel = new QLabel(tr("Model value:"), content);
QLineEdit *modelSpin = new QLineEdit(content);
modelSpin->setObjectName("sensitivityModel_" + varName);
// 基准值来自真实模型,只作参照;输入框支持科学计数法和完整精度。
modelSpin->setReadOnly(true);
QLabel *unitLabel = new QLabel(tr(""), content); // ⭐ 单位也从数据加载
QWidget *modelRowWidget = new QWidget(content);
QHBoxLayout *modelRowLayout = new QHBoxLayout(modelRowWidget);
modelRowLayout->setContentsMargins(0, 0, 0, 0);
modelRowLayout->addWidget(modelSpin);
modelRowLayout->addWidget(unitLabel);
modelRowLayout->addStretch();
paramLayout->addWidget(modelLabel, row, 0);
paramLayout->addWidget(modelRowWidget, row, 1);
++row;
// Minimum
QLabel *minLabel = new QLabel(tr("Minimum:"), content);
QLineEdit *minSpin = new QLineEdit(content);
minSpin->setObjectName("sensitivityMinimum_" + varName);
paramLayout->addWidget(minLabel, row, 0);
paramLayout->addWidget(minSpin, row, 1);
++row;
// Maximum
QLabel *maxLabel = new QLabel(tr("Maximum:"), content);
QLineEdit *maxSpin = new QLineEdit(content);
maxSpin->setObjectName("sensitivityMaximum_" + varName);
paramLayout->addWidget(maxLabel, row, 0);
paramLayout->addWidget(maxSpin, row, 1);
++row;
// 统一编辑框宽度
int fullWidth = modelSpin->sizeHint().width();
int editWidth = qMax(150, fullWidth);
modeCombo->setFixedWidth(editWidth);
numSpin->setFixedWidth(editWidth);
modelSpin->setFixedWidth(editWidth);
minSpin->setFixedWidth(editWidth);
maxSpin->setFixedWidth(editWidth);
modeCombo->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
numSpin->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
modelSpin->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
minSpin->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
maxSpin->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
QWidget *paramWidget = new QWidget(content);
QVBoxLayout *leftVBox = new QVBoxLayout(paramWidget);
leftVBox->setContentsMargins(0, 6, 0, 0);
leftVBox->addLayout(paramLayout);
leftVBox->addStretch();
// 右:Values 表格(双列:箭头 + 值)
QTableWidget *valuesTable = new QTableWidget(content);
valuesTable->setObjectName("sensitivityValues_" + varName);
valuesTable->setColumnCount(2);
QStringList headers;
headers << "" << tr("Values");
valuesTable->setHorizontalHeaderLabels(headers);
valuesTable->verticalHeader()->setVisible(false);
valuesTable->setColumnWidth(0, 15);
valuesTable->setColumnWidth(1, 130);
QHeaderView *hh = valuesTable->horizontalHeader();
hh->setResizeMode(QHeaderView::Fixed);
hh->setHighlightSections(false);
valuesTable->setStyleSheet(
"QHeaderView::section {"
" border-top: 0px;"
" border-left: 0px;"
" border-right: 1px solid lightgray;"
" border-bottom: 1px solid lightgray;"
"}"
"QTableCornerButton::section {"
" border-top: 0px;"
" border-left: 0px;"
" border-right: 1px solid lightgray;"
" border-bottom: 1px solid lightgray;"
"}"
);
valuesTable->horizontalHeader()->setStretchLastSection(false);
valuesTable->setEditTriggers(QAbstractItemView::DoubleClicked);
valuesTable->setSelectionBehavior(QAbstractItemView::SelectRows);
valuesTable->setSelectionMode(QAbstractItemView::NoSelection);
valuesTable->setFocusPolicy(Qt::NoFocus);
valuesTable->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
valuesTable->setShowGrid(true);
valuesTable->setItemDelegateForColumn(1, new RightAlignDelegate(valuesTable));
int tableWidth = valuesTable->frameWidth() * 2
+ valuesTable->verticalHeader()->sizeHint().width()
+ valuesTable->columnWidth(0)
+ valuesTable->columnWidth(1);
valuesTable->setFixedWidth(tableWidth);
QWidget *tableContainer = new QWidget(content);
QVBoxLayout *tableLayout = new QVBoxLayout(tableContainer);
tableLayout->setContentsMargins(0, 0, 0, 0);
tableLayout->setSpacing(0);
tableLayout->addWidget(valuesTable);
tableContainer->setFixedWidth(tableWidth);
contentLayout->addWidget(paramWidget, 1);
contentLayout->addStretch();
contentLayout->addWidget(tableContainer, 0, Qt::AlignRight);
panelLayout->addWidget(content);
pageLayout->addWidget(samplingPanel);
page->setEnabled(isSupportedSamplingVariable(varName));
// ===== 保存控件指针 =====
m_modeCombos.append(modeCombo);
m_logChecks.append(logCheck);
m_numSpins.append(numSpin);
m_modelSpins.append(modelSpin);
m_minSpins.append(minSpin);
m_maxSpins.append(maxSpin);
m_valueTables.append(valuesTable);
m_unitLabels.append(unitLabel);
connect(numSpin, SIGNAL(valueChanged(int)),
this, SLOT(onVariableParamChanged()));
connect(minSpin, SIGNAL(textChanged(QString)),
this, SLOT(onVariableParamChanged()));
connect(maxSpin, SIGNAL(textChanged(QString)),
this, SLOT(onVariableParamChanged()));
connect(minSpin, SIGNAL(editingFinished()),
this, SLOT(onSamplingEditingFinished()));
connect(maxSpin, SIGNAL(editingFinished()),
this, SLOT(onSamplingEditingFinished()));
connect(modeCombo, SIGNAL(currentIndexChanged(int)),
this, SLOT(onVariableParamChanged()));
connect(logCheck, SIGNAL(toggled(bool)),
this, SLOT(onVariableParamChanged()));
connect(valuesTable, SIGNAL(cellClicked(int,int)),
this, SLOT(onTableCellClicked(int,int)));
return page;
}
void nmWxSensitive::updateValuesForPage(int idx)
{
if (idx < 0 || idx >= m_numSpins.size()) return;
// 自动模式由范围生成只读表格;手动模式保留每行值,不因选中行而重算。
QTableWidget* table = m_valueTables[idx];
const bool automatic = m_modeCombos[idx]->currentIndex() == 0;
m_minSpins[idx]->setEnabled(automatic);
m_maxSpins[idx]->setEnabled(automatic);
m_logChecks[idx]->setEnabled(automatic);
const int number = m_numSpins[idx]->value();
table->setRowCount(number);
nmDataSensitive::VariableSampling variable;
for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) {
if (findVariableIndex(m_sensitiveData.getVariable(i).getVarName()) == idx) {
variable = m_sensitiveData.getVariable(i);
break;
}
}
QVector<double> samples;
const QString name = variable.getVarName();
const QString unit = m_unitLabels[idx]->text();
if (automatic) {
bool minOk = false, maxOk = false;
double minimum = samplingEditValue(m_minSpins[idx], &minOk);
double maximum = samplingEditValue(m_maxSpins[idx], &maxOk);
if (minOk && maxOk && minimum <= maximum &&
convertSamplingValue(name, minimum, unit, samplingBaseUnit(name), minimum) &&
convertSamplingValue(name, maximum, unit, samplingBaseUnit(name), maximum) &&
validSamplingValue(name, minimum) && validSamplingValue(name, maximum)) {
variable.setMode(nmDataSensitive::VariableSampling::MODE_AUTOMATIC);
variable.setNumber(number);
variable.setLog(m_logChecks[idx]->isChecked());
variable.getMinValue().setValue(minimum);
variable.getMaxValue().setValue(maximum);
samples = variable.samplingValues();
}
}
for (int row = 0; row < number; ++row) {
QTableWidgetItem* arrow = table->item(row, 0);
if (!arrow) { arrow = new QTableWidgetItem(); table->setItem(row, 0, arrow); }
arrow->setFlags(Qt::ItemIsEnabled);
arrow->setIcon(row == m_selectedValueIndex ? createTriangleIcon() : QIcon());
QTableWidgetItem* item = table->item(row, 1);
if (!item) { item = new QTableWidgetItem(); table->setItem(row, 1, item); }
item->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter);
item->setFlags(automatic ? Qt::ItemIsEnabled : Qt::ItemIsEnabled | Qt::ItemIsEditable);
if (automatic) {
double display = row < samples.size() ? samples[row] : 0.0;
const bool valid = row < samples.size() &&
convertSamplingValue(name, display, samplingBaseUnit(name), unit, display);
item->setData(Qt::UserRole, valid ? QVariant(display) : QVariant());
item->setText(valid ? formatSamplingValue(display, 3) : QString());
}
}
}
void nmWxSensitive::onSamplingEditingFinished()
{
// 输入结束后截断越界范围,再按六位有效数字显示,计算仍使用原始精度。
QLineEdit* edit = qobject_cast<QLineEdit*>(sender());
if (m_loading || !edit) return;
bool ok = false;
double value = samplingEditValue(edit, &ok);
if (!ok || !qIsFinite(value)) return;
for (int idx = 0; idx < m_minSpins.size(); ++idx) {
if (edit != m_minSpins[idx] && edit != m_maxSpins[idx]) continue;
for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) {
const QString name = m_sensitiveData.getVariable(i).getVarName();
if (findVariableIndex(name) != idx) continue;
double lower = 0.0, upper = 0.0;
const QString unit = m_unitLabels[idx]->text();
const QString baseUnit = samplingBaseUnit(name);
if (!samplingPhysicalRange(name, lower, upper) ||
!convertSamplingValue(name, value, unit, baseUnit, value)) return;
value = qBound(lower, value, upper);
if (!convertSamplingValue(name, value, baseUnit, unit, value)) return;
setSamplingEditValue(edit, value);
updateValuesForPage(idx);
return;
}
}
}
void nmWxSensitive::onVariableParamChanged()
{
// 数据加载时不响应控件逐项赋值,编辑时只刷新所属参数页。
if (m_loading) return;
for (int i = 0; i < m_numSpins.size(); ++i) {
if (sender() == m_numSpins[i] || sender() == m_minSpins[i] ||
sender() == m_maxSpins[i] || sender() == m_modeCombos[i] ||
sender() == m_logChecks[i]) {
m_selectedValueIndex = qMin(m_selectedValueIndex, m_numSpins[i]->value() - 1);
updateValuesForPage(i);
break;
}
}
updateModelCount();
}
void nmWxSensitive::onTableCellClicked(int row, int column)
{
Q_UNUSED(column);
// 选中标记不改写表格内容,避免自动重算或丢失手动输入。
m_selectedValueIndex = row;
QTableWidget* table = qobject_cast<QTableWidget*>(sender());
if (!table) return;
for (int i = 0; i < table->rowCount(); ++i)
if (table->item(i, 0))
table->item(i, 0)->setIcon(i == row ? createTriangleIcon() : QIcon());
}
void nmWxSensitive::onTreeItemChanged(QTreeWidgetItem* item, int column)
{
Q_UNUSED(column);
if (m_loading) return;
// 分组选择与子项选择双向同步,禁用的参数不参与全选和总次数。
m_loading = true;
variablesTree->blockSignals(true);
if (item && item->childCount() > 0) {
const Qt::CheckState state = item->checkState(0);
for (int i = 0; i < item->childCount(); ++i)
if (!item->child(i)->isDisabled()) item->child(i)->setCheckState(0, state);
}
for (int group = 0; group < variablesTree->topLevelItemCount(); ++group) {
QTreeWidgetItem* parent = variablesTree->topLevelItem(group);
int available = 0, checked = 0;
for (int i = 0; i < parent->childCount(); ++i) {
if (parent->child(i)->isDisabled()) continue;
++available;
if (parent->child(i)->checkState(0) == Qt::Checked) ++checked;
}
parent->setCheckState(0, checked == 0 ? Qt::Unchecked
: checked == available ? Qt::Checked : Qt::PartiallyChecked);
parent->setDisabled(available == 0);
}
variablesTree->blockSignals(false);
m_loading = false;
updateModelCount();
}
void nmWxSensitive::onGenerateClicked()
{
if(m_calculationRunning || !m_pDataManager || !saveDataFromUI()) return;
nmDataWellBase* well = m_pDataManager->findWellByCode(m_pDataManager->getPrimaryWellCode());
if(!well || well->getWellName().isEmpty()) {
QMessageBox::warning(this, tr("Sensitivity"), tr("The target well is unavailable."));
return;
}
// 气井转换器使用本窗口所属分析的配置,不能随当前活动窗口切换。
if(m_pDataManager->getSolverModelType() == SMT_Gas_VariablePvt) {
iSubWndFitting* fitting = nullptr;
QMap<iSubWndFitting*, nmDataAnalyzeManager*>::const_iterator it =
nmDataAnalyzeManager::s_mapDataAnalManager.constBegin();
for(; it != nmDataAnalyzeManager::s_mapDataAnalManager.constEnd(); ++it) {
if(it.value() == m_pDataManager) { fitting = it.key(); break; }
}
iAnalRun* analRun = fitting ? fitting->getAnalRun() : nullptr;
if(!analRun || !analRun->configPsAbouts(true, fitting->getModelOption(),
false, fitting->getAllWxPtr())) {
QMessageBox::warning(this, tr("Sensitivity"),
tr("Gas pseudo-pressure data is unavailable or invalid."));
return;
}
}
QString error;
QScopedPointer<nmCalculationDllPebiSolverTask> task;
try {
// 在管理器所属的界面线程捕获基准并复制一次,后台任务不持有管理器。
nmCalculationDllPebiSolverTask source(QString(), m_pDataManager, well->getWellName());
task.reset(source.createSensitivityBatch(m_sensitiveData, &error));
} catch(const std::bad_alloc&) {
error = tr("Insufficient memory to prepare solver input.");
}
if(!task) {
QMessageBox::warning(this, tr("Sensitivity"), error);
return;
}
m_pDataManager->updateSensitiveData(m_sensitiveData);
// 新任务准备成功后才替换上次临时结果,准备失败仍可保留旧结果。
delete m_pSensitivityTask;
m_pSensitivityTask = task.take();
m_pSensitivityTask->setParent(this);
// 进度框和停止按钮由主窗口统一管理;接管失败时不启动本次任务。
emit sigSensitivityTaskReady(this, m_pSensitivityTask);
if(!m_calculationRunning) {
delete m_pSensitivityTask;
m_pSensitivityTask = nullptr;
}
}
void nmWxSensitive::startSensitivityCalculation()
{
// 读取和清理必须等 QThread 真正结束,不能由 run() 内的提前通知触发。
connect(m_pSensitivityTask, SIGNAL(finished()),
this, SLOT(onSensitivityFinished()), Qt::QueuedConnection);
setCalculationRunning(true);
// 仅关闭参数界面;控制器及任务仍存活,结束后继续保留临时计算结果。
hide();
m_pSensitivityTask->start();
}
void nmWxSensitive::onCancelClicked()
{
if(m_calculationRunning && m_pSensitivityTask) {
// 关闭或 Esc 也走主窗口现有停止路径,保持进度框的等待状态一致。
emit sigSensitivityCancelRequested();
return;
}
reject();
}
void nmWxSensitive::setCalculationRunning(bool running)
{
// 计算过程中固定采样输入,仅保留停止操作;结束后恢复编辑与再次生成。
m_calculationRunning = running;
calculationTypeCombo->setEnabled(!running);
variablesTree->setEnabled(!running);
detailStack->setEnabled(!running);
previewButton->setEnabled(!running);
cancelButton->setEnabled(!running);
if(running) {
generateButton->setEnabled(false);
} else {
updateModelCount();
}
}
void nmWxSensitive::onSensitivityFinished()
{
if(sender() != m_pSensitivityTask) return;
m_pSensitivityTask->wait();
// 数据层接管曲线后释放计算任务及网格输入,结果窗口不依赖任务存活。
QSharedPointer<nmDataSensitive::BatchResult> batch = m_pSensitivityTask->getSensitivityBatchResult();
delete m_pSensitivityTask;
m_pSensitivityTask = nullptr;
setCalculationRunning(false);
m_sensitiveData.setResults(batch);
m_pDataManager->publishSensitiveResults(m_sensitiveData);
const std::vector<nmDataSensitive::RunResult>& results = batch->m_runs;
QStringList errors;
for(size_t i = 0; i < results.size() && errors.size() < 5; ++i) {
const nmDataSensitive::RunResult& result = results[i];
if(!result.m_bSucceeded && !result.m_bCancelled)
errors.append(tr("Run %1: %2").arg(result.m_nRunIndex + 1).arg(result.m_sFailureReason));
}
const QString fatalError = batch->m_sFailureReason;
// 实际计算错误仍需提示原因,取消组不作为错误报告。
if(fatalError.isEmpty() && errors.isEmpty()) return;
QString message = fatalError;
if(!errors.isEmpty()) {
if(!message.isEmpty()) message += "\n\n";
message += errors.join("\n");
}
QWidget* ownerWindow = parentWidget() ? parentWidget()->window() : nullptr;
QMessageBox::warning(ownerWindow, tr("Sensitivity"), message);
}
void nmWxSensitive::onResultsClicked()
{
// 绘图窗口仍由主界面创建;本窗口只提供运行列表与显示选择。
onResultsChanged();
if(!resultsButton->isEnabled()) return;
emit sigSensitiveResultsRequested();
if(m_resultsDialog.isNull()) {
m_resultsDialog = new QDialog(this);
m_resultsDialog->setAttribute(Qt::WA_DeleteOnClose);
m_resultsDialog->setWindowTitle(tr("Sensitivity Results"));
m_resultsDialog->resize(700, 380);
QVBoxLayout* layout = new QVBoxLayout(m_resultsDialog.data());
QTableView* table = new QTableView(m_resultsDialog.data());
m_resultsModel = new SensitivityResultsModel(m_resultsDialog.data());
m_resultsModel->setResults(m_pDataManager->getSensitiveData()->getResults());
table->setModel(m_resultsModel.data());
table->setSelectionBehavior(QAbstractItemView::SelectRows);
table->setSelectionMode(QAbstractItemView::SingleSelection);
table->setEditTriggers(QAbstractItemView::NoEditTriggers);
table->setWordWrap(false);
table->verticalHeader()->setVisible(false);
table->horizontalHeader()->setResizeMode(0, QHeaderView::ResizeToContents);
table->horizontalHeader()->setResizeMode(1, QHeaderView::ResizeToContents);
table->horizontalHeader()->setResizeMode(2, QHeaderView::Stretch);
layout->addWidget(table);
QHBoxLayout* buttons = new QHBoxLayout();
QPushButton* showAll = new QPushButton(tr("Show all"), m_resultsDialog.data());
QPushButton* hideAll = new QPushButton(tr("Hide all"), m_resultsDialog.data());
// 明确按钮顺序,避免系统的标准按钮布局把 Apply 排到最后。
QPushButton* apply = new QPushButton(tr("Apply"), m_resultsDialog.data());
QPushButton* ok = new QPushButton(tr("OK"), m_resultsDialog.data());
QPushButton* cancel = new QPushButton(tr("Cancel"), m_resultsDialog.data());
apply->setAutoDefault(false);
cancel->setAutoDefault(false);
ok->setDefault(true);
buttons->addWidget(showAll);
buttons->addWidget(hideAll);
buttons->addStretch();
buttons->addWidget(apply);
buttons->addWidget(ok);
buttons->addWidget(cancel);
layout->addLayout(buttons);
connect(showAll, SIGNAL(clicked()), this, SLOT(onShowAllResults()));
connect(hideAll, SIGNAL(clicked()), this, SLOT(onHideAllResults()));
connect(apply, SIGNAL(clicked()), this, SLOT(onApplyResults()));
connect(ok, SIGNAL(clicked()), this, SLOT(onAcceptResults()));
connect(cancel, SIGNAL(clicked()), m_resultsDialog.data(), SLOT(reject()));
}
onResultsChanged();
hide();
m_resultsDialog->show();
m_resultsDialog->raise();
m_resultsDialog->activateWindow();
}
void nmWxSensitive::onResultsChanged()
{
// 新结果替换旧批次时,已经打开的列表也切到同一份结果。
QSharedPointer<nmDataSensitive::BatchResult> results;
if(m_pDataManager && m_pDataManager->getSensitiveData())
results = m_pDataManager->getSensitiveData()->getResults();
resultsButton->setEnabled(!results.isNull());
m_sensitiveData.setResults(results);
if(!m_resultsModel.isNull()) m_resultsModel->setResults(results);
// 新模型清除旧批次后关闭结果列表,不能继续操作上一模型的运行记录。
if(results.isNull()) {
if(!m_resultsDialog.isNull()) m_resultsDialog->close();
return;
}
if(!m_resultsDialog.isNull()) {
const QString status = results.isNull() ? QString() : results->m_bStopped ? tr("Stopped")
: results->m_bCompleted ? tr("Completed") : tr("Failed");
m_resultsDialog->setWindowTitle(tr("Sensitivity Results")
+ (status.isEmpty() ? QString() : " - " + status));
}
}
void nmWxSensitive::onApplyResults()
{
// 应用选择后通知主界面刷新,结果选择窗口保持打开。
if(m_pDataManager && !m_resultsModel.isNull() && m_resultsModel->applyVisibility())
m_pDataManager->publishSensitiveResults(m_pDataManager->getSensitiveDataCopy());
}
void nmWxSensitive::onAcceptResults()
{
// OK 与 Apply 提交相同的选择,随后关闭窗口。
onApplyResults();
if(!m_resultsDialog.isNull()) m_resultsDialog->accept();
}
void nmWxSensitive::onShowAllResults()
{
// 勾选所有成功组,等待 Apply 或 OK 更新曲线。
if(!m_resultsModel.isNull()) m_resultsModel->setAllVisible(true);
}
void nmWxSensitive::onHideAllResults()
{
// 取消所有采样组的勾选,测量曲线和原模型数值解仍作为参考。
if(!m_resultsModel.isNull()) m_resultsModel->setAllVisible(false);
}
void nmWxSensitive::onCalculationTypeChanged(int index)
{
Q_UNUSED(index);
// 两种确定性方法共用参数设置,仅切换总次数及组合规则。
updateModelCount();
}
void nmWxSensitive::onTreeItemClicked(QTreeWidgetItem* item, int column)
{
Q_UNUSED(column);
if (item->childCount() > 0) { detailStack->setCurrentIndex(0); return; }
const int pageIndex = item->data(0, Qt::UserRole).toInt();
if (pageIndex <= 0) return;
detailStack->setCurrentIndex(pageIndex);
// 页面切换只更新箭头,保留用户正在编辑的采样表。
m_selectedValueIndex = 0;
QTableWidget* table = m_valueTables[pageIndex - 1];
for (int row = 0; row < table->rowCount(); ++row)
if (table->item(row, 0))
table->item(row, 0)->setIcon(row == 0 ? createTriangleIcon() : QIcon());
}
void nmWxSensitive::onPreviewClicked()
{
if (!saveDataFromUI()) return;
// Saphir 风格的连续运行列表,组合数再大也不显示分页控件。
QDialog dialog(this);
dialog.setWindowTitle(tr("Sensitivity Runs"));
dialog.setWindowFlags(dialog.windowFlags() & ~Qt::WindowContextHelpButtonHint);
dialog.setMinimumSize(420, 360);
dialog.resize(470, 650);
QVBoxLayout* layout = new QVBoxLayout(&dialog);
layout->setContentsMargins(16, 16, 16, 12);
layout->setSpacing(8);
m_previewView = new SensitivityRunsView(m_sensitiveData, createTriangleIcon(), &dialog);
layout->addWidget(m_previewView);
QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Close, Qt::Horizontal, &dialog);
buttons->setCenterButtons(true);
buttons->button(QDialogButtonBox::Close)->setMinimumWidth(110);
layout->addWidget(buttons);
connect(buttons, SIGNAL(rejected()), &dialog, SLOT(reject()));
connect(m_previewView->verticalScrollBar(), SIGNAL(actionTriggered(int)),
this, SLOT(onPreviewScrollAction(int)));
dialog.exec();
m_previewView = nullptr;
}
void nmWxSensitive::onPreviewScrollAction(int action)
{
// 将原生滚动条的单步、整屏和拖动操作转交连续列表。
if (m_previewView) m_previewView->scrollAction(action);
}