From 3ffd1a80f08a66fe6fe983734d064a224192da4a Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Fri, 6 Mar 2026 06:57:36 +0000 Subject: [PATCH 1/2] Check that op intent details extents are within reference extents --- .basedpyright/baseline.json | 8 -- .../scenarios/astm/utm/test_steps.py | 101 ++++++++++++++++++ .../utm/validate_shared_operational_intent.md | 4 + 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index d8d8937c98..767794c432 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -19705,14 +19705,6 @@ "lineCount": 1 } }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 16, - "endColumn": 23, - "lineCount": 1 - } - }, { "code": "reportArgumentType", "range": { diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py b/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py index 001b93a0e4..a693024d95 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py @@ -1,5 +1,6 @@ from __future__ import annotations +import datetime from enum import Enum from implicitdict import ImplicitDict, Optional @@ -37,6 +38,9 @@ TestScenarioType, ) +NUMERIC_PRECISION_TIME = datetime.timedelta(milliseconds=1) +NUMERIC_PRECISION_DISTANCE = 0.001 # meters + class OpIntentValidator: """ @@ -420,6 +424,7 @@ def _check_op_intent_details( details=f"Received status code {oi_full_query.status_code} from {self._flight_planner.participant_id} when querying for details of operational intent {oi_ref.id}; {e}", query_timestamps=[oi_full_query.request.timestamp], ) + raise ScenarioDidNotStopError(check) validation_failures = self._evaluate_op_intent_validation(oi_full_query) with self._scenario.check( @@ -459,6 +464,102 @@ def _check_op_intent_details( query_timestamps=[oi_full_query.request.timestamp], ) + with self._scenario.check( + "Details 4D extents are within reference extents", + [self._flight_planner.participant_id], + ) as check: + all_volumes = oi_full.details.get("volumes", []) + oi_full.details.get( + "off_nominal_volumes", [] + ) + for v in all_volumes: + # Time start check + v_time_start = v.get("time_start") + ref_time_start = oi_ref.get("time_start") + if not v_time_start: + if ref_time_start: + check.record_failed( + summary="Details volume starts before reference", + details="A volume in the operational intent details has no start time (infinite past), but the operational intent reference specifies a start time.", + query_timestamps=[oi_full_query.request.timestamp], + ) + elif ref_time_start: + if ( + v_time_start.value.datetime + < ref_time_start.value.datetime - NUMERIC_PRECISION_TIME + ): + check.record_failed( + summary="Details volume starts before reference", + details=f"A volume in the operational intent details starts at {v_time_start.value.datetime}, which is before the operational intent reference start time {ref_time_start.value.datetime}.", + query_timestamps=[oi_full_query.request.timestamp], + ) + # Time end check + v_time_end = v.get("time_end") + ref_time_end = oi_ref.get("time_end") + if not v_time_end: + if ref_time_end: + check.record_failed( + summary="Details volume ends after reference", + details="A volume in the operational intent details has no end time (infinite future), but the operational intent reference specifies an end time.", + query_timestamps=[oi_full_query.request.timestamp], + ) + elif ref_time_end: + if ( + v_time_end.value.datetime + > ref_time_end.value.datetime + NUMERIC_PRECISION_TIME + ): + check.record_failed( + summary="Details volume ends after reference", + details=f"A volume in the operational intent details ends at {v_time_end.value.datetime}, which is after the operational intent reference end time {ref_time_end.value.datetime}.", + query_timestamps=[oi_full_query.request.timestamp], + ) + # Altitude check (if reference specifies altitude, which it typically doesn't in F3548, but implemented defensively just in case) + v_altitude_lower = ( + v.get("volume", {}).get("altitude_lower") + if v.get("volume") + else None + ) + ref_altitude_lower = oi_ref.get("altitude_lower") + if not v_altitude_lower: + if ref_altitude_lower: + check.record_failed( + summary="Details volume lower altitude below reference", + details="A volume in the operational intent details has no lower altitude bound (infinite downward), but the operational intent reference specifies a lower altitude.", + query_timestamps=[oi_full_query.request.timestamp], + ) + elif ref_altitude_lower: + if ( + v_altitude_lower.value + < ref_altitude_lower.value - NUMERIC_PRECISION_DISTANCE + ): + check.record_failed( + summary="Details volume lower altitude below reference", + details=f"A volume in the operational intent details has lower altitude {v_altitude_lower.value}, which is below the operational intent reference lower altitude {ref_altitude_lower.value}.", + query_timestamps=[oi_full_query.request.timestamp], + ) + v_altitude_upper = ( + v.get("volume", {}).get("altitude_upper") + if v.get("volume") + else None + ) + ref_altitude_upper = oi_ref.get("altitude_upper") + if not v_altitude_upper: + if ref_altitude_upper: + check.record_failed( + summary="Details volume upper altitude above reference", + details="A volume in the operational intent details has no upper altitude bound (infinite upward), but the operational intent reference specifies an upper altitude.", + query_timestamps=[oi_full_query.request.timestamp], + ) + elif ref_altitude_upper: + if ( + v_altitude_upper.value + > ref_altitude_upper.value + NUMERIC_PRECISION_DISTANCE + ): + check.record_failed( + summary="Details volume upper altitude above reference", + details=f"A volume in the operational intent details has upper altitude {v_altitude_upper.value}, which is above the operational intent reference upper altitude {ref_altitude_upper.value}.", + query_timestamps=[oi_full_query.request.timestamp], + ) + with self._scenario.check( "Off-nominal volumes", [self._flight_planner.participant_id] ) as check: diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/validate_shared_operational_intent.md b/monitoring/uss_qualifier/scenarios/astm/utm/validate_shared_operational_intent.md index c6e8756f76..912ff5a56f 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/validate_shared_operational_intent.md +++ b/monitoring/uss_qualifier/scenarios/astm/utm/validate_shared_operational_intent.md @@ -33,6 +33,10 @@ If the operational intent details response does not validate against [the GetOpe If the operational intent details reported by the USS do not match the user's flight intent, this check will fail per **[interuss.automated_testing.flight_planning.ExpectedBehavior](../../../requirements/interuss/automated_testing/flight_planning.md)** and **[astm.f3548.v21.OPIN0025](../../../requirements/astm/f3548/v21.md)**. +## 🛑 Details 4D extents are within reference extents check + +If the 4D extents (start time, end time, and altitude if specified) of any of the detailed operational intent volumes are not fully contained within the 4D extents of the operational intent reference, this check will fail per **[astm.f3548.v21.USS0105,1](../../../requirements/astm/f3548/v21.md)**. + ## ⚠️ Off-nominal volumes check **[astm.f3548.v21.OPIN0015](../../../requirements/astm/f3548/v21.md)** specifies that nominal operational intents (Accepted and Activated) must not include any off-nominal 4D volumes, so this check will fail if an Accepted or Activated operational intent includes off-nominal volumes. From 9caf5c491b1e2a87befba0c6d9c5af0179b3c4fe Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Wed, 11 Mar 2026 17:41:48 +0000 Subject: [PATCH 2/2] Address comments --- monitoring/monitorlib/geo.py | 2 +- monitoring/monitorlib/geotemporal.py | 30 ++- .../scenarios/astm/utm/test_steps.py | 177 +++++++++--------- .../utm/validate_shared_operational_intent.md | 4 +- 4 files changed, 118 insertions(+), 95 deletions(-) diff --git a/monitoring/monitorlib/geo.py b/monitoring/monitorlib/geo.py index db205bebf9..a6cbc4faab 100644 --- a/monitoring/monitorlib/geo.py +++ b/monitoring/monitorlib/geo.py @@ -208,7 +208,7 @@ def to_flight_planning_api(self) -> fp_api.Altitude: units=fp_api.AltitudeUnits.M, ) - def to_w84_m(self): + def to_w84_m(self) -> float: """This altitude expressed in WGS84 meters, if possible to convert to it.""" if self.reference != AltitudeDatum.W84: raise NotImplementedError( diff --git a/monitoring/monitorlib/geotemporal.py b/monitoring/monitorlib/geotemporal.py index bf08cc224c..e4bf3f9843 100644 --- a/monitoring/monitorlib/geotemporal.py +++ b/monitoring/monitorlib/geotemporal.py @@ -13,7 +13,15 @@ from uas_standards.interuss.automated_testing.scd.v1 import api as interuss_scd_api from monitoring.monitorlib import geo -from monitoring.monitorlib.geo import Altitude, Circle, LatLngPoint, Polygon, Volume3D +from monitoring.monitorlib.geo import ( + Altitude, + AltitudeDatum, + Circle, + DistanceUnits, + LatLngPoint, + Polygon, + Volume3D, +) from monitoring.monitorlib.temporal import ( TestTime, TestTimeContext, @@ -304,6 +312,26 @@ def time_end(self) -> Time | None: else None ) + @property + def altitude_lower(self) -> Altitude | None: + return Altitude( + value=min(v.volume.altitude_lower_wgs84_m() for v in self) + if all(v.volume.altitude_lower_wgs84_m() is not None for v in self) + else None, + reference=AltitudeDatum.W84, + units=DistanceUnits.M, + ) + + @property + def altitude_upper(self) -> Altitude | None: + return Altitude( + value=min(v.volume.altitude_upper_wgs84_m() for v in self) + if all(v.volume.altitude_upper_wgs84_m() is not None for v in self) + else None, + reference=AltitudeDatum.W84, + units=DistanceUnits.M, + ) + def offset_times(self, dt: timedelta) -> Volume4DCollection: return Volume4DCollection([v.offset_time(dt) for v in self]) diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py b/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py index 053e375efb..942008864b 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py @@ -481,100 +481,95 @@ def _check_op_intent_details( ) with self._scenario.check( - "Details 4D extents are within reference extents", + "Operational intent details extents are contained within reference extents", [self._flight_planner.participant_id], ) as check: - all_volumes = oi_full.details.get("volumes", []) + oi_full.details.get( - "off_nominal_volumes", [] + all_volumes = Volume4DCollection.from_f3548v21( + oi_full.details.get("volumes", []) + + oi_full.details.get("off_nominal_volumes", []) ) - for v in all_volumes: - # Time start check - v_time_start = v.get("time_start") - ref_time_start = oi_ref.get("time_start") - if not v_time_start: - if ref_time_start: - check.record_failed( - summary="Details volume starts before reference", - details="A volume in the operational intent details has no start time (infinite past), but the operational intent reference specifies a start time.", - query_timestamps=[oi_full_query.request.timestamp], - ) - elif ref_time_start: - if ( - v_time_start.value.datetime - < ref_time_start.value.datetime - NUMERIC_PRECISION_TIME - ): - check.record_failed( - summary="Details volume starts before reference", - details=f"A volume in the operational intent details starts at {v_time_start.value.datetime}, which is before the operational intent reference start time {ref_time_start.value.datetime}.", - query_timestamps=[oi_full_query.request.timestamp], - ) - # Time end check - v_time_end = v.get("time_end") - ref_time_end = oi_ref.get("time_end") - if not v_time_end: - if ref_time_end: - check.record_failed( - summary="Details volume ends after reference", - details="A volume in the operational intent details has no end time (infinite future), but the operational intent reference specifies an end time.", - query_timestamps=[oi_full_query.request.timestamp], - ) - elif ref_time_end: - if ( - v_time_end.value.datetime - > ref_time_end.value.datetime + NUMERIC_PRECISION_TIME - ): - check.record_failed( - summary="Details volume ends after reference", - details=f"A volume in the operational intent details ends at {v_time_end.value.datetime}, which is after the operational intent reference end time {ref_time_end.value.datetime}.", - query_timestamps=[oi_full_query.request.timestamp], - ) - # Altitude check (if reference specifies altitude, which it typically doesn't in F3548, but implemented defensively just in case) - v_altitude_lower = ( - v.get("volume", {}).get("altitude_lower") - if v.get("volume") - else None - ) - ref_altitude_lower = oi_ref.get("altitude_lower") - if not v_altitude_lower: - if ref_altitude_lower: - check.record_failed( - summary="Details volume lower altitude below reference", - details="A volume in the operational intent details has no lower altitude bound (infinite downward), but the operational intent reference specifies a lower altitude.", - query_timestamps=[oi_full_query.request.timestamp], - ) - elif ref_altitude_lower: - if ( - v_altitude_lower.value - < ref_altitude_lower.value - NUMERIC_PRECISION_DISTANCE - ): - check.record_failed( - summary="Details volume lower altitude below reference", - details=f"A volume in the operational intent details has lower altitude {v_altitude_lower.value}, which is below the operational intent reference lower altitude {ref_altitude_lower.value}.", - query_timestamps=[oi_full_query.request.timestamp], - ) - v_altitude_upper = ( - v.get("volume", {}).get("altitude_upper") - if v.get("volume") - else None - ) - ref_altitude_upper = oi_ref.get("altitude_upper") - if not v_altitude_upper: - if ref_altitude_upper: - check.record_failed( - summary="Details volume upper altitude above reference", - details="A volume in the operational intent details has no upper altitude bound (infinite upward), but the operational intent reference specifies an upper altitude.", - query_timestamps=[oi_full_query.request.timestamp], - ) - elif ref_altitude_upper: - if ( - v_altitude_upper.value - > ref_altitude_upper.value + NUMERIC_PRECISION_DISTANCE - ): - check.record_failed( - summary="Details volume upper altitude above reference", - details=f"A volume in the operational intent details has upper altitude {v_altitude_upper.value}, which is above the operational intent reference upper altitude {ref_altitude_upper.value}.", - query_timestamps=[oi_full_query.request.timestamp], - ) + + # Time start check + v_time_start = all_volumes.time_start + ref_time_start = oi_ref.get("time_start") + if not v_time_start: + if ref_time_start: + check.record_failed( + summary="Details volume starts before reference", + details="A volume in the operational intent details has no start time (infinite past), but the operational intent reference specifies a start time.", + query_timestamps=[oi_full_query.request.timestamp], + ) + elif ref_time_start: + if ( + v_time_start.datetime + < ref_time_start.value.datetime - NUMERIC_PRECISION_TIME + ): + check.record_failed( + summary="Details volume starts before reference", + details=f"A volume in the operational intent details starts at {v_time_start}, which is before the operational intent reference start time {ref_time_start.value.datetime}.", + query_timestamps=[oi_full_query.request.timestamp], + ) + + # Time end check + v_time_end = all_volumes.time_end + ref_time_end = oi_ref.get("time_end") + if not v_time_end: + if ref_time_end: + check.record_failed( + summary="Details volume ends after reference", + details="A volume in the operational intent details has no end time (infinite future), but the operational intent reference specifies an end time.", + query_timestamps=[oi_full_query.request.timestamp], + ) + elif ref_time_end: + if ( + v_time_end.datetime + > ref_time_end.value.datetime + NUMERIC_PRECISION_TIME + ): + check.record_failed( + summary="Details volume ends after reference", + details=f"A volume in the operational intent details ends at {v_time_end.datetime}, which is after the operational intent reference end time {ref_time_end.value.datetime}.", + query_timestamps=[oi_full_query.request.timestamp], + ) + + # Altitude check (if reference specifies altitude, which it typically doesn't in F3548, but implemented defensively just in case) + v_altitude_lower = all_volumes.altitude_lower + ref_altitude_lower = oi_ref.get("altitude_lower") + if not v_altitude_lower: + if ref_altitude_lower: + check.record_failed( + summary="Details volume lower altitude below reference", + details="A volume in the operational intent details has no lower altitude bound (infinite downward), but the operational intent reference specifies a lower altitude.", + query_timestamps=[oi_full_query.request.timestamp], + ) + elif ref_altitude_lower: + if ( + v_altitude_lower.value + < ref_altitude_lower.value - NUMERIC_PRECISION_DISTANCE + ): + check.record_failed( + summary="Details volume lower altitude below reference", + details=f"A volume in the operational intent details has lower altitude {v_altitude_lower.value}, which is below the operational intent reference lower altitude {ref_altitude_lower.value}.", + query_timestamps=[oi_full_query.request.timestamp], + ) + v_altitude_upper = all_volumes.altitude_upper + ref_altitude_upper = oi_ref.get("altitude_upper") + if not v_altitude_upper: + if ref_altitude_upper: + check.record_failed( + summary="Details volume upper altitude above reference", + details="A volume in the operational intent details has no upper altitude bound (infinite upward), but the operational intent reference specifies an upper altitude.", + query_timestamps=[oi_full_query.request.timestamp], + ) + elif ref_altitude_upper: + if ( + v_altitude_upper.value + > ref_altitude_upper.value + NUMERIC_PRECISION_DISTANCE + ): + check.record_failed( + summary="Details volume upper altitude above reference", + details=f"A volume in the operational intent details has upper altitude {v_altitude_upper.value}, which is above the operational intent reference upper altitude {ref_altitude_upper.value}.", + query_timestamps=[oi_full_query.request.timestamp], + ) with self._scenario.check( "Off-nominal volumes", [self._flight_planner.participant_id] diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/validate_shared_operational_intent.md b/monitoring/uss_qualifier/scenarios/astm/utm/validate_shared_operational_intent.md index c14d208ea3..0cafa7e1f3 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/validate_shared_operational_intent.md +++ b/monitoring/uss_qualifier/scenarios/astm/utm/validate_shared_operational_intent.md @@ -37,9 +37,9 @@ If any of the values in the operational intent reference reported by the USS do If the operational intent details reported by the USS do not match the user's flight intent, this check will fail per **[interuss.automated_testing.flight_planning.ExpectedBehavior](../../../requirements/interuss/automated_testing/flight_planning.md)** and **[astm.f3548.v21.OPIN0025](../../../requirements/astm/f3548/v21.md)**. -## 🛑 Details 4D extents are within reference extents check +## 🛑 Operational intent details extents are contained within reference extents check -If the 4D extents (start time, end time, and altitude if specified) of any of the detailed operational intent volumes are not fully contained within the 4D extents of the operational intent reference, this check will fail per **[astm.f3548.v21.USS0105,1](../../../requirements/astm/f3548/v21.md)**. +If the 4D extents (start time, end time, and altitude if specified) of any of the operational intent detail volumes are not fully contained within the 4D extents of the operational intent reference, this check will fail per **[astm.f3548.v21.USS0105,1](../../../requirements/astm/f3548/v21.md)**. ## ⚠️ Off-nominal volumes check