From 2384ddb76a0f8be78863d749d130f434466fd50d Mon Sep 17 00:00:00 2001 From: lvjunjie Date: Fri, 16 Oct 2026 17:53:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(nmNum):=20=E5=A2=9E=E5=8A=A0=E6=8B=9F?= =?UTF-8?q?=E5=90=88=E5=8F=82=E6=95=B0=E7=BB=93=E6=9E=9C=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 内存保留每组最优参数,结果表增加拟合参数列。 - 直方图和散点图支持切换拟合参数。 - 基准点使用求解快照,误差轴保留阈值筛选。 --- .../nmCalculationDllPebiSolverTask.h | 2 +- Include/nmNum/nmData/nmDataSensitive.h | 6 +- Include/nmNum/nmSubWnd/nmSubWndMain.h | 2 +- Include/nmNum/nmSubWxs/nmWxSensitive.h | 4 +- .../nmCalculationDllPebiSolverTask.cpp | 32 ++++++- Src/nmNum/nmData/nmDataSensitive.cpp | 13 +++ Src/nmNum/nmSubWnd/nmSubWndMain.cpp | 50 ++++++---- Src/nmNum/nmSubWxs/nmWxSensitive.cpp | 95 +++++++++++++------ 8 files changed, 149 insertions(+), 55 deletions(-) diff --git a/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h b/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h index 1826f4c1..4c043119 100644 --- a/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h +++ b/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h @@ -149,7 +149,7 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { /** @brief 复用当前独立副本串行计算,保留进度、停止状态及每组失败原因。 */ bool executeSensitivityBatch(); /** @brief 固定本组采样参数,在独立输入上拟合并生成最终曲线。 */ - bool improveSensitivityRun(); + bool improveSensitivityRun(QVector& fittedParameters); /** @brief 执行当前唯一的 PEBI 求解模式。 */ bool execute(); /** @brief 成对释放构造时登记的 DataManager 后台使用权。 */ diff --git a/Include/nmNum/nmData/nmDataSensitive.h b/Include/nmNum/nmData/nmDataSensitive.h index fd41683c..c38ad5ea 100644 --- a/Include/nmNum/nmData/nmDataSensitive.h +++ b/Include/nmNum/nmData/nmDataSensitive.h @@ -106,6 +106,7 @@ public: RunResult() : m_nRunIndex(0), m_dPressureRmse(-1.0), m_bSucceeded(false), m_bCancelled(false), m_bVisible(true) {} quint64 m_nRunIndex; QVector m_vecParameters; + QVector m_vecFittedParameters; // 本组 LM 最优值,与批次拟合参数顺序一致,仅存内存。 QVector > m_vecPressure; QVector > m_vecLogLog; QVector > m_vecSemiLog; @@ -118,9 +119,10 @@ public: struct NM_DATA_EXPORT BatchResult { - BatchResult() : m_calcType(CALC_DETERMINISTIC), m_dFilterThreshold(-1.0), m_bCompleted(false), m_bStopped(false) {} + BatchResult() : m_calcType(CALC_DETERMINISTIC), m_dFilterThreshold(-1.0), m_nHistogramVariable(-1), m_bCompleted(false), m_bStopped(false) {} CalculationType m_calcType; QList m_variables; // 求解时冻结的配置,独立于之后的参数编辑。 + QList m_fitParameters; // 名称、基准单位和求解快照中的模型值,仅存内存。 QString m_sWellCode; QString m_sWellName; QString m_sAxisXName; @@ -132,6 +134,8 @@ public: std::vector m_runs; // 散点筛选阈值只随当前批次保存在内存中,负值表示尚未筛选。 double m_dFilterThreshold; + int m_nHistogramVariable; // -1 为误差,其余为拟合参数下标,仅存内存。 + bool statisticValue(const RunResult& run, int variable, double& value) const; bool filteredParameterBounds(int index, double& minimum, double& maximum) const; bool m_bCompleted; bool m_bStopped; diff --git a/Include/nmNum/nmSubWnd/nmSubWndMain.h b/Include/nmNum/nmSubWnd/nmSubWndMain.h index 5a1977b0..de6084f2 100644 --- a/Include/nmNum/nmSubWnd/nmSubWndMain.h +++ b/Include/nmNum/nmSubWnd/nmSubWndMain.h @@ -173,7 +173,7 @@ class NM_SUB_WND_EXPORT nmSubWndMain : public iSubWndBaseFit { /** @brief 刷新主界面的误差直方图,修改分箱数时复用当前批次结果。 */ void updateSensitiveHistogramPlot(); void onSensitiveHistogramOptions(); - void onSensitiveHistogramBinsChanged(int bins); + void onSensitiveHistogramSettingsChanged(int bins, int variable); void onSensitiveScatter(); /** @brief 当前分析的建网提交结束后,恢复待启动或待捕获的求解流程。 */ void onPebiGridGenerationFinished(quint64 nGridInputRevision, diff --git a/Include/nmNum/nmSubWxs/nmWxSensitive.h b/Include/nmNum/nmSubWxs/nmWxSensitive.h index e2838f3e..3de2f69b 100644 --- a/Include/nmNum/nmSubWxs/nmWxSensitive.h +++ b/Include/nmNum/nmSubWxs/nmWxSensitive.h @@ -35,12 +35,13 @@ public: nmWxSensitivityHistogramOptions(const QSharedPointer& results, int bins, QWidget* parent); signals: - void binsChanged(int bins); + void settingsChanged(int bins, int variable); private slots: void onApply(); void onAccept(); private: QSpinBox* m_bins; + QComboBox* m_variable; }; class NM_SUB_WXS_EXPORT nmWxSensitivityScatter : public iDlgBase @@ -65,6 +66,7 @@ private: double m_defaultThreshold; bool m_draggingThreshold; QComboBox* m_parameter; + QComboBox* m_ordinate; double m_threshold; QLabel* m_legend; QAction* m_resultsInPlot; diff --git a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp index d8455bab..bc365544 100644 --- a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp @@ -28,6 +28,8 @@ #include "nmDataPvtParaForPebi.h" #include +#include +#include #include #include #include @@ -991,6 +993,29 @@ nmCalculationDllPebiSolverTask* nmCalculationDllPebiSolverTask::createSensitivit task->m_pSensitivityResult->m_sWellCode = task->m_sAutoFitTargetWellCode; if(m_pDataManager != nullptr) { const QSharedPointer solved = m_pDataManager->getPebiResultSnapshot(); + // 拟合参数的蓝色基准点来自原模型,不使用设置窗口中可编辑的拟合初值。 + if(improveSettings && !solved.isNull()) { + const nmDataAutomaticFitting& fit = improveSettings->parameters; + const bool selected[] = { fit.getPermeabilitySelected(), fit.getSkinSelected(), + fit.getWellboreStorageSelected(), fit.getPorositySelected(), + fit.getFractureConductivitySelected(), fit.getFractureHalfLengthSelected() }; + const char* names[] = { "Permeability", "Skin", "Wellbore storage", "Porosity", + "Fracture conductivity", "Fracture half length" }; + const char* units[] = { "mD", "", "m^3/MPa", "", "mD.m", "m" }; + for(int i = 0; i < solved->getWellCount(); ++i) { + const nmPebiResultWellSnapshot* well = solved->getWellAt(i); + if(well->m_sWellName != improveSettings->targetWell) continue; + double halfLength = std::numeric_limits::quiet_NaN(); + if(selected[5]) getSolvedFractureHalfLength(solved->getSensitivityInput(), well->m_sWellCode, halfLength); + const double values[] = { m_pInputSnapshot->m_oStaticPropertyInput.m_dReferencePermeabilityMilliDarcy, + well->m_dSkin, well->m_dWellboreStorage, + m_pInputSnapshot->m_oStaticPropertyInput.m_dReferencePorosity, well->m_dDfc, halfLength }; + for(int j = 0; j < 6; ++j) if(selected[j]) + task->m_pSensitivityResult->m_fitParameters.append(nmDataAttribute( + QCoreApplication::translate("nmWxAutomaticFitting", names[j]), values[j], units[j])); + break; + } + } for(int i = 0; !solved.isNull() && i < solved->getWellCount(); ++i) { const nmPebiResultWellSnapshot* well = solved->getWellAt(i); if(well->m_sWellCode != task->m_sAutoFitTargetWellCode) continue; @@ -1065,7 +1090,7 @@ bool nmCalculationDllPebiSolverTask::getSolvedFractureHalfLength( return false; } -bool nmCalculationDllPebiSolverTask::improveSensitivityRun() +bool nmCalculationDllPebiSolverTask::improveSensitivityRun(QVector& fittedParameters) { // 采样值已经写入本任务;候选只覆盖选中的拟合参数,不触碰管理器和正式结果。 const nmSensitivityFitSettings& settings = *m_pSensitivityFitSettings; @@ -1172,6 +1197,8 @@ bool nmCalculationDllPebiSolverTask::improveSensitivityRun() return fail(error.isEmpty() ? tr("No valid fitting result was obtained.") : error); } evaluate(best); + // 最优参数与最终曲线一同返回,失败组不留下可显示的拟合值。 + if(m_lastRunSucceeded) fittedParameters = best; return m_lastRunSucceeded; } @@ -1210,7 +1237,7 @@ bool nmCalculationDllPebiSolverTask::executeSensitivityBatch() calculateSingleRun(); result.m_bSucceeded = m_lastRunSucceeded; } else { - result.m_bSucceeded = improveSensitivityRun(); + result.m_bSucceeded = improveSensitivityRun(result.m_vecFittedParameters); } if(result.m_bSucceeded) { result.m_vecPressure = m_autoFitResultPressure; @@ -1235,6 +1262,7 @@ bool nmCalculationDllPebiSolverTask::executeSensitivityBatch() result.m_vecLogLog.clear(); result.m_vecSemiLog.clear(); result.m_dPressureRmse = -1.0; + result.m_vecFittedParameters.clear(); } else if(!result.m_bSucceeded) { ++failed; } diff --git a/Src/nmNum/nmData/nmDataSensitive.cpp b/Src/nmNum/nmData/nmDataSensitive.cpp index 359b4f31..ac1e66af 100644 --- a/Src/nmNum/nmData/nmDataSensitive.cpp +++ b/Src/nmNum/nmData/nmDataSensitive.cpp @@ -5,6 +5,19 @@ #include #include +bool nmDataSensitive::BatchResult::statisticValue(const RunResult& run, int variable, double& value) const +{ + // 表格、直方图和散点图共用有效性判断,拟合参数不依赖压力误差是否可用。 + if(!run.m_bSucceeded || run.m_bCancelled) return false; + if(variable == -1) { + value = run.m_dPressureRmse; + return qIsFinite(value) && value >= 0.0; + } + if(variable < 0 || variable >= m_fitParameters.size() || variable >= run.m_vecFittedParameters.size()) return false; + value = run.m_vecFittedParameters[variable]; + return qIsFinite(value); +} + bool nmDataSensitive::BatchResult::filteredParameterBounds(int index, double& minimum, double& maximum) const { // 所有参数共用同一批误差严格小于阈值的样本,不受曲线可见性影响。 diff --git a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp index 3481589b..6c95f67f 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp @@ -2231,32 +2231,37 @@ void nmSubWndMain::updateSensitiveHistogramPlot() } iGuiPlot* plot = window->widget()->findChild(); const int binCount = window->widget()->property("nmHistogramBins").toInt(); - QVector errors; + // 拟合参数与误差共用分箱绘制,轴名称和单位随本批选择同步切换。 + const int variable = batch->m_nHistogramVariable; + plot->setAxisX(variable < 0 ? tr("Goodness of fit") : batch->m_fitParameters[variable].getName(), + variable < 0 ? "MPa" : batch->m_fitParameters[variable].getUnit()); + QVector values; 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); + // 显示勾选不改变统计总体,失败、取消或无有效统计值的运行不参与统计。 + double value; + if(batch->statisticValue(run, variable, value)) values.append(value); } 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(!values.isEmpty()) { + minimum = maximum = values.first(); + for(int i = 1; i < values.size(); ++i) { + minimum = qMin(minimum, values[i]); + maximum = qMax(maximum, values[i]); } - // 单个结果或全部相同的误差也需要非零横轴范围。 + // 单个结果或全部相同的统计值也需要非零横轴范围。 if(minimum == maximum) { - const double padding = qMax(1e-6, maximum * 0.05); - minimum = qMax(0.0, minimum - padding); + const double padding = qMax(1e-6, qAbs(maximum) * 0.05); + minimum -= padding; + if(variable < 0) minimum = qMax(0.0, minimum); 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)); + for(int i = 0; i < values.size(); ++i) { + // 等宽分箱,左闭右开,最大值归入最后一箱。 + const int index = qMin(binCount - 1, int((values[i] - minimum) / width)); ++counts[index]; } for(int i = plot->m_pPlot->getObjCount() - 1; i >= 0; --i) @@ -2264,7 +2269,7 @@ void nmSubWndMain::updateSensitiveHistogramPlot() plot->clearUndoStack(); QVector curves; int largestCount = 0; - if(!errors.isEmpty()) { + if(!values.isEmpty()) { QVector bars; iCurveDesc cumulative; cumulative.m_sCurveName = tr("Cumulative probability"); @@ -2281,7 +2286,7 @@ void nmSubWndMain::updateSensitiveHistogramPlot() largestCount = qMax(largestCount, counts[i]); total += counts[i]; cumulative.m_vecX.append(center); - cumulative.m_vecY.append(100.0 * total / errors.size()); + cumulative.m_vecY.append(100.0 * total / values.size()); } // 普通曲线入口不支持柱状图,直接使用框架柱状图元,累计概率仍走已有曲线入口。 nmSensitivityHistogramBars* histogram = new nmSensitivityHistogramBars(width); @@ -2334,7 +2339,7 @@ void nmSubWndMain::onSensitiveHistogramOptions() nmWxSensitivityHistogramOptions* dialog = window->findChild(); if(!dialog) { dialog = new nmWxSensitivityHistogramOptions(batch, window->widget()->property("nmHistogramBins").toInt(), window->widget()); - connect(dialog, SIGNAL(binsChanged(int)), this, SLOT(onSensitiveHistogramBinsChanged(int))); + connect(dialog, SIGNAL(settingsChanged(int,int)), this, SLOT(onSensitiveHistogramSettingsChanged(int,int))); connect(manager, SIGNAL(sigSensitiveResultsChanged()), dialog, SLOT(reject())); } dialog->show(); @@ -2342,9 +2347,14 @@ void nmSubWndMain::onSensitiveHistogramOptions() dialog->activateWindow(); } -void nmSubWndMain::onSensitiveHistogramBinsChanged(int bins) +void nmSubWndMain::onSensitiveHistogramSettingsChanged(int bins, int variable) { - // 分箱设置仅影响本图,散点图继续使用未分箱的逐组误差。 + // 直方图变量仅存于本批内存结果,散点图独立选择自己的纵轴。 + nmDataAnalyzeManager* manager = m_pResultRefreshDataManager.data(); + if(!manager || !manager->getSensitiveData()) return; + const QSharedPointer batch = manager->getSensitiveData()->getResults(); + if(batch.isNull()) return; + batch->m_nHistogramVariable = variable; iSubWndFitting* fitting = m_pResultRefreshFitting.data(); if(!fitting || !fitting->m_pMdiArea) return; QMdiSubWindow* window = fitting->m_pMdiArea->findChild("nmSensitivityHistogram"); diff --git a/Src/nmNum/nmSubWxs/nmWxSensitive.cpp b/Src/nmNum/nmSubWxs/nmWxSensitive.cpp index a6793402..3811d8fb 100644 --- a/Src/nmNum/nmSubWxs/nmWxSensitive.cpp +++ b/Src/nmNum/nmSubWxs/nmWxSensitive.cpp @@ -246,15 +246,22 @@ public: int columnCount(const QModelIndex& parent = QModelIndex()) const override { - return parent.isValid() ? 0 : 3; + return parent.isValid() ? 0 : 3 + (m_statistics ? m_results->m_fitParameters.size() : 0); } QVariant headerData(int section, Qt::Orientation orientation, int role) const override { // 表格仅保留显示选择、运行编号与冻结参数。 if(orientation != Qt::Horizontal || role != Qt::DisplayRole) return QVariant(); - if(m_statistics) return (QStringList() << "#" << nmWxSensitive::tr("Realization") + if(m_statistics) { + if(section >= 3 && section - 3 < m_results->m_fitParameters.size()) { + const nmDataAttribute& parameter = m_results->m_fitParameters[section - 3]; + return parameter.getName() + (parameter.getUnit().isEmpty() ? QString() + : " [" + parameter.getUnit() + "]"); + } + return (QStringList() << "#" << nmWxSensitive::tr("Realization") << nmWxSensitive::tr("Goodness of fit [MPa]")).value(section); + } const QStringList titles = QStringList() << nmWxSensitive::tr("Show") << "#" << nmWxSensitive::tr("Parameters"); return titles.value(section); @@ -270,7 +277,8 @@ public: if(role != Qt::DisplayRole && role != Qt::ToolTipRole) return QVariant(); if(index.column() == 0) return QString::number(run.m_nRunIndex + 1); if(index.column() == 1) return sensitivityRunParameters(*m_results, run); - return hasSensitivityError(run) ? QString::number(run.m_dPressureRmse, 'g', 6) + double value; + return m_results->statisticValue(run, index.column() - 3, value) ? QString::number(value, 'g', 6) : run.m_bCancelled ? nmWxSensitive::tr("Cancelled") : !run.m_bSucceeded ? nmWxSensitive::tr("Failed") : nmWxSensitive::tr("N/A"); } @@ -420,7 +428,7 @@ nmWxSensitivityHistogramOptions::nmWxSensitivityHistogramOptions( const QSharedPointer& results, int bins, QWidget* parent) : iDlgBase(parent) { - // 复用运行表格的参数展示,普通 Monte-Carlo 的统计变量只有误差。 + // 复用运行表格;第四种在误差后追加拟合参数,选择变量后按原有 Apply/OK 流程生效。 setAttribute(Qt::WA_DeleteOnClose); setObjectName("nmHistogramOptions"); setWindowTitle(tr("Histogram options")); @@ -434,9 +442,12 @@ nmWxSensitivityHistogramOptions::nmWxSensitivityHistogramOptions( options->addWidget(m_bins); options->addStretch(); options->addWidget(new QLabel(tr("Variable:"), this)); - QComboBox* variable = new QComboBox(this); - variable->addItem(tr("Goodness of fit")); - options->addWidget(variable); + m_variable = new QComboBox(this); + m_variable->addItem(tr("Goodness of fit"), -1); + for(int i = 0; i < results->m_fitParameters.size(); ++i) + m_variable->addItem(results->m_fitParameters[i].getName(), i); + m_variable->setCurrentIndex(m_variable->findData(results->m_nHistogramVariable)); + options->addWidget(m_variable); layout->addLayout(options); QTableView* table = new QTableView(this); table->setStyleSheet("QTableView { background-color: white; }"); @@ -447,7 +458,8 @@ nmWxSensitivityHistogramOptions::nmWxSensitivityHistogramOptions( table->verticalHeader()->hide(); table->horizontalHeader()->setResizeMode(0, QHeaderView::ResizeToContents); table->horizontalHeader()->setResizeMode(1, QHeaderView::Stretch); - table->horizontalHeader()->setResizeMode(2, QHeaderView::ResizeToContents); + for(int i = 2; i < table->model()->columnCount(); ++i) + table->horizontalHeader()->setResizeMode(i, QHeaderView::ResizeToContents); layout->addWidget(table); QHBoxLayout* buttons = new QHBoxLayout; buttons->addStretch(); @@ -465,8 +477,8 @@ nmWxSensitivityHistogramOptions::nmWxSensitivityHistogramOptions( void nmWxSensitivityHistogramOptions::onApply() { - // 只提交分箱数,取消不会撤销已经应用的设置。 - emit binsChanged(m_bins->value()); + // 分箱数与横轴变量一起提交,取消不会撤销已经应用的设置。 + emit settingsChanged(m_bins->value(), m_variable->itemData(m_variable->currentIndex()).toInt()); } void nmWxSensitivityHistogramOptions::onAccept() @@ -496,9 +508,11 @@ nmWxSensitivityScatter::nmWxSensitivityScatter( options->addWidget(m_parameter, 1); options->addSpacing(20); options->addWidget(new QLabel(tr("Y Axis:"), this)); - QComboBox* ordinate = new QComboBox(this); - ordinate->addItem(tr("Goodness of fit")); - options->addWidget(ordinate, 1); + m_ordinate = new QComboBox(this); + m_ordinate->addItem(tr("Goodness of fit"), -1); + for(int i = 0; i < results->m_fitParameters.size(); ++i) + m_ordinate->addItem(results->m_fitParameters[i].getName(), i); + options->addWidget(m_ordinate, 1); for(size_t i = 0; i < results->m_runs.size(); ++i) if(hasSensitivityError(results->m_runs[i])) m_defaultThreshold = qMax(m_defaultThreshold, results->m_runs[i].m_dPressureRmse); if(qIsFinite(baseError) && baseError >= 0.0) m_defaultThreshold = baseError; @@ -526,13 +540,19 @@ nmWxSensitivityScatter::nmWxSensitivityScatter( updateLegend(); m_plot->m_pPlotView->viewport()->installEventFilter(this); connect(m_parameter, SIGNAL(currentIndexChanged(int)), this, SLOT(updateParameter())); + connect(m_ordinate, SIGNAL(currentIndexChanged(int)), this, SLOT(updateParameter())); connect(m_plot->m_pPlotView, SIGNAL(sigAdjustContextMenu(QMenu*)), this, SLOT(adjustContextMenu(QMenu*))); updateParameter(); } void nmWxSensitivityScatter::updateParameter() { - // 切换横轴时重建参数坐标,阈值在各参数之间共用。 + // 两轴切换都从本批结果重建坐标;拟合参数轴不使用误差阈值。 + m_draggingThreshold = false; + const int ordinate = m_ordinate->itemData(m_ordinate->currentIndex()).toInt(); + const bool errorAxis = ordinate == -1; + const double modelY = errorAxis ? m_baseError : m_results->m_fitParameters[ordinate].getValue().toDouble(); + m_plot->setAxisY(m_ordinate->currentText(), errorAxis ? "MPa" : m_results->m_fitParameters[ordinate].getUnit()); m_points.clear(); m_runRows.clear(); if(m_parameter->currentIndex() < 0) { @@ -545,17 +565,19 @@ void nmWxSensitivityScatter::updateParameter() m_plot->setAxisX(variable.m_varName, unit); double modelValue = variable.m_modelValue.getValue().toDouble(); const bool hasModel = convertSamplingValue(variable.m_varName, modelValue, - samplingBaseUnit(variable.m_varName), unit, modelValue) && m_baseError >= 0.0 && qIsFinite(m_baseError); + samplingBaseUnit(variable.m_varName), unit, modelValue) && qIsFinite(modelY) && (!errorAxis || modelY >= 0.0); for(size_t i = 0; i < m_results->m_runs.size(); ++i) { const nmDataSensitive::RunResult& run = m_results->m_runs[i]; - if(!hasSensitivityError(run) || index >= run.m_vecParameters.size()) continue; + double ordinateValue; + if(!m_results->statisticValue(run, ordinate, ordinateValue) || index >= run.m_vecParameters.size()) continue; double value = run.m_vecParameters[index]; if(!convertSamplingValue(variable.m_varName, value, samplingBaseUnit(variable.m_varName), unit, value)) continue; - m_points.append(QPointF(value, run.m_dPressureRmse)); + m_points.append(QPointF(value, ordinateValue)); m_runRows.append(int(i)); } QVector curves; for(int i = 0; i < 4; ++i) { + if(!errorAxis && (i == 1 || i == 3)) continue; iCurveDesc curve; curve.m_sCurveName = i == 0 ? tr("sensitivity data") : i == 1 ? tr("Below threshold") : i == 2 ? tr("reference") : tr("Threshold"); @@ -570,7 +592,7 @@ void nmWxSensitivityScatter::updateParameter() } if(i == 2 && hasModel) { curve.m_vecX.append(modelValue); - curve.m_vecY.append(m_baseError); + curve.m_vecY.append(modelY); } if(i == 3) { curve.m_bLineVisible = true; @@ -589,30 +611,41 @@ void nmWxSensitivityScatter::updateParameter() m_plot->runUpdate(); m_plot->setReadOnly(true); double left = hasModel ? modelValue : m_points.isEmpty() ? 0.0 : m_points.first().x(); - double right = left, top = hasModel ? m_baseError : 0.0; + double right = left; + double bottom = hasModel ? modelY : m_points.isEmpty() ? 0.0 : m_points.first().y(); + double top = bottom; for(int i = 0; i < m_points.size(); ++i) { left = qMin(left, m_points[i].x()); right = qMax(right, m_points[i].x()); + bottom = qMin(bottom, m_points[i].y()); top = qMax(top, m_points[i].y()); } const double padding = qMax(1e-6, (right > left ? right - left : qMax(1.0, qAbs(left))) * 0.05); left -= padding; right += padding; - top = qMax(1e-6, qMax(top, m_threshold) * 1.1); + if(errorAxis) { + bottom = 0.0; + top = qMax(1e-6, qMax(top, m_threshold) * 1.1); + } else { + const double margin = qMax(1e-6, (top > bottom ? top - bottom : qMax(1.0, qAbs(top))) * 0.05); + bottom -= margin; + top += margin; + } VecDouble ticksX, ticksY; for(int i = 0; i <= 5; ++i) { ticksX.append(left + i * (right - left) / 5.0); - ticksY.append(i * top / 5.0); + ticksY.append(bottom + i * (top - bottom) / 5.0); } m_plot->setAxisScaleX(left, right, ticksX); - m_plot->setAxisScaleY(0.0, top, ticksY); - updateThreshold(); + m_plot->setAxisScaleY(bottom, top, ticksY); + updateLegend(); + if(errorAxis) updateThreshold(); } void nmWxSensitivityScatter::updateThreshold() { // 阈值以下的点填黑,并同步图例中的阈值显示。 - if(m_parameter->currentIndex() < 0) return; + if(m_parameter->currentIndex() < 0 || m_ordinate->currentIndex() != 0) return; // 拖动时只更新图内反馈,松开后再保存阈值并刷新结果面板。 if(!m_draggingThreshold && m_results->m_dFilterThreshold != m_threshold) { m_results->m_dFilterThreshold = m_threshold; @@ -669,6 +702,7 @@ void nmWxSensitivityScatter::resetThreshold() void nmWxSensitivityScatter::adjustContextMenu(QMenu* menu) { // 复用绘图右键菜单,保留框架的缩放、复制与导出入口。 + if(m_ordinate->currentIndex() != 0) return; menu->addAction(m_resultsInPlot); menu->addAction(tr("Reset line"), this, SLOT(resetThreshold())); } @@ -678,7 +712,7 @@ void nmWxSensitivityScatter::updateLegend() // 勾选后在原有两项图例下追加阈值,拖线和重置时实时更新。 QString text = "
□" + tr("sensitivity data") + "
■" + tr("reference") + "
"; - if(m_resultsInPlot->isChecked()) + if(m_ordinate->currentIndex() == 0 && m_resultsInPlot->isChecked()) text += "
" + tr("Threshold: %1 MPa").arg(m_threshold, 0, 'g', 6) + "
"; m_legend->setText(text); m_legend->adjustSize(); @@ -693,7 +727,8 @@ bool nmWxSensitivityScatter::eventFilter(QObject* object, QEvent* event) if(event->type() == QEvent::Paint) m_legend->move(view->mapFromScene(m_plot->m_pPlot->getInnerRectF().topLeft()).toPoint() + QPoint(8, 8)); // 只接管阈值线附近的左键拖动,其余缩放、右键及导出仍由框架处理。 - if(event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseMove || event->type() == QEvent::MouseButtonRelease) { + if(m_ordinate->currentIndex() == 0 && (event->type() == QEvent::MouseButtonPress || + event->type() == QEvent::MouseMove || event->type() == QEvent::MouseButtonRelease)) { QMouseEvent* mouse = static_cast(event); const QPointF scene = view->mapToScene(QPointF(mouse->pos())); const double lineY = view->mapFromScene(QPointF(scene.x(), axisY->getPosForValue(m_threshold))).y(); @@ -713,9 +748,11 @@ bool nmWxSensitivityScatter::eventFilter(QObject* object, QEvent* event) const QPointF position = view->mapFromScene(QPointF(m_plot->getAxisOf(0)->getPosForValue(m_points[i].x()), axisY->getPosForValue(m_points[i].y()))); if(qAbs(position.x() - help->pos().x()) > 6.0 || qAbs(position.y() - help->pos().y()) > 6.0) continue; const nmDataSensitive::RunResult& run = m_results->m_runs[m_runRows[i]]; - QToolTip::showText(help->globalPos(), tr("Run %1\n%2\nGoodness of fit: %3 MPa") - .arg(run.m_nRunIndex + 1).arg(sensitivityRunParameters(*m_results, run)) - .arg(run.m_dPressureRmse, 0, 'g', 6), view->viewport()); + const int ordinate = m_ordinate->itemData(m_ordinate->currentIndex()).toInt(); + const QString unit = ordinate < 0 ? "MPa" : m_results->m_fitParameters[ordinate].getUnit(); + QToolTip::showText(help->globalPos(), tr("Run %1\n%2").arg(run.m_nRunIndex + 1) + .arg(sensitivityRunParameters(*m_results, run)) + "\n" + m_ordinate->currentText() + + ": " + QString::number(m_points[i].y(), 'g', 6) + " " + unit, view->viewport()); return true; } QToolTip::hideText();