diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventSegment.cpp b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventSegment.cpp index 3c3d785ddf..99751c75be 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventSegment.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventSegment.cpp @@ -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 RimWellEventSegment::fixedSegmentLength() const +{ + if ( m_fixedSegmentLength().first ) return m_fixedSegmentLength().second; + return std::nullopt; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::optional RimWellEventSegment::minSegmentLength() const +{ + if ( m_minSegmentLength().first ) return m_minSegmentLength().second; + return std::nullopt; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::optional RimWellEventSegment::maxSegmentLength() const +{ + if ( m_maxSegmentLength().first ) return m_maxSegmentLength().second; + return std::nullopt; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimWellEventSegment::setFixedSegmentLength( std::optional length ) +{ + m_fixedSegmentLength = std::make_pair( length.has_value(), length.value_or( m_fixedSegmentLength().second ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimWellEventSegment::setMinSegmentLength( std::optional length ) +{ + m_minSegmentLength = std::make_pair( length.has_value(), length.value_or( m_minSegmentLength().second ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimWellEventSegment::setMaxSegmentLength( std::optional 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(); } diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventSegment.h b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventSegment.h index de4e9da85c..5db6d52a41 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventSegment.h +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventSegment.h @@ -22,6 +22,9 @@ #include "cafPdmField.h" +#include +#include + //================================================================================================== /// /// 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 fixedSegmentLength() const; + std::optional minSegmentLength() const; + std::optional maxSegmentLength() const; + void setFixedSegmentLength( std::optional length ); + void setMinSegmentLength( std::optional length ); + void setMaxSegmentLength( std::optional length ); + // Override from RimWellEvent EventType eventType() const override; QString generateScheduleKeyword( const QString& wellName ) const override; @@ -60,4 +71,8 @@ private: caf::PdmField m_endMD; caf::PdmField m_innerDiameter; caf::PdmField m_roughness; + + caf::PdmField> m_fixedSegmentLength; + caf::PdmField> m_minSegmentLength; + caf::PdmField> m_maxSegmentLength; }; diff --git a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp index e152b7d57f..6fbffa8a57 100644 --- a/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp +++ b/ApplicationLibCode/ProjectDataModel/WellEvents/RimWellEventTimeline.cpp @@ -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; } diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp index 7ff3a1d8a0..44d9a0b69c 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.cpp @@ -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 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( 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; } diff --git a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h index 5fd9eeaab8..efe46443d4 100644 --- a/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h +++ b/ApplicationLibCode/ProjectDataModelCommands/RimcWellEventTimeline.h @@ -155,6 +155,9 @@ private: caf::PdmField m_endMd; caf::PdmField m_innerDiameter; caf::PdmField m_roughness; + caf::PdmField m_fixedSegmentLength; + caf::PdmField m_minSegmentLength; + caf::PdmField m_maxSegmentLength; }; //================================================================================================== diff --git a/GrpcInterface/Python/rips/PythonExamples/experimental/well_event_schedule_simulator_events.py b/GrpcInterface/Python/rips/PythonExamples/experimental/well_event_schedule_simulator_events.py index 1f1b52eb1c..da06ab99d9 100644 --- a/GrpcInterface/Python/rips/PythonExamples/experimental/well_event_schedule_simulator_events.py +++ b/GrpcInterface/Python/rips/PythonExamples/experimental/well_event_schedule_simulator_events.py @@ -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} diff --git a/GrpcInterface/Python/rips/simulator_events.py b/GrpcInterface/Python/rips/simulator_events.py index f19c45f9a9..9218ec922e 100644 --- a/GrpcInterface/Python/rips/simulator_events.py +++ b/GrpcInterface/Python/rips/simulator_events.py @@ -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 diff --git a/GrpcInterface/Python/rips/tests/test_simulator_events.py b/GrpcInterface/Python/rips/tests/test_simulator_events.py index eb8c5f8b97..e5bad6c3f2 100644 --- a/GrpcInterface/Python/rips/tests/test_simulator_events.py +++ b/GrpcInterface/Python/rips/tests/test_simulator_events.py @@ -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 diff --git a/GrpcInterface/Python/rips/tests/test_well_events.py b/GrpcInterface/Python/rips/tests/test_well_events.py index ca8978f034..f2307c66cc 100644 --- a/GrpcInterface/Python/rips/tests/test_well_events.py +++ b/GrpcInterface/Python/rips/tests/test_well_events.py @@ -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.""" diff --git a/docs/simulatorEvents.md b/docs/simulatorEvents.md index 78136ac2b4..031c21bfb1 100644 --- a/docs/simulatorEvents.md +++ b/docs/simulatorEvents.md @@ -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)