From e98267c9c3b4d08c772a83493257a3dfacb0ed24 Mon Sep 17 00:00:00 2001 From: lh <2334563547@qq.com> Date: Wed, 16 Sep 2026 14:47:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(nmData):=20=E5=86=BB=E7=BB=93=20PEBI=20?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E5=BF=AB=E7=85=A7=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 冻结求解时的 k/phi/h 并按 OriginalCellId 映射结果属性 - 升级 V5 快照持久化及属性目录校验 - 支持通用属性帧切换和双缓冲渲染 - 加固基础网格映射与井曲线完整性验证 --- .../nmCalculationDllPebiSolverTask.h | 6 +- Include/nmNum/nmData/nmPebiResultSnapshot.h | 47 +- .../nmData/nmPebiResultSnapshotBuilder.h | 13 +- .../nmPebiResultSnapshotRenderAdapter.h | 43 +- .../nmData/nmPebiResultSnapshotSerializer.h | 11 +- .../nmCalculationDllPebiSolverTask.cpp | 93 ++- Src/nmNum/nmData/nmPebiResultSnapshot.cpp | 264 ++++++- .../nmData/nmPebiResultSnapshotBuilder.cpp | 244 +++++- .../nmPebiResultSnapshotRenderAdapter.cpp | 136 +++- .../nmData/nmPebiResultSnapshotSerializer.cpp | 745 +++++++++++++++++- 10 files changed, 1443 insertions(+), 159 deletions(-) diff --git a/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h b/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h index 309d7e9b..89616978 100644 --- a/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h +++ b/Include/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.h @@ -20,6 +20,7 @@ class nmPebiResultSnapshot; class vtkDoubleArray; struct nmPebiSolverInputSnapshot; struct nmPebiManualCaptureState; +struct nmPebiStaticPropertyValues; // 主窗口现在直接创建DLL求解线程任务,因此类需要导出,供nmSubWnd模块跨DLL使用。 class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { @@ -101,9 +102,10 @@ class NMCALCULATION_EXPORT nmCalculationDllPebiSolverTask : public QThread { /** @brief 按求解器井顺序组装压力、曲线和场结果快照,不修改 DataManager。 */ bool buildPebiModeResult(HX_NWTM_MODEL_OUTPUT& p1, int nModelType, - const HX_NWTM_GRID_OUTPUT1& oGridOutput); + nmPebiStaticPropertyValues& oStaticProperties); /** @brief 用任务局部输出构造并封闭完整结果候选,不访问实时数据对象。 */ - bool buildPebiResultSnapshotCandidate(); + bool buildPebiResultSnapshotCandidate( + nmPebiStaticPropertyValues& oStaticProperties); /** @brief 输出精简版 DLL 输入日志,用于定位求解异常。 */ void logHX_NWTM_MODEL_INPUT_Simplified(const HX_NWTM_MODEL_INPUT& p0); diff --git a/Include/nmNum/nmData/nmPebiResultSnapshot.h b/Include/nmNum/nmData/nmPebiResultSnapshot.h index 8b5efa4f..b8f5b0b2 100644 --- a/Include/nmNum/nmData/nmPebiResultSnapshot.h +++ b/Include/nmNum/nmData/nmPebiResultSnapshot.h @@ -3,6 +3,7 @@ #include "nmData_global.h" #include "nmDataNumericalAnalysisCase.h" +#include "nmPebiStaticPropertyValues.h" #include #include @@ -248,7 +249,7 @@ struct NM_DATA_EXPORT nmPebiResultSolverSettings /** * @brief 最后一次成功 PEBI 求解的完整只读结果。 * - * 普通调用方只能读取值类型,或要求快照把结构和压力绑定到调用方拥有的 + * 普通调用方只能读取值类型,或要求快照把结构和属性绑定到调用方拥有的 * VTK 对象,不能取得快照内部 VTK 对象的可写指针。 */ class NM_DATA_EXPORT nmPebiResultSnapshot @@ -284,6 +285,22 @@ public: /** @brief 返回结果基础网格单元数。 */ vtkIdType getResultCellCount() const; + /** @brief 按属性目录顺序返回当前快照包含的正式属性 ID。 */ + QStringList getAvailablePropertyIds() const; + /** @brief 判断快照是否包含指定正式属性。 */ + bool hasProperty(const QString& sPropertyId) const; + /** @brief 返回属性帧数;未知属性返回 0,静态属性固定返回 1。 */ + int getPropertyFrameCount(const QString& sPropertyId) const; + /** + * @brief 返回动态属性指定帧的时间。 + * @note 静态属性、未知属性或越界帧返回 false,且不修改 dTime。 + */ + bool getPropertyTimeAt(const QString& sPropertyId, + int nFrameIndex, + double& dTime) const; + /** @brief 返回指定属性的冻结显示范围。 */ + bool getPropertyRange(const QString& sPropertyId, + double aRange[2]) const; /** @brief 返回压力时间步数量。 */ int getPressureFrameCount() const; /** @brief 返回指定压力帧的时间,越界时返回 0。 */ @@ -318,22 +335,24 @@ private: nmPebiResultSnapshot& operator=(const nmPebiResultSnapshot&); /** - * @brief 只复制拓扑到适配器拥有的渲染网格,不复制快照的 CellData。 + * @brief 复制拓扑和内部单元映射,不复制任何正式结果属性数组。 * @note VTK 7.1 的 CopyStructure 会共享点和单元拓扑,适配器不得修改它们。 */ bool copyResultGridStructure(vtkUnstructuredGrid* pTargetGrid) const; /** - * @brief 将快照压力帧临时共享绑定到适配器的独立渲染网格。 + * @brief 将指定正式属性帧临时共享绑定到适配器的独立渲染网格。 * @note 目标网格会临时持有同一 VTK 数组引用,下游流程不得改写数组。 */ - bool bindPressureFrame(int nIndex, + bool bindPropertyFrame(const QString& sPropertyId, + int nFrameIndex, vtkUnstructuredGrid* pTargetGrid) const; - /** @brief 将一帧压力复制到适配器复用的缓冲数组。 */ - bool copyPressureFrame(int nIndex, - vtkDoubleArray* pTargetPressure) const; + /** @brief 将一帧正式属性复制到适配器提供的备用缓冲数组。 */ + bool copyPropertyFrame(const QString& sPropertyId, + int nFrameIndex, + vtkDoubleArray* pTargetScalars) const; private: - // 网格与压力分开保存,避免基础网格通过 CellData 暴露可变压力数组。 + // 网格与结果属性分开保存,避免基础网格通过 CellData 暴露可变数组。 /** @brief 快照 UUID。 */ QString m_sSnapshotId; /** @brief 生成结果时使用的网格输入版本。 */ @@ -342,8 +361,18 @@ private: quint64 m_nResultInputRevision; /** @brief 是否已由 Builder 完整校验并封闭。 */ bool m_bComplete; - /** @brief 不含压力 p 数组和活动标量的结果基础网格。 */ + /** @brief 只含几何和内部映射、不含正式结果标量的基础网格。 */ vtkSmartPointer m_pResultGrid; + /** @brief 按原始 PEBI 单元顺序保存的三项规范静态属性。 */ + nmPebiStaticPropertyValues m_oStaticProperties; + /** @brief 按可见 VTK Cell 顺序缓存的渗透率。 */ + vtkSmartPointer m_pPermeability; + /** @brief 按可见 VTK Cell 顺序缓存的孔隙度。 */ + vtkSmartPointer m_pPorosity; + /** @brief 按可见 VTK Cell 顺序缓存的厚度。 */ + vtkSmartPointer m_pThickness; + /** @brief 三项静态属性的可见单元范围,顺序为 k/phi/h。 */ + double m_aStaticRanges[3][2]; /** @brief 按时间严格递增保存的全部压力帧。 */ QVector m_vecPressureFrames; /** @brief 全部压力帧中的全局最小值。 */ diff --git a/Include/nmNum/nmData/nmPebiResultSnapshotBuilder.h b/Include/nmNum/nmData/nmPebiResultSnapshotBuilder.h index 62c1c556..8c4037ed 100644 --- a/Include/nmNum/nmData/nmPebiResultSnapshotBuilder.h +++ b/Include/nmNum/nmData/nmPebiResultSnapshotBuilder.h @@ -29,6 +29,11 @@ public: /** @brief 成功后接管网格且清空调用方智能指针,不执行 DeepCopy。 */ bool takeResultGrid( vtkSmartPointer& pResultGrid); + /** + * @brief 接管原始顺序的三项静态属性并建立只读 VTK 可见单元缓存。 + * @note 失败时调用方三个数组保持不变;成功后通过 qSwap 清空调用方。 + */ + bool takeStaticProperties(nmPebiStaticPropertyValues& oValues); /** @brief 成功后接管压力数组且清空调用方智能指针,不复制整帧数据。 */ bool addPressureFrame( double dTime, @@ -66,7 +71,7 @@ private: bool canWrite(); /** @brief 将 Builder 永久置为失败状态并输出错误信息。 */ bool fail(const QString& sError, QString* pError = NULL); - /** @brief 校验候选全部结构,并移除基础网格的压力标量。 */ + /** @brief 校验候选全部结构,不修改已经接管的候选载荷。 */ bool validate(QString* pError); private: @@ -79,12 +84,18 @@ private: bool m_bFailed; /** @brief 调用方是否显式设置过标量范围。 */ bool m_bHasScalarRange; + /** @brief 是否已经接管并建立完整静态属性缓存。 */ + bool m_bHasStaticProperties; /** @brief 调用方是否显式设置过储层参数。 */ bool m_bHasReservoirParameters; /** @brief 调用方是否显式设置过 PVT 参数。 */ bool m_bHasPvtParameters; /** @brief 调用方是否显式设置过求解设置。 */ bool m_bHasSolverSettings; + /** @brief 基础网格中 VTK Cell 到原始 PEBI 单元的冻结映射。 */ + QVector m_vecVtkToOriginal; + /** @brief 本次候选的原始 PEBI 单元总数。 */ + vtkIdType m_nOriginalCellCount; /** @brief 最近一次失败的英文内部诊断文本。 */ QString m_sLastError; }; diff --git a/Include/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.h b/Include/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.h index e0bf082f..3962c23b 100644 --- a/Include/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.h +++ b/Include/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.h @@ -23,11 +23,18 @@ class vtkUnstructuredGrid; class NM_DATA_EXPORT nmPebiResultSnapshotRenderAdapter { public: - /** @brief 压力帧与渲染网格之间的绑定方式。 */ + /** @brief 正式属性数组与渲染网格之间的绑定方式。 */ + enum PropertyBindingMode + { + SharedPropertyArray = 0, ///< 临时共享快照只读数组,不产生整帧复制。 + CopiedPropertyArray = 1 ///< 复制到双缓冲,隔离不可信下游流程。 + }; + + /** @brief 旧压力调用方使用的兼容枚举。 */ enum PressureBindingMode { - SharedPressureFrame = 0, ///< 临时共享快照压力数组,不产生整帧复制。 - CopiedPressureFrame = 1 ///< 复制到可复用缓冲,隔离不可信下游流程。 + SharedPressureFrame = SharedPropertyArray, + CopiedPressureFrame = CopiedPropertyArray }; /** @brief 创建尚未绑定快照的渲染适配器。 */ @@ -36,26 +43,36 @@ public: ~nmPebiResultSnapshotRenderAdapter(); /** - * @brief 绑定一套完整快照并建立不含 CellData 的渲染网格结构。 + * @brief 绑定快照并建立仅含内部单元映射的渲染网格结构。 * @param pSnapshot 调用期间保持只读的结果快照。 - * @param eBindingMode 共享压力或单帧复制模式。 + * @param eBindingMode 共享属性或单帧复制模式。 */ + bool initialize( + const QSharedPointer& pSnapshot, + PropertyBindingMode eBindingMode); + /** @brief 使用旧压力枚举初始化,行为与通用绑定模式一致。 */ bool initialize( const QSharedPointer& pSnapshot, PressureBindingMode eBindingMode); - /** @brief 解除输出、压力数组和快照引用,使适配器恢复为空状态。 */ + /** @brief 解除输出、属性数组和快照引用,使适配器恢复为空状态。 */ void clear(); + /** @brief 事务式发布指定属性帧,失败时保持原画面和状态不变。 */ + bool setPropertyFrame(const QString& sPropertyId, int nFrameIndex); /** @brief 将指定压力帧发布到当前 VTK 输出端口。 */ bool setPressureFrame(int nFrameIndex); /** @brief 返回供 Mapper 或过滤器建立连接的 VTK 输出端口。 */ vtkAlgorithmOutput* getOutputPort() const; /** @brief 返回当前已经绑定的压力帧下标,尚未切帧时为 -1。 */ int getCurrentFrameIndex() const; + /** @brief 返回当前已经绑定的属性 ID,尚未绑定时为空。 */ + QString getCurrentPropertyId() const; /** @brief 返回适配器渲染网格的单元数。 */ vtkIdType getCellCount() const; /** @brief 返回当前是否使用零整帧复制的共享压力模式。 */ bool isUsingSharedPressure() const; + /** @brief 返回当前是否使用零整帧复制的共享属性模式。 */ + bool isUsingSharedProperties() const; private: /** @brief 禁止复制包含 VTK 管道和快照引用的适配器。 */ @@ -70,13 +87,17 @@ private: QSharedPointer m_pSnapshot; /** @brief 适配器独占的渲染网格,结构可与快照基础网格共享。 */ vtkSmartPointer m_pRenderGrid; - /** @brief 复制模式复用的单帧压力缓冲。 */ - vtkSmartPointer m_pPressureBuffer; + /** @brief 复制模式当前绑定到 CellData 的活动缓冲。 */ + vtkSmartPointer m_pActiveBuffer; + /** @brief 复制模式在切换成功前独立填充和校验的备用缓冲。 */ + vtkSmartPointer m_pStandbyBuffer; /** @brief 将独立渲染网格接入 VTK 管道的生产者。 */ vtkSmartPointer m_pProducer; - /** @brief 当前压力帧绑定方式。 */ - PressureBindingMode m_eBindingMode; - /** @brief 当前已经绑定的压力帧下标。 */ + /** @brief 当前属性数组绑定方式。 */ + PropertyBindingMode m_eBindingMode; + /** @brief 当前已经绑定的属性 ID。 */ + QString m_sCurrentPropertyId; + /** @brief 当前已经绑定的属性帧下标。 */ int m_nCurrentFrameIndex; }; diff --git a/Include/nmNum/nmData/nmPebiResultSnapshotSerializer.h b/Include/nmNum/nmData/nmPebiResultSnapshotSerializer.h index 7b6e7fbe..6082f370 100644 --- a/Include/nmNum/nmData/nmPebiResultSnapshotSerializer.h +++ b/Include/nmNum/nmData/nmPebiResultSnapshotSerializer.h @@ -11,7 +11,7 @@ class nmPebiResultSnapshot; /** - * @brief 负责当前快照三文件载荷及 Snapshot.json 的流式保存和加载。 + * @brief 负责当前快照四文件载荷及 Snapshot.json 的流式保存和加载。 * * 序列化器是快照的受限友元,只在保存时读取内部 VTK 对象;加载仍通过 * nmPebiResultSnapshotBuilder 构造和封闭候选。 @@ -26,13 +26,13 @@ public: nmNumericalFileReference& oSnapshotJsonReference, QString* pError = NULL); - /** @brief 校验 Snapshot.json 及其三条引用,不创建快照对象。 */ + /** @brief 校验 Snapshot.json 及其四条引用,不发布快照对象。 */ static bool validate( const QString& sWindowDirectory, const nmNumericalFileReference& oSnapshotJsonReference, QString* pError = NULL); - /** @brief 返回 Snapshot.json 中三个规范化载荷路径,供窗口级全局查重。 */ + /** @brief 返回 Snapshot.json 中四个规范化载荷路径,供窗口级全局查重。 */ static bool getPayloadRelativePaths( const QString& sWindowDirectory, const nmNumericalFileReference& oSnapshotJsonReference, @@ -47,6 +47,11 @@ public: QString* pError = NULL); private: + /** @brief 写出原始 PEBI 单元顺序的三项规范静态属性。 */ + static bool writeStaticProperties( + const nmPebiResultSnapshot& oSnapshot, + const QString& sFilePath, + QString* pError); /** @brief 从快照内部数组流式写出全部压力帧。 */ static bool writePressureFrames(const nmPebiResultSnapshot& oSnapshot, const QString& sFilePath, diff --git a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp index 147cb04f..9774f693 100644 --- a/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp +++ b/Src/nmNum/nmCalculation/nmCalculationDllPebiSolverTask.cpp @@ -15,6 +15,7 @@ #include "nmDataFault.h" #include "nmDataTimeStepSetting.h" #include "nmPebiResultSnapshotBuilder.h" +#include "nmPebiCellMapping.h" #include "nmPebiStaticPropertyCalculator.h" #include "nmCalculationPebiGrid.h" @@ -1460,7 +1461,7 @@ bool nmCalculationDllPebiSolverTask::execPebiMode() const bool bSucceeded = buildPebiModeResult( oModelOutput, oModelInput.T, - oGridResult.m_oGridOutput1); + oStaticProperties); if(!bSucceeded && !isCancelRequested() && m_sFailureReason.isEmpty()) { fail(tr("The solver result data is invalid.")); } @@ -1491,7 +1492,7 @@ std::vector HX_logderivative(const std::vector& x, const std::ve bool nmCalculationDllPebiSolverTask::buildPebiModeResult( HX_NWTM_MODEL_OUTPUT& p1, int modelType, - const HX_NWTM_GRID_OUTPUT1& oGridOutput) + nmPebiStaticPropertyValues& oStaticProperties) { if(m_pInputSnapshot == nullptr) { return fail(tr("The solver result data is invalid.")); @@ -1821,47 +1822,51 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( : fail(tr("The calculated pressure curves are invalid.")); } - // 第四步:完整求解还必须具有与时间轴、网格绘图单元严格对应的场压力。 + // 第四步:完整求解只接受携带有效 OriginalCellId 的冻结基础网格。 + // 场压力按同一映射过滤,不能再根据 isplot 顺序推测 VTK Cell 身份。 if(p1.p.size() != p1.t.size() || - oGridOutput.PEBI_cell.isplot.empty()) { + m_pInputSnapshot->m_pBaseGrid == nullptr || + m_pInputSnapshot->m_pBaseGrid->GetNumberOfCells() <= 0) { return fail(tr("The solver returned incomplete field pressure data.")); } - - // 计算有效单元数量 - size_t actualPlotCellsCount = 0; - - for(size_t i = 0; i < oGridOutput.PEBI_cell.isplot.size(); ++i) { - if((i % 256) == 0 && isCancelRequested()) { - return false; - } - if(oGridOutput.PEBI_cell.isplot[i] == 1) { - actualPlotCellsCount++; - } - } - - if(actualPlotCellsCount == 0) { + QVector vecVtkToOriginal; + QVector vecOriginalToVtk; + QString sMappingError; + if(nmPebiCellMapping::read(m_pInputSnapshot->m_pBaseGrid, + &vecVtkToOriginal, &vecOriginalToVtk, &sMappingError) != + nmPebiCellMapping::MappingStatus_Valid || + vecVtkToOriginal.isEmpty() || vecOriginalToVtk.isEmpty() || + vecVtkToOriginal.size() != + m_pInputSnapshot->m_pBaseGrid->GetNumberOfCells()) { + qWarning() << "Invalid PEBI result cell mapping:" << sMappingError; return fail(tr("The generated grid is invalid.")); } + const int nOriginalCellCount = vecOriginalToVtk.size(); + const int nVisibleCellCount = vecVtkToOriginal.size(); for(size_t nTimeIndex = 0; nTimeIndex < p1.p.size(); ++nTimeIndex) { if(isCancelRequested()) { return false; } - if(p1.p[nTimeIndex].size() < oGridOutput.PEBI_cell.isplot.size()) { + if(p1.p[nTimeIndex].size() != + static_cast(nOriginalCellCount)) { qWarning() << "Incomplete field pressure returned for time index:" << static_cast(nTimeIndex); return fail(tr("The solver returned incomplete field pressure data.")); } - for(size_t nCellIndex = 0; - nCellIndex < oGridOutput.PEBI_cell.isplot.size(); - ++nCellIndex) { - if((nCellIndex % 256) == 0 && isCancelRequested()) { + for(int nVtkCellId = 0; + nVtkCellId < nVisibleCellCount; ++nVtkCellId) { + if((nVtkCellId % 256) == 0 && isCancelRequested()) { return false; } - if(oGridOutput.PEBI_cell.isplot[nCellIndex] == 1 && - !isFiniteSolverNumber(p1.p[nTimeIndex][nCellIndex])) { + const vtkIdType nOriginalCellId = + vecVtkToOriginal[nVtkCellId]; + if(nOriginalCellId < 0 || + nOriginalCellId >= nOriginalCellCount || + !isFiniteSolverNumber(p1.p[nTimeIndex][ + static_cast(nOriginalCellId)])) { qWarning() << "PEBI returned a non-finite field pressure at time/cell:" << static_cast(nTimeIndex) - << static_cast(nCellIndex); + << static_cast(nOriginalCellId); return fail(tr("The solver returned invalid field pressure data.")); } } @@ -1888,27 +1893,24 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( vtkSmartPointer pressureData = vtkSmartPointer::New(); pressureData->SetName("p"); // 设置标量数据的名称 - // 直接使用外面计算好的 actualPlotCellsCount - pressureData->SetNumberOfValues(actualPlotCellsCount); + pressureData->SetNumberOfComponents(1); + pressureData->SetNumberOfValues(nVisibleCellCount); - size_t destIdx = 0; // 目标数组的索引 double dCurrentTimeMin = DBL_MAX; // 当前时间步的最小值 double dCurrentTimeMax = -DBL_MAX; // 当前时间步的最大值 - // 遍历原始数据,并根据 isplot 填充到 pressureData - for(size_t i = 0; i < oGridOutput.PEBI_cell.isplot.size(); ++i) { - if((i % 256) == 0 && isCancelRequested()) { + for(int nVtkCellId = 0; + nVtkCellId < nVisibleCellCount; ++nVtkCellId) { + if((nVtkCellId % 256) == 0 && isCancelRequested()) { return false; } - if(oGridOutput.PEBI_cell.isplot[i] == 1) { // 只有当 isplot 为1时才考虑这个单元格 - pressureData->SetValue(destIdx, p1.p[timeIdx][i]); - - // 更新当前时间步的范围 - dCurrentTimeMin = qMin(dCurrentTimeMin, p1.p[timeIdx][i]); - dCurrentTimeMax = qMax(dCurrentTimeMax, p1.p[timeIdx][i]); - - destIdx++; - } + const vtkIdType nOriginalCellId = + vecVtkToOriginal[nVtkCellId]; + const double dPressure = p1.p[timeIdx][ + static_cast(nOriginalCellId)]; + pressureData->SetValue(nVtkCellId, dPressure); + dCurrentTimeMin = qMin(dCurrentTimeMin, dPressure); + dCurrentTimeMax = qMax(dCurrentTimeMax, dPressure); } // 更新全局范围 @@ -1951,7 +1953,8 @@ bool nmCalculationDllPebiSolverTask::buildPebiModeResult( m_dPendingScalarMin = dMinP; m_dPendingScalarMax = dMaxP; - m_bPendingFullResultReady = buildPebiResultSnapshotCandidate(); + m_bPendingFullResultReady = + buildPebiResultSnapshotCandidate(oStaticProperties); if(!m_bPendingFullResultReady) { // 候选结构或内存校验失败时立即释放大结果,旧快照保持原状。 fail(tr("Failed to build the result snapshot.")); @@ -1978,7 +1981,8 @@ void nmCalculationDllPebiSolverTask::discardPendingFullResult() } } -bool nmCalculationDllPebiSolverTask::buildPebiResultSnapshotCandidate() +bool nmCalculationDllPebiSolverTask::buildPebiResultSnapshotCandidate( + nmPebiStaticPropertyValues& oStaticProperties) { if(m_pInputSnapshot == NULL || m_pInputSnapshot->m_bAutoFitTargetOnly || @@ -2000,6 +2004,9 @@ bool nmCalculationDllPebiSolverTask::buildPebiResultSnapshotCandidate() if(!oBuilder.takeResultGrid(m_pInputSnapshot->m_pBaseGrid)) { return false; } + if(!oBuilder.takeStaticProperties(oStaticProperties)) { + return false; + } QMap >::const_iterator oTimeIt = m_mapPendingTimeSteps.constBegin(); diff --git a/Src/nmNum/nmData/nmPebiResultSnapshot.cpp b/Src/nmNum/nmData/nmPebiResultSnapshot.cpp index 838a1f37..e0f2292c 100644 --- a/Src/nmNum/nmData/nmPebiResultSnapshot.cpp +++ b/Src/nmNum/nmData/nmPebiResultSnapshot.cpp @@ -1,11 +1,49 @@ #include "nmPebiResultSnapshot.h" +#include "nmPebiCellMapping.h" +#include "nmPebiResultPropertyCatalog.h" + +#include #include +#include #include +#include #include #include +namespace { + +bool deepCopyNamedArray(vtkFieldData* pSource, + vtkFieldData* pTarget, + const char* pArrayName) +{ + if(pSource == NULL || pTarget == NULL || pArrayName == NULL || + pTarget->GetAbstractArray(pArrayName) != NULL) + { + return false; + } + + vtkAbstractArray* pSourceArray = + pSource->GetAbstractArray(pArrayName); + if(pSourceArray == NULL) + { + return false; + } + + vtkSmartPointer pTargetArray; + pTargetArray.TakeReference(pSourceArray->NewInstance()); + if(pTargetArray == NULL) + { + return false; + } + pTargetArray->DeepCopy(pSourceArray); + return pTarget->AddArray(pTargetArray) >= 0 && + pTarget->GetAbstractArray(pArrayName) == pTargetArray.GetPointer(); +} + +} + // VTK 结构和压力操作仅供渲染适配器调用,普通结果读取接口不暴露可写对象。 nmPebiResultSolverSlot::nmPebiResultSolverSlot() : m_nSolverIndex(-1), @@ -86,6 +124,11 @@ nmPebiResultSnapshot::nmPebiResultSnapshot() m_dScalarMin(0.0), m_dScalarMax(0.0) { + for(int nPropertyIndex = 0; nPropertyIndex < 3; ++nPropertyIndex) + { + m_aStaticRanges[nPropertyIndex][0] = 0.0; + m_aStaticRanges[nPropertyIndex][1] = 0.0; + } } nmPebiResultSnapshot::~nmPebiResultSnapshot() @@ -171,6 +214,107 @@ vtkIdType nmPebiResultSnapshot::getResultCellCount() const return m_pResultGrid != NULL ? m_pResultGrid->GetNumberOfCells() : 0; } +QStringList nmPebiResultSnapshot::getAvailablePropertyIds() const +{ + QStringList listPropertyIds; + if(!m_bComplete) + { + return listPropertyIds; + } + const QList listDescriptors = + nmPebiResultPropertyCatalog::descriptors(); + for(int nIndex = 0; nIndex < listDescriptors.size(); ++nIndex) + { + if(hasProperty(listDescriptors[nIndex].m_sPropertyId)) + { + listPropertyIds.append(listDescriptors[nIndex].m_sPropertyId); + } + } + return listPropertyIds; +} + +bool nmPebiResultSnapshot::hasProperty(const QString& sPropertyId) const +{ + if(!m_bComplete) + { + return false; + } + if(sPropertyId == QString::fromLatin1("permeability")) + { + return m_pPermeability != NULL; + } + if(sPropertyId == QString::fromLatin1("porosity")) + { + return m_pPorosity != NULL; + } + if(sPropertyId == QString::fromLatin1("thickness")) + { + return m_pThickness != NULL; + } + if(sPropertyId == QString::fromLatin1("pressure")) + { + return !m_vecPressureFrames.isEmpty(); + } + return false; +} + +int nmPebiResultSnapshot::getPropertyFrameCount( + const QString& sPropertyId) const +{ + if(!hasProperty(sPropertyId)) + { + return 0; + } + return sPropertyId == QString::fromLatin1("pressure") + ? m_vecPressureFrames.size() : 1; +} + +bool nmPebiResultSnapshot::getPropertyTimeAt( + const QString& sPropertyId, + int nFrameIndex, + double& dTime) const +{ + if(sPropertyId != QString::fromLatin1("pressure") || + !hasProperty(sPropertyId) || nFrameIndex < 0 || + nFrameIndex >= m_vecPressureFrames.size()) + { + return false; + } + dTime = m_vecPressureFrames[nFrameIndex].m_dTime; + return true; +} + +bool nmPebiResultSnapshot::getPropertyRange( + const QString& sPropertyId, + double aRange[2]) const +{ + if(aRange == NULL || !hasProperty(sPropertyId)) + { + return false; + } + if(sPropertyId == QString::fromLatin1("permeability")) + { + aRange[0] = m_aStaticRanges[0][0]; + aRange[1] = m_aStaticRanges[0][1]; + return true; + } + if(sPropertyId == QString::fromLatin1("porosity")) + { + aRange[0] = m_aStaticRanges[1][0]; + aRange[1] = m_aStaticRanges[1][1]; + return true; + } + if(sPropertyId == QString::fromLatin1("thickness")) + { + aRange[0] = m_aStaticRanges[2][0]; + aRange[1] = m_aStaticRanges[2][1]; + return true; + } + aRange[0] = m_dScalarMin; + aRange[1] = m_dScalarMax; + return true; +} + int nmPebiResultSnapshot::getPressureFrameCount() const { return m_vecPressureFrames.size(); @@ -187,13 +331,7 @@ double nmPebiResultSnapshot::getPressureTimeAt(int nIndex) const bool nmPebiResultSnapshot::getScalarRange(double aRange[2]) const { - if(aRange == NULL || !m_bComplete) - { - return false; - } - aRange[0] = m_dScalarMin; - aRange[1] = m_dScalarMax; - return true; + return getPropertyRange(QString::fromLatin1("pressure"), aRange); } const nmPebiResultReservoirParameters& @@ -222,61 +360,117 @@ bool nmPebiResultSnapshot::copyResultGridStructure( } // CopyStructure 在当前 VTK 7.1 中共享点和单元拓扑,不复制大网格。 - // 随后清空目标 CellData,确保压力只能通过受控接口按帧挂载。 + // 结果属性仍由受控接口按帧挂载;内部映射单独深拷贝,避免渲染端 + // 修改数组时污染不可变快照。 pTargetGrid->Initialize(); pTargetGrid->CopyStructure(m_pResultGrid); pTargetGrid->GetCellData()->Initialize(); - return pTargetGrid->GetNumberOfCells() == - m_pResultGrid->GetNumberOfCells(); + pTargetGrid->GetFieldData()->Initialize(); + if(pTargetGrid->GetNumberOfCells() != + m_pResultGrid->GetNumberOfCells() || + !deepCopyNamedArray(m_pResultGrid->GetCellData(), + pTargetGrid->GetCellData(), + nmPebiCellMapping::originalCellIdArrayName()) || + !deepCopyNamedArray(m_pResultGrid->GetFieldData(), + pTargetGrid->GetFieldData(), + nmPebiCellMapping::originalCellCountArrayName()) || + pTargetGrid->GetCellData()->GetScalars() != NULL) + { + pTargetGrid->Initialize(); + return false; + } + return true; } -bool nmPebiResultSnapshot::bindPressureFrame( - int nIndex, +bool nmPebiResultSnapshot::bindPropertyFrame( + const QString& sPropertyId, + int nFrameIndex, vtkUnstructuredGrid* pTargetGrid) const { - if(!m_bComplete || pTargetGrid == NULL || nIndex < 0 || - nIndex >= m_vecPressureFrames.size() || + if(!m_bComplete || pTargetGrid == NULL || nFrameIndex < 0 || + nFrameIndex >= getPropertyFrameCount(sPropertyId) || pTargetGrid->GetNumberOfCells() != getResultCellCount()) { return false; } - vtkDoubleArray* pPressure = - m_vecPressureFrames[nIndex].m_pPressure.GetPointer(); - if(pPressure == NULL || - pPressure->GetNumberOfTuples() != getResultCellCount()) + vtkDoubleArray* pScalars = NULL; + if(sPropertyId == QString::fromLatin1("permeability")) + { + pScalars = m_pPermeability.GetPointer(); + } + else if(sPropertyId == QString::fromLatin1("porosity")) + { + pScalars = m_pPorosity.GetPointer(); + } + else if(sPropertyId == QString::fromLatin1("thickness")) + { + pScalars = m_pThickness.GetPointer(); + } + else if(sPropertyId == QString::fromLatin1("pressure")) + { + pScalars = m_vecPressureFrames[nFrameIndex].m_pPressure.GetPointer(); + } + const QString sExpectedArrayName = + sPropertyId == QString::fromLatin1("pressure") + ? QString::fromLatin1("p") : sPropertyId; + if(pScalars == NULL || pScalars->GetName() == NULL || + QString::fromLatin1(pScalars->GetName()) != sExpectedArrayName || + pScalars->GetNumberOfComponents() != 1 || + pScalars->GetNumberOfTuples() != getResultCellCount()) { return false; } - // SetScalars 会增加 VTK 引用计数,压力的规范所有权仍属于快照。 - // 渲染网格销毁或切换快照时应及时解除这个临时引用。 - pTargetGrid->GetCellData()->SetScalars(pPressure); + // 共享模式只在完整校验后替换活动数组;事后校验失败时恢复旧引用。 + vtkDataArray* pPreviousScalars = + pTargetGrid->GetCellData()->GetScalars(); + pTargetGrid->GetCellData()->SetScalars(pScalars); + if(pTargetGrid->GetCellData()->GetScalars() != pScalars) + { + pTargetGrid->GetCellData()->SetScalars(pPreviousScalars); + return false; + } pTargetGrid->Modified(); - return pTargetGrid->GetCellData()->GetScalars() == pPressure; + return true; } -bool nmPebiResultSnapshot::copyPressureFrame( - int nIndex, - vtkDoubleArray* pTargetPressure) const +bool nmPebiResultSnapshot::copyPropertyFrame( + const QString& sPropertyId, + int nFrameIndex, + vtkDoubleArray* pTargetScalars) const { - if(!m_bComplete || pTargetPressure == NULL || nIndex < 0 || - nIndex >= m_vecPressureFrames.size()) + if(!m_bComplete || pTargetScalars == NULL || nFrameIndex < 0 || + nFrameIndex >= getPropertyFrameCount(sPropertyId)) { return false; } - vtkDoubleArray* pSourcePressure = - m_vecPressureFrames[nIndex].m_pPressure.GetPointer(); - if(pSourcePressure == NULL) + vtkDoubleArray* pSourceScalars = NULL; + if(sPropertyId == QString::fromLatin1("permeability")) + { + pSourceScalars = m_pPermeability.GetPointer(); + } + else if(sPropertyId == QString::fromLatin1("porosity")) + { + pSourceScalars = m_pPorosity.GetPointer(); + } + else if(sPropertyId == QString::fromLatin1("thickness")) + { + pSourceScalars = m_pThickness.GetPointer(); + } + else if(sPropertyId == QString::fromLatin1("pressure")) + { + pSourceScalars = + m_vecPressureFrames[nFrameIndex].m_pPressure.GetPointer(); + } + if(pSourceScalars == NULL) { return false; } - // 回退路径只复制当前帧,调用方应在动画期间复用同一个缓冲数组。 - pTargetPressure->DeepCopy(pSourcePressure); - pTargetPressure->SetName("p"); - return pTargetPressure->GetNumberOfComponents() == 1 && - pTargetPressure->GetNumberOfTuples() == getResultCellCount(); + pTargetScalars->DeepCopy(pSourceScalars); + return pTargetScalars->GetNumberOfComponents() == 1 && + pTargetScalars->GetNumberOfTuples() == getResultCellCount(); } diff --git a/Src/nmNum/nmData/nmPebiResultSnapshotBuilder.cpp b/Src/nmNum/nmData/nmPebiResultSnapshotBuilder.cpp index 0abe9119..24812fd0 100644 --- a/Src/nmNum/nmData/nmPebiResultSnapshotBuilder.cpp +++ b/Src/nmNum/nmData/nmPebiResultSnapshotBuilder.cpp @@ -1,5 +1,7 @@ #include "nmPebiResultSnapshotBuilder.h" +#include "nmPebiCellMapping.h" + #include #include #include @@ -30,6 +32,22 @@ bool isFiniteValue(double dValue) #endif } +// 这四个名称由结果快照独占。基础网格若已携带同名数组,无法判断其 +// 来源和语义,必须在接管所有权前拒绝,而不是在 finalize() 中静默删除。 +bool hasReservedResultPropertyArray(vtkUnstructuredGrid* pGrid) +{ + if(pGrid == NULL || pGrid->GetCellData() == NULL) + { + return false; + } + + vtkCellData* pCellData = pGrid->GetCellData(); + return pCellData->GetAbstractArray("p") != NULL || + pCellData->GetAbstractArray("permeability") != NULL || + pCellData->GetAbstractArray("porosity") != NULL || + pCellData->GetAbstractArray("thickness") != NULL; +} + bool isValidInstanceId(const QString& sInstanceId) { return !sInstanceId.isEmpty() && !QUuid(sInstanceId).isNull(); @@ -377,15 +395,111 @@ bool areSolverSettingsValid(const nmPebiResultSolverSettings& oSettings) oSettings.m_dMaxDeltaT >= oSettings.m_dMinDeltaT); } +bool buildStaticPropertyArray( + const QVector& vecOriginalValues, + const QVector& vecVtkToOriginal, + const char* pArrayName, + vtkSmartPointer& pArray, + double aRange[2]) +{ + if(pArrayName == NULL || vecOriginalValues.isEmpty() || + vecVtkToOriginal.isEmpty() || aRange == NULL) + { + return false; + } + vtkSmartPointer pCandidate = + vtkSmartPointer::New(); + if(pCandidate == NULL) + { + return false; + } + pCandidate->SetName(pArrayName); + pCandidate->SetNumberOfComponents(1); + pCandidate->SetNumberOfTuples(vecVtkToOriginal.size()); + double dMinimum = DBL_MAX; + double dMaximum = -DBL_MAX; + for(int nVtkCellId = 0; + nVtkCellId < vecVtkToOriginal.size(); ++nVtkCellId) + { + const vtkIdType nOriginalCellId = vecVtkToOriginal[nVtkCellId]; + if(nOriginalCellId < 0 || + nOriginalCellId >= vecOriginalValues.size()) + { + return false; + } + const double dValue = + vecOriginalValues[static_cast(nOriginalCellId)]; + if(!isFiniteValue(dValue)) + { + return false; + } + pCandidate->SetValue(nVtkCellId, dValue); + dMinimum = qMin(dMinimum, dValue); + dMaximum = qMax(dMaximum, dValue); + } + if(pCandidate->GetNumberOfComponents() != 1 || + pCandidate->GetNumberOfTuples() != vecVtkToOriginal.size() || + dMinimum == DBL_MAX || dMaximum == -DBL_MAX) + { + return false; + } + pArray = pCandidate; + aRange[0] = dMinimum; + aRange[1] = dMaximum; + return true; +} + +bool validateStaticPropertyArray( + const QVector& vecOriginalValues, + const QVector& vecVtkToOriginal, + vtkDoubleArray* pArray, + const char* pArrayName, + const double aRange[2]) +{ + if(pArray == NULL || pArrayName == NULL || aRange == NULL || + pArray->GetName() == NULL || + QString::fromLatin1(pArray->GetName()) != + QString::fromLatin1(pArrayName) || + pArray->GetNumberOfComponents() != 1 || + pArray->GetNumberOfTuples() != vecVtkToOriginal.size()) + { + return false; + } + double dMinimum = DBL_MAX; + double dMaximum = -DBL_MAX; + for(int nVtkCellId = 0; + nVtkCellId < vecVtkToOriginal.size(); ++nVtkCellId) + { + const vtkIdType nOriginalCellId = vecVtkToOriginal[nVtkCellId]; + if(nOriginalCellId < 0 || + nOriginalCellId >= vecOriginalValues.size()) + { + return false; + } + const double dExpected = + vecOriginalValues[static_cast(nOriginalCellId)]; + const double dActual = pArray->GetValue(nVtkCellId); + if(!isFiniteValue(dActual) || dActual != dExpected) + { + return false; + } + dMinimum = qMin(dMinimum, dActual); + dMaximum = qMax(dMaximum, dActual); + } + return dMinimum == aRange[0] && dMaximum == aRange[1]; +} + } nmPebiResultSnapshotBuilder::nmPebiResultSnapshotBuilder() : m_bFinalized(false), m_bFailed(false), m_bHasScalarRange(false), + m_bHasStaticProperties(false), m_bHasReservoirParameters(false), m_bHasPvtParameters(false), - m_bHasSolverSettings(false) + m_bHasSolverSettings(false), + m_nOriginalCellCount(0) { try { @@ -460,16 +574,93 @@ bool nmPebiResultSnapshotBuilder::takeResultGrid( vtkSmartPointer& pResultGrid) { if(!canWrite() || pResultGrid == NULL || - pResultGrid->GetNumberOfCells() <= 0) + pResultGrid->GetNumberOfCells() <= 0 || + m_pCandidate->m_pResultGrid != NULL) { return fail("Invalid PEBI result grid."); } + if(hasReservedResultPropertyArray(pResultGrid)) + { + return fail("PEBI result grid contains reserved property arrays."); + } + QVector vecVtkToOriginal; + QVector vecOriginalToVtk; + QString sMappingError; + if(nmPebiCellMapping::read(pResultGrid, + &vecVtkToOriginal, &vecOriginalToVtk, &sMappingError) != + nmPebiCellMapping::MappingStatus_Valid || + vecVtkToOriginal.size() != pResultGrid->GetNumberOfCells() || + vecOriginalToVtk.isEmpty()) + { + return fail(sMappingError.isEmpty() + ? QString::fromLatin1("Invalid PEBI result cell mapping.") + : sMappingError); + } // 通过交换所有权避免大网格 DeepCopy;成功后调用方不能再修改源网格。 + m_vecVtkToOriginal.swap(vecVtkToOriginal); + m_nOriginalCellCount = vecOriginalToVtk.size(); m_pCandidate->m_pResultGrid = pResultGrid; pResultGrid = NULL; return true; } +bool nmPebiResultSnapshotBuilder::takeStaticProperties( + nmPebiStaticPropertyValues& oValues) +{ + if(!canWrite() || m_pCandidate->m_pResultGrid == NULL || + m_bHasStaticProperties || m_nOriginalCellCount <= 0 || + !oValues.isValidForCellCount( + static_cast(m_nOriginalCellCount))) + { + return fail("Invalid PEBI static property values."); + } + + vtkSmartPointer pPermeability; + vtkSmartPointer pPorosity; + vtkSmartPointer pThickness; + double aPermeabilityRange[2] = { 0.0, 0.0 }; + double aPorosityRange[2] = { 0.0, 0.0 }; + double aThicknessRange[2] = { 0.0, 0.0 }; + if(!buildStaticPropertyArray( + oValues.m_vecPermeabilityMilliDarcy, + m_vecVtkToOriginal, "permeability", + pPermeability, aPermeabilityRange) || + !buildStaticPropertyArray( + oValues.m_vecPorosity, + m_vecVtkToOriginal, "porosity", + pPorosity, aPorosityRange) || + !buildStaticPropertyArray( + oValues.m_vecThicknessMeters, + m_vecVtkToOriginal, "thickness", + pThickness, aThicknessRange)) + { + return fail("Cannot build PEBI static property render caches."); + } + + // 到这里之后不再执行可能失败的操作,连续交换保证调用方只在成功时清空。 + m_pCandidate->m_pPermeability = pPermeability; + m_pCandidate->m_pPorosity = pPorosity; + m_pCandidate->m_pThickness = pThickness; + for(int nRangeIndex = 0; nRangeIndex < 2; ++nRangeIndex) + { + m_pCandidate->m_aStaticRanges[0][nRangeIndex] = + aPermeabilityRange[nRangeIndex]; + m_pCandidate->m_aStaticRanges[1][nRangeIndex] = + aPorosityRange[nRangeIndex]; + m_pCandidate->m_aStaticRanges[2][nRangeIndex] = + aThicknessRange[nRangeIndex]; + } + qSwap(m_pCandidate->m_oStaticProperties. + m_vecPermeabilityMilliDarcy, + oValues.m_vecPermeabilityMilliDarcy); + qSwap(m_pCandidate->m_oStaticProperties.m_vecPorosity, + oValues.m_vecPorosity); + qSwap(m_pCandidate->m_oStaticProperties.m_vecThicknessMeters, + oValues.m_vecThicknessMeters); + m_bHasStaticProperties = true; + return true; +} + bool nmPebiResultSnapshotBuilder::addPressureFrame( double dTime, vtkSmartPointer& pPressure) @@ -711,6 +902,41 @@ bool nmPebiResultSnapshotBuilder::validate(QString* pError) { return fail("PEBI result grid is missing.", pError); } + QVector vecVerifiedVtkToOriginal; + QVector vecVerifiedOriginalToVtk; + QString sMappingError; + if(nmPebiCellMapping::read(m_pCandidate->m_pResultGrid, + &vecVerifiedVtkToOriginal, &vecVerifiedOriginalToVtk, + &sMappingError) != nmPebiCellMapping::MappingStatus_Valid || + vecVerifiedVtkToOriginal != m_vecVtkToOriginal || + vecVerifiedOriginalToVtk.size() != m_nOriginalCellCount) + { + return fail(sMappingError.isEmpty() + ? QString::fromLatin1( + "PEBI result cell mapping changed during construction.") + : sMappingError, + pError); + } + if(!m_bHasStaticProperties || + !m_pCandidate->m_oStaticProperties.isValidForCellCount( + static_cast(m_nOriginalCellCount)) || + !validateStaticPropertyArray( + m_pCandidate->m_oStaticProperties. + m_vecPermeabilityMilliDarcy, + m_vecVtkToOriginal, m_pCandidate->m_pPermeability, + "permeability", m_pCandidate->m_aStaticRanges[0]) || + !validateStaticPropertyArray( + m_pCandidate->m_oStaticProperties.m_vecPorosity, + m_vecVtkToOriginal, m_pCandidate->m_pPorosity, + "porosity", m_pCandidate->m_aStaticRanges[1]) || + !validateStaticPropertyArray( + m_pCandidate->m_oStaticProperties.m_vecThicknessMeters, + m_vecVtkToOriginal, m_pCandidate->m_pThickness, + "thickness", m_pCandidate->m_aStaticRanges[2])) + { + return fail("PEBI static properties are incomplete or inconsistent.", + pError); + } if(m_pCandidate->m_vecPressureFrames.isEmpty()) { return fail("PEBI pressure frames are missing.", pError); @@ -746,6 +972,9 @@ bool nmPebiResultSnapshotBuilder::validate(QString* pError) if(!isFiniteValue(oFrame.m_dTime) || (nIndex > 0 && oFrame.m_dTime <= dPreviousTime) || oFrame.m_pPressure == NULL || + oFrame.m_pPressure->GetName() == NULL || + QString::fromLatin1(oFrame.m_pPressure->GetName()) != + QString::fromLatin1("p") || oFrame.m_pPressure->GetNumberOfComponents() != 1 || oFrame.m_pPressure->GetNumberOfTuples() != nCellCount) { @@ -885,13 +1114,12 @@ bool nmPebiResultSnapshotBuilder::validate(QString* pError) setDisplayWellIds.insert(sWellInstanceId); } - // 发布前基础网格必须与压力帧解耦,渲染网格只能按帧受控绑定。 - m_pCandidate->m_pResultGrid->GetCellData()->SetScalars(NULL); - m_pCandidate->m_pResultGrid->GetCellData()->RemoveArray("p"); - if(m_pCandidate->m_pResultGrid->GetCellData()->GetScalars() != NULL || - m_pCandidate->m_pResultGrid->GetCellData()->GetArray("p") != NULL) + // finalize() 只验证候选,不再修改基础网格。正式结果数组必须由快照 + // 独立持有,其他模块写入的非保留数组和活动属性则保持原样。 + if(hasReservedResultPropertyArray(m_pCandidate->m_pResultGrid)) { - return fail("PEBI result grid still owns pressure scalars.", pError); + return fail("PEBI result grid still owns result property scalars.", + pError); } return true; } diff --git a/Src/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.cpp b/Src/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.cpp index a43b1425..abf8a45b 100644 --- a/Src/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.cpp +++ b/Src/nmNum/nmData/nmPebiResultSnapshotRenderAdapter.cpp @@ -2,15 +2,59 @@ #include #include +#include #include #include #include +#include + +#include + +#if !defined(_MSC_VER) +#include +#endif + +namespace { + +bool isFiniteValue(double dValue) +{ +#if defined(_MSC_VER) + return _finite(dValue) != 0; +#else + return std::isfinite(dValue); +#endif +} + +bool isValidCopiedScalars(vtkDoubleArray* pScalars, + vtkIdType nExpectedCellCount, + const QString& sExpectedArrayName) +{ + if(pScalars == NULL || pScalars->GetName() == NULL || + QString::fromLatin1(pScalars->GetName()) != sExpectedArrayName || + pScalars->GetNumberOfComponents() != 1 || + pScalars->GetNumberOfTuples() != nExpectedCellCount) + { + return false; + } + for(vtkIdType nCellId = 0; nCellId < nExpectedCellCount; ++nCellId) + { + if(!isFiniteValue(pScalars->GetValue(nCellId))) + { + return false; + } + } + return true; +} + +} + nmPebiResultSnapshotRenderAdapter::nmPebiResultSnapshotRenderAdapter() : m_pRenderGrid(vtkSmartPointer::New()), - m_pPressureBuffer(vtkSmartPointer::New()), + m_pActiveBuffer(vtkSmartPointer::New()), + m_pStandbyBuffer(vtkSmartPointer::New()), m_pProducer(vtkSmartPointer::New()), - m_eBindingMode(SharedPressureFrame), + m_eBindingMode(SharedPropertyArray), m_nCurrentFrameIndex(-1) { } @@ -22,12 +66,12 @@ nmPebiResultSnapshotRenderAdapter::~nmPebiResultSnapshotRenderAdapter() bool nmPebiResultSnapshotRenderAdapter::initialize( const QSharedPointer& pSnapshot, - PressureBindingMode eBindingMode) + PropertyBindingMode eBindingMode) { clear(); if(pSnapshot.isNull() || !pSnapshot->isComplete() || - (eBindingMode != SharedPressureFrame && - eBindingMode != CopiedPressureFrame)) + (eBindingMode != SharedPropertyArray && + eBindingMode != CopiedPropertyArray)) { return false; } @@ -46,6 +90,20 @@ bool nmPebiResultSnapshotRenderAdapter::initialize( return true; } +bool nmPebiResultSnapshotRenderAdapter::initialize( + const QSharedPointer& pSnapshot, + PressureBindingMode eBindingMode) +{ + if(eBindingMode != SharedPressureFrame && + eBindingMode != CopiedPressureFrame) + { + return false; + } + return initialize(pSnapshot, + eBindingMode == CopiedPressureFrame + ? CopiedPropertyArray : SharedPropertyArray); +} + void nmPebiResultSnapshotRenderAdapter::clear() { // 先断开管道和 CellData,及时释放对旧快照压力数组的临时 VTK 引用。 @@ -59,38 +117,62 @@ void nmPebiResultSnapshotRenderAdapter::clear() m_pRenderGrid->GetCellData()->Initialize(); m_pRenderGrid->Initialize(); } - if(m_pPressureBuffer != NULL) + if(m_pActiveBuffer != NULL) { - m_pPressureBuffer->Initialize(); + m_pActiveBuffer->Initialize(); + } + if(m_pStandbyBuffer != NULL) + { + m_pStandbyBuffer->Initialize(); } m_pSnapshot.clear(); - m_eBindingMode = SharedPressureFrame; + m_eBindingMode = SharedPropertyArray; + m_sCurrentPropertyId.clear(); m_nCurrentFrameIndex = -1; } -bool nmPebiResultSnapshotRenderAdapter::setPressureFrame(int nFrameIndex) +bool nmPebiResultSnapshotRenderAdapter::setPropertyFrame( + const QString& sPropertyId, + int nFrameIndex) { if(m_pSnapshot.isNull() || m_pRenderGrid == NULL || - nFrameIndex < 0 || - nFrameIndex >= m_pSnapshot->getPressureFrameCount()) + nFrameIndex < 0 || !m_pSnapshot->hasProperty(sPropertyId) || + nFrameIndex >= m_pSnapshot->getPropertyFrameCount(sPropertyId)) { return false; } bool bBound = false; - if(m_eBindingMode == SharedPressureFrame) + if(m_eBindingMode == SharedPropertyArray) { - bBound = m_pSnapshot->bindPressureFrame( - nFrameIndex, m_pRenderGrid); + bBound = m_pSnapshot->bindPropertyFrame( + sPropertyId, nFrameIndex, m_pRenderGrid); } else { - bBound = m_pSnapshot->copyPressureFrame( - nFrameIndex, m_pPressureBuffer); + // 活动缓冲在整个准备阶段保持不变;只有备用缓冲完整有效后才切换。 + bBound = m_pSnapshot->copyPropertyFrame( + sPropertyId, nFrameIndex, m_pStandbyBuffer) && + isValidCopiedScalars( + m_pStandbyBuffer, m_pSnapshot->getResultCellCount(), + sPropertyId == QString::fromLatin1("pressure") + ? QString::fromLatin1("p") : sPropertyId); if(bBound) { - m_pRenderGrid->GetCellData()->SetScalars(m_pPressureBuffer); - m_pRenderGrid->Modified(); + vtkDataArray* pPreviousScalars = + m_pRenderGrid->GetCellData()->GetScalars(); + m_pRenderGrid->GetCellData()->SetScalars(m_pStandbyBuffer); + if(m_pRenderGrid->GetCellData()->GetScalars() != + m_pStandbyBuffer.GetPointer()) + { + m_pRenderGrid->GetCellData()->SetScalars(pPreviousScalars); + bBound = false; + } + else + { + qSwap(m_pActiveBuffer, m_pStandbyBuffer); + m_pRenderGrid->Modified(); + } } } @@ -98,11 +180,17 @@ bool nmPebiResultSnapshotRenderAdapter::setPressureFrame(int nFrameIndex) { return false; } + m_sCurrentPropertyId = sPropertyId; m_nCurrentFrameIndex = nFrameIndex; m_pProducer->Modified(); return true; } +bool nmPebiResultSnapshotRenderAdapter::setPressureFrame(int nFrameIndex) +{ + return setPropertyFrame(QString::fromLatin1("pressure"), nFrameIndex); +} + vtkAlgorithmOutput* nmPebiResultSnapshotRenderAdapter::getOutputPort() const { @@ -115,6 +203,11 @@ int nmPebiResultSnapshotRenderAdapter::getCurrentFrameIndex() const return m_nCurrentFrameIndex; } +QString nmPebiResultSnapshotRenderAdapter::getCurrentPropertyId() const +{ + return m_sCurrentPropertyId; +} + vtkIdType nmPebiResultSnapshotRenderAdapter::getCellCount() const { return m_pRenderGrid == NULL ? 0 : @@ -122,7 +215,12 @@ vtkIdType nmPebiResultSnapshotRenderAdapter::getCellCount() const } bool nmPebiResultSnapshotRenderAdapter::isUsingSharedPressure() const +{ + return isUsingSharedProperties(); +} + +bool nmPebiResultSnapshotRenderAdapter::isUsingSharedProperties() const { return !m_pSnapshot.isNull() && - m_eBindingMode == SharedPressureFrame; + m_eBindingMode == SharedPropertyArray; } diff --git a/Src/nmNum/nmData/nmPebiResultSnapshotSerializer.cpp b/Src/nmNum/nmData/nmPebiResultSnapshotSerializer.cpp index 8c51c3b9..28e1bca4 100644 --- a/Src/nmNum/nmData/nmPebiResultSnapshotSerializer.cpp +++ b/Src/nmNum/nmData/nmPebiResultSnapshotSerializer.cpp @@ -1,6 +1,8 @@ #include "nmPebiResultSnapshotSerializer.h" #include "nmDataJsonTools.h" +#include "nmPebiCellMapping.h" +#include "nmPebiResultPropertyCatalog.h" #include "nmPebiResultSnapshot.h" #include "nmPebiResultSnapshotBuilder.h" @@ -9,6 +11,7 @@ #include #include #include +#include #include #include @@ -26,8 +29,9 @@ namespace { const char g_aPressureMagic[8] = { 'N', 'M', 'P', 'R', 'E', 'S', '3', 0 }; const char g_aCurvesMagic[8] = { 'N', 'M', 'C', 'U', 'R', 'V', '3', 0 }; +const char g_aStaticMagic[8] = { 'N', 'M', 'S', 'T', 'A', 'T', '5', 0 }; const quint32 g_nBinaryFormatVersion = 1; -const int g_nSnapshotFormatVersion = 4; +const int g_nSnapshotFormatVersion = 5; const quint32 g_nEndianMarker = 0x01020304u; const quint64 g_nMaximumCellCount = 50000000ull; const quint32 g_nMaximumFrameCount = 10000u; @@ -35,8 +39,11 @@ const quint32 g_nMaximumWellCount = 10000u; const quint32 g_nMaximumCurveSeries = 64u; const quint64 g_nMaximumCurvePoints = 50000000ull; const quint32 g_nMaximumUuidBytes = 128u; +const quint32 g_nMaximumPropertyIdBytes = 64u; +const quint32 g_nStaticPropertyCount = 3u; const quint64 g_nMaximumSnapshotJsonBytes = 64ull * 1024ull * 1024ull; const qint64 g_nStreamChunkBytes = 4 * 1024 * 1024; +const int g_nCurveValidationBufferValues = 4096; bool setError(QString* pError, const QString& sError) { @@ -145,6 +152,151 @@ rapidjson::Value toJsonString(const QString& sValue, oAllocator); } +QString propertyTimeTypeText(NM_PEBI_RESULT_PROPERTY_TIME_TYPE eType) +{ + return eType == NM_PebiResultProperty_Dynamic + ? QString::fromLatin1("dynamic") + : QString::fromLatin1("static"); +} + +QString propertySourceText(NM_PEBI_RESULT_PROPERTY_SOURCE eSource) +{ + return eSource == NM_PebiResultPropertySource_SolverOutput + ? QString::fromLatin1("solver-output") + : QString::fromLatin1("static-calculation"); +} + +QString propertyAssociationText( + NM_PEBI_RESULT_PROPERTY_ASSOCIATION eAssociation) +{ + Q_UNUSED(eAssociation); + return QString::fromLatin1("cell-data"); +} + +QString propertyRangePolicyText( + NM_PEBI_RESULT_PROPERTY_RANGE_POLICY ePolicy) +{ + return ePolicy == NM_PebiResultPropertyRange_AllFrames + ? QString::fromLatin1("all-frames") + : QString::fromLatin1("current-array"); +} + +bool readRequiredString(const rapidjson::Value& oParent, + const char* pName, + QString& sValue) +{ + if(!oParent.IsObject() || !oParent.HasMember(pName) || + !oParent[pName].IsString()) + { + return false; + } + sValue = QString::fromUtf8(oParent[pName].GetString(), + static_cast(oParent[pName].GetStringLength())); + return true; +} + +void addPropertyCatalog( + rapidjson::Value& oParent, + rapidjson::Document::AllocatorType& oAllocator) +{ + rapidjson::Value oProperties(rapidjson::kArrayType); + const QList listDescriptors = + nmPebiResultPropertyCatalog::descriptors(); + for(int nIndex = 0; nIndex < listDescriptors.size(); ++nIndex) + { + const nmPebiResultPropertyDescriptor& oDescriptor = + listDescriptors[nIndex]; + rapidjson::Value oProperty(rapidjson::kObjectType); + oProperty.AddMember("Id", toJsonString( + oDescriptor.m_sPropertyId, oAllocator), oAllocator); + oProperty.AddMember("DisplayNameSourceText", toJsonString( + oDescriptor.m_sDisplayNameSourceText, oAllocator), oAllocator); + oProperty.AddMember("BaseUnit", toJsonString( + oDescriptor.m_sBaseUnit, oAllocator), oAllocator); + oProperty.AddMember("DefaultDisplayUnit", toJsonString( + oDescriptor.m_sDefaultDisplayUnit, oAllocator), oAllocator); + oProperty.AddMember("TimeType", toJsonString( + propertyTimeTypeText(oDescriptor.m_eTimeType), + oAllocator), oAllocator); + oProperty.AddMember("Source", toJsonString( + propertySourceText(oDescriptor.m_eSource), + oAllocator), oAllocator); + oProperty.AddMember("Association", toJsonString( + propertyAssociationText(oDescriptor.m_eAssociation), + oAllocator), oAllocator); + oProperty.AddMember("RangePolicy", toJsonString( + propertyRangePolicyText(oDescriptor.m_eRangePolicy), + oAllocator), oAllocator); + oProperties.PushBack(oProperty, oAllocator); + } + oParent.AddMember("Properties", oProperties, oAllocator); +} + +bool validatePropertyCatalog(const rapidjson::Value& oDocument, + QString* pError) +{ + if(!oDocument.IsObject() || !oDocument.HasMember("Properties") || + !oDocument["Properties"].IsArray()) + { + return setError(pError, "PEBI snapshot property catalog is missing."); + } + const QList listDescriptors = + nmPebiResultPropertyCatalog::descriptors(); + const rapidjson::Value& oProperties = oDocument["Properties"]; + if(oProperties.Size() != + static_cast(listDescriptors.size())) + { + return setError(pError, "PEBI snapshot property catalog is incomplete."); + } + QSet setPropertyIds; + for(rapidjson::SizeType nIndex = 0; + nIndex < oProperties.Size(); ++nIndex) + { + const rapidjson::Value& oProperty = oProperties[nIndex]; + QString sPropertyId; + QString sDisplayName; + QString sBaseUnit; + QString sDisplayUnit; + QString sTimeType; + QString sSource; + QString sAssociation; + QString sRangePolicy; + if(!readRequiredString(oProperty, "Id", sPropertyId) || + !readRequiredString(oProperty, "DisplayNameSourceText", + sDisplayName) || + !readRequiredString(oProperty, "BaseUnit", sBaseUnit) || + !readRequiredString(oProperty, "DefaultDisplayUnit", + sDisplayUnit) || + !readRequiredString(oProperty, "TimeType", sTimeType) || + !readRequiredString(oProperty, "Source", sSource) || + !readRequiredString(oProperty, "Association", sAssociation) || + !readRequiredString(oProperty, "RangePolicy", sRangePolicy) || + setPropertyIds.contains(sPropertyId)) + { + return setError(pError, + "PEBI snapshot property descriptor is invalid."); + } + nmPebiResultPropertyDescriptor oExpected; + if(!nmPebiResultPropertyCatalog::findDescriptor( + sPropertyId, oExpected) || + sDisplayName != oExpected.m_sDisplayNameSourceText || + sBaseUnit != oExpected.m_sBaseUnit || + sDisplayUnit != oExpected.m_sDefaultDisplayUnit || + sTimeType != propertyTimeTypeText(oExpected.m_eTimeType) || + sSource != propertySourceText(oExpected.m_eSource) || + sAssociation != propertyAssociationText( + oExpected.m_eAssociation) || + sRangePolicy != propertyRangePolicyText( + oExpected.m_eRangePolicy)) + { + return setError(pError, + "PEBI snapshot property catalog does not match V5."); + } + setPropertyIds.insert(sPropertyId); + } + return setPropertyIds.size() == listDescriptors.size(); +} + void addFileReference(rapidjson::Value& oParent, const char* pName, const nmNumericalFileReference& oReference, @@ -280,9 +432,31 @@ bool readCurve(QFile& oFile, nTotalPoints += nPointCount; if(pCurve == NULL) { - if(!oFile.seek(oFile.pos() + static_cast(nBytes))) + // validate() 不构造完整曲线,但仍必须真实读取并检查所有数值。 + // 固定大小缓冲避免损坏文件利用声明长度造成额外大块分配。 + double aBuffer[g_nCurveValidationBufferValues]; + quint64 nRemaining = nPointCount; + while(nRemaining > 0) { - return false; + const int nChunk = static_cast(qMin( + nRemaining, + static_cast( + g_nCurveValidationBufferValues))); + const quint64 nChunkBytes = + static_cast(nChunk) * sizeof(double); + if(!readAll(oFile, reinterpret_cast(aBuffer), + nChunkBytes)) + { + return false; + } + for(int nIndex = 0; nIndex < nChunk; ++nIndex) + { + if(!isFiniteValue(aBuffer[nIndex])) + { + return false; + } + } + nRemaining -= static_cast(nChunk); } } else @@ -294,6 +468,13 @@ bool readCurve(QFile& oFile, { return false; } + for(int nIndex = 0; nIndex < vecValues.size(); ++nIndex) + { + if(!isFiniteValue(vecValues[nIndex])) + { + return false; + } + } } } if(pCurve != NULL) @@ -307,7 +488,9 @@ bool readPressureFrames(const QString& sFilePath, quint64 nExpectedCellCount, quint32 nExpectedFrameCount, nmPebiResultSnapshotBuilder* pBuilder, - QString* pError) + QString* pError, + double* pActualMinimum = NULL, + double* pActualMaximum = NULL) { const QFileInfo oInfo(sFilePath); if(!oInfo.isFile() || oInfo.size() < 0 || @@ -348,6 +531,8 @@ bool readPressureFrames(const QString& sFilePath, safeAdd(nHeaderBytes, nPayloadBytes, nExpectedBytes) && nExpectedBytes == static_cast(oInfo.size()); double dPreviousTime = 0.0; + double dActualMinimum = DBL_MAX; + double dActualMaximum = -DBL_MAX; for(quint32 nFrame = 0; bOk && nFrame < nFrameCount; ++nFrame) { double dTime = 0.0; @@ -359,7 +544,26 @@ bool readPressureFrames(const QString& sFilePath, } if(pBuilder == NULL) { - bOk = oFile.seek(oFile.pos() + static_cast(nFrameBytes)); + // 发布前验证必须真正读取压力值,不能通过 seek 跳过损坏或非有限数据。 + double aBuffer[1024]; + quint64 nRemaining = nCellCount; + while(bOk && nRemaining > 0) + { + const quint64 nChunkValues = qMin( + static_cast(1024), nRemaining); + const quint64 nChunkBytes = + nChunkValues * sizeof(double); + bOk = readAll(oFile, + reinterpret_cast(aBuffer), nChunkBytes); + for(quint64 nCell = 0; + bOk && nCell < nChunkValues; ++nCell) + { + bOk = isFiniteValue(aBuffer[nCell]); + dActualMinimum = qMin(dActualMinimum, aBuffer[nCell]); + dActualMaximum = qMax(dActualMaximum, aBuffer[nCell]); + } + nRemaining -= nChunkValues; + } } else { @@ -377,6 +581,8 @@ bool readPressureFrames(const QString& sFilePath, for(quint64 nCell = 0; bOk && nCell < nCellCount; ++nCell) { bOk = isFiniteValue(pValues[nCell]); + dActualMinimum = qMin(dActualMinimum, pValues[nCell]); + dActualMaximum = qMax(dActualMaximum, pValues[nCell]); } bOk = bOk && pBuilder->addPressureFrame(dTime, pPressure) && pPressure == NULL; @@ -385,14 +591,250 @@ bool readPressureFrames(const QString& sFilePath, } bOk = bOk && oFile.atEnd(); oFile.close(); - return bOk ? true : setError(pError, "PEBI pressure frame file is damaged."); + if(!bOk || dActualMinimum == DBL_MAX || dActualMaximum == -DBL_MAX) + { + return setError(pError, "PEBI pressure frame file is damaged."); + } + if(pActualMinimum != NULL) + { + *pActualMinimum = dActualMinimum; + } + if(pActualMaximum != NULL) + { + *pActualMaximum = dActualMaximum; + } + return true; +} + +bool readStaticArrayValues(QFile& oFile, + quint64 nValueCount, + QVector* pValues) +{ + quint64 nBytes = 0; + if(!safeMultiply(nValueCount, sizeof(double), nBytes)) + { + return false; + } + if(pValues != NULL) + { + pValues->resize(static_cast(nValueCount)); + if(nBytes > 0 && !readAll(oFile, + reinterpret_cast(pValues->data()), nBytes)) + { + return false; + } + for(int nIndex = 0; nIndex < pValues->size(); ++nIndex) + { + if(!isFiniteValue((*pValues)[nIndex])) + { + return false; + } + } + return true; + } + + double aBuffer[1024]; + quint64 nRemaining = nValueCount; + while(nRemaining > 0) + { + const quint64 nChunkValues = qMin( + static_cast(1024), nRemaining); + const quint64 nChunkBytes = nChunkValues * sizeof(double); + if(!readAll(oFile, reinterpret_cast(aBuffer), nChunkBytes)) + { + return false; + } + for(quint64 nIndex = 0; nIndex < nChunkValues; ++nIndex) + { + if(!isFiniteValue(aBuffer[nIndex])) + { + return false; + } + } + nRemaining -= nChunkValues; + } + return true; +} + +bool readStaticProperties( + const QString& sFilePath, + quint64 nExpectedOriginalCellCount, + nmPebiStaticPropertyValues* pValues, + QString* pError) +{ + if(pValues != NULL) + { + pValues->clear(); + } + const QFileInfo oInfo(sFilePath); + if(!oInfo.isFile() || oInfo.size() <= 0 || + static_cast(oInfo.size()) > + nmNumericalResultPersistence::maximumBinaryPayloadBytes()) + { + return setError(pError, + "PEBI static property payload is missing or too large."); + } + QFile oFile(sFilePath); + if(!oFile.open(QIODevice::ReadOnly)) + { + return setError(pError, + "Cannot open PEBI static property file."); + } + + char aMagic[8] = { 0 }; + quint32 nVersion = 0; + quint32 nEndian = 0; + quint64 nOriginalCellCount = 0; + quint32 nPropertyCount = 0; + if(!readAll(oFile, aMagic, 8) || + !hasMagic(aMagic, g_aStaticMagic) || + !readPod(oFile, nVersion) || nVersion != g_nBinaryFormatVersion || + !readPod(oFile, nEndian) || nEndian != g_nEndianMarker || + !readPod(oFile, nOriginalCellCount) || + nOriginalCellCount == 0 || + nOriginalCellCount > g_nMaximumCellCount || + nOriginalCellCount > static_cast(INT_MAX) || + nOriginalCellCount != nExpectedOriginalCellCount || + !readPod(oFile, nPropertyCount) || + nPropertyCount != g_nStaticPropertyCount) + { + return setError(pError, + "PEBI static property header is invalid."); + } + + nmPebiStaticPropertyValues oLoadedValues; + QSet setPropertyIds; + for(quint32 nPropertyIndex = 0; + nPropertyIndex < nPropertyCount; ++nPropertyIndex) + { + quint32 nPropertyIdBytes = 0; + if(!readPod(oFile, nPropertyIdBytes) || nPropertyIdBytes == 0 || + nPropertyIdBytes > g_nMaximumPropertyIdBytes) + { + return setError(pError, + "PEBI static property ID is invalid."); + } + QByteArray baPropertyId; + baPropertyId.resize(static_cast(nPropertyIdBytes)); + if(!readAll(oFile, baPropertyId.data(), nPropertyIdBytes)) + { + return setError(pError, + "PEBI static property ID is incomplete."); + } + const QString sPropertyId = QString::fromLatin1( + baPropertyId.constData(), baPropertyId.size()); + quint64 nElementCount = 0; + if(setPropertyIds.contains(sPropertyId) || + !readPod(oFile, nElementCount) || + nElementCount != nOriginalCellCount) + { + return setError(pError, + "PEBI static property dimensions are invalid."); + } + + QVector* pTargetValues = NULL; + if(sPropertyId == QString::fromLatin1("permeability")) + { + pTargetValues = pValues == NULL ? NULL : + &oLoadedValues.m_vecPermeabilityMilliDarcy; + } + else if(sPropertyId == QString::fromLatin1("porosity")) + { + pTargetValues = pValues == NULL ? NULL : + &oLoadedValues.m_vecPorosity; + } + else if(sPropertyId == QString::fromLatin1("thickness")) + { + pTargetValues = pValues == NULL ? NULL : + &oLoadedValues.m_vecThicknessMeters; + } + else + { + return setError(pError, + "PEBI static property file contains an unknown ID."); + } + quint64 nValueBytes = 0; + quint64 nValueEnd = 0; + if(!safeMultiply(nElementCount, sizeof(double), nValueBytes) || + oFile.pos() < 0 || + !safeAdd(static_cast(oFile.pos()), + nValueBytes, nValueEnd) || + nValueEnd > static_cast(oInfo.size()) || + !readStaticArrayValues(oFile, nElementCount, pTargetValues)) + { + return setError(pError, + "PEBI static property values are invalid."); + } + setPropertyIds.insert(sPropertyId); + } + + if(setPropertyIds.size() != static_cast(g_nStaticPropertyCount) || + !setPropertyIds.contains(QString::fromLatin1("permeability")) || + !setPropertyIds.contains(QString::fromLatin1("porosity")) || + !setPropertyIds.contains(QString::fromLatin1("thickness")) || + oFile.pos() != oInfo.size() || + (pValues != NULL && !oLoadedValues.isValidForCellCount( + static_cast(nOriginalCellCount)))) + { + return setError(pError, + "PEBI static property payload is inconsistent."); + } + if(pValues != NULL) + { + qSwap(pValues->m_vecPermeabilityMilliDarcy, + oLoadedValues.m_vecPermeabilityMilliDarcy); + qSwap(pValues->m_vecPorosity, oLoadedValues.m_vecPorosity); + qSwap(pValues->m_vecThicknessMeters, + oLoadedValues.m_vecThicknessMeters); + } + return true; +} + +bool writeStaticPropertyArray(QFile& oFile, + const char* pPropertyId, + const QVector& vecValues, + quint64 nExpectedCount) +{ + if(pPropertyId == NULL || + static_cast(vecValues.size()) != nExpectedCount) + { + return false; + } + const QByteArray baPropertyId(pPropertyId); + const quint32 nPropertyIdBytes = + static_cast(baPropertyId.size()); + quint64 nValueBytes = 0; + if(nPropertyIdBytes == 0 || + nPropertyIdBytes > g_nMaximumPropertyIdBytes || + !safeMultiply(nExpectedCount, sizeof(double), nValueBytes)) + { + return false; + } + for(int nIndex = 0; nIndex < vecValues.size(); ++nIndex) + { + if(!isFiniteValue(vecValues[nIndex])) + { + return false; + } + } + return writePod(oFile, nPropertyIdBytes) && + writeAll(oFile, baPropertyId.constData(), nPropertyIdBytes) && + writePod(oFile, nExpectedCount) && + writeAll(oFile, + reinterpret_cast(vecValues.constData()), + nValueBytes); } bool readWellCurves(const QString& sFilePath, quint32 nExpectedWellCount, QMap* pCurves, + QSet* pWellIds, QString* pError) { + if(pWellIds != NULL) + { + pWellIds->clear(); + } const QFileInfo oInfo(sFilePath); if(!oInfo.isFile() || oInfo.size() < 0 || static_cast(oInfo.size()) > @@ -430,8 +872,10 @@ bool readWellCurves(const QString& sFilePath, baUuid.resize(static_cast(nUuidBytes)); bOk = readAll(oFile, baUuid.data(), nUuidBytes); } - const QString sUuid = QString::fromUtf8(baUuid); - bOk = bOk && !sUuid.isEmpty() && !setWellIds.contains(sUuid); + const QString sUuid = QString::fromUtf8( + baUuid.constData(), baUuid.size()); + bOk = bOk && !sUuid.isEmpty() && !QUuid(sUuid).isNull() && + !setWellIds.contains(sUuid); nmPebiResultWellCurves oCurves; bOk = bOk && readCurve(oFile, pCurves == NULL ? NULL : &oCurves.m_vecHistoryPressure, @@ -470,6 +914,10 @@ bool readWellCurves(const QString& sFilePath, { pCurves->swap(mapLoaded); } + if(pWellIds != NULL) + { + *pWellIds = setWellIds; + } return true; } @@ -513,13 +961,51 @@ bool readRequiredBool(const rapidjson::Value& oParent, return true; } +bool readJsonWellIds(const rapidjson::Document& oDocument, + quint32 nExpectedWellCount, + QSet& setWellIds) +{ + setWellIds.clear(); + if(!oDocument.IsObject() || !oDocument.HasMember("Wells") || + !oDocument["Wells"].IsArray() || + oDocument["Wells"].Size() != nExpectedWellCount) + { + return false; + } + + QSet setLoaded; + const rapidjson::Value& oWells = oDocument["Wells"]; + for(rapidjson::SizeType nIndex = 0; nIndex < oWells.Size(); ++nIndex) + { + const rapidjson::Value& oWell = oWells[nIndex]; + if(!oWell.IsObject() || !oWell.HasMember("WellInstanceId") || + !oWell["WellInstanceId"].IsString()) + { + return false; + } + const rapidjson::Value& oId = oWell["WellInstanceId"]; + const QString sWellId = QString::fromUtf8( + oId.GetString(), static_cast(oId.GetStringLength())); + if(sWellId.isEmpty() || QUuid(sWellId).isNull() || + setLoaded.contains(sWellId)) + { + return false; + } + setLoaded.insert(sWellId); + } + setWellIds = setLoaded; + return true; +} + bool parseSnapshotJson( const QString& sSnapshotJsonPath, rapidjson::Document& oDocument, nmNumericalFileReference& oGridReference, + nmNumericalFileReference& oStaticReference, nmNumericalFileReference& oPressureReference, nmNumericalFileReference& oCurvesReference, quint64& nCellCount, + quint64& nOriginalCellCount, quint32& nFrameCount, quint32& nWellCount, QString* pError) @@ -533,9 +1019,21 @@ bool parseSnapshotJson( } if(!oDocument.IsObject() || !oDocument.HasMember("SnapshotFormatVersion") || - !oDocument["SnapshotFormatVersion"].IsInt() || - oDocument["SnapshotFormatVersion"].GetInt() != - g_nSnapshotFormatVersion || + !oDocument["SnapshotFormatVersion"].IsInt()) + { + return setError(pError, + "Snapshot.json format version is missing or invalid."); + } + const int nSnapshotVersion = + oDocument["SnapshotFormatVersion"].GetInt(); + if(nSnapshotVersion != g_nSnapshotFormatVersion) + { + return setError(pError, QString::fromLatin1( + "Unsupported snapshot format version %1.") + .arg(nSnapshotVersion)); + } + if(!oDocument.HasMember("OriginalCellCount") || + !oDocument["OriginalCellCount"].IsUint64() || !oDocument.HasMember("ResultCellCount") || !oDocument["ResultCellCount"].IsUint64() || !oDocument.HasMember("PressureFrameCount") || @@ -543,31 +1041,127 @@ bool parseSnapshotJson( !oDocument.HasMember("WellCount") || !oDocument["WellCount"].IsUint() || !readFileReference(oDocument, "ResultGrid", oGridReference) || + !readFileReference(oDocument, "StaticProperties", oStaticReference) || !readFileReference(oDocument, "PressureFrames", oPressureReference) || !readFileReference(oDocument, "WellCurves", oCurvesReference)) { return setError(pError, "Snapshot.json header or file references are invalid."); } + if(!validatePropertyCatalog(oDocument, pError)) + { + return false; + } nCellCount = oDocument["ResultCellCount"].GetUint64(); + nOriginalCellCount = oDocument["OriginalCellCount"].GetUint64(); nFrameCount = oDocument["PressureFrameCount"].GetUint(); nWellCount = oDocument["WellCount"].GetUint(); if(nCellCount == 0 || nCellCount > g_nMaximumCellCount || + nOriginalCellCount == 0 || + nOriginalCellCount > g_nMaximumCellCount || + nOriginalCellCount > static_cast(INT_MAX) || + nOriginalCellCount < nCellCount || nFrameCount == 0 || nFrameCount > g_nMaximumFrameCount || - nWellCount > g_nMaximumWellCount || - oGridReference.m_sRelativePath.compare( - oPressureReference.m_sRelativePath, Qt::CaseInsensitive) == 0 || - oGridReference.m_sRelativePath.compare( - oCurvesReference.m_sRelativePath, Qt::CaseInsensitive) == 0 || - oPressureReference.m_sRelativePath.compare( - oCurvesReference.m_sRelativePath, Qt::CaseInsensitive) == 0) + nWellCount > g_nMaximumWellCount) { return setError(pError, "Snapshot.json declares invalid or duplicate payloads."); } + QSet setPayloadPaths; + setPayloadPaths.insert(oGridReference.m_sRelativePath.toLower()); + setPayloadPaths.insert(oStaticReference.m_sRelativePath.toLower()); + setPayloadPaths.insert(oPressureReference.m_sRelativePath.toLower()); + setPayloadPaths.insert(oCurvesReference.m_sRelativePath.toLower()); + if(setPayloadPaths.size() != 4) + { + return setError(pError, + "Snapshot.json declares invalid or duplicate payloads."); + } return true; } } +bool nmPebiResultSnapshotSerializer::writeStaticProperties( + const nmPebiResultSnapshot& oSnapshot, + const QString& sFilePath, + QString* pError) +{ + const int nOriginalCellCount = + oSnapshot.m_oStaticProperties.m_vecPermeabilityMilliDarcy.size(); + if(nOriginalCellCount <= 0 || + nOriginalCellCount > static_cast(g_nMaximumCellCount) || + !oSnapshot.m_oStaticProperties.isValidForCellCount( + nOriginalCellCount)) + { + return setError(pError, + "Cannot save invalid PEBI static properties."); + } + + const char* aPropertyIds[3] = { + "permeability", "porosity", "thickness" + }; + quint64 nValueBytes = 0; + quint64 nExpectedBytes = 8 + sizeof(quint32) + sizeof(quint32) + + sizeof(quint64) + sizeof(quint32); + if(!safeMultiply(static_cast(nOriginalCellCount), + sizeof(double), nValueBytes)) + { + return setError(pError, + "PEBI static property payload size overflows."); + } + for(int nIndex = 0; nIndex < 3; ++nIndex) + { + quint64 nArrayBytes = 0; + quint64 nNextBytes = 0; + const quint64 nIdBytes = static_cast( + QByteArray(aPropertyIds[nIndex]).size()); + if(!safeAdd(sizeof(quint32), nIdBytes, nArrayBytes) || + !safeAdd(nArrayBytes, sizeof(quint64), nArrayBytes) || + !safeAdd(nArrayBytes, nValueBytes, nArrayBytes) || + !safeAdd(nExpectedBytes, nArrayBytes, nNextBytes)) + { + return setError(pError, + "PEBI static property payload size overflows."); + } + nExpectedBytes = nNextBytes; + } + if(nExpectedBytes > + nmNumericalResultPersistence::maximumBinaryPayloadBytes()) + { + return setError(pError, + "PEBI static property payload is too large."); + } + + QFile oFile(sFilePath); + if(!oFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) + { + return setError(pError, + "Cannot create PEBI static property file."); + } + const quint64 nCount = static_cast(nOriginalCellCount); + bool bOk = writeAll(oFile, g_aStaticMagic, 8) && + writePod(oFile, g_nBinaryFormatVersion) && + writePod(oFile, g_nEndianMarker) && + writePod(oFile, nCount) && + writePod(oFile, g_nStaticPropertyCount) && + writeStaticPropertyArray(oFile, aPropertyIds[0], + oSnapshot.m_oStaticProperties. + m_vecPermeabilityMilliDarcy, nCount) && + writeStaticPropertyArray(oFile, aPropertyIds[1], + oSnapshot.m_oStaticProperties.m_vecPorosity, nCount) && + writeStaticPropertyArray(oFile, aPropertyIds[2], + oSnapshot.m_oStaticProperties.m_vecThicknessMeters, nCount) && + oFile.flush() && + static_cast(oFile.size()) == nExpectedBytes; + oFile.close(); + if(!bOk) + { + QFile::remove(sFilePath); + return setError(pError, + "Cannot write complete PEBI static property file."); + } + return true; +} + bool nmPebiResultSnapshotSerializer::writePressureFrames( const nmPebiResultSnapshot& oSnapshot, const QString& sFilePath, @@ -673,6 +1267,8 @@ bool nmPebiResultSnapshotSerializer::save( } const QString sGridPath = QDir(sWindowDirectory).filePath( "Snapshot/ResultGrid.vtu"); + const QString sStaticPath = QDir(sWindowDirectory).filePath( + "Snapshot/StaticProperties.bin"); const QString sPressurePath = QDir(sWindowDirectory).filePath( "Snapshot/PressureFrames.bin"); const QString sCurvesPath = QDir(sWindowDirectory).filePath( @@ -681,12 +1277,17 @@ bool nmPebiResultSnapshotSerializer::save( "Snapshot/Snapshot.json"); nmNumericalFileReference oGridReference; + nmNumericalFileReference oStaticReference; nmNumericalFileReference oPressureReference; nmNumericalFileReference oCurvesReference; if(!nmNumericalResultPersistence::writeGrid( pSnapshot->m_pResultGrid, sGridPath, pError) || !nmNumericalResultPersistence::buildFileReference( sGridPath, "Snapshot/ResultGrid.vtu", oGridReference) || + !writeStaticProperties(*pSnapshot, sStaticPath, pError) || + !nmNumericalResultPersistence::buildFileReference( + sStaticPath, "Snapshot/StaticProperties.bin", + oStaticReference) || !writePressureFrames(*pSnapshot, sPressurePath, pError) || !nmNumericalResultPersistence::buildFileReference( sPressurePath, "Snapshot/PressureFrames.bin", @@ -702,7 +1303,7 @@ bool nmPebiResultSnapshotSerializer::save( oDocument.SetObject(); rapidjson::Document::AllocatorType& oAllocator = oDocument.GetAllocator(); - // v4 在井目录中增加求解时冻结的完整流量制度,不兼容旧快照。 + // V5 冻结原始单元顺序的静态属性及稳定属性目录,不兼容旧快照。 oDocument.AddMember("SnapshotFormatVersion", g_nSnapshotFormatVersion, oAllocator); oDocument.AddMember("SnapshotId", @@ -714,6 +1315,9 @@ bool nmPebiResultSnapshotSerializer::save( oDocument.AddMember("ResultCellCount", static_cast(pSnapshot->m_pResultGrid->GetNumberOfCells()), oAllocator); + oDocument.AddMember("OriginalCellCount", static_cast( + pSnapshot->m_oStaticProperties. + m_vecPermeabilityMilliDarcy.size()), oAllocator); oDocument.AddMember("PressureFrameCount", static_cast(pSnapshot->m_vecPressureFrames.size()), oAllocator); @@ -722,8 +1326,11 @@ bool nmPebiResultSnapshotSerializer::save( oDocument.AddMember("ScalarMinimum", pSnapshot->m_dScalarMin, oAllocator); oDocument.AddMember("ScalarMaximum", pSnapshot->m_dScalarMax, oAllocator); addFileReference(oDocument, "ResultGrid", oGridReference, oAllocator); + addFileReference(oDocument, "StaticProperties", + oStaticReference, oAllocator); addFileReference(oDocument, "PressureFrames", oPressureReference, oAllocator); addFileReference(oDocument, "WellCurves", oCurvesReference, oAllocator); + addPropertyCatalog(oDocument, oAllocator); rapidjson::Value oSlots(rapidjson::kArrayType); for(int nIndex = 0; nIndex < pSnapshot->m_vecSolverSlots.size(); ++nIndex) @@ -888,33 +1495,88 @@ bool nmPebiResultSnapshotSerializer::validate( } rapidjson::Document oDocument; nmNumericalFileReference oGridReference; + nmNumericalFileReference oStaticReference; nmNumericalFileReference oPressureReference; nmNumericalFileReference oCurvesReference; quint64 nCellCount = 0; + quint64 nOriginalCellCount = 0; quint32 nFrameCount = 0; quint32 nWellCount = 0; if(!parseSnapshotJson(sJsonPath, oDocument, oGridReference, - oPressureReference, oCurvesReference, nCellCount, - nFrameCount, nWellCount, pError)) + oStaticReference, oPressureReference, oCurvesReference, + nCellCount, nOriginalCellCount, nFrameCount, + nWellCount, pError)) { return false; } + QSet setJsonWellIds; + if(!readJsonWellIds(oDocument, nWellCount, setJsonWellIds)) + { + return setError(pError, + "PEBI snapshot well directory is invalid."); + } QString sGridPath; + QString sStaticPath; QString sPressurePath; QString sCurvesPath; + QSet setCurveWellIds; + double dActualPressureMinimum = 0.0; + double dActualPressureMaximum = 0.0; if(!nmNumericalResultPersistence::validateFileReference( sWindowDirectory, oGridReference, &sGridPath, pError) || + !nmNumericalResultPersistence::validateFileReference( + sWindowDirectory, oStaticReference, &sStaticPath, pError) || !nmNumericalResultPersistence::validateFileReference( sWindowDirectory, oPressureReference, &sPressurePath, pError) || !nmNumericalResultPersistence::validateFileReference( sWindowDirectory, oCurvesReference, &sCurvesPath, pError) || + !readStaticProperties(sStaticPath, nOriginalCellCount, NULL, pError) || !readPressureFrames(sPressurePath, nCellCount, nFrameCount, - NULL, pError) || - !readWellCurves(sCurvesPath, nWellCount, NULL, pError)) + NULL, pError, &dActualPressureMinimum, + &dActualPressureMaximum) || + !readWellCurves(sCurvesPath, nWellCount, NULL, + &setCurveWellIds, pError)) { return false; } + if(setCurveWellIds != setJsonWellIds) + { + return setError(pError, + "PEBI snapshot well curve mapping is inconsistent."); + } + double dDeclaredPressureMinimum = 0.0; + double dDeclaredPressureMaximum = 0.0; + const double dRangeScale = qMax(1.0, qMax( + fabs(dActualPressureMinimum), fabs(dActualPressureMaximum))); + const double dRangeTolerance = dRangeScale * 1.0e-10; + if(!readRequiredDouble(oDocument, "ScalarMinimum", + dDeclaredPressureMinimum) || + !readRequiredDouble(oDocument, "ScalarMaximum", + dDeclaredPressureMaximum) || + fabs(dDeclaredPressureMinimum - dActualPressureMinimum) > + dRangeTolerance || + fabs(dDeclaredPressureMaximum - dActualPressureMaximum) > + dRangeTolerance) + { + return setError(pError, + "PEBI snapshot pressure range is inconsistent."); + } + vtkSmartPointer pGrid; + QVector vecVtkToOriginal; + QVector vecOriginalToVtk; + if(!nmNumericalResultPersistence::readGrid(sGridPath, pGrid, pError) || + pGrid == NULL || + static_cast(pGrid->GetNumberOfCells()) != nCellCount || + nmPebiCellMapping::read(pGrid, &vecVtkToOriginal, + &vecOriginalToVtk, pError) != + nmPebiCellMapping::MappingStatus_Valid || + static_cast(vecOriginalToVtk.size()) != + nOriginalCellCount) + { + return setError(pError, pError != NULL && !pError->isEmpty() + ? *pError : "PEBI snapshot grid mapping is invalid."); + } return true; } @@ -933,18 +1595,22 @@ bool nmPebiResultSnapshotSerializer::getPayloadRelativePaths( } rapidjson::Document oDocument; nmNumericalFileReference oGridReference; + nmNumericalFileReference oStaticReference; nmNumericalFileReference oPressureReference; nmNumericalFileReference oCurvesReference; quint64 nCellCount = 0; + quint64 nOriginalCellCount = 0; quint32 nFrameCount = 0; quint32 nWellCount = 0; if(!parseSnapshotJson(sJsonPath, oDocument, oGridReference, - oPressureReference, oCurvesReference, nCellCount, - nFrameCount, nWellCount, pError)) + oStaticReference, oPressureReference, oCurvesReference, + nCellCount, nOriginalCellCount, nFrameCount, + nWellCount, pError)) { return false; } listRelativePaths << oGridReference.m_sRelativePath + << oStaticReference.m_sRelativePath << oPressureReference.m_sRelativePath << oCurvesReference.m_sRelativePath; return true; @@ -972,22 +1638,29 @@ bool nmPebiResultSnapshotSerializer::load( } rapidjson::Document oDocument; nmNumericalFileReference oGridReference; + nmNumericalFileReference oStaticReference; nmNumericalFileReference oPressureReference; nmNumericalFileReference oCurvesReference; quint64 nCellCount = 0; + quint64 nOriginalCellCount = 0; quint32 nFrameCount = 0; quint32 nWellCount = 0; if(!parseSnapshotJson(sJsonPath, oDocument, oGridReference, - oPressureReference, oCurvesReference, nCellCount, - nFrameCount, nWellCount, pError)) + oStaticReference, oPressureReference, oCurvesReference, + nCellCount, nOriginalCellCount, nFrameCount, + nWellCount, pError)) { return false; } QString sGridPath; + QString sStaticPath; QString sPressurePath; QString sCurvesPath; if(!nmNumericalResultPersistence::validateFileReference( sWindowDirectory, oGridReference, &sGridPath, pError) || + !nmNumericalResultPersistence::validateFileReference( + sWindowDirectory, oStaticReference, + &sStaticPath, pError) || !nmNumericalResultPersistence::validateFileReference( sWindowDirectory, oPressureReference, &sPressurePath, pError) || !nmNumericalResultPersistence::validateFileReference( @@ -998,9 +1671,21 @@ bool nmPebiResultSnapshotSerializer::load( vtkSmartPointer pGrid; QMap mapCurves; + nmPebiStaticPropertyValues oStaticProperties; + QVector vecVtkToOriginal; + QVector vecOriginalToVtk; if(!nmNumericalResultPersistence::readGrid(sGridPath, pGrid, pError) || + pGrid == NULL || static_cast(pGrid->GetNumberOfCells()) != nCellCount || - !readWellCurves(sCurvesPath, nWellCount, &mapCurves, pError)) + nmPebiCellMapping::read(pGrid, &vecVtkToOriginal, + &vecOriginalToVtk, pError) != + nmPebiCellMapping::MappingStatus_Valid || + static_cast(vecOriginalToVtk.size()) != + nOriginalCellCount || + !readStaticProperties(sStaticPath, nOriginalCellCount, + &oStaticProperties, pError) || + !readWellCurves(sCurvesPath, nWellCount, &mapCurves, + NULL, pError)) { return setError(pError, pError != NULL && !pError->isEmpty() ? *pError : "PEBI snapshot grid or curves are invalid."); @@ -1019,6 +1704,10 @@ bool nmPebiResultSnapshotSerializer::load( oDocument["GridInputRevision"].GetUint64(), oDocument["ResultInputRevision"].GetUint64()) || !oBuilder.takeResultGrid(pGrid) || pGrid != NULL || + !oBuilder.takeStaticProperties(oStaticProperties) || + !oStaticProperties.m_vecPermeabilityMilliDarcy.isEmpty() || + !oStaticProperties.m_vecPorosity.isEmpty() || + !oStaticProperties.m_vecThicknessMeters.isEmpty() || !readPressureFrames(sPressurePath, nCellCount, nFrameCount, &oBuilder, pError)) {