mirror of
https://github.com/OPM/ResInsight.git
synced 2026-10-24 14:17:52 -05:00
#14961 Sim Events: Support segment lengths in SEGMENT events
Add FIXED_SEGMENT_LENGTH, MIN_SEGMENT_LENGTH and MAX_SEGMENT_LENGTH to SIMEVENTS SEGMENT events. The lengths are stored on RimWellEventSegment (new AddSegmentEvent arguments, 0 = not set) and applied to the segment interval when the timeline is applied. Invalid combinations (fixed with min/max, min > max, non-positive values) are reported as parse errors.
This commit is contained in:
1 parent
c56e921160
commit
8174e21561
10 files changed
+401
-4
No files matched your search
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "cafPdmFieldScriptingCapability.h"
|
||||
#include "cafPdmObjectScriptingCapability.h"
|
||||
#include "cafPdmUiCheckBoxAndTextEditor.h"
|
||||
|
||||
CAF_PDM_SOURCE_INIT( RimWellEventSegment, "WellEventSegment" );
|
||||
|
||||
@@ -35,6 +36,13 @@ RimWellEventSegment::RimWellEventSegment()
|
||||
CAF_PDM_InitScriptableField( &m_innerDiameter, "InnerDiameter", 0.15, "Inner Diameter [m]" );
|
||||
CAF_PDM_InitScriptableField( &m_roughness, "Roughness", 1.0e-5, "Roughness [m]" );
|
||||
|
||||
CAF_PDM_InitScriptableField( &m_fixedSegmentLength, "FixedSegmentLength", std::make_pair( false, 50.0 ), "Fixed Segment Length" );
|
||||
m_fixedSegmentLength.uiCapability()->setUiEditorTypeName( caf::PdmUiCheckBoxAndTextEditor::uiEditorTypeName() );
|
||||
CAF_PDM_InitScriptableField( &m_minSegmentLength, "MinSegmentLength", std::make_pair( false, 10.0 ), "Min Segment Length" );
|
||||
m_minSegmentLength.uiCapability()->setUiEditorTypeName( caf::PdmUiCheckBoxAndTextEditor::uiEditorTypeName() );
|
||||
CAF_PDM_InitScriptableField( &m_maxSegmentLength, "MaxSegmentLength", std::make_pair( false, 100.0 ), "Max Segment Length" );
|
||||
m_maxSegmentLength.uiCapability()->setUiEditorTypeName( caf::PdmUiCheckBoxAndTextEditor::uiEditorTypeName() );
|
||||
|
||||
setDeletable( true );
|
||||
}
|
||||
|
||||
@@ -109,6 +117,57 @@ void RimWellEventSegment::setRoughness( double roughness )
|
||||
m_roughness = roughness;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::optional<double> RimWellEventSegment::fixedSegmentLength() const
|
||||
{
|
||||
if ( m_fixedSegmentLength().first ) return m_fixedSegmentLength().second;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::optional<double> RimWellEventSegment::minSegmentLength() const
|
||||
{
|
||||
if ( m_minSegmentLength().first ) return m_minSegmentLength().second;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::optional<double> RimWellEventSegment::maxSegmentLength() const
|
||||
{
|
||||
if ( m_maxSegmentLength().first ) return m_maxSegmentLength().second;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellEventSegment::setFixedSegmentLength( std::optional<double> length )
|
||||
{
|
||||
m_fixedSegmentLength = std::make_pair( length.has_value(), length.value_or( m_fixedSegmentLength().second ) );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellEventSegment::setMinSegmentLength( std::optional<double> length )
|
||||
{
|
||||
m_minSegmentLength = std::make_pair( length.has_value(), length.value_or( m_minSegmentLength().second ) );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RimWellEventSegment::setMaxSegmentLength( std::optional<double> length )
|
||||
{
|
||||
m_maxSegmentLength = std::make_pair( length.has_value(), length.value_or( m_maxSegmentLength().second ) );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -131,6 +190,16 @@ QString RimWellEventSegment::generateScheduleKeyword( const QString& wellName )
|
||||
.arg( m_innerDiameter(), 0, 'f', 4 )
|
||||
.arg( m_roughness(), 0, 'e', 3 );
|
||||
|
||||
QStringList lengths;
|
||||
if ( auto length = fixedSegmentLength() ) lengths.push_back( QString( "Fixed seg len: %1 m" ).arg( *length, 0, 'f', 2 ) );
|
||||
if ( auto length = minSegmentLength() ) lengths.push_back( QString( "Min seg len: %1 m" ).arg( *length, 0, 'f', 2 ) );
|
||||
if ( auto length = maxSegmentLength() ) lengths.push_back( QString( "Max seg len: %1 m" ).arg( *length, 0, 'f', 2 ) );
|
||||
if ( !lengths.isEmpty() )
|
||||
{
|
||||
line.chop( 1 );
|
||||
line += ", " + lengths.join( ", " ) + "\n";
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
@@ -145,6 +214,9 @@ void RimWellEventSegment::defineUiOrdering( QString uiConfigName, caf::PdmUiOrde
|
||||
uiOrdering.add( &m_endMD );
|
||||
uiOrdering.add( &m_innerDiameter );
|
||||
uiOrdering.add( &m_roughness );
|
||||
uiOrdering.add( &m_fixedSegmentLength );
|
||||
uiOrdering.add( &m_minSegmentLength );
|
||||
uiOrdering.add( &m_maxSegmentLength );
|
||||
|
||||
uiOrdering.skipRemainingFields();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
|
||||
#include "cafPdmField.h"
|
||||
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
//==================================================================================================
|
||||
///
|
||||
/// Segment event - defines changes to segment (tubing/liner) diameter and roughness
|
||||
@@ -47,6 +50,14 @@ public:
|
||||
void setInnerDiameter( double diameter );
|
||||
void setRoughness( double roughness );
|
||||
|
||||
// Segmentation. Fixed length cannot be combined with min/max length.
|
||||
std::optional<double> fixedSegmentLength() const;
|
||||
std::optional<double> minSegmentLength() const;
|
||||
std::optional<double> maxSegmentLength() const;
|
||||
void setFixedSegmentLength( std::optional<double> length );
|
||||
void setMinSegmentLength( std::optional<double> length );
|
||||
void setMaxSegmentLength( std::optional<double> length );
|
||||
|
||||
// Override from RimWellEvent
|
||||
EventType eventType() const override;
|
||||
QString generateScheduleKeyword( const QString& wellName ) const override;
|
||||
@@ -60,4 +71,8 @@ private:
|
||||
caf::PdmField<double> m_endMD;
|
||||
caf::PdmField<double> m_innerDiameter;
|
||||
caf::PdmField<double> m_roughness;
|
||||
|
||||
caf::PdmField<std::pair<bool, double>> m_fixedSegmentLength;
|
||||
caf::PdmField<std::pair<bool, double>> m_minSegmentLength;
|
||||
caf::PdmField<std::pair<bool, double>> m_maxSegmentLength;
|
||||
};
|
||||
@@ -569,6 +569,14 @@ bool RimWellEventTimeline::applySegmentEvent( const RimWellEventSegment& event,
|
||||
interval->enableCustomStartDate( true );
|
||||
interval->setCustomStartDate( event.eventDate().date() );
|
||||
|
||||
// The event defines the complete segmentation of the interval
|
||||
interval->setFixedSegmentLength( std::nullopt );
|
||||
interval->setMinSegmentLength( std::nullopt );
|
||||
interval->setMaxSegmentLength( std::nullopt );
|
||||
interval->setMinSegmentLength( event.minSegmentLength() );
|
||||
interval->setMaxSegmentLength( event.maxSegmentLength() );
|
||||
interval->setFixedSegmentLength( event.fixedSegmentLength() );
|
||||
|
||||
segmentCollection->updateConnectedEditors();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -335,6 +335,9 @@ RimcWellEventTimeline_addSegmentEvent::RimcWellEventTimeline_addSegmentEvent( ca
|
||||
CAF_PDM_InitScriptableField( &m_endMd, "EndMd", 0.0, "", "", "", "End Measured Depth" );
|
||||
CAF_PDM_InitScriptableField( &m_innerDiameter, "InnerDiameter", 0.15, "", "", "", "Inner Diameter [m]" );
|
||||
CAF_PDM_InitScriptableField( &m_roughness, "Roughness", 1.0e-5, "", "", "", "Roughness [m]" );
|
||||
CAF_PDM_InitScriptableField( &m_fixedSegmentLength, "FixedSegmentLength", 0.0, "", "", "", "Fixed Segment Length [m] (0 = not set)" );
|
||||
CAF_PDM_InitScriptableField( &m_minSegmentLength, "MinSegmentLength", 0.0, "", "", "", "Min Segment Length [m] (0 = not set)" );
|
||||
CAF_PDM_InitScriptableField( &m_maxSegmentLength, "MaxSegmentLength", 0.0, "", "", "", "Max Segment Length [m] (0 = not set)" );
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@@ -355,11 +358,34 @@ std::expected<caf::PdmObjectHandle*, QString> RimcWellEventTimeline_addSegmentEv
|
||||
return std::unexpected( QString( "Invalid date format: %1. Expected YYYY-MM-DD" ).arg( m_eventDate() ) );
|
||||
}
|
||||
|
||||
if ( m_fixedSegmentLength() < 0.0 || m_minSegmentLength() < 0.0 || m_maxSegmentLength() < 0.0 )
|
||||
{
|
||||
return std::unexpected( QString( "Segment lengths must be positive (0 = not set)" ) );
|
||||
}
|
||||
|
||||
auto toOptional = []( double length ) { return length > 0.0 ? std::optional<double>( length ) : std::nullopt; };
|
||||
|
||||
const auto fixedLength = toOptional( m_fixedSegmentLength() );
|
||||
const auto minLength = toOptional( m_minSegmentLength() );
|
||||
const auto maxLength = toOptional( m_maxSegmentLength() );
|
||||
|
||||
if ( fixedLength && ( minLength || maxLength ) )
|
||||
{
|
||||
return std::unexpected( QString( "Fixed segment length cannot be combined with min/max segment length" ) );
|
||||
}
|
||||
if ( minLength && maxLength && *minLength > *maxLength )
|
||||
{
|
||||
return std::unexpected( QString( "Min segment length cannot exceed max segment length" ) );
|
||||
}
|
||||
|
||||
auto* event = timeline->addSegmentEvent( m_wellPath(), date );
|
||||
event->setStartMD( m_startMd() );
|
||||
event->setEndMD( m_endMd() );
|
||||
event->setInnerDiameter( m_innerDiameter() );
|
||||
event->setRoughness( m_roughness() );
|
||||
event->setFixedSegmentLength( fixedLength );
|
||||
event->setMinSegmentLength( minLength );
|
||||
event->setMaxSegmentLength( maxLength );
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
@@ -155,6 +155,9 @@ private:
|
||||
caf::PdmField<double> m_endMd;
|
||||
caf::PdmField<double> m_innerDiameter;
|
||||
caf::PdmField<double> m_roughness;
|
||||
caf::PdmField<double> m_fixedSegmentLength;
|
||||
caf::PdmField<double> m_minSegmentLength;
|
||||
caf::PdmField<double> m_maxSegmentLength;
|
||||
};
|
||||
|
||||
//==================================================================================================
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ WELL W1
|
||||
2024-04-15 WELSPECS CROSSFLOW=False REFDEPTH=1000 PHASE=OIL
|
||||
|
||||
# COMMENT is stored on the event and safely emitted as a schedule comment.
|
||||
STARTUP SEGMENT MDSTART=0 MDEND=2500 INNER_DIAMETER=0.15 ROUGHNESS=1.0e-5 COMMENT="Install production segment"
|
||||
STARTUP SEGMENT MDSTART=0 MDEND=2500 INNER_DIAMETER=0.15 ROUGHNESS=1.0e-5 MIN_SEGMENT_LENGTH=5 MAX_SEGMENT_LENGTH=50 COMMENT="Install production segment"
|
||||
|
||||
# Perforations; COMPLETION_NUMBER groups connections for COMPLUMP. Same-date
|
||||
# perforations are kept as separate events during normalization.{filter_comment}
|
||||
|
||||
@@ -744,6 +744,7 @@ def merge_simulator_events_documents(
|
||||
result.wells, result.groups, result.schedule_events
|
||||
)
|
||||
errors.extend(_wellspec_validation_issues(result.wells))
|
||||
errors.extend(_segment_validation_issues(result.wells))
|
||||
if errors:
|
||||
raise SimulatorEventsParseError(errors=errors)
|
||||
return result
|
||||
@@ -849,6 +850,7 @@ def parse_simulator_events(
|
||||
_set_event_scopes(wells, groups, schedule_events)
|
||||
errors.extend(_restart_validation_issues(wells, groups, schedule_events))
|
||||
errors.extend(_wellspec_validation_issues(wells))
|
||||
errors.extend(_segment_validation_issues(wells))
|
||||
insert_date_events, report_errors = _expand_report_specs(
|
||||
report_specs, wells, groups, schedule_events
|
||||
)
|
||||
@@ -1016,6 +1018,70 @@ def _wellspec_validation_issues(wells: List[WellBlock]) -> List[ParseIssue]:
|
||||
return issues
|
||||
|
||||
|
||||
def _segment_validation_issues(wells: List[WellBlock]) -> List[ParseIssue]:
|
||||
"""Validate the segment-length attributes of SEGMENT events.
|
||||
|
||||
Lengths must be positive numbers, a fixed length cannot be combined with
|
||||
min/max lengths, and the min length cannot exceed the max length.
|
||||
"""
|
||||
issues: List[ParseIssue] = []
|
||||
for well in wells:
|
||||
for event in well.events:
|
||||
if event.event_type.upper() != "SEGMENT":
|
||||
continue
|
||||
lengths: Dict[str, float] = {}
|
||||
for name in _SEGMENT_LENGTH_KWARGS:
|
||||
attr = event.attributes.get(name)
|
||||
if attr is None:
|
||||
continue
|
||||
value = attr.value
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||
issues.append(
|
||||
ParseIssue(
|
||||
f"{_event_context(event)}: {name} must be a number, "
|
||||
f"got {attr.raw!r}",
|
||||
event.loc,
|
||||
)
|
||||
)
|
||||
elif value <= 0:
|
||||
issues.append(
|
||||
ParseIssue(
|
||||
f"{_event_context(event)}: {name} must be greater "
|
||||
f"than zero, got {attr.raw}",
|
||||
event.loc,
|
||||
)
|
||||
)
|
||||
else:
|
||||
lengths[name] = float(value)
|
||||
|
||||
present = [
|
||||
name for name in _SEGMENT_LENGTH_KWARGS if name in event.attributes
|
||||
]
|
||||
if "FIXED_SEGMENT_LENGTH" in present and len(present) > 1:
|
||||
issues.append(
|
||||
ParseIssue(
|
||||
f"{_event_context(event)}: FIXED_SEGMENT_LENGTH cannot be "
|
||||
"combined with MIN_SEGMENT_LENGTH or MAX_SEGMENT_LENGTH",
|
||||
event.loc,
|
||||
)
|
||||
)
|
||||
min_length = lengths.get("MIN_SEGMENT_LENGTH")
|
||||
max_length = lengths.get("MAX_SEGMENT_LENGTH")
|
||||
if (
|
||||
min_length is not None
|
||||
and max_length is not None
|
||||
and min_length > max_length
|
||||
):
|
||||
issues.append(
|
||||
ParseIssue(
|
||||
f"{_event_context(event)}: MIN_SEGMENT_LENGTH ({min_length:g}) "
|
||||
f"exceeds MAX_SEGMENT_LENGTH ({max_length:g})",
|
||||
event.loc,
|
||||
)
|
||||
)
|
||||
return issues
|
||||
|
||||
|
||||
_SUPPORTED_VERSION = "1.2"
|
||||
|
||||
|
||||
@@ -1788,8 +1854,17 @@ _SEGMENT_KNOWN = {
|
||||
"MDEND",
|
||||
"INNER_DIAMETER",
|
||||
"ROUGHNESS",
|
||||
"FIXED_SEGMENT_LENGTH",
|
||||
"MIN_SEGMENT_LENGTH",
|
||||
"MAX_SEGMENT_LENGTH",
|
||||
"COMMENT",
|
||||
}
|
||||
# SEGMENT length attribute -> add_segment_event keyword-argument name.
|
||||
_SEGMENT_LENGTH_KWARGS = {
|
||||
"FIXED_SEGMENT_LENGTH": "fixed_segment_length",
|
||||
"MIN_SEGMENT_LENGTH": "min_segment_length",
|
||||
"MAX_SEGMENT_LENGTH": "max_segment_length",
|
||||
}
|
||||
_PRESSURE_COMPONENTS = {"H--", "HF-", "HFA"}
|
||||
_VALVE_REQUIRED = ("MD", "TYPE")
|
||||
_VALVE_KNOWN = {"MD", "TYPE", "STATE", "CV", "AREA", "COMMENT"} | {
|
||||
@@ -2559,17 +2634,27 @@ def _apply_segment(
|
||||
)
|
||||
if "ROUGHNESS" in attrs:
|
||||
kwargs["roughness"] = float(_as_number(attrs["ROUGHNESS"], event.loc))
|
||||
lengths: Dict[str, float] = {}
|
||||
for name, kwarg in _SEGMENT_LENGTH_KWARGS.items():
|
||||
if name in attrs:
|
||||
lengths[kwarg] = float(_as_number(attrs[name], event.loc))
|
||||
except SimulatorEventsParseError as exc:
|
||||
_record_event_parse_error(event, exc, report)
|
||||
return
|
||||
|
||||
timeline_event = timeline.add_segment_event(**kwargs)
|
||||
well_path.segment_collection().add_segment_interval(
|
||||
timeline_event = timeline.add_segment_event(**kwargs, **lengths)
|
||||
interval = well_path.segment_collection().add_segment_interval(
|
||||
start_md=start_md,
|
||||
end_md=end_md,
|
||||
diameter=kwargs.get("inner_diameter", 0.152),
|
||||
roughness_factor=kwargs.get("roughness", 1.0e-5),
|
||||
)
|
||||
if "min_segment_length" in lengths:
|
||||
interval.set_min_segment_length(length=lengths["min_segment_length"])
|
||||
if "max_segment_length" in lengths:
|
||||
interval.set_max_segment_length(length=lengths["max_segment_length"])
|
||||
if "fixed_segment_length" in lengths:
|
||||
interval.set_fixed_segment_length(length=lengths["fixed_segment_length"])
|
||||
_apply_event_comment(event, timeline_event)
|
||||
report.events_applied += 1
|
||||
|
||||
|
||||
@@ -70,13 +70,34 @@ class FakeCompletionSettings:
|
||||
self.pressure_drop = "HF-"
|
||||
|
||||
|
||||
class FakeSegmentInterval:
|
||||
def __init__(self):
|
||||
self.segment_length_calls = []
|
||||
|
||||
def set_fixed_segment_length(self, length):
|
||||
self.segment_length_calls.append(("fixed", length))
|
||||
return self
|
||||
|
||||
def set_min_segment_length(self, length):
|
||||
self.segment_length_calls.append(("min", length))
|
||||
return self
|
||||
|
||||
def set_max_segment_length(self, length):
|
||||
self.segment_length_calls.append(("max", length))
|
||||
return self
|
||||
|
||||
|
||||
class FakeSegmentCollection:
|
||||
def __init__(self):
|
||||
self.segment_interval_calls = []
|
||||
self.intervals = []
|
||||
self.update_calls = 0
|
||||
|
||||
def add_segment_interval(self, **kwargs):
|
||||
self.segment_interval_calls.append(kwargs)
|
||||
interval = FakeSegmentInterval()
|
||||
self.intervals.append(interval)
|
||||
return interval
|
||||
|
||||
def update(self):
|
||||
self.update_calls += 1
|
||||
@@ -1445,6 +1466,64 @@ class TestDiagnostics:
|
||||
with pytest.raises(SimulatorEventsParseError, match="did you mean 'DURATION'"):
|
||||
parse_simulator_events("SIMEVENTS 1.2\nDURATON X = 5\n")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"lengths, expected",
|
||||
[
|
||||
(
|
||||
"FIXED_SEGMENT_LENGTH=20 MIN_SEGMENT_LENGTH=5",
|
||||
"FIXED_SEGMENT_LENGTH cannot be combined",
|
||||
),
|
||||
(
|
||||
"FIXED_SEGMENT_LENGTH=20 MAX_SEGMENT_LENGTH=50",
|
||||
"FIXED_SEGMENT_LENGTH cannot be combined",
|
||||
),
|
||||
(
|
||||
"MIN_SEGMENT_LENGTH=60 MAX_SEGMENT_LENGTH=50",
|
||||
"MIN_SEGMENT_LENGTH \\(60\\) exceeds MAX_SEGMENT_LENGTH \\(50\\)",
|
||||
),
|
||||
("MIN_SEGMENT_LENGTH=0", "MIN_SEGMENT_LENGTH must be greater than zero"),
|
||||
("MAX_SEGMENT_LENGTH=-5", "MAX_SEGMENT_LENGTH must be greater than zero"),
|
||||
("FIXED_SEGMENT_LENGTH=long", "FIXED_SEGMENT_LENGTH must be a number"),
|
||||
],
|
||||
)
|
||||
def test_invalid_segment_lengths_are_parse_errors(self, lengths, expected):
|
||||
text = (
|
||||
'SIMEVENTS 1.2\nWELL "W"\n'
|
||||
" 2024-01-01 PERFORATION MDSTART=1 MDEND=2\n"
|
||||
f" 2024-01-01 SEGMENT MDSTART=0 MDEND=2500 {lengths}\n" # line 4
|
||||
)
|
||||
with pytest.raises(SimulatorEventsParseError, match=expected) as excinfo:
|
||||
parse_simulator_events(text)
|
||||
assert [issue.loc.line for issue in excinfo.value.errors] == [4]
|
||||
|
||||
def test_invalid_segment_lengths_are_rejected_when_merging(self):
|
||||
first = parse_simulator_events(
|
||||
'SIMEVENTS 1.2\nWELL "W"\n'
|
||||
" 2024-01-01 SEGMENT MDSTART=0 MDEND=100 FIXED_SEGMENT_LENGTH=10\n"
|
||||
)
|
||||
first.wells[0].events[0].attributes.update(
|
||||
parse_simulator_events(
|
||||
'SIMEVENTS 1.2\nWELL "W"\n'
|
||||
" 2024-01-01 SEGMENT MDSTART=0 MDEND=100 MIN_SEGMENT_LENGTH=5\n"
|
||||
)
|
||||
.wells[0]
|
||||
.events[0]
|
||||
.attributes
|
||||
)
|
||||
with pytest.raises(
|
||||
SimulatorEventsParseError, match="FIXED_SEGMENT_LENGTH cannot be combined"
|
||||
):
|
||||
merge_simulator_events_documents([first])
|
||||
|
||||
def test_valid_segment_lengths_parse(self):
|
||||
document = parse_simulator_events(
|
||||
'SIMEVENTS 1.2\nWELL "W"\n'
|
||||
" 2024-01-01 SEGMENT MDSTART=0 MDEND=100 MIN_SEGMENT_LENGTH=5 "
|
||||
"MAX_SEGMENT_LENGTH=5\n"
|
||||
" 2024-01-01 SEGMENT MDSTART=100 MDEND=200 FIXED_SEGMENT_LENGTH=12.5\n"
|
||||
)
|
||||
assert len(document.wells[0].events) == 2
|
||||
|
||||
|
||||
class TestValidatorCli:
|
||||
def _write(self, tmp_path, text):
|
||||
@@ -1950,6 +2029,38 @@ class TestApplying:
|
||||
}
|
||||
]
|
||||
|
||||
def test_segment_min_max_lengths_are_mapped(self):
|
||||
text = (
|
||||
'SIMEVENTS 1.2\nWELL "55_33-A-1"\n'
|
||||
" 2024-01-01 SEGMENT MDSTART=0 MDEND=2500 MIN_SEGMENT_LENGTH=5 "
|
||||
"MAX_SEGMENT_LENGTH=50\n"
|
||||
)
|
||||
timeline, report = self._apply(text)
|
||||
assert report.errors == []
|
||||
assert report.events_applied == 1
|
||||
call = timeline.segment_calls[0]
|
||||
assert call["min_segment_length"] == 5.0
|
||||
assert call["max_segment_length"] == 50.0
|
||||
assert "fixed_segment_length" not in call
|
||||
|
||||
interval = call["well_path"].segment_collection().intervals[0]
|
||||
assert interval.segment_length_calls == [("min", 5.0), ("max", 50.0)]
|
||||
|
||||
def test_segment_fixed_length_is_mapped(self):
|
||||
text = (
|
||||
'SIMEVENTS 1.2\nWELL "55_33-A-1"\n'
|
||||
" 2024-01-01 SEGMENT MDSTART=0 MDEND=2500 FIXED_SEGMENT_LENGTH=12\n"
|
||||
)
|
||||
timeline, report = self._apply(text)
|
||||
assert report.events_applied == 1
|
||||
call = timeline.segment_calls[0]
|
||||
assert call["fixed_segment_length"] == 12.0
|
||||
assert "min_segment_length" not in call
|
||||
assert "max_segment_length" not in call
|
||||
|
||||
interval = call["well_path"].segment_collection().intervals[0]
|
||||
assert interval.segment_length_calls == [("fixed", 12.0)]
|
||||
|
||||
def test_segment_rejects_pressure_components(self):
|
||||
text = (
|
||||
'SIMEVENTS 1.2\nWELL "55_33-A-1"\n'
|
||||
@@ -3016,6 +3127,44 @@ class TestSimulatorEventsIntegration:
|
||||
"INSERT_DATE date should be emitted as a DATES entry"
|
||||
)
|
||||
|
||||
def test_segment_lengths_are_applied_to_interval(self, project_with_case_and_wells):
|
||||
project, _case, timeline = project_with_case_and_wells
|
||||
well = next(wp for wp in project.well_paths() if "A" in wp.name)
|
||||
|
||||
document = parse_simulator_events(
|
||||
"SIMEVENTS 1.2\n"
|
||||
f'WELL "{well.name}"\n'
|
||||
" 2024-01-01 SEGMENT MDSTART=0 MDEND=1000 MIN_SEGMENT_LENGTH=5 "
|
||||
"MAX_SEGMENT_LENGTH=50\n"
|
||||
" 2024-01-01 SEGMENT MDSTART=1000 MDEND=2500 FIXED_SEGMENT_LENGTH=25\n"
|
||||
)
|
||||
report = apply_simulator_events_document(document, timeline, project)
|
||||
assert report.errors == []
|
||||
assert report.events_applied == 2
|
||||
|
||||
events = timeline.descendants(rips.WellEventSegment)
|
||||
min_max_event = next(e for e in events if e.end_md == 1000.0)
|
||||
assert min_max_event.min_segment_length == (True, 5.0)
|
||||
assert min_max_event.max_segment_length == (True, 50.0)
|
||||
assert min_max_event.fixed_segment_length[0] is False
|
||||
|
||||
def check_intervals():
|
||||
intervals = {
|
||||
(i.start_md, i.end_md): i for i in well.segment_collection().intervals()
|
||||
}
|
||||
min_max = intervals[(0.0, 1000.0)]
|
||||
assert min_max.min_segment_length == (True, 5.0)
|
||||
assert min_max.max_segment_length == (True, 50.0)
|
||||
assert min_max.fixed_segment_length[0] is False
|
||||
fixed = intervals[(1000.0, 2500.0)]
|
||||
assert fixed.fixed_segment_length == (True, 25.0)
|
||||
assert fixed.min_segment_length[0] is False
|
||||
assert fixed.max_segment_length[0] is False
|
||||
|
||||
check_intervals()
|
||||
timeline.set_timestamp(timestamp="2024-12-24")
|
||||
check_intervals()
|
||||
|
||||
def test_apply_full_event_coverage_and_schedule(self, project_with_case_and_wells):
|
||||
"""All event kinds from well_event_schedule.py expressed as SIMEVENTS."""
|
||||
project, case, timeline = project_with_case_and_wells
|
||||
|
||||
@@ -115,6 +115,39 @@ class TestWellEventTimeline:
|
||||
|
||||
assert event is not None, "Segment event should be created"
|
||||
|
||||
def test_add_segment_event_segment_lengths(self, test_well_path):
|
||||
"""Segment lengths are stored on the event; 0 means not set."""
|
||||
wellpath, timeline = test_well_path
|
||||
|
||||
event = timeline.add_segment_event(
|
||||
event_date="2024-01-01",
|
||||
well_path=wellpath,
|
||||
start_md=500.0,
|
||||
end_md=1000.0,
|
||||
min_segment_length=5.0,
|
||||
max_segment_length=50.0,
|
||||
)
|
||||
assert event.min_segment_length == (True, 5.0)
|
||||
assert event.max_segment_length == (True, 50.0)
|
||||
assert event.fixed_segment_length[0] is False
|
||||
|
||||
with pytest.raises(rips.RipsError, match="cannot be combined"):
|
||||
timeline.add_segment_event(
|
||||
well_path=wellpath,
|
||||
start_md=500.0,
|
||||
end_md=1000.0,
|
||||
fixed_segment_length=10.0,
|
||||
min_segment_length=5.0,
|
||||
)
|
||||
with pytest.raises(rips.RipsError, match="cannot exceed"):
|
||||
timeline.add_segment_event(
|
||||
well_path=wellpath,
|
||||
start_md=500.0,
|
||||
end_md=1000.0,
|
||||
min_segment_length=60.0,
|
||||
max_segment_length=50.0,
|
||||
)
|
||||
|
||||
|
||||
class TestWellEventScheduleApplication:
|
||||
"""Tests for applying well events to create completions."""
|
||||
|
||||
@@ -80,7 +80,8 @@ event_line = "@" , date_expr , event_type , { attribute } ;
|
||||
event_type = completion_event_type | eclipse_keyword ;
|
||||
completion_event_type = "PERFORATION" | "SEGMENT" | "VALVE" | "STATE" | "WELSPECS" ;
|
||||
segment_attribute = "MDSTART" | "MDEND" | "INNER_DIAMETER" | "ROUGHNESS"
|
||||
| "COMMENT" ;
|
||||
| "FIXED_SEGMENT_LENGTH" | "MIN_SEGMENT_LENGTH"
|
||||
| "MAX_SEGMENT_LENGTH" | "COMMENT" ;
|
||||
(* accepted attribute names when event_type is "SEGMENT" *)
|
||||
wellspec_attribute = "GROUP" | "CROSSFLOW" | "REFDEPTH" | "PHASE"
|
||||
| "PRESSURE_COMPONENTS" | "COMMENT" ;
|
||||
@@ -219,6 +220,11 @@ Each event adds the underlying segment timeline event and a corresponding **Segm
|
||||
| `MDEND` | yes | segment event and segment interval `end_md` |
|
||||
| `INNER_DIAMETER` | no | segment event `inner_diameter` [m] |
|
||||
| `ROUGHNESS` | no | segment event `roughness` [m] |
|
||||
| `FIXED_SEGMENT_LENGTH` | no | segment event `fixed_segment_length` and segment interval fixed segment length [m] |
|
||||
| `MIN_SEGMENT_LENGTH` | no | segment event `min_segment_length` and segment interval min segment length [m] |
|
||||
| `MAX_SEGMENT_LENGTH` | no | segment event `max_segment_length` and segment interval max segment length [m] |
|
||||
|
||||
The segment lengths control the MSW segmentation of the interval. Lengths must be positive. `FIXED_SEGMENT_LENGTH` cannot be combined with `MIN_SEGMENT_LENGTH` or `MAX_SEGMENT_LENGTH`, and `MIN_SEGMENT_LENGTH` cannot exceed `MAX_SEGMENT_LENGTH`. Violations are reported as parse errors.
|
||||
|
||||
**VALVE** → `add_valve_event` (requires an existing perforation at the MD)
|
||||
|
||||
|
||||
Reference in new issue
Block a user