CompletionExport: Add RicMswBranchBuilder and RicWellPathExportMswGeometryPath

Add new MswExport subdirectory with branch builder and geometry path classes
as part of the simplified MSW export approach. Update CMakeLists to include
the new files and add CompletionExportCommands to the include path.
This commit is contained in:
Magne Sjaastad committed 2026-03-27 14:47:32 +01:00
1 parent 47f2967ccb
commit 8d0c3f340b
9 files changed
+1634 -16

No files matched your search

+4 -2
View File
@@ -89,8 +89,10 @@ if(RESINSIGHT_ENABLE_PRECOMPILED_HEADERS)
endif()
target_include_directories(
${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/EclipseCommands
${PROJECT_NAME}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/EclipseCommands
${CMAKE_CURRENT_SOURCE_DIR}/CompletionExportCommands
)
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
@@ -20,6 +20,8 @@ set(SOURCE_GROUP_HEADER_FILES
${CMAKE_CURRENT_LIST_DIR}/RicWellPathFractureReportItem.h
${CMAKE_CURRENT_LIST_DIR}/RicExportCompletionsForTemporaryLgrsFeature.h
${CMAKE_CURRENT_LIST_DIR}/RicWellPathExportMswTableData.h
${CMAKE_CURRENT_LIST_DIR}/MswExport/RicWellPathExportMswGeometryPath.h
${CMAKE_CURRENT_LIST_DIR}/MswExport/RicMswBranchBuilder.h
${CMAKE_CURRENT_LIST_DIR}/RicMswTableDataTools.h
${CMAKE_CURRENT_LIST_DIR}/RicScheduleDataGenerator.h
${CMAKE_CURRENT_LIST_DIR}/RicTransmissibilityCalculator.h
@@ -47,6 +49,8 @@ set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RicWellPathFractureReportItem.cpp
${CMAKE_CURRENT_LIST_DIR}/RicExportCompletionsForTemporaryLgrsFeature.cpp
${CMAKE_CURRENT_LIST_DIR}/RicWellPathExportMswTableData.cpp
${CMAKE_CURRENT_LIST_DIR}/MswExport/RicWellPathExportMswGeometryPath.cpp
${CMAKE_CURRENT_LIST_DIR}/MswExport/RicMswBranchBuilder.cpp
${CMAKE_CURRENT_LIST_DIR}/RicMswTableDataTools.cpp
${CMAKE_CURRENT_LIST_DIR}/RicScheduleDataGenerator.cpp
${CMAKE_CURRENT_LIST_DIR}/RicTransmissibilityCalculator.cpp
@@ -0,0 +1,850 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RicMswBranchBuilder.h"
#include "RicExportFractureCompletionsImpl.h"
#include "RicMswTableDataTools.h"
#include "RigActiveCellInfo.h"
#include "RigEclipseCaseData.h"
#include "RigGridBase.h"
#include "RigMainGrid.h"
#include "Well/RigWellPathIntersectionTools.h"
#include "RimEclipseCase.h"
#include "RimFishbones.h"
#include "RimFishbonesCollection.h"
#include "RimFractureTemplate.h"
#include "RimMswCompletionParameters.h"
#include "RimPerforationCollection.h"
#include "RimPerforationInterval.h"
#include "RimWellPath.h"
#include "RimWellPathCompletions.h"
#include "RimWellPathFracture.h"
#include "RimWellPathFractureCollection.h"
#include "RimWellPathValve.h"
#include "RiaLogging.h"
#include <algorithm>
#include <limits>
namespace RicMswBranchBuilder
{
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::optional<RigMswCellIntersection>
toMswCellIntersection( const WellPathCellIntersectionInfo& cellInfo, const RigMainGrid* mainGrid, double distanceStart, double distanceEnd )
{
if ( cellInfo.globCellIndex >= mainGrid->totalCellCount() ) return std::nullopt;
size_t localIdx = 0;
const RigGridBase* localGrid = mainGrid->gridAndGridLocalIdxFromGlobalCellIdx( cellInfo.globCellIndex, &localIdx );
size_t i, j, k;
localGrid->ijkFromCellIndex( localIdx, &i, &j, &k );
if ( mainGrid->isDualPorosity() ) k += mainGrid->cellCountK();
RigMswCellIntersection ci;
ci.i = i + 1; // 1-based
ci.j = j + 1;
ci.k = k + 1;
ci.distanceStart = distanceStart;
ci.distanceEnd = distanceEnd;
if ( localGrid != mainGrid ) ci.gridName = localGrid->gridName();
return ci;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
double segmentMidpointMD( const std::vector<CellSegmentEntry>& cellSegMap, int segmentNumber )
{
for ( const auto& entry : cellSegMap )
{
if ( entry.lastSubSegmentNumber == segmentNumber ) return 0.5 * ( entry.cellStartMD + entry.cellEndMD );
}
return 0.0;
}
// Range-based lookup: find the segment whose [cellStartMD, cellEndMD] range contains md.
// Used for ICD valves where the valve overlap start always falls within the host main-bore cell.
// Fractures use findOutletSegmentForMD (midpoint-based) to match the tree approach.
int findContainingSegmentForMD( const std::vector<CellSegmentEntry>& cellSegMap, double md )
{
if ( cellSegMap.empty() ) return 1;
for ( const auto& entry : cellSegMap )
{
if ( md >= entry.cellStartMD && md < entry.cellEndMD ) return entry.lastSubSegmentNumber;
}
// Fallback: md is beyond all segments — return the last segment.
return cellSegMap.back().lastSubSegmentNumber;
}
int findOutletSegmentForMD( const std::vector<CellSegmentEntry>& cellSegMap, double md )
{
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
// 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;
if ( dist >= 0.0 && dist < bestDist )
{
best = &entry;
bestDist = dist;
}
}
// Fallback: md is shallower than all segment midpoints — connect to the first (shallowest) segment.
return best ? best->lastSubSegmentNumber : cellSegMap.front().lastSubSegmentNumber;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RigMswBranch buildMainBoreBranchFromGeometry( const RimWellPath* wellPath,
const std::vector<WellPathCellIntersectionInfo>& filteredIntersections,
const RigMainGrid* mainGrid,
const std::vector<const RimPerforationInterval*>& perforationIntervals,
const std::set<const RimPerforationInterval*>& valvedIntervals,
const std::string& infoType,
double heelMD,
double heelTVD,
int branchNumber,
int& segmentNumber,
int outletSegmentNumber,
double maxSegmentLength,
const std::vector<std::pair<double, double>>& customSegmentIntervals,
const std::optional<QDateTime>& exportDate,
RiaDefines::EclipseUnitSystem unitSystem,
std::vector<CellSegmentEntry>* cellSegMap,
const RigActiveCellInfo* activeCellInfo )
{
std::vector<RigMswSegment> result;
const double segmentLengthThreshold = 1.0e-3;
double prevOutMD = heelMD;
double prevOutTVD = heelTVD;
int prevSegNum = outletSegmentNumber;
bool firstSeg = true;
for ( const auto& cellInfo : filteredIntersections )
{
const double cellLength = std::fabs( cellInfo.endMD - cellInfo.startMD );
if ( cellLength <= segmentLengthThreshold )
{
RiaLogging::info( QString( "Skipping segment, threshold = %1, length = %2" ).arg( segmentLengthThreshold ).arg( cellLength ) );
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( cellInfo.globCellIndex ) )
continue;
if ( auto ci = toMswCellIntersection( cellInfo, mainGrid, overlapStart, overlapEnd ) ) cellCompsegs.push_back( *ci );
}
}
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 );
double curPrevOutMD = prevOutMD;
double curPrevOutTVD = prevOutTVD;
if ( midPointMD < curPrevOutMD )
{
curPrevOutMD = heelMD;
curPrevOutTVD = heelTVD;
}
double length = 0.0;
double depth = 0.0;
if ( infoType == "INC" )
{
length = midPointMD - curPrevOutMD;
depth = midPointTVD - curPrevOutTVD;
}
else
{
length = midPointMD;
depth = midPointTVD;
}
double diameter = 0.0;
double roughness = 0.0;
if ( exportDate.has_value() )
{
diameter = wellPath->mswCompletionParameters()->getDiameterAtMD( midPointMD, unitSystem, *exportDate );
roughness = wellPath->mswCompletionParameters()->getRoughnessAtMD( midPointMD, unitSystem, *exportDate );
}
else
{
diameter = wellPath->mswCompletionParameters()->getDiameterAtMD( midPointMD, unitSystem );
roughness = wellPath->mswCompletionParameters()->getRoughnessAtMD( midPointMD, 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 && firstSubSeg )
{
seg.description = "Segments on main bore";
firstSeg = false;
}
if ( firstSubSeg ) seg.intersections = cellCompsegs;
lastSubSegNum = segmentNumber;
prevSegNum = segmentNumber++;
prevOutMD = midPointMD;
prevOutTVD = midPointTVD;
firstSubSeg = false;
if ( cellSegMap ) cellSegMap->push_back( { subStartMD, subEndMD, lastSubSegNum } );
result.push_back( std::move( seg ) );
}
}
return RigMswBranch{ branchNumber, std::nullopt, std::move( result ) };
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RigMswBranch> buildValveBranchesFromGeometry( const RimWellPath* wellPath,
const std::vector<WellPathCellIntersectionInfo>& filteredIntersections,
const RigMainGrid* mainGrid,
const std::vector<const RimPerforationInterval*>& perforationIntervals,
const std::vector<CellSegmentEntry>& cellSegMap,
const std::string& infoType,
const std::string& wellNameForExport,
int& segmentNumber,
int& branchNumber,
double maxSegmentLength,
const std::vector<std::pair<double, double>>& customSegmentIntervals,
const std::optional<QDateTime>& exportDate,
RiaDefines::EclipseUnitSystem unitSystem )
{
std::vector<RigMswBranch> result;
const auto linerDiameter = wellPath->mswCompletionParameters()->linerDiameter( unitSystem );
const auto roughnessFactor = wellPath->mswCompletionParameters()->roughnessFactor( unitSystem );
for ( const auto* perf : perforationIntervals )
{
if ( exportDate.has_value() && !perf->isActiveOnDate( exportDate.value() ) ) continue;
auto valves = perf->descendantsIncludingThisOfType<RimWellPathValve>();
for ( const auto* valve : valves )
{
if ( !valve->isChecked() ) continue;
if ( exportDate.has_value() && !valve->isActiveOnDate( exportDate.value() ) ) continue;
const auto valveType = valve->componentType();
const bool isIcdOrIcv =
( valveType == RiaDefines::WellPathComponentType::ICD || valveType == RiaDefines::WellPathComponentType::ICV );
const bool isAicdOrSicd =
( valveType == RiaDefines::WellPathComponentType::AICD || valveType == RiaDefines::WellPathComponentType::SICD );
if ( !isIcdOrIcv && !isAicdOrSicd ) continue;
const auto& valveLocations = valve->valveLocations();
const auto& valveSegs = valve->valveSegments(); // one (startMD, endMD) per location
// ICD: one branch per overlapping cell with proportional area, matching tree approach
if ( valveType == RiaDefines::WellPathComponentType::ICD )
{
for ( size_t vi = 0; vi < valveLocations.size(); ++vi )
{
const double valveSegStart = valveSegs[vi].first;
const double valveSegEnd = valveSegs[vi].second;
// Collect cells overlapping this ICD's coverage range and total overlap length
struct CellEntry
{
RigMswCellIntersection ci;
double overlapMD;
};
std::vector<CellEntry> cells;
double totalOverlapMD = 0.0;
for ( const auto& cellInfo : filteredIntersections )
{
const double overlapStart = std::max( valveSegStart, cellInfo.startMD );
const double overlapEnd = std::min( valveSegEnd, cellInfo.endMD );
if ( overlapEnd <= overlapStart ) continue;
if ( auto ci = toMswCellIntersection( cellInfo, mainGrid, overlapStart, overlapEnd ) )
{
double overlapMD = overlapEnd - overlapStart;
cells.push_back( { *ci, overlapMD } );
totalOverlapMD += overlapMD;
}
}
// One branch+segment per cell with area proportional to overlap length
for ( auto& entry : cells )
{
const int cellBranch = ++branchNumber;
const int outletSeg = findContainingSegmentForMD( cellSegMap, entry.ci.distanceStart );
const double valveMD = segmentMidpointMD( cellSegMap, outletSeg );
const double valveEndMD = valveMD + RicMswTableDataTools::valveSegmentLength;
const double startTVD = RicMswTableDataTools::tvdFromMeasuredDepth( wellPath, valveMD );
const double endTVD = RicMswTableDataTools::tvdFromMeasuredDepth( wellPath, valveEndMD );
double length = 0.0, depth = 0.0;
if ( infoType == "INC" )
{
length = valveEndMD - valveMD;
depth = endTVD - startTVD;
}
else
{
length = valveEndMD;
depth = endTVD;
}
RigMswSegment seg;
seg.segmentNumber = segmentNumber;
seg.outletSegmentNumber = outletSeg;
seg.length = length;
seg.depth = depth;
seg.diameter = linerDiameter;
seg.roughness = roughnessFactor;
seg.sourceWellName = wellPath->name().toStdString();
seg.description = QString( "%1 #%2" ).arg( valve->name() ).arg( vi + 1 ).toStdString();
seg.intersections = { entry.ci };
WsegvalvRow wv;
wv.well = wellNameForExport;
wv.segmentNumber = segmentNumber;
wv.cv = valve->flowCoefficient();
wv.area = totalOverlapMD > 0.0 ? valve->area( unitSystem ) * entry.overlapMD / totalOverlapMD : 0.0;
wv.status = valve->isOpen() ? "OPEN" : "SHUT";
wv.description = valve->name().toStdString();
seg.wsegvalvData = wv;
segmentNumber++;
result.push_back( RigMswBranch{ cellBranch, std::nullopt, { std::move( seg ) } } );
}
}
continue;
}
// AICD: one branch per overlapping cell with proportional flow scaling factor, matching tree approach
if ( valveType == RiaDefines::WellPathComponentType::AICD )
{
const RimWellPathAicdParameters* aicdParams = valve->aicdParameters();
if ( aicdParams && aicdParams->isValid() )
{
const auto aicdValues = aicdParams->doubleValues();
auto setOptional = []( double value ) -> std::optional<double>
{ return std::isinf( value ) ? std::nullopt : std::optional<double>{ value }; };
for ( size_t vi = 0; vi < valveLocations.size(); ++vi )
{
const double valveSegStart = valveSegs[vi].first;
const double valveSegEnd = valveSegs[vi].second;
struct CellEntry
{
RigMswCellIntersection ci;
double overlapMD;
};
std::vector<CellEntry> cells;
double totalOverlapMD = 0.0;
for ( const auto& cellInfo : filteredIntersections )
{
const double overlapStart = std::max( valveSegStart, cellInfo.startMD );
const double overlapEnd = std::min( valveSegEnd, cellInfo.endMD );
if ( overlapEnd <= overlapStart ) continue;
if ( auto ci = toMswCellIntersection( cellInfo, mainGrid, overlapStart, overlapEnd ) )
{
cells.push_back( { *ci, overlapEnd - overlapStart } );
totalOverlapMD += overlapEnd - overlapStart;
}
}
for ( auto& entry : cells )
{
const int cellBranch = ++branchNumber;
const int outletSeg = findContainingSegmentForMD( cellSegMap, entry.ci.distanceStart );
const double valveMD = segmentMidpointMD( cellSegMap, outletSeg );
const double valveEndMD = valveMD + RicMswTableDataTools::valveSegmentLength;
const double startTVD = RicMswTableDataTools::tvdFromMeasuredDepth( wellPath, valveMD );
const double endTVD = RicMswTableDataTools::tvdFromMeasuredDepth( wellPath, valveEndMD );
double length = 0.0, depth = 0.0;
if ( infoType == "INC" )
{
length = valveEndMD - valveMD;
depth = endTVD - startTVD;
}
else
{
length = valveEndMD;
depth = endTVD;
}
RigMswSegment seg;
seg.segmentNumber = segmentNumber;
seg.outletSegmentNumber = outletSeg;
seg.length = length;
seg.depth = depth;
seg.diameter = linerDiameter;
seg.roughness = roughnessFactor;
seg.sourceWellName = wellPath->name().toStdString();
seg.description = QString( "%1 #%2" ).arg( valve->name() ).arg( vi + 1 ).toStdString();
seg.intersections = { entry.ci };
WsegaicdRow wa;
wa.well = wellNameForExport;
wa.segment1 = segmentNumber;
wa.segment2 = segmentNumber;
wa.strength = aicdValues[AICD_STRENGTH];
wa.length = totalOverlapMD > 0.0 ? totalOverlapMD / entry.overlapMD : 0.0;
wa.densityCali = setOptional( aicdValues[AICD_DENSITY_CALIB_FLUID] );
wa.viscosityCali = setOptional( aicdValues[AICD_VISCOSITY_CALIB_FLUID] );
wa.criticalValue = setOptional( aicdValues[AICD_CRITICAL_WATER_IN_LIQUID_FRAC] );
wa.widthTrans = setOptional( aicdValues[AICD_EMULSION_VISC_TRANS_REGION] );
wa.maxViscRatio = setOptional( aicdValues[AICD_MAX_RATIO_EMULSION_VISC] );
wa.methodScalingFactor = 1;
wa.maxAbsRate = aicdValues[AICD_MAX_FLOW_RATE];
wa.flowRateExponent = aicdValues[AICD_VOL_FLOW_EXP];
wa.viscExponent = aicdValues[AICD_VISOSITY_FUNC_EXP];
wa.status = aicdParams->isOpen() ? "OPEN" : "SHUT";
wa.description = valve->name().toStdString();
seg.wsegaicdData = wa;
segmentNumber++;
result.push_back( RigMswBranch{ cellBranch, std::nullopt, { std::move( seg ) } } );
}
}
}
continue;
}
// ICV: one branch, one segment per valve location
const int valveBranch = ++branchNumber;
std::vector<RigMswSegment> branchSegs;
for ( size_t vi = 0; vi < valveLocations.size(); ++vi )
{
const double valveMD = valveLocations[vi];
const double valveEndMD = valveMD + RicMswTableDataTools::valveSegmentLength;
const double valveSegStart = valveSegs[vi].first;
const double valveSegEnd = valveSegs[vi].second;
const int outletSeg = findOutletSegmentForMD( cellSegMap, valveMD );
const double valveStartTVD = RicMswTableDataTools::tvdFromMeasuredDepth( wellPath, valveMD );
const double valveEndTVD = RicMswTableDataTools::tvdFromMeasuredDepth( wellPath, valveEndMD );
double length = 0.0;
double depth = 0.0;
if ( infoType == "INC" )
{
length = valveEndMD - valveMD;
depth = valveEndTVD - valveStartTVD;
}
else
{
length = valveEndMD;
depth = valveEndTVD;
}
RigMswSegment valveSeg;
valveSeg.segmentNumber = segmentNumber;
valveSeg.outletSegmentNumber = outletSeg;
valveSeg.length = length;
valveSeg.depth = depth;
valveSeg.diameter = linerDiameter;
valveSeg.roughness = roughnessFactor;
valveSeg.sourceWellName = wellPath->name().toStdString();
valveSeg.description = QString( "%1 #%2" ).arg( valve->name() ).arg( vi + 1 ).toStdString();
// COMPSEGS: cells that overlap this valve's coverage range (valveSegStart..valveSegEnd)
for ( const auto& cellInfo : filteredIntersections )
{
const double overlapStart = std::max( valveSegStart, cellInfo.startMD );
const double overlapEnd = std::min( valveSegEnd, cellInfo.endMD );
if ( overlapEnd > overlapStart )
{
if ( auto ci = toMswCellIntersection( cellInfo, mainGrid, overlapStart, overlapEnd ) )
valveSeg.intersections.push_back( *ci );
}
}
// WSEGVALV for ICV
WsegvalvRow wv;
wv.well = wellNameForExport;
wv.segmentNumber = segmentNumber;
wv.cv = valve->flowCoefficient();
wv.area = valve->area( unitSystem );
wv.status = valve->isOpen() ? "OPEN" : "SHUT";
wv.description = valve->name().toStdString();
valveSeg.wsegvalvData = wv;
segmentNumber++;
branchSegs.push_back( std::move( valveSeg ) );
}
result.push_back( RigMswBranch{ valveBranch, std::nullopt, std::move( branchSegs ) } );
}
}
return result;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RigMswBranch> buildFractureBranchesFromGeometry( RimEclipseCase* eclipseCase,
const RimWellPath* wellPath,
const RigMainGrid* mainGrid,
const std::vector<CellSegmentEntry>& cellSegMap,
const std::string& infoType,
int& segmentNumber,
int& branchNumber )
{
std::vector<RigMswBranch> result;
const QString wellNameForExport = wellPath->completionSettings()->wellNameForExport();
for ( RimWellPathFracture* fracture : wellPath->fractureCollection()->activeFractures() )
{
fracture->ensureValidNonDarcyProperties();
double position = fracture->fractureMD();
double width = fracture->fractureTemplate()->computeFractureWidth( fracture );
if ( fracture->fractureTemplate()->orientationType() == RimFractureTemplate::ALONG_WELL_PATH )
{
double perforationLength = fracture->fractureTemplate()->perforationLength();
position -= 0.5 * perforationLength;
width = perforationLength;
}
const double endMD = position + width;
const double startTVD = RicMswTableDataTools::tvdFromMeasuredDepth( wellPath, position );
const double endTVD = RicMswTableDataTools::tvdFromMeasuredDepth( wellPath, endMD );
double length = 0.0;
double depth = 0.0;
if ( infoType == "INC" )
{
length = width;
depth = endTVD - startTVD;
}
else
{
length = endMD;
depth = endTVD;
}
const int outletSeg = findOutletSegmentForMD( cellSegMap, position );
const int fracBranch = ++branchNumber;
// Get COMPSEGS cell intersections from fracture completion data
std::vector<RigCompletionData> completionData = RicExportFractureCompletionsImpl::generateCompdatValues( eclipseCase,
wellNameForExport,
wellPath->wellPathGeometry(),
{ fracture },
nullptr,
nullptr );
std::vector<RigMswCellIntersection> compsegs;
for ( const auto& compData : completionData )
{
const auto& cell = compData.completionDataGridCell();
// 0-based -> 1-based
size_t i = cell.localCellIndexI() + 1;
size_t j = cell.localCellIndexJ() + 1;
size_t k = cell.localCellIndexK() + 1;
// Shift K for dual porosity models
if ( mainGrid->isDualPorosity() ) k += mainGrid->cellCountK();
RigMswCellIntersection ci;
ci.i = i;
ci.j = j;
ci.k = k;
ci.distanceStart = position;
ci.distanceEnd = endMD;
ci.gridName = cell.lgrName().toStdString();
compsegs.push_back( ci );
}
RigMswSegment seg;
seg.segmentNumber = segmentNumber++;
seg.outletSegmentNumber = outletSeg;
seg.length = length;
seg.depth = depth;
seg.diameter = 0.15; // RicMswSegment default (tree approach uses same value)
seg.roughness = 5.0e-5; // RicMswSegment default (tree approach uses same value)
seg.sourceWellName = wellPath->name().toStdString();
seg.description = fracture->name().toStdString();
seg.intersections = std::move( compsegs );
result.push_back( RigMswBranch{ fracBranch, std::nullopt, { std::move( seg ) } } );
}
return result;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<RigMswBranch> buildFishbonesBranchesFromGeometry( const RimEclipseCase* eclipseCase,
const RimWellPath* wellPath,
const RigMainGrid* mainGrid,
const std::vector<WellPathCellIntersectionInfo>& filteredIntersections,
const std::vector<CellSegmentEntry>& cellSegMap,
const std::string& infoType,
const std::string& wellNameForExport,
int& segmentNumber,
int& branchNumber,
RiaDefines::EclipseUnitSystem unitSystem )
{
std::vector<RigMswBranch> result;
auto fishbonesSubs = wellPath->completions()->fishbonesCollection()->activeFishbonesSubs();
for ( const RimFishbones* subs : fishbonesSubs )
{
// Group laterals by sub index
std::map<size_t, std::vector<size_t>> subAndLateralIndices;
for ( const auto& [subIndex, lateralIndex] : subs->installedLateralIndices() )
subAndLateralIndices[subIndex].push_back( lateralIndex );
// Identify which filtered intersections are closest to each sub, and which
// intersection contains the sub's MD. This mirrors appendFishbonesMswExportInfo.
const double fishboneSectionStart = subs->startMD();
const double fishboneSectionEnd = subs->endMD();
std::map<size_t, std::vector<size_t>> closestSubForCellIntersections;
std::map<size_t, size_t> cellIntersectionContainingSubIndex;
for ( size_t ii = 0; ii < filteredIntersections.size(); ++ii )
{
const auto& ci = filteredIntersections[ii];
if ( fishboneSectionEnd < ci.startMD || fishboneSectionStart > ci.endMD ) continue;
const double midpoint = 0.5 * ( ci.startMD + ci.endMD );
size_t closestSubIdx = 0;
double closestDist = std::numeric_limits<double>::infinity();
for ( const auto& [subIdx, lats] : subAndLateralIndices )
{
const double subMD = subs->measuredDepth( subIdx );
if ( ci.startMD <= subMD && subMD <= ci.endMD ) cellIntersectionContainingSubIndex[subIdx] = ii;
const double dist = std::abs( subMD - midpoint );
if ( dist < closestDist )
{
closestDist = dist;
closestSubIdx = subIdx;
}
}
closestSubForCellIntersections[closestSubIdx].push_back( ii );
}
// ICD parameters
const double icdOrificeRadius = subs->icdOrificeDiameter( unitSystem ) / 2.0;
const double icdArea = icdOrificeRadius * icdOrificeRadius * cvf::PI_D * static_cast<double>( subs->icdCount() );
const double icdCv = subs->icdFlowCoefficient();
// Track used global cell indices to deduplicate COMPSEGS, matching the tree's intersectedCells logic.
std::set<size_t> usedGlobalCellIndices;
for ( const auto& [subIndex, lateralIndices] : subAndLateralIndices )
{
const double subEndMd = subs->measuredDepth( subIndex );
const double startValveMd = subEndMd - RicMswTableDataTools::valveSegmentLength;
const double startValveTVD = RicMswTableDataTools::tvdFromMeasuredDepth( wellPath, startValveMd );
const double endValveTVD = RicMswTableDataTools::tvdFromMeasuredDepth( wellPath, subEndMd );
const int outletSeg = findOutletSegmentForMD( cellSegMap, subEndMd );
const int icdBranch = ++branchNumber;
const int icdSegNum = segmentNumber++;
// COMPSEGS for the ICD segment: cells closest to this sub, clipped to [startValveMd, subEndMd]
std::set<size_t> icdCellIndices;
if ( closestSubForCellIntersections.count( subIndex ) )
{
for ( auto idx : closestSubForCellIntersections[subIndex] )
icdCellIndices.insert( idx );
}
if ( cellIntersectionContainingSubIndex.count( subIndex ) )
icdCellIndices.insert( cellIntersectionContainingSubIndex[subIndex] );
std::vector<RigMswCellIntersection> icdCompsegs;
for ( auto idx : icdCellIndices )
{
const auto& ci = filteredIntersections[idx];
const double overlapStart = std::max( startValveMd, ci.startMD );
const double overlapEnd = std::min( subEndMd, ci.endMD );
if ( overlapEnd <= overlapStart ) continue;
if ( auto mci = toMswCellIntersection( ci, mainGrid, overlapStart, overlapEnd ) )
{
icdCompsegs.push_back( *mci );
usedGlobalCellIndices.insert( ci.globCellIndex );
}
}
double icdLength = 0.0;
double icdDepth = 0.0;
if ( infoType == "INC" )
{
icdLength = subEndMd - startValveMd;
icdDepth = endValveTVD - startValveTVD;
}
else
{
icdLength = subEndMd;
icdDepth = endValveTVD;
}
RigMswSegment icdSeg;
icdSeg.segmentNumber = icdSegNum;
icdSeg.outletSegmentNumber = outletSeg;
icdSeg.length = icdLength;
icdSeg.depth = icdDepth;
// Use RigMswSegment defaults for diameter/roughness (matching tree approach for fishbones ICD)
icdSeg.sourceWellName = wellPath->name().toStdString();
icdSeg.description = QString( "ICD sub %1" ).arg( subIndex + 1 ).toStdString();
icdSeg.intersections = std::move( icdCompsegs );
WsegvalvRow wv;
wv.well = wellNameForExport;
wv.segmentNumber = icdSegNum;
wv.cv = icdCv;
wv.area = icdArea;
wv.status = "OPEN";
wv.description = QString( "ICD sub %1" ).arg( subIndex + 1 ).toStdString();
icdSeg.wsegvalvData = wv;
result.push_back( RigMswBranch{ icdBranch, std::nullopt, { std::move( icdSeg ) } } );
// Lateral sub-segments
for ( size_t lateralIndex : lateralIndices )
{
auto lateralCoordMDPairs = subs->coordsAndMDForLateral( subIndex, lateralIndex );
if ( lateralCoordMDPairs.empty() ) continue;
std::vector<cvf::Vec3d> lateralCoords;
std::vector<double> lateralMDs;
for ( auto& [coord, md] : lateralCoordMDPairs )
{
lateralCoords.push_back( coord );
lateralMDs.push_back( md );
}
auto lateralIntersections = RigWellPathIntersectionTools::findCellIntersectionInfosAlongPath( eclipseCase->eclipseCaseData(),
wellPath->name(),
lateralCoords,
lateralMDs );
if ( lateralIntersections.empty() ) continue;
const int latBranch = ++branchNumber;
double prevMD = lateralMDs.front();
double prevTVD = -lateralCoords.front().z();
int latOutletSeg = icdSegNum;
bool firstLatSeg = true;
const auto lateralLabel = QString( "Lateral %1" ).arg( lateralIndex + 1 ).toStdString();
std::vector<RigMswSegment> latSegs;
for ( const auto& cellIntInfo : lateralIntersections )
{
if ( auto mci = toMswCellIntersection( cellIntInfo, mainGrid, cellIntInfo.startMD, cellIntInfo.endMD ) )
{
double len = 0.0;
double dep = 0.0;
if ( infoType == "INC" )
{
len = cellIntInfo.endMD - prevMD;
dep = cellIntInfo.endTVD() - prevTVD;
}
else
{
len = cellIntInfo.endMD;
dep = cellIntInfo.endTVD();
}
// Deduplicate COMPSEGS: skip cells already connected (matching tree intersectedCells logic)
const bool isNewCell = usedGlobalCellIndices.insert( cellIntInfo.globCellIndex ).second;
RigMswSegment latSeg;
latSeg.segmentNumber = segmentNumber++;
latSeg.outletSegmentNumber = latOutletSeg;
latSeg.length = len;
latSeg.depth = dep;
latSeg.diameter = subs->equivalentDiameter( unitSystem );
latSeg.roughness = subs->openHoleRoughnessFactor( unitSystem );
latSeg.sourceWellName = wellPath->name().toStdString();
if ( firstLatSeg ) latSeg.description = lateralLabel;
latSeg.intersections = isNewCell ? std::vector<RigMswCellIntersection>{ *mci } : std::vector<RigMswCellIntersection>{};
latOutletSeg = latSeg.segmentNumber;
prevMD = cellIntInfo.endMD;
prevTVD = cellIntInfo.endTVD();
firstLatSeg = false;
latSegs.push_back( std::move( latSeg ) );
}
}
result.push_back( RigMswBranch{ latBranch, std::nullopt, std::move( latSegs ) } );
}
}
}
return result;
}
} // namespace RicMswBranchBuilder
@@ -0,0 +1,139 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "CompletionsMsw/RigMswSegment.h"
#include "RiaDefines.h"
#include "RigActiveCellInfo.h"
#include "Well/RigWellLogExtractor.h"
#include <optional>
#include <set>
#include <string>
#include <vector>
#include <QDateTime>
class RimEclipseCase;
class RimWellPath;
class RigMainGrid;
class RimPerforationInterval;
//--------------------------------------------------------------------------------------------------
/// Free functions that build RigMswSegment lists directly from well-path geometry and Rim
/// completion objects — without creating the intermediate RicMswBranch / RicMswItem tree.
///
/// Extracted from the former `internal` anonymous namespace in RicWellPathExportMswGeometryPath.cpp
/// so that they can be called from other translation units and unit-tested in isolation.
//--------------------------------------------------------------------------------------------------
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).
/// Used by valve, fracture, and lateral builders to locate the outlet segment for a given MD.
//--------------------------------------------------------------------------------------------------
struct CellSegmentEntry
{
double cellStartMD;
double cellEndMD;
int lastSubSegmentNumber;
};
//--------------------------------------------------------------------------------------------------
/// Convert a WellPathCellIntersectionInfo global-cell index to a RigMswCellIntersection (1-based i,j,k).
/// Returns std::nullopt for gap-segments (globCellIndex >= totalCellCount).|
//--------------------------------------------------------------------------------------------------
std::optional<RigMswCellIntersection>
toMswCellIntersection( const WellPathCellIntersectionInfo& cellInfo, const RigMainGrid* mainGrid, double distanceStart, double distanceEnd );
//--------------------------------------------------------------------------------------------------
/// Find the main-bore outlet segment number for a given measured depth.
/// Returns 1 (heel) if cellSegMap is empty; returns the last entry's segment number if md is
/// beyond the end of all mapped cells.
//--------------------------------------------------------------------------------------------------
int findOutletSegmentForMD( const std::vector<CellSegmentEntry>& cellSegMap, double md );
//--------------------------------------------------------------------------------------------------
/// 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.
//--------------------------------------------------------------------------------------------------
RigMswBranch buildMainBoreBranchFromGeometry( const RimWellPath* wellPath,
const std::vector<WellPathCellIntersectionInfo>& filteredIntersections,
const RigMainGrid* mainGrid,
const std::vector<const RimPerforationInterval*>& perforationIntervals,
const std::set<const RimPerforationInterval*>& valvedIntervals,
const std::string& infoType,
double heelMD,
double heelTVD,
int branchNumber,
int& segmentNumber,
int outletSegmentNumber,
double maxSegmentLength,
const std::vector<std::pair<double, double>>& customSegmentIntervals,
const std::optional<QDateTime>& exportDate,
RiaDefines::EclipseUnitSystem unitSystem,
std::vector<CellSegmentEntry>* cellSegMap,
const RigActiveCellInfo* activeCellInfo = nullptr );
//--------------------------------------------------------------------------------------------------
/// Build WELSEGS + COMPSEGS segments for ICD/ICV/AICD/SICD valve completions.
//--------------------------------------------------------------------------------------------------
std::vector<RigMswBranch> buildValveBranchesFromGeometry( const RimWellPath* wellPath,
const std::vector<WellPathCellIntersectionInfo>& filteredIntersections,
const RigMainGrid* mainGrid,
const std::vector<const RimPerforationInterval*>& perforationIntervals,
const std::vector<CellSegmentEntry>& cellSegMap,
const std::string& infoType,
const std::string& wellNameForExport,
int& segmentNumber,
int& branchNumber,
double maxSegmentLength,
const std::vector<std::pair<double, double>>& customSegmentIntervals,
const std::optional<QDateTime>& exportDate,
RiaDefines::EclipseUnitSystem unitSystem );
//--------------------------------------------------------------------------------------------------
/// Build WELSEGS + COMPSEGS segments for fracture completions.
//--------------------------------------------------------------------------------------------------
std::vector<RigMswBranch> buildFractureBranchesFromGeometry( RimEclipseCase* eclipseCase,
const RimWellPath* wellPath,
const RigMainGrid* mainGrid,
const std::vector<CellSegmentEntry>& cellSegMap,
const std::string& infoType,
int& segmentNumber,
int& branchNumber );
//--------------------------------------------------------------------------------------------------
/// Build WELSEGS + COMPSEGS + WSEGVALV segments for fishbones completions.
//--------------------------------------------------------------------------------------------------
std::vector<RigMswBranch> buildFishbonesBranchesFromGeometry( const RimEclipseCase* eclipseCase,
const RimWellPath* wellPath,
const RigMainGrid* mainGrid,
const std::vector<WellPathCellIntersectionInfo>& filteredIntersections,
const std::vector<CellSegmentEntry>& cellSegMap,
const std::string& infoType,
const std::string& wellNameForExport,
int& segmentNumber,
int& branchNumber,
RiaDefines::EclipseUnitSystem unitSystem );
} // namespace RicMswBranchBuilder
@@ -0,0 +1,495 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RicWellPathExportMswGeometryPath.h"
#include "RicMswBranchBuilder.h"
#include "RicMswTableDataTools.h"
#include "RicWellPathExportMswTableData.h"
#include "CompletionsMsw/RigMswTableData.h"
#include "RigActiveCellInfo.h"
#include "RigEclipseCaseData.h"
#include "Well/RigWellPath.h"
#include "RimEclipseCase.h"
#include "RimMswCompletionParameters.h"
#include "RimPerforationCollection.h"
#include "RimPerforationInterval.h"
#include "RimValveCollection.h"
#include "RimWellPath.h"
#include "RimWellPathCompletionSettings.h"
#include "RimWellPathTieIn.h"
#include "RimWellPathValve.h"
#include <algorithm>
namespace RicWellPathExportMswGeometryPath
{
using CompletionType = RicWellPathExportMswTableData::CompletionType;
//--------------------------------------------------------------------------------------------------
/// Recursively build all WELSEGS/COMPSEGS/valve segments for one lateral (child well path)
/// and any of its own child laterals.
//--------------------------------------------------------------------------------------------------
std::vector<RigMswBranch> buildLateralBranches( RimEclipseCase* eclipseCase,
const RimWellPath* wellPath,
const RigMainGrid* mainGrid,
int outletSegNum,
CompletionType completionType,
const std::optional<QDateTime>& exportDate,
int& segmentNumber,
int& branchNumber,
RiaDefines::EclipseUnitSystem unitSystem )
{
std::vector<RigMswBranch> result;
auto mswParameters = wellPath->mswCompletionParameters();
if ( !mswParameters ) return result;
const std::string infoType = mswParameters->lengthAndDepth().text().toStdString();
const double tieInMD = wellPath->wellPathTieIn()->tieInMeasuredDepth();
const double tieInTVD = -wellPath->wellPathGeometry()->interpolatedPointAlongWellPath( tieInMD ).z();
const std::string wellNameForExport = wellPath->completionSettings()->wellNameForExport().toStdString();
const int lateralBranchNum = ++branchNumber;
int childOutletSeg = outletSegNum;
// Optional ICV valve at the tie-in point — stored on the branch, not as a separate branch
std::optional<RigMswSegment> tieInValve;
const RimWellPathValve* outletValve = wellPath->wellPathTieIn()->outletValve();
if ( outletValve && outletValve->isChecked() )
{
const bool isActive = !exportDate.has_value() || outletValve->isActiveOnDate( *exportDate );
if ( isActive )
{
const double valveMD = wellPath->wellPathTieIn()->branchValveMeasuredDepth();
const double offset = ( valveMD == tieInMD ) ? RicMswTableDataTools::valveSegmentLength : 0.0;
const double valveEndMD = valveMD + offset;
const double valveStartTVD = RicMswTableDataTools::tvdFromMeasuredDepth( wellPath, valveMD );
const double valveEndTVD = RicMswTableDataTools::tvdFromMeasuredDepth( wellPath, valveEndMD );
double length = 0.0;
double depth = 0.0;
if ( infoType == "INC" )
{
length = valveEndMD - valveMD;
depth = valveEndTVD - valveStartTVD;
}
else
{
length = valveEndMD;
depth = valveEndTVD;
}
const int valveSegNum = segmentNumber++;
RigMswSegment valveSeg;
valveSeg.segmentNumber = valveSegNum;
valveSeg.outletSegmentNumber = outletSegNum;
valveSeg.length = length;
valveSeg.depth = depth;
valveSeg.diameter = mswParameters->linerDiameter( unitSystem );
valveSeg.roughness = mswParameters->roughnessFactor( unitSystem );
valveSeg.sourceWellName = wellPath->name().toStdString();
valveSeg.description = QString( "%1 valve for %2" ).arg( outletValve->componentLabel() ).arg( wellPath->name() ).toStdString();
WsegvalvRow wv;
wv.well = wellNameForExport;
wv.segmentNumber = valveSegNum;
wv.cv = outletValve->flowCoefficient();
wv.area = outletValve->area( unitSystem );
wv.status = outletValve->isOpen() ? "OPEN" : "SHUT";
wv.description = valveSeg.description;
valveSeg.wsegvalvData = wv;
tieInValve = std::move( valveSeg );
childOutletSeg = valveSegNum;
}
}
// Child main-bore segments
auto cellIntersections = RicWellPathExportMswTableData::generateCellSegments( eclipseCase, wellPath );
auto filteredIntersections =
RicWellPathExportMswTableData::filterIntersections( cellIntersections, tieInMD, wellPath->wellPathGeometry(), eclipseCase );
const bool includePerforations = ( completionType & CompletionType::PERFORATIONS ) == CompletionType::PERFORATIONS;
const std::vector<const RimPerforationInterval*> perforationIntervals =
includePerforations ? wellPath->perforationIntervalCollection()->activePerforations() : std::vector<const RimPerforationInterval*>();
std::set<const RimPerforationInterval*> valvedIntervals;
for ( const auto* perf : perforationIntervals )
{
if ( exportDate.has_value() && !perf->isActiveOnDate( exportDate.value() ) ) continue;
for ( const auto* valve : perf->descendantsIncludingThisOfType<RimWellPathValve>() )
{
if ( !valve->isChecked() ) continue;
if ( exportDate.has_value() && !valve->isActiveOnDate( exportDate.value() ) ) continue;
const auto t = valve->componentType();
if ( t == RiaDefines::WellPathComponentType::ICD || t == RiaDefines::WellPathComponentType::ICV ||
t == RiaDefines::WellPathComponentType::AICD || t == RiaDefines::WellPathComponentType::SICD )
{
valvedIntervals.insert( perf );
break;
}
}
}
std::vector<RicMswBranchBuilder::CellSegmentEntry> childCellSegMap;
const RigActiveCellInfo* activeCellInfo = eclipseCase->eclipseCaseData()->activeCellInfo( RiaDefines::PorosityModelType::MATRIX_MODEL );
auto lateralBranch = RicMswBranchBuilder::buildMainBoreBranchFromGeometry( wellPath,
filteredIntersections,
mainGrid,
perforationIntervals,
valvedIntervals,
infoType,
tieInMD,
tieInTVD,
lateralBranchNum,
segmentNumber,
childOutletSeg,
mswParameters->maxSegmentLength(),
{},
exportDate,
unitSystem,
&childCellSegMap,
activeCellInfo );
if ( tieInValve && !lateralBranch.segments.empty() ) lateralBranch.segments.front().description.clear();
lateralBranch.tieInValve = std::move( tieInValve );
// Standalone valves (from valveCollection, not inside perforation intervals)
for ( const auto* valve : wellPath->valveCollection()->activeValves() )
{
if ( exportDate.has_value() && !valve->isActiveOnDate( *exportDate ) ) continue;
const int segNum = RicMswBranchBuilder::findOutletSegmentForMD( childCellSegMap, valve->startMD() );
for ( auto& seg : lateralBranch.segments )
{
if ( seg.segmentNumber == segNum )
{
WsegvalvRow wv;
wv.well = wellNameForExport;
wv.segmentNumber = segNum;
wv.cv = valve->flowCoefficient();
wv.area = valve->area( unitSystem );
wv.status = valve->isOpen() ? "OPEN" : "SHUT";
wv.description = valve->name().toStdString();
seg.wsegvalvData = wv;
break;
}
}
}
result.push_back( std::move( lateralBranch ) );
auto valveBranches = RicMswBranchBuilder::buildValveBranchesFromGeometry( wellPath,
filteredIntersections,
mainGrid,
perforationIntervals,
childCellSegMap,
infoType,
wellNameForExport,
segmentNumber,
branchNumber,
mswParameters->maxSegmentLength(),
{},
exportDate,
unitSystem );
result.insert( result.end(), std::make_move_iterator( valveBranches.begin() ), std::make_move_iterator( valveBranches.end() ) );
if ( ( completionType & CompletionType::FRACTURES ) == CompletionType::FRACTURES )
{
auto fracBranches = RicMswBranchBuilder::buildFractureBranchesFromGeometry( eclipseCase,
wellPath,
mainGrid,
childCellSegMap,
infoType,
segmentNumber,
branchNumber );
result.insert( result.end(), std::make_move_iterator( fracBranches.begin() ), std::make_move_iterator( fracBranches.end() ) );
}
if ( ( completionType & CompletionType::FISHBONES ) == CompletionType::FISHBONES )
{
auto fishBranches = RicMswBranchBuilder::buildFishbonesBranchesFromGeometry( eclipseCase,
wellPath,
mainGrid,
filteredIntersections,
childCellSegMap,
infoType,
wellNameForExport,
segmentNumber,
branchNumber,
unitSystem );
result.insert( result.end(), std::make_move_iterator( fishBranches.begin() ), std::make_move_iterator( fishBranches.end() ) );
}
// Recurse into grandchildren
for ( auto* grandchild : RicWellPathExportMswTableData::wellPathsWithTieIn( wellPath ) )
{
const int grandchildOutlet =
RicMswBranchBuilder::findOutletSegmentForMD( childCellSegMap, grandchild->wellPathTieIn()->branchValveMeasuredDepth() );
auto grandchildBranches =
buildLateralBranches( eclipseCase, grandchild, mainGrid, grandchildOutlet, completionType, exportDate, segmentNumber, branchNumber, unitSystem );
result.insert( result.end(), std::make_move_iterator( grandchildBranches.begin() ), std::make_move_iterator( grandchildBranches.end() ) );
}
return result;
}
//--------------------------------------------------------------------------------------------------
/// Build MSW export data directly from well-path geometry and Rim completion objects,
/// without building the RicMswBranch / RicMswItem tree.
///
/// Currently implemented: main-bore WELSEGS segments + perforation COMPSEGS entries.
/// TODO: valve completions (ICD/AICD/SICD/ICV), fishbones laterals, fractures, tie-in wells.
//--------------------------------------------------------------------------------------------------
RigMswWellExportData buildMswWellExportData( RimEclipseCase* eclipseCase,
const RimWellPath* wellPath,
double maxSegmentLength,
const std::vector<std::pair<double, double>>& customSegmentIntervals,
CompletionType completionType,
const std::optional<QDateTime>& exportDate )
{
auto mswParameters = wellPath->mswCompletionParameters();
CVF_ASSERT( mswParameters );
const RiaDefines::EclipseUnitSystem unitSystem = eclipseCase->eclipseCaseData()->unitsType();
const RigMainGrid* mainGrid = eclipseCase->mainGrid();
const std::string infoType = mswParameters->lengthAndDepth().text().toStdString();
auto cellIntersections = RicWellPathExportMswTableData::generateCellSegments( eclipseCase, wellPath );
double initialMD = RicWellPathExportMswTableData::computeIntitialMeasuredDepth( eclipseCase, wellPath, mswParameters, cellIntersections );
double initialTVD = -wellPath->wellPathGeometry()->interpolatedPointAlongWellPath( initialMD ).z();
auto filteredIntersections =
RicWellPathExportMswTableData::filterIntersections( cellIntersections, initialMD, wellPath->wellPathGeometry(), eclipseCase );
WelsegsHeader header;
header.well = wellPath->completionSettings()->wellNameForExport().toStdString();
header.topLength = initialMD;
header.topDepth = initialTVD;
header.infoType = infoType;
header.pressureComponents = mswParameters->pressureDrop().text().toStdString();
const bool includePerforations = ( completionType & CompletionType::PERFORATIONS ) == CompletionType::PERFORATIONS;
const std::vector<const RimPerforationInterval*> perforationIntervals =
includePerforations ? wellPath->perforationIntervalCollection()->activePerforations() : std::vector<const RimPerforationInterval*>();
// Determine which perforation intervals have at least one active, checked valve.
// Main bore COMPSEGS are skipped for these; the valve builder will emit them instead.
std::set<const RimPerforationInterval*> valvedIntervals;
for ( const auto* perf : perforationIntervals )
{
if ( exportDate.has_value() && !perf->isActiveOnDate( exportDate.value() ) ) continue;
for ( const auto* valve : perf->descendantsIncludingThisOfType<RimWellPathValve>() )
{
if ( !valve->isChecked() ) continue;
if ( exportDate.has_value() && !valve->isActiveOnDate( exportDate.value() ) ) continue;
const auto t = valve->componentType();
if ( t == RiaDefines::WellPathComponentType::ICD || t == RiaDefines::WellPathComponentType::ICV ||
t == RiaDefines::WellPathComponentType::AICD || t == RiaDefines::WellPathComponentType::SICD )
{
valvedIntervals.insert( perf );
break;
}
}
}
int segmentNumber = 2; // Segment 1 is the implicit well heel.
int branchNumber = 1; // Incremented for each new branch.
std::vector<RicMswBranchBuilder::CellSegmentEntry> cellSegMap;
const RigActiveCellInfo* activeCellInfo = eclipseCase->eclipseCaseData()->activeCellInfo( RiaDefines::PorosityModelType::MATRIX_MODEL );
auto mainBoreBranch = RicMswBranchBuilder::buildMainBoreBranchFromGeometry( wellPath,
filteredIntersections,
mainGrid,
perforationIntervals,
valvedIntervals,
infoType,
initialMD,
initialTVD,
branchNumber,
segmentNumber,
1, // outlet = heel (segment 1)
maxSegmentLength,
customSegmentIntervals,
exportDate,
unitSystem,
&cellSegMap,
activeCellInfo );
const std::string wellNameForExport = wellPath->completionSettings()->wellNameForExport().toStdString();
// Standalone valves (from valveCollection, not inside perforation intervals)
for ( const auto* valve : wellPath->valveCollection()->activeValves() )
{
if ( exportDate.has_value() && !valve->isActiveOnDate( *exportDate ) ) continue;
const int segNum = RicMswBranchBuilder::findOutletSegmentForMD( cellSegMap, valve->startMD() );
for ( auto& seg : mainBoreBranch.segments )
{
if ( seg.segmentNumber == segNum )
{
WsegvalvRow wv;
wv.well = wellNameForExport;
wv.segmentNumber = segNum;
wv.cv = valve->flowCoefficient();
wv.area = valve->area( unitSystem );
wv.status = valve->isOpen() ? "OPEN" : "SHUT";
wv.description = valve->name().toStdString();
seg.wsegvalvData = wv;
break;
}
}
}
auto valveBranches = RicMswBranchBuilder::buildValveBranchesFromGeometry( wellPath,
filteredIntersections,
mainGrid,
perforationIntervals,
cellSegMap,
infoType,
wellNameForExport,
segmentNumber,
branchNumber,
maxSegmentLength,
customSegmentIntervals,
exportDate,
unitSystem );
const bool includeFractures = ( completionType & CompletionType::FRACTURES ) == CompletionType::FRACTURES;
std::vector<RigMswBranch> fractureBranches;
if ( includeFractures )
{
fractureBranches =
RicMswBranchBuilder::buildFractureBranchesFromGeometry( eclipseCase, wellPath, mainGrid, cellSegMap, infoType, segmentNumber, branchNumber );
}
const bool includeFishbones = ( completionType & CompletionType::FISHBONES ) == CompletionType::FISHBONES;
std::vector<RigMswBranch> fishbonesBranches;
if ( includeFishbones )
{
fishbonesBranches = RicMswBranchBuilder::buildFishbonesBranchesFromGeometry( eclipseCase,
wellPath,
mainGrid,
filteredIntersections,
cellSegMap,
infoType,
wellNameForExport,
segmentNumber,
branchNumber,
unitSystem );
}
// Tie-in child laterals (recursive)
std::vector<RigMswBranch> lateralBranches;
for ( auto* childWellPath : RicWellPathExportMswTableData::wellPathsWithTieIn( wellPath ) )
{
const int childOutlet =
RicMswBranchBuilder::findOutletSegmentForMD( cellSegMap, childWellPath->wellPathTieIn()->branchValveMeasuredDepth() );
auto childBranches =
buildLateralBranches( eclipseCase, childWellPath, mainGrid, childOutlet, completionType, exportDate, segmentNumber, branchNumber, unitSystem );
lateralBranches.insert( lateralBranches.end(),
std::make_move_iterator( childBranches.begin() ),
std::make_move_iterator( childBranches.end() ) );
}
RigMswWellExportData result;
result.header = header;
result.branches.push_back( std::move( mainBoreBranch ) );
result.branches.insert( result.branches.end(),
std::make_move_iterator( valveBranches.begin() ),
std::make_move_iterator( valveBranches.end() ) );
result.branches.insert( result.branches.end(),
std::make_move_iterator( fractureBranches.begin() ),
std::make_move_iterator( fractureBranches.end() ) );
result.branches.insert( result.branches.end(),
std::make_move_iterator( fishbonesBranches.begin() ),
std::make_move_iterator( fishbonesBranches.end() ) );
result.branches.insert( result.branches.end(),
std::make_move_iterator( lateralBranches.begin() ),
std::make_move_iterator( lateralBranches.end() ) );
return result;
}
//--------------------------------------------------------------------------------------------------
/// Populate RigMswTableData from a pre-built RigMswWellExportData.
/// This replaces the recursive tree-based collection functions (collectWelsegsData etc.) with
/// simple iteration over RigMswSegment objects.
//--------------------------------------------------------------------------------------------------
RigMswTableData collectTableData( const RigMswWellExportData& exportData, RiaDefines::EclipseUnitSystem unitSystem )
{
RigMswTableData tableData( exportData.header.well, unitSystem );
tableData.setWelsegsHeader( exportData.header );
auto emitSegment = [&]( const RigMswBranch& branch, const RigMswSegment& seg )
{
// WELSEGS row
WelsegsRow welsegsRow;
welsegsRow.segment1 = seg.segmentNumber;
welsegsRow.segment2 = seg.segmentNumber;
welsegsRow.branch = branch.branchNumber;
welsegsRow.joinSegment = seg.outletSegmentNumber;
welsegsRow.length = seg.length;
welsegsRow.depth = seg.depth;
welsegsRow.diameter = seg.diameter;
welsegsRow.roughness = seg.roughness;
welsegsRow.description = seg.description;
welsegsRow.sourceWellName = seg.sourceWellName;
tableData.addWelsegsRow( welsegsRow );
// COMPSEGS rows
for ( const auto& inter : seg.intersections )
{
CompsegsRow compRow;
compRow.i = inter.i;
compRow.j = inter.j;
compRow.k = inter.k;
compRow.branch = branch.branchNumber;
compRow.distanceStart = inter.distanceStart;
compRow.distanceEnd = inter.distanceEnd;
compRow.gridName = inter.gridName;
tableData.addCompsegsRow( compRow );
}
// WSEGVALV row
if ( seg.wsegvalvData ) tableData.addWsegvalvRow( *seg.wsegvalvData );
// WSEGAICD row
if ( seg.wsegaicdData ) tableData.addWsegaicdRow( *seg.wsegaicdData );
// WSEGSICD row
if ( seg.wsegsicdData ) tableData.addWsegsicdRow( *seg.wsegsicdData );
};
for ( const auto& branch : exportData.branches )
{
if ( branch.tieInValve ) emitSegment( branch, *branch.tieInValve );
for ( const auto& seg : branch.segments )
emitSegment( branch, seg );
tableData.addMswBranch( branch );
}
return tableData;
}
} // namespace RicWellPathExportMswGeometryPath
@@ -0,0 +1,61 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 Equinor ASA
//
// ResInsight is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.
//
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "RicWellPathExportMswTableData.h"
#include "CompletionsMsw/RigMswSegment.h"
#include "CompletionsMsw/RigMswTableData.h"
#include <optional>
#include <vector>
#include <QDateTime>
class RimEclipseCase;
class RimWellPath;
class RigMainGrid;
//--------------------------------------------------------------------------------------------------
/// Free functions that implement the direct geometry path for MSW export.
/// These build WELSEGS/COMPSEGS/valve segment data directly from well-path geometry
//--------------------------------------------------------------------------------------------------
namespace RicWellPathExportMswGeometryPath
{
std::vector<RigMswBranch> buildLateralBranches( RimEclipseCase* eclipseCase,
const RimWellPath* wellPath,
const RigMainGrid* mainGrid,
int outletSegNum,
RicWellPathExportMswTableData::CompletionType completionType,
const std::optional<QDateTime>& exportDate,
int& segmentNumber,
int& branchNumber,
RiaDefines::EclipseUnitSystem unitSystem );
RigMswWellExportData buildMswWellExportData( RimEclipseCase* eclipseCase,
const RimWellPath* wellPath,
double maxSegmentLength,
const std::vector<std::pair<double, double>>& customSegmentIntervals,
RicWellPathExportMswTableData::CompletionType completionType,
const std::optional<QDateTime>& exportDate );
RigMswTableData collectTableData( const RigMswWellExportData& exportData, RiaDefines::EclipseUnitSystem unitSystem );
} // namespace RicWellPathExportMswGeometryPath
@@ -205,4 +205,6 @@ std::vector<std::pair<double, double>> createSubSegmentMDPairs( double
double tvdFromMeasuredDepth( gsl::not_null<const RimWellPath*> wellPath, double measuredDepth );
inline constexpr double valveSegmentLength = 0.1;
} // namespace RicMswTableDataTools
@@ -20,6 +20,7 @@
#include "RiaLogging.h"
#include "MswExport/RicWellPathExportMswGeometryPath.h"
#include "RicExportCompletionDataSettingsUi.h"
#include "RicExportFractureCompletionsImpl.h"
#include "RicMswCompletions.h"
@@ -68,6 +69,57 @@ std::expected<RigMswTableData, std::string>
bool exportCompletionsAfterMainBoreSegments,
CompletionType completionType,
const std::optional<QDateTime>& exportDate )
{
bool exportAsTree = true; // This can be made configurable if needed, but for now we will always export as tree structure to preserve
// the hierarchy of the well path segments and completions
if ( exportAsTree )
{
return extractSingleWellMswDataTree( eclipseCase, wellPath, exportCompletionsAfterMainBoreSegments, completionType, exportDate );
}
else
{
return extractSingleWellMswDataGeometry( eclipseCase, wellPath, exportCompletionsAfterMainBoreSegments, completionType, exportDate );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::expected<RigMswTableData, std::string>
RicWellPathExportMswTableData::extractSingleWellMswDataGeometry( RimEclipseCase* eclipseCase,
RimWellPath* wellPath,
bool exportCompletionsAfterMainBoreSegments,
CompletionType completionType,
const std::optional<QDateTime>& exportDate )
{
if ( !eclipseCase || !wellPath || eclipseCase->eclipseCaseData() == nullptr )
return std::unexpected( "Invalid eclipse case or well path provided" );
auto mswParameters = wellPath->mswCompletionParameters();
if ( !mswParameters ) return std::unexpected( "Missing MSW completion parameters" );
const std::vector<std::pair<double, double>> customSegmentIntervals = mswParameters->getSegmentIntervals();
auto wellExportData = RicWellPathExportMswGeometryPath::buildMswWellExportData( eclipseCase,
wellPath,
mswParameters->maxSegmentLength(),
customSegmentIntervals,
completionType,
exportDate );
auto unitSystem = eclipseCase->eclipseCaseData()->unitsType();
return RicWellPathExportMswGeometryPath::collectTableData( wellExportData, unitSystem );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::expected<RigMswTableData, std::string>
RicWellPathExportMswTableData::extractSingleWellMswDataTree( RimEclipseCase* eclipseCase,
RimWellPath* wellPath,
bool exportCompletionsAfterMainBoreSegments,
CompletionType completionType,
const std::optional<QDateTime>& exportDate )
{
if ( !eclipseCase || !wellPath || eclipseCase->eclipseCaseData() == nullptr )
{
@@ -70,6 +70,19 @@ public:
CompletionType completionType = CompletionType::ALL,
const std::optional<QDateTime>& exportDate = std::nullopt );
static std::expected<RigMswTableData, std::string> extractSingleWellMswDataTree( RimEclipseCase* eclipseCase,
RimWellPath* wellPath,
bool exportCompletionsAfterMainBoreSegments = true,
CompletionType completionType = CompletionType::ALL,
const std::optional<QDateTime>& exportDate = std::nullopt );
static std::expected<RigMswTableData, std::string>
extractSingleWellMswDataGeometry( RimEclipseCase* eclipseCase,
RimWellPath* wellPath,
bool exportCompletionsAfterMainBoreSegments = true,
CompletionType completionType = CompletionType::ALL,
const std::optional<QDateTime>& exportDate = std::nullopt );
static CompletionType convertFromExportSettings( const class RicExportCompletionDataSettingsUi& settings );
static void generateFishbonesMswExportInfoForWell( const RimEclipseCase* eclipseCase,
@@ -77,6 +90,20 @@ public:
RicMswExportInfo* exportInfo,
RicMswBranch* branch );
static std::vector<WellPathCellIntersectionInfo> generateCellSegments( const RimEclipseCase* eclipseCase, const RimWellPath* wellPath );
static std::vector<WellPathCellIntersectionInfo> filterIntersections( const std::vector<WellPathCellIntersectionInfo>& intersections,
double initialMD,
gsl::not_null<const RigWellPath*> wellPathGeometry,
gsl::not_null<const RimEclipseCase*> eclipseCase );
static std::vector<RimWellPath*> wellPathsWithTieIn( const RimWellPath* wellPath );
static double computeIntitialMeasuredDepth( const RimEclipseCase* eclipseCase,
const RimWellPath* wellPath,
const RimMswCompletionParameters* mswParameters,
const std::vector<WellPathCellIntersectionInfo>& allIntersections );
private:
static void updateDataForMultipleItemsInSameGridCell( gsl::not_null<RicMswBranch*> branch );
@@ -103,18 +130,6 @@ private:
gsl::not_null<RicMswExportInfo*> exportInfo,
gsl::not_null<RicMswBranch*> branch );
static std::vector<WellPathCellIntersectionInfo> generateCellSegments( const RimEclipseCase* eclipseCase, const RimWellPath* wellPath );
static double computeIntitialMeasuredDepth( const RimEclipseCase* eclipseCase,
const RimWellPath* wellPath,
const RimMswCompletionParameters* mswParameters,
const std::vector<WellPathCellIntersectionInfo>& allIntersections );
static std::vector<WellPathCellIntersectionInfo> filterIntersections( const std::vector<WellPathCellIntersectionInfo>& intersections,
double initialMD,
gsl::not_null<const RigWellPath*> wellPathGeometry,
gsl::not_null<const RimEclipseCase*> eclipseCase );
static std::pair<double, double> calculateOverlapWithActiveCells( double startMD,
double endMD,
const std::vector<WellPathCellIntersectionInfo>& wellPathIntersections,
@@ -184,8 +199,6 @@ private:
gsl::not_null<int*> branchNumber );
static std::unique_ptr<RicMswBranch> createChildMswBranch( const RimWellPath* childWellPath );
static std::vector<RimWellPath*> wellPathsWithTieIn( const RimWellPath* wellPath );
};
ENABLE_BITMASK_OPERATORS( RicWellPathExportMswTableData::CompletionType )