feat(nmNum): 增加误差直方图窗口

- 主界面显示误差柱状图与累计概率曲线,支持调整分箱数。
- 调整计数轴范围和刻度,概率刻度间隔为 10%。
- 支持关闭重开,新模型生成后清除旧图。
feature/Sensitivity-20261012
lvjunjie 1 week ago
parent 04a8759b64
commit c375603d22

@ -168,6 +168,8 @@ class NM_SUB_WND_EXPORT nmSubWndMain : public iSubWndBaseFit {
void onSensitivityTaskDestroyed(); void onSensitivityTaskDestroyed();
/** @brief 在框架已有的敏感性子窗口叠加显示本分析的双对数结果。 */ /** @brief 在框架已有的敏感性子窗口叠加显示本分析的双对数结果。 */
void onShowSensitiveResults(); void onShowSensitiveResults();
/** @brief 刷新主界面的误差直方图,修改分箱数时复用当前批次结果。 */
void updateSensitiveHistogramPlot();
/** @brief 当前分析的建网提交结束后,恢复待启动或待捕获的求解流程。 */ /** @brief 当前分析的建网提交结束后,恢复待启动或待捕获的求解流程。 */
void onPebiGridGenerationFinished(quint64 nGridInputRevision, void onPebiGridGenerationFinished(quint64 nGridInputRevision,
bool bSucceeded); bool bSucceeded);

@ -13,6 +13,7 @@
#include "ZxPlot.h" #include "ZxPlot.h"
#include "ZxObjCurve.h" #include "ZxObjCurve.h"
#include "ZxObjCurveBar.h"
#include "ZxSubTitle.h" #include "ZxSubTitle.h"
#include "tCurvePlotScene.h" #include "tCurvePlotScene.h"
#include "tCurvePlotView.h" #include "tCurvePlotView.h"
@ -74,6 +75,9 @@
#include <QSet> #include <QSet>
#include <QHash> #include <QHash>
#include <QPushButton> #include <QPushButton>
#include <QSpinBox>
#include <QLabel>
#include <cmath>
#include <QMdiSubWindow> #include <QMdiSubWindow>
#include "ZxMdiArea.h" #include "ZxMdiArea.h"
@ -108,6 +112,25 @@ namespace
QPointer<nmCalculationDllPebiSolverTask> s_pRunningSolverTask; QPointer<nmCalculationDllPebiSolverTask> s_pRunningSolverTask;
QPointer<nmSubWndMain> s_pPendingSolverWindow; QPointer<nmSubWndMain> s_pPendingSolverWindow;
class nmSensitivityHistogramBars : public ZxObjCurveBar
{
public:
explicit nmSensitivityHistogramBars(double binWidth) : m_binWidth(binWidth) {}
protected:
void drawSubBars(QPainter* painter, QVector<QPointF>& points) override
{
// 框架柱宽使用绘图坐标,按分箱宽度换算,缩放和窗口调整后仍对应同一区间。
const ZxSubAxisX* axis = getAxisX();
const double left = axis->getRangeMin();
m_dBarWidth = 0.95 * qAbs(axis->getPosForValue(left + m_binWidth) - axis->getPosForValue(left));
ZxObjCurveBar::drawSubBars(painter, points);
}
private:
double m_binWidth;
};
// 历史窗口仍是单流量曲线结构,因此显示与双对数、半对数后处理一致的制度参考相。 // 历史窗口仍是单流量曲线结构,因此显示与双对数、半对数后处理一致的制度参考相。
const QVector<double>* resultReferenceRatesOf( const QVector<double>* resultReferenceRatesOf(
const nmPebiResultWellSnapshot* pWell) const nmPebiResultWellSnapshot* pWell)
@ -1875,6 +1898,7 @@ void nmSubWndMain::onShowSensitiveResults()
iSubWndFitting* fitting = m_pResultRefreshFitting.data(); iSubWndFitting* fitting = m_pResultRefreshFitting.data();
if(!manager || !fitting || !manager->getSensitiveData()) return; if(!manager || !fitting || !manager->getSensitiveData()) return;
updateSensitiveHistoryPlot(); updateSensitiveHistoryPlot();
updateSensitiveHistogramPlot();
QSharedPointer<nmDataSensitive::BatchResult> result = manager->getSensitiveData()->getResults(); QSharedPointer<nmDataSensitive::BatchResult> result = manager->getSensitiveData()->getResults();
iGuiPlot* plot = fitting->getPlotByTag(s_Tag_ChartSensitivity); iGuiPlot* plot = fitting->getPlotByTag(s_Tag_ChartSensitivity);
if(result.isNull()) { if(result.isNull()) {
@ -2089,6 +2113,152 @@ void nmSubWndMain::updateSensitiveHistoryPlot()
if(created || restore) area->relayoutSubWnds(); if(created || restore) area->relayoutSubWnds();
} }
void nmSubWndMain::updateSensitiveHistogramPlot()
{
iSubWndFitting* fitting = m_pResultRefreshFitting.data();
nmDataAnalyzeManager* manager = m_pResultRefreshDataManager.data();
if(!fitting || !fitting->m_pMdiArea || !manager || !manager->getSensitiveData()) return;
ZxMdiArea* area = fitting->m_pMdiArea;
QMdiSubWindow* window = area->findChild<QMdiSubWindow*>("nmSensitivityHistogram");
const QSharedPointer<nmDataSensitive::BatchResult> batch = manager->getSensitiveData()->getResults();
if(batch.isNull() || batch->m_calcType != nmDataSensitive::CALC_MONTE_CARLO) {
// 新模型或确定性批次不保留上一批 Monte-Carlo 的统计图。
if(window) {
area->removeSubWindow(window);
delete window;
}
return;
}
const bool created = window == nullptr;
if(created) {
QWidget* page = new QWidget;
QVBoxLayout* layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 0, 0, 0);
QHBoxLayout* options = new QHBoxLayout;
options->addWidget(new QLabel(tr("Bins:"), page));
QSpinBox* bins = new QSpinBox(page);
bins->setObjectName("nmHistogramBins");
bins->setRange(1, 1000);
bins->setValue(20);
options->addWidget(bins);
QLabel* count = new QLabel(page);
count->setObjectName("nmHistogramCount");
options->addWidget(count);
options->addStretch();
layout->addLayout(options);
iGuiPlot* plot = new iGuiPlot(true, page);
plot->initUI(tr("Sensitivity histogram plot"));
plot->setDropEnabled(false);
plot->setAxisX(tr("Goodness of fit"), "MPa");
plot->setAxisY(tr("Count"));
plot->setAxisY(tr("Cumulative probability"), "%", true);
plot->m_pPlot->setBKColor(Qt::white);
for(int i = 0; i < 3; ++i) {
plot->setAxisXYLog(false, i);
plot->getAxisOf(i)->setVisible(true);
}
layout->addWidget(plot);
window = area->addSubWindow(page);
window->setObjectName("nmSensitivityHistogram");
window->setWindowTitle(tr("Sensitivity histogram plot"));
window->setAttribute(Qt::WA_DeleteOnClose, false);
connect(bins, SIGNAL(valueChanged(int)), this, SLOT(updateSensitiveHistogramPlot()));
}
iGuiPlot* plot = window->findChild<iGuiPlot*>();
const int binCount = window->findChild<QSpinBox*>("nmHistogramBins")->value();
QVector<double> errors;
for(size_t i = 0; i < batch->m_runs.size(); ++i) {
const nmDataSensitive::RunResult& run = batch->m_runs[i];
// 显示勾选不改变统计总体,失败、取消或无有效误差的运行不参与统计。
if(run.m_bSucceeded && !run.m_bCancelled && qIsFinite(run.m_dPressureRmse) && run.m_dPressureRmse >= 0.0)
errors.append(run.m_dPressureRmse);
}
window->findChild<QLabel*>("nmHistogramCount")->setText(errors.isEmpty()
? tr("No valid goodness-of-fit results") : tr("Valid runs: %1").arg(errors.size()));
double minimum = 0.0, maximum = 1.0;
if(!errors.isEmpty()) {
minimum = maximum = errors.first();
for(int i = 1; i < errors.size(); ++i) {
minimum = qMin(minimum, errors[i]);
maximum = qMax(maximum, errors[i]);
}
// 单个结果或全部相同的误差也需要非零横轴范围。
if(minimum == maximum) {
const double padding = qMax(1e-6, maximum * 0.05);
minimum = qMax(0.0, minimum - padding);
maximum += padding;
}
}
const double width = (maximum - minimum) / binCount;
QVector<int> counts(binCount, 0);
for(int i = 0; i < errors.size(); ++i) {
// 等宽分箱,左闭右开,最大误差归入最后一箱。
const int index = qMin(binCount - 1, int((errors[i] - minimum) / width));
++counts[index];
}
for(int i = plot->m_pPlot->getObjCount() - 1; i >= 0; --i)
if(qobject_cast<ZxObjCurve*>(plot->m_pPlot->getObjByIndex(i))) plot->m_pPlot->removeObjByIndex(i);
plot->clearUndoStack();
QVector<iCurveDesc> curves;
int largestCount = 0;
if(!errors.isEmpty()) {
QVector<QPointF> bars;
iCurveDesc cumulative;
cumulative.m_sCurveName = tr("Cumulative probability");
cumulative.m_oCurveType = POT_CurveDiscrete;
cumulative.m_bUseY2 = true;
cumulative.m_bLineVisible = true;
cumulative.m_bPointVisible = true;
cumulative.m_oPen.setColor(Qt::red);
cumulative.m_oDot = ZxDot(DTS_Circle, Qt::red, 0.6, true);
int total = 0;
for(int i = 0; i < binCount; ++i) {
const double center = minimum + (i + 0.5) * width;
bars.append(QPointF(center, counts[i]));
largestCount = qMax(largestCount, counts[i]);
total += counts[i];
cumulative.m_vecX.append(center);
cumulative.m_vecY.append(100.0 * total / errors.size());
}
// 普通曲线入口不支持柱状图,直接使用框架柱状图元,累计概率仍走已有曲线入口。
nmSensitivityHistogramBars* histogram = new nmSensitivityHistogramBars(width);
histogram->init(tr("Count"), plot->m_pPlot->getMainAxisX(), plot->m_pPlot->getMainAxisY());
plot->m_pPlot->addOneObj(histogram);
histogram->setAllValues(bars);
histogram->setCurvePty(QPen(Qt::black), ZxDot(), true, false);
histogram->setBarFill(true);
histogram->setBarFillClr(QColor(85, 107, 47));
histogram->setShow3D(false);
histogram->setPinValue(false);
curves.append(cumulative);
}
plot->setCurveData(curves);
plot->runUpdate();
plot->setReadOnly(true);
VecDouble ticksX, ticksY, ticksY2;
for(int i = 0; i <= 5; ++i) {
ticksX.append(minimum + i * (maximum - minimum) / 5.0);
}
// 累计概率轴每隔 10% 标注,100% 以上保留顶部留白。
for(int i = 0; i <= 10; ++i) ticksY2.append(i * 10.0);
// 小样本时放大计数轴范围,使柱子保持较低;样本较多时自动扩展并留出顶部空间。
const double countRange = qMax(16.0, largestCount * 1.2);
const double step = std::ceil(countRange / 8.0);
const double top = std::ceil(countRange / step) * step;
// 保持原轴范围,将主刻度加密一倍,计数刻度仍使用整数。
const double tickStep = qMax(1.0, std::ceil(step / 2.0));
for(double tick = 0.0; tick < top; tick += tickStep) ticksY.append(tick);
ticksY.append(top);
plot->setAxisScaleX(minimum, maximum, ticksX);
plot->setAxisScaleY(0.0, top, ticksY);
plot->setAxisScaleY2(0.0, 110.0, ticksY2);
const bool restore = window->isHidden();
plot->show();
window->widget()->show();
window->show();
if(created || restore) area->relayoutSubWnds();
}
void nmSubWndMain::startSolverTask() void nmSubWndMain::startSolverTask()
{ {
if(!m_bSolverStartPending) { if(!m_bSolverStartPending) {

Loading…
Cancel
Save