From 9a01e09ecd7d53a16310103bb743f322730a5637 Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Mon, 19 Oct 2026 14:00:02 +0200 Subject: [PATCH] Sector export: Complete all refined sub-cells along well connections COMPDAT and COMPSEGS cells were mapped to the center refined sub-cell only, so with K refinement only every other refined layer was completed, and the original connection factor was copied unchanged. Along the connection penetration direction (COMPDAT DIR) all sub-cells are now completed, with explicit CF and KH scaled by the sub-cell length fraction. COMPSEGS records are split the same way with the MD interval divided between the sub-cells. Perpendicular directions still use the center sub-cell. --- .../SimulationFile/RigSimulationInputTool.cpp | 360 +++++++++++++++--- .../SimulationFile/RigSimulationInputTool.h | 27 +- .../UnitTests/RigSimulationInputTool-Test.cpp | 177 ++++++++- 3 files changed, 497 insertions(+), 67 deletions(-) diff --git a/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.cpp b/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.cpp index b13e0d3a1e..07f03cea60 100644 --- a/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.cpp +++ b/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.cpp @@ -1264,12 +1264,114 @@ std::expected RigSimulationInputTool::processWelspecsR return Opm::DeckRecord{ std::move( items ) }; } +//-------------------------------------------------------------------------------------------------- +/// Refined sub-cell index and its length fraction of the original cell +//-------------------------------------------------------------------------------------------------- +namespace +{ +struct RefinedSubcell +{ + size_t index = 0; + double fraction = 1.0; +}; +} // namespace + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -std::expected RigSimulationInputTool::processCompdatRecord( const Opm::DeckRecord& record, - const std::string& wellName, - const RigSimulationInputSettings& settings ) +static size_t centerSubcellIndex( const RigRefinement& refinement, RigRefinement::Dimension dim, size_t sectorIndex ) +{ + return refinement.cumulativeOffset( dim, sectorIndex ) + refinement.subcellCount( dim, sectorIndex ) / 2; +} + +//-------------------------------------------------------------------------------------------------- +/// All refined sub-cells of an original (sector-relative) cell index, with length fractions +//-------------------------------------------------------------------------------------------------- +static std::vector allSubcells( const RigRefinement& refinement, RigRefinement::Dimension dim, size_t sectorIndex ) +{ + const size_t offset = refinement.cumulativeOffset( dim, sectorIndex ); + const auto& cumulative = refinement.cumulativeFractions( dim, sectorIndex ); + + std::vector subcells; + double previous = 0.0; + for ( size_t s = 0; s < cumulative.size(); ++s ) + { + subcells.push_back( { offset + s, cumulative[s] - previous } ); + previous = cumulative[s]; + } + return subcells; +} + +//-------------------------------------------------------------------------------------------------- +/// Penetration direction from COMPDAT DIR item (X -> I, Y -> J, Z or default -> K) +//-------------------------------------------------------------------------------------------------- +static RigRefinement::Dimension compdatPenetrationDimension( const Opm::DeckRecord& record ) +{ + using C = Opm::ParserKeywords::COMPDAT; + if ( record.hasItem( C::DIR::itemName ) ) + { + const auto& item = record.getItem( C::DIR::itemName ); + if ( item.hasValue( 0 ) ) + { + const auto dir = item.get( 0 ); + if ( !dir.empty() && ( dir[0] == 'X' || dir[0] == 'x' ) ) return RigRefinement::DimI; + if ( !dir.empty() && ( dir[0] == 'Y' || dir[0] == 'y' ) ) return RigRefinement::DimJ; + } + } + return RigRefinement::DimK; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +static bool hasExplicitDoubleValue( const Opm::DeckRecord& record, const std::string& itemName ) +{ + if ( !record.hasItem( itemName ) ) return false; + const auto& item = record.getItem( itemName ); + return item.hasValue( 0 ) && !item.defaultApplied( 0 ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RigSimulationInputTool::ConnectionDirectionMap RigSimulationInputTool::connectionDirections( const std::vector& compdatKeywords ) +{ + ConnectionDirectionMap directions; + + for ( const auto& keyword : compdatKeywords ) + { + for ( size_t r = 0; r < keyword.size(); ++r ) + { + const auto& record = keyword.getRecord( r ); + if ( record.size() < 5 ) continue; + + const auto& wellItem = record.getItem( 0 ); + if ( !wellItem.hasValue( 0 ) || wellItem.getType() != Opm::type_tag::string ) continue; + + const auto wellName = wellItem.get( 0 ); + const int i = record.getItem( 1 ).get( 0 ) - 1; + const int j = record.getItem( 2 ).get( 0 ) - 1; + const int k1 = record.getItem( 3 ).get( 0 ) - 1; + const int k2 = record.getItem( 4 ).get( 0 ) - 1; + if ( i < 0 || j < 0 || k1 < 0 || k2 < k1 ) continue; + + const auto dim = compdatPenetrationDimension( record ); + for ( int k = k1; k <= k2; ++k ) + { + directions[{ wellName, static_cast( i ), static_cast( j ), static_cast( k ) }] = dim; + } + } + } + + return directions; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected, QString> RigSimulationInputTool::processCompdatRecord( const Opm::DeckRecord& record, + const std::string& wellName, + const RigSimulationInputSettings& settings ) { // COMPDAT format: WELL I J K1 K2 STATE ... // Items: 0=WELL, 1=I, 2=J, 3=K1, 4=K2 @@ -1280,71 +1382,167 @@ std::expected RigSimulationInputTool::processCompdatRe .arg( record.size() ) ); } - std::vector items; - - // Copy well name - items.push_back( record.getItem( 0 ) ); - - // Transform I, J, K1, K2 int origI = record.getItem( 1 ).get( 0 ) - 1; // Convert to 0-based int origJ = record.getItem( 2 ).get( 0 ) - 1; int origK1 = record.getItem( 3 ).get( 0 ) - 1; int origK2 = record.getItem( 4 ).get( 0 ) - 1; - // Transform K1 - caf::VecIjk0 origIjkK1( origI, origJ, origK1 ); - auto transformResultK1 = - RigGridExportAdapter::transformIjkToSectorCoordinates( origIjkK1, settings.min(), settings.max(), settings.refinement(), true ); - - // Transform K2 - caf::VecIjk0 origIjkK2( origI, origJ, origK2 ); - auto transformResultK2 = - RigGridExportAdapter::transformIjkToSectorCoordinates( origIjkK2, settings.min(), settings.max(), settings.refinement(), true ); + const auto& refinement = settings.refinement(); + auto transformResultK1 = RigGridExportAdapter::transformIjkToSectorCoordinates( caf::VecIjk0( origI, origJ, origK1 ), + settings.min(), + settings.max(), + refinement, + true ); if ( !transformResultK1 ) { return std::unexpected( QString( "COMPDAT K1 coordinate for well %1 is out of sector bounds: %2" ).arg( wellName.c_str() ).arg( transformResultK1.error() ) ); } + auto transformResultK2 = RigGridExportAdapter::transformIjkToSectorCoordinates( caf::VecIjk0( origI, origJ, origK2 ), + settings.min(), + settings.max(), + refinement, + true ); if ( !transformResultK2 ) { return std::unexpected( QString( "COMPDAT K2 coordinate for well %1 is out of sector bounds: %2" ).arg( wellName.c_str() ).arg( transformResultK2.error() ) ); } - using C = Opm::ParserKeywords::COMPDAT; - items.push_back( RifOpmDeckTools::item( C::I::itemName, static_cast( transformResultK1->toOneBased().x() ) ) ); - items.push_back( RifOpmDeckTools::item( C::J::itemName, static_cast( transformResultK1->toOneBased().y() ) ) ); - items.push_back( RifOpmDeckTools::item( C::K1::itemName, static_cast( transformResultK1->toOneBased().z() ) ) ); - items.push_back( RifOpmDeckTools::item( C::K2::itemName, static_cast( transformResultK2->toOneBased().z() ) ) ); - - // Copy remaining items - for ( size_t i = 5; i < record.size(); ++i ) + if ( origK2 < origK1 ) { - items.push_back( record.getItem( i ) ); + return std::unexpected( + QString( "COMPDAT record for well %1 has K2 (%2) < K1 (%3)" ).arg( wellName.c_str() ).arg( origK2 + 1 ).arg( origK1 + 1 ) ); } - return Opm::DeckRecord{ std::move( items ) }; + using C = Opm::ParserKeywords::COMPDAT; + + const auto dim = compdatPenetrationDimension( record ); + const size_t sectorI = origI - settings.min().x(); + const size_t sectorJ = origJ - settings.min().y(); + const size_t sectorK1 = origK1 - settings.min().z(); + const size_t sectorK2 = origK2 - settings.min().z(); + + const bool hasExplicitFactors = hasExplicitDoubleValue( record, C::CONNECTION_TRANSMISSIBILITY_FACTOR::itemName ) || + hasExplicitDoubleValue( record, C::Kh::itemName ); + + bool isKRefinedInRange = false; + for ( size_t k = sectorK1; k <= sectorK2; ++k ) + { + if ( refinement.subcellCount( RigRefinement::DimK, k ) > 1 ) isKRefinedInRange = true; + } + + struct KSpan + { + size_t k1 = 0; + size_t k2 = 0; + double fraction = 1.0; + }; + + std::vector kSpans; + if ( !isKRefinedInRange ) + { + kSpans.push_back( { refinement.cumulativeOffset( RigRefinement::DimK, sectorK1 ), + refinement.cumulativeOffset( RigRefinement::DimK, sectorK2 ), + 1.0 } ); + } + else if ( dim == RigRefinement::DimK && !hasExplicitFactors ) + { + // The simulator computes the connection factors, so the full refined range can be given in one record + kSpans.push_back( { refinement.cumulativeOffset( RigRefinement::DimK, sectorK1 ), + refinement.cumulativeOffset( RigRefinement::DimK, sectorK2 + 1 ) - 1, + 1.0 } ); + } + else if ( dim == RigRefinement::DimK ) + { + for ( size_t k = sectorK1; k <= sectorK2; ++k ) + { + for ( const auto& subcell : allSubcells( refinement, RigRefinement::DimK, k ) ) + { + kSpans.push_back( { subcell.index, subcell.index, subcell.fraction } ); + } + } + } + else + { + for ( size_t k = sectorK1; k <= sectorK2; ++k ) + { + const size_t centerK = centerSubcellIndex( refinement, RigRefinement::DimK, k ); + kSpans.push_back( { centerK, centerK, 1.0 } ); + } + } + + const std::vector iSubcells = + ( dim == RigRefinement::DimI ) + ? allSubcells( refinement, RigRefinement::DimI, sectorI ) + : std::vector{ { centerSubcellIndex( refinement, RigRefinement::DimI, sectorI ), 1.0 } }; + const std::vector jSubcells = + ( dim == RigRefinement::DimJ ) + ? allSubcells( refinement, RigRefinement::DimJ, sectorJ ) + : std::vector{ { centerSubcellIndex( refinement, RigRefinement::DimJ, sectorJ ), 1.0 } }; + + auto createRecord = [&]( size_t i, size_t j, size_t k1, size_t k2, double fraction ) + { + std::vector items; + items.push_back( record.getItem( 0 ) ); + items.push_back( RifOpmDeckTools::item( C::I::itemName, static_cast( i + 1 ) ) ); + items.push_back( RifOpmDeckTools::item( C::J::itemName, static_cast( j + 1 ) ) ); + items.push_back( RifOpmDeckTools::item( C::K1::itemName, static_cast( k1 + 1 ) ) ); + items.push_back( RifOpmDeckTools::item( C::K2::itemName, static_cast( k2 + 1 ) ) ); + + for ( size_t idx = 5; idx < record.size(); ++idx ) + { + const auto& item = record.getItem( idx ); + const bool isScalable = item.name() == C::CONNECTION_TRANSMISSIBILITY_FACTOR::itemName || item.name() == C::Kh::itemName; + if ( fraction != 1.0 && isScalable && hasExplicitDoubleValue( record, item.name() ) ) + { + items.push_back( RifOpmDeckTools::item( item.name(), item.get( 0 ) * fraction ) ); + } + else + { + items.push_back( item ); + } + } + + return Opm::DeckRecord{ std::move( items ) }; + }; + + std::vector records; + for ( const auto& kSpan : kSpans ) + { + for ( const auto& iSubcell : iSubcells ) + { + for ( const auto& jSubcell : jSubcells ) + { + records.push_back( + createRecord( iSubcell.index, jSubcell.index, kSpan.k1, kSpan.k2, kSpan.fraction * iSubcell.fraction * jSubcell.fraction ) ); + } + } + } + + return records; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- -std::expected RigSimulationInputTool::processCompsegsRecord( const Opm::DeckRecord& record, - const std::string& wellName, - bool isWellNameRecord, - const RigSimulationInputSettings& settings ) +std::expected, QString> RigSimulationInputTool::processCompsegsRecord( const Opm::DeckRecord& record, + const std::string& wellName, + bool isWellNameRecord, + const RigSimulationInputSettings& settings, + const ConnectionDirectionMap& directions ) { // COMPSEGS format: first record is well name, subsequent records are segment data // Well name record: just copy as-is if ( isWellNameRecord ) { - return Opm::DeckRecord( record ); + return std::vector{ Opm::DeckRecord( record ) }; } - // Segment record format: I J K BRANCH START_MD END_MD ... - // Items: 0=I, 1=J, 2=K + // Segment record format: I J K BRANCH START_MD END_MD DIRECTION END_IJK ... + // Items: 0=I, 1=J, 2=K, 4=START_MD, 5=END_MD, 6=DIRECTION if ( record.size() < 3 ) { return std::unexpected( QString( "COMPSEGS segment record for well %1 has insufficient items (expected at least 3, got %2)" ) @@ -1352,13 +1550,11 @@ std::expected RigSimulationInputTool::processCompsegsR .arg( record.size() ) ); } - std::vector items; + caf::VecIjk0 origIjk = extractIjk( record, 0, 1, 2 ).toZeroBased(); - // Transform I, J, K (first three items) - caf::VecIjk1 origIjk = extractIjk( record, 0, 1, 2 ); + const auto& refinement = settings.refinement(); - auto transformResult = - RigGridExportAdapter::transformIjkToSectorCoordinates( origIjk.toZeroBased(), settings.min(), settings.max(), settings.refinement(), true ); + auto transformResult = RigGridExportAdapter::transformIjkToSectorCoordinates( origIjk, settings.min(), settings.max(), refinement, true ); if ( !transformResult ) { @@ -1366,18 +1562,69 @@ std::expected RigSimulationInputTool::processCompsegsR QString( "COMPSEGS segment coordinate for well %1 is out of sector bounds: %2" ).arg( wellName.c_str() ).arg( transformResult.error() ) ); } - // Add transformed I, J, K - items.push_back( RifOpmDeckTools::item( "I", static_cast( transformResult->toOneBased().x() ) ) ); - items.push_back( RifOpmDeckTools::item( "J", static_cast( transformResult->toOneBased().y() ) ) ); - items.push_back( RifOpmDeckTools::item( "K", static_cast( transformResult->toOneBased().z() ) ) ); - - // Copy remaining items - for ( size_t i = 3; i < record.size(); ++i ) + auto dim = RigRefinement::DimK; + if ( auto it = directions.find( { wellName, origIjk.x(), origIjk.y(), origIjk.z() } ); it != directions.end() ) { - items.push_back( record.getItem( i ) ); + dim = it->second; } - return Opm::DeckRecord{ std::move( items ) }; + const size_t sectorIndex = origIjk[dim] - settings.min()[dim]; + + std::vector subcells = { { ( *transformResult )[dim], 1.0 } }; + + const bool hasMdInterval = record.size() > 5 && record.getItem( 4 ).hasValue( 0 ) && record.getItem( 5 ).hasValue( 0 ); + const bool isCellRange = record.size() > 6 && record.getItem( 6 ).hasValue( 0 ); + if ( refinement.subcellCount( dim, sectorIndex ) > 1 ) + { + if ( hasMdInterval && !isCellRange ) + { + subcells = allSubcells( refinement, dim, sectorIndex ); + } + else + { + RiaLogging::warning( std::format( "COMPSEGS record for well {} at ({}, {}, {}) has no MD interval or uses a cell range. " + "Only the center refined sub-cell is assigned to the segment.", + wellName, + origIjk.x() + 1, + origIjk.y() + 1, + origIjk.z() + 1 ) ); + } + } + + std::vector records; + + double startFraction = 0.0; + for ( const auto& subcell : subcells ) + { + caf::VecIjk0 refinedIjk = *transformResult; + refinedIjk[dim] = subcell.index; + + std::vector items; + items.push_back( RifOpmDeckTools::item( "I", static_cast( refinedIjk.x() + 1 ) ) ); + items.push_back( RifOpmDeckTools::item( "J", static_cast( refinedIjk.y() + 1 ) ) ); + items.push_back( RifOpmDeckTools::item( "K", static_cast( refinedIjk.z() + 1 ) ) ); + + const double endFraction = startFraction + subcell.fraction; + for ( size_t i = 3; i < record.size(); ++i ) + { + if ( subcells.size() > 1 && ( i == 4 || i == 5 ) ) + { + const double startMd = record.getItem( 4 ).get( 0 ); + const double endMd = record.getItem( 5 ).get( 0 ); + const double fraction = ( i == 4 ) ? startFraction : endFraction; + items.push_back( RifOpmDeckTools::item( record.getItem( i ).name(), startMd + ( endMd - startMd ) * fraction ) ); + } + else + { + items.push_back( record.getItem( i ) ); + } + } + startFraction = endFraction; + + records.push_back( Opm::DeckRecord{ std::move( items ) } ); + } + + return records; } //-------------------------------------------------------------------------------------------------- @@ -2114,6 +2361,9 @@ std::expected RigSimulationInputTool::filterAndUpdateWellKeywords "WINJCLN", "WINJMULT", "WINJTEMP", "WLIFTOPT", "WPAVEDEP", "WRFTPLT", "WTEMP", "WTEST", "WTRACER", "WSEGVALV", "WSEGAICD", "WSEGSICD" }; + // Collect connection directions from the original COMPDAT before it is transformed, used to split COMPSEGS + const auto directions = connectionDirections( deckFile.findAllKeywords( "COMPDAT" ) ); + // Process each type of well keyword // Use findAllKeywordsWithIndices to get all occurrences with their positions for ( const auto& keywordName : wellKeywords ) @@ -2194,7 +2444,10 @@ std::expected RigSimulationInputTool::filterAndUpdateWellKeywords auto result = processCompdatRecord( record, wellName, settings ); if ( result ) { - filteredKeyword.addRecord( std::move( result.value() ) ); + for ( auto& rec : result.value() ) + { + filteredKeyword.addRecord( std::move( rec ) ); + } } else { @@ -2204,10 +2457,13 @@ std::expected RigSimulationInputTool::filterAndUpdateWellKeywords } else if ( keywordName == "COMPSEGS" ) { - auto result = processCompsegsRecord( record, wellName, isWellNameRecord, settings ); + auto result = processCompsegsRecord( record, wellName, isWellNameRecord, settings, directions ); if ( result ) { - filteredKeyword.addRecord( std::move( result.value() ) ); + for ( auto& rec : result.value() ) + { + filteredKeyword.addRecord( std::move( rec ) ); + } } else { diff --git a/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.h b/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.h index aff9f54ce1..d5124c0053 100644 --- a/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.h +++ b/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.h @@ -30,6 +30,7 @@ #include #include #include +#include #include class RimEclipseView; @@ -102,6 +103,24 @@ public: static std::expected processAqunumRecord( const Opm::DeckRecord& record, const caf::VecIjk0& min, const caf::VecIjk0& max, const RigRefinement& refinement ); + // Penetration direction of well connections, keyed on (well name, original 0-based I, J, K) + using ConnectionDirectionMap = std::map, RigRefinement::Dimension>; + + static ConnectionDirectionMap connectionDirections( const std::vector& compdatKeywords ); + + // Refined connections along the penetration direction are split into one record per sub-cell with + // explicit CF and KH scaled by the sub-cell length fraction. Perpendicular directions use the center sub-cell. + static std::expected, QString> + processCompdatRecord( const Opm::DeckRecord& record, const std::string& wellName, const RigSimulationInputSettings& settings ); + + // Segment records are split into one record per sub-cell along the connection penetration direction, + // with the MD interval divided by the sub-cell length fractions + static std::expected, QString> processCompsegsRecord( const Opm::DeckRecord& record, + const std::string& wellName, + bool isWellNameRecord, + const RigSimulationInputSettings& settings, + const ConnectionDirectionMap& directions ); + static std::vector createRefinedVisibility( const RigGridExportAdapter& gridAdapter ); static std::expected @@ -193,14 +212,6 @@ private: static std::expected processWelspecsRecord( const Opm::DeckRecord& record, const std::string& wellName, const RigSimulationInputSettings& settings ); - static std::expected - processCompdatRecord( const Opm::DeckRecord& record, const std::string& wellName, const RigSimulationInputSettings& settings ); - - static std::expected processCompsegsRecord( const Opm::DeckRecord& record, - const std::string& wellName, - bool isWellNameRecord, - const RigSimulationInputSettings& settings ); - static std::expected replaceKeywordWithBoxIndices( const std::string& keywordName, RimEclipseCase* eclipseCase, const RigSimulationInputSettings& settings, diff --git a/ApplicationLibCode/UnitTests/RigSimulationInputTool-Test.cpp b/ApplicationLibCode/UnitTests/RigSimulationInputTool-Test.cpp index bfa38a1cf8..3e4c3e1ac6 100644 --- a/ApplicationLibCode/UnitTests/RigSimulationInputTool-Test.cpp +++ b/ApplicationLibCode/UnitTests/RigSimulationInputTool-Test.cpp @@ -29,6 +29,7 @@ #include "RigMainGrid.h" #include "RigModelPaddingSettings.h" #include "RigNoRefinement.h" +#include "RigNonUniformRefinement.h" #include "RigSimulationInputSettings.h" #include "RigSimulationInputTool.h" #include "RigUniformRefinement.h" @@ -1297,9 +1298,9 @@ TEST( RigSimulationInputTool, ExportModel5WithRefinement ) } } - // Verify specific COMPDAT coordinates are centered correctly + // Verify COMPDAT K ranges cover all refined sub-cells // For refinement (1, 3, 5): - // - K dimension: refinement=5, center offset = (5+1)/2 = 3 + // - K dimension: refinement=5, original layer k maps to refined layers (k-1)*5+1 .. k*5 auto checkCompdatCoordinates = [&compdatKeywords]( const std::string& wellName, int expectedK1, int expectedK2 ) -> bool { return std::any_of( compdatKeywords.begin(), @@ -1319,13 +1320,13 @@ TEST( RigSimulationInputTool, ExportModel5WithRefinement ) } ); }; - // B-1H: original K1=1, K2=5, sector_min K=0 - // K1: (1-0)*5 + 3 = 3, K2: (5-0)*5 + 3 = 23 - EXPECT_TRUE( checkCompdatCoordinates( "B-1H", 3, 23 ) ) << "B-1H COMPDAT K coordinates not centered correctly"; + // B-1H: original K1=1, K2=5 (defaulted CF), sector_min K=0 + // All sub-cells of the original range: K1 = 1, K2 = 5*5 = 25 + EXPECT_TRUE( checkCompdatCoordinates( "B-1H", 1, 25 ) ) << "B-1H COMPDAT K range does not cover all refined sub-cells"; // B-2H: original K1=1, K2=6 - // K1: (1-0)*5 + 3 = 3, K2: (6-0)*5 + 3 = 28 - EXPECT_TRUE( checkCompdatCoordinates( "B-2H", 3, 28 ) ) << "B-2H COMPDAT K coordinates not centered correctly"; + // K1 = 1, K2 = 6*5 = 30 + EXPECT_TRUE( checkCompdatCoordinates( "B-2H", 1, 30 ) ) << "B-2H COMPDAT K range does not cover all refined sub-cells"; } //-------------------------------------------------------------------------------------------------- @@ -3057,3 +3058,165 @@ TEST( RigSimulationInputTool, ExportModel5WithBcconBcpropAndPadding ) EXPECT_EQ( 1, minKForNegZ ) << "\"Z-\" exterior face must remain at K=1 (top of padded grid)"; EXPECT_EQ( 1, maxKForNegZ ) << "\"Z-\" exterior face must remain at K=1 (top of padded grid)"; } + +//-------------------------------------------------------------------------------------------------- +/// Helper: parse a keyword from deck text and return all its records +//-------------------------------------------------------------------------------------------------- +static std::vector parseKeywordRecords( const std::string& deckText, const std::string& keywordName ) +{ + Opm::Parser parser; + auto deck = parser.parseString( deckText ); + const auto& keyword = deck[keywordName].back(); + + std::vector records; + for ( size_t i = 0; i < keyword.size(); ++i ) + { + records.push_back( keyword.getRecord( i ) ); + } + return records; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +static RigSimulationInputSettings createSectorSettings( const cvf::Vec3st& refCounts ) +{ + RigSimulationInputSettings settings; + settings.setMin( caf::VecIjk0( 0, 0, 0 ) ); + settings.setMax( caf::VecIjk0( 9, 9, 9 ) ); + setSettingsRefinement( settings, refCounts ); + return settings; +} + +//-------------------------------------------------------------------------------------------------- +/// A Z-direction connection in a K-refined layer must be completed in every sub-cell, with explicit +/// CF and KH split between the sub-cells (regression: only every other refined layer was completed) +//-------------------------------------------------------------------------------------------------- +TEST( RigSimulationInputTool, ProcessCompdatRecord_ZDirectionSplitAcrossKSubcells ) +{ + auto settings = createSectorSettings( cvf::Vec3st( 1, 1, 2 ) ); + auto records = parseKeywordRecords( "COMPDAT\n 'W' 3 4 2 2 'OPEN' 1* 10.0 0.216 100.0 2* 'Z' /\n/\n", "COMPDAT" ); + + auto result = RigSimulationInputTool::processCompdatRecord( records[0], "W", settings ); + ASSERT_TRUE( result.has_value() ) << result.error().toStdString(); + ASSERT_EQ( 2u, result->size() ); + + for ( size_t s = 0; s < 2; ++s ) + { + const auto& rec = result->at( s ); + EXPECT_EQ( 3, rec.getItem( "I" ).get( 0 ) ); + EXPECT_EQ( 4, rec.getItem( "J" ).get( 0 ) ); + EXPECT_EQ( 3 + static_cast( s ), rec.getItem( "K1" ).get( 0 ) ); + EXPECT_EQ( 3 + static_cast( s ), rec.getItem( "K2" ).get( 0 ) ); + EXPECT_DOUBLE_EQ( 5.0, rec.getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get( 0 ) ); + EXPECT_DOUBLE_EQ( 50.0, rec.getItem( "Kh" ).get( 0 ) ); + EXPECT_DOUBLE_EQ( 0.216, rec.getItem( "DIAMETER" ).get( 0 ) ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// With defaulted CF and KH the simulator computes the factors, so the full refined K range is kept in one record +//-------------------------------------------------------------------------------------------------- +TEST( RigSimulationInputTool, ProcessCompdatRecord_DefaultedFactorsKeepsSingleRecord ) +{ + auto settings = createSectorSettings( cvf::Vec3st( 1, 1, 2 ) ); + auto records = parseKeywordRecords( "COMPDAT\n 'W' 3 4 2 3 'OPEN' 2* 0.216 /\n/\n", "COMPDAT" ); + + auto result = RigSimulationInputTool::processCompdatRecord( records[0], "W", settings ); + ASSERT_TRUE( result.has_value() ) << result.error().toStdString(); + ASSERT_EQ( 1u, result->size() ); + EXPECT_EQ( 3, result->at( 0 ).getItem( "K1" ).get( 0 ) ); + EXPECT_EQ( 6, result->at( 0 ).getItem( "K2" ).get( 0 ) ); + EXPECT_FALSE( result->at( 0 ).getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).hasValue( 0 ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// An X-direction connection is split across the I sub-cells; J and K use the center sub-cell +//-------------------------------------------------------------------------------------------------- +TEST( RigSimulationInputTool, ProcessCompdatRecord_XDirectionSplitAcrossISubcells ) +{ + auto settings = createSectorSettings( cvf::Vec3st( 3, 2, 2 ) ); + auto records = parseKeywordRecords( "COMPDAT\n 'W' 2 1 1 1 'OPEN' 1* 9.0 0.216 90.0 2* 'X' /\n/\n", "COMPDAT" ); + + auto result = RigSimulationInputTool::processCompdatRecord( records[0], "W", settings ); + ASSERT_TRUE( result.has_value() ) << result.error().toStdString(); + ASSERT_EQ( 3u, result->size() ); + + for ( size_t s = 0; s < 3; ++s ) + { + const auto& rec = result->at( s ); + EXPECT_EQ( 4 + static_cast( s ), rec.getItem( "I" ).get( 0 ) ); + EXPECT_EQ( 2, rec.getItem( "J" ).get( 0 ) ); + EXPECT_EQ( 2, rec.getItem( "K1" ).get( 0 ) ); + EXPECT_EQ( 2, rec.getItem( "K2" ).get( 0 ) ); + EXPECT_DOUBLE_EQ( 3.0, rec.getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get( 0 ) ); + EXPECT_DOUBLE_EQ( 30.0, rec.getItem( "Kh" ).get( 0 ) ); + } +} + +//-------------------------------------------------------------------------------------------------- +/// Non-uniform K refinement splits the CF by the sub-cell thickness fractions +//-------------------------------------------------------------------------------------------------- +TEST( RigSimulationInputTool, ProcessCompdatRecord_NonUniformKRefinementScalesByFraction ) +{ + RigSimulationInputSettings settings; + settings.setMin( caf::VecIjk0( 0, 0, 0 ) ); + settings.setMax( caf::VecIjk0( 9, 9, 9 ) ); + auto refinement = std::make_unique( cvf::Vec3st( 10, 10, 10 ) ); + refinement->setCumulativeFractions( RigRefinement::DimK, 1, { 0.25, 1.0 } ); + settings.setRefinement( std::move( refinement ) ); + + auto records = parseKeywordRecords( "COMPDAT\n 'W' 3 4 2 2 'OPEN' 1* 8.0 0.216 /\n/\n", "COMPDAT" ); + + auto result = RigSimulationInputTool::processCompdatRecord( records[0], "W", settings ); + ASSERT_TRUE( result.has_value() ) << result.error().toStdString(); + ASSERT_EQ( 2u, result->size() ); + EXPECT_EQ( 2, result->at( 0 ).getItem( "K1" ).get( 0 ) ); + EXPECT_EQ( 3, result->at( 1 ).getItem( "K1" ).get( 0 ) ); + EXPECT_DOUBLE_EQ( 2.0, result->at( 0 ).getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get( 0 ) ); + EXPECT_DOUBLE_EQ( 6.0, result->at( 1 ).getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get( 0 ) ); + EXPECT_TRUE( result->at( 0 ).getItem( "Kh" ).defaultApplied( 0 ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// Without refinement the record is passed through unchanged +//-------------------------------------------------------------------------------------------------- +TEST( RigSimulationInputTool, ProcessCompdatRecord_NoRefinementUnchanged ) +{ + auto settings = createSectorSettings( cvf::Vec3st( 1, 1, 1 ) ); + auto records = parseKeywordRecords( "COMPDAT\n 'W' 3 4 2 5 'OPEN' 1* 10.0 0.216 100.0 2* 'Z' /\n/\n", "COMPDAT" ); + + auto result = RigSimulationInputTool::processCompdatRecord( records[0], "W", settings ); + ASSERT_TRUE( result.has_value() ) << result.error().toStdString(); + ASSERT_EQ( 1u, result->size() ); + EXPECT_EQ( 2, result->at( 0 ).getItem( "K1" ).get( 0 ) ); + EXPECT_EQ( 5, result->at( 0 ).getItem( "K2" ).get( 0 ) ); + EXPECT_DOUBLE_EQ( 10.0, result->at( 0 ).getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get( 0 ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// COMPSEGS cells are split along the connection direction with the MD interval divided between sub-cells +//-------------------------------------------------------------------------------------------------- +TEST( RigSimulationInputTool, ProcessCompsegsRecord_SplitAcrossKSubcells ) +{ + auto settings = createSectorSettings( cvf::Vec3st( 1, 1, 2 ) ); + + Opm::Parser parser; + auto deck = parser.parseString( "COMPDAT\n 'W' 3 4 2 2 'OPEN' 1* 10.0 0.216 /\n/\n" + "COMPSEGS\n 'W' /\n 3 4 2 1 100.0 120.0 /\n/\n" ); + + std::vector compdatKeywords = { deck["COMPDAT"].back() }; + auto directions = RigSimulationInputTool::connectionDirections( compdatKeywords ); + + const auto& compsegs = deck["COMPSEGS"].back(); + auto result = RigSimulationInputTool::processCompsegsRecord( compsegs.getRecord( 1 ), "W", false, settings, directions ); + ASSERT_TRUE( result.has_value() ) << result.error().toStdString(); + ASSERT_EQ( 2u, result->size() ); + + EXPECT_EQ( 3, result->at( 0 ).getItem( 2 ).get( 0 ) ); + EXPECT_EQ( 4, result->at( 1 ).getItem( 2 ).get( 0 ) ); + EXPECT_DOUBLE_EQ( 100.0, result->at( 0 ).getItem( 4 ).get( 0 ) ); + EXPECT_DOUBLE_EQ( 110.0, result->at( 0 ).getItem( 5 ).get( 0 ) ); + EXPECT_DOUBLE_EQ( 110.0, result->at( 1 ).getItem( 4 ).get( 0 ) ); + EXPECT_DOUBLE_EQ( 120.0, result->at( 1 ).getItem( 5 ).get( 0 ) ); +}