feat(nmNum): 新增 Monte-Carlo 参数采样与预览

- 增加 Monte-Carlo 入口及六种概率分布设置,采用拉丁超立方采样,支持自定义总次数和每次预览重新抽样。
- 显示累计概率与归一化概率密度曲线,复用运行预览列表,切换方法时保留确定性参数设置。
- 放大分析窗口,调整绘图样式、参数固定行布局及 Values 表格宽度,三角分布 Mode 与 Mean 共用位置。
- 本次仅开放采样和预览,暂不接入 Monte-Carlo 求解、统计结果及保存逻辑。
feature/Sensitivity-20261012
lvjunjie 1 week ago
parent ed50f2c1d0
commit 7d2076b99e

@ -21,7 +21,8 @@ public:
enum CalculationType enum CalculationType
{ {
CALC_DETERMINISTIC = 0, CALC_DETERMINISTIC = 0,
CALC_DETERMINISTIC_MULTIVARIATE = 1 CALC_DETERMINISTIC_MULTIVARIATE = 1,
CALC_MONTE_CARLO = 2
}; };
struct NM_DATA_EXPORT VariableSampling struct NM_DATA_EXPORT VariableSampling
@ -50,6 +51,12 @@ public:
nmDataAttribute m_maxValue; // Maximum nmDataAttribute m_maxValue; // Maximum
// 表格中的数值列表 // 表格中的数值列表
QVector<double> m_values; QVector<double> m_values;
// Monte-Carlo 分布设置与确定性取值独立,仅保存在内存中。
enum Distribution { NORMAL, LOGNORMAL, TRUNCATED_NORMAL, TRUNCATED_LOGNORMAL, UNIFORM, TRIANGULAR };
Distribution m_distribution;
bool m_distributionInitialized;
double m_mean, m_stdDev, m_peak, m_distributionMin, m_distributionMax;
bool distributionQuantile(double probability, double& value) const;
VariableSampling(); VariableSampling();
// JSON 序列化 / 反序列化 // JSON 序列化 / 反序列化
@ -149,6 +156,10 @@ public:
quint64 calculateModelCount(bool* overflow = nullptr) const; quint64 calculateModelCount(bool* overflow = nullptr) const;
// 返回与变量列表对应的基准单位值,未改变的参数保持模型值。 // 返回与变量列表对应的基准单位值,未改变的参数保持模型值。
bool getRunValues(quint64 runIndex, QVector<double>& values) const; bool getRunValues(quint64 runIndex, QVector<double>& values) const;
// 每次打开 Monte-Carlo 预览重新分层抽样,预览滚动只读取本批结果。
bool resampleMonteCarlo(QString* error = nullptr);
int getMonteCarloCount() const;
void setMonteCarloCount(int count);
bool usesBaseUnits() const; bool usesBaseUnits() const;
void setUsesBaseUnits(bool enabled); void setUsesBaseUnits(bool enabled);
// 记录采样范围对应的求解快照,成功重新求解后再初始化范围。 // 记录采样范围对应的求解快照,成功重新求解后再初始化范围。
@ -172,6 +183,8 @@ private:
bool m_usesBaseUnits; bool m_usesBaseUnits;
QString m_modelSnapshotId; QString m_modelSnapshotId;
QList<VariableSampling> m_variables; QList<VariableSampling> m_variables;
int m_monteCarloCount;
QVector<QVector<double> > m_monteCarloValues;
QSharedPointer<BatchResult> m_results; QSharedPointer<BatchResult> m_results;
}; };

