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

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

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

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

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

@ -86,6 +86,7 @@ rapidjson::Value nmDataSensitive::BatchResult::ToJsonValue(
value.AddMember("Stopped", m_bStopped, allocator);
value.AddMember("FailureReason", rapidjson::Value(m_sFailureReason.toUtf8().constData(), allocator).Move(), allocator);
value.AddMember("HistoryLogLog", sensitiveCurvesToJson(m_vecHistoryLogLog, allocator), allocator);
value.AddMember("BaseLogLog", sensitiveCurvesToJson(m_vecBaseLogLog, allocator), allocator);
rapidjson::Value variables(rapidjson::kArrayType);
for(int i = 0; i < m_variables.size(); ++i)
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_bStopped = value.HasMember("Stopped") && value["Stopped"].IsBool() && value["Stopped"].GetBool();
m_vecHistoryLogLog = sensitiveCurvesFromJson(value, "HistoryLogLog");
m_vecBaseLogLog = sensitiveCurvesFromJson(value, "BaseLogLog");
if(value.HasMember("Variables") && value["Variables"].IsArray()) {
const rapidjson::Value& variables = value["Variables"];
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;
QVector<iCurveDesc> runCurves;
if(!initloglog(data, runCurves, false)) continue;
// 每组使用稳定名称和颜色,压力实线、导数虚线;组间不会覆盖同名曲线。
// 每组压力与导数均使用实线,稳定名称和颜色避免组间曲线覆盖。
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[1].m_sCurveName = nmWxSensitive::tr("Run %1 - Derivative").arg(run.m_nRunIndex + 1);
runCurves[0].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;
}
// 原模型数值解沿用普通双对数图的压力、导数样式,仅区分曲线名称。
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) {
QVector<double> x, y;
@ -1925,6 +1937,19 @@ void nmSubWndMain::onShowSensitiveResults()
plot->setAxisXYLog(true, 1);
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);
// 框架刷新只创建或更新传入曲线,不删除缺席曲线,先移除已隐藏或上一批残留的图元。
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->runUpdate();
plot->setReadOnly(true);

