diff --git a/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.cpp b/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.cpp index 096ef5a3a2..111177f0e0 100644 --- a/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.cpp +++ b/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.cpp @@ -1070,6 +1070,46 @@ bool RifOpmFlowDeckFile::replaceKeywordAtIndex( const Opm::FileDeck::Index& inde return true; } +//-------------------------------------------------------------------------------------------------- +/// Remove all but one occurrence of the given keyword. Keeping an original occurrence in place +/// preserves its source block, so a later in-place replace keeps the keyword in its original +/// (possibly included) file when the deck is saved. Keep the first occurrence for cumulative +/// keywords (e.g. FAULTS), and the last occurrence for keywords where a later occurrence +/// overrides an earlier one (e.g. ACTNUM). Returns number of removed keywords. +//-------------------------------------------------------------------------------------------------- +int RifOpmFlowDeckFile::removeDuplicateKeywords( const std::string& keywordName, bool keepFirstOccurrence ) +{ + if ( m_fileDeck.get() == nullptr ) return 0; + + // Find all indices where this keyword appears + std::vector indices; + for ( auto it = m_fileDeck->start(); it != m_fileDeck->stop(); it++ ) + { + const auto& kw = m_fileDeck->operator[]( it ); + if ( kw.name() == keywordName ) + { + indices.push_back( it ); + } + } + + if ( indices.size() < 2 ) return 0; + + // Remove in reverse order to maintain valid indices, keeping one occurrence + auto first = indices.rbegin(); + auto last = indices.rend(); + if ( keepFirstOccurrence ) + --last; + else + ++first; + + for ( auto it = first; it != last; ++it ) + { + m_fileDeck->erase( *it ); + } + + return static_cast( indices.size() ) - 1; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.h b/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.h index 6026f5a31f..d32f1fe539 100644 --- a/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.h +++ b/ApplicationLibCode/FileInterface/RifOpmFlowDeckFile.h @@ -110,6 +110,7 @@ public: bool replaceKeywordAtIndex( const Opm::FileDeck::Index& index, const Opm::DeckKeyword& keyword ); int removeKeywords( const std::string& keywordName ); + int removeDuplicateKeywords( const std::string& keywordName, bool keepFirstOccurrence ); bool removeKeywordAtIndex( const Opm::FileDeck::Index& index ); private: diff --git a/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.cpp b/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.cpp index 635a36432d..b13e0d3a1e 100644 --- a/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.cpp +++ b/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.cpp @@ -210,8 +210,10 @@ std::expected RigSimulationInputTool::updateCornerPointGridInDeck RigResdataGridConverter::convertGridToCornerPointArrays( gridAdapter, coordArray, zcornArray, actnumArray ); - // Remove ACTNUM to handle deck where it appears more than once. - deckFile.removeKeywords( "ACTNUM" ); + // Collapse ACTNUM to a single occurrence so the replacement below stays in its original + // (possibly included) file when the deck is saved. Keep the last occurrence, as it is the + // effective one when ACTNUM appears more than once. + deckFile.removeDuplicateKeywords( "ACTNUM", false ); auto keywords = deckFile.keywords( false ); @@ -1123,8 +1125,10 @@ std::expected RigSimulationInputTool::addFaultsToDeckFile( RimEcl const RigSimulationInputSettings& settings, RifOpmFlowDeckFile& deckFile ) { - // Remove FAULTS to handle deck where it appears more than once. - deckFile.removeKeywords( "FAULTS" ); + // Collapse FAULTS to a single occurrence so the replacement below stays in its original + // (possibly included) file when the deck is saved. Keep the first occurrence so fault + // definitions do not move past keywords referencing them (e.g. MULTFLT). + deckFile.removeDuplicateKeywords( "FAULTS", true ); // Create FAULTS keyword using the factory Opm::DeckKeyword faultsKw = diff --git a/ApplicationLibCode/UnitTests/RigSimulationInputTool-Test.cpp b/ApplicationLibCode/UnitTests/RigSimulationInputTool-Test.cpp index 654c718f92..bfa38a1cf8 100644 --- a/ApplicationLibCode/UnitTests/RigSimulationInputTool-Test.cpp +++ b/ApplicationLibCode/UnitTests/RigSimulationInputTool-Test.cpp @@ -41,6 +41,7 @@ #include #include +#include #include #include @@ -488,6 +489,93 @@ TEST( RigSimulationInputTool, ExportModel5 ) EXPECT_TRUE( fileContent.contains( " 20 15 10 /" ) ) << "File does not contain expected sector dimensions 20 15 10"; } +//-------------------------------------------------------------------------------------------------- +/// Test that ACTNUM and FAULTS sourced from include files stay in their include files when +/// exporting, instead of being written inline into the main DATA file. See issue #14622. +//-------------------------------------------------------------------------------------------------- +TEST( RigSimulationInputTool, ExportModel5_ActnumFaultsStayInIncludeFiles ) +{ + // Load model5 test data + QDir baseFolder( TEST_DATA_DIR ); + bool subFolderExists = baseFolder.cd( "RigSimulationInputTool/model5" ); + ASSERT_TRUE( subFolderExists ); + + QString egridFilename( "0_BASE_MODEL5.EGRID" ); + QString egridFilePath = baseFolder.absoluteFilePath( egridFilename ); + ASSERT_TRUE( QFile::exists( egridFilePath ) ); + + // Deck with an inline ACTNUM overridden by ACTNUM in an include file, and FAULTS in an include file + QString dataFilename( "3_ACTNUM_FAULTS_MODEL5.DATA" ); + QString dataFilePath = baseFolder.absoluteFilePath( dataFilename ); + ASSERT_TRUE( QFile::exists( dataFilePath ) ); + + // Create Eclipse case and load grid + std::unique_ptr resultCase( new RimEclipseResultCase ); + cvf::ref caseData = new RigEclipseCaseData( resultCase.get() ); + + cvf::ref reader = new RifReaderEclipseOutput; + bool loadResult = reader->open( egridFilePath, caseData.p() ); + ASSERT_TRUE( loadResult ); + ASSERT_TRUE( caseData->mainGrid() != nullptr ); + + // Create temporary directory for export + QTemporaryDir tempDir; + ASSERT_TRUE( tempDir.isValid() ); + + QString exportFilePath = tempDir.path() + "/exported_model.DATA"; + + // Set up export settings for sector export + RigSimulationInputSettings settings; + settings.setMin( caf::VecIjk0( 0, 0, 0 ) ); + settings.setMax( caf::VecIjk0( 19, 14, 9 ) ); // Sector (0-based inclusive) + setSettingsRefinement( settings, cvf::Vec3st( 1, 1, 1 ) ); // No refinement + settings.setInputDeckFileName( dataFilePath ); + settings.setOutputDeckFileName( exportFilePath ); + + // Create visibility from IJK bounds + cvf::ref visibility = + RigEclipseCaseDataTools::createVisibilityFromIjkBounds( caseData.p(), settings.min(), settings.max() ); + + // Export simulation input + resultCase->setReservoirData( caseData.p() ); + auto exportResult = RigSimulationInputTool::exportSimulationInput( *resultCase, settings, visibility.p() ); + ASSERT_TRUE( exportResult.has_value() ) << "Export failed: " << exportResult.error().toStdString(); + ASSERT_TRUE( QFile::exists( exportFilePath ) ); + + // The exported deck must still parse + RifOpmFlowDeckFile deckFile; + ASSERT_TRUE( deckFile.loadDeck( exportFilePath.toStdString() ).has_value() ) << "Failed to load exported deck file"; + + // The duplicate ACTNUM must be collapsed to a single occurrence with sector-sized data (20x15x10) + auto actnumKeywords = deckFile.findAllKeywords( "ACTNUM" ); + ASSERT_EQ( 1u, actnumKeywords.size() ); + EXPECT_EQ( 20u * 15u * 10u, actnumKeywords.front().getRecord( 0 ).getItem( 0 ).data_size() ); + + auto readTextFile = []( const QString& filePath ) -> QString + { + QFile file( filePath ); + EXPECT_TRUE( file.open( QIODevice::ReadOnly | QIODevice::Text ) ) << "Failed to open " << filePath.toStdString(); + return file.readAll(); + }; + + // The main DATA file must reference the include files instead of containing the keywords inline + QString dataContent = readTextFile( exportFilePath ); + const QRegularExpression actnumRegex( "^ACTNUM", QRegularExpression::MultilineOption ); + const QRegularExpression faultsRegex( "^FAULTS", QRegularExpression::MultilineOption ); + EXPECT_FALSE( dataContent.contains( actnumRegex ) ) << "ACTNUM must not be written inline in the main DATA file"; + EXPECT_FALSE( dataContent.contains( faultsRegex ) ) << "FAULTS must not be written inline in the main DATA file"; + EXPECT_TRUE( dataContent.contains( "include/actnum_model5.grdecl" ) ); + EXPECT_TRUE( dataContent.contains( "include/faults_model5.inc" ) ); + + // The include files must contain the transformed sector-specific keywords + QString actnumContent = readTextFile( tempDir.path() + "/include/actnum_model5.grdecl" ); + EXPECT_TRUE( actnumContent.contains( actnumRegex ) ) << "ACTNUM missing from exported include file"; + + QString faultsContent = readTextFile( tempDir.path() + "/include/faults_model5.inc" ); + EXPECT_TRUE( faultsContent.contains( faultsRegex ) ) << "FAULTS missing from exported include file"; + EXPECT_FALSE( faultsContent.contains( "F1" ) ) << "Original FAULTS data should have been replaced"; +} + //-------------------------------------------------------------------------------------------------- /// Test exportSimulationInput with model5 data using BCCON_BCPROP boundary condition //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/UnitTests/TestData/RigSimulationInputTool/model5/3_ACTNUM_FAULTS_MODEL5.DATA b/ApplicationLibCode/UnitTests/TestData/RigSimulationInputTool/model5/3_ACTNUM_FAULTS_MODEL5.DATA new file mode 100644 index 0000000000..a664439390 --- /dev/null +++ b/ApplicationLibCode/UnitTests/TestData/RigSimulationInputTool/model5/3_ACTNUM_FAULTS_MODEL5.DATA @@ -0,0 +1,322 @@ +-- This reservoir simulation deck is made available under the Open Database +-- License: http://opendatacommons.org/licenses/odbl/1.0/. Any rights in +-- individual contents of the database are licensed under the Database Contents +-- License: http://opendatacommons.org/licenses/dbcl/1.0/ + +-- Copyright (C) 2020 Equinor + +-- This is the base case for model5. +-- + + +------------------------------------------------------------------------------------------------ +RUNSPEC +------------------------------------------------------------------------------------------------ + + +DIMENS + 20 30 10 / + + +OIL +WATER +GAS +DISGAS +--VAPOIL + +METRIC + +START + 01 'JAN' 2020 / + +-- +GRIDOPTS + 'YES' 0 / + +EQLDIMS + 1 1* 25 / + + +REGDIMS +-- max. ntfip nmfipr max. nrfreg max. ntfreg + 3 2 1* 2 / + +-- +TABDIMS +--ntsfun ntpvt max.nssfun max.nppvt max.ntfip max.nrpvt + 1 1 150 60 3 60 / + +-- +WELLDIMS +--max.well max.con/well max.grup max.w/grup + 10 15 9 10 / + +--FLOW THP WCT GCT ALQ VFP +VFPPDIMS + 22 13 10 13 13 50 / + + + +UNIFIN +UNIFOUT + +------------------------------------------------------------------------------------------------ +GRID +------------------------------------------------------------------------------------------------ + +-- +NEWTRAN + +-- +GRIDFILE + 0 1 / + +-- +GRIDUNIT +METRES / + +-- +INIT + + +INCLUDE + 'include/test1_20x30x10.grdecl' / + +INCLUDE + 'include/permx_model5.grdecl' / + + +-- Inline ACTNUM that is overridden by the ACTNUM in the include file below +ACTNUM + 6000*1 / + +INCLUDE + 'include/actnum_model5.grdecl' / + +INCLUDE + 'include/faults_model5.inc' / + +PORO + 6000*0.28 / + +COPY + PERMX PERMY / + PERMX PERMZ / +/ + +MULTIPLY + PERMZ 0.1 / +/ + +RPTGRID + 'ALLNNC' / + +EQUALS + 'MULTY' 0.01 1 20 14 14 1 10 / +/ + + +------------------------------------------------------------------------------------------------ +EDIT +------------------------------------------------------------------------------------------------ + + +------------------------------------------------------------------------------------------------ +PROPS +------------------------------------------------------------------------------------------------ + +NOECHO + +INCLUDE + 'include/pvt_live_oil_dgas.ecl' / + + +INCLUDE + 'include/rock.inc' / + +INCLUDE + 'include/relperm.inc' / + + +------------------------------------------------------------------------------------------------ +REGIONS +------------------------------------------------------------------------------------------------ + +EQLNUM + 6000*1 / + +EQUALS + FIPNUM 1 1 20 1 14 1 10 / + FIPNUM 2 1 20 15 30 1 10 / +/ + +SATNUM + 6000*1 / + +-- custom region +FIPABC + 2000*1 2000*2 2000*3 / + +------------------------------------------------------------------------------------------------ +SOLUTION +------------------------------------------------------------------------------------------------ + + +RPTRST + 'BASIC = 2' 'PBPD' / + +EQUIL +-- Datum P woc Pc goc Pc Rsvd Rvvd + 2000.00 195.0 2070 0.0 500.00 0.0 1 0 0 / + +PBVD + 2000.00 75.00 + 2150.00 75.00 / + + + +------------------------------------------------------------------------------------------------ +SUMMARY +------------------------------------------------------------------------------------------------ + + +INCLUDE + 'include/summary.inc' / + + +------------------------------------------------------------------------------------------------ +SCHEDULE +------------------------------------------------------------------------------------------------ + +-- +-- FIELD +-- | +-- PLAT-A +-- ---------------+--------------------- +-- | | +-- M5S M5N +-- ---------+---------- -----+------- +-- | | | | +-- B1 G1 C1 F1 +-- ----+------ ---+--- ---+--- ---+--- +-- | | | | | | | | | +-- B-1H B-2H B-3H G-3H G-4H C-1H C-2H F-1H F-2H +-- + +TUNING + 0.5 1 / + / + 2* 50 1* 20 / + +NUPCOL + 4 / + + +GRUPTREE + 'PROD' 'FIELD' / + + 'M5S' 'PLAT-A' / + 'M5N' 'PLAT-A' / + + 'C1' 'M5N' / + 'F1' 'M5N' / + 'B1' 'M5S' / + 'G1' 'M5S' / + / + +RPTRST + 'BASIC=2' / + + +INCLUDE + 'include/well_vfp.ecl' / + +INCLUDE + 'include/flowl_b_vfp.ecl' / + +INCLUDE + 'include/flowl_c_vfp.ecl' / + + +WELSPECS +--WELL GROUP IHEEL JHEEL DREF PHASE DRAD INFEQ SIINS XFLOW PRTAB DENS + 'B-1H' 'B1' 11 3 1* OIL 1* 1* SHUT 1* 1* 1* / + 'B-2H' 'B1' 4 7 1* OIL 1* 1* SHUT 1* 1* 1* / + 'B-3H' 'B1' 11 12 1* OIL 1* 1* SHUT 1* 1* 1* / + 'C-1H' 'C1' 13 20 1* OIL 1* 1* SHUT 1* 1* 1* / + 'C-2H' 'C1' 12 27 1* OIL 1* 1* SHUT 1* 1* 1* / +/ + +WELSPECS + 'F-1H' 'F1' 19 4 1* WATER 1* 1* SHUT 1* 1* 1* / + 'F-2H' 'F1' 19 12 1* WATER 1* 1* SHUT 1* 1* 1* / + 'G-3H' 'G1' 19 21 1* WATER 1* 1* SHUT 1* 1* 1* / + 'G-4H' 'G1' 19 25 1* WATER 1* 1* SHUT 1* 1* 1* / +/ + +COMPDAT +--WELL I J K1 K2 OP/SH SATN TRAN WBDIA KH SKIN DFACT DIR PEQVR + 'B-1H' 11 3 1 5 OPEN 1* 1* 0.216 1* 0 1* Z 1* / + 'B-2H' 4 7 1 6 OPEN 1* 1* 0.216 1* 0 1* Z 1* / + 'B-3H' 11 12 1 4 OPEN 1* 1* 0.216 1* 0 1* Z 1* / + 'C-1H' 13 20 1 4 OPEN 1* 1* 0.216 1* 0 1* Z 1* / + 'C-2H' 12 27 1 5 OPEN 1* 1* 0.216 1* 0 1* Z 1* / +/ + +COMPDAT + 'F-1H' 19 4 6 10 OPEN 1* 1* 0.216 1* 0 1* Z 1* / + 'F-2H' 19 12 6 10 OPEN 1* 1* 0.216 1* 0 1* Z 1* / + 'G-3H' 19 21 6 10 OPEN 1* 1* 0.216 1* 0 1* Z 1* / + 'G-4H' 19 25 6 10 OPEN 1* 1* 0.216 1* 0 1* Z 1* / +/ + +WCONPROD +-- Well_name Status Ctrl Orate Wrate Grate Lrate RFV FBHP WHP VFP Glift + 'B-1H' OPEN ORAT 1500.0 1* 1* 3000.0 1* 100.0 30 1 1* / + 'B-2H' SHUT ORAT 1500.0 1* 1* 3000.0 1* 100.0 30 1 1* / + 'B-3H' OPEN ORAT 1500.0 1* 1* 3000.0 1* 100.0 30 1 1* / + 'C-1H' OPEN ORAT 1500.0 1* 1* 3000.0 1* 100.0 30 1 1* / + 'C-2H' SHUT ORAT 1500.0 1* 1* 3000.0 1* 100.0 30 1 1* / +/ + +GCONINJE + 'FIELD' 'WATER' 'VREP' 3* 1.020 'NO' 5* / +/ + + +WCONINJE +-- Well_name Type Status Ctrl SRate1 Rrate BHP THP VFP + 'F-1H' WATER OPEN GRUP 4000 1* 225.0 1* 1* / + 'F-2H' WATER OPEN GRUP 4000 1* 225.0 1* 1* / + 'G-3H' WATER OPEN GRUP 4000 1* 225.0 1* 1* / + 'G-4H' WATER OPEN GRUP 4000 1* 225.0 1* 1* / +/ + +TSTEP + 0.5 / + + +DATES + 1 FEB 2020 / +/ + + +WELOPEN + 'B-2H' OPEN / +/ + +DATES + 1 MAR 2020 / +/ + +WELOPEN + 'C-2H' OPEN / +/ + +DATES + 1 APR 2020 / + 1 MAY 2020 / +/ + +END + diff --git a/ApplicationLibCode/UnitTests/TestData/RigSimulationInputTool/model5/include/actnum_model5.grdecl b/ApplicationLibCode/UnitTests/TestData/RigSimulationInputTool/model5/include/actnum_model5.grdecl new file mode 100644 index 0000000000..ac26bed1ab --- /dev/null +++ b/ApplicationLibCode/UnitTests/TestData/RigSimulationInputTool/model5/include/actnum_model5.grdecl @@ -0,0 +1,4 @@ +NOECHO +ACTNUM + 6000*1 / +ECHO diff --git a/ApplicationLibCode/UnitTests/TestData/RigSimulationInputTool/model5/include/faults_model5.inc b/ApplicationLibCode/UnitTests/TestData/RigSimulationInputTool/model5/include/faults_model5.inc new file mode 100644 index 0000000000..18b98b6748 --- /dev/null +++ b/ApplicationLibCode/UnitTests/TestData/RigSimulationInputTool/model5/include/faults_model5.inc @@ -0,0 +1,3 @@ +FAULTS + 'F1' 5 5 1 15 1 10 'I' / +/