feat(nmNum): 支持多记录选择与多相流量求解

- 支持每口井多条压力和流量记录的读取、选择及持久化
- 按 GaugeDataEx2 的 N×4 格式读取油气水多相流量
- 将公共段时长及 qo/qg/qw 原样传入 PEBI 求解器
- 修正包含井多相产量状态与流量记录识别
- 增加压力流量数据诊断信息
- 解耦独立建网与流量、PVT 等求解输入
feature/nmNum-multi-gauge-record
lh 3 days ago
parent 882592dd40
commit 8356fe0b8f

@ -2842,6 +2842,10 @@ Reason: %1</source>
<source>solver error</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>The solver currently supports oil, gas, and water single-phase models and the oil-water two-phase model only.</source>
<translation></translation>
</message>
<message>
<source>solver failed!</source>
<translation type="unfinished"></translation>
@ -7185,6 +7189,79 @@ Average pressure in contour: %2 MPa</source>
<message><source>h</source><translation>h</translation></message>
<message><source>MPa</source><translation>MPa</translation></message>
<message><source>m³/d</source><translation>m³/d</translation></message>
<message><source>Pressure Record:</source><translation></translation></message>
<message><source>Flow Record:</source><translation></translation></message>
<message><source>Display Phase:</source><translation></translation></message>
<message><source> (Invalid)</source><translation> ()</translation></message>
<message><source> (Empty)</source><translation> ()</translation></message>
<message><source> (Corrupt)</source><translation> ()</translation></message>
<message><source>Oil</source><translation></translation></message>
<message><source>Gas</source><translation></translation></message>
<message><source>Water</source><translation></translation></message>
<message><source>Check Data</source><translation></translation></message>
<message><source>Pressure/Flow Data Check</source><translation></translation></message>
<message><source>Close</source><translation></translation></message>
<message><source>No well is bound to this page.</source><translation></translation></message>
<message><source>=== Well ===</source><translation>=== ===</translation></message>
<message><source>Well name: %1</source><translation>%1</translation></message>
<message><source>Well code: %1</source><translation>%1</translation></message>
<message><source>Well category: %1</source><translation>%1</translation></message>
<message><source>Reference flow phase: %1</source><translation>%1</translation></message>
<message><source>Oil well</source><translation></translation></message>
<message><source>Gas well</source><translation></translation></message>
<message><source>Water well</source><translation></translation></message>
<message><source>Unknown</source><translation></translation></message>
<message><source>Usable</source><translation></translation></message>
<message><source>Empty</source><translation></translation></message>
<message><source>Corrupt</source><translation></translation></message>
<message><source>Invalid identity</source><translation></translation></message>
<message><source>(none)</source><translation></translation></message>
<message><source>=== Pressure records ===</source><translation>=== ===</translation></message>
<message><source>Total pressure records: %1</source><translation>%1</translation></message>
<message><source>Selected pressure gauge code: %1</source><translation> Code%1</translation></message>
<message><source>[%1] Name: %2</source><translation>[%1] %2</translation></message>
<message><source>Code: %1</source><translation>Code%1</translation></message>
<message><source>Time: %1</source><translation>%1</translation></message>
<message><source>Status: %1</source><translation>%1</translation></message>
<message><source>Point count: %1</source><translation>%1</translation></message>
<message><source>No pressure points.</source><translation></translation></message>
<message><source>First point = %1, last point = %2</source><translation> = %1 = %2</translation></message>
<message><source>All points = %1</source><translation> = %1</translation></message>
<message><source>=== Flow records ===</source><translation>=== ===</translation></message>
<message><source>Total flow records: %1</source><translation>%1</translation></message>
<message><source>Selected flow gauge code: %1</source><translation> Code%1</translation></message>
<message><source>Current display phase: %1</source><translation>%1</translation></message>
<message><source>Multiphase: %1</source><translation>%1</translation></message>
<message><source>Yes</source><translation></translation></message>
<message><source>No</source><translation></translation></message>
<message><source>Own flow segment index (1-based): %1</source><translation>%1</translation></message>
<message><source>no data</source><translation></translation></message>
<message><source>raw points = %1, real segments = %2, total duration = %3</source><translation> = %1 = %2 = %3</translation></message>
<message><source>first point is a (0,0) placeholder</source><translation> (0,0) </translation></message>
<message><source>Phase time consistency: %1</source><translation>%1</translation></message>
<message><source>less than two phases with real data, no comparison</source><translation></translation></message>
<message><source>segment counts differ: %1</source><translation>%1</translation></message>
<message><source>segment counts equal (%1), but segment %2 time differs: %3 = %4, %5 = %6</source><translation>%1 %2 %3 = %4%5 = %6</translation></message>
<message><source>segment counts equal (%1) and all segment times identical</source><translation>%1</translation></message>
<message><source>Oil + Gas</source><translation> + </translation></message>
<message><source>Oil + Water</source><translation> + </translation></message>
<message><source>Gas + Water</source><translation> + </translation></message>
<message><source>Oil + Gas + Water</source><translation> + + </translation></message>
<message><source>None</source><translation></translation></message>
<message><source>Other (%1)</source><translation>%1</translation></message>
<message><source>=== Current flow-segment multiphase data ===</source><translation>=== ===</translation></message>
<message><source>Current fitting primary well code: %1</source><translation> Code%1</translation></message>
<message><source>Snapshot belongs to edited well: %1</source><translation>%1</translation></message>
<message><source>This snapshot belongs to the current fitting primary well and does not override the edited other well.</source><translation></translation></message>
<message><source>Snapshot available: %1</source><translation>%1</translation></message>
<message><source>PVT phase type: %1</source><translation>PVT %1</translation></message>
<message><source>Segment multiphase flag: %1</source><translation>%1</translation></message>
<message><source>Flow curve name: %1</source><translation>线%1</translation></message>
<message><source>Matrix vector count: %1</source><translation>%1</translation></message>
<message><source>Vector sizes: %1</source><translation>%1</translation></message>
<message><source>Four-vector shape valid: %1</source><translation>%1</translation></message>
<message><source>Data rows (time, oil, gas, water):</source><translation></translation></message>
<message><source> ... %1 additional rows omitted</source><translation> ... %1 </translation></message>
</context>
<context>
<name>nmReservoirParameterPage</name>

