fix(nmNum): 冻结结果井流量制度并同步刷新历史曲线

- 在求解输入和结果快照中保存公共段时长、三相流量、分相启用状态、制度参考相及流动段索引
- 求解完成后将实际使用的流量制度写入结果快照,避免结果切换依赖实时井数据
- 切换结果井时同步刷新历史压力、计算压力、参考相流量曲线和流动段高亮
- 多相流量完整保留给求解器,历史窗口按求解时的制度参考相显示单条流量曲线
- 将 Snapshot.json 格式升级至 v4,不再兼容旧格式结果快照
develop
lh 10 hours ago
parent 768632b546
commit 4542d1132a

@ -34,6 +34,9 @@ struct nmPebiWellInputSnapshot
m_dWellboreStorage(0.0),
m_dSkin(0.0),
m_dDfc(0.0),
m_bUseOilRate(false),
m_bUseGasRate(false),
m_bUseWaterRate(false),
m_bRateControlled(false),
m_bRealWell(false),
m_bHasPerforation(false),
@ -64,6 +67,9 @@ struct nmPebiWellInputSnapshot
double m_dWellboreStorage; ///< 求解时井筒储集系数。
double m_dSkin; ///< 求解时表皮系数。
double m_dDfc; ///< 求解时裂缝导流能力。
bool m_bUseOilRate; ///< 求解时是否启用油相流量。
bool m_bUseGasRate; ///< 求解时是否启用气相流量。
bool m_bUseWaterRate; ///< 求解时是否启用水相流量。
bool m_bRateControlled; ///< 是否提供源汇项。
bool m_bRealWell; ///< 是否为真实井而非手工裂缝槽位。
bool m_bHasPerforation; ///< 求解时是否存在射孔。

@ -78,6 +78,24 @@ struct NM_DATA_EXPORT nmPebiResultWellSnapshot
NM_CASE_WELL_MODE m_eWellMode;
/** @brief 求解时冻结的规范化 Gauge 输入签名。 */
QString m_sGaugeInputSha1;
/** @brief 求解时冻结的公共流动段时长,不包含框架占位行。 */
QVector<double> m_vecFlowDurations;
/** @brief 与公共流动段逐项对应的油相流量。 */
QVector<double> m_vecOilRates;
/** @brief 与公共流动段逐项对应的气相流量。 */
QVector<double> m_vecGasRates;
/** @brief 与公共流动段逐项对应的水相流量。 */
QVector<double> m_vecWaterRates;
/** @brief 历史流量图及旧单相后处理使用的制度参考相。 */
NM_PHASE_TYPE m_eFlowSchedulePhase;
/** @brief 求解时冻结的一基流动段索引。 */
int m_nFlowSectionIndex;
/** @brief 求解时是否启用油相流量。 */
bool m_bUseOilRate;
/** @brief 求解时是否启用气相流量。 */
bool m_bUseGasRate;
/** @brief 求解时是否启用水相流量。 */
bool m_bUseWaterRate;
/** @brief 求解时冻结的井平面位置。 */
QPointF m_oLocation;
/** @brief 求解时是否存在射孔数据。 */

