#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.
This commit is contained in:
Magne Sjaastad committed 2026-10-12 19:12:08 +02:00
1 parent 2f7de9469f
commit d8f346e9ab
22 files changed
+1866 -149

No files matched your search

@@ -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
)
@@ -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 <QAction>
#include <QIcon>
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<RimSegmentCollection>() != nullptr;
}
void RicNewSegmentIntervalFeature::onActionTriggered( bool isChecked )
void RicCreateSegmentIntervalFeature::onActionTriggered( bool isChecked )
{
auto* collection = caf::SelectionManager::instance()->selectedItemOfType<RimSegmentCollection>();
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" ) );
}
@@ -23,7 +23,7 @@
//==================================================================================================
///
//==================================================================================================
class RicNewSegmentIntervalFeature : public caf::CmdFeature
class RicCreateSegmentIntervalFeature : public caf::CmdFeature
{
CAF_CMD_HEADER_INIT;
@@ -203,7 +203,7 @@ double segmentMidpointMD( const std::vector<CellSegmentEntry>& 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<CellSegmentEntry>& 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<double>::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<CellSegmentEntry>& 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<NodeSpan> segmentSpans( const std::vector<std::pair<double, double>>& cellPieces, const std::vector<double>& nodes )
{
std::vector<NodeSpan> 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<double> 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<NodeSpan> 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<const RimPerforationInterval*>& perforationIntervals;
const std::set<const RimPerforationInterval*>& valvedIntervals;
const std::optional<QDateTime>& exportDate;
const RigMainGrid* mainGrid;
const RigActiveCellInfo* activeCellInfo;
std::map<const RimPerforationInterval*, std::set<size_t>>& perforationCompletionCells;
std::map<const RimPerforationInterval*, RicPerforationCellFilterEvaluator>& filterEvaluators;
std::map<const RimPerforationInterval*, size_t>& overlapCountPerInterval;
std::map<const RimPerforationInterval*, size_t>& 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<RigMswCellIntersection> collectCellCompsegs( const WellPathCellIntersectionInfo& cellInfo, PerforationCompsegsContext& context )
{
std::vector<RigMswCellIntersection> 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<std::pair<double, double>>& customSegmentIntervals,
const RicMswTableDataTools::SegmentationSettings& segmentationSettings,
const std::optional<QDateTime>& exportDate,
RiaDefines::EclipseUnitSystem unitSystem,
std::vector<CellSegmentEntry>* 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<std::pair<double, double>> cellPieces;
std::vector<std::vector<RigMswCellIntersection>> compsegsPerCell;
std::vector<double> 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<RigMswCellIntersection> 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<double> 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<double>::infinity();
double lastPerforationMD = -std::numeric_limits<double>::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<int>( span.nodeIndex ), nodes[span.nodeIndex] } );
}
}
@@ -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<RigMswCellIntersection>
//--------------------------------------------------------------------------------------------------
int findOutletSegmentForMD( const std::vector<CellSegmentEntry>& 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<NodeSpan> segmentSpans( const std::vector<std::pair<double, double>>& cellPieces, const std::vector<double>& 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<WellPathCellIntersectionInfo>& filteredIntersections,
@@ -155,6 +179,7 @@ RigMswBranch buildMainBoreBranch( const RimWellPath*
int outletSegmentNumber,
double maxSegmentLength,
const std::vector<std::pair<double, double>>& customSegmentIntervals,
const RicMswTableDataTools::SegmentationSettings& segmentationSettings,
const std::optional<QDateTime>& exportDate,
RiaDefines::EclipseUnitSystem unitSystem,
std::vector<CellSegmentEntry>* cellSegMap,
@@ -190,6 +190,7 @@ std::vector<RigMswBranch> 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 } );
}
@@ -65,6 +65,11 @@ RigMswWellExportData buildMswWellExportData( RimEclipseCase*
RicWellPathExportMswTableData::CompletionType completionType,
const std::optional<QDateTime>& 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
@@ -20,12 +20,15 @@
#include "Well/RigWellPath.h"
#include "RimSegmentCollection.h"
#include "RimSegmentInterval.h"
#include "RimWellPath.h"
#include "cafAssert.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <set>
//--------------------------------------------------------------------------------------------------
@@ -134,6 +137,226 @@ std::vector<std::pair<double, double>>
return subSegmentMDPairs;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RicMswTableDataTools::SegmentationInterval>
RicMswTableDataTools::segmentationIntervals( const RimSegmentCollection* segmentCollection, const std::optional<QDateTime>& exportDate )
{
std::vector<SegmentationInterval> 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<QDateTime>& 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<RicMswTableDataTools::SegmentationInterval>& intervals,
double md )
{
for ( const auto& interval : intervals )
{
if ( md >= interval.startMD && md <= interval.endMD ) return &interval;
}
return nullptr;
}
} // namespace
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<double> RicMswTableDataTools::placeSegmentNodes( double outletMD,
const std::vector<double>& allCandidateNodes,
const std::vector<SegmentationInterval>& 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<double> 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<double> 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<double> 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<int>( 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<double> RicMswTableDataTools::collapseNodesOutsidePerforations( const std::vector<double>& 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<double> 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<size_t> RicMswTableDataTools::upstreamNodeIndex( const std::vector<double>& nodes, double md )
{
if ( nodes.empty() ) return std::nullopt;
std::optional<size_t> upstream;
double minDistance = std::numeric_limits<double>::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<size_t> RicMswTableDataTools::nearestNodeIndex( const std::vector<double>& nodes, double md )
{
std::optional<size_t> nearest;
double minDistance = std::numeric_limits<double>::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;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
@@ -18,10 +18,15 @@
#pragma once
#include <QDateTime>
#include <limits>
#include <optional>
#include <utility>
#include <vector>
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<double> minLength;
std::optional<double> maxLength;
std::optional<double> fixedLength;
};
//--------------------------------------------------------------------------------------------------
/// Segmentation rules of a segment collection
//--------------------------------------------------------------------------------------------------
struct SegmentationSettings
{
std::vector<SegmentationInterval> intervals;
bool singleSegmentBeforeFirstPerforation = false;
bool singleSegmentAfterLastPerforation = false;
};
std::vector<std::pair<double, double>> createSubSegmentMDPairs( double startMD,
double endMD,
double maxSegmentLength,
const std::vector<std::pair<double, double>>& customSegmentIntervals = {} );
std::vector<SegmentationInterval> segmentationIntervals( const RimSegmentCollection* segmentCollection,
const std::optional<QDateTime>& exportDate );
SegmentationSettings segmentationSettings( const RimSegmentCollection* segmentCollection, const std::optional<QDateTime>& 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<double> placeSegmentNodes( double outletMD,
const std::vector<double>& candidateNodes,
const std::vector<SegmentationInterval>& intervals,
double lastMD = std::numeric_limits<double>::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<double> collapseNodesOutsidePerforations( const std::vector<double>& 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<size_t> upstreamNodeIndex( const std::vector<double>& 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<size_t> nearestNodeIndex( const std::vector<double>& nodes, double md );
double tvdFromMeasuredDepth( const RimWellPath* wellPath, double measuredDepth );
inline constexpr double valveSegmentLength = 0.1;
@@ -84,7 +84,7 @@ std::expected<RigMswTableData, std::string> RicWellPathExportMswTableData::extra
exportDate );
auto unitSystem = eclipseCase->eclipseCaseData()->unitsType();
return RicWellPathExportMswGeometryPath::collectTableData( wellExportData, unitSystem );
return RicWellPathExportMswGeometryPath::collectTableData( wellExportData, unitSystem, segmentCollection->exportCompsegsSegmentNumber() );
}
//--------------------------------------------------------------------------------------------------
@@ -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<double>::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<std::pair<double, double>> RimSegmentCollection::getSegmentIntervals
return result;
}
//--------------------------------------------------------------------------------------------------
/// Valid intervals with a fixed, min or max segment length rule, active on the export date if given
//--------------------------------------------------------------------------------------------------
std::vector<const RimSegmentInterval*> RimSegmentCollection::segmentationIntervals( const std::optional<QDateTime>& exportDate ) const
{
std::vector<const RimSegmentInterval*> 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();
@@ -60,9 +60,16 @@ public:
INC
};
using ReferenceMDEnum = caf::AppEnum<ReferenceMDType>;
using PressureDropEnum = caf::AppEnum<PressureDropType>;
using LengthAndDepthEnum = caf::AppEnum<LengthAndDepthType>;
enum class CompsegsSegmentNumberType
{
ASSIGNED_BY_SIMULATOR,
EXPORTED
};
using ReferenceMDEnum = caf::AppEnum<ReferenceMDType>;
using PressureDropEnum = caf::AppEnum<PressureDropType>;
using LengthAndDepthEnum = caf::AppEnum<LengthAndDepthType>;
using CompsegsSegmentNumberEnum = caf::AppEnum<CompsegsSegmentNumberType>;
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<std::pair<double, double>> getSegmentIntervals() const;
std::vector<const RimSegmentInterval*> segmentationIntervals( const std::optional<QDateTime>& exportDate ) const;
bool hasCustomSegmentIntervals() const;
void setUnitSystemSpecificDefaults();
@@ -132,4 +146,8 @@ private:
caf::PdmField<LengthAndDepthEnum> m_lengthAndDepth;
caf::PdmField<bool> m_enforceMaxSegmentLength;
caf::PdmField<double> m_maxSegmentLength;
caf::PdmField<bool> m_singleSegmentBeforeFirstPerforation;
caf::PdmField<bool> m_singleSegmentAfterLastPerforation;
caf::PdmField<CompsegsSegmentNumberEnum> m_compsegsSegmentNumber;
};
@@ -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<double> RimSegmentInterval::fixedSegmentLength() const
{
if ( m_fixedSegmentLength().first && m_fixedSegmentLength().second > 0.0 ) return m_fixedSegmentLength().second;
return std::nullopt;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::optional<double> RimSegmentInterval::minSegmentLength() const
{
if ( m_minSegmentLength().first && m_minSegmentLength().second > 0.0 ) return m_minSegmentLength().second;
return std::nullopt;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::optional<double> RimSegmentInterval::maxSegmentLength() const
{
if ( m_maxSegmentLength().first && m_maxSegmentLength().second > 0.0 ) return m_maxSegmentLength().second;
return std::nullopt;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimSegmentInterval::setFixedSegmentLength( std::optional<double> 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<double> 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<double> 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 );
}
}
//--------------------------------------------------------------------------------------------------
@@ -27,6 +27,8 @@
#include <QDateTime>
#include <QString>
#include <optional>
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<double> fixedSegmentLength() const;
std::optional<double> minSegmentLength() const;
std::optional<double> maxSegmentLength() const;
void setFixedSegmentLength( std::optional<double> length );
void setMinSegmentLength( std::optional<double> length );
void setMaxSegmentLength( std::optional<double> 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<double> m_startMD;
@@ -111,6 +123,10 @@ private:
caf::PdmField<double> m_diameter;
caf::PdmField<double> m_roughnessFactor;
caf::PdmField<std::pair<bool, double>> m_fixedSegmentLength;
caf::PdmField<std::pair<bool, double>> m_minSegmentLength;
caf::PdmField<std::pair<bool, double>> m_maxSegmentLength;
caf::PdmField<bool> m_useCustomStartDate;
caf::PdmField<QDateTime> m_startDate;
};
@@ -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<caf::PdmObjectHandle*, QString> RimcSegmentInterval_setSegmentLengthBase::execute()
{
auto* interval = self<RimSegmentInterval>();
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<double> length = m_enable() ? std::optional<double>( 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;
}
@@ -39,3 +39,71 @@ private:
caf::PdmField<double> m_diameter;
caf::PdmField<double> m_roughnessFactor;
};
//==================================================================================================
///
//==================================================================================================
class RimcSegmentInterval_setSegmentLengthBase : public caf::PdmObjectMethod
{
public:
RimcSegmentInterval_setSegmentLengthBase( caf::PdmObjectHandle* self, const QString& description );
std::expected<caf::PdmObjectHandle*, QString> execute() override;
QString classKeywordReturnedType() const override;
protected:
enum class LengthType
{
FIXED,
MIN,
MAX
};
virtual LengthType lengthType() const = 0;
private:
caf::PdmField<double> m_length;
caf::PdmField<bool> 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;
};
@@ -44,7 +44,7 @@ TEST( RicMswBranchBuilder, FindOutlet_EmptyMap )
//--------------------------------------------------------------------------------------------------
TEST( RicMswBranchBuilder, FindOutlet_MDInFirstCell )
{
std::vector<CellSegmentEntry> map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 }, { 300.0, 400.0, 7 } };
std::vector<CellSegmentEntry> 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<CellSegmentEntry> map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 }, { 300.0, 400.0, 7 } };
std::vector<CellSegmentEntry> 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<CellSegmentEntry> map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 }, { 300.0, 400.0, 7 } };
std::vector<CellSegmentEntry> 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<CellSegmentEntry> map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 } };
std::vector<CellSegmentEntry> 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<CellSegmentEntry> map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 } };
std::vector<CellSegmentEntry> 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<CellSegmentEntry> map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 } };
std::vector<CellSegmentEntry> 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<CellSegmentEntry> map = { { 100.0, 200.0, 5 }, { 200.0, 300.0, 6 }, { 300.0, 400.0, 7 } };
std::vector<CellSegmentEntry> 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<CellSegmentEntry> map = { { 0.0, 100.0, 3 } };
std::vector<CellSegmentEntry> 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<std::pair<double, double>> exampleCells = { { 0.0, 10.0 }, { 10.0, 14.0 }, { 14.0, 30.0 }, { 30.0, 33.0 }, { 33.0, 60.0 } };
std::vector<double> centres( const std::vector<std::pair<double, double>>& pieces )
{
std::vector<double> result;
for ( const auto& [start, end] : pieces )
{
result.push_back( 0.5 * ( start + end ) );
}
return result;
}
void expectNodes( const std::vector<double>& expected, const std::vector<double>& 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<std::pair<double, double>> 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<SegmentationInterval> 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<double> 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<double> 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<size_t>( 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<size_t>( 0 ), RicMswTableDataTools::upstreamNodeIndex( nodes, 22.0 ) );
EXPECT_EQ( std::optional<size_t>( 1 ), RicMswTableDataTools::upstreamNodeIndex( nodes, 46.0 ) );
EXPECT_EQ( std::optional<size_t>( 2 ), RicMswTableDataTools::upstreamNodeIndex( nodes, 100.0 ) );
}
TEST( RicMswBranchBuilder, UpstreamNodeIndex_NodeAtMDIsUsed )
{
EXPECT_EQ( std::optional<size_t>( 1 ), RicMswTableDataTools::upstreamNodeIndex( { 10.0, 20.0 }, 20.0 ) );
EXPECT_EQ( std::optional<size_t>( 0 ), RicMswTableDataTools::upstreamNodeIndex( { 10.0, 20.0 }, 19.999 ) );
}
//==================================================================================================
// Nearest node tests
//==================================================================================================
TEST( RicMswBranchBuilder, NearestNodeIndex )
{
const std::vector<double> nodes = { 12.0, 31.5, 46.5 };
EXPECT_FALSE( RicMswTableDataTools::nearestNodeIndex( {}, 10.0 ).has_value() );
EXPECT_EQ( std::optional<size_t>( 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<size_t>( 1 ), RicMswTableDataTools::nearestNodeIndex( nodes, 22.0 ) );
EXPECT_EQ( std::optional<size_t>( 2 ), RicMswTableDataTools::nearestNodeIndex( nodes, 100.0 ) );
}
TEST( RicMswBranchBuilder, NearestNodeIndex_TieGoesToFirstNode )
{
EXPECT_EQ( std::optional<size_t>( 0 ), RicMswTableDataTools::nearestNodeIndex( { 10.0, 20.0 }, 15.0 ) );
}
//==================================================================================================
// Segment span tests
//==================================================================================================
namespace
{
void expectSpans( const std::vector<NodeSpan>& expected, const std::vector<NodeSpan>& 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
//==================================================================================================
@@ -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 <QDir>
#include <QFile>
#include <QStringList>
#include <QTextStream>
#include <algorithm>
#include <cmath>
#include <limits>
#include <map>
#include <optional>
#include <tuple>
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<QStringList> formattedCompsegsRows( const RigMswTableData& tableData, bool isLgrData )
{
QString text;
QTextStream stream( &text );
RifTextDataTableFormatter formatter( stream );
RigMswDataFormatter::formatCompsegsTable( formatter, tableData, isLgrData );
std::vector<QStringList> 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<int>( 2 ), result.compsegsData()[0].segmentNumber );
EXPECT_EQ( std::optional<int>( 3 ), result.compsegsData()[1].segmentNumber );
EXPECT_EQ( std::optional<int>( 3 ), result.compsegsData()[2].segmentNumber );
const auto rows = formattedCompsegsRows( result, false );
ASSERT_EQ( 2u, rows.size() );
const std::vector<QString> 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<double> nodeMDs;
std::vector<std::vector<RigMswCellIntersection>> 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<BranchExport> 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<std::tuple<size_t, size_t, size_t, double, double>> compsegsRows( const BranchExport& branch )
{
std::vector<std::tuple<size_t, size_t, size_t, double, double>> 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<double>( minLength ), interval->minSegmentLength() );
ASSERT_EQ( std::optional<double>( 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<double>::infinity();
double lastPerforationMD = -std::numeric_limits<double>::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<double>& nodes )
{ return std::ranges::count_if( nodes, [&]( double md ) { return md < firstPerforationMD; } ); };
const auto countAfter = [&]( const std::vector<double>& 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<double>& 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 ) );
}
@@ -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<double>( 12.0 ), interval.minSegmentLength() );
EXPECT_EQ( std::optional<double>( 30.0 ), interval.maxSegmentLength() );
// Fixed replaces min and max
interval.setFixedSegmentLength( 20.0 );
EXPECT_EQ( std::optional<double>( 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<double>( 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<double>( 5.0 ), restored.minSegmentLength() );
EXPECT_EQ( std::optional<double>( 25.0 ), restored.maxSegmentLength() );
// A project written before the controls existed has all controls off
RimSegmentInterval legacy;
legacy.readObjectFromXmlString( "<SegmentInterval><StartMd>100</StartMd><EndMd>200</EndMd></SegmentInterval>",
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;