@ -28,6 +28,7 @@
#include <QTableWidget>
#include <QTableView>
#include <QAbstractTableModel>
#include <QMap>
#include <QAbstractScrollArea>
#include <QScrollBar>
#include <QWheelEvent>
@ -178,13 +179,14 @@ public:
void setResults(const QSharedPointer<nmDataSensitive::BatchResult>& results)
{
// 同一批次可能在其他结果列表中改变可见性,同步勾选而不重置滚动位置。
// 同一批次同步已应用的选择,并保留本窗口尚未应用的勾选。
if(m_results == results) {
if(rowCount() > 0) emit dataChanged(index(0, 0), index(rowCount() - 1, 0));
return;
}
beginResetModel();
m_results = results;
m_pendingVisibility.clear();
endResetModel();
}
@ -196,15 +198,15 @@ public:
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
{
// 表格显示运行编号、冻结参数与实际计算状态。
// 表格仅保留显示选择、运行编号与冻结参数。
if(orientation != Qt::Horizontal || role != Qt::DisplayRole) return QVariant();
const QStringList titles = QStringList() << nmWxSensitive::tr("Show") << "#"
<< nmWxSensitive::tr("Parameters") << nmWxSensitive::tr("Status");
<< nmWxSensitive::tr("Parameters");
return titles.value(section);
}
@ -214,16 +216,13 @@ public:
if(!index.isValid() || index.row() >= rowCount()) return QVariant();
const nmDataSensitive::RunResult& run = m_results->m_runs[index.row()];
if(role == Qt::CheckStateRole && index.column() == 0)
return run.m_bSucceeded && !run.m_bCancelled && run.m_bVisible ? Qt::Checked : Qt::Unchecked;
if(role == Qt::DecorationRole && index.column() == 0)
return QColor::fromHsv(int((run.m_nRunIndex % 360) * 137 % 360), 190, 190);
return run.m_bSucceeded && !run.m_bCancelled &&
m_pendingVisibility.value(index.row(), run.m_bVisible) ? Qt::Checked : Qt::Unchecked;
if(role == Qt::TextAlignmentRole && index.column() == 1)
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(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();
QStringList descriptions;
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
{
// 可见性只在结果发布后的界面线程改变,不触发再次求解。
// 勾选只记在本窗口,点击 Apply 或 OK 后才写入绘图使用的结果。
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;
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);
return true;
}
void setAllVisible(bool visible)
{
// 批量切换后只发送一次通知,整批曲线也只重新绘制一次。
// 全选与全隐藏也只改变待应用选择,不立即刷新曲线。
if(m_results.isNull() || m_results->m_runs.empty()) return;
for(size_t i = 0; i < m_results->m_runs.size(); ++i)
m_results->m_runs[i].m_bVisible = visible;
for(int i = 0; i < rowCount(); ++i) {
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));
}
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:
QSharedPointer<nmDataSensitive::BatchResult> m_results;
QMap<int, bool> m_pendingVisibility;
};
// ========= 右对齐委托:保证显示和编辑时都是右对齐 =========
@ -575,7 +597,7 @@ private:
};
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_previewView(nullptr), m_pSensitivityTask(nullptr), m_calculationRunning(false)
{
@ -1672,22 +1694,29 @@ void nmWxSensitive::onResultsClicked()
table->horizontalHeader()->setResizeMode(0, QHeaderView::ResizeToContents);
table->horizontalHeader()->setResizeMode(1, QHeaderView::ResizeToContents);
table->horizontalHeader()->setResizeMode(2, QHeaderView::Stretch);
table->horizontalHeader()->setResizeMode(3, QHeaderView::ResizeToContents);
layout->addWidget(table);
QHBoxLayout* buttons = new QHBoxLayout();
QPushButton* showAll = new QPushButton(tr("Show 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(hideAll);
buttons->addStretch();
buttons->addWidget(close);
buttons->addWidget(apply);
buttons->addWidget(ok);
buttons->addWidget(cancel);
layout->addLayout(buttons);
connect(m_resultsModel.data(), SIGNAL(dataChanged(QModelIndex,QModelIndex)),
this, SLOT(onResultsVisibilityChanged()));
connect(showAll, SIGNAL(clicked()), this, SLOT(onShowAllResults()));
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();
hide();
@ -1704,10 +1733,7 @@ void nmWxSensitive::onResultsChanged()
results = m_pDataManager->getSensitiveData()->getResults();
resultsButton->setEnabled(!results.isNull());
m_sensitiveData.setResults(results);
// 同步其他列表的勾选只刷新显示,不能再次发布结果造成循环通知。
m_refreshingResults = true;
if(!m_resultsModel.isNull()) m_resultsModel->setResults(results);
m_refreshingResults = false;
if(!m_resultsDialog.isNull()) {
const QString status = results.isNull() ? QString() : results->m_bStopped ? tr("Stopped")
: 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());
}
void nmWxSensitive::onAcceptResults()
{
// OK 与 Apply 提交相同的选择,随后关闭窗口。
onApplyResults();
if(!m_resultsDialog.isNull()) m_resultsDialog->accept();
}
void nmWxSensitive::onShowAllResults()
{
// 恢复所有成功组的叠加曲线。
// 勾选所有成功组,等待 Apply 或 OK 更新曲线。
if(!m_resultsModel.isNull()) m_resultsModel->setAllVisible(true);
}
void nmWxSensitive::onHideAllResults()
{
// 隐藏所有计算组,保留测量曲线作为参考。
// 取消所有采样组的勾选,测量曲线和原模型数值解仍作为参考。
if(!m_resultsModel.isNull()) m_resultsModel->setAllVisible(false);
}

Loading…
Cancel
Save