diff --git a/ApplicationLibCode/Commands/CMakeLists.txt b/ApplicationLibCode/Commands/CMakeLists.txt index 0ecee04926..2189f63d98 100644 --- a/ApplicationLibCode/Commands/CMakeLists.txt +++ b/ApplicationLibCode/Commands/CMakeLists.txt @@ -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") diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/CMakeLists_files.cmake b/ApplicationLibCode/Commands/CompletionExportCommands/CMakeLists_files.cmake index f8193ed570..7d5877a2a9 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/CMakeLists_files.cmake +++ b/ApplicationLibCode/Commands/CompletionExportCommands/CMakeLists_files.cmake @@ -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 diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicMswBranchBuilder.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicMswBranchBuilder.cpp new file mode 100644 index 0000000000..0c53880893 --- /dev/null +++ b/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicMswBranchBuilder.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 +// 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 +#include + +namespace RicMswBranchBuilder +{ + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::optional + 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& 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& 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& 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::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& filteredIntersections, + const RigMainGrid* mainGrid, + const std::vector& perforationIntervals, + const std::set& valvedIntervals, + const std::string& infoType, + double heelMD, + double heelTVD, + int branchNumber, + int& segmentNumber, + int outletSegmentNumber, + double maxSegmentLength, + const std::vector>& customSegmentIntervals, + const std::optional& exportDate, + RiaDefines::EclipseUnitSystem unitSystem, + std::vector* cellSegMap, + const RigActiveCellInfo* activeCellInfo ) +{ + std::vector 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 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 buildValveBranchesFromGeometry( const RimWellPath* wellPath, + const std::vector& filteredIntersections, + const RigMainGrid* mainGrid, + const std::vector& perforationIntervals, + const std::vector& cellSegMap, + const std::string& infoType, + const std::string& wellNameForExport, + int& segmentNumber, + int& branchNumber, + double maxSegmentLength, + const std::vector>& customSegmentIntervals, + const std::optional& exportDate, + RiaDefines::EclipseUnitSystem unitSystem ) +{ + std::vector 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(); + 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 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 + { return std::isinf( value ) ? std::nullopt : std::optional{ 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 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 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 buildFractureBranchesFromGeometry( RimEclipseCase* eclipseCase, + const RimWellPath* wellPath, + const RigMainGrid* mainGrid, + const std::vector& cellSegMap, + const std::string& infoType, + int& segmentNumber, + int& branchNumber ) +{ + std::vector 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 completionData = RicExportFractureCompletionsImpl::generateCompdatValues( eclipseCase, + wellNameForExport, + wellPath->wellPathGeometry(), + { fracture }, + nullptr, + nullptr ); + + std::vector 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 buildFishbonesBranchesFromGeometry( const RimEclipseCase* eclipseCase, + const RimWellPath* wellPath, + const RigMainGrid* mainGrid, + const std::vector& filteredIntersections, + const std::vector& cellSegMap, + const std::string& infoType, + const std::string& wellNameForExport, + int& segmentNumber, + int& branchNumber, + RiaDefines::EclipseUnitSystem unitSystem ) +{ + std::vector result; + + auto fishbonesSubs = wellPath->completions()->fishbonesCollection()->activeFishbonesSubs(); + for ( const RimFishbones* subs : fishbonesSubs ) + { + // Group laterals by sub index + std::map> 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> closestSubForCellIntersections; + std::map 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::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( subs->icdCount() ); + const double icdCv = subs->icdFlowCoefficient(); + + // Track used global cell indices to deduplicate COMPSEGS, matching the tree's intersectedCells logic. + std::set 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 icdCellIndices; + if ( closestSubForCellIntersections.count( subIndex ) ) + { + for ( auto idx : closestSubForCellIntersections[subIndex] ) + icdCellIndices.insert( idx ); + } + if ( cellIntersectionContainingSubIndex.count( subIndex ) ) + icdCellIndices.insert( cellIntersectionContainingSubIndex[subIndex] ); + + std::vector 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 lateralCoords; + std::vector 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 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{ *mci } : std::vector{}; + + 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 diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicMswBranchBuilder.h b/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicMswBranchBuilder.h new file mode 100644 index 0000000000..a4dc9f744d --- /dev/null +++ b/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicMswBranchBuilder.h @@ -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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "CompletionsMsw/RigMswSegment.h" +#include "RiaDefines.h" +#include "RigActiveCellInfo.h" +#include "Well/RigWellLogExtractor.h" + +#include +#include +#include +#include + +#include + +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 + 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& 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& filteredIntersections, + const RigMainGrid* mainGrid, + const std::vector& perforationIntervals, + const std::set& valvedIntervals, + const std::string& infoType, + double heelMD, + double heelTVD, + int branchNumber, + int& segmentNumber, + int outletSegmentNumber, + double maxSegmentLength, + const std::vector>& customSegmentIntervals, + const std::optional& exportDate, + RiaDefines::EclipseUnitSystem unitSystem, + std::vector* cellSegMap, + const RigActiveCellInfo* activeCellInfo = nullptr ); + +//-------------------------------------------------------------------------------------------------- +/// Build WELSEGS + COMPSEGS segments for ICD/ICV/AICD/SICD valve completions. +//-------------------------------------------------------------------------------------------------- +std::vector buildValveBranchesFromGeometry( const RimWellPath* wellPath, + const std::vector& filteredIntersections, + const RigMainGrid* mainGrid, + const std::vector& perforationIntervals, + const std::vector& cellSegMap, + const std::string& infoType, + const std::string& wellNameForExport, + int& segmentNumber, + int& branchNumber, + double maxSegmentLength, + const std::vector>& customSegmentIntervals, + const std::optional& exportDate, + RiaDefines::EclipseUnitSystem unitSystem ); + +//-------------------------------------------------------------------------------------------------- +/// Build WELSEGS + COMPSEGS segments for fracture completions. +//-------------------------------------------------------------------------------------------------- +std::vector buildFractureBranchesFromGeometry( RimEclipseCase* eclipseCase, + const RimWellPath* wellPath, + const RigMainGrid* mainGrid, + const std::vector& cellSegMap, + const std::string& infoType, + int& segmentNumber, + int& branchNumber ); + +//-------------------------------------------------------------------------------------------------- +/// Build WELSEGS + COMPSEGS + WSEGVALV segments for fishbones completions. +//-------------------------------------------------------------------------------------------------- +std::vector buildFishbonesBranchesFromGeometry( const RimEclipseCase* eclipseCase, + const RimWellPath* wellPath, + const RigMainGrid* mainGrid, + const std::vector& filteredIntersections, + const std::vector& cellSegMap, + const std::string& infoType, + const std::string& wellNameForExport, + int& segmentNumber, + int& branchNumber, + RiaDefines::EclipseUnitSystem unitSystem ); + +} // namespace RicMswBranchBuilder diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicWellPathExportMswGeometryPath.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicWellPathExportMswGeometryPath.cpp new file mode 100644 index 0000000000..72d4cd5327 --- /dev/null +++ b/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicWellPathExportMswGeometryPath.cpp @@ -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 +// 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 + +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 buildLateralBranches( RimEclipseCase* eclipseCase, + const RimWellPath* wellPath, + const RigMainGrid* mainGrid, + int outletSegNum, + CompletionType completionType, + const std::optional& exportDate, + int& segmentNumber, + int& branchNumber, + RiaDefines::EclipseUnitSystem unitSystem ) +{ + std::vector 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 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 perforationIntervals = + includePerforations ? wellPath->perforationIntervalCollection()->activePerforations() : std::vector(); + + std::set valvedIntervals; + for ( const auto* perf : perforationIntervals ) + { + if ( exportDate.has_value() && !perf->isActiveOnDate( exportDate.value() ) ) continue; + for ( const auto* valve : perf->descendantsIncludingThisOfType() ) + { + 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 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>& customSegmentIntervals, + CompletionType completionType, + const std::optional& 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 perforationIntervals = + includePerforations ? wellPath->perforationIntervalCollection()->activePerforations() : std::vector(); + + // 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 valvedIntervals; + for ( const auto* perf : perforationIntervals ) + { + if ( exportDate.has_value() && !perf->isActiveOnDate( exportDate.value() ) ) continue; + for ( const auto* valve : perf->descendantsIncludingThisOfType() ) + { + 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 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 fractureBranches; + if ( includeFractures ) + { + fractureBranches = + RicMswBranchBuilder::buildFractureBranchesFromGeometry( eclipseCase, wellPath, mainGrid, cellSegMap, infoType, segmentNumber, branchNumber ); + } + + const bool includeFishbones = ( completionType & CompletionType::FISHBONES ) == CompletionType::FISHBONES; + std::vector fishbonesBranches; + if ( includeFishbones ) + { + fishbonesBranches = RicMswBranchBuilder::buildFishbonesBranchesFromGeometry( eclipseCase, + wellPath, + mainGrid, + filteredIntersections, + cellSegMap, + infoType, + wellNameForExport, + segmentNumber, + branchNumber, + unitSystem ); + } + + // Tie-in child laterals (recursive) + std::vector 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 diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicWellPathExportMswGeometryPath.h b/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicWellPathExportMswGeometryPath.h new file mode 100644 index 0000000000..bf573eecbd --- /dev/null +++ b/ApplicationLibCode/Commands/CompletionExportCommands/MswExport/RicWellPathExportMswGeometryPath.h @@ -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 +// for more details. +// +///////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "RicWellPathExportMswTableData.h" + +#include "CompletionsMsw/RigMswSegment.h" +#include "CompletionsMsw/RigMswTableData.h" + +#include +#include + +#include + +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 buildLateralBranches( RimEclipseCase* eclipseCase, + const RimWellPath* wellPath, + const RigMainGrid* mainGrid, + int outletSegNum, + RicWellPathExportMswTableData::CompletionType completionType, + const std::optional& exportDate, + int& segmentNumber, + int& branchNumber, + RiaDefines::EclipseUnitSystem unitSystem ); + +RigMswWellExportData buildMswWellExportData( RimEclipseCase* eclipseCase, + const RimWellPath* wellPath, + double maxSegmentLength, + const std::vector>& customSegmentIntervals, + RicWellPathExportMswTableData::CompletionType completionType, + const std::optional& exportDate ); + +RigMswTableData collectTableData( const RigMswWellExportData& exportData, RiaDefines::EclipseUnitSystem unitSystem ); + +} // namespace RicWellPathExportMswGeometryPath diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicMswTableDataTools.h b/ApplicationLibCode/Commands/CompletionExportCommands/RicMswTableDataTools.h index 76aaf873b9..ea3f97490b 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicMswTableDataTools.h +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicMswTableDataTools.h @@ -205,4 +205,6 @@ std::vector> createSubSegmentMDPairs( double double tvdFromMeasuredDepth( gsl::not_null wellPath, double measuredDepth ); +inline constexpr double valveSegmentLength = 0.1; + } // namespace RicMswTableDataTools diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportMswTableData.cpp b/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportMswTableData.cpp index fa6d95c824..81dd778aac 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportMswTableData.cpp +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportMswTableData.cpp @@ -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 bool exportCompletionsAfterMainBoreSegments, CompletionType completionType, const std::optional& 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 + RicWellPathExportMswTableData::extractSingleWellMswDataGeometry( RimEclipseCase* eclipseCase, + RimWellPath* wellPath, + bool exportCompletionsAfterMainBoreSegments, + CompletionType completionType, + const std::optional& 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> 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 + RicWellPathExportMswTableData::extractSingleWellMswDataTree( RimEclipseCase* eclipseCase, + RimWellPath* wellPath, + bool exportCompletionsAfterMainBoreSegments, + CompletionType completionType, + const std::optional& exportDate ) { if ( !eclipseCase || !wellPath || eclipseCase->eclipseCaseData() == nullptr ) { diff --git a/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportMswTableData.h b/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportMswTableData.h index fb3a45ad2c..2e670db07c 100644 --- a/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportMswTableData.h +++ b/ApplicationLibCode/Commands/CompletionExportCommands/RicWellPathExportMswTableData.h @@ -70,6 +70,19 @@ public: CompletionType completionType = CompletionType::ALL, const std::optional& exportDate = std::nullopt ); + static std::expected extractSingleWellMswDataTree( RimEclipseCase* eclipseCase, + RimWellPath* wellPath, + bool exportCompletionsAfterMainBoreSegments = true, + CompletionType completionType = CompletionType::ALL, + const std::optional& exportDate = std::nullopt ); + + static std::expected + extractSingleWellMswDataGeometry( RimEclipseCase* eclipseCase, + RimWellPath* wellPath, + bool exportCompletionsAfterMainBoreSegments = true, + CompletionType completionType = CompletionType::ALL, + const std::optional& 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 generateCellSegments( const RimEclipseCase* eclipseCase, const RimWellPath* wellPath ); + + static std::vector filterIntersections( const std::vector& intersections, + double initialMD, + gsl::not_null wellPathGeometry, + gsl::not_null eclipseCase ); + + static std::vector wellPathsWithTieIn( const RimWellPath* wellPath ); + + static double computeIntitialMeasuredDepth( const RimEclipseCase* eclipseCase, + const RimWellPath* wellPath, + const RimMswCompletionParameters* mswParameters, + const std::vector& allIntersections ); + private: static void updateDataForMultipleItemsInSameGridCell( gsl::not_null branch ); @@ -103,18 +130,6 @@ private: gsl::not_null exportInfo, gsl::not_null branch ); - static std::vector generateCellSegments( const RimEclipseCase* eclipseCase, const RimWellPath* wellPath ); - - static double computeIntitialMeasuredDepth( const RimEclipseCase* eclipseCase, - const RimWellPath* wellPath, - const RimMswCompletionParameters* mswParameters, - const std::vector& allIntersections ); - - static std::vector filterIntersections( const std::vector& intersections, - double initialMD, - gsl::not_null wellPathGeometry, - gsl::not_null eclipseCase ); - static std::pair calculateOverlapWithActiveCells( double startMD, double endMD, const std::vector& wellPathIntersections, @@ -184,8 +199,6 @@ private: gsl::not_null branchNumber ); static std::unique_ptr createChildMswBranch( const RimWellPath* childWellPath ); - - static std::vector wellPathsWithTieIn( const RimWellPath* wellPath ); }; ENABLE_BITMASK_OPERATORS( RicWellPathExportMswTableData::CompletionType )