diff --git a/ApplicationLibCode/CMakeLists.txt b/ApplicationLibCode/CMakeLists.txt index 2b68026625..88afa13ae3 100644 --- a/ApplicationLibCode/CMakeLists.txt +++ b/ApplicationLibCode/CMakeLists.txt @@ -89,6 +89,7 @@ list( ReservoirDataModel/CMakeLists_files.cmake ReservoirDataModel/CMakeLists_filesNotToUnitTest.cmake ReservoirDataModel/Completions/CMakeLists_files.cmake + ReservoirDataModel/SimulationFile/CMakeLists_files.cmake ReservoirDataModel/CompletionsMsw/CMakeLists_files.cmake ReservoirDataModel/ContourMap/CMakeLists_files.cmake ReservoirDataModel/ResultAccessors/CMakeLists_files.cmake @@ -391,6 +392,7 @@ target_include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/ReservoirDataModel/Completions ${CMAKE_CURRENT_SOURCE_DIR}/ReservoirDataModel/ResultAccessors ${CMAKE_CURRENT_SOURCE_DIR}/ReservoirDataModel/ResultCalculators + ${CMAKE_CURRENT_SOURCE_DIR}/ReservoirDataModel/SimulationFile ${CMAKE_CURRENT_SOURCE_DIR}/ResultStatisticsCache ${CMAKE_CURRENT_SOURCE_DIR}/SocketInterface ${CMAKE_CURRENT_SOURCE_DIR}/UserInterface diff --git a/ApplicationLibCode/Commands/ExportCommands/RicExportEclipseSectorModelFeature.cpp b/ApplicationLibCode/Commands/ExportCommands/RicExportEclipseSectorModelFeature.cpp index 467c558aa6..22ddbd3c6c 100644 --- a/ApplicationLibCode/Commands/ExportCommands/RicExportEclipseSectorModelFeature.cpp +++ b/ApplicationLibCode/Commands/ExportCommands/RicExportEclipseSectorModelFeature.cpp @@ -49,6 +49,8 @@ #include "RigGridExportAdapter.h" #include "RigMainGrid.h" #include "RigResdataGridConverter.h" +#include "RigSimulationInputSettings.h" +#include "RigSimulationInputTool.h" #include "Well/RigSimWellData.h" #include "Well/RigWellResultFrame.h" #include "Well/RigWellResultPoint.h" @@ -151,7 +153,29 @@ void RicExportEclipseSectorModelFeature::executeCommand( RimEclipseView* // Export simulation input if enabled if ( exportSettings.m_exportSimulationInput() ) { - if ( auto result = exportSimulationInput( *view, *view->eclipseCase(), exportSettings ); !result ) + // Convert UI settings to RigSimulationInputSettings + RigSimulationInputSettings settings; + settings.setMin( exportSettings.min() ); + settings.setMax( exportSettings.max() ); + settings.setRefinement( exportSettings.refinement() ); + + std::vector bcpropKeywords; + for ( auto bcprop : exportSettings.m_bcpropKeywords ) + { + Opm::DeckKeyword kw = bcprop->keyword(); + const auto& record = kw.getRecord( 0 ); + bcpropKeywords.push_back( record ); + } + settings.setBcpropKeywords( bcpropKeywords ); + settings.setBoundaryCondition( exportSettings.m_boundaryCondition() ); + + // Get input deck file name from eclipse case + QFileInfo fi( view->eclipseCase()->gridFileName() ); + QString dataFileName = fi.absolutePath() + "/" + fi.completeBaseName() + ".DATA"; + settings.setInputDeckFileName( dataFileName ); + settings.setOutputDeckFileName( exportSettings.exportGridFilename() ); + + if ( auto result = RigSimulationInputTool::exportSimulationInput( *view, *view->eclipseCase(), settings ); !result ) { RiaLogging::error( QString( "Failed to export simulation input: %1" ).arg( result.error() ) ); } @@ -288,381 +312,6 @@ void RicExportEclipseSectorModelFeature::exportFaults( RimEclipseView* view, con } } -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::expected RicExportEclipseSectorModelFeature::exportSimulationInput( RimEclipseView& view, - RimEclipseCase& eclipseCase, - const RicExportEclipseSectorModelUi& exportSettings ) -{ - // Load the deck file - QFileInfo fi( eclipseCase.gridFileName() ); - QString dataFileName = fi.absolutePath() + "/" + fi.completeBaseName() + ".DATA"; - - RifOpmFlowDeckFile deckFile; - if ( !deckFile.loadDeck( dataFileName.toStdString() ) ) - { - return std::unexpected( QString( "Unable to load deck file '%1'" ).arg( dataFileName ) ); - } - - QFileInfo exportGridInfo( exportSettings.exportGridFilename() ); - QString outputFolder = exportGridInfo.absolutePath(); - QString outputFile = exportGridInfo.completeBaseName() + ".DATA"; - - // Only change values when exporting to modified box: original values should just work (tm) for full grid box - if ( exportSettings.exportGridBox() != RicExportEclipseSectorModelUi::GridBoxSelection::FULL_GRID_BOX ) - { - if ( auto result = updateCornerPointGridInDeckFile( &eclipseCase, exportSettings, deckFile ); !result ) - { - return result; - } - - // Generate BORDNUM result based on the selected grid box - auto bordnumVisibility = createVisibilityBasedOnBoxSelection( &view, exportSettings ); - if ( !bordnumVisibility.isNull() ) - { - RigEclipseResultTools::generateBorderResult( &eclipseCase, bordnumVisibility, RiaResultNames::bordnum() ); - if ( exportSettings.m_boundaryCondition() == RicExportEclipseSectorModelUi::BoundaryCondition::BCCON_BCPROP ) - { - // Generate BCCON result to assign values 1-6 based on which face of the box the border cells are on - RigEclipseResultTools::generateBcconResult( &eclipseCase, exportSettings.min(), exportSettings.max() ); - } - else if ( exportSettings.m_boundaryCondition() == RicExportEclipseSectorModelUi::BoundaryCondition::OPERNUM_OPERATER ) - { - // Generate OPERNUM result based on BORDNUM (border cells get max existing OPERNUM + 1) - int operNumRegion = RigEclipseResultTools::generateOperNumResult( &eclipseCase ); - - if ( auto result = addOperNumRegionAndOperater( &eclipseCase, exportSettings, deckFile, operNumRegion ); !result ) - { - return result; - } - } - } - - if ( auto result = replaceKeywordValuesInDeckFile( &eclipseCase, exportSettings, deckFile ); !result ) - { - return result; - } - - if ( auto result = replaceEqualsKeywordIndices( &eclipseCase, exportSettings, deckFile ); !result ) - { - return result; - } - - if ( auto result = addBorderBoundaryConditions( &eclipseCase, exportSettings, deckFile ); !result ) - { - return result; - } - - if ( auto result = addFaultsToDeckFile( &eclipseCase, exportSettings, deckFile ); !result ) - { - return result; - } - - if ( auto result = filterAndUpdateWellKeywords( &eclipseCase, exportSettings, deckFile ); !result ) - { - return result; - } - } - - // Remove SKIP keywords that were used as placeholders for filtered-out keywords - deckFile.removeKeywords( "SKIP" ); - - // TODO: fix this.. - deckFile.removeKeywords( "MAPAXES" ); - - deckFile.removeKeywords( "SPECGRID" ); - - // Save the modified deck file to the export directory - if ( !deckFile.saveDeckInline( outputFolder.toStdString(), outputFile.toStdString() ) ) - { - return std::unexpected( QString( "Failed to save modified deck file to '%1/%2'" ).arg( outputFolder ).arg( outputFile ) ); - } - - RiaLogging::info( QString( "Saved modified deck file to '%1/%2'" ).arg( outputFolder ).arg( outputFile ) ); - return {}; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::expected - RicExportEclipseSectorModelFeature::updateCornerPointGridInDeckFile( RimEclipseCase* eclipseCase, - const RicExportEclipseSectorModelUi& exportSettings, - RifOpmFlowDeckFile& deckFile ) -{ - // Get grid bounds for extraction - - RigGridExportAdapter gridAdapter( eclipseCase->eclipseCaseData(), exportSettings.min(), exportSettings.max(), exportSettings.refinement() ); - - std::vector coordArray; - std::vector zcornArray; - std::vector actnumArray; - - RigResdataGridConverter::convertGridToCornerPointArrays( gridAdapter, coordArray, zcornArray, actnumArray ); - - // Sector dimensions (after refinement) - std::vector dimens = { static_cast( gridAdapter.cellCountI() ), - static_cast( gridAdapter.cellCountJ() ), - static_cast( gridAdapter.cellCountK() ) }; - - if ( !deckFile.replaceKeywordData( "DIMENS", dimens ) ) - { - return std::unexpected( "Failed to replace DIMENS keyword in deck file" ); - } - - auto convertToDoubleVector = []( const std::vector& vec ) - { - std::vector outVec; - outVec.reserve( vec.size() ); - for ( float f : vec ) - outVec.push_back( f ); - return outVec; - }; - std::vector coords = convertToDoubleVector( coordArray ); - std::vector zcorn = convertToDoubleVector( zcornArray ); - - auto keywords = deckFile.keywords( false ); - if ( std::find( keywords.begin(), keywords.end(), "ACTNUM" ) == keywords.end() ) - { - Opm::DeckKeyword newKw( Opm::ParserKeyword( "ACTNUM" ) ); - deckFile.addKeyword( "GRID", newKw ); - } - - if ( !deckFile.replaceKeywordData( "COORD", coords ) ) - { - return std::unexpected( "Failed to replace COORD keyword in deck file" ); - } - - if ( !deckFile.replaceKeywordData( "ZCORN", zcorn ) ) - { - return std::unexpected( "Failed to replace ZCORN keyword in deck file" ); - } - - if ( !deckFile.replaceKeywordData( "ACTNUM", actnumArray ) ) - { - return std::unexpected( "Failed to replace ACTNUM keyword in deck file" ); - } - - // TODO: deal with map axis - return {}; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::expected - RicExportEclipseSectorModelFeature::replaceKeywordValuesInDeckFile( RimEclipseCase* eclipseCase, - const RicExportEclipseSectorModelUi& exportSettings, - RifOpmFlowDeckFile& deckFile ) -{ - // Extract and replace keyword data for all keywords in the deck - auto keywords = deckFile.keywords( false ); - RiaLogging::info( QString( "Processing %1 keywords from deck file" ).arg( keywords.size() ) ); - - for ( const auto& keywordStdStr : keywords ) - { - QString keyword = QString::fromStdString( keywordStdStr ); - - // Skip special keywords that aren't cell properties - if ( keyword.startsWith( "DATES" ) || keyword == "SCHEDULE" || keyword == "GRID" || keyword == "PROPS" || keyword == "SOLUTION" || - keyword == "RUNSPEC" || keyword == "SUMMARY" ) - { - continue; - } - - // Try to extract keyword data - auto result = RifEclipseInputFileTools::extractKeywordData( eclipseCase->eclipseCaseData(), - keyword, - exportSettings.min(), - exportSettings.max(), - exportSettings.refinement() ); - if ( result ) - { - // Replace keyword values in deck with extracted data - if ( deckFile.replaceKeywordData( keywordStdStr, result.value() ) ) - { - RiaLogging::info( - QString( "Successfully replaced data for keyword '%1' (%2 values)" ).arg( keyword ).arg( result.value().size() ) ); - } - else - { - RiaLogging::warning( QString( "Failed to replace keyword '%1' in deck" ).arg( keyword ) ); - } - } - else - { - // Not all keywords will have data - this is expected - RiaLogging::debug( - QString( "Could not extract data for keyword '%1': %2" ).arg( keyword ).arg( QString::fromStdString( result.error() ) ) ); - } - } - - return {}; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::expected RicExportEclipseSectorModelFeature::addBorderBoundaryConditions( RimEclipseCase* eclipseCase, - const RicExportEclipseSectorModelUi& exportSettings, - RifOpmFlowDeckFile& deckFile ) -{ - // Generate border cell faces - auto borderCellFaces = RigEclipseResultTools::generateBorderCellFaces( eclipseCase ); - - if ( !borderCellFaces.empty() ) - { - // Transform border cell face coordinates to sector-relative coordinates - for ( auto& face : borderCellFaces ) - { - auto transformResult = RigGridExportAdapter::transformIjkToSectorCoordinates( face.ijk, - exportSettings.min(), - exportSettings.max(), - exportSettings.refinement() ); - - if ( !transformResult ) - { - RiaLogging::warning( QString( "Failed to transform border cell face at (%1, %2, %3): %4" ) - .arg( face.ijk.x() ) - .arg( face.ijk.y() ) - .arg( face.ijk.z() ) - .arg( transformResult.error() ) ); - continue; - } - - // Update the IJK coordinates to sector-relative (1-based Eclipse coordinates) - // Note: RigGridExportAdapter::transformIjkToSectorCoordinates returns 1-based coordinates, but we need to convert back to - // 0-based for the BorderCellFace struct - face.ijk = transformResult->toZeroBased(); - } - - // Create BCCON keyword using the factory - Opm::DeckKeyword bcconKw = RimKeywordFactory::bcconKeyword( borderCellFaces ); - - // Replace BCCON keyword in GRID section - if ( !deckFile.replaceKeyword( "GRID", bcconKw ) ) - { - return std::unexpected( "Failed to replace BCCON keyword in deck file" ); - } - - // Build BCPROP records from the UI configuration - std::vector bcpropRecords; - for ( const auto& bcprop : exportSettings.m_bcpropKeywords ) - { - if ( bcprop != nullptr ) - { - Opm::DeckKeyword kw = bcprop->keyword(); - const auto& record = kw.getRecord( 0 ); - bcpropRecords.push_back( record ); - } - } - - // Create BCPROP keyword using the factory - Opm::DeckKeyword bcpropKw = RimKeywordFactory::bcpropKeyword( borderCellFaces, bcpropRecords ); - - // Replace BCPROP keyword in GRID section - if ( !deckFile.replaceKeyword( Opm::ParserKeywords::SCHEDULE::keywordName, bcpropKw ) ) - { - return std::unexpected( "Failed to replace BCPROP keyword in deck file" ); - } - } - else - { - RiaLogging::warning( "No border cells found - skipping BCCON/BCPROP keyword generation" ); - } - - return {}; -} - -std::expected RicExportEclipseSectorModelFeature::replaceEqualsKeywordIndices( RimEclipseCase* eclipseCase, - const RicExportEclipseSectorModelUi& exportSettings, - RifOpmFlowDeckFile& deckFile ) -{ - auto keywords = deckFile.keywords( false ); - - for ( const auto& keyword : keywords ) - { - // Handle EQUALS keyword specially - it has IJK coordinates that need transformation - if ( keyword == "EQUALS" ) - { - auto equalsKeywordsWithIndices = deckFile.findAllKeywordsWithIndices( keyword ); - if ( !equalsKeywordsWithIndices.empty() ) - { - RiaLogging::info( QString( "Processing %1 occurrence(s) of EQUALS keyword" ).arg( equalsKeywordsWithIndices.size() ) ); - - // Process in reverse order so indices remain valid after modifications - for ( auto it = equalsKeywordsWithIndices.rbegin(); it != equalsKeywordsWithIndices.rend(); ++it ) - { - const Opm::FileDeck::Index& index = it->first; - const Opm::DeckKeyword& kw = it->second; - - // Create new keyword with transformed coordinates - Opm::DeckKeyword transformedKeyword( kw.location(), kw.name() ); - - try - { - for ( size_t recordIdx = 0; recordIdx < kw.size(); ++recordIdx ) - { - const auto& record = kw.getRecord( recordIdx ); - auto result = - processEqualsRecord( record, exportSettings.min(), exportSettings.max(), exportSettings.refinement() ); - - if ( result ) - { - transformedKeyword.addRecord( std::move( result.value() ) ); - } - else - { - RiaLogging::warning( QString( "Failed to process EQUALS record: %1" ).arg( result.error() ) ); - } - } - - // Replace with transformed keyword - if ( transformedKeyword.size() > 0 ) - { - RiaLogging::info( QString( "Got %1 keywords." ).arg( transformedKeyword.size() ) ); - deckFile.replaceKeywordAtIndex( index, transformedKeyword ); - } - else - { - // If all records failed, remove the keyword - deckFile.replaceKeywordAtIndex( index, Opm::DeckKeyword( kw.location(), "SKIP" ) ); - } - } - catch ( std::exception& e ) - { - return std::unexpected( QString( "Exception processing EQUALS keyword: %1" ).arg( e.what() ) ); - } - } - } - } - } - - return {}; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::expected RicExportEclipseSectorModelFeature::addFaultsToDeckFile( RimEclipseCase* eclipseCase, - const RicExportEclipseSectorModelUi& exportSettings, - RifOpmFlowDeckFile& deckFile ) -{ - // Create FAULTS keyword using the factory - Opm::DeckKeyword faultsKw = - RimKeywordFactory::faultsKeyword( eclipseCase->mainGrid(), exportSettings.min(), exportSettings.max(), exportSettings.refinement() ); - - // Replace FAULTS keyword in GRID section - if ( !deckFile.replaceKeyword( "GRID", faultsKw ) ) - { - return std::unexpected( "Failed to replace FAULTS keyword in deck file" ); - } - - RiaLogging::info( "Successfully replaced FAULTS keyword in deck file" ); - return {}; -} - //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -794,653 +443,3 @@ cvf::ref return nullptr; } } - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::vector - RicExportEclipseSectorModelFeature::findIntersectingWells( RimEclipseCase* eclipseCase, const cvf::Vec3st& min, const cvf::Vec3st& max ) -{ - std::vector intersectingWells; - - if ( !eclipseCase || !eclipseCase->eclipseCaseData() ) return intersectingWells; - - const auto& wellResults = eclipseCase->eclipseCaseData()->wellResults(); - - for ( size_t wellIdx = 0; wellIdx < wellResults.size(); ++wellIdx ) - { - const RigSimWellData* wellData = wellResults[wellIdx].p(); - if ( !wellData ) continue; - - bool intersects = false; - - // Check all time steps for this well - for ( const auto& wellFrame : wellData->m_wellCellsTimeSteps ) - { - // Check all result points in this frame - auto resultPoints = wellFrame.allResultPoints(); - for ( const auto& point : resultPoints ) - { - // Get IJK if available - auto ijkOpt = point.cellIjk(); - if ( !ijkOpt.has_value() ) continue; - - const auto& ijk = ijkOpt.value(); - - // Check if point is within bounding box (inclusive) - if ( static_cast( ijk.i() ) >= min.x() && static_cast( ijk.i() ) <= max.x() && - static_cast( ijk.j() ) >= min.y() && static_cast( ijk.j() ) <= max.y() && - static_cast( ijk.k() ) >= min.z() && static_cast( ijk.k() ) <= max.z() ) - { - intersects = true; - break; - } - } - if ( intersects ) break; - } - - if ( intersects ) - { - // const_cast is safe here - we only need write access to collect well pointers - intersectingWells.push_back( const_cast( wellData ) ); - } - } - - return intersectingWells; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::expected - RicExportEclipseSectorModelFeature::processWelspecsRecord( const Opm::DeckRecord& record, - const std::string& wellName, - const RicExportEclipseSectorModelUi& exportSettings ) -{ - // WELSPECS format: WELL GROUP HEAD_I HEAD_J REF_DEPTH ... - // Items: 0=WELL, 1=GROUP, 2=HEAD_I, 3=HEAD_J - if ( record.size() < 4 ) - { - return std::unexpected( QString( "WELSPECS record for well %1 has insufficient items (expected at least 4, got %2)" ) - .arg( wellName.c_str() ) - .arg( record.size() ) ); - } - - std::vector items; - - // Copy well name and group - items.push_back( record.getItem( 0 ) ); - items.push_back( record.getItem( 1 ) ); - - // Transform HEAD_I and HEAD_J - // Note: HEAD coordinates might be outside sector even if well has completions inside - // Clamp to sector bounds before transformation - int origI = record.getItem( 2 ).get( 0 ) - 1; // Convert to 0-based - int origJ = record.getItem( 3 ).get( 0 ) - 1; - - // Clamp to sector bounds - size_t clampedI = std::max( exportSettings.min().x(), std::min( exportSettings.max().x(), static_cast( origI ) ) ); - size_t clampedJ = std::max( exportSettings.min().y(), std::min( exportSettings.max().y(), static_cast( origJ ) ) ); - - // Transform with clamped coordinates (this will always succeed) - size_t sectorI = ( clampedI - exportSettings.min().x() ) * exportSettings.refinement().x() + 1; - size_t sectorJ = ( clampedJ - exportSettings.min().y() ) * exportSettings.refinement().y() + 1; - - if ( origI < static_cast( exportSettings.min().x() ) || origI > static_cast( exportSettings.max().x() ) || - origJ < static_cast( exportSettings.min().y() ) || origJ > static_cast( exportSettings.max().y() ) ) - { - RiaLogging::info( QString( "Well %1 HEAD position (%2, %3) outside sector, clamped to (%4, %5)" ) - .arg( wellName.c_str() ) - .arg( origI + 1 ) - .arg( origJ + 1 ) - .arg( sectorI ) - .arg( sectorJ ) ); - } - - using W = Opm::ParserKeywords::WELSPECS; - items.push_back( RifOpmDeckTools::item( W::HEAD_I::itemName, static_cast( sectorI ) ) ); - items.push_back( RifOpmDeckTools::item( W::HEAD_J::itemName, static_cast( sectorJ ) ) ); - - // Copy remaining items - for ( size_t i = 4; i < record.size(); ++i ) - { - items.push_back( record.getItem( i ) ); - } - - return Opm::DeckRecord{ std::move( items ) }; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::expected - RicExportEclipseSectorModelFeature::processCompdatRecord( const Opm::DeckRecord& record, - const std::string& wellName, - const RicExportEclipseSectorModelUi& exportSettings ) -{ - // COMPDAT format: WELL I J K1 K2 STATE ... - // Items: 0=WELL, 1=I, 2=J, 3=K1, 4=K2 - if ( record.size() < 5 ) - { - return std::unexpected( QString( "COMPDAT record for well %1 has insufficient items (expected at least 5, got %2)" ) - .arg( wellName.c_str() ) - .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, - exportSettings.min(), - exportSettings.max(), - exportSettings.refinement() ); - - // Transform K2 - caf::VecIjk0 origIjkK2( origI, origJ, origK2 ); - auto transformResultK2 = RigGridExportAdapter::transformIjkToSectorCoordinates( origIjkK2, - exportSettings.min(), - exportSettings.max(), - exportSettings.refinement() ); - - 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() ) ); - } - - 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->x() ) ) ); - items.push_back( RifOpmDeckTools::item( C::J::itemName, static_cast( transformResultK1->y() ) ) ); - items.push_back( RifOpmDeckTools::item( C::K1::itemName, static_cast( transformResultK1->z() ) ) ); - items.push_back( RifOpmDeckTools::item( C::K2::itemName, static_cast( transformResultK2->z() ) ) ); - - // Copy remaining items - for ( size_t i = 5; i < record.size(); ++i ) - { - items.push_back( record.getItem( i ) ); - } - - return Opm::DeckRecord{ std::move( items ) }; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::expected - RicExportEclipseSectorModelFeature::processCompsegsRecord( const Opm::DeckRecord& record, - const std::string& wellName, - bool isWellNameRecord, - const RicExportEclipseSectorModelUi& exportSettings ) -{ - // 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 ); - } - - // Segment record format: I J K BRANCH START_MD END_MD ... - // Items: 0=I, 1=J, 2=K - if ( record.size() < 3 ) - { - return std::unexpected( QString( "COMPSEGS segment record for well %1 has insufficient items (expected at least 3, got %2)" ) - .arg( wellName.c_str() ) - .arg( record.size() ) ); - } - - std::vector items; - - // Transform I, J, K (first three items) - int origI = record.getItem( 0 ).get( 0 ) - 1; // Convert to 0-based - int origJ = record.getItem( 1 ).get( 0 ) - 1; - int origK = record.getItem( 2 ).get( 0 ) - 1; - - caf::VecIjk0 origIjk( origI, origJ, origK ); - auto transformResult = - RigGridExportAdapter::transformIjkToSectorCoordinates( origIjk, exportSettings.min(), exportSettings.max(), exportSettings.refinement() ); - - if ( !transformResult ) - { - return std::unexpected( - 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->x() ) ) ); - items.push_back( RifOpmDeckTools::item( "J", static_cast( transformResult->y() ) ) ); - items.push_back( RifOpmDeckTools::item( "K", static_cast( transformResult->z() ) ) ); - - // Copy remaining items - for ( size_t i = 3; i < record.size(); ++i ) - { - items.push_back( record.getItem( i ) ); - } - - return Opm::DeckRecord{ std::move( items ) }; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::expected RicExportEclipseSectorModelFeature::processEqualsRecord( const Opm::DeckRecord& record, - const caf::VecIjk0& min, - const caf::VecIjk0& max, - const cvf::Vec3st& refinement ) -{ - // EQUALS format: FIELD VALUE I1 I2 J1 J2 K1 K2 - // Items: 0=FIELD, 1=VALUE, 2=I1, 3=I2, 4=J1, 5=J2, 6=K1, 7=K2 - if ( record.size() < 8 ) - { - return std::unexpected( QString( "EQUALS record has insufficient items (expected at least 8, got %1)" ).arg( record.size() ) ); - } - - std::vector items; - - // Copy field name and value (first two items) - items.push_back( record.getItem( 0 ) ); - items.push_back( record.getItem( 1 ) ); - - // Transform IJK box coordinates (items 2-7: I1, I2, J1, J2, K1, K2) - // Note: EQUALS uses 1-based Eclipse coordinates - int origI1 = record.getItem( 2 ).get( 0 ) - 1; // Convert to 0-based - int origI2 = record.getItem( 3 ).get( 0 ) - 1; - int origJ1 = record.getItem( 4 ).get( 0 ) - 1; - int origJ2 = record.getItem( 5 ).get( 0 ) - 1; - int origK1 = record.getItem( 6 ).get( 0 ) - 1; - int origK2 = record.getItem( 7 ).get( 0 ) - 1; - - // Create bounding boxes (both use inclusive min/max coordinates) - RigBoundingBoxIjk equalsBox( cvf::Vec3st( origI1, origJ1, origK1 ), cvf::Vec3st( origI2, origJ2, origK2 ) ); - RigBoundingBoxIjk sectorBox( cvf::Vec3st( min.x(), min.y(), min.z() ), cvf::Vec3st( max.x(), max.y(), max.z() ) ); - - // Check if boxes overlap and get intersection - auto intersection = equalsBox.intersection( sectorBox ); - if ( !intersection ) - { - // No overlap with sector - skip this record - QString fieldName = QString::fromStdString( record.getItem( 0 ).get( 0 ) ); - RiaLogging::info( QString( "EQUALS record for field '%1' [%2-%3, %4-%5, %6-%7] does not overlap with sector - skipping" ) - .arg( fieldName ) - .arg( origI1 + 1 ) - .arg( origI2 + 1 ) - .arg( origJ1 + 1 ) - .arg( origJ2 + 1 ) - .arg( origK1 + 1 ) - .arg( origK2 + 1 ) ); - return std::unexpected( "EQUALS record does not overlap with sector" ); - } - - // Get the clamped coordinates from the intersection - cvf::Vec3st corner1 = intersection->min(); - cvf::Vec3st corner2 = intersection->max(); - - // Log if clamping occurred (partial overlap) - if ( origI1 != static_cast( corner1.x() ) || origI2 != static_cast( corner2.x() ) || origJ1 != static_cast( corner1.y() ) || - origJ2 != static_cast( corner2.y() ) || origK1 != static_cast( corner1.z() ) || origK2 != static_cast( corner2.z() ) ) - { - QString fieldName = QString::fromStdString( record.getItem( 0 ).get( 0 ) ); - RiaLogging::info( - QString( "EQUALS record for field '%1' partially overlaps sector, clamped from [%2-%3, %4-%5, %6-%7] to [%8-%9, %10-%11, " - "%12-%13]" ) - .arg( fieldName ) - .arg( origI1 + 1 ) - .arg( origI2 + 1 ) - .arg( origJ1 + 1 ) - .arg( origJ2 + 1 ) - .arg( origK1 + 1 ) - .arg( origK2 + 1 ) - .arg( corner1.x() + 1 ) - .arg( corner2.x() + 1 ) - .arg( corner1.y() + 1 ) - .arg( corner2.y() + 1 ) - .arg( corner1.z() + 1 ) - .arg( corner2.z() + 1 ) ); - } - - auto transformResult1 = - RigGridExportAdapter::transformIjkToSectorCoordinates( caf::VecIjk0( corner1.x(), corner1.y(), corner1.z() ), min, max, refinement ); - auto transformResult2 = - RigGridExportAdapter::transformIjkToSectorCoordinates( caf::VecIjk0( corner2.x(), corner2.y(), corner2.z() ), min, max, refinement ); - - if ( !transformResult1 ) - { - return std::unexpected( QString( "EQUALS I1,J1,K1 coordinate is out of sector bounds: %1" ).arg( transformResult1.error() ) ); - } - - if ( !transformResult2 ) - { - return std::unexpected( QString( "EQUALS I2,J2,K2 coordinate is out of sector bounds: %1" ).arg( transformResult2.error() ) ); - } - - using E = Opm::ParserKeywords::EQUALS; - items.push_back( RifOpmDeckTools::item( E::I1::itemName, static_cast( transformResult1->x() ) ) ); - items.push_back( RifOpmDeckTools::item( E::I2::itemName, static_cast( transformResult2->x() ) ) ); - items.push_back( RifOpmDeckTools::item( E::J1::itemName, static_cast( transformResult1->y() ) ) ); - items.push_back( RifOpmDeckTools::item( E::J2::itemName, static_cast( transformResult2->y() ) ) ); - items.push_back( RifOpmDeckTools::item( E::K1::itemName, static_cast( transformResult1->z() ) ) ); - items.push_back( RifOpmDeckTools::item( E::K2::itemName, static_cast( transformResult2->z() ) ) ); - - return Opm::DeckRecord{ std::move( items ) }; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::expected RicExportEclipseSectorModelFeature::filterAndUpdateWellKeywords( RimEclipseCase* eclipseCase, - const RicExportEclipseSectorModelUi& exportSettings, - RifOpmFlowDeckFile& deckFile ) -{ - // Find wells that intersect with the sector - auto intersectingWells = findIntersectingWells( eclipseCase, exportSettings.min(), exportSettings.max() ); - - if ( intersectingWells.empty() ) - { - RiaLogging::info( "No wells intersect with the selected sector - no well filtering needed" ); - return {}; - } - - // Create set of valid well names for fast lookup - std::set validWellNames; - for ( const auto& wellData : intersectingWells ) - { - if ( wellData ) - { - validWellNames.insert( wellData->m_wellName.toStdString() ); - } - } - - RiaLogging::info( QString( "Found %1 wells intersecting with sector: %2" ) - .arg( validWellNames.size() ) - .arg( QString::fromStdString( - [&validWellNames]() - { - std::string names; - for ( const auto& name : validWellNames ) - { - if ( !names.empty() ) names += ", "; - names += name; - } - return names; - }() ) ) ); - - // List of well-related keywords to filter (keywords that reference well names) - std::vector wellKeywords = { "COMPDAT", - "COMPLUMP", - "COMPORD", - "COMPSEGS", - "WCONHIST", - "WCONINJH", - "WCONINJE", - "WCONPROD", - "WELSEGS", - "WELSPECS", - "WELOPEN", - "WELTARG", - "WPAVEDEP", - "WRFTPLT", - "WTRACER" }; - - // Process each type of well keyword - // Use findAllKeywordsWithIndices to get all occurrences with their positions - for ( const auto& keywordName : wellKeywords ) - { - auto keywordsWithIndices = deckFile.findAllKeywordsWithIndices( keywordName ); - if ( keywordsWithIndices.empty() ) continue; - - RiaLogging::info( - QString( "Processing %1 occurrence(s) of keyword '%2'" ).arg( keywordsWithIndices.size() ).arg( keywordName.c_str() ) ); - - int replacedCount = 0; - int removedCount = 0; - - // Process in reverse order so indices remain valid after modifications - for ( auto it = keywordsWithIndices.rbegin(); it != keywordsWithIndices.rend(); ++it ) - { - const Opm::FileDeck::Index& index = it->first; - const Opm::DeckKeyword& keyword = it->second; - - // Create new empty keyword with same name and location - Opm::DeckKeyword filteredKeyword( keyword.location(), keyword.name() ); - - // Track current well for COMPSEGS/WELSEGS (where only first record has well name) - std::string currentSegmentWell; - bool keepSegmentRecords = false; - - try - { - for ( size_t recordIdx = 0; recordIdx < keyword.size(); ++recordIdx ) - { - const auto& record = keyword.getRecord( recordIdx ); - - // First item in well keywords is typically the well name - if ( record.size() == 0 ) continue; - - const auto& wellNameItem = record.getItem( 0 ); - - // Check if first item is a string (well name) or other type (segment data) - bool isWellNameRecord = wellNameItem.hasValue( 0 ) && ( wellNameItem.getType() == Opm::type_tag::string ); - - std::string wellName; - if ( isWellNameRecord ) - { - wellName = wellNameItem.get( 0 ); - - // For COMPSEGS/WELSEGS, update the current well context - if ( keywordName == "COMPSEGS" || keywordName == "WELSEGS" ) - { - currentSegmentWell = wellName; - keepSegmentRecords = ( validWellNames.find( wellName ) != validWellNames.end() ); - } - } - else if ( keywordName == "COMPSEGS" || keywordName == "WELSEGS" ) - { - // Segment data record - use the current well context - wellName = currentSegmentWell; - } - - // Check if this well is in our valid set - if ( ( isWellNameRecord && validWellNames.find( wellName ) != validWellNames.end() ) || - ( !isWellNameRecord && ( keywordName == "COMPSEGS" || keywordName == "WELSEGS" ) && keepSegmentRecords ) ) - { - // For keywords with IJK coordinates, we need to transform them - if ( keywordName == "WELSPECS" ) - { - auto result = processWelspecsRecord( record, wellName, exportSettings ); - if ( result ) - { - filteredKeyword.addRecord( std::move( result.value() ) ); - } - else - { - RiaLogging::warning( - QString( "Failed to process WELSPECS for well %1: %2" ).arg( wellName.c_str() ).arg( result.error() ) ); - } - } - else if ( keywordName == "COMPDAT" ) - { - auto result = processCompdatRecord( record, wellName, exportSettings ); - if ( result ) - { - filteredKeyword.addRecord( std::move( result.value() ) ); - } - else - { - RiaLogging::warning( - QString( "Failed to process COMPDAT for well %1: %2" ).arg( wellName.c_str() ).arg( result.error() ) ); - } - } - else if ( keywordName == "COMPSEGS" ) - { - auto result = processCompsegsRecord( record, wellName, isWellNameRecord, exportSettings ); - if ( result ) - { - filteredKeyword.addRecord( std::move( result.value() ) ); - } - else - { - RiaLogging::warning( - QString( "Failed to process COMPSEGS for well %1: %2" ).arg( wellName.c_str() ).arg( result.error() ) ); - } - } - else if ( keywordName == "WELSEGS" ) - { - // WELSEGS: just copy all records for now - filteredKeyword.addRecord( Opm::DeckRecord( record ) ); - } - else - { - // For other keywords (WCONHIST, WELTARG, etc.), just copy if well name matches - filteredKeyword.addRecord( Opm::DeckRecord( record ) ); - } - } - } - } - catch ( std::exception& e ) - { - RiaLogging::warning( QString( "EXCEPTION for keyword '%1': %2" ).arg( keyword.name().c_str() ).arg( e.what() ) ); - } - - // Replace or remove this keyword occurrence in place - if ( filteredKeyword.size() == 0 ) - { - // Keyword is empty after filtering - remove it - deckFile.replaceKeywordAtIndex( index, Opm::DeckKeyword( keyword.location(), "SKIP" ) ); - removedCount++; - } - else - { - // Replace with filtered version - deckFile.replaceKeywordAtIndex( index, filteredKeyword ); - replacedCount++; - } - } - - RiaLogging::info( - QString( "Processed keyword '%1': %2 replaced, %3 removed" ).arg( keywordName.c_str() ).arg( replacedCount ).arg( removedCount ) ); - } - - return {}; -} - -//-------------------------------------------------------------------------------------------------- -/// -//-------------------------------------------------------------------------------------------------- -std::expected RicExportEclipseSectorModelFeature::addOperNumRegionAndOperater( RimEclipseCase* eclipseCase, - const RicExportEclipseSectorModelUi& exportSettings, - RifOpmFlowDeckFile& deckFile, - int operNumRegion ) -{ - // Update REGDIMS and add OPERATER keyword for OPERNUM regions - // Get the OPERNUM region number that was assigned to border cells - - RiaLogging::info( QString( "Using OPERNUM region %1 for border cells" ).arg( operNumRegion ) ); - - // Ensure REGDIMS keyword exists - if ( !deckFile.ensureRegdimsKeyword() ) - { - return std::unexpected( "Failed to ensure REGDIMS keyword exists in RUNSPEC section" ); - } - - // Read current REGDIMS values - auto regdimsValues = deckFile.regdims(); - if ( regdimsValues.empty() ) - { - // TODO: improve this? - // Use defaults if reading failed - regdimsValues = { 1, 1, 0, 0, 0, 1, 0 }; // NTFIP NMFIPR NRFREG NTFREG MAX_ETRACK NTCREG MAX_OPERNUM - } - - // Update MAX_OPERNUM (item 7) to the new region number - if ( regdimsValues.size() < 7 ) - { - regdimsValues.resize( 7, 0 ); - } - regdimsValues[6] = operNumRegion; // Index 6 is item 7 (MAX_OPERNUM) - - // Update REGDIMS in deck - if ( !deckFile.setRegdims( regdimsValues[0], - regdimsValues[1], - regdimsValues[2], - regdimsValues[3], - regdimsValues[4], - regdimsValues[5], - regdimsValues[6] ) ) - { - return std::unexpected( "Failed to update REGDIMS keyword in deck file" ); - } - - // Check if OPERNUM keyword already exists in deck - auto opernumKeyword = deckFile.findKeyword( "OPERNUM" ); - if ( !opernumKeyword.has_value() ) - { - auto keywords = deckFile.keywords( false ); - if ( std::find( keywords.begin(), keywords.end(), "OPERNUM" ) == keywords.end() ) - { - Opm::DeckKeyword newKw( ( Opm::ParserKeywords::OPERNUM() ) ); - deckFile.addKeyword( "GRID", newKw ); - } - - // Try to extract keyword data - auto result = RifEclipseInputFileTools::extractKeywordData( eclipseCase->eclipseCaseData(), - "OPERNUM", - exportSettings.min(), - exportSettings.max(), - exportSettings.refinement() ); - if ( result ) - { - // Replace keyword values in deck with extracted data - if ( deckFile.replaceKeywordData( "OPERNUM", result.value() ) ) - { - RiaLogging::info( "Added replaced opernum value in exising position." ); - } - else - { - return std::unexpected( "Unable to replace OPERNUM values" ); - } - } - else - { - return std::unexpected( "No OPERNUM result found." ); - } - } - else - { - RiaLogging::info( "OPERNUM keyword already exists in deck, skipping INCLUDE" ); - } - - // Create OPERATER keyword to multiply pore volume in border region - // OPERATER format: TARGET_ARRAY REGION_NUMBER OPERATION ARRAY_PARAMETER PARAM1 PARAM2 REGION_NAME - // Example: OPERATER / PORV 1 MULTX PORV 1.0e6 1* 1* / - float porvMultiplier = static_cast( exportSettings.m_porvMultiplier() ); - Opm::DeckKeyword operaterKw = RimKeywordFactory::operaterKeyword( "PORV", operNumRegion, "MULTX", "PORV", porvMultiplier ); - - // Add OPERATER keyword to EDIT section - if ( !deckFile.replaceKeyword( "EDIT", operaterKw ) ) - { - return std::unexpected( "Failed to add OPERATER keyword to EDIT section" ); - } - - RiaLogging::info( QString( "Added OPERATER keyword to multiply PORV in region %1 by %2" ).arg( operNumRegion ).arg( porvMultiplier ) ); - - return {}; -} diff --git a/ApplicationLibCode/Commands/ExportCommands/RicExportEclipseSectorModelFeature.h b/ApplicationLibCode/Commands/ExportCommands/RicExportEclipseSectorModelFeature.h index 6343b7ddee..a851c9203b 100644 --- a/ApplicationLibCode/Commands/ExportCommands/RicExportEclipseSectorModelFeature.h +++ b/ApplicationLibCode/Commands/ExportCommands/RicExportEclipseSectorModelFeature.h @@ -52,9 +52,6 @@ public: static std::pair getVisibleCellRange( RimEclipseView* view, const cvf::UByteArray& cellVisibility ); - static std::expected - processEqualsRecord( const Opm::DeckRecord& record, const caf::VecIjk0& min, const caf::VecIjk0& max, const cvf::Vec3st& refinement ); - protected: bool isCommandEnabled() const override; void onActionTriggered( bool isChecked ) override; @@ -64,52 +61,7 @@ private: RimEclipseView* selectedView() const; static cvf::ref createVisibilityBasedOnBoxSelection( RimEclipseView* view, const RicExportEclipseSectorModelUi& exportSettings ); - static std::expected - exportSimulationInput( RimEclipseView& view, RimEclipseCase& eclipseCase, const RicExportEclipseSectorModelUi& exportSettings ); - static void exportGrid( RimEclipseView* view, const RicExportEclipseSectorModelUi& exportSettings ); - static void exportFaults( RimEclipseView* view, const RicExportEclipseSectorModelUi& exportSettings ); - static void exportParameters( RimEclipseView* view, const RicExportEclipseSectorModelUi& exportSettings ); - - static std::expected - addFaultsToDeckFile( RimEclipseCase* eclipseCase, const RicExportEclipseSectorModelUi& exportSettings, RifOpmFlowDeckFile& deckFile ); - - static std::expected addBorderBoundaryConditions( RimEclipseCase* eclipseCase, - const RicExportEclipseSectorModelUi& exportSettings, - RifOpmFlowDeckFile& deckFile ); - - static std::expected replaceKeywordValuesInDeckFile( RimEclipseCase* eclipseCase, - const RicExportEclipseSectorModelUi& exportSettings, - RifOpmFlowDeckFile& deckFile ); - - static std::expected updateCornerPointGridInDeckFile( RimEclipseCase* eclipseCase, - const RicExportEclipseSectorModelUi& exportSettings, - RifOpmFlowDeckFile& deckFile ); - - static std::expected filterAndUpdateWellKeywords( RimEclipseCase* eclipseCase, - const RicExportEclipseSectorModelUi& exportSettings, - RifOpmFlowDeckFile& deckFile ); - - static std::expected addOperNumRegionAndOperater( RimEclipseCase* eclipseCase, - const RicExportEclipseSectorModelUi& exportSettings, - RifOpmFlowDeckFile& deckFile, - int operNumRegion ); - - static std::expected replaceEqualsKeywordIndices( RimEclipseCase* eclipseCase, - const RicExportEclipseSectorModelUi& exportSettings, - RifOpmFlowDeckFile& deckFile ); - - static std::vector findIntersectingWells( RimEclipseCase* eclipseCase, const cvf::Vec3st& min, const cvf::Vec3st& max ); - - static std::expected processWelspecsRecord( const Opm::DeckRecord& record, - const std::string& wellName, - const RicExportEclipseSectorModelUi& exportSettings ); - - static std::expected processCompdatRecord( const Opm::DeckRecord& record, - const std::string& wellName, - const RicExportEclipseSectorModelUi& exportSettings ); - - static std::expected processCompsegsRecord( const Opm::DeckRecord& record, - const std::string& wellName, - bool isWellNameRecord, - const RicExportEclipseSectorModelUi& exportSettings ); + static void exportGrid( RimEclipseView* view, const RicExportEclipseSectorModelUi& exportSettings ); + static void exportFaults( RimEclipseView* view, const RicExportEclipseSectorModelUi& exportSettings ); + static void exportParameters( RimEclipseView* view, const RicExportEclipseSectorModelUi& exportSettings ); }; diff --git a/ApplicationLibCode/ReservoirDataModel/SimulationFile/CMakeLists_files.cmake b/ApplicationLibCode/ReservoirDataModel/SimulationFile/CMakeLists_files.cmake new file mode 100644 index 0000000000..bb106af44c --- /dev/null +++ b/ApplicationLibCode/ReservoirDataModel/SimulationFile/CMakeLists_files.cmake @@ -0,0 +1,12 @@ +set(SOURCE_GROUP_HEADER_FILES + ${CMAKE_CURRENT_LIST_DIR}/RigSimulationInputSettings.h + ${CMAKE_CURRENT_LIST_DIR}/RigSimulationInputTool.h +) + +set(SOURCE_GROUP_SOURCE_FILES + ${CMAKE_CURRENT_LIST_DIR}/RigSimulationInputSettings.cpp + ${CMAKE_CURRENT_LIST_DIR}/RigSimulationInputTool.cpp +) + +list(APPEND CODE_HEADER_FILES ${SOURCE_GROUP_HEADER_FILES}) +list(APPEND CODE_SOURCE_FILES ${SOURCE_GROUP_SOURCE_FILES}) diff --git a/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputSettings.cpp b/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputSettings.cpp new file mode 100644 index 0000000000..6d27932312 --- /dev/null +++ b/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputSettings.cpp @@ -0,0 +1,144 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RigSimulationInputSettings.h" + +#include "opm/input/eclipse/Deck/DeckRecord.hpp" + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RigSimulationInputSettings::RigSimulationInputSettings() + : m_min( caf::VecIjk0::ZERO ) + , m_max( caf::VecIjk0::ZERO ) + , m_refinement( 1, 1, 1 ) + , m_boundaryCondition( 0 ) +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +caf::VecIjk0 RigSimulationInputSettings::min() const +{ + return m_min; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +caf::VecIjk0 RigSimulationInputSettings::max() const +{ + return m_max; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RigSimulationInputSettings::setMin( const caf::VecIjk0& min ) +{ + m_min = min; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RigSimulationInputSettings::setMax( const caf::VecIjk0& max ) +{ + m_max = max; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +cvf::Vec3st RigSimulationInputSettings::refinement() const +{ + return m_refinement; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RigSimulationInputSettings::setRefinement( const cvf::Vec3st& refinement ) +{ + m_refinement = refinement; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RigSimulationInputSettings::bcpropKeywords() const +{ + return m_bcpropKeywords; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RigSimulationInputSettings::setBcpropKeywords( const std::vector& keywords ) +{ + m_bcpropKeywords = keywords; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +int RigSimulationInputSettings::boundaryCondition() const +{ + return m_boundaryCondition; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RigSimulationInputSettings::setBoundaryCondition( int value ) +{ + m_boundaryCondition = value; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RigSimulationInputSettings::inputDeckFileName() const +{ + return m_inputDeckFileName; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RigSimulationInputSettings::setInputDeckFileName( const QString& fileName ) +{ + m_inputDeckFileName = fileName; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RigSimulationInputSettings::outputDeckFileName() const +{ + return m_outputDeckFileName; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RigSimulationInputSettings::setOutputDeckFileName( const QString& fileName ) +{ + m_outputDeckFileName = fileName; +} diff --git a/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputSettings.h b/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputSettings.h new file mode 100644 index 0000000000..e7e45752bc --- /dev/null +++ b/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputSettings.h @@ -0,0 +1,74 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "cafVecIjk.h" +#include "cvfVector3.h" + +#include +#include + +namespace Opm +{ +class DeckRecord; +} + +//================================================================================================== +/// +/// Settings for exporting simulation input files (sector models) +/// +//================================================================================================== +class RigSimulationInputSettings +{ +public: + RigSimulationInputSettings(); + + // Sector bounds (0-based grid coordinates) + caf::VecIjk0 min() const; + caf::VecIjk0 max() const; + void setMin( const caf::VecIjk0& min ); + void setMax( const caf::VecIjk0& max ); + + // Grid refinement + cvf::Vec3st refinement() const; + void setRefinement( const cvf::Vec3st& refinement ); + + // Boundary conditions + std::vector bcpropKeywords() const; + void setBcpropKeywords( const std::vector& keywords ); + + int boundaryCondition() const; + void setBoundaryCondition( int value ); + + // File paths + QString inputDeckFileName() const; + void setInputDeckFileName( const QString& fileName ); + + QString outputDeckFileName() const; + void setOutputDeckFileName( const QString& fileName ); + +private: + caf::VecIjk0 m_min; + caf::VecIjk0 m_max; + cvf::Vec3st m_refinement; + std::vector m_bcpropKeywords; + int m_boundaryCondition; + QString m_inputDeckFileName; + QString m_outputDeckFileName; +}; diff --git a/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.cpp b/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.cpp new file mode 100644 index 0000000000..b5ccc48293 --- /dev/null +++ b/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.cpp @@ -0,0 +1,1031 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#include "RigSimulationInputTool.h" + +#include "RiaLogging.h" + +#include "RigSimulationInputSettings.h" + +#include "RifEclipseInputFileTools.h" +#include "RifOpmDeckTools.h" +#include "RifOpmFlowDeckFile.h" + +#include "ProjectDataModel/Jobs/RimKeywordFactory.h" +#include "RimEclipseCase.h" + +#include "RigBoundingBoxIjk.h" +#include "RigEclipseCaseData.h" +#include "RigEclipseResultTools.h" +#include "RigGridExportAdapter.h" +#include "RigResdataGridConverter.h" +#include "Well/RigSimWellData.h" +#include "Well/RigWellResultFrame.h" +#include "Well/RigWellResultPoint.h" + +#include "opm/input/eclipse/Deck/DeckItem.hpp" +#include "opm/input/eclipse/Deck/DeckKeyword.hpp" +#include "opm/input/eclipse/Deck/DeckRecord.hpp" +#include "opm/input/eclipse/Parser/ParserKeyword.hpp" +#include "opm/input/eclipse/Parser/ParserKeywords/C.hpp" +#include "opm/input/eclipse/Parser/ParserKeywords/E.hpp" +#include "opm/input/eclipse/Parser/ParserKeywords/O.hpp" +#include "opm/input/eclipse/Parser/ParserKeywords/S.hpp" +#include "opm/input/eclipse/Parser/ParserKeywords/W.hpp" + +#include + +#include + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RigSimulationInputTool::exportSimulationInput( RimEclipseView& view, + RimEclipseCase& eclipseCase, + const RigSimulationInputSettings& settings ) +{ + // Load the deck file + QString inputDeckFileName = settings.inputDeckFileName(); + + RifOpmFlowDeckFile deckFile; + if ( !deckFile.loadDeck( inputDeckFileName.toStdString() ) ) + { + return std::unexpected( QString( "Unable to load deck file '%1'" ).arg( inputDeckFileName ) ); + } + + QString outputDeckFileName = settings.outputDeckFileName(); + QFileInfo exportGridInfo( outputDeckFileName ); + QString outputFolder = exportGridInfo.absolutePath(); + QString outputFile = exportGridInfo.completeBaseName() + ".DATA"; + + if ( auto result = updateCornerPointGridInDeckFile( &eclipseCase, settings, deckFile ); !result ) + { + return result; + } + + if ( auto result = replaceKeywordValuesInDeckFile( &eclipseCase, settings, deckFile ); !result ) + { + return result; + } + + if ( auto result = replaceEqualsKeywordIndices( &eclipseCase, settings, deckFile ); !result ) + { + return result; + } + + if ( auto result = addBorderBoundaryConditions( &eclipseCase, settings, deckFile ); !result ) + { + return result; + } + + if ( auto result = addFaultsToDeckFile( &eclipseCase, settings, deckFile ); !result ) + { + return result; + } + + if ( auto result = filterAndUpdateWellKeywords( &eclipseCase, settings, deckFile ); !result ) + { + return result; + } + + // Remove SKIP keywords that were used as placeholders for filtered-out keywords + deckFile.removeKeywords( "SKIP" ); + + // TODO: fix this.. + deckFile.removeKeywords( "MAPAXES" ); + + deckFile.removeKeywords( "SPECGRID" ); + + // Save the modified deck file to the export directory + if ( !deckFile.saveDeckInline( outputFolder.toStdString(), outputFile.toStdString() ) ) + { + return std::unexpected( QString( "Failed to save modified deck file to '%1/%2'" ).arg( outputFolder ).arg( outputFile ) ); + } + + RiaLogging::info( QString( "Saved modified deck file to '%1/%2'" ).arg( outputFolder ).arg( outputFile ) ); + return {}; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RigSimulationInputTool::updateCornerPointGridInDeckFile( RimEclipseCase* eclipseCase, + const RigSimulationInputSettings& settings, + RifOpmFlowDeckFile& deckFile ) +{ + // Get grid bounds for extraction + + RigGridExportAdapter gridAdapter( eclipseCase->eclipseCaseData(), settings.min(), settings.max(), settings.refinement() ); + + std::vector coordArray; + std::vector zcornArray; + std::vector actnumArray; + + RigResdataGridConverter::convertGridToCornerPointArrays( gridAdapter, coordArray, zcornArray, actnumArray ); + + // Sector dimensions (after refinement) + std::vector dimens = { static_cast( gridAdapter.cellCountI() ), + static_cast( gridAdapter.cellCountJ() ), + static_cast( gridAdapter.cellCountK() ) }; + + if ( !deckFile.replaceKeywordData( "DIMENS", dimens ) ) + { + return std::unexpected( "Failed to replace DIMENS keyword in deck file" ); + } + + auto convertToDoubleVector = []( const std::vector& vec ) + { + std::vector outVec; + outVec.reserve( vec.size() ); + for ( float f : vec ) + outVec.push_back( f ); + return outVec; + }; + std::vector coords = convertToDoubleVector( coordArray ); + std::vector zcorn = convertToDoubleVector( zcornArray ); + + auto keywords = deckFile.keywords( false ); + if ( std::find( keywords.begin(), keywords.end(), "ACTNUM" ) == keywords.end() ) + { + Opm::DeckKeyword newKw( Opm::ParserKeyword( "ACTNUM" ) ); + deckFile.addKeyword( "GRID", newKw ); + } + + if ( !deckFile.replaceKeywordData( "COORD", coords ) ) + { + return std::unexpected( "Failed to replace COORD keyword in deck file" ); + } + + if ( !deckFile.replaceKeywordData( "ZCORN", zcorn ) ) + { + return std::unexpected( "Failed to replace ZCORN keyword in deck file" ); + } + + if ( !deckFile.replaceKeywordData( "ACTNUM", actnumArray ) ) + { + return std::unexpected( "Failed to replace ACTNUM keyword in deck file" ); + } + + // TODO: deal with map axis + return {}; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RigSimulationInputTool::replaceKeywordValuesInDeckFile( RimEclipseCase* eclipseCase, + const RigSimulationInputSettings& settings, + RifOpmFlowDeckFile& deckFile ) +{ + // Extract and replace keyword data for all keywords in the deck + auto keywords = deckFile.keywords( false ); + RiaLogging::info( QString( "Processing %1 keywords from deck file" ).arg( keywords.size() ) ); + + for ( const auto& keywordStdStr : keywords ) + { + QString keyword = QString::fromStdString( keywordStdStr ); + + // Skip special keywords that aren't cell properties + if ( keyword.startsWith( "DATES" ) || keyword == "SCHEDULE" || keyword == "GRID" || keyword == "PROPS" || keyword == "SOLUTION" || + keyword == "RUNSPEC" || keyword == "SUMMARY" ) + { + continue; + } + + // Try to extract keyword data + auto result = RifEclipseInputFileTools::extractKeywordData( eclipseCase->eclipseCaseData(), + keyword, + settings.min(), + settings.max(), + settings.refinement() ); + if ( result ) + { + // Replace keyword values in deck with extracted data + if ( deckFile.replaceKeywordData( keywordStdStr, result.value() ) ) + { + RiaLogging::info( + QString( "Successfully replaced data for keyword '%1' (%2 values)" ).arg( keyword ).arg( result.value().size() ) ); + } + else + { + RiaLogging::warning( QString( "Failed to replace keyword '%1' in deck" ).arg( keyword ) ); + } + } + else + { + // Not all keywords will have data - this is expected + RiaLogging::debug( + QString( "Could not extract data for keyword '%1': %2" ).arg( keyword ).arg( QString::fromStdString( result.error() ) ) ); + } + } + + return {}; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RigSimulationInputTool::addBorderBoundaryConditions( RimEclipseCase* eclipseCase, + const RigSimulationInputSettings& settings, + RifOpmFlowDeckFile& deckFile ) +{ + // Generate border cell faces + auto borderCellFaces = RigEclipseResultTools::generateBorderCellFaces( eclipseCase ); + + if ( !borderCellFaces.empty() ) + { + // Transform border cell face coordinates to sector-relative coordinates + for ( auto& face : borderCellFaces ) + { + auto transformResult = + RigGridExportAdapter::transformIjkToSectorCoordinates( face.ijk, settings.min(), settings.max(), settings.refinement() ); + + if ( !transformResult ) + { + RiaLogging::warning( QString( "Failed to transform border cell face at (%1, %2, %3): %4" ) + .arg( face.ijk.x() ) + .arg( face.ijk.y() ) + .arg( face.ijk.z() ) + .arg( transformResult.error() ) ); + continue; + } + + // Update the IJK coordinates to sector-relative (1-based Eclipse coordinates) + // Note: RigGridExportAdapter::transformIjkToSectorCoordinates returns 1-based coordinates, but we need to convert back to + // 0-based for the BorderCellFace struct + face.ijk = transformResult->toZeroBased(); + } + + // Create BCCON keyword using the factory + Opm::DeckKeyword bcconKw = RimKeywordFactory::bcconKeyword( borderCellFaces ); + + // Replace BCCON keyword in GRID section + if ( !deckFile.replaceKeyword( "GRID", bcconKw ) ) + { + return std::unexpected( "Failed to replace BCCON keyword in deck file" ); + } + + // Build BCPROP records from the settings configuration + std::vector bcpropRecords = settings.bcpropKeywords(); + + // Create BCPROP keyword using the factory + Opm::DeckKeyword bcpropKw = RimKeywordFactory::bcpropKeyword( borderCellFaces, bcpropRecords ); + + // Replace BCPROP keyword in GRID section + if ( !deckFile.replaceKeyword( Opm::ParserKeywords::SCHEDULE::keywordName, bcpropKw ) ) + { + return std::unexpected( "Failed to replace BCPROP keyword in deck file" ); + } + } + else + { + RiaLogging::warning( "No border cells found - skipping BCCON/BCPROP keyword generation" ); + } + + return {}; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RigSimulationInputTool::replaceEqualsKeywordIndices( RimEclipseCase* eclipseCase, + const RigSimulationInputSettings& settings, + RifOpmFlowDeckFile& deckFile ) +{ + auto keywords = deckFile.keywords( false ); + + for ( const auto& keyword : keywords ) + { + // Handle EQUALS keyword specially - it has IJK coordinates that need transformation + if ( keyword == "EQUALS" ) + { + auto equalsKeywordsWithIndices = deckFile.findAllKeywordsWithIndices( keyword ); + if ( !equalsKeywordsWithIndices.empty() ) + { + RiaLogging::info( QString( "Processing %1 occurrence(s) of EQUALS keyword" ).arg( equalsKeywordsWithIndices.size() ) ); + + // Process in reverse order so indices remain valid after modifications + for ( auto it = equalsKeywordsWithIndices.rbegin(); it != equalsKeywordsWithIndices.rend(); ++it ) + { + const Opm::FileDeck::Index& index = it->first; + const Opm::DeckKeyword& kw = it->second; + + // Create new keyword with transformed coordinates + Opm::DeckKeyword transformedKeyword( kw.location(), kw.name() ); + + try + { + for ( size_t recordIdx = 0; recordIdx < kw.size(); ++recordIdx ) + { + const auto& record = kw.getRecord( recordIdx ); + auto result = processEqualsRecord( record, settings.min(), settings.max(), settings.refinement() ); + + if ( result ) + { + transformedKeyword.addRecord( std::move( result.value() ) ); + } + else + { + RiaLogging::warning( QString( "Failed to process EQUALS record: %1" ).arg( result.error() ) ); + } + } + + // Replace with transformed keyword + if ( transformedKeyword.size() > 0 ) + { + RiaLogging::info( QString( "Got %1 keywords." ).arg( transformedKeyword.size() ) ); + deckFile.replaceKeywordAtIndex( index, transformedKeyword ); + } + else + { + // If all records failed, remove the keyword + deckFile.replaceKeywordAtIndex( index, Opm::DeckKeyword( kw.location(), "SKIP" ) ); + } + } + catch ( std::exception& e ) + { + return std::unexpected( QString( "Exception processing EQUALS keyword: %1" ).arg( e.what() ) ); + } + } + } + } + } + + return {}; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RigSimulationInputTool::addFaultsToDeckFile( RimEclipseCase* eclipseCase, + const RigSimulationInputSettings& settings, + RifOpmFlowDeckFile& deckFile ) +{ + // Create FAULTS keyword using the factory + Opm::DeckKeyword faultsKw = + RimKeywordFactory::faultsKeyword( eclipseCase->mainGrid(), settings.min(), settings.max(), settings.refinement() ); + + // Replace FAULTS keyword in GRID section + if ( !deckFile.replaceKeyword( "GRID", faultsKw ) ) + { + return std::unexpected( "Failed to replace FAULTS keyword in deck file" ); + } + + RiaLogging::info( "Successfully replaced FAULTS keyword in deck file" ); + return {}; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector + RigSimulationInputTool::findIntersectingWells( RimEclipseCase* eclipseCase, const cvf::Vec3st& min, const cvf::Vec3st& max ) +{ + std::vector intersectingWells; + + if ( !eclipseCase || !eclipseCase->eclipseCaseData() ) return intersectingWells; + + const auto& wellResults = eclipseCase->eclipseCaseData()->wellResults(); + + for ( size_t wellIdx = 0; wellIdx < wellResults.size(); ++wellIdx ) + { + const RigSimWellData* wellData = wellResults[wellIdx].p(); + if ( !wellData ) continue; + + bool intersects = false; + + // Check all time steps for this well + for ( const auto& wellFrame : wellData->m_wellCellsTimeSteps ) + { + // Check all result points in this frame + auto resultPoints = wellFrame.allResultPoints(); + for ( const auto& point : resultPoints ) + { + // Get IJK if available + auto ijkOpt = point.cellIjk(); + if ( !ijkOpt.has_value() ) continue; + + const auto& ijk = ijkOpt.value(); + + // Check if point is within bounding box (inclusive) + if ( static_cast( ijk.i() ) >= min.x() && static_cast( ijk.i() ) <= max.x() && + static_cast( ijk.j() ) >= min.y() && static_cast( ijk.j() ) <= max.y() && + static_cast( ijk.k() ) >= min.z() && static_cast( ijk.k() ) <= max.z() ) + { + intersects = true; + break; + } + } + if ( intersects ) break; + } + + if ( intersects ) + { + // const_cast is safe here - we only need write access to collect well pointers + intersectingWells.push_back( const_cast( wellData ) ); + } + } + + return intersectingWells; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RigSimulationInputTool::processWelspecsRecord( const Opm::DeckRecord& record, + const std::string& wellName, + const RigSimulationInputSettings& settings ) +{ + // WELSPECS format: WELL GROUP HEAD_I HEAD_J REF_DEPTH ... + // Items: 0=WELL, 1=GROUP, 2=HEAD_I, 3=HEAD_J + if ( record.size() < 4 ) + { + return std::unexpected( QString( "WELSPECS record for well %1 has insufficient items (expected at least 4, got %2)" ) + .arg( wellName.c_str() ) + .arg( record.size() ) ); + } + + std::vector items; + + // Copy well name and group + items.push_back( record.getItem( 0 ) ); + items.push_back( record.getItem( 1 ) ); + + // Transform HEAD_I and HEAD_J + // Note: HEAD coordinates might be outside sector even if well has completions inside + // Clamp to sector bounds before transformation + int origI = record.getItem( 2 ).get( 0 ) - 1; // Convert to 0-based + int origJ = record.getItem( 3 ).get( 0 ) - 1; + + // Clamp to sector bounds + size_t clampedI = std::max( settings.min().x(), std::min( settings.max().x(), static_cast( origI ) ) ); + size_t clampedJ = std::max( settings.min().y(), std::min( settings.max().y(), static_cast( origJ ) ) ); + + // Transform with clamped coordinates (this will always succeed) + size_t sectorI = ( clampedI - settings.min().x() ) * settings.refinement().x() + 1; + size_t sectorJ = ( clampedJ - settings.min().y() ) * settings.refinement().y() + 1; + + if ( origI < static_cast( settings.min().x() ) || origI > static_cast( settings.max().x() ) || + origJ < static_cast( settings.min().y() ) || origJ > static_cast( settings.max().y() ) ) + { + RiaLogging::info( QString( "Well %1 HEAD position (%2, %3) outside sector, clamped to (%4, %5)" ) + .arg( wellName.c_str() ) + .arg( origI + 1 ) + .arg( origJ + 1 ) + .arg( sectorI ) + .arg( sectorJ ) ); + } + + using W = Opm::ParserKeywords::WELSPECS; + items.push_back( RifOpmDeckTools::item( W::HEAD_I::itemName, static_cast( sectorI ) ) ); + items.push_back( RifOpmDeckTools::item( W::HEAD_J::itemName, static_cast( sectorJ ) ) ); + + // Copy remaining items + for ( size_t i = 4; i < record.size(); ++i ) + { + items.push_back( record.getItem( i ) ); + } + + return Opm::DeckRecord{ std::move( items ) }; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected 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 + if ( record.size() < 5 ) + { + return std::unexpected( QString( "COMPDAT record for well %1 has insufficient items (expected at least 5, got %2)" ) + .arg( wellName.c_str() ) + .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() ); + + // Transform K2 + caf::VecIjk0 origIjkK2( origI, origJ, origK2 ); + auto transformResultK2 = + RigGridExportAdapter::transformIjkToSectorCoordinates( origIjkK2, settings.min(), settings.max(), settings.refinement() ); + + 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() ) ); + } + + 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->x() ) ) ); + items.push_back( RifOpmDeckTools::item( C::J::itemName, static_cast( transformResultK1->y() ) ) ); + items.push_back( RifOpmDeckTools::item( C::K1::itemName, static_cast( transformResultK1->z() ) ) ); + items.push_back( RifOpmDeckTools::item( C::K2::itemName, static_cast( transformResultK2->z() ) ) ); + + // Copy remaining items + for ( size_t i = 5; i < record.size(); ++i ) + { + items.push_back( record.getItem( i ) ); + } + + return Opm::DeckRecord{ std::move( items ) }; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RigSimulationInputTool::processCompsegsRecord( const Opm::DeckRecord& record, + const std::string& wellName, + bool isWellNameRecord, + const RigSimulationInputSettings& settings ) +{ + // 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 ); + } + + // Segment record format: I J K BRANCH START_MD END_MD ... + // Items: 0=I, 1=J, 2=K + if ( record.size() < 3 ) + { + return std::unexpected( QString( "COMPSEGS segment record for well %1 has insufficient items (expected at least 3, got %2)" ) + .arg( wellName.c_str() ) + .arg( record.size() ) ); + } + + std::vector items; + + // Transform I, J, K (first three items) + int origI = record.getItem( 0 ).get( 0 ) - 1; // Convert to 0-based + int origJ = record.getItem( 1 ).get( 0 ) - 1; + int origK = record.getItem( 2 ).get( 0 ) - 1; + + caf::VecIjk0 origIjk( origI, origJ, origK ); + auto transformResult = + RigGridExportAdapter::transformIjkToSectorCoordinates( origIjk, settings.min(), settings.max(), settings.refinement() ); + + if ( !transformResult ) + { + return std::unexpected( + 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->x() ) ) ); + items.push_back( RifOpmDeckTools::item( "J", static_cast( transformResult->y() ) ) ); + items.push_back( RifOpmDeckTools::item( "K", static_cast( transformResult->z() ) ) ); + + // Copy remaining items + for ( size_t i = 3; i < record.size(); ++i ) + { + items.push_back( record.getItem( i ) ); + } + + return Opm::DeckRecord{ std::move( items ) }; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RigSimulationInputTool::processEqualsRecord( const Opm::DeckRecord& record, + const caf::VecIjk0& min, + const caf::VecIjk0& max, + const cvf::Vec3st& refinement ) +{ + // EQUALS format: FIELD VALUE I1 I2 J1 J2 K1 K2 + // Items: 0=FIELD, 1=VALUE, 2=I1, 3=I2, 4=J1, 5=J2, 6=K1, 7=K2 + if ( record.size() < 8 ) + { + return std::unexpected( QString( "EQUALS record has insufficient items (expected at least 8, got %1)" ).arg( record.size() ) ); + } + + std::vector items; + + // Copy field name and value (first two items) + items.push_back( record.getItem( 0 ) ); + items.push_back( record.getItem( 1 ) ); + + // Transform IJK box coordinates (items 2-7: I1, I2, J1, J2, K1, K2) + // Note: EQUALS uses 1-based Eclipse coordinates + int origI1 = record.getItem( 2 ).get( 0 ) - 1; // Convert to 0-based + int origI2 = record.getItem( 3 ).get( 0 ) - 1; + int origJ1 = record.getItem( 4 ).get( 0 ) - 1; + int origJ2 = record.getItem( 5 ).get( 0 ) - 1; + int origK1 = record.getItem( 6 ).get( 0 ) - 1; + int origK2 = record.getItem( 7 ).get( 0 ) - 1; + + // Create bounding boxes (both use inclusive min/max coordinates) + RigBoundingBoxIjk equalsBox( cvf::Vec3st( origI1, origJ1, origK1 ), cvf::Vec3st( origI2, origJ2, origK2 ) ); + RigBoundingBoxIjk sectorBox( cvf::Vec3st( min.x(), min.y(), min.z() ), cvf::Vec3st( max.x(), max.y(), max.z() ) ); + + // Check if boxes overlap and get intersection + auto intersection = equalsBox.intersection( sectorBox ); + if ( !intersection ) + { + // No overlap with sector - skip this record + QString fieldName = QString::fromStdString( record.getItem( 0 ).get( 0 ) ); + RiaLogging::info( QString( "EQUALS record for field '%1' [%2-%3, %4-%5, %6-%7] does not overlap with sector - skipping" ) + .arg( fieldName ) + .arg( origI1 + 1 ) + .arg( origI2 + 1 ) + .arg( origJ1 + 1 ) + .arg( origJ2 + 1 ) + .arg( origK1 + 1 ) + .arg( origK2 + 1 ) ); + return std::unexpected( "EQUALS record does not overlap with sector" ); + } + + // Get the clamped coordinates from the intersection + cvf::Vec3st corner1 = intersection->min(); + cvf::Vec3st corner2 = intersection->max(); + + // Log if clamping occurred (partial overlap) + if ( origI1 != static_cast( corner1.x() ) || origI2 != static_cast( corner2.x() ) || origJ1 != static_cast( corner1.y() ) || + origJ2 != static_cast( corner2.y() ) || origK1 != static_cast( corner1.z() ) || origK2 != static_cast( corner2.z() ) ) + { + QString fieldName = QString::fromStdString( record.getItem( 0 ).get( 0 ) ); + RiaLogging::info( + QString( "EQUALS record for field '%1' partially overlaps sector, clamped from [%2-%3, %4-%5, %6-%7] to [%8-%9, %10-%11, " + "%12-%13]" ) + .arg( fieldName ) + .arg( origI1 + 1 ) + .arg( origI2 + 1 ) + .arg( origJ1 + 1 ) + .arg( origJ2 + 1 ) + .arg( origK1 + 1 ) + .arg( origK2 + 1 ) + .arg( corner1.x() + 1 ) + .arg( corner2.x() + 1 ) + .arg( corner1.y() + 1 ) + .arg( corner2.y() + 1 ) + .arg( corner1.z() + 1 ) + .arg( corner2.z() + 1 ) ); + } + + auto transformResult1 = + RigGridExportAdapter::transformIjkToSectorCoordinates( caf::VecIjk0( corner1.x(), corner1.y(), corner1.z() ), min, max, refinement ); + auto transformResult2 = + RigGridExportAdapter::transformIjkToSectorCoordinates( caf::VecIjk0( corner2.x(), corner2.y(), corner2.z() ), min, max, refinement ); + + if ( !transformResult1 ) + { + return std::unexpected( QString( "EQUALS I1,J1,K1 coordinate is out of sector bounds: %1" ).arg( transformResult1.error() ) ); + } + + if ( !transformResult2 ) + { + return std::unexpected( QString( "EQUALS I2,J2,K2 coordinate is out of sector bounds: %1" ).arg( transformResult2.error() ) ); + } + + using E = Opm::ParserKeywords::EQUALS; + items.push_back( RifOpmDeckTools::item( E::I1::itemName, static_cast( transformResult1->x() ) ) ); + items.push_back( RifOpmDeckTools::item( E::I2::itemName, static_cast( transformResult2->x() ) ) ); + items.push_back( RifOpmDeckTools::item( E::J1::itemName, static_cast( transformResult1->y() ) ) ); + items.push_back( RifOpmDeckTools::item( E::J2::itemName, static_cast( transformResult2->y() ) ) ); + items.push_back( RifOpmDeckTools::item( E::K1::itemName, static_cast( transformResult1->z() ) ) ); + items.push_back( RifOpmDeckTools::item( E::K2::itemName, static_cast( transformResult2->z() ) ) ); + + return Opm::DeckRecord{ std::move( items ) }; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RigSimulationInputTool::filterAndUpdateWellKeywords( RimEclipseCase* eclipseCase, + const RigSimulationInputSettings& settings, + RifOpmFlowDeckFile& deckFile ) +{ + // Find wells that intersect with the sector + auto intersectingWells = findIntersectingWells( eclipseCase, settings.min(), settings.max() ); + + if ( intersectingWells.empty() ) + { + RiaLogging::info( "No wells intersect with the selected sector - no well filtering needed" ); + return {}; + } + + // Create set of valid well names for fast lookup + std::set validWellNames; + for ( const auto& wellData : intersectingWells ) + { + if ( wellData ) + { + validWellNames.insert( wellData->m_wellName.toStdString() ); + } + } + + RiaLogging::info( QString( "Found %1 wells intersecting with sector: %2" ) + .arg( validWellNames.size() ) + .arg( QString::fromStdString( + [&validWellNames]() + { + std::string names; + for ( const auto& name : validWellNames ) + { + if ( !names.empty() ) names += ", "; + names += name; + } + return names; + }() ) ) ); + + // List of well-related keywords to filter (keywords that reference well names) + std::vector wellKeywords = { "COMPDAT", + "COMPLUMP", + "COMPORD", + "COMPSEGS", + "WCONHIST", + "WCONINJH", + "WCONINJE", + "WCONPROD", + "WELSEGS", + "WELSPECS", + "WELOPEN", + "WELTARG", + "WPAVEDEP", + "WRFTPLT", + "WTRACER" }; + + // Process each type of well keyword + // Use findAllKeywordsWithIndices to get all occurrences with their positions + for ( const auto& keywordName : wellKeywords ) + { + auto keywordsWithIndices = deckFile.findAllKeywordsWithIndices( keywordName ); + if ( keywordsWithIndices.empty() ) continue; + + RiaLogging::info( + QString( "Processing %1 occurrence(s) of keyword '%2'" ).arg( keywordsWithIndices.size() ).arg( keywordName.c_str() ) ); + + int replacedCount = 0; + int removedCount = 0; + + // Process in reverse order so indices remain valid after modifications + for ( auto it = keywordsWithIndices.rbegin(); it != keywordsWithIndices.rend(); ++it ) + { + const Opm::FileDeck::Index& index = it->first; + const Opm::DeckKeyword& keyword = it->second; + + // Create new empty keyword with same name and location + Opm::DeckKeyword filteredKeyword( keyword.location(), keyword.name() ); + + // Track current well for COMPSEGS/WELSEGS (where only first record has well name) + std::string currentSegmentWell; + bool keepSegmentRecords = false; + + try + { + for ( size_t recordIdx = 0; recordIdx < keyword.size(); ++recordIdx ) + { + const auto& record = keyword.getRecord( recordIdx ); + + // First item in well keywords is typically the well name + if ( record.size() == 0 ) continue; + + const auto& wellNameItem = record.getItem( 0 ); + + // Check if first item is a string (well name) or other type (segment data) + bool isWellNameRecord = wellNameItem.hasValue( 0 ) && ( wellNameItem.getType() == Opm::type_tag::string ); + + std::string wellName; + if ( isWellNameRecord ) + { + wellName = wellNameItem.get( 0 ); + + // For COMPSEGS/WELSEGS, update the current well context + if ( keywordName == "COMPSEGS" || keywordName == "WELSEGS" ) + { + currentSegmentWell = wellName; + keepSegmentRecords = ( validWellNames.find( wellName ) != validWellNames.end() ); + } + } + else if ( keywordName == "COMPSEGS" || keywordName == "WELSEGS" ) + { + // Segment data record - use the current well context + wellName = currentSegmentWell; + } + + // Check if this well is in our valid set + if ( ( isWellNameRecord && validWellNames.find( wellName ) != validWellNames.end() ) || + ( !isWellNameRecord && ( keywordName == "COMPSEGS" || keywordName == "WELSEGS" ) && keepSegmentRecords ) ) + { + // For keywords with IJK coordinates, we need to transform them + if ( keywordName == "WELSPECS" ) + { + auto result = processWelspecsRecord( record, wellName, settings ); + if ( result ) + { + filteredKeyword.addRecord( std::move( result.value() ) ); + } + else + { + RiaLogging::warning( + QString( "Failed to process WELSPECS for well %1: %2" ).arg( wellName.c_str() ).arg( result.error() ) ); + } + } + else if ( keywordName == "COMPDAT" ) + { + auto result = processCompdatRecord( record, wellName, settings ); + if ( result ) + { + filteredKeyword.addRecord( std::move( result.value() ) ); + } + else + { + RiaLogging::warning( + QString( "Failed to process COMPDAT for well %1: %2" ).arg( wellName.c_str() ).arg( result.error() ) ); + } + } + else if ( keywordName == "COMPSEGS" ) + { + auto result = processCompsegsRecord( record, wellName, isWellNameRecord, settings ); + if ( result ) + { + filteredKeyword.addRecord( std::move( result.value() ) ); + } + else + { + RiaLogging::warning( + QString( "Failed to process COMPSEGS for well %1: %2" ).arg( wellName.c_str() ).arg( result.error() ) ); + } + } + else if ( keywordName == "WELSEGS" ) + { + // WELSEGS: just copy all records for now + filteredKeyword.addRecord( Opm::DeckRecord( record ) ); + } + else + { + // For other keywords (WCONHIST, WELTARG, etc.), just copy if well name matches + filteredKeyword.addRecord( Opm::DeckRecord( record ) ); + } + } + } + } + catch ( std::exception& e ) + { + RiaLogging::warning( QString( "EXCEPTION for keyword '%1': %2" ).arg( keyword.name().c_str() ).arg( e.what() ) ); + } + + // Replace or remove this keyword occurrence in place + if ( filteredKeyword.size() == 0 ) + { + // Keyword is empty after filtering - remove it + deckFile.replaceKeywordAtIndex( index, Opm::DeckKeyword( keyword.location(), "SKIP" ) ); + removedCount++; + } + else + { + // Replace with filtered version + deckFile.replaceKeywordAtIndex( index, filteredKeyword ); + replacedCount++; + } + } + + RiaLogging::info( + QString( "Processed keyword '%1': %2 replaced, %3 removed" ).arg( keywordName.c_str() ).arg( replacedCount ).arg( removedCount ) ); + } + + return {}; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RigSimulationInputTool::addOperNumRegionAndOperater( RimEclipseCase* eclipseCase, + const RigSimulationInputSettings& settings, + RifOpmFlowDeckFile& deckFile, + int operNumRegion ) +{ + // Update REGDIMS and add OPERATER keyword for OPERNUM regions + // Get the OPERNUM region number that was assigned to border cells + + RiaLogging::info( QString( "Using OPERNUM region %1 for border cells" ).arg( operNumRegion ) ); + + // Ensure REGDIMS keyword exists + if ( !deckFile.ensureRegdimsKeyword() ) + { + return std::unexpected( "Failed to ensure REGDIMS keyword exists in RUNSPEC section" ); + } + + // Read current REGDIMS values + auto regdimsValues = deckFile.regdims(); + if ( regdimsValues.empty() ) + { + // TODO: improve this? + // Use defaults if reading failed + regdimsValues = { 1, 1, 0, 0, 0, 1, 0 }; // NTFIP NMFIPR NRFREG NTFREG MAX_ETRACK NTCREG MAX_OPERNUM + } + + // Update MAX_OPERNUM (item 7) to the new region number + if ( regdimsValues.size() < 7 ) + { + regdimsValues.resize( 7, 0 ); + } + regdimsValues[6] = operNumRegion; // Index 6 is item 7 (MAX_OPERNUM) + + // Update REGDIMS in deck + if ( !deckFile.setRegdims( regdimsValues[0], + regdimsValues[1], + regdimsValues[2], + regdimsValues[3], + regdimsValues[4], + regdimsValues[5], + regdimsValues[6] ) ) + { + return std::unexpected( "Failed to update REGDIMS keyword in deck file" ); + } + + // Check if OPERNUM keyword already exists in deck + auto opernumKeyword = deckFile.findKeyword( "OPERNUM" ); + if ( !opernumKeyword.has_value() ) + { + auto keywords = deckFile.keywords( false ); + if ( std::find( keywords.begin(), keywords.end(), "OPERNUM" ) == keywords.end() ) + { + Opm::DeckKeyword newKw( ( Opm::ParserKeywords::OPERNUM() ) ); + deckFile.addKeyword( "GRID", newKw ); + } + + // Try to extract keyword data + auto result = RifEclipseInputFileTools::extractKeywordData( eclipseCase->eclipseCaseData(), + "OPERNUM", + settings.min(), + settings.max(), + settings.refinement() ); + if ( result ) + { + // Replace keyword values in deck with extracted data + if ( deckFile.replaceKeywordData( "OPERNUM", result.value() ) ) + { + RiaLogging::info( "Added replaced opernum value in exising position." ); + } + else + { + return std::unexpected( "Unable to replace OPERNUM values" ); + } + } + else + { + return std::unexpected( "No OPERNUM result found." ); + } + } + else + { + RiaLogging::info( "OPERNUM keyword already exists in deck, skipping INCLUDE" ); + } + + // Create OPERATER keyword to multiply pore volume in border region + // OPERATER format: TARGET_ARRAY REGION_NUMBER OPERATION ARRAY_PARAMETER PARAM1 PARAM2 REGION_NAME + // Example: OPERATER / PORV 1 MULTX PORV 1.0e6 1* 1* / + // Note: We don't have access to m_porvMultiplier from settings, so we use a default value + float porvMultiplier = 1.0e6f; // Default value + Opm::DeckKeyword operaterKw = RimKeywordFactory::operaterKeyword( "PORV", operNumRegion, "MULTX", "PORV", porvMultiplier ); + + // Add OPERATER keyword to EDIT section + if ( !deckFile.replaceKeyword( "EDIT", operaterKw ) ) + { + return std::unexpected( "Failed to add OPERATER keyword to EDIT section" ); + } + + RiaLogging::info( QString( "Added OPERATER keyword to multiply PORV in region %1 by %2" ).arg( operNumRegion ).arg( porvMultiplier ) ); + + return {}; +} diff --git a/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.h b/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.h new file mode 100644 index 0000000000..2420a9486b --- /dev/null +++ b/ApplicationLibCode/ReservoirDataModel/SimulationFile/RigSimulationInputTool.h @@ -0,0 +1,94 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2025- Equinor ASA +// +// ResInsight is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY +// WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. +// +// See the GNU General Public License at +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "cafVecIjk.h" +#include "cvfVector3.h" + +#include + +#include +#include +#include + +class RimEclipseView; +class RimEclipseCase; +class RigSimulationInputSettings; +class RifOpmFlowDeckFile; +class RigSimWellData; + +namespace Opm +{ +class DeckRecord; +class DeckKeyword; +} // namespace Opm + +//================================================================================================== +/// +/// Tool for exporting simulation input files (sector models) +/// +//================================================================================================== +class RigSimulationInputTool +{ +public: + static std::expected + exportSimulationInput( RimEclipseView& view, RimEclipseCase& eclipseCase, const RigSimulationInputSettings& settings ); + + static std::expected + processEqualsRecord( const Opm::DeckRecord& record, const caf::VecIjk0& min, const caf::VecIjk0& max, const cvf::Vec3st& refinement ); + +private: + static std::expected updateCornerPointGridInDeckFile( RimEclipseCase* eclipseCase, + const RigSimulationInputSettings& settings, + RifOpmFlowDeckFile& deckFile ); + + static std::expected replaceKeywordValuesInDeckFile( RimEclipseCase* eclipseCase, + const RigSimulationInputSettings& settings, + RifOpmFlowDeckFile& deckFile ); + + static std::expected + addBorderBoundaryConditions( RimEclipseCase* eclipseCase, const RigSimulationInputSettings& settings, RifOpmFlowDeckFile& deckFile ); + + static std::expected + replaceEqualsKeywordIndices( RimEclipseCase* eclipseCase, const RigSimulationInputSettings& settings, RifOpmFlowDeckFile& deckFile ); + + static std::expected + addFaultsToDeckFile( RimEclipseCase* eclipseCase, const RigSimulationInputSettings& settings, RifOpmFlowDeckFile& deckFile ); + + static std::expected + filterAndUpdateWellKeywords( RimEclipseCase* eclipseCase, const RigSimulationInputSettings& settings, RifOpmFlowDeckFile& deckFile ); + + static std::expected addOperNumRegionAndOperater( RimEclipseCase* eclipseCase, + const RigSimulationInputSettings& settings, + RifOpmFlowDeckFile& deckFile, + int operNumRegion ); + + static std::vector findIntersectingWells( RimEclipseCase* eclipseCase, const cvf::Vec3st& min, const cvf::Vec3st& max ); + + 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 ); +}; diff --git a/ApplicationLibCode/UnitTests/CMakeLists.txt b/ApplicationLibCode/UnitTests/CMakeLists.txt index 6ace34f381..e9ebcf2d1d 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists.txt +++ b/ApplicationLibCode/UnitTests/CMakeLists.txt @@ -8,7 +8,7 @@ endif(MSVC) set(SOURCE_UNITTEST_FILES ${CMAKE_CURRENT_LIST_DIR}/cvfGeometryTools-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/Ert-Test.cpp - ${CMAKE_CURRENT_LIST_DIR}/RicExportEclipseSectorModelFeature-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RigSimulationInputTool-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RigBoundingBoxIjk-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifcCommandCore-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifEclipseInputFileTools-Test.cpp diff --git a/ApplicationLibCode/UnitTests/RicExportEclipseSectorModelFeature-Test.cpp b/ApplicationLibCode/UnitTests/RigSimulationInputTool-Test.cpp similarity index 87% rename from ApplicationLibCode/UnitTests/RicExportEclipseSectorModelFeature-Test.cpp rename to ApplicationLibCode/UnitTests/RigSimulationInputTool-Test.cpp index 3c7a22deec..1c74082801 100644 --- a/ApplicationLibCode/UnitTests/RicExportEclipseSectorModelFeature-Test.cpp +++ b/ApplicationLibCode/UnitTests/RigSimulationInputTool-Test.cpp @@ -18,7 +18,7 @@ #include "gtest/gtest.h" -#include "ExportCommands/RicExportEclipseSectorModelFeature.h" +#include "RigSimulationInputTool.h" #include "RifOpmDeckTools.h" @@ -50,7 +50,7 @@ static Opm::DeckRecord createEqualsRecord( const std::string& fieldName, int val //-------------------------------------------------------------------------------------------------- /// Test EQUALS record with no overlap - should return error //-------------------------------------------------------------------------------------------------- -TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_NoOverlap ) +TEST( RigSimulationInputTool, ProcessEqualsRecord_NoOverlap ) { // Sector: sectorMin=(0, 15, 0), sectorMax=(19, 29, 9) (inclusive, cells [0-19, 15-29, 0-9]) caf::VecIjk0 sectorMin( 0, 15, 0 ); @@ -62,7 +62,7 @@ TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_NoOverlap ) // Sector J is [15-29], so no overlap in J dimension auto record = createEqualsRecord( "FIPNUM", 1, 1, 20, 1, 14, 1, 10 ); - auto result = RicExportEclipseSectorModelFeature::processEqualsRecord( record, sectorMin, sectorMax, refinement ); + auto result = RigSimulationInputTool::processEqualsRecord( record, sectorMin, sectorMax, refinement ); EXPECT_FALSE( result.has_value() ); EXPECT_TRUE( result.error().contains( "does not overlap" ) ); @@ -71,7 +71,7 @@ TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_NoOverlap ) //-------------------------------------------------------------------------------------------------- /// Test EQUALS record with partial overlap requiring clamping //-------------------------------------------------------------------------------------------------- -TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_PartialOverlapWithClamping ) +TEST( RigSimulationInputTool, ProcessEqualsRecord_PartialOverlapWithClamping ) { // Sector: sectorMin=(0, 15, 0), sectorMax=(19, 29, 9) (inclusive, cells [0-19, 15-29, 0-9]) caf::VecIjk0 sectorMin( 0, 15, 0 ); @@ -84,7 +84,7 @@ TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_PartialOverlapWith // Intersection (clamped): I[0-19], J[15-29], K[0-9] auto record = createEqualsRecord( "FIPNUM", 2, 1, 20, 15, 30, 1, 10 ); - auto result = RicExportEclipseSectorModelFeature::processEqualsRecord( record, sectorMin, sectorMax, refinement ); + auto result = RigSimulationInputTool::processEqualsRecord( record, sectorMin, sectorMax, refinement ); ASSERT_TRUE( result.has_value() ); @@ -109,7 +109,7 @@ TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_PartialOverlapWith //-------------------------------------------------------------------------------------------------- /// Test EQUALS record completely inside sector (no clamping) //-------------------------------------------------------------------------------------------------- -TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_CompletelyInside ) +TEST( RigSimulationInputTool, ProcessEqualsRecord_CompletelyInside ) { // Sector: min=(0, 0, 0), max=(19, 19, 9) (inclusive, cells [0-19, 0-19, 0-9]) caf::VecIjk0 sectorMin( 0, 0, 0 ); @@ -120,7 +120,7 @@ TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_CompletelyInside ) // Converts to 0-based: I[4-14], J[4-14], K[1-7] auto record = createEqualsRecord( "FIPNUM", 3, 5, 15, 5, 15, 2, 8 ); - auto result = RicExportEclipseSectorModelFeature::processEqualsRecord( record, sectorMin, sectorMax, refinement ); + auto result = RigSimulationInputTool::processEqualsRecord( record, sectorMin, sectorMax, refinement ); ASSERT_TRUE( result.has_value() ); @@ -139,7 +139,7 @@ TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_CompletelyInside ) //-------------------------------------------------------------------------------------------------- /// Test EQUALS record completely outside sector //-------------------------------------------------------------------------------------------------- -TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_CompletelyOutside ) +TEST( RigSimulationInputTool, ProcessEqualsRecord_CompletelyOutside ) { // Sector: min=(0, 0, 0), max=(9, 9, 9) (inclusive, cells [0-9, 0-9, 0-9]) caf::VecIjk0 sectorMin( 0, 0, 0 ); @@ -151,7 +151,7 @@ TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_CompletelyOutside // No overlap with sector auto record = createEqualsRecord( "FIPNUM", 4, 20, 30, 20, 30, 1, 10 ); - auto result = RicExportEclipseSectorModelFeature::processEqualsRecord( record, sectorMin, sectorMax, refinement ); + auto result = RigSimulationInputTool::processEqualsRecord( record, sectorMin, sectorMax, refinement ); EXPECT_FALSE( result.has_value() ); EXPECT_TRUE( result.error().contains( "does not overlap" ) ); @@ -160,7 +160,7 @@ TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_CompletelyOutside //-------------------------------------------------------------------------------------------------- /// Test invalid EQUALS record (insufficient items) //-------------------------------------------------------------------------------------------------- -TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_InvalidRecord ) +TEST( RigSimulationInputTool, ProcessEqualsRecord_InvalidRecord ) { caf::VecIjk0 sectorMin( 0, 0, 0 ); caf::VecIjk0 sectorMax( 9, 9, 9 ); @@ -177,7 +177,7 @@ TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_InvalidRecord ) Opm::DeckRecord record{ std::move( items ) }; - auto result = RicExportEclipseSectorModelFeature::processEqualsRecord( record, sectorMin, sectorMax, refinement ); + auto result = RigSimulationInputTool::processEqualsRecord( record, sectorMin, sectorMax, refinement ); EXPECT_FALSE( result.has_value() ); EXPECT_TRUE( result.error().contains( "insufficient items" ) ); @@ -186,7 +186,7 @@ TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_InvalidRecord ) //-------------------------------------------------------------------------------------------------- /// Test EQUALS record at sector boundary //-------------------------------------------------------------------------------------------------- -TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_AtBoundary ) +TEST( RigSimulationInputTool, ProcessEqualsRecord_AtBoundary ) { // Sector: min=(5, 5, 5), max=(14, 14, 14) (inclusive, cells [5-14, 5-14, 5-14]) caf::VecIjk0 sectorMin( 5, 5, 5 ); @@ -197,7 +197,7 @@ TEST( RicExportEclipseSectorModelFeature, ProcessEqualsRecord_AtBoundary ) // Converts to 0-based: I[5-14], J[5-14], K[5-14] auto record = createEqualsRecord( "FIPNUM", 5, 6, 15, 6, 15, 6, 15 ); - auto result = RicExportEclipseSectorModelFeature::processEqualsRecord( record, sectorMin, sectorMax, refinement ); + auto result = RigSimulationInputTool::processEqualsRecord( record, sectorMin, sectorMax, refinement ); ASSERT_TRUE( result.has_value() );