diff --git a/Bin/Config/Lang/cn/nmNum_cn.qm b/Bin/Config/Lang/cn/nmNum_cn.qm index 5cb22c9a..398b4c45 100644 Binary files a/Bin/Config/Lang/cn/nmNum_cn.qm and b/Bin/Config/Lang/cn/nmNum_cn.qm differ diff --git a/Bin/Config/Lang/cn/nmNum_cn.ts b/Bin/Config/Lang/cn/nmNum_cn.ts index 93aefd4a..f1afb9c1 100644 --- a/Bin/Config/Lang/cn/nmNum_cn.ts +++ b/Bin/Config/Lang/cn/nmNum_cn.ts @@ -7130,6 +7130,12 @@ Average pressure in contour: %2 MPa Well information井信息 Well number井号 Well name井名 + Well category井别 + Oil well油井 + Gas well气井 + Water well水井 + Unknown未知 + Well model井型 Well type井型 Project-level unique well identifier项目级唯一井标识 Vertical well直井 diff --git a/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h b/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h index f784c750..3a19c921 100644 --- a/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h +++ b/Include/nmNum/nmCalculation/nmCalculationPebiGrid.h @@ -27,7 +27,8 @@ class nmDataWellBase; struct nmPebiWellInputSnapshot { nmPebiWellInputSnapshot() - : m_nFlowSectionIndex(1), + : m_eWellCategory(NM_WellCategory_Unknown), + m_nFlowSectionIndex(1), m_dRadius(0.0), m_dWellboreStorage(0.0), m_dSkin(0.0), @@ -44,6 +45,7 @@ struct nmPebiWellInputSnapshot QString m_sWellName; ///< 捕获时井名称。 QString m_sWellInstanceId; ///< 完整场结果使用的稳定井 UUID。 QVector m_vecFlowPoints; ///< 求解使用的流量制度。 + NM_WELL_CATEGORY m_eWellCategory; ///< 求解时冻结的油井、气井或水井井别。 /** @brief 求解时冻结的历史压力曲线。 */ QVector > m_vecHistoryPressure; /** @brief 求解时冻结的历史双对数曲线。 */ @@ -51,7 +53,7 @@ struct nmPebiWellInputSnapshot /** @brief 求解时冻结的历史半对数曲线。 */ QVector > m_vecHistorySemiLog; QPointF m_oLocation; ///< 求解时井平面位置。 - int m_nFlowSectionIndex; ///< 当前流动段下标。 + int m_nFlowSectionIndex; ///< 当前流动段一基索引。 double m_dRadius; ///< 求解时井筒半径。 double m_dWellboreStorage; ///< 求解时井筒储集系数。 double m_dSkin; ///< 求解时表皮系数。 diff --git a/Include/nmNum/nmData/nmDataAnalyzeManager.h b/Include/nmNum/nmData/nmDataAnalyzeManager.h index 502ecb64..1bf892a2 100644 --- a/Include/nmNum/nmData/nmDataAnalyzeManager.h +++ b/Include/nmNum/nmData/nmDataAnalyzeManager.h @@ -121,6 +121,37 @@ enum NM_SOLVER_MODEL_TYPE { SMT_Oil_Gas_Water_ThreePhase = 10 }; +/** @brief 判断求解模型是否包含指定工程井别对应的流量相。 */ +inline bool nmSolverModelSupportsWellCategory( + NM_SOLVER_MODEL_TYPE eSolverModelType, + NM_WELL_CATEGORY eWellCategory) +{ + switch(eSolverModelType) { + case SMT_Oil_ConstPvt: + case SMT_Oil_VariablePvt: + return eWellCategory == NM_WellCategory_Oil; + case SMT_Water_ConstPvt: + case SMT_Water_VariablePvt: + return eWellCategory == NM_WellCategory_Water; + case SMT_Gas_VariablePvt: + case SMT_Gas_PseudoPressure: + return eWellCategory == NM_WellCategory_Gas; + case SMT_Oil_Gas_TwoPhase: + return eWellCategory == NM_WellCategory_Oil || + eWellCategory == NM_WellCategory_Gas; + case SMT_Oil_Water_TwoPhase: + return eWellCategory == NM_WellCategory_Oil || + eWellCategory == NM_WellCategory_Water; + case SMT_Gas_Water_TwoPhase: + return eWellCategory == NM_WellCategory_Gas || + eWellCategory == NM_WellCategory_Water; + case SMT_Oil_Gas_Water_ThreePhase: + return nmIsValidWellCategory(eWellCategory); + default: + return false; + } +} + class nmDataWellBase; class ZxDataWell; class nmDataReservoir; @@ -704,7 +735,8 @@ public: } // 加载井数组里对应的压力、流量数据,这个是在井里没有这些数据的情况下调用 - void loadWellPreAndFlow(); + /** @brief 按保存井别重新读取工程井压力和流量,任一井映射失败时返回 false。 */ + bool loadWellPreAndFlow(); // 设置当前分析下的VTK网格对象 void setUnstructuredGrid(vtkSmartPointer grid); diff --git a/Include/nmNum/nmData/nmDataWellBase.h b/Include/nmNum/nmData/nmDataWellBase.h index eccbc6fd..ccfc297e 100644 --- a/Include/nmNum/nmData/nmDataWellBase.h +++ b/Include/nmNum/nmData/nmDataWellBase.h @@ -61,9 +61,14 @@ public: void setWellCode(const QString& code); QString getWellCode() const; + /** @brief 保存工程井的油井、气井或水井井别;该值不由数值井编辑器修改。 */ + void setWellCategory(NM_WELL_CATEGORY eWellCategory); + /** @brief 返回工程井井别,用于选择流量大字段及求解器相别数组。 */ + NM_WELL_CATEGORY getWellCategory() const; + /** @brief 返回井对象的稳定实例标识,与可编辑井编码相互独立。 */ QString getWellInstanceId() const; - /** @brief v3 加载或井型替换时恢复已有实例标识。 */ + /** @brief 项目加载或井型替换时恢复已有实例标识。 */ bool restoreWellInstanceId(const QString& sWellInstanceId); /** @brief 为新建井生成不带花括号的 UUID。 */ static QString createWellInstanceId(); @@ -133,6 +138,12 @@ public: QVector getFlowPoints() const; // 设置流量曲线点数据 void setFlowPoints(const QVector& points); + /** @brief 返回去除可选 (0,0) 占位点后的真实流动段。 */ + QVector getFlowSegmentPoints() const; + /** @brief 返回真实流动段数量,不把首个 (0,0) 占位点计入。 */ + int getFlowSegmentCount() const; + /** @brief 校验当前流动段索引是否位于一基的真实流动段范围内。 */ + bool hasValidFlowSectionIndex() const; int getIndexF() const; void setIndexF(const int newIndex); @@ -237,6 +248,7 @@ protected: QString m_sWellInstanceId; QString m_wellName; // 井名 QString m_wellCode; // 井编码(项目级唯一标识,来自ZxDataWell::getCode()) + NM_WELL_CATEGORY m_eWellCategory; // 工程井井别:油井、气井或水井 // Location---位置 nmDataAttribute m_inputWellHead; //井口坐标输入 @@ -265,7 +277,7 @@ protected: nmDataAttribute m_bottomholeMD; // 井底测量深度 QVector m_vecPtsP; // 压力曲线点数据 - QVector m_vecPtsF; // 流量曲线点数据 + QVector m_vecPtsF; // 流量曲线点数据,允许首项为 (0,0) 占位点 int m_nIndexF; // 当前流量段的索引 diff --git a/Include/nmNum/nmData/nmDefines.h b/Include/nmNum/nmData/nmDefines.h index 7aa4e66e..53230289 100644 --- a/Include/nmNum/nmData/nmDefines.h +++ b/Include/nmNum/nmData/nmDefines.h @@ -56,6 +56,22 @@ enum NM_PHASE_TYPE { PHASE_UNKNOWN, // 其它 PHASE_Oil_Water // 油水 }; + +// 工程井井别。数值模块只保存工程井井别的只读副本,流量相别由该值唯一派生。 +// Oil/Gas/Water 的数值与 ZxDataWell::getWellTypeIndex() 及流量大字段索引保持一致。 +enum NM_WELL_CATEGORY { + NM_WellCategory_Unknown = -1, + NM_WellCategory_Oil = 0, + NM_WellCategory_Gas = 1, + NM_WellCategory_Water = 2 +}; + +inline bool nmIsValidWellCategory(NM_WELL_CATEGORY eWellCategory) +{ + return eWellCategory == NM_WellCategory_Oil || + eWellCategory == NM_WellCategory_Gas || + eWellCategory == NM_WellCategory_Water; +} // 结构树节点类型 enum NM_TREE_TYPE { TREE_OilReservoir, // 油藏节点 diff --git a/Include/nmNum/nmData/nmPebiResultSnapshot.h b/Include/nmNum/nmData/nmPebiResultSnapshot.h index b7080923..05f61433 100644 --- a/Include/nmNum/nmData/nmPebiResultSnapshot.h +++ b/Include/nmNum/nmData/nmPebiResultSnapshot.h @@ -35,6 +35,8 @@ struct NM_DATA_EXPORT nmPebiResultSolverSlot QString m_sWellCode; /** @brief 求解时冻结的井模型类型。 */ NM_WELL_MODEL m_eWellType; + /** @brief 求解时冻结的油井、气井或水井井别。 */ + NM_WELL_CATEGORY m_eWellCategory; }; /** @brief 一口结果井的历史曲线和计算曲线。 */ @@ -70,6 +72,8 @@ struct NM_DATA_EXPORT nmPebiResultWellSnapshot QString m_sWellName; /** @brief 求解时冻结的井模型类型。 */ NM_WELL_MODEL m_eWellType; + /** @brief 求解时冻结的油井、气井或水井井别。 */ + NM_WELL_CATEGORY m_eWellCategory; /** @brief 求解时冻结的井角色。 */ NM_CASE_WELL_MODE m_eWellMode; /** @brief 求解时冻结的井平面位置。 */ diff --git a/Include/nmNum/nmData/nmPebiResultSnapshotBuilder.h b/Include/nmNum/nmData/nmPebiResultSnapshotBuilder.h index 6ea13c61..67317f27 100644 --- a/Include/nmNum/nmData/nmPebiResultSnapshotBuilder.h +++ b/Include/nmNum/nmData/nmPebiResultSnapshotBuilder.h @@ -21,7 +21,7 @@ public: /** @brief 销毁尚未被 takeCandidate() 转交的候选。 */ ~nmPebiResultSnapshotBuilder(); - /** @brief v3 加载时恢复已保存的快照 UUID;求解候选可使用自动生成值。 */ + /** @brief v4 加载时恢复已保存的快照 UUID;求解候选可使用自动生成值。 */ bool setSnapshotId(const QString& sSnapshotId); /** @brief 设置候选创建时对应的网格输入版本和求解输入版本。 */ bool setInputRevisions(quint64 nGridInputRevision, diff --git a/Include/nmNum/nmData/nmPebiResultSnapshotSerializer.h b/Include/nmNum/nmData/nmPebiResultSnapshotSerializer.h index 77f687b8..bf2018fd 100644 --- a/Include/nmNum/nmData/nmPebiResultSnapshotSerializer.h +++ b/Include/nmNum/nmData/nmPebiResultSnapshotSerializer.h @@ -10,7 +10,7 @@ class nmPebiResultSnapshot; /** - * @brief 负责 v3 快照三文件载荷及 Snapshot.json 的流式保存和加载。 + * @brief 负责 v4 快照三文件载荷及 Snapshot.json 的流式保存和加载。 * * 序列化器是快照的受限友元,只在保存时读取内部 VTK 对象;加载仍通过 * nmPebiResultSnapshotBuilder 构造和封闭候选。 diff --git a/Include/nmNum/nmSubWxs/nmWxIncludeOtherWells.h b/Include/nmNum/nmSubWxs/nmWxIncludeOtherWells.h index e3bfdb0a..0efc62fc 100644 --- a/Include/nmNum/nmSubWxs/nmWxIncludeOtherWells.h +++ b/Include/nmNum/nmSubWxs/nmWxIncludeOtherWells.h @@ -23,6 +23,7 @@ struct WellDataRow { bool m_bIsIncluded; ///< 是否被用户勾选为其他主动井。 bool m_bIsPrimary; ///< 主井防御标记;主井正常情况下不会进入本表。 bool m_bCanCalculate; ///< 当前井型和几何参数是否可进入 PEBI 模型。 + NM_WELL_CATEGORY m_eWellCategory; ///< 工程井井别,决定哪一列显示流量。 NM_CASE_WELL_MODE m_eMode; ///< 本列表固定保存有产量主动井角色。 QString m_sOilProd; ///< 油产量列的摘要文本。 QString m_sGasProd; ///< 气产量列的摘要文本。 @@ -34,6 +35,7 @@ struct WellDataRow { : m_bIsIncluded(false), m_bIsPrimary(false), m_bCanCalculate(false), + m_eWellCategory(NM_WellCategory_Unknown), m_eMode(NM_CaseWell_RateControlled) { } @@ -89,7 +91,7 @@ private: QHBoxLayout* m_pButtonLayout; ///< 底部操作按钮布局。 QVBoxLayout* m_pMainLayout; ///< 对话框主布局。 - QVector m_wellData; ///< 与表格行一一对应的候选井数据。 + QVector m_vecWellData; ///< 与表格行一一对应的候选井数据。 }; #endif // NMWXINCLUDEOTHERWELLS_H diff --git a/Include/nmNum/nmSubWxs/nmWxWellEditor.h b/Include/nmNum/nmSubWxs/nmWxWellEditor.h index fe0a7afb..af1262a8 100644 --- a/Include/nmNum/nmSubWxs/nmWxWellEditor.h +++ b/Include/nmNum/nmSubWxs/nmWxWellEditor.h @@ -173,6 +173,7 @@ private: nmWellEditorController* m_pWellEditorController; ///< 轨迹交互控制器。 QLineEdit* m_pWellCodeLineEdit; ///< 只读项目级井号。 QLineEdit* m_pWellNameLineEdit; ///< 井名输入框。 + QLineEdit* m_pWellCategoryLineEdit; ///< 只读工程井井别。 QComboBox* m_pWellTypeComboBox; ///< 井型选择器。 QCheckBox* m_pTimeDependentCheckBox; ///< 时间变表皮开关。 QToolButton* m_pTimeDependentButton; ///< 时间变表皮详情入口。 diff --git a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp index f70eae99..0bd52880 100644 --- a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp @@ -195,6 +195,8 @@ bool captureResultWellMetadata( { if(pWellData == NULL || !oWellInput.m_bRealWell || pWellData->getWellCode() != oWellInput.m_sWellCode || + pWellData->getWellCategory() != oWellInput.m_eWellCategory || + !nmIsValidWellCategory(oWellInput.m_eWellCategory) || pWellData->getWellInstanceId().isEmpty() || QUuid(pWellData->getWellInstanceId()).isNull()) { return false; @@ -213,7 +215,7 @@ bool captureResultWellMetadata( // 无产量井仍作为观察井保留在求解槽位和快照元数据中,但不进入结果下拉框。 oWellInput.m_bDisplayResultWell = bDisplayResultWell && oWellInput.m_bRateControlled && - oWellInput.m_vecFlowPoints.size() >= 2; + !oWellInput.m_vecFlowPoints.isEmpty(); nmDataVerticalFracturedWell* pVerticalFracturedWell = dynamic_cast(pWellData); @@ -1512,7 +1514,10 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( const bool bRateControlled = oWellInput.m_bRateControlled && - oWellInput.m_vecFlowPoints.size() >= 2; + !oWellInput.m_vecFlowPoints.isEmpty() && + oWellInput.m_nFlowSectionIndex >= 1 && + oWellInput.m_nFlowSectionIndex <= + oWellInput.m_vecFlowPoints.size(); vvecLogLog.clear(); vvecSemiLog.clear(); @@ -1549,14 +1554,14 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( // 准备流量段数据 const QVector& vecTimeQ = oWellInput.m_vecFlowPoints; - // 防御空流量数据,避免 -1 转成 std::vector 的巨大无符号长度。 - int nTimeNumQ = qMax(0, vecTimeQ.size() - 1); // 移除第一个 0 点 + // 输入快照已经移除可选 (0,0) 占位点,此处逐段传给曲线 DLL。 + const int nTimeNumQ = vecTimeQ.size(); std::vector timeQ(nTimeNumQ); std::vector q(nTimeNumQ); for(int i = 0; i < nTimeNumQ; ++i) { - timeQ[i] = vecTimeQ[i + 1].x(); - q[i] = vecTimeQ[i + 1].y(); + timeQ[i] = vecTimeQ[i].x(); + q[i] = vecTimeQ[i].y(); } // 调用外部 DLL 计算双对数曲线 @@ -1913,6 +1918,7 @@ bool nmCalculationDllPebiSolverTask::buildPebiResultSnapshotCandidate() oSlot.m_eEntryKind = oWellRef.m_eEntryKind; oSlot.m_sWellCode = oWellRef.m_sWellCode; oSlot.m_eWellType = oWellRef.m_eWellType; + oSlot.m_eWellCategory = oWellInput.m_eWellCategory; if(oWellRef.m_eEntryKind == NM_SolverEntry_Well) { oSlot.m_sWellInstanceId = oWellInput.m_sWellInstanceId; } @@ -1941,6 +1947,7 @@ bool nmCalculationDllPebiSolverTask::buildPebiResultSnapshotCandidate() oWell.m_sWellCode = oWellInput.m_sWellCode; oWell.m_sWellName = oWellInput.m_sWellName; oWell.m_eWellType = oWellRef.m_eWellType; + oWell.m_eWellCategory = oWellInput.m_eWellCategory; oWell.m_eWellMode = oWellInput.m_bRateControlled ? NM_CaseWell_RateControlled : NM_CaseWell_Observation; diff --git a/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp b/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp index 6552c1da..added2b4 100644 --- a/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationPebiGrid.cpp @@ -1103,7 +1103,8 @@ bool nmCalculationPebiGrid::appendManualWellInputSnapshot( oWellInput.m_bRealWell = true; oWellInput.m_sWellCode = pWellData->getWellCode(); oWellInput.m_sWellName = pWellData->getWellName(); - oWellInput.m_vecFlowPoints = pWellData->getFlowPoints(); + oWellInput.m_eWellCategory = pWellData->getWellCategory(); + oWellInput.m_vecFlowPoints = pWellData->getFlowSegmentPoints(); oWellInput.m_nFlowSectionIndex = pWellData->getIndexF(); oWellInput.m_oLocation = QPointF( pWellData->getX().getValue().toDouble(), @@ -1115,8 +1116,10 @@ bool nmCalculationPebiGrid::appendManualWellInputSnapshot( : 0.0; oWellInput.m_bRateControlled = nWellMode == static_cast(NM_CaseWell_RateControlled); - if(oWellInput.m_bRateControlled && - oWellInput.m_vecFlowPoints.size() < 2) { + if(!nmIsValidWellCategory(oWellInput.m_eWellCategory) || + (oWellInput.m_bRateControlled && + (oWellInput.m_vecFlowPoints.isEmpty() || + !pWellData->hasValidFlowSectionIndex()))) { return false; } @@ -1378,7 +1381,8 @@ bool nmCalculationPebiGrid::captureInputSnapshot( oWellInput.m_bRealWell = true; oWellInput.m_sWellName = pWellData->getWellName(); - oWellInput.m_vecFlowPoints = pWellData->getFlowPoints(); + oWellInput.m_eWellCategory = pWellData->getWellCategory(); + oWellInput.m_vecFlowPoints = pWellData->getFlowSegmentPoints(); oWellInput.m_nFlowSectionIndex = pWellData->getIndexF(); oWellInput.m_oLocation = QPointF( pWellData->getX().getValue().toDouble(), @@ -1392,8 +1396,14 @@ bool nmCalculationPebiGrid::captureInputSnapshot( oWellInput.m_bRateControlled = mapWellModes.value(oWellRef.m_sWellCode) == static_cast(NM_CaseWell_RateControlled); + if(!nmIsValidWellCategory(oWellInput.m_eWellCategory)) { + qWarning() << "Solver well has an invalid category:" + << oWellRef.m_sWellCode; + return false; + } if(oWellInput.m_bRateControlled && - oWellInput.m_vecFlowPoints.size() < 2) { + (oWellInput.m_vecFlowPoints.isEmpty() || + !pWellData->hasValidFlowSectionIndex())) { qWarning() << "Rate-controlled well has no valid rate schedule:" << oWellRef.m_sWellCode; return false; @@ -1519,7 +1529,7 @@ bool nmCalculationPebiGrid::buildPebiScene( oScene.wellName.push_back(vecWellInputs[nIndex].m_sWellName); } - // 第二步:按求解器模型填充每个槽位的产量制度。 + // 第二步:按每口井冻结的井别填充产量制度;求解模型只负责相容性校验。 const NM_SOLVER_MODEL_TYPE eSolverModelType = static_cast(oScene.solverType); oScene.Rate.t.resize(vecSolverWellOrder.size()); @@ -1545,26 +1555,31 @@ bool nmCalculationPebiGrid::buildPebiScene( continue; } - QVector vecTimeQ = oWellInput.m_vecFlowPoints; - if(!vecTimeQ.isEmpty() && - vecTimeQ[0].x() == 0.0 && - vecTimeQ[0].y() == 0.0) { - vecTimeQ.remove(0); + if(!nmIsValidWellCategory(oWellInput.m_eWellCategory) || + !nmSolverModelSupportsWellCategory( + eSolverModelType, oWellInput.m_eWellCategory) || + oWellInput.m_vecFlowPoints.isEmpty() || + oWellInput.m_nFlowSectionIndex < 1 || + oWellInput.m_nFlowSectionIndex > + oWellInput.m_vecFlowPoints.size()) { + qWarning() << "Rate-controlled well input is incompatible with solver model:" + << oWellInput.m_sWellCode; + return false; } std::vector vecTime; std::vector vecRate; - vecTime.reserve(vecTimeQ.size()); - vecRate.reserve(vecTimeQ.size()); + vecTime.reserve(oWellInput.m_vecFlowPoints.size()); + vecRate.reserve(oWellInput.m_vecFlowPoints.size()); for(int nPointIndex = 0; - nPointIndex < vecTimeQ.size(); + nPointIndex < oWellInput.m_vecFlowPoints.size(); ++nPointIndex) { if((nPointIndex % 256) == 0 && isCancellationRequested(pCancelRequested)) { return false; } - vecTime.push_back(vecTimeQ[nPointIndex].x()); - vecRate.push_back(vecTimeQ[nPointIndex].y()); + vecTime.push_back(oWellInput.m_vecFlowPoints[nPointIndex].x()); + vecRate.push_back(oWellInput.m_vecFlowPoints[nPointIndex].y()); } oScene.Rate.t[nWellIndex] = vecTime; @@ -1572,32 +1587,15 @@ bool nmCalculationPebiGrid::buildPebiScene( oScene.Rate.qg[nWellIndex].assign(vecRate.size(), 0.0); oScene.Rate.qw[nWellIndex].assign(vecRate.size(), 0.0); - if(eSolverModelType == SMT_Gas_VariablePvt || - eSolverModelType == SMT_Gas_PseudoPressure) { + if(oWellInput.m_eWellCategory == NM_WellCategory_Gas) { oScene.Rate.qg[nWellIndex] = vecRate; - } else if(eSolverModelType == SMT_Water_ConstPvt || - eSolverModelType == SMT_Water_VariablePvt) { + } else if(oWellInput.m_eWellCategory == NM_WellCategory_Water) { oScene.Rate.qw[nWellIndex] = vecRate; } else { oScene.Rate.qo[nWellIndex] = vecRate; } } - // 油水两相当前按定产油量处理,气、水产量数组保持等长零值。 - if(eSolverModelType == SMT_Oil_Water_TwoPhase) { - for(size_t nWellIndex = 0; - nWellIndex < oScene.Rate.t.size(); - ++nWellIndex) { - if(isCancellationRequested(pCancelRequested)) { - return false; - } - oScene.Rate.qg[nWellIndex].assign( - oScene.Rate.t[nWellIndex].size(), 0.0); - oScene.Rate.qw[nWellIndex].assign( - oScene.Rate.t[nWellIndex].size(), 0.0); - } - } - // 第三步:复制井筒储集、表皮和流量段索引。 oScene.CS.C.resize(vecSolverWellOrder.size()); oScene.CS.S.resize(vecSolverWellOrder.size()); @@ -1622,8 +1620,10 @@ bool nmCalculationPebiGrid::buildPebiScene( oScene.CS.C[nWellIndex] = oWellInput.m_dWellboreStorage; oScene.CS.S[nWellIndex] = oWellInput.m_dSkin; + // 观察井没有流量段时该字段不参与物理计算,仍写入合法的一基占位值。 oScene.wellFlowSectionIndex[nWellIndex] = - oWellInput.m_nFlowSectionIndex; + oWellInput.m_nFlowSectionIndex >= 1 + ? oWellInput.m_nFlowSectionIndex : 1; } oSnapshot.m_bValid = !isCancellationRequested(pCancelRequested); diff --git a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp index 7ffbb79e..079a4648 100644 --- a/Src/nmNum/nmData/nmDataAnalyzeManager.cpp +++ b/Src/nmNum/nmData/nmDataAnalyzeManager.cpp @@ -39,6 +39,7 @@ #include "nmDataPvtParaForPebi.h" #include "ZxDataWell.h" +#include "ZxDataGaugeBase.h" #include "ZxDataGaugeP.h" #include "ZxDataGaugeF.h" #include "zxSysUtils.h" @@ -124,7 +125,8 @@ static bool isWellParaOf(const WellParaDesc& desc, NM_WELL_MODEL eWellType) // 当前 PEBI 网格只支持这三类数值井。Map 中其他类型的图元不能进入求解井集合。 static bool isSupportedNumericalWell(const nmDataWellBase* pWellData) { - if(pWellData == nullptr || pWellData->getWellCode().isEmpty()) { + if(pWellData == nullptr || pWellData->getWellCode().isEmpty() || + !nmIsValidWellCategory(pWellData->getWellCategory())) { return false; } @@ -134,6 +136,85 @@ static bool isSupportedNumericalWell(const nmDataWellBase* pWellData) eWellType == NM_WELL_MODEL::Horizontal_Fractured_Well; } +// 将工程井井别转换为数值模块的稳定枚举。枚举值同时就是流量大字段索引。 +static NM_WELL_CATEGORY frameworkWellCategory(const ZxDataWell* pWellData) +{ + if(pWellData == nullptr) { + return NM_WellCategory_Unknown; + } + + const NM_WELL_CATEGORY eWellCategory = + static_cast(pWellData->getWellTypeIndex()); + return nmIsValidWellCategory(eWellCategory) + ? eWellCategory : NM_WellCategory_Unknown; +} + +// 从一条测量数据中复制二维点。流量调用方必须显式传入油、气、水井别索引。 +static bool readGaugePoints(ZxDataGaugeBase* pGaugeData, + int nObjectIndex, + QVector& vecPoints) +{ + vecPoints.clear(); + if(pGaugeData == nullptr) { + return false; + } + + VecDouble vecX; + VecDouble vecY; + if(!pGaugeData->getDataVecXY(vecX, vecY, nObjectIndex)) { + return false; + } + + const int nPointCount = qMin( + static_cast(vecX.size()), + static_cast(vecY.size())); + vecPoints.reserve(nPointCount); + for(int nIndex = 0; nIndex < nPointCount; ++nIndex) { + vecPoints.append(QPointF(vecX[nIndex], vecY[nIndex])); + } + return true; +} + +// 读取工程井的第一条压力和流量数据;井别是流量相别的唯一事实来源。 +static bool readFrameworkWellCurves( + ZxDataWell* pWellData, + QVector& vecPressurePoints, + QVector& vecFlowPoints, + NM_WELL_CATEGORY& eWellCategory) +{ + vecPressurePoints.clear(); + vecFlowPoints.clear(); + eWellCategory = frameworkWellCategory(pWellData); + if(pWellData == nullptr || !nmIsValidWellCategory(eWellCategory)) { + return false; + } + + ZxDataGaugeP* pGaugeP = nullptr; + const ZxDataObjectList listGaugeP = + pWellData->getChildren(iDataModelType::sTypeDataGaugeP); + for(int nIndex = 0; nIndex < listGaugeP.size(); ++nIndex) { + pGaugeP = dynamic_cast(listGaugeP[nIndex]); + if(pGaugeP != nullptr) { + break; + } + } + + ZxDataGaugeF* pGaugeF = nullptr; + const ZxDataObjectList listGaugeF = + pWellData->getChildren(iDataModelType::sTypeDataGaugeF); + for(int nIndex = 0; nIndex < listGaugeF.size(); ++nIndex) { + pGaugeF = dynamic_cast(listGaugeF[nIndex]); + if(pGaugeF != nullptr) { + break; + } + } + + // 压力数据仍读取二维主对象;流量必须使用井别对应的 0/1/2 大字段索引。 + readGaugePoints(pGaugeP, 0, vecPressurePoints); + readGaugePoints(pGaugeF, static_cast(eWellCategory), vecFlowPoints); + return true; +} + static bool isWellParaAttrName(const QString& name) { for (int i = 0; i < WELL_PARA_DESC_COUNT; ++i) { @@ -1363,61 +1444,14 @@ void nmDataAnalyzeManager::initCurWellData() // 判断是哪一种井类型,初始化对应的参数 QString wellClass = pWellData->getWellClassEn(); - // 获取当前井的压力、流量数据 - ZxDataObjectList m_listGaugeP = pWellData->getChildren(iDataModelType::sTypeDataGaugeP); - ZxDataObjectList m_listGaugeF = pWellData->getChildren(iDataModelType::sTypeDataGaugeF); - - ZxDataGaugeP* pGaugeP = nullptr; - ZxDataGaugeF* pGaugeF = nullptr; - - // 遍历压力数据列表 - for(int i = 0; i < m_listGaugeP.size(); ++i) { - if(pGaugeP = dynamic_cast(m_listGaugeP[i])) { // 拿到第一条压力数据 - break; - } - } - - // 遍历流量数据列表 - for(int i = 0; i < m_listGaugeF.size(); ++i) { - if(pGaugeF = dynamic_cast(m_listGaugeF[i])) { // 拿到第一条流量数据 - break; - } - } - - // 获取的压力、流量数据 - QVector vecPtsP, vecPtsF; - // 临时存储x,y坐标 - VecDouble vecX, vecY; - - if(pGaugeP != nullptr) { - // 获取压力数据 - if(pGaugeP->getDataVecXY(vecX, vecY)) { - int pointCount = vecX.size(); - if(pointCount > vecY.size()) { - pointCount = vecY.size(); - } - for(int i = 0; i < pointCount; ++i) { - QPointF pt(vecX[i], vecY[i]); - vecPtsP.append(pt); - } - } - } - - if(pGaugeF != nullptr) { - vecX.clear(); - vecY.clear(); - - // 获取流量数据 - if(pGaugeF->getDataVecXY(vecX, vecY)) { - int pointCount = vecX.size(); - if(pointCount > vecY.size()) { - pointCount = vecY.size(); - } - for(int i = 0; i < pointCount; ++i) { - QPointF pt(vecX[i], vecY[i]); - vecPtsF.append(pt); - } - } + QVector vecPtsP; + QVector vecPtsF; + NM_WELL_CATEGORY eWellCategory = NM_WellCategory_Unknown; + if(!readFrameworkWellCurves(pWellData, vecPtsP, vecPtsF, + eWellCategory)) { + qWarning() << "Cannot import numerical well with invalid category:" + << pWellData->getCode(); + return; } if(ZxBaseUtil::isSameStr(wellClass, "VerticalWell")) { @@ -1430,6 +1464,7 @@ void nmDataAnalyzeManager::initCurWellData() pVerticalWell->setWellName(pWellData->getName()); pVerticalWell->setWellCode(pWellData->getCode()); + pVerticalWell->setWellCategory(eWellCategory); nmDataAttribute tempAttr = pVerticalWell->getX(); tempAttr.setValue(pWellData->getLocationX()); pVerticalWell->setX(tempAttr); @@ -1468,6 +1503,7 @@ void nmDataAnalyzeManager::initCurWellData() pVFracturedWell->setWellName(pWellData->getName()); pVFracturedWell->setWellCode(pWellData->getCode()); + pVFracturedWell->setWellCategory(eWellCategory); nmDataAttribute tempAttr = pVFracturedWell->getX(); tempAttr.setValue(pWellData->getLocationX()); pVFracturedWell->setX(tempAttr); @@ -1509,6 +1545,7 @@ void nmDataAnalyzeManager::initCurWellData() pHFracturedWell->setWellName(pWellData->getName()); pHFracturedWell->setWellCode(pWellData->getCode()); + pHFracturedWell->setWellCategory(eWellCategory); nmDataAttribute tempAttr = pHFracturedWell->getX(); tempAttr.setValue(pWellData->getLocationX()); pHFracturedWell->setX(tempAttr); @@ -1549,7 +1586,7 @@ void nmDataAnalyzeManager::initCurWellData() const QString sPrimaryWellCode = m_pCurDataWell->getWellCode(); m_oNumericalAnalysisCase.setPrimaryWellCode(sPrimaryWellCode); m_oNumericalAnalysisCase.setPrimaryWellMode( - m_pCurDataWell->getFlowPoints().size() >= 2 + m_pCurDataWell->getFlowSegmentCount() > 0 ? NM_CaseWell_RateControlled : NM_CaseWell_Observation); } @@ -1593,23 +1630,20 @@ void nmDataAnalyzeManager::calculationLogData( vvecHistoryData[1].append(qpoint.y()); // y } - // Prepare flow-rate segment data. - QVector vecTimeQ = pWellData->getFlowPoints(); - if(vecPressure.isEmpty() || vecTimeQ.size() < 2) { + // 流动段索引采用一基;这里只使用去除可选 (0,0) 占位点后的真实段。 + const QVector vecTimeQ = pWellData->getFlowSegmentPoints(); + if(vecPressure.isEmpty() || vecTimeQ.isEmpty() || + !pWellData->hasValidFlowSectionIndex()) { return; } - int nTimeNumQ = vecTimeQ.size() - 1; - if (nTimeNumQ <= 0) { - // 没有流量段数据,无法计算双对数/半对数曲线 - return; - } + const int nTimeNumQ = vecTimeQ.size(); std::vector timeQ(nTimeNumQ); std::vector q(nTimeNumQ); for(int i = 0; i < nTimeNumQ; ++i) { - timeQ[i] = vecTimeQ[i + 1].x(); - q[i] = vecTimeQ[i + 1].y(); + timeQ[i] = vecTimeQ[i].x(); + q[i] = vecTimeQ[i].y(); } // 调用 DLL 计算双对数曲线 @@ -1666,71 +1700,18 @@ nmDataWellBase* nmDataAnalyzeManager::appendWellData(ZxDataWell* pWellData) return pExistingWell; } - // 判断是哪一种井类型,初始化对应的参数 - QString sWellClass = pWellData->getWellClassEn(); - - // 获取当前井的压力、流量数据 - ZxDataObjectList m_listGaugeP = pWellData->getChildren(iDataModelType::sTypeDataGaugeP); - ZxDataObjectList m_listGaugeF = pWellData->getChildren(iDataModelType::sTypeDataGaugeF); - - ZxDataGaugeP* pGaugeP = nullptr; - ZxDataGaugeF* pGaugeF = nullptr; - - // 遍历压力数据列表 - for(int i = 0; i < m_listGaugeP.size(); ++i) { - if(pGaugeP = dynamic_cast(m_listGaugeP[i])) { // 拿到第一条压力数据 - break; - } - } - - // 遍历流量数据列表 - for(int i = 0; i < m_listGaugeF.size(); ++i) { - if(pGaugeF = dynamic_cast(m_listGaugeF[i])) { // 拿到第一条流量数据 - break; - } - } - - // 获取的压力、流量数据 - QVector vecPtsP, vecPtsF; - // 临时存储x,y坐标 - VecDouble vecX, vecY; - - if(pGaugeP != nullptr) { - // 获取压力数据 - if(pGaugeP->getDataVecXY(vecX, vecY)) { - int pointCount = vecX.size(); - if(pointCount > vecY.size()) { - pointCount = vecY.size(); - } - for(int i = 0; i < pointCount; ++i) { - QPointF pt(vecX[i], vecY[i]); - vecPtsP.append(pt); - } - } - } - - if(pGaugeF != nullptr) { - vecX.clear(); - vecY.clear(); - - // 获取流量数据 - if(pGaugeF->getDataVecXY(vecX, vecY)) { - int pointCount = vecX.size(); - if(pointCount > vecY.size()) { - pointCount = vecY.size(); - } - for(int i = 0; i < pointCount; ++i) { - QPointF pt(vecX[i], vecY[i]); - vecPtsF.append(pt); - } - } + // 井别决定流量大字段;压力仍读取第一条压力数据的主对象。 + const QString sWellClass = pWellData->getWellClassEn(); + QVector vecPtsP; + QVector vecPtsF; + NM_WELL_CATEGORY eWellCategory = NM_WellCategory_Unknown; + if(!readFrameworkWellCurves(pWellData, vecPtsP, vecPtsF, + eWellCategory)) { + qWarning() << "Cannot append numerical well with invalid category:" + << pWellData->getCode(); + return nullptr; } - int nIndexF; //当前井的流动段索引 - - // 更新当前井的流动段的索引,默认为最后一段 - nIndexF = vecPtsF.count() - 1; - nmDataWellBase* pAddedWell = nullptr; if(ZxBaseUtil::isSameStr(sWellClass, "VerticalWell")) { // 初始化直井默认参数 @@ -1742,7 +1723,8 @@ nmDataWellBase* nmDataAnalyzeManager::appendWellData(ZxDataWell* pWellData) pAddedWell = pVerticalWell; pVerticalWell->setWellName(pWellData->getName()); - pVerticalWell->setWellCode(pWellData->getCode()); + pVerticalWell->setWellCode(pWellData->getCode()); + pVerticalWell->setWellCategory(eWellCategory); nmDataAttribute tempAttr = pVerticalWell->getX(); tempAttr.setValue(pWellData->getLocationX()); pVerticalWell->setX(tempAttr); @@ -1757,8 +1739,8 @@ nmDataWellBase* nmDataAnalyzeManager::appendWellData(ZxDataWell* pWellData) pVerticalWell->setPressurePoints(vecPtsP); pVerticalWell->setFlowPoints(vecPtsF); - // 设置流量段索引 - pVerticalWell->setIndexF(nIndexF); + // 其他井默认选择最后一个真实流动段,索引保持一基。 + pVerticalWell->setIndexF(pVerticalWell->getFlowSegmentCount()); } else if(ZxBaseUtil::isSameStr(sWellClass, "VerticalFracturedWell")) { // 初始化垂直裂缝井默认参数 @@ -1770,7 +1752,8 @@ nmDataWellBase* nmDataAnalyzeManager::appendWellData(ZxDataWell* pWellData) pAddedWell = pVFracturedWell; pVFracturedWell->setWellName(pWellData->getName()); - pVFracturedWell->setWellCode(pWellData->getCode()); + pVFracturedWell->setWellCode(pWellData->getCode()); + pVFracturedWell->setWellCategory(eWellCategory); nmDataAttribute tempAttr = pVFracturedWell->getX(); tempAttr.setValue(pWellData->getLocationX()); pVFracturedWell->setX(tempAttr); @@ -1787,8 +1770,8 @@ nmDataWellBase* nmDataAnalyzeManager::appendWellData(ZxDataWell* pWellData) // 更新裂缝位置信息 pVFracturedWell->setFracs(); - // 设置流量段索引 - pVFracturedWell->setIndexF(nIndexF); + // 其他井默认选择最后一个真实流动段,索引保持一基。 + pVFracturedWell->setIndexF(pVFracturedWell->getFlowSegmentCount()); } else if(ZxBaseUtil::isSameStr(sWellClass, "HorizontalMultiFracturedWell")) { // 初始化多段压裂水平井默认参数 @@ -1800,7 +1783,8 @@ nmDataWellBase* nmDataAnalyzeManager::appendWellData(ZxDataWell* pWellData) pAddedWell = pHFracturedWell; pHFracturedWell->setWellName(pWellData->getName()); - pHFracturedWell->setWellCode(pWellData->getCode()); + pHFracturedWell->setWellCode(pWellData->getCode()); + pHFracturedWell->setWellCategory(eWellCategory); nmDataAttribute tempAttr = pHFracturedWell->getX(); tempAttr.setValue(pWellData->getLocationX()); pHFracturedWell->setX(tempAttr); @@ -1818,8 +1802,8 @@ nmDataWellBase* nmDataAnalyzeManager::appendWellData(ZxDataWell* pWellData) // 计算裂缝数据 pHFracturedWell->setFracs(); - // 设置流量段索引 - pHFracturedWell->setIndexF(nIndexF); + // 其他井默认选择最后一个真实流动段,索引保持一基。 + pHFracturedWell->setIndexF(pHFracturedWell->getFlowSegmentCount()); } // 第二步:不支持的项目井型不进入 Map,也不发送伪造的新增井通知。 @@ -3655,7 +3639,8 @@ void nmDataAnalyzeManager::setIncludedCalculationWells( nmDataWellBase* pWellData = findWellByCode(oWellRef.m_sWellCode); if(isSupportedNumericalWell(pWellData) && oWellRef.m_sWellCode != getPrimaryWellCode() && - pWellData->getFlowPoints().size() >= 2) { + nmIsValidWellCategory(pWellData->getWellCategory()) && + pWellData->getFlowSegmentCount() > 0) { vecValidWells.append(nmCalculationWellRef( oWellRef.m_sWellCode, NM_CaseWell_RateControlled)); @@ -3725,7 +3710,9 @@ nmDataAnalyzeManager::getEffectiveCalculationWells() const continue; } - const bool bHasValidRate = pWellData->getFlowPoints().size() >= 2; + const bool bHasValidRate = + nmIsValidWellCategory(pWellData->getWellCategory()) && + pWellData->getFlowSegmentCount() > 0; if(!bHasValidRate) { vecEffectiveWells.append(nmCalculationWellRef( pWellData->getWellCode(), @@ -3998,6 +3985,7 @@ bool nmDataAnalyzeManager::commitPebiResultSnapshot( findWellByCode(pWell->m_sWellCode); if(pWell == NULL || pLiveWell == NULL || pLiveWell->getWellInstanceId() != pWell->m_sWellInstanceId || + pLiveWell->getWellCategory() != pWell->m_eWellCategory || setWellInstanceIds.contains(pWell->m_sWellInstanceId)) { sError = "PEBI result well identity is stale."; @@ -4470,7 +4458,8 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) // 新数据模型不兼容旧项目。明确校验版本,避免按井名或对象顺序猜测关联关系。 - const int nSupportedProjectVersion = 3; + const int nSupportedProjectVersion = + nmNumericalResultPersistence::projectVersion(); if(!doc.HasMember("NumericalProjectVersion") || !doc["NumericalProjectVersion"].IsInt() || doc["NumericalProjectVersion"].GetInt() != nSupportedProjectVersion) { @@ -4483,7 +4472,7 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) quint64 nSavedResultInputRevision = 0; if(!doc.HasMember("InputRevisions") || !doc["InputRevisions"].IsObject()) { - qWarning() << "Numerical v3 project has no input revisions:" << filePath; + qWarning() << "Numerical v4 project has no input revisions:" << filePath; return false; } const rapidjson::Value& oRevisionJson = doc["InputRevisions"]; @@ -4493,7 +4482,7 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) !oRevisionJson["BuiltGridRevision"].IsUint64() || !oRevisionJson.HasMember("ResultInputRevision") || !oRevisionJson["ResultInputRevision"].IsUint64()) { - qWarning() << "Numerical v3 project contains invalid input revisions:" + qWarning() << "Numerical v4 project contains invalid input revisions:" << filePath; return false; } @@ -4506,7 +4495,7 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) if(nSavedGridInputRevision == 0 || nSavedResultInputRevision == 0 || (nSavedBuiltGridRevision != 0 && nSavedBuiltGridRevision != nSavedGridInputRevision)) { - qWarning() << "Numerical v3 project input revision values are invalid:" + qWarning() << "Numerical v4 project input revision values are invalid:" << filePath; return false; } @@ -4518,7 +4507,7 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) QVector vecSavedIncludedWells; QVector vecSavedSolverOrder; - // 第一步:v3 必须完整保存当前分析方案,缺字段时不按旧项目规则猜测。 + // 第一步:v4 必须完整保存当前分析方案,缺字段时不按旧项目规则猜测。 if(!doc.HasMember("NumericalAnalysisCase") || !doc["NumericalAnalysisCase"].IsObject() || !doc.HasMember("Wells") || @@ -4574,7 +4563,9 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) !oWellJson.HasMember("WellInstanceId") || !oWellJson["WellInstanceId"].IsString() || !oWellJson.HasMember("WellType") || - !oWellJson["WellType"].IsInt()) { + !oWellJson["WellType"].IsInt() || + !oWellJson.HasMember("WellCategory") || + !oWellJson["WellCategory"].IsInt()) { qWarning() << "Numerical project contains an invalid well entry:" << filePath; return false; } @@ -4588,11 +4579,15 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) oWellInstanceUuid.toString().remove('{').remove('}'); const NM_WELL_MODEL eWellType = static_cast( oWellJson["WellType"].GetInt()); + const NM_WELL_CATEGORY eWellCategory = + static_cast( + oWellJson["WellCategory"].GetInt()); const bool bSupportedWellType = eWellType == NM_WELL_MODEL::Vertical_Well || eWellType == NM_WELL_MODEL::Vertical_Fractured_Well || eWellType == NM_WELL_MODEL::Horizontal_Fractured_Well; if(sWellCode.isEmpty() || !bSupportedWellType || + !nmIsValidWellCategory(eWellCategory) || mapSavedWellTypes.contains(sWellCode) || oWellInstanceUuid.isNull() || sNormalizedWellInstanceId != sWellInstanceId || @@ -4713,7 +4708,7 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) } } - // 前置校验已完成,从这里开始替换当前输入。旧快照要到全部 v3 载荷 + // 前置校验已完成,从这里开始替换当前输入。旧快照要到全部当前版本载荷 // 成功后才替换,加载失败或内存不足时仍保留上一套已发布快照。 // 清空现有数据,确保从头加载新数据 foreach(nmDataFracture* pFractureData, m_vFractureData) { @@ -5134,7 +5129,7 @@ bool nmDataAnalyzeManager::ReadProjectData(const QString & filePath) nSavedGridInputRevision, nSavedBuiltGridRevision, nSavedResultInputRevision)) { - qWarning() << "Failed to restore numerical v3 input revisions:" + qWarning() << "Failed to restore numerical v4 input revisions:" << filePath; return false; } @@ -5148,7 +5143,7 @@ bool nmDataAnalyzeManager::WriteProjectData( const nmNumericalWindowPayloadReferences* pPayloadReferences) { if(pPayloadReferences == NULL) { - qWarning() << "Numerical v3 project requires explicit payload references."; + qWarning() << "Numerical v4 project requires explicit payload references."; return false; } // 第一步:保存前校验 Map 井目录,持久化身份只允许非空且唯一的 WellCode。 @@ -5163,6 +5158,7 @@ bool nmDataAnalyzeManager::WriteProjectData( const QString sNormalizedWellInstanceId = oWellInstanceUuid.toString().remove('{').remove('}'); if(pWellData == nullptr || pWellData->getWellCode().isEmpty() || + !nmIsValidWellCategory(pWellData->getWellCategory()) || setWellCodes.contains(pWellData->getWellCode()) || oWellInstanceUuid.isNull() || sNormalizedWellInstanceId != sWellInstanceId || @@ -5238,7 +5234,8 @@ bool nmDataAnalyzeManager::WriteProjectData( doc.SetObject(); // 根节点是对象 rapidjson::Document::AllocatorType& allocator = doc.GetAllocator(); // 获取内存分配器 - doc.AddMember("NumericalProjectVersion", 3, allocator); + doc.AddMember("NumericalProjectVersion", + nmNumericalResultPersistence::projectVersion(), allocator); rapidjson::Value oRevisionJson(rapidjson::kObjectType); oRevisionJson.AddMember("GridInputRevision", m_oNumericalAnalysisCase.getGridInputRevision(), allocator); @@ -5879,18 +5876,40 @@ bool nmDataAnalyzeManager::loadNmResultV3InPlace( pWell->setHistorySemiLog(oHistory.m_vecSemiLog); } if(!mapLoadedHistories.isEmpty()) { - qWarning() << "Numerical v3 project contains unreferenced well histories."; + qWarning() << "Numerical v4 project contains unreferenced well histories."; return false; } - loadWellPreAndFlow(); + if(!loadWellPreAndFlow()) { + qWarning() << "Failed to reload numerical well pressure/rate data."; + return false; + } + // 结果快照保存的是求解时井别;加载时必须与当前工程井保持一致。 + if(!pLoadedCandidate.isNull()) { + for(int nIndex = 0; + nIndex < pLoadedCandidate->getWellCount(); ++nIndex) { + const nmPebiResultWellSnapshot* pSavedWell = + pLoadedCandidate->getWellAt(nIndex); + nmDataWellBase* pLiveWell = pSavedWell == nullptr + ? nullptr : findWellByCode(pSavedWell->m_sWellCode); + if(pSavedWell == nullptr || pLiveWell == nullptr || + pLiveWell->getWellInstanceId() != + pSavedWell->m_sWellInstanceId || + pLiveWell->getWellCategory() != + pSavedWell->m_eWellCategory) { + qWarning() << "Loaded PEBI result well category is stale."; + return false; + } + } + } const QVector vecIncludedWells = getIncludedCalculationWells(); for(int nIndex = 0; nIndex < vecIncludedWells.size(); ++nIndex) { nmDataWellBase* pWell = findWellByCode( vecIncludedWells[nIndex].m_sWellCode); if(!isSupportedNumericalWell(pWell) || - pWell->getFlowPoints().size() < 2) { + !nmIsValidWellCategory(pWell->getWellCategory()) || + pWell->getFlowSegmentCount() <= 0) { qWarning() << "Failed to restore included numerical well:" << vecIncludedWells[nIndex].m_sWellCode; return false; @@ -6026,98 +6045,51 @@ bool nmDataAnalyzeManager::ensureDirectoryExists(const QString & dirPath) return true; } -void nmDataAnalyzeManager::loadWellPreAndFlow() +bool nmDataAnalyzeManager::loadWellPreAndFlow() { - // 获取当前工区里所有的ZxDataWell井 + // 当前项目文件只保存井别和井身份,压力/流量仍从工程井节点重新读取。 if(zxCurProject == nullptr) { - return; + return false; } - ZxDataObjectList wellList = zxCurProject->getChildren(iDataModelType::sTypeWell); - - foreach(nmDataWellBase* pWell, m_vWellData) { - if(pWell == nullptr) { + const ZxDataObjectList listWells = + zxCurProject->getChildren(iDataModelType::sTypeWell); + QHash mapFrameworkWells; + for(int nIndex = 0; nIndex < listWells.size(); ++nIndex) { + ZxDataWell* pFrameworkWell = + dynamic_cast(listWells[nIndex]); + if(pFrameworkWell == nullptr || + pFrameworkWell->getCode().isEmpty() || + mapFrameworkWells.contains(pFrameworkWell->getCode())) { continue; } + mapFrameworkWells.insert(pFrameworkWell->getCode(), pFrameworkWell); + } - QString sWellCode = pWell->getWellCode(); - - ZxDataWell* pWellData = nullptr; - - for(int i = 0; i < wellList.size(); i++) { - ZxDataWell* pCandidateWell = dynamic_cast(wellList[i]); - - if(pCandidateWell != nullptr && - pCandidateWell->getCode() == sWellCode) { - pWellData = pCandidateWell; - break; - } + foreach(nmDataWellBase* pWell, m_vWellData) { + if(pWell == nullptr || pWell->getWellCode().isEmpty()) { + return false; } - // 拿到了井的数据 - if(pWellData) { - // 获取当前井的压力、流量数据 - ZxDataObjectList m_listGaugeP = pWellData->getChildren(iDataModelType::sTypeDataGaugeP); - ZxDataObjectList m_listGaugeF = pWellData->getChildren(iDataModelType::sTypeDataGaugeF); - - ZxDataGaugeP* pGaugeP = nullptr; - ZxDataGaugeF* pGaugeF = nullptr; - - // 遍历压力数据列表 - for(int i = 0; i < m_listGaugeP.size(); ++i) { - if(pGaugeP = dynamic_cast(m_listGaugeP[i])) { // 拿到第一条压力数据 - break; - } - } - - // 遍历流量数据列表 - for(int i = 0; i < m_listGaugeF.size(); ++i) { - if(pGaugeF = dynamic_cast(m_listGaugeF[i])) { // 拿到第一条流量数据 - break; - } - } - - // 获取的压力、流量数据 - QVector vecPtsP, vecPtsF; - // 临时存储x,y坐标 - VecDouble vecX, vecY; - - if(pGaugeP != nullptr) { - // 获取压力数据 - if(pGaugeP->getDataVecXY(vecX, vecY)) { - int pointCount = vecX.size(); - if(pointCount > vecY.size()) { - pointCount = vecY.size(); - } - for(int i = 0; i < pointCount; ++i) { - QPointF pt(vecX[i], vecY[i]); - vecPtsP.append(pt); - } - } - } - - if(pGaugeF != nullptr) { - vecX.clear(); - vecY.clear(); - - // 获取流量数据 - if(pGaugeF->getDataVecXY(vecX, vecY)) { - int pointCount = vecX.size(); - if(pointCount > vecY.size()) { - pointCount = vecY.size(); - } - for(int i = 0; i < pointCount; ++i) { - QPointF pt(vecX[i], vecY[i]); - vecPtsF.append(pt); - } - } - } - - // 设置井的压力数据、流量数据 - pWell->setPressurePoints(vecPtsP); - pWell->setFlowPoints(vecPtsF); + ZxDataWell* pFrameworkWell = + mapFrameworkWells.value(pWell->getWellCode(), nullptr); + QVector vecPressurePoints; + QVector vecFlowPoints; + NM_WELL_CATEGORY eFrameworkCategory = NM_WellCategory_Unknown; + if(!readFrameworkWellCurves(pFrameworkWell, + vecPressurePoints, + vecFlowPoints, + eFrameworkCategory) || + pWell->getWellCategory() != eFrameworkCategory) { + qWarning() << "Numerical well category does not match framework well:" + << pWell->getWellCode(); + return false; } + + pWell->setPressurePoints(vecPressurePoints); + pWell->setFlowPoints(vecFlowPoints); } + return true; } vtkSmartPointer nmDataAnalyzeManager::getUnstructuredGrid() const diff --git a/Src/nmNum/nmData/nmDataWellBase.cpp b/Src/nmNum/nmData/nmDataWellBase.cpp index eb0d77a4..9bbc63b6 100644 --- a/Src/nmNum/nmData/nmDataWellBase.cpp +++ b/Src/nmNum/nmData/nmDataWellBase.cpp @@ -12,7 +12,8 @@ #include nmDataWellBase::nmDataWellBase() - : m_nIndexF(0) + : m_eWellCategory(NM_WellCategory_Unknown) + , m_nIndexF(0) , m_bPlotVisible(true) , m_bTimeDependentSkin(false) , m_eWellType(Vertical_Well) @@ -111,6 +112,7 @@ nmDataWellBase& nmDataWellBase::operator=(const nmDataWellBase& other) m_sWellInstanceId = other.m_sWellInstanceId; m_wellName = other.m_wellName; m_wellCode = other.m_wellCode; + m_eWellCategory = other.m_eWellCategory; m_x = other.m_x; m_y = other.m_y; m_radius = other.m_radius; @@ -190,6 +192,9 @@ rapidjson::Value nmDataWellBase::ToJsonValue(rapidjson::Document::AllocatorType& // 序列化井类型 wellObject.AddMember("WellType", rapidjson::Value(static_cast(m_eWellType)), allocator); + // 井别决定流量大字段的油、气、水索引,v4 项目必须显式保存。 + wellObject.AddMember("WellCategory", + rapidjson::Value(static_cast(m_eWellCategory)), allocator); // 序列化 nmDataAttribute 类型的成员 // 调用 nmDataAttribute 自身的 ToJsonValue 方法进行递归序列化 @@ -256,7 +261,7 @@ void nmDataWellBase::FromJsonValue(const rapidjson::Value& jsonValue) m_wellCode = QString::fromUtf8(jsonValue["WellCode"].GetString()); } - // v3 已在 Manager 中完成格式校验,这里恢复求解时保存的稳定井身份。 + // 项目格式已在 Manager 中完成校验,这里恢复求解时保存的稳定井身份。 if(jsonValue.HasMember("WellInstanceId") && jsonValue["WellInstanceId"].IsString()) { restoreWellInstanceId(QString::fromUtf8( @@ -268,6 +273,13 @@ void nmDataWellBase::FromJsonValue(const rapidjson::Value& jsonValue) m_eWellType = static_cast(jsonValue["WellType"].GetInt()); } + // v4 不兼容旧项目,缺失或非法井别由 Manager 的严格结构校验直接拒绝。 + if(jsonValue.HasMember("WellCategory") && + jsonValue["WellCategory"].IsInt()) { + m_eWellCategory = static_cast( + jsonValue["WellCategory"].GetInt()); + } + // 反序列化 nmDataAttribute 类型的成员 // 调用 nmDataAttribute 自身的 FromJsonValue 方法进行递归反序列化 if(jsonValue.HasMember("X") && jsonValue["X"].IsObject()) { @@ -495,6 +507,17 @@ QString nmDataWellBase::getWellCode() const return m_wellCode; } +void nmDataWellBase::setWellCategory(NM_WELL_CATEGORY eWellCategory) +{ + m_eWellCategory = nmIsValidWellCategory(eWellCategory) + ? eWellCategory : NM_WellCategory_Unknown; +} + +NM_WELL_CATEGORY nmDataWellBase::getWellCategory() const +{ + return m_eWellCategory; +} + QString nmDataWellBase::getWellInstanceId() const { return m_sWellInstanceId; @@ -668,6 +691,33 @@ void nmDataWellBase::setFlowPoints(const QVector& points) m_vecPtsF = points; } +QVector nmDataWellBase::getFlowSegmentPoints() const +{ + QVector vecSegments = m_vecPtsF; + // 工程流量数据通常以 (0,0) 作为非业务占位点;真实数据没有占位点时不得误删首段。 + if(!vecSegments.isEmpty() && + vecSegments.first().x() == 0.0 && + vecSegments.first().y() == 0.0) { + vecSegments.remove(0); + } + return vecSegments; +} + +int nmDataWellBase::getFlowSegmentCount() const +{ + if(m_vecPtsF.isEmpty()) { + return 0; + } + return m_vecPtsF.first().x() == 0.0 && + m_vecPtsF.first().y() == 0.0 + ? m_vecPtsF.size() - 1 : m_vecPtsF.size(); +} + +bool nmDataWellBase::hasValidFlowSectionIndex() const +{ + return m_nIndexF >= 1 && m_nIndexF <= getFlowSegmentCount(); +} + int nmDataWellBase::getIndexF() const { return m_nIndexF; @@ -1433,29 +1483,18 @@ bool nmDataWellBase::exportHistoryLogLogToCsv(const QString& outDir) bool nmDataWellBase::exportHistoryRateToCsv(const QString& outDir) { - // 1) 获取流量点 (t, q) - QVector vecTimeQ = this->getFlowPoints(); + // 1) 获取真实流量段 (t, q),不把可选 (0,0) 占位点写入文件。 + const QVector vecTimeQ = this->getFlowSegmentPoints(); - if(vecTimeQ.size() < 2) { + if(vecTimeQ.isEmpty() || !hasValidFlowSectionIndex() || + !nmIsValidWellCategory(m_eWellCategory)) { return false; } - // 2) 移除第一个点 (0,0) - if(!vecTimeQ.isEmpty() - && vecTimeQ[0].x() == 0.0 - && vecTimeQ[0].y() == 0.0) - { - vecTimeQ.remove(0); - } - - if(vecTimeQ.size() < 2) { - return false; - } - - // 3) sectionIndex(流动段索引) + // 2) sectionIndex(流动段一基索引) int sectionIndex = this->getIndexF(); - // 4) 输出文件名 + // 3) 输出文件名 QString wellName = this->getWellName(); if(wellName.isEmpty()) wellName = "UnknownWell"; @@ -1468,10 +1507,13 @@ bool nmDataWellBase::exportHistoryRateToCsv(const QString& outDir) QTextStream out(&file); - // 5) 写表头 - out << "t,qo,sectionIndex\n"; + // 4) 表头与井别保持一致,便于离线排查实际进入求解器的相别。 + const char* pRateColumn = m_eWellCategory == NM_WellCategory_Gas + ? "qg" : (m_eWellCategory == NM_WellCategory_Water + ? "qw" : "qo"); + out << "t," << pRateColumn << ",sectionIndex\n"; - // 6) 写入每个点 + // 5) 写入每个点 for(int i = 0; i < vecTimeQ.size(); ++i) { double ti = vecTimeQ[i].x(); diff --git a/Src/nmNum/nmData/nmNumericalResultPersistence.cpp b/Src/nmNum/nmData/nmNumericalResultPersistence.cpp index 306e6650..29e9fc3d 100644 --- a/Src/nmNum/nmData/nmNumericalResultPersistence.cpp +++ b/Src/nmNum/nmData/nmNumericalResultPersistence.cpp @@ -204,7 +204,8 @@ nmNumericalWindowPayloadReferences::nmNumericalWindowPayloadReferences() int nmNumericalResultPersistence::projectVersion() { - return 3; + // v4 强制保存井别,旧版本不再按默认油相解释流量字段。 + return 4; } quint64 nmNumericalResultPersistence::maximumBinaryPayloadBytes() diff --git a/Src/nmNum/nmData/nmPebiResultSnapshot.cpp b/Src/nmNum/nmData/nmPebiResultSnapshot.cpp index af22721a..623a2dda 100644 --- a/Src/nmNum/nmData/nmPebiResultSnapshot.cpp +++ b/Src/nmNum/nmData/nmPebiResultSnapshot.cpp @@ -10,12 +10,14 @@ nmPebiResultSolverSlot::nmPebiResultSolverSlot() : m_nSolverIndex(-1), m_eEntryKind(NM_SolverEntry_Well), - m_eWellType(Unknow_Well) + m_eWellType(Unknow_Well), + m_eWellCategory(NM_WellCategory_Unknown) { } nmPebiResultWellSnapshot::nmPebiResultWellSnapshot() : m_eWellType(Unknow_Well), + m_eWellCategory(NM_WellCategory_Unknown), m_eWellMode(NM_CaseWell_Observation), m_bHasPerforation(false), m_bHasSkin(false), diff --git a/Src/nmNum/nmData/nmPebiResultSnapshotBuilder.cpp b/Src/nmNum/nmData/nmPebiResultSnapshotBuilder.cpp index d29c3df4..822b3506 100644 --- a/Src/nmNum/nmData/nmPebiResultSnapshotBuilder.cpp +++ b/Src/nmNum/nmData/nmPebiResultSnapshotBuilder.cpp @@ -523,6 +523,7 @@ bool nmPebiResultSnapshotBuilder::takeWell( qSwap(oTarget.m_sWellCode, oWell.m_sWellCode); qSwap(oTarget.m_sWellName, oWell.m_sWellName); oTarget.m_eWellType = oWell.m_eWellType; + oTarget.m_eWellCategory = oWell.m_eWellCategory; oTarget.m_eWellMode = oWell.m_eWellMode; oTarget.m_oLocation = oWell.m_oLocation; oTarget.m_bHasPerforation = oWell.m_bHasPerforation; @@ -547,6 +548,7 @@ bool nmPebiResultSnapshotBuilder::takeWell( // 清除剩余值,保证调用方不再保留候选井的可写语义。 oWell.m_eWellType = Unknow_Well; + oWell.m_eWellCategory = NM_WellCategory_Unknown; oWell.m_eWellMode = NM_CaseWell_Observation; oWell.m_oLocation = QPointF(); oWell.m_bHasPerforation = false; @@ -720,6 +722,7 @@ bool nmPebiResultSnapshotBuilder::validate(QString* pError) if(!isValidInstanceId(oWell.m_sWellInstanceId) || oWell.m_sWellCode.isEmpty() || oWell.m_sWellName.isEmpty() || oWell.m_eWellType == Unknow_Well || + !nmIsValidWellCategory(oWell.m_eWellCategory) || (oWell.m_eWellMode != NM_CaseWell_RateControlled && oWell.m_eWellMode != NM_CaseWell_Observation) || !isFiniteValue(oWell.m_oLocation.x()) || @@ -759,7 +762,8 @@ bool nmPebiResultSnapshotBuilder::validate(QString* pError) } if(oSlot.m_eEntryKind == NM_SolverEntry_ManualFracture) { - if(!oSlot.m_sWellInstanceId.isEmpty()) + if(!oSlot.m_sWellInstanceId.isEmpty() || + oSlot.m_eWellCategory != NM_WellCategory_Unknown) { return fail("Manual fracture slot has a well identity.", pError); } @@ -770,6 +774,8 @@ bool nmPebiResultSnapshotBuilder::validate(QString* pError) m_pCandidate->findWell(oSlot.m_sWellInstanceId); if(pWell == NULL || pWell->m_sWellCode != oSlot.m_sWellCode || pWell->m_eWellType != oSlot.m_eWellType || + pWell->m_eWellCategory != oSlot.m_eWellCategory || + !nmIsValidWellCategory(oSlot.m_eWellCategory) || setSlotWellIds.contains(oSlot.m_sWellInstanceId)) { return fail("PEBI solver slot well mapping is invalid.", pError); diff --git a/Src/nmNum/nmData/nmPebiResultSnapshotSerializer.cpp b/Src/nmNum/nmData/nmPebiResultSnapshotSerializer.cpp index d84b56e1..27163ed6 100644 --- a/Src/nmNum/nmData/nmPebiResultSnapshotSerializer.cpp +++ b/Src/nmNum/nmData/nmPebiResultSnapshotSerializer.cpp @@ -528,7 +528,7 @@ bool parseSnapshotJson( if(!oDocument.IsObject() || !oDocument.HasMember("SnapshotFormatVersion") || !oDocument["SnapshotFormatVersion"].IsInt() || - oDocument["SnapshotFormatVersion"].GetInt() != 1 || + oDocument["SnapshotFormatVersion"].GetInt() != 2 || !oDocument.HasMember("ResultCellCount") || !oDocument["ResultCellCount"].IsUint64() || !oDocument.HasMember("PressureFrameCount") || @@ -692,7 +692,8 @@ bool nmPebiResultSnapshotSerializer::save( oDocument.SetObject(); rapidjson::Document::AllocatorType& oAllocator = oDocument.GetAllocator(); - oDocument.AddMember("SnapshotFormatVersion", 1, oAllocator); + // v2 增加井别元数据;压力帧和井曲线二进制格式保持不变。 + oDocument.AddMember("SnapshotFormatVersion", 2, oAllocator); oDocument.AddMember("SnapshotId", toJsonString(pSnapshot->m_sSnapshotId, oAllocator), oAllocator); oDocument.AddMember("GridInputRevision", @@ -726,6 +727,8 @@ bool nmPebiResultSnapshotSerializer::save( oJson.AddMember("WellCode", toJsonString(oSlot.m_sWellCode, oAllocator), oAllocator); oJson.AddMember("WellType", static_cast(oSlot.m_eWellType), oAllocator); + oJson.AddMember("WellCategory", static_cast( + oSlot.m_eWellCategory), oAllocator); oSlots.PushBack(oJson, oAllocator); } oDocument.AddMember("SolverSlots", oSlots, oAllocator); @@ -742,6 +745,8 @@ bool nmPebiResultSnapshotSerializer::save( oJson.AddMember("WellName", toJsonString( oWell.m_sWellName, oAllocator), oAllocator); oJson.AddMember("WellType", static_cast(oWell.m_eWellType), oAllocator); + oJson.AddMember("WellCategory", static_cast( + oWell.m_eWellCategory), oAllocator); oJson.AddMember("WellMode", static_cast(oWell.m_eWellMode), oAllocator); oJson.AddMember("X", oWell.m_oLocation.x(), oAllocator); oJson.AddMember("Y", oWell.m_oLocation.y(), oAllocator); @@ -979,9 +984,11 @@ bool nmPebiResultSnapshotSerializer::load( nmPebiResultSolverSlot oSlot; int nEntryKind = 0; int nWellType = 0; + int nWellCategory = 0; if(!readRequiredInt(oJson, "SolverIndex", oSlot.m_nSolverIndex) || !readRequiredInt(oJson, "EntryKind", nEntryKind) || !readRequiredInt(oJson, "WellType", nWellType) || + !readRequiredInt(oJson, "WellCategory", nWellCategory) || !oJson.HasMember("WellInstanceId") || !oJson["WellInstanceId"].IsString() || !oJson.HasMember("WellCode") || !oJson["WellCode"].IsString()) @@ -990,6 +997,8 @@ bool nmPebiResultSnapshotSerializer::load( } oSlot.m_eEntryKind = static_cast(nEntryKind); oSlot.m_eWellType = static_cast(nWellType); + oSlot.m_eWellCategory = static_cast( + nWellCategory); oSlot.m_sWellInstanceId = QString::fromUtf8( oJson["WellInstanceId"].GetString()); oSlot.m_sWellCode = QString::fromUtf8(oJson["WellCode"].GetString()); @@ -1011,6 +1020,7 @@ bool nmPebiResultSnapshotSerializer::load( const rapidjson::Value& oJson = oWells[nIndex]; nmPebiResultWellSnapshot oWell; int nWellType = 0; + int nWellCategory = 0; int nWellMode = 0; if(!oJson.IsObject() || !oJson.HasMember("WellInstanceId") || @@ -1018,6 +1028,7 @@ bool nmPebiResultSnapshotSerializer::load( !oJson.HasMember("WellCode") || !oJson["WellCode"].IsString() || !oJson.HasMember("WellName") || !oJson["WellName"].IsString() || !readRequiredInt(oJson, "WellType", nWellType) || + !readRequiredInt(oJson, "WellCategory", nWellCategory) || !readRequiredInt(oJson, "WellMode", nWellMode) || !readRequiredDouble(oJson, "X", oWell.m_oLocation.rx()) || !readRequiredDouble(oJson, "Y", oWell.m_oLocation.ry()) || @@ -1036,6 +1047,8 @@ bool nmPebiResultSnapshotSerializer::load( oWell.m_sWellCode = QString::fromUtf8(oJson["WellCode"].GetString()); oWell.m_sWellName = QString::fromUtf8(oJson["WellName"].GetString()); oWell.m_eWellType = static_cast(nWellType); + oWell.m_eWellCategory = static_cast( + nWellCategory); oWell.m_eWellMode = static_cast(nWellMode); if(setJsonWellIds.contains(oWell.m_sWellInstanceId) || !mapCurves.contains(oWell.m_sWellInstanceId)) diff --git a/Src/nmNum/nmPlot/nmGuiPlot.cpp b/Src/nmNum/nmPlot/nmGuiPlot.cpp index 61134298..77df26ca 100644 --- a/Src/nmNum/nmPlot/nmGuiPlot.cpp +++ b/Src/nmNum/nmPlot/nmGuiPlot.cpp @@ -1,4 +1,4 @@ -#include "ZxBaseUtil.h" +#include "ZxBaseUtil.h" #include "zxLogInstance.h" #include @@ -254,6 +254,14 @@ void nmGuiPlot::initDefultGeoObj() // 判断是哪一种井类型,初始化对应的参数 QString wellClass = pWellData->getWellClassEn(); + const NM_WELL_CATEGORY eWellCategory = + static_cast( + pWellData->getWellTypeIndex()); + if(!nmIsValidWellCategory(eWellCategory)) { + this->setPlotsByDataManger( + nmDataAnalyzeManager::getCurrentInstance()); + return; + } // 获取当前井的压力、流量数据 ZxDataObjectList m_listGaugeP = pWellData->getChildren(iDataModelType::sTypeDataGaugeP); @@ -285,7 +293,7 @@ void nmGuiPlot::initDefultGeoObj() if(pGaugeP != nullptr) { // 获取压力数据 - if(pGaugeP->getDataVecXY(vecX, vecY)) { + if(pGaugeP->getDataVecXY(vecX, vecY, 0)) { for(int i = 0; i < vecX.size(); ++i) { if(i < vecY.size()) { QPointF pt(vecX[i], vecY[i]); @@ -300,7 +308,9 @@ void nmGuiPlot::initDefultGeoObj() vecY.clear(); // 获取流量数据 - if(pGaugeF->getDataVecXY(vecX, vecY)) { + // 井别是流量大字段索引,单相井也不能固定读取默认的油相字段。 + if(pGaugeF->getDataVecXY( + vecX, vecY, static_cast(eWellCategory))) { for(int i = 0; i < vecX.size(); ++i) { if(i < vecY.size()) { QPointF pt(vecX[i], vecY[i]); @@ -327,6 +337,7 @@ void nmGuiPlot::initDefultGeoObj() m_VerticalWell->setWellName(pWellData->getName()); m_VerticalWell->setWellCode(pWellData->getCode()); + m_VerticalWell->setWellCategory(eWellCategory); nmDataAttribute tempAttr = m_VerticalWell->getX(); tempAttr.setValue(pWellData->getLocationX()); m_VerticalWell->setX(tempAttr); @@ -364,6 +375,7 @@ void nmGuiPlot::initDefultGeoObj() m_VFracturedWell->setWellName(pWellData->getName()); m_VFracturedWell->setWellCode(pWellData->getCode()); + m_VFracturedWell->setWellCategory(eWellCategory); nmDataAttribute tempAttr = m_VFracturedWell->getX(); tempAttr.setValue(pWellData->getLocationX()); m_VFracturedWell->setX(tempAttr); @@ -404,6 +416,7 @@ void nmGuiPlot::initDefultGeoObj() m_HFracturedWell->setWellName(pWellData->getName()); m_HFracturedWell->setWellCode(pWellData->getCode()); + m_HFracturedWell->setWellCategory(eWellCategory); nmDataAttribute tempAttr = m_HFracturedWell->getX(); tempAttr.setValue(pWellData->getLocationX()); m_HFracturedWell->setX(tempAttr); diff --git a/Src/nmNum/nmPlot/nmPlotGraphicBinder.cpp b/Src/nmNum/nmPlot/nmPlotGraphicBinder.cpp index 14988bd4..60354984 100644 --- a/Src/nmNum/nmPlot/nmPlotGraphicBinder.cpp +++ b/Src/nmNum/nmPlot/nmPlotGraphicBinder.cpp @@ -1747,6 +1747,12 @@ bool nmPlotGraphicBinder::ensureSelectedWellData(nmObjPointWell* pGraphic, return false; } + const NM_WELL_CATEGORY eWellCategory = + static_cast(pWellData->getWellTypeIndex()); + if(!nmIsValidWellCategory(eWellCategory)) { + return false; + } + pData = createWellForGraphic(pGraphic, wellType); if (pData == nullptr) { return false; @@ -1755,6 +1761,7 @@ bool nmPlotGraphicBinder::ensureSelectedWellData(nmObjPointWell* pGraphic, // 重新勾选井时在这里补建nmDataWell,并将工程树中的基础属性同步进去。 pData->setWellName(pWellData->getName()); pData->setWellCode(pWellData->getCode()); + pData->setWellCategory(eWellCategory); nmDataAttribute tempAttr = pData->getX(); tempAttr.setValue(pWellData->getLocationX()); pData->setX(tempAttr); @@ -1785,7 +1792,7 @@ bool nmPlotGraphicBinder::ensureSelectedWellData(nmObjPointWell* pGraphic, QVector pressurePoints, flowPoints; VecDouble vecX, vecY; - if(pGaugeP != nullptr && pGaugeP->getDataVecXY(vecX, vecY)) { + if(pGaugeP != nullptr && pGaugeP->getDataVecXY(vecX, vecY, 0)) { for(int i = 0; i < vecX.size(); ++i) { if(i < vecY.size()) { pressurePoints.append(QPointF(vecX[i], vecY[i])); @@ -1795,7 +1802,10 @@ bool nmPlotGraphicBinder::ensureSelectedWellData(nmObjPointWell* pGraphic, vecX.clear(); vecY.clear(); - if(pGaugeF != nullptr && pGaugeF->getDataVecXY(vecX, vecY)) { + // 流量大字段的 0/1/2 分别对应油井、气井和水井,禁止使用默认索引猜测。 + if(pGaugeF != nullptr && + pGaugeF->getDataVecXY(vecX, vecY, + static_cast(eWellCategory))) { for(int i = 0; i < vecX.size(); ++i) { if(i < vecY.size()) { flowPoints.append(QPointF(vecX[i], vecY[i])); @@ -1803,17 +1813,21 @@ bool nmPlotGraphicBinder::ensureSelectedWellData(nmObjPointWell* pGraphic, } } - int indexF = -2; + int nFlowSectionIndex = -2; if(pWellData != zxCurWell) { - // 非当前查看井默认使用最后一段流量段。 - indexF = flowPoints.count() - 1; + // 非当前查看井在设置流量点后选择最后一个真实段,索引保持一基。 + nFlowSectionIndex = flowPoints.size(); + if(!flowPoints.isEmpty() && flowPoints.first().x() == 0.0 && + flowPoints.first().y() == 0.0) { + --nFlowSectionIndex; + } } pData->setPressurePoints(pressurePoints); pData->setFlowPoints(flowPoints); - if(indexF >= -1) { - pData->setIndexF(indexF); + if(nFlowSectionIndex >= -1) { + pData->setIndexF(nFlowSectionIndex); } if (wellType == NM_WELL_MODEL::Vertical_Fractured_Well) { diff --git a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp index e0e3a25f..f1b04b84 100644 --- a/Src/nmNum/nmSubWnd/nmSubWndMain.cpp +++ b/Src/nmNum/nmSubWnd/nmSubWndMain.cpp @@ -1611,7 +1611,9 @@ void nmSubWndMain::solveAndAnalyze() } bHasRateControlledWell = true; - if(pWellData->getFlowPoints().size() < 2) { + if(!nmIsValidWellCategory(pWellData->getWellCategory()) || + pWellData->getFlowSegmentCount() <= 0 || + !pWellData->hasValidFlowSectionIndex()) { QMessageBox::warning( this, tr("solver error"), @@ -1619,6 +1621,16 @@ 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/nmWellEditorSession.cpp b/Src/nmNum/nmSubWxs/nmWellEditorSession.cpp index 7f4158e1..ffb1d761 100644 --- a/Src/nmNum/nmSubWxs/nmWellEditorSession.cpp +++ b/Src/nmNum/nmSubWxs/nmWellEditorSession.cpp @@ -354,6 +354,8 @@ bool nmWellEditorSession::copySharedWellData( } pTarget->setWellName(pSource->getWellName()); pTarget->setWellCode(pSource->getWellCode()); + // 井型切换只改变几何模型,工程井井别和流量相别必须保持不变。 + pTarget->setWellCategory(pSource->getWellCategory()); pTarget->setX(pSource->getX()); pTarget->setY(pSource->getY()); pTarget->setRadius(pSource->getRadius()); diff --git a/Src/nmNum/nmSubWxs/nmWxEditWellPlot.cpp b/Src/nmNum/nmSubWxs/nmWxEditWellPlot.cpp index d8fcbeb8..c6116831 100644 --- a/Src/nmNum/nmSubWxs/nmWxEditWellPlot.cpp +++ b/Src/nmNum/nmSubWxs/nmWxEditWellPlot.cpp @@ -1,4 +1,4 @@ -#include "nmWxEditWellPlot.h" +#include "nmWxEditWellPlot.h" #include "nmWxNumericalDesign.h" #include "nmWxParameterProperty.h" @@ -787,6 +787,7 @@ void nmWxEditWellPlot::onWellTypeChanged(int index) // 历史数据 pNewWellData->setPressurePoints(pOldWellData->getPressurePoints()); pNewWellData->setFlowPoints(pOldWellData->getFlowPoints()); + pNewWellData->setWellCategory(pOldWellData->getWellCategory()); pNewWellData->setIndexF(pOldWellData->getIndexF()); // 结果数据 diff --git a/Src/nmNum/nmSubWxs/nmWxIncludeOtherWells.cpp b/Src/nmNum/nmSubWxs/nmWxIncludeOtherWells.cpp index ce2491bb..c121296a 100644 --- a/Src/nmNum/nmSubWxs/nmWxIncludeOtherWells.cpp +++ b/Src/nmNum/nmSubWxs/nmWxIncludeOtherWells.cpp @@ -93,7 +93,7 @@ void nmWxIncludeOtherWells::setupTable() QString sPrimaryWellCode = m_pDataManager != nullptr ? m_pDataManager->getPrimaryWellCode() : QString(); - int displayRow = 0; + int nDisplayRow = 0; // 第一步:候选来源是 Map,而不是整个项目井库。该对话框只管理 // 其他有产量的生产/注入井;主井和无产量观察井都不在这里显示。 for(int i = 0; i < vecMapWells.size(); i++) { @@ -103,40 +103,49 @@ void nmWxIncludeOtherWells::setupTable() } const bool bIsPrimary = pWell->getWellCode() == sPrimaryWellCode; - const bool bHasValidRate = pWell->getFlowPoints().size() >= 2; + const bool bHasValidRate = + nmIsValidWellCategory(pWell->getWellCategory()) && + pWell->getFlowSegmentCount() > 0; const bool bHasValidPressure = pWell->getPressurePoints().size() >= 2; if(bIsPrimary || !bHasValidRate) { continue; } - WellDataRow wellRow; - wellRow.m_sWellCode = pWell->getWellCode(); - wellRow.m_sName = pWell->getWellName(); - wellRow.m_bIsPrimary = bIsPrimary; + WellDataRow oWellRow; + oWellRow.m_sWellCode = pWell->getWellCode(); + oWellRow.m_sName = pWell->getWellName(); + oWellRow.m_bIsPrimary = bIsPrimary; + oWellRow.m_eWellCategory = pWell->getWellCategory(); // 第二步:读取这口有产量干扰井上次确认的包含状态。 - wellRow.m_bCanCalculate = true; - wellRow.m_bIsIncluded = + oWellRow.m_bCanCalculate = true; + oWellRow.m_bIsIncluded = m_pDataManager != nullptr && m_pDataManager->getNumericalAnalysisCase()->isIncludedWell( - wellRow.m_sWellCode); - - // 第三步:IncludedWells 中的候选固定是有产量主动井。 - wellRow.m_sPressure = bHasValidPressure ? "History" : "None"; - wellRow.m_sOilProd = "Rate"; - wellRow.m_eMode = NM_CaseWell_RateControlled; - - // 气相和水相暂时设为None - wellRow.m_sGasProd = "None"; - wellRow.m_sWaterProd = "None"; + 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) { + oWellRow.m_sOilProd = tr("Rate"); + } else if(oWellRow.m_eWellCategory == NM_WellCategory_Gas) { + oWellRow.m_sGasProd = tr("Rate"); + } else if(oWellRow.m_eWellCategory == NM_WellCategory_Water) { + oWellRow.m_sWaterProd = tr("Rate"); + } + oWellRow.m_eMode = NM_CaseWell_RateControlled; // 添加到数据列表和表格 - m_wellData.append(wellRow); - addWellDataToTable(wellRow, displayRow); - displayRow++; + m_vecWellData.append(oWellRow); + addWellDataToTable(oWellRow, nDisplayRow); + ++nDisplayRow; } - m_pTableWidget->setRowCount(displayRow); // 调整实际显示的行数 + m_pTableWidget->setRowCount(nDisplayRow); // 调整实际显示的行数 m_pTableWidget->horizontalHeader()->setStretchLastSection(true); m_pTableWidget->horizontalHeader()->setResizeMode(QHeaderView::Stretch); } @@ -147,6 +156,7 @@ void nmWxIncludeOtherWells::addWellDataToTable(const WellDataRow& data, int row) QTableWidgetItem* nameItem = new QTableWidgetItem(data.m_sName); nameItem->setFlags(nameItem->flags() & ~Qt::ItemIsEditable); nameItem->setData(Qt::UserRole, data.m_sWellCode); // 表格内部始终携带 WellCode + nameItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); m_pTableWidget->setItem(row, 0, nameItem); // Included列 - 使用自定义复选框组件 @@ -180,6 +190,7 @@ void nmWxIncludeOtherWells::addWellDataToTable(const WellDataRow& data, int row) QTableWidgetItem* item = new QTableWidgetItem(listOtherData[col-2]); item->setFlags(item->flags() & ~Qt::ItemIsEditable); item->setData(Qt::UserRole, row); + item->setTextAlignment(Qt::AlignCenter); m_pTableWidget->setItem(row, col, item); } } @@ -192,18 +203,18 @@ void nmWxIncludeOtherWells::onCheckBoxStateChanged(int state) } const int nRow = pCheckBox->property("row").toInt(); - if(nRow < 0 || nRow >= m_wellData.size()) { + if(nRow < 0 || nRow >= m_vecWellData.size()) { return; } // 主井正常不会进入本表;保留该判断作为防御,避免未来改动误取消主井。 - if(m_wellData[nRow].m_bIsPrimary) { - m_wellData[nRow].m_bIsIncluded = true; + if(m_vecWellData[nRow].m_bIsPrimary) { + m_vecWellData[nRow].m_bIsIncluded = true; return; } // 其他候选井只记录选择状态,绝不在该对话框内创建或删除 Map 井。 - m_wellData[nRow].m_bIsIncluded = (state == Qt::Checked); + m_vecWellData[nRow].m_bIsIncluded = (state == Qt::Checked); m_pTableWidget->selectRow(nRow); } @@ -213,7 +224,7 @@ void nmWxIncludeOtherWells::onItemClicked(QTableWidgetItem* item) QVector nmWxIncludeOtherWells::getWellData() const { - return m_wellData; + return m_vecWellData; } void nmWxIncludeOtherWells::setupButtons() diff --git a/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp b/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp index a70dc840..3e781e46 100644 --- a/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp +++ b/Src/nmNum/nmSubWxs/nmWxNumericalDesign.cpp @@ -1098,7 +1098,8 @@ void nmWxNumericalDesign::onOptionsIconClicked() continue; } - if(pWellData->getFlowPoints().size() < 2) { + if(!nmIsValidWellCategory(pWellData->getWellCategory()) || + pWellData->getFlowSegmentCount() <= 0) { // 对话框打开后若产量数据发生变化,提交时仍要阻止它混入包含列表。 continue; } @@ -1214,7 +1215,9 @@ void nmWxNumericalDesign::updateIncludeOtherWellsAvailability() continue; } - const bool bHasRateTarget = pWellData->getFlowPoints().size() >= 2; + const bool bHasRateTarget = + nmIsValidWellCategory(pWellData->getWellCategory()) && + pWellData->getFlowSegmentCount() > 0; if(bHasRateTarget) { bHasEligibleOtherWell = true; break; diff --git a/Src/nmNum/nmSubWxs/nmWxWellEditor.cpp b/Src/nmNum/nmSubWxs/nmWxWellEditor.cpp index 03a9929a..aab9c1f9 100644 --- a/Src/nmNum/nmSubWxs/nmWxWellEditor.cpp +++ b/Src/nmNum/nmSubWxs/nmWxWellEditor.cpp @@ -51,6 +51,7 @@ nmWxWellEditor::nmWxWellEditor(const nmWellEditContext& oContext, m_pWellEditorController(nullptr), m_pWellCodeLineEdit(nullptr), m_pWellNameLineEdit(nullptr), + m_pWellCategoryLineEdit(nullptr), m_pWellTypeComboBox(nullptr), m_pTimeDependentCheckBox(nullptr), m_pTimeDependentButton(nullptr), @@ -105,6 +106,22 @@ bool nmWxWellEditor::initialize(QString& sError) // 第一步:同步井信息和时间变表皮控件。 m_pWellCodeLineEdit->setText(pWell->getWellCode()); m_pWellNameLineEdit->setText(pWell->getWellName()); + QString sWellCategory; + switch(pWell->getWellCategory()) { + case NM_WellCategory_Oil: + sWellCategory = tr("Oil well"); + break; + case NM_WellCategory_Gas: + sWellCategory = tr("Gas well"); + break; + case NM_WellCategory_Water: + sWellCategory = tr("Water well"); + break; + default: + sWellCategory = tr("Unknown"); + break; + } + m_pWellCategoryLineEdit->setText(sWellCategory); int nWellTypeIndex = m_pWellTypeComboBox->findData( static_cast(pWell->getWellType())); m_bChangingWellType = true; @@ -645,25 +662,31 @@ QWidget* nmWxWellEditor::createWellInformationWidget(QWidget* pParent) QLabel* pCodeLabel = new QLabel(tr("Well number"), pInformationGroup); QLabel* pNameLabel = new QLabel(tr("Well name"), pInformationGroup); - QLabel* pTypeLabel = new QLabel(tr("Well type"), pInformationGroup); + QLabel* pCategoryLabel = new QLabel( + tr("Well category"), pInformationGroup); + QLabel* pTypeLabel = new QLabel(tr("Well model"), pInformationGroup); QLabel* pTimeDependentLabel = new QLabel( tr("Time-dependent skin"), pInformationGroup); pCodeLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); pNameLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); + pCategoryLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); pTypeLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); pTimeDependentLabel->setAlignment( Qt::AlignRight | Qt::AlignVCenter); m_pWellCodeLineEdit = new QLineEdit(pInformationGroup); m_pWellNameLineEdit = new QLineEdit(pInformationGroup); + m_pWellCategoryLineEdit = new QLineEdit(pInformationGroup); m_pWellTypeComboBox = new QComboBox(pInformationGroup); m_pWellCodeLineEdit->setReadOnly(true); + m_pWellCategoryLineEdit->setReadOnly(true); m_pWellCodeLineEdit->setToolTip( tr("Project-level unique well identifier")); QList listEditors; listEditors << m_pWellCodeLineEdit << m_pWellNameLineEdit + << m_pWellCategoryLineEdit << m_pWellTypeComboBox; for (int nIndex = 0; nIndex < listEditors.size(); ++nIndex) { @@ -701,10 +724,12 @@ QWidget* nmWxWellEditor::createWellInformationWidget(QWidget* pParent) pInformationLayout->addWidget(m_pWellCodeLineEdit, 0, 1); pInformationLayout->addWidget(pNameLabel, 1, 0); pInformationLayout->addWidget(m_pWellNameLineEdit, 1, 1); - pInformationLayout->addWidget(pTypeLabel, 2, 0); - pInformationLayout->addWidget(m_pWellTypeComboBox, 2, 1); - pInformationLayout->addWidget(pTimeDependentLabel, 3, 0); - pInformationLayout->addWidget(pTimeDependentEditor, 3, 1); + pInformationLayout->addWidget(pCategoryLabel, 2, 0); + pInformationLayout->addWidget(m_pWellCategoryLineEdit, 2, 1); + pInformationLayout->addWidget(pTypeLabel, 3, 0); + pInformationLayout->addWidget(m_pWellTypeComboBox, 3, 1); + pInformationLayout->addWidget(pTimeDependentLabel, 4, 0); + pInformationLayout->addWidget(pTimeDependentEditor, 4, 1); connect(m_pTimeDependentCheckBox, SIGNAL(toggled(bool)), this, SLOT(slotTimeDependentToggled(bool)));