diff --git a/ApplicationLibCode/FileInterface/RifMultipleSummaryReaders.cpp b/ApplicationLibCode/FileInterface/RifMultipleSummaryReaders.cpp index 0ae413c6c3..214563552f 100644 --- a/ApplicationLibCode/FileInterface/RifMultipleSummaryReaders.cpp +++ b/ApplicationLibCode/FileInterface/RifMultipleSummaryReaders.cpp @@ -176,6 +176,30 @@ void RifMultipleSummaryReaders::createAndSetAddresses() } } +//-------------------------------------------------------------------------------------------------- +/// Recreate the addresses of the calculated readers, and leave the addresses of the native readers untouched. Calculation objects can be +/// created and modified after the addresses of a summary case have been created, and the addresses of the calculated readers must be +/// updated to reflect this. +//-------------------------------------------------------------------------------------------------- +void RifMultipleSummaryReaders::refreshCalculatedAddresses() +{ + // If no addresses have been created, do nothing. The calculated addresses will be created along with the addresses of the native + // readers in createAndSetAddresses(). + if ( m_allResultAddresses.empty() ) return; + + std::erase_if( m_allResultAddresses, []( const RifEclipseSummaryAddress& adr ) { return adr.isCalculated(); } ); + + for ( const auto& r : m_readers ) + { + if ( dynamic_cast( r.get() ) == nullptr ) continue; + + r->createAndSetAddresses(); + + const auto& resultAddresses = r->allResultAddresses(); + m_allResultAddresses.insert( resultAddresses.begin(), resultAddresses.end() ); + } +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/FileInterface/RifMultipleSummaryReaders.h b/ApplicationLibCode/FileInterface/RifMultipleSummaryReaders.h index 294efdaaa9..d2e6f40c2d 100644 --- a/ApplicationLibCode/FileInterface/RifMultipleSummaryReaders.h +++ b/ApplicationLibCode/FileInterface/RifMultipleSummaryReaders.h @@ -41,6 +41,7 @@ public: RiaDefines::EclipseUnitSystem unitSystem() const override; void createAndSetAddresses() override; + void refreshCalculatedAddresses() override; protected: size_t keywordCount() const override; diff --git a/ApplicationLibCode/FileInterface/RifSummaryReaderInterface.cpp b/ApplicationLibCode/FileInterface/RifSummaryReaderInterface.cpp index 54fd74fa51..52bb32f283 100644 --- a/ApplicationLibCode/FileInterface/RifSummaryReaderInterface.cpp +++ b/ApplicationLibCode/FileInterface/RifSummaryReaderInterface.cpp @@ -69,6 +69,13 @@ void RifSummaryReaderInterface::createAddressesIfRequired() } } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RifSummaryReaderInterface::refreshCalculatedAddresses() +{ +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/FileInterface/RifSummaryReaderInterface.h b/ApplicationLibCode/FileInterface/RifSummaryReaderInterface.h index a05f488c70..2f9235c997 100644 --- a/ApplicationLibCode/FileInterface/RifSummaryReaderInterface.h +++ b/ApplicationLibCode/FileInterface/RifSummaryReaderInterface.h @@ -55,6 +55,10 @@ public: virtual void createAndSetAddresses(); void createAddressesIfRequired(); + // Recreate the addresses provided by calculation objects. Does nothing if no addresses have been created yet, as the calculated + // addresses in that case are created as part of createAddressesIfRequired(). + virtual void refreshCalculatedAddresses(); + int serialNumber() const; // Returns the number of result addresses. If no addresses are present, keywordCount() is returned. diff --git a/ApplicationLibCode/ProjectDataModel/RimSummaryCalculationCollection.cpp b/ApplicationLibCode/ProjectDataModel/RimSummaryCalculationCollection.cpp index b6e5a3ee26..50e76eb289 100644 --- a/ApplicationLibCode/ProjectDataModel/RimSummaryCalculationCollection.cpp +++ b/ApplicationLibCode/ProjectDataModel/RimSummaryCalculationCollection.cpp @@ -22,6 +22,7 @@ #include "RifSummaryReaderInterface.h" +#include "RimDeltaSummaryEnsemble.h" #include "RimObservedSummaryData.h" #include "RimSummaryCalculation.h" #include "RimSummaryCase.h" @@ -87,7 +88,23 @@ void RimSummaryCalculationCollection::updateDataDependingOnCalculations() if ( auto summaryCaseCollection = RiaSummaryTools::summaryCaseMainCollection() ) { + // A delta ensemble derives its addresses from the source ensembles, and must be updated after the source ensembles. The delta + // ensembles are stored after the source ensembles, as an ensemble must exist before it can be used as a source. + std::vector deltaEnsembles; + for ( auto ensemble : summaryCaseCollection->summaryEnsembles() ) + { + if ( dynamic_cast( ensemble ) ) + { + deltaEnsembles.push_back( ensemble ); + } + else + { + ensemble->onCalculationUpdated(); + } + } + + for ( auto ensemble : deltaEnsembles ) { ensemble->onCalculationUpdated(); } diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimDeltaSummaryCase.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimDeltaSummaryCase.cpp index 22aaa86b0d..8b69da768c 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimDeltaSummaryCase.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimDeltaSummaryCase.cpp @@ -420,6 +420,20 @@ void RimDeltaSummaryCase::createSummaryReaderInterface() } } +//-------------------------------------------------------------------------------------------------- +/// The addresses of a delta case is the union of the addresses of the source cases. Rebuild the union to pick up calculated addresses +/// created after the source case addresses were created, and discard cached values based on calculations that may have changed. +//-------------------------------------------------------------------------------------------------- +void RimDeltaSummaryCase::refreshCalculatedAddresses() +{ + // If no addresses have been created, do nothing. The addresses will be created on demand in summaryReader(). + if ( m_allResultAddresses.empty() ) return; + + std::erase_if( m_dataCache, []( const auto& item ) { return item.first.isCalculated(); } ); + + createSummaryReaderInterface(); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimDeltaSummaryCase.h b/ApplicationLibCode/ProjectDataModel/Summary/RimDeltaSummaryCase.h index cbc53c02bc..f1cbf84c86 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimDeltaSummaryCase.h +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimDeltaSummaryCase.h @@ -25,6 +25,7 @@ #include "cafPdmObject.h" #include "cafPdmPtrField.h" +#include #include class RifEclipseSummaryAddress; @@ -79,6 +80,7 @@ public: void createSummaryReaderInterface() override; RifSummaryReaderInterface* summaryReader() override; + void refreshCalculatedAddresses() override; void updateDisplayNameFromCases(); diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryEnsemble.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryEnsemble.cpp index ce59d075ad..afc917f997 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryEnsemble.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryEnsemble.cpp @@ -1297,6 +1297,17 @@ void RimSummaryEnsemble::onCalculationUpdated() } } + // The addresses of the other cases in the ensemble are not created up front for performance reasons. If addresses have been created, + // the calculated addresses must be refreshed to reflect the current set of calculation objects. + // https://github.com/OPM/ResInsight/issues/14559 + for ( auto summaryCase : allSummaryCases() ) + { + if ( auto reader = summaryCase->summaryReader() ) + { + reader->refreshCalculatedAddresses(); + } + } + m_dataVectorFolders->deleteCalculatedAddresses(); m_dataVectorFolders->updateFolderStructure( ensembleSummaryAddresses(), -1, m_ensembleId ); diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index c1b7fb500b..b539c1887e 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -166,6 +166,7 @@ set(SOURCE_UNITTEST_FILES ${CMAKE_CURRENT_LIST_DIR}/RimMockSummaryCase.h ${CMAKE_CURRENT_LIST_DIR}/RimMockSummaryCase-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimSummaryCalculation-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RifMultipleSummaryReaders-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RiaConnectorTools-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RigWellTargetMappingTools-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RicWellPathExportMswGeometryPath-Test.cpp diff --git a/ApplicationLibCode/UnitTests/RifMultipleSummaryReaders-Test.cpp b/ApplicationLibCode/UnitTests/RifMultipleSummaryReaders-Test.cpp new file mode 100644 index 0000000000..bb21c4a068 --- /dev/null +++ b/ApplicationLibCode/UnitTests/RifMultipleSummaryReaders-Test.cpp @@ -0,0 +1,124 @@ +#include "gtest/gtest.h" + +#include "RifEclipseSummaryAddress.h" +#include "RifMultipleSummaryReaders.h" + +#include "RimCalculatedSummaryCurveReader.h" +#include "RimMockSummaryCase.h" +#include "RimProject.h" +#include "RimSummaryAddress.h" +#include "RimSummaryCalculation.h" +#include "RimSummaryCalculationCollection.h" +#include "RimSummaryCalculationVariable.h" + +#include + +namespace +{ +//-------------------------------------------------------------------------------------------------- +/// Minimal stand-in for a native file reader, providing a single address +//-------------------------------------------------------------------------------------------------- +class StubNativeSummaryReader : public RifSummaryReaderInterface +{ +public: + std::vector timeSteps( const RifEclipseSummaryAddress& resultAddress ) const override { return {}; } + std::pair> values( const RifEclipseSummaryAddress& resultAddress ) const override { return { false, {} }; } + std::string unitName( const RifEclipseSummaryAddress& resultAddress ) const override { return {}; } + RiaDefines::EclipseUnitSystem unitSystem() const override { return RiaDefines::EclipseUnitSystem::UNITS_METRIC; } + + void createAndSetAddresses() override { m_allResultAddresses.insert( RifEclipseSummaryAddress::fieldAddress( "FOPT" ) ); } + + size_t keywordCount() const override { return m_allResultAddresses.size(); } +}; + +//-------------------------------------------------------------------------------------------------- +/// Create a calculation producing a single field address +//-------------------------------------------------------------------------------------------------- +RimSummaryCalculation* createFieldCalculation() +{ + auto* calculation = dynamic_cast( RimProject::current()->calculationCollection()->addCalculation() ); + + calculation->setExpression( "MY_CALCULATION := x + 1" ); + calculation->parseExpression(); + + auto* variable = dynamic_cast( calculation->variables()->at( 0 ) ); + + RimSummaryAddress address; + address.setAddress( RifEclipseSummaryAddress::fieldAddress( "FOPT" ) ); + variable->setSummaryAddress( address ); + + return calculation; +} + +size_t calculatedAddressCount( const RifSummaryReaderInterface& reader ) +{ + size_t count = 0; + for ( const auto& adr : reader.allResultAddresses() ) + { + if ( adr.isCalculated() ) count++; + } + + return count; +} +} // namespace + +//-------------------------------------------------------------------------------------------------- +/// A calculation can be created after the addresses of a summary case have been created. The addresses of the calculated readers must +/// then be refreshed, as done by RifMultipleSummaryReaders::refreshCalculatedAddresses(). +/// +/// https://github.com/OPM/ResInsight/issues/14559 +//-------------------------------------------------------------------------------------------------- +TEST( RifMultipleSummaryReaders, RefreshCalculatedAddresses ) +{ + RimMockSummaryCase summaryCase; + + RifMultipleSummaryReaders multipleReaders; + multipleReaders.addReader( std::make_unique() ); + multipleReaders.addReader( std::make_unique( &summaryCase ) ); + + multipleReaders.createAndSetAddresses(); + + EXPECT_EQ( 1u, multipleReaders.allResultAddresses().size() ); + EXPECT_EQ( 0u, calculatedAddressCount( multipleReaders ) ); + + auto* calculation = createFieldCalculation(); + + multipleReaders.refreshCalculatedAddresses(); + + EXPECT_EQ( 2u, multipleReaders.allResultAddresses().size() ); + EXPECT_EQ( 1u, calculatedAddressCount( multipleReaders ) ); + + RimProject::current()->calculationCollection()->deleteCalculation( calculation ); + + multipleReaders.refreshCalculatedAddresses(); + + EXPECT_EQ( 1u, multipleReaders.allResultAddresses().size() ); + EXPECT_EQ( 0u, calculatedAddressCount( multipleReaders ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// Addresses are not created up front for the realizations of an ensemble. Refreshing the calculated addresses must not create a partial +/// set of addresses, as the calculated addresses are created along with the native addresses in createAndSetAddresses(). +//-------------------------------------------------------------------------------------------------- +TEST( RifMultipleSummaryReaders, RefreshCalculatedAddressesForReaderWithNoAddresses ) +{ + RimMockSummaryCase summaryCase; + + RifMultipleSummaryReaders multipleReaders; + multipleReaders.addReader( std::make_unique() ); + multipleReaders.addReader( std::make_unique( &summaryCase ) ); + + auto* calculation = createFieldCalculation(); + + multipleReaders.refreshCalculatedAddresses(); + + EXPECT_TRUE( multipleReaders.allResultAddresses().empty() ); + + // The calculated addresses are created along with the addresses of the native readers + multipleReaders.createAndSetAddresses(); + + EXPECT_EQ( 2u, multipleReaders.allResultAddresses().size() ); + EXPECT_EQ( 1u, calculatedAddressCount( multipleReaders ) ); + + RimProject::current()->calculationCollection()->deleteCalculation( calculation ); +}