diff --git a/Include/nmNum/nmData/nmDataSensitive.h b/Include/nmNum/nmData/nmDataSensitive.h index 056f58e8..4acf003d 100644 --- a/Include/nmNum/nmData/nmDataSensitive.h +++ b/Include/nmNum/nmData/nmDataSensitive.h @@ -56,7 +56,7 @@ public: Distribution m_distribution; bool m_distributionInitialized; double m_mean, m_stdDev, m_peak, m_distributionMin, m_distributionMax; - bool distributionQuantile(double probability, double& value) const; + bool distributionQuantile(double probability, double& value, bool limitToPhysicalRange = true) const; VariableSampling(); // JSON 序列化 / 反序列化 diff --git a/Src/nmNum/nmData/nmDataSensitive.cpp b/Src/nmNum/nmData/nmDataSensitive.cpp index ff645df9..f7af77a8 100644 --- a/Src/nmNum/nmData/nmDataSensitive.cpp +++ b/Src/nmNum/nmData/nmDataSensitive.cpp @@ -16,10 +16,9 @@ double normalCdf(double z) return z < 0.0 ? tail : 1.0 - tail; } -double normalQuantile(double probability) +double normalQuantile(double probability, double low = -10.0, double high = 10.0) { // 在累积分布上二分求逆,避免引入新的数学库。 - 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; @@ -409,12 +408,14 @@ QVector nmDataSensitive::VariableSampling::samplingValues() const } -bool nmDataSensitive::VariableSampling::distributionQuantile(double probability, double& value) const +bool nmDataSensitive::VariableSampling::distributionQuantile(double probability, double& value, bool limitToPhysicalRange) 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; + const bool specifiedBounds = m_distribution >= TRUNCATED_NORMAL; + const bool bounded = specifiedBounds || limitToPhysicalRange; + const double low = specifiedBounds ? m_distributionMin : m_modelValue.getMinValue().toDouble(); + const double high = specifiedBounds ? m_distributionMax : m_modelValue.getMaxValue().toDouble(); if(bounded && (!qIsFinite(low) || !qIsFinite(high) || low >= high)) return false; if(m_distribution == UNIFORM) value = low + probability * (high - low); else if(m_distribution == TRIANGULAR) { @@ -436,14 +437,23 @@ bool nmDataSensitive::VariableSampling::distributionQuantile(double probability, sigma = std::sqrt(variance); } if(!qIsFinite(mean) || !qIsFinite(sigma) || sigma <= 0.0) return false; + double quantile = 0.0; 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); + // 在允许范围内重新分配概率后取样,保持拉丁超立方分层,不裁剪或重抽越界样本。 + double zLow = logarithmic && low == 0.0 ? -std::numeric_limits::infinity() + : ((logarithmic ? std::log(low) : low) - mean) / sigma; + double zHigh = ((logarithmic ? std::log(high) : high) - mean) / sigma; + // 右尾利用正态分布对称性转到左尾,避免两个接近 1 的累积概率相减。 + const bool reflected = zLow > 0.0; + if(reflected) { const double previousLow = zLow; zLow = -zHigh; zHigh = -previousLow; } + const double a = normalCdf(zLow), b = normalCdf(zHigh); if(!(b > a)) return false; - probability = a + probability * (b - a); - } - value = mean + sigma * normalQuantile(probability); + probability = a + (reflected ? 1.0 - probability : probability) * (b - a); + // 超过 40 个标准差时双精度尾概率已下溢;缩小二分区间,同时保留实际边界。 + quantile = normalQuantile(probability, qMax(zLow, -40.0), qMin(zHigh, 40.0)); + if(reflected) quantile = -quantile; + } else quantile = normalQuantile(probability); + value = mean + sigma * quantile; if(logarithmic) value = std::exp(value); } return qIsFinite(value); diff --git a/Src/nmNum/nmSubWxs/nmWxSensitive.cpp b/Src/nmNum/nmSubWxs/nmWxSensitive.cpp index 8f2b9714..8ffbf8bc 100644 --- a/Src/nmNum/nmSubWxs/nmWxSensitive.cpp +++ b/Src/nmNum/nmSubWxs/nmWxSensitive.cpp @@ -323,7 +323,9 @@ protected: // 两端加密以平滑显示尾部,有界分布的横轴保留精确上下限。 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]); + // Skin 保留原分布绘图,其余参数显示与实际采样一致的受限分布。 + valid = m_variable.distributionQuantile(qBound(1e-10, cumulativeValues[i], 1.0 - 1e-10), x[i], + m_variable.m_varName != "Skin"); 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), @@ -1086,9 +1088,7 @@ bool nmWxSensitive::saveDataFromUI() detailStack->setCurrentIndex(idx + 1); return false; } - // 均值、众数及有界分布的上下限沿用确定性方法的系统允许范围。 - 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); @@ -2119,28 +2119,7 @@ void nmWxSensitive::onPreviewClicked() QMessageBox::warning(this, tr("Sensitivity"), error); return; } - // 普通正态、对数正态仍可能抽到越界值;整批检查通过后才显示预览,不裁剪或重抽。 - QVector 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 风格的连续运行列表,组合数再大也不显示分页控件。 QDialog dialog(this);