diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index fe7f48bb..a7529eb5 100644 Binary files a/Bin/Config/Lang/cn/nmNum_cn.qm and b/Bin/Config/Lang/cn/nmNum_cn.qm differ diff --git a/Bin/Config/Lang/cn/nmNum_cn.ts b/Bin/Config/Lang/cn/nmNum_cn.ts index aeed24c5..42b27a43 100644 --- a/Bin/Config/Lang/cn/nmNum_cn.ts +++ b/Bin/Config/Lang/cn/nmNum_cn.ts @@ -7709,6 +7709,66 @@ Average pressure in contour: %2 MPa nmWxTimeDependentSkin + + Select a well with valid model results before estimating skin. + 请先选择具有有效模型结果的井,再估算表皮。 + + + Skin estimation requires a single perforation. + 自动估算表皮仅支持单射孔。 + + + Skin estimation does not yet support perforation closing intervals. + 自动估算表皮暂不支持设有射孔封堵区间的模型。 + + + Generate a model with the current inputs and a constant skin before computing skin steps. + 请先使用当前输入和恒定表皮生成模型结果,再计算各段表皮。 + + + The model result does not match this well's pressure and rate data. Generate the model again. + 模型结果与当前井的压力、流量数据不一致,请重新生成模型。 + + + Observation wells have no controlled rate for skin estimation. + 观察井没有可用于估算表皮的自身控制流量。 + + + Skin estimation requires model and measured pressure curves. + 自动估算表皮需要模型压力曲线和实测压力曲线。 + + + The model requires valid permeability, thickness and skin. + 模型的渗透率、厚度和表皮参数必须有效。 + + + Skin estimation supports single-phase oil, gas, water and oil-water models only. + 自动估算表皮支持单相油、气、水以及油水两相模型。 + + + Set positive reference volume factors and viscosities in the analysis PVT settings. + 请在分析的 PVT 设置中填写有效的参考体积系数和黏度,数值必须大于零。 + + + Oil-water estimation requires finite phase rates with the same flow direction. + 油水两相估算要求各相流量有效,且油、水流动方向一致。 + + + Variable-PVT skin estimation requires finite, positive B and viscosity values on a monotonic pressure table. + 变 PVT 表皮估算要求压力表严格单调,体积系数和黏度均为有效正数。 + + + Cannot read reference PVT values from this analysis. + 无法读取本分析的 PVT 参考值。 + + + The model rate schedule or pressure conversion factor is invalid. + 模型的流量制度或压力换算系数无效。 + + + No skin steps could be estimated. Check pressure data, non-zero rate steps and the gas PVT pressure range. + 没有可估算的表皮分段。请检查压力数据、非零流量段及气体 PVT 压力范围。 + Time Dependent Skin 时间相关表皮系数 diff --git a/Include/nmNum/nmData/nmDataAnalyzeManager.h b/Include/nmNum/nmData/nmDataAnalyzeManager.h index 57d3cd56..eebfc868 100644 --- a/Include/nmNum/nmData/nmDataAnalyzeManager.h +++ b/Include/nmNum/nmData/nmDataAnalyzeManager.h @@ -473,6 +473,8 @@ public: // 获取PEBI求解器需要的pvt数据 nmDataPvtParaForPebi* getPebiPvtPara(); + // 读取所属分析窗口当前选用的 PVT 单值,参数名与底层 PVT 接口一致。 + bool getAnalysisPvtValues(const QStringList& names, QMap& values) const; // 获取气单相变化 PVT 求解所需的压力/拟压力转换表。 bool getPebiPseudoPressureTable(std::vector& pressure, std::vector& pseudoPressure); diff --git a/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h b/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h index fe7ca7de..5fc29164 100644 --- a/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h +++ b/Include/nmNum/nmSubWxs/nmWxTimeDependentSkin.h @@ -6,6 +6,7 @@ #include #include #include +#include #include "nmDataAnalyzeManager.h" #include "nmDataWellBase.h" @@ -44,6 +45,7 @@ public: void onRemoveClicked(); void onSplitStepsClicked(); void onComputeClicked(); + void updateComputeState(); void onSkinVsRateClicked(); void onPerforationClosingBtnClicked(); void onPerforationClosingAccepted(); @@ -95,6 +97,9 @@ private: // 更新图表 void updateCharts(); + // 检查自动估算表皮所需的基准结果和模型范围。 + QString skinComputeUnavailableReason() const; + void showInteractiveChart(); // 显示交互式图表 void restoreOriginalCharts(); // 恢复原始占位符 void initializeChartFromData(); @@ -211,6 +216,7 @@ private: QString m_dSdQDisplayUnit; // 数据成员 + QPointer m_pDataManager; // 窗口所属分析,计算时不随当前页签切换 nmDataWellBase* m_currentWell; // 当前井数据 nmDataPerforation m_perforationData; // 射孔段数据 int m_currentPerforationIndex; // 当前射孔段索引 diff --git a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp index e0b8f5ec..9f8559d7 100644 --- a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp +++ b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp @@ -4169,6 +4169,14 @@ nmDataPvtParaForPebi* nmDataAnalyzeManager::getPebiPvtPara() return m_pebiPvtPara; } +bool nmDataAnalyzeManager::getAnalysisPvtValues(const QStringList& names, QMap& values) const +{ + // 按创建本 Manager 时绑定的分析读取,避免切换页签后串用其他分析的参考物性。 + values.clear(); + nmDataAnalyzeContextProvider* context = nmDataAnalyzeContext::provider(); + return context && m_pOwnerFitting && context->getPvtParaValues(m_pOwnerFitting, names, values); +} + bool nmDataAnalyzeManager::getPebiPseudoPressureTable(std::vector& pressure, std::vector& pseudoPressure) { diff --git a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp index 94149fe3..f655c35b 100644 --- a/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp +++ b/Src/nmNum/nmSubWxs/nmWxTimeDependentSkin.cpp @@ -3,6 +3,7 @@ #include "nmDialogPreviewGuard.h" #include "nmWxPressFlowChartWidget.h" #include "nmWxChartWidget.h" +#include "nmPebiResultSnapshot.h" #include "iUnitGroup.h" #include "iUnitHelper.h" #include "ZxDataEditorDelegate.h" @@ -38,6 +39,274 @@ #include #include +#include +#include + +namespace { + +const double skinPressureUnitFactor = 1e-3 / (86400.0 * 2.0 * 3.14159265358979323846 * 9.869233e-16 * 1e6); + +bool validSkinPressureCurve(const QVector >& curve) +{ + // 插值只接受同一时间基准下有限、严格递增的压力序列。 + if(curve.size() < 2 || curve[0].size() < 2 || curve[0].size() != curve[1].size()) return false; + for(int i = 0; i < curve[0].size(); ++i) { + if(!qIsFinite(curve[0][i]) || !qIsFinite(curve[1][i]) || + (i > 0 && curve[0][i] <= curve[0][i - 1])) return false; + } + return true; +} + +bool interpolateSkinPressure(const QVector >& curve, double time, double& pressure, + bool extrapolate = false) +{ + // 压力取样按步骤末端线性插值、沿端点斜率外推;PVT 查值默认仍限制在表内。 + const QVector& times = curve[0]; + if(!extrapolate && (time < times.first() || time > times.last())) return false; + const int index = static_cast(std::lower_bound(times.constBegin(), times.constEnd(), time) - times.constBegin()); + const int right = qBound(1, index, times.size() - 1); + const double fraction = (time - times[right - 1]) / (times[right] - times[right - 1]); + pressure = curve[1][right - 1] + fraction * (curve[1][right] - curve[1][right - 1]); + return qIsFinite(pressure); +} + +// 只保存本次估算用的值数据;液体参考物性读取所属分析,曲线和制度沿用模型快照。 +struct SkinComputeData +{ + QVector rates; + QVector > pressureMobility; + double pressureFactor; +}; + +bool skinPvtCurve(const QVector& pressure, const QVector& values, + QVector >& curve) +{ + // PVT 表可按压力升序或降序保存,统一方向后复用压力曲线的校验和插值。 + curve.resize(2); + curve[0] = pressure; + curve[1] = values; + if(pressure.size() > 1 && pressure.first() > pressure.last()) { + std::reverse(curve[0].begin(), curve[0].end()); + std::reverse(curve[1].begin(), curve[1].end()); + } + if(!validSkinPressureCurve(curve)) return false; + for(int i = 0; i < curve[0].size(); ++i) { + if(curve[0][i] <= 0.0 || curve[1][i] <= 0.0) return false; + } + return true; +} + +QString prepareSkinCompute(const nmPebiResultWellSnapshot& well, + const nmPebiResultReservoirParameters& reservoir, + const nmPebiResultPvtParameters& pvt, int modelType, + const QMap& referencePvt, + SkinComputeData& data) +{ + data = SkinComputeData(); + // mD、m、mPa.s、m³/d、MPa 的单位换算;普通压差用参考物性,气拟压力模式保留积分。 + data.pressureFactor = reservoir.m_dPermeability * reservoir.m_dThickness / skinPressureUnitFactor; + if(!qIsFinite(reservoir.m_dPermeability) || reservoir.m_dPermeability <= 0.0 || + !qIsFinite(reservoir.m_dThickness) || reservoir.m_dThickness <= 0.0 || + !qIsFinite(data.pressureFactor) || data.pressureFactor <= 0.0 || !qIsFinite(well.m_dSkin)) + return nmWxTimeDependentSkin::tr("The model requires valid permeability, thickness and skin."); + + const QVector* volume = nullptr; + const QVector* viscosity = nullptr; + double constantVolume = 0.0, constantViscosity = 0.0; + NM_PHASE_TYPE phase = PHASE_UNKNOWN; + switch(modelType) { + case SMT_Oil_ConstPvt: + case SMT_Oil_VariablePvt: + phase = PHASE_Oil; + constantVolume = referencePvt.value("Bo"); + constantViscosity = referencePvt.value("Miuo"); + break; + case SMT_Water_ConstPvt: + case SMT_Water_VariablePvt: + phase = PHASE_Water; + constantVolume = referencePvt.value("Bw"); + constantViscosity = referencePvt.value("Miuw"); + break; + case SMT_Gas_VariablePvt: + // 气变化 PVT 按不使用拟压力的方式估算表皮,与油、水共用参考单值换算。 + phase = PHASE_Gas; + constantVolume = referencePvt.value("Bg"); + constantViscosity = referencePvt.value("Miug"); + break; + case SMT_Gas_PseudoPressure: + // 气拟压力模式的结果压力为 MPa;Bg 已包含气体标准条件换算。 + phase = PHASE_Gas; + volume = &pvt.m_vecBg; + viscosity = &pvt.m_vecMiug; + break; + case SMT_Oil_Water_TwoPhase: + break; + default: + return nmWxTimeDependentSkin::tr("Skin estimation supports single-phase oil, gas, water and oil-water models only."); + } + + if(modelType == SMT_Oil_Water_TwoPhase) { + // 按模型读取已冻结的油水制度,只检查数组长度以保证逐段取值有效。 + if(well.m_vecOilRates.size() != well.m_vecFlowDurations.size() || + well.m_vecWaterRates.size() != well.m_vecFlowDurations.size()) + return nmWxTimeDependentSkin::tr("The model rate schedule or pressure conversion factor is invalid."); + + // 未启用 Perrine:使用模型渗透率和油相参考物性,水量通过 Bw/Bo 换算。 + const double bo = referencePvt.value("Bo"), bw = referencePvt.value("Bw"); + const double muo = referencePvt.value("Miuo"); + if(!qIsFinite(bo) || bo <= 0.0 || !qIsFinite(bw) || bw <= 0.0 || + !qIsFinite(muo) || muo <= 0.0) + return nmWxTimeDependentSkin::tr("Set positive reference volume factors and viscosities in the analysis PVT settings."); + + data.pressureFactor /= bo * muo; + for(int i = 0; i < well.m_vecFlowDurations.size(); ++i) { + const double qo = well.m_vecOilRates[i]; + const double qw = well.m_vecWaterRates[i]; + // 两相反向流动不适用当前的等效生产流量计算。 + if(!qIsFinite(qo) || !qIsFinite(qw) || (qo < 0.0 && qw > 0.0) || (qo > 0.0 && qw < 0.0)) + return nmWxTimeDependentSkin::tr("Oil-water estimation requires finite phase rates with the same flow direction."); + data.rates.append(qo + qw * bw / bo); + } + } else { + // 求解输入已将未使用的相补零,这里直接取模型对应相的实际计算流量。 + data.rates = phase == PHASE_Oil ? well.m_vecOilRates : + (phase == PHASE_Water ? well.m_vecWaterRates : well.m_vecGasRates); + if(volume) { + QVector > volumeCurve, viscosityCurve; + if(!skinPvtCurve(pvt.m_vecPressure, *volume, volumeCurve) || + !skinPvtCurve(pvt.m_vecPressure, *viscosity, viscosityCurve)) + return nmWxTimeDependentSkin::tr("Variable-PVT skin estimation requires finite, positive B and viscosity values on a monotonic pressure table."); + // 底层 calPS 依赖全局模式且缺少冻结的配套尺度,这里保留同一快照的常规气体积分。 + data.pressureMobility = volumeCurve; + for(int i = 0; i < volumeCurve[0].size(); ++i) { + const double inverse = 1.0 / (volumeCurve[1][i] * viscosityCurve[1][i]); + if(!qIsFinite(inverse) || inverse <= 0.0) + return nmWxTimeDependentSkin::tr("Variable-PVT skin estimation requires finite, positive B and viscosity values on a monotonic pressure table."); + data.pressureMobility[1][i] = inverse; + } + } else { + // 普通压差模式使用本次读取的参考 B、黏度,不按 Pi 重新查表。 + if(!qIsFinite(constantVolume) || constantVolume <= 0.0 || + !qIsFinite(constantViscosity) || constantViscosity <= 0.0) + return nmWxTimeDependentSkin::tr("Set positive reference volume factors and viscosities in the analysis PVT settings."); + data.pressureFactor /= constantVolume * constantViscosity; + } + } + + if(!qIsFinite(data.pressureFactor) || data.pressureFactor <= 0.0 || data.rates.isEmpty() || + data.rates.size() != well.m_vecFlowDurations.size()) + return nmWxTimeDependentSkin::tr("The model rate schedule or pressure conversion factor is invalid."); + for(int i = 0; i < data.rates.size(); ++i) { + if(!qIsFinite(data.rates[i]) || !qIsFinite(well.m_vecFlowDurations[i]) || well.m_vecFlowDurations[i] <= 0.0) + return nmWxTimeDependentSkin::tr("The model rate schedule or pressure conversion factor is invalid."); + } + return QString(); +} + +bool skinPressureDifference(const SkinComputeData& data, double measuredPressure, + double modelPressure, double& difference) +{ + difference = modelPressure - measuredPressure; + if(data.pressureMobility.isEmpty()) return qIsFinite(difference); + + // 直接对冻结 PVT 的 1/(Bμ) 积分,等价于配套系数消去归一化常数后的拟压力差。 + // 分段线性插值积分密度;端点也参与积分,避免仅对累计积分值插值的区间内误差。 + const double lower = qMin(measuredPressure, modelPressure); + const double upper = qMax(measuredPressure, modelPressure); + double lowerValue, upperValue; + if(!interpolateSkinPressure(data.pressureMobility, lower, lowerValue) || + !interpolateSkinPressure(data.pressureMobility, upper, upperValue)) return false; + double integral = 0.0, left = lower, leftValue = lowerValue; + const QVector& pressure = data.pressureMobility[0]; + int index = static_cast(std::upper_bound(pressure.constBegin(), pressure.constEnd(), lower) - pressure.constBegin()); + for(; index < pressure.size() && pressure[index] < upper; ++index) { + const double value = data.pressureMobility[1][index]; + integral += (pressure[index] - left) * (leftValue + value) * 0.5; + left = pressure[index]; + leftValue = value; + } + integral += (upper - left) * (leftValue + upperValue) * 0.5; + difference = modelPressure >= measuredPressure ? integral : -integral; + return qIsFinite(difference); +} + +int estimateSkinSegments(const nmPebiResultWellSnapshot& well, + const SkinComputeData& data, + const QVector >& measured, + QVector& segments) +{ + const QVector >& model = well.m_oCurves.m_vecResultPressure; + const QVector& rates = data.rates; + if(!validSkinPressureCurve(model) || !validSkinPressureCurve(measured) || + rates.size() != well.m_vecFlowDurations.size() || rates.isEmpty()) return 0; + + QVector ends; + double end = 0.0; + for(int i = 0; i < rates.size(); ++i) { + const double duration = well.m_vecFlowDurations[i]; + if(!qIsFinite(duration) || duration <= 0.0 || !qIsFinite(rates[i])) return 0; + end += duration; + if(!qIsFinite(end)) return 0; + ends.append(end); + } + + int computedCount = 0; + QVector unmatchedSegments; + for(int i = 0; i < segments.size(); ++i) { + const double start = segments[i].segmentStart.getValue().toDouble(); + const double finish = segments[i].segmentEnd.getValue().toDouble(); + if(!qIsFinite(start) || !qIsFinite(finish) || finish <= start) { + continue; + } + + // 按 Saphir 取起点落在当前表皮段内的最后一个非零流量段,并在该流量段末端取样。 + double sampleTime = 0.0; + double rate = 0.0; + int rateIndex = -1; + for(int j = 0; j < rates.size(); ++j) { + const double rateStart = j == 0 ? 0.0 : ends[j - 1]; + if(rateStart > finish - 1e-5) break; + if(rateStart >= start - 1e-5 && rates[j] != 0.0) { + sampleTime = ends[j]; + rate = rates[j]; + rateIndex = j; + } + } + // 没有符合条件的非零流量段时删除该分界;先完成原始分段取样,避免合并改变本次计算。 + if(rateIndex < 0) { + unmatchedSegments.append(i); + continue; + } + double modelPressure = 0.0, measuredPressure = 0.0; + if(!interpolateSkinPressure(model, sampleTime, modelPressure, true) || + !interpolateSkinPressure(measured, sampleTime, measuredPressure, true)) { + continue; + } + + double pressureDifference = 0.0; + if(!skinPressureDifference(data, measuredPressure, modelPressure, pressureDifference)) { + continue; + } + // 修正始终相对目标井的冻结表皮,重复点击或换井不会累加、串用基准值。 + const double skin = well.m_dSkin + data.pressureFactor * pressureDifference / rate; + if(!qIsFinite(skin)) { + continue; + } + segments[i].skinValue.setValue(qMax(-5.0, skin)); + // 与 Compute 的单点校正一致,只估计总表皮,不从一个压力差同时反演 dS/dQ。 + segments[i].dSdQ.setValue(0.0); + ++computedCount; + } + // 从后往前移除无匹配段,前段延伸至被删段末端,保留其已算出的表皮值。 + for(int i = unmatchedSegments.size() - 1; i >= 0; --i) { + const int index = unmatchedSegments[i]; + if(index > 0) segments[index - 1].segmentEnd = segments[index].segmentEnd; + segments.remove(index); + } + return computedCount; +} + +} // 静态成员变量定义 nmWxTimeDependentSkin* nmWxTimeDependentSkin::s_instance = nullptr; @@ -76,6 +345,7 @@ void nmWxTimeDependentSkin::showForCurrentWell(nmDataWellBase* pSpecificWellData nmWxTimeDependentSkin::nmWxTimeDependentSkin(nmDataWellBase* pWellData, QWidget *parent) : iDlgBase(parent) + , m_pDataManager(nmDataAnalyzeManager::getCurrentInstance()) , m_pChartWidget(nullptr) , m_pOriginalPressureChart(nullptr) , m_pOriginalFlowRateChart(nullptr) @@ -217,6 +487,11 @@ nmWxTimeDependentSkin::nmWxTimeDependentSkin(nmDataWellBase* pWellData, QWidget connect(m_pShowDatesCheckBox, SIGNAL(toggled(bool)), this, SLOT(onShowDatesToggled(bool))); connect(m_pComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(onPerforationChanged(int))); + + // 新结果发布或失效后同步按钮状态;点击时仍重新检查,避免使用旧分析结果。 + nmDataAnalyzeManager* manager = m_pDataManager.data(); + if(manager) connect(manager, SIGNAL(sigPebiResultSnapshotChanged()), this, SLOT(updateComputeState())); + updateComputeState(); } nmWxTimeDependentSkin::~nmWxTimeDependentSkin() @@ -275,8 +550,6 @@ void nmWxTimeDependentSkin::initUI() createButton(m_pRemoveBtn, "nmTimeDepSkin3", tr("Remove")); createButton(m_pSplitStepsBtn, "nmTimeDepSkin4", tr("Split Steps")); createButton(m_pComputeBtn, "nmTimeDepSkin5", tr("Compute")); - // 计算功能尚未实现,隐藏图标和文字容器,让射孔封堵前移到该位置。 - m_pComputeBtn->parentWidget()->hide(); createButton(m_pPerforationClosingBtn, "nmTimeDepSkin6", tr("Perforation Closing")); m_pToolbarLayout->addStretch(); @@ -576,8 +849,6 @@ void nmWxTimeDependentSkin::updateTableFromFlowSegments() const QVector& segments = m_perforationData.getFlowSegments(); - if(segments.isEmpty()) return; - const bool enableRateDep = m_perforationData.isRateDependentEnabled(); const int requiredCols = enableRateDep ? 4 : 3; @@ -897,9 +1168,107 @@ void nmWxTimeDependentSkin::onSplitStepsClicked() updateTableFromFlowSegments(); } +QString nmWxTimeDependentSkin::skinComputeUnavailableReason() const +{ + // 多井按井 UUID 查找各自的冻结结果,观察井没有可用于表皮反算的自身流量。 + nmDataAnalyzeManager* manager = m_pDataManager.data(); + if(!manager || !m_currentWell) + return tr("Select a well with valid model results before estimating skin."); + if(m_currentWell->getPerforationCount() != 1) + return tr("Skin estimation requires a single perforation."); + if(!m_perforationData.getTimeValues().isEmpty()) + return tr("Skin estimation does not yet support perforation closing intervals."); + if(!manager->isPebiResultSnapshotCurrent()) + return tr("Generate a model with the current inputs and a constant skin before computing skin steps."); + + QSharedPointer snapshot = manager->getPebiResultSnapshot(); + const nmPebiResultWellSnapshot* well = snapshot->findWell(m_currentWell->getWellInstanceId()); + if(!well || !well->m_bHasSkin || + well->m_sGaugeInputSha1.isEmpty() || well->m_sGaugeInputSha1 != m_currentWell->calculateGaugeInputSha1()) + return tr("The model result does not match this well's pressure and rate data. Generate the model again."); + if(well->m_eWellMode != NM_CaseWell_RateControlled) + return tr("Observation wells have no controlled rate for skin estimation."); + // 直井、压裂井和多段压裂井共用模型压力与井总流量,估算整井等效表皮。 + if(!validSkinPressureCurve(well->m_oCurves.m_vecResultPressure) || m_currentWell->getPressurePoints().size() < 2) + return tr("Skin estimation requires model and measured pressure curves."); + return QString(); +} + +void nmWxTimeDependentSkin::updateComputeState() +{ + // 仅更新计算按钮的可用状态,切换页面不改变原有模型设置。 + const QString reason = skinComputeUnavailableReason(); + m_pComputeBtn->setEnabled(reason.isEmpty() && !m_pChartWidget); +} + void nmWxTimeDependentSkin::onComputeClicked() { + const QString reason = skinComputeUnavailableReason(); + if(!reason.isEmpty()) { + updateComputeState(); + QMessageBox::information(this, tr("Compute"), reason); + return; + } + QSharedPointer snapshot = + m_pDataManager->getPebiResultSnapshot(); + const nmPebiResultWellSnapshot* well = snapshot->findWell(m_currentWell->getWellInstanceId()); + const nmPebiResultPvtParameters& pvt = snapshot->getPvtParameters(); + const int modelType = snapshot->getSolverSettings().m_nSolverModelType; + // 每次点击从所属分析重新读取参考单值,不回退到旧快照或默认物性。 + QStringList names; + if(modelType == SMT_Oil_ConstPvt || modelType == SMT_Oil_VariablePvt || modelType == SMT_Oil_Water_TwoPhase) + names << "Bo" << "Miuo"; + if(modelType == SMT_Water_ConstPvt || modelType == SMT_Water_VariablePvt) + names << "Bw" << "Miuw"; + else if(modelType == SMT_Oil_Water_TwoPhase) + names << "Bw"; + if(modelType == SMT_Gas_VariablePvt) + names << "Bg" << "Miug"; + QMap referencePvt; + if(!names.isEmpty() && !m_pDataManager->getAnalysisPvtValues(names, referencePvt)) { + QMessageBox::information(this, tr("Compute"), tr("Cannot read reference PVT values from this analysis.")); + return; + } + SkinComputeData data; + const QString preparationError = prepareSkinCompute(*well, snapshot->getReservoirParameters(), pvt, modelType, referencePvt, data); + if(!preparationError.isEmpty()) { + QMessageBox::information(this, tr("Compute"), preparationError); + return; + } + QVector > measured(2); + const QVector points = m_currentWell->getPressurePoints(); + for(int i = 0; i < points.size(); ++i) { + measured[0].append(points[i].x()); + measured[1].append(points[i].y()); + } + // 只更新本窗口副本;计算后移除无匹配流量段的分界,取消仍可放弃本次估算。 + QVector segments = m_perforationData.getFlowSegments(); + const int originalSegmentCount = segments.size(); + const int computedCount = estimateSkinSegments(*well, data, measured, segments); + if(computedCount == 0 && segments.size() == originalSegmentCount) { + QMessageBox::information(this, tr("Compute"), + tr("No skin steps could be estimated. Check pressure data, non-zero rate steps and the gas PVT pressure range.")); + return; + } + // 分段删除后重新定位选中项,避免旧索引指向其他行或越界;空结果由表格刷新清除旧行。 + int selectedIndex = 0; + if(segments.size() != originalSegmentCount && !segments.isEmpty()) { + for(int i = 0; i < segments.size(); ++i) { + if(segments[i].isSelected) { + selectedIndex = i; + break; + } + } + for(int i = 0; i < segments.size(); ++i) segments[i].isSelected = i == selectedIndex; + } + m_perforationData.setFlowSegments(segments); + if(segments.size() != originalSegmentCount && !segments.isEmpty()) + m_perforationData.setSelectedSegmentIndex(selectedIndex); + // 原来的表皮/流量拟合线已不对应新估算值,下次进入第二页时重新拟合。 + m_perforationData.setLinePositions(-999.0, -999.0, -999.0, -999.0); + updateCharts(); + updateTableFromFlowSegments(); } void nmWxTimeDependentSkin::onPerforationClosingBtnClicked() @@ -918,6 +1287,7 @@ void nmWxTimeDependentSkin::onPerforationClosingAccepted() m_perforationData.setTimeValues(data.getTimeValues()); m_perforationData.setTimeStates(data.getTimeStates()); m_perforationData.setBaseTime(data.getBaseTime()); + updateComputeState(); } void nmWxTimeDependentSkin::onSkinVsRateClicked() @@ -1207,9 +1577,10 @@ void nmWxTimeDependentSkin::restoreToolbarState() m_pSplitStepsBtn->parentWidget()->setVisible(true); } - // 切回默认界面时仍保持计算入口隐藏。 + // 计算入口仅在默认的表皮分段页面显示。 if(m_pComputeBtn && m_pComputeBtn->parentWidget()) { - m_pComputeBtn->parentWidget()->setVisible(false); + m_pComputeBtn->parentWidget()->setVisible(true); + updateComputeState(); } if(m_pPerforationClosingBtn && m_pPerforationClosingBtn->parentWidget()) {