|
|
#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 "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 <QDesktopWidget>
|
|
|
#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>
|
|
|
|
|
|
#include <math.h>
|
|
|
#include <float.h>
|
|
|
#include <limits.h>
|
|
|
|
|
|
namespace
|
|
|
{
|
|
|
const char* distributionFields[] = {"mean", "stdDev", "peak", "minimum", "maximum"};
|
|
|
|
|
|
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 SensitivityDistributionPlot : public QWidget
|
|
|
{
|
|
|
public:
|
|
|
explicit SensitivityDistributionPlot(QWidget* parent) : QWidget(parent), m_valid(false)
|
|
|
{
|
|
|
// 分布图复用参数分位数计算,避免绘图与实际采样使用两套分布定义。
|
|
|
setMinimumSize(340, 300);
|
|
|
}
|
|
|
void setVariable(const nmDataSensitive::VariableSampling& variable, bool valid)
|
|
|
{
|
|
|
m_variable = variable;
|
|
|
m_valid = valid;
|
|
|
update();
|
|
|
}
|
|
|
protected:
|
|
|
void paintEvent(QPaintEvent*) override
|
|
|
{
|
|
|
// 蓝线为累积概率,红色虚线为按峰值归一化的概率密度,与 Saphir 的显示一致。
|
|
|
QPainter painter(this);
|
|
|
painter.fillRect(rect(), palette().color(QPalette::Window));
|
|
|
painter.setRenderHint(QPainter::Antialiasing);
|
|
|
QVector<double> x(201), cumulativeValues(201);
|
|
|
const bool bounded = m_variable.m_distribution >= nmDataSensitive::VariableSampling::TRUNCATED_NORMAL;
|
|
|
bool valid = m_valid;
|
|
|
for(int i = 0; valid && i < x.size(); ++i) {
|
|
|
// 两端加密以平滑显示尾部,有界分布的横轴保留精确上下限。
|
|
|
const double probability = (1.0 - std::cos(3.141592653589793 * i / 200.0)) * 0.5;
|
|
|
cumulativeValues[i] = bounded ? probability : 0.0001 + 0.9998 * probability;
|
|
|
valid = m_variable.distributionQuantile(qBound(1e-10, cumulativeValues[i], 1.0 - 1e-10), x[i]);
|
|
|
if(bounded && i == 0) x[i] = m_variable.m_distributionMin;
|
|
|
if(bounded && i == 200) x[i] = m_variable.m_distributionMax;
|
|
|
valid = valid && convertSamplingValue(m_variable.m_varName, x[i], samplingBaseUnit(m_variable.m_varName),
|
|
|
m_variable.m_modelValue.getUnit(), x[i]);
|
|
|
}
|
|
|
if(!valid || x.last() <= x.first()) {
|
|
|
painter.drawText(rect(), Qt::AlignCenter, nmWxSensitive::tr("Invalid distribution parameters"));
|
|
|
return;
|
|
|
}
|
|
|
// 坐标外侧沿用窗口底色,绘图区留白,标题放在纵轴左侧。
|
|
|
const QRectF plot(76, 12, width() - 96, height() - 66);
|
|
|
painter.fillRect(plot, Qt::white);
|
|
|
const QRectF dataArea = plot.adjusted(12, 16, -12, -16);
|
|
|
// 横轴采用 1、2、5 的整齐步长,按分布范围自适应,不限制实际采样值。
|
|
|
const double roughStep = (x.last() - x.first()) / 5.0;
|
|
|
const double scale = std::pow(10.0, std::floor(std::log10(roughStep)));
|
|
|
const double fraction = roughStep / scale;
|
|
|
const double step = (fraction <= 1.0 ? 1.0 : fraction <= 2.0 ? 2.0 : fraction <= 5.0 ? 5.0 : 10.0) * scale;
|
|
|
const double axisMin = std::floor(x.first() / step) * step;
|
|
|
const double axisMax = std::ceil(x.last() / step) * step;
|
|
|
QVector<double> density(201);
|
|
|
double peak = 0.0;
|
|
|
for(int i = 0; i < x.size(); ++i) {
|
|
|
const int a = qMax(0, i - 1), b = qMin(200, i + 1);
|
|
|
density[i] = x[b] > x[a] ? (cumulativeValues[b] - cumulativeValues[a]) / (x[b] - x[a]) : 0.0;
|
|
|
if(m_variable.m_distribution == nmDataSensitive::VariableSampling::TRIANGULAR &&
|
|
|
((i == 0 && m_variable.m_peak > m_variable.m_distributionMin) ||
|
|
|
(i == 200 && m_variable.m_peak < m_variable.m_distributionMax))) density[i] = 0.0;
|
|
|
peak = qMax(peak, density[i]);
|
|
|
}
|
|
|
// 蓝色点状主网格配合黑色主、次刻度,概率轴按 0.1 标注。
|
|
|
const int intervals = qRound((axisMax - axisMin) / step);
|
|
|
for(int i = 0; i <= intervals * 5; ++i) {
|
|
|
const double value = axisMin + i * step / 5.0;
|
|
|
const double px = dataArea.left() + (value - axisMin) / (axisMax - axisMin) * dataArea.width();
|
|
|
if(i % 5 == 0) {
|
|
|
painter.setPen(QPen(QColor(100, 120, 255), 1, Qt::DotLine));
|
|
|
painter.drawLine(QPointF(px, plot.top()), QPointF(px, plot.bottom()));
|
|
|
painter.setPen(Qt::black);
|
|
|
painter.drawText(QRectF(px - 36, plot.bottom() + 8, 72, 20), Qt::AlignCenter,
|
|
|
formatSamplingValue(value, 4));
|
|
|
}
|
|
|
painter.setPen(Qt::black);
|
|
|
painter.drawLine(QPointF(px, plot.bottom()), QPointF(px, plot.bottom() + (i % 5 == 0 ? 7 : 4)));
|
|
|
}
|
|
|
for(int i = 0; i <= 50; ++i) {
|
|
|
const double ratio = i / 50.0;
|
|
|
const double py = dataArea.bottom() - ratio * dataArea.height();
|
|
|
if(i % 10 == 0) {
|
|
|
painter.setPen(QPen(QColor(100, 120, 255), 1, Qt::DotLine));
|
|
|
painter.drawLine(QPointF(plot.left(), py), QPointF(plot.right(), py));
|
|
|
}
|
|
|
painter.setPen(Qt::black);
|
|
|
painter.drawLine(QPointF(plot.left(), py), QPointF(plot.left() - (i % 5 == 0 ? 7 : 4), py));
|
|
|
if(i % 5 == 0)
|
|
|
painter.drawText(QRectF(28, py - 10, plot.left() - 40, 20), Qt::AlignRight | Qt::AlignVCenter,
|
|
|
QString::number(ratio));
|
|
|
}
|
|
|
QPolygonF cumulative, probability;
|
|
|
for(int i = 0; i < x.size(); ++i) {
|
|
|
const double px = dataArea.left() + (x[i] - axisMin) / (axisMax - axisMin) * dataArea.width();
|
|
|
cumulative << QPointF(px, dataArea.bottom() - cumulativeValues[i] * dataArea.height());
|
|
|
probability << QPointF(px, dataArea.bottom() - (peak > 0.0 ? density[i] / peak : 0.0) * dataArea.height());
|
|
|
}
|
|
|
painter.setPen(QPen(Qt::blue, 1.5));
|
|
|
painter.drawPolyline(cumulative);
|
|
|
painter.setPen(QPen(Qt::red, 1, Qt::DashLine));
|
|
|
painter.drawPolyline(probability);
|
|
|
painter.setPen(Qt::black);
|
|
|
painter.drawRect(plot);
|
|
|
painter.save();
|
|
|
painter.translate(17, plot.center().y());
|
|
|
painter.rotate(-90);
|
|
|
painter.drawText(QRectF(-plot.height() / 2, -10, plot.height(), 20), Qt::AlignCenter,
|
|
|
nmWxSensitive::tr("Probability distribution function"));
|
|
|
painter.restore();
|
|
|
painter.drawText(QRectF(plot.left(), height() - 22, plot.width(), 20), Qt::AlignCenter,
|
|
|
m_variable.m_varName + " [" + m_variable.m_modelValue.getUnit() + "]");
|
|
|
}
|
|
|
private:
|
|
|
nmDataSensitive::VariableSampling m_variable;
|
|
|
bool m_valid;
|
|
|
};
|
|
|
|
|
|
// ========= 右对齐委托:保证显示和编辑时都是右对齐 =========
|
|
|
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();
|
|
|
const QString wellCode = !snapshot.isNull() ? snapshot->getSensitivityWellCode() : QString();
|
|
|
nmDataWellBase* well = m_pDataManager ? m_pDataManager->findWellByCode(wellCode) : nullptr;
|
|
|
nmDataReservoir* reservoir = m_pDataManager ? m_pDataManager->getReservoirData() : nullptr;
|
|
|
const nmPebiResultWellSnapshot* solvedWell = nullptr;
|
|
|
for(int i = 0; !snapshot.isNull() && i < snapshot->getWellCount(); ++i) {
|
|
|
if(snapshot->getWellAt(i)->m_sWellCode == wellCode) {
|
|
|
solvedWell = snapshot->getWellAt(i);
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
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 (!snapshot.isNull()) {
|
|
|
if (attribute) solvedAttribute = *attribute;
|
|
|
else solvedAttribute.setUnit(samplingBaseUnit(name));
|
|
|
attribute = &solvedAttribute;
|
|
|
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);
|
|
|
}
|
|
|
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);
|
|
|
if(!variable.m_distributionInitialized || resetRanges) {
|
|
|
// Saphir 默认正态均值为模型值、标准差为其三分之一;Skin=0 时使用 5/3。
|
|
|
const double model = attribute->getValue().toDouble();
|
|
|
variable.m_mean = model;
|
|
|
variable.m_stdDev = model == 0.0 ? 5.0 / 3.0 : std::fabs(model) / 3.0;
|
|
|
variable.m_distributionMin = minimum;
|
|
|
variable.m_distributionMax = maximum;
|
|
|
// Monte-Carlo 的 Skin 分布允许负值,不能沿用求解器截断后的范围。
|
|
|
if(name == "Skin") {
|
|
|
variable.m_distributionMin = model == 0.0 ? -1.0 : qMin(model * 0.9, model * 1.1);
|
|
|
variable.m_distributionMax = model == 0.0 ? 1.0 : qMax(model * 0.9, model * 1.1);
|
|
|
}
|
|
|
variable.m_peak = qBound(variable.m_distributionMin, model * 1.01, variable.m_distributionMax);
|
|
|
variable.m_distributionInitialized = true;
|
|
|
}
|
|
|
// 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()));
|
|
|
m_monteCarloCount->setValue(m_sensitiveData.getMonteCarloCount());
|
|
|
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);
|
|
|
m_distributionCombos[idx]->setCurrentIndex(int(variable.m_distribution));
|
|
|
const double distributionValues[] = {variable.m_mean, variable.m_stdDev, variable.m_peak,
|
|
|
variable.m_distributionMin, variable.m_distributionMax};
|
|
|
for(int field = 0; field < 5; ++field) {
|
|
|
double display = distributionValues[field];
|
|
|
convertSamplingValue(name, display, samplingBaseUnit(name), unit, display);
|
|
|
setSamplingEditValue(m_distributionPanels[idx]->findChild<QLineEdit*>(distributionFields[field]), display);
|
|
|
}
|
|
|
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;
|
|
|
m_sensitiveData.setMonteCarloCount(m_monteCarloCount->value());
|
|
|
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);
|
|
|
const bool monteCarlo = m_sensitiveData.getCalculationType() == nmDataSensitive::CALC_MONTE_CARLO;
|
|
|
modelCountLabel->setVisible(!monteCarlo);
|
|
|
m_monteCarloCount->setVisible(monteCarlo);
|
|
|
generateButton->setEnabled(!monteCarlo && m_pDataManager && !overflow && count > 0 && !m_calculationRunning);
|
|
|
generateButton->setToolTip(monteCarlo ? tr("Monte-Carlo sampling preview is available; calculation is not yet available.") : QString());
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
if(candidate.getCalculationType() == nmDataSensitive::CALC_MONTE_CARLO) {
|
|
|
double median = 0.0;
|
|
|
const bool valid = readDistributionPage(idx, variable) && variable.distributionQuantile(0.5, median);
|
|
|
if(!valid && variable.getEnabled()) {
|
|
|
QMessageBox::warning(this, tr("Sensitivity"), tr("%1: invalid distribution parameters.").arg(name));
|
|
|
detailStack->setCurrentIndex(idx + 1);
|
|
|
return false;
|
|
|
}
|
|
|
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;
|
|
|
}
|
|
|
}
|
|
|
m_sensitiveData = candidate;
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
void nmWxSensitive::setupUI()
|
|
|
{
|
|
|
setWindowTitle(tr("Sensitivity"));
|
|
|
setMinimumSize(900, 550);
|
|
|
// 默认放大窗口,同时为屏幕边缘和窗口标题栏留出空间。
|
|
|
const QSize available = QApplication::desktop()->availableGeometry(this).size() - QSize(60, 80);
|
|
|
resize(QSize(1250, 760).boundedTo(available));
|
|
|
|
|
|
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);
|
|
|
centerSplitter->setSizes(QList<int>() << 260 << 960);
|
|
|
|
|
|
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);
|
|
|
m_monteCarloCount = new QSpinBox(this);
|
|
|
m_monteCarloCount->setObjectName("sensitivityMonteCarloCount");
|
|
|
m_monteCarloCount->setRange(1, 100000);
|
|
|
m_monteCarloCount->setValue(m_sensitiveData.getMonteCarloCount());
|
|
|
m_monteCarloCount->setButtonSymbols(QAbstractSpinBox::NoButtons);
|
|
|
m_monteCarloCount->setVisible(false);
|
|
|
|
|
|
bottomLayout->addWidget(totalLabel);
|
|
|
bottomLayout->addWidget(modelCountLabel);
|
|
|
bottomLayout->addWidget(m_monteCarloCount);
|
|
|
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(m_monteCarloCount, SIGNAL(valueChanged(int)), this, SLOT(onVariableParamChanged()));
|
|
|
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);
|
|
|
QComboBox* distributionCombo = new QComboBox(content);
|
|
|
distributionCombo->setObjectName("sensitivityDistribution_" + varName);
|
|
|
distributionCombo->addItems(QStringList() << tr("Normal") << tr("Lognormal")
|
|
|
<< tr("Truncated normal") << tr("Truncated lognormal") << tr("Uniform") << tr("Triangular"));
|
|
|
distributionCombo->setFixedWidth(180);
|
|
|
modeRowLayout->addWidget(distributionCombo);
|
|
|
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;
|
|
|
|
|
|
// 模式、数量与下方采样参数之间留出约三行空白。
|
|
|
int gapRow = row;
|
|
|
paramLayout->setRowMinimumHeight(gapRow, numSpin->sizeHint().height() * 3);
|
|
|
++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;
|
|
|
|
|
|
// Monte-Carlo 使用独立分布字段,切回确定性方法时保留原范围与手动表格。
|
|
|
QWidget* distributionPanel = new QWidget(content);
|
|
|
QGridLayout* distributionLayout = new QGridLayout(distributionPanel);
|
|
|
distributionLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
distributionLayout->setHorizontalSpacing(6);
|
|
|
distributionLayout->setVerticalSpacing(0);
|
|
|
distributionLayout->setColumnMinimumWidth(0, modelLabel->sizeHint().width());
|
|
|
const int fieldHeight = modelSpin->sizeHint().height();
|
|
|
// Mean 与三角分布的 Mode 共用一行,其余字段保持固定位置。
|
|
|
distributionPanel->setFixedHeight(fieldHeight * 4 + 6 * 3);
|
|
|
const QStringList distributionLabels = QStringList() << tr("Mean:") << tr("Std. dev.:")
|
|
|
<< tr("Mode:") << tr("Minimum:") << tr("Maximum:");
|
|
|
for(int field = 0; field < 5; ++field) {
|
|
|
QLineEdit* edit = new QLineEdit(distributionPanel);
|
|
|
edit->setObjectName(distributionFields[field]);
|
|
|
edit->setFixedSize(qMax(150, modelSpin->sizeHint().width()), fieldHeight);
|
|
|
QLabel* label = new QLabel(distributionLabels[field], distributionPanel);
|
|
|
label->setObjectName(QString(distributionFields[field]) + "Label");
|
|
|
label->setFixedHeight(fieldHeight);
|
|
|
const int fieldRow = field == 2 ? 0 : field > 2 ? field - 1 : field;
|
|
|
distributionLayout->setRowMinimumHeight(fieldRow, fieldHeight + (fieldRow < 3 ? 6 : 0));
|
|
|
distributionLayout->addWidget(label, fieldRow, 0, Qt::AlignTop);
|
|
|
distributionLayout->addWidget(edit, fieldRow, 1, Qt::AlignTop);
|
|
|
connect(edit, SIGNAL(textChanged(QString)), this, SLOT(onVariableParamChanged()));
|
|
|
}
|
|
|
distributionLayout->setColumnStretch(2, 1);
|
|
|
paramLayout->addWidget(distributionPanel, row++, 0, 1, 2);
|
|
|
|
|
|
// 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);
|
|
|
QHeaderView *hh = valuesTable->horizontalHeader();
|
|
|
hh->setResizeMode(QHeaderView::Fixed);
|
|
|
hh->setHighlightSections(false);
|
|
|
// 箭头列固定,数值列在下方设置为填满加宽后的表格。
|
|
|
valuesTable->setColumnWidth(0, 15);
|
|
|
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(true);
|
|
|
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));
|
|
|
|
|
|
// 初始空表的列宽尚未稳定,直接固定容器宽度,不从未显示的表头反算。
|
|
|
const int tableWidth = 190;
|
|
|
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);
|
|
|
SensitivityDistributionPlot* distributionPlot = new SensitivityDistributionPlot(content);
|
|
|
contentLayout->addWidget(distributionPlot, 2);
|
|
|
|
|
|
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);
|
|
|
m_numLabels.append(numLabel);
|
|
|
m_minLabels.append(minLabel);
|
|
|
m_maxLabels.append(maxLabel);
|
|
|
m_distributionCombos.append(distributionCombo);
|
|
|
m_distributionPanels.append(distributionPanel);
|
|
|
m_distributionPlots.append(distributionPlot);
|
|
|
connect(distributionCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onVariableParamChanged()));
|
|
|
|
|
|
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;
|
|
|
const bool monteCarlo = calculationTypeCombo->currentIndex() == nmDataSensitive::CALC_MONTE_CARLO;
|
|
|
m_modeCombos[idx]->setVisible(!monteCarlo);
|
|
|
m_logChecks[idx]->setVisible(!monteCarlo);
|
|
|
m_numLabels[idx]->setVisible(!monteCarlo);
|
|
|
m_numSpins[idx]->setVisible(!monteCarlo);
|
|
|
m_minLabels[idx]->setVisible(!monteCarlo);
|
|
|
m_maxLabels[idx]->setVisible(!monteCarlo);
|
|
|
m_minSpins[idx]->setVisible(!monteCarlo);
|
|
|
m_maxSpins[idx]->setVisible(!monteCarlo);
|
|
|
m_valueTables[idx]->parentWidget()->setVisible(!monteCarlo);
|
|
|
m_distributionCombos[idx]->setVisible(monteCarlo);
|
|
|
m_distributionPanels[idx]->setVisible(monteCarlo);
|
|
|
m_distributionPlots[idx]->setVisible(monteCarlo);
|
|
|
// Monte-Carlo 参数区只占所需宽度,窗口增宽时优先扩展分布图。
|
|
|
QHBoxLayout* contentLayout = static_cast<QHBoxLayout*>(m_distributionPlots[idx]->parentWidget()->layout());
|
|
|
contentLayout->setStretch(0, monteCarlo ? 0 : 1);
|
|
|
if(monteCarlo) { updateDistributionPage(idx); 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());
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
bool nmWxSensitive::readDistributionPage(int idx, nmDataSensitive::VariableSampling& variable) const
|
|
|
{
|
|
|
// 字段转换沿用现有参数单位,仅校验当前分布实际使用的字段。
|
|
|
variable.m_distribution = static_cast<nmDataSensitive::VariableSampling::Distribution>(
|
|
|
m_distributionCombos[idx]->currentIndex());
|
|
|
variable.m_modelValue.setUnit(m_unitLabels[idx]->text());
|
|
|
double* fields[] = {&variable.m_mean, &variable.m_stdDev, &variable.m_peak,
|
|
|
&variable.m_distributionMin, &variable.m_distributionMax};
|
|
|
const int distribution = variable.m_distribution;
|
|
|
for(int field = 0; field < 5; ++field) {
|
|
|
const bool used = field < 2 ? distribution < nmDataSensitive::VariableSampling::UNIFORM
|
|
|
: field == 2 ? distribution == nmDataSensitive::VariableSampling::TRIANGULAR
|
|
|
: distribution >= nmDataSensitive::VariableSampling::TRUNCATED_NORMAL;
|
|
|
if(!used) continue;
|
|
|
bool ok = false;
|
|
|
double value = samplingEditValue(m_distributionPanels[idx]->findChild<QLineEdit*>(distributionFields[field]), &ok);
|
|
|
if(!ok || !qIsFinite(value) || !convertSamplingValue(variable.m_varName, value,
|
|
|
m_unitLabels[idx]->text(), samplingBaseUnit(variable.m_varName), *fields[field])) return false;
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
void nmWxSensitive::updateDistributionPage(int idx)
|
|
|
{
|
|
|
// 只隐藏所选分布不用的字段,保留各行位置;编辑时更新曲线,不提前抽样。
|
|
|
const int distribution = m_distributionCombos[idx]->currentIndex();
|
|
|
for(int field = 0; field < 5; ++field) {
|
|
|
QWidget* edit = m_distributionPanels[idx]->findChild<QLineEdit*>(distributionFields[field]);
|
|
|
const bool used = field < 2 ? distribution < nmDataSensitive::VariableSampling::UNIFORM
|
|
|
: field == 2 ? distribution == nmDataSensitive::VariableSampling::TRIANGULAR
|
|
|
: distribution >= nmDataSensitive::VariableSampling::TRUNCATED_NORMAL;
|
|
|
edit->setVisible(used);
|
|
|
m_distributionPanels[idx]->findChild<QLabel*>(QString(distributionFields[field]) + "Label")->setVisible(used);
|
|
|
}
|
|
|
for(int i = 0; i < m_sensitiveData.getVariableCount(); ++i) {
|
|
|
nmDataSensitive::VariableSampling variable = m_sensitiveData.getVariable(i);
|
|
|
if(findVariableIndex(variable.m_varName) != idx) continue;
|
|
|
const bool valid = readDistributionPage(idx, variable);
|
|
|
m_distributionPlots[idx]->setVariable(variable, valid);
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
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] || sender() == m_distributionCombos[i] ||
|
|
|
sender()->parent() == m_distributionPanels[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()
|
|
|
{
|
|
|
// 本阶段只开放 Monte-Carlo 参数采样与预览,尚不启动数值计算。
|
|
|
if(calculationTypeCombo->currentIndex() == nmDataSensitive::CALC_MONTE_CARLO) return;
|
|
|
if(m_calculationRunning || !m_pDataManager || !saveDataFromUI()) return;
|
|
|
QString error;
|
|
|
QScopedPointer<nmCalculationDllPebiSolverTask> task;
|
|
|
try {
|
|
|
// 从最后成功结果恢复完整输入,当前编辑参数和全局拟压力配置不参与本次分析。
|
|
|
nmCalculationDllPebiSolverTask source(QString(), m_pDataManager, QString(), nullptr, false, true);
|
|
|
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()) {
|
|
|
// 与参数窗口共用框架对话框样式,避免普通 QDialog 使用默认灰色背景。
|
|
|
m_resultsDialog = new iDlgBase(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());
|
|
|
table->setStyleSheet("QTableView { background-color: white; }");
|
|
|
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);
|
|
|
// 复用现有参数页,切换方法只改变可见控件,不覆盖另一种方法的设置。
|
|
|
if(m_loading) return;
|
|
|
for(int i = 0; i < m_numSpins.size(); ++i) updateValuesForPage(i);
|
|
|
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;
|
|
|
if(m_sensitiveData.getCalculationType() == nmDataSensitive::CALC_MONTE_CARLO) {
|
|
|
QString error;
|
|
|
if(!m_sensitiveData.resampleMonteCarlo(&error)) {
|
|
|
QMessageBox::warning(this, tr("Sensitivity"), error);
|
|
|
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);
|
|
|
}
|