mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-25 14:40:26 -05:00
#14477 MSW: Apply max segment length to fishbones laterals
The geometry path emitted one WELSEGS row per cell intersection of a fishbones lateral, ignoring the max segment length and the custom segment intervals. The tree path split them like every other completion, in collectCompletionWelsegsSegments. Split each cell intersection with createSubSegmentMDPairs and chain the resulting rows. The lateral geometry is not part of the well path, so the TVD of a sub-segment cannot be interpolated along the well path. Interpolate linearly between the start and end TVD of the intersection instead, as the tree path did for fishbones. The cell is connected by the first sub-segment only, leaving COMPSEGS unchanged, and the lateral label stays on the first row. An intersection now covers several segment numbers that share one effective diameter, and must still contribute once to the sum for its cell. FishbonesLateralSegment therefore holds the segment numbers of an intersection rather than a single one, and the intersections are grouped per lateral in FishbonesLateral. This replaces the separate list of first and second segment numbers, since the reduction rule is now expressed on the intersections of a lateral. Verified with a copy of fishbones.rsp with max segment length enforced at 4 m: the three laterals are split into three or four rows each, depths are evenly interpolated, effective diameters are unchanged at 0.01664 and 0.00960, and COMPSEGS is identical. The sweep against TestModels/msw-export is unchanged, as none of the projects enforce a max segment length.
This commit is contained in:
1 parent
32850cac9e
commit
74a6c0827b
4 files changed
+146
-74
No files matched your search
+88
-46
@@ -62,26 +62,38 @@ namespace RicMswBranchBuilder
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void applyEffectiveDiameters( const FishbonesExportContext& context, RigMswWellExportData& exportData )
|
||||
{
|
||||
if ( context.lateralSegments.empty() ) return;
|
||||
if ( context.laterals.empty() ) return;
|
||||
|
||||
// Deff = sqrt(d1^2 + d2^2 + ..) for all lateral segments sharing a grid cell
|
||||
// Deff = sqrt(d1^2 + d2^2 + ..) for all cell intersections sharing a grid cell. A cell
|
||||
// intersection contributes once, no matter how many WELSEGS rows it was split into.
|
||||
std::map<size_t, double> sumOfSquaresPerCell;
|
||||
for ( const auto& lateralSegment : context.lateralSegments )
|
||||
for ( const auto& lateral : context.laterals )
|
||||
{
|
||||
sumOfSquaresPerCell[lateralSegment.globalCellIndex] += lateralSegment.equivalentDiameter * lateralSegment.equivalentDiameter;
|
||||
for ( const auto& lateralSegment : lateral.segments )
|
||||
{
|
||||
sumOfSquaresPerCell[lateralSegment.globalCellIndex] += lateralSegment.equivalentDiameter * lateralSegment.equivalentDiameter;
|
||||
}
|
||||
}
|
||||
|
||||
std::map<int, double> effectiveDiameterPerSegment;
|
||||
for ( const auto& lateralSegment : context.lateralSegments )
|
||||
for ( const auto& lateral : context.laterals )
|
||||
{
|
||||
effectiveDiameterPerSegment[lateralSegment.segmentNumber] = std::sqrt( sumOfSquaresPerCell[lateralSegment.globalCellIndex] );
|
||||
}
|
||||
std::vector<double> effectiveDiameters;
|
||||
for ( const auto& lateralSegment : lateral.segments )
|
||||
{
|
||||
effectiveDiameters.push_back( std::sqrt( sumOfSquaresPerCell[lateralSegment.globalCellIndex] ) );
|
||||
}
|
||||
|
||||
// Reduce the diameter of the segment sharing a cell with the main bore
|
||||
for ( const auto& [firstSegment, secondSegment] : context.firstAndSecondSegments )
|
||||
{
|
||||
auto it = effectiveDiameterPerSegment.find( secondSegment );
|
||||
if ( it != effectiveDiameterPerSegment.end() ) effectiveDiameterPerSegment[firstSegment] = it->second;
|
||||
// Reduce the diameter of the cell intersection shared with the main bore
|
||||
if ( effectiveDiameters.size() > 1 ) effectiveDiameters[0] = effectiveDiameters[1];
|
||||
|
||||
for ( size_t i = 0; i < lateral.segments.size(); i++ )
|
||||
{
|
||||
for ( auto segmentNumber : lateral.segments[i].segmentNumbers )
|
||||
{
|
||||
effectiveDiameterPerSegment[segmentNumber] = effectiveDiameters[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for ( auto& branch : exportData.branches )
|
||||
@@ -842,6 +854,8 @@ std::vector<RigMswBranch> buildFishbonesBranches( const RimEclipseCase*
|
||||
const std::string& wellNameForExport,
|
||||
int& segmentNumber,
|
||||
int& branchNumber,
|
||||
double maxSegmentLength,
|
||||
const std::vector<std::pair<double, double>>& customSegmentIntervals,
|
||||
RiaDefines::EclipseUnitSystem unitSystem,
|
||||
FishbonesExportContext& fishbonesContext )
|
||||
{
|
||||
@@ -998,52 +1012,80 @@ std::vector<RigMswBranch> buildFishbonesBranches( const RimEclipseCase*
|
||||
const auto lateralLabel = QString( "Lateral %1" ).arg( lateralIndex + 1 ).toStdString();
|
||||
|
||||
std::vector<RigMswSegment> latSegs;
|
||||
FishbonesLateral lateralContext;
|
||||
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>{};
|
||||
const double startMD = prevMD;
|
||||
const double endMD = cellIntInfo.endMD;
|
||||
const double startTVD = prevTVD;
|
||||
const double endTVD = cellIntInfo.endTVD();
|
||||
|
||||
fishbonesContext.lateralSegments.push_back(
|
||||
{ latSeg.segmentNumber, cellIntInfo.globCellIndex, subs->equivalentDiameter( unitSystem ) } );
|
||||
FishbonesLateralSegment lateralSegmentContext;
|
||||
lateralSegmentContext.globalCellIndex = cellIntInfo.globCellIndex;
|
||||
lateralSegmentContext.equivalentDiameter = subs->equivalentDiameter( unitSystem );
|
||||
|
||||
latOutletSeg = latSeg.segmentNumber;
|
||||
prevMD = cellIntInfo.endMD;
|
||||
prevTVD = cellIntInfo.endTVD();
|
||||
firstLatSeg = false;
|
||||
latSegs.push_back( std::move( latSeg ) );
|
||||
const auto subSegs =
|
||||
RicMswTableDataTools::createSubSegmentMDPairs( startMD, endMD, maxSegmentLength, customSegmentIntervals );
|
||||
|
||||
bool firstSubSeg = true;
|
||||
for ( const auto& [subStartMD, subEndMD] : subSegs )
|
||||
{
|
||||
// The lateral geometry is not part of the well path, so the TVD of a sub-segment
|
||||
// cannot be interpolated along the well path. Interpolate linearly instead.
|
||||
const double mdRange = endMD - startMD;
|
||||
const double startWeight = mdRange > 0.0 ? ( subStartMD - startMD ) / mdRange : 0.0;
|
||||
const double endWeight = mdRange > 0.0 ? ( subEndMD - startMD ) / mdRange : 1.0;
|
||||
|
||||
const double subStartTVD = startTVD * ( 1.0 - startWeight ) + endTVD * startWeight;
|
||||
const double subEndTVD = startTVD * ( 1.0 - endWeight ) + endTVD * endWeight;
|
||||
|
||||
double len = 0.0;
|
||||
double dep = 0.0;
|
||||
if ( infoType == "INC" )
|
||||
{
|
||||
len = subEndMD - subStartMD;
|
||||
dep = subEndTVD - subStartTVD;
|
||||
}
|
||||
else
|
||||
{
|
||||
len = subEndMD;
|
||||
dep = subEndTVD;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// The cell is connected by the first sub-segment only
|
||||
if ( firstSubSeg && isNewCell ) latSeg.intersections = { *mci };
|
||||
|
||||
lateralSegmentContext.segmentNumbers.push_back( latSeg.segmentNumber );
|
||||
|
||||
latOutletSeg = latSeg.segmentNumber;
|
||||
firstLatSeg = false;
|
||||
firstSubSeg = false;
|
||||
latSegs.push_back( std::move( latSeg ) );
|
||||
}
|
||||
|
||||
lateralContext.segments.push_back( std::move( lateralSegmentContext ) );
|
||||
|
||||
prevMD = cellIntInfo.endMD;
|
||||
prevTVD = cellIntInfo.endTVD();
|
||||
}
|
||||
}
|
||||
|
||||
if ( latSegs.size() > 1 )
|
||||
{
|
||||
fishbonesContext.firstAndSecondSegments.emplace_back( latSegs[0].segmentNumber, latSegs[1].segmentNumber );
|
||||
}
|
||||
fishbonesContext.laterals.push_back( std::move( lateralContext ) );
|
||||
|
||||
result.push_back( RigMswBranch{ latBranch, std::nullopt, std::move( latSegs ) } );
|
||||
}
|
||||
|
||||
+23
-13
@@ -59,14 +59,26 @@ struct CellSegmentEntry
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// One fishbones lateral segment, recorded while the lateral branches are built so that effective
|
||||
/// diameters can be computed once all laterals of the well are known.
|
||||
/// One grid-cell intersection of a fishbones lateral, recorded while the lateral branches are built
|
||||
/// so that effective diameters can be computed once all laterals of the well are known.
|
||||
///
|
||||
/// A cell intersection longer than the max segment length is split into several WELSEGS rows. They
|
||||
/// all belong to the same intersection and share one effective diameter, so the segment numbers are
|
||||
/// kept together here.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
struct FishbonesLateralSegment
|
||||
{
|
||||
int segmentNumber;
|
||||
size_t globalCellIndex;
|
||||
double equivalentDiameter;
|
||||
std::vector<int> segmentNumbers;
|
||||
size_t globalCellIndex;
|
||||
double equivalentDiameter;
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// The cell intersections of one fishbones lateral, in order from the ICD sub and outwards.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
struct FishbonesLateral
|
||||
{
|
||||
std::vector<FishbonesLateralSegment> segments;
|
||||
};
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -83,11 +95,7 @@ struct FishbonesIcdSegment
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
struct FishbonesExportContext
|
||||
{
|
||||
std::vector<FishbonesLateralSegment> lateralSegments;
|
||||
|
||||
/// Segment numbers of the first and second segment of each lateral having more than one segment.
|
||||
std::vector<std::pair<int, int>> firstAndSecondSegments;
|
||||
|
||||
std::vector<FishbonesLateral> laterals;
|
||||
std::vector<FishbonesIcdSegment> icdSegments;
|
||||
};
|
||||
|
||||
@@ -98,9 +106,9 @@ struct FishbonesExportContext
|
||||
/// Deff = sqrt(d1^2 + d2^2 + ..) over the lateral segments in that cell, see
|
||||
/// https://github.com/OPM/ResInsight/issues/7686
|
||||
///
|
||||
/// The first segment of a lateral shares its cell with the main bore and with the first segment of
|
||||
/// every other lateral on the same sub, which inflates Deff. It therefore inherits the effective
|
||||
/// diameter of the second segment of the same lateral, see
|
||||
/// The first cell intersection of a lateral shares its cell with the main bore and with the first
|
||||
/// intersection of every other lateral on the same sub, which inflates Deff. It therefore inherits
|
||||
/// the effective diameter of the second intersection of the same lateral, see
|
||||
/// https://github.com/OPM/ResInsight/issues/7731
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void applyEffectiveDiameters( const FishbonesExportContext& context, RigMswWellExportData& exportData );
|
||||
@@ -193,6 +201,8 @@ std::vector<RigMswBranch> buildFishbonesBranches( const RimEclipseCase*
|
||||
const std::string& wellNameForExport,
|
||||
int& segmentNumber,
|
||||
int& branchNumber,
|
||||
double maxSegmentLength,
|
||||
const std::vector<std::pair<double, double>>& customSegmentIntervals,
|
||||
RiaDefines::EclipseUnitSystem unitSystem,
|
||||
FishbonesExportContext& fishbonesContext );
|
||||
|
||||
|
||||
+4
@@ -236,6 +236,8 @@ std::vector<RigMswBranch> buildLateralBranches( RimEclipseCase*
|
||||
wellNameForExport,
|
||||
segmentNumber,
|
||||
branchNumber,
|
||||
mswParameters->maxSegmentLength(),
|
||||
{},
|
||||
unitSystem,
|
||||
fishbonesContext );
|
||||
result.insert( result.end(), std::make_move_iterator( fishBranches.begin() ), std::make_move_iterator( fishBranches.end() ) );
|
||||
@@ -409,6 +411,8 @@ RigMswWellExportData buildMswWellExportData( RimEclipseCase*
|
||||
wellNameForExport,
|
||||
segmentNumber,
|
||||
branchNumber,
|
||||
maxSegmentLength,
|
||||
customSegmentIntervals,
|
||||
unitSystem,
|
||||
fishbonesContext );
|
||||
}
|
||||
|
||||
@@ -425,22 +425,40 @@ TEST( RicMswBranchBuilder, IcdAreaPerCell_NonIcdValveIsUntouched )
|
||||
TEST( RicMswBranchBuilder, EffectiveDiameters_SharedCellCombinesDiameters )
|
||||
{
|
||||
FishbonesExportContext context;
|
||||
context.lateralSegments.push_back( { 10, 100, 0.0096 } );
|
||||
context.lateralSegments.push_back( { 20, 100, 0.0096 } );
|
||||
context.lateralSegments.push_back( { 30, 100, 0.0096 } );
|
||||
context.laterals.push_back( { { { { 10 }, 100, 0.0096 } } } );
|
||||
context.laterals.push_back( { { { { 20 }, 100, 0.0096 } } } );
|
||||
context.laterals.push_back( { { { { 30 }, 100, 0.0096 } } } );
|
||||
|
||||
auto exportData = makeExportData( { { 10, 0.0 }, { 20, 0.0 }, { 30, 0.0 } } );
|
||||
applyEffectiveDiameters( context, exportData );
|
||||
|
||||
const double expected = std::sqrt( 3.0 ) * 0.0096;
|
||||
for ( int segmentNumber : { 10, 20, 30 } )
|
||||
for ( const auto& segment : exportData.branches[0].segments )
|
||||
{
|
||||
for ( const auto& segment : exportData.branches[0].segments )
|
||||
{
|
||||
if ( segment.segmentNumber != segmentNumber ) continue;
|
||||
ASSERT_TRUE( segment.diameter.has_value() );
|
||||
EXPECT_NEAR( expected, *segment.diameter, 1.0e-12 );
|
||||
}
|
||||
ASSERT_TRUE( segment.diameter.has_value() );
|
||||
EXPECT_NEAR( expected, *segment.diameter, 1.0e-12 );
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// A cell intersection split into several WELSEGS rows contributes once to the sum, and all its rows
|
||||
/// get the same effective diameter.
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
TEST( RicMswBranchBuilder, EffectiveDiameters_SplitIntersectionCountsOnce )
|
||||
{
|
||||
FishbonesExportContext context;
|
||||
context.laterals.push_back( { { { { 10, 11, 12 }, 100, 0.0096 } } } );
|
||||
context.laterals.push_back( { { { { 20 }, 100, 0.0096 } } } );
|
||||
|
||||
auto exportData = makeExportData( { { 10, 0.0 }, { 11, 0.0 }, { 12, 0.0 }, { 20, 0.0 } } );
|
||||
applyEffectiveDiameters( context, exportData );
|
||||
|
||||
// Two intersections in cell 100, not four rows.
|
||||
const double expected = std::sqrt( 2.0 ) * 0.0096;
|
||||
for ( const auto& segment : exportData.branches[0].segments )
|
||||
{
|
||||
ASSERT_TRUE( segment.diameter.has_value() );
|
||||
EXPECT_NEAR( expected, *segment.diameter, 1.0e-12 );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,10 +470,8 @@ TEST( RicMswBranchBuilder, EffectiveDiameters_FirstSegmentInheritsSecond )
|
||||
FishbonesExportContext context;
|
||||
|
||||
// Two laterals start in cell 100, only the first one continues into cell 200.
|
||||
context.lateralSegments.push_back( { 10, 100, 0.0096 } );
|
||||
context.lateralSegments.push_back( { 11, 200, 0.0096 } );
|
||||
context.lateralSegments.push_back( { 20, 100, 0.0096 } );
|
||||
context.firstAndSecondSegments.emplace_back( 10, 11 );
|
||||
context.laterals.push_back( { { { { 10 }, 100, 0.0096 }, { { 11 }, 200, 0.0096 } } } );
|
||||
context.laterals.push_back( { { { { 20 }, 100, 0.0096 } } } );
|
||||
|
||||
auto exportData = makeExportData( { { 10, 0.0 }, { 11, 0.0 }, { 20, 0.0 } } );
|
||||
applyEffectiveDiameters( context, exportData );
|
||||
@@ -466,6 +482,6 @@ TEST( RicMswBranchBuilder, EffectiveDiameters_FirstSegmentInheritsSecond )
|
||||
EXPECT_NEAR( 0.0096, *segments[0].diameter, 1.0e-12 );
|
||||
EXPECT_NEAR( 0.0096, *segments[1].diameter, 1.0e-12 );
|
||||
|
||||
// Segment 20 has no second segment and keeps the combined value of cell 100.
|
||||
// The second lateral has a single intersection and keeps the combined value of cell 100.
|
||||
EXPECT_NEAR( std::sqrt( 2.0 ) * 0.0096, *segments[2].diameter, 1.0e-12 );
|
||||
}
|
||||
Reference in new issue
Block a user