mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-24 14:17:52 -05:00
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.
This commit is contained in:
1 parent
1960cabc15
commit
9a01e09ecd
3 files changed
+497
-67
No files matched your search
@@ -1264,12 +1264,114 @@ std::expected<Opm::DeckRecord, QString> 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<Opm::DeckRecord, QString> 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<RefinedSubcell> 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<RefinedSubcell> 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<std::string>( 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<Opm::DeckKeyword>& 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<std::string>( 0 );
|
||||
const int i = record.getItem( 1 ).get<int>( 0 ) - 1;
|
||||
const int j = record.getItem( 2 ).get<int>( 0 ) - 1;
|
||||
const int k1 = record.getItem( 3 ).get<int>( 0 ) - 1;
|
||||
const int k2 = record.getItem( 4 ).get<int>( 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<size_t>( i ), static_cast<size_t>( j ), static_cast<size_t>( k ) }] = dim;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return directions;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::expected<std::vector<Opm::DeckRecord>, 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<Opm::DeckRecord, QString> RigSimulationInputTool::processCompdatRe
|
||||
.arg( record.size() ) );
|
||||
}
|
||||
|
||||
std::vector<Opm::DeckItem> items;
|
||||
|
||||
// Copy well name
|
||||
items.push_back( record.getItem( 0 ) );
|
||||
|
||||
// Transform I, J, K1, K2
|
||||
int origI = record.getItem( 1 ).get<int>( 0 ) - 1; // Convert to 0-based
|
||||
int origJ = record.getItem( 2 ).get<int>( 0 ) - 1;
|
||||
int origK1 = record.getItem( 3 ).get<int>( 0 ) - 1;
|
||||
int origK2 = record.getItem( 4 ).get<int>( 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<int>( transformResultK1->toOneBased().x() ) ) );
|
||||
items.push_back( RifOpmDeckTools::item( C::J::itemName, static_cast<int>( transformResultK1->toOneBased().y() ) ) );
|
||||
items.push_back( RifOpmDeckTools::item( C::K1::itemName, static_cast<int>( transformResultK1->toOneBased().z() ) ) );
|
||||
items.push_back( RifOpmDeckTools::item( C::K2::itemName, static_cast<int>( 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<KSpan> 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<RefinedSubcell> iSubcells =
|
||||
( dim == RigRefinement::DimI )
|
||||
? allSubcells( refinement, RigRefinement::DimI, sectorI )
|
||||
: std::vector<RefinedSubcell>{ { centerSubcellIndex( refinement, RigRefinement::DimI, sectorI ), 1.0 } };
|
||||
const std::vector<RefinedSubcell> jSubcells =
|
||||
( dim == RigRefinement::DimJ )
|
||||
? allSubcells( refinement, RigRefinement::DimJ, sectorJ )
|
||||
: std::vector<RefinedSubcell>{ { 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<Opm::DeckItem> items;
|
||||
items.push_back( record.getItem( 0 ) );
|
||||
items.push_back( RifOpmDeckTools::item( C::I::itemName, static_cast<int>( i + 1 ) ) );
|
||||
items.push_back( RifOpmDeckTools::item( C::J::itemName, static_cast<int>( j + 1 ) ) );
|
||||
items.push_back( RifOpmDeckTools::item( C::K1::itemName, static_cast<int>( k1 + 1 ) ) );
|
||||
items.push_back( RifOpmDeckTools::item( C::K2::itemName, static_cast<int>( 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<double>( 0 ) * fraction ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
items.push_back( item );
|
||||
}
|
||||
}
|
||||
|
||||
return Opm::DeckRecord{ std::move( items ) };
|
||||
};
|
||||
|
||||
std::vector<Opm::DeckRecord> 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<Opm::DeckRecord, QString> RigSimulationInputTool::processCompsegsRecord( const Opm::DeckRecord& record,
|
||||
const std::string& wellName,
|
||||
bool isWellNameRecord,
|
||||
const RigSimulationInputSettings& settings )
|
||||
std::expected<std::vector<Opm::DeckRecord>, 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>{ 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<Opm::DeckRecord, QString> RigSimulationInputTool::processCompsegsR
|
||||
.arg( record.size() ) );
|
||||
}
|
||||
|
||||
std::vector<Opm::DeckItem> 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<Opm::DeckRecord, QString> 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<int>( transformResult->toOneBased().x() ) ) );
|
||||
items.push_back( RifOpmDeckTools::item( "J", static_cast<int>( transformResult->toOneBased().y() ) ) );
|
||||
items.push_back( RifOpmDeckTools::item( "K", static_cast<int>( 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<RefinedSubcell> 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<Opm::DeckRecord> records;
|
||||
|
||||
double startFraction = 0.0;
|
||||
for ( const auto& subcell : subcells )
|
||||
{
|
||||
caf::VecIjk0 refinedIjk = *transformResult;
|
||||
refinedIjk[dim] = subcell.index;
|
||||
|
||||
std::vector<Opm::DeckItem> items;
|
||||
items.push_back( RifOpmDeckTools::item( "I", static_cast<int>( refinedIjk.x() + 1 ) ) );
|
||||
items.push_back( RifOpmDeckTools::item( "J", static_cast<int>( refinedIjk.y() + 1 ) ) );
|
||||
items.push_back( RifOpmDeckTools::item( "K", static_cast<int>( 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<double>( 0 );
|
||||
const double endMd = record.getItem( 5 ).get<double>( 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<void, QString> 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<void, QString> 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<void, QString> 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
|
||||
{
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
class RimEclipseView;
|
||||
@@ -102,6 +103,24 @@ public:
|
||||
static std::expected<Opm::DeckRecord, QString>
|
||||
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<std::tuple<std::string, size_t, size_t, size_t>, RigRefinement::Dimension>;
|
||||
|
||||
static ConnectionDirectionMap connectionDirections( const std::vector<Opm::DeckKeyword>& 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<std::vector<Opm::DeckRecord>, 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<std::vector<Opm::DeckRecord>, QString> processCompsegsRecord( const Opm::DeckRecord& record,
|
||||
const std::string& wellName,
|
||||
bool isWellNameRecord,
|
||||
const RigSimulationInputSettings& settings,
|
||||
const ConnectionDirectionMap& directions );
|
||||
|
||||
static std::vector<int> createRefinedVisibility( const RigGridExportAdapter& gridAdapter );
|
||||
|
||||
static std::expected<void, QString>
|
||||
@@ -193,14 +212,6 @@ private:
|
||||
static std::expected<Opm::DeckRecord, QString>
|
||||
processWelspecsRecord( const Opm::DeckRecord& record, const std::string& wellName, const RigSimulationInputSettings& settings );
|
||||
|
||||
static std::expected<Opm::DeckRecord, QString>
|
||||
processCompdatRecord( const Opm::DeckRecord& record, const std::string& wellName, const RigSimulationInputSettings& settings );
|
||||
|
||||
static std::expected<Opm::DeckRecord, QString> processCompsegsRecord( const Opm::DeckRecord& record,
|
||||
const std::string& wellName,
|
||||
bool isWellNameRecord,
|
||||
const RigSimulationInputSettings& settings );
|
||||
|
||||
static std::expected<void, QString> replaceKeywordWithBoxIndices( const std::string& keywordName,
|
||||
RimEclipseCase* eclipseCase,
|
||||
const RigSimulationInputSettings& settings,
|
||||
|
||||
@@ -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<Opm::DeckRecord> 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<Opm::DeckRecord> 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<int>( 0 ) );
|
||||
EXPECT_EQ( 4, rec.getItem( "J" ).get<int>( 0 ) );
|
||||
EXPECT_EQ( 3 + static_cast<int>( s ), rec.getItem( "K1" ).get<int>( 0 ) );
|
||||
EXPECT_EQ( 3 + static_cast<int>( s ), rec.getItem( "K2" ).get<int>( 0 ) );
|
||||
EXPECT_DOUBLE_EQ( 5.0, rec.getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get<double>( 0 ) );
|
||||
EXPECT_DOUBLE_EQ( 50.0, rec.getItem( "Kh" ).get<double>( 0 ) );
|
||||
EXPECT_DOUBLE_EQ( 0.216, rec.getItem( "DIAMETER" ).get<double>( 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<int>( 0 ) );
|
||||
EXPECT_EQ( 6, result->at( 0 ).getItem( "K2" ).get<int>( 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<int>( s ), rec.getItem( "I" ).get<int>( 0 ) );
|
||||
EXPECT_EQ( 2, rec.getItem( "J" ).get<int>( 0 ) );
|
||||
EXPECT_EQ( 2, rec.getItem( "K1" ).get<int>( 0 ) );
|
||||
EXPECT_EQ( 2, rec.getItem( "K2" ).get<int>( 0 ) );
|
||||
EXPECT_DOUBLE_EQ( 3.0, rec.getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get<double>( 0 ) );
|
||||
EXPECT_DOUBLE_EQ( 30.0, rec.getItem( "Kh" ).get<double>( 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<RigNonUniformRefinement>( 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<int>( 0 ) );
|
||||
EXPECT_EQ( 3, result->at( 1 ).getItem( "K1" ).get<int>( 0 ) );
|
||||
EXPECT_DOUBLE_EQ( 2.0, result->at( 0 ).getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get<double>( 0 ) );
|
||||
EXPECT_DOUBLE_EQ( 6.0, result->at( 1 ).getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get<double>( 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<int>( 0 ) );
|
||||
EXPECT_EQ( 5, result->at( 0 ).getItem( "K2" ).get<int>( 0 ) );
|
||||
EXPECT_DOUBLE_EQ( 10.0, result->at( 0 ).getItem( "CONNECTION_TRANSMISSIBILITY_FACTOR" ).get<double>( 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<Opm::DeckKeyword> 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<int>( 0 ) );
|
||||
EXPECT_EQ( 4, result->at( 1 ).getItem( 2 ).get<int>( 0 ) );
|
||||
EXPECT_DOUBLE_EQ( 100.0, result->at( 0 ).getItem( 4 ).get<double>( 0 ) );
|
||||
EXPECT_DOUBLE_EQ( 110.0, result->at( 0 ).getItem( 5 ).get<double>( 0 ) );
|
||||
EXPECT_DOUBLE_EQ( 110.0, result->at( 1 ).getItem( 4 ).get<double>( 0 ) );
|
||||
EXPECT_DOUBLE_EQ( 120.0, result->at( 1 ).getItem( 5 ).get<double>( 0 ) );
|
||||
}
|
||||
Reference in new issue
Block a user