feat(nmNum): 完善敏感性分析曲线显示与选择控制

- 敏感性分析曲线统一使用实线,叠加并保存原模型数值解,沿用普通双对数图样式。
- 结果列表移除状态与颜色显示,支持应用、确定和取消,应用后保持窗口打开。
- 刷新绘图时移除隐藏及上一批残留曲线,修复取消勾选后曲线仍显示的问题。
feature/Sensitivity-20261012
lvjunjie 1 week ago
parent 72d4063539
commit 6c6ba205ba

@ -120,6 +120,7 @@ public:
QString m_sAxisYName; QString m_sAxisYName;
QString m_sAxisYUnit; QString m_sAxisYUnit;
QVector<QVector<double> > m_vecHistoryLogLog; QVector<QVector<double> > m_vecHistoryLogLog;
QVector<QVector<double> > m_vecBaseLogLog; // 批次启动时原模型已有的数值解。
std::vector<RunResult> m_runs; std::vector<RunResult> m_runs;
bool m_bCompleted; bool m_bCompleted;
bool m_bStopped; bool m_bStopped;

@ -60,7 +60,8 @@ protected:
void onSensitivityFinished(); void onSensitivityFinished();
void onResultsClicked(); void onResultsClicked();
void onResultsChanged(); void onResultsChanged();
void onResultsVisibilityChanged(); void onApplyResults();
void onAcceptResults();
void onShowAllResults(); void onShowAllResults();
void onHideAllResults(); void onHideAllResults();
@ -91,7 +92,6 @@ private:
QPushButton *resultsButton; QPushButton *resultsButton;
QPointer<QDialog> m_resultsDialog; QPointer<QDialog> m_resultsDialog;
QPointer<SensitivityResultsModel> m_resultsModel; QPointer<SensitivityResultsModel> m_resultsModel;
bool m_refreshingResults;
// 各个变量页面的控件列表 // 各个变量页面的控件列表
QList<QComboBox*> m_modeCombos; QList<QComboBox*> m_modeCombos;

@ -920,6 +920,8 @@ nmCalculationDllPebiSolverTask* nmCalculationDllPebiSolverTask::createSensitivit
if(well != nullptr) { if(well != nullptr) {
task->m_pSensitivityResult->m_sWellName = well->getWellName(); task->m_pSensitivityResult->m_sWellName = well->getWellName();
task->m_pSensitivityResult->m_vecHistoryLogLog = well->getHistoryLogLog(); task->m_pSensitivityResult->m_vecHistoryLogLog = well->getHistoryLogLog();
// 复用原模型已有数值解作为对比基准,不增加一次求解或跟随后续结果变化。
task->m_pSensitivityResult->m_vecBaseLogLog = well->getResultLogLog();
} }
} }
return task.take(); return task.take();

