Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion monitoring/monitorlib/geo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
30 changes: 29 additions & 1 deletion monitoring/monitorlib/geotemporal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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])

Expand Down
97 changes: 96 additions & 1 deletion monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import datetime
from enum import Enum

from implicitdict import ImplicitDict, Optional
Expand Down Expand Up @@ -38,6 +39,9 @@
TestScenarioType,
)

NUMERIC_PRECISION_TIME = datetime.timedelta(milliseconds=1)
NUMERIC_PRECISION_DISTANCE = 0.001 # meters


class OpIntentValidator:
"""
Expand Down Expand Up @@ -421,7 +425,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],
)
return
raise ScenarioDidNotStopError(check)

validation_failures = self._evaluate_op_intent_validation(oi_full_query)
with self._scenario.check(
Expand Down Expand Up @@ -476,6 +480,97 @@ def _check_op_intent_details(
query_timestamps=[oi_full_query.request.timestamp],
)

with self._scenario.check(
"Operational intent details extents are contained within reference extents",
[self._flight_planner.participant_id],
) as check:
all_volumes = Volume4DCollection.from_f3548v21(
oi_full.details.get("volumes", [])
+ oi_full.details.get("off_nominal_volumes", [])
)

# 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]
) as check:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ 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)**.

## 🛑 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 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

**[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.
Expand Down
Loading