From d8f346e9ab9de2f786fb8b63ab2d90db8e04c06d Mon Sep 17 00:00:00 2001 From: Magne Sjaastad Date: Fri, 9 Oct 2026 14:36:40 +0200 Subject: [PATCH] #14804 MSW export: Segmentation controls per interval and around perforations Add fixed, min and max segment length per segment interval (fixed is exclusive with min/max, min and max can be combined), connect COMPSEGS rows to the nearest segment node, and add collection options for a single segment before the first and after the last perforation. --- ApplicationExeCode/Resources/ResInsight.qrc | 1 + ApplicationExeCode/Resources/Segment.svg | 3 + .../CompletionCommands/CMakeLists_files.cmake | 2 +- ...pp => RicCreateSegmentIntervalFeature.cpp} | 14 +- ...re.h => RicCreateSegmentIntervalFeature.h} | 2 +- .../MswExport/RicMswBranchBuilder.cpp | 355 ++++++++---- .../MswExport/RicMswBranchBuilder.h | 33 +- .../RicWellPathExportMswGeometryPath.cpp | 5 +- .../RicWellPathExportMswGeometryPath.h | 7 +- .../RicMswTableDataTools.cpp | 223 ++++++++ .../RicMswTableDataTools.h | 72 +++ .../RicWellPathExportMswTableData.cpp | 2 +- .../Completions/RimSegmentCollection.cpp | 115 +++- .../Completions/RimSegmentCollection.h | 24 +- .../Completions/RimSegmentInterval.cpp | 129 ++++- .../Completions/RimSegmentInterval.h | 16 + .../RimcWellPathCompletionSettings.cpp | 101 ++++ .../RimcWellPathCompletionSettings.h | 68 +++ .../UnitTests/RicMswBranchBuilder-Test.cpp | 228 +++++++- .../RicWellPathExportMswGeometryPath-Test.cpp | 517 ++++++++++++++++++ .../UnitTests/RimWellPathCompletions-Test.cpp | 56 ++ .../tests/test_custom_segment_intervals.py | 42 ++ 22 files changed, 1866 insertions(+), 149 deletions(-) create mode 100644 ApplicationExeCode/Resources/Segment.svg rename ApplicationLibCode/Commands/CompletionCommands/{RicNewSegmentIntervalFeature.cpp => RicCreateSegmentIntervalFeature.cpp} (71%) rename ApplicationLibCode/Commands/CompletionCommands/{RicNewSegmentIntervalFeature.h => RicCreateSegmentIntervalFeature.h} (94%) diff --git a/ApplicationExeCode/Resources/ResInsight.qrc b/ApplicationExeCode/Resources/ResInsight.qrc index 8ba437ff64..4589087e25 100644 --- a/ApplicationExeCode/Resources/ResInsight.qrc +++ b/ApplicationExeCode/Resources/ResInsight.qrc @@ -111,6 +111,7 @@ Parallel.svg PerforationInterval16x16.png PerforationIntervals16x16.png + Segment.svg Perspective.svg PlotWindow.svg Plus.png diff --git a/ApplicationExeCode/Resources/Segment.svg b/ApplicationExeCode/Resources/Segment.svg new file mode 100644 index 0000000000..991684ed64 --- /dev/null +++ b/ApplicationExeCode/Resources/Segment.svg @@ -0,0 +1,3 @@ + + + diff --git a/ApplicationLibCode/Commands/CompletionCommands/CMakeLists_files.cmake b/ApplicationLibCode/Commands/CompletionCommands/CMakeLists_files.cmake index 896f1cc3db..15ae14ccf0 100644 --- a/ApplicationLibCode/Commands/CompletionCommands/CMakeLists_files.cmake +++ b/ApplicationLibCode/Commands/CompletionCommands/CMakeLists_files.cmake @@ -14,7 +14,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RicNewStimPlanModelPlotFeature.cpp ${CMAKE_CURRENT_LIST_DIR}/RicImportEnsembleFractureStatisticsFeature.cpp ${CMAKE_CURRENT_LIST_DIR}/RicImportValveTemplatesFeature.cpp - ${CMAKE_CURRENT_LIST_DIR}/RicNewSegmentIntervalFeature.cpp + ${CMAKE_CURRENT_LIST_DIR}/RicCreateSegmentIntervalFeature.cpp ${CMAKE_CURRENT_LIST_DIR}/RicDeleteSegmentIntervalFeature.cpp ) diff --git a/ApplicationLibCode/Commands/CompletionCommands/RicNewSegmentIntervalFeature.cpp b/ApplicationLibCode/Commands/CompletionCommands/RicCreateSegmentIntervalFeature.cpp similarity index 71% rename from ApplicationLibCode/Commands/CompletionCommands/RicNewSegmentIntervalFeature.cpp rename to ApplicationLibCode/Commands/CompletionCommands/RicCreateSegmentIntervalFeature.cpp index 75c0e96bbc..94953ab4d4 100644 --- a/ApplicationLibCode/Commands/CompletionCommands/RicNewSegmentIntervalFeature.cpp +++ b/ApplicationLibCode/Commands/CompletionCommands/RicCreateSegmentIntervalFeature.cpp @@ -8,7 +8,7 @@ // (at your option) any later version. // ///////////////////////////////////////////////////////////////////////////////// -#include "RicNewSegmentIntervalFeature.h" +#include "RicCreateSegmentIntervalFeature.h" #include "RimProject.h" #include "RimSegmentCollection.h" @@ -19,15 +19,16 @@ #include "cafSelectionManager.h" #include +#include -CAF_CMD_SOURCE_INIT( RicNewSegmentIntervalFeature, "RicNewSegmentIntervalFeature" ); +CAF_CMD_SOURCE_INIT( RicCreateSegmentIntervalFeature, "RicCreateSegmentIntervalFeature" ); -bool RicNewSegmentIntervalFeature::isCommandEnabled() const +bool RicCreateSegmentIntervalFeature::isCommandEnabled() const { return caf::SelectionManager::instance()->selectedItemOfType() != nullptr; } -void RicNewSegmentIntervalFeature::onActionTriggered( bool isChecked ) +void RicCreateSegmentIntervalFeature::onActionTriggered( bool isChecked ) { auto* collection = caf::SelectionManager::instance()->selectedItemOfType(); if ( !collection ) return; @@ -39,7 +40,8 @@ void RicNewSegmentIntervalFeature::onActionTriggered( bool isChecked ) if ( auto* project = RimProject::current() ) project->scheduleCreateDisplayModelAndRedrawAllViews(); } -void RicNewSegmentIntervalFeature::setupActionLook( QAction* actionToSetup ) +void RicCreateSegmentIntervalFeature::setupActionLook( QAction* actionToSetup ) { - actionToSetup->setText( "New Segment Interval" ); + actionToSetup->setText( "Create Segment Interval" ); + actionToSetup->setIcon( QIcon( ":/Segment.svg" ) ); } diff --git a/ApplicationLibCode/Commands/CompletionCommands/RicNewSegmentIntervalFeature.h b/ApplicationLibCode/Commands/CompletionCommands/RicCreateSegmentIntervalFeature.h similarity index 94% rename from ApplicationLibCode/Commands/CompletionCommands/RicNewSegmentIntervalFeature.h rename to ApplicationLibCode/Commands/CompletionCommands/RicCreateSegmentIntervalFeature.h index 6dfdd9a9fc..50264d30aa 100644 --- a/ApplicationLibCode/Commands/CompletionCommands/RicNewSegmentIntervalFeature.h +++ b/ApplicationLibCode/Commands/CompletionCommands/RicCreateSegmentIntervalFeature.h @@ -23,7 +23,7 @@ //================================================================================================== /// //================================================================================================== -class RicNewSegmentIntervalFeature : public caf::CmdFeature +class RicCreateSegmentIntervalFeature : public caf::CmdFeature { CAF_CMD_HEADER_INIT; diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicMswBranchBuilder.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicMswBranchBuilder.cpp index dcc2cc2546..5bb1983d41 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicMswBranchBuilder.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicMswBranchBuilder.cpp @@ -203,7 +203,7 @@ double segmentMidpointMD( const std::vector& cellSegMap, int s { for ( const auto& entry : cellSegMap ) { - if ( entry.lastSubSegmentNumber == segmentNumber ) return 0.5 * ( entry.cellStartMD + entry.cellEndMD ); + if ( entry.lastSubSegmentNumber == segmentNumber ) return entry.nodeMD; } return 0.0; } @@ -228,17 +228,16 @@ int findOutletSegmentForMD( const std::vector& cellSegMap, dou { if ( cellSegMap.empty() ) return 1; - // Match the tree-based logic (findClosestSegmentWithLowerMD): find the sub-segment whose - // midpoint is closest to, but not greater than, the given MD. Range-based lookup is - // incorrect when a cell's midpoint lies above the completion MD (e.g. a fracture near the + // Match the tree-based logic (findClosestSegmentWithLowerMD): find the segment whose node is + // closest to, but not greater than, the given MD. Range-based lookup is + // incorrect when a segment node lies above the completion MD (e.g. a fracture near the // heel of a long cell), which would assign the wrong outlet segment. const CellSegmentEntry* best = nullptr; double bestDist = std::numeric_limits::infinity(); for ( const auto& entry : cellSegMap ) { - const double midpoint = 0.5 * ( entry.cellStartMD + entry.cellEndMD ); - const double dist = md - midpoint; + const double dist = md - entry.nodeMD; if ( dist >= 0.0 && dist < bestDist ) { best = &entry; @@ -246,10 +245,134 @@ int findOutletSegmentForMD( const std::vector& cellSegMap, dou } } - // Fallback: md is shallower than all segment midpoints — connect to the first (shallowest) segment. + // Fallback: md is shallower than all segment nodes — connect to the first (shallowest) segment. return best ? best->lastSubSegmentNumber : cellSegMap.front().lastSubSegmentNumber; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector segmentSpans( const std::vector>& cellPieces, const std::vector& nodes ) +{ + std::vector parts; + if ( nodes.empty() ) return parts; + + // Nodes unchanged by the segmentation rules: each piece is served by its own centre node + bool nodesAreCandidates = nodes.size() == cellPieces.size(); + for ( size_t i = 0; nodesAreCandidates && i < nodes.size(); ++i ) + { + nodesAreCandidates = nodes[i] == 0.5 * ( cellPieces[i].first + cellPieces[i].second ); + } + if ( nodesAreCandidates ) + { + for ( size_t i = 0; i < cellPieces.size(); ++i ) + { + parts.push_back( { cellPieces[i].first, cellPieces[i].second, i } ); + } + return parts; + } + + // Split the pieces at the nodes, each part belongs to the node upstream of it + for ( const auto& [pieceStart, pieceEnd] : cellPieces ) + { + std::vector cuts = { pieceStart }; + for ( const double nodeMD : nodes ) + { + if ( nodeMD > pieceStart && nodeMD < pieceEnd ) cuts.push_back( nodeMD ); + } + std::ranges::sort( cuts ); + cuts.push_back( pieceEnd ); + + for ( size_t k = 0; k + 1 < cuts.size(); ++k ) + { + parts.push_back( { cuts[k], cuts[k + 1], *RicMswTableDataTools::upstreamNodeIndex( nodes, cuts[k] ) } ); + } + } + + // Merge consecutive parts served by the same node + std::vector spans; + for ( const auto& part : parts ) + { + if ( !spans.empty() && spans.back().nodeIndex == part.nodeIndex ) + { + spans.back().endMD = part.endMD; + } + else + { + spans.push_back( part ); + } + } + return spans; +} + +namespace +{ + //-------------------------------------------------------------------------------------------------- + /// Data shared by the COMPSEGS collection of all cells along a main bore branch + //-------------------------------------------------------------------------------------------------- + struct PerforationCompsegsContext + { + const std::vector& perforationIntervals; + const std::set& valvedIntervals; + const std::optional& exportDate; + const RigMainGrid* mainGrid; + const RigActiveCellInfo* activeCellInfo; + std::map>& perforationCompletionCells; + std::map& filterEvaluators; + std::map& overlapCountPerInterval; + std::map& emittedCountPerInterval; + }; + + //-------------------------------------------------------------------------------------------------- + /// Collect COMPSEGS only for bare perforations (no active valve) on the main bore. + /// Valved perforations get their COMPSEGS on the valve segment. + //-------------------------------------------------------------------------------------------------- + std::vector collectCellCompsegs( const WellPathCellIntersectionInfo& cellInfo, PerforationCompsegsContext& context ) + { + std::vector cellCompsegs; + for ( const auto* perf : context.perforationIntervals ) + { + if ( context.valvedIntervals.count( perf ) ) continue; // valve handles this interval + if ( context.exportDate.has_value() && !perf->isActiveOnDate( context.exportDate.value() ) ) continue; + + const double overlapStart = std::max( perf->startMD(), cellInfo.startMD ); + const double overlapEnd = std::min( perf->endMD(), cellInfo.endMD ); + if ( overlapEnd > overlapStart ) + { + if ( context.activeCellInfo && cellInfo.globCellIndex < context.mainGrid->totalCellCount() && + !context.activeCellInfo->isActive( ReservoirCellIndex( cellInfo.globCellIndex ) ) ) + continue; + + // Skip cells the perforation does not actually connect to (see perforationCompletionCells). + const auto& completionCells = context.perforationCompletionCells[perf]; + if ( !completionCells.empty() && !completionCells.count( cellInfo.globCellIndex ) ) continue; + + context.overlapCountPerInterval[perf]++; + auto& filterEval = context.filterEvaluators.at( perf ); + if ( !filterEval.includesGlobalCell( cellInfo.globCellIndex ) ) continue; + if ( auto ci = toMswCellIntersection( cellInfo, context.mainGrid, overlapStart, overlapEnd ) ) + { + // All candidates here describe the same grid cell, as they are derived from a single cellInfo. + // Several perforation intervals may overlap that cell, but the cell must be connected only once, + // matching the single COMPDAT connection. Widen the existing range instead of adding another row. + if ( cellCompsegs.empty() ) + { + cellCompsegs.push_back( *ci ); + } + else + { + auto& existing = cellCompsegs.front(); + existing.distanceStart = std::min( existing.distanceStart, ci->distanceStart ); + existing.distanceEnd = std::max( existing.distanceEnd, ci->distanceEnd ); + } + context.emittedCountPerInterval[perf]++; + } + } + } + return cellCompsegs; + } +} // namespace + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -267,6 +390,7 @@ RigMswBranch buildMainBoreBranch( const RimWellPath* int outletSegmentNumber, double maxSegmentLength, const std::vector>& customSegmentIntervals, + const RicMswTableDataTools::SegmentationSettings& segmentationSettings, const std::optional& exportDate, RiaDefines::EclipseUnitSystem unitSystem, std::vector* cellSegMap, @@ -326,6 +450,20 @@ RigMswBranch buildMainBoreBranch( const RimWellPath* } } + PerforationCompsegsContext compsegsContext{ perforationIntervals, + valvedIntervals, + exportDate, + mainGrid, + activeCellInfo, + perforationCompletionCells, + filterEvaluators, + overlapCountPerInterval, + emittedCountPerInterval }; + + // Pass 1: the cell pieces give the candidate nodes at their centres, the segmentation rules place the nodes + std::vector> cellPieces; + std::vector> compsegsPerCell; + std::vector candidateNodes; for ( const auto& cellInfo : filteredIntersections ) { const double cellLength = std::fabs( cellInfo.endMD - cellInfo.startMD ); @@ -335,115 +473,120 @@ RigMswBranch buildMainBoreBranch( const RimWellPath* continue; } - // Collect COMPSEGS only for bare perforations (no active valve) on the main bore. - // Valved perforations get their COMPSEGS on the valve segment. - std::vector cellCompsegs; - for ( const auto* perf : perforationIntervals ) - { - if ( valvedIntervals.count( perf ) ) continue; // valve handles this interval - if ( exportDate.has_value() && !perf->isActiveOnDate( exportDate.value() ) ) continue; - - const double overlapStart = std::max( perf->startMD(), cellInfo.startMD ); - const double overlapEnd = std::min( perf->endMD(), cellInfo.endMD ); - if ( overlapEnd > overlapStart ) - { - if ( activeCellInfo && cellInfo.globCellIndex < mainGrid->totalCellCount() && - !activeCellInfo->isActive( ReservoirCellIndex( cellInfo.globCellIndex ) ) ) - continue; - - // Skip cells the perforation does not actually connect to (see perforationCompletionCells above). - const auto& completionCells = perforationCompletionCells[perf]; - if ( !completionCells.empty() && !completionCells.count( cellInfo.globCellIndex ) ) continue; - - overlapCountPerInterval[perf]++; - auto& filterEval = filterEvaluators.at( perf ); - if ( !filterEval.includesGlobalCell( cellInfo.globCellIndex ) ) continue; - if ( auto ci = toMswCellIntersection( cellInfo, mainGrid, overlapStart, overlapEnd ) ) - { - // All candidates here describe the same grid cell, as they are derived from a single cellInfo. - // Several perforation intervals may overlap that cell, but the cell must be connected only once, - // matching the single COMPDAT connection. Widen the existing range instead of adding another row. - if ( cellCompsegs.empty() ) - { - cellCompsegs.push_back( *ci ); - } - else - { - auto& existing = cellCompsegs.front(); - existing.distanceStart = std::min( existing.distanceStart, ci->distanceStart ); - existing.distanceEnd = std::max( existing.distanceEnd, ci->distanceEnd ); - } - emittedCountPerInterval[perf]++; - } - } - } + compsegsPerCell.push_back( collectCellCompsegs( cellInfo, compsegsContext ) ); const auto subSegs = RicMswTableDataTools::createSubSegmentMDPairs( cellInfo.startMD, cellInfo.endMD, maxSegmentLength, customSegmentIntervals ); - - bool firstSubSeg = true; - int lastSubSegNum = prevSegNum; for ( const auto& [subStartMD, subEndMD] : subSegs ) { - const double midPointMD = 0.5 * ( subStartMD + subEndMD ); - const double midPointTVD = RicMswTableDataTools::tvdFromMeasuredDepth( wellPath, midPointMD ); + cellPieces.push_back( { subStartMD, subEndMD } ); + candidateNodes.push_back( 0.5 * ( subStartMD + subEndMD ) ); + } + } - double curPrevOutMD = prevOutMD; - double curPrevOutTVD = prevOutTVD; - if ( midPointMD < curPrevOutMD ) - { - curPrevOutMD = heelMD; - curPrevOutTVD = heelTVD; - } + std::vector nodes = RicMswTableDataTools::placeSegmentNodes( heelMD, + candidateNodes, + segmentationSettings.intervals, + cellPieces.empty() ? heelMD : cellPieces.back().second ); - double length = 0.0; - double depth = 0.0; - if ( infoType == "INC" ) - { - length = midPointMD - curPrevOutMD; - depth = midPointTVD - curPrevOutTVD; - } - else - { - length = midPointMD; - depth = midPointTVD; - } + if ( !cellPieces.empty() && !perforationIntervals.empty() && + ( segmentationSettings.singleSegmentBeforeFirstPerforation || segmentationSettings.singleSegmentAfterLastPerforation ) ) + { + double firstPerforationMD = std::numeric_limits::infinity(); + double lastPerforationMD = -std::numeric_limits::infinity(); + for ( const auto* perf : perforationIntervals ) + { + firstPerforationMD = std::min( firstPerforationMD, perf->startMD() ); + lastPerforationMD = std::max( lastPerforationMD, perf->endMD() ); + } - double diameter = 0.0; - double roughness = 0.0; - if ( exportDate.has_value() ) - { - diameter = wellPath->segmentCollection()->getDiameterAtMD( midPointMD, unitSystem, *exportDate ); - roughness = wellPath->segmentCollection()->getRoughnessAtMD( midPointMD, unitSystem, *exportDate ); - } - else - { - diameter = wellPath->segmentCollection()->getDiameterAtMD( midPointMD, unitSystem ); - roughness = wellPath->segmentCollection()->getRoughnessAtMD( midPointMD, unitSystem ); - } + nodes = RicMswTableDataTools::collapseNodesOutsidePerforations( nodes, + cellPieces.front().first, + cellPieces.back().second, + firstPerforationMD, + lastPerforationMD, + segmentationSettings.singleSegmentBeforeFirstPerforation, + segmentationSettings.singleSegmentAfterLastPerforation ); + } - RigMswSegment seg; - seg.segmentNumber = segmentNumber; - seg.outletSegmentNumber = prevSegNum; - seg.length = length; - seg.depth = depth; - seg.diameter = diameter; - seg.roughness = roughness; - seg.sourceWellName = wellPath->name().toStdString(); - if ( firstSeg && firstSubSeg ) - { - seg.description = "Segments on main bore"; - firstSeg = false; - } - if ( firstSubSeg ) seg.intersections = cellCompsegs; + // Pass 2: one segment per node, the outlet is the previous node + const int firstSegmentNumber = segmentNumber; + for ( const double nodeMD : nodes ) + { + const double nodeTVD = RicMswTableDataTools::tvdFromMeasuredDepth( wellPath, nodeMD ); - lastSubSegNum = segmentNumber; - prevSegNum = segmentNumber++; - prevOutMD = midPointMD; - prevOutTVD = midPointTVD; - firstSubSeg = false; - if ( cellSegMap ) cellSegMap->push_back( { subStartMD, subEndMD, lastSubSegNum } ); - result.push_back( std::move( seg ) ); + double curPrevOutMD = prevOutMD; + double curPrevOutTVD = prevOutTVD; + if ( nodeMD < curPrevOutMD ) + { + curPrevOutMD = heelMD; + curPrevOutTVD = heelTVD; + } + + double length = 0.0; + double depth = 0.0; + if ( infoType == "INC" ) + { + length = nodeMD - curPrevOutMD; + depth = nodeTVD - curPrevOutTVD; + } + else + { + length = nodeMD; + depth = nodeTVD; + } + + double diameter = 0.0; + double roughness = 0.0; + if ( exportDate.has_value() ) + { + diameter = wellPath->segmentCollection()->getDiameterAtMD( nodeMD, unitSystem, *exportDate ); + roughness = wellPath->segmentCollection()->getRoughnessAtMD( nodeMD, unitSystem, *exportDate ); + } + else + { + diameter = wellPath->segmentCollection()->getDiameterAtMD( nodeMD, unitSystem ); + roughness = wellPath->segmentCollection()->getRoughnessAtMD( nodeMD, unitSystem ); + } + + RigMswSegment seg; + seg.segmentNumber = segmentNumber; + seg.outletSegmentNumber = prevSegNum; + seg.length = length; + seg.depth = depth; + seg.diameter = diameter; + seg.roughness = roughness; + seg.sourceWellName = wellPath->name().toStdString(); + if ( firstSeg ) + { + seg.description = "Segments on main bore"; + firstSeg = false; + } + + prevSegNum = segmentNumber++; + prevOutMD = nodeMD; + prevOutTVD = nodeTVD; + result.push_back( std::move( seg ) ); + } + + // COMPSEGS rows go to the segment with the node nearest the row centre, as the simulator assigns them + for ( const auto& cellCompsegs : compsegsPerCell ) + { + for ( const auto& ci : cellCompsegs ) + { + if ( auto nodeIndex = RicMswTableDataTools::nearestNodeIndex( nodes, 0.5 * ( ci.distanceStart + ci.distanceEnd ) ) ) + { + result[*nodeIndex].intersections.push_back( ci ); + } + } + } + + if ( cellSegMap ) + { + for ( const auto& span : segmentSpans( cellPieces, nodes ) ) + { + cellSegMap->push_back( + { span.startMD, span.endMD, firstSegmentNumber + static_cast( span.nodeIndex ), nodes[span.nodeIndex] } ); } } diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicMswBranchBuilder.h b/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicMswBranchBuilder.h index 2951cf9a05..8a8d0d00d1 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicMswBranchBuilder.h +++ b/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicMswBranchBuilder.h @@ -20,6 +20,7 @@ #include "CompletionsMsw/RigMswSegment.h" #include "RiaDefines.h" +#include "RicMswTableDataTools.h" #include "RigActiveCellInfo.h" #include "Well/RigWellLogExtractor.h" @@ -47,8 +48,10 @@ namespace RicMswBranchBuilder { //-------------------------------------------------------------------------------------------------- -/// Mapping from a sub-segment's MD range to its main-bore segment number. -/// One entry is emitted per sub-segment (a cell split into N sub-segments produces N entries). +/// Mapping from a main-bore segment's MD span to its segment number and node MD. +/// The span is the part of the branch served by the segment node. Without segmentation rules one +/// entry is emitted per sub-segment (a cell split into N sub-segments produces N entries), and the +/// node is at the centre of the span. /// Used by valve, fracture, and lateral builders to locate the outlet segment for a given MD. //-------------------------------------------------------------------------------------------------- struct CellSegmentEntry @@ -56,6 +59,17 @@ struct CellSegmentEntry double cellStartMD; double cellEndMD; int lastSubSegmentNumber; + double nodeMD; +}; + +//-------------------------------------------------------------------------------------------------- +/// MD span served by one segment node, nodeIndex refers to the node list. +//-------------------------------------------------------------------------------------------------- +struct NodeSpan +{ + double startMD; + double endMD; + size_t nodeIndex; }; //-------------------------------------------------------------------------------------------------- @@ -136,10 +150,20 @@ std::optional //-------------------------------------------------------------------------------------------------- int findOutletSegmentForMD( const std::vector& cellSegMap, double md ); +//-------------------------------------------------------------------------------------------------- +/// Split the cell pieces of a branch into the spans served by each segment node. Without +/// segmentation rules each piece is served by its own centre node. Otherwise the pieces are split +/// at the nodes, and each part is served by the nearest node upstream of it (the most upstream node +/// for parts above all nodes). Consecutive parts served by the same node are merged. +//-------------------------------------------------------------------------------------------------- +std::vector segmentSpans( const std::vector>& cellPieces, const std::vector& nodes ); + //-------------------------------------------------------------------------------------------------- /// Build main-bore WELSEGS segments directly from well-path geometry. -/// For each grid-cell intersection overlapping a bare perforation (no active valve) a COMPSEGS -/// entry is embedded. Optionally fills cellSegMap for later valve outlet-segment lookups. +/// Segment nodes are placed at cell centres and adjusted by the segmentation intervals (min/max +/// segment length). A COMPSEGS entry is embedded for each grid-cell intersection overlapping a bare +/// perforation (no active valve), on the segment whose node is nearest the entry centre. +/// Optionally fills cellSegMap for later valve outlet-segment lookups. //-------------------------------------------------------------------------------------------------- RigMswBranch buildMainBoreBranch( const RimWellPath* wellPath, const std::vector& filteredIntersections, @@ -155,6 +179,7 @@ RigMswBranch buildMainBoreBranch( const RimWellPath* int outletSegmentNumber, double maxSegmentLength, const std::vector>& customSegmentIntervals, + const RicMswTableDataTools::SegmentationSettings& segmentationSettings, const std::optional& exportDate, RiaDefines::EclipseUnitSystem unitSystem, std::vector* cellSegMap, diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicWellPathExportMswGeometryPath.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicWellPathExportMswGeometryPath.cpp index e5972e06ac..b3457c1bb9 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicWellPathExportMswGeometryPath.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicWellPathExportMswGeometryPath.cpp @@ -190,6 +190,7 @@ std::vector buildLateralBranches( RimEclipseCase* childOutletSeg, mswParameters->maxSegmentLength(), segmentIntervals, + RicMswTableDataTools::segmentationSettings( mswParameters, exportDate ), exportDate, unitSystem, &childCellSegMap, @@ -382,6 +383,7 @@ RigMswWellExportData buildMswWellExportData( RimEclipseCase* 1, // outlet = heel (segment 1) maxSegmentLength, customSegmentIntervals, + RicMswTableDataTools::segmentationSettings( mswParameters, exportDate ), exportDate, unitSystem, &cellSegMap, @@ -502,7 +504,7 @@ RigMswWellExportData buildMswWellExportData( RimEclipseCase* /// Populate RigMswTableData from a pre-built RigMswWellExportData by simple iteration over /// RigMswSegment objects. //-------------------------------------------------------------------------------------------------- -RigMswTableData collectTableData( const RigMswWellExportData& exportData, RiaDefines::EclipseUnitSystem unitSystem ) +RigMswTableData collectTableData( const RigMswWellExportData& exportData, RiaDefines::EclipseUnitSystem unitSystem, bool exportSegmentNumbers ) { RigMswTableData tableData( exportData.header.well, unitSystem ); tableData.setWelsegsHeader( exportData.header ); @@ -543,6 +545,7 @@ RigMswTableData collectTableData( const RigMswWellExportData& exportData, RiaDef compRow.distanceStart = inter.distanceStart; compRow.distanceEnd = inter.distanceEnd; compRow.gridName = inter.gridName; + if ( exportSegmentNumbers ) compRow.segmentNumber = seg.segmentNumber; compsegsRows.push_back( { compRow, branch.source } ); } diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicWellPathExportMswGeometryPath.h b/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicWellPathExportMswGeometryPath.h index f4b7910853..80adbb6a5b 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicWellPathExportMswGeometryPath.h +++ b/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicWellPathExportMswGeometryPath.h @@ -65,6 +65,11 @@ RigMswWellExportData buildMswWellExportData( RimEclipseCase* RicWellPathExportMswTableData::CompletionType completionType, const std::optional& exportDate ); -RigMswTableData collectTableData( const RigMswWellExportData& exportData, RiaDefines::EclipseUnitSystem unitSystem ); +//-------------------------------------------------------------------------------------------------- +/// Collect the table data of the export. The segment of each COMPSEGS row is written to ISEG if +/// exportSegmentNumbers is set, otherwise the simulator assigns the segment. +//-------------------------------------------------------------------------------------------------- +RigMswTableData + collectTableData( const RigMswWellExportData& exportData, RiaDefines::EclipseUnitSystem unitSystem, bool exportSegmentNumbers = false ); } // namespace RicWellPathExportMswGeometryPath diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicMswTableDataTools.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/RicMswTableDataTools.cpp index dc7df4132e..de9241c10c 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicMswTableDataTools.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicMswTableDataTools.cpp @@ -20,12 +20,15 @@ #include "Well/RigWellPath.h" +#include "RimSegmentCollection.h" +#include "RimSegmentInterval.h" #include "RimWellPath.h" #include "cafAssert.h" #include #include +#include #include //-------------------------------------------------------------------------------------------------- @@ -134,6 +137,226 @@ std::vector> return subSegmentMDPairs; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector + RicMswTableDataTools::segmentationIntervals( const RimSegmentCollection* segmentCollection, const std::optional& exportDate ) +{ + std::vector result; + if ( !segmentCollection ) return result; + + for ( const auto* interval : segmentCollection->segmentationIntervals( exportDate ) ) + { + result.push_back( + { interval->startMD(), interval->endMD(), interval->minSegmentLength(), interval->maxSegmentLength(), interval->fixedSegmentLength() } ); + } + return result; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RicMswTableDataTools::SegmentationSettings RicMswTableDataTools::segmentationSettings( const RimSegmentCollection* segmentCollection, + const std::optional& exportDate ) +{ + SegmentationSettings settings; + if ( !segmentCollection ) return settings; + + settings.intervals = segmentationIntervals( segmentCollection, exportDate ); + settings.singleSegmentBeforeFirstPerforation = segmentCollection->singleSegmentBeforeFirstPerforation(); + settings.singleSegmentAfterLastPerforation = segmentCollection->singleSegmentAfterLastPerforation(); + return settings; +} + +namespace +{ +const RicMswTableDataTools::SegmentationInterval* intervalAtMD( const std::vector& intervals, + double md ) +{ + for ( const auto& interval : intervals ) + { + if ( md >= interval.startMD && md <= interval.endMD ) return &interval; + } + return nullptr; +} +} // namespace + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RicMswTableDataTools::placeSegmentNodes( double outletMD, + const std::vector& allCandidateNodes, + const std::vector& intervals, + double lastMD ) +{ + if ( intervals.empty() ) return allCandidateNodes; + + const double tolerance = 1.0e-6; + + // Fixed length: replace the candidates inside the interval by nodes at the centre of fixed length segments + std::vector candidateNodes; + for ( double candidate : allCandidateNodes ) + { + const auto* interval = intervalAtMD( intervals, candidate ); + if ( !interval || !interval->fixedLength.has_value() ) candidateNodes.push_back( candidate ); + } + for ( const auto& interval : intervals ) + { + if ( !interval.fixedLength.has_value() || *interval.fixedLength <= 0.0 ) continue; + + const double length = *interval.fixedLength; + const double endMD = std::min( interval.endMD, lastMD ); + for ( int k = 0;; ++k ) + { + const double segmentStart = std::max( interval.startMD + k * length, outletMD ); + const double segmentEnd = std::min( interval.startMD + ( k + 1 ) * length, endMD ); + if ( interval.startMD + k * length >= endMD - tolerance ) break; + if ( segmentEnd - segmentStart > tolerance ) candidateNodes.push_back( 0.5 * ( segmentStart + segmentEnd ) ); + } + } + std::ranges::sort( candidateNodes ); + + // Min length: greedily keep candidates at least the min length from the previous kept node + std::vector keptNodes; + double previousMD = outletMD; + for ( size_t i = 0; i < candidateNodes.size(); ) + { + const auto* interval = intervalAtMD( intervals, candidateNodes[i] ); + if ( !interval || !interval->minLength.has_value() ) + { + keptNodes.push_back( candidateNodes[i] ); + previousMD = candidateNodes[i++]; + continue; + } + + const double minLength = *interval->minLength; + bool anyKept = false; + size_t j = i; + for ( ; j < candidateNodes.size() && intervalAtMD( intervals, candidateNodes[j] ) == interval; ++j ) + { + if ( candidateNodes[j] - previousMD >= minLength - tolerance ) + { + keptNodes.push_back( candidateNodes[j] ); + previousMD = candidateNodes[j]; + anyKept = true; + } + } + + // Keep one node for an interval shorter than the min length, the last one gives the longest segment + if ( !anyKept ) + { + keptNodes.push_back( candidateNodes[j - 1] ); + previousMD = candidateNodes[j - 1]; + } + i = j; + } + + // Max length: insert evenly spaced nodes where the spacing to the previous node is too long + std::vector nodes; + previousMD = outletMD; + for ( double nodeMD : keptNodes ) + { + const auto* interval = intervalAtMD( intervals, nodeMD ); + if ( interval && interval->maxLength.has_value() ) + { + const double maxLength = *interval->maxLength; + const double gap = nodeMD - previousMD; + if ( gap > maxLength + tolerance ) + { + const int segmentCount = static_cast( std::ceil( ( gap - tolerance ) / maxLength ) ); + const double spacing = gap / segmentCount; + for ( int k = 1; k < segmentCount; ++k ) + { + nodes.push_back( previousMD + k * spacing ); + } + } + } + nodes.push_back( nodeMD ); + previousMD = nodeMD; + } + + return nodes; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RicMswTableDataTools::collapseNodesOutsidePerforations( const std::vector& nodes, + double regionStartMD, + double regionEndMD, + double firstPerforationMD, + double lastPerforationMD, + bool singleBefore, + bool singleAfter ) +{ + if ( firstPerforationMD > lastPerforationMD ) return nodes; + + const auto countBefore = std::ranges::count_if( nodes, [&]( double md ) { return md < firstPerforationMD; } ); + const auto countAfter = std::ranges::count_if( nodes, [&]( double md ) { return md > lastPerforationMD; } ); + const bool collapseBefore = singleBefore && countBefore >= 2; + const bool collapseAfter = singleAfter && countAfter >= 2; + + std::vector result; + if ( collapseBefore ) result.push_back( 0.5 * ( regionStartMD + firstPerforationMD ) ); + bool afterAdded = false; + for ( double md : nodes ) + { + if ( collapseBefore && md < firstPerforationMD ) continue; + if ( collapseAfter && md > lastPerforationMD ) + { + if ( !afterAdded ) result.push_back( 0.5 * ( lastPerforationMD + regionEndMD ) ); + afterAdded = true; + continue; + } + result.push_back( md ); + } + return result; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::optional RicMswTableDataTools::upstreamNodeIndex( const std::vector& nodes, double md ) +{ + if ( nodes.empty() ) return std::nullopt; + + std::optional upstream; + double minDistance = std::numeric_limits::infinity(); + for ( size_t i = 0; i < nodes.size(); ++i ) + { + const double distance = md - nodes[i]; + if ( distance >= 0.0 && distance < minDistance ) + { + minDistance = distance; + upstream = i; + } + } + + // md is upstream of all nodes, use the most upstream node + if ( !upstream ) upstream = std::ranges::min_element( nodes ) - nodes.begin(); + return upstream; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::optional RicMswTableDataTools::nearestNodeIndex( const std::vector& nodes, double md ) +{ + std::optional nearest; + double minDistance = std::numeric_limits::infinity(); + for ( size_t i = 0; i < nodes.size(); ++i ) + { + const double distance = std::abs( md - nodes[i] ); + if ( distance < minDistance ) + { + minDistance = distance; + nearest = i; + } + } + return nearest; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicMswTableDataTools.h b/ApplicationLibCode/Commands/CompletionExportCommands/RicMswTableDataTools.h index d9a7ab92ff..3bd1581145 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicMswTableDataTools.h +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicMswTableDataTools.h @@ -18,10 +18,15 @@ #pragma once +#include + +#include +#include #include #include class RimWellPath; +class RimSegmentCollection; //-------------------------------------------------------------------------------------------------- /// Helper functions shared by the MSW table data export code paths. @@ -29,11 +34,78 @@ class RimWellPath; namespace RicMswTableDataTools { +//-------------------------------------------------------------------------------------------------- +/// MD range where the spacing between consecutive segment nodes is controlled. A fixed length cannot +/// be combined with min/max length, min and max length can be combined. A rule applies to the +/// segments whose node lies inside [startMD, endMD]. +//-------------------------------------------------------------------------------------------------- +struct SegmentationInterval +{ + double startMD; + double endMD; + std::optional minLength; + std::optional maxLength; + std::optional fixedLength; +}; + +//-------------------------------------------------------------------------------------------------- +/// Segmentation rules of a segment collection +//-------------------------------------------------------------------------------------------------- +struct SegmentationSettings +{ + std::vector intervals; + bool singleSegmentBeforeFirstPerforation = false; + bool singleSegmentAfterLastPerforation = false; +}; + std::vector> createSubSegmentMDPairs( double startMD, double endMD, double maxSegmentLength, const std::vector>& customSegmentIntervals = {} ); +std::vector segmentationIntervals( const RimSegmentCollection* segmentCollection, + const std::optional& exportDate ); + +SegmentationSettings segmentationSettings( const RimSegmentCollection* segmentCollection, const std::optional& exportDate ); + +//-------------------------------------------------------------------------------------------------- +/// Place the segment nodes of a branch. Candidate nodes are the cell (piece) centres in MD order. +/// Inside a fixed length interval, the candidates are replaced by nodes at the centre of segments of +/// the fixed length, starting at the interval start and limited to [outletMD, lastMD]. Inside a min +/// length interval, candidates closer than the min length to the previous node are dropped. Inside a +/// max length interval, evenly spaced nodes are inserted until no spacing exceeds the max length. +/// The spacing of the first node is measured from the outlet node. +//-------------------------------------------------------------------------------------------------- +std::vector placeSegmentNodes( double outletMD, + const std::vector& candidateNodes, + const std::vector& intervals, + double lastMD = std::numeric_limits::infinity() ); + +//-------------------------------------------------------------------------------------------------- +/// Replace the nodes in [regionStartMD, firstPerforationMD) and/or (lastPerforationMD, regionEndMD] +/// by a single node at the centre of the region. Regions with a single node are unchanged. +//-------------------------------------------------------------------------------------------------- +std::vector collapseNodesOutsidePerforations( const std::vector& nodes, + double regionStartMD, + double regionEndMD, + double firstPerforationMD, + double lastPerforationMD, + bool singleBefore, + bool singleAfter ); + +//-------------------------------------------------------------------------------------------------- +/// Index of the nearest node upstream of md, i.e. the node with the largest MD not greater than md. +/// If md is upstream of all nodes, the most upstream node is used. Returns std::nullopt if there are +/// no nodes. +//-------------------------------------------------------------------------------------------------- +std::optional upstreamNodeIndex( const std::vector& nodes, double md ); + +//-------------------------------------------------------------------------------------------------- +/// Index of the node nearest to md, the first node wins ties. Mirrors the COMPSEGS connection to +/// segment assignment in OPM (Compsegs.cpp). Returns std::nullopt if there are no nodes. +//-------------------------------------------------------------------------------------------------- +std::optional nearestNodeIndex( const std::vector& nodes, double md ); + double tvdFromMeasuredDepth( const RimWellPath* wellPath, double measuredDepth ); inline constexpr double valveSegmentLength = 0.1; diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportMswTableData.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportMswTableData.cpp index 6d6f2b60ec..627d219413 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportMswTableData.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportMswTableData.cpp @@ -84,7 +84,7 @@ std::expected RicWellPathExportMswTableData::extra exportDate ); auto unitSystem = eclipseCase->eclipseCaseData()->unitsType(); - return RicWellPathExportMswGeometryPath::collectTableData( wellExportData, unitSystem ); + return RicWellPathExportMswGeometryPath::collectTableData( wellExportData, unitSystem, segmentCollection->exportCompsegsSegmentNumber() ); } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/Completions/RimSegmentCollection.cpp b/ApplicationLibCode/ProjectDataModel/Completions/RimSegmentCollection.cpp index 6814f78bdc..c0e46ff5a1 100644 --- a/ApplicationLibCode/ProjectDataModel/Completions/RimSegmentCollection.cpp +++ b/ApplicationLibCode/ProjectDataModel/Completions/RimSegmentCollection.cpp @@ -73,6 +73,17 @@ void RimSegmentCollection::LengthAndDepthEnum::setUp() setDefault( RimSegmentCollection::LengthAndDepthType::ABS ); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +template <> +void RimSegmentCollection::CompsegsSegmentNumberEnum::setUp() +{ + addItem( RimSegmentCollection::CompsegsSegmentNumberType::ASSIGNED_BY_SIMULATOR, "AssignedBySimulator", "Assigned by Simulator (1*)" ); + addItem( RimSegmentCollection::CompsegsSegmentNumberType::EXPORTED, "Exported", "Exported" ); + setDefault( RimSegmentCollection::CompsegsSegmentNumberType::ASSIGNED_BY_SIMULATOR ); +} + } // namespace caf CAF_PDM_SOURCE_INIT( RimSegmentCollection, "SegmentCollection" ); @@ -82,7 +93,7 @@ CAF_PDM_SOURCE_INIT( RimSegmentCollection, "SegmentCollection" ); //-------------------------------------------------------------------------------------------------- RimSegmentCollection::RimSegmentCollection() { - CAF_PDM_InitScriptableObject( "Segments", ":/CompletionsSymbol16x16.png", "", "SegmentCollection" ); + CAF_PDM_InitScriptableObject( "Segments", ":/Segment.svg", "", "SegmentCollection" ); CAF_PDM_InitScriptableFieldWithScriptKeywordNoDefault( &m_refMDType, "RefMDType", "ReferenceMdType", "Reference MD Type" ); CAF_PDM_InitScriptableFieldWithScriptKeyword( &m_refMD, "RefMD", "UserDefinedReferenceMd", 0.0, "User Defined Reference MD" ); @@ -98,6 +109,18 @@ RimSegmentCollection::RimSegmentCollection() CAF_PDM_InitScriptableField( &m_enforceMaxSegmentLength, "EnforceMaxSegmentLength", false, "Enforce Max Segment Length" ); CAF_PDM_InitScriptableField( &m_maxSegmentLength, "MaxSegmentLength", 200.0, "Max Segment Length" ); m_maxSegmentLength.uiCapability()->setUiHidden( true ); + CAF_PDM_InitScriptableField( &m_singleSegmentBeforeFirstPerforation, + "SingleSegmentBeforeFirstPerforation", + false, + "Single Segment Before First Perforation" ); + CAF_PDM_InitScriptableField( &m_singleSegmentAfterLastPerforation, + "SingleSegmentAfterLastPerforation", + false, + "Single Segment After Last Perforation" ); + + CAF_PDM_InitScriptableFieldNoDefault( &m_compsegsSegmentNumber, "CompsegsSegmentNumber", "COMPSEGS Segment Number (ISEG)" ); + m_compsegsSegmentNumber.uiCapability()->setUiToolTip( + "Export the segment each COMPSEGS connection belongs to, or leave it to the simulator to assign the segment" ); } //-------------------------------------------------------------------------------------------------- @@ -201,6 +224,54 @@ double RimSegmentCollection::maxSegmentLength() const return m_enforceMaxSegmentLength() ? m_maxSegmentLength() : std::numeric_limits::infinity(); } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimSegmentCollection::setSingleSegmentBeforeFirstPerforation( bool enable ) +{ + m_singleSegmentBeforeFirstPerforation = enable; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimSegmentCollection::setSingleSegmentAfterLastPerforation( bool enable ) +{ + m_singleSegmentAfterLastPerforation = enable; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimSegmentCollection::singleSegmentBeforeFirstPerforation() const +{ + return m_singleSegmentBeforeFirstPerforation(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimSegmentCollection::singleSegmentAfterLastPerforation() const +{ + return m_singleSegmentAfterLastPerforation(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +bool RimSegmentCollection::exportCompsegsSegmentNumber() const +{ + return m_compsegsSegmentNumber() == CompsegsSegmentNumberType::EXPORTED; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimSegmentCollection::setCompsegsSegmentNumber( CompsegsSegmentNumberType segmentNumberType ) +{ + m_compsegsSegmentNumber = segmentNumberType; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -389,6 +460,22 @@ std::vector> RimSegmentCollection::getSegmentIntervals return result; } +//-------------------------------------------------------------------------------------------------- +/// Valid intervals with a fixed, min or max segment length rule, active on the export date if given +//-------------------------------------------------------------------------------------------------- +std::vector RimSegmentCollection::segmentationIntervals( const std::optional& exportDate ) const +{ + std::vector result; + for ( auto* interval : intervals() ) + { + if ( !interval || !interval->isValidInterval() ) continue; + if ( exportDate.has_value() && !interval->isActiveOnDate( *exportDate ) ) continue; + if ( interval->fixedSegmentLength().has_value() || interval->minSegmentLength().has_value() || interval->maxSegmentLength().has_value() ) + result.push_back( interval ); + } + return result; +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -412,12 +499,15 @@ void RimSegmentCollection::setUnitSystemSpecificDefaults() //-------------------------------------------------------------------------------------------------- void RimSegmentCollection::updateFromTopLevelWell( const RimSegmentCollection* topLevelWellParameters ) { - m_refMDType = topLevelWellParameters->m_refMDType(); - m_refMD = topLevelWellParameters->m_refMD(); - m_pressureDrop = topLevelWellParameters->m_pressureDrop(); - m_lengthAndDepth = topLevelWellParameters->m_lengthAndDepth(); - m_enforceMaxSegmentLength = topLevelWellParameters->m_enforceMaxSegmentLength(); - m_maxSegmentLength = topLevelWellParameters->m_maxSegmentLength(); + m_refMDType = topLevelWellParameters->m_refMDType(); + m_refMD = topLevelWellParameters->m_refMD(); + m_pressureDrop = topLevelWellParameters->m_pressureDrop(); + m_lengthAndDepth = topLevelWellParameters->m_lengthAndDepth(); + m_enforceMaxSegmentLength = topLevelWellParameters->m_enforceMaxSegmentLength(); + m_maxSegmentLength = topLevelWellParameters->m_maxSegmentLength(); + m_compsegsSegmentNumber = topLevelWellParameters->m_compsegsSegmentNumber(); + m_singleSegmentBeforeFirstPerforation = topLevelWellParameters->m_singleSegmentBeforeFirstPerforation(); + m_singleSegmentAfterLastPerforation = topLevelWellParameters->m_singleSegmentAfterLastPerforation(); } //-------------------------------------------------------------------------------------------------- @@ -515,7 +605,7 @@ void RimSegmentCollection::fieldChangedByUi( const caf::PdmFieldHandle* changedF //-------------------------------------------------------------------------------------------------- void RimSegmentCollection::appendMenuItems( caf::CmdFeatureMenuBuilder& menuBuilder ) const { - menuBuilder << "RicNewSegmentIntervalFeature"; + menuBuilder << "RicCreateSegmentIntervalFeature"; if ( hasIntervals() ) menuBuilder << "RicDeleteSegmentIntervalFeature"; } @@ -548,8 +638,13 @@ void RimSegmentCollection::defineUiOrdering( QString uiConfigName, caf::PdmUiOrd { uiOrdering.add( &m_pressureDrop ); uiOrdering.add( &m_lengthAndDepth ); - uiOrdering.add( &m_enforceMaxSegmentLength ); - uiOrdering.add( &m_maxSegmentLength ); + uiOrdering.add( &m_compsegsSegmentNumber ); + + auto* segmentationGroup = uiOrdering.addNewGroup( "Segmentation" ); + segmentationGroup->add( &m_enforceMaxSegmentLength ); + segmentationGroup->add( &m_maxSegmentLength ); + segmentationGroup->add( &m_singleSegmentBeforeFirstPerforation ); + segmentationGroup->add( &m_singleSegmentAfterLastPerforation ); } const bool readOnly = !wellPath->isTopLevelWellPath() && !m_customValuesForLateral(); diff --git a/ApplicationLibCode/ProjectDataModel/Completions/RimSegmentCollection.h b/ApplicationLibCode/ProjectDataModel/Completions/RimSegmentCollection.h index faec4ce403..b47d694f95 100644 --- a/ApplicationLibCode/ProjectDataModel/Completions/RimSegmentCollection.h +++ b/ApplicationLibCode/ProjectDataModel/Completions/RimSegmentCollection.h @@ -60,9 +60,16 @@ public: INC }; - using ReferenceMDEnum = caf::AppEnum; - using PressureDropEnum = caf::AppEnum; - using LengthAndDepthEnum = caf::AppEnum; + enum class CompsegsSegmentNumberType + { + ASSIGNED_BY_SIMULATOR, + EXPORTED + }; + + using ReferenceMDEnum = caf::AppEnum; + using PressureDropEnum = caf::AppEnum; + using LengthAndDepthEnum = caf::AppEnum; + using CompsegsSegmentNumberEnum = caf::AppEnum; RimSegmentCollection(); @@ -75,6 +82,9 @@ public: PressureDropEnum pressureDrop() const; LengthAndDepthEnum lengthAndDepth() const; double maxSegmentLength() const; + bool singleSegmentBeforeFirstPerforation() const; + bool singleSegmentAfterLastPerforation() const; + bool exportCompsegsSegmentNumber() const; void setReferenceMDType( ReferenceMDType refType ); void setManualReferenceMD( double manualRefMD ); @@ -82,6 +92,9 @@ public: void setRoughnessFactor( double roughnessFactor ); void setPressureDrop( PressureDropType pressureDropType ); void setLengthAndDepth( LengthAndDepthType lengthAndDepthType ); + void setCompsegsSegmentNumber( CompsegsSegmentNumberType segmentNumberType ); + void setSingleSegmentBeforeFirstPerforation( bool enable ); + void setSingleSegmentAfterLastPerforation( bool enable ); double getDiameterAtMD( double md, RiaDefines::EclipseUnitSystem unitSystem ) const; double getRoughnessAtMD( double md, RiaDefines::EclipseUnitSystem unitSystem ) const; @@ -98,6 +111,7 @@ public: void updateOverlapVisualFeedback(); std::vector> getSegmentIntervals() const; + std::vector segmentationIntervals( const std::optional& exportDate ) const; bool hasCustomSegmentIntervals() const; void setUnitSystemSpecificDefaults(); @@ -132,4 +146,8 @@ private: caf::PdmField m_lengthAndDepth; caf::PdmField m_enforceMaxSegmentLength; caf::PdmField m_maxSegmentLength; + caf::PdmField m_singleSegmentBeforeFirstPerforation; + caf::PdmField m_singleSegmentAfterLastPerforation; + + caf::PdmField m_compsegsSegmentNumber; }; diff --git a/ApplicationLibCode/ProjectDataModel/Completions/RimSegmentInterval.cpp b/ApplicationLibCode/ProjectDataModel/Completions/RimSegmentInterval.cpp index 5b20405ca4..22b82b7836 100644 --- a/ApplicationLibCode/ProjectDataModel/Completions/RimSegmentInterval.cpp +++ b/ApplicationLibCode/ProjectDataModel/Completions/RimSegmentInterval.cpp @@ -28,6 +28,7 @@ #include "cafCmdFeatureMenuBuilder.h" #include "cafPdmFieldScriptingCapability.h" #include "cafPdmObjectScriptingCapability.h" +#include "cafPdmUiCheckBoxAndTextEditor.h" #include "cafPdmUiDoubleSliderEditor.h" #include "cafPdmUiDoubleValueEditor.h" #include "cafPdmUiTreeOrdering.h" @@ -41,7 +42,7 @@ CAF_PDM_SOURCE_INIT( RimSegmentInterval, "SegmentInterval" ); //-------------------------------------------------------------------------------------------------- RimSegmentInterval::RimSegmentInterval() { - CAF_PDM_InitScriptableObject( "Segment Interval", ":/WellPathComponent16x16.png", "", "SegmentInterval" ); + CAF_PDM_InitScriptableObject( "Segment Interval", ":/Segment.svg", "", "SegmentInterval" ); CAF_PDM_InitScriptableField( &m_startMD, "StartMd", 0.0, "Start MD" ); CAF_PDM_InitScriptableField( &m_endMD, "EndMd", 0.0, "End MD" ); CAF_PDM_InitScriptableField( &m_diameter, @@ -53,6 +54,13 @@ RimSegmentInterval::RimSegmentInterval() RimSegmentCollection::defaultRoughnessFactor( RiaDefines::EclipseUnitSystem::UNITS_METRIC ), "Roughness Factor" ); + CAF_PDM_InitField( &m_fixedSegmentLength, "FixedSegmentLength", std::make_pair( false, 50.0 ), "Fixed Segment Length" ); + m_fixedSegmentLength.uiCapability()->setUiEditorTypeName( caf::PdmUiCheckBoxAndTextEditor::uiEditorTypeName() ); + CAF_PDM_InitField( &m_minSegmentLength, "MinSegmentLength", std::make_pair( false, 10.0 ), "Min Segment Length" ); + m_minSegmentLength.uiCapability()->setUiEditorTypeName( caf::PdmUiCheckBoxAndTextEditor::uiEditorTypeName() ); + CAF_PDM_InitField( &m_maxSegmentLength, "MaxSegmentLength", std::make_pair( false, 100.0 ), "Max Segment Length" ); + m_maxSegmentLength.uiCapability()->setUiEditorTypeName( caf::PdmUiCheckBoxAndTextEditor::uiEditorTypeName() ); + CAF_PDM_InitField( &m_useCustomStartDate, "UseCustomStartDate", false, "Custom Start Date" ); CAF_PDM_InitField( &m_startDate, "StartDate", QDateTime::currentDateTime(), "Start Date" ); } @@ -158,6 +166,60 @@ void RimSegmentInterval::setRoughnessFactor( double roughness ) m_roughnessFactor = roughness; } +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::optional RimSegmentInterval::fixedSegmentLength() const +{ + if ( m_fixedSegmentLength().first && m_fixedSegmentLength().second > 0.0 ) return m_fixedSegmentLength().second; + return std::nullopt; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::optional RimSegmentInterval::minSegmentLength() const +{ + if ( m_minSegmentLength().first && m_minSegmentLength().second > 0.0 ) return m_minSegmentLength().second; + return std::nullopt; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::optional RimSegmentInterval::maxSegmentLength() const +{ + if ( m_maxSegmentLength().first && m_maxSegmentLength().second > 0.0 ) return m_maxSegmentLength().second; + return std::nullopt; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimSegmentInterval::setFixedSegmentLength( std::optional length ) +{ + m_fixedSegmentLength = std::make_pair( length.has_value(), length.value_or( m_fixedSegmentLength().second ) ); + if ( length ) enforceSegmentationRules( &m_fixedSegmentLength ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimSegmentInterval::setMinSegmentLength( std::optional length ) +{ + m_minSegmentLength = std::make_pair( length.has_value(), length.value_or( m_minSegmentLength().second ) ); + if ( length ) enforceSegmentationRules( &m_minSegmentLength ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimSegmentInterval::setMaxSegmentLength( std::optional length ) +{ + m_maxSegmentLength = std::make_pair( length.has_value(), length.value_or( m_maxSegmentLength().second ) ); + if ( length ) enforceSegmentationRules( &m_maxSegmentLength ); +} + //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- @@ -307,7 +369,7 @@ void RimSegmentInterval::fieldChangedByUi( const caf::PdmFieldHandle* changedFie { if ( changedField == &m_startMD || changedField == &m_endMD ) { - uiCapability()->setUiName( QString( "%1 - %2" ).arg( m_startMD() ).arg( m_endMD() ) ); + updateUiName(); // Validate interval if ( m_startMD >= m_endMD ) @@ -323,6 +385,17 @@ void RimSegmentInterval::fieldChangedByUi( const caf::PdmFieldHandle* changedFie } } + const bool segmentationEnabled = ( changedField == &m_fixedSegmentLength && m_fixedSegmentLength().first ) || + ( changedField == &m_minSegmentLength && m_minSegmentLength().first ) || + ( changedField == &m_maxSegmentLength && m_maxSegmentLength().first ); + if ( segmentationEnabled ) enforceSegmentationRules( changedField ); + + if ( changedField == &m_fixedSegmentLength || changedField == &m_minSegmentLength || changedField == &m_maxSegmentLength ) + { + updateUiName(); + uiCapability()->updateConnectedEditors(); + } + updateConnectedEditors(); } @@ -339,12 +412,23 @@ void RimSegmentInterval::defineUiOrdering( QString uiConfigName, caf::PdmUiOrder m_endMD.uiCapability()->setUiName( isMetric ? "End MD [m]" : "End MD [ft]" ); m_diameter.uiCapability()->setUiName( isMetric ? "Diameter [m]" : "Diameter [ft]" ); m_roughnessFactor.uiCapability()->setUiName( isMetric ? "Roughness Factor [m]" : "Roughness Factor [ft]" ); + m_fixedSegmentLength.uiCapability()->setUiName( isMetric ? "Fixed Segment Length [m]" : "Fixed Segment Length [ft]" ); + m_minSegmentLength.uiCapability()->setUiName( isMetric ? "Min Segment Length [m]" : "Min Segment Length [ft]" ); + m_maxSegmentLength.uiCapability()->setUiName( isMetric ? "Max Segment Length [m]" : "Max Segment Length [ft]" ); } - uiOrdering.add( &m_startMD ); - uiOrdering.add( &m_endMD ); - uiOrdering.add( &m_diameter ); - uiOrdering.add( &m_roughnessFactor ); + auto* intervalGroup = uiOrdering.addNewGroup( "Interval" ); + intervalGroup->add( &m_startMD ); + intervalGroup->add( &m_endMD ); + + auto* diameterGroup = uiOrdering.addNewGroup( "Diameter and Roughness" ); + diameterGroup->add( &m_diameter ); + diameterGroup->add( &m_roughnessFactor ); + + auto* segmentationGroup = uiOrdering.addNewGroup( "Segmentation" ); + segmentationGroup->add( &m_fixedSegmentLength ); + segmentationGroup->add( &m_minSegmentLength ); + segmentationGroup->add( &m_maxSegmentLength ); } //-------------------------------------------------------------------------------------------------- @@ -352,7 +436,19 @@ void RimSegmentInterval::defineUiOrdering( QString uiConfigName, caf::PdmUiOrder //-------------------------------------------------------------------------------------------------- void RimSegmentInterval::defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName ) { - uiCapability()->setUiName( QString( "%1 - %2" ).arg( m_startMD() ).arg( m_endMD() ) ); + updateUiName(); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimSegmentInterval::updateUiName() +{ + QString name = QString( "%1 - %2" ).arg( m_startMD() ).arg( m_endMD() ); + if ( auto fixedLength = fixedSegmentLength() ) name += QString( " (fixed %1)" ).arg( *fixedLength ); + if ( auto minLength = minSegmentLength() ) name += QString( " (min %1)" ).arg( *minLength ); + if ( auto maxLength = maxSegmentLength() ) name += QString( " (max %1)" ).arg( *maxLength ); + uiCapability()->setUiName( name ); } //-------------------------------------------------------------------------------------------------- @@ -365,6 +461,25 @@ void RimSegmentInterval::updateConnectedEditors() m_endMD.uiCapability()->updateConnectedEditors(); m_diameter.uiCapability()->updateConnectedEditors(); m_roughnessFactor.uiCapability()->updateConnectedEditors(); + m_fixedSegmentLength.uiCapability()->updateConnectedEditors(); + m_minSegmentLength.uiCapability()->updateConnectedEditors(); + m_maxSegmentLength.uiCapability()->updateConnectedEditors(); +} + +//-------------------------------------------------------------------------------------------------- +/// Fixed length cannot be combined with min/max length. Min and max length can be combined. +//-------------------------------------------------------------------------------------------------- +void RimSegmentInterval::enforceSegmentationRules( const caf::PdmFieldHandle* activeField ) +{ + if ( activeField == &m_fixedSegmentLength ) + { + m_minSegmentLength = std::make_pair( false, m_minSegmentLength().second ); + m_maxSegmentLength = std::make_pair( false, m_maxSegmentLength().second ); + } + else + { + m_fixedSegmentLength = std::make_pair( false, m_fixedSegmentLength().second ); + } } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/ProjectDataModel/Completions/RimSegmentInterval.h b/ApplicationLibCode/ProjectDataModel/Completions/RimSegmentInterval.h index 446c8d125e..56938a505a 100644 --- a/ApplicationLibCode/ProjectDataModel/Completions/RimSegmentInterval.h +++ b/ApplicationLibCode/ProjectDataModel/Completions/RimSegmentInterval.h @@ -27,6 +27,8 @@ #include #include +#include + class RimWellPath; namespace caf @@ -62,6 +64,14 @@ public: void setDiameter( double diameter ); void setRoughnessFactor( double roughness ); + // Segmentation. Fixed length cannot be combined with min/max length, min and max length can be combined. + std::optional fixedSegmentLength() const; + std::optional minSegmentLength() const; + std::optional maxSegmentLength() const; + void setFixedSegmentLength( std::optional length ); + void setMinSegmentLength( std::optional length ); + void setMaxSegmentLength( std::optional length ); + // Date tracking void enableCustomStartDate( bool enable ); void setCustomStartDate( const QDate& date ); @@ -103,7 +113,9 @@ protected: private: void updateConnectedEditors(); + void updateUiName(); QString generateDisplayLabel() const; + void enforceSegmentationRules( const caf::PdmFieldHandle* activeField ); private: caf::PdmField m_startMD; @@ -111,6 +123,10 @@ private: caf::PdmField m_diameter; caf::PdmField m_roughnessFactor; + caf::PdmField> m_fixedSegmentLength; + caf::PdmField> m_minSegmentLength; + caf::PdmField> m_maxSegmentLength; + caf::PdmField m_useCustomStartDate; caf::PdmField m_startDate; }; \ No newline at end of file diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellPathCompletionSettings.cpp b/ApplicationLibCode/ProjectDataModelCommands/RimcWellPathCompletionSettings.cpp index 801463cb82..f40efed49b 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellPathCompletionSettings.cpp +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellPathCompletionSettings.cpp @@ -50,3 +50,104 @@ QString RimcSegmentCollection_addSegmentInterval::classKeywordReturnedType() con { return RimSegmentInterval::classKeywordStatic(); } + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimcSegmentInterval_setSegmentLengthBase::RimcSegmentInterval_setSegmentLengthBase( caf::PdmObjectHandle* self, const QString& description ) + : caf::PdmObjectMethod( self, PdmObjectMethod::NullPointerType::NULL_IS_INVALID, PdmObjectMethod::ResultType::PERSISTENT_TRUE ) +{ + CAF_PDM_InitObject( description ); + CAF_PDM_InitScriptableField( &m_length, "Length", 50.0, "Length" ); + CAF_PDM_InitScriptableField( &m_enable, "Enable", true, "Enable" ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::expected RimcSegmentInterval_setSegmentLengthBase::execute() +{ + auto* interval = self(); + if ( !interval ) return std::unexpected( "Segment interval is null." ); + if ( m_enable() && m_length() <= 0.0 ) return std::unexpected( "Segment length must be greater than zero." ); + + const std::optional length = m_enable() ? std::optional( m_length() ) : std::nullopt; + + switch ( lengthType() ) + { + case LengthType::FIXED: + interval->setFixedSegmentLength( length ); + break; + case LengthType::MIN: + interval->setMinSegmentLength( length ); + break; + case LengthType::MAX: + interval->setMaxSegmentLength( length ); + break; + } + + interval->updateAllRequiredEditors(); + return interval; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +QString RimcSegmentInterval_setSegmentLengthBase::classKeywordReturnedType() const +{ + return RimSegmentInterval::classKeywordStatic(); +} + +CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimSegmentInterval, RimcSegmentInterval_setFixedSegmentLength, "SetFixedSegmentLength" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimcSegmentInterval_setFixedSegmentLength::RimcSegmentInterval_setFixedSegmentLength( caf::PdmObjectHandle* self ) + : RimcSegmentInterval_setSegmentLengthBase( self, "Set Fixed Segment Length" ) +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimcSegmentInterval_setSegmentLengthBase::LengthType RimcSegmentInterval_setFixedSegmentLength::lengthType() const +{ + return LengthType::FIXED; +} + +CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimSegmentInterval, RimcSegmentInterval_setMinSegmentLength, "SetMinSegmentLength" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimcSegmentInterval_setMinSegmentLength::RimcSegmentInterval_setMinSegmentLength( caf::PdmObjectHandle* self ) + : RimcSegmentInterval_setSegmentLengthBase( self, "Set Min Segment Length" ) +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimcSegmentInterval_setSegmentLengthBase::LengthType RimcSegmentInterval_setMinSegmentLength::lengthType() const +{ + return LengthType::MIN; +} + +CAF_PDM_OBJECT_METHOD_SOURCE_INIT( RimSegmentInterval, RimcSegmentInterval_setMaxSegmentLength, "SetMaxSegmentLength" ); + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimcSegmentInterval_setMaxSegmentLength::RimcSegmentInterval_setMaxSegmentLength( caf::PdmObjectHandle* self ) + : RimcSegmentInterval_setSegmentLengthBase( self, "Set Max Segment Length" ) +{ +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +RimcSegmentInterval_setSegmentLengthBase::LengthType RimcSegmentInterval_setMaxSegmentLength::lengthType() const +{ + return LengthType::MAX; +} \ No newline at end of file diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellPathCompletionSettings.h b/ApplicationLibCode/ProjectDataModelCommands/RimcWellPathCompletionSettings.h index 37e47088c2..a7eb66d710 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellPathCompletionSettings.h +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellPathCompletionSettings.h @@ -39,3 +39,71 @@ private: caf::PdmField m_diameter; caf::PdmField m_roughnessFactor; }; + +//================================================================================================== +/// +//================================================================================================== +class RimcSegmentInterval_setSegmentLengthBase : public caf::PdmObjectMethod +{ +public: + RimcSegmentInterval_setSegmentLengthBase( caf::PdmObjectHandle* self, const QString& description ); + + std::expected execute() override; + QString classKeywordReturnedType() const override; + +protected: + enum class LengthType + { + FIXED, + MIN, + MAX + }; + + virtual LengthType lengthType() const = 0; + +private: + caf::PdmField m_length; + caf::PdmField m_enable; +}; + +//================================================================================================== +/// +//================================================================================================== +class RimcSegmentInterval_setFixedSegmentLength : public RimcSegmentInterval_setSegmentLengthBase +{ + CAF_PDM_HEADER_INIT; + +public: + RimcSegmentInterval_setFixedSegmentLength( caf::PdmObjectHandle* self ); + +protected: + LengthType lengthType() const override; +}; + +//================================================================================================== +/// +//================================================================================================== +class RimcSegmentInterval_setMinSegmentLength : public RimcSegmentInterval_setSegmentLengthBase +{ + CAF_PDM_HEADER_INIT; + +public: + RimcSegmentInterval_setMinSegmentLength( caf::PdmObjectHandle* self ); + +protected: + LengthType lengthType() const override; +}; + +//================================================================================================== +/// +//================================================================================================== +class RimcSegmentInterval_setMaxSegmentLength : public RimcSegmentInterval_setSegmentLengthBase +{ + CAF_PDM_HEADER_INIT; + +public: + RimcSegmentInterval_setMaxSegmentLength( caf::PdmObjectHandle* self ); + +protected: + LengthType lengthType() const override; +}; \ No newline at end of file diff --git a/ApplicationLibCode/UnitTests/RicMswBranchBuilder-Test.cpp b/ApplicationLibCode/UnitTests/RicMswBranchBuilder-Test.cpp index 61f00d4268..ace18a6015 100644 --- a/ApplicationLibCode/UnitTests/RicMswBranchBuilder-Test.cpp +++ b/ApplicationLibCode/UnitTests/RicMswBranchBuilder-Test.cpp @@ -44,7 +44,7 @@ TEST( RicMswBranchBuilder, FindOutlet_EmptyMap ) //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_MDInFirstCell ) { - std::vector map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 }, { 300.0, 400.0, 7 } }; + std::vector map = { { 100.0, 200.0, 5, 150.0 }, { 200.0, 300.0, 6, 250.0 }, { 300.0, 400.0, 7, 350.0 } }; EXPECT_EQ( 5, findOutletSegmentForMD( map, 150.0 ) ); } @@ -53,7 +53,7 @@ TEST( RicMswBranchBuilder, FindOutlet_MDInFirstCell ) //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_MDInMiddleCell ) { - std::vector map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 }, { 300.0, 400.0, 7 } }; + std::vector map = { { 100.0, 200.0, 5, 150.0 }, { 200.0, 300.0, 6, 250.0 }, { 300.0, 400.0, 7, 350.0 } }; EXPECT_EQ( 6, findOutletSegmentForMD( map, 250.0 ) ); } @@ -62,7 +62,7 @@ TEST( RicMswBranchBuilder, FindOutlet_MDInMiddleCell ) //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_MDInLastCell ) { - std::vector map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 }, { 300.0, 400.0, 7 } }; + std::vector map = { { 100.0, 200.0, 5, 150.0 }, { 200.0, 300.0, 6, 250.0 }, { 300.0, 400.0, 7, 350.0 } }; EXPECT_EQ( 7, findOutletSegmentForMD( map, 350.0 ) ); } @@ -74,7 +74,7 @@ TEST( RicMswBranchBuilder, FindOutlet_MDInLastCell ) //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_MDAtCellStart ) { - std::vector map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 } }; + std::vector map = { { 100.0, 200.0, 5, 150.0 }, { 200.0, 300.0, 6, 250.0 } }; EXPECT_EQ( 5, findOutletSegmentForMD( map, 100.0 ) ); // shallower than midpoint 150 → first seg EXPECT_EQ( 5, findOutletSegmentForMD( map, 200.0 ) ); // midpoint 150 ≤ 200 < midpoint 250 → seg 5 } @@ -85,7 +85,7 @@ TEST( RicMswBranchBuilder, FindOutlet_MDAtCellStart ) //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_MDAtCellEnd_ExclusiveBoundary ) { - std::vector map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 } }; + std::vector map = { { 100.0, 200.0, 5, 150.0 }, { 200.0, 300.0, 6, 250.0 } }; EXPECT_EQ( 5, findOutletSegmentForMD( map, 200.0 ) ); } @@ -95,7 +95,7 @@ TEST( RicMswBranchBuilder, FindOutlet_MDAtCellEnd_ExclusiveBoundary ) //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_MDBelowAllCells ) { - std::vector map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 } }; + std::vector map = { { 100.0, 200.0, 5, 150.0 }, { 200.0, 300.0, 6, 250.0 } }; EXPECT_EQ( 5, findOutletSegmentForMD( map, 50.0 ) ); // shallower than all midpoints → first seg } @@ -104,7 +104,7 @@ TEST( RicMswBranchBuilder, FindOutlet_MDBelowAllCells ) //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_MDBeyondAllCells ) { - std::vector map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 }, { 300.0, 400.0, 7 } }; + std::vector map = { { 100.0, 200.0, 5, 150.0 }, { 200.0, 300.0, 6, 250.0 }, { 300.0, 400.0, 7, 350.0 } }; EXPECT_EQ( 7, findOutletSegmentForMD( map, 999.0 ) ); } @@ -113,12 +113,224 @@ TEST( RicMswBranchBuilder, FindOutlet_MDBeyondAllCells ) //-------------------------------------------------------------------------------------------------- TEST( RicMswBranchBuilder, FindOutlet_SingleCell ) { - std::vector map = { { 0.0, 100.0, 3 } }; + std::vector map = { { 0.0, 100.0, 3, 50.0 } }; EXPECT_EQ( 3, findOutletSegmentForMD( map, 50.0 ) ); EXPECT_EQ( 3, findOutletSegmentForMD( map, 0.0 ) ); EXPECT_EQ( 3, findOutletSegmentForMD( map, 200.0 ) ); // beyond → fallback = 3 } +//================================================================================================== +// Segmentation helpers +//================================================================================================== + +namespace +{ +//-------------------------------------------------------------------------------------------------- +/// Cells [0,10] [10,14] [14,30] [30,33] [33,60] from the heel at MD 0, centres 5, 12, 22, 31.5, 46.5 +//-------------------------------------------------------------------------------------------------- +const std::vector> exampleCells = { { 0.0, 10.0 }, { 10.0, 14.0 }, { 14.0, 30.0 }, { 30.0, 33.0 }, { 33.0, 60.0 } }; + +std::vector centres( const std::vector>& pieces ) +{ + std::vector result; + for ( const auto& [start, end] : pieces ) + { + result.push_back( 0.5 * ( start + end ) ); + } + return result; +} + +void expectNodes( const std::vector& expected, const std::vector& actual ) +{ + ASSERT_EQ( expected.size(), actual.size() ); + for ( size_t i = 0; i < expected.size(); ++i ) + { + EXPECT_NEAR( expected[i], actual[i], 1.0e-9 ) << "node " << i; + } +} +} // namespace + +//================================================================================================== +// Segment node placement tests +//================================================================================================== + +using RicMswTableDataTools::SegmentationInterval; + +TEST( RicMswBranchBuilder, PlaceSegmentNodes_NoRuleKeepsCellCentres ) +{ + const auto candidates = centres( exampleCells ); + expectNodes( candidates, RicMswTableDataTools::placeSegmentNodes( 0.0, candidates, {} ) ); + expectNodes( candidates, RicMswTableDataTools::placeSegmentNodes( 0.0, candidates, { { 0.0, 60.0, std::nullopt, std::nullopt } } ) ); +} + +TEST( RicMswBranchBuilder, PlaceSegmentNodes_MinLength ) +{ + const auto nodes = RicMswTableDataTools::placeSegmentNodes( 0.0, centres( exampleCells ), { { 0.0, 60.0, 12.0, std::nullopt } } ); + expectNodes( { 12.0, 31.5, 46.5 }, nodes ); +} + +TEST( RicMswBranchBuilder, PlaceSegmentNodes_MaxLength ) +{ + const auto nodes = RicMswTableDataTools::placeSegmentNodes( 0.0, centres( exampleCells ), { { 0.0, 60.0, std::nullopt, 10.0 } } ); + expectNodes( { 5.0, 12.0, 22.0, 31.5, 39.0, 46.5 }, nodes ); +} + +TEST( RicMswBranchBuilder, PlaceSegmentNodes_MaxLengthInsertsSeveralNodes ) +{ + const auto nodes = RicMswTableDataTools::placeSegmentNodes( 0.0, { 5.0, 35.0 }, { { 0.0, 60.0, std::nullopt, 10.0 } } ); + expectNodes( { 5.0, 15.0, 25.0, 35.0 }, nodes ); +} + +TEST( RicMswBranchBuilder, PlaceSegmentNodes_MinLengthLargerThanIntervalKeepsOneNode ) +{ + const auto nodes = RicMswTableDataTools::placeSegmentNodes( 0.0, centres( exampleCells ), { { 0.0, 60.0, 100.0, std::nullopt } } ); + expectNodes( { 46.5 }, nodes ); +} + +TEST( RicMswBranchBuilder, PlaceSegmentNodes_IntervalStartingMidCell ) +{ + // The cell [14,30] is split at the interval start 20 + const std::vector> pieces = + { { 0.0, 10.0 }, { 10.0, 14.0 }, { 14.0, 20.0 }, { 20.0, 30.0 }, { 30.0, 33.0 }, { 33.0, 60.0 } }; + + const auto nodes = RicMswTableDataTools::placeSegmentNodes( 0.0, centres( pieces ), { { 20.0, 60.0, 12.0, std::nullopt } } ); + + // The spacing inside the interval is measured from the last node before it, 17 + expectNodes( { 5.0, 12.0, 17.0, 31.5, 46.5 }, nodes ); +} + +TEST( RicMswBranchBuilder, PlaceSegmentNodes_BackToBackIntervals ) +{ + const std::vector intervals = { { 0.0, 14.0, 12.0, std::nullopt }, { 14.0, 60.0, std::nullopt, 10.0 } }; + + const auto nodes = RicMswTableDataTools::placeSegmentNodes( 0.0, centres( exampleCells ), intervals ); + expectNodes( { 12.0, 22.0, 31.5, 39.0, 46.5 }, nodes ); +} + +TEST( RicMswBranchBuilder, PlaceSegmentNodes_MeasuredFromOutletNode ) +{ + // The outlet node at 100 is the heel of the branch, the first candidate is too close to it + const auto nodes = RicMswTableDataTools::placeSegmentNodes( 100.0, { 105.0, 115.0, 125.0 }, { { 100.0, 130.0, 12.0, std::nullopt } } ); + expectNodes( { 115.0 }, nodes ); +} + +TEST( RicMswBranchBuilder, PlaceSegmentNodes_MinAndMaxLengthCombined ) +{ + const auto nodes = RicMswTableDataTools::placeSegmentNodes( 0.0, centres( exampleCells ), { { 0.0, 60.0, 12.0, 15.0 } } ); + expectNodes( { 12.0, 21.75, 31.5, 46.5 }, nodes ); +} + +TEST( RicMswBranchBuilder, PlaceSegmentNodes_FixedLength ) +{ + // Segments [0,20], [20,40], [40,60] with nodes at their centres + expectNodes( { 10.0, 30.0, 50.0 }, + RicMswTableDataTools::placeSegmentNodes( 0.0, centres( exampleCells ), { { 0.0, 60.0, std::nullopt, std::nullopt, 20.0 } } ) ); + + // Cell centres outside the interval are kept, the fixed segments start at the interval start + expectNodes( { 5.0, 12.0, 30.0, 50.0 }, + RicMswTableDataTools::placeSegmentNodes( 0.0, centres( exampleCells ), { { 20.0, 60.0, std::nullopt, std::nullopt, 20.0 } } ) ); + + // The last segment is cut at the end of the branch + expectNodes( { 10.0, 30.0, 42.5 }, + RicMswTableDataTools::placeSegmentNodes( 0.0, centres( exampleCells ), { { 0.0, 50.0, std::nullopt, std::nullopt, 20.0 } }, 45.0 ) ); +} + +TEST( RicMswBranchBuilder, CollapseNodesOutsidePerforations ) +{ + const std::vector nodes = { 5.0, 12.0, 22.0, 31.5, 46.5, 55.0 }; + + expectNodes( { 10.0, 22.0, 31.5, 46.5, 55.0 }, + RicMswTableDataTools::collapseNodesOutsidePerforations( nodes, 0.0, 60.0, 20.0, 40.0, true, false ) ); + expectNodes( { 5.0, 12.0, 22.0, 31.5, 50.0 }, + RicMswTableDataTools::collapseNodesOutsidePerforations( nodes, 0.0, 60.0, 20.0, 40.0, false, true ) ); + expectNodes( nodes, RicMswTableDataTools::collapseNodesOutsidePerforations( nodes, 0.0, 60.0, 20.0, 40.0, false, false ) ); + + // A single node outside the perforations is unchanged + expectNodes( { 5.0, 22.0, 31.5 }, + RicMswTableDataTools::collapseNodesOutsidePerforations( { 5.0, 22.0, 31.5 }, 0.0, 60.0, 20.0, 40.0, true, true ) ); +} + +//================================================================================================== +// Upstream node tests +//================================================================================================== + +TEST( RicMswBranchBuilder, UpstreamNodeIndex ) +{ + const std::vector nodes = { 12.0, 31.5, 46.5 }; + + EXPECT_FALSE( RicMswTableDataTools::upstreamNodeIndex( {}, 10.0 ).has_value() ); + + // Upstream of all nodes, the most upstream node is used + EXPECT_EQ( std::optional( 0 ), RicMswTableDataTools::upstreamNodeIndex( nodes, 5.0 ) ); + + // Cell 3 in the example, centre 22, is closer to 31.5 but 12 is the nearest node upstream + EXPECT_EQ( std::optional( 0 ), RicMswTableDataTools::upstreamNodeIndex( nodes, 22.0 ) ); + EXPECT_EQ( std::optional( 1 ), RicMswTableDataTools::upstreamNodeIndex( nodes, 46.0 ) ); + EXPECT_EQ( std::optional( 2 ), RicMswTableDataTools::upstreamNodeIndex( nodes, 100.0 ) ); +} + +TEST( RicMswBranchBuilder, UpstreamNodeIndex_NodeAtMDIsUsed ) +{ + EXPECT_EQ( std::optional( 1 ), RicMswTableDataTools::upstreamNodeIndex( { 10.0, 20.0 }, 20.0 ) ); + EXPECT_EQ( std::optional( 0 ), RicMswTableDataTools::upstreamNodeIndex( { 10.0, 20.0 }, 19.999 ) ); +} + +//================================================================================================== +// Nearest node tests +//================================================================================================== + +TEST( RicMswBranchBuilder, NearestNodeIndex ) +{ + const std::vector nodes = { 12.0, 31.5, 46.5 }; + + EXPECT_FALSE( RicMswTableDataTools::nearestNodeIndex( {}, 10.0 ).has_value() ); + EXPECT_EQ( std::optional( 0 ), RicMswTableDataTools::nearestNodeIndex( nodes, 5.0 ) ); + + // Cell 3 in the example, centre 22, is 9.5 from 31.5 and 10 from 12 + EXPECT_EQ( std::optional( 1 ), RicMswTableDataTools::nearestNodeIndex( nodes, 22.0 ) ); + EXPECT_EQ( std::optional( 2 ), RicMswTableDataTools::nearestNodeIndex( nodes, 100.0 ) ); +} + +TEST( RicMswBranchBuilder, NearestNodeIndex_TieGoesToFirstNode ) +{ + EXPECT_EQ( std::optional( 0 ), RicMswTableDataTools::nearestNodeIndex( { 10.0, 20.0 }, 15.0 ) ); +} + +//================================================================================================== +// Segment span tests +//================================================================================================== + +namespace +{ +void expectSpans( const std::vector& expected, const std::vector& actual ) +{ + ASSERT_EQ( expected.size(), actual.size() ); + for ( size_t i = 0; i < expected.size(); ++i ) + { + EXPECT_NEAR( expected[i].startMD, actual[i].startMD, 1.0e-9 ) << "span " << i; + EXPECT_NEAR( expected[i].endMD, actual[i].endMD, 1.0e-9 ) << "span " << i; + EXPECT_EQ( expected[i].nodeIndex, actual[i].nodeIndex ) << "span " << i; + } +} +} // namespace + +TEST( RicMswBranchBuilder, SegmentSpans_NoRuleOneSpanPerCell ) +{ + expectSpans( { { 0.0, 10.0, 0 }, { 10.0, 14.0, 1 }, { 14.0, 30.0, 2 }, { 30.0, 33.0, 3 }, { 33.0, 60.0, 4 } }, + segmentSpans( exampleCells, centres( exampleCells ) ) ); +} + +TEST( RicMswBranchBuilder, SegmentSpans_MinLengthSpansSeveralCells ) +{ + expectSpans( { { 0.0, 31.5, 0 }, { 31.5, 46.5, 1 }, { 46.5, 60.0, 2 } }, segmentSpans( exampleCells, { 12.0, 31.5, 46.5 } ) ); +} + +TEST( RicMswBranchBuilder, SegmentSpans_MaxLengthSplitsCellsAtNodes ) +{ + expectSpans( { { 0.0, 12.0, 0 }, { 12.0, 22.0, 1 }, { 22.0, 31.5, 2 }, { 31.5, 39.0, 3 }, { 39.0, 46.5, 4 }, { 46.5, 60.0, 5 } }, + segmentSpans( exampleCells, { 5.0, 12.0, 22.0, 31.5, 39.0, 46.5 } ) ); +} + //================================================================================================== // toMswCellIntersection tests //================================================================================================== diff --git a/ApplicationLibCode/UnitTests/RicWellPathExportMswGeometryPath-Test.cpp b/ApplicationLibCode/UnitTests/RicWellPathExportMswGeometryPath-Test.cpp index f64394cbec..4a6fc94cd9 100644 --- a/ApplicationLibCode/UnitTests/RicWellPathExportMswGeometryPath-Test.cpp +++ b/ApplicationLibCode/UnitTests/RicWellPathExportMswGeometryPath-Test.cpp @@ -21,23 +21,37 @@ #include "CompletionExportCommands/MswExport/RicWellPathExportMswGeometryPath.h" #include "CompletionExportCommands/RicWellPathExportMswTableData.h" +#include "CompletionsMsw/RigMswDataFormatter.h" #include "CompletionsMsw/RigMswSegment.h" #include "CompletionsMsw/RigMswTableData.h" #include "CompletionsMsw/RigMswTableRows.h" +#include "RifTextDataTableFormatter.h" + #include "RiaApplication.h" #include "RiaDefines.h" #include "RiaTestDataDirectory.h" #include "RimEclipseCase.h" +#include "RimPerforationCollection.h" +#include "RimPerforationInterval.h" #include "RimProject.h" +#include "RimSegmentCollection.h" +#include "RimSegmentInterval.h" #include "RimWellPath.h" +#include "RimWellPathTieIn.h" #include #include +#include +#include #include +#include +#include #include +#include +#include namespace { @@ -216,6 +230,116 @@ TEST( RicWellPathExportMswGeometryPath, MainGridAndLgrIntersections ) EXPECT_EQ( 6u, lgr.k ); } +namespace +{ +//-------------------------------------------------------------------------------------------------- +/// The data rows of a formatted COMPSEGS (or COMPSEGL) table, split into items +//-------------------------------------------------------------------------------------------------- +std::vector formattedCompsegsRows( const RigMswTableData& tableData, bool isLgrData ) +{ + QString text; + QTextStream stream( &text ); + RifTextDataTableFormatter formatter( stream ); + RigMswDataFormatter::formatCompsegsTable( formatter, tableData, isLgrData ); + + std::vector rows; + bool isWellNameRow = true; + for ( const QString& line : text.split( '\n' ) ) + { + QStringList items = line.simplified().split( ' ', Qt::SkipEmptyParts ); + if ( items.isEmpty() || items.front().startsWith( "--" ) || items.front() == "COMPSEGS" || items.front() == "COMPSEGL" || + items.front() == "/" ) + continue; + + if ( isWellNameRow ) + { + isWellNameRow = false; + continue; + } + if ( items.back() == "/" ) items.removeLast(); + rows.push_back( items ); + } + return rows; +} + +//-------------------------------------------------------------------------------------------------- +/// Two segments, each intersecting one main grid cell, and one LGR cell on the second segment +//-------------------------------------------------------------------------------------------------- +RigMswWellExportData exportDataWithTwoConnectedSegments() +{ + RigMswWellExportData exportData; + exportData.header = makeHeader(); + + RigMswSegment first = makeSegment( 2, 1 ); + first.intersections = { RigMswCellIntersection{ 3, 5, 7, 100.0, 110.0, "" } }; + RigMswSegment second = makeSegment( 3, 2 ); + second.intersections = { RigMswCellIntersection{ 3, 5, 8, 110.0, 120.0, "" }, RigMswCellIntersection{ 1, 2, 3, 120.0, 125.0, "LGR_1" } }; + + exportData.branches = { makeBranch( 1, { first, second } ) }; + return exportData; +} +} // anonymous namespace + +//-------------------------------------------------------------------------------------------------- +/// By default the segment number is left to the simulator, and COMPSEGS ends at the end length +//-------------------------------------------------------------------------------------------------- +TEST( RicWellPathExportMswGeometryPath, CompsegsSegmentNumber_NotExportedByDefault ) +{ + auto result = RicWellPathExportMswGeometryPath::collectTableData( exportDataWithTwoConnectedSegments(), + RiaDefines::EclipseUnitSystem::UNITS_METRIC ); + + ASSERT_EQ( 3u, result.compsegsData().size() ); + for ( const auto& row : result.compsegsData() ) + { + EXPECT_FALSE( row.segmentNumber.has_value() ); + } + + const auto rows = formattedCompsegsRows( result, false ); + ASSERT_EQ( 2u, rows.size() ); + for ( const auto& row : rows ) + { + EXPECT_EQ( 6, row.size() ) << row.join( ' ' ).toStdString(); + } +} + +//-------------------------------------------------------------------------------------------------- +/// With exported segment numbers, ISEG is written after four defaulted items +//-------------------------------------------------------------------------------------------------- +TEST( RicWellPathExportMswGeometryPath, CompsegsSegmentNumber_Exported ) +{ + auto result = RicWellPathExportMswGeometryPath::collectTableData( exportDataWithTwoConnectedSegments(), + RiaDefines::EclipseUnitSystem::UNITS_METRIC, + true ); + + ASSERT_EQ( 3u, result.compsegsData().size() ); + EXPECT_EQ( std::optional( 2 ), result.compsegsData()[0].segmentNumber ); + EXPECT_EQ( std::optional( 3 ), result.compsegsData()[1].segmentNumber ); + EXPECT_EQ( std::optional( 3 ), result.compsegsData()[2].segmentNumber ); + + const auto rows = formattedCompsegsRows( result, false ); + ASSERT_EQ( 2u, rows.size() ); + const std::vector expectedSegments = { "2", "3" }; + for ( size_t r = 0; r < rows.size(); ++r ) + { + const auto& row = rows[r]; + ASSERT_EQ( 11, row.size() ) << row.join( ' ' ).toStdString(); + EXPECT_EQ( "3", row[0] ); + EXPECT_EQ( "5", row[1] ); + EXPECT_EQ( "1", row[3] ); + for ( int item = 6; item < 10; ++item ) + { + EXPECT_EQ( "1*", row[item] ); + } + EXPECT_EQ( expectedSegments[r], row[10] ); + } + + const auto lgrRows = formattedCompsegsRows( result, true ); + ASSERT_EQ( 1u, lgrRows.size() ); + ASSERT_EQ( 12, lgrRows[0].size() ) << lgrRows[0].join( ' ' ).toStdString(); + EXPECT_EQ( "LGR_1", lgrRows[0][0] ); + EXPECT_EQ( "3", lgrRows[0][11] ); +} + //-------------------------------------------------------------------------------------------------- /// Segment with wsegvalvData — 1 WSEGVALV row added; no WSEGAICD or WSEGSICD rows. //-------------------------------------------------------------------------------------------------- @@ -650,3 +774,396 @@ TEST( RicWellPathExportMswGeometryPath, MultipleLaterals_CompsegsOrderedByBranch // listed first in WELSEGS. EXPECT_LT( branchNumbers.front(), branchNumbers.back() ); } + +//================================================================================================== +// Segmentation rules on segment intervals, using lateral Y2 of the multiple_laterals test project. +//================================================================================================== + +namespace +{ + +//-------------------------------------------------------------------------------------------------- +/// The segment nodes and COMPSEGS rows of one well path branch in the export +//-------------------------------------------------------------------------------------------------- +struct BranchExport +{ + std::vector nodeMDs; + std::vector> intersectionsPerSegment; +}; + +//-------------------------------------------------------------------------------------------------- +/// Export Well-A Y1 and pick the branch of the given lateral. The node MD is written directly for +/// ABS, and relative to the outlet node for INC. +//-------------------------------------------------------------------------------------------------- +std::optional exportLateralBranch( const MswExportInput& input, const RimWellPath* lateral ) +{ + auto tableData = RicWellPathExportMswTableData::extractSingleWellMswData( input.eclipseCase, input.wellPath ); + if ( !tableData ) return std::nullopt; + + const bool isIncremental = tableData->welsegsHeader().infoType == "INC"; + const double tieInMD = lateral->wellPathTieIn()->tieInMeasuredDepth(); + const auto description = QString( "Segments on lateral %1" ).arg( lateral->name() ).toStdString(); + + for ( const auto& branch : tableData->mswBranches() ) + { + if ( branch.segments.empty() || branch.segments.front().description != description ) continue; + + BranchExport result; + double outletMD = tieInMD; + for ( const auto& segment : branch.segments ) + { + const double nodeMD = isIncremental ? outletMD + segment.length : segment.length; + result.nodeMDs.push_back( nodeMD ); + result.intersectionsPerSegment.push_back( segment.intersections ); + outletMD = nodeMD; + } + return result; + } + return std::nullopt; +} + +//-------------------------------------------------------------------------------------------------- +/// All COMPSEGS rows of the branch as (I, J, K, start, end), in export order +//-------------------------------------------------------------------------------------------------- +std::vector> compsegsRows( const BranchExport& branch ) +{ + std::vector> rows; + for ( const auto& intersections : branch.intersectionsPerSegment ) + { + for ( const auto& ci : intersections ) + { + rows.emplace_back( ci.i, ci.j, ci.k, ci.distanceStart, ci.distanceEnd ); + } + } + return rows; +} + +//-------------------------------------------------------------------------------------------------- +/// Every COMPSEGS row sits on the segment whose node is nearest the row centre, as in the simulator +//-------------------------------------------------------------------------------------------------- +void expectRowsOnNearestNode( const BranchExport& branch ) +{ + for ( size_t segmentIndex = 0; segmentIndex < branch.intersectionsPerSegment.size(); ++segmentIndex ) + { + for ( const auto& ci : branch.intersectionsPerSegment[segmentIndex] ) + { + const double centre = 0.5 * ( ci.distanceStart + ci.distanceEnd ); + const double distance = std::abs( centre - branch.nodeMDs[segmentIndex] ); + for ( size_t other = 0; other < branch.nodeMDs.size(); ++other ) + { + if ( other < segmentIndex ) + EXPECT_LT( distance, std::abs( centre - branch.nodeMDs[other] ) ); + else + EXPECT_LE( distance, std::abs( centre - branch.nodeMDs[other] ) ); + } + } + } +} + +//-------------------------------------------------------------------------------------------------- +/// Add a segment interval covering the whole lateral, with the default diameter and roughness +//-------------------------------------------------------------------------------------------------- +RimSegmentInterval* addSegmentIntervalCoveringWell( RimWellPath* wellPath ) +{ + auto* segments = wellPath->segmentCollection(); + return segments->createInterval( 0.0, 1.0e5, segments->linerDiameter(), segments->roughnessFactor() ); +} + +} // anonymous namespace + +//-------------------------------------------------------------------------------------------------- +/// With a min segment length, segments span several cells. The node spacing is at least the min +/// length, and every perforated cell is still connected exactly once, to its nearest node. +//-------------------------------------------------------------------------------------------------- +TEST( RicWellPathExportMswGeometryPath, SegmentInterval_MinSegmentLengthMergesCells ) +{ + ProjectCloser projectCloser; + + auto input = loadMultipleLateralsProject( "Well-A Y1" ); + ASSERT_TRUE( input.eclipseCase != nullptr ); + ASSERT_TRUE( input.wellPath != nullptr ); + + RimWellPath* lateral = nullptr; + for ( auto* wellPath : RimProject::current()->allWellPaths() ) + { + if ( wellPath->name() == "Well-A Y2" ) lateral = wellPath; + } + ASSERT_TRUE( lateral != nullptr ); + + auto* interval = addSegmentIntervalCoveringWell( lateral ); + + const auto baseline = exportLateralBranch( input, lateral ); + ASSERT_TRUE( baseline.has_value() ); + ASSERT_GT( baseline->nodeMDs.size(), 3u ); + ASSERT_FALSE( compsegsRows( *baseline ).empty() ); + + const double tieInMD = lateral->wellPathTieIn()->tieInMeasuredDepth(); + const double branchLength = baseline->nodeMDs.back() - tieInMD; + const double minLength = 3.0 * branchLength / baseline->nodeMDs.size(); + + interval->setMinSegmentLength( minLength ); + const auto merged = exportLateralBranch( input, lateral ); + ASSERT_TRUE( merged.has_value() ); + + EXPECT_LT( merged->nodeMDs.size(), baseline->nodeMDs.size() ); + + double previousMD = tieInMD; + for ( double nodeMD : merged->nodeMDs ) + { + EXPECT_GE( nodeMD - previousMD, minLength - 1.0e-6 ); + previousMD = nodeMD; + } + + // Nodes are kept at cell centres + for ( double nodeMD : merged->nodeMDs ) + { + EXPECT_TRUE( std::ranges::any_of( baseline->nodeMDs, [nodeMD]( double md ) { return std::abs( md - nodeMD ) < 1.0e-6; } ) ); + } + + EXPECT_EQ( compsegsRows( *baseline ), compsegsRows( *merged ) ); + + const size_t segmentsWithSeveralCells = + std::ranges::count_if( merged->intersectionsPerSegment, []( const auto& intersections ) { return intersections.size() > 1; } ); + EXPECT_GT( segmentsWithSeveralCells, 0u ); + + expectRowsOnNearestNode( *merged ); +} + +//-------------------------------------------------------------------------------------------------- +/// With a max segment length, nodes are inserted until the node spacing is at most the max length. +/// The COMPSEGS rows are unchanged, and each sits on its nearest node. +//-------------------------------------------------------------------------------------------------- +TEST( RicWellPathExportMswGeometryPath, SegmentInterval_MaxSegmentLengthInsertsNodes ) +{ + ProjectCloser projectCloser; + + auto input = loadMultipleLateralsProject( "Well-A Y1" ); + ASSERT_TRUE( input.eclipseCase != nullptr ); + ASSERT_TRUE( input.wellPath != nullptr ); + + RimWellPath* lateral = nullptr; + for ( auto* wellPath : RimProject::current()->allWellPaths() ) + { + if ( wellPath->name() == "Well-A Y2" ) lateral = wellPath; + } + ASSERT_TRUE( lateral != nullptr ); + + auto* interval = addSegmentIntervalCoveringWell( lateral ); + + const auto baseline = exportLateralBranch( input, lateral ); + ASSERT_TRUE( baseline.has_value() ); + + const double tieInMD = lateral->wellPathTieIn()->tieInMeasuredDepth(); + double maxSpacing = 0.0; + double previousMD = tieInMD; + for ( double nodeMD : baseline->nodeMDs ) + { + maxSpacing = std::max( maxSpacing, nodeMD - previousMD ); + previousMD = nodeMD; + } + const double maxLength = 0.4 * maxSpacing; + + interval->setMaxSegmentLength( maxLength ); + const auto refined = exportLateralBranch( input, lateral ); + ASSERT_TRUE( refined.has_value() ); + + EXPECT_GT( refined->nodeMDs.size(), baseline->nodeMDs.size() ); + + previousMD = tieInMD; + for ( double nodeMD : refined->nodeMDs ) + { + EXPECT_LE( nodeMD - previousMD, maxLength + 1.0e-6 ); + EXPECT_GT( nodeMD - previousMD, 0.0 ); + previousMD = nodeMD; + } + + // All cell centre nodes are kept + for ( double nodeMD : baseline->nodeMDs ) + { + EXPECT_TRUE( std::ranges::any_of( refined->nodeMDs, [nodeMD]( double md ) { return std::abs( md - nodeMD ) < 1.0e-6; } ) ); + } + + EXPECT_EQ( compsegsRows( *baseline ), compsegsRows( *refined ) ); + expectRowsOnNearestNode( *refined ); +} + +//-------------------------------------------------------------------------------------------------- +/// A segment interval without a segmentation rule leaves the export unchanged +//-------------------------------------------------------------------------------------------------- +TEST( RicWellPathExportMswGeometryPath, SegmentInterval_NoRuleGivesUnchangedExport ) +{ + ProjectCloser projectCloser; + + auto input = loadMultipleLateralsProject( "Well-A Y1" ); + ASSERT_TRUE( input.eclipseCase != nullptr ); + ASSERT_TRUE( input.wellPath != nullptr ); + + RimWellPath* lateral = nullptr; + for ( auto* wellPath : RimProject::current()->allWellPaths() ) + { + if ( wellPath->name() == "Well-A Y2" ) lateral = wellPath; + } + ASSERT_TRUE( lateral != nullptr ); + + auto* interval = addSegmentIntervalCoveringWell( lateral ); + const auto before = exportLateralBranch( input, lateral ); + ASSERT_TRUE( before.has_value() ); + + interval->setMinSegmentLength( 50.0 ); + interval->setMinSegmentLength( std::nullopt ); + const auto after = exportLateralBranch( input, lateral ); + ASSERT_TRUE( after.has_value() ); + + EXPECT_EQ( before->nodeMDs, after->nodeMDs ); + EXPECT_EQ( compsegsRows( *before ), compsegsRows( *after ) ); +} + +namespace +{ +RimWellPath* findLateralY2() +{ + for ( auto* wellPath : RimProject::current()->allWellPaths() ) + { + if ( wellPath->name() == "Well-A Y2" ) return wellPath; + } + return nullptr; +} +} // namespace + +//-------------------------------------------------------------------------------------------------- +/// A fixed segment length gives equally long segments from the interval start, the last one is cut +/// at the end of the branch. Every perforated cell is still connected exactly once. +//-------------------------------------------------------------------------------------------------- +TEST( RicWellPathExportMswGeometryPath, SegmentInterval_FixedSegmentLength ) +{ + ProjectCloser projectCloser; + + auto input = loadMultipleLateralsProject( "Well-A Y1" ); + ASSERT_TRUE( input.eclipseCase != nullptr ); + ASSERT_TRUE( input.wellPath != nullptr ); + + RimWellPath* lateral = findLateralY2(); + ASSERT_TRUE( lateral != nullptr ); + + auto* interval = addSegmentIntervalCoveringWell( lateral ); + + const auto baseline = exportLateralBranch( input, lateral ); + ASSERT_TRUE( baseline.has_value() ); + ASSERT_GT( baseline->nodeMDs.size(), 3u ); + + const double tieInMD = lateral->wellPathTieIn()->tieInMeasuredDepth(); + const double branchLength = baseline->nodeMDs.back() - tieInMD; + const double fixedLength = branchLength / 4.0; + + interval->setFixedSegmentLength( fixedLength ); + const auto fixedExport = exportLateralBranch( input, lateral ); + ASSERT_TRUE( fixedExport.has_value() ); + + // Nodes of full segments are one fixed length apart, the first and last segments are cut by the branch ends + ASSERT_GE( fixedExport->nodeMDs.size(), 4u ); + for ( size_t k = 1; k + 2 < fixedExport->nodeMDs.size(); ++k ) + { + EXPECT_NEAR( fixedExport->nodeMDs[k + 1] - fixedExport->nodeMDs[k], fixedLength, 1.0e-6 ) << "node " << k; + } + + EXPECT_EQ( compsegsRows( *baseline ), compsegsRows( *fixedExport ) ); + expectRowsOnNearestNode( *fixedExport ); +} + +//-------------------------------------------------------------------------------------------------- +/// Min and max segment length can be combined. Cells are merged to the min length, then segments longer +/// than the max length are split. +//-------------------------------------------------------------------------------------------------- +TEST( RicWellPathExportMswGeometryPath, SegmentInterval_MinAndMaxSegmentLengthCombined ) +{ + ProjectCloser projectCloser; + + auto input = loadMultipleLateralsProject( "Well-A Y1" ); + ASSERT_TRUE( input.eclipseCase != nullptr ); + ASSERT_TRUE( input.wellPath != nullptr ); + + RimWellPath* lateral = findLateralY2(); + ASSERT_TRUE( lateral != nullptr ); + + auto* interval = addSegmentIntervalCoveringWell( lateral ); + + const auto baseline = exportLateralBranch( input, lateral ); + ASSERT_TRUE( baseline.has_value() ); + ASSERT_GT( baseline->nodeMDs.size(), 3u ); + + const double tieInMD = lateral->wellPathTieIn()->tieInMeasuredDepth(); + const double minLength = 3.0 * ( baseline->nodeMDs.back() - tieInMD ) / baseline->nodeMDs.size(); + const double maxLength = 3.0 * minLength; + + interval->setMinSegmentLength( minLength ); + interval->setMaxSegmentLength( maxLength ); + ASSERT_EQ( std::optional( minLength ), interval->minSegmentLength() ); + ASSERT_EQ( std::optional( maxLength ), interval->maxSegmentLength() ); + + const auto combined = exportLateralBranch( input, lateral ); + ASSERT_TRUE( combined.has_value() ); + + double previousMD = tieInMD; + for ( double nodeMD : combined->nodeMDs ) + { + EXPECT_GE( nodeMD - previousMD, minLength - 1.0e-6 ); + EXPECT_LE( nodeMD - previousMD, maxLength + 1.0e-6 ); + previousMD = nodeMD; + } + + EXPECT_EQ( compsegsRows( *baseline ), compsegsRows( *combined ) ); + expectRowsOnNearestNode( *combined ); +} + +//-------------------------------------------------------------------------------------------------- +/// With a single segment before the first and after the last perforation, each region has at most one +/// node. The nodes inside the perforated region are unchanged. +//-------------------------------------------------------------------------------------------------- +TEST( RicWellPathExportMswGeometryPath, SingleSegmentOutsidePerforations ) +{ + ProjectCloser projectCloser; + + auto input = loadMultipleLateralsProject( "Well-A Y1" ); + ASSERT_TRUE( input.eclipseCase != nullptr ); + ASSERT_TRUE( input.wellPath != nullptr ); + + RimWellPath* lateral = findLateralY2(); + ASSERT_TRUE( lateral != nullptr ); + + const auto baseline = exportLateralBranch( input, lateral ); + ASSERT_TRUE( baseline.has_value() ); + + double firstPerforationMD = std::numeric_limits::infinity(); + double lastPerforationMD = -std::numeric_limits::infinity(); + for ( const auto* perf : lateral->perforationIntervalCollection()->activePerforations() ) + { + firstPerforationMD = std::min( firstPerforationMD, perf->startMD() ); + lastPerforationMD = std::max( lastPerforationMD, perf->endMD() ); + } + ASSERT_LT( firstPerforationMD, lastPerforationMD ); + + const auto countBefore = [&]( const std::vector& nodes ) + { return std::ranges::count_if( nodes, [&]( double md ) { return md < firstPerforationMD; } ); }; + const auto countAfter = [&]( const std::vector& nodes ) + { return std::ranges::count_if( nodes, [&]( double md ) { return md > lastPerforationMD; } ); }; + + // The lateral takes the settings from the top level well + for ( auto* wellPath : { input.wellPath, lateral } ) + { + wellPath->segmentCollection()->setSingleSegmentBeforeFirstPerforation( true ); + wellPath->segmentCollection()->setSingleSegmentAfterLastPerforation( true ); + } + + const auto collapsed = exportLateralBranch( input, lateral ); + ASSERT_TRUE( collapsed.has_value() ); + + EXPECT_LE( countBefore( collapsed->nodeMDs ), 1 ); + EXPECT_LE( countAfter( collapsed->nodeMDs ), 1 ); + EXPECT_LE( collapsed->nodeMDs.size(), baseline->nodeMDs.size() ); + + const auto insideCount = [&]( const std::vector& nodes ) + { return std::ranges::count_if( nodes, [&]( double md ) { return md >= firstPerforationMD && md <= lastPerforationMD; } ); }; + EXPECT_EQ( insideCount( baseline->nodeMDs ), insideCount( collapsed->nodeMDs ) ); + + EXPECT_EQ( compsegsRows( *baseline ), compsegsRows( *collapsed ) ); +} diff --git a/ApplicationLibCode/UnitTests/RimWellPathCompletions-Test.cpp b/ApplicationLibCode/UnitTests/RimWellPathCompletions-Test.cpp index 63d04a9c90..a2f1c53c9a 100644 --- a/ApplicationLibCode/UnitTests/RimWellPathCompletions-Test.cpp +++ b/ApplicationLibCode/UnitTests/RimWellPathCompletions-Test.cpp @@ -9,6 +9,7 @@ #include "RimWellPath.h" #include "RimWellPathCompletions.h" +#include "cafPdmDefaultObjectFactory.h" #include "cafPdmUiFieldHandle.h" #include "cafPdmUiObjectHandle.h" #include "cafPdmUiOrdering.h" @@ -34,6 +35,61 @@ TEST( RimWellPathCompletions, SegmentIntervalsDefineCustomSegmentation ) EXPECT_DOUBLE_EQ( 3.0e-5, segments->getRoughnessAtMD( 250.0, RiaDefines::EclipseUnitSystem::UNITS_METRIC ) ); } +TEST( RimWellPathCompletions, SegmentIntervalFixedLengthIsExclusiveWithMinMax ) +{ + RimSegmentInterval interval; + EXPECT_FALSE( interval.fixedSegmentLength().has_value() ); + EXPECT_FALSE( interval.minSegmentLength().has_value() ); + EXPECT_FALSE( interval.maxSegmentLength().has_value() ); + + // Min and max can be combined + interval.setMinSegmentLength( 12.0 ); + interval.setMaxSegmentLength( 30.0 ); + EXPECT_EQ( std::optional( 12.0 ), interval.minSegmentLength() ); + EXPECT_EQ( std::optional( 30.0 ), interval.maxSegmentLength() ); + + // Fixed replaces min and max + interval.setFixedSegmentLength( 20.0 ); + EXPECT_EQ( std::optional( 20.0 ), interval.fixedSegmentLength() ); + EXPECT_FALSE( interval.minSegmentLength().has_value() ); + EXPECT_FALSE( interval.maxSegmentLength().has_value() ); + + // Min or max replaces fixed + interval.setMaxSegmentLength( 8.0 ); + EXPECT_FALSE( interval.fixedSegmentLength().has_value() ); + EXPECT_EQ( std::optional( 8.0 ), interval.maxSegmentLength() ); + + interval.setMinSegmentLength( std::nullopt ); + EXPECT_FALSE( interval.minSegmentLength().has_value() ); + + // A length <= 0 is treated as not set + interval.setMaxSegmentLength( 0.0 ); + EXPECT_FALSE( interval.maxSegmentLength().has_value() ); +} + +TEST( RimWellPathCompletions, SegmentIntervalSegmentationControlsArePersisted ) +{ + RimSegmentInterval original; + original.setMaxSegmentLength( 25.0 ); + original.setMinSegmentLength( 5.0 ); + const QString xml = original.writeObjectToXmlString(); + + RimSegmentInterval restored; + restored.readObjectFromXmlString( xml, caf::PdmDefaultObjectFactory::instance() ); + EXPECT_FALSE( restored.fixedSegmentLength().has_value() ); + EXPECT_EQ( std::optional( 5.0 ), restored.minSegmentLength() ); + EXPECT_EQ( std::optional( 25.0 ), restored.maxSegmentLength() ); + + // A project written before the controls existed has all controls off + RimSegmentInterval legacy; + legacy.readObjectFromXmlString( "100200", + caf::PdmDefaultObjectFactory::instance() ); + EXPECT_FALSE( legacy.fixedSegmentLength().has_value() ); + EXPECT_FALSE( legacy.minSegmentLength().has_value() ); + EXPECT_FALSE( legacy.maxSegmentLength().has_value() ); + EXPECT_DOUBLE_EQ( 200.0, legacy.endMD() ); +} + TEST( RimWellPathCompletions, SegmentIntervalUiUsesMeasuredDepthNameAndMetricUnits ) { RimWellPath wellPath; diff --git a/GrpcInterface/Python/rips/tests/test_custom_segment_intervals.py b/GrpcInterface/Python/rips/tests/test_custom_segment_intervals.py index 80c5b1f145..ffa5546d62 100644 --- a/GrpcInterface/Python/rips/tests/test_custom_segment_intervals.py +++ b/GrpcInterface/Python/rips/tests/test_custom_segment_intervals.py @@ -78,3 +78,45 @@ def test_custom_segment_interval_invalid_range(rips_instance, initialize_test): # Also test equal values (start_md == end_md), which should also fail with pytest.raises(rips.RipsError, match="End MD must be greater than Start MD"): well_path.segment_collection().add_segment_interval(start_md=150, end_md=150) + + +def test_segment_interval_segmentation_lengths(rips_instance, initialize_test): + """Fixed segment length is exclusive with min/max; min and max can be combined""" + + well_path_coll = rips_instance.project.well_path_collection() + well_path = well_path_coll.add_new_object(rips.ModeledWellPath) + well_path.name = "Test Well for Segmentation Lengths" + + interval = well_path.segment_collection().add_segment_interval( + start_md=100, end_md=200 + ) + + interval.set_min_segment_length(length=10.0) + interval.set_max_segment_length(length=40.0) + min_length = interval.set_min_segment_length(length=10.0) + assert min_length is not None + + interval.set_fixed_segment_length(length=25.0) + interval.set_max_segment_length(length=30.0) + interval.set_fixed_segment_length(enable=False) + + with pytest.raises(rips.RipsError, match="greater than zero"): + interval.set_min_segment_length(length=0.0) + + +def test_segment_collection_single_segment_around_perforations( + rips_instance, initialize_test +): + well_path_coll = rips_instance.project.well_path_collection() + well_path = well_path_coll.add_new_object(rips.ModeledWellPath) + well_path.name = "Test Well for Single Segment Options" + + collection = well_path.segment_collection() + assert collection.single_segment_before_first_perforation is False + assert collection.single_segment_after_last_perforation is False + + collection.single_segment_before_first_perforation = True + collection.single_segment_after_last_perforation = True + collection.update() + assert collection.single_segment_before_first_perforation is True + assert collection.single_segment_after_last_perforation is True