From c375603d2253975ea2113bffa6b87527a4f70604 Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Thu, 15 Oct 2026 14:36:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(nmNum):=20=E5=A2=9E=E5=8A=A0=E8=AF=AF?= =?UTF-8?q?=E5=B7=AE=E7=9B=B4=E6=96=B9=E5=9B=BE=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 主界面显示误差柱状图与累计概率曲线,支持调整分箱数。 - 调整计数轴范围和刻度,概率刻度间隔为 10%。 - 支持关闭重开,新模型生成后清除旧图。 --- Include/nmNum/nmSubWnd/nmSubWndMain.h | 2 + Src/nmNum/nmSubWnd/nmSubWndMain.cpp | 170 ++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) diff --git a/Include/nmNum/nmSubWnd/nmSubWndMain.h b/Include/nmNum/nmSubWnd/nmSubWndMain.h index ba0288e3..dc811318 100644 --- a/Include/nmNum/nmSubWnd/nmSubWndMain.h +++ b/Include/nmNum/nmSubWnd/nmSubWndMain.h @@ -168,6 +168,8 @@ class NM_SUB_WND_EXPORT nmSubWndMain : public iSubWndBaseFit { void onSensitivityTaskDestroyed(); /** @brief 在框架已有的敏感性子窗口叠加显示本分析的双对数结果。 */ void onShowSensitiveResults(); + /** @brief 刷新主界面的误差直方图,修改分箱数时复用当前批次结果。 */ + void updateSensitiveHistogramPlot(); /** @brief 当前分析的建网提交结束后,恢复待启动或待捕获的求解流程。 */ void onPebiGridGenerationFinished(quint64 nGridInputRevision, bool bSucceeded); diff --git a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp index aa6c3e9c..72a286e5 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp @@ -13,6 +13,7 @@ #include "ZxPlot.h" #include "ZxObjCurve.h" +#include "ZxObjCurveBar.h" #include "ZxSubTitle.h" #include "tCurvePlotScene.h" #include "tCurvePlotView.h" @@ -74,6 +75,9 @@ #include #include #include +#include +#include +#include #include #include "ZxMdiArea.h" @@ -108,6 +112,25 @@ namespace QPointer s_pRunningSolverTask; QPointer s_pPendingSolverWindow; +class nmSensitivityHistogramBars : public ZxObjCurveBar +{ +public: + explicit nmSensitivityHistogramBars(double binWidth) : m_binWidth(binWidth) {} + +protected: + void drawSubBars(QPainter* painter, QVector& 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* resultReferenceRatesOf( const nmPebiResultWellSnapshot* pWell) @@ -1875,6 +1898,7 @@ void nmSubWndMain::onShowSensitiveResults() iSubWndFitting* fitting = m_pResultRefreshFitting.data(); if(!manager || !fitting || !manager->getSensitiveData()) return; updateSensitiveHistoryPlot(); + updateSensitiveHistogramPlot(); QSharedPointer result = manager->getSensitiveData()->getResults(); iGuiPlot* plot = fitting->getPlotByTag(s_Tag_ChartSensitivity); if(result.isNull()) { @@ -2089,6 +2113,152 @@ void nmSubWndMain::updateSensitiveHistoryPlot() 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("nmSensitivityHistogram"); + const QSharedPointer 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(); + const int binCount = window->findChild("nmHistogramBins")->value(); + QVector 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("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 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(plot->m_pPlot->getObjByIndex(i))) plot->m_pPlot->removeObjByIndex(i); + plot->clearUndoStack(); + QVector curves; + int largestCount = 0; + if(!errors.isEmpty()) { + QVector 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() { if(!m_bSolverStartPending) {