diff --git a/monitoring/monitorlib/clients/flight_planning/flight_info_template.py b/monitoring/monitorlib/clients/flight_planning/flight_info_template.py index b18f936a80..2251a23dc6 100644 --- a/monitoring/monitorlib/clients/flight_planning/flight_info_template.py +++ b/monitoring/monitorlib/clients/flight_planning/flight_info_template.py @@ -1,4 +1,4 @@ -from typing import Optional, Dict +from typing import Optional, Dict, List from implicitdict import ImplicitDict @@ -11,11 +11,13 @@ BasicFlightPlanInformation, FlightInfo, ) +from monitoring.monitorlib.geo import LatLngPoint from monitoring.monitorlib.geotemporal import ( Volume4DTemplateCollection, Volume4DCollection, ) from monitoring.monitorlib.temporal import Time, TimeDuringTest +from monitoring.monitorlib.transformations import Transformation from uas_standards.interuss.automated_testing.scd.v1 import api as scd_api @@ -51,9 +53,16 @@ class FlightInfoTemplate(ImplicitDict): additional_information: Optional[dict] """Any information relevant to a particular jurisdiction or use case not described in the standard schema. The keys and values must be agreed upon between the test designers and USSs under test.""" + transformations: Optional[List[Transformation]] + """If specified, transform this flight according to these transformations in order (after all templates are resolved).""" + def resolve(self, times: Dict[TimeDuringTest, Time]) -> FlightInfo: - kwargs = {k: v for k, v in self.items()} - kwargs["basic_information"] = self.basic_information.resolve(times) + kwargs = {k: v for k, v in self.items() if k not in {"transformations"}} + basic_info = self.basic_information.resolve(times) + if "transformations" in self and self.transformations: + for xform in self.transformations: + basic_info.area = [v.transform(xform) for v in basic_info.area] + kwargs["basic_information"] = basic_info return ImplicitDict.parse(kwargs, FlightInfo) def to_scd_inject_request( diff --git a/monitoring/monitorlib/geo.py b/monitoring/monitorlib/geo.py index b47b483bb2..ad559ac7cd 100644 --- a/monitoring/monitorlib/geo.py +++ b/monitoring/monitorlib/geo.py @@ -10,6 +10,12 @@ from s2sphere import LatLng from scipy.interpolate import RectBivariateSpline as Spline import shapely.geometry + +from monitoring.monitorlib.transformations import ( + Transformation, + RelativeTranslation, + AbsoluteTranslation, +) from uas_standards.astm.f3548.v21 import api as f3548v21 from uas_standards.astm.f3411.v19 import api as f3411v19 from uas_standards.astm.f3411.v22a import api as f3411v22a @@ -69,6 +75,10 @@ def from_f3411( def to_flight_planning_api(self) -> fp_api.LatLngPoint: return fp_api.LatLngPoint(lat=self.lat, lng=self.lng) + @staticmethod + def from_s2(p: s2sphere.LatLng) -> LatLngPoint: + return LatLngPoint(lat=p.lat().degrees, lng=p.lng().degrees) + def as_s2sphere(self) -> s2sphere.LatLng: return s2sphere.LatLng.from_degrees(self.lat, self.lng) @@ -89,7 +99,12 @@ def in_meters(self) -> float: class Polygon(ImplicitDict): - vertices: List[LatLngPoint] + vertices: Optional[List[LatLngPoint]] + + def vertex_average(self) -> LatLngPoint: + lat = sum(p.lat for p in self.vertices) / len(self.vertices) + lng = sum(p.lng for p in self.vertices) / len(self.vertices) + return LatLngPoint(lat=lat, lng=lng) @staticmethod def from_coords(coords: List[Tuple[float, float]]) -> Polygon: @@ -264,6 +279,81 @@ def intersects_vol3(self, vol3_2: Volume3D) -> bool: return footprint1.intersects(footprint2) + def transform(self, transformation: Transformation): + if ( + "relative_translation" in transformation + and transformation.relative_translation + ): + return self.translate_relative(transformation.relative_translation) + elif ( + "absolute_translation" in transformation + and transformation.absolute_translation + ): + return self.translate_absolute(transformation.absolute_translation) + raise ValueError( + f"No supported transformation defined (keys: {', '.join(transformation)})" + ) + + def translate_relative(self, translation: RelativeTranslation) -> Volume3D: + def offset(p0: LatLngPoint, p: LatLngPoint) -> LatLngPoint: + s2_p0 = p0.as_s2sphere() + xy = flatten(s2_p0, p.as_s2sphere()) + if "meters_east" in translation and translation.meters_east: + xy = (xy[0] + translation.meters_east, xy[1]) + if "meters_north" in translation and translation.meters_north: + xy = (xy[0], xy[1] + translation.meters_north) + p1 = LatLngPoint.from_s2(unflatten(s2_p0, xy)) + if "degrees_east" in translation and translation.degrees_east: + p1.lng += translation.degrees_east + if "degrees_north" in translation and translation.degrees_north: + p1.lat += translation.degrees_north + return p1 + + kwargs = {k: v for k, v in self.items() if v is not None} + if self.outline_circle is not None: + kwargs["outline_circle"] = Circle( + center=offset(self.outline_circle.center, self.outline_circle.center), + radius=self.outline_circle.radius, + ) + if self.outline_polygon is not None: + ref0 = self.outline_polygon.vertex_average() + vertices = [offset(ref0, p) for p in self.outline_polygon.vertices] + kwargs["outline_polygon"] = Polygon(vertices=vertices) + result = Volume3D(**kwargs) + if "meters_up" in translation and translation.meters_up: + if result.altitude_lower: + if result.altitude_lower.units == DistanceUnits.M: + result.altitude_lower.value += translation.meters_up + else: + raise NotImplementedError( + f"Cannot yet translate meters_up with {result.altitude_lower.units} lower altitude units" + ) + if result.altitude_upper: + if result.altitude_upper.units == DistanceUnits.M: + result.altitude_upper.value += translation.meters_up + else: + raise NotImplementedError( + f"Cannot yet translate meters_up with {result.altitude_lower.units} upper altitude units" + ) + return result + + def translate_absolute(self, translation: AbsoluteTranslation) -> Volume3D: + new_center = LatLngPoint( + lat=translation.new_latitude, lng=translation.new_longitude + ) + kwargs = {k: v for k, v in self.items() if v is not None} + if self.outline_circle is not None: + kwargs["outline_circle"] = Circle( + center=new_center, radius=self.outline_circle.radius + ) + if self.outline_polygon is not None: + ref0 = self.outline_polygon.vertex_average().as_s2sphere() + xy = [flatten(ref0, p.as_s2sphere()) for p in self.outline_polygon.vertices] + ref1 = new_center.as_s2sphere() + vertices = [LatLngPoint.from_s2(unflatten(ref1, p)) for p in xy] + kwargs["outline_polygon"] = Polygon(vertices=vertices) + return Volume3D(**kwargs) + @staticmethod def from_flight_planning_api(vol: fp_api.Volume3D) -> Volume3D: return ImplicitDict.parse(vol, Volume3D) diff --git a/monitoring/monitorlib/geotemporal.py b/monitoring/monitorlib/geotemporal.py index 10a553d1fe..9114e6444a 100644 --- a/monitoring/monitorlib/geotemporal.py +++ b/monitoring/monitorlib/geotemporal.py @@ -4,9 +4,10 @@ from datetime import datetime, timedelta from typing import Optional, List, Tuple, Dict -import arrow from implicitdict import ImplicitDict, StringBasedTimeDelta import s2sphere as s2sphere + +from monitoring.monitorlib.transformations import Transformation from uas_standards.astm.f3548.v21 import api as f3548v21 from uas_standards.interuss.automated_testing.flight_planning.v1 import api as fp_api from uas_standards.interuss.automated_testing.scd.v1 import api as interuss_scd_api @@ -38,6 +39,9 @@ class Volume4DTemplate(ImplicitDict): altitude_upper: Optional[Altitude] = None """The maximum altitude at which the virtual user will fly while using this volume for their flight.""" + transformations: Optional[List[Transformation]] = None + """If specified, transform this volume according to these transformations in order.""" + def resolve(self, times: Dict[TimeDuringTest, Time]) -> Volume4D: """Resolve Volume4DTemplate into concrete Volume4D.""" # Make 3D volume @@ -84,7 +88,13 @@ def resolve(self, times: Dict[TimeDuringTest, Time]) -> Volume4D: if time_end is not None: kwargs["time_end"] = time_end - return Volume4D(**kwargs) + result = Volume4D(**kwargs) + + if self.transformations: + for xform in self.transformations: + result = result.transform(xform) + + return result class Volume4D(ImplicitDict): @@ -102,6 +112,11 @@ def offset_time(self, dt: timedelta) -> Volume4D: kwargs["time_end"] = self.time_end.offset(dt) return Volume4D(**kwargs) + def transform(self, transformation: Transformation) -> Volume4D: + kwargs = {k: v for k, v in self.items() if v is not None} + kwargs["volume"] = self.volume.transform(transformation) + return Volume4D(**kwargs) + def intersects_vol4(self, vol4_2: Volume4D) -> bool: vol4_1 = self if vol4_1.time_end.datetime < vol4_2.time_start.datetime: diff --git a/monitoring/monitorlib/transformations.py b/monitoring/monitorlib/transformations.py new file mode 100644 index 0000000000..8286fe658b --- /dev/null +++ b/monitoring/monitorlib/transformations.py @@ -0,0 +1,40 @@ +from typing import Optional + +from implicitdict import ImplicitDict + + +class RelativeTranslation(ImplicitDict): + """Offset a geo feature by a particular amount.""" + + meters_east: Optional[float] + """Number of meters east to translate.""" + + meters_north: Optional[float] + """Number of meters north to translate.""" + + meters_up: Optional[float] + """Number of meters upward to translate.""" + + degrees_east: Optional[float] + """Number of degrees of longitude east to translate.""" + + degrees_north: Optional[float] + """Number of degrees of latitude north to translate.""" + + +class AbsoluteTranslation(ImplicitDict): + """Move a geo feature to a specified location.""" + + new_latitude: float + """The new latitude at which the feature should be located (degrees).""" + + new_longitude: float + """The new longitude at which the feature should be located (degrees).""" + + +class Transformation(ImplicitDict): + """A transformation to apply to a geotemporal feature. Exactly one field must be specified.""" + + relative_translation: Optional[RelativeTranslation] + + absolute_translation: Optional[AbsoluteTranslation] diff --git a/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml b/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml index df3d99dc50..fdf8365ab8 100644 --- a/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml +++ b/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml @@ -80,21 +80,42 @@ v1: resource_type: resources.flight_planning.FlightIntentsResource specification: file: - path: file://./test_data/che/flight_intents/conflicting_flights.yaml + path: file://./test_data/flight_intents/standard/conflicting_flights.yaml + transformations: + - relative_translation: + # Put these flight intents in the DFW area + degrees_north: 32.7181 + degrees_east: -96.7587 + + # EGM96 geoid is 27.3 meters below the WGS84 ellipsoid at 32.7181, -96.7587 + # Ground level starts at roughly 120m above the EGM96 geoid + # Therefore, ground level is at roughly 93m above the WGS84 ellipsoid + meters_up: 93 # Details of flights with invalid operational intents (used in flight intent validation scenario) invalid_flight_intents: resource_type: resources.flight_planning.FlightIntentsResource specification: intent_collection: - $ref: test_data.che.flight_intents.invalid_flight_intents + $ref: test_data.flight_intents.standard.invalid_flight_intents + transformations: + - relative_translation: + degrees_north: 32.7181 + degrees_east: -96.7587 + meters_up: 93 # Details of non-conflicting flights (used in data validation scenario) non_conflicting_flights: resource_type: resources.flight_planning.FlightIntentsResource specification: intent_collection: - $ref: file://../../test_data/usa/kentland/flight_intents/non_conflicting.yaml + # Note that $refs are relative to the file with the $ref (this one, in this case) + $ref: file://../../test_data/flight_intents/standard/non_conflicting.yaml + transformations: + - relative_translation: + degrees_north: 32.7181 + degrees_east: -96.7587 + meters_up: 93 # Location of DSS instance that can be used to verify flight planning outcomes dss: diff --git a/monitoring/uss_qualifier/configurations/dev/library/resources.yaml b/monitoring/uss_qualifier/configurations/dev/library/resources.yaml index 959f8ff398..ee56be7c3e 100644 --- a/monitoring/uss_qualifier/configurations/dev/library/resources.yaml +++ b/monitoring/uss_qualifier/configurations/dev/library/resources.yaml @@ -132,9 +132,14 @@ che_conflicting_flights: resource_type: resources.flight_planning.FlightIntentsResource specification: file: - path: file://./test_data/che/flight_intents/conflicting_flights.yaml + path: file://./test_data/flight_intents/standard/conflicting_flights.yaml # Note that this hash_sha512 field can be safely deleted if the content changes - hash_sha512: 26ee66a5065e555512f8b1e354334678dfe1614c6fbba4898a1541e6306341620e96de8b48e4095c7b03ab6fd58d0aeeee9e69cf367e1b7346e0c5f287460792 + hash_sha512: b5432d496928aaa1876acc754e9ffa12f407809a014fa90e23f450c013fb20e2321328d48a419bc129276f7e9e26002c0fea6fec9baf3952b60daec6197de6b7 + transformations: + - relative_translation: + degrees_north: 46.9748 + degrees_east: 7.4774 + meters_up: 605 che_invalid_flight_intents: $content_schema: monitoring/uss_qualifier/resources/definitions/ResourceDeclaration.json @@ -142,7 +147,12 @@ che_invalid_flight_intents: specification: intent_collection: # Note that $refs may use package-based paths - $ref: test_data.che.flight_intents.invalid_flight_intents + $ref: test_data.flight_intents.standard.invalid_flight_intents + transformations: + - relative_translation: + degrees_north: 46.9748 + degrees_east: 7.4774 + meters_up: 605 che_general_flight_auth_flights: $content_schema: monitoring/uss_qualifier/resources/definitions/ResourceDeclaration.json @@ -158,7 +168,12 @@ che_non_conflicting_flights: specification: file: # Note that ExternalFile paths may be package-based - path: test_data.che.flight_intents.non_conflicting + path: test_data.flight_intents.standard.non_conflicting + transformations: + - relative_translation: + degrees_north: 46.9748 + degrees_east: 7.4774 + meters_up: 605 # ===== General flight authorization ===== diff --git a/monitoring/uss_qualifier/resources/flight_planning/flight_intent.py b/monitoring/uss_qualifier/resources/flight_planning/flight_intent.py index 03fe381d3a..0eb7e42220 100644 --- a/monitoring/uss_qualifier/resources/flight_planning/flight_intent.py +++ b/monitoring/uss_qualifier/resources/flight_planning/flight_intent.py @@ -1,7 +1,7 @@ from __future__ import annotations import json -from typing import Optional, Dict +from typing import Optional, Dict, List import arrow @@ -10,6 +10,7 @@ FlightInfoTemplate, ) from monitoring.monitorlib.temporal import Time, TimeDuringTest +from monitoring.monitorlib.transformations import Transformation from monitoring.uss_qualifier.resources.files import ExternalFile from monitoring.uss_qualifier.resources.overrides import apply_overrides @@ -71,6 +72,9 @@ class FlightIntentCollection(ImplicitDict): intents: Dict[FlightIntentID, FlightIntentCollectionElement] """Flight planning actions that users want to perform.""" + transformations: Optional[List[Transformation]] + """Transformations to append to all FlightInfoTemplates.""" + def resolve(self) -> Dict[FlightIntentID, FlightInfoTemplate]: """Resolve the underlying delta flight intents.""" @@ -114,6 +118,16 @@ def resolve(self) -> Dict[FlightIntentID, FlightInfoTemplate]: + ", ".join(i_id for i_id in unprocessed_intent_ids) ) + if "transformations" in self and self.transformations: + for v in processed_intents.values(): + xforms = ( + v.transformations.copy() + if v.has_field_with_value("transformations") + else [] + ) + xforms.extend(self.transformations) + v.transformations = xforms + return processed_intents @@ -125,3 +139,6 @@ class FlightIntentsSpecification(ImplicitDict): file: Optional[ExternalFile] """Location of file to load, containing a FlightIntentCollection""" + + transformations: Optional[List[Transformation]] + """Transformations to apply to all flight intents' 4D volumes after resolution (if specified)""" diff --git a/monitoring/uss_qualifier/resources/flight_planning/flight_intents_resource.py b/monitoring/uss_qualifier/resources/flight_planning/flight_intents_resource.py index a448abe098..78b4436a4c 100644 --- a/monitoring/uss_qualifier/resources/flight_planning/flight_intents_resource.py +++ b/monitoring/uss_qualifier/resources/flight_planning/flight_intents_resource.py @@ -1,8 +1,6 @@ from typing import Dict, List -import arrow from implicitdict import ImplicitDict -from uas_standards.astm.f3548.v21.api import OperationalIntentState from monitoring.monitorlib.clients.flight_planning.flight_info_template import ( FlightInfoTemplate, @@ -41,6 +39,16 @@ def __init__(self, specification: FlightIntentsSpecification): ) elif has_literal: self._intent_collection = specification.intent_collection + if "transformations" in specification and specification.transformations: + if ( + "transformations" in self._intent_collection + and self._intent_collection.transformations + ): + self._intent_collection.transformations.extend( + specification.transformations + ) + else: + self._intent_collection.transformations = specification.transformations def get_flight_intents(self) -> Dict[FlightIntentID, FlightInfoTemplate]: return self._intent_collection.resolve() diff --git a/monitoring/uss_qualifier/resources/overrides.py b/monitoring/uss_qualifier/resources/overrides.py index bfca1479e3..b40a2ab934 100644 --- a/monitoring/uss_qualifier/resources/overrides.py +++ b/monitoring/uss_qualifier/resources/overrides.py @@ -50,15 +50,18 @@ def _apply_overrides(base_object, overrides): f"Attempted to override field with type {type(base_object)} with type {type(overrides)} ({json.dumps(base_object)} -> {json.dumps(overrides)})" ) for field in overrides: + src_field = field if field.startswith("+"): replace = True field = field[1:] else: replace = False if field in base_object and base_object[field] is not None and not replace: - result[field] = _apply_overrides(base_object[field], overrides[field]) + result[field] = _apply_overrides( + base_object[field], overrides[src_field] + ) else: - result[field] = overrides[field] + result[field] = overrides[src_field] return result else: diff --git a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml index 33f1acea31..6b1fd3c636 100644 --- a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml +++ b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml @@ -40,9 +40,9 @@ actions: flight_planners: flight_planners dss: dss flight_intents: invalid_flight_intents - flight_intents2: priority_preemption_flights - flight_intents3: conflicting_flights - flight_intents4: non_conflicting_flights + flight_intents2: priority_preemption_flights? + flight_intents3: conflicting_flights? + flight_intents4: non_conflicting_flights? on_failure: Abort - action_generator: generator_type: action_generators.flight_planning.FlightPlannerCombinations @@ -158,14 +158,14 @@ actions: resources: flight_planners: flight_planners nominal_planning_selector: nominal_planning_selector? - priority_preemption_flights: priority_preemption_flights + conflicting_flights: conflicting_flights dss: dss specification: action_to_repeat: test_scenario: scenario_type: scenarios.astm.utm.DownUSSEqualPriorityNotPermitted resources: - flight_intents: priority_preemption_flights + flight_intents: conflicting_flights tested_uss: uss1 dss: dss on_failure: Continue diff --git a/monitoring/uss_qualifier/test_data/che/flight_intents/non_conflicting.yaml b/monitoring/uss_qualifier/test_data/che/flight_intents/non_conflicting.yaml deleted file mode 100644 index e2f50c972a..0000000000 --- a/monitoring/uss_qualifier/test_data/che/flight_intents/non_conflicting.yaml +++ /dev/null @@ -1,106 +0,0 @@ -intents: - flight_1: - full: - basic_information: - usage_state: Planned - uas_state: Nominal - area: - - outline_polygon: - vertices: - - lat: 47.20642344604623 - lng: 7.58508994131496 - - lat: 47.21359527809636 - lng: 7.5767365824006 - - lat: 47.215812746083586 - lng: 7.58144645497978 - - lat: 47.2146332499489 - lng: 7.58408279875103 - altitude_lower: - value: 474 - reference: W84 - units: M - altitude_upper: - value: 560 - reference: W84 - units: M - start_time: - offset_from: - starting_from: - time_during_test: StartOfTestRun - offset: -1s - duration: 5m - - astm_f3548_21: - priority: 0 - - uspace_flight_authorisation: - uas_serial_number: 1AF49UL5CC5J6K - operation_category: Open - operation_mode: Vlos - uas_class: C0 - identification_technologies: - - ASTMNetRID - connectivity_methods: - - cellular - endurance_minutes: 30 - emergency_procedure_url: https://example.interussplatform.org/emergency - operator_id: CHEo5kut30e0mt01-qwe - uas_id: '' - uas_type_certificate: '' - - - flight_2: - full: - basic_information: - usage_state: Planned - uas_state: Nominal - area: - - outline_polygon: - vertices: - - lat: 47.214119359044275 - lng: 7.58443600701524 - - lat: 47.212388260776436 - lng: 7.58824885811198 - - lat: 47.20211436858821 - lng: 7.5856832012991 - - lat: 47.21394908884487 - lng: 7.57474352572189 - - lat: 47.206173006943104 - lng: 7.5852199577081 - altitude_lower: - value: 483 - reference: W84 - units: M - altitude_upper: - value: 519 - reference: W84 - units: M - start_time: - offset_from: - starting_from: - time_during_test: StartOfTestRun - offset: -1s - duration: 5m - - astm_f3548_21: - priority: 0 - - uspace_flight_authorisation: - uas_serial_number: 1AF49UL5CC5J6K - operation_category: Open - operation_mode: Vlos - uas_class: C0 - identification_technologies: - - ASTMNetRID - connectivity_methods: - - cellular - endurance_minutes: 30 - emergency_procedure_url: https://example.interussplatform.org/emergency - operator_id: CHEo5kut30e0mt01-qwe - uas_id: '' - uas_type_certificate: '' - - - - - diff --git a/monitoring/uss_qualifier/test_data/flight_intents/standard/README.md b/monitoring/uss_qualifier/test_data/flight_intents/standard/README.md new file mode 100644 index 0000000000..e4ccd19bf4 --- /dev/null +++ b/monitoring/uss_qualifier/test_data/flight_intents/standard/README.md @@ -0,0 +1,3 @@ +# Standard flight intents + +This folder contains a set of standard flight intents intended to be used for various flight planning testing purposes, but they are built to be agnostic of location. Specifically, the geographical features are defined at 0 latitude and 0 longitude with the expectation that these intents will be offset to the appropriate location when used. They also assume ground level is at roughly 0m above the WGS-84 ellipsoid and so therefore should be offset in altitude as well. diff --git a/monitoring/uss_qualifier/test_data/che/flight_intents/conflicting_flights.yaml b/monitoring/uss_qualifier/test_data/flight_intents/standard/conflicting_flights.yaml similarity index 60% rename from monitoring/uss_qualifier/test_data/che/flight_intents/conflicting_flights.yaml rename to monitoring/uss_qualifier/test_data/flight_intents/standard/conflicting_flights.yaml index bc8372cf56..9d59b63457 100644 --- a/monitoring/uss_qualifier/test_data/che/flight_intents/conflicting_flights.yaml +++ b/monitoring/uss_qualifier/test_data/flight_intents/standard/conflicting_flights.yaml @@ -7,21 +7,19 @@ intents: uas_state: Nominal area: - outline_polygon: + # There are 0.00000898331 (roughly 0.00001) degrees of latitude (and longitude, at the equator) per meter. + # Therefore, each value below is roughly in meters. vertices: - - lng: 7.477419 - lat: 46.974785 - - lng: 7.478261 - lat: 46.974776 - - lng: 7.478263 - lat: 46.975519 - - lng: 7.477415 - lat: 46.975528 + - {lng: -0.00015, lat: -0.00020} + - {lng: -0.00015, lat: 0.00010} + - {lng: 0.00050, lat: 0.00010} + - {lng: 0.00055, lat: -0.00025} altitude_lower: - value: 605.01 + value: 0.01 reference: W84 units: M altitude_upper: - value: 635 + value: 30 reference: W84 units: M start_time: @@ -39,39 +37,7 @@ intents: priority: 0 uspace_flight_authorisation: - uas_serial_number: 1AF49UL5CC5J6K - operation_category: Open - operation_mode: Vlos - uas_class: C0 - identification_technologies: - - ASTMNetRID - connectivity_methods: - - cellular - endurance_minutes: 30 - emergency_procedure_url: https://example.interussplatform.org/emergency - operator_id: CHEo5kut30e0mt01-qwe - uas_id: '' - uas_type_certificate: '' - - flight1_activated: - delta: - source: flight1_planned - mutation: - basic_information: - usage_state: InUse - area: - - altitude_lower: - value: 605.02 - - flight1m_planned: - delta: - source: flight1_planned - mutation: - basic_information: - area: - - outline_polygon: - altitude_lower: - value: 575.03 + $ref: ./flight_auths.yaml#/standard flight1c_planned: delta: @@ -79,30 +45,47 @@ intents: mutation: basic_information: area: - - altitude_lower: - value: 650.04 + - outline_polygon: + +vertices: + - {lng: 0.00030, lat: -0.00020} + - {lng: 0.00030, lat: 0.00010} + - {lng: 0.00050, lat: 0.00010} + - {lng: 0.00055, lat: -0.00025} + altitude_lower: + value: -5.04 altitude_upper: - value: 705 + value: 35 - flight1m_activated: + flight1c_activated: delta: - source: flight1m_planned + source: flight1c_planned mutation: basic_information: usage_state: InUse area: - - altitude_lower: - value: 575.05 + - outline_polygon: + +vertices: + - {lng: 0.00030, lat: -0.00020} + - {lng: 0.00030, lat: 0.00050} + - {lng: 0.00050, lat: 0.00010} + - {lng: 0.00055, lat: -0.00025} + altitude_lower: + value: 0.06 - flight1c_activated: + flight1m_planned: delta: - source: flight1c_planned + source: flight1_planned mutation: basic_information: - usage_state: InUse area: - - altitude_lower: - value: 650.06 + - outline_polygon: + +vertices: + - {lng: 0.00010, lat: -0.00020} + - {lng: 0.00010, lat: 0.00010} + - {lng: 0.00050, lat: 0.00010} + - {lng: 0.00055, lat: -0.00025} + altitude_lower: + value: -30.03 flight2_planned: full: @@ -112,20 +95,16 @@ intents: area: - outline_polygon: vertices: - - lng: 7.477918 - lat: 46.974515 - - lng: 7.478182 - lat: 46.974719 - - lng: 7.474489 - lat: 46.977435 - - lng: 7.474139 - lat: 46.977289 + - {lng: -0.00020, lat: 0.00020} + - {lng: 0.00000, lat: -0.00025} + - {lng: 0.00020, lat: 0.00015} + - {lng: 0.00000, lat: 0.00050} altitude_lower: - value: 605.07 + value: -1.07 reference: W84 units: M altitude_upper: - value: 635 + value: 28 reference: W84 units: M start_time: @@ -143,19 +122,27 @@ intents: priority: 100 uspace_flight_authorisation: - uas_serial_number: 1AF49UL5CC5J6K - operation_category: Open - operation_mode: Vlos - uas_class: C0 - identification_technologies: - - ASTMNetRID - connectivity_methods: - - cellular - endurance_minutes: 30 - emergency_procedure_url: https://example.interussplatform.org/emergency - operator_id: CHEo5kut30e0mt01-qwe - uas_id: '' - uas_type_certificate: '' + $ref: ./flight_auths.yaml#/standard + + flight1_activated: + delta: + source: flight1_planned + mutation: + basic_information: + usage_state: InUse + area: + - altitude_lower: + value: 0.02 + + flight1m_activated: + delta: + source: flight1m_planned + mutation: + basic_information: + usage_state: InUse + area: + - altitude_lower: + value: -30.05 flight2_activated: delta: @@ -165,7 +152,7 @@ intents: usage_state: InUse area: - altitude_lower: - value: 605.08 + value: 0.08 flight2m_activated: delta: @@ -173,10 +160,15 @@ intents: mutation: basic_information: area: - - altitude_lower: - value: 650.09 + - outline_polygon: + +vertices: + - {lng: -0.00020, lat: 0.00020} + - {lng: 0.00040, lat: 0.00015} + - {lng: 0.00000, lat: 0.00050} + altitude_lower: + value: -3.09 altitude_upper: - value: 705 + value: 50 equal_prio_flight2m_planned: delta: @@ -186,31 +178,31 @@ intents: usage_state: Planned area: - altitude_lower: - value: 650.13 + value: 45.13 astm_f3548_21: priority: 0 equal_prio_flight2_planned: delta: - source: flight2_planned + source: flight2_activated mutation: basic_information: + usage_state: Planned area: - altitude_lower: - value: 605.10 + value: 0.10 astm_f3548_21: priority: 0 equal_prio_flight2_activated: delta: - source: flight2_activated + source: equal_prio_flight2_planned mutation: basic_information: + usage_state: InUse area: - altitude_lower: - value: 605.11 - astm_f3548_21: - priority: 0 + value: 0.11 equal_prio_flight2_nonconforming: delta: @@ -220,4 +212,4 @@ intents: uas_state: OffNominal area: - altitude_lower: - value: 605.12 + value: 0.12 diff --git a/monitoring/uss_qualifier/test_data/flight_intents/standard/flight_auths.yaml b/monitoring/uss_qualifier/test_data/flight_intents/standard/flight_auths.yaml new file mode 100644 index 0000000000..21d80beb24 --- /dev/null +++ b/monitoring/uss_qualifier/test_data/flight_intents/standard/flight_auths.yaml @@ -0,0 +1,15 @@ +# Standard valid flight authorisation +standard: + uas_serial_number: 1AF49UL5CC5J6K + operation_category: Open + operation_mode: Vlos + uas_class: C0 + identification_technologies: + - ASTMNetRID + connectivity_methods: + - cellular + endurance_minutes: 30 + emergency_procedure_url: https://example.interussplatform.org/emergency + operator_id: CHEo5kut30e0mt01-qwe + uas_id: '' + uas_type_certificate: '' diff --git a/monitoring/uss_qualifier/test_data/che/flight_intents/invalid_flight_intents.yaml b/monitoring/uss_qualifier/test_data/flight_intents/standard/invalid_flight_intents.yaml similarity index 64% rename from monitoring/uss_qualifier/test_data/che/flight_intents/invalid_flight_intents.yaml rename to monitoring/uss_qualifier/test_data/flight_intents/standard/invalid_flight_intents.yaml index 8325959d2d..61d41bfffa 100644 --- a/monitoring/uss_qualifier/test_data/che/flight_intents/invalid_flight_intents.yaml +++ b/monitoring/uss_qualifier/test_data/flight_intents/standard/invalid_flight_intents.yaml @@ -7,19 +7,18 @@ intents: uas_state: Nominal area: - outline_polygon: + # There are 0.00000898331 (roughly 0.00001) degrees of latitude (and longitude, at the equator) per meter. + # Therefore, each value below is roughly in meters. vertices: - - lat: 46.1929645 - lng: 6.1965395 - - lat: 46.192911 - lng: 6.196423 - - lat: 46.192918 - lng: 6.196678 + - {lng: 0.00000, lat: 0.00000} + - {lng: -0.00030, lat: 0.00042} + - {lng: 0.00030, lat: 0.00042} altitude_lower: - value: 605 + value: 0 reference: W84 units: M altitude_upper: - value: 635 + value: 30 reference: W84 units: M start_time: @@ -33,17 +32,7 @@ intents: priority: 0 uspace_flight_authorisation: - uas_serial_number: 1AF49UL5CC5J6K - operation_category: Open - operation_mode: Vlos - uas_class: C0 - identification_technologies: - - ASTMNetRID - connectivity_methods: - - cellular - endurance_minutes: 30 - emergency_procedure_url: https://uav.example.com/emergency - operator_id: CHEo5kut30e0mt01-qwe + $ref: ./flight_auths.yaml#/standard valid_activated: delta: @@ -74,9 +63,6 @@ intents: - outline_polygon: # polygon overlaps with polygon of valid_flight, distance between first vertex of each is 1.112cm vertices: - - lat: 46.1929644 - lng: 6.1965395 - - lat: 46.193236 - lng: 6.196235 - - lat: 46.193174 - lng: 6.196925 + - {lng: 0.00000, lat: 0.0000001} + - {lng: -0.00030, lat: -0.00042} + - {lng: 0.00030, lat: -0.00042} diff --git a/monitoring/uss_qualifier/test_data/flight_intents/standard/non_conflicting.yaml b/monitoring/uss_qualifier/test_data/flight_intents/standard/non_conflicting.yaml new file mode 100644 index 0000000000..7ce6fdcd84 --- /dev/null +++ b/monitoring/uss_qualifier/test_data/flight_intents/standard/non_conflicting.yaml @@ -0,0 +1,70 @@ +intents: + flight_1: + full: + basic_information: + usage_state: Planned + uas_state: Nominal + area: + - outline_polygon: + # There are 0.00000898331 (roughly 0.00001) degrees of latitude (and longitude, at the equator) per meter. + # Therefore, each value below is roughly in meters. + vertices: + - {lat: 0.00000, lng: 0.00000} + - {lat: 0.00717, lng: -0.00835} + - {lat: 0.00939, lng: -0.00364} + - {lat: 0.00821, lng: -0.00101} + altitude_lower: + value: -9 + reference: W84 + units: M + altitude_upper: + value: 76 + reference: W84 + units: M + start_time: + offset_from: + starting_from: + time_during_test: StartOfTestRun + offset: -1s + duration: 5m + + astm_f3548_21: + priority: 0 + + uspace_flight_authorisation: + $ref: ./flight_auths.yaml#/standard + + + flight_2: + full: + basic_information: + usage_state: Planned + uas_state: Nominal + area: + - outline_polygon: + vertices: + - {lat: 0.00770, lng: -0.00065} + - {lat: 0.00596, lng: 0.00316} + - {lat: -0.00431, lng: 0.00059} + - {lat: 0.00753, lng: -0.01035} + - {lat: -0.00025, lng: 0.00013} + altitude_lower: + value: 0 + reference: W84 + units: M + altitude_upper: + value: 58 + reference: W84 + units: M + start_time: + offset_from: + starting_from: + time_during_test: StartOfTestRun + offset: -1s + duration: 5m + + astm_f3548_21: + priority: 0 + + uspace_flight_authorisation: + $ref: ./flight_auths.yaml#/standard diff --git a/monitoring/uss_qualifier/test_data/usa/kentland/flight_intents/non_conflicting.yaml b/monitoring/uss_qualifier/test_data/usa/kentland/flight_intents/non_conflicting.yaml deleted file mode 100644 index aa4952594e..0000000000 --- a/monitoring/uss_qualifier/test_data/usa/kentland/flight_intents/non_conflicting.yaml +++ /dev/null @@ -1,103 +0,0 @@ -intents: - flight_1: - full: - basic_information: - usage_state: Planned - uas_state: Nominal - area: - - outline_polygon: - vertices: - - lat: 37.20642344604623 - lng: -80.58508994131496 - - lat: 37.21359527809636 - lng: -80.5767365824006 - - lat: 37.215812746083586 - lng: -80.58144645497978 - - lat: 37.2146332499489 - lng: -80.58408279875103 - altitude_lower: - value: 474 - reference: W84 - units: M - altitude_upper: - value: 560 - reference: W84 - units: M - start_time: - offset_from: - starting_from: - time_during_test: StartOfTestRun - offset: -1s - duration: 5m - - astm_f3548_21: - priority: 0 - - # TODO: Remove flight_authorisation section when it is optional - uspace_flight_authorisation: - uas_serial_number: 1AF49UL5CC5J6K - operation_category: Open - operation_mode: Vlos - uas_class: C0 - identification_technologies: - - ASTMNetRID - connectivity_methods: - - cellular - endurance_minutes: 30 - emergency_procedure_url: https://example.interussplatform.org/emergency - operator_id: CHEo5kut30e0mt01-qwe - uas_id: '' - uas_type_certificate: '' - - - flight_2: - full: - basic_information: - usage_state: Planned - uas_state: Nominal - area: - - outline_polygon: - vertices: - - lat: 37.214119359044275 - lng: -80.58443600701524 - - lat: 37.212388260776436 - lng: -80.58824885811198 - - lat: 37.20211436858821 - lng: -80.5856832012991 - - lat: 37.21394908884487 - lng: -80.57474352572189 - - lat: 37.206173006943104 - lng: -80.5852199577081 - altitude_lower: - value: 483 - reference: W84 - units: M - altitude_upper: - value: 519 - reference: W84 - units: M - start_time: - offset_from: - starting_from: - time_during_test: StartOfTestRun - offset: -1s - duration: 5m - - astm_f3548_21: - priority: 0 - - # TODO: Remove flight_authorisation section when it is optional - uspace_flight_authorisation: - uas_serial_number: 1AF49UL5CC5J6K - operation_category: Open - operation_mode: Vlos - uas_class: C0 - identification_technologies: - - ASTMNetRID - connectivity_methods: - - cellular - endurance_minutes: 30 - emergency_procedure_url: https://example.interussplatform.org/emergency - operator_id: CHEo5kut30e0mt01-qwe - uas_id: '' - uas_type_certificate: '' diff --git a/schemas/monitoring/monitorlib/clients/flight_planning/flight_info_template/FlightInfoTemplate.json b/schemas/monitoring/monitorlib/clients/flight_planning/flight_info_template/FlightInfoTemplate.json index c44de2b563..76a714d1e9 100644 --- a/schemas/monitoring/monitorlib/clients/flight_planning/flight_info_template/FlightInfoTemplate.json +++ b/schemas/monitoring/monitorlib/clients/flight_planning/flight_info_template/FlightInfoTemplate.json @@ -37,6 +37,16 @@ } ] }, + "transformations": { + "description": "If specified, transform this flight according to these transformations in order (after all templates are resolved).", + "items": { + "$ref": "../../../transformations/Transformation.json" + }, + "type": [ + "array", + "null" + ] + }, "uspace_flight_authorisation": { "oneOf": [ { diff --git a/schemas/monitoring/monitorlib/geo/Polygon.json b/schemas/monitoring/monitorlib/geo/Polygon.json index c7bb037d14..ae8e2e6629 100644 --- a/schemas/monitoring/monitorlib/geo/Polygon.json +++ b/schemas/monitoring/monitorlib/geo/Polygon.json @@ -11,11 +11,11 @@ "items": { "$ref": "LatLngPoint.json" }, - "type": "array" + "type": [ + "array", + "null" + ] } }, - "required": [ - "vertices" - ], "type": "object" } \ No newline at end of file diff --git a/schemas/monitoring/monitorlib/geotemporal/Volume4DTemplate.json b/schemas/monitoring/monitorlib/geotemporal/Volume4DTemplate.json index 9770a694c4..f867b1969e 100644 --- a/schemas/monitoring/monitorlib/geotemporal/Volume4DTemplate.json +++ b/schemas/monitoring/monitorlib/geotemporal/Volume4DTemplate.json @@ -80,6 +80,16 @@ "$ref": "../temporal/TestTime.json" } ] + }, + "transformations": { + "description": "If specified, transform this volume according to these transformations in order.", + "items": { + "$ref": "../transformations/Transformation.json" + }, + "type": [ + "array", + "null" + ] } }, "type": "object" diff --git a/schemas/monitoring/monitorlib/transformations/AbsoluteTranslation.json b/schemas/monitoring/monitorlib/transformations/AbsoluteTranslation.json new file mode 100644 index 0000000000..5bc444a7be --- /dev/null +++ b/schemas/monitoring/monitorlib/transformations/AbsoluteTranslation.json @@ -0,0 +1,24 @@ +{ + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/monitorlib/transformations/AbsoluteTranslation.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Move a geo feature to a specified location.\n\nmonitoring.monitorlib.transformations.AbsoluteTranslation, as defined in monitoring/monitorlib/transformations.py", + "properties": { + "$ref": { + "description": "Path to content that replaces the $ref", + "type": "string" + }, + "new_latitude": { + "description": "The new latitude at which the feature should be located (degrees).", + "type": "number" + }, + "new_longitude": { + "description": "The new longitude at which the feature should be located (degrees).", + "type": "number" + } + }, + "required": [ + "new_latitude", + "new_longitude" + ], + "type": "object" +} \ No newline at end of file diff --git a/schemas/monitoring/monitorlib/transformations/RelativeTranslation.json b/schemas/monitoring/monitorlib/transformations/RelativeTranslation.json new file mode 100644 index 0000000000..79473b962b --- /dev/null +++ b/schemas/monitoring/monitorlib/transformations/RelativeTranslation.json @@ -0,0 +1,47 @@ +{ + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/monitorlib/transformations/RelativeTranslation.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "Offset a geo feature by a particular amount.\n\nmonitoring.monitorlib.transformations.RelativeTranslation, as defined in monitoring/monitorlib/transformations.py", + "properties": { + "$ref": { + "description": "Path to content that replaces the $ref", + "type": "string" + }, + "degrees_east": { + "description": "Number of degrees of longitude east to translate.", + "type": [ + "number", + "null" + ] + }, + "degrees_north": { + "description": "Number of degrees of latitude north to translate.", + "type": [ + "number", + "null" + ] + }, + "meters_east": { + "description": "Number of meters east to translate.", + "type": [ + "number", + "null" + ] + }, + "meters_north": { + "description": "Number of meters north to translate.", + "type": [ + "number", + "null" + ] + }, + "meters_up": { + "description": "Number of meters upward to translate.", + "type": [ + "number", + "null" + ] + } + }, + "type": "object" +} \ No newline at end of file diff --git a/schemas/monitoring/monitorlib/transformations/Transformation.json b/schemas/monitoring/monitorlib/transformations/Transformation.json new file mode 100644 index 0000000000..87966c40a9 --- /dev/null +++ b/schemas/monitoring/monitorlib/transformations/Transformation.json @@ -0,0 +1,32 @@ +{ + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/monitorlib/transformations/Transformation.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "A transformation to apply to a geotemporal feature. Exactly one field must be specified.\n\nmonitoring.monitorlib.transformations.Transformation, as defined in monitoring/monitorlib/transformations.py", + "properties": { + "$ref": { + "description": "Path to content that replaces the $ref", + "type": "string" + }, + "absolute_translation": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "AbsoluteTranslation.json" + } + ] + }, + "relative_translation": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "RelativeTranslation.json" + } + ] + } + }, + "type": "object" +} \ No newline at end of file diff --git a/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_intent/FlightIntentCollection.json b/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_intent/FlightIntentCollection.json index f9fec61adc..0606503812 100644 --- a/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_intent/FlightIntentCollection.json +++ b/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_intent/FlightIntentCollection.json @@ -19,6 +19,16 @@ } }, "type": "object" + }, + "transformations": { + "description": "Transformations to append to all FlightInfoTemplates.", + "items": { + "$ref": "../../../../monitorlib/transformations/Transformation.json" + }, + "type": [ + "array", + "null" + ] } }, "required": [ diff --git a/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_intent/FlightIntentsSpecification.json b/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_intent/FlightIntentsSpecification.json index 7dcb75a48b..b39df5ee4d 100644 --- a/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_intent/FlightIntentsSpecification.json +++ b/schemas/monitoring/uss_qualifier/resources/flight_planning/flight_intent/FlightIntentsSpecification.json @@ -28,6 +28,16 @@ "$ref": "FlightIntentCollection.json" } ] + }, + "transformations": { + "description": "Transformations to apply to all flight intents' 4D volumes after resolution (if specified)", + "items": { + "$ref": "../../../../monitorlib/transformations/Transformation.json" + }, + "type": [ + "array", + "null" + ] } }, "type": "object"