@ -240,7 +240,7 @@ class NM_SUB_WND_EXPORT nmSubWndMain : public iSubWndBaseFit {
// 填充已经存在的数值三维和结果参数页面。
bool populateResultWidgets(iSubWndFitting* pSubWndFit,
nmDataAnalyzeManager* pDataManager);
/** @brief 用快照中冻结的历史和计算曲线刷新三个结果窗口。 */
/** @brief 用快照中冻结的压力、流量和计算曲线刷新三个结果窗口。 */
bool refreshPebiResultCurves(
iSubWndFitting* pSubWndFit,
nmDataAnalyzeManager* pDataManager,

@ -2015,6 +2015,24 @@ bool nmCalculationDllPebiSolverTask::buildPebiResultSnapshotCandidate()
? NM_CaseWell_RateControlled
: NM_CaseWell_Observation;
oWell.m_sGaugeInputSha1 = oWellInput.m_sGaugeInputSha1;
if(oWellInput.m_bRateControlled) {
// 结果切井必须显示本轮求解实际使用的制度,不能重新读取实时井。
oWell.m_eFlowSchedulePhase =
oWellInput.m_eFlowSchedulePhase;
oWell.m_nFlowSectionIndex =
oWellInput.m_nFlowSectionIndex;
oWell.m_bUseOilRate = oWellInput.m_bUseOilRate;
oWell.m_bUseGasRate = oWellInput.m_bUseGasRate;
oWell.m_bUseWaterRate = oWellInput.m_bUseWaterRate;
qSwap(oWell.m_vecFlowDurations,
oWellInput.m_vecFlowDurations);
qSwap(oWell.m_vecOilRates,
oWellInput.m_vecOilRates);
qSwap(oWell.m_vecGasRates,
oWellInput.m_vecGasRates);
qSwap(oWell.m_vecWaterRates,
oWellInput.m_vecWaterRates);
}
oWell.m_oLocation = oWellInput.m_oLocation;
oWell.m_bHasPerforation = oWellInput.m_bHasPerforation;
oWell.m_bHasSkin = oWellInput.m_bHasPerforation;

@ -216,6 +216,10 @@ bool captureWellFlowSchedule(
oWellInput.m_vecOilRates.clear();
oWellInput.m_vecGasRates.clear();
oWellInput.m_vecWaterRates.clear();
oWellInput.m_eFlowSchedulePhase = PHASE_UNKNOWN;
oWellInput.m_bUseOilRate = false;
oWellInput.m_bUseGasRate = false;
oWellInput.m_bUseWaterRate = false;
if(!oGaugeInput.hasAlignedFlowSchedule()) {
return false;
}
@ -224,6 +228,9 @@ bool captureWellFlowSchedule(
oWellInput.m_vecOilRates = oGaugeInput.vecOilRates;
oWellInput.m_vecGasRates = oGaugeInput.vecGasRates;
oWellInput.m_vecWaterRates = oGaugeInput.vecWaterRates;
oWellInput.m_bUseOilRate = oGaugeInput.bUseOilRate;
oWellInput.m_bUseGasRate = oGaugeInput.bUseGasRate;
oWellInput.m_bUseWaterRate = oGaugeInput.bUseWaterRate;
return true;
}

@ -19,6 +19,11 @@ nmPebiResultWellSnapshot::nmPebiResultWellSnapshot()
: m_eWellType(Unknow_Well),
m_eWellCategory(NM_WellCategory_Unknown),
m_eWellMode(NM_CaseWell_Observation),
m_eFlowSchedulePhase(PHASE_UNKNOWN),
m_nFlowSectionIndex(0),
m_bUseOilRate(false),
m_bUseGasRate(false),
m_bUseWaterRate(false),
m_bHasPerforation(false),
m_bHasSkin(false),
m_bHasDfc(false),

@ -20,7 +20,7 @@
namespace {
// Builder 只校验内存模型的完整性;持久化文件的数量上限和字节上限
// 由 v3 加载器在分配 VTK/Qt 容器之前单独检查。
// 由当前成果加载器在分配 VTK/Qt 容器之前单独检查。
bool isFiniteValue(double dValue)
{
#if defined(_MSC_VER)
@ -128,6 +128,45 @@ bool areWellCurvesValid(const nmPebiResultWellCurves& oCurves)
isCurveValid(oCurves.m_vecResultSemiLog, 2, false);
}
bool isWellFlowScheduleValid(const nmPebiResultWellSnapshot& oWell)
{
// 观察井不向求解器提供源汇项,结果快照中也不伪造流量制度。
if(oWell.m_eWellMode == NM_CaseWell_Observation)
{
return oWell.m_vecFlowDurations.isEmpty() &&
oWell.m_vecOilRates.isEmpty() &&
oWell.m_vecGasRates.isEmpty() &&
oWell.m_vecWaterRates.isEmpty() &&
oWell.m_eFlowSchedulePhase == PHASE_UNKNOWN &&
oWell.m_nFlowSectionIndex == 0 &&
!oWell.m_bUseOilRate && !oWell.m_bUseGasRate &&
!oWell.m_bUseWaterRate;
}
const int nSegmentCount = oWell.m_vecFlowDurations.size();
if(nSegmentCount <= 0 ||
oWell.m_vecOilRates.size() != nSegmentCount ||
oWell.m_vecGasRates.size() != nSegmentCount ||
oWell.m_vecWaterRates.size() != nSegmentCount ||
!isFiniteVector(oWell.m_vecFlowDurations) ||
!isFiniteVector(oWell.m_vecOilRates) ||
!isFiniteVector(oWell.m_vecGasRates) ||
!isFiniteVector(oWell.m_vecWaterRates) ||
oWell.m_nFlowSectionIndex < 1 ||
oWell.m_nFlowSectionIndex > nSegmentCount)
{
return false;
}
// 参考相必须是本次求解实际启用的相,历史流量图才能与对数曲线一致。
return (oWell.m_eFlowSchedulePhase == PHASE_Oil &&
oWell.m_bUseOilRate) ||
(oWell.m_eFlowSchedulePhase == PHASE_Gas &&
oWell.m_bUseGasRate) ||
(oWell.m_eFlowSchedulePhase == PHASE_Water &&
oWell.m_bUseWaterRate);
}
bool areReservoirParametersFinite(
const nmPebiResultReservoirParameters& oParameters)
{
@ -541,9 +580,18 @@ bool nmPebiResultSnapshotBuilder::takeWell(
qSwap(oTarget.m_sWellCode, oWell.m_sWellCode);
qSwap(oTarget.m_sWellName, oWell.m_sWellName);
qSwap(oTarget.m_sGaugeInputSha1, oWell.m_sGaugeInputSha1);
qSwap(oTarget.m_vecFlowDurations, oWell.m_vecFlowDurations);
qSwap(oTarget.m_vecOilRates, oWell.m_vecOilRates);
qSwap(oTarget.m_vecGasRates, oWell.m_vecGasRates);
qSwap(oTarget.m_vecWaterRates, oWell.m_vecWaterRates);
oTarget.m_eWellType = oWell.m_eWellType;
oTarget.m_eWellCategory = oWell.m_eWellCategory;
oTarget.m_eWellMode = oWell.m_eWellMode;
oTarget.m_eFlowSchedulePhase = oWell.m_eFlowSchedulePhase;
oTarget.m_nFlowSectionIndex = oWell.m_nFlowSectionIndex;
oTarget.m_bUseOilRate = oWell.m_bUseOilRate;
oTarget.m_bUseGasRate = oWell.m_bUseGasRate;
oTarget.m_bUseWaterRate = oWell.m_bUseWaterRate;
oTarget.m_oLocation = oWell.m_oLocation;
oTarget.m_bHasPerforation = oWell.m_bHasPerforation;
oTarget.m_bHasSkin = oWell.m_bHasSkin;
@ -569,6 +617,11 @@ bool nmPebiResultSnapshotBuilder::takeWell(
oWell.m_eWellType = Unknow_Well;
oWell.m_eWellCategory = NM_WellCategory_Unknown;
oWell.m_eWellMode = NM_CaseWell_Observation;
oWell.m_eFlowSchedulePhase = PHASE_UNKNOWN;
oWell.m_nFlowSectionIndex = 0;
oWell.m_bUseOilRate = false;
oWell.m_bUseGasRate = false;
oWell.m_bUseWaterRate = false;
oWell.m_oLocation = QPointF();
oWell.m_bHasPerforation = false;
oWell.m_bHasSkin = false;
@ -762,6 +815,11 @@ bool nmPebiResultSnapshotBuilder::validate(QString* pError)
return fail(QString("PEBI result well curves are invalid: %1")
.arg(oWell.m_sWellCode), pError);
}
if(!isWellFlowScheduleValid(oWell))
{
return fail(QString("PEBI result well flow schedule is invalid: %1")
.arg(oWell.m_sWellCode), pError);
}
setWellIds.insert(oWell.m_sWellInstanceId);
setWellCodes.insert(oWell.m_sWellCode);
}
@ -815,7 +873,11 @@ bool nmPebiResultSnapshotBuilder::validate(QString* pError)
{
const QString& sWellInstanceId =
m_pCandidate->m_listDisplayWellInstanceIds[nIndex];
const nmPebiResultWellSnapshot* pDisplayWell =
m_pCandidate->findWell(sWellInstanceId);
if(!setWellIds.contains(sWellInstanceId) ||
pDisplayWell == NULL ||
pDisplayWell->m_eWellMode != NM_CaseWell_RateControlled ||
setDisplayWellIds.contains(sWellInstanceId))
{
return fail("PEBI display well list is invalid.", pError);

@ -27,6 +27,7 @@ namespace {
const char g_aPressureMagic[8] = { 'N', 'M', 'P', 'R', 'E', 'S', '3', 0 };
const char g_aCurvesMagic[8] = { 'N', 'M', 'C', 'U', 'R', 'V', '3', 0 };
const quint32 g_nBinaryFormatVersion = 1;
const int g_nSnapshotFormatVersion = 4;
const quint32 g_nEndianMarker = 0x01020304u;
const quint64 g_nMaximumCellCount = 50000000ull;
const quint32 g_nMaximumFrameCount = 10000u;
@ -533,7 +534,8 @@ bool parseSnapshotJson(
if(!oDocument.IsObject() ||
!oDocument.HasMember("SnapshotFormatVersion") ||
!oDocument["SnapshotFormatVersion"].IsInt() ||
oDocument["SnapshotFormatVersion"].GetInt() != 3 ||
oDocument["SnapshotFormatVersion"].GetInt() !=
g_nSnapshotFormatVersion ||
!oDocument.HasMember("ResultCellCount") ||
!oDocument["ResultCellCount"].IsUint64() ||
!oDocument.HasMember("PressureFrameCount") ||
@ -700,8 +702,9 @@ bool nmPebiResultSnapshotSerializer::save(
oDocument.SetObject();
rapidjson::Document::AllocatorType& oAllocator =
oDocument.GetAllocator();
// v3 强制保存每口结果井的 Gauge 输入签名。
oDocument.AddMember("SnapshotFormatVersion", 3, oAllocator);
// v4 在井目录中增加求解时冻结的完整流量制度,不兼容旧快照。
oDocument.AddMember("SnapshotFormatVersion",
g_nSnapshotFormatVersion, oAllocator);
oDocument.AddMember("SnapshotId",
toJsonString(pSnapshot->m_sSnapshotId, oAllocator), oAllocator);
oDocument.AddMember("GridInputRevision",
@ -758,6 +761,21 @@ bool nmPebiResultSnapshotSerializer::save(
oJson.AddMember("WellMode", static_cast<int>(oWell.m_eWellMode), oAllocator);
oJson.AddMember("GaugeInputSha1", toJsonString(
oWell.m_sGaugeInputSha1, oAllocator), oAllocator);
oJson.AddMember("FlowSchedulePhase", static_cast<int>(
oWell.m_eFlowSchedulePhase), oAllocator);
oJson.AddMember("FlowSectionIndex",
oWell.m_nFlowSectionIndex, oAllocator);
oJson.AddMember("UseOilRate", oWell.m_bUseOilRate, oAllocator);
oJson.AddMember("UseGasRate", oWell.m_bUseGasRate, oAllocator);
oJson.AddMember("UseWaterRate", oWell.m_bUseWaterRate, oAllocator);
addDoubleVector(oJson, "FlowDurations",
oWell.m_vecFlowDurations, oAllocator);
addDoubleVector(oJson, "OilRates",
oWell.m_vecOilRates, oAllocator);
addDoubleVector(oJson, "GasRates",
oWell.m_vecGasRates, oAllocator);
addDoubleVector(oJson, "WaterRates",
oWell.m_vecWaterRates, oAllocator);
oJson.AddMember("X", oWell.m_oLocation.x(), oAllocator);
oJson.AddMember("Y", oWell.m_oLocation.y(), oAllocator);
oJson.AddMember("HasPerforation", oWell.m_bHasPerforation, oAllocator);
@ -1064,6 +1082,7 @@ bool nmPebiResultSnapshotSerializer::load(
int nWellType = 0;
int nWellCategory = 0;
int nWellMode = 0;
int nFlowSchedulePhase = 0;
if(!oJson.IsObject() ||
!oJson.HasMember("WellInstanceId") ||
!oJson["WellInstanceId"].IsString() ||
@ -1075,6 +1094,24 @@ bool nmPebiResultSnapshotSerializer::load(
!readRequiredInt(oJson, "WellType", nWellType) ||
!readRequiredInt(oJson, "WellCategory", nWellCategory) ||
!readRequiredInt(oJson, "WellMode", nWellMode) ||
!readRequiredInt(oJson, "FlowSchedulePhase",
nFlowSchedulePhase) ||
!readRequiredInt(oJson, "FlowSectionIndex",
oWell.m_nFlowSectionIndex) ||
!readRequiredBool(oJson, "UseOilRate",
oWell.m_bUseOilRate) ||
!readRequiredBool(oJson, "UseGasRate",
oWell.m_bUseGasRate) ||
!readRequiredBool(oJson, "UseWaterRate",
oWell.m_bUseWaterRate) ||
!readDoubleVector(oJson, "FlowDurations",
oWell.m_vecFlowDurations) ||
!readDoubleVector(oJson, "OilRates",
oWell.m_vecOilRates) ||
!readDoubleVector(oJson, "GasRates",
oWell.m_vecGasRates) ||
!readDoubleVector(oJson, "WaterRates",
oWell.m_vecWaterRates) ||
!readRequiredDouble(oJson, "X", oWell.m_oLocation.rx()) ||
!readRequiredDouble(oJson, "Y", oWell.m_oLocation.ry()) ||
!readRequiredBool(oJson, "HasPerforation", oWell.m_bHasPerforation) ||
@ -1099,6 +1136,8 @@ bool nmPebiResultSnapshotSerializer::load(
oWell.m_eWellCategory = static_cast<NM_WELL_CATEGORY>(
nWellCategory);
oWell.m_eWellMode = static_cast<NM_CASE_WELL_MODE>(nWellMode);
oWell.m_eFlowSchedulePhase = static_cast<NM_PHASE_TYPE>(
nFlowSchedulePhase);
if(setJsonWellIds.contains(oWell.m_sWellInstanceId) ||
!mapCurves.contains(oWell.m_sWellInstanceId))
{

@ -64,6 +64,8 @@
#include "nmWxGeoRefDlg.h"
#include "iSubWndFitting.h"
#include "iGuiPlotPF.h"
#include "ZxObjCurveFlow.h"
#include "iAnalRun.h"
#include <QVector>
@ -99,6 +101,25 @@ namespace
// QPointer会在QObject销毁后自动变空避免保留已经释放的线程指针。
QPointer<nmCalculationDllPebiSolverTask> s_pRunningSolverTask;
QPointer<nmSubWndMain> s_pPendingSolverWindow;
// 历史窗口仍是单流量曲线结构,因此显示与双对数、半对数后处理一致的制度参考相。
const QVector<double>* resultReferenceRatesOf(
const nmPebiResultWellSnapshot* pWell)
{
if(pWell == NULL) {
return NULL;
}
switch(pWell->m_eFlowSchedulePhase) {
case PHASE_Oil:
return pWell->m_bUseOilRate ? &pWell->m_vecOilRates : NULL;
case PHASE_Gas:
return pWell->m_bUseGasRate ? &pWell->m_vecGasRates : NULL;
case PHASE_Water:
return pWell->m_bUseWaterRate ? &pWell->m_vecWaterRates : NULL;
default:
return NULL;
}
}
}
nmSubWndMain::nmSubWndMain(QWidget *parent, QString sExt) :
@ -2772,6 +2793,51 @@ bool nmSubWndMain::refreshPebiResultCurves(
vecResultPressure, false);
pSubWndFit->adjustFitSubPlotBy(
FSRT_Hist, vecResultPressure, true, &sErrorMessage);
// 压力和流量属于同一口结果井;切井时必须一起替换,避免下图残留上一口井制度。
const QVector<double>* pReferenceRates =
resultReferenceRatesOf(pWell);
iCurveDesc oFlowCurve;
oFlowCurve.m_oCurveType = POT_CurveFlow;
oFlowCurve.m_sCurveName = s_HistorySouce_CurveF;
oFlowCurve.m_bLineVisible = true;
oFlowCurve.m_bPointVisible = false;
oFlowCurve.m_oPen.setColor(Qt::red);
if(pWell != NULL && pReferenceRates != NULL &&
pReferenceRates->size() == pWell->m_vecFlowDurations.size()) {
// 快照只保存真实流量段;绘图对象仍按框架原始格式使用首个
// (0,0) 占位点,一基流动段索引因此可以直接恢复而无需换算。
oFlowCurve.m_vecX = pWell->m_vecFlowDurations;
oFlowCurve.m_vecY = *pReferenceRates;
oFlowCurve.m_vecX.prepend(0.0);
oFlowCurve.m_vecY.prepend(0.0);
}
QVector<iCurveDesc> vecHistoryFlow;
vecHistoryFlow.append(oFlowCurve);
pSubWndFit->adjustFitSubPlotBy(
FSRT_Hist, vecHistoryFlow, true, &sErrorMessage);
// 框架流动段索引与 ZxSegmentInfo 相同,均为一基。这里只更新只读结果图元,
// 阻断选择信号,不能让结果井切换反向修改当前分析井的流动段配置。
QWidget* pHistoryWidget = pSubWndFit->getFitSubRstWxOf(
FSRT_Hist, false, &sErrorMessage);
iGuiPlotPF* pHistoryPlot = qobject_cast<iGuiPlotPF*>(pHistoryWidget);
if(pHistoryPlot != NULL) {
ZxObjCurveBase* pFlowObject = NULL;
if(pHistoryPlot->getChartDataObjOfPF(pFlowObject, false) &&
pFlowObject != NULL) {
ZxObjCurveFlow* pFlowCurve =
dynamic_cast<ZxObjCurveFlow*>(pFlowObject);
if(pFlowCurve != NULL) {
const bool bSignalsBlocked = pFlowCurve->blockSignals(true);
pFlowCurve->setCurSegPtIndex(
pWell == NULL ? 0 :
pWell->m_nFlowSectionIndex);
pFlowCurve->blockSignals(bSignalsBlocked);
pHistoryPlot->runUpdate();
}
}
}
return true;
}

Loading…
Cancel
Save