@ -24,6 +24,7 @@ class nmCalculationDllPebiSolverTask;
class QCloseEvent; class QCloseEvent;
class QDialog; class QDialog;
class SensitivityResultsModel; class SensitivityResultsModel;
class SensitivityDistributionPlot;
class NM_SUB_WXS_EXPORT nmWxSensitive : public iDlgBase class NM_SUB_WXS_EXPORT nmWxSensitive : public iDlgBase
{ {
@ -73,6 +74,8 @@ private:
void createDetailPages(); void createDetailPages();
QWidget *createVariablePage(const QString &varName); QWidget *createVariablePage(const QString &varName);
void updateValuesForPage(int idx); void updateValuesForPage(int idx);
void updateDistributionPage(int idx);
bool readDistributionPage(int idx, nmDataSensitive::VariableSampling& variable) const;
QIcon createTriangleIcon(); // 创建三角箭头图标 QIcon createTriangleIcon(); // 创建三角箭头图标
void loadDataToUI(bool refreshRangesOnly = false); // 从数据加载到界面 void loadDataToUI(bool refreshRangesOnly = false); // 从数据加载到界面
@ -87,6 +90,7 @@ private:
QTreeWidget *variablesTree; QTreeWidget *variablesTree;
QStackedWidget *detailStack; QStackedWidget *detailStack;
QLabel *modelCountLabel; QLabel *modelCountLabel;
QSpinBox *m_monteCarloCount;
QPushButton *generateButton; QPushButton *generateButton;
QPushButton *cancelButton; QPushButton *cancelButton;
QPushButton *previewButton; QPushButton *previewButton;
@ -103,6 +107,10 @@ private:
QList<QLineEdit*> m_maxSpins; QList<QLineEdit*> m_maxSpins;
QList<QTableWidget*> m_valueTables; QList<QTableWidget*> m_valueTables;
QList<QLabel*> m_unitLabels; QList<QLabel*> m_unitLabels;
QList<QLabel*> m_numLabels, m_minLabels, m_maxLabels;
QList<QComboBox*> m_distributionCombos;
QList<QWidget*> m_distributionPanels;
QList<SensitivityDistributionPlot*> m_distributionPlots;
// 跟踪当前选中的值索引 // 跟踪当前选中的值索引
int m_selectedValueIndex; int m_selectedValueIndex;

@ -1,10 +1,33 @@
#include "nmDataSensitive.h" #include "nmDataSensitive.h"
#include <cmath> #include <cmath>
#include <limits> #include <limits>
#include <random>
#include <QDateTime>
#include <QtCore/qnumeric.h> #include <QtCore/qnumeric.h>
namespace namespace
{ {
double normalCdf(double z)
{
// 标准正态累积分布近似,最大绝对误差约 7.5e-8,兼容 VS2010。
const double x = std::fabs(z), t = 1.0 / (1.0 + 0.2316419 * x);
const double tail = 0.3989422804014327 * std::exp(-0.5 * x * x) * t *
(0.319381530 + t * (-0.356563782 + t * (1.781477937 + t * (-1.821255978 + t * 1.330274429))));
return z < 0.0 ? tail : 1.0 - tail;
}
double normalQuantile(double probability)
{
// 在累积分布上二分求逆,避免引入新的数学库。
double low = -10.0, high = 10.0;
for(int i = 0; i < 60; ++i) {
const double mid = (low + high) * 0.5;
if(normalCdf(mid) < probability) low = mid;
else high = mid;
}
return (low + high) * 0.5;
}
rapidjson::Value sensitiveNumbersToJson(const QVector<double>& values, rapidjson::Value sensitiveNumbersToJson(const QVector<double>& values,
rapidjson::Document::AllocatorType& allocator) rapidjson::Document::AllocatorType& allocator)
{ {
@ -164,6 +187,8 @@ nmDataSensitive::VariableSampling::VariableSampling()
, m_mode(MODE_AUTOMATIC) , m_mode(MODE_AUTOMATIC)
, m_log(false) , m_log(false)
, m_number(0) , m_number(0)
, m_distribution(NORMAL), m_distributionInitialized(false), m_mean(0.0), m_stdDev(1.0), m_peak(0.0)
, m_distributionMin(0.0), m_distributionMax(1.0)
{ {
m_modelValue = nmDataAttribute("Model value", "", ""); m_modelValue = nmDataAttribute("Model value", "", "");
m_minValue = nmDataAttribute("Minimum", "", ""); m_minValue = nmDataAttribute("Minimum", "", "");
@ -384,11 +409,52 @@ QVector<double> nmDataSensitive::VariableSampling::samplingValues() const
} }
bool nmDataSensitive::VariableSampling::distributionQuantile(double probability, double& value) const
{
// 六种分布都由分位数取值;对数正态的均值、标准差仍使用参数原单位。
if(!qIsFinite(probability) || probability <= 0.0 || probability >= 1.0) return false;
const bool bounded = m_distribution >= TRUNCATED_NORMAL;
const double low = m_distributionMin, high = m_distributionMax;
if(bounded && (!qIsFinite(low) || !qIsFinite(high) || low >= high)) return false;
if(m_distribution == UNIFORM) value = low + probability * (high - low);
else if(m_distribution == TRIANGULAR) {
if(!qIsFinite(m_peak) || m_peak < low || m_peak > high) return false;
const double split = (m_peak - low) / (high - low);
value = probability < split
? low + std::sqrt(probability * (high - low) * (m_peak - low))
: high - std::sqrt((1.0 - probability) * (high - low) * (high - m_peak));
} else {
if(m_distribution < NORMAL || m_distribution > TRUNCATED_LOGNORMAL ||
!qIsFinite(m_mean) || !qIsFinite(m_stdDev) || m_stdDev <= 0.0) return false;
const bool logarithmic = m_distribution == LOGNORMAL || m_distribution == TRUNCATED_LOGNORMAL;
if(logarithmic && (m_mean <= 0.0 || (bounded && low < 0.0))) return false;
double mean = m_mean, sigma = m_stdDev;
if(logarithmic) {
const double ratio = m_stdDev / m_mean;
const double variance = std::log(1.0 + ratio * ratio);
mean = std::log(m_mean) - variance * 0.5;
sigma = std::sqrt(variance);
}
if(!qIsFinite(mean) || !qIsFinite(sigma) || sigma <= 0.0) return false;
if(bounded) {
const double a = logarithmic && low == 0.0 ? 0.0
: normalCdf(((logarithmic ? std::log(low) : low) - mean) / sigma);
const double b = normalCdf(((logarithmic ? std::log(high) : high) - mean) / sigma);
if(!(b > a)) return false;
probability = a + probability * (b - a);
}
value = mean + sigma * normalQuantile(probability);
if(logarithmic) value = std::exp(value);
}
return qIsFinite(value);
}
nmDataSensitive::nmDataSensitive() nmDataSensitive::nmDataSensitive()
: m_calcType(CALC_DETERMINISTIC) : m_calcType(CALC_DETERMINISTIC)
, m_totalModelCount(0) , m_totalModelCount(0)
, m_usesBaseUnits(false) , m_usesBaseUnits(false)
, m_variables() , m_variables()
, m_monteCarloCount(50)
{ {
} }
@ -407,6 +473,8 @@ nmDataSensitive& nmDataSensitive::operator=(const nmDataSensitive& other)
m_usesBaseUnits = other.m_usesBaseUnits; m_usesBaseUnits = other.m_usesBaseUnits;
m_modelSnapshotId = other.m_modelSnapshotId; m_modelSnapshotId = other.m_modelSnapshotId;
m_variables = other.m_variables; m_variables = other.m_variables;
m_monteCarloCount = other.m_monteCarloCount;
m_monteCarloValues = other.m_monteCarloValues;
m_results = other.m_results; m_results = other.m_results;
} }
return *this; return *this;
@ -513,6 +581,11 @@ quint64 nmDataSensitive::calculateModelCount(bool* overflow) const
{ {
// 未选择变量时为零;两种确定性方法分别累计和、积。 // 未选择变量时为零;两种确定性方法分别累计和、积。
if (overflow) *overflow = false; if (overflow) *overflow = false;
if(m_calcType == CALC_MONTE_CARLO) {
for(int i = 0; i < m_variables.size(); ++i)
if(m_variables[i].m_enabled) return qMax(0, m_monteCarloCount);
return 0;
}
quint64 count = 0; quint64 count = 0;
bool selected = false; bool selected = false;
for (int i = 0; i < m_variables.size(); ++i) { for (int i = 0; i < m_variables.size(); ++i) {
@ -540,6 +613,11 @@ bool nmDataSensitive::getRunValues(quint64 runIndex, QVector<double>& values) co
bool overflow = false; bool overflow = false;
const quint64 count = calculateModelCount(&overflow); const quint64 count = calculateModelCount(&overflow);
if (overflow || runIndex >= count) return false; if (overflow || runIndex >= count) return false;
if(m_calcType == CALC_MONTE_CARLO) {
if(m_monteCarloValues.size() != int(count)) return false;
values = m_monteCarloValues[int(runIndex)];
return true;
}
values.clear(); values.clear();
for (int i = 0; i < m_variables.size(); ++i) for (int i = 0; i < m_variables.size(); ++i)
values.append(m_variables[i].m_modelValue.getValue().toDouble()); values.append(m_variables[i].m_modelValue.getValue().toDouble());
@ -570,6 +648,51 @@ bool nmDataSensitive::getRunValues(quint64 runIndex, QVector<double>& values) co
return false; return false;
} }
int nmDataSensitive::getMonteCarloCount() const
{
// Monte-Carlo 总次数由用户指定,不使用单参数次数的和或积。
return m_monteCarloCount;
}
void nmDataSensitive::setMonteCarloCount(int count)
{
// 次数变化后旧预览失效,下一次预览统一重新抽样。
if(count != m_monteCarloCount) m_monteCarloValues.clear();
m_monteCarloCount = count;
}
bool nmDataSensitive::resampleMonteCarlo(QString* error)
{
// 各参数的累积概率均分为 N 层,每层抽一点,再独立打乱,组成 N 组参数。
if(error) error->clear();
m_monteCarloValues.clear();
const int count = int(calculateModelCount());
if(m_calcType != CALC_MONTE_CARLO || count <= 0) return false;
std::mt19937 random(uint(QDateTime::currentMSecsSinceEpoch()) ^ uint(qrand()));
std::uniform_real_distribution<double> uniform(0.0, 1.0);
QVector<QVector<double> > runs(count, QVector<double>(m_variables.size()));
for(int column = 0; column < m_variables.size(); ++column) {
const VariableSampling& variable = m_variables[column];
QVector<double> samples(count, variable.m_modelValue.getValue().toDouble());
if(variable.m_enabled) {
for(int row = 0; row < count; ++row) {
const double jitter = qBound(1e-12, uniform(random), 1.0 - 1e-12);
if(!variable.distributionQuantile((row + jitter) / count, samples[row])) {
if(error) *error = tr("%1: invalid distribution parameters.").arg(variable.m_varName);
return false;
}
}
for(int row = count - 1; row > 0; --row) {
std::uniform_int_distribution<int> index(0, row);
qSwap(samples[row], samples[index(random)]);
}
}
for(int row = 0; row < count; ++row) runs[row][column] = samples[row];
}
m_monteCarloValues.swap(runs);
return true;
}
bool nmDataSensitive::usesBaseUnits() const bool nmDataSensitive::usesBaseUnits() const
{ {
return m_usesBaseUnits; return m_usesBaseUnits;

@ -24,6 +24,7 @@
#include <QDialog> #include <QDialog>
#include <QDialogButtonBox> #include <QDialogButtonBox>
#include <QApplication> #include <QApplication>
#include <QDesktopWidget>
#include <QTableWidget> #include <QTableWidget>
#include <QTableView> #include <QTableView>
#include <QAbstractTableModel> #include <QAbstractTableModel>
@ -66,6 +67,8 @@
namespace namespace
{ {
const char* distributionFields[] = {"mean", "stdDev", "peak", "minimum", "maximum"};
QString formatSamplingValue(double value, int digits) QString formatSamplingValue(double value, int digits)
{ {
// 仅对显示值取有效数字,较大或较小的数自动使用科学计数法。 // 仅对显示值取有效数字,较大或较小的数自动使用科学计数法。
@ -292,6 +295,119 @@ private:
QMap<int, bool> m_pendingVisibility; 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 class RightAlignDelegate : public QStyledItemDelegate
{ {
@ -768,6 +884,21 @@ void nmWxSensitive::refreshModelValues()
variable.getMinValue().setValue(minimum); variable.getMinValue().setValue(minimum);
variable.getMaxValue().setValue(maximum); variable.getMaxValue().setValue(maximum);
variable.setValues(values); 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 范围的实际状态。 // Saphir 在重新打开对话框时同步无效 Log 范围的实际状态。
if (variable.getMode() == nmDataSensitive::VariableSampling::MODE_AUTOMATIC && if (variable.getMode() == nmDataSensitive::VariableSampling::MODE_AUTOMATIC &&
(minimum <= 0.0 || maximum <= 0.0)) variable.setLog(false); (minimum <= 0.0 || maximum <= 0.0)) variable.setLog(false);
@ -796,6 +927,7 @@ void nmWxSensitive::loadDataToUI(bool refreshRangesOnly)
// 加载过程中阻止联动刷新,先恢复设置,再一次生成各参数表格。 // 加载过程中阻止联动刷新,先恢复设置,再一次生成各参数表格。
if (!refreshRangesOnly) if (!refreshRangesOnly)
calculationTypeCombo->setCurrentIndex(int(m_sensitiveData.getCalculationType())); calculationTypeCombo->setCurrentIndex(int(m_sensitiveData.getCalculationType()));
m_monteCarloCount->setValue(m_sensitiveData.getMonteCarloCount());
for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) { for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) {
nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i); nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i);
const QString name = variable.getVarName(); const QString name = variable.getVarName();
@ -833,6 +965,14 @@ void nmWxSensitive::loadDataToUI(bool refreshRangesOnly)
setSamplingEditValue(m_modelSpins[idx], model); setSamplingEditValue(m_modelSpins[idx], model);
setSamplingEditValue(m_minSpins[idx], minimum); setSamplingEditValue(m_minSpins[idx], minimum);
setSamplingEditValue(m_maxSpins[idx], maximum); 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 displayMin = variable.getModelValue().getMinValue().toDouble();
double displayMax = variable.getModelValue().getMaxValue().toDouble(); double displayMax = variable.getModelValue().getMaxValue().toDouble();
convertSamplingValue(name, displayMin, samplingBaseUnit(name), unit, displayMin); convertSamplingValue(name, displayMin, samplingBaseUnit(name), unit, displayMin);
@ -888,11 +1028,16 @@ void nmWxSensitive::updateModelCount()
} }
} }
bool overflow = false; bool overflow = false;
m_sensitiveData.setMonteCarloCount(m_monteCarloCount->value());
const quint64 count = m_sensitiveData.calculateModelCount(&overflow); const quint64 count = m_sensitiveData.calculateModelCount(&overflow);
m_sensitiveData.setTotalModelCount(count); m_sensitiveData.setTotalModelCount(count);
modelCountLabel->setText(overflow ? tr("Too many models") : QString::number(count)); modelCountLabel->setText(overflow ? tr("Too many models") : QString::number(count));
previewButton->setEnabled(!overflow && count > 0); previewButton->setEnabled(!overflow && count > 0);
generateButton->setEnabled(m_pDataManager && !overflow && count > 0 && !m_calculationRunning); 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() bool nmWxSensitive::saveDataFromUI()
@ -906,6 +1051,16 @@ bool nmWxSensitive::saveDataFromUI()
const QString name = variable.getVarName(); const QString name = variable.getVarName();
const int idx = findVariableIndex(name); const int idx = findVariableIndex(name);
if (idx < 0 || !isSupportedSamplingVariable(name)) continue; 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.setMode(static_cast<nmDataSensitive::VariableSampling::Mode>(m_modeCombos[idx]->currentIndex()));
variable.setLog(m_logChecks[idx]->isChecked()); variable.setLog(m_logChecks[idx]->isChecked());
const QString unit = m_unitLabels[idx]->text(); const QString unit = m_unitLabels[idx]->text();
@ -965,6 +1120,9 @@ void nmWxSensitive::setupUI()
{ {
setWindowTitle(tr("Sensitivity")); setWindowTitle(tr("Sensitivity"));
setMinimumSize(900, 550); 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); QVBoxLayout *mainLayout = new QVBoxLayout(this);
@ -974,7 +1132,7 @@ void nmWxSensitive::setupUI()
calculationTypeCombo = new QComboBox(this); calculationTypeCombo = new QComboBox(this);
calculationTypeCombo->addItem(tr("Deterministic")); calculationTypeCombo->addItem(tr("Deterministic"));
calculationTypeCombo->addItem(tr("Deterministic multivariate")); calculationTypeCombo->addItem(tr("Deterministic multivariate"));
//calculationTypeCombo->addItem(tr("Monte-Carlo")); calculationTypeCombo->addItem(tr("Monte-Carlo"));
//calculationTypeCombo->addItem(tr("Monte-Carlo + Improve")); //calculationTypeCombo->addItem(tr("Monte-Carlo + Improve"));
calcTypeLayout->addWidget(calcTypeLabel); calcTypeLayout->addWidget(calcTypeLabel);
@ -1061,6 +1219,7 @@ void nmWxSensitive::setupUI()
// 左边宽度基本固定,右边自适应 // 左边宽度基本固定,右边自适应
centerSplitter->setStretchFactor(0, 0); centerSplitter->setStretchFactor(0, 0);
centerSplitter->setStretchFactor(1, 1); centerSplitter->setStretchFactor(1, 1);
centerSplitter->setSizes(QList<int>() << 260 << 960);
mainLayout->addWidget(centerSplitter, 1); mainLayout->addWidget(centerSplitter, 1);
@ -1069,9 +1228,16 @@ void nmWxSensitive::setupUI()
QLabel *totalLabel = new QLabel(tr("Total number of models:"), this); QLabel *totalLabel = new QLabel(tr("Total number of models:"), this);
modelCountLabel = new QLabel(tr("0"), 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(totalLabel);
bottomLayout->addWidget(modelCountLabel); bottomLayout->addWidget(modelCountLabel);
bottomLayout->addWidget(m_monteCarloCount);
previewButton = new QPushButton(this); previewButton = new QPushButton(this);
previewButton->setIcon(QIcon(QCoreApplication::applicationDirPath().section('/', 0, -2) previewButton->setIcon(QIcon(QCoreApplication::applicationDirPath().section('/', 0, -2)
+ "/Res/Icon/Eyes.png")); + "/Res/Icon/Eyes.png"));
@ -1094,6 +1260,7 @@ void nmWxSensitive::setupUI()
void nmWxSensitive::createConnections() void nmWxSensitive::createConnections()
{ {
connect(m_monteCarloCount, SIGNAL(valueChanged(int)), this, SLOT(onVariableParamChanged()));
connect(generateButton, SIGNAL(clicked()), connect(generateButton, SIGNAL(clicked()),
this, SLOT(onGenerateClicked())); this, SLOT(onGenerateClicked()));
connect(cancelButton, SIGNAL(clicked()), connect(cancelButton, SIGNAL(clicked()),
@ -1267,6 +1434,12 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
QHBoxLayout *modeRowLayout = new QHBoxLayout(modeRowWidget); QHBoxLayout *modeRowLayout = new QHBoxLayout(modeRowWidget);
modeRowLayout->setContentsMargins(0, 0, 0, 0); modeRowLayout->setContentsMargins(0, 0, 0, 0);
modeRowLayout->addWidget(modeCombo); 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->addWidget(logCheck);
modeRowLayout->addStretch(); modeRowLayout->addStretch();
@ -1285,9 +1458,9 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
paramLayout->addWidget(numSpin, row, 1); paramLayout->addWidget(numSpin, row, 1);
++row; ++row;
// 空行(Number 和 Model value 之间) // 模式、数量与下方采样参数之间留出约三行空白。
int gapRow = row; int gapRow = row;
paramLayout->setRowMinimumHeight(gapRow, 12); paramLayout->setRowMinimumHeight(gapRow, numSpin->sizeHint().height() * 3);
++row; ++row;
// Model value + 单位 // Model value + 单位
@ -1310,6 +1483,34 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
paramLayout->addWidget(modelRowWidget, row, 1); paramLayout->addWidget(modelRowWidget, row, 1);
++row; ++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 // Minimum
QLabel *minLabel = new QLabel(tr("Minimum:"), content); QLabel *minLabel = new QLabel(tr("Minimum:"), content);
QLineEdit *minSpin = new QLineEdit(content); QLineEdit *minSpin = new QLineEdit(content);
@ -1359,12 +1560,11 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
valuesTable->setHorizontalHeaderLabels(headers); valuesTable->setHorizontalHeaderLabels(headers);
valuesTable->verticalHeader()->setVisible(false); valuesTable->verticalHeader()->setVisible(false);
valuesTable->setColumnWidth(0, 15);
valuesTable->setColumnWidth(1, 130);
QHeaderView *hh = valuesTable->horizontalHeader(); QHeaderView *hh = valuesTable->horizontalHeader();
hh->setResizeMode(QHeaderView::Fixed); hh->setResizeMode(QHeaderView::Fixed);
hh->setHighlightSections(false); hh->setHighlightSections(false);
// 箭头列固定,数值列在下方设置为填满加宽后的表格。
valuesTable->setColumnWidth(0, 15);
valuesTable->setStyleSheet( valuesTable->setStyleSheet(
"QHeaderView::section {" "QHeaderView::section {"
" border-top: 0px;" " border-top: 0px;"
@ -1380,7 +1580,7 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
"}" "}"
); );
valuesTable->horizontalHeader()->setStretchLastSection(false); valuesTable->horizontalHeader()->setStretchLastSection(true);
valuesTable->setEditTriggers(QAbstractItemView::DoubleClicked); valuesTable->setEditTriggers(QAbstractItemView::DoubleClicked);
valuesTable->setSelectionBehavior(QAbstractItemView::SelectRows); valuesTable->setSelectionBehavior(QAbstractItemView::SelectRows);
valuesTable->setSelectionMode(QAbstractItemView::NoSelection); valuesTable->setSelectionMode(QAbstractItemView::NoSelection);
@ -1390,10 +1590,8 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
valuesTable->setItemDelegateForColumn(1, new RightAlignDelegate(valuesTable)); valuesTable->setItemDelegateForColumn(1, new RightAlignDelegate(valuesTable));
int tableWidth = valuesTable->frameWidth() * 2 // 初始空表的列宽尚未稳定,直接固定容器宽度,不从未显示的表头反算。
+ valuesTable->verticalHeader()->sizeHint().width() const int tableWidth = 190;
+ valuesTable->columnWidth(0)
+ valuesTable->columnWidth(1);
valuesTable->setFixedWidth(tableWidth); valuesTable->setFixedWidth(tableWidth);
QWidget *tableContainer = new QWidget(content); QWidget *tableContainer = new QWidget(content);
@ -1406,6 +1604,8 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
contentLayout->addWidget(paramWidget, 1); contentLayout->addWidget(paramWidget, 1);
contentLayout->addStretch(); contentLayout->addStretch();
contentLayout->addWidget(tableContainer, 0, Qt::AlignRight); contentLayout->addWidget(tableContainer, 0, Qt::AlignRight);
SensitivityDistributionPlot* distributionPlot = new SensitivityDistributionPlot(content);
contentLayout->addWidget(distributionPlot, 2);
panelLayout->addWidget(content); panelLayout->addWidget(content);
pageLayout->addWidget(samplingPanel); pageLayout->addWidget(samplingPanel);
@ -1420,6 +1620,13 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
m_maxSpins.append(maxSpin); m_maxSpins.append(maxSpin);
m_valueTables.append(valuesTable); m_valueTables.append(valuesTable);
m_unitLabels.append(unitLabel); 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)), connect(numSpin, SIGNAL(valueChanged(int)),
this, SLOT(onVariableParamChanged())); this, SLOT(onVariableParamChanged()));
@ -1445,6 +1652,23 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
void nmWxSensitive::updateValuesForPage(int idx) void nmWxSensitive::updateValuesForPage(int idx)
{ {
if (idx < 0 || idx >= m_numSpins.size()) return; 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]; QTableWidget* table = m_valueTables[idx];
const bool automatic = m_modeCombos[idx]->currentIndex() == 0; const bool automatic = m_modeCombos[idx]->currentIndex() == 0;
@ -1498,6 +1722,49 @@ void nmWxSensitive::updateValuesForPage(int idx)
} }
} }
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() void nmWxSensitive::onSamplingEditingFinished()
{ {
// 输入结束后截断越界范围,再按六位有效数字显示,计算仍使用原始精度。 // 输入结束后截断越界范围,再按六位有效数字显示,计算仍使用原始精度。
@ -1532,7 +1799,8 @@ void nmWxSensitive::onVariableParamChanged()
for (int i = 0; i < m_numSpins.size(); ++i) { for (int i = 0; i < m_numSpins.size(); ++i) {
if (sender() == m_numSpins[i] || sender() == m_minSpins[i] || if (sender() == m_numSpins[i] || sender() == m_minSpins[i] ||
sender() == m_maxSpins[i] || sender() == m_modeCombos[i] || sender() == m_maxSpins[i] || sender() == m_modeCombos[i] ||
sender() == m_logChecks[i]) { sender() == m_logChecks[i] || sender() == m_distributionCombos[i] ||
sender()->parent() == m_distributionPanels[i]) {
m_selectedValueIndex = qMin(m_selectedValueIndex, m_numSpins[i]->value() - 1); m_selectedValueIndex = qMin(m_selectedValueIndex, m_numSpins[i]->value() - 1);
updateValuesForPage(i); updateValuesForPage(i);
break; break;
@ -1584,6 +1852,8 @@ void nmWxSensitive::onTreeItemChanged(QTreeWidgetItem* item, int column)
void nmWxSensitive::onGenerateClicked() void nmWxSensitive::onGenerateClicked()
{ {
// 本阶段只开放 Monte-Carlo 参数采样与预览,尚不启动数值计算。
if(calculationTypeCombo->currentIndex() == nmDataSensitive::CALC_MONTE_CARLO) return;
if(m_calculationRunning || !m_pDataManager || !saveDataFromUI()) return; if(m_calculationRunning || !m_pDataManager || !saveDataFromUI()) return;
QString error; QString error;
QScopedPointer<nmCalculationDllPebiSolverTask> task; QScopedPointer<nmCalculationDllPebiSolverTask> task;
@ -1787,7 +2057,9 @@ void nmWxSensitive::onHideAllResults()
void nmWxSensitive::onCalculationTypeChanged(int index) void nmWxSensitive::onCalculationTypeChanged(int index)
{ {
Q_UNUSED(index); Q_UNUSED(index);
// 两种确定性方法共用参数设置,仅切换总次数及组合规则。 // 复用现有参数页,切换方法只改变可见控件,不覆盖另一种方法的设置。
if(m_loading) return;
for(int i = 0; i < m_numSpins.size(); ++i) updateValuesForPage(i);
updateModelCount(); updateModelCount();
} }
@ -1809,6 +2081,13 @@ void nmWxSensitive::onTreeItemClicked(QTreeWidgetItem* item, int column)
void nmWxSensitive::onPreviewClicked() void nmWxSensitive::onPreviewClicked()
{ {
if (!saveDataFromUI()) return; 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 风格的连续运行列表,组合数再大也不显示分页控件。 // Saphir 风格的连续运行列表,组合数再大也不显示分页控件。
QDialog dialog(this); QDialog dialog(this);
dialog.setWindowTitle(tr("Sensitivity Runs")); dialog.setWindowTitle(tr("Sensitivity Runs"));

Loading…
Cancel
Save