From 64009462e52491c97283234195416a685cfd1eb3 Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Wed, 14 Oct 2026 16:36:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(nmNum):=20=E5=AE=8C=E5=96=84=E9=87=87?= =?UTF-8?q?=E6=A0=B7=E8=8C=83=E5=9B=B4=E6=A0=A1=E9=AA=8C=E4=B8=8E=E7=BB=98?= =?UTF-8?q?=E5=9B=BE=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Monte-Carlo 沿用系统参数范围,拦截越界设置和样本。 - 分布曲线按允许范围裁剪,Skin 绘图暂不限制。 - 分离曲线边界与坐标范围,修复起点刻度缺失。 --- Src/nmNum/nmSubWxs/nmWxSensitive.cpp | 150 ++++++++++++++++++--------- 1 file changed, 102 insertions(+), 48 deletions(-) diff --git a/Src/nmNum/nmSubWxs/nmWxSensitive.cpp b/Src/nmNum/nmSubWxs/nmWxSensitive.cpp index f61631f5..8f2b9714 100644 --- a/Src/nmNum/nmSubWxs/nmWxSensitive.cpp +++ b/Src/nmNum/nmSubWxs/nmWxSensitive.cpp @@ -342,8 +342,27 @@ protected: 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; + double axisMin = std::floor(x.first() / 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 density(201); double peak = 0.0; for(int i = 0; i < x.size(); ++i) { @@ -355,9 +374,13 @@ protected: peak = qMax(peak, density[i]); } // 蓝色点状主网格配合黑色主、次刻度,概率轴按 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) { - 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(); if(i % 5 == 0) { painter.setPen(QPen(QColor(100, 120, 255), 1, Qt::DotLine)); @@ -388,10 +411,16 @@ protected: 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.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.drawPolyline(cumulative); painter.setPen(QPen(Qt::red, 1, Qt::DashLine)); painter.drawPolyline(probability); + painter.restore(); painter.setPen(Qt::black); painter.drawRect(plot); 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_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; } @@ -1051,53 +1075,61 @@ bool nmWxSensitive::saveDataFromUI() const QString name = variable.getVarName(); const int idx = findVariableIndex(name); 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) { 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()) { QMessageBox::warning(this, tr("Sensitivity"), tr("%1: invalid distribution parameters.").arg(name)); detailStack->setCurrentIndex(idx + 1); return false; } - continue; - } - variable.setMode(static_cast(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 values = variable.samplingValues(); - for (int row = 0; valid && row < values.size(); ++row) - valid = validSamplingValue(name, values[row]); - if (valid) variable.setValues(values); - } + // 均值、众数及有界分布的上下限沿用确定性方法的系统允许范围。 + 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 { - QVector 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); + variable.setMode(static_cast(m_modeCombos[idx]->currentIndex())); + variable.setLog(m_logChecks[idx]->isChecked()); + 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 values = variable.samplingValues(); + for (int row = 0; valid && row < values.size(); ++row) + valid = validSamplingValue(name, values[row]); + if (valid) variable.setValues(values); + } + } else { + QVector 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); } - valid = valid && values.size() == variable.getNumber(); - if (valid) variable.setValues(values); } if (!valid && variable.getEnabled()) { double lower = 0.0, upper = 0.0; @@ -1105,7 +1137,7 @@ bool nmWxSensitive::saveDataFromUI() 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.") + 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) : tr("%1: the physical range is unavailable.").arg(name)); detailStack->setCurrentIndex(idx + 1); @@ -2087,6 +2119,28 @@ 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);