#14646 MSW: Report a grid cell only once across COMPSEGS branches

A grid cell has a single COMPDAT connection and must be connected to exactly one segment, but the geometry-based export emitted one COMPSEGS row per branch intersecting the cell. Restore the well-global bookkeeping the removed tree path had: tag each branch with its origin and let the branch carrying most flow claim a shared cell, perforations before fishbones before fractures.
This commit is contained in:
Magne Sjaastad committed 2026-08-31 10:28:41 +02:00
1 parent 377751eb68
commit d085f6ce74
4 files changed
+126 -10

No files matched your search

@@ -848,7 +848,7 @@ std::vector<RigMswBranch> buildFractureBranches( RimEclipseCase*
seg.description = fracture->name().toStdString();
seg.intersections = std::move( compsegs );
result.push_back( RigMswBranch{ fracBranch, std::nullopt, { std::move( seg ) } } );
result.push_back( RigMswBranch{ fracBranch, std::nullopt, { std::move( seg ) }, RigMswBranchSource::Fracture } );
}
return result;
@@ -993,7 +993,7 @@ std::vector<RigMswBranch> buildFishbonesBranches( const RimEclipseCase*
fishbonesContext.icdSegments.push_back( { icdSegNum, std::move( icdGlobalCellIndices ) } );
result.push_back( RigMswBranch{ icdBranch, std::nullopt, { std::move( icdSeg ) } } );
result.push_back( RigMswBranch{ icdBranch, std::nullopt, { std::move( icdSeg ) }, RigMswBranchSource::Fishbones } );
// Lateral sub-segments
for ( size_t lateralIndex : lateralIndices )
@@ -1099,7 +1099,7 @@ std::vector<RigMswBranch> buildFishbonesBranches( const RimEclipseCase*
fishbonesContext.laterals.push_back( std::move( lateralContext ) );
result.push_back( RigMswBranch{ latBranch, std::nullopt, std::move( latSegs ) } );
result.push_back( RigMswBranch{ latBranch, std::nullopt, std::move( latSegs ), RigMswBranchSource::Fishbones } );
}
}
}
@@ -41,6 +41,9 @@
#include "cafAssert.h"
#include <algorithm>
#include <numeric>
#include <set>
#include <tuple>
namespace RicWellPathExportMswGeometryPath
{
@@ -504,7 +507,13 @@ RigMswTableData collectTableData( const RigMswWellExportData& exportData, RiaDef
tableData.setWelsegsHeader( exportData.header );
// Collected while the segments are visited, and added to the table data ordered by branch number.
std::vector<CompsegsRow> compsegsRows;
// The branch source is kept alongside the row to resolve cells claimed by several branches.
struct CollectedCompsegsRow
{
CompsegsRow row;
RigMswBranchSource source;
};
std::vector<CollectedCompsegsRow> compsegsRows;
auto emitSegment = [&]( const RigMswBranch& branch, const RigMswSegment& seg )
{
@@ -533,7 +542,7 @@ RigMswTableData collectTableData( const RigMswWellExportData& exportData, RiaDef
compRow.distanceStart = inter.distanceStart;
compRow.distanceEnd = inter.distanceEnd;
compRow.gridName = inter.gridName;
compsegsRows.push_back( compRow );
compsegsRows.push_back( { compRow, branch.source } );
}
// WSEGVALV row
@@ -554,14 +563,37 @@ RigMswTableData collectTableData( const RigMswWellExportData& exportData, RiaDef
tableData.addMswBranch( branch );
}
// A grid cell has a single COMPDAT connection and must be connected to exactly one segment. When
// several branches intersect the same cell, the branch carrying most flow claims it: perforations
// first, then fishbones, then fractures. Visit the rows in that order and drop the later claims.
std::vector<size_t> claimOrder( compsegsRows.size() );
std::iota( claimOrder.begin(), claimOrder.end(), size_t( 0 ) );
std::stable_sort( claimOrder.begin(),
claimOrder.end(),
[&]( size_t lhs, size_t rhs ) { return compsegsRows[lhs].source < compsegsRows[rhs].source; } );
std::set<std::tuple<std::string, size_t, size_t, size_t>> connectedCells;
std::vector<bool> isCellOwner( compsegsRows.size(), false );
for ( size_t index : claimOrder )
{
const auto& row = compsegsRows[index].row;
isCellOwner[index] = connectedCells.emplace( row.gridName, row.i, row.j, row.k ).second;
}
// COMPSEGS is ordered by branch number. WELSEGS keeps the order the branches are listed in, where
// the completion branches follow the lateral they are connected to and thus are not sorted by
// branch number. The sort is stable, so the rows of a branch keep their order along the well path.
std::stable_sort( compsegsRows.begin(),
compsegsRows.end(),
std::vector<CompsegsRow> ownedRows;
for ( size_t index = 0; index < compsegsRows.size(); ++index )
{
if ( isCellOwner[index] ) ownedRows.push_back( compsegsRows[index].row );
}
std::stable_sort( ownedRows.begin(),
ownedRows.end(),
[]( const CompsegsRow& lhs, const CompsegsRow& rhs ) { return lhs.branch < rhs.branch; } );
for ( const auto& compsegsRow : compsegsRows )
for ( const auto& compsegsRow : ownedRows )
{
tableData.addCompsegsRow( compsegsRow );
}
@@ -68,6 +68,19 @@ struct RigMswSegment
};
//==================================================================================================
/// Origin of a branch, used to decide which branch connects a grid cell when several branches
/// intersect the same cell. The enum is ordered by priority, so the branch carrying most flow
/// claims the cell first.
//==================================================================================================
enum class RigMswBranchSource
{
Perforation, // Main bore and perforation valve branches
Fishbones,
Fracture
};
//==================================================================================================
/// One branch in the MSW export.
/// All segments share the same IBRANCH number, which is stored here rather than per-segment.
@@ -78,6 +91,8 @@ struct RigMswBranch
int branchNumber; // IBRANCH for all segments in this branch
std::optional<RigMswSegment> tieInValve; // Optional ICV at the tie-in point (laterals only)
std::vector<RigMswSegment> segments; // Segments of this branch
RigMswBranchSource source = RigMswBranchSource::Perforation; // Decides COMPSEGS cell ownership
};
@@ -62,9 +62,19 @@ RigMswSegment makeSegment( int segNum, int outletSegNum, double length = 10.0, d
//--------------------------------------------------------------------------------------------------
/// Build a minimal RigMswBranch with the given branch number and segments.
//--------------------------------------------------------------------------------------------------
RigMswBranch makeBranch( int branchNum, std::vector<RigMswSegment> segs )
RigMswBranch makeBranch( int branchNum, std::vector<RigMswSegment> segs, RigMswBranchSource source = RigMswBranchSource::Perforation )
{
return RigMswBranch{ branchNum, std::nullopt, std::move( segs ) };
return RigMswBranch{ branchNum, std::nullopt, std::move( segs ), source };
}
//--------------------------------------------------------------------------------------------------
/// Build a branch with a single segment intersecting the given main grid cell.
//--------------------------------------------------------------------------------------------------
RigMswBranch makeBranchIntersectingCell( int branchNum, int segNum, size_t i, size_t j, size_t k, RigMswBranchSource source )
{
RigMswSegment seg = makeSegment( segNum, 1 );
seg.intersections = { RigMswCellIntersection{ i, j, k, 100.0, 110.0, "" } };
return makeBranch( branchNum, { seg }, source );
}
//--------------------------------------------------------------------------------------------------
@@ -431,6 +441,65 @@ TEST( RicWellPathExportMswGeometryPath, HeaderFieldsPropagated )
EXPECT_EQ( RiaDefines::EclipseUnitSystem::UNITS_FIELD, result.unitSystem() );
}
//--------------------------------------------------------------------------------------------------
/// A grid cell has a single COMPDAT connection and must be connected to one segment only. When two
/// branches of the same kind intersect the cell, the branch listed first keeps it.
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportMswGeometryPath, CompsegsCellIsConnectedOnlyOnce )
{
RigMswWellExportData exportData;
exportData.header = makeHeader();
exportData.branches = { makeBranchIntersectingCell( 2, 10, 3, 5, 7, RigMswBranchSource::Fracture ),
makeBranchIntersectingCell( 3, 11, 3, 5, 7, RigMswBranchSource::Fracture ) };
auto result = RicWellPathExportMswGeometryPath::collectTableData( exportData, RiaDefines::EclipseUnitSystem::UNITS_METRIC );
ASSERT_EQ( 2u, result.welsegsData().size() );
ASSERT_EQ( 1u, result.compsegsData().size() );
EXPECT_EQ( 2, result.compsegsData()[0].branch );
}
//--------------------------------------------------------------------------------------------------
/// The branch carrying most flow claims a shared cell: perforations before fishbones before
/// fractures, regardless of the order the branches are listed in.
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportMswGeometryPath, CompsegsCellIsClaimedByTheBranchWithMostFlow )
{
RigMswWellExportData exportData;
exportData.header = makeHeader();
exportData.branches = { makeBranchIntersectingCell( 4, 12, 3, 5, 7, RigMswBranchSource::Fracture ),
makeBranchIntersectingCell( 3, 11, 3, 5, 7, RigMswBranchSource::Fishbones ),
makeBranchIntersectingCell( 2, 10, 3, 5, 7, RigMswBranchSource::Perforation ) };
auto result = RicWellPathExportMswGeometryPath::collectTableData( exportData, RiaDefines::EclipseUnitSystem::UNITS_METRIC );
ASSERT_EQ( 1u, result.compsegsData().size() );
EXPECT_EQ( 2, result.compsegsData()[0].branch );
}
//--------------------------------------------------------------------------------------------------
/// The same IJK in the main grid and in an LGR are different cells, and both are connected.
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportMswGeometryPath, CompsegsSeparatesMainGridAndLgrCells )
{
RigMswWellExportData exportData;
exportData.header = makeHeader();
RigMswSegment lgrSeg = makeSegment( 11, 1 );
lgrSeg.intersections = { RigMswCellIntersection{ 3, 5, 7, 100.0, 110.0, "LGR_NEAR_WELL" } };
exportData.branches = { makeBranchIntersectingCell( 2, 10, 3, 5, 7, RigMswBranchSource::Fracture ),
makeBranch( 3, { lgrSeg }, RigMswBranchSource::Fracture ) };
auto result = RicWellPathExportMswGeometryPath::collectTableData( exportData, RiaDefines::EclipseUnitSystem::UNITS_METRIC );
ASSERT_EQ( 2u, result.compsegsData().size() );
EXPECT_TRUE( result.compsegsData()[0].isMainGrid() );
EXPECT_EQ( "LGR_NEAR_WELL", result.compsegsData()[1].gridName );
}
//==================================================================================================
// Branch numbering, using the multiple_laterals test project.
//