diff --git a/Bin/Config/Lang/cn/nmNum_cn.ts b/Bin/Config/Lang/cn/nmNum_cn.ts index da15d6b..91a9ef4 100644 --- a/Bin/Config/Lang/cn/nmNum_cn.ts +++ b/Bin/Config/Lang/cn/nmNum_cn.ts @@ -2842,6 +2842,10 @@ Reason: %1 solver error + + The solver currently supports oil, gas, and water single-phase models and the oil-water two-phase model only. + 当前求解器仅支持油、气、水单相模型和油水两相模型。 + solver failed! @@ -7185,6 +7189,79 @@ Average pressure in contour: %2 MPa hh MPaMPa m³/dm³/d + Pressure Record:压力记录: + Flow Record:流量记录: + Display Phase:显示相: + (Invalid) (无效) + (Empty) (空) + (Corrupt) (损坏) + Oil + Gas + Water + Check Data检查数据 + Pressure/Flow Data Check压力流量数据检查 + Close关闭 + No well is bound to this page.当前页面未绑定井。 + === Well ====== 井信息 === + Well name: %1井名称:%1 + Well code: %1井编码:%1 + Well category: %1井别:%1 + Reference flow phase: %1井别参考相:%1 + Oil well油井 + Gas well气井 + Water well水井 + Unknown未知 + Usable可用 + Empty + Corrupt损坏 + Invalid identity身份无效 + (none)(无) + === Pressure records ====== 压力记录 === + Total pressure records: %1压力记录总数:%1 + Selected pressure gauge code: %1当前选中压力记录 Code:%1 + [%1] Name: %2[%1] 名称:%2 + Code: %1Code:%1 + Time: %1测量时间:%1 + Status: %1状态:%1 + Point count: %1点数:%1 + No pressure points.无压力点。 + First point = %1, last point = %2首点 = %1,末点 = %2 + All points = %1全部点 = %1 + === Flow records ====== 流量记录 === + Total flow records: %1流量记录总数:%1 + Selected flow gauge code: %1当前选中流量记录 Code:%1 + Current display phase: %1当前显示相:%1 + Multiphase: %1多相:%1 + Yes + No + Own flow segment index (1-based): %1自身流动段索引(一基):%1 + no data无数据 + raw points = %1, real segments = %2, total duration = %3原始点数 = %1,真实段数 = %2,累计时长 = %3 + first point is a (0,0) placeholder首个点为 (0,0) 占位点 + Phase time consistency: %1相间时间一致性:%1 + less than two phases with real data, no comparison有真实数据的相少于两个,不比较 + segment counts differ: %1段数不一致:%1 + segment counts equal (%1), but segment %2 time differs: %3 = %4, %5 = %6段数一致(%1),但第 %2 段时间不一致:%3 = %4,%5 = %6 + segment counts equal (%1) and all segment times identical段数一致(%1),且每段时间完全一致 + Oil + Gas油 + 气 + Oil + Water油 + 水 + Gas + Water气 + 水 + Oil + Gas + Water油 + 气 + 水 + None + Other (%1)其他(%1) + === Current flow-segment multiphase data ====== 当前流动段分析多相数据 === + Current fitting primary well code: %1当前流动段分析主井 Code:%1 + Snapshot belongs to edited well: %1该快照是否属于正在编辑的井:%1 + This snapshot belongs to the current fitting primary well and does not override the edited other well.该快照属于当前流动段分析主井,不会覆盖正在编辑的其他井。 + Snapshot available: %1快照可获取:%1 + PVT phase type: %1PVT 相态:%1 + Segment multiphase flag: %1流动段多相标志:%1 + Flow curve name: %1流量曲线名称:%1 + Matrix vector count: %1矩阵向量数:%1 + Vector sizes: %1各向量长度:%1 + Four-vector shape valid: %1四向量结构有效:%1 + Data rows (time, oil, gas, water):数据行(时间、油、气、水): + ... %1 additional rows omitted ... 另有 %1 行未显示 nmReservoirParameterPage diff --git a/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h b/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h index 3a19c92..4f2b7a2 100644 --- a/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h +++ b/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h @@ -44,7 +44,10 @@ struct nmPebiWellInputSnapshot QString m_sWellCode; ///< 项目内稳定井编码。 QString m_sWellName; ///< 捕获时井名称。 QString m_sWellInstanceId; ///< 完整场结果使用的稳定井 UUID。 - QVector m_vecFlowPoints; ///< 求解使用的流量制度。 + QVector m_vecFlowDurations; ///< 去除框架占位行后的公共流动段时长。 + QVector m_vecOilRates; ///< 与公共流动段逐项对应的油相流量。 + QVector m_vecGasRates; ///< 与公共流动段逐项对应的气相流量。 + QVector m_vecWaterRates; ///< 与公共流动段逐项对应的水相流量。 NM_WELL_CATEGORY m_eWellCategory; ///< 求解时冻结的油井、气井或水井井别。 /** @brief 求解时冻结的历史压力曲线。 */ QVector > m_vecHistoryPressure; @@ -77,6 +80,7 @@ struct nmPebiGridInputSnapshot nmPebiGridInputSnapshot() : m_nGridInputRevision(0), m_bCaptured(false), + m_bRequireSolverInput(false), m_bValid(false) { } @@ -88,6 +92,7 @@ struct nmPebiGridInputSnapshot QString m_sLicensePath; ///< 网格 DLL 授权文件路径副本。 quint64 m_nGridInputRevision; ///< 捕获时的几何输入版本。 bool m_bCaptured; ///< DataManager 值是否已在所属线程完整捕获。 + bool m_bRequireSolverInput; ///< 是否还需组装产量、PVT 等求解输入。 bool m_bValid; ///< 后台场景准备是否完成,可否提交给网格 DLL。 }; @@ -119,10 +124,11 @@ public: nmCalculationPebiGrid(); ~nmCalculationPebiGrid(); - /** @brief 在调用线程从 DataManager 复制一份完整且不含对象指针的网格输入。 */ + /** @brief 在调用线程从 DataManager 复制一份完整且不含对象指针的输入快照。 */ bool captureInputSnapshot(nmDataAnalyzeManager* pDataManager, nmPebiGridInputSnapshot& oSnapshot, - bool bDeferPreparation = false); + bool bDeferPreparation = false, + bool bRequireSolverInput = true); /** @brief 初始化手工求解的分批网格快照,只在 DataManager 所属线程调用。 */ bool beginManualInputSnapshot(nmDataAnalyzeManager* pDataManager, nmPebiGridInputSnapshot& oSnapshot); diff --git a/Include/nmNum/nmData/nmDataAnalyzeContextProvider.h b/Include/nmNum/nmData/nmDataAnalyzeContextProvider.h index 28de168..19c0ce1 100644 --- a/Include/nmNum/nmData/nmDataAnalyzeContextProvider.h +++ b/Include/nmNum/nmData/nmDataAnalyzeContextProvider.h @@ -18,6 +18,20 @@ class NM_DATA_EXPORT nmDataAnalyzeContextProvider { // 获取当前分析窗口选中的流动段索引 virtual bool getCurrentSegmentIndex(void* pFitting, int& nIndexF) = 0; + // 获取当前流动段分析实际使用的压力、流量 Gauge Code。 + virtual bool getCurrentSegmentGaugeCodes( + void* pFitting, + QString& sPressureCurveName, + QString& sFlowCurveName) = 0; + + // 获取当前流动段分析保存的多相流量矩阵。 + // 数据按纵向向量组织:[0]时间、[1]油、[2]气、[3]水;这里只复制窗口快照,不修改业务数据。 + virtual bool getCurrentSegmentMultiPhaseData( + void* pFitting, + bool& bMultiPhase, + QString& sFlowCurveName, + VVecDouble& vvecMultiPhaseData) = 0; + // 获取基础数据中的PVT相态类型 virtual bool getBasicPft(void* pFitting, PvtFluidType& eType) = 0; diff --git a/Include/nmNum/nmData/nmDataAnalyzeManager.h b/Include/nmNum/nmData/nmDataAnalyzeManager.h index 57409d1..85e3a9a 100644 --- a/Include/nmNum/nmData/nmDataAnalyzeManager.h +++ b/Include/nmNum/nmData/nmDataAnalyzeManager.h @@ -123,6 +123,24 @@ enum NM_SOLVER_MODEL_TYPE { SMT_Oil_Gas_Water_ThreePhase = 10 }; +/** @brief 当前 PEBI 接入层已经开放的求解模型范围。 */ +inline bool nmIsSupportedPebiSolverModel( + NM_SOLVER_MODEL_TYPE eSolverModelType) +{ + switch(eSolverModelType) { + case SMT_Oil_ConstPvt: + case SMT_Oil_VariablePvt: + case SMT_Water_ConstPvt: + case SMT_Water_VariablePvt: + case SMT_Gas_VariablePvt: + case SMT_Gas_PseudoPressure: + case SMT_Oil_Water_TwoPhase: + return true; + default: + return false; + } +} + /** @brief 判断求解模型是否包含指定工程井别对应的流量相。 */ inline bool nmSolverModelSupportsWellCategory( NM_SOLVER_MODEL_TYPE eSolverModelType, @@ -243,13 +261,16 @@ public: NM_WELL_CATEGORY& eWellCategory); /** - * @brief 将枚举到的记录列表写入数值井,并按默认规则选中压力/流量记录。 + * @brief 将枚举到的记录列表写入数值井,并优先恢复指定的压力、流量记录。 * @note 找不到任何可用记录时仍会写入列表,只是选中 Code 为空,不阻止建井。 */ static void applyGaugeRecordsToWell( nmDataWellBase* pWellData, const QVector& vecPressureRecords, - const QVector& vecFlowRecords); + const QVector& vecFlowRecords, + const QString& sPreferredPressureGaugeCode = QString(), + const QString& sPreferredFlowGaugeCode = QString(), + int nPreferredFlowIndex = 0); // 井的接口 // 添加井 diff --git a/Include/nmNum/nmData/nmDataGaugeRecord.h b/Include/nmNum/nmData/nmDataGaugeRecord.h index 798869a..c9d46ad 100644 --- a/Include/nmNum/nmData/nmDataGaugeRecord.h +++ b/Include/nmNum/nmData/nmDataGaugeRecord.h @@ -26,8 +26,8 @@ struct nmPressureGaugeRecord { /** * @brief 工程井下一条流量测量记录的只读快照。 - * @note 单相记录只解析主块并按井别归入对应相,忽略可能残留的 Ex2/Ex3; - * 多相记录分别解析主块、Ex2、Ex3 为油、气、水三相。 + * @note 单相记录解析 GaugeData 并按井别归入对应相;多相记录从 + * GaugeDataEx2 的 N 行数据中按 [段时长、油、气、水] 解析全部相。 */ struct nmFlowGaugeRecord { QString sGaugeCode; // 对应 ZxDataGaugeF::getCode(),选择持久化键 diff --git a/Include/nmNum/nmSubWnd/nmSubWndDataAnalyzeController.h b/Include/nmNum/nmSubWnd/nmSubWndDataAnalyzeController.h index 1b26f1f..15f1ce1 100644 --- a/Include/nmNum/nmSubWnd/nmSubWndDataAnalyzeController.h +++ b/Include/nmNum/nmSubWnd/nmSubWndDataAnalyzeController.h @@ -13,6 +13,19 @@ class NM_SUB_WND_EXPORT nmSubWndDataAnalyzeController : public nmDataAnalyzeCont // 获取当前分析窗口选中的流动段索引 bool getCurrentSegmentIndex(void* pFitting, int& nIndexF); + // 获取当前流动段分析实际使用的压力、流量 Gauge Code + bool getCurrentSegmentGaugeCodes( + void* pFitting, + QString& sPressureCurveName, + QString& sFlowCurveName); + + // 获取当前流动段分析中的多相流量矩阵 + bool getCurrentSegmentMultiPhaseData( + void* pFitting, + bool& bMultiPhase, + QString& sFlowCurveName, + VVecDouble& vvecMultiPhaseData); + // 获取基础数据中的PVT相态类型 bool getBasicPft(void* pFitting, PvtFluidType& eType); diff --git a/Include/nmNum/nmSubWxs/nmWellPressureFlowPage.h b/Include/nmNum/nmSubWxs/nmWellPressureFlowPage.h index f87fa2a..6bb9304 100644 --- a/Include/nmNum/nmSubWxs/nmWellPressureFlowPage.h +++ b/Include/nmNum/nmSubWxs/nmWellPressureFlowPage.h @@ -14,6 +14,7 @@ class iUnitItem; class nmDataWellBase; class nmWxPressFlowChartWidget; class QComboBox; +class QPushButton; class QSplitter; class QStackedWidget; class QTableWidget; @@ -67,6 +68,9 @@ private slots: /** @brief 切换显示相下拉框,仅重新按新相取点刷新视图,不改变井数据。 */ void slotDisplayPhaseChanged(int nIndex); + /** @brief 临时诊断入口:弹出只读窗口展示当前井全部压力/流量记录摘要,不修改业务数据。 */ + void slotCheckDataClicked(); + private: /** @brief 创建左右分栏、上下图表和只读流量表格。 */ void createUi(); @@ -136,6 +140,9 @@ private: /** @brief 清空数据副本和视图,但保留当前单位及井绑定。 */ void resetDataViews(); + /** @brief 即时从井对象重新读取全部记录并生成只读诊断文本,约15位有效数字。 */ + QString buildDiagnosticText() const; + nmDataWellBase* m_pWell; ///< 当前工作副本,不拥有。 QStackedWidget* m_pContentStack; QSplitter* m_pHorizontalSplitter; @@ -165,6 +172,7 @@ private: QComboBox* m_pFlowGaugeCombo; ///< 流量记录下拉框,itemData 存 Gauge Code。 QComboBox* m_pDisplayPhaseCombo; ///< 显示相下拉框,itemData 存 NM_PHASE_TYPE。 QWidget* m_pDisplayPhaseContainer; ///< 承载显示相下拉框与标签,整体隐藏用。 + QPushButton* m_pCheckDataButton; ///< 临时“检查数据”诊断按钮,只读展示用。 NM_PHASE_TYPE m_eDisplayPhase; ///< 当前预览的流量相,纯展示状态,不写回井对象。 QVector m_vecFlowGaugeRecords; ///< 每次填充流量下拉框时缓存的记录列表,供 currentFlowRecord() 安全返回内部指针。 }; diff --git a/Include/nmNum/nmSubWxs/nmWxIncludeOtherWells.h b/Include/nmNum/nmSubWxs/nmWxIncludeOtherWells.h index 0efc62f..c064b08 100644 --- a/Include/nmNum/nmSubWxs/nmWxIncludeOtherWells.h +++ b/Include/nmNum/nmSubWxs/nmWxIncludeOtherWells.h @@ -23,7 +23,7 @@ struct WellDataRow { bool m_bIsIncluded; ///< 是否被用户勾选为其他主动井。 bool m_bIsPrimary; ///< 主井防御标记;主井正常情况下不会进入本表。 bool m_bCanCalculate; ///< 当前井型和几何参数是否可进入 PEBI 模型。 - NM_WELL_CATEGORY m_eWellCategory; ///< 工程井井别,决定哪一列显示流量。 + NM_WELL_CATEGORY m_eWellCategory; ///< 工程井井别,用于单相记录归相及求解模型匹配。 NM_CASE_WELL_MODE m_eMode; ///< 本列表固定保存有产量主动井角色。 QString m_sOilProd; ///< 油产量列的摘要文本。 QString m_sGasProd; ///< 气产量列的摘要文本。 diff --git a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp index 0bd5288..4b6b5c8 100644 --- a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp @@ -176,6 +176,23 @@ bool isFiniteSolverNumber(double value) #endif } +// 旧的双对数曲线 DLL 仍是单相接口,只在结果后处理时按井别取参考相。 +// PEBI 主求解已经在场景中接收油、气、水三相全部流量。 +const QVector* referenceRatesOf( + const nmPebiWellInputSnapshot& oWellInput) +{ + switch(oWellInput.m_eWellCategory) { + case NM_WellCategory_Oil: + return &oWellInput.m_vecOilRates; + case NM_WellCategory_Gas: + return &oWellInput.m_vecGasRates; + case NM_WellCategory_Water: + return &oWellInput.m_vecWaterRates; + default: + return nullptr; + } +} + bool isDisplayResultWell( const nmDataNumericalAnalysisCase* pAnalysisCase, const QString& sWellCode) @@ -215,7 +232,7 @@ bool captureResultWellMetadata( // 无产量井仍作为观察井保留在求解槽位和快照元数据中,但不进入结果下拉框。 oWellInput.m_bDisplayResultWell = bDisplayResultWell && oWellInput.m_bRateControlled && - !oWellInput.m_vecFlowPoints.isEmpty(); + !oWellInput.m_vecFlowDurations.isEmpty(); nmDataVerticalFracturedWell* pVerticalFracturedWell = dynamic_cast(pWellData); @@ -1512,12 +1529,17 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( return false; } + const QVector* pReferenceRates = + referenceRatesOf(oWellInput); const bool bRateControlled = oWellInput.m_bRateControlled && - !oWellInput.m_vecFlowPoints.isEmpty() && + pReferenceRates != nullptr && + pReferenceRates->size() == + oWellInput.m_vecFlowDurations.size() && + !oWellInput.m_vecFlowDurations.isEmpty() && oWellInput.m_nFlowSectionIndex >= 1 && oWellInput.m_nFlowSectionIndex <= - oWellInput.m_vecFlowPoints.size(); + oWellInput.m_vecFlowDurations.size(); vvecLogLog.clear(); vvecSemiLog.clear(); @@ -1552,16 +1574,15 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( } // 准备流量段数据 - const QVector& vecTimeQ = - oWellInput.m_vecFlowPoints; // 输入快照已经移除可选 (0,0) 占位点,此处逐段传给曲线 DLL。 - const int nTimeNumQ = vecTimeQ.size(); + const int nTimeNumQ = + oWellInput.m_vecFlowDurations.size(); std::vector timeQ(nTimeNumQ); std::vector q(nTimeNumQ); for(int i = 0; i < nTimeNumQ; ++i) { - timeQ[i] = vecTimeQ[i].x(); - q[i] = vecTimeQ[i].y(); + timeQ[i] = oWellInput.m_vecFlowDurations[i]; + q[i] = (*pReferenceRates)[i]; } // 调用外部 DLL 计算双对数曲线 diff --git a/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp b/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp index added2b..7f2d963 100644 --- a/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp @@ -206,6 +206,73 @@ void fillScenePseudoPressureTable(nmDataBinaryTools::NM_PEBI_SCENE& scene, } } +// 把当前所选流量记录整理成求解器的公共段时长和三相数组。 +// 单相记录缺少的相只补同轴零值;多相记录的三相数值不做业务修正。 +bool captureWellFlowSchedule( + nmDataWellBase* pWellData, + nmPebiWellInputSnapshot& oWellInput) +{ + oWellInput.m_vecFlowDurations.clear(); + oWellInput.m_vecOilRates.clear(); + oWellInput.m_vecGasRates.clear(); + oWellInput.m_vecWaterRates.clear(); + if(pWellData == nullptr) { + return false; + } + + const QVector vecOilPoints = + pWellData->getFlowSegmentPoints(PHASE_Oil); + const QVector vecGasPoints = + pWellData->getFlowSegmentPoints(PHASE_Gas); + const QVector vecWaterPoints = + pWellData->getFlowSegmentPoints(PHASE_Water); + + const QVector* pTimePoints = nullptr; + switch(pWellData->getReferenceFlowPhase()) { + case PHASE_Oil: + pTimePoints = &vecOilPoints; + break; + case PHASE_Gas: + pTimePoints = &vecGasPoints; + break; + case PHASE_Water: + pTimePoints = &vecWaterPoints; + break; + default: + break; + } + if(pTimePoints == nullptr || pTimePoints->isEmpty()) { + pTimePoints = !vecOilPoints.isEmpty() ? &vecOilPoints + : (!vecGasPoints.isEmpty() ? &vecGasPoints + : (!vecWaterPoints.isEmpty() ? &vecWaterPoints : nullptr)); + } + if(pTimePoints == nullptr || pTimePoints->isEmpty()) { + return false; + } + + const int nSegmentCount = pTimePoints->size(); + if((!vecOilPoints.isEmpty() && vecOilPoints.size() != nSegmentCount) || + (!vecGasPoints.isEmpty() && vecGasPoints.size() != nSegmentCount) || + (!vecWaterPoints.isEmpty() && vecWaterPoints.size() != nSegmentCount)) { + return false; + } + + oWellInput.m_vecFlowDurations.reserve(nSegmentCount); + oWellInput.m_vecOilRates.reserve(nSegmentCount); + oWellInput.m_vecGasRates.reserve(nSegmentCount); + oWellInput.m_vecWaterRates.reserve(nSegmentCount); + for(int nIndex = 0; nIndex < nSegmentCount; ++nIndex) { + oWellInput.m_vecFlowDurations.append((*pTimePoints)[nIndex].x()); + oWellInput.m_vecOilRates.append( + vecOilPoints.isEmpty() ? 0.0 : vecOilPoints[nIndex].y()); + oWellInput.m_vecGasRates.append( + vecGasPoints.isEmpty() ? 0.0 : vecGasPoints[nIndex].y()); + oWellInput.m_vecWaterRates.append( + vecWaterPoints.isEmpty() ? 0.0 : vecWaterPoints[nIndex].y()); + } + return true; +} + } #ifdef Q_OS_WIN @@ -999,6 +1066,8 @@ bool nmCalculationPebiGrid::beginManualInputSnapshot( return false; } + oSnapshot.m_bRequireSolverInput = true; + const nmDataNumericalAnalysisCase* pAnalysisCase = pDataManager->getNumericalAnalysisCase(); if(pAnalysisCase == nullptr) { @@ -1104,7 +1173,6 @@ bool nmCalculationPebiGrid::appendManualWellInputSnapshot( oWellInput.m_sWellCode = pWellData->getWellCode(); oWellInput.m_sWellName = pWellData->getWellName(); oWellInput.m_eWellCategory = pWellData->getWellCategory(); - oWellInput.m_vecFlowPoints = pWellData->getFlowSegmentPoints(); oWellInput.m_nFlowSectionIndex = pWellData->getIndexF(); oWellInput.m_oLocation = QPointF( pWellData->getX().getValue().toDouble(), @@ -1118,8 +1186,10 @@ bool nmCalculationPebiGrid::appendManualWellInputSnapshot( nWellMode == static_cast(NM_CaseWell_RateControlled); if(!nmIsValidWellCategory(oWellInput.m_eWellCategory) || (oWellInput.m_bRateControlled && - (oWellInput.m_vecFlowPoints.isEmpty() || - !pWellData->hasValidFlowSectionIndex()))) { + (!captureWellFlowSchedule(pWellData, oWellInput) || + oWellInput.m_nFlowSectionIndex < 1 || + oWellInput.m_nFlowSectionIndex > + oWellInput.m_vecFlowDurations.size()))) { return false; } @@ -1257,7 +1327,8 @@ bool nmCalculationPebiGrid::finishManualInputSnapshot( bool nmCalculationPebiGrid::captureInputSnapshot( nmDataAnalyzeManager* pDataManager, nmPebiGridInputSnapshot& oSnapshot, - bool bDeferPreparation) + bool bDeferPreparation, + bool bRequireSolverInput) { // 使用具名常量触发复制赋值,兼容 Qt 4.8 配套的 VS2010 运行库 ABI。 const nmPebiGridInputSnapshot oEmptySnapshot; @@ -1265,6 +1336,7 @@ bool nmCalculationPebiGrid::captureInputSnapshot( if(pDataManager == nullptr) { return false; } + oSnapshot.m_bRequireSolverInput = bRequireSolverInput; // DataManager 及井对象都由所属线程维护。快照只允许在该线程创建,后台任务 // 随后只读取复制出的 STL/Qt 值类型,不能再次访问这些可变对象。 @@ -1344,6 +1416,15 @@ bool nmCalculationPebiGrid::captureInputSnapshot( return false; } + // 独立建网到此已经取得 DLL 实际消费的全部几何输入和井槽位顺序。 + // 不再读取流量、流动段、PVT、储层或时间步,避免建网依赖求解数据状态。 + if(!oSnapshot.m_bRequireSolverInput) { + oSnapshot.m_sLicensePath = pDataManager->getLicensePath(); + oSnapshot.m_bCaptured = true; + return bDeferPreparation + ? true : prepareInputSnapshot(oSnapshot); + } + // 第五步:建立 WellCode 索引并一次复制每口井,避免五十口井时反复线性查找。 QHash mapWellsByCode; const QVector vecAllWells = @@ -1382,7 +1463,6 @@ bool nmCalculationPebiGrid::captureInputSnapshot( oWellInput.m_bRealWell = true; oWellInput.m_sWellName = pWellData->getWellName(); oWellInput.m_eWellCategory = pWellData->getWellCategory(); - oWellInput.m_vecFlowPoints = pWellData->getFlowSegmentPoints(); oWellInput.m_nFlowSectionIndex = pWellData->getIndexF(); oWellInput.m_oLocation = QPointF( pWellData->getX().getValue().toDouble(), @@ -1396,14 +1476,18 @@ bool nmCalculationPebiGrid::captureInputSnapshot( oWellInput.m_bRateControlled = mapWellModes.value(oWellRef.m_sWellCode) == static_cast(NM_CaseWell_RateControlled); - if(!nmIsValidWellCategory(oWellInput.m_eWellCategory)) { + if(oSnapshot.m_bRequireSolverInput && + !nmIsValidWellCategory(oWellInput.m_eWellCategory)) { qWarning() << "Solver well has an invalid category:" << oWellRef.m_sWellCode; return false; } - if(oWellInput.m_bRateControlled && - (oWellInput.m_vecFlowPoints.isEmpty() || - !pWellData->hasValidFlowSectionIndex())) { + if(oSnapshot.m_bRequireSolverInput && + oWellInput.m_bRateControlled && + (!captureWellFlowSchedule(pWellData, oWellInput) || + oWellInput.m_nFlowSectionIndex < 1 || + oWellInput.m_nFlowSectionIndex > + oWellInput.m_vecFlowDurations.size())) { qWarning() << "Rate-controlled well has no valid rate schedule:" << oWellRef.m_sWellCode; return false; @@ -1483,12 +1567,21 @@ bool nmCalculationPebiGrid::prepareInputSnapshot( { oSnapshot.m_bValid = false; if(!oSnapshot.m_bCaptured || - oSnapshot.m_vecWellInputs.size() != - oSnapshot.m_vecSolverWellOrder.size() || isCancellationRequested(pCancelRequested)) { return false; } + // 独立建网只消费几何输入,不因流量记录或求解参数不可用而失败。 + if(!oSnapshot.m_bRequireSolverInput) { + oSnapshot.m_bValid = true; + return true; + } + + if(oSnapshot.m_vecWellInputs.size() != + oSnapshot.m_vecSolverWellOrder.size()) { + return false; + } + return buildPebiScene(oSnapshot, pCancelRequested); } @@ -1529,9 +1622,14 @@ bool nmCalculationPebiGrid::buildPebiScene( oScene.wellName.push_back(vecWellInputs[nIndex].m_sWellName); } - // 第二步:按每口井冻结的井别填充产量制度;求解模型只负责相容性校验。 + // 第二步:把冻结的公共时间轴及油、气、水流量原样写入求解器输入。 const NM_SOLVER_MODEL_TYPE eSolverModelType = static_cast(oScene.solverType); + if(!nmIsSupportedPebiSolverModel(eSolverModelType)) { + qWarning() << "PEBI solver model is not supported:" + << static_cast(eSolverModelType); + return false; + } oScene.Rate.t.resize(vecSolverWellOrder.size()); oScene.Rate.qo.resize(vecSolverWellOrder.size()); oScene.Rate.qg.resize(vecSolverWellOrder.size()); @@ -1555,45 +1653,48 @@ bool nmCalculationPebiGrid::buildPebiScene( continue; } - if(!nmIsValidWellCategory(oWellInput.m_eWellCategory) || - !nmSolverModelSupportsWellCategory( - eSolverModelType, oWellInput.m_eWellCategory) || - oWellInput.m_vecFlowPoints.isEmpty() || + // 多相记录已经明确提供 qo/qg/qw,井别不再用于裁剪或拦截流量相。 + // 此处只检查求解器数组能否按公共段时长逐项组装。 + if(oWellInput.m_vecFlowDurations.isEmpty() || + oWellInput.m_vecOilRates.size() != + oWellInput.m_vecFlowDurations.size() || + oWellInput.m_vecGasRates.size() != + oWellInput.m_vecFlowDurations.size() || + oWellInput.m_vecWaterRates.size() != + oWellInput.m_vecFlowDurations.size() || oWellInput.m_nFlowSectionIndex < 1 || oWellInput.m_nFlowSectionIndex > - oWellInput.m_vecFlowPoints.size()) { - qWarning() << "Rate-controlled well input is incompatible with solver model:" + oWellInput.m_vecFlowDurations.size()) { + qWarning() << "Rate-controlled well has inconsistent rate schedule arrays:" << oWellInput.m_sWellCode; return false; } std::vector vecTime; - std::vector vecRate; - vecTime.reserve(oWellInput.m_vecFlowPoints.size()); - vecRate.reserve(oWellInput.m_vecFlowPoints.size()); + std::vector vecOil; + std::vector vecGas; + std::vector vecWater; + vecTime.reserve(oWellInput.m_vecFlowDurations.size()); + vecOil.reserve(oWellInput.m_vecFlowDurations.size()); + vecGas.reserve(oWellInput.m_vecFlowDurations.size()); + vecWater.reserve(oWellInput.m_vecFlowDurations.size()); for(int nPointIndex = 0; - nPointIndex < oWellInput.m_vecFlowPoints.size(); + nPointIndex < oWellInput.m_vecFlowDurations.size(); ++nPointIndex) { if((nPointIndex % 256) == 0 && isCancellationRequested(pCancelRequested)) { return false; } - vecTime.push_back(oWellInput.m_vecFlowPoints[nPointIndex].x()); - vecRate.push_back(oWellInput.m_vecFlowPoints[nPointIndex].y()); + vecTime.push_back(oWellInput.m_vecFlowDurations[nPointIndex]); + vecOil.push_back(oWellInput.m_vecOilRates[nPointIndex]); + vecGas.push_back(oWellInput.m_vecGasRates[nPointIndex]); + vecWater.push_back(oWellInput.m_vecWaterRates[nPointIndex]); } oScene.Rate.t[nWellIndex] = vecTime; - oScene.Rate.qo[nWellIndex].assign(vecRate.size(), 0.0); - oScene.Rate.qg[nWellIndex].assign(vecRate.size(), 0.0); - oScene.Rate.qw[nWellIndex].assign(vecRate.size(), 0.0); - - if(oWellInput.m_eWellCategory == NM_WellCategory_Gas) { - oScene.Rate.qg[nWellIndex] = vecRate; - } else if(oWellInput.m_eWellCategory == NM_WellCategory_Water) { - oScene.Rate.qw[nWellIndex] = vecRate; - } else { - oScene.Rate.qo[nWellIndex] = vecRate; - } + oScene.Rate.qo[nWellIndex] = vecOil; + oScene.Rate.qg[nWellIndex] = vecGas; + oScene.Rate.qw[nWellIndex] = vecWater; } // 第三步:复制井筒储集、表皮和流量段索引。 diff --git a/Src/nmNum/nmCalculation/nmCalculationPebiGridTask.cpp b/Src/nmNum/nmCalculation/nmCalculationPebiGridTask.cpp index b5697fb..a6b1d9b 100644 --- a/Src/nmNum/nmCalculation/nmCalculationPebiGridTask.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationPebiGridTask.cpp @@ -15,11 +15,12 @@ nmCalculationPebiGridTask::nmCalculationPebiGridTask( m_pDataManager->beginBackgroundUse(); m_bManagerUseActive = true; - // 第一步:构造函数运行在主线程,此处一次性复制全部 Map 和求解输入。 + // 第一步:构造函数运行在主线程,此处只复制建网所需的 Map 几何输入。 + // 流量、PVT 等求解制度不可用时不能阻止独立建网。 // run() 启动后不得再读取 DataManager 中的井、断层或参数对象。 m_bSnapshotValid = nmCalculationPebiGrid::getInstance()->captureInputSnapshot( - m_pDataManager, m_oInputSnapshot); + m_pDataManager, m_oInputSnapshot, false, false); } } diff --git a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp index 02a61a5..798d8c5 100644 --- a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp +++ b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp @@ -192,27 +192,137 @@ static NM_GAUGE_RECORD_STATUS readBlockAndClassify( ? NM_GaugeRecord_Usable : NM_GaugeRecord_Corrupt; } -// 多相流量记录:主块=油、Ex2=气、Ex3=水;任一非空块损坏则整条记录损坏,三块全空才是空记录。 +// 框架的 GaugeDataEx2 保存完整多相表,格式为 N 行 x 4 列; +// 每行依次为 [段时长、油、气、水]。数值模块读取时拆成三组同轴流量点, +// 不根据井别或非零值删改任一相的数据。 +static bool readMultiPhaseFlowTable( + const QByteArray& baGaugeData, + QVector& vecOilPoints, + QVector& vecGasPoints, + QVector& vecWaterPoints) +{ + vecOilPoints.clear(); + vecGasPoints.clear(); + vecWaterPoints.clear(); + if(baGaugeData.isEmpty()) { + return true; + } + + QByteArray baData = baGaugeData; + VVecVariant vvecRows; + if(!ZxBaHelper::convertBa2VVec(vvecRows, baData) || + vvecRows.isEmpty()) { + return false; + } + + const int nPointCount = vvecRows.size(); + for(int nPointIndex = 0; + nPointIndex < nPointCount; + ++nPointIndex) { + if(vvecRows[nPointIndex].size() != 4) { + return false; + } + } + + vecOilPoints.reserve(nPointCount); + vecGasPoints.reserve(nPointCount); + vecWaterPoints.reserve(nPointCount); + for(int nPointIndex = 0; + nPointIndex < nPointCount; + ++nPointIndex) { + bool bTimeOk = false; + bool bOilOk = false; + bool bGasOk = false; + bool bWaterOk = false; + const QVector& vecRow = vvecRows[nPointIndex]; + const double dTime = vecRow[0].toDouble(&bTimeOk); + const double dOil = vecRow[1].toDouble(&bOilOk); + const double dGas = vecRow[2].toDouble(&bGasOk); + const double dWater = vecRow[3].toDouble(&bWaterOk); + if(!bTimeOk || !bOilOk || !bGasOk || !bWaterOk) { + vecOilPoints.clear(); + vecGasPoints.clear(); + vecWaterPoints.clear(); + return false; + } + + vecOilPoints.append(QPointF(dTime, dOil)); + vecGasPoints.append(QPointF(dTime, dGas)); + vecWaterPoints.append(QPointF(dTime, dWater)); + } + return true; +} + +// 多相流量记录只解析 GaugeDataEx2;GaugeDataEx3 不是水相数据来源。 static NM_GAUGE_RECORD_STATUS classifyMultiPhaseFlowRecord( ZxDataGaugeF* pGaugeF, QVector& vecOilPoints, QVector& vecGasPoints, QVector& vecWaterPoints) { - const QByteArray baOil = pGaugeF->getGaugeData(); - const QByteArray baGas = pGaugeF->getGaugeDataEx2(); - const QByteArray baWater = pGaugeF->getGaugeDataEx3(); + const QByteArray baMultiPhase = pGaugeF->getGaugeDataEx2(); + if(baMultiPhase.isEmpty()) { + return NM_GaugeRecord_Empty; + } + return readMultiPhaseFlowTable( + baMultiPhase, + vecOilPoints, + vecGasPoints, + vecWaterPoints) + ? NM_GaugeRecord_Usable : NM_GaugeRecord_Corrupt; +} - const bool bOilOk = readFlowGaugeBlock(baOil, vecOilPoints); - const bool bGasOk = readFlowGaugeBlock(baGas, vecGasPoints); - const bool bWaterOk = readFlowGaugeBlock(baWater, vecWaterPoints); - if(!bOilOk || !bGasOk || !bWaterOk) { - return NM_GaugeRecord_Corrupt; +// 当前分析窗口的多相工作副本按列保存:[0]时间、[1]油、[2]气、[3]水。 +// 它只覆盖同一 Flow Gauge Code 的主井记录,不能套用到包含井。 +static bool applyCurrentMultiPhaseFlowData( + QVector& vecRecords, + const QString& sFlowGaugeCode, + const VVecDouble& vvecMultiPhaseData, + int nIndexF) +{ + if(sFlowGaugeCode.isEmpty() || vvecMultiPhaseData.size() != 4) { + return false; } - if(baOil.isEmpty() && baGas.isEmpty() && baWater.isEmpty()) { - return NM_GaugeRecord_Empty; + + const int nPointCount = vvecMultiPhaseData[0].size(); + if(nPointCount <= 0 || + vvecMultiPhaseData[1].size() != nPointCount || + vvecMultiPhaseData[2].size() != nPointCount || + vvecMultiPhaseData[3].size() != nPointCount) { + return false; + } + + for(int nRecordIndex = 0; + nRecordIndex < vecRecords.size(); + ++nRecordIndex) { + nmFlowGaugeRecord& oRecord = vecRecords[nRecordIndex]; + if(oRecord.sGaugeCode != sFlowGaugeCode) { + continue; + } + + oRecord.vecOilPoints.clear(); + oRecord.vecGasPoints.clear(); + oRecord.vecWaterPoints.clear(); + oRecord.vecOilPoints.reserve(nPointCount); + oRecord.vecGasPoints.reserve(nPointCount); + oRecord.vecWaterPoints.reserve(nPointCount); + for(int nPointIndex = 0; + nPointIndex < nPointCount; + ++nPointIndex) { + const double dTime = vvecMultiPhaseData[0][nPointIndex]; + oRecord.vecOilPoints.append(QPointF( + dTime, vvecMultiPhaseData[1][nPointIndex])); + oRecord.vecGasPoints.append(QPointF( + dTime, vvecMultiPhaseData[2][nPointIndex])); + oRecord.vecWaterPoints.append(QPointF( + dTime, vvecMultiPhaseData[3][nPointIndex])); + } + oRecord.bMultiPhase = true; + oRecord.eStatus = NM_GaugeRecord_Usable; + oRecord.nIndexF = nIndexF; + return true; } - return NM_GaugeRecord_Usable; + return false; } // 单相流量记录:只解析主块,并按井别把数据归入油、气或水相;忽略可能残留的 Ex2/Ex3。 @@ -355,22 +465,55 @@ static QString selectDefaultFlowGaugeCode( return QString(); } -// 把枚举到的记录列表写入数值井,并按默认规则选中压力/流量记录。 +// 把枚举到的记录列表写入数值井;只恢复仍然存在且可用的首选记录。 // 找不到任何可用记录时仍会写入(可能为空的)列表,只是选中 Code 为空——不阻止建井。 void nmDataAnalyzeManager::applyGaugeRecordsToWell( nmDataWellBase* pWellData, const QVector& vecPressureRecords, - const QVector& vecFlowRecords) + const QVector& vecFlowRecords, + const QString& sPreferredPressureGaugeCode, + const QString& sPreferredFlowGaugeCode, + int nPreferredFlowIndex) { if(pWellData == nullptr) { return; } pWellData->setPressureRecords(vecPressureRecords); pWellData->setFlowRecords(vecFlowRecords); - pWellData->selectPressureGaugeCode( - selectDefaultPressureGaugeCode(vecPressureRecords)); - pWellData->selectFlowGaugeCode( - selectDefaultFlowGaugeCode(vecFlowRecords)); + + QString sSelectedPressureGaugeCode = + selectDefaultPressureGaugeCode(vecPressureRecords); + for(int nIndex = 0; nIndex < vecPressureRecords.size(); ++nIndex) { + if(vecPressureRecords[nIndex].sGaugeCode == + sPreferredPressureGaugeCode && + vecPressureRecords[nIndex].eStatus == NM_GaugeRecord_Usable) { + sSelectedPressureGaugeCode = sPreferredPressureGaugeCode; + break; + } + } + pWellData->selectPressureGaugeCode(sSelectedPressureGaugeCode); + + QString sSelectedFlowGaugeCode = + selectDefaultFlowGaugeCode(vecFlowRecords); + for(int nIndex = 0; nIndex < vecFlowRecords.size(); ++nIndex) { + if(vecFlowRecords[nIndex].sGaugeCode == sPreferredFlowGaugeCode && + vecFlowRecords[nIndex].eStatus == NM_GaugeRecord_Usable) { + sSelectedFlowGaugeCode = sPreferredFlowGaugeCode; + break; + } + } + pWellData->selectFlowGaugeCode(sSelectedFlowGaugeCode); + + // 每条流量记录可以有自己的段数。首选索引不适用于回退记录, + // 因此回退时统一选择该记录的最后一个真实流动段。 + const int nFlowSegmentCount = pWellData->getFlowSegmentCount(); + int nSelectedFlowIndex = sSelectedFlowGaugeCode == + sPreferredFlowGaugeCode ? nPreferredFlowIndex : 0; + if(nSelectedFlowIndex < 1 || + nSelectedFlowIndex > nFlowSegmentCount) { + nSelectedFlowIndex = nFlowSegmentCount; + } + pWellData->setIndexF(nSelectedFlowIndex); } static bool isWellParaAttrName(const QString& name) @@ -1612,6 +1755,48 @@ void nmDataAnalyzeManager::initCurWellData() return; } + // 主井必须跟随当前流动段分析实际使用的压力、流量 Gauge Code, + // 而不是默认选择工程树中的第一条记录。多相时再覆盖同 Code 工作矩阵。 + QString sCurrentPressureGaugeCode; + QString sCurrentFlowGaugeCode; + int nCurrentFlowIndex = 0; + bool bCurrentMultiPhase = false; + VVecDouble vvecCurrentMultiPhaseData; + iSubWndFitting* pSubWndFitting = nmDataAnalyzeManager::getCurrentFitting(); + nmDataAnalyzeContextProvider* pContextProvider = + nmDataAnalyzeContext::provider(); + if(pSubWndFitting != nullptr && pContextProvider != nullptr) { + pContextProvider->getCurrentSegmentIndex( + pSubWndFitting, nCurrentFlowIndex); + pContextProvider->getCurrentSegmentGaugeCodes( + pSubWndFitting, + sCurrentPressureGaugeCode, + sCurrentFlowGaugeCode); + if(pContextProvider->getCurrentSegmentMultiPhaseData( + pSubWndFitting, + bCurrentMultiPhase, + sCurrentFlowGaugeCode, + vvecCurrentMultiPhaseData) && + bCurrentMultiPhase) { + applyCurrentMultiPhaseFlowData( + vecFlowRecords, + sCurrentFlowGaugeCode, + vvecCurrentMultiPhaseData, + nCurrentFlowIndex); + } + } + + // 单相记录同样保存当前流动段索引,切换到其他记录再切回时可以恢复。 + for(int nRecordIndex = 0; + nRecordIndex < vecFlowRecords.size(); + ++nRecordIndex) { + if(vecFlowRecords[nRecordIndex].sGaugeCode == + sCurrentFlowGaugeCode) { + vecFlowRecords[nRecordIndex].nIndexF = nCurrentFlowIndex; + break; + } + } + if(ZxBaseUtil::isSameStr(wellClass, "VerticalWell")) { // 初始化直井默认参数 nmDataWellBase* pWell = this->createWell(NM_WELL_MODEL::Vertical_Well); @@ -1634,7 +1819,12 @@ void nmDataAnalyzeManager::initCurWellData() pVerticalWell->setRadius(tempAttr); // 设置井的压力、流量记录列表,并按默认规则选中当前记录 - applyGaugeRecordsToWell(pVerticalWell, vecPressureRecords, vecFlowRecords); + applyGaugeRecordsToWell(pVerticalWell, + vecPressureRecords, + vecFlowRecords, + sCurrentPressureGaugeCode, + sCurrentFlowGaugeCode, + nCurrentFlowIndex); QVector> vvecHistoryPressureData; //压力历史数据 QVector> vvecHistoryLogData; // 历史双对数曲线数据 @@ -1672,7 +1862,12 @@ void nmDataAnalyzeManager::initCurWellData() pVFracturedWell->setRadius(tempAttr); // 设置井的压力、流量记录列表,并按默认规则选中当前记录 - applyGaugeRecordsToWell(pVFracturedWell, vecPressureRecords, vecFlowRecords); + applyGaugeRecordsToWell(pVFracturedWell, + vecPressureRecords, + vecFlowRecords, + sCurrentPressureGaugeCode, + sCurrentFlowGaugeCode, + nCurrentFlowIndex); QVector> vvecHistoryPressureData; //压力历史数据 QVector> vvecHistoryLogData; // 历史双对数曲线数据 @@ -1713,7 +1908,12 @@ void nmDataAnalyzeManager::initCurWellData() pHFracturedWell->setRadius(tempAttr); // 设置井的压力、流量记录列表,并按默认规则选中当前记录 - applyGaugeRecordsToWell(pHFracturedWell, vecPressureRecords, vecFlowRecords); + applyGaugeRecordsToWell(pHFracturedWell, + vecPressureRecords, + vecFlowRecords, + sCurrentPressureGaugeCode, + sCurrentFlowGaugeCode, + nCurrentFlowIndex); QVector> vvecHistoryPressureData; //压力历史数据 QVector> vvecHistoryLogData; // 历史双对数曲线数据 @@ -2748,6 +2948,19 @@ void nmDataAnalyzeManager::initPvtParaFromSubFit() break; } + case WFT_Oil_Gas: + // 数据接收层保留完整相态,提交求解时统一提示当前未开放。 + setSolverModelType(SMT_Oil_Gas_TwoPhase); + break; + + case WFT_Gas_Water: + setSolverModelType(SMT_Gas_Water_TwoPhase); + break; + + case WTF_Oil_Gas_Water: + setSolverModelType(SMT_Oil_Gas_Water_ThreePhase); + break; + default: break; } @@ -6274,7 +6487,7 @@ bool nmDataAnalyzeManager::ensureDirectoryExists(const QString & dirPath) bool nmDataAnalyzeManager::loadWellPreAndFlow() { - // 当前项目文件只保存井别和井身份,压力/流量仍从工程井节点重新读取。 + // 项目文件只保存当前选择的 Gauge Code 和段索引;曲线数据始终从工程井重新读取。 if(zxCurProject == nullptr) { return false; } @@ -6298,6 +6511,13 @@ bool nmDataAnalyzeManager::loadWellPreAndFlow() return false; } + // 在替换记录列表之前保存 JSON 已恢复的选择,避免重新枚举后退回第一条记录。 + const QString sPreferredPressureGaugeCode = + pWell->getSelectedPressureGaugeCode(); + const QString sPreferredFlowGaugeCode = + pWell->getSelectedFlowGaugeCode(); + const int nPreferredFlowIndex = pWell->getIndexF(); + ZxDataWell* pFrameworkWell = mapFrameworkWells.value(pWell->getWellCode(), nullptr); QVector vecPressureRecords; @@ -6313,7 +6533,12 @@ bool nmDataAnalyzeManager::loadWellPreAndFlow() return false; } - applyGaugeRecordsToWell(pWell, vecPressureRecords, vecFlowRecords); + applyGaugeRecordsToWell(pWell, + vecPressureRecords, + vecFlowRecords, + sPreferredPressureGaugeCode, + sPreferredFlowGaugeCode, + nPreferredFlowIndex); } return true; } diff --git a/Src/nmNum/nmData/nmDataWellBase.cpp b/Src/nmNum/nmData/nmDataWellBase.cpp index e7de6f1..c7f1c21 100644 --- a/Src/nmNum/nmData/nmDataWellBase.cpp +++ b/Src/nmNum/nmData/nmDataWellBase.cpp @@ -273,6 +273,19 @@ rapidjson::Value nmDataWellBase::ToJsonValue(rapidjson::Document::AllocatorType& // 序列化井的流动段索引 (m_nIndexF) wellObject.AddMember("IndexFlow", m_nIndexF, allocator); + // 曲线内容由框架保存,这里只持久化当前选择的稳定 Gauge Code。 + const QByteArray baPressureGaugeCode = + m_sSelectedPressureGaugeCode.toUtf8(); + wellObject.AddMember("SelectedPressureGaugeCode", + rapidjson::Value(baPressureGaugeCode.constData(), + static_cast(baPressureGaugeCode.size()), + allocator).Move(), allocator); + const QByteArray baFlowGaugeCode = m_sSelectedFlowGaugeCode.toUtf8(); + wellObject.AddMember("SelectedFlowGaugeCode", + rapidjson::Value(baFlowGaugeCode.constData(), + static_cast(baFlowGaugeCode.size()), + allocator).Move(), allocator); + // 序列化射孔段集合 (现在处理指针) rapidjson::Value perforationsArray(rapidjson::kArrayType); @@ -431,6 +444,18 @@ void nmDataWellBase::FromJsonValue(const rapidjson::Value& jsonValue) m_nIndexF = jsonValue["IndexFlow"].GetInt(); } + // 先恢复选择键;记录数据在 Manager 完成 JSON 读取后从框架重新枚举。 + if(jsonValue.HasMember("SelectedPressureGaugeCode") && + jsonValue["SelectedPressureGaugeCode"].IsString()) { + m_sSelectedPressureGaugeCode = QString::fromUtf8( + jsonValue["SelectedPressureGaugeCode"].GetString()); + } + if(jsonValue.HasMember("SelectedFlowGaugeCode") && + jsonValue["SelectedFlowGaugeCode"].IsString()) { + m_sSelectedFlowGaugeCode = QString::fromUtf8( + jsonValue["SelectedFlowGaugeCode"].GetString()); + } + // 反序列化射孔段集合 (现在处理指针) if(jsonValue.HasMember("Perforations") && jsonValue["Perforations"].IsArray()) { // 清空并释放现有射孔段对象 @@ -888,10 +913,29 @@ QVector nmDataWellBase::getFlowSegmentPoints( NM_PHASE_TYPE eFlowPhase) const { QVector vecSegments = getFlowPoints(eFlowPhase); - // 工程流量数据通常以 (0,0) 作为非业务占位点;真实数据没有占位点时不得误删首段。 - if(!vecSegments.isEmpty() && - vecSegments.first().x() == 0.0 && - vecSegments.first().y() == 0.0) { + const nmFlowGaugeRecord* pRecord = + findGaugeRecordByCode(m_vecFlowRecords, + m_sSelectedFlowGaugeCode); + bool bHasPlaceholder = false; + if(pRecord != nullptr && pRecord->bMultiPhase) { + // 多相数据共用同一时间列,只有完整的 (0,0,0,0) 才是占位行。 + // 不能按单相分别删除,否则某相首段流量为零时会造成三相错位。 + bHasPlaceholder = !pRecord->vecOilPoints.isEmpty() && + !pRecord->vecGasPoints.isEmpty() && + !pRecord->vecWaterPoints.isEmpty() && + pRecord->vecOilPoints.first().x() == 0.0 && + pRecord->vecGasPoints.first().x() == 0.0 && + pRecord->vecWaterPoints.first().x() == 0.0 && + pRecord->vecOilPoints.first().y() == 0.0 && + pRecord->vecGasPoints.first().y() == 0.0 && + pRecord->vecWaterPoints.first().y() == 0.0; + } else { + // 单相记录沿用框架约定,首个 (0,0) 为非业务占位点。 + bHasPlaceholder = !vecSegments.isEmpty() && + vecSegments.first().x() == 0.0 && + vecSegments.first().y() == 0.0; + } + if(bHasPlaceholder && !vecSegments.isEmpty()) { vecSegments.remove(0); } return vecSegments; diff --git a/Src/nmNum/nmSubWnd/nmSubWndDataAnalyzeController.cpp b/Src/nmNum/nmSubWnd/nmSubWndDataAnalyzeController.cpp index 50c7248..4dfa095 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndDataAnalyzeController.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndDataAnalyzeController.cpp @@ -66,6 +66,57 @@ bool nmSubWndDataAnalyzeController::getCurrentSegmentIndex(void* pFitting, int& return true; } +// 复制当前流动段绑定的压力、流量曲线 Code,供数值井恢复实际使用的记录。 +bool nmSubWndDataAnalyzeController::getCurrentSegmentGaugeCodes( + void* pFitting, + QString& sPressureCurveName, + QString& sFlowCurveName) +{ + sPressureCurveName.clear(); + sFlowCurveName.clear(); + + iSubWndFitting* pSubWndFitting = toFitting(pFitting); + if(pSubWndFitting == nullptr) { + return false; + } + + ZxSegmentInfo* pSegInfo = pSubWndFitting->getSegmentInfo(); + if(pSegInfo == nullptr) { + return false; + } + + sPressureCurveName = pSegInfo->m_sCurveNameP; + sFlowCurveName = pSegInfo->m_sCurveNameF; + return true; +} + +// 复制当前流动段分析的多相流量快照,保持 nmData 与具体窗口类之间的模块边界。 +bool nmSubWndDataAnalyzeController::getCurrentSegmentMultiPhaseData( + void* pFitting, + bool& bMultiPhase, + QString& sFlowCurveName, + VVecDouble& vvecMultiPhaseData) +{ + bMultiPhase = false; + sFlowCurveName.clear(); + vvecMultiPhaseData.clear(); + + iSubWndFitting* pSubWndFitting = toFitting(pFitting); + if(pSubWndFitting == nullptr) { + return false; + } + + ZxSegmentInfo* pSegInfo = pSubWndFitting->getSegmentInfo(); + if(pSegInfo == nullptr) { + return false; + } + + bMultiPhase = pSegInfo->m_bMultiPhase; + sFlowCurveName = pSegInfo->m_sCurveNameF; + vvecMultiPhaseData = pSegInfo->m_vvecMpData; + return true; +} + // 获取基础数据中的PVT相态类型 bool nmSubWndDataAnalyzeController::getBasicPft(void* pFitting, PvtFluidType& eType) { diff --git a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp index 6798e82..3603ee5 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp @@ -1583,6 +1583,17 @@ void nmSubWndMain::solveAndAnalyze() } // 第一步:求解时间轴必须由至少一口主动井的产注制度驱动。 + // 当前数值接入只开放油、气、水单相和油水两相;其余相态保留数据, + // 等求解器支持后再开放,不在这里改写或丢弃框架流量。 + if(!nmIsSupportedPebiSolverModel( + pDataManager->getSolverModelType())) { + QMessageBox::warning( + this, + tr("solver error"), + tr("The solver currently supports oil, gas, and water single-phase models and the oil-water two-phase model only.")); + return; + } + // 观察井可以完全不传产量,但不能在所有井都是观察井时启动无源汇计算。 QVector vecEffectiveWells = pDataManager->getEffectiveCalculationWells(); @@ -1621,16 +1632,6 @@ void nmSubWndMain::solveAndAnalyze() .arg(pWellData->getWellName())); return; } - if(!nmSolverModelSupportsWellCategory( - pDataManager->getSolverModelType(), - pWellData->getWellCategory())) { - QMessageBox::warning( - this, - tr("solver error"), - tr("Well '%1' category is not supported by the current solver model.") - .arg(pWellData->getWellName())); - return; - } } if(!bHasRateControlledWell) { QMessageBox::warning( diff --git a/Src/nmNum/nmSubWxs/nmWellPressureFlowPage.cpp b/Src/nmNum/nmSubWxs/nmWellPressureFlowPage.cpp index 50f2a26..b6c15d5 100644 --- a/Src/nmNum/nmSubWxs/nmWellPressureFlowPage.cpp +++ b/Src/nmNum/nmSubWxs/nmWellPressureFlowPage.cpp @@ -3,18 +3,29 @@ #include "iUnitGroup.h" #include "iUnitHelper.h" #include "iUnitItem.h" +#include "nmDataAnalyzeContext.h" +#include "nmDataAnalyzeContextProvider.h" +#include "nmDataAnalyzeManager.h" #include "nmDataWellBase.h" #include "nmWxPressFlowChartWidget.h" #include "ZxBaseUtil.h" +#include "ZxBaHelper.h" +#include "ZxDataGaugeF.h" +#include "ZxDataProject.h" +#include "ZxDataWell.h" #include "ZxTableHeaderViewUnit.h" +#include "zxSysUtils.h" #include #include +#include #include #include #include #include #include +#include +#include #include #include #include @@ -85,21 +96,25 @@ QString displayValueText(double dValue, int nDigit) return ZxBaseUtil::getValidStr(dValue, nDigit); } -// 判断某相流量点是否至少包含一个真实流动段,去除可选首个(0,0)占位点后非空即为真实。 -// 与 nmDataAnalyzeManager.cpp 中同名判断语义一致,但该处是其它文件的 file-static 函数, -// 无法跨文件复用,因此在本文件内新增等价判断。 +// 显示相下拉框只列出实际出现过非零流量的相;多相表中的固定全零列 +// 仍保留在井记录和求解输入中,但不应显示成该井存在对应相产量。 bool hasRealPhaseData(const QVector& vecPoints) { - if (vecPoints.isEmpty()) + int nStartIndex = 0; + if (!vecPoints.isEmpty() && + vecPoints.first().x() == 0.0 && + vecPoints.first().y() == 0.0) { - return false; + nStartIndex = 1; } - if (vecPoints.size() == 1 && - vecPoints.first().x() == 0.0 && vecPoints.first().y() == 0.0) + for (int nIndex = nStartIndex; nIndex < vecPoints.size(); ++nIndex) { - return false; + if (vecPoints[nIndex].y() != 0.0) + { + return true; + } } - return true; + return false; } // 压力/流量记录下拉项文本:优先显示计量名称,否则回退到 Gauge Code;再拼接测量时间。 @@ -153,6 +168,482 @@ void appendGaugeComboItem(QComboBox* pCombo, } } } + +// 诊断文本统一使用约 15 位有效数字,避免浮点差异被显示精度掩盖。 +QString diagNumberText(double dValue) +{ + return QString::number(dValue, 'g', 15); +} + +// 诊断文本中的单个数据点,括号与逗号为固定格式不参与翻译。 +QString diagPointText(const QPointF& oPoint) +{ + return QString("(%1, %2)") + .arg(diagNumberText(oPoint.x())) + .arg(diagNumberText(oPoint.y())); +} + +// 空字符串在诊断文本中显示为“(none)”,避免与真实空白混淆。 +QString diagTextOrNone(const QString& sText) +{ + return sText.isEmpty() + ? nmWellPressureFlowPage::tr("(none)") : sText; +} + +// 记录状态的诊断文本,与下拉框后缀语义一致,单独成词便于阅读。 +QString diagStatusText(NM_GAUGE_RECORD_STATUS eStatus) +{ + switch (eStatus) + { + case NM_GaugeRecord_Usable: + return nmWellPressureFlowPage::tr("Usable"); + case NM_GaugeRecord_Empty: + return nmWellPressureFlowPage::tr("Empty"); + case NM_GaugeRecord_Corrupt: + return nmWellPressureFlowPage::tr("Corrupt"); + case NM_GaugeRecord_InvalidIdentity: + return nmWellPressureFlowPage::tr("Invalid identity"); + default: + return nmWellPressureFlowPage::tr("Unknown"); + } +} + +// 井别的诊断文本。 +QString diagWellCategoryText(NM_WELL_CATEGORY eWellCategory) +{ + switch (eWellCategory) + { + case NM_WellCategory_Oil: + return nmWellPressureFlowPage::tr("Oil well"); + case NM_WellCategory_Gas: + return nmWellPressureFlowPage::tr("Gas well"); + case NM_WellCategory_Water: + return nmWellPressureFlowPage::tr("Water well"); + default: + return nmWellPressureFlowPage::tr("Unknown"); + } +} + +// 相名的诊断文本,与显示相下拉框文本保持一致。 +QString diagPhaseText(NM_PHASE_TYPE ePhase) +{ + switch (ePhase) + { + case PHASE_Oil: + return nmWellPressureFlowPage::tr("Oil"); + case PHASE_Gas: + return nmWellPressureFlowPage::tr("Gas"); + case PHASE_Water: + return nmWellPressureFlowPage::tr("Water"); + default: + return nmWellPressureFlowPage::tr("Unknown"); + } +} + +// 当前分析窗口的PVT相态决定实际参与分析的相,不能仅依赖多相布尔标志判断。 +QString diagFluidTypeText(PvtFluidType eFluidType) +{ + switch (eFluidType) + { + case WFT_Oil: + return nmWellPressureFlowPage::tr("Oil"); + case WFT_Gas: + return nmWellPressureFlowPage::tr("Gas"); + case WFT_Water: + return nmWellPressureFlowPage::tr("Water"); + case WFT_Oil_Gas: + return nmWellPressureFlowPage::tr("Oil + Gas"); + case WFT_Oil_Water: + return nmWellPressureFlowPage::tr("Oil + Water"); + case WFT_Gas_Water: + return nmWellPressureFlowPage::tr("Gas + Water"); + case WTF_Oil_Gas_Water: + return nmWellPressureFlowPage::tr("Oil + Gas + Water"); + case WFT_Null: + return nmWellPressureFlowPage::tr("None"); + default: + return nmWellPressureFlowPage::tr("Other (%1)") + .arg(static_cast(eFluidType)); + } +} + +// 判断首个点是否为 (0,0) 占位点,与 hasRealPhaseData 的占位点语义一致。 +bool hasLeadingPlaceholderPoint(const QVector& vecPoints) +{ + return !vecPoints.isEmpty() && + vecPoints.first().x() == 0.0 && + vecPoints.first().y() == 0.0; +} + +// 去除可选的首个 (0,0) 占位点后返回真实流量段列表。 +QVector realPhaseSegments(const QVector& vecPoints) +{ + QVector vecSegments = vecPoints; + if (hasLeadingPlaceholderPoint(vecSegments)) + { + vecSegments.remove(0); + } + return vecSegments; +} + +// 生成单个相的诊断摘要行:原始点数、真实段数、累计时长、首末点与占位点标识。 +void appendPhaseDiagLines(QStringList& listLines, + const QString& sPhaseName, + const QVector& vecPoints) +{ + if (vecPoints.isEmpty()) + { + listLines << QString(" %1: %2") + .arg(sPhaseName) + .arg(nmWellPressureFlowPage::tr("no data")); + return; + } + + // 累计时长只统计真实段,流量点 X 值是段持续时间而非绝对时刻。 + QVector vecSegments = realPhaseSegments(vecPoints); + double dTotalDuration = 0.0; + for (int nIndex = 0; nIndex < vecSegments.size(); ++nIndex) + { + dTotalDuration += vecSegments[nIndex].x(); + } + + listLines << QString(" %1: %2") + .arg(sPhaseName) + .arg(nmWellPressureFlowPage::tr( + "raw points = %1, real segments = %2, total duration = %3") + .arg(vecPoints.size()) + .arg(vecSegments.size()) + .arg(diagNumberText(dTotalDuration))); + listLines << QLatin1String(" ") + + nmWellPressureFlowPage::tr("First point = %1, last point = %2") + .arg(diagPointText(vecPoints.first())) + .arg(diagPointText(vecPoints.last())); + // 点数较少时列出全部原始点,便于核对各相中间点数值是否分相正确。 + const int nMaxListedPoints = 16; + if (vecPoints.size() <= nMaxListedPoints) + { + QStringList listPointTexts; + for (int nIndex = 0; nIndex < vecPoints.size(); ++nIndex) + { + listPointTexts << diagPointText(vecPoints[nIndex]); + } + listLines << QLatin1String(" ") + + nmWellPressureFlowPage::tr("All points = %1") + .arg(listPointTexts.join(QLatin1String(", "))); + } + if (hasLeadingPlaceholderPoint(vecPoints)) + { + listLines << QLatin1String(" ") + + nmWellPressureFlowPage::tr("first point is a (0,0) placeholder"); + } +} + +// 检查多相记录有效相之间段数与每段时间是否完全一致,返回结论文本。 +QString diagPhaseConsistencyText(const nmFlowGaugeRecord& oRecord) +{ + // 只统计有真实数据的相,与显示相下拉框的判定保持一致;比较前先去除占位点。 + QStringList listPhaseNames; + QVector > listPhaseSegments; + if (hasRealPhaseData(oRecord.vecOilPoints)) + { + listPhaseNames.append(nmWellPressureFlowPage::tr("Oil")); + listPhaseSegments.append(realPhaseSegments(oRecord.vecOilPoints)); + } + if (hasRealPhaseData(oRecord.vecGasPoints)) + { + listPhaseNames.append(nmWellPressureFlowPage::tr("Gas")); + listPhaseSegments.append(realPhaseSegments(oRecord.vecGasPoints)); + } + if (hasRealPhaseData(oRecord.vecWaterPoints)) + { + listPhaseNames.append(nmWellPressureFlowPage::tr("Water")); + listPhaseSegments.append(realPhaseSegments(oRecord.vecWaterPoints)); + } + + if (listPhaseSegments.size() < 2) + { + return nmWellPressureFlowPage::tr( + "less than two phases with real data, no comparison"); + } + + // 先比较段数,段数不同时直接列出各相段数。 + for (int nPhase = 1; nPhase < listPhaseSegments.size(); ++nPhase) + { + if (listPhaseSegments[nPhase].size() != + listPhaseSegments.first().size()) + { + QStringList listCounts; + for (int nItem = 0; nItem < listPhaseSegments.size(); ++nItem) + { + listCounts << QString("%1 = %2") + .arg(listPhaseNames[nItem]) + .arg(listPhaseSegments[nItem].size()); + } + return nmWellPressureFlowPage::tr("segment counts differ: %1") + .arg(listCounts.join(QLatin1String(", "))); + } + } + + // 段数一致后逐段严格按位比较时间,不引入容差,浮点差异按不一致处理。 + for (int nSegment = 0; + nSegment < listPhaseSegments.first().size(); + ++nSegment) + { + for (int nPhase = 1; nPhase < listPhaseSegments.size(); ++nPhase) + { + if (listPhaseSegments[nPhase][nSegment].x() != + listPhaseSegments.first()[nSegment].x()) + { + return nmWellPressureFlowPage::tr( + "segment counts equal (%1), but segment %2 time differs: %3 = %4, %5 = %6") + .arg(listPhaseSegments.first().size()) + .arg(nSegment + 1) + .arg(listPhaseNames.first()) + .arg(diagNumberText( + listPhaseSegments.first()[nSegment].x())) + .arg(listPhaseNames[nPhase]) + .arg(diagNumberText( + listPhaseSegments[nPhase][nSegment].x())); + } + } + } + return nmWellPressureFlowPage::tr( + "segment counts equal (%1) and all segment times identical") + .arg(listPhaseSegments.first().size()); +} + +// 输出一个原始字节块按 VVecVariant 解码后的形状和少量样本值。 +// 诊断只读取数据,不参与正式的流量记录状态判定。 +void appendRawVVecDiagnostic(QStringList& listLines, + const QString& sBlockName, + const QByteArray& baSource) +{ + listLines << QString(" %1 bytes: %2") + .arg(sBlockName) + .arg(baSource.size()); + if (baSource.isEmpty()) + { + listLines << QString(" %1 is empty").arg(sBlockName); + return; + } + + QByteArray baData = baSource; + VVecVariant vvecData; + const bool bDecoded = ZxBaHelper::convertBa2VVec(vvecData, baData); + listLines << QString(" VVec decode: %1") + .arg(bDecoded ? QLatin1String("success") + : QLatin1String("failed")); + if (!bDecoded) + { + return; + } + + QStringList listSizes; + for (int nVectorIndex = 0; + nVectorIndex < vvecData.size(); + ++nVectorIndex) + { + listSizes << QString("[%1]=%2") + .arg(nVectorIndex) + .arg(vvecData[nVectorIndex].size()); + } + listLines << QString(" Vector count: %1; sizes: %2") + .arg(vvecData.size()) + .arg(listSizes.isEmpty() + ? QLatin1String("(none)") + : listSizes.join(QLatin1String(", "))); + + // GaugeDataEx2 在框架中的正式布局是 N 行 x 4 列,直接按业务字段展示。 + if (sBlockName == QLatin1String("GaugeDataEx2")) + { + bool bRowLayoutValid = !vvecData.isEmpty(); + for (int nStoredRow = 0; + bRowLayoutValid && nStoredRow < vvecData.size(); + ++nStoredRow) + { + bRowLayoutValid = vvecData[nStoredRow].size() == 4; + } + listLines << QString(" N x 4 row layout valid: %1") + .arg(bRowLayoutValid ? QLatin1String("Yes") + : QLatin1String("No")); + if (bRowLayoutValid) + { + const int nMaxStoredRows = 8; + const int nStoredRowCount = qMin( + vvecData.size(), nMaxStoredRows); + for (int nStoredRow = 0; + nStoredRow < nStoredRowCount; + ++nStoredRow) + { + listLines << QString(" Stored row %1: duration=%2, oil=%3, gas=%4, water=%5") + .arg(nStoredRow + 1) + .arg(vvecData[nStoredRow][0].toString()) + .arg(vvecData[nStoredRow][1].toString()) + .arg(vvecData[nStoredRow][2].toString()) + .arg(vvecData[nStoredRow][3].toString()); + } + return; + } + } + + // 其他未知形状的扩展块按矩阵公共范围输出样本,便于继续排查。 + const int nMaxSampleRows = 8; + int nCommonSize = vvecData.isEmpty() ? 0 : vvecData.first().size(); + for (int nVectorIndex = 1; + nVectorIndex < vvecData.size(); + ++nVectorIndex) + { + nCommonSize = qMin(nCommonSize, vvecData[nVectorIndex].size()); + } + const int nSampleRows = qMin(nCommonSize, nMaxSampleRows); + for (int nRowIndex = 0; nRowIndex < nSampleRows; ++nRowIndex) + { + QStringList listValues; + for (int nVectorIndex = 0; + nVectorIndex < vvecData.size(); + ++nVectorIndex) + { + listValues << QString("[%1]=%2") + .arg(nVectorIndex) + .arg(vvecData[nVectorIndex][nRowIndex].toString()); + } + listLines << QString(" Row %1: %2") + .arg(nRowIndex + 1) + .arg(listValues.join(QLatin1String(", "))); + } +} + +// 直接定位当前数值井对应的框架井,检查每条流量记录的三个存储块及 +// getGaugeDataOf() 返回值,区分“数值层解析错误”和“框架原始数据形状异常”。 +void appendFrameworkFlowStorageDiagnostic( + QStringList& listLines, + const QString& sWellCode, + const QString& sSelectedFlowGaugeCode) +{ + listLines << QString(); + listLines << QLatin1String("=== Framework raw flow storage ==="); + listLines << QString("Requested well code: %1") + .arg(sWellCode.isEmpty() ? QLatin1String("(none)") : sWellCode); + listLines << QString("Selected flow gauge code: %1") + .arg(sSelectedFlowGaugeCode.isEmpty() + ? QLatin1String("(none)") : sSelectedFlowGaugeCode); + + if (zxCurProject == nullptr) + { + listLines << QLatin1String("Current framework project: unavailable"); + return; + } + + const ZxDataObjectList listWells = + zxCurProject->getChildren(iDataModelType::sTypeWell); + ZxDataWell* pMatchedWell = nullptr; + int nMatchedWellCount = 0; + for (int nWellIndex = 0; + nWellIndex < listWells.size(); + ++nWellIndex) + { + ZxDataWell* pFrameworkWell = + dynamic_cast(listWells[nWellIndex]); + if (pFrameworkWell != nullptr && + pFrameworkWell->getCode() == sWellCode) + { + ++nMatchedWellCount; + if (pMatchedWell == nullptr) + { + pMatchedWell = pFrameworkWell; + } + } + } + + listLines << QString("Framework wells matched by code: %1") + .arg(nMatchedWellCount); + if (pMatchedWell == nullptr) + { + return; + } + + listLines << QString("Matched framework well name: %1") + .arg(pMatchedWell->getName()); + listLines << QString("Framework well type index: %1") + .arg(pMatchedWell->getWellTypeIndex()); + + const ZxDataObjectList listGaugeF = pMatchedWell->getChildren( + iDataModelType::sTypeDataGaugeF); + listLines << QString("Framework flow-record count: %1") + .arg(listGaugeF.size()); + bool bSelectedGaugeFound = false; + for (int nGaugeIndex = 0; + nGaugeIndex < listGaugeF.size(); + ++nGaugeIndex) + { + ZxDataGaugeF* pGaugeF = + dynamic_cast(listGaugeF[nGaugeIndex]); + if (pGaugeF == nullptr) + { + listLines << QString("[%1] Object is not ZxDataGaugeF") + .arg(nGaugeIndex + 1); + continue; + } + + const bool bIsSelected = + pGaugeF->getCode() == sSelectedFlowGaugeCode; + bSelectedGaugeFound = bSelectedGaugeFound || bIsSelected; + const QByteArray baGaugeData = pGaugeF->getGaugeData(); + const QByteArray baGaugeDataEx2 = pGaugeF->getGaugeDataEx2(); + const QByteArray baGaugeDataEx3 = pGaugeF->getGaugeDataEx3(); + const QByteArray baDataOf0 = pGaugeF->getGaugeDataOf(0); + const QByteArray baDataOf1 = pGaugeF->getGaugeDataOf(1); + const QByteArray baDataOf2 = pGaugeF->getGaugeDataOf(2); + + listLines << QString(); + listLines << QString("[%1] Code: %2; name: %3; selected: %4") + .arg(nGaugeIndex + 1) + .arg(pGaugeF->getCode()) + .arg(pGaugeF->getGaugeName()) + .arg(bIsSelected ? QLatin1String("Yes") + : QLatin1String("No")); + listLines << QString(" Time: %1; multiphase flag: %2") + .arg(pGaugeF->getGaugeTime()) + .arg(pGaugeF->getMultiPhase() + ? QLatin1String("Yes") : QLatin1String("No")); + + // 主块按单相 XY 格式再解一次,确认单相数据是否仍然存在。 + QByteArray baMainCopy = baGaugeData; + VecDouble vecMainX; + VecDouble vecMainY; + const bool bMainDecoded = !baGaugeData.isEmpty() && + ZxBaHelper::convertBa2VecXY( + vecMainX, vecMainY, baMainCopy); + listLines << QString(" GaugeData bytes: %1; XY decode: %2; X/Y sizes: %3/%4") + .arg(baGaugeData.size()) + .arg(bMainDecoded ? QLatin1String("success") + : QLatin1String("failed")) + .arg(static_cast(vecMainX.size())) + .arg(static_cast(vecMainY.size())); + + appendRawVVecDiagnostic( + listLines, QLatin1String("GaugeDataEx2"), baGaugeDataEx2); + appendRawVVecDiagnostic( + listLines, QLatin1String("GaugeDataEx3"), baGaugeDataEx3); + + listLines << QString(" getGaugeDataOf sizes: [0]=%1, [1]=%2, [2]=%3") + .arg(baDataOf0.size()) + .arg(baDataOf1.size()) + .arg(baDataOf2.size()); + listLines << QString(" getGaugeDataOf equality: [0]=GaugeData %1, [1]=Ex2 %2, [2]=Ex3 %3") + .arg(baDataOf0 == baGaugeData + ? QLatin1String("Yes") : QLatin1String("No")) + .arg(baDataOf1 == baGaugeDataEx2 + ? QLatin1String("Yes") : QLatin1String("No")) + .arg(baDataOf2 == baGaugeDataEx3 + ? QLatin1String("Yes") : QLatin1String("No")); + } + + listLines << QString(); + listLines << QString("Selected gauge found under matched framework well: %1") + .arg(bSelectedGaugeFound + ? QLatin1String("Yes") : QLatin1String("No")); +} } nmWellPressureFlowPage::nmWellPressureFlowPage(QWidget* pParent) @@ -182,6 +673,7 @@ nmWellPressureFlowPage::nmWellPressureFlowPage(QWidget* pParent) m_pFlowGaugeCombo(nullptr), m_pDisplayPhaseCombo(nullptr), m_pDisplayPhaseContainer(nullptr), + m_pCheckDataButton(nullptr), m_eDisplayPhase(PHASE_UNKNOWN) { createUi(); @@ -383,7 +875,7 @@ void nmWellPressureFlowPage::slotFlowGaugeChanged(int nIndex) } // 必须调用 switchSelectedFlowGaugeCode 而不是 selectFlowGaugeCode,才能根据新记录 - // 自己的 nIndexF 恢复/校验流动段选择,否则换记录后选择器就回帰到默认 0。 + // 自己的 nIndexF 恢复/校验流动段选择,否则换记录后选择器就回到默认 0。 m_pWell->switchSelectedFlowGaugeCode(sCode); // 换记录后显示相重置为新记录的默认相,m_vecFlowGaugeRecords 仍为本井完整记录列表, // 因此可在 refreshData() 重新填充下拉框前先用旧缓存安全查找新选中记录。 @@ -410,6 +902,32 @@ void nmWellPressureFlowPage::slotDisplayPhaseChanged(int nIndex) refreshData(); } +void nmWellPressureFlowPage::slotCheckDataClicked() +{ + // 诊断窗口为模态只读展示,允许缩放、滚动与复制文本,不修改任何业务数据。 + QDialog oDialog(this); + oDialog.setWindowTitle(tr("Pressure/Flow Data Check")); + oDialog.setWindowFlags(oDialog.windowFlags() | + Qt::WindowMaximizeButtonHint); + oDialog.resize(820, 600); + + QVBoxLayout* pDialogLayout = new QVBoxLayout(&oDialog); + QPlainTextEdit* pTextEdit = new QPlainTextEdit(&oDialog); + pTextEdit->setReadOnly(true); + pTextEdit->setLineWrapMode(QPlainTextEdit::NoWrap); + pTextEdit->setPlainText(buildDiagnosticText()); + + QPushButton* pCloseButton = new QPushButton(tr("Close"), &oDialog); + QHBoxLayout* pButtonLayout = new QHBoxLayout(); + pButtonLayout->addStretch(1); + pButtonLayout->addWidget(pCloseButton); + + pDialogLayout->addWidget(pTextEdit, 1); + pDialogLayout->addLayout(pButtonLayout); + connect(pCloseButton, SIGNAL(clicked()), &oDialog, SLOT(accept())); + oDialog.exec(); +} + void nmWellPressureFlowPage::createUi() { QVBoxLayout* pMainLayout = new QVBoxLayout(this); @@ -569,6 +1087,10 @@ void nmWellPressureFlowPage::createRecordSelectionBar() pPhaseLayout->addWidget(m_pDisplayPhaseCombo); m_pDisplayPhaseContainer->setVisible(false); + // 临时诊断按钮:只读展示井对象解析结果,验证完成后再决定删除或保留。 + m_pCheckDataButton = new QPushButton( + tr("Check Data"), m_pRecordSelectionBar); + pBarLayout->addWidget(pPressureLabel); pBarLayout->addWidget(m_pPressureGaugeCombo); pBarLayout->addSpacing(12); @@ -577,6 +1099,7 @@ void nmWellPressureFlowPage::createRecordSelectionBar() pBarLayout->addSpacing(12); pBarLayout->addWidget(m_pDisplayPhaseContainer); pBarLayout->addStretch(1); + pBarLayout->addWidget(m_pCheckDataButton); connect(m_pPressureGaugeCombo, SIGNAL(currentIndexChanged(int)), @@ -590,6 +1113,10 @@ void nmWellPressureFlowPage::createRecordSelectionBar() SIGNAL(currentIndexChanged(int)), this, SLOT(slotDisplayPhaseChanged(int))); + connect(m_pCheckDataButton, + SIGNAL(clicked()), + this, + SLOT(slotCheckDataClicked())); } void nmWellPressureFlowPage::populatePressureGaugeCombo() @@ -1224,3 +1751,216 @@ void nmWellPressureFlowPage::resetDataViews() updateViews(); } + +QString nmWellPressureFlowPage::buildDiagnosticText() const +{ + if (m_pWell == nullptr) + { + return tr("No well is bound to this page."); + } + + QStringList listLines; + + // 井基础信息:全部即时从井对象重新读取,验证框架解析结果而非页面缓存。 + listLines << tr("=== Well ==="); + listLines << tr("Well name: %1") + .arg(diagTextOrNone(m_pWell->getWellName())); + listLines << tr("Well code: %1") + .arg(diagTextOrNone(m_pWell->getWellCode())); + listLines << tr("Well category: %1") + .arg(diagWellCategoryText(m_pWell->getWellCategory())); + listLines << tr("Reference flow phase: %1") + .arg(diagPhaseText(m_pWell->getReferenceFlowPhase())); + listLines << QString(); + + // 压力记录摘要:枚举全部记录,含损坏、空与身份无效记录。 + QVector vecPressureRecords = + m_pWell->getPressureRecords(); + listLines << tr("=== Pressure records ==="); + listLines << tr("Total pressure records: %1") + .arg(vecPressureRecords.size()); + listLines << tr("Selected pressure gauge code: %1") + .arg(diagTextOrNone(m_pWell->getSelectedPressureGaugeCode())); + for (int nIndex = 0; nIndex < vecPressureRecords.size(); ++nIndex) + { + const nmPressureGaugeRecord& oRecord = vecPressureRecords[nIndex]; + listLines << QString(); + listLines << tr("[%1] Name: %2") + .arg(nIndex + 1) + .arg(diagTextOrNone(oRecord.sGaugeName)); + listLines << QLatin1String(" ") + + tr("Code: %1").arg(diagTextOrNone(oRecord.sGaugeCode)); + listLines << QLatin1String(" ") + + tr("Time: %1").arg(diagTextOrNone(oRecord.sGaugeTime)); + listLines << QLatin1String(" ") + + tr("Status: %1").arg(diagStatusText(oRecord.eStatus)); + listLines << QLatin1String(" ") + + tr("Point count: %1").arg(oRecord.vecPressurePoints.size()); + if (oRecord.vecPressurePoints.isEmpty()) + { + listLines << QLatin1String(" ") + tr("No pressure points."); + } + else + { + listLines << QLatin1String(" ") + + tr("First point = %1, last point = %2") + .arg(diagPointText(oRecord.vecPressurePoints.first())) + .arg(diagPointText(oRecord.vecPressurePoints.last())); + } + } + listLines << QString(); + + // 流量记录摘要:油、气、水三相分别统计,多相记录额外检查相间时间一致性。 + QVector vecFlowRecords = m_pWell->getFlowRecords(); + listLines << tr("=== Flow records ==="); + listLines << tr("Total flow records: %1").arg(vecFlowRecords.size()); + listLines << tr("Selected flow gauge code: %1") + .arg(diagTextOrNone(m_pWell->getSelectedFlowGaugeCode())); + listLines << tr("Current display phase: %1") + .arg(diagPhaseText(m_eDisplayPhase)); + for (int nIndex = 0; nIndex < vecFlowRecords.size(); ++nIndex) + { + const nmFlowGaugeRecord& oRecord = vecFlowRecords[nIndex]; + listLines << QString(); + listLines << tr("[%1] Name: %2") + .arg(nIndex + 1) + .arg(diagTextOrNone(oRecord.sGaugeName)); + listLines << QLatin1String(" ") + + tr("Code: %1").arg(diagTextOrNone(oRecord.sGaugeCode)); + listLines << QLatin1String(" ") + + tr("Time: %1").arg(diagTextOrNone(oRecord.sGaugeTime)); + listLines << QLatin1String(" ") + + tr("Status: %1").arg(diagStatusText(oRecord.eStatus)); + listLines << QLatin1String(" ") + + tr("Multiphase: %1") + .arg(oRecord.bMultiPhase ? tr("Yes") : tr("No")); + listLines << QLatin1String(" ") + + tr("Own flow segment index (1-based): %1") + .arg(oRecord.nIndexF); + appendPhaseDiagLines(listLines, tr("Oil"), oRecord.vecOilPoints); + appendPhaseDiagLines(listLines, tr("Gas"), oRecord.vecGasPoints); + appendPhaseDiagLines(listLines, tr("Water"), oRecord.vecWaterPoints); + if (oRecord.bMultiPhase) + { + listLines << QLatin1String(" ") + + tr("Phase time consistency: %1") + .arg(diagPhaseConsistencyText(oRecord)); + } + } + + // 数值记录显示 Corrupt 时继续向下追溯对应框架井的原始存储,尤其检查 + // 另一口井自己的 GaugeDataEx2 是否确实为 N 行 x 4 列。 + appendFrameworkFlowStorageDiagnostic( + listLines, + m_pWell->getWellCode(), + m_pWell->getSelectedFlowGaugeCode()); + + // 单独读取当前流动段分析快照,验证“多相数据”弹框保存的矩阵是否位于 + // ZxSegmentInfo::m_vvecMpData。此处只展示,不覆盖工程井记录或求解输入。 + listLines << QString(); + listLines << tr("=== Current flow-segment multiphase data ==="); + nmDataAnalyzeContextProvider* pContextProvider = + nmDataAnalyzeContext::provider(); + iSubWndFitting* pSubWndFitting = + nmDataAnalyzeManager::getCurrentFitting(); + nmDataAnalyzeManager* pCurrentManager = + nmDataAnalyzeManager::getCurrentInstance(); + const QString sCurrentPrimaryWellCode = pCurrentManager != nullptr + ? pCurrentManager->getPrimaryWellCode() : QString(); + const bool bSnapshotBelongsToEditedWell = + !sCurrentPrimaryWellCode.isEmpty() && + sCurrentPrimaryWellCode == m_pWell->getWellCode(); + listLines << tr("Current fitting primary well code: %1") + .arg(diagTextOrNone(sCurrentPrimaryWellCode)); + listLines << tr("Snapshot belongs to edited well: %1") + .arg(bSnapshotBelongsToEditedWell ? tr("Yes") : tr("No")); + if (!bSnapshotBelongsToEditedWell) + { + listLines << tr("This snapshot belongs to the current fitting primary well and does not override the edited other well."); + } + bool bSegmentMultiPhase = false; + QString sFlowCurveName; + VVecDouble vvecMultiPhaseData; + const bool bSnapshotAvailable = pContextProvider != nullptr && + pSubWndFitting != nullptr && + pContextProvider->getCurrentSegmentMultiPhaseData( + pSubWndFitting, + bSegmentMultiPhase, + sFlowCurveName, + vvecMultiPhaseData); + listLines << tr("Snapshot available: %1") + .arg(bSnapshotAvailable ? tr("Yes") : tr("No")); + if (bSnapshotAvailable) + { + PvtFluidType eFluidType = WFT_Null; + const bool bFluidTypeAvailable = + pContextProvider->getBasicPft(pSubWndFitting, eFluidType); + listLines << tr("PVT phase type: %1") + .arg(bFluidTypeAvailable + ? diagFluidTypeText(eFluidType) : tr("Unknown")); + listLines << tr("Segment multiphase flag: %1") + .arg(bSegmentMultiPhase ? tr("Yes") : tr("No")); + listLines << tr("Flow curve name: %1") + .arg(diagTextOrNone(sFlowCurveName)); + listLines << tr("Matrix vector count: %1") + .arg(vvecMultiPhaseData.size()); + + QStringList listVectorSizes; + for (int nVectorIndex = 0; + nVectorIndex < vvecMultiPhaseData.size(); + ++nVectorIndex) + { + listVectorSizes << QString("[%1] = %2") + .arg(nVectorIndex) + .arg(vvecMultiPhaseData[nVectorIndex].size()); + } + listLines << tr("Vector sizes: %1") + .arg(listVectorSizes.isEmpty() + ? tr("None") + : listVectorSizes.join(QLatin1String(", "))); + + bool bMatrixShapeValid = vvecMultiPhaseData.size() == 4; + int nPointCount = bMatrixShapeValid + ? vvecMultiPhaseData[0].size() : 0; + for (int nVectorIndex = 1; + bMatrixShapeValid && nVectorIndex < 4; + ++nVectorIndex) + { + if (vvecMultiPhaseData[nVectorIndex].size() != nPointCount) + { + bMatrixShapeValid = false; + } + } + listLines << tr("Four-vector shape valid: %1") + .arg(bMatrixShapeValid ? tr("Yes") : tr("No")); + + if (bMatrixShapeValid) + { + listLines << tr("Data rows (time, oil, gas, water):"); + const int nMaxListedRows = 64; + const int nListedRows = qMin(nPointCount, nMaxListedRows); + for (int nPointIndex = 0; + nPointIndex < nListedRows; + ++nPointIndex) + { + listLines << QString(" [%1] t=%2, oil=%3, gas=%4, water=%5") + .arg(nPointIndex + 1) + .arg(diagNumberText( + vvecMultiPhaseData[0][nPointIndex])) + .arg(diagNumberText( + vvecMultiPhaseData[1][nPointIndex])) + .arg(diagNumberText( + vvecMultiPhaseData[2][nPointIndex])) + .arg(diagNumberText( + vvecMultiPhaseData[3][nPointIndex])); + } + if (nPointCount > nListedRows) + { + listLines << tr(" ... %1 additional rows omitted") + .arg(nPointCount - nListedRows); + } + } + } + + return listLines.join(QLatin1String("\n")); +} diff --git a/Src/nmNum/nmSubWxs/nmWxEditWellPlot.cpp b/Src/nmNum/nmSubWxs/nmWxEditWellPlot.cpp index 84fc80a..fe53f83 100644 --- a/Src/nmNum/nmSubWxs/nmWxEditWellPlot.cpp +++ b/Src/nmNum/nmSubWxs/nmWxEditWellPlot.cpp @@ -660,6 +660,12 @@ void nmWxEditWellPlot::onOkClicked() pStoredWell, m_pNmDataWell); const bool bGridInputChanged = hasPebiGridInputChanged(pStoredWell, m_pNmDataWell); + const bool bCurveSelectionChanged = pStoredWell == nullptr || + pStoredWell->getSelectedPressureGaugeCode() != + m_pNmDataWell->getSelectedPressureGaugeCode() || + pStoredWell->getSelectedFlowGaugeCode() != + m_pNmDataWell->getSelectedFlowGaugeCode() || + pStoredWell->getIndexF() != m_pNmDataWell->getIndexF(); // 应用时间变表皮状态到真实数据 nmDataWellBase* pRealWell = pManager->getCurWellData(); @@ -697,6 +703,25 @@ void nmWxEditWellPlot::onOkClicked() return; } + // 压力记录、流量记录或流动段切换后,旧的历史曲线缓存已经不再对应 + // 当前选择。正式井写回完成后重新计算,保证结果快照和自动拟合读取新记录。 + if(bCurveSelectionChanged) { + nmDataWellBase* pUpdatedWell = pManager->findWellByInstanceId( + m_pNmDataWell->getWellInstanceId()); + if(pUpdatedWell != nullptr) { + QVector > vecHistoryPressure; + QVector > vecHistoryLogLog; + QVector > vecHistorySemiLog; + pManager->calculationLogData(pUpdatedWell, + vecHistoryPressure, + vecHistoryLogLog, + vecHistorySemiLog); + pUpdatedWell->setHistoryPressure(vecHistoryPressure); + pUpdatedWell->setHistoryLogLog(vecHistoryLogLog); + pUpdatedWell->setHistorySemiLog(vecHistorySemiLog); + } + } + // 人工确认后的输入变化必须推进版本,阻止后台旧任务提交过期结果。 if(bInputChanged) { if(bGridInputChanged) { diff --git a/Src/nmNum/nmSubWxs/nmWxIncludeOtherWells.cpp b/Src/nmNum/nmSubWxs/nmWxIncludeOtherWells.cpp index c121296..f61de15 100644 --- a/Src/nmNum/nmSubWxs/nmWxIncludeOtherWells.cpp +++ b/Src/nmNum/nmSubWxs/nmWxIncludeOtherWells.cpp @@ -19,6 +19,24 @@ bool isSupportedNumericalWell(nmDataWellBase* pWellData) eWellType == NM_WELL_MODEL::Vertical_Fractured_Well || eWellType == NM_WELL_MODEL::Horizontal_Fractured_Well; } + +// 表格中的“产量”表示该相至少出现过一个非零流量;全零相仍会保留在 +// 求解输入中,但不能在摘要里误导用户为该井实际存在该相产量。 +bool hasNonZeroRate(nmDataWellBase* pWellData, NM_PHASE_TYPE ePhase) +{ + if(pWellData == nullptr) { + return false; + } + + const QVector vecPoints = + pWellData->getFlowSegmentPoints(ePhase); + for(int nIndex = 0; nIndex < vecPoints.size(); ++nIndex) { + if(vecPoints[nIndex].y() != 0.0) { + return true; + } + } + return false; +} } nmWxIncludeOtherWells::nmWxIncludeOtherWells( @@ -124,17 +142,19 @@ void nmWxIncludeOtherWells::setupTable() m_pDataManager->getNumericalAnalysisCase()->isIncludedWell( oWellRow.m_sWellCode); - // 第三步:井别唯一决定产量列;不再把所有井都显示为油量。 + // 第三步:多相井可同时具有多列产量,逐相读取实际非零流量。 oWellRow.m_sPressure = bHasValidPressure ? tr("History") : tr("None"); oWellRow.m_sOilProd = tr("None"); oWellRow.m_sGasProd = tr("None"); oWellRow.m_sWaterProd = tr("None"); - if(oWellRow.m_eWellCategory == NM_WellCategory_Oil) { + if(hasNonZeroRate(pWell, PHASE_Oil)) { oWellRow.m_sOilProd = tr("Rate"); - } else if(oWellRow.m_eWellCategory == NM_WellCategory_Gas) { + } + if(hasNonZeroRate(pWell, PHASE_Gas)) { oWellRow.m_sGasProd = tr("Rate"); - } else if(oWellRow.m_eWellCategory == NM_WellCategory_Water) { + } + if(hasNonZeroRate(pWell, PHASE_Water)) { oWellRow.m_sWaterProd = tr("Rate"); } oWellRow.m_eMode = NM_CaseWell_RateControlled;