diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index f5950f3aca..f32352e3d0 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -5290,14 +5290,6 @@ "endColumn": 44, "lineCount": 1 } - }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 38, - "endColumn": 69, - "lineCount": 1 - } } ], "./monitoring/uss_qualifier/resources/flight_planning/flight_planners.py": [ @@ -19113,14 +19105,6 @@ "lineCount": 1 } }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 44, - "endColumn": 58, - "lineCount": 1 - } - }, { "code": "reportCallIssue", "range": { diff --git a/monitoring/uss_qualifier/resources/README.md b/monitoring/uss_qualifier/resources/README.md index 620e4d85af..e53d100d7a 100644 --- a/monitoring/uss_qualifier/resources/README.md +++ b/monitoring/uss_qualifier/resources/README.md @@ -31,3 +31,15 @@ Resources for a given test configuration are all declared in a single global res * Example: `netrid.flight_data.nominal_flights`, which would provide flight data for nominal flights which could be injected into Service Providers under test. 2. Every type of test resource must define a "resource specification", which is a serializable data type that fully defines how to create an instance of that resource type. 3. Every type of test resource must define how to create an instance of the test resource from an instance of the resource specification. + + +## Resource modifiers + +A `ResourceModifier` is a resource that wraps another resource and produces variants of it based on an integer index. This is useful when a test scenario needs multiple unique-but-related instances of a resource (e.g., distinct flights derived from a single base flight). + +To use a `ResourceModifier`: + +1. Declare it like any other resource, with its `base_resource` dependency pointing to the resource to be modified. +2. When need, call `adjust(index)` to obtain a modified copy of the base resource. Different `index` values produce different (unique) variants; the same `index` produces equivalent results. + +The base resource itself remains available as `base_resource` on the modifier. diff --git a/monitoring/uss_qualifier/resources/dev/__init__.py b/monitoring/uss_qualifier/resources/dev/__init__.py index 486e66e1f5..cbe1a75734 100644 --- a/monitoring/uss_qualifier/resources/dev/__init__.py +++ b/monitoring/uss_qualifier/resources/dev/__init__.py @@ -1,2 +1,6 @@ from .noop import NoOpResource as NoOpResource from .test_exclusions import TestExclusionsResource as TestExclusionsResource +from .test_modifier import TestModifierModifierResource as TestModifierModifierResource +from .test_modifier import TestModifierResource as TestModifierResource +from .test_modifier import TestSquareModifier as TestSquareModifier +from .test_modifier import TestSquareResource as TestSquareResource diff --git a/monitoring/uss_qualifier/resources/dev/test_modifier.py b/monitoring/uss_qualifier/resources/dev/test_modifier.py new file mode 100644 index 0000000000..64c880dc15 --- /dev/null +++ b/monitoring/uss_qualifier/resources/dev/test_modifier.py @@ -0,0 +1,104 @@ +from implicitdict import ImplicitDict + +from monitoring.monitorlib.geo import LatLngBoundingBox +from monitoring.uss_qualifier.resources.modifiers import ( + GeospatialModifier, + GeospatialResource, +) +from monitoring.uss_qualifier.resources.resource import Resource, ResourceModifier + + +class TestModifierSpecification(ImplicitDict): + base_id: int + + +class TestModifierResource(Resource[TestModifierSpecification]): + """TestModifierResource is a simple resource returing 10 number, starting from base_id. Used for unit tests.""" + + _spec: TestModifierSpecification + + def __init__( + self, + specification: TestModifierSpecification, + resource_origin: str, + ): + super().__init__(specification, resource_origin) + self._spec = specification + + def build_ids(self) -> list[int]: + return list(range(self._spec.base_id, self._spec.base_id + 10)) + + +class TestModifierModifierSpecification(ImplicitDict): + shift_interval: int + + +class TestModifierModifierResource( + ResourceModifier[TestModifierModifierSpecification, TestModifierResource] +): + """Modifier for a TestModifierResource. Used for unit tests.""" + + def adjust(self, index: int) -> TestModifierResource: + + # 'Clone' the resource with new specs + return TestModifierResource( + TestModifierSpecification( + base_id=self.base_resource._spec.base_id + + self._spec.shift_interval * index, + ), + resource_origin=self.base_resource.resource_origin, + ) + + +class TestSquareSpecification(ImplicitDict): + lat_center: float + lng_center: float + + +class TestSquareResource(Resource[TestSquareSpecification], GeospatialResource): + """1km x 1km square centered at (lat_center, lng_center). Used for unit tests.""" + + SQUARE_SIDE_M = 1000.0 + + _spec: TestSquareSpecification + + def __init__( + self, + specification: TestSquareSpecification, + resource_origin: str, + ): + super().__init__(specification, resource_origin) + self._spec = specification + + def get_extents(self) -> LatLngBoundingBox: + point = LatLngBoundingBox( + lat_min=self._spec.lat_center, + lat_max=self._spec.lat_center, + lng_min=self._spec.lng_center, + lng_max=self._spec.lng_center, + ) + return point.expand( + north_meters=self.SQUARE_SIDE_M / 2, + east_meters=self.SQUARE_SIDE_M / 2, + south_meters=self.SQUARE_SIDE_M / 2, + west_meters=self.SQUARE_SIDE_M / 2, + ) + + def move(self, meters_east: float, meters_north: float) -> "TestSquareResource": + shifted = self.get_extents().expand( + north_meters=meters_north, + east_meters=meters_east, + south_meters=-meters_north, + west_meters=-meters_east, + ) + return TestSquareResource( + TestSquareSpecification( + lat_center=(shifted.lat_min + shifted.lat_max) / 2, + lng_center=(shifted.lng_min + shifted.lng_max) / 2, + ), + resource_origin=self.resource_origin, + ) + + +class TestSquareModifier(GeospatialModifier[TestSquareResource]): + pass diff --git a/monitoring/uss_qualifier/resources/flight_planning/__init__.py b/monitoring/uss_qualifier/resources/flight_planning/__init__.py index 35397e9f6c..635d7efc71 100644 --- a/monitoring/uss_qualifier/resources/flight_planning/__init__.py +++ b/monitoring/uss_qualifier/resources/flight_planning/__init__.py @@ -1,3 +1,4 @@ +from .flight_intents_resource import FlightIntentsModifier as FlightIntentsModifier from .flight_intents_resource import FlightIntentsResource as FlightIntentsResource from .flight_planners import ( FlightPlannerCombinationSelectorResource as FlightPlannerCombinationSelectorResource, diff --git a/monitoring/uss_qualifier/resources/flight_planning/flight_intents_resource.py b/monitoring/uss_qualifier/resources/flight_planning/flight_intents_resource.py index e4189a1109..580217fee8 100644 --- a/monitoring/uss_qualifier/resources/flight_planning/flight_intents_resource.py +++ b/monitoring/uss_qualifier/resources/flight_planning/flight_intents_resource.py @@ -1,22 +1,38 @@ +import json +import math + +import s2sphere from implicitdict import ImplicitDict from monitoring.monitorlib.clients.flight_planning.flight_info_template import ( FlightInfoTemplate, ) +from monitoring.monitorlib.geo import EARTH_CIRCUMFERENCE_M, LatLngBoundingBox +from monitoring.monitorlib.geotemporal import Volume4D +from monitoring.monitorlib.transformations import ( + RelativeTranslation, + Transformation, +) from monitoring.uss_qualifier.resources.files import load_dict from monitoring.uss_qualifier.resources.flight_planning.flight_intent import ( FlightIntentCollection, FlightIntentID, FlightIntentsSpecification, ) +from monitoring.uss_qualifier.resources.modifiers import ( + GeospatialModifier, + GeospatialResource, +) from monitoring.uss_qualifier.resources.resource import Resource -class FlightIntentsResource(Resource[FlightIntentsSpecification]): +class FlightIntentsResource(Resource[FlightIntentsSpecification], GeospatialResource): + _spec: FlightIntentsSpecification _intent_collection: FlightIntentCollection def __init__(self, specification: FlightIntentsSpecification, resource_origin: str): super().__init__(specification, resource_origin) + self._spec = specification has_file = "file" in specification and specification.file has_literal = ( "intent_collection" in specification and specification.intent_collection @@ -34,17 +50,70 @@ def __init__(self, specification: FlightIntentsSpecification, resource_origin: s load_dict(specification.file), FlightIntentCollection ) elif has_literal: - self._intent_collection = specification.intent_collection + self._intent_collection = ImplicitDict.parse( + json.loads( + json.dumps(specification.intent_collection) + ), # NB: We need a copy to avoid sharing '_intent_collection' between instances + FlightIntentCollection, + ) if "transformations" in specification and specification.transformations: if ( "transformations" in self._intent_collection and self._intent_collection.transformations ): self._intent_collection.transformations.extend( - specification.transformations + specification.transformations[::] ) else: - self._intent_collection.transformations = specification.transformations + self._intent_collection.transformations = specification.transformations[ # NB: We do a copy to be independent between instances + :: + ] def get_flight_intents(self) -> dict[FlightIntentID, FlightInfoTemplate]: return self._intent_collection.resolve() + + def get_extents(self) -> LatLngBoundingBox: + rect = s2sphere.LatLngRect.empty() + for template in self.get_flight_intents().values(): + transformations = ( + template.transformations + if "transformations" in template and template.transformations + else [] + ) + for vt in template.basic_information.area: + v4d = Volume4D(volume=vt.resolve_3d()) + for transformation in transformations: + v4d = v4d.transform(transformation) + rect = rect.union(v4d.rect_bounds) + return LatLngBoundingBox.from_latlng_rect(rect) + + def move(self, meters_east: float, meters_north: float) -> "FlightIntentsResource": + new_spec = FlightIntentsSpecification(self._spec) + + # Apply the translation as degrees, not meters. RelativeTranslation in + # meters is converted per-polygon using each polygon's vertex_average as + # the tangent-plane origin, which yields slightly different absolute + # offsets for different polygons. That sub-meter drift is enough to break + # pre-existing intent overlaps (e.g. "tiny_overlap" conflicts). Converting + # meters → degrees here using the resource's overall extents produces a + # rigid lat/lng shift applied identically to every vertex. + extents = self.get_extents() + lat0 = (extents.lat_min + extents.lat_max) / 2 + longitude_length = EARTH_CIRCUMFERENCE_M * math.cos(math.radians(lat0)) + + transformation = Transformation( + relative_translation=RelativeTranslation( + degrees_east=meters_east * 360 / longitude_length, + degrees_north=meters_north * 360 / EARTH_CIRCUMFERENCE_M, + ) + ) + + if "transformations" in new_spec and new_spec.transformations: + new_spec.transformations = new_spec.transformations + [transformation] + else: + new_spec.transformations = [transformation] + return FlightIntentsResource(new_spec, resource_origin=self.resource_origin) + + +class FlightIntentsModifier(GeospatialModifier[FlightIntentsResource]): + pass diff --git a/monitoring/uss_qualifier/resources/flight_planning/flight_intents_resource_test.py b/monitoring/uss_qualifier/resources/flight_planning/flight_intents_resource_test.py new file mode 100644 index 0000000000..41c415577b --- /dev/null +++ b/monitoring/uss_qualifier/resources/flight_planning/flight_intents_resource_test.py @@ -0,0 +1,60 @@ +import unittest + +from monitoring.monitorlib.geo import area_of_latlngrect +from monitoring.uss_qualifier.resources.definitions import ( + ResourceDeclaration, + ResourceID, +) +from monitoring.uss_qualifier.resources.modifiers import ( + GeospatialModifierSpecification, +) +from monitoring.uss_qualifier.resources.resource import ( + create_resources, +) + + +class TestFlightIntentsGeospatialModifier(unittest.TestCase): + def _build_declarations(self) -> dict[ResourceID, ResourceDeclaration]: + return { + "flight_intents": ResourceDeclaration( + resource_type="resources.flight_planning.FlightIntentsResource", + specification={ + "file": { + "path": "file://./test_data/che/flight_intents/general_flight_auth_flights.yaml", + }, + }, + ), + "flight_intents_modifier": ResourceDeclaration( + resource_type="resources.flight_planning.FlightIntentsModifier", + specification=GeospatialModifierSpecification(), + dependencies={ + "base_resource": "flight_intents", + }, + ), + } + + def test_overlap_only_for_same_index(self): + resources = create_resources(self._build_declarations(), "test", True) + modifier = resources["flight_intents_modifier"] + + extents = [modifier.adjust(i).get_extents() for i in range(11)] + base_area = area_of_latlngrect(extents[0].to_latlngrect()) + + for i in range(11): + for j in range(11): + overlap = area_of_latlngrect( + extents[i].to_latlngrect().intersection(extents[j].to_latlngrect()) + ) + if i == j: + assert ( + overlap > 0.99 * base_area + ), ( # Use 99% to compensate for errors + f"index {i}: self-overlap area {overlap:.2f}m² " + f"expected ~{base_area:.2f}m²" + ) + else: + assert ( + overlap < 0.01 * base_area + ), ( # Use 1% to compensate for errors + f"indices {i},{j}: unexpected overlap area {overlap:.2f}m²" + ) diff --git a/monitoring/uss_qualifier/resources/modifiers.py b/monitoring/uss_qualifier/resources/modifiers.py new file mode 100644 index 0000000000..246e6baf4c --- /dev/null +++ b/monitoring/uss_qualifier/resources/modifiers.py @@ -0,0 +1,45 @@ +from abc import ABC, abstractmethod +from math import isqrt +from typing import Self + +from implicitdict import ImplicitDict + +from monitoring.monitorlib.geo import LatLngBoundingBox, flatten +from monitoring.uss_qualifier.resources.resource import ResourceModifier + + +class GeospatialResource(ABC): + @abstractmethod + def get_extents(self) -> LatLngBoundingBox: + pass + + @abstractmethod + def move(self, meters_east: float, meters_north: float) -> Self: + pass + + +class GeospatialModifierSpecification(ImplicitDict): + meters_east_margin: float = 1000 + meters_north_margin: float = 1000 + + +class GeospatialModifier[GeospatialResourceType: GeospatialResource]( + ResourceModifier[GeospatialModifierSpecification, GeospatialResourceType] +): + def adjust(self, index: int) -> GeospatialResourceType: + # Make a grid based on index: + # x -> + # y 0 1 3 6 + # | 2 4 7 + # v 5 8 + # 9 + k = (isqrt(1 + 8 * index) - 1) // 2 + offset = index - k * (k + 1) // 2 + x = k - offset + y = offset + + rect = self.base_resource.get_extents().to_latlngrect() + width_m, height_m = flatten(rect.lo(), rect.hi()) + width_m += self._spec.meters_east_margin + height_m += self._spec.meters_north_margin + return self.base_resource.move(x * width_m, y * height_m) diff --git a/monitoring/uss_qualifier/resources/modifiers_test.py b/monitoring/uss_qualifier/resources/modifiers_test.py new file mode 100644 index 0000000000..667026e17f --- /dev/null +++ b/monitoring/uss_qualifier/resources/modifiers_test.py @@ -0,0 +1,59 @@ +import unittest + +from monitoring.monitorlib.geo import area_of_latlngrect +from monitoring.uss_qualifier.resources.definitions import ( + ResourceDeclaration, + ResourceID, +) +from monitoring.uss_qualifier.resources.dev.test_modifier import ( + TestSquareSpecification, +) +from monitoring.uss_qualifier.resources.modifiers import ( + GeospatialModifierSpecification, +) +from monitoring.uss_qualifier.resources.resource import create_resources + + +class TestGeospatialModifier(unittest.TestCase): + def _build_declarations(self) -> dict[ResourceID, ResourceDeclaration]: + return { + "square": ResourceDeclaration( + resource_type="resources.dev.TestSquareResource", + specification=TestSquareSpecification(lat_center=46.5, lng_center=6.5), + ), + "square_modifier": ResourceDeclaration( + resource_type="resources.dev.TestSquareModifier", + specification=GeospatialModifierSpecification(), + dependencies={ + "base_resource": "square", + }, + ), + } + + def test_overlap_only_for_same_index(self): + resources = create_resources(self._build_declarations(), "test", True) + modifier = resources["square_modifier"] + + extents = [modifier.adjust(i).get_extents() for i in range(11)] + square_area = ( + resources["square"].SQUARE_SIDE_M * resources["square"].SQUARE_SIDE_M + ) + + for i in range(11): + for j in range(11): + rect_i = extents[i].to_latlngrect() + rect_j = extents[j].to_latlngrect() + overlap = area_of_latlngrect(rect_i.intersection(rect_j)) + if i == j: + assert ( + overlap > 0.99 * square_area + ), ( # Use 99% to compensate for errors + f"index {i}: self-overlap area {overlap:.2f}m² " + f"expected ~{square_area:.2f}m²" + ) + else: + assert ( + overlap < 0.01 * square_area + ), ( # Use 1% to compensate for errors + f"indices {i},{j}: unexpected overlap area {overlap:.2f}m²" + ) diff --git a/monitoring/uss_qualifier/resources/resource.py b/monitoring/uss_qualifier/resources/resource.py index 1797ca1638..a9be8aa4bc 100644 --- a/monitoring/uss_qualifier/resources/resource.py +++ b/monitoring/uss_qualifier/resources/resource.py @@ -1,5 +1,5 @@ -from abc import ABC -from typing import TypeVar, get_type_hints +from abc import ABC, abstractmethod +from typing import TypeVar, get_args, get_origin, get_type_hints from implicitdict import ImplicitDict from loguru import logger @@ -43,6 +43,36 @@ def is_type(self, resource_type: str) -> bool: ResourceType = TypeVar("ResourceType", bound=Resource) +class ResourceModifier[SpecificationType: ImplicitDict, ResourceType]( + Resource[SpecificationType], ABC +): + """A specifc type of resources that can return adjusted an resource that shall unique based on a specifc 'index'. + The underlying resource shall be a dependency named 'base_resource'. + + Concrete subclass must implement 'adjust' as needed. + """ + + _spec: SpecificationType + base_resource: ResourceType + + def __init__( + self, + specification: SpecificationType, + resource_origin: str, + base_resource: ResourceType, + ): + super().__init__(specification, resource_origin) + self._spec = specification + self.base_resource = base_resource + + @abstractmethod + def adjust(self, index: int) -> ResourceType: + """ + Return a new instance of the base resource, modified to be unique based on 'index' value. + """ + pass + + class MissingResourceError(ValueError): missing_resource_name: str @@ -156,6 +186,26 @@ def get_resource_types( ) constructor_signature = get_type_hints(resource_type.__init__) + + # Resolve generic type vars, walking up the inheritance chain + def _collect(cls: type, mapping: dict) -> None: + for base in getattr(cls, "__orig_bases__", ()): + origin = get_origin(base) + if origin is None: + continue + params = getattr(origin, "__parameters__", ()) + for param, arg in zip(params, get_args(base)): + resolved = mapping.get(arg, arg) + if not isinstance(resolved, TypeVar): + mapping[param] = resolved + _collect(origin, mapping) + + typevar_map: dict = {} + _collect(resource_type, typevar_map) + constructor_signature = { + name: typevar_map.get(t, t) for name, t in constructor_signature.items() + } + specification_type = None for arg_name, arg_type in constructor_signature.items(): if arg_name == "return": diff --git a/monitoring/uss_qualifier/resources/resources_test.py b/monitoring/uss_qualifier/resources/resources_test.py new file mode 100644 index 0000000000..c80bff6cbc --- /dev/null +++ b/monitoring/uss_qualifier/resources/resources_test.py @@ -0,0 +1,130 @@ +import unittest + +from monitoring.uss_qualifier.resources.definitions import ( + ResourceDeclaration, + ResourceID, +) +from monitoring.uss_qualifier.resources.dev.test_modifier import ( + TestModifierModifierSpecification, + TestModifierSpecification, +) +from monitoring.uss_qualifier.resources.resource import create_resources + + +class TestResourceModifier(unittest.TestCase): + def _build_test_modifier_declaration( + self, base_id + ) -> dict[ResourceID, ResourceDeclaration]: + return { + "test": ResourceDeclaration( + resource_type="resources.dev.TestModifierResource", + specification=TestModifierSpecification(base_id=base_id), + ) + } + + def _build_test_modifier_modifier_declaration( + self, base_id, shift_interval + ) -> dict[ResourceID, ResourceDeclaration]: + return { + "test": self._build_test_modifier_declaration(base_id)["test"], + "test_modifier": ResourceDeclaration( + resource_type="resources.dev.TestModifierModifierResource", + specification=TestModifierModifierSpecification( + shift_interval=shift_interval + ), + dependencies={ + "base_resource": "test", + }, + ), + } + + def test_base_resource(self): + """Test basic usage of the resource""" + declaration = self._build_test_modifier_declaration(42) + + resources = create_resources(declaration, "test", True) + assert "test" in resources + + resource = resources["test"] + + assert resource.build_ids() == [42, 43, 44, 45, 46, 47, 48, 49, 50, 51] + + def test_base_resource_base_id(self): + """Test that base id works as expected""" + + declaration = self._build_test_modifier_declaration(52) + + resources = create_resources(declaration, "test", True) + assert "test" in resources + + resource = resources["test"] + + assert resource.build_ids() == [52, 53, 54, 55, 56, 57, 58, 59, 60, 61] + + def test_modifier_resource(self): + """Test basic usage of the resource modifier""" + declaration = self._build_test_modifier_modifier_declaration(42, 10) + + resources = create_resources(declaration, "test", True) + assert "test_modifier" in resources + + resource = resources["test_modifier"] + + assert resource.adjust(0).build_ids() == [ + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + ] + assert resource.adjust(1).build_ids() == [ + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + ] + + def test_modifier_resource_shift(self): + """Test shift usage of the resource modifier""" + declaration = self._build_test_modifier_modifier_declaration(42, 20) + + resources = create_resources(declaration, "test", True) + assert "test_modifier" in resources + + resource = resources["test_modifier"] + + assert resource.adjust(0).build_ids() == [ + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + ] + assert resource.adjust(1).build_ids() == [ + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + ] diff --git a/schemas/manage_type_schemas.py b/schemas/manage_type_schemas.py index 8b7812cd53..bd4c4028ed 100644 --- a/schemas/manage_type_schemas.py +++ b/schemas/manage_type_schemas.py @@ -87,6 +87,29 @@ def _make_type_schemas( ) +def _resolve_resource_spec_type(cls: type) -> type: + """Find the spec type bound to Resource[Spec] for a Resource subclass, + resolving TypeVars through intermediate generic bases (e.g., ResourceModifier).""" + + def walk(c: type, subst: dict): + for base in getattr(c, "__orig_bases__", ()): + origin = get_origin(base) + if origin is None: + continue + args = tuple(subst.get(a, a) for a in get_args(base)) + if origin is Resource: + return args[0] + result = walk(origin, dict(zip(origin.__parameters__, args))) + if result is not None: + return result + return None + + result = walk(cls, {}) + if result is None: + raise ValueError(f"Could not resolve Resource specification type for {cls}") + return result + + def _find_specifications( module, repo: dict[str, type[ImplicitDict]], @@ -105,7 +128,9 @@ def _find_specifications( _find_specifications(member, repo, already_checked) elif inspect.isclass(member): if issubclass(member, Resource) and member != Resource: - spec_type = get_args(member.__orig_bases__[0])[0] + if inspect.isabstract(member): + continue + spec_type = _resolve_resource_spec_type(member) repo[fullname(spec_type)] = spec_type elif issubclass(member, ActionGenerator) and member != ActionGenerator: spec_type = get_args(member.__orig_bases__[0])[0] diff --git a/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierModifierSpecification.json b/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierModifierSpecification.json new file mode 100644 index 0000000000..24a9d04c65 --- /dev/null +++ b/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierModifierSpecification.json @@ -0,0 +1,18 @@ +{ + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierModifierSpecification.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "monitoring.uss_qualifier.resources.dev.test_modifier.TestModifierModifierSpecification, as defined in monitoring/uss_qualifier/resources/dev/test_modifier.py", + "properties": { + "$ref": { + "description": "Path to content that replaces the $ref", + "type": "string" + }, + "shift_interval": { + "type": "integer" + } + }, + "required": [ + "shift_interval" + ], + "type": "object" +} \ No newline at end of file diff --git a/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierSpecification.json b/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierSpecification.json new file mode 100644 index 0000000000..1824bdad7e --- /dev/null +++ b/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierSpecification.json @@ -0,0 +1,18 @@ +{ + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierSpecification.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "monitoring.uss_qualifier.resources.dev.test_modifier.TestModifierSpecification, as defined in monitoring/uss_qualifier/resources/dev/test_modifier.py", + "properties": { + "$ref": { + "description": "Path to content that replaces the $ref", + "type": "string" + }, + "base_id": { + "type": "integer" + } + }, + "required": [ + "base_id" + ], + "type": "object" +} \ No newline at end of file diff --git a/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestSquareSpecification.json b/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestSquareSpecification.json new file mode 100644 index 0000000000..ead3fcd30b --- /dev/null +++ b/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestSquareSpecification.json @@ -0,0 +1,22 @@ +{ + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestSquareSpecification.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "monitoring.uss_qualifier.resources.dev.test_modifier.TestSquareSpecification, as defined in monitoring/uss_qualifier/resources/dev/test_modifier.py", + "properties": { + "$ref": { + "description": "Path to content that replaces the $ref", + "type": "string" + }, + "lat_center": { + "type": "number" + }, + "lng_center": { + "type": "number" + } + }, + "required": [ + "lat_center", + "lng_center" + ], + "type": "object" +} \ No newline at end of file diff --git a/schemas/monitoring/uss_qualifier/resources/modifiers/GeospatialModifierSpecification.json b/schemas/monitoring/uss_qualifier/resources/modifiers/GeospatialModifierSpecification.json new file mode 100644 index 0000000000..26be305caa --- /dev/null +++ b/schemas/monitoring/uss_qualifier/resources/modifiers/GeospatialModifierSpecification.json @@ -0,0 +1,18 @@ +{ + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/modifiers/GeospatialModifierSpecification.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "monitoring.uss_qualifier.resources.modifiers.GeospatialModifierSpecification, as defined in monitoring/uss_qualifier/resources/modifiers.py", + "properties": { + "$ref": { + "description": "Path to content that replaces the $ref", + "type": "string" + }, + "meters_east_margin": { + "type": "number" + }, + "meters_north_margin": { + "type": "number" + } + }, + "type": "object" +} \ No newline at end of file