|
|
|
|
@ -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 <QWidget>
|
|
|
|
|
|
|
|
|
|
#include <QCoreApplication>
|
|
|
|
|
#include <QtCore/qnumeric.h>
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
const double skinPressureUnitFactor = 1e-3 / (86400.0 * 2.0 * 3.14159265358979323846 * 9.869233e-16 * 1e6);
|
|
|
|
|
|
|
|
|
|
bool validSkinPressureCurve(const QVector<QVector<double> >& 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<QVector<double> >& curve, double time, double& pressure,
|
|
|
|
|
bool extrapolate = false)
|
|
|
|
|
{
|
|
|
|
|
// 压力取样按步骤末端线性插值、沿端点斜率外推;PVT 查值默认仍限制在表内。
|
|
|
|
|
const QVector<double>& times = curve[0];
|
|
|
|
|
if(!extrapolate && (time < times.first() || time > times.last())) return false;
|
|
|
|
|
const int index = static_cast<int>(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<double> rates;
|
|
|
|
|
QVector<QVector<double> > pressureMobility;
|
|
|
|
|
double pressureFactor;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
bool skinPvtCurve(const QVector<double>& pressure, const QVector<double>& values,
|
|
|
|
|
QVector<QVector<double> >& 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<QString, double>& 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<double>* volume = nullptr;
|
|
|
|
|
const QVector<double>* 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<QVector<double> > 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<double>& pressure = data.pressureMobility[0];
|
|
|
|
|
int index = static_cast<int>(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<QVector<double> >& measured,
|
|
|
|
|
QVector<FlowSegmentData>& segments)
|
|
|
|
|
{
|
|
|
|
|
const QVector<QVector<double> >& model = well.m_oCurves.m_vecResultPressure;
|
|
|
|
|
const QVector<double>& rates = data.rates;
|
|
|
|
|
if(!validSkinPressureCurve(model) || !validSkinPressureCurve(measured) ||
|
|
|
|
|
rates.size() != well.m_vecFlowDurations.size() || rates.isEmpty()) return 0;
|
|
|
|
|
|
|
|
|
|
QVector<double> 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<int> 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<FlowSegmentData>& 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<const nmPebiResultSnapshot> 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<const nmPebiResultSnapshot> 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<QString, double> 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<QVector<double> > measured(2);
|
|
|
|
|
const QVector<QPointF> 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<FlowSegmentData> 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()) {
|
|
|
|
|
|