diff --git a/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.cpp b/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.cpp index 1fb67d27f4..27690bee4e 100644 --- a/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.cpp +++ b/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.cpp @@ -62,7 +62,9 @@ #include "cafProgressInfo.h" #include +#include #include +#include #include #include @@ -217,6 +219,9 @@ RimStatisticsContourMap::RimStatisticsContourMap() CAF_PDM_InitFieldNoDefault( &m_cacheExpandedBoundingBox, "CacheExpandedBoundingBox", "Cache Expanded Bounding Box" ); m_cacheExpandedBoundingBox.uiCapability()->setUiHidden( true ); + CAF_PDM_InitFieldNoDefault( &m_cacheMapSize, "CacheMapSize", "Cache Map Size" ); + m_cacheMapSize.uiCapability()->setUiHidden( true ); + setDeletable( true ); } @@ -979,7 +984,7 @@ void RimStatisticsContourMap::ensureResultsComputed() } //-------------------------------------------------------------------------------------------------- -/// Hash of all settings that affect the computed statistics. Cached results are only reused when +/// Hash of all settings and input grid files 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 @@ -1014,10 +1019,24 @@ QString RimStatisticsContourMap::computeCacheValidityKey() const 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(); + // Identify a grid file by path, size and modification time, so that the cache is invalidated both when the + // ensemble is replaced or its cases are added/removed, and when a grid file is overwritten in place + auto gridFileFingerprint = []( const RimEclipseCase* gridCase ) -> QString + { + const QString gridFileName = gridCase->gridFileName(); + const QFileInfo fileInfo( gridFileName ); + if ( !fileInfo.exists() ) return gridFileName; - for ( RimEclipseCase* eCase : ensembleCases() ) - parts << eCase->gridFileName(); + return QString( "%1,%2,%3" ).arg( gridFileName ).arg( fileInfo.size() ).arg( fileInfo.lastModified().toMSecsSinceEpoch() ); + }; + + // Label and count the two sets of cases, so that a primary case can not be mistaken for the first ensemble case + parts << "primaryCase" << ( eclipseCase() != nullptr ? gridFileFingerprint( eclipseCase() ) : QString() ); + + const std::vector cases = ensembleCases(); + parts << "ensembleCases" << QString::number( cases.size() ); + for ( const RimEclipseCase* eCase : cases ) + parts << gridFileFingerprint( eCase ); const QByteArray hash = QCryptographicHash::hash( parts.join( ";" ).toUtf8(), QCryptographicHash::Md5 ); return QString::fromLatin1( hash.toHex() ); @@ -1033,15 +1052,23 @@ bool RimStatisticsContourMap::loadCachedResults() if ( m_cacheSampleSpacing() <= 0.0 || m_cacheOriginalBoundingBox().size() != 6 || m_cacheExpandedBoundingBox().size() != 6 ) return false; + // The map size is stored explicitly, as recomputing it from the extent of the expanded bounding box is sensitive to + // the limited precision of the doubles in the project file + if ( m_cacheMapSize().size() != 2 || m_cacheMapSize()[0] <= 0 || m_cacheMapSize()[1] <= 0 ) return false; + const QString expectedKey = computeCacheValidityKey(); if ( m_cacheValidityKey().isEmpty() || m_cacheValidityKey() != expectedKey ) return false; auto toBoundingBox = []( const std::vector& c ) { return cvf::BoundingBox( cvf::Vec3d( c[0], c[1], c[2] ), cvf::Vec3d( c[3], c[4], c[5] ) ); }; - auto contourMapGrid = std::make_unique( toBoundingBox( m_cacheOriginalBoundingBox() ), - toBoundingBox( m_cacheExpandedBoundingBox() ), - m_cacheSampleSpacing() ); + const cvf::BoundingBox originalBoundingBox = toBoundingBox( m_cacheOriginalBoundingBox() ); + const cvf::BoundingBox expandedBoundingBox = toBoundingBox( m_cacheExpandedBoundingBox() ); + + const cvf::Vec2ui storedMapSize( static_cast( m_cacheMapSize()[0] ), static_cast( m_cacheMapSize()[1] ) ); + + auto contourMapGrid = + std::make_unique( originalBoundingBox, expandedBoundingBox, m_cacheSampleSpacing(), storedMapSize ); const cvf::Vec2ui& mapSize = contourMapGrid->mapSize(); @@ -1184,6 +1211,8 @@ void RimStatisticsContourMap::setupBeforeSave() m_cacheSampleSpacing = m_contourMapGrid->sampleSpacing(); m_cacheOriginalBoundingBox = boundingBoxCoords( m_contourMapGrid->originalBoundingBox() ); m_cacheExpandedBoundingBox = boundingBoxCoords( m_contourMapGrid->expandedBoundingBox() ); + m_cacheMapSize = + std::vector{ static_cast( m_contourMapGrid->mapSize().x() ), static_cast( m_contourMapGrid->mapSize().y() ) }; } else { @@ -1242,6 +1271,7 @@ void RimStatisticsContourMap::clearCacheFields() m_cacheSampleSpacing = 0.0; m_cacheOriginalBoundingBox = std::vector(); m_cacheExpandedBoundingBox = std::vector(); + m_cacheMapSize = std::vector(); } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.h b/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.h index 3a94122814..66d009b6a5 100644 --- a/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.h +++ b/ApplicationLibCode/ProjectDataModel/ContourMap/RimStatisticsContourMap.h @@ -165,6 +165,7 @@ private: caf::PdmField m_cacheSampleSpacing; caf::PdmField> m_cacheOriginalBoundingBox; caf::PdmField> m_cacheExpandedBoundingBox; + caf::PdmField> m_cacheMapSize; std::unique_ptr m_contourMapGrid; std::map>> m_timeResults; diff --git a/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapGrid.cpp b/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapGrid.cpp index 0912e76079..dd21422f58 100644 --- a/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapGrid.cpp +++ b/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapGrid.cpp @@ -58,6 +58,22 @@ RigContourMapGrid::RigContourMapGrid( const cvf::BoundingBox& originalBoundingBo m_expandedBoundingBox = makeMaxPointMultipleOfCellSize( m_expandedBoundingBox, m_mapSize, sampleSpacing ); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RigContourMapGrid::RigContourMapGrid( const cvf::BoundingBox& originalBoundingBox, + const cvf::BoundingBox& expandedBoundingBox, + double sampleSpacing, + const cvf::Vec2ui& mapSize ) + : m_sampleSpacing( sampleSpacing ) + , m_mapSize( mapSize ) + , m_originalBoundingBox( originalBoundingBox ) + , m_expandedBoundingBox( expandedBoundingBox ) +{ + // Re-jig max point to be an exact multiple of cell size + m_expandedBoundingBox = makeMaxPointMultipleOfCellSize( m_expandedBoundingBox, m_mapSize, sampleSpacing ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapGrid.h b/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapGrid.h index c310715239..43430c7b1c 100644 --- a/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapGrid.h +++ b/ApplicationLibCode/ReservoirDataModel/ContourMap/RigContourMapGrid.h @@ -33,6 +33,13 @@ public: RigContourMapGrid( const cvf::BoundingBox& originalBoundingBox, double sampleSpacing ); RigContourMapGrid( const cvf::BoundingBox& originalBoundingBox, const cvf::BoundingBox& expandedBoundingBox, double sampleSpacing ); + // Use an explicit map size instead of computing it from the extent of the expanded bounding box. Intended for + // restoring a sample grid from a project file, where the bounding box is stored with limited precision. + RigContourMapGrid( const cvf::BoundingBox& originalBoundingBox, + const cvf::BoundingBox& expandedBoundingBox, + double sampleSpacing, + const cvf::Vec2ui& mapSize ); + // Copy constructor RigContourMapGrid( const RigContourMapGrid& other ) : m_sampleSpacing( other.m_sampleSpacing ) diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index 0df1a18468..f54e29435a 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -12,6 +12,7 @@ set(SOURCE_UNITTEST_FILES ${CMAKE_CURRENT_LIST_DIR}/RigPadModel-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RigSimulationInputTool-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RigBoundingBoxIjk-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RigContourMapGrid-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifcCommandCore-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifEclipseInputFileTools-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifEclipseOutputFileTools-Test.cpp diff --git a/ApplicationLibCode/UnitTests/RigContourMapGrid-Test.cpp b/ApplicationLibCode/UnitTests/RigContourMapGrid-Test.cpp new file mode 100644 index 0000000000..b311f197ae --- /dev/null +++ b/ApplicationLibCode/UnitTests/RigContourMapGrid-Test.cpp @@ -0,0 +1,65 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2026- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "gtest/gtest.h" + +#include "ContourMap/RigContourMapGrid.h" + +//-------------------------------------------------------------------------------------------------- +/// The map size of a cached ensemble contour map is stored in the project file, and is used directly instead of being +/// recomputed from the extent of the expanded bounding box, which is stored with limited precision. The expanded +/// bounding box must still end up as an exact multiple of the sample spacing. +//-------------------------------------------------------------------------------------------------- +TEST( RigContourMapGrid, ExplicitMapSizeIsUsedWhenRestoringGrid ) +{ + const double sampleSpacing = 134.078604562017; + + const cvf::BoundingBox originalBoundingBox( cvf::Vec3d( 455778.046191406, 5926228.75742188, -2013.02879333496 ), + cvf::Vec3d( 467774.981152344, 5939762.94570312, -1549.74769592285 ) ); + + const RigContourMapGrid computedGrid( originalBoundingBox, sampleSpacing ); + EXPECT_EQ( 94u, computedGrid.mapSize().x() ); + EXPECT_EQ( 105u, computedGrid.mapSize().y() ); + + // The expanded bounding box as read back from a project file, stored with 15 significant digits + const cvf::BoundingBox restoredExpandedBoundingBox( cvf::Vec3d( 455509.888982282, 5925960.60021275, -2013.02879333496 ), + cvf::Vec3d( 468113.277811112, 5940038.85369176, -1549.74769592285 ) ); + + const RigContourMapGrid restoredGrid( originalBoundingBox, restoredExpandedBoundingBox, sampleSpacing, computedGrid.mapSize() ); + EXPECT_EQ( computedGrid.mapSize().x(), restoredGrid.mapSize().x() ); + EXPECT_EQ( computedGrid.mapSize().y(), restoredGrid.mapSize().y() ); + + const cvf::Vec3d extent = restoredGrid.expandedBoundingBox().extent(); + EXPECT_NEAR( restoredGrid.mapSize().x() * sampleSpacing, extent.x(), 1.0e-9 ); + EXPECT_NEAR( restoredGrid.mapSize().y() * sampleSpacing, extent.y(), 1.0e-9 ); +} + +//-------------------------------------------------------------------------------------------------- +/// A bounding box extent that is clearly larger than a whole number of cells must add a cell +//-------------------------------------------------------------------------------------------------- +TEST( RigContourMapGrid, MapSizeIsRoundedUpForPartialCell ) +{ + const double sampleSpacing = 100.0; + + // The bounding box is expanded by two cells in each direction, giving an extent of 4.5 cells in x and 4 cells in y + const cvf::BoundingBox originalBoundingBox( cvf::Vec3d( 0.0, 0.0, 0.0 ), cvf::Vec3d( 50.0, 0.0, 10.0 ) ); + + const RigContourMapGrid grid( originalBoundingBox, sampleSpacing ); + EXPECT_EQ( 5u, grid.mapSize().x() ); + EXPECT_EQ( 4u, grid.mapSize().y() ); +}