From 23fa9b0737c1a87f9f52e4f1686bc7cde047b709 Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Thu, 28 Sep 2023 09:21:09 -0700 Subject: [PATCH] [monitorlib] Clean out scd library (#223) * Clean out scd library * Adjust ValueErrors per comment --- monitoring/mock_uss/scdsc/database.py | 9 +- monitoring/mock_uss/scdsc/routes_injection.py | 73 +-- monitoring/mock_uss/scdsc/routes_scdsc.py | 16 +- .../mock_uss/tracer/observation_areas.py | 3 +- .../tracer/routes/observation_areas.py | 4 +- monitoring/monitorlib/clients/scd.py | 38 +- monitoring/monitorlib/fetch/scd.py | 8 +- monitoring/monitorlib/geo.py | 136 +++++- monitoring/monitorlib/geotemporal.py | 262 ++++++++++- monitoring/monitorlib/mutate/scd.py | 8 +- monitoring/monitorlib/scd.py | 416 +----------------- monitoring/prober/run_locally.sh | 1 - .../prober/scd/test_constraint_simple.py | 48 +- .../test_constraints_with_subscriptions.py | 18 +- .../test_operation_references_error_cases.py | 22 +- .../prober/scd/test_operation_simple.py | 66 +-- .../test_operation_simple_heavy_traffic.py | 49 ++- ...eration_simple_heavy_traffic_concurrent.py | 22 +- .../prober/scd/test_operations_simple.py | 62 +-- .../prober/scd/test_subscription_queries.py | 82 ++-- .../scd/test_subscription_query_time.py | 12 +- .../prober/scd/test_subscription_simple.py | 32 +- .../test_subscription_update_validation.py | 16 +- .../resources/astm/f3548/v21/dss.py | 13 +- .../scenarios/astm/utm/evaluation.py | 12 +- .../flight_intent_validation.py | 15 +- .../conflict_equal_priority_not_permitted.py | 38 +- .../conflict_higher_priority.py | 29 +- .../scenarios/astm/utm/test_steps.py | 18 +- .../scenarios/flight_planning/test_steps.py | 4 +- 30 files changed, 806 insertions(+), 726 deletions(-) diff --git a/monitoring/mock_uss/scdsc/database.py b/monitoring/mock_uss/scdsc/database.py index d59669ce25..156cdf2dd6 100644 --- a/monitoring/mock_uss/scdsc/database.py +++ b/monitoring/mock_uss/scdsc/database.py @@ -1,10 +1,13 @@ import json from typing import Dict, Optional -from monitoring.monitorlib import scd from monitoring.monitorlib.multiprocessing import SynchronizedValue from monitoring.monitorlib.scd_automated_testing import scd_injection_api from implicitdict import ImplicitDict +from uas_standards.astm.f3548.v21.api import ( + OperationalIntentReference, + OperationalIntent, +) class FlightRecord(ImplicitDict): @@ -12,7 +15,7 @@ class FlightRecord(ImplicitDict): op_intent_injection: scd_injection_api.OperationalIntentTestInjection flight_authorisation: scd_injection_api.FlightAuthorisationData - op_intent_reference: scd.OperationalIntentReference + op_intent_reference: OperationalIntentReference locked: bool = False @@ -20,7 +23,7 @@ class Database(ImplicitDict): """Simple in-memory pseudo-database tracking the state of the mock system""" flights: Dict[str, Optional[FlightRecord]] = {} - cached_operations: Dict[str, scd.OperationalIntent] = {} + cached_operations: Dict[str, OperationalIntent] = {} db = SynchronizedValue( diff --git a/monitoring/mock_uss/scdsc/routes_injection.py b/monitoring/mock_uss/scdsc/routes_injection.py index 0254d1c57b..7a55826268 100644 --- a/monitoring/mock_uss/scdsc/routes_injection.py +++ b/monitoring/mock_uss/scdsc/routes_injection.py @@ -2,7 +2,7 @@ import traceback from datetime import datetime, timedelta import time -from typing import List, Tuple, Dict, Optional +from typing import List, Tuple import uuid import flask @@ -10,6 +10,14 @@ import requests.exceptions from monitoring.mock_uss.scdsc.routes_scdsc import op_intent_from_flightrecord +from monitoring.monitorlib.geo import Polygon +from monitoring.monitorlib.geotemporal import Volume4D, Volume4DCollection +from uas_standards.astm.f3548.v21.api import ( + OperationalIntent, + PutOperationalIntentDetailsParameters, + ImplicitSubscriptionParameters, + PutOperationalIntentReferenceParameters, +) from uas_standards.astm.f3548.v21.constants import OiMaxPlanHorizonDays, OiMaxVertices from monitoring.mock_uss.config import KEY_BASE_URL, KEY_BEHAVIOR_LOCALITY @@ -37,7 +45,7 @@ from monitoring.mock_uss import webapp, require_config_value from monitoring.mock_uss.auth import requires_scope from monitoring.mock_uss.scdsc import database, utm_client -from monitoring.mock_uss.scdsc.database import db, FlightRecord +from monitoring.mock_uss.scdsc.database import db from monitoring.monitorlib.uspace import problems_with_flight_authorisation @@ -55,7 +63,7 @@ def _make_stacktrace(e) -> str: def query_operational_intents( area_of_interest: scd.Volume4D, -) -> List[scd.OperationalIntent]: +) -> List[OperationalIntent]: """Retrieve a complete set of operational intents in an area, including details. :param area_of_interest: Area where intersecting operational intents must be discovered @@ -190,7 +198,9 @@ def inject_flight(flight_id: str, req_body: InjectFlightRequest) -> Tuple[dict, ) # Validate max planning horizon for creation - start_time = scd.start_of(req_body.operational_intent.volumes) + start_time = Volume4DCollection.from_f3548v21( + req_body.operational_intent.volumes + ).time_start.datetime time_delta = start_time - datetime.now(tz=start_time.tzinfo) if ( time_delta.days > OiMaxPlanHorizonDays @@ -269,19 +279,9 @@ def inject_flight(flight_id: str, req_body: InjectFlightRequest) -> Tuple[dict, logger.debug( f"[inject_flight/{pid}:{flight_id}] Obtaining latest operational intent information" ) - start_time = scd.start_of(req_body.operational_intent.volumes) - end_time = scd.end_of(req_body.operational_intent.volumes) - area = scd.rect_bounds_of(req_body.operational_intent.volumes) - alt_lo, alt_hi = scd.meter_altitude_bounds_of( + vol4 = Volume4DCollection.from_f3548v21( req_body.operational_intent.volumes - ) - vol4 = scd.make_vol4( - start_time, - end_time, - alt_lo, - alt_hi, - polygon=scd.make_polygon(latlngrect=area), - ) + ).bounding_volume.to_f3548v21() op_intents = query_operational_intents(vol4) # Check for intersections @@ -289,7 +289,7 @@ def inject_flight(flight_id: str, req_body: InjectFlightRequest) -> Tuple[dict, logger.debug( f"[inject_flight/{pid}:{flight_id}] Checking for intersections with {', '.join(op_intent.reference.id for op_intent in op_intents)}" ) - v1 = req_body.operational_intent.volumes + v1 = Volume4DCollection.from_f3548v21(req_body.operational_intent.volumes) for op_intent in op_intents: if ( existing_flight @@ -315,7 +315,9 @@ def inject_flight(flight_id: str, req_body: InjectFlightRequest) -> Tuple[dict, ) continue - v2 = op_intent.details.volumes + op_intent.details.off_nominal_volumes + v2 = Volume4DCollection.from_f3548v21( + op_intent.details.volumes + op_intent.details.off_nominal_volumes + ) if ( existing_flight @@ -323,16 +325,16 @@ def inject_flight(flight_id: str, req_body: InjectFlightRequest) -> Tuple[dict, == OperationalIntentState.Activated and req_body.operational_intent.state == OperationalIntentState.Activated - and ( - scd.vol4s_intersect(existing_flight.op_intent_injection.volumes, v2) - ) + and Volume4DCollection.from_f3548v21( + existing_flight.op_intent_injection.volumes + ).intersects_vol4s(v2) ): logger.debug( f"[inject_flight/{pid}:{flight_id}] intersection with {op_intent.reference.id} not considered: modification of Activated operational intent with a pre-existing conflict" ) continue - if scd.vol4s_intersect(v1, v2): + if v1.intersects_vol4s(v2): notes = f"Requested flight (priority {req_body.operational_intent.priority}) intersected {op_intent.reference.manager}'s operational intent {op_intent.reference.id} (priority {op_intent.details.priority})" return ( InjectFlightResponse( @@ -347,13 +349,13 @@ def inject_flight(flight_id: str, req_body: InjectFlightRequest) -> Tuple[dict, f"[inject_flight/{pid}:{flight_id}] Sharing operational intent with DSS" ) base_url = "{}/mock/scd".format(webapp.config[KEY_BASE_URL]) - req = scd.PutOperationalIntentReferenceParameters( + req = PutOperationalIntentReferenceParameters( extents=req_body.operational_intent.volumes + req_body.operational_intent.off_nominal_volumes, key=[op.reference.ovn for op in op_intents], state=req_body.operational_intent.state, uss_base_url=base_url, - new_subscription=scd.ImplicitSubscriptionParameters(uss_base_url=base_url), + new_subscription=ImplicitSubscriptionParameters(uss_base_url=base_url), ) if existing_flight: id = existing_flight.op_intent_reference.id @@ -372,7 +374,7 @@ def inject_flight(flight_id: str, req_body: InjectFlightRequest) -> Tuple[dict, # Notify subscribers subscriber_list = ", ".join(s.uss_base_url for s in result.subscribers) step_name = f"notifying subscribers {{{subscriber_list}}}" - operational_intent = scd.OperationalIntent( + operational_intent = OperationalIntent( reference=result.operational_intent_reference, details=req_body.operational_intent, ) @@ -380,7 +382,7 @@ def inject_flight(flight_id: str, req_body: InjectFlightRequest) -> Tuple[dict, if subscriber.uss_base_url == base_url: # Do not notify ourselves continue - update = scd.PutOperationalIntentDetailsParameters( + update = PutOperationalIntentDetailsParameters( operational_intent_id=result.operational_intent_reference.id, operational_intent=operational_intent, subscriptions=subscriber.subscriptions, @@ -512,7 +514,7 @@ def delete_flight(flight_id) -> Tuple[dict, int]: if subscriber.uss_base_url == base_url: # Do not notify ourselves continue - update = scd.PutOperationalIntentDetailsParameters( + update = PutOperationalIntentDetailsParameters( operational_intent_id=result.operational_intent_reference.id, subscriptions=subscriber.subscriptions, ) @@ -579,17 +581,20 @@ def make_result(success: bool, msg: str) -> ClearAreaResponse: try: # Find operational intents in the DSS step_name = "constructing DSS operational intent query" - start_time = scd.start_of([req.extent]) - end_time = scd.end_of([req.extent]) - area = scd.rect_bounds_of([req.extent]) - alt_lo, alt_hi = scd.meter_altitude_bounds_of([req.extent]) - vol4 = scd.make_vol4( + # TODO: Simply use the req.extent 4D volume more directly + extent = Volume4D.from_f3548v21(req.extent) + start_time = extent.time_start.datetime + end_time = extent.time_end.datetime + area = extent.rect_bounds + alt_lo = extent.volume.altitude_lower_wgs84_m() + alt_hi = extent.volume.altitude_upper_wgs84_m() + vol4 = Volume4D.from_values( start_time, end_time, alt_lo, alt_hi, - polygon=scd.make_polygon(latlngrect=area), - ) + polygon=Polygon.from_latlng_rect(latlngrect=area), + ).to_f3548v21() step_name = "finding operational intents in the DSS" op_intent_refs = scd_client.query_operational_intent_references( utm_client, vol4 diff --git a/monitoring/mock_uss/scdsc/routes_scdsc.py b/monitoring/mock_uss/scdsc/routes_scdsc.py index 4fa22b9e0c..a998b0dbba 100644 --- a/monitoring/mock_uss/scdsc/routes_scdsc.py +++ b/monitoring/mock_uss/scdsc/routes_scdsc.py @@ -4,6 +4,12 @@ from monitoring.mock_uss import webapp from monitoring.mock_uss.auth import requires_scope from monitoring.mock_uss.scdsc.database import db, FlightRecord +from uas_standards.astm.f3548.v21.api import ( + GetOperationalIntentDetailsResponse, + ErrorResponse, + OperationalIntent, + OperationalIntentDetails, +) @webapp.route("/mock/scd/uss/v1/operational_intents/", methods=["GET"]) @@ -23,7 +29,7 @@ def scdsc_get_operational_intent_details(entityid: str): if flight is None: return ( flask.jsonify( - scd.ErrorResponse( + ErrorResponse( message="Operational intent {} not known by this USS".format( entityid ) @@ -33,16 +39,16 @@ def scdsc_get_operational_intent_details(entityid: str): ) # Return nominal response with details - response = scd.GetOperationalIntentDetailsResponse( + response = GetOperationalIntentDetailsResponse( operational_intent=op_intent_from_flightrecord(flight), ) return flask.jsonify(response), 200 -def op_intent_from_flightrecord(flight: FlightRecord) -> scd.OperationalIntent: - return scd.OperationalIntent( +def op_intent_from_flightrecord(flight: FlightRecord) -> OperationalIntent: + return OperationalIntent( reference=flight.op_intent_reference, - details=scd.OperationalIntentDetails( + details=OperationalIntentDetails( volumes=flight.op_intent_injection.volumes, off_nominal_volumes=flight.op_intent_injection.off_nominal_volumes, priority=flight.op_intent_injection.priority, diff --git a/monitoring/mock_uss/tracer/observation_areas.py b/monitoring/mock_uss/tracer/observation_areas.py index 6a48207225..899bb5314d 100644 --- a/monitoring/mock_uss/tracer/observation_areas.py +++ b/monitoring/mock_uss/tracer/observation_areas.py @@ -2,11 +2,10 @@ from implicitdict import ImplicitDict -from monitoring.monitorlib.geo import Volume4D +from monitoring.monitorlib.geotemporal import Volume4D from monitoring.monitorlib.infrastructure import AuthSpec from monitoring.monitorlib.rid import RIDVersion - ObservationAreaID = str """Unique identifier of observation area.""" diff --git a/monitoring/mock_uss/tracer/routes/observation_areas.py b/monitoring/mock_uss/tracer/routes/observation_areas.py index 6ffae21ddd..d01a4a1795 100644 --- a/monitoring/mock_uss/tracer/routes/observation_areas.py +++ b/monitoring/mock_uss/tracer/routes/observation_areas.py @@ -27,8 +27,8 @@ from monitoring.mock_uss.tracer.tracer_poll import TASK_POLL_OBSERVATION_AREAS from monitoring.monitorlib import fetch import monitoring.monitorlib.fetch.rid -import monitoring.monitorlib.fetch.scd -from monitoring.monitorlib.geo import Volume4D, Volume3D +from monitoring.monitorlib.geo import Volume3D +from monitoring.monitorlib.geotemporal import Volume4D @webapp.route("/tracer/observation_areas", methods=["GET"]) diff --git a/monitoring/monitorlib/clients/scd.py b/monitoring/monitorlib/clients/scd.py index 89f27e16e4..563daeaca8 100644 --- a/monitoring/monitorlib/clients/scd.py +++ b/monitoring/monitorlib/clients/scd.py @@ -5,18 +5,18 @@ from monitoring.monitorlib.fetch import QueryError, Query, QueryType from monitoring.monitorlib.infrastructure import UTMClientSession from implicitdict import ImplicitDict -from loguru import logger +from uas_standards.astm.f3548.v21 import api # === DSS operations defined in ASTM API === def query_operational_intent_references( - utm_client: UTMClientSession, area_of_interest: scd.Volume4D -) -> List[scd.OperationalIntentReference]: + utm_client: UTMClientSession, area_of_interest: api.Volume4D +) -> List[api.OperationalIntentReference]: url = "/dss/v1/operational_intent_references/query" subject = f"queryOperationalIntentReferences from {url}" - req = scd.QueryOperationalIntentReferenceParameters( + req = api.QueryOperationalIntentReferenceParameters( area_of_interest=area_of_interest ) query = fetch.query_and_describe( @@ -31,7 +31,7 @@ def query_operational_intent_references( ) try: resp_body = ImplicitDict.parse( - query.response.json, scd.QueryOperationalIntentReferenceResponse + query.response.json, api.QueryOperationalIntentReferenceResponse ) except KeyError: raise QueryError( @@ -47,8 +47,8 @@ def query_operational_intent_references( def create_operational_intent_reference( utm_client: UTMClientSession, id: str, - req: scd.PutOperationalIntentReferenceParameters, -) -> scd.ChangeOperationalIntentReferenceResponse: + req: api.PutOperationalIntentReferenceParameters, +) -> api.ChangeOperationalIntentReferenceResponse: url = "/dss/v1/operational_intent_references/{}".format(id) subject = f"createOperationalIntentReference to {url}" query = fetch.query_and_describe( @@ -63,7 +63,7 @@ def create_operational_intent_reference( ) try: return ImplicitDict.parse( - query.response.json, scd.ChangeOperationalIntentReferenceResponse + query.response.json, api.ChangeOperationalIntentReferenceResponse ) except KeyError: raise QueryError( @@ -79,8 +79,8 @@ def update_operational_intent_reference( utm_client: UTMClientSession, id: str, ovn: str, - req: scd.PutOperationalIntentReferenceParameters, -) -> scd.ChangeOperationalIntentReferenceResponse: + req: api.PutOperationalIntentReferenceParameters, +) -> api.ChangeOperationalIntentReferenceResponse: url = "/dss/v1/operational_intent_references/{}/{}".format(id, ovn) subject = f"updateOperationalIntentReference to {url}" query = fetch.query_and_describe( @@ -95,7 +95,7 @@ def update_operational_intent_reference( ) try: return ImplicitDict.parse( - query.response.json, scd.ChangeOperationalIntentReferenceResponse + query.response.json, api.ChangeOperationalIntentReferenceResponse ) except KeyError: raise QueryError( @@ -109,7 +109,7 @@ def update_operational_intent_reference( def delete_operational_intent_reference( utm_client: UTMClientSession, id: str, ovn: str -) -> scd.ChangeOperationalIntentReferenceResponse: +) -> api.ChangeOperationalIntentReferenceResponse: url = f"/dss/v1/operational_intent_references/{id}/{ovn}" subject = f"deleteOperationalIntentReference from {url}" query = fetch.query_and_describe(utm_client, "DELETE", url, scope=scd.SCOPE_SC) @@ -122,7 +122,7 @@ def delete_operational_intent_reference( ) try: return ImplicitDict.parse( - query.response.json, scd.ChangeOperationalIntentReferenceResponse + query.response.json, api.ChangeOperationalIntentReferenceResponse ) except KeyError: raise QueryError( @@ -139,7 +139,7 @@ def delete_operational_intent_reference( def get_operational_intent_details( utm_client: UTMClientSession, uss_base_url: str, id: str -) -> Tuple[scd.OperationalIntent, Query]: +) -> Tuple[api.OperationalIntent, Query]: url = f"{uss_base_url}/uss/v1/operational_intents/{id}" subject = f"getOperationalIntentDetails from {url}" query = fetch.query_and_describe( @@ -159,7 +159,7 @@ def get_operational_intent_details( ) try: resp_body = ImplicitDict.parse( - query.response.json, scd.GetOperationalIntentDetailsResponse + query.response.json, api.GetOperationalIntentDetailsResponse ) except KeyError: raise QueryError( @@ -175,7 +175,7 @@ def get_operational_intent_details( def notify_operational_intent_details_changed( utm_client: UTMClientSession, uss_base_url: str, - update: scd.PutOperationalIntentDetailsParameters, + update: api.PutOperationalIntentDetailsParameters, ) -> Query: url = f"{uss_base_url}/uss/v1/operational_intents" subject = f"notifyOperationalIntentDetailsChanged to {url}" @@ -204,8 +204,8 @@ def notify_operational_intent_details_changed( def notify_subscribers( utm_client: UTMClientSession, id: str, - operational_intent: Optional[scd.OperationalIntent], - subscribers: List[scd.SubscriberToNotify], + operational_intent: Optional[api.OperationalIntent], + subscribers: List[api.SubscriberToNotify], ): for subscriber in subscribers: kwargs = { @@ -214,7 +214,7 @@ def notify_subscribers( } if operational_intent is not None: kwargs["operational_intent"] = operational_intent - update = scd.PutOperationalIntentDetailsParameters(**kwargs) + update = api.PutOperationalIntentDetailsParameters(**kwargs) notify_operational_intent_details_changed( utm_client, subscriber.uss_base_url, update ) diff --git a/monitoring/monitorlib/fetch/scd.py b/monitoring/monitorlib/fetch/scd.py index a8d8272f68..3d933a7b2b 100644 --- a/monitoring/monitorlib/fetch/scd.py +++ b/monitoring/monitorlib/fetch/scd.py @@ -7,6 +7,8 @@ from implicitdict import ImplicitDict from monitoring.monitorlib import fetch, infrastructure, scd +from monitoring.monitorlib.geo import Polygon +from monitoring.monitorlib.geotemporal import Volume4D class FetchedEntityReferences(fetch.Query): @@ -83,13 +85,13 @@ def _entity_references( ) -> FetchedEntityReferences: # Query DSS for Entities in 4D volume of interest request_body = { - "area_of_interest": scd.make_vol4( + "area_of_interest": Volume4D.from_values( start_time, end_time, alt_min_m, alt_max_m, - polygon=scd.make_polygon(latlngrect=area), - ) + polygon=Polygon.from_latlng_rect(latlngrect=area), + ).to_f3548v21() } url = "/dss/v1/{}/query".format(dss_resource_name) scope = scd.SCOPE_CP if "constraint" in dss_resource_name else scd.SCOPE_SC diff --git a/monitoring/monitorlib/geo.py b/monitoring/monitorlib/geo.py index 9aefb6a04f..5d6c138bcb 100644 --- a/monitoring/monitorlib/geo.py +++ b/monitoring/monitorlib/geo.py @@ -1,8 +1,12 @@ +from __future__ import annotations import math from enum import Enum from typing import List, Tuple, Union, Optional + +from implicitdict import ImplicitDict import s2sphere -from implicitdict import ImplicitDict, StringBasedDateTime +import shapely.geometry +from uas_standards.astm.f3548.v21 import api as f3548v21 EARTH_CIRCUMFERENCE_KM = 40075 EARTH_CIRCUMFERENCE_M = EARTH_CIRCUMFERENCE_KM * 1000 @@ -39,11 +43,62 @@ class Radius(ImplicitDict): class Polygon(ImplicitDict): vertices: List[LatLngPoint] + @staticmethod + def from_coords(coords: List[Tuple[float, float]]) -> Polygon: + return Polygon( + vertices=[LatLngPoint(lat=lat, lng=lng) for (lat, lng) in coords] + ) + + @staticmethod + def from_latlng_rect(latlngrect: s2sphere.LatLngRect) -> Polygon: + return Polygon( + vertices=[ + LatLngPoint( + lat=latlngrect.lat_lo().degrees, lng=latlngrect.lng_lo().degrees + ), + LatLngPoint( + lat=latlngrect.lat_lo().degrees, lng=latlngrect.lng_hi().degrees + ), + LatLngPoint( + lat=latlngrect.lat_hi().degrees, lng=latlngrect.lng_hi().degrees + ), + LatLngPoint( + lat=latlngrect.lat_hi().degrees, lng=latlngrect.lng_lo().degrees + ), + ] + ) + + @staticmethod + def from_f3548v21(vol: Union[f3548v21.Polygon, dict]) -> Polygon: + if not isinstance(vol, f3548v21.Polygon) and isinstance(vol, dict): + vol = ImplicitDict.parse(vol, f3548v21.Polygon) + return Polygon( + vertices=[ImplicitDict.parse(p, LatLngPoint) for p in vol.vertices] + ) + class Circle(ImplicitDict): center: LatLngPoint radius: Radius + @staticmethod + def from_meters( + lat_degrees: float, lng_degrees: float, radius_meters: float + ) -> Circle: + return Circle( + center=LatLngPoint(lat=lat_degrees, lng=lng_degrees), + radius=Radius(value=radius_meters, units="M"), + ) + + @staticmethod + def from_f3548v21(vol: Union[f3548v21.Circle, dict]) -> Circle: + if not isinstance(vol, f3548v21.Circle) and isinstance(vol, dict): + vol = ImplicitDict.parse(vol, f3548v21.Circle) + return Circle( + center=ImplicitDict.parse(vol.center, LatLngPoint), + radius=ImplicitDict.parse(vol.radius, Radius), + ) + class AltitudeDatum(str, Enum): W84 = "W84" @@ -59,11 +114,15 @@ class Altitude(ImplicitDict): units: DistanceUnits @staticmethod - def w84m(value: Optional[float]): - if not value: + def w84m(value: Optional[float]) -> Optional[Altitude]: + if value is None: return None return Altitude(value=value, reference=AltitudeDatum.W84, units=DistanceUnits.M) + @staticmethod + def from_f3548v21(vol: Union[f3548v21.Altitude, dict]) -> Altitude: + return ImplicitDict.parse(vol, Altitude) + class Volume3D(ImplicitDict): outline_circle: Optional[Circle] = None @@ -103,13 +162,70 @@ def altitude_upper_wgs84_m(self, default_value: Optional[float] = None) -> float ) return self.altitude_upper.value + def intersects_vol3(self, vol3_2: Volume3D) -> bool: + vol3_1 = self + if vol3_1.altitude_upper.value < vol3_2.altitude_lower.value: + return False + if vol3_1.altitude_lower.value > vol3_2.altitude_upper.value: + return False + + if vol3_1.outline_circle: + circle = vol3_1.outline_circle + if circle.radius.units != "M": + raise NotImplementedError( + "Unsupported circle radius units: {}".format(circle.radius.units) + ) + ref = s2sphere.LatLng.from_degrees(circle.center.lat, circle.center.lng) + footprint1 = shapely.geometry.Point(0, 0).buffer( + vol3_1.outline_circle.radius.value + ) + elif vol3_1.outline_polygon: + p = vol3_1.outline_polygon.vertices[0] + ref = s2sphere.LatLng.from_degrees(p.lat, p.lng) + footprint1 = shapely.geometry.Polygon( + flatten(ref, s2sphere.LatLng.from_degrees(v.lat, v.lng)) + for v in vol3_1.outline_polygon.vertices + ) + else: + raise ValueError("Neither outline_circle nor outline_polygon specified") + + if vol3_2.outline_circle: + circle = vol3_2.outline_circle + if circle.radius.units != "M": + raise NotImplementedError( + "Unsupported circle radius units: {}".format(circle.radius.units) + ) + xy = flatten( + ref, s2sphere.LatLng.from_degrees(circle.center.lat, circle.center.lng) + ) + footprint2 = shapely.geometry.Point(*xy).buffer(circle.radius.value) + elif vol3_2.outline_polygon: + footprint2 = shapely.geometry.Polygon( + flatten(ref, s2sphere.LatLng.from_degrees(v.lat, v.lng)) + for v in vol3_2.outline_polygon.vertices + ) + else: + raise ValueError("Neither outline_circle nor outline_polygon specified") -class Volume4D(ImplicitDict): - """Generic representation of a 4D volume, usable across multiple standards and formats.""" + return footprint1.intersects(footprint2) - volume: Volume3D - time_start: Optional[StringBasedDateTime] = None - time_end: Optional[StringBasedDateTime] = None + @staticmethod + def from_f3548v21(vol: Union[f3548v21.Volume3D, dict]) -> Volume3D: + if not isinstance(vol, f3548v21.Volume3D) and isinstance(vol, dict): + vol = ImplicitDict.parse(vol, f3548v21.Volume3D) + kwargs = {} + if "outline_circle" in vol and vol.outline_circle: + kwargs["outline_circle"] = Circle.from_f3548v21(vol.outline_circle) + if "outline_polygon" in vol and vol.outline_polygon: + kwargs["outline_polygon"] = Polygon.from_f3548v21(vol.outline_polygon) + if "altitude_lower" in vol and vol.altitude_lower: + kwargs["altitude_lower"] = Altitude.from_f3548v21(vol.altitude_lower) + if "altitude_upper" in vol and vol.altitude_upper: + kwargs["altitude_upper"] = Altitude.from_f3548v21(vol.altitude_upper) + return Volume3D(**kwargs) + + def to_f3548v21(self) -> f3548v21.Volume3D: + return ImplicitDict.parse(self, f3548v21.Volume3D) def make_latlng_rect(area) -> s2sphere.LatLngRect: @@ -265,3 +381,7 @@ def to_vertices(self) -> List[s2sphere.LatLng]: s2sphere.LatLng.from_degrees(self.lat_max, self.lng_max), s2sphere.LatLng.from_degrees(self.lat_min, self.lng_max), ] + + +def latitude_degrees(distance_meters: float) -> float: + return 360 * distance_meters / EARTH_CIRCUMFERENCE_M diff --git a/monitoring/monitorlib/geotemporal.py b/monitoring/monitorlib/geotemporal.py index 248b56bdd7..51f4d1b861 100644 --- a/monitoring/monitorlib/geotemporal.py +++ b/monitoring/monitorlib/geotemporal.py @@ -1,14 +1,20 @@ from __future__ import annotations +import math +from ctypes import Union from datetime import datetime, timedelta from enum import Enum -from typing import Optional, List +from typing import Optional, List, Tuple import arrow from implicitdict import ImplicitDict, StringBasedTimeDelta, StringBasedDateTime from pvlib.solarposition import get_solarposition -from monitoring.monitorlib.geo import LatLngPoint -from monitoring.monitorlib.scd import Polygon, Circle, Altitude, Volume3D, Volume4D +import s2sphere as s2sphere +from uas_standards.astm.f3411.v22a.api import Polygon +from uas_standards.astm.f3548.v21 import api as f3548v21 + +from monitoring.monitorlib import geo +from monitoring.monitorlib.geo import LatLngPoint, Circle, Altitude, Volume3D class OffsetTime(ImplicitDict): @@ -139,6 +145,14 @@ def _sun_elevation(t: datetime, lat_deg: float, lng_deg: float) -> float: return get_solarposition(t, lat_deg, lng_deg).elevation.values[0] +class Time(StringBasedDateTime): + def offset(self, dt: timedelta) -> Time: + return Time(self.datetime + dt) + + def to_f3548v21(self) -> f3548v21.Time: + return f3548v21.Time(value=self) + + def resolve_time(test_time: TestTime, start_of_test: datetime) -> datetime: """Resolve TestTime into specific datetime.""" result = None @@ -221,6 +235,107 @@ def resolve_time(test_time: TestTime, start_of_test: datetime) -> datetime: return result +class Volume4D(ImplicitDict): + """Generic representation of a 4D volume, usable across multiple standards and formats.""" + + volume: Volume3D + time_start: Optional[Time] = None + time_end: Optional[Time] = None + + def offset_time(self, dt: timedelta) -> Volume4D: + kwargs = {"volume": self.volume} + if self.time_start: + kwargs["time_start"] = self.time_start.offset(dt) + if self.time_end: + kwargs["time_end"] = self.time_end.offset(dt) + 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: + return False + if vol4_1.time_start.datetime > vol4_2.time_end.datetime: + return False + return self.volume.intersects_vol3(vol4_2.volume) + + @property + def rect_bounds(self) -> s2sphere.LatLngRect: + lat_min = 90 + lat_max = -90 + lng_min = 360 + lng_max = -360 + if self.volume.outline_polygon: + for v in self.volume.outline_polygon.vertices: + lat_min = min(lat_min, v.lat) + lat_max = max(lat_max, v.lat) + lng_min = min(lng_min, v.lng) + lng_max = max(lng_max, v.lng) + if self.volume.outline_circle: + circle = self.volume.outline_circle + if circle.radius.units != "M": + raise NotImplementedError( + "Unsupported circle radius units: {}".format(circle.radius.units) + ) + lat_radius = 360 * circle.radius.value / geo.EARTH_CIRCUMFERENCE_M + lng_radius = ( + 360 + * circle.radius.value + / (geo.EARTH_CIRCUMFERENCE_M * math.cos(math.radians(lat_radius))) + ) + lat_min = min(lat_min, circle.center.lat - lat_radius) + lat_max = max(lat_max, circle.center.lat + lat_radius) + lng_min = min(lng_min, circle.center.lng - lng_radius) + lng_max = max(lng_max, circle.center.lng + lng_radius) + p1 = s2sphere.LatLng.from_degrees(lat_min, lng_min) + p2 = s2sphere.LatLng.from_degrees(lat_max, lng_max) + return s2sphere.LatLngRect.from_point_pair(p1, p2) + + @staticmethod + def from_values( + t0: Optional[datetime] = None, + t1: Optional[datetime] = None, + alt0: Optional[float] = None, + alt1: Optional[float] = None, + circle: Optional[Circle] = None, + polygon: Optional[Polygon] = None, + ) -> Volume4D: + kwargs = dict() + if circle is not None: + kwargs["outline_circle"] = circle + if polygon is not None: + kwargs["outline_polygon"] = polygon + if alt0 is not None: + kwargs["altitude_lower"] = Altitude.w84m(alt0) + if alt1 is not None: + kwargs["altitude_upper"] = Altitude.w84m(alt1) + vol3 = Volume3D(**kwargs) + kwargs = {"volume": vol3} + if t0 is not None: + kwargs["time_start"] = Time(t0) + if t1 is not None: + kwargs["time_end"] = Time(t1) + return Volume4D(**kwargs) + + @staticmethod + def from_f3548v21(vol: Union[f3548v21.Volume4D, dict]) -> Volume4D: + if not isinstance(vol, f3548v21.Volume4D) and isinstance(vol, dict): + vol = ImplicitDict.parse(vol, f3548v21.Volume4D) + kwargs = {"volume": Volume3D.from_f3548v21(vol.volume)} + if "time_start" in vol and vol.time_start: + kwargs["time_start"] = Time(vol.time_start.value) + if "time_end" in vol and vol.time_end: + kwargs["time_end"] = Time(vol.time_end.value) + return Volume4D(**kwargs) + + def to_f3548v21(self) -> f3548v21.Volume4D: + kwargs = {"volume": self.volume.to_f3548v21()} + if "time_start" in self and self.time_start: + kwargs["time_start"] = self.time_start.to_f3548v21() + if "time_end" in self and self.time_end: + kwargs["time_end"] = self.time_end.to_f3548v21() + return f3548v21.Volume4D(**kwargs) + + def resolve_volume4d(template: Volume4DTemplate, start_of_test: datetime) -> Volume4D: """Resolve Volume4DTemplate into concrete Volume4D.""" # Make 3D volume @@ -274,3 +389,144 @@ def resolve_volume4d(template: Volume4DTemplate, start_of_test: datetime) -> Vol kwargs["time_end"] = time_end return Volume4D(**kwargs) + + +class Volume4DCollection(ImplicitDict): + volumes: List[Volume4D] + + @property + def time_start(self) -> Optional[Time]: + return ( + Time(value=min(v.time_start.datetime for v in self.volumes)) + if all("time_start" in v and v.time_start for v in self.volumes) + else None + ) + + @property + def time_end(self) -> Optional[Time]: + return ( + Time(value=min(v.time_end.datetime for v in self.volumes)) + if all("time_end" in v and v.time_end for v in self.volumes) + else None + ) + + def offset_times(self, dt: timedelta) -> Volume4DCollection: + return Volume4DCollection(volumes=[v.offset_time(dt) for v in self.volumes]) + + @property + def rect_bounds(self) -> s2sphere.LatLngRect: + if not self.volumes: + raise ValueError( + "Cannot compute rectangular bounds when no volumes are present" + ) + lat_min = math.inf + lat_max = -math.inf + lng_min = math.inf + lng_max = -math.inf + for vol4 in self.volumes: + if "outline_polygon" in vol4.volume and vol4.volume.outline_polygon: + for v in vol4.volume.outline_polygon.vertices: + lat_min = min(lat_min, v.lat) + lat_max = max(lat_max, v.lat) + lng_min = min(lng_min, v.lng) + lng_max = max(lng_max, v.lng) + if "outline_circle" in vol4.volume and vol4.volume.outline_circle: + circle = vol4.volume.outline_circle + if circle.radius.units != "M": + raise NotImplementedError( + "Unsupported circle radius units: {}".format( + circle.radius.units + ) + ) + lat_radius = 360 * circle.radius.value / geo.EARTH_CIRCUMFERENCE_M + lng_radius = ( + 360 + * circle.radius.value + / (geo.EARTH_CIRCUMFERENCE_M * math.cos(math.radians(lat_radius))) + ) + lat_min = min(lat_min, circle.center.lat - lat_radius) + lat_max = max(lat_max, circle.center.lat + lat_radius) + lng_min = min(lng_min, circle.center.lng - lng_radius) + lng_max = max(lng_max, circle.center.lng + lng_radius) + p1 = s2sphere.LatLng.from_degrees(lat_min, lng_min) + p2 = s2sphere.LatLng.from_degrees(lat_max, lng_max) + return s2sphere.LatLngRect.from_point_pair(p1, p2) + + @property + def bounding_volume(self) -> Volume4D: + v_min, v_max = self.meter_altitude_bounds + rect_bound = self.rect_bounds + lat_lo = rect_bound.lat_lo().degrees + lng_lo = rect_bound.lng_lo().degrees + lat_hi = rect_bound.lat_hi().degrees + lng_hi = rect_bound.lng_hi().degrees + kwargs = { + "volume": Volume3D( + altitude_lower=Altitude.w84m(v_min), + altitude_upper=Altitude.w84m(v_max), + outline_polygon=Polygon( + vertices=[ + LatLngPoint(lat=lat_lo, lng=lng_lo), + LatLngPoint(lat=lat_hi, lng=lng_lo), + LatLngPoint(lat=lat_hi, lng=lng_hi), + LatLngPoint(lat=lat_lo, lng=lng_hi), + ] + ), + ) + } + if self.time_start is not None: + kwargs["time_start"] = self.time_start + if self.time_end is not None: + kwargs["time_end"] = self.time_end + return Volume4D(**kwargs) + + @property + def meter_altitude_bounds(self) -> Tuple[float, float]: + alt_lo = min( + vol4.volume.altitude_lower.value + for vol4 in self.volumes + if "altitude_lower" in vol4.volume + ) + alt_hi = max( + vol4.volume.altitude_upper.value + for vol4 in self.volumes + if "altitude_upper" in vol4.volume + ) + units = [ + vol4.volume.altitude_lower.units + for vol4 in self.volumes + if "altitude_lower" in vol4.volume + and vol4.volume.altitude_lower.units != "M" + ] + if units: + raise NotImplementedError( + f"altitude_lower units must currently be M; found instead {', '.join(units)}" + ) + units = [ + vol4.volume.altitude_upper.units + for vol4 in self.volumes + if "altitude_upper" in vol4.volume + and vol4.volume.altitude_upper.units != "M" + ] + if units: + raise NotImplementedError( + f"altitude_upper units must currently be M; found instead {', '.join(units)}" + ) + return alt_lo, alt_hi + + def intersects_vol4s(self, vol4s_2: Volume4DCollection) -> bool: + for v1 in self.volumes: + for v2 in vol4s_2.volumes: + if v1.intersects_vol4(v2): + return True + return False + + @staticmethod + def from_f3548v21( + vol4s: List[Union[f3548v21.Volume4D, dict]] + ) -> Volume4DCollection: + volumes = [Volume4D.from_f3548v21(v) for v in vol4s] + return Volume4DCollection(volumes=volumes) + + def to_f3548v21(self) -> List[f3548v21.Volume4D]: + return [v.to_f3548v21() for v in self.volumes] diff --git a/monitoring/monitorlib/mutate/scd.py b/monitoring/monitorlib/mutate/scd.py index 378dc8ca90..179e2b29e0 100644 --- a/monitoring/monitorlib/mutate/scd.py +++ b/monitoring/monitorlib/mutate/scd.py @@ -7,6 +7,8 @@ from monitoring.monitorlib import infrastructure, scd from monitoring.monitorlib import fetch +from monitoring.monitorlib.geo import Polygon +from monitoring.monitorlib.geotemporal import Volume4D class MutatedSubscription(fetch.Query): @@ -58,13 +60,13 @@ def put_subscription( server_id: Optional[str] = None, ) -> MutatedSubscription: body = { - "extents": scd.make_vol4( + "extents": Volume4D.from_values( start_time, end_time, min_alt_m, max_alt_m, - polygon=scd.make_polygon(latlngrect=area), - ), + polygon=Polygon.from_latlng_rect(latlngrect=area), + ).to_f3548v21(), "uss_base_url": base_url, "notify_for_operational_intents": notify_for_op_intents, "notify_for_constraints": notify_for_constraints, diff --git a/monitoring/monitorlib/scd.py b/monitoring/monitorlib/scd.py index 71c0b389c1..0917fb6b88 100644 --- a/monitoring/monitorlib/scd.py +++ b/monitoring/monitorlib/scd.py @@ -7,14 +7,22 @@ import arrow import s2sphere import shapely.geometry -from uas_standards.astm.f3548.v21.api import OperationalIntentState + +from monitoring.monitorlib.geo import LatLngPoint, Radius +from uas_standards.astm.f3548.v21.api import ( + OperationalIntentState, + Polygon, + Volume4D, + Volume3D, + Time, + Altitude, + Circle, +) from monitoring.monitorlib import geo -TIME_FORMAT_CODE = "RFC3339" DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" -EARTH_CIRCUMFERENCE_M = 40.075e6 # API version 0.3.17 is programmatically identical to version 1.0.0, so both these versions can be used interchangeably. API_1_0_0 = "1.0.0" @@ -29,22 +37,6 @@ NO_OVN_PHRASES = {"", "Available from USS"} -def latitude_degrees(distance_meters: float) -> float: - return 360 * distance_meters / EARTH_CIRCUMFERENCE_M - - -def parse_time(time: Dict) -> datetime: - t_str = time["value"] - return arrow.get(t_str).datetime - - -def offset_time(vol4s: List[Dict], dt: timedelta) -> List[Dict]: - for vol4 in vol4s: - vol4["time_start"] = make_time(parse_time(vol4["time_start"]) + dt) - vol4["time_end"] = make_time(parse_time(vol4["time_end"]) + dt) - return vol4s - - class Subscription(dict): @property def valid(self) -> bool: @@ -57,392 +49,6 @@ def version(self) -> Optional[str]: return self.get("version", None) -################################################################################ -#################### Start of ASTM-standard definitions ##################### -#################### interfaces/astm-utm/Protocol/utm.yaml ##################### -################################################################################ - - -class LatLngPoint(ImplicitDict): - """A class to hold information about a location as Latitude / Longitude pair""" - - lat: float - lng: float - - -class Radius(ImplicitDict): - """A class to hold the radius of a circle for the outline_circle object""" - - value: float - units: str - - -class Polygon(ImplicitDict): - """A class to hold the polygon object, used in the outline_polygon of the Volume3D object""" - - vertices: List[LatLngPoint] # A minimum of three LatLngPoints are required - - -class Circle(ImplicitDict): - """A class the details of a circle object used in the outline_circle object""" - - center: LatLngPoint - radius: Radius - - -class AltitudeDatum(str, Enum): - W84 = "W84" - """WGS84 reference ellipsoid""" - - SFC = "SFC" - """Surface of the ground""" - - -class Altitude(ImplicitDict): - """A class to hold altitude information""" - - value: float - reference: AltitudeDatum - units: str - - -class Time(ImplicitDict): - """A class to hold Time details""" - - value: StringBasedDateTime - format: Literal["RFC3339"] - - -class Volume3D(ImplicitDict): - """A class to hold Volume3D objects""" - - outline_circle: Optional[Circle] - outline_polygon: Optional[Polygon] - altitude_lower: Optional[Altitude] - altitude_upper: Optional[Altitude] - - -class Volume4D(ImplicitDict): - """A class to hold Volume4D objects""" - - volume: Volume3D - time_start: Optional[Time] - time_end: Optional[Time] - - -class OperationalIntentReference(ImplicitDict): - id: str - manager: str - uss_availability: str - version: int - state: str - ovn: str - time_start: Time - time_end: Time - uss_base_url: str - subscription_id: str - - -class ErrorResponse(ImplicitDict): - message: str - - -class QueryOperationalIntentReferenceParameters(ImplicitDict): - area_of_interest: Volume4D - - -class QueryOperationalIntentReferenceResponse(ImplicitDict): - operational_intent_references: List[OperationalIntentReference] - - -class ImplicitSubscriptionParameters(ImplicitDict): - uss_base_url: str - notify_for_constraints: Optional[bool] - - -class PutOperationalIntentReferenceParameters(ImplicitDict): - extents: List[Volume4D] - key: List[str] - state: str - uss_base_url: str - subscription_id: Optional[str] - new_subscription: Optional[ImplicitSubscriptionParameters] - - -class SubscriptionState(ImplicitDict): - subscription_id: str - notification_index: int - - -class SubscriberToNotify(ImplicitDict): - uss_base_url: str - subscriptions: List[SubscriptionState] - - -class ChangeOperationalIntentReferenceResponse(ImplicitDict): - subscribers: List[SubscriberToNotify] - operational_intent_reference: OperationalIntentReference - - -class OperationalIntentDetails(ImplicitDict): - volumes: List[Volume4D] - off_nominal_volumes: List[Volume4D] - priority: int - - -class OperationalIntent(ImplicitDict): - reference: OperationalIntentReference - details: OperationalIntentDetails - - -class GetOperationalIntentDetailsResponse(ImplicitDict): - operational_intent: OperationalIntent - - -class PutOperationalIntentDetailsParameters(ImplicitDict): - operational_intent_id: str - operational_intent: Optional[OperationalIntent] - subscriptions: List[SubscriptionState] - - -################################################################################ -#################### End of ASTM-standard definitions ##################### -#################### interfaces/astm-utm/Protocol/utm.yaml ##################### -################################################################################ - - -class DeleteAllFlightsRequest(ImplicitDict): - extents: List[Volume4D] - - -def make_vol4( - t0: Optional[datetime] = None, - t1: Optional[datetime] = None, - alt0: Optional[float] = None, - alt1: Optional[float] = None, - circle: Dict = None, - polygon: Dict = None, -) -> Volume4D: - kwargs = dict() - if circle is not None: - kwargs["outline_circle"] = circle - if polygon is not None: - kwargs["outline_polygon"] = polygon - if alt0 is not None: - kwargs["altitude_lower"] = make_altitude(alt0) - if alt1 is not None: - kwargs["altitude_upper"] = make_altitude(alt1) - vol3 = Volume3D(**kwargs) - kwargs = {"volume": vol3} - if t0 is not None: - kwargs["time_start"] = make_time(t0) - if t1 is not None: - kwargs["time_end"] = make_time(t1) - return Volume4D(**kwargs) - - -def make_time(t: datetime) -> Time: - s = t.isoformat() - if t.tzinfo is None: - s += "Z" - return Time(value=StringBasedDateTime(s), format="RFC3339") - - -def make_altitude(alt_meters: float) -> Altitude: - return Altitude(value=alt_meters, reference="W84", units="M") - - -def make_circle(lat: float, lng: float, radius: float) -> Circle: - return Circle( - center=LatLngPoint(lat=lat, lng=lng), radius=Radius(value=radius, units="M") - ) - - -def make_polygon( - coords: List[Tuple[float, float]] = None, latlngrect: s2sphere.LatLngRect = None -) -> Polygon: - if coords is not None: - return Polygon( - vertices=[LatLngPoint(lat=lat, lng=lng) for (lat, lng) in coords] - ) - - return Polygon( - vertices=[ - LatLngPoint( - lat=latlngrect.lat_lo().degrees, lng=latlngrect.lng_lo().degrees - ), - LatLngPoint( - lat=latlngrect.lat_lo().degrees, lng=latlngrect.lng_hi().degrees - ), - LatLngPoint( - lat=latlngrect.lat_hi().degrees, lng=latlngrect.lng_hi().degrees - ), - LatLngPoint( - lat=latlngrect.lat_hi().degrees, lng=latlngrect.lng_lo().degrees - ), - ] - ) - - -def start_of(vol4s: List[Volume4D]) -> datetime: - return min([parse_time(vol4["time_start"]) for vol4 in vol4s]) - - -def end_of(vol4s: List[Volume4D]) -> datetime: - return max([parse_time(vol4["time_end"]) for vol4 in vol4s]) - - -def rect_bounds_of(vol4s: List[Volume4D]) -> s2sphere.LatLngRect: - lat_min = 90 - lat_max = -90 - lng_min = 360 - lng_max = -360 - for vol4 in vol4s: - if "outline_polygon" in vol4.volume: - for v in vol4.volume.outline_polygon.vertices: - lat_min = min(lat_min, v.lat) - lat_max = max(lat_max, v.lat) - lng_min = min(lng_min, v.lng) - lng_max = max(lng_max, v.lng) - if "outline_circle" in vol4.volume: - circle = vol4.volume.outline_circle - if circle.radius.units != "M": - raise ValueError( - "Unsupported circle radius units: {}".format(circle.radius.units) - ) - lat_radius = 360 * circle.radius.value / geo.EARTH_CIRCUMFERENCE_M - lng_radius = ( - 360 - * circle.radius.value - / (geo.EARTH_CIRCUMFERENCE_M * math.cos(math.radians(lat_radius))) - ) - lat_min = min(lat_min, circle.center.lat - lat_radius) - lat_max = max(lat_max, circle.center.lat + lat_radius) - lng_min = min(lng_min, circle.center.lng - lng_radius) - lng_max = max(lng_max, circle.center.lng + lng_radius) - p1 = s2sphere.LatLng.from_degrees(lat_min, lng_min) - p2 = s2sphere.LatLng.from_degrees(lat_max, lng_max) - return s2sphere.LatLngRect.from_point_pair(p1, p2) - - -def meter_altitude_bounds_of(vol4s: List[Volume4D]) -> Tuple[float, float]: - alt_lo = min( - vol4.volume.altitude_lower.value - for vol4 in vol4s - if "altitude_lower" in vol4.volume - ) - alt_hi = max( - vol4.volume.altitude_upper.value - for vol4 in vol4s - if "altitude_upper" in vol4.volume - ) - units = [ - vol4.volume.altitude_lower.units - for vol4 in vol4s - if "altitude_lower" in vol4.volume and vol4.volume.altitude_lower.units != "M" - ] - if units: - raise ValueError( - f"altitude_lower units must always be M; found instead {', '.join(units)}" - ) - units = [ - vol4.volume.altitude_upper.units - for vol4 in vol4s - if "altitude_upper" in vol4.volume and vol4.volume.altitude_upper.units != "M" - ] - if units: - raise ValueError( - f"altitude_upper units must always be M; found instead {', '.join(units)}" - ) - return alt_lo, alt_hi - - -def bounding_vol4(vol4s: List[Volume4D]) -> Volume4D: - t_start = start_of(vol4s) - t_end = end_of(vol4s) - v_min, v_max = meter_altitude_bounds_of(vol4s) - rect_bound = rect_bounds_of(vol4s) - lat_lo = rect_bound.lat_lo().degrees - lng_lo = rect_bound.lng_lo().degrees - lat_hi = rect_bound.lat_hi().degrees - lng_hi = rect_bound.lng_hi().degrees - return Volume4D( - time_start=make_time(t_start), - time_end=make_time(t_end), - volume=Volume3D( - altitude_lower=make_altitude(v_min), - altitude_upper=make_altitude(v_max), - outline_polygon=Polygon( - vertices=[ - LatLngPoint(lat=lat_lo, lng=lng_lo), - LatLngPoint(lat=lat_hi, lng=lng_lo), - LatLngPoint(lat=lat_hi, lng=lng_hi), - LatLngPoint(lat=lat_lo, lng=lng_hi), - ] - ), - ), - ) - - -def vol4_intersect(vol4_1: Volume4D, vol4_2: Volume4D) -> bool: - if parse_time(vol4_1.time_end) < parse_time(vol4_2.time_start): - return False - if parse_time(vol4_1.time_start) > parse_time(vol4_2.time_end): - return False - if vol4_1.volume.altitude_upper.value < vol4_2.volume.altitude_lower.value: - return False - if vol4_1.volume.altitude_lower.value > vol4_2.volume.altitude_upper.value: - return False - - if "outline_circle" in vol4_1.volume: - circle = vol4_1.volume.outline_circle - if circle.radius.units != "M": - raise ValueError( - "Unsupported circle radius units: {}".format(circle.radius.units) - ) - ref = s2sphere.LatLng.from_degrees(circle.center.lat, circle.center.lng) - footprint1 = shapely.geometry.Point(0, 0).buffer( - vol4_1.volume.outline_circle.radius.value - ) - elif "outline_polygon" in vol4_1.volume: - p = vol4_1.volume.outline_polygon.vertices[0] - ref = s2sphere.LatLng.from_degrees(p.lat, p.lng) - footprint1 = shapely.geometry.Polygon( - geo.flatten(ref, s2sphere.LatLng.from_degrees(v.lat, v.lng)) - for v in vol4_1.volume.outline_polygon.vertices - ) - else: - raise ValueError("Neither outline_circle nor outline_polygon specified") - - if "outline_circle" in vol4_2.volume: - circle = vol4_2.volume.outline_circle - if circle.radius.units != "M": - raise ValueError( - "Unsupported circle radius units: {}".format(circle.radius.units) - ) - xy = geo.flatten( - ref, s2sphere.LatLng.from_degrees(circle.center.lat, circle.center.lng) - ) - footprint2 = shapely.geometry.Point(*xy).buffer(circle.radius.value) - elif "outline_polygon" in vol4_1.volume: - footprint2 = shapely.geometry.Polygon( - geo.flatten(ref, s2sphere.LatLng.from_degrees(v.lat, v.lng)) - for v in vol4_2.volume.outline_polygon.vertices - ) - else: - raise ValueError("Neither outline_circle nor outline_polygon specified") - - return footprint1.intersects(footprint2) - - -def vol4s_intersect(vol4s_1: List[Volume4D], vol4s_2: List[Volume4D]) -> bool: - for v1 in vol4s_1: - for v2 in vol4s_2: - if vol4_intersect(v1, v2): - return True - return False - - def op_intent_transition_valid( transition_from: Optional[OperationalIntentState], transition_to: Optional[OperationalIntentState], diff --git a/monitoring/prober/run_locally.sh b/monitoring/prober/run_locally.sh index 7051a2f82f..2babedc80f 100755 --- a/monitoring/prober/run_locally.sh +++ b/monitoring/prober/run_locally.sh @@ -38,7 +38,6 @@ done OUTPUT_DIR="monitoring/prober/output" mkdir -p "$OUTPUT_DIR" -# TODO(#17): Remove F3411_22A_ALTITUDE_REFERENCE environment variable once DSS behaves correctly if ! docker run \ -u "$(id -u):$(id -g)" \ --network interop_ecosystem_network \ diff --git a/monitoring/prober/scd/test_constraint_simple.py b/monitoring/prober/scd/test_constraint_simple.py index 314e922170..fd823bcb09 100644 --- a/monitoring/prober/scd/test_constraint_simple.py +++ b/monitoring/prober/scd/test_constraint_simple.py @@ -10,6 +10,8 @@ import datetime +from monitoring.monitorlib.geo import Circle +from monitoring.monitorlib.geotemporal import Volume4D from monitoring.monitorlib.infrastructure import default_scope from monitoring.monitorlib import scd from monitoring.monitorlib.scd import ( @@ -39,7 +41,9 @@ def _make_c1_request(): time_end = time_start + datetime.timedelta(minutes=60) return { "extents": [ - scd.make_vol4(time_start, time_end, 0, 120, scd.make_circle(-56, 178, 50)) + Volume4D.from_values( + time_start, time_end, 0, 120, Circle.from_meters(-56, 178, 50) + ).to_f3548v21() ], "old_version": 0, "uss_base_url": BASE_URL, @@ -77,9 +81,9 @@ def test_constraint_does_not_exist_query(ids, scd_api, scd_session): resp = scd_session.post( "/constraint_references/query", json={ - "area_of_interest": scd.make_vol4( - time_now, time_now, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + time_now, time_now, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, scope=scope, ) @@ -197,9 +201,9 @@ def test_get_constraint_by_search(ids, scd_api, scd_session): resp = scd_session.post( "/constraint_references/query", json={ - "area_of_interest": scd.make_vol4( - None, None, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + None, None, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, scope=scope, ) @@ -217,9 +221,9 @@ def test_get_constraint_by_search_earliest_time_included(ids, scd_api, scd_sessi resp = scd_session.post( "/constraint_references/query", json={ - "area_of_interest": scd.make_vol4( - earliest_time, None, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + earliest_time, None, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -236,9 +240,9 @@ def test_get_constraint_by_search_earliest_time_excluded(ids, scd_api, scd_sessi resp = scd_session.post( "/constraint_references/query", json={ - "area_of_interest": scd.make_vol4( - earliest_time, None, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + earliest_time, None, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -255,9 +259,9 @@ def test_get_constraint_by_search_latest_time_included(ids, scd_api, scd_session resp = scd_session.post( "/constraint_references/query", json={ - "area_of_interest": scd.make_vol4( - None, latest_time, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + None, latest_time, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -274,9 +278,9 @@ def test_get_constraint_by_search_latest_time_excluded(ids, scd_api, scd_session resp = scd_session.post( "/constraint_references/query", json={ - "area_of_interest": scd.make_vol4( - None, latest_time, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + None, latest_time, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -401,9 +405,9 @@ def test_get_deleted_constraint_by_search(ids, scd_api, scd_session): resp = scd_session.post( "/constraint_references/query", json={ - "area_of_interest": scd.make_vol4( - None, None, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + None, None, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content diff --git a/monitoring/prober/scd/test_constraints_with_subscriptions.py b/monitoring/prober/scd/test_constraints_with_subscriptions.py index 5a58c2ce78..e228c5081c 100644 --- a/monitoring/prober/scd/test_constraints_with_subscriptions.py +++ b/monitoring/prober/scd/test_constraints_with_subscriptions.py @@ -11,6 +11,8 @@ import datetime from typing import Dict +from monitoring.monitorlib.geo import Circle +from monitoring.monitorlib.geotemporal import Volume4D from monitoring.monitorlib.infrastructure import default_scope from monitoring.monitorlib import scd from monitoring.monitorlib.scd import SCOPE_CM, SCOPE_SC, SCOPE_CP @@ -35,9 +37,13 @@ def _make_c1_request(): time_end = time_start + datetime.timedelta(minutes=60) return { "extents": [ - scd.make_vol4( - time_start, time_end, 0, 120, scd.make_circle(-12.00001, 33.99999, 50) - ) + Volume4D.from_values( + time_start, + time_end, + 0, + 120, + Circle.from_meters(-12.00001, 33.99999, 50), + ).to_f3548v21() ], "old_version": 0, "uss_base_url": CONSTRAINT_BASE_URL_1, @@ -48,9 +54,9 @@ def _make_sub_req(base_url: str, notify_ops: bool, notify_constraints: bool) -> time_start = datetime.datetime.utcnow() time_end = time_start + datetime.timedelta(minutes=60) return { - "extents": scd.make_vol4( - time_start, time_end, 0, 1000, scd.make_circle(-12, 34, 300) - ), + "extents": Volume4D.from_values( + time_start, time_end, 0, 1000, Circle.from_meters(-12, 34, 300) + ).to_f3548v21(), "old_version": 0, "uss_base_url": base_url, "notify_for_operations": notify_ops, diff --git a/monitoring/prober/scd/test_operation_references_error_cases.py b/monitoring/prober/scd/test_operation_references_error_cases.py index 692b660ccd..5a5419e4ea 100644 --- a/monitoring/prober/scd/test_operation_references_error_cases.py +++ b/monitoring/prober/scd/test_operation_references_error_cases.py @@ -7,6 +7,7 @@ import arrow import yaml +from monitoring.monitorlib.geotemporal import Volume4D, Volume4DCollection from monitoring.monitorlib.infrastructure import default_scope from monitoring.monitorlib import scd from monitoring.monitorlib.scd import SCOPE_SC @@ -258,8 +259,9 @@ def test_missing_conflicted_operation(ids, scd_api, scd_session): # Emplace the initial version of Operation 1 with open("./scd/resources/op_missing_initial.yaml", "r") as f: req = yaml.full_load(f) - dt = arrow.utcnow().datetime - scd.start_of(req["extents"]) - req["extents"] = scd.offset_time(req["extents"], dt) + extents = Volume4DCollection.from_f3548v21(req["extents"]) + dt = arrow.utcnow().datetime - extents.time_start.datetime + req["extents"] = extents.offset_times(dt).to_f3548v21() resp = scd_session.put( "/operational_intent_references/{}".format(ids(OP_TYPE)), json=req ) @@ -270,7 +272,8 @@ def test_missing_conflicted_operation(ids, scd_api, scd_session): # Emplace the pre-existing Operation that conflicted in the original observation with open("./scd/resources/op_missing_preexisting_unknown.yaml", "r") as f: req = yaml.full_load(f) - req["extents"] = scd.offset_time(req["extents"], dt) + extents = Volume4DCollection.from_f3548v21(req["extents"]) + req["extents"] = extents.offset_times(dt).to_f3548v21() req["key"] = [ovn1a] resp = scd_session.put( "/operational_intent_references/{}".format(ids(OP_TYPE2)), json=req @@ -280,7 +283,9 @@ def test_missing_conflicted_operation(ids, scd_api, scd_session): # Attempt to update Operation 1 without OVN for the pre-existing Operation with open("./scd/resources/op_missing_update.json", "r") as f: req = json.load(f) - req["extents"] = scd.offset_time(req["extents"], dt) + req["extents"] = ( + Volume4DCollection.from_f3548v21(req["extents"]).offset_times(dt).to_f3548v21() + ) req["key"] = [ovn1a] req["subscription_id"] = sub_id resp = scd_session.put( @@ -299,7 +304,9 @@ def test_missing_conflicted_operation(ids, scd_api, scd_session): # Perform an area-based query on the area occupied by Operation 1 with open("./scd/resources/op_missing_query.json", "r") as f: req = json.load(f) - req["area_of_interest"] = scd.offset_time([req["area_of_interest"]], dt)[0] + req["area_of_interest"] = ( + Volume4D.from_f3548v21(req["area_of_interest"]).offset_time(dt).to_f3548v21() + ) resp = scd_session.post("/operational_intent_references/query", json=req) assert resp.status_code == 200, resp.content ops = [op["id"] for op in resp.json()["operational_intent_references"]] @@ -315,8 +322,9 @@ def test_missing_conflicted_operation(ids, scd_api, scd_session): def test_big_operation_search(scd_api, scd_session): with open("./scd/resources/op_big_operation.json", "r") as f: req = json.load(f) - dt = arrow.utcnow().datetime - scd.start_of([req["area_of_interest"]]) - req["area_of_interest"] = scd.offset_time([req["area_of_interest"]], dt)[0] + aoi = Volume4D.from_f3548v21(req["area_of_interest"]) + dt = arrow.utcnow().datetime - aoi.time_start.datetime + req["area_of_interest"] = aoi.offset_time(dt).to_f3548v21() resp = scd_session.post("/operational_intent_references/query", json=req) assert resp.status_code == 400, resp.content diff --git a/monitoring/prober/scd/test_operation_simple.py b/monitoring/prober/scd/test_operation_simple.py index ea99296944..3f803ae1ce 100644 --- a/monitoring/prober/scd/test_operation_simple.py +++ b/monitoring/prober/scd/test_operation_simple.py @@ -10,6 +10,8 @@ import datetime +from monitoring.monitorlib.geo import Circle +from monitoring.monitorlib.geotemporal import Volume4D from monitoring.monitorlib.infrastructure import default_scope from monitoring.monitorlib import scd from monitoring.monitorlib.scd import SCOPE_SC, SCOPE_CM, SCOPE_CP @@ -36,7 +38,9 @@ def _make_op1_request(): time_end = time_start + datetime.timedelta(minutes=60) return { "extents": [ - scd.make_vol4(time_start, time_end, 0, 120, scd.make_circle(-56, 178, 50)) + Volume4D.from_values( + time_start, time_end, 0, 120, Circle.from_meters(-56, 178, 50) + ).to_f3548v21() ], "old_version": 0, "state": "Accepted", @@ -62,9 +66,9 @@ def test_op_does_not_exist_query( resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - time_now, end_time, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + time_now, end_time, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, scope=SCOPE_SC, ) @@ -77,9 +81,9 @@ def test_op_does_not_exist_query( resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - time_now, end_time, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + time_now, end_time, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, scope=SCOPE_CP, ) @@ -89,9 +93,9 @@ def test_op_does_not_exist_query( resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - time_now, end_time, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + time_now, end_time, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, scope=SCOPE_CM, ) @@ -218,9 +222,9 @@ def test_get_op_by_search(ids, scd_api, scd_session): resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - None, None, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + None, None, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -236,9 +240,9 @@ def test_get_op_by_search_earliest_time_included(ids, scd_api, scd_session): resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - earliest_time, None, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + earliest_time, None, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -254,9 +258,9 @@ def test_get_op_by_search_earliest_time_excluded(ids, scd_api, scd_session): resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - earliest_time, None, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + earliest_time, None, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -272,9 +276,9 @@ def test_get_op_by_search_latest_time_included(ids, scd_api, scd_session): resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - None, latest_time, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + None, latest_time, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -310,9 +314,9 @@ def test_get_op_by_query_other_uss(ids, scd_session2): resp = scd_session2.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - None, None, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + None, None, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -342,9 +346,9 @@ def test_get_op_by_search_latest_time_excluded(ids, scd_api, scd_session): resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - None, latest_time, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + None, latest_time, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -445,9 +449,9 @@ def test_get_deleted_op_by_search(ids, scd_api, scd_session): resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - None, None, 0, 5000, scd.make_circle(-56, 178, 300) - ) + "area_of_interest": Volume4D.from_values( + None, None, 0, 5000, Circle.from_meters(-56, 178, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content diff --git a/monitoring/prober/scd/test_operation_simple_heavy_traffic.py b/monitoring/prober/scd/test_operation_simple_heavy_traffic.py index 7a8d6e5f16..7653e5fba9 100644 --- a/monitoring/prober/scd/test_operation_simple_heavy_traffic.py +++ b/monitoring/prober/scd/test_operation_simple_heavy_traffic.py @@ -12,11 +12,12 @@ import datetime from monitoring.monitorlib import scd +from monitoring.monitorlib.geo import Circle +from monitoring.monitorlib.geotemporal import Volume4D from monitoring.monitorlib.scd import SCOPE_SC from monitoring.monitorlib.infrastructure import default_scope from monitoring.monitorlib.testing import assert_datetimes_are_equal from monitoring.prober.infrastructure import ( - depends_on, for_api_versions, register_resource_type, ) @@ -39,7 +40,9 @@ def _make_op_request(idx): lat = -56 - 0.001 * idx return { "extents": [ - scd.make_vol4(time_start, time_end, 0, 120, scd.make_circle(lat, 178, 50)) + Volume4D.from_values( + time_start, time_end, 0, 120, Circle.from_meters(lat, 178, 50) + ).to_f3548v21() ], "old_version": 0, "state": "Accepted", @@ -75,9 +78,9 @@ def test_ops_do_not_exist_query(ids, scd_api, scd_session): resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - time_now, end_time, 0, 5000, scd.make_circle(-56, 178, 12000) - ) + "area_of_interest": Volume4D.from_values( + time_now, end_time, 0, 5000, Circle.from_meters(-56, 178, 12000) + ).to_f3548v21() }, scope=SCOPE_SC, ) @@ -140,9 +143,9 @@ def test_get_ops_by_search(ids, scd_api, scd_session): resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - None, None, 0, 5000, scd.make_circle(-56, 178, 12000) - ) + "area_of_interest": Volume4D.from_values( + None, None, 0, 5000, Circle.from_meters(-56, 178, 12000) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -160,9 +163,9 @@ def test_get_ops_by_search_earliest_time_included(ids, scd_api, scd_session): resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - earliest_time, None, 0, 5000, scd.make_circle(-56, 178, 12000) - ) + "area_of_interest": Volume4D.from_values( + earliest_time, None, 0, 5000, Circle.from_meters(-56, 178, 12000) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -179,9 +182,9 @@ def test_get_ops_by_search_earliest_time_excluded(ids, scd_api, scd_session): resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - earliest_time, None, 0, 5000, scd.make_circle(-56, 178, 12000) - ) + "area_of_interest": Volume4D.from_values( + earliest_time, None, 0, 5000, Circle.from_meters(-56, 178, 12000) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -196,9 +199,9 @@ def test_get_ops_by_search_latest_time_included(ids, scd_api, scd_session): resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - None, latest_time, 0, 5000, scd.make_circle(-56, 178, 12000) - ) + "area_of_interest": Volume4D.from_values( + None, latest_time, 0, 5000, Circle.from_meters(-56, 178, 12000) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -215,9 +218,9 @@ def test_get_ops_by_search_latest_time_excluded(ids, scd_api, scd_session): resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - None, latest_time, 0, 5000, scd.make_circle(-56, 178, 12000) - ) + "area_of_interest": Volume4D.from_values( + None, latest_time, 0, 5000, Circle.from_meters(-56, 178, 12000) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -302,9 +305,9 @@ def test_get_deleted_ops_by_search(ids, scd_api, scd_session): resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - None, None, 0, 5000, scd.make_circle(-56, 178, 12000) - ) + "area_of_interest": Volume4D.from_values( + None, None, 0, 5000, Circle.from_meters(-56, 178, 12000) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content diff --git a/monitoring/prober/scd/test_operation_simple_heavy_traffic_concurrent.py b/monitoring/prober/scd/test_operation_simple_heavy_traffic_concurrent.py index 92621be2af..8a9d8d21e0 100644 --- a/monitoring/prober/scd/test_operation_simple_heavy_traffic_concurrent.py +++ b/monitoring/prober/scd/test_operation_simple_heavy_traffic_concurrent.py @@ -16,6 +16,8 @@ import json import inspect +from monitoring.monitorlib.geo import Circle +from monitoring.monitorlib.geotemporal import Volume4D from monitoring.monitorlib.infrastructure import default_scope from monitoring.monitorlib import scd from monitoring.monitorlib.scd import SCOPE_SC @@ -66,7 +68,9 @@ def _make_op_request_differ_in_2d(idx): time_end = time_start + datetime.timedelta(minutes=60) lat = _calculate_lat(idx) - vol4 = scd.make_vol4(time_start, time_end, 0, 120, scd.make_circle(lat, 178, 50)) + vol4 = Volume4D.from_values( + time_start, time_end, 0, 120, Circle.from_meters(lat, 178, 50) + ).to_f3548v21() return _make_op_request_with_extents(vol4) @@ -80,9 +84,9 @@ def _make_op_request_differ_in_altitude(idx): alt0 = delta * idx alt1 = alt0 + delta - 1 - vol4 = scd.make_vol4( - time_start, time_end, alt0, alt1, scd.make_circle(-56, 178, 50) - ) + vol4 = Volume4D.from_values( + time_start, time_end, alt0, alt1, Circle.from_meters(-56, 178, 50) + ).to_f3548v21() return _make_op_request_with_extents(vol4) @@ -96,7 +100,9 @@ def _make_op_request_differ_in_time(idx, time_gap): ) time_end = time_start + datetime.timedelta(minutes=delta - 1) - vol4 = scd.make_vol4(time_start, time_end, 0, 120, scd.make_circle(-56, 178, 50)) + vol4 = Volume4D.from_values( + time_start, time_end, 0, 120, Circle.from_meters(-56, 178, 50) + ).to_f3548v21() return _make_op_request_with_extents(vol4) @@ -169,9 +175,9 @@ async def _get_operation_async(op_id, scd_session_async, scd_api): async def _query_operation_async(idx, scd_session_async, scd_api): lat = _calculate_lat(idx) req_json = { - "area_of_interest": scd.make_vol4( - None, None, 0, 5000, scd.make_circle(lat, 178, 12000) - ) + "area_of_interest": Volume4D.from_values( + None, None, 0, 5000, Circle.from_meters(lat, 178, 12000) + ).to_f3548v21() } async with SEMAPHORE: if scd_api == scd.API_0_3_17: diff --git a/monitoring/prober/scd/test_operations_simple.py b/monitoring/prober/scd/test_operations_simple.py index aa78d6a04a..e7292875dd 100644 --- a/monitoring/prober/scd/test_operations_simple.py +++ b/monitoring/prober/scd/test_operations_simple.py @@ -12,6 +12,8 @@ import datetime from typing import Dict, Tuple +from monitoring.monitorlib.geo import Circle, Altitude +from monitoring.monitorlib.geotemporal import Volume4D, Time from monitoring.monitorlib.infrastructure import default_scope from monitoring.monitorlib import scd from monitoring.monitorlib.scd import SCOPE_SC @@ -41,7 +43,9 @@ def _make_op1_request(): time_end = time_start + datetime.timedelta(minutes=60) return { "extents": [ - scd.make_vol4(time_start, time_end, 0, 120, scd.make_circle(90, 0, 200)) + Volume4D.from_values( + time_start, time_end, 0, 120, Circle.from_meters(90, 0, 200) + ).to_f3548v21() ], "old_version": 0, "state": "Accepted", @@ -55,7 +59,9 @@ def _make_op2_request(): time_end = time_start + datetime.timedelta(minutes=60) return { "extents": [ - scd.make_vol4(time_start, time_end, 0, 120, scd.make_circle(89.999, 0, 200)) + Volume4D.from_values( + time_start, time_end, 0, 120, Circle.from_meters(89.999, 0, 200) + ).to_f3548v21() ], "old_version": 0, "state": "Accepted", @@ -129,9 +135,9 @@ def test_op1_does_not_exist_query_1(ids, scd_api, scd_session, scd_session2): resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - time_now, end_time, 0, 5000, scd.make_circle(89.999, 180, 300) - ) + "area_of_interest": Volume4D.from_values( + time_now, end_time, 0, 5000, Circle.from_meters(89.999, 180, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -153,9 +159,9 @@ def test_op1_does_not_exist_query_2(ids, scd_api, scd_session, scd_session2): resp = scd_session2.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - time_now, end_time, 0, 5000, scd.make_circle(89.999, 180, 300) - ) + "area_of_interest": Volume4D.from_values( + time_now, end_time, 0, 5000, Circle.from_meters(89.999, 180, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -256,9 +262,9 @@ def test_create_op2sub(ids, scd_api, scd_session, scd_session2): time_start = datetime.datetime.utcnow() time_end = time_start + datetime.timedelta(minutes=70) req = { - "extents": scd.make_vol4( - time_start, time_end, 0, 1000, scd.make_circle(89.999, 0, 250) - ), + "extents": Volume4D.from_values( + time_start, time_end, 0, 1000, Circle.from_meters(89.999, 0, 250) + ).to_f3548v21(), "uss_base_url": URL_SUB2, "notify_for_constraints": False, } @@ -367,9 +373,9 @@ def test_read_ops_from_uss1(ids, scd_api, scd_session, scd_session2): resp = scd_session.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - time_now, end_time, 0, 5000, scd.make_circle(89.999, 180, 300) - ) + "area_of_interest": Volume4D.from_values( + time_now, end_time, 0, 5000, Circle.from_meters(89.999, 180, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -399,9 +405,9 @@ def test_read_ops_from_uss2(ids, scd_api, scd_session, scd_session2): resp = scd_session2.post( "/operational_intent_references/query", json={ - "area_of_interest": scd.make_vol4( - time_now, end_time, 0, 5000, scd.make_circle(89.999, 180, 300) - ) + "area_of_interest": Volume4D.from_values( + time_now, end_time, 0, 5000, Circle.from_meters(89.999, 180, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -550,8 +556,8 @@ def test_mutate_sub2(ids, scd_api, scd_session, scd_session2): req["extents"] = req["extents"][0] del req["state"] req["notify_for_constraints"] = False - req["extents"]["time_start"] = scd.make_time(time_start) - req["extents"]["time_end"] = scd.make_time(time_end) + req["extents"]["time_start"] = Time(time_start).to_f3548v21() + req["extents"]["time_end"] = Time(time_end).to_f3548v21() req["notify_for_operational_intents"] = False resp = scd_session2.put("/subscriptions/{}".format(ids(SUB2_TYPE)), json=req) @@ -560,38 +566,38 @@ def test_mutate_sub2(ids, scd_api, scd_session, scd_session2): # Attempt mutation with start time that doesn't cover Op2 - req["extents"]["time_start"] = scd.make_time( + req["extents"]["time_start"] = Time( time_now + datetime.timedelta(minutes=5) - ) + ).to_f3548v21() resp = scd_session2.put( "/subscriptions/{}/{}".format(ids(SUB2_TYPE), sub2_version), json=req ) assert resp.status_code == 400, resp.content - req["extents"]["time_start"] = scd.make_time(time_start) + req["extents"]["time_start"] = Time(time_start).to_f3548v21() # Attempt mutation with end time that doesn't cover Op2 - req["extents"]["time_end"] = scd.make_time(time_now) + req["extents"]["time_end"] = Time(time_now).to_f3548v21() resp = scd_session2.put( "/subscriptions/{}/{}".format(ids(SUB2_TYPE), sub2_version), json=req ) assert resp.status_code == 400, resp.content - req["extents"]["time_end"] = scd.make_time(time_end) + req["extents"]["time_end"] = Time(time_end).to_f3548v21() # # Attempt mutation with minimum altitude that doesn't cover Op2 - req["extents"]["volume"]["altitude_lower"] = scd.make_altitude(10) + req["extents"]["volume"]["altitude_lower"] = Altitude.w84m(10) resp = scd_session2.put( "/subscriptions/{}/{}".format(ids(SUB2_TYPE), sub2_version), json=req ) assert resp.status_code == 400, resp.content - req["extents"]["volume"]["altitude_lower"] = scd.make_altitude(0) + req["extents"]["volume"]["altitude_lower"] = Altitude.w84m(0) # Attempt mutation with maximum altitude that doesn't cover Op2 - req["extents"]["volume"]["altitude_upper"] = scd.make_altitude(10) + req["extents"]["volume"]["altitude_upper"] = Altitude.w84m(10) resp = scd_session2.put( "/subscriptions/{}/{}".format(ids(SUB2_TYPE), sub2_version), json=req ) assert resp.status_code == 400, resp.content - req["extents"]["volume"]["altitude_upper"] = scd.make_altitude(200) + req["extents"]["volume"]["altitude_upper"] = Altitude.w84m(200) # # Attempt mutation with outline that doesn't cover Op2 old_lat = req["extents"]["volume"]["outline_circle"]["center"]["lat"] diff --git a/monitoring/prober/scd/test_subscription_queries.py b/monitoring/prober/scd/test_subscription_queries.py index a7a813047c..befa993e1d 100644 --- a/monitoring/prober/scd/test_subscription_queries.py +++ b/monitoring/prober/scd/test_subscription_queries.py @@ -6,6 +6,8 @@ import datetime +from monitoring.monitorlib.geo import latitude_degrees, Circle +from monitoring.monitorlib.geotemporal import Volume4D from monitoring.monitorlib.infrastructure import default_scope from monitoring.monitorlib import scd from monitoring.monitorlib.scd import SCOPE_SC @@ -29,11 +31,11 @@ def _make_sub1_req(scd_api): time_start = datetime.datetime.utcnow() time_end = time_start + datetime.timedelta(minutes=60) - lat = LAT0 - scd.latitude_degrees(FOOTPRINT_SPACING_M) + lat = LAT0 - latitude_degrees(FOOTPRINT_SPACING_M) req = { - "extents": scd.make_vol4( - None, time_end, 0, 300, scd.make_circle(lat, LNG0, 100) - ), + "extents": Volume4D.from_values( + None, time_end, 0, 300, Circle.from_meters(lat, LNG0, 100) + ).to_f3548v21(), "uss_base_url": "https://example.com/foo", "notify_for_constraints": False, } @@ -45,9 +47,9 @@ def _make_sub2_req(scd_api): time_start = datetime.datetime.utcnow() + datetime.timedelta(hours=2) time_end = time_start + datetime.timedelta(minutes=60) req = { - "extents": scd.make_vol4( - time_start, time_end, 350, 650, scd.make_circle(LAT0, LNG0, 100) - ), + "extents": Volume4D.from_values( + time_start, time_end, 350, 650, Circle.from_meters(LAT0, LNG0, 100) + ).to_f3548v21(), "old_version": 0, "uss_base_url": "https://example.com/foo", "notify_for_operations": True, @@ -60,11 +62,11 @@ def _make_sub2_req(scd_api): def _make_sub3_req(scd_api): time_start = datetime.datetime.utcnow() + datetime.timedelta(hours=4) time_end = time_start + datetime.timedelta(minutes=60) - lat = LAT0 + scd.latitude_degrees(FOOTPRINT_SPACING_M) + lat = LAT0 + latitude_degrees(FOOTPRINT_SPACING_M) req = { - "extents": scd.make_vol4( - time_start, time_end, 700, 1000, scd.make_circle(lat, LNG0, 100) - ), + "extents": Volume4D.from_values( + time_start, time_end, 700, 1000, Circle.from_meters(lat, LNG0, 100) + ).to_f3548v21(), "uss_base_url": "https://example.com/foo", "notify_for_constraints": False, } @@ -96,9 +98,9 @@ def test_subs_do_not_exist_query(ids, scd_api, scd_session): resp = scd_session.post( "/subscriptions/query", json={ - "area_of_interest": scd.make_vol4( - None, None, 0, 5000, scd.make_circle(LAT0, LNG0, FOOTPRINT_SPACING_M) - ) + "area_of_interest": Volume4D.from_values( + None, None, 0, 5000, Circle.from_meters(LAT0, LNG0, FOOTPRINT_SPACING_M) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -136,9 +138,9 @@ def test_search_find_all_subs(ids, scd_api, scd_session): resp = scd_session.post( "/subscriptions/query", json={ - "area_of_interest": scd.make_vol4( - None, None, 0, 3000, scd.make_circle(LAT0, LNG0, FOOTPRINT_SPACING_M) - ) + "area_of_interest": Volume4D.from_values( + None, None, 0, 3000, Circle.from_meters(LAT0, LNG0, FOOTPRINT_SPACING_M) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -152,14 +154,14 @@ def test_search_find_all_subs(ids, scd_api, scd_session): @for_api_versions(scd.API_0_3_17) @default_scope(SCOPE_SC) def test_search_footprint(ids, scd_api, scd_session): - lat = LAT0 - scd.latitude_degrees(FOOTPRINT_SPACING_M) + lat = LAT0 - latitude_degrees(FOOTPRINT_SPACING_M) print(lat) resp = scd_session.post( "/subscriptions/query", json={ - "area_of_interest": scd.make_vol4( - None, None, 0, 3000, scd.make_circle(lat, LNG0, 50) - ) + "area_of_interest": Volume4D.from_values( + None, None, 0, 3000, Circle.from_meters(lat, LNG0, 50) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -171,9 +173,9 @@ def test_search_footprint(ids, scd_api, scd_session): resp = scd_session.post( "/subscriptions/query", json={ - "area_of_interest": scd.make_vol4( - None, None, 0, 3000, scd.make_circle(LAT0, LNG0, 50) - ) + "area_of_interest": Volume4D.from_values( + None, None, 0, 3000, Circle.from_meters(LAT0, LNG0, 50) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -194,13 +196,13 @@ def test_search_time(ids, scd_api, scd_session): resp = scd_session.post( "/subscriptions/query", json={ - "area_of_interest": scd.make_vol4( + "area_of_interest": Volume4D.from_values( time_start, time_end, 0, 3000, - scd.make_circle(LAT0, LNG0, FOOTPRINT_SPACING_M), - ) + Circle.from_meters(LAT0, LNG0, FOOTPRINT_SPACING_M), + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -212,13 +214,13 @@ def test_search_time(ids, scd_api, scd_session): resp = scd_session.post( "/subscriptions/query", json={ - "area_of_interest": scd.make_vol4( + "area_of_interest": Volume4D.from_values( None, time_end, 0, 3000, - scd.make_circle(LAT0, LNG0, FOOTPRINT_SPACING_M), - ) + Circle.from_meters(LAT0, LNG0, FOOTPRINT_SPACING_M), + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -233,13 +235,13 @@ def test_search_time(ids, scd_api, scd_session): resp = scd_session.post( "/subscriptions/query", json={ - "area_of_interest": scd.make_vol4( + "area_of_interest": Volume4D.from_values( time_start, time_end, 0, 3000, - scd.make_circle(LAT0, LNG0, FOOTPRINT_SPACING_M), - ) + Circle.from_meters(LAT0, LNG0, FOOTPRINT_SPACING_M), + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -251,13 +253,13 @@ def test_search_time(ids, scd_api, scd_session): resp = scd_session.post( "/subscriptions/query", json={ - "area_of_interest": scd.make_vol4( + "area_of_interest": Volume4D.from_values( time_start, None, 0, 3000, - scd.make_circle(LAT0, LNG0, FOOTPRINT_SPACING_M), - ) + Circle.from_meters(LAT0, LNG0, FOOTPRINT_SPACING_M), + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -274,17 +276,17 @@ def test_search_time(ids, scd_api, scd_session): def test_search_time_footprint(ids, scd_api, scd_session): time_start = datetime.datetime.utcnow() time_end = time_start + datetime.timedelta(hours=2.5) - lat = LAT0 + scd.latitude_degrees(FOOTPRINT_SPACING_M) + lat = LAT0 + latitude_degrees(FOOTPRINT_SPACING_M) resp = scd_session.post( "/subscriptions/query", json={ - "area_of_interest": scd.make_vol4( + "area_of_interest": Volume4D.from_values( time_start, time_end, 0, 3000, - scd.make_circle(lat, LNG0, FOOTPRINT_SPACING_M), - ) + Circle.from_meters(lat, LNG0, FOOTPRINT_SPACING_M), + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content diff --git a/monitoring/prober/scd/test_subscription_query_time.py b/monitoring/prober/scd/test_subscription_query_time.py index 599ced0f3f..695155a826 100644 --- a/monitoring/prober/scd/test_subscription_query_time.py +++ b/monitoring/prober/scd/test_subscription_query_time.py @@ -5,6 +5,8 @@ import datetime +from monitoring.monitorlib.geo import Circle +from monitoring.monitorlib.geotemporal import Volume4D from monitoring.monitorlib.infrastructure import default_scope from monitoring.monitorlib import scd from monitoring.monitorlib.scd import SCOPE_SC @@ -17,9 +19,13 @@ def _make_sub_req(time_start, time_end, alt_start, alt_end, radius, scd_api): req = { - "extents": scd.make_vol4( - time_start, time_end, alt_start, alt_end, scd.make_circle(-56, 178, radius) - ), + "extents": Volume4D.from_values( + time_start, + time_end, + alt_start, + alt_end, + Circle.from_meters(-56, 178, radius), + ).to_f3548v21(), "old_version": 0, "uss_base_url": BASE_URL, "notify_for_constraints": False, diff --git a/monitoring/prober/scd/test_subscription_simple.py b/monitoring/prober/scd/test_subscription_simple.py index 56e8fe2c31..4e1c80fafe 100644 --- a/monitoring/prober/scd/test_subscription_simple.py +++ b/monitoring/prober/scd/test_subscription_simple.py @@ -12,6 +12,10 @@ import datetime +from monitoring.monitorlib.geo import Circle +from monitoring.monitorlib.geotemporal import Volume4D +from uas_standards.astm.f3548.v21 import api + from monitoring.monitorlib.infrastructure import default_scope from monitoring.monitorlib import scd from monitoring.monitorlib.scd import SCOPE_SC @@ -32,9 +36,9 @@ def _make_sub1_req(scd_api): time_start = datetime.datetime.utcnow() time_end = time_start + datetime.timedelta(minutes=60) req = { - "extents": scd.make_vol4( - time_start, time_end, 0, 1000, scd.make_circle(12, -34, 300) - ), + "extents": Volume4D.from_values( + time_start, time_end, 0, 1000, Circle.from_meters(12, -34, 300) + ).to_f3548v21(), "uss_base_url": "https://example.com/foo", "notify_for_constraints": False, } @@ -48,8 +52,8 @@ def _check_sub1(data, sub_id, scd_api): data["subscription"]["notification_index"] == 0 ) assert data["subscription"]["uss_base_url"] == "https://example.com/foo" - assert data["subscription"]["time_start"]["format"] == scd.TIME_FORMAT_CODE - assert data["subscription"]["time_end"]["format"] == scd.TIME_FORMAT_CODE + assert data["subscription"]["time_start"]["format"] == api.TimeFormat.RFC3339 + assert data["subscription"]["time_end"]["format"] == api.TimeFormat.RFC3339 assert ("notify_for_constraints" not in data["subscription"]) or ( data["subscription"]["notify_for_constraints"] == False ) @@ -79,9 +83,9 @@ def test_sub_does_not_exist_query(ids, scd_api, scd_session): resp = scd_session.post( "/subscriptions/query", json={ - "area_of_interest": scd.make_vol4( - None, None, 0, 5000, scd.make_circle(12, -34, 300) - ) + "area_of_interest": Volume4D.from_values( + None, None, 0, 5000, Circle.from_meters(12, -34, 300) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content @@ -133,9 +137,9 @@ def test_get_sub_by_search(ids, scd_api, scd_session): resp = scd_session.post( "/subscriptions/query", json={ - "area_of_interest": scd.make_vol4( - time_now, time_now, 0, 120, scd.make_circle(12.00001, -34.00001, 50) - ) + "area_of_interest": Volume4D.from_values( + time_now, time_now, 0, 120, Circle.from_meters(12.00001, -34.00001, 50) + ).to_f3548v21() }, ) if resp.status_code != 200: @@ -217,9 +221,9 @@ def test_get_deleted_sub_by_search(ids, scd_api, scd_session): resp = scd_session.post( "/subscriptions/query", json={ - "area_of_interest": scd.make_vol4( - time_now, time_now, 0, 120, scd.make_circle(12.00001, -34.00001, 50) - ) + "area_of_interest": Volume4D.from_values( + time_now, time_now, 0, 120, Circle.from_meters(12.00001, -34.00001, 50) + ).to_f3548v21() }, ) assert resp.status_code == 200, resp.content diff --git a/monitoring/prober/scd/test_subscription_update_validation.py b/monitoring/prober/scd/test_subscription_update_validation.py index 55fc82d4af..964c8bd156 100644 --- a/monitoring/prober/scd/test_subscription_update_validation.py +++ b/monitoring/prober/scd/test_subscription_update_validation.py @@ -14,6 +14,8 @@ import datetime from monitoring.monitorlib import scd +from monitoring.monitorlib.geo import Circle +from monitoring.monitorlib.geotemporal import Volume4D from monitoring.monitorlib.infrastructure import default_scope from monitoring.monitorlib.scd import SCOPE_SC from monitoring.monitorlib.testing import assert_datetimes_are_equal @@ -35,7 +37,9 @@ def _make_op_req(): time_end = time_start + datetime.timedelta(minutes=60) return { "extents": [ - scd.make_vol4(time_start, time_end, 0, 1000, scd.make_circle(-56, 178, 500)) + Volume4D.from_values( + time_start, time_end, 0, 1000, Circle.from_meters(-56, 178, 500) + ).to_f3548v21() ], "old_version": 0, "state": "Accepted", @@ -46,9 +50,13 @@ def _make_op_req(): def _make_sub_req(time_start, time_end, alt_start, alt_end, radius, scd_api): req = { - "extents": scd.make_vol4( - time_start, time_end, alt_start, alt_end, scd.make_circle(-56, 178, radius) - ), + "extents": Volume4D.from_values( + time_start, + time_end, + alt_start, + alt_end, + Circle.from_meters(-56, 178, radius), + ).to_f3548v21(), "old_version": 0, "uss_base_url": BASE_URL, "notify_for_constraints": False, diff --git a/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py b/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py index a9e8b923ad..ed937b0694 100644 --- a/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py +++ b/monitoring/uss_qualifier/resources/astm/f3548/v21/dss.py @@ -1,21 +1,20 @@ -from datetime import datetime from typing import Tuple, List from urllib.parse import urlparse from implicitdict import ImplicitDict from monitoring.monitorlib import infrastructure, fetch -from monitoring.monitorlib.scd import ( - Volume4D, +from monitoring.monitorlib.scd import SCOPE_SC +from monitoring.uss_qualifier.resources.resource import Resource +from monitoring.uss_qualifier.resources.communications import AuthAdapterResource +from uas_standards.astm.f3548.v21.api import ( QueryOperationalIntentReferenceParameters, - SCOPE_SC, - QueryOperationalIntentReferenceResponse, + Volume4D, OperationalIntentReference, + QueryOperationalIntentReferenceResponse, OperationalIntent, GetOperationalIntentDetailsResponse, ) -from monitoring.uss_qualifier.resources.resource import Resource -from monitoring.uss_qualifier.resources.communications import AuthAdapterResource class DSSInstanceSpecification(ImplicitDict): diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/evaluation.py b/monitoring/uss_qualifier/scenarios/astm/utm/evaluation.py index b4d9325d91..a4a2afc85d 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/evaluation.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/evaluation.py @@ -1,8 +1,9 @@ from datetime import timedelta from typing import Optional, List -from monitoring.monitorlib import scd -from monitoring.monitorlib.scd import OperationalIntentDetails, Volume4D +from monitoring.monitorlib.geotemporal import Volume4DCollection +from uas_standards.astm.f3548.v21.api import OperationalIntentDetails, Volume4D + NUMERIC_PRECISION = 0.001 @@ -24,9 +25,10 @@ def validate_op_intent_details( # Check that the USS is providing reasonable volume 4D resp_vol4s = op_intent_details.volumes + op_intent_details.off_nominal_volumes - resp_alts = scd.meter_altitude_bounds_of(resp_vol4s) - resp_start = scd.start_of(resp_vol4s) - resp_end = scd.end_of(resp_vol4s) + vol4c = Volume4DCollection.from_f3548v21(resp_vol4s) + resp_alts = vol4c.meter_altitude_bounds + resp_start = vol4c.time_start.datetime + resp_end = vol4c.time_end.datetime if resp_alts[0] > expected_extent.volume.altitude_lower.value + NUMERIC_PRECISION: errors_text.append( "Lower altitude specified by USS in operational intent details ({} m WGS84) is above the lower altitude in the injected flight ({} m WGS84)".format( diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py b/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py index dc018aac97..40bbe66d17 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py @@ -1,7 +1,7 @@ +from monitoring.monitorlib.geotemporal import Volume4DCollection from uas_standards.astm.f3548.v21.api import OperationalIntentState from uas_standards.astm.f3548.v21.constants import OiMaxPlanHorizonDays -from monitoring.monitorlib import scd from monitoring.monitorlib.scd_automated_testing.scd_injection_api import ( Capability, InjectFlightResult, @@ -105,9 +105,9 @@ def __init__( ), "valid_conflict_tiny_overlap must have state Accepted" time_delta = ( - scd.start_of( + Volume4DCollection.from_f3548v21( self.invalid_too_far_away.request.operational_intent.volumes - ) + ).time_start.datetime - self.invalid_too_far_away.reference_time.datetime ) assert ( @@ -127,9 +127,12 @@ def __init__( > 0 ), "invalid_activated_offnominal must have at least one off-nominal volume" - assert scd.vol4s_intersect( - self.valid_flight.request.operational_intent.volumes, - self.valid_conflict_tiny_overlap.request.operational_intent.volumes, + assert Volume4DCollection.from_f3548v21( + self.valid_flight.request.operational_intent.volumes + ).intersects_vol4s( + Volume4DCollection.from_f3548v21( + self.valid_conflict_tiny_overlap.request.operational_intent.volumes + ) ), "valid_flight and valid_conflict_tiny_overlap must intersect" except KeyError as e: diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py index c108218b86..4b19205347 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.py @@ -1,8 +1,8 @@ from typing import Optional +from monitoring.monitorlib.geotemporal import Volume4DCollection from uas_standards.astm.f3548.v21.api import OperationalIntentState -from monitoring.monitorlib import scd from monitoring.monitorlib.scd_automated_testing.scd_injection_api import ( Capability, InjectFlightResult, @@ -141,21 +141,33 @@ def __init__( self.flight_2_equal_prio_planned_time_range_B.request.operational_intent.priority == self.flight_1_planned_time_range_A.request.operational_intent.priority ), "flight_2 must have priority equal to flight_1" - assert not scd.vol4s_intersect( - self.flight_1_planned_time_range_A.request.operational_intent.volumes, - self.flight_2_equal_prio_planned_time_range_B.request.operational_intent.volumes, + assert not Volume4DCollection.from_f3548v21( + self.flight_1_planned_time_range_A.request.operational_intent.volumes + ).intersects_vol4s( + Volume4DCollection.from_f3548v21( + self.flight_2_equal_prio_planned_time_range_B.request.operational_intent.volumes + ) ), "flight_1_planned_time_range_A and flight_2_equal_prio_planned_time_range_B must not intersect" - assert not scd.vol4s_intersect( - self.flight_1_planned_time_range_A.request.operational_intent.volumes, - self.flight_1_activated_time_range_B.request.operational_intent.volumes, + assert not Volume4DCollection.from_f3548v21( + self.flight_1_planned_time_range_A.request.operational_intent.volumes + ).intersects_vol4s( + Volume4DCollection.from_f3548v21( + self.flight_1_activated_time_range_B.request.operational_intent.volumes + ) ), "flight_1_planned_time_range_A and flight_1_activated_time_range_B must not intersect" - assert scd.vol4s_intersect( - self.flight_1_activated_time_range_B.request.operational_intent.volumes, - self.flight_2_equal_prio_activated_time_range_B.request.operational_intent.volumes, + assert Volume4DCollection.from_f3548v21( + self.flight_1_activated_time_range_B.request.operational_intent.volumes + ).intersects_vol4s( + Volume4DCollection.from_f3548v21( + self.flight_2_equal_prio_activated_time_range_B.request.operational_intent.volumes + ) ), "flight_1_activated_time_range_B and flight_2_equal_prio_activated_time_range_B must intersect" - assert scd.vol4s_intersect( - self.flight_1_activated_time_range_A.request.operational_intent.volumes, - self.flight_2_equal_prio_nonconforming_time_range_A.request.operational_intent.off_nominal_volumes, + assert Volume4DCollection.from_f3548v21( + self.flight_1_activated_time_range_A.request.operational_intent.volumes + ).intersects_vol4s( + Volume4DCollection.from_f3548v21( + self.flight_2_equal_prio_nonconforming_time_range_A.request.operational_intent.off_nominal_volumes + ) ), "flight_1_activated_time_range_A.volumes and flight_2_equal_prio_nonconforming_time_range_A.off_nominal_volumes must intersect" assert ( diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.py b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.py index cba0d78edc..cdac5197d7 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.py @@ -1,8 +1,8 @@ from typing import Optional +from monitoring.monitorlib.geotemporal import Volume4DCollection from uas_standards.astm.f3548.v21.api import OperationalIntentState -from monitoring.monitorlib import scd from monitoring.monitorlib.scd_automated_testing.scd_injection_api import ( Capability, ) @@ -133,17 +133,26 @@ def __init__( self.flight_2_planned_time_range_A.request.operational_intent.priority > self.flight_1_planned_time_range_A.request.operational_intent.priority ), "flight_2 must have higher priority than flight_1" - assert scd.vol4s_intersect( - self.flight_1_planned_time_range_A.request.operational_intent.volumes, - self.flight_2_planned_time_range_A.request.operational_intent.volumes, + assert Volume4DCollection.from_f3548v21( + self.flight_1_planned_time_range_A.request.operational_intent.volumes + ).intersects_vol4s( + Volume4DCollection.from_f3548v21( + self.flight_2_planned_time_range_A.request.operational_intent.volumes + ) ), "flight_1_planned_time_range_A and flight_2_planned_time_range_A must intersect" - assert scd.vol4s_intersect( - self.flight_1_planned_time_range_A.request.operational_intent.volumes, - self.flight_1_planned_time_range_A_extended.request.operational_intent.volumes, + assert Volume4DCollection.from_f3548v21( + self.flight_1_planned_time_range_A.request.operational_intent.volumes + ).intersects_vol4s( + Volume4DCollection.from_f3548v21( + self.flight_1_planned_time_range_A_extended.request.operational_intent.volumes + ) ), "flight_1_planned_time_range_A and flight_1_planned_time_range_A_extended must intersect" - assert not scd.vol4s_intersect( - self.flight_1_planned_time_range_A.request.operational_intent.volumes, - self.flight_1_activated_time_range_B.request.operational_intent.volumes, + assert not Volume4DCollection.from_f3548v21( + self.flight_1_planned_time_range_A.request.operational_intent.volumes + ).intersects_vol4s( + Volume4DCollection.from_f3548v21( + self.flight_1_activated_time_range_B.request.operational_intent.volumes + ) ), "flight_1_planned_time_range_A and flight_1_activated_time_range_B must not intersect" except KeyError as e: diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py b/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py index 3e7eeeeaf2..c2cee3e5e3 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/test_steps.py @@ -1,13 +1,13 @@ from typing import List from monitoring.monitorlib import schema_validation, fetch -from uas_standards.astm.f3548.v21.api import OperationalIntentState - -from monitoring.monitorlib.scd import ( - bounding_vol4, - OperationalIntentReference, +from monitoring.monitorlib.geotemporal import Volume4DCollection +from uas_standards.astm.f3548.v21.api import ( + OperationalIntentState, Volume4D, + OperationalIntentReference, ) + from monitoring.monitorlib.scd_automated_testing.scd_injection_api import ( InjectFlightRequest, ) @@ -53,10 +53,10 @@ def __init__( self._dss = dss self._test_step = test_step - self._flight_intent_extent = bounding_vol4( + self._flight_intent_extent = Volume4DCollection.from_f3548v21( flight_intent.operational_intent.volumes + flight_intent.operational_intent.off_nominal_volumes - ) + ).bounding_volume.to_f3548v21() def __enter__(self): self._initial_op_intent_refs, self._initial_query = self._dss.find_op_intent( @@ -112,10 +112,10 @@ def validate_shared_operational_intent( operational intent was not found and skip_if_not_found was True. """ scenario.begin_test_step(test_step) - extent = bounding_vol4( + extent = Volume4DCollection.from_f3548v21( flight_intent.operational_intent.volumes + flight_intent.operational_intent.off_nominal_volumes - ) + ).bounding_volume.to_f3548v21() op_intent_refs, query = dss.find_op_intent(extent) scenario.record_query(query) with scenario.check("DSS response", [dss.participant_id]) as check: diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py b/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py index 9bf85ad654..45465af3ae 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py +++ b/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py @@ -2,10 +2,10 @@ from datetime import datetime from typing import List, Union, Optional, Tuple, Iterable, Set, Dict +from monitoring.monitorlib.geotemporal import Volume4DCollection from uas_standards.astm.f3548.v21.api import OperationalIntentState from monitoring.monitorlib.fetch import QueryError -from monitoring.monitorlib.scd import bounding_vol4 from monitoring.monitorlib.scd_automated_testing.scd_injection_api import ( InjectFlightRequest, Capability, @@ -48,7 +48,7 @@ def clear_area( for flight_intent in flight_intents: volumes += flight_intent.request.operational_intent.volumes volumes += flight_intent.request.operational_intent.off_nominal_volumes - extent = bounding_vol4(volumes) + extent = Volume4DCollection.from_f3548v21(volumes).bounding_volume.to_f3548v21() for uss in flight_planners: with scenario.check("Area cleared successfully", [uss.participant_id]) as check: try: