fix(nmNum): 完善采样范围校验与绘图边界

- Monte-Carlo 沿用系统参数范围,拦截越界设置和样本。
- 分布曲线按允许范围裁剪,Skin 绘图暂不限制。
- 分离曲线边界与坐标范围,修复起点刻度缺失。
feature/Sensitivity-20261012
lvjunjie 1 week ago
parent 7d2076b99e
commit 64009462e5

@ -342,8 +342,27 @@ protected:
const double scale = std::pow(10.0, std::floor(std::log10(roughStep))); const double scale = std::pow(10.0, std::floor(std::log10(roughStep)));
const double fraction = roughStep / scale; 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 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; double axisMin = std::floor(x.first() / step) * step;
const double axisMax = std::ceil(x.last() / step) * step; double axisMax = std::ceil(x.last() / step) * step;
// 除 Skin 外,绘图范围与系统允许范围取交集;只裁剪显示,不改变分布概率。
if(m_variable.m_varName != "Skin") {
double minimum = 0.0, maximum = 0.0;
const QString unit = m_variable.m_modelValue.getUnit();
if(samplingPhysicalRange(m_variable.m_varName, minimum, maximum) &&
convertSamplingValue(m_variable.m_varName, minimum, samplingBaseUnit(m_variable.m_varName), unit, minimum) &&
convertSamplingValue(m_variable.m_varName, maximum, samplingBaseUnit(m_variable.m_varName), unit, maximum)) {
axisMin = qMax(axisMin, minimum);
axisMax = qMin(axisMax, maximum);
}
}
if(axisMax <= axisMin) {
painter.drawText(plot, Qt::AlignCenter, nmWxSensitive::tr("Distribution outside the allowed range"));
return;
}
// 曲线保留真实边界,坐标刻度向外取整;压力下限 0.1 等情形也能显示 0 刻度。
const double curveMin = axisMin, curveMax = axisMax;
axisMin = std::floor(curveMin / step) * step;
axisMax = std::ceil(curveMax / step) * step;
QVector<double> density(201); QVector<double> density(201);
double peak = 0.0; double peak = 0.0;
for(int i = 0; i < x.size(); ++i) { for(int i = 0; i < x.size(); ++i) {
@ -355,9 +374,13 @@ protected:
peak = qMax(peak, density[i]); peak = qMax(peak, density[i]);
} }
// 蓝色点状主网格配合黑色主、次刻度,概率轴按 0.1 标注。 // 蓝色点状主网格配合黑色主、次刻度,概率轴按 0.1 标注。
const int intervals = qRound((axisMax - axisMin) / step); const double tickStart = std::floor(axisMin / step) * step;
const int intervals = int(std::ceil((axisMax - tickStart) / step));
// 仅容忍刻度计算的浮点舍入误差。
const double tickTolerance = DBL_EPSILON * 8.0 * qMax(qAbs(axisMin), qAbs(axisMax));
for(int i = 0; i <= intervals * 5; ++i) { for(int i = 0; i <= intervals * 5; ++i) {
const double value = axisMin + i * step / 5.0; const double value = tickStart + i * step / 5.0;
if(value < axisMin - tickTolerance || value > axisMax + tickTolerance) continue;
const double px = dataArea.left() + (value - axisMin) / (axisMax - axisMin) * dataArea.width(); const double px = dataArea.left() + (value - axisMin) / (axisMax - axisMin) * dataArea.width();
if(i % 5 == 0) { if(i % 5 == 0) {
painter.setPen(QPen(QColor(100, 120, 255), 1, Qt::DotLine)); painter.setPen(QPen(QColor(100, 120, 255), 1, Qt::DotLine));
@ -388,10 +411,16 @@ protected:
cumulative << QPointF(px, dataArea.bottom() - cumulativeValues[i] * dataArea.height()); cumulative << QPointF(px, dataArea.bottom() - cumulativeValues[i] * dataArea.height());
probability << QPointF(px, dataArea.bottom() - (peak > 0.0 ? density[i] / peak : 0.0) * dataArea.height()); probability << QPointF(px, dataArea.bottom() - (peak > 0.0 ? density[i] / peak : 0.0) * dataArea.height());
} }
// 曲线也按允许的横轴区间裁剪,避免负值部分画进坐标留白或标签区域。
painter.save();
const double clipLeft = dataArea.left() + (curveMin - axisMin) / (axisMax - axisMin) * dataArea.width();
const double clipWidth = (curveMax - curveMin) / (axisMax - axisMin) * dataArea.width();
painter.setClipRect(QRectF(clipLeft, plot.top(), clipWidth, plot.height()));
painter.setPen(QPen(Qt::blue, 1.5)); painter.setPen(QPen(Qt::blue, 1.5));
painter.drawPolyline(cumulative); painter.drawPolyline(cumulative);
painter.setPen(QPen(Qt::red, 1, Qt::DashLine)); painter.setPen(QPen(Qt::red, 1, Qt::DashLine));
painter.drawPolyline(probability); painter.drawPolyline(probability);
painter.restore();
painter.setPen(Qt::black); painter.setPen(Qt::black);
painter.drawRect(plot); painter.drawRect(plot);
painter.save(); painter.save();
@ -891,11 +920,6 @@ void nmWxSensitive::refreshModelValues()
variable.m_stdDev = model == 0.0 ? 5.0 / 3.0 : std::fabs(model) / 3.0; variable.m_stdDev = model == 0.0 ? 5.0 / 3.0 : std::fabs(model) / 3.0;
variable.m_distributionMin = minimum; variable.m_distributionMin = minimum;
variable.m_distributionMax = maximum; 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_peak = qBound(variable.m_distributionMin, model * 1.01, variable.m_distributionMax);
variable.m_distributionInitialized = true; variable.m_distributionInitialized = true;
} }
@ -1051,21 +1075,28 @@ 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;
const QString unit = m_unitLabels[idx]->text();
const QString baseUnit = samplingBaseUnit(name);
bool valid = true;
if(candidate.getCalculationType() == nmDataSensitive::CALC_MONTE_CARLO) { if(candidate.getCalculationType() == nmDataSensitive::CALC_MONTE_CARLO) {
double median = 0.0; double median = 0.0;
const bool valid = readDistributionPage(idx, variable) && variable.distributionQuantile(0.5, median); valid = readDistributionPage(idx, variable) && variable.distributionQuantile(0.5, median);
if(!valid && variable.getEnabled()) { if(!valid && variable.getEnabled()) {
QMessageBox::warning(this, tr("Sensitivity"), tr("%1: invalid distribution parameters.").arg(name)); QMessageBox::warning(this, tr("Sensitivity"), tr("%1: invalid distribution parameters.").arg(name));
detailStack->setCurrentIndex(idx + 1); detailStack->setCurrentIndex(idx + 1);
return false; return false;
} }
continue; // 均值、众数及有界分布的上下限沿用确定性方法的系统允许范围。
} if(valid && variable.m_distribution < nmDataSensitive::VariableSampling::UNIFORM)
valid = validSamplingValue(name, variable.m_mean);
if(valid && variable.m_distribution >= nmDataSensitive::VariableSampling::TRUNCATED_NORMAL)
valid = validSamplingValue(name, variable.m_distributionMin) &&
validSamplingValue(name, variable.m_distributionMax);
if(valid && variable.m_distribution == nmDataSensitive::VariableSampling::TRIANGULAR)
valid = validSamplingValue(name, variable.m_peak);
} else {
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 baseUnit = samplingBaseUnit(name);
bool valid = true;
if (variable.getMode() == nmDataSensitive::VariableSampling::MODE_AUTOMATIC) { if (variable.getMode() == nmDataSensitive::VariableSampling::MODE_AUTOMATIC) {
bool minOk = false, maxOk = false; bool minOk = false, maxOk = false;
double minimum = samplingEditValue(m_minSpins[idx], &minOk); double minimum = samplingEditValue(m_minSpins[idx], &minOk);
@ -1099,13 +1130,14 @@ bool nmWxSensitive::saveDataFromUI()
valid = valid && values.size() == variable.getNumber(); valid = valid && values.size() == variable.getNumber();
if (valid) variable.setValues(values); if (valid) variable.setValues(values);
} }
}
if (!valid && variable.getEnabled()) { if (!valid && variable.getEnabled()) {
double lower = 0.0, upper = 0.0; double lower = 0.0, upper = 0.0;
const bool hasRange = samplingPhysicalRange(name, lower, upper) && const bool hasRange = samplingPhysicalRange(name, lower, upper) &&
convertSamplingValue(name, lower, baseUnit, unit, lower) && convertSamplingValue(name, lower, baseUnit, unit, lower) &&
convertSamplingValue(name, upper, baseUnit, unit, upper); convertSamplingValue(name, upper, baseUnit, unit, upper);
QMessageBox::warning(this, tr("Sensitivity"), QMessageBox::warning(this, tr("Sensitivity"),
hasRange ? tr("%1: invalid sampling values. Allowed range: [%2, %3] %4. Check the range and every manual row.") hasRange ? tr("%1: invalid sampling values. Allowed range: [%2, %3] %4. Check the sampling settings.")
.arg(name).arg(formatSamplingValue(lower, 6)).arg(formatSamplingValue(upper, 6)).arg(unit) .arg(name).arg(formatSamplingValue(lower, 6)).arg(formatSamplingValue(upper, 6)).arg(unit)
: tr("%1: the physical range is unavailable.").arg(name)); : tr("%1: the physical range is unavailable.").arg(name));
detailStack->setCurrentIndex(idx + 1); detailStack->setCurrentIndex(idx + 1);
@ -2087,6 +2119,28 @@ void nmWxSensitive::onPreviewClicked()
QMessageBox::warning(this, tr("Sensitivity"), error); QMessageBox::warning(this, tr("Sensitivity"), error);
return; return;
} }
// 普通正态、对数正态仍可能抽到越界值;整批检查通过后才显示预览,不裁剪或重抽。
QVector<double> values;
for(int run = 0; run < m_sensitiveData.getMonteCarloCount(); ++run) {
if(!m_sensitiveData.getRunValues(run, values)) return;
for(int i = 0; i < m_sensitiveData.getVariableCount(); ++i) {
const nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i);
if(!variable.getEnabled() || validSamplingValue(variable.m_varName, values[i])) continue;
const QString name = variable.m_varName;
const QString unit = variable.m_modelValue.getUnit();
double lower = 0.0, upper = 0.0, value = values[i];
samplingPhysicalRange(name, lower, upper);
convertSamplingValue(name, lower, samplingBaseUnit(name), unit, lower);
convertSamplingValue(name, upper, samplingBaseUnit(name), unit, upper);
convertSamplingValue(name, value, samplingBaseUnit(name), unit, value);
QMessageBox::warning(this, tr("Sensitivity"),
tr("%1: run %2 sampled %3 %4 outside the allowed range [%5, %6]. Adjust the distribution settings or use a bounded distribution.")
.arg(name).arg(run + 1).arg(formatSamplingValue(value, 6)).arg(unit)
.arg(formatSamplingValue(lower, 6)).arg(formatSamplingValue(upper, 6)));
detailStack->setCurrentIndex(findVariableIndex(name) + 1);
return;
}
}
} }
// Saphir 风格的连续运行列表,组合数再大也不显示分页控件。 // Saphir 风格的连续运行列表,组合数再大也不显示分页控件。
QDialog dialog(this); QDialog dialog(this);

Loading…
Cancel
Save