diff --git a/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.cpp b/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.cpp index 69cc374975..1efb916c19 100644 --- a/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.cpp +++ b/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.cpp @@ -26,6 +26,9 @@ #include "RicNewStatisticsContourMapViewFeature.h" #include "RifReaderSettings.h" +#include "RifRoffWriter.h" + +#include "roffcpp/src/Reader.hpp" #include "ContourMap/RigContourMapCalculator.h" #include "ContourMap/RigContourMapGrid.h" @@ -60,7 +63,13 @@ #include "cafPdmUiTreeSelectionEditor.h" #include "cafProgressInfo.h" +#include +#include +#include +#include + #include +#include #include #include #include @@ -192,6 +201,12 @@ RimStatisticsContourMap::RimStatisticsContourMap() m_selectedPolygons.uiCapability()->setUiEditorTypeName( caf::PdmUiTreeSelectionEditor::uiEditorTypeName() ); m_selectedPolygons.uiCapability()->setUiLabelPosition( caf::PdmUiItemInfo::LabelPosition::TOP ); + CAF_PDM_InitField( &m_cacheFileName, "CacheFileName", QString(), "Cache File Name" ); + m_cacheFileName.uiCapability()->setUiHidden( true ); + + CAF_PDM_InitField( &m_cacheValidityKey, "CacheValidityKey", QString(), "Cache Validity Key" ); + m_cacheValidityKey.uiCapability()->setUiHidden( true ); + setDeletable( true ); } @@ -782,6 +797,7 @@ void RimStatisticsContourMap::computeStatisticsForMaps( const std::vectorm_contourMapGrid = std::move( ctx.contourMapGrid ); ctx.map->doStatisticsCalculation( ctx.timestepResults ); + ctx.map->m_computedValidityKey = ctx.active ? ctx.map->computeCacheValidityKey() : QString(); } } @@ -894,11 +910,11 @@ std::vector RimStatisticsContourMap::selectedFormations() const //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -std::vector> RimStatisticsContourMap::selectedPolygons() +std::vector> RimStatisticsContourMap::selectedPolygons() const { std::vector> allLines; - for ( auto p : m_selectedPolygons() ) + for ( auto p : m_selectedPolygons.ptrReferencedObjectsByType() ) { auto pData = p->polyLinesData(); if ( pData.isNull() ) continue; @@ -935,6 +951,8 @@ void RimStatisticsContourMap::ensureResultsComputed() // Use the Compute button to force a recomputation after changing settings. if ( m_contourMapGrid ) return; + if ( loadCachedResults() ) return; + // Compute all pending sibling contour maps in the same sweep over the ensemble realizations, so // that each realization is opened once instead of once per contour map std::vector maps = { this }; @@ -942,13 +960,266 @@ void RimStatisticsContourMap::ensureResultsComputed() { for ( auto sibling : ensemble->statisticsContourMaps() ) { - if ( sibling != this && !sibling->m_contourMapGrid && !sibling->views().empty() ) maps.push_back( sibling ); + if ( sibling != this && !sibling->m_contourMapGrid && !sibling->views().empty() && !sibling->loadCachedResults() ) + maps.push_back( sibling ); } } computeStatisticsForMaps( maps ); } +//-------------------------------------------------------------------------------------------------- +/// Hash of all settings that affect the computed statistics. Cached results are only reused when +/// the stored key matches the key computed from the current settings. +//-------------------------------------------------------------------------------------------------- +QString RimStatisticsContourMap::computeCacheValidityKey() const +{ + QStringList parts; + + parts << m_resultAggregation().text(); + parts << m_resolution().text(); + parts << m_gridImportMode().text(); + parts << QString::number( m_boundingBoxExpPercent(), 'g', 12 ); + + parts << m_oilFloodingType().text() << QString::number( m_userDefinedFloodingOil(), 'g', 12 ); + parts << m_gasFloodingType().text() << QString::number( m_userDefinedFloodingGas(), 'g', 12 ); + + if ( m_resultDefinition() != nullptr ) + { + parts << caf::AppEnum::text( m_resultDefinition->resultType() ); + parts << m_resultDefinition->resultVariable(); + parts << caf::AppEnum::text( m_resultDefinition->porosityModel() ); + } + + for ( int timeStep : selectedTimeSteps() ) + parts << QString::number( timeStep ); + + parts << ( m_enableFormationFilter() ? "formationFilter" : "noFormationFilter" ); + for ( const QString& formation : m_selectedFormations() ) + parts << formation; + + for ( const auto& polygonLine : selectedPolygons() ) + { + for ( const auto& point : polygonLine ) + parts << QString( "%1,%2,%3" ).arg( point.x(), 0, 'f', 3 ).arg( point.y(), 0, 'f', 3 ).arg( point.z(), 0, 'f', 3 ); + } + + if ( auto primaryCase = eclipseCase() ) parts << primaryCase->gridFileName(); + + for ( RimEclipseCase* eCase : ensembleCases() ) + parts << eCase->gridFileName(); + + const QByteArray hash = QCryptographicHash::hash( parts.join( ";" ).toUtf8(), QCryptographicHash::Md5 ); + return QString::fromLatin1( hash.toHex() ); +} + +//-------------------------------------------------------------------------------------------------- +/// Restore the contour map grid and statistics results from the project-adjacent cache file. +/// Returns false when no valid cache exists, and computation is needed. +//-------------------------------------------------------------------------------------------------- +bool RimStatisticsContourMap::loadCachedResults() +{ + if ( m_cacheFileName().isEmpty() ) return false; + + const QString fileName = getCacheDirectoryPath() + "/" + m_cacheFileName(); + if ( !QFile::exists( fileName ) ) return false; + + const QString expectedKey = computeCacheValidityKey(); + if ( m_cacheValidityKey().isEmpty() || m_cacheValidityKey() != expectedKey ) return false; + + try + { + std::ifstream stream( fileName.toStdString(), std::ios::binary ); + if ( !stream.good() ) return false; + + roff::Reader reader( stream ); + reader.parse(); + + auto scalarValues = reader.scalarNamedValues(); + + auto findString = [&scalarValues]( const std::string& keyword ) -> std::optional + { + for ( const auto& [key, value] : scalarValues ) + { + if ( key == keyword ) return std::get( value ); + } + return std::nullopt; + }; + + auto storedKey = findString( "cacheInfo.key" ); + if ( !storedKey || QString::fromStdString( *storedKey ) != expectedKey ) return false; + + auto readBoundingBox = [&reader]( const std::string& keyword ) -> std::optional + { + std::vector c = reader.getDoubleArray( keyword ); + if ( c.size() != 6 ) return std::nullopt; + return cvf::BoundingBox( cvf::Vec3d( c[0], c[1], c[2] ), cvf::Vec3d( c[3], c[4], c[5] ) ); + }; + + auto originalBoundingBox = readBoundingBox( "grid.originalBoundingBox" ); + auto expandedBoundingBox = readBoundingBox( "grid.expandedBoundingBox" ); + std::vector sampleSpacing = reader.getDoubleArray( "grid.sampleSpacing" ); + if ( !originalBoundingBox || !expandedBoundingBox || sampleSpacing.size() != 1 ) return false; + + auto contourMapGrid = std::make_unique( *originalBoundingBox, *expandedBoundingBox, sampleSpacing.front() ); + + const size_t cellCount = contourMapGrid->numberOfCells(); + + std::map>> timeResults; + for ( int timeStep : reader.getIntArray( "timesteps.indices" ) ) + { + for ( size_t statisticsTypeIndex = 0; statisticsTypeIndex < caf::AppEnum::size(); ++statisticsTypeIndex ) + { + const StatisticsType statisticsType = caf::AppEnum::fromIndex( statisticsTypeIndex ); + const QString parameterName = QString( "%1_%2" ).arg( caf::AppEnum::text( statisticsType ) ).arg( timeStep ); + + std::vector values = reader.getDoubleArray( parameterName.toStdString() ); + if ( values.size() != cellCount ) return false; + + timeResults[timeStep][statisticsType] = std::move( values ); + } + } + + if ( timeResults.empty() ) return false; + + m_contourMapGrid = std::move( contourMapGrid ); + m_timeResults = std::move( timeResults ); + m_computedValidityKey = expectedKey; + + RiaLogging::info( std::format( "Loaded ensemble contour map statistics from cache: {}", fileName ) ); + return true; + } + catch ( ... ) + { + RiaLogging::warning( std::format( "Failed to read ensemble contour map statistics cache, recomputing: {}", fileName ) ); + m_contourMapGrid.reset(); + m_timeResults.clear(); + return false; + } +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimStatisticsContourMap::writeCachedResults( const QString& fileName, const QString& validityKey ) const +{ + if ( !m_contourMapGrid || m_timeResults.empty() ) return false; + + std::ofstream stream( fileName.toStdString(), std::ios::binary ); + if ( !stream.good() ) return false; + + RifRoffWriter writer( stream ); + writer.writeFileType(); + writer.writeFileDataTag( "statisticsContourMap" ); + writer.writeVersionTag(); + + writer.startTag( "cacheInfo" ); + writer.writeString( "key", validityKey.toStdString() ); + writer.endTag(); + + const cvf::Vec2ui& mapSize = m_contourMapGrid->mapSize(); + writer.startTag( "dimensions" ); + writer.writeInt( "nX", static_cast( mapSize.x() ) ); + writer.writeInt( "nY", static_cast( mapSize.y() ) ); + writer.writeInt( "nZ", 1 ); + writer.endTag(); + + auto boundingBoxCoords = []( const cvf::BoundingBox& boundingBox ) -> std::vector + { + const cvf::Vec3d& min = boundingBox.min(); + const cvf::Vec3d& max = boundingBox.max(); + return { min.x(), min.y(), min.z(), max.x(), max.y(), max.z() }; + }; + + writer.startTag( "grid" ); + writer.writeDoubleArray( "sampleSpacing", { m_contourMapGrid->sampleSpacing() } ); + writer.writeDoubleArray( "originalBoundingBox", boundingBoxCoords( m_contourMapGrid->originalBoundingBox() ) ); + writer.writeDoubleArray( "expandedBoundingBox", boundingBoxCoords( m_contourMapGrid->expandedBoundingBox() ) ); + writer.endTag(); + + std::vector timeStepIndices; + for ( const auto& [timeStep, statisticsResults] : m_timeResults ) + timeStepIndices.push_back( static_cast( timeStep ) ); + + writer.startTag( "timesteps" ); + writer.writeIntArray( "indices", timeStepIndices ); + writer.endTag(); + + for ( const auto& [timeStep, statisticsResults] : m_timeResults ) + { + for ( const auto& [statisticsType, values] : statisticsResults ) + { + const QString parameterName = QString( "%1_%2" ).arg( caf::AppEnum::text( statisticsType ) ).arg( timeStep ); + + writer.startTag( "parameter" ); + writer.writeString( "name", parameterName.toStdString() ); + writer.writeDoubleArray( "data", values ); + writer.endTag(); + } + } + + writer.writeEofTag(); + return stream.good(); +} + +//-------------------------------------------------------------------------------------------------- +/// Write the computed statistics to a cache file next to the project file, so that the ensemble +/// sweep can be skipped when the project is loaded again with unchanged settings +//-------------------------------------------------------------------------------------------------- +void RimStatisticsContourMap::setupBeforeSave() +{ + // Delete any existing cache file, it is rewritten below if valid results exist + if ( !m_cacheFileName().isEmpty() ) + { + const QString previousFileName = getCacheDirectoryPath() + "/" + m_cacheFileName(); + if ( QFile::exists( previousFileName ) ) QFile::remove( previousFileName ); + } + + // Only write results computed from (or previously cached for) the current settings + if ( !m_contourMapGrid || m_timeResults.empty() || m_computedValidityKey.isEmpty() ) + { + m_cacheFileName = QString(); + m_cacheValidityKey = QString(); + return; + } + + QDir::root().mkpath( getCacheDirectoryPath() ); + + const QString fileName = getValidCacheFileName(); + if ( writeCachedResults( getCacheDirectoryPath() + "/" + fileName, m_computedValidityKey ) ) + { + m_cacheFileName = fileName; + m_cacheValidityKey = m_computedValidityKey; + } + else + { + RiaLogging::warning( std::format( "Failed to write ensemble contour map statistics cache: {}", fileName ) ); + m_cacheFileName = QString(); + m_cacheValidityKey = QString(); + } +} + +//-------------------------------------------------------------------------------------------------- +/// File name of the cache file within the cache directory +//-------------------------------------------------------------------------------------------------- +QString RimStatisticsContourMap::getValidCacheFileName() const +{ + if ( m_cacheFileName().isEmpty() ) + { + return QUuid::createUuid().toString( QUuid::WithoutBraces ) + ".roffbin"; + } + + return m_cacheFileName(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimStatisticsContourMap::getCacheDirectoryPath() +{ + return RimTools::getCacheRootDirectoryPathFromProject() + "_cache"; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.h b/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.h index 75d23e0dfe..eca1976ca8 100644 --- a/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.h +++ b/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.h @@ -99,13 +99,14 @@ public: QString timeStepName( int timeStep ) const; std::vector selectedFormations() const; - std::vector> selectedPolygons(); + std::vector> selectedPolygons() const; protected: void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override; void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override; void defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) override; void initAfterRead() override; + void setupBeforeSave() override; void appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const override; QList calculateValueOptions( const caf::PdmFieldHandle* fieldNeedingOptions ) override; @@ -118,6 +119,13 @@ private: void onComputeStatisticsClicked(); void doStatisticsCalculation( TimestepResultsMap& timestep_results ); + // Project-adjacent statistics cache, keyed by a hash of all settings affecting the results + QString computeCacheValidityKey() const; + bool loadCachedResults(); + bool writeCachedResults( const QString& fileName, const QString& validityKey ) const; + QString getValidCacheFileName() const; + static QString getCacheDirectoryPath(); + RimFormationNames* activeFormationNames() const; std::vector ensembleCases() const; std::set ensembleCasesInViews() const; @@ -142,10 +150,19 @@ private: caf::PdmField m_userDefinedFloodingGas; caf::PdmField m_userDefinedFloodingOil; + // File name of the cache file within the project cache directory. Stored as a plain file name + // to stay relocatable with the project and out of reach of the global path list macros + caf::PdmField m_cacheFileName; + caf::PdmField m_cacheValidityKey; + std::unique_ptr m_contourMapGrid; std::map>> m_timeResults; caf::PdmChildArrayField m_views; RimEclipseCase* m_openEclipseCase; + + // Validity key captured when the results were computed or loaded from cache, so that results + // from outdated settings are never written to the cache + QString m_computedValidityKey; };