|
|
|
|
@ -2,6 +2,9 @@
|
|
|
|
|
|
|
|
|
|
#include "nmDataAnalyzeManager.h"
|
|
|
|
|
#include "nmWxAutomaticFitting.h"
|
|
|
|
|
#include "nmParameterField.h"
|
|
|
|
|
#include "ZxBaseUtil.h"
|
|
|
|
|
#include <QGroupBox>
|
|
|
|
|
#include "nmDataSensitive.h"
|
|
|
|
|
#include "nmDataReservoir.h"
|
|
|
|
|
#include "nmDataWellBase.h"
|
|
|
|
|
@ -122,21 +125,33 @@ QString samplingBaseUnit(const QString& name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool convertSamplingValue(const QString& name, double value,
|
|
|
|
|
QString from, QString to, double& converted)
|
|
|
|
|
{
|
|
|
|
|
// 优先复用项目单位组;旧配置的 psi 按绝对压力单位迁移。
|
|
|
|
|
if (from == "psi") from = "psia";
|
|
|
|
|
if (to == "psi") to = "psia";
|
|
|
|
|
QString from, QString to, double& converted, bool difference = false)
|
|
|
|
|
{
|
|
|
|
|
// 标准差是差值,单位换算只应用比例,不带入表压和绝压之间的偏移。
|
|
|
|
|
if (difference) {
|
|
|
|
|
double zero = 0.0;
|
|
|
|
|
if (!convertSamplingValue(name, 0.0, from, to, zero) ||
|
|
|
|
|
!convertSamplingValue(name, value, from, to, converted)) return false;
|
|
|
|
|
converted -= zero;
|
|
|
|
|
return qIsFinite(converted);
|
|
|
|
|
}
|
|
|
|
|
// 优先使用框架单位组中的原始名称,兼容下拉框内的全部单位。
|
|
|
|
|
if (from == "dimensionless") from.clear();
|
|
|
|
|
if (to == "dimensionless") to.clear();
|
|
|
|
|
if (from == to || (from.isEmpty() && to.isEmpty())) {
|
|
|
|
|
converted = value;
|
|
|
|
|
return qIsFinite(converted);
|
|
|
|
|
}
|
|
|
|
|
// 与井参数编辑共用名称解析,使 m^3/MPa 等旧写法也能参与框架换算。
|
|
|
|
|
from = nmParameterField::resolveUnitSpelling(from);
|
|
|
|
|
to = nmParameterField::resolveUnitSpelling(to);
|
|
|
|
|
iUnitGroup* group = iUnitHelper::getUnitGroupByUnit(from);
|
|
|
|
|
int digit = 0;
|
|
|
|
|
if (group && group->indexOf(to) >= 0 &&
|
|
|
|
|
group->convert(from, value, to, converted, digit)) return qIsFinite(converted);
|
|
|
|
|
// 旧配置未被框架识别时,沿用业务层的绝对压力单位换算。
|
|
|
|
|
if (from == "psi") from = "psia";
|
|
|
|
|
if (to == "psi") to = "psia";
|
|
|
|
|
if (name == "h") converted = nmDataAttribute::convertLengthUnit(value, from, to);
|
|
|
|
|
else if (name == "Pi") converted = nmDataAttribute::convertPressureUnit(value, from, to);
|
|
|
|
|
else if (name == "k") converted = nmDataAttribute::convertPermeabilityUnit(value, from, to);
|
|
|
|
|
@ -227,6 +242,18 @@ public:
|
|
|
|
|
highlightRun(-1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QSharedPointer<nmDataSensitive::BatchResult> results() const { return m_results; }
|
|
|
|
|
|
|
|
|
|
void setResults(const QSharedPointer<nmDataSensitive::BatchResult>& results)
|
|
|
|
|
{
|
|
|
|
|
// 重置行数与列数,旧批次的选择和高亮不能带到新结果。
|
|
|
|
|
highlightRun(-1);
|
|
|
|
|
beginResetModel();
|
|
|
|
|
m_pendingVisibility.clear();
|
|
|
|
|
m_results = results;
|
|
|
|
|
endResetModel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QList<ZxObjCurve*> runCurves(int row) const
|
|
|
|
|
{
|
|
|
|
|
// 双对数图的一组包含压力与导数,历史压力图只包含压力。
|
|
|
|
|
@ -257,7 +284,7 @@ public:
|
|
|
|
|
|
|
|
|
|
int columnCount(const QModelIndex& parent = QModelIndex()) const override
|
|
|
|
|
{
|
|
|
|
|
return parent.isValid() ? 0 : 3 + (m_statistics ? m_results->m_fitParameters.size() : 0);
|
|
|
|
|
return parent.isValid() ? 0 : 3 + (m_statistics && !m_results.isNull() ? m_results->m_fitParameters.size() : 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVariant headerData(int section, Qt::Orientation orientation, int role) const override
|
|
|
|
|
@ -278,7 +305,7 @@ public:
|
|
|
|
|
}
|
|
|
|
|
if(role != Qt::DisplayRole) return QVariant();
|
|
|
|
|
if(m_statistics) {
|
|
|
|
|
if(section >= 3 && section - 3 < m_results->m_fitParameters.size()) {
|
|
|
|
|
if(!m_results.isNull() && 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() + "]");
|
|
|
|
|
@ -474,7 +501,7 @@ nmWxSensitivityShow::nmWxSensitivityShow(
|
|
|
|
|
: iDlgBase(parent)
|
|
|
|
|
{
|
|
|
|
|
// 沿用原 Results 的白色表格及按钮布局,勾选在 Apply 或 OK 后生效。
|
|
|
|
|
setAttribute(Qt::WA_DeleteOnClose);
|
|
|
|
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
|
|
|
|
setObjectName("nmSensitivityShowDialog");
|
|
|
|
|
resize(700, 380);
|
|
|
|
|
QVBoxLayout* layout = new QVBoxLayout(this);
|
|
|
|
|
@ -522,10 +549,20 @@ nmWxSensitivityShow::nmWxSensitivityShow(
|
|
|
|
|
void nmWxSensitivityShow::done(int result)
|
|
|
|
|
{
|
|
|
|
|
// 确认、取消及关闭都结束临时高亮,不改变已应用的显示选择。
|
|
|
|
|
m_model->highlightRun(-1);
|
|
|
|
|
setResults(m_model->results());
|
|
|
|
|
iDlgBase::done(result);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void nmWxSensitivityShow::setResults(const QSharedPointer<nmDataSensitive::BatchResult>& results)
|
|
|
|
|
{
|
|
|
|
|
// 保留窗口和模型对象,重新打开时取消未应用的勾选和临时高亮。
|
|
|
|
|
m_highlight->blockSignals(true);
|
|
|
|
|
m_highlight->setChecked(false);
|
|
|
|
|
m_highlight->blockSignals(false);
|
|
|
|
|
m_model->setResults(results);
|
|
|
|
|
updateHighlight();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void nmWxSensitivityShow::onApply()
|
|
|
|
|
{
|
|
|
|
|
// 应用选择,保留窗口以便继续调整。
|
|
|
|
|
@ -559,7 +596,7 @@ nmWxSensitivityHistogramOptions::nmWxSensitivityHistogramOptions(
|
|
|
|
|
: iDlgBase(parent)
|
|
|
|
|
{
|
|
|
|
|
// 复用运行表格;第四种在误差后追加拟合参数,选择变量后按原有 Apply/OK 流程生效。
|
|
|
|
|
setAttribute(Qt::WA_DeleteOnClose);
|
|
|
|
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
|
|
|
|
setObjectName("nmHistogramOptions");
|
|
|
|
|
setWindowTitle(tr("Histogram options"));
|
|
|
|
|
resize(720, 400);
|
|
|
|
|
@ -579,7 +616,7 @@ nmWxSensitivityHistogramOptions::nmWxSensitivityHistogramOptions(
|
|
|
|
|
m_variable->setCurrentIndex(m_variable->findData(results->m_nHistogramVariable));
|
|
|
|
|
options->addWidget(m_variable);
|
|
|
|
|
layout->addLayout(options);
|
|
|
|
|
QTableView* table = new QTableView(this);
|
|
|
|
|
QTableView* table = m_table = new QTableView(this);
|
|
|
|
|
table->setStyleSheet("QTableView { background-color: white; }");
|
|
|
|
|
table->setModel(new SensitivityResultsModel(results, nullptr, table, true));
|
|
|
|
|
table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
|
|
|
@ -605,6 +642,22 @@ nmWxSensitivityHistogramOptions::nmWxSensitivityHistogramOptions(
|
|
|
|
|
connect(cancel, SIGNAL(clicked()), this, SLOT(reject()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void nmWxSensitivityHistogramOptions::setResults(const QSharedPointer<nmDataSensitive::BatchResult>& results)
|
|
|
|
|
{
|
|
|
|
|
// 复用表格模型,同时恢复当前批次已应用的分箱和统计变量。
|
|
|
|
|
static_cast<SensitivityResultsModel*>(m_table->model())->setResults(results);
|
|
|
|
|
m_variable->clear();
|
|
|
|
|
m_variable->addItem(tr("Goodness of fit"), -1);
|
|
|
|
|
if(!results.isNull()) {
|
|
|
|
|
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));
|
|
|
|
|
m_bins->setValue(results->m_nHistogramBins);
|
|
|
|
|
}
|
|
|
|
|
for(int i = 2; i < m_table->model()->columnCount(); ++i)
|
|
|
|
|
m_table->horizontalHeader()->setResizeMode(i, QHeaderView::ResizeToContents);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void nmWxSensitivityHistogramOptions::onApply()
|
|
|
|
|
{
|
|
|
|
|
// 分箱数与横轴变量一起提交,取消不会撤销已经应用的设置。
|
|
|
|
|
@ -625,7 +678,7 @@ nmWxSensitivityScatter::nmWxSensitivityScatter(
|
|
|
|
|
{
|
|
|
|
|
// 散点与基准均来自同一批次快照,不读取后来编辑的模型参数。
|
|
|
|
|
setObjectName("nmSensitivityScatterPlot");
|
|
|
|
|
setAttribute(Qt::WA_DeleteOnClose);
|
|
|
|
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
|
|
|
|
setWindowTitle(tr("Scatter plot"));
|
|
|
|
|
resize(850, 550);
|
|
|
|
|
QVBoxLayout* layout = new QVBoxLayout(this);
|
|
|
|
|
@ -675,6 +728,42 @@ nmWxSensitivityScatter::nmWxSensitivityScatter(
|
|
|
|
|
updateParameter();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void nmWxSensitivityScatter::setResults(const QSharedPointer<nmDataSensitive::BatchResult>& results, double baseError)
|
|
|
|
|
{
|
|
|
|
|
// 同一批次保留坐标选择,新批次先清空旧图元与统计引用。
|
|
|
|
|
if(m_results == results && !results.isNull()) return;
|
|
|
|
|
m_results = results;
|
|
|
|
|
m_baseError = baseError;
|
|
|
|
|
m_draggingThreshold = false;
|
|
|
|
|
m_points.clear();
|
|
|
|
|
m_runRows.clear();
|
|
|
|
|
for(int i = m_plot->m_pPlot->getObjCount() - 1; i >= 0; --i)
|
|
|
|
|
if(qobject_cast<ZxObjCurve*>(m_plot->m_pPlot->getObjByIndex(i))) m_plot->m_pPlot->removeObjByIndex(i);
|
|
|
|
|
m_plot->clearUndoStack();
|
|
|
|
|
m_plot->setCurveData(QVector<iCurveDesc>());
|
|
|
|
|
m_plot->runUpdate();
|
|
|
|
|
m_parameter->blockSignals(true);
|
|
|
|
|
m_ordinate->blockSignals(true);
|
|
|
|
|
m_parameter->clear();
|
|
|
|
|
m_ordinate->clear();
|
|
|
|
|
m_ordinate->addItem(tr("Goodness of fit"), -1);
|
|
|
|
|
m_defaultThreshold = 0.0;
|
|
|
|
|
if(!results.isNull()) {
|
|
|
|
|
for(int i = 0; i < results->m_variables.size(); ++i)
|
|
|
|
|
if(results->m_variables[i].m_enabled) m_parameter->addItem(results->m_variables[i].displayName(), i);
|
|
|
|
|
for(int i = 0; i < results->m_fitParameters.size(); ++i)
|
|
|
|
|
m_ordinate->addItem(results->m_fitParameters[i].getName(), i);
|
|
|
|
|
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;
|
|
|
|
|
m_threshold = results->m_dFilterThreshold >= 0.0 ? results->m_dFilterThreshold : m_defaultThreshold;
|
|
|
|
|
}
|
|
|
|
|
m_parameter->blockSignals(false);
|
|
|
|
|
m_ordinate->blockSignals(false);
|
|
|
|
|
if(!results.isNull()) updateParameter();
|
|
|
|
|
else m_legend->clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void nmWxSensitivityScatter::updateParameter()
|
|
|
|
|
{
|
|
|
|
|
// 两轴切换都从本批结果重建坐标;拟合参数轴不使用误差阈值。
|
|
|
|
|
@ -910,7 +999,7 @@ protected:
|
|
|
|
|
{
|
|
|
|
|
// 蓝线为累积概率,红色虚线为按峰值归一化的概率密度,与 Saphir 的显示一致。
|
|
|
|
|
QPainter painter(this);
|
|
|
|
|
painter.fillRect(rect(), palette().color(QPalette::Window));
|
|
|
|
|
painter.fillRect(rect(), Qt::white);
|
|
|
|
|
painter.setRenderHint(QPainter::Antialiasing);
|
|
|
|
|
QVector<double> x(201), cumulativeValues(201);
|
|
|
|
|
const bool bounded = m_variable.m_distribution >= nmDataSensitive::VariableSampling::TRUNCATED_NORMAL;
|
|
|
|
|
@ -931,7 +1020,7 @@ protected:
|
|
|
|
|
painter.drawText(rect(), Qt::AlignCenter, nmWxSensitive::tr("Invalid distribution parameters"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// 坐标外侧沿用窗口底色,绘图区留白,标题放在纵轴左侧。
|
|
|
|
|
// 坐标内外统一使用白底,标题放在纵轴左侧。
|
|
|
|
|
const QRectF plot(76, 12, width() - 96, height() - 66);
|
|
|
|
|
painter.fillRect(plot, Qt::white);
|
|
|
|
|
const QRectF dataArea = plot.adjusted(12, 16, -12, -16);
|
|
|
|
|
@ -1098,6 +1187,8 @@ public:
|
|
|
|
|
m_total(data.getTotalModelCount()), m_first(0), m_selected(0),
|
|
|
|
|
m_visibleRows(1), m_updating(false), m_wheelDelta(0)
|
|
|
|
|
{
|
|
|
|
|
// 预览只持有采样配置,不保留计算结果批次。
|
|
|
|
|
m_data.setResults(QSharedPointer<nmDataSensitive::BatchResult>());
|
|
|
|
|
// 使用现有表格控件绘制表头、行选中和单元格,滚动条控制完整列表。
|
|
|
|
|
setFrameShape(QFrame::StyledPanel);
|
|
|
|
|
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
|
|
|
|
@ -1339,8 +1430,11 @@ private:
|
|
|
|
|
nmWxSensitive::nmWxSensitive(QWidget *parent, nmDataAnalyzeManager* manager)
|
|
|
|
|
: iDlgBase(parent), m_selectedValueIndex(0), m_loading(true),
|
|
|
|
|
m_pDataManager(manager ? manager : nmDataAnalyzeManager::getCurrentInstance()),
|
|
|
|
|
m_sourceData(m_pDataManager ? m_pDataManager->getSensitiveData() : nullptr),
|
|
|
|
|
m_previewView(nullptr), m_pSensitivityTask(nullptr), m_calculationRunning(false)
|
|
|
|
|
{
|
|
|
|
|
// 关闭只隐藏,由所属分析统一销毁,重新打开保留当前输入。
|
|
|
|
|
setAttribute(Qt::WA_DeleteOnClose, false);
|
|
|
|
|
// 显式绑定所属分析窗口,后续切换窗口不会改变配置保存目标。
|
|
|
|
|
if (m_pDataManager) m_sensitiveData = m_pDataManager->getSensitiveDataCopy();
|
|
|
|
|
refreshModelValues();
|
|
|
|
|
@ -1375,7 +1469,11 @@ void nmWxSensitive::reject()
|
|
|
|
|
{
|
|
|
|
|
// Esc 与取消按钮使用相同的停止路径,保留已完成组供结束后查看。
|
|
|
|
|
if(m_calculationRunning) onCancelClicked();
|
|
|
|
|
else iDlgBase::reject();
|
|
|
|
|
else {
|
|
|
|
|
foreach(QDialog* dialog, findChildren<QDialog*>())
|
|
|
|
|
if(dialog->isWindow()) dialog->close();
|
|
|
|
|
iDlgBase::reject();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void nmWxSensitive::closeEvent(QCloseEvent* event)
|
|
|
|
|
@ -1443,7 +1541,9 @@ void nmWxSensitive::refreshModelValues()
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const QString baseUnit = samplingBaseUnit(name);
|
|
|
|
|
QString displayUnit = attribute->getUnit();
|
|
|
|
|
// 已保存的采样显示单位独立于模型单位,重开及读取项目时继续沿用。
|
|
|
|
|
QString displayUnit = !fresh && m_sensitiveData.usesBaseUnits()
|
|
|
|
|
? variable.getModelValue().getUnit() : attribute->getUnit();
|
|
|
|
|
if (displayUnit == "dimensionless") displayUnit.clear();
|
|
|
|
|
double converted = 0.0;
|
|
|
|
|
if (!convertSamplingValue(name, attribute->getValue().toDouble(), baseUnit, displayUnit, converted))
|
|
|
|
|
@ -1527,12 +1627,35 @@ void nmWxSensitive::refreshModelValues()
|
|
|
|
|
|
|
|
|
|
void nmWxSensitive::onModelResultChanged()
|
|
|
|
|
{
|
|
|
|
|
// 通知后按当前模型刷新数值;未改变的参数保留用户范围。
|
|
|
|
|
// 项目读取替换数据对象时重新加载;普通模型刷新保留当前未提交的采样输入。
|
|
|
|
|
reloadInputData();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void nmWxSensitive::reloadInputData()
|
|
|
|
|
{
|
|
|
|
|
if(m_calculationRunning) return;
|
|
|
|
|
m_loading = true;
|
|
|
|
|
if(m_pDataManager && m_sourceData != m_pDataManager->getSensitiveData())
|
|
|
|
|
m_sensitiveData = m_pDataManager->getSensitiveDataCopy();
|
|
|
|
|
m_sourceData = m_pDataManager ? m_pDataManager->getSensitiveData() : nullptr;
|
|
|
|
|
foreach(QDialog* dialog, findChildren<QDialog*>())
|
|
|
|
|
if(dialog->isWindow()) dialog->close();
|
|
|
|
|
delete m_previewView;
|
|
|
|
|
m_previewView = nullptr;
|
|
|
|
|
refreshModelValues();
|
|
|
|
|
loadDataToUI(true);
|
|
|
|
|
// 井列表可能改变,重建参数页,保留外层输入窗口及底部菜单。
|
|
|
|
|
while(detailStack->count() > 1) delete detailStack->widget(1);
|
|
|
|
|
m_modeCombos.clear(); m_logChecks.clear(); m_numSpins.clear();
|
|
|
|
|
m_modelSpins.clear(); m_minSpins.clear(); m_maxSpins.clear();
|
|
|
|
|
m_valueTables.clear(); m_unitCombos.clear();
|
|
|
|
|
m_numLabels.clear(); m_minLabels.clear(); m_maxLabels.clear();
|
|
|
|
|
m_distributionCombos.clear(); m_distributionPanels.clear(); m_distributionPlots.clear();
|
|
|
|
|
populateTreeWidget();
|
|
|
|
|
createDetailPages();
|
|
|
|
|
loadDataToUI();
|
|
|
|
|
m_loading = false;
|
|
|
|
|
updateModelCount();
|
|
|
|
|
onTreeItemChanged(nullptr, 0);
|
|
|
|
|
onResultsChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void nmWxSensitive::loadDataToUI(bool refreshRangesOnly)
|
|
|
|
|
@ -1566,9 +1689,17 @@ void nmWxSensitive::loadDataToUI(bool refreshRangesOnly)
|
|
|
|
|
m_numSpins[idx]->setValue(qMax(1, variable.getNumber()));
|
|
|
|
|
}
|
|
|
|
|
// 已打开窗口沿用原显示单位,避免手动表格的数值被换成另一种单位解释。
|
|
|
|
|
const QString unit = refreshRangesOnly ? m_unitLabels[idx]->text() : variable.getModelValue().getUnit();
|
|
|
|
|
const QString unit = nmParameterField::resolveUnitSpelling(
|
|
|
|
|
refreshRangesOnly ? m_unitCombos[idx]->currentText() : variable.getModelValue().getUnit());
|
|
|
|
|
variable.getModelValue().setUnit(unit);
|
|
|
|
|
m_unitLabels[idx]->setText(unit);
|
|
|
|
|
QComboBox* unitCombo = m_unitCombos[idx];
|
|
|
|
|
unitCombo->clear();
|
|
|
|
|
iUnitGroup* unitGroup = iUnitHelper::getUnitGroupByUnit(
|
|
|
|
|
nmParameterField::resolveUnitSpelling(samplingBaseUnit(name)));
|
|
|
|
|
if (unitGroup) unitCombo->addItems(unitGroup->getAllUnitNames());
|
|
|
|
|
if (unitCombo->findText(unit) < 0) unitCombo->addItem(unit);
|
|
|
|
|
unitCombo->setCurrentIndex(unitCombo->findText(unit));
|
|
|
|
|
unitCombo->setVisible(!unit.isEmpty());
|
|
|
|
|
double model = variable.getModelValue().getValue().toDouble();
|
|
|
|
|
double minimum = variable.getMinValue().getValue().toDouble();
|
|
|
|
|
double maximum = variable.getMaxValue().getValue().toDouble();
|
|
|
|
|
@ -1583,7 +1714,7 @@ void nmWxSensitive::loadDataToUI(bool refreshRangesOnly)
|
|
|
|
|
variable.m_distributionMin, variable.m_distributionMax};
|
|
|
|
|
for(int field = 0; field < 5; ++field) {
|
|
|
|
|
double display = distributionValues[field];
|
|
|
|
|
convertSamplingValue(name, display, samplingBaseUnit(name), unit, display);
|
|
|
|
|
convertSamplingValue(name, display, samplingBaseUnit(name), unit, display, field == 1);
|
|
|
|
|
setSamplingEditValue(m_distributionPanels[idx]->findChild<QLineEdit*>(distributionFields[field]), display);
|
|
|
|
|
}
|
|
|
|
|
double displayMin = variable.getModelValue().getMinValue().toDouble();
|
|
|
|
|
@ -1689,8 +1820,9 @@ bool nmWxSensitive::saveDataFromUI()
|
|
|
|
|
const QString name = variable.getVarName();
|
|
|
|
|
const int idx = findVariableIndex(variable);
|
|
|
|
|
if (idx < 0 || !isSupportedSamplingVariable(name)) continue;
|
|
|
|
|
const QString unit = m_unitLabels[idx]->text();
|
|
|
|
|
const QString unit = m_unitCombos[idx]->currentText();
|
|
|
|
|
const QString baseUnit = samplingBaseUnit(name);
|
|
|
|
|
variable.getModelValue().setUnit(unit);
|
|
|
|
|
bool valid = true;
|
|
|
|
|
if(candidate.getCalculationType() == nmDataSensitive::CALC_MONTE_CARLO) {
|
|
|
|
|
double median = 0.0;
|
|
|
|
|
@ -1764,15 +1896,21 @@ void nmWxSensitive::setupUI()
|
|
|
|
|
{
|
|
|
|
|
setWindowTitle(tr("Sensitivity"));
|
|
|
|
|
setMinimumSize(900, 550);
|
|
|
|
|
// 默认放大窗口,同时为屏幕边缘和窗口标题栏留出空间。
|
|
|
|
|
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
|
|
|
|
// 适当加宽窗口,为整页单位下拉框和分布图保留空间。
|
|
|
|
|
const QSize available = QApplication::desktop()->availableGeometry(this).size() - QSize(60, 80);
|
|
|
|
|
resize(QSize(1250, 760).boundedTo(available));
|
|
|
|
|
resize(QSize(1120, 660).boundedTo(available));
|
|
|
|
|
|
|
|
|
|
// 沿用储层参数窗口的分组框、内容边距和底部按钮间距。
|
|
|
|
|
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
|
|
|
|
mainLayout->setContentsMargins(12, 10, 12, 0);
|
|
|
|
|
mainLayout->setSpacing(8);
|
|
|
|
|
|
|
|
|
|
// ===== 顶部:Calculation type =====
|
|
|
|
|
QHBoxLayout *calcTypeLayout = new QHBoxLayout();
|
|
|
|
|
QLabel *calcTypeLabel = new QLabel(tr("Calculation type:"), this);
|
|
|
|
|
QGroupBox* calcTypeGroup = new QGroupBox(tr("Calculation type:"), this);
|
|
|
|
|
QHBoxLayout *calcTypeLayout = new QHBoxLayout(calcTypeGroup);
|
|
|
|
|
calcTypeLayout->setContentsMargins(10, 10, 10, 10);
|
|
|
|
|
calcTypeLayout->setSpacing(8);
|
|
|
|
|
calculationTypeCombo = new QComboBox(this);
|
|
|
|
|
calculationTypeCombo->addItem(tr("Deterministic"), nmDataSensitive::CALC_DETERMINISTIC);
|
|
|
|
|
calculationTypeCombo->addItem(tr("Deterministic multivariate"), nmDataSensitive::CALC_DETERMINISTIC_MULTIVARIATE);
|
|
|
|
|
@ -1784,58 +1922,20 @@ void nmWxSensitive::setupUI()
|
|
|
|
|
m_improveSettingsButton->setFixedHeight(calculationTypeCombo->sizeHint().height());
|
|
|
|
|
m_improveSettingsButton->hide();
|
|
|
|
|
|
|
|
|
|
calcTypeLayout->addWidget(calcTypeLabel);
|
|
|
|
|
calcTypeLayout->addWidget(calculationTypeCombo);
|
|
|
|
|
calcTypeLayout->addWidget(m_improveSettingsButton);
|
|
|
|
|
calcTypeLayout->addStretch();
|
|
|
|
|
mainLayout->addLayout(calcTypeLayout);
|
|
|
|
|
mainLayout->addWidget(calcTypeGroup);
|
|
|
|
|
|
|
|
|
|
// ===== 中间:左 Variables 面板 + 右 detailStack =====
|
|
|
|
|
QSplitter *centerSplitter = new QSplitter(Qt::Horizontal, this);
|
|
|
|
|
centerSplitter->setChildrenCollapsible(false); // 不允许把某一侧完全折叠
|
|
|
|
|
centerSplitter->setHandleWidth(4); // 中间拖动条宽度
|
|
|
|
|
centerSplitter->setStyleSheet(
|
|
|
|
|
"QSplitter::handle:horizontal {"
|
|
|
|
|
" background-color: #C0C0C0;"
|
|
|
|
|
"}"
|
|
|
|
|
"QSplitter::handle:horizontal:hover {"
|
|
|
|
|
" background-color: #A0A0A0;"
|
|
|
|
|
"}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// --- 左侧:Variables 面板(QFrame + 标题栏 + Tree)---
|
|
|
|
|
QFrame *variablesPanel = new QFrame(this);
|
|
|
|
|
variablesPanel->setFrameShape(QFrame::StyledPanel);
|
|
|
|
|
variablesPanel->setFrameShadow(QFrame::Raised);
|
|
|
|
|
variablesPanel->setMinimumWidth(230);
|
|
|
|
|
|
|
|
|
|
// 左侧参数树使用标准分组框,样式交给主软件统一处理。
|
|
|
|
|
QGroupBox *variablesPanel = new QGroupBox(tr("Variables"), this);
|
|
|
|
|
variablesPanel->setMinimumWidth(200);
|
|
|
|
|
QVBoxLayout *variablesPanelLayout = new QVBoxLayout(variablesPanel);
|
|
|
|
|
variablesPanelLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
variablesPanelLayout->setSpacing(0);
|
|
|
|
|
|
|
|
|
|
// 标题栏
|
|
|
|
|
QWidget *varHeader = new QWidget(variablesPanel);
|
|
|
|
|
varHeader->setFixedHeight(24);
|
|
|
|
|
varHeader->setAutoFillBackground(true);
|
|
|
|
|
{
|
|
|
|
|
QPalette pal = varHeader->palette();
|
|
|
|
|
pal.setColor(QPalette::Window,
|
|
|
|
|
palette().color(QPalette::Midlight));
|
|
|
|
|
varHeader->setPalette(pal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QHBoxLayout *varHeaderLayout = new QHBoxLayout(varHeader);
|
|
|
|
|
varHeaderLayout->setContentsMargins(6, 0, 6, 0);
|
|
|
|
|
varHeaderLayout->setSpacing(0);
|
|
|
|
|
|
|
|
|
|
QLabel *varTitle = new QLabel(tr("Variables"), varHeader);
|
|
|
|
|
varTitle->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
|
|
|
|
|
QFont f = varTitle->font();
|
|
|
|
|
f.setBold(true);
|
|
|
|
|
varTitle->setFont(f);
|
|
|
|
|
|
|
|
|
|
varHeaderLayout->addWidget(varTitle);
|
|
|
|
|
varHeaderLayout->addStretch();
|
|
|
|
|
variablesPanelLayout->setContentsMargins(10, 10, 10, 10);
|
|
|
|
|
|
|
|
|
|
// Tree 区域
|
|
|
|
|
variablesTree = new QTreeWidget(variablesPanel);
|
|
|
|
|
@ -1843,7 +1943,6 @@ void nmWxSensitive::setupUI()
|
|
|
|
|
variablesTree->setFrameShape(QFrame::NoFrame); // 外层已经有 frame
|
|
|
|
|
variablesTree->setMinimumHeight(350);
|
|
|
|
|
|
|
|
|
|
variablesPanelLayout->addWidget(varHeader);
|
|
|
|
|
variablesPanelLayout->addWidget(variablesTree, 1);
|
|
|
|
|
|
|
|
|
|
// 把左侧 panel 加进 splitter
|
|
|
|
|
@ -1869,12 +1968,14 @@ void nmWxSensitive::setupUI()
|
|
|
|
|
// 左边宽度基本固定,右边自适应
|
|
|
|
|
centerSplitter->setStretchFactor(0, 0);
|
|
|
|
|
centerSplitter->setStretchFactor(1, 1);
|
|
|
|
|
centerSplitter->setSizes(QList<int>() << 260 << 960);
|
|
|
|
|
centerSplitter->setSizes(QList<int>() << 220 << 860);
|
|
|
|
|
|
|
|
|
|
mainLayout->addWidget(centerSplitter, 1);
|
|
|
|
|
|
|
|
|
|
// ===== 底部:Total number of models + 按钮 =====
|
|
|
|
|
QHBoxLayout *bottomLayout = new QHBoxLayout();
|
|
|
|
|
bottomLayout->setContentsMargins(4, 5, 4, 5);
|
|
|
|
|
bottomLayout->setSpacing(8);
|
|
|
|
|
|
|
|
|
|
QLabel *totalLabel = new QLabel(tr("Total number of models:"), this);
|
|
|
|
|
modelCountLabel = new QLabel(tr("0"), this);
|
|
|
|
|
@ -1898,7 +1999,20 @@ void nmWxSensitive::setupUI()
|
|
|
|
|
|
|
|
|
|
generateButton = new QPushButton(tr("Generate"), this);
|
|
|
|
|
cancelButton = new QPushButton(tr("Cancel"), this);
|
|
|
|
|
|
|
|
|
|
generateButton->setIcon(zxLoadIcon("OK"));
|
|
|
|
|
cancelButton->setIcon(zxLoadIcon("Cancel"));
|
|
|
|
|
|
|
|
|
|
// 查看已有计算结果,不改变采样配置,也不重新求解。
|
|
|
|
|
m_resultPlotsButton = new QPushButton(tr("Result plots"), this);
|
|
|
|
|
m_resultPlotsButton->setObjectName("nmSensitivityResultPlots");
|
|
|
|
|
QMenu* resultMenu = new QMenu(m_resultPlotsButton);
|
|
|
|
|
resultMenu->addAction(tr("Loglog plot"))->setData(LoglogResult);
|
|
|
|
|
resultMenu->addAction(tr("History plot"))->setData(HistoryResult);
|
|
|
|
|
m_histogramResultAction = resultMenu->addAction(tr("Histogram"));
|
|
|
|
|
m_histogramResultAction->setData(HistogramResult);
|
|
|
|
|
m_resultPlotsButton->setMenu(resultMenu);
|
|
|
|
|
connect(resultMenu, SIGNAL(triggered(QAction*)), this, SLOT(onResultPlotTriggered(QAction*)));
|
|
|
|
|
bottomLayout->addWidget(m_resultPlotsButton);
|
|
|
|
|
bottomLayout->addWidget(generateButton);
|
|
|
|
|
bottomLayout->addWidget(cancelButton);
|
|
|
|
|
|
|
|
|
|
@ -2032,48 +2146,22 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
|
|
|
|
|
pageLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
pageLayout->setSpacing(0);
|
|
|
|
|
|
|
|
|
|
// ===== 整个 Sampling 面板:QFrame + 标题栏 + 内容 =====
|
|
|
|
|
QFrame *samplingPanel = new QFrame(page);
|
|
|
|
|
samplingPanel->setFrameShape(QFrame::StyledPanel);
|
|
|
|
|
samplingPanel->setFrameShadow(QFrame::Raised);
|
|
|
|
|
|
|
|
|
|
// 采样页与左侧参数树统一使用标准分组框。
|
|
|
|
|
QGroupBox *samplingPanel = new QGroupBox(tr("Sampling %1").arg(varName), page);
|
|
|
|
|
QVBoxLayout *panelLayout = new QVBoxLayout(samplingPanel);
|
|
|
|
|
panelLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
panelLayout->setSpacing(0);
|
|
|
|
|
|
|
|
|
|
// ---- 标题栏:Sampling Xxx ----
|
|
|
|
|
QWidget *header = new QWidget(samplingPanel);
|
|
|
|
|
header->setFixedHeight(24);
|
|
|
|
|
header->setAutoFillBackground(true);
|
|
|
|
|
{
|
|
|
|
|
QPalette pal = header->palette();
|
|
|
|
|
pal.setColor(QPalette::Window,
|
|
|
|
|
palette().color(QPalette::Midlight));
|
|
|
|
|
header->setPalette(pal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QHBoxLayout *headerLayout = new QHBoxLayout(header);
|
|
|
|
|
headerLayout->setContentsMargins(6, 0, 6, 0);
|
|
|
|
|
headerLayout->setSpacing(0);
|
|
|
|
|
|
|
|
|
|
QLabel *title = new QLabel(tr("Sampling %1").arg(varName), header);
|
|
|
|
|
QFont tf = title->font();
|
|
|
|
|
tf.setBold(true);
|
|
|
|
|
title->setFont(tf);
|
|
|
|
|
headerLayout->addWidget(title);
|
|
|
|
|
headerLayout->addStretch();
|
|
|
|
|
|
|
|
|
|
panelLayout->addWidget(header);
|
|
|
|
|
panelLayout->setContentsMargins(10, 10, 10, 10);
|
|
|
|
|
panelLayout->setSpacing(8);
|
|
|
|
|
|
|
|
|
|
// ---- 内容区:左参数 + 右表格 ----
|
|
|
|
|
QWidget *content = new QWidget(samplingPanel);
|
|
|
|
|
QHBoxLayout *contentLayout = new QHBoxLayout(content);
|
|
|
|
|
contentLayout->setContentsMargins(6, 0, 6, 6);
|
|
|
|
|
contentLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
contentLayout->setSpacing(12);
|
|
|
|
|
|
|
|
|
|
// 左:参数区(GridLayout)
|
|
|
|
|
QGridLayout *paramLayout = new QGridLayout();
|
|
|
|
|
paramLayout->setVerticalSpacing(6);
|
|
|
|
|
paramLayout->setVerticalSpacing(8);
|
|
|
|
|
paramLayout->setHorizontalSpacing(8);
|
|
|
|
|
int row = 0;
|
|
|
|
|
|
|
|
|
|
// Mode + Log?
|
|
|
|
|
@ -2091,7 +2179,6 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
|
|
|
|
|
distributionCombo->setObjectName("sensitivityDistribution_" + varName);
|
|
|
|
|
distributionCombo->addItems(QStringList() << tr("Normal") << tr("Lognormal")
|
|
|
|
|
<< tr("Truncated normal") << tr("Truncated lognormal") << tr("Uniform") << tr("Triangular"));
|
|
|
|
|
distributionCombo->setFixedWidth(180);
|
|
|
|
|
modeRowLayout->addWidget(distributionCombo);
|
|
|
|
|
modeRowLayout->addWidget(logCheck);
|
|
|
|
|
modeRowLayout->addStretch();
|
|
|
|
|
@ -2111,11 +2198,6 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
|
|
|
|
|
paramLayout->addWidget(numSpin, row, 1);
|
|
|
|
|
++row;
|
|
|
|
|
|
|
|
|
|
// 模式、数量与下方采样参数之间留出约三行空白。
|
|
|
|
|
int gapRow = row;
|
|
|
|
|
paramLayout->setRowMinimumHeight(gapRow, numSpin->sizeHint().height() * 3);
|
|
|
|
|
++row;
|
|
|
|
|
|
|
|
|
|
// Model value + 单位
|
|
|
|
|
QLabel *modelLabel = new QLabel(tr("Model value:"), content);
|
|
|
|
|
QLineEdit *modelSpin = new QLineEdit(content);
|
|
|
|
|
@ -2123,13 +2205,18 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
|
|
|
|
|
// 基准值来自真实模型,只作参照;输入框支持科学计数法和完整精度。
|
|
|
|
|
modelSpin->setReadOnly(true);
|
|
|
|
|
|
|
|
|
|
QLabel *unitLabel = new QLabel(tr(""), content); // ⭐ 单位也从数据加载
|
|
|
|
|
// 当前参数整页共用一个单位,下拉选项和换算均复用框架单位组。
|
|
|
|
|
QComboBox *unitCombo = new QComboBox(content);
|
|
|
|
|
unitCombo->setObjectName("sensitivityUnit_" + varName);
|
|
|
|
|
unitCombo->setMinimumWidth(86);
|
|
|
|
|
unitCombo->setMaximumWidth(120);
|
|
|
|
|
unitCombo->setFixedHeight(25);
|
|
|
|
|
|
|
|
|
|
QWidget *modelRowWidget = new QWidget(content);
|
|
|
|
|
QHBoxLayout *modelRowLayout = new QHBoxLayout(modelRowWidget);
|
|
|
|
|
modelRowLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
modelRowLayout->addWidget(modelSpin);
|
|
|
|
|
modelRowLayout->addWidget(unitLabel);
|
|
|
|
|
modelRowLayout->addWidget(unitCombo);
|
|
|
|
|
modelRowLayout->addStretch();
|
|
|
|
|
|
|
|
|
|
paramLayout->addWidget(modelLabel, row, 0);
|
|
|
|
|
@ -2140,9 +2227,8 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
|
|
|
|
|
QWidget* distributionPanel = new QWidget(content);
|
|
|
|
|
QGridLayout* distributionLayout = new QGridLayout(distributionPanel);
|
|
|
|
|
distributionLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
distributionLayout->setHorizontalSpacing(6);
|
|
|
|
|
distributionLayout->setHorizontalSpacing(8);
|
|
|
|
|
distributionLayout->setVerticalSpacing(0);
|
|
|
|
|
distributionLayout->setColumnMinimumWidth(0, modelLabel->sizeHint().width());
|
|
|
|
|
const int fieldHeight = modelSpin->sizeHint().height();
|
|
|
|
|
// Mean 与三角分布的 Mode 共用一行,其余字段保持固定位置。
|
|
|
|
|
distributionPanel->setFixedHeight(fieldHeight * 4 + 6 * 3);
|
|
|
|
|
@ -2182,9 +2268,17 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
|
|
|
|
|
paramLayout->addWidget(maxSpin, row, 1);
|
|
|
|
|
++row;
|
|
|
|
|
|
|
|
|
|
// 统一编辑框宽度
|
|
|
|
|
int fullWidth = modelSpin->sizeHint().width();
|
|
|
|
|
int editWidth = qMax(150, fullWidth);
|
|
|
|
|
// 基本参数与分布参数使用相同标签列宽、列间距和输入宽度,保证上下对齐。
|
|
|
|
|
QList<QLabel*> labels;
|
|
|
|
|
labels << modeLabel << numLabel << modelLabel << minLabel << maxLabel;
|
|
|
|
|
labels.append(distributionPanel->findChildren<QLabel*>());
|
|
|
|
|
int labelWidth = 0;
|
|
|
|
|
foreach(QLabel* label, labels) labelWidth = qMax(labelWidth, label->sizeHint().width());
|
|
|
|
|
foreach(QLabel* label, labels) label->setFixedWidth(labelWidth);
|
|
|
|
|
const int editWidth = qMax(180, modelSpin->sizeHint().width());
|
|
|
|
|
distributionCombo->setFixedWidth(editWidth);
|
|
|
|
|
foreach(QLineEdit* edit, distributionPanel->findChildren<QLineEdit*>())
|
|
|
|
|
edit->setFixedWidth(editWidth);
|
|
|
|
|
|
|
|
|
|
modeCombo->setFixedWidth(editWidth);
|
|
|
|
|
numSpin->setFixedWidth(editWidth);
|
|
|
|
|
@ -2200,7 +2294,7 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
|
|
|
|
|
|
|
|
|
|
QWidget *paramWidget = new QWidget(content);
|
|
|
|
|
QVBoxLayout *leftVBox = new QVBoxLayout(paramWidget);
|
|
|
|
|
leftVBox->setContentsMargins(0, 6, 0, 0);
|
|
|
|
|
leftVBox->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
leftVBox->addLayout(paramLayout);
|
|
|
|
|
leftVBox->addStretch();
|
|
|
|
|
|
|
|
|
|
@ -2212,27 +2306,40 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
|
|
|
|
|
headers << "" << tr("Values");
|
|
|
|
|
valuesTable->setHorizontalHeaderLabels(headers);
|
|
|
|
|
|
|
|
|
|
valuesTable->verticalHeader()->setVisible(false);
|
|
|
|
|
// 沿用编辑井的行号、隔行底色和网格样式,保留采样行的箭头标记。
|
|
|
|
|
valuesTable->verticalHeader()->setVisible(true);
|
|
|
|
|
valuesTable->verticalHeader()->setMovable(false);
|
|
|
|
|
valuesTable->verticalHeader()->setDefaultAlignment(Qt::AlignCenter);
|
|
|
|
|
valuesTable->verticalHeader()->setMinimumWidth(18);
|
|
|
|
|
QHeaderView *hh = valuesTable->horizontalHeader();
|
|
|
|
|
hh->setResizeMode(QHeaderView::Fixed);
|
|
|
|
|
hh->setHighlightSections(false);
|
|
|
|
|
// 箭头列固定,数值列在下方设置为填满加宽后的表格。
|
|
|
|
|
valuesTable->setColumnWidth(0, 15);
|
|
|
|
|
valuesTable->setStyleSheet(
|
|
|
|
|
"QHeaderView::section {"
|
|
|
|
|
" border-top: 0px;"
|
|
|
|
|
" border-left: 0px;"
|
|
|
|
|
" border-right: 1px solid lightgray;"
|
|
|
|
|
" border-bottom: 1px solid lightgray;"
|
|
|
|
|
"}"
|
|
|
|
|
"QTableCornerButton::section {"
|
|
|
|
|
" border-top: 0px;"
|
|
|
|
|
" border-left: 0px;"
|
|
|
|
|
" border-right: 1px solid lightgray;"
|
|
|
|
|
" border-bottom: 1px solid lightgray;"
|
|
|
|
|
"}"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
valuesTable->setAlternatingRowColors(true);
|
|
|
|
|
valuesTable->setStyleSheet(
|
|
|
|
|
"QTableWidget {"
|
|
|
|
|
" background-color: palette(base);"
|
|
|
|
|
" alternate-background-color: rgb(232, 232, 232);"
|
|
|
|
|
" gridline-color: palette(midlight);"
|
|
|
|
|
" border: 1px solid palette(mid);"
|
|
|
|
|
"}"
|
|
|
|
|
"QHeaderView {"
|
|
|
|
|
" background-color: palette(button);"
|
|
|
|
|
" color: palette(button-text);"
|
|
|
|
|
"}"
|
|
|
|
|
"QHeaderView::section {"
|
|
|
|
|
" background-color: palette(button);"
|
|
|
|
|
" color: palette(button-text);"
|
|
|
|
|
" border: none;"
|
|
|
|
|
" border-right: 1px solid palette(mid);"
|
|
|
|
|
" border-bottom: 1px solid palette(mid);"
|
|
|
|
|
"}"
|
|
|
|
|
"QTableCornerButton::section {"
|
|
|
|
|
" background-color: palette(base);"
|
|
|
|
|
" border: none;"
|
|
|
|
|
" border-right: 1px solid palette(mid);"
|
|
|
|
|
" border-bottom: 1px solid palette(mid);"
|
|
|
|
|
"}");
|
|
|
|
|
valuesTable->horizontalHeader()->setStretchLastSection(true);
|
|
|
|
|
valuesTable->setEditTriggers(QAbstractItemView::DoubleClicked);
|
|
|
|
|
valuesTable->setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
|
|
|
@ -2243,22 +2350,27 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
|
|
|
|
|
|
|
|
|
|
valuesTable->setItemDelegateForColumn(1, new RightAlignDelegate(valuesTable));
|
|
|
|
|
|
|
|
|
|
// 初始空表的列宽尚未稳定,直接固定容器宽度,不从未显示的表头反算。
|
|
|
|
|
const int tableWidth = 190;
|
|
|
|
|
valuesTable->setFixedWidth(tableWidth);
|
|
|
|
|
// 与编辑井一样使用可拖动分栏,表格和分布图共用右侧空间。
|
|
|
|
|
QSplitter* samplingSplitter = new QSplitter(Qt::Horizontal, content);
|
|
|
|
|
samplingSplitter->setChildrenCollapsible(false);
|
|
|
|
|
samplingSplitter->setHandleWidth(6);
|
|
|
|
|
// 分栏仍可拖动,但不绘制参数与图表之间的灰色竖线。
|
|
|
|
|
samplingSplitter->setStyleSheet("QSplitter::handle { background: transparent; border: none; }");
|
|
|
|
|
samplingSplitter->addWidget(paramWidget);
|
|
|
|
|
|
|
|
|
|
QWidget *tableContainer = new QWidget(content);
|
|
|
|
|
QWidget *tableContainer = new QWidget(samplingSplitter);
|
|
|
|
|
QVBoxLayout *tableLayout = new QVBoxLayout(tableContainer);
|
|
|
|
|
tableLayout->setContentsMargins(0, 0, 0, 0);
|
|
|
|
|
tableLayout->setSpacing(0);
|
|
|
|
|
tableLayout->addWidget(valuesTable);
|
|
|
|
|
tableContainer->setFixedWidth(tableWidth);
|
|
|
|
|
|
|
|
|
|
contentLayout->addWidget(paramWidget, 1);
|
|
|
|
|
contentLayout->addStretch();
|
|
|
|
|
contentLayout->addWidget(tableContainer, 0, Qt::AlignRight);
|
|
|
|
|
SensitivityDistributionPlot* distributionPlot = new SensitivityDistributionPlot(content);
|
|
|
|
|
contentLayout->addWidget(distributionPlot, 2);
|
|
|
|
|
valuesTable->setMinimumWidth(190);
|
|
|
|
|
SensitivityDistributionPlot* distributionPlot = new SensitivityDistributionPlot(tableContainer);
|
|
|
|
|
tableLayout->addWidget(distributionPlot);
|
|
|
|
|
samplingSplitter->addWidget(tableContainer);
|
|
|
|
|
samplingSplitter->setStretchFactor(0, 1);
|
|
|
|
|
samplingSplitter->setStretchFactor(1, 0);
|
|
|
|
|
samplingSplitter->setSizes(QList<int>() << 630 << 210);
|
|
|
|
|
contentLayout->addWidget(samplingSplitter);
|
|
|
|
|
|
|
|
|
|
panelLayout->addWidget(content);
|
|
|
|
|
pageLayout->addWidget(samplingPanel);
|
|
|
|
|
@ -2272,7 +2384,8 @@ QWidget *nmWxSensitive::createVariablePage(const QString &varName)
|
|
|
|
|
m_minSpins.append(minSpin);
|
|
|
|
|
m_maxSpins.append(maxSpin);
|
|
|
|
|
m_valueTables.append(valuesTable);
|
|
|
|
|
m_unitLabels.append(unitLabel);
|
|
|
|
|
m_unitCombos.append(unitCombo);
|
|
|
|
|
connect(unitCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(onSamplingUnitChanged()));
|
|
|
|
|
m_numLabels.append(numLabel);
|
|
|
|
|
m_minLabels.append(minLabel);
|
|
|
|
|
m_maxLabels.append(maxLabel);
|
|
|
|
|
@ -2314,13 +2427,23 @@ void nmWxSensitive::updateValuesForPage(int idx)
|
|
|
|
|
m_maxLabels[idx]->setVisible(!monteCarlo);
|
|
|
|
|
m_minSpins[idx]->setVisible(!monteCarlo);
|
|
|
|
|
m_maxSpins[idx]->setVisible(!monteCarlo);
|
|
|
|
|
m_valueTables[idx]->parentWidget()->setVisible(!monteCarlo);
|
|
|
|
|
m_valueTables[idx]->setVisible(!monteCarlo);
|
|
|
|
|
m_distributionCombos[idx]->setVisible(monteCarlo);
|
|
|
|
|
m_distributionPanels[idx]->setVisible(monteCarlo);
|
|
|
|
|
m_distributionPlots[idx]->setVisible(monteCarlo);
|
|
|
|
|
// Monte-Carlo 参数区只占所需宽度,窗口增宽时优先扩展分布图。
|
|
|
|
|
QHBoxLayout* contentLayout = static_cast<QHBoxLayout*>(m_distributionPlots[idx]->parentWidget()->layout());
|
|
|
|
|
contentLayout->setStretch(0, monteCarlo ? 0 : 1);
|
|
|
|
|
// 表格采用较窄的默认宽度;分布图让参数区仅占所需宽度,避免沿用表格分栏。
|
|
|
|
|
// 只在布局类型切换时调整,编辑参数时保留用户拖动后的宽度。
|
|
|
|
|
QSplitter* splitter = static_cast<QSplitter*>(m_valueTables[idx]->parentWidget()->parentWidget());
|
|
|
|
|
const QVariant previousLayout = splitter->property("monteCarloLayout");
|
|
|
|
|
if (!previousLayout.isValid() || previousLayout.toBool() != monteCarlo) {
|
|
|
|
|
splitter->setProperty("monteCarloLayout", monteCarlo);
|
|
|
|
|
splitter->setStretchFactor(0, monteCarlo ? 0 : 1);
|
|
|
|
|
splitter->setStretchFactor(1, monteCarlo ? 1 : 0);
|
|
|
|
|
const int availableWidth = qMax(840, splitter->width() - splitter->handleWidth());
|
|
|
|
|
const int parameterWidth = monteCarlo ? splitter->widget(0)->minimumSizeHint().width()
|
|
|
|
|
: availableWidth - 210;
|
|
|
|
|
splitter->setSizes(QList<int>() << parameterWidth << availableWidth - parameterWidth);
|
|
|
|
|
}
|
|
|
|
|
if(monteCarlo) { updateDistributionPage(idx); return; }
|
|
|
|
|
// 自动模式由范围生成只读表格;手动模式保留每行值,不因选中行而重算。
|
|
|
|
|
QTableWidget* table = m_valueTables[idx];
|
|
|
|
|
@ -2339,7 +2462,7 @@ void nmWxSensitive::updateValuesForPage(int idx)
|
|
|
|
|
}
|
|
|
|
|
QVector<double> samples;
|
|
|
|
|
const QString name = variable.getVarName();
|
|
|
|
|
const QString unit = m_unitLabels[idx]->text();
|
|
|
|
|
const QString unit = m_unitCombos[idx]->currentText();
|
|
|
|
|
if (automatic) {
|
|
|
|
|
bool minOk = false, maxOk = false;
|
|
|
|
|
double minimum = samplingEditValue(m_minSpins[idx], &minOk);
|
|
|
|
|
@ -2380,7 +2503,7 @@ bool nmWxSensitive::readDistributionPage(int idx, nmDataSensitive::VariableSampl
|
|
|
|
|
// 字段转换沿用现有参数单位,仅校验当前分布实际使用的字段。
|
|
|
|
|
variable.m_distribution = static_cast<nmDataSensitive::VariableSampling::Distribution>(
|
|
|
|
|
m_distributionCombos[idx]->currentIndex());
|
|
|
|
|
variable.m_modelValue.setUnit(m_unitLabels[idx]->text());
|
|
|
|
|
variable.m_modelValue.setUnit(m_unitCombos[idx]->currentText());
|
|
|
|
|
double* fields[] = {&variable.m_mean, &variable.m_stdDev, &variable.m_peak,
|
|
|
|
|
&variable.m_distributionMin, &variable.m_distributionMax};
|
|
|
|
|
const int distribution = variable.m_distribution;
|
|
|
|
|
@ -2392,7 +2515,7 @@ bool nmWxSensitive::readDistributionPage(int idx, nmDataSensitive::VariableSampl
|
|
|
|
|
bool ok = false;
|
|
|
|
|
double value = samplingEditValue(m_distributionPanels[idx]->findChild<QLineEdit*>(distributionFields[field]), &ok);
|
|
|
|
|
if(!ok || !qIsFinite(value) || !convertSamplingValue(variable.m_varName, value,
|
|
|
|
|
m_unitLabels[idx]->text(), samplingBaseUnit(variable.m_varName), *fields[field])) return false;
|
|
|
|
|
m_unitCombos[idx]->currentText(), samplingBaseUnit(variable.m_varName), *fields[field], field == 1)) return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
@ -2418,6 +2541,81 @@ void nmWxSensitive::updateDistributionPage(int idx)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void nmWxSensitive::onSamplingUnitChanged()
|
|
|
|
|
{
|
|
|
|
|
if (m_loading) return;
|
|
|
|
|
QComboBox* combo = qobject_cast<QComboBox*>(sender());
|
|
|
|
|
const int idx = m_unitCombos.indexOf(combo);
|
|
|
|
|
if (idx < 0) return;
|
|
|
|
|
for (int i = 0; i < m_sensitiveData.getVariableCount(); ++i) {
|
|
|
|
|
nmDataSensitive::VariableSampling& variable = m_sensitiveData.getVariable(i);
|
|
|
|
|
if (findVariableIndex(variable) != idx) continue;
|
|
|
|
|
const QString from = variable.getModelValue().getUnit();
|
|
|
|
|
const QString to = combo->currentText();
|
|
|
|
|
if (from == to) return;
|
|
|
|
|
|
|
|
|
|
// 先换算当前界面草稿,全部成功后再更新,避免覆盖未提交的手动输入。
|
|
|
|
|
QList<QLineEdit*> edits;
|
|
|
|
|
edits << m_modelSpins[idx] << m_minSpins[idx] << m_maxSpins[idx];
|
|
|
|
|
for (int field = 0; field < 5; ++field)
|
|
|
|
|
edits << m_distributionPanels[idx]->findChild<QLineEdit*>(distributionFields[field]);
|
|
|
|
|
QVector<double> values(edits.size());
|
|
|
|
|
bool valid = true;
|
|
|
|
|
for (int field = 0; valid && field < edits.size(); ++field) {
|
|
|
|
|
bool ok = false;
|
|
|
|
|
const double value = samplingEditValue(edits[field], &ok);
|
|
|
|
|
valid = ok && convertSamplingValue(variable.m_varName, value, from, to, values[field], field == 4);
|
|
|
|
|
}
|
|
|
|
|
QTableWidget* table = m_valueTables[idx];
|
|
|
|
|
QVector<double> tableValues(table->rowCount());
|
|
|
|
|
for (int row = 0; valid && row < table->rowCount(); ++row) {
|
|
|
|
|
QTableWidgetItem* item = table->item(row, 1);
|
|
|
|
|
if (!item || item->text().isEmpty()) continue;
|
|
|
|
|
bool ok = false;
|
|
|
|
|
const double value = item->data(Qt::UserRole).isValid()
|
|
|
|
|
? item->data(Qt::UserRole).toDouble(&ok) : item->text().trimmed().toDouble(&ok);
|
|
|
|
|
valid = ok && convertSamplingValue(variable.m_varName, value, from, to, tableValues[row]);
|
|
|
|
|
}
|
|
|
|
|
if (!valid) {
|
|
|
|
|
combo->blockSignals(true);
|
|
|
|
|
combo->setCurrentIndex(combo->findText(from));
|
|
|
|
|
combo->blockSignals(false);
|
|
|
|
|
double lower = variable.getModelValue().getMinValue().toDouble();
|
|
|
|
|
double upper = variable.getModelValue().getMaxValue().toDouble();
|
|
|
|
|
convertSamplingValue(variable.m_varName, lower, samplingBaseUnit(variable.m_varName), from, lower);
|
|
|
|
|
convertSamplingValue(variable.m_varName, upper, samplingBaseUnit(variable.m_varName), from, upper);
|
|
|
|
|
QMessageBox::warning(this, tr("Sensitivity"),
|
|
|
|
|
tr("%1: invalid sampling values. Allowed range: [%2, %3] %4. Check the sampling settings.")
|
|
|
|
|
.arg(variable.m_varName).arg(formatSamplingValue(lower, 6))
|
|
|
|
|
.arg(formatSamplingValue(upper, 6)).arg(from));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 仅更新显示单位和显示值,基准采样数据及已生成的随机样本保持不变。
|
|
|
|
|
m_loading = true;
|
|
|
|
|
variable.getModelValue().setUnit(to);
|
|
|
|
|
for (int field = 0; field < edits.size(); ++field)
|
|
|
|
|
setSamplingEditValue(edits[field], values[field]);
|
|
|
|
|
for (int row = 0; row < table->rowCount(); ++row) {
|
|
|
|
|
QTableWidgetItem* item = table->item(row, 1);
|
|
|
|
|
if (!item || item->text().isEmpty()) continue;
|
|
|
|
|
item->setData(Qt::UserRole, tableValues[row]);
|
|
|
|
|
item->setText(formatSamplingValue(tableValues[row], 3));
|
|
|
|
|
}
|
|
|
|
|
double lower = variable.getModelValue().getMinValue().toDouble();
|
|
|
|
|
double upper = variable.getModelValue().getMaxValue().toDouble();
|
|
|
|
|
convertSamplingValue(variable.m_varName, lower, samplingBaseUnit(variable.m_varName), to, lower);
|
|
|
|
|
convertSamplingValue(variable.m_varName, upper, samplingBaseUnit(variable.m_varName), to, upper);
|
|
|
|
|
const QString rangeHint = tr("Allowed range: [%1, %2] %3")
|
|
|
|
|
.arg(formatSamplingValue(lower, 6)).arg(formatSamplingValue(upper, 6)).arg(to);
|
|
|
|
|
m_minSpins[idx]->setToolTip(rangeHint);
|
|
|
|
|
m_maxSpins[idx]->setToolTip(rangeHint);
|
|
|
|
|
updateValuesForPage(idx);
|
|
|
|
|
m_loading = false;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void nmWxSensitive::onSamplingEditingFinished()
|
|
|
|
|
{
|
|
|
|
|
// 输入结束后截断越界范围,再按六位有效数字显示,计算仍使用原始精度。
|
|
|
|
|
@ -2433,7 +2631,7 @@ void nmWxSensitive::onSamplingEditingFinished()
|
|
|
|
|
const QString name = variable.getVarName();
|
|
|
|
|
if (findVariableIndex(variable) != idx) continue;
|
|
|
|
|
double lower = 0.0, upper = 0.0;
|
|
|
|
|
const QString unit = m_unitLabels[idx]->text();
|
|
|
|
|
const QString unit = m_unitCombos[idx]->currentText();
|
|
|
|
|
const QString baseUnit = samplingBaseUnit(name);
|
|
|
|
|
if (!samplingPhysicalRange(name, lower, upper) ||
|
|
|
|
|
!convertSamplingValue(name, value, unit, baseUnit, value)) return;
|
|
|
|
|
@ -2564,6 +2762,7 @@ void nmWxSensitive::onGenerateClicked()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_pDataManager->updateSensitiveData(m_sensitiveData);
|
|
|
|
|
m_sourceData = m_pDataManager->getSensitiveData();
|
|
|
|
|
// 新任务准备成功后才替换上次临时结果,准备失败仍可保留旧结果。
|
|
|
|
|
delete m_pSensitivityTask;
|
|
|
|
|
m_pSensitivityTask = task.take();
|
|
|
|
|
@ -2582,7 +2781,7 @@ void nmWxSensitive::startSensitivityCalculation()
|
|
|
|
|
connect(m_pSensitivityTask, SIGNAL(finished()),
|
|
|
|
|
this, SLOT(onSensitivityFinished()), Qt::QueuedConnection);
|
|
|
|
|
setCalculationRunning(true);
|
|
|
|
|
// 仅关闭参数界面;控制器及任务仍存活,结束后继续保留临时计算结果。
|
|
|
|
|
// 计算期间隐藏输入窗口,任务结束后继续复用。
|
|
|
|
|
hide();
|
|
|
|
|
m_pSensitivityTask->start();
|
|
|
|
|
}
|
|
|
|
|
@ -2606,6 +2805,7 @@ void nmWxSensitive::setCalculationRunning(bool running)
|
|
|
|
|
detailStack->setEnabled(!running);
|
|
|
|
|
m_monteCarloCount->setEnabled(!running);
|
|
|
|
|
previewButton->setEnabled(!running);
|
|
|
|
|
m_resultPlotsButton->setEnabled(!running && !m_sensitiveData.getResults().isNull());
|
|
|
|
|
cancelButton->setEnabled(!running);
|
|
|
|
|
if(running) {
|
|
|
|
|
generateButton->setEnabled(false);
|
|
|
|
|
@ -2651,6 +2851,16 @@ void nmWxSensitive::onResultsChanged()
|
|
|
|
|
if(m_pDataManager && m_pDataManager->getSensitiveData())
|
|
|
|
|
results = m_pDataManager->getSensitiveData()->getResults();
|
|
|
|
|
m_sensitiveData.setResults(results);
|
|
|
|
|
m_resultPlotsButton->setEnabled(!m_calculationRunning && !results.isNull());
|
|
|
|
|
m_histogramResultAction->setEnabled(!results.isNull() &&
|
|
|
|
|
results->m_calcType == nmDataSensitive::CALC_MONTE_CARLO);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void nmWxSensitive::onResultPlotTriggered(QAction* action)
|
|
|
|
|
{
|
|
|
|
|
// 菜单按已生成批次开放,切换输入计算方法不改变旧结果可查看的图形。
|
|
|
|
|
if(!action || m_calculationRunning || m_sensitiveData.getResults().isNull()) return;
|
|
|
|
|
emit sigShowSensitivityResult(action->data().toInt());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void nmWxSensitive::onCalculationTypeChanged(int index)
|
|
|
|
|
@ -2672,9 +2882,16 @@ void nmWxSensitive::onImproveSettingsClicked()
|
|
|
|
|
QStringList sampledParameters;
|
|
|
|
|
foreach(const nmDataSensitive::VariableSampling& variable, m_sensitiveData.getVariables())
|
|
|
|
|
if(variable.m_enabled) sampledParameters.append(variable.parameterKey());
|
|
|
|
|
nmWxAutomaticFitting dialog(this, &settings, sampledParameters);
|
|
|
|
|
if(dialog.exec() == QDialog::Accepted) {
|
|
|
|
|
m_sensitiveData.setImproveSettings(QSharedPointer<const nmSensitivityFitSettings>(new nmSensitivityFitSettings(settings)));
|
|
|
|
|
if(m_improveSettingsDraft.isNull()) m_improveSettingsDraft = QSharedPointer<nmSensitivityFitSettings>(new nmSensitivityFitSettings);
|
|
|
|
|
*m_improveSettingsDraft = settings;
|
|
|
|
|
nmWxAutomaticFitting* dialog = findChild<nmWxAutomaticFitting*>("nmSensitivityImproveSettings");
|
|
|
|
|
if(!dialog) {
|
|
|
|
|
dialog = new nmWxAutomaticFitting(this, m_improveSettingsDraft.data(), sampledParameters);
|
|
|
|
|
dialog->setObjectName("nmSensitivityImproveSettings");
|
|
|
|
|
dialog->setAttribute(Qt::WA_DeleteOnClose, false);
|
|
|
|
|
} else dialog->reloadSensitivitySettings(sampledParameters);
|
|
|
|
|
if(dialog->exec() == QDialog::Accepted) {
|
|
|
|
|
m_sensitiveData.setImproveSettings(QSharedPointer<const nmSensitivityFitSettings>(new nmSensitivityFitSettings(*m_improveSettingsDraft)));
|
|
|
|
|
updateModelCount();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -2706,25 +2923,31 @@ void nmWxSensitive::onPreviewClicked()
|
|
|
|
|
// 样本已在允许范围内生成,直接显示预览。
|
|
|
|
|
}
|
|
|
|
|
// Saphir 风格的连续运行列表,组合数再大也不显示分页控件。
|
|
|
|
|
QDialog dialog(this);
|
|
|
|
|
dialog.setWindowTitle(tr("Sensitivity Runs"));
|
|
|
|
|
dialog.setWindowFlags(dialog.windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
|
|
|
|
dialog.setMinimumSize(420, 360);
|
|
|
|
|
dialog.resize(470, 650);
|
|
|
|
|
QVBoxLayout* layout = new QVBoxLayout(&dialog);
|
|
|
|
|
layout->setContentsMargins(16, 16, 16, 12);
|
|
|
|
|
layout->setSpacing(8);
|
|
|
|
|
m_previewView = new SensitivityRunsView(m_sensitiveData, createTriangleIcon(), &dialog);
|
|
|
|
|
layout->addWidget(m_previewView);
|
|
|
|
|
QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Close, Qt::Horizontal, &dialog);
|
|
|
|
|
buttons->setCenterButtons(true);
|
|
|
|
|
buttons->button(QDialogButtonBox::Close)->setMinimumWidth(110);
|
|
|
|
|
layout->addWidget(buttons);
|
|
|
|
|
connect(buttons, SIGNAL(rejected()), &dialog, SLOT(reject()));
|
|
|
|
|
QDialog* dialog = findChild<QDialog*>("nmSensitivityPreview");
|
|
|
|
|
if(!dialog) {
|
|
|
|
|
dialog = new QDialog(this);
|
|
|
|
|
dialog->setObjectName("nmSensitivityPreview");
|
|
|
|
|
dialog->setAttribute(Qt::WA_DeleteOnClose, false);
|
|
|
|
|
dialog->setWindowTitle(tr("Sensitivity Runs"));
|
|
|
|
|
dialog->setWindowFlags(dialog->windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
|
|
|
|
dialog->setMinimumSize(420, 360);
|
|
|
|
|
dialog->resize(470, 650);
|
|
|
|
|
QVBoxLayout* layout = new QVBoxLayout(dialog);
|
|
|
|
|
layout->setContentsMargins(16, 16, 16, 12);
|
|
|
|
|
layout->setSpacing(8);
|
|
|
|
|
QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Close, Qt::Horizontal, dialog);
|
|
|
|
|
buttons->setCenterButtons(true);
|
|
|
|
|
buttons->button(QDialogButtonBox::Close)->setMinimumWidth(110);
|
|
|
|
|
layout->addWidget(buttons);
|
|
|
|
|
connect(buttons, SIGNAL(rejected()), dialog, SLOT(reject()));
|
|
|
|
|
}
|
|
|
|
|
// 预览窗口保持不变,运行列表按本次采样重建,避免保留旧行数。
|
|
|
|
|
delete m_previewView;
|
|
|
|
|
m_previewView = new SensitivityRunsView(m_sensitiveData, createTriangleIcon(), dialog);
|
|
|
|
|
qobject_cast<QVBoxLayout*>(dialog->layout())->insertWidget(0, m_previewView);
|
|
|
|
|
connect(m_previewView->verticalScrollBar(), SIGNAL(actionTriggered(int)),
|
|
|
|
|
this, SLOT(onPreviewScrollAction(int)));
|
|
|
|
|
dialog.exec();
|
|
|
|
|
m_previewView = nullptr;
|
|
|
|
|
dialog->exec();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void nmWxSensitive::onPreviewScrollAction(int action)
|
|
|
|
|
|