@ -86,6 +86,7 @@ rapidjson::Value nmDataSensitive::BatchResult::ToJsonValue(
value.AddMember("Stopped", m_bStopped, allocator); value.AddMember("Stopped", m_bStopped, allocator);
value.AddMember("FailureReason", rapidjson::Value(m_sFailureReason.toUtf8().constData(), allocator).Move(), allocator); value.AddMember("FailureReason", rapidjson::Value(m_sFailureReason.toUtf8().constData(), allocator).Move(), allocator);
value.AddMember("HistoryLogLog", sensitiveCurvesToJson(m_vecHistoryLogLog, allocator), allocator); value.AddMember("HistoryLogLog", sensitiveCurvesToJson(m_vecHistoryLogLog, allocator), allocator);
value.AddMember("BaseLogLog", sensitiveCurvesToJson(m_vecBaseLogLog, allocator), allocator);
rapidjson::Value variables(rapidjson::kArrayType); rapidjson::Value variables(rapidjson::kArrayType);
for(int i = 0; i < m_variables.size(); ++i) for(int i = 0; i < m_variables.size(); ++i)
variables.PushBack(m_variables[i].ToJsonValue(allocator), allocator); variables.PushBack(m_variables[i].ToJsonValue(allocator), allocator);
@ -127,6 +128,7 @@ void nmDataSensitive::BatchResult::FromJsonValue(const rapidjson::Value& value)
m_bCompleted = value.HasMember("Completed") && value["Completed"].IsBool() && value["Completed"].GetBool(); m_bCompleted = value.HasMember("Completed") && value["Completed"].IsBool() && value["Completed"].GetBool();
m_bStopped = value.HasMember("Stopped") && value["Stopped"].IsBool() && value["Stopped"].GetBool(); m_bStopped = value.HasMember("Stopped") && value["Stopped"].IsBool() && value["Stopped"].GetBool();
m_vecHistoryLogLog = sensitiveCurvesFromJson(value, "HistoryLogLog"); m_vecHistoryLogLog = sensitiveCurvesFromJson(value, "HistoryLogLog");
m_vecBaseLogLog = sensitiveCurvesFromJson(value, "BaseLogLog");
if(value.HasMember("Variables") && value["Variables"].IsArray()) { if(value.HasMember("Variables") && value["Variables"].IsArray()) {
const rapidjson::Value& variables = value["Variables"]; const rapidjson::Value& variables = value["Variables"];
for(rapidjson::SizeType i = 0; i < variables.Size(); ++i) { for(rapidjson::SizeType i = 0; i < variables.Size(); ++i) {

@ -1897,15 +1897,27 @@ void nmSubWndMain::onShowSensitiveResults()
data[0].size() != data[2].size() || data[0].isEmpty()) continue; data[0].size() != data[2].size() || data[0].isEmpty()) continue;
QVector<iCurveDesc> runCurves; QVector<iCurveDesc> runCurves;
if(!initloglog(data, runCurves, false)) continue; if(!initloglog(data, runCurves, false)) continue;
// 每组使用稳定名称和颜色,压力实线、导数虚线;组间不会覆盖同名曲线。 // 每组压力与导数均使用实线,稳定名称和颜色避免组间曲线覆盖。
const QColor color = QColor::fromHsv(int((run.m_nRunIndex % 360) * 137 % 360), 190, 190); const QColor color = QColor::fromHsv(int((run.m_nRunIndex % 360) * 137 % 360), 190, 190);
runCurves[0].m_sCurveName = nmWxSensitive::tr("Run %1 - Pressure").arg(run.m_nRunIndex + 1); runCurves[0].m_sCurveName = nmWxSensitive::tr("Run %1 - Pressure").arg(run.m_nRunIndex + 1);
runCurves[1].m_sCurveName = nmWxSensitive::tr("Run %1 - Derivative").arg(run.m_nRunIndex + 1); runCurves[1].m_sCurveName = nmWxSensitive::tr("Run %1 - Derivative").arg(run.m_nRunIndex + 1);
runCurves[0].m_oPen.setColor(color); runCurves[0].m_oPen.setColor(color);
runCurves[1].m_oPen.setColor(color); runCurves[1].m_oPen.setColor(color);
runCurves[1].m_oPen.setStyle(Qt::DashLine); runCurves[0].m_oPen.setStyle(Qt::SolidLine);
runCurves[1].m_oPen.setStyle(Qt::SolidLine);
curves += runCurves; curves += runCurves;
} }
// 原模型数值解沿用普通双对数图的压力、导数样式,仅区分曲线名称。
QVector<QVector<double> > base = result->m_vecBaseLogLog;
if(base.size() == 3 && base[0].size() == base[1].size() &&
base[0].size() == base[2].size() && !base[0].isEmpty()) {
QVector<iCurveDesc> baseCurves;
if(initloglog(base, baseCurves, false)) {
baseCurves[0].m_sCurveName = nmWxSensitive::tr("Base model - Pressure");
baseCurves[1].m_sCurveName = nmWxSensitive::tr("Base model - Derivative");
curves += baseCurves;
}
}
// 对数坐标只接受有限正数,分别过滤压力与导数,保持各自时间和值对应。 // 对数坐标只接受有限正数,分别过滤压力与导数,保持各自时间和值对应。
for(int i = 0; i < curves.size(); ++i) { for(int i = 0; i < curves.size(); ++i) {
QVector<double> x, y; QVector<double> x, y;
@ -1925,6 +1937,19 @@ void nmSubWndMain::onShowSensitiveResults()
plot->setAxisXYLog(true, 1); plot->setAxisXYLog(true, 1);
if(!result->m_sAxisXName.isEmpty()) plot->setAxisX(result->m_sAxisXName, result->m_sAxisXUnit); if(!result->m_sAxisXName.isEmpty()) plot->setAxisX(result->m_sAxisXName, result->m_sAxisXUnit);
if(!result->m_sAxisYName.isEmpty()) plot->setAxisY(result->m_sAxisYName, result->m_sAxisYUnit); if(!result->m_sAxisYName.isEmpty()) plot->setAxisY(result->m_sAxisYName, result->m_sAxisYUnit);
// 框架刷新只创建或更新传入曲线,不删除缺席曲线,先移除已隐藏或上一批残留的图元。
QSet<QString> visibleNames;
for(int i = 0; i < curves.size(); ++i) visibleNames.insert(curves[i].m_sCurveName);
bool removed = false;
for(int i = plot->m_pPlot->getObjCount() - 1; i >= 0; --i) {
ZxObjCurve* curve = qobject_cast<ZxObjCurve*>(plot->m_pPlot->getObjByIndex(i));
if(curve && !visibleNames.contains(curve->getName())) {
plot->m_pPlot->removeObjByIndex(i);
removed = true;
}
}
// 清理删除曲线相关的撤销记录,避免后续操作恢复已经隐藏的图元。
if(removed) plot->clearUndoStack();
plot->setCurveData(curves); plot->setCurveData(curves);
plot->runUpdate(); plot->runUpdate();
plot->setReadOnly(true); plot->setReadOnly(true);

@ -28,6 +28,7 @@
#include <QTableWidget> #include <QTableWidget>
#include <QTableView> #include <QTableView>
#include <QAbstractTableModel> #include <QAbstractTableModel>
#include <QMap>
#include <QAbstractScrollArea> #include <QAbstractScrollArea>
#include <QScrollBar> #include <QScrollBar>
#include <QWheelEvent> #include <QWheelEvent>
@ -178,13 +179,14 @@ public:
void setResults(const QSharedPointer<nmDataSensitive::BatchResult>& results) void setResults(const QSharedPointer<nmDataSensitive::BatchResult>& results)
{ {
// 同一批次可能在其他结果列表中改变可见性,同步勾选而不重置滚动位置。 // 同一批次同步已应用的选择,并保留本窗口尚未应用的勾选。
if(m_results == results) { if(m_results == results) {
if(rowCount() > 0) emit dataChanged(index(0, 0), index(rowCount() - 1, 0)); if(rowCount() > 0) emit dataChanged(index(0, 0), index(rowCount() - 1, 0));
return; return;
} }
beginResetModel(); beginResetModel();
m_results = results; m_results = results;
m_pendingVisibility.clear();
endResetModel(); endResetModel();
} }
@ -196,15 +198,15 @@ public:
int columnCount(const QModelIndex& parent = QModelIndex()) const override int columnCount(const QModelIndex& parent = QModelIndex()) const override
{ {
return parent.isValid() ? 0 : 4; return parent.isValid() ? 0 : 3;
} }
QVariant headerData(int section, Qt::Orientation orientation, int role) const override QVariant headerData(int section, Qt::Orientation orientation, int role) const override
{ {
// 表格显示运行编号、冻结参数与实际计算状态。 // 表格仅保留显示选择、运行编号与冻结参数。
if(orientation != Qt::Horizontal || role != Qt::DisplayRole) return QVariant(); if(orientation != Qt::Horizontal || role != Qt::DisplayRole) return QVariant();
const QStringList titles = QStringList() << nmWxSensitive::tr("Show") << "#" const QStringList titles = QStringList() << nmWxSensitive::tr("Show") << "#"
<< nmWxSensitive::tr("Parameters") << nmWxSensitive::tr("Status"); << nmWxSensitive::tr("Parameters");
return titles.value(section); return titles.value(section);
} }
@ -214,16 +216,13 @@ public:
if(!index.isValid() || index.row() >= rowCount()) return QVariant(); if(!index.isValid() || index.row() >= rowCount()) return QVariant();
const nmDataSensitive::RunResult& run = m_results->m_runs[index.row()]; const nmDataSensitive::RunResult& run = m_results->m_runs[index.row()];
if(role == Qt::CheckStateRole && index.column() == 0) if(role == Qt::CheckStateRole && index.column() == 0)
return run.m_bSucceeded && !run.m_bCancelled && run.m_bVisible ? Qt::Checked : Qt::Unchecked; return run.m_bSucceeded && !run.m_bCancelled &&
if(role == Qt::DecorationRole && index.column() == 0) m_pendingVisibility.value(index.row(), run.m_bVisible) ? Qt::Checked : Qt::Unchecked;
return QColor::fromHsv(int((run.m_nRunIndex % 360) * 137 % 360), 190, 190);
if(role == Qt::TextAlignmentRole && index.column() == 1) if(role == Qt::TextAlignmentRole && index.column() == 1)
return int(Qt::AlignRight | Qt::AlignVCenter); return int(Qt::AlignRight | Qt::AlignVCenter);
if(role == Qt::ToolTipRole && index.column() == 3) return run.m_sFailureReason; if(role == Qt::ToolTipRole && !run.m_sFailureReason.isEmpty()) return run.m_sFailureReason;
if(role != Qt::DisplayRole && role != Qt::ToolTipRole) return QVariant(); if(role != Qt::DisplayRole && role != Qt::ToolTipRole) return QVariant();
if(index.column() == 1) return QString::number(run.m_nRunIndex + 1); if(index.column() == 1) return QString::number(run.m_nRunIndex + 1);
if(index.column() == 3) return run.m_bCancelled ? nmWxSensitive::tr("Stopped")
: run.m_bSucceeded ? nmWxSensitive::tr("Succeeded") : nmWxSensitive::tr("Failed");
if(index.column() != 2) return QVariant(); if(index.column() != 2) return QVariant();
QStringList descriptions; QStringList descriptions;
for(int i = 0; i < m_results->m_variables.size() && i < run.m_vecParameters.size(); ++i) { for(int i = 0; i < m_results->m_variables.size() && i < run.m_vecParameters.size(); ++i) {
@ -251,26 +250,49 @@ public:
bool setData(const QModelIndex& index, const QVariant& value, int role) override bool setData(const QModelIndex& index, const QVariant& value, int role) override
{ {
// 可见性只在结果发布后的界面线程改变,不触发再次求解。 // 勾选只记在本窗口,点击 Apply 或 OK 后才写入绘图使用的结果。
if(!index.isValid() || index.column() != 0 || role != Qt::CheckStateRole) return false; if(!index.isValid() || index.column() != 0 || role != Qt::CheckStateRole) return false;
nmDataSensitive::RunResult& run = m_results->m_runs[index.row()]; const nmDataSensitive::RunResult& run = m_results->m_runs[index.row()];
if(!run.m_bSucceeded || run.m_bCancelled) return false; if(!run.m_bSucceeded || run.m_bCancelled) return false;
run.m_bVisible = value.toInt() == Qt::Checked; const bool visible = value.toInt() == Qt::Checked;
if(visible == run.m_bVisible) m_pendingVisibility.remove(index.row());
else m_pendingVisibility.insert(index.row(), visible);
emit dataChanged(index, index); emit dataChanged(index, index);
return true; return true;
} }
void setAllVisible(bool visible) void setAllVisible(bool visible)
{ {
// 批量切换后只发送一次通知,整批曲线也只重新绘制一次。 // 全选与全隐藏也只改变待应用选择,不立即刷新曲线。
if(m_results.isNull() || m_results->m_runs.empty()) return; if(m_results.isNull() || m_results->m_runs.empty()) return;
for(size_t i = 0; i < m_results->m_runs.size(); ++i) for(int i = 0; i < rowCount(); ++i) {
m_results->m_runs[i].m_bVisible = visible; const nmDataSensitive::RunResult& run = m_results->m_runs[i];
if(!run.m_bSucceeded || run.m_bCancelled) continue;
if(visible == run.m_bVisible) m_pendingVisibility.remove(i);
else m_pendingVisibility.insert(i, visible);
}
emit dataChanged(index(0, 0), index(rowCount() - 1, 0)); emit dataChanged(index(0, 0), index(rowCount() - 1, 0));
} }
bool applyVisibility()
{
// 只提交本窗口改过的组,其他结果列表已应用的选择保持不变。
bool changed = false;
QMap<int, bool>::const_iterator it = m_pendingVisibility.constBegin();
for(; it != m_pendingVisibility.constEnd(); ++it) {
nmDataSensitive::RunResult& run = m_results->m_runs[it.key()];
if(run.m_bVisible != it.value()) {
run.m_bVisible = it.value();
changed = true;
}
}
m_pendingVisibility.clear();
return changed;
}
private: private:
QSharedPointer<nmDataSensitive::BatchResult> m_results; QSharedPointer<nmDataSensitive::BatchResult> m_results;
QMap<int, bool> m_pendingVisibility;
}; };
// ========= 右对齐委托:保证显示和编辑时都是右对齐 ========= // ========= 右对齐委托:保证显示和编辑时都是右对齐 =========
@ -575,7 +597,7 @@ private:
}; };
nmWxSensitive::nmWxSensitive(QWidget *parent, nmDataAnalyzeManager* manager) nmWxSensitive::nmWxSensitive(QWidget *parent, nmDataAnalyzeManager* manager)
: iDlgBase(parent), m_refreshingResults(false), m_selectedValueIndex(0), m_loading(true), : iDlgBase(parent), m_selectedValueIndex(0), m_loading(true),
m_pDataManager(manager ? manager : nmDataAnalyzeManager::getCurrentInstance()), m_pDataManager(manager ? manager : nmDataAnalyzeManager::getCurrentInstance()),
m_previewView(nullptr), m_pSensitivityTask(nullptr), m_calculationRunning(false) m_previewView(nullptr), m_pSensitivityTask(nullptr), m_calculationRunning(false)
{ {
@ -1672,22 +1694,29 @@ void nmWxSensitive::onResultsClicked()
table->horizontalHeader()->setResizeMode(0, QHeaderView::ResizeToContents); table->horizontalHeader()->setResizeMode(0, QHeaderView::ResizeToContents);
table->horizontalHeader()->setResizeMode(1, QHeaderView::ResizeToContents); table->horizontalHeader()->setResizeMode(1, QHeaderView::ResizeToContents);
table->horizontalHeader()->setResizeMode(2, QHeaderView::Stretch); table->horizontalHeader()->setResizeMode(2, QHeaderView::Stretch);
table->horizontalHeader()->setResizeMode(3, QHeaderView::ResizeToContents);
layout->addWidget(table); layout->addWidget(table);
QHBoxLayout* buttons = new QHBoxLayout(); QHBoxLayout* buttons = new QHBoxLayout();
QPushButton* showAll = new QPushButton(tr("Show all"), m_resultsDialog.data()); QPushButton* showAll = new QPushButton(tr("Show all"), m_resultsDialog.data());
QPushButton* hideAll = new QPushButton(tr("Hide all"), m_resultsDialog.data()); QPushButton* hideAll = new QPushButton(tr("Hide all"), m_resultsDialog.data());
QPushButton* close = new QPushButton(tr("Close"), m_resultsDialog.data()); // 明确按钮顺序,避免系统的标准按钮布局把 Apply 排到最后。
QPushButton* apply = new QPushButton(tr("Apply"), m_resultsDialog.data());
QPushButton* ok = new QPushButton(tr("OK"), m_resultsDialog.data());
QPushButton* cancel = new QPushButton(tr("Cancel"), m_resultsDialog.data());
apply->setAutoDefault(false);
cancel->setAutoDefault(false);
ok->setDefault(true);
buttons->addWidget(showAll); buttons->addWidget(showAll);
buttons->addWidget(hideAll); buttons->addWidget(hideAll);
buttons->addStretch(); buttons->addStretch();
buttons->addWidget(close); buttons->addWidget(apply);
buttons->addWidget(ok);
buttons->addWidget(cancel);
layout->addLayout(buttons); layout->addLayout(buttons);
connect(m_resultsModel.data(), SIGNAL(dataChanged(QModelIndex,QModelIndex)),
this, SLOT(onResultsVisibilityChanged()));
connect(showAll, SIGNAL(clicked()), this, SLOT(onShowAllResults())); connect(showAll, SIGNAL(clicked()), this, SLOT(onShowAllResults()));
connect(hideAll, SIGNAL(clicked()), this, SLOT(onHideAllResults())); connect(hideAll, SIGNAL(clicked()), this, SLOT(onHideAllResults()));
connect(close, SIGNAL(clicked()), m_resultsDialog.data(), SLOT(close())); connect(apply, SIGNAL(clicked()), this, SLOT(onApplyResults()));
connect(ok, SIGNAL(clicked()), this, SLOT(onAcceptResults()));
connect(cancel, SIGNAL(clicked()), m_resultsDialog.data(), SLOT(reject()));
} }
onResultsChanged(); onResultsChanged();
hide(); hide();
@ -1704,10 +1733,7 @@ void nmWxSensitive::onResultsChanged()
results = m_pDataManager->getSensitiveData()->getResults(); results = m_pDataManager->getSensitiveData()->getResults();
resultsButton->setEnabled(!results.isNull()); resultsButton->setEnabled(!results.isNull());
m_sensitiveData.setResults(results); m_sensitiveData.setResults(results);
// 同步其他列表的勾选只刷新显示,不能再次发布结果造成循环通知。
m_refreshingResults = true;
if(!m_resultsModel.isNull()) m_resultsModel->setResults(results); if(!m_resultsModel.isNull()) m_resultsModel->setResults(results);
m_refreshingResults = false;
if(!m_resultsDialog.isNull()) { if(!m_resultsDialog.isNull()) {
const QString status = results.isNull() ? QString() : results->m_bStopped ? tr("Stopped") const QString status = results.isNull() ? QString() : results->m_bStopped ? tr("Stopped")
: results->m_bCompleted ? tr("Completed") : tr("Failed"); : results->m_bCompleted ? tr("Completed") : tr("Failed");
@ -1716,22 +1742,29 @@ void nmWxSensitive::onResultsChanged()
} }
} }
void nmWxSensitive::onResultsVisibilityChanged() void nmWxSensitive::onApplyResults()
{ {
// 结果选择与项目保存共用数据,通知主界面刷新图形而不重新计算。 // 应用选择后通知主界面刷新,结果选择窗口保持打开。
if(m_pDataManager && !m_refreshingResults) if(m_pDataManager && !m_resultsModel.isNull() && m_resultsModel->applyVisibility())
m_pDataManager->publishSensitiveResults(m_pDataManager->getSensitiveDataCopy()); m_pDataManager->publishSensitiveResults(m_pDataManager->getSensitiveDataCopy());
} }
void nmWxSensitive::onAcceptResults()
{
// OK 与 Apply 提交相同的选择,随后关闭窗口。
onApplyResults();
if(!m_resultsDialog.isNull()) m_resultsDialog->accept();
}
void nmWxSensitive::onShowAllResults() void nmWxSensitive::onShowAllResults()
{ {
// 恢复所有成功组的叠加曲线。 // 勾选所有成功组,等待 Apply 或 OK 更新曲线。
if(!m_resultsModel.isNull()) m_resultsModel->setAllVisible(true); if(!m_resultsModel.isNull()) m_resultsModel->setAllVisible(true);
} }
void nmWxSensitive::onHideAllResults() void nmWxSensitive::onHideAllResults()
{ {
// 隐藏所有计算组,保留测量曲线作为参考。 // 取消所有采样组的勾选,测量曲线和原模型数值解仍作为参考。
if(!m_resultsModel.isNull()) m_resultsModel->setAllVisible(false); if(!m_resultsModel.isNull()) m_resultsModel->setAllVisible(false);
} }

Loading…
Cancel
Save