@ -44,7 +44,10 @@ struct nmPebiWellInputSnapshot
QString m_sWellCode; ///< 项目内稳定井编码。
QString m_sWellName; ///< 捕获时井名称。
QString m_sWellInstanceId; ///< 完整场结果使用的稳定井 UUID。
QVector<QPointF> m_vecFlowPoints; ///< 求解使用的流量制度。
QVector<double> m_vecFlowDurations; ///< 去除框架占位行后的公共流动段时长。
QVector<double> m_vecOilRates; ///< 与公共流动段逐项对应的油相流量。
QVector<double> m_vecGasRates; ///< 与公共流动段逐项对应的气相流量。
QVector<double> m_vecWaterRates; ///< 与公共流动段逐项对应的水相流量。
NM_WELL_CATEGORY m_eWellCategory; ///< 求解时冻结的油井、气井或水井井别。
/** @brief 求解时冻结的历史压力曲线。 */
QVector<QVector<double> > 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);

@ -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;

@ -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<nmPressureGaugeRecord>& vecPressureRecords,
const QVector<nmFlowGaugeRecord>& vecFlowRecords);
const QVector<nmFlowGaugeRecord>& vecFlowRecords,
const QString& sPreferredPressureGaugeCode = QString(),
const QString& sPreferredFlowGaugeCode = QString(),
int nPreferredFlowIndex = 0);
// 井的接口
// 添加井

@ -26,8 +26,8 @@ struct nmPressureGaugeRecord {
/**
* @brief
* @note Ex2/Ex3
* Ex2Ex3
* @note GaugeData
* GaugeDataEx2 N []
*/
struct nmFlowGaugeRecord {
QString sGaugeCode; // 对应 ZxDataGaugeF::getCode(),选择持久化键

@ -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);

@ -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<nmFlowGaugeRecord> m_vecFlowGaugeRecords; ///< 每次填充流量下拉框时缓存的记录列表,供 currentFlowRecord() 安全返回内部指针。
};

@ -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; ///< 气产量列的摘要文本。

@ -176,6 +176,23 @@ bool isFiniteSolverNumber(double value)
#endif
}
// 旧的双对数曲线 DLL 仍是单相接口,只在结果后处理时按井别取参考相。
// PEBI 主求解已经在场景中接收油、气、水三相全部流量。
const QVector<double>* 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<nmDataVerticalFracturedWell*>(pWellData);
@ -1512,12 +1529,17 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult(
return false;
}
const QVector<double>* 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<QPointF>& vecTimeQ =
oWellInput.m_vecFlowPoints;
// 输入快照已经移除可选 (0,0) 占位点,此处逐段传给曲线 DLL。
const int nTimeNumQ = vecTimeQ.size();
const int nTimeNumQ =
oWellInput.m_vecFlowDurations.size();
std::vector<double> timeQ(nTimeNumQ);
std::vector<double> 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 计算双对数曲线

@ -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<QPointF> vecOilPoints =
pWellData->getFlowSegmentPoints(PHASE_Oil);
const QVector<QPointF> vecGasPoints =
pWellData->getFlowSegmentPoints(PHASE_Gas);
const QVector<QPointF> vecWaterPoints =
pWellData->getFlowSegmentPoints(PHASE_Water);
const QVector<QPointF>* 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<int>(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<QString, nmDataWellBase*> mapWellsByCode;
const QVector<nmDataWellBase*> 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<int>(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<NM_SOLVER_MODEL_TYPE>(oScene.solverType);
if(!nmIsSupportedPebiSolverModel(eSolverModelType)) {
qWarning() << "PEBI solver model is not supported:"
<< static_cast<int>(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<double> vecTime;
std::vector<double> vecRate;
vecTime.reserve(oWellInput.m_vecFlowPoints.size());
vecRate.reserve(oWellInput.m_vecFlowPoints.size());
std::vector<double> vecOil;
std::vector<double> vecGas;
std::vector<double> 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;
}
// 第三步:复制井筒储集、表皮和流量段索引。

@ -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);
}
}

@ -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<QPointF>& vecOilPoints,
QVector<QPointF>& vecGasPoints,
QVector<QPointF>& 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<QVariant>& 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;
}
// 多相流量记录只解析 GaugeDataEx2GaugeDataEx3 不是水相数据来源。
static NM_GAUGE_RECORD_STATUS classifyMultiPhaseFlowRecord(
ZxDataGaugeF* pGaugeF,
QVector<QPointF>& vecOilPoints,
QVector<QPointF>& vecGasPoints,
QVector<QPointF>& 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<nmFlowGaugeRecord>& 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<nmPressureGaugeRecord>& vecPressureRecords,
const QVector<nmFlowGaugeRecord>& vecFlowRecords)
const QVector<nmFlowGaugeRecord>& 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<QVector<double>> vvecHistoryPressureData; //压力历史数据
QVector<QVector<double>> vvecHistoryLogData; // 历史双对数曲线数据
@ -1672,7 +1862,12 @@ void nmDataAnalyzeManager::initCurWellData()
pVFracturedWell->setRadius(tempAttr);
// 设置井的压力、流量记录列表,并按默认规则选中当前记录
applyGaugeRecordsToWell(pVFracturedWell, vecPressureRecords, vecFlowRecords);
applyGaugeRecordsToWell(pVFracturedWell,
vecPressureRecords,
vecFlowRecords,
sCurrentPressureGaugeCode,
sCurrentFlowGaugeCode,
nCurrentFlowIndex);
QVector<QVector<double>> vvecHistoryPressureData; //压力历史数据
QVector<QVector<double>> vvecHistoryLogData; // 历史双对数曲线数据
@ -1713,7 +1908,12 @@ void nmDataAnalyzeManager::initCurWellData()
pHFracturedWell->setRadius(tempAttr);
// 设置井的压力、流量记录列表,并按默认规则选中当前记录
applyGaugeRecordsToWell(pHFracturedWell, vecPressureRecords, vecFlowRecords);
applyGaugeRecordsToWell(pHFracturedWell,
vecPressureRecords,
vecFlowRecords,
sCurrentPressureGaugeCode,
sCurrentFlowGaugeCode,
nCurrentFlowIndex);
QVector<QVector<double>> vvecHistoryPressureData; //压力历史数据
QVector<QVector<double>> 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<nmPressureGaugeRecord> vecPressureRecords;
@ -6313,7 +6533,12 @@ bool nmDataAnalyzeManager::loadWellPreAndFlow()
return false;
}
applyGaugeRecordsToWell(pWell, vecPressureRecords, vecFlowRecords);
applyGaugeRecordsToWell(pWell,
vecPressureRecords,
vecFlowRecords,
sPreferredPressureGaugeCode,
sPreferredFlowGaugeCode,
nPreferredFlowIndex);
}
return true;
}

@ -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<rapidjson::SizeType>(baPressureGaugeCode.size()),
allocator).Move(), allocator);
const QByteArray baFlowGaugeCode = m_sSelectedFlowGaugeCode.toUtf8();
wellObject.AddMember("SelectedFlowGaugeCode",
rapidjson::Value(baFlowGaugeCode.constData(),
static_cast<rapidjson::SizeType>(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<QPointF> nmDataWellBase::getFlowSegmentPoints(
NM_PHASE_TYPE eFlowPhase) const
{
QVector<QPointF> 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;

@ -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)
{

@ -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<nmCalculationWellRef> 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(

@ -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 <QAbstractItemView>
#include <QComboBox>
#include <QDialog>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QList>
#include <QPalette>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QSplitter>
#include <QStackedWidget>
#include <QStandardItem>
@ -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<QPointF>& 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<int>(eFluidType));
}
}
// 判断首个点是否为 (0,0) 占位点,与 hasRealPhaseData 的占位点语义一致。
bool hasLeadingPlaceholderPoint(const QVector<QPointF>& vecPoints)
{
return !vecPoints.isEmpty() &&
vecPoints.first().x() == 0.0 &&
vecPoints.first().y() == 0.0;
}
// 去除可选的首个 (0,0) 占位点后返回真实流量段列表。
QVector<QPointF> realPhaseSegments(const QVector<QPointF>& vecPoints)
{
QVector<QPointF> vecSegments = vecPoints;
if (hasLeadingPlaceholderPoint(vecSegments))
{
vecSegments.remove(0);
}
return vecSegments;
}
// 生成单个相的诊断摘要行:原始点数、真实段数、累计时长、首末点与占位点标识。
void appendPhaseDiagLines(QStringList& listLines,
const QString& sPhaseName,
const QVector<QPointF>& vecPoints)
{
if (vecPoints.isEmpty())
{
listLines << QString(" %1: %2")
.arg(sPhaseName)
.arg(nmWellPressureFlowPage::tr("no data"));
return;
}
// 累计时长只统计真实段,流量点 X 值是段持续时间而非绝对时刻。
QVector<QPointF> 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<QVector<QPointF> > 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<ZxDataWell*>(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<ZxDataGaugeF*>(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<int>(vecMainX.size()))
.arg(static_cast<int>(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<nmPressureGaugeRecord> 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<nmFlowGaugeRecord> 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"));
}

@ -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<QVector<double> > vecHistoryPressure;
QVector<QVector<double> > vecHistoryLogLog;
QVector<QVector<double> > vecHistorySemiLog;
pManager->calculationLogData(pUpdatedWell,
vecHistoryPressure,
vecHistoryLogLog,
vecHistorySemiLog);
pUpdatedWell->setHistoryPressure(vecHistoryPressure);
pUpdatedWell->setHistoryLogLog(vecHistoryLogLog);
pUpdatedWell->setHistorySemiLog(vecHistorySemiLog);
}
}
// 人工确认后的输入变化必须推进版本,阻止后台旧任务提交过期结果。
if(bInputChanged) {
if(bGridInputChanged) {

@ -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<QPointF> 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;

Loading…
Cancel
Save