From 523458bb455bb35d3f03ca06b489f6d6b4dacbd6 Mon Sep 17 00:00:00 2001 From: Hrishikesh Ballal Date: Mon, 29 Dec 2025 16:05:55 +0000 Subject: [PATCH 01/12] Added test for operational intent submission --- ...ht_declaration_via_operational_intent.json | 6 + pyproject.toml | 4 +- .../flight_declaration_template.json | 2 +- ...ation_via_operational_intent_template.json | 21 ++++ .../flight_blender/flight_blender_client.py | 106 +++++++++++++++++- .../core/execution/config_models.py | 15 ++- .../core/execution/dependencies.py | 60 ++++++++-- .../core/execution/scenario_runner.py | 55 ++++++++- .../core/reporting/reporting_models.py | 10 +- src/openutm_verification/scenarios/common.py | 12 +- .../scenarios/registry.py | 16 ++- .../scenarios/test_add_operational_intent.py | 29 +++++ .../simulator/flight_declaration.py | 83 +++++++++++++- .../simulator/models/declaration_models.py | 35 ++++++ uv.lock | 18 +-- 15 files changed, 434 insertions(+), 38 deletions(-) create mode 100644 config/bern/flight_declaration_via_operational_intent.json create mode 100644 src/openutm_verification/assets/simulator_templates/flight_declaration_via_operational_intent_template.json create mode 100644 src/openutm_verification/scenarios/test_add_operational_intent.py diff --git a/config/bern/flight_declaration_via_operational_intent.json b/config/bern/flight_declaration_via_operational_intent.json new file mode 100644 index 0000000..1528b14 --- /dev/null +++ b/config/bern/flight_declaration_via_operational_intent.json @@ -0,0 +1,6 @@ +{ + "minx": 7.4719589491516558, + "miny": 46.9799127188803993, + "maxx": 7.4870457729811619, + "maxy": 46.9865389634242945 +} diff --git a/pyproject.toml b/pyproject.toml index 1558cd0..dc215be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,8 +24,7 @@ dependencies = [ "jwcrypto==1.5.6", "pyproj==3.7.1", "shapely==2.1.0", - "implicitdict==3.0.0", - "uas-standards==3.5.0", + "implicitdict==4.0.1", "geojson==3.2.0", "folium>=0.20.0", "faker==9.3.1", @@ -45,6 +44,7 @@ dependencies = [ "pydantic-settings>=2.10.1", "websocket-client==1.9.0", "markdown>=3.10", + "uas-standards==4.2.0" ] [project.scripts] diff --git a/src/openutm_verification/assets/simulator_templates/flight_declaration_template.json b/src/openutm_verification/assets/simulator_templates/flight_declaration_template.json index 25e18ff..fa01dbc 100644 --- a/src/openutm_verification/assets/simulator_templates/flight_declaration_template.json +++ b/src/openutm_verification/assets/simulator_templates/flight_declaration_template.json @@ -16,6 +16,6 @@ "type_of_operation": 0, "vehicle_id": "157de9bb-6b49-496b-bf3f-0b768ce6a3b6", "operator_id": "4a725cb5-02d2-4f78-888f-b93088d324be", - "flight_declaration_geo_json": { + "operational_intent_volume4ds": { } } diff --git a/src/openutm_verification/assets/simulator_templates/flight_declaration_via_operational_intent_template.json b/src/openutm_verification/assets/simulator_templates/flight_declaration_via_operational_intent_template.json new file mode 100644 index 0000000..25e18ff --- /dev/null +++ b/src/openutm_verification/assets/simulator_templates/flight_declaration_via_operational_intent_template.json @@ -0,0 +1,21 @@ +{ + "exchange_type": "flight_declaration", + "aircraft_id": "a5dd8899-bc19-c8c4-2dd7-57f786d1379d", + "flight_id": "5a7f3377-b991-4cc8-af2d-379d57f786d1", + "plan_id": "a5b5484c-a23c-4e83-8bb8-a6a5c294e45b", + "flight_state": 2, + "flight_approved": 0, + "sequence_number": 0, + "start_datetime": "2023-06-12T16:35:08.842Z", + "end_datetime": "2023-06-12T16:40:08.842Z", + "version": "1.0.0", + "purpose": "Delivery", + "expect_telemetry": true, + "originating_party": "Medicine Delivery Company", + "contact_url": "https://utm.originatingparty.com/contact?5a7f3377-b991-4cc8-af2d-379d57f786d1", + "type_of_operation": 0, + "vehicle_id": "157de9bb-6b49-496b-bf3f-0b768ce6a3b6", + "operator_id": "4a725cb5-02d2-4f78-888f-b93088d324be", + "flight_declaration_geo_json": { + } +} diff --git a/src/openutm_verification/core/clients/flight_blender/flight_blender_client.py b/src/openutm_verification/core/clients/flight_blender/flight_blender_client.py index 3879a9f..9422659 100644 --- a/src/openutm_verification/core/clients/flight_blender/flight_blender_client.py +++ b/src/openutm_verification/core/clients/flight_blender/flight_blender_client.py @@ -40,7 +40,9 @@ RIDOperatorDetails, UAClassificationEU, ) -from openutm_verification.simulator.models.flight_data_types import FlightObservationSchema +from openutm_verification.simulator.models.flight_data_types import ( + FlightObservationSchema, +) def _create_rid_operator_details(operation_id: str) -> RIDOperatorDetails: @@ -251,6 +253,65 @@ async def upload_flight_declaration(self, declaration: str | BaseModel) -> dict[ return response_json + @scenario_step("Upload Flight Declaration Via Operational Intent") + async def upload_flight_declaration_via_operational_intent(self, declaration: str | BaseModel) -> dict[str, Any]: + """Upload a flight declaration to the Flight Blender API. + + Accepts either a filename (str) containing JSON declaration data, or a + FlightDeclaration model instance. Adjusts datetimes to current time + offsets, + and posts it. Raises an error if the declaration is not approved. + + Args: + declaration: Either a path to the JSON flight declaration file (str), + or a FlightDeclaration model instance. + + Returns: + The JSON response from the API. + + Raises: + FlightBlenderError: If the declaration is not approved or the request fails. + json.JSONDecodeError: If the file content is invalid JSON (when using filename). + """ + endpoint = "/flight_declaration_ops/set_operational_intent" + + # Handle different input types + if isinstance(declaration, str): + # Load from file + logger.debug(f"Uploading flight declaration from {declaration}") + with open(declaration, "r", encoding="utf-8") as flight_declaration_file: + f_d = flight_declaration_file.read() + flight_declaration = json.loads(f_d) + else: + # Assume it's a model with model_dump method + logger.debug("Uploading flight declaration from model") + flight_declaration = declaration.model_dump(mode="json") + + # Adjust datetimes to current time + offsets + now = arrow.now() + few_seconds_from_now = now.shift(seconds=5) + four_minutes_from_now = now.shift(minutes=4) + + flight_declaration["start_datetime"] = few_seconds_from_now.isoformat() + flight_declaration["end_datetime"] = four_minutes_from_now.isoformat() + + response = await self.post(endpoint, json=flight_declaration) + logger.info(f"Flight declaration upload response: {response.status_code}") + + response_json = response.json() + + if not response_json.get("is_approved"): + logger.error(f"Flight declaration not approved. State: {OperationState(response_json.get('state')).name}") + raise FlightBlenderError(f"Flight declaration not approved. State: {OperationState(response_json.get('state')).name}") + # Store latest declaration id for later use + try: + self.latest_flight_declaration_id = response_json.get("id") + logger.info(f"Flight declaration uploaded and approved, ID: {self.latest_flight_declaration_id}") + except AttributeError: + self.latest_flight_declaration_id = None + logger.warning("Failed to extract flight declaration ID from response") + + return response_json + @scenario_step("Wait for User Input") async def wait_for_user_input(self, prompt: str = "Press Enter to continue...") -> str: """Wait for user input to proceed. @@ -775,9 +836,37 @@ async def teardown_flight_declaration(self): logger.info("Tearing down flight declaration...") await self.delete_flight_declaration() + @scenario_step("Setup Flight Declaration via Operational Intent") + async def setup_flight_declaration_via_operational_intent( + self, + flight_declaration_via_operational_intent_path: str, + trajectory_path: str, + ) -> None: + from openutm_verification.scenarios.common import ( + generate_flight_declaration_via_operational_intent, + generate_telemetry, + ) + + """Generates data and uploads flight declaration via Operational Intent.""" + flight_declaration = generate_flight_declaration_via_operational_intent(flight_declaration_via_operational_intent_path) + telemetry_states = generate_telemetry(trajectory_path) + + self.telemetry_states = telemetry_states + + # Store data in ScenarioContext for reporting + ScenarioContext.set_flight_declaration_via_operational_intent_data(flight_declaration) + ScenarioContext.set_telemetry_data(telemetry_states) + + upload_result = await self.upload_flight_declaration_via_operational_intent(flight_declaration) + + if upload_result.status == Status.FAIL: + logger.error(f"Flight declaration upload failed: {upload_result}") + raise FlightBlenderError("Failed to upload flight declaration during setup_flight_declaration_via_operational_intent") + @scenario_step("Setup Flight Declaration") async def setup_flight_declaration(self, flight_declaration_path: str, trajectory_path: str) -> None: """Generates data and uploads flight declaration.""" + from openutm_verification.scenarios.common import ( generate_flight_declaration, generate_telemetry, @@ -808,3 +897,18 @@ async def create_flight_declaration(self, data_files: DataFiles): yield finally: logger.info("All test steps complete..") + + @asynccontextmanager + async def create_flight_declaration_via_operational_intent(self, data_files: DataFiles): + """Context manager to setup and teardown a flight operation based on scenario config.""" + assert data_files.flight_declaration_via_operational_intent is not None, ( + "Flight declaration via operational intent file path must be provided" + ) + assert data_files.trajectory is not None, "Trajectory file path must be provided" + await self.setup_flight_declaration_via_operational_intent( + flight_declaration_via_operational_intent_path=data_files.flight_declaration_via_operational_intent, trajectory_path=data_files.trajectory + ) + try: + yield + finally: + logger.info("All test steps complete..") diff --git a/src/openutm_verification/core/execution/config_models.py b/src/openutm_verification/core/execution/config_models.py index 34d1dea..d0dc55a 100644 --- a/src/openutm_verification/core/execution/config_models.py +++ b/src/openutm_verification/core/execution/config_models.py @@ -68,8 +68,14 @@ class DataFiles(StrictBaseModel): trajectory: str | None = None flight_declaration: str | None = None geo_fence: str | None = None - - @field_validator("trajectory", "flight_declaration", "geo_fence") + flight_declaration_via_operational_intent: str | None = None + + @field_validator( + "trajectory", + "flight_declaration", + "flight_declaration_via_operational_intent", + "geo_fence", + ) @classmethod def validate_path(cls, v: str | None) -> str | None: """Validate that path is a non-empty string if provided.""" @@ -99,6 +105,11 @@ def resolve_and_validate_path(path_str: str, field_name: str) -> str: self.trajectory = resolve_and_validate_path(self.trajectory, "Trajectory") if self.flight_declaration: self.flight_declaration = resolve_and_validate_path(self.flight_declaration, "Flight declaration") + if self.flight_declaration_via_operational_intent: + self.flight_declaration_via_operational_intent = resolve_and_validate_path( + self.flight_declaration_via_operational_intent, + "Flight declaration via operational intent", + ) if self.geo_fence: self.geo_fence = resolve_and_validate_path(self.geo_fence, "Geo-fence") diff --git a/src/openutm_verification/core/execution/dependencies.py b/src/openutm_verification/core/execution/dependencies.py index 4f285c1..dad22c0 100644 --- a/src/openutm_verification/core/execution/dependencies.py +++ b/src/openutm_verification/core/execution/dependencies.py @@ -1,15 +1,40 @@ -from typing import Any, AsyncGenerator, Callable, Coroutine, Generator, Iterable, TypeVar, cast +from typing import ( + Any, + AsyncGenerator, + Callable, + Coroutine, + Generator, + Iterable, + TypeVar, + cast, +) from loguru import logger from openutm_verification.auth.providers import get_auth_provider -from openutm_verification.core.clients.air_traffic.air_traffic_client import AirTrafficClient -from openutm_verification.core.clients.air_traffic.base_client import create_air_traffic_settings -from openutm_verification.core.clients.flight_blender.flight_blender_client import FlightBlenderClient -from openutm_verification.core.clients.opensky.base_client import create_opensky_settings +from openutm_verification.core.clients.air_traffic.air_traffic_client import ( + AirTrafficClient, +) +from openutm_verification.core.clients.air_traffic.base_client import ( + create_air_traffic_settings, +) +from openutm_verification.core.clients.flight_blender.flight_blender_client import ( + FlightBlenderClient, +) +from openutm_verification.core.clients.opensky.base_client import ( + create_opensky_settings, +) from openutm_verification.core.clients.opensky.opensky_client import OpenSkyClient -from openutm_verification.core.execution.config_models import AppConfig, DataFiles, ScenarioId, get_settings -from openutm_verification.core.execution.dependency_resolution import CONTEXT, dependency +from openutm_verification.core.execution.config_models import ( + AppConfig, + DataFiles, + ScenarioId, + get_settings, +) +from openutm_verification.core.execution.dependency_resolution import ( + CONTEXT, + dependency, +) from openutm_verification.core.reporting.reporting_models import ScenarioResult from openutm_verification.scenarios.registry import SCENARIO_REGISTRY @@ -66,7 +91,14 @@ def scenarios() -> Iterable[tuple[str, Callable[..., Coroutine[Any, Any, Scenari scenario_func = SCENARIO_REGISTRY[scenario_id].get("func") docs_content = get_scenario_docs(scenario_id) - CONTEXT.set({"scenario_id": scenario_id, "suite_scenario": suite_scenario, "suite_name": suite_name, "docs": docs_content}) + CONTEXT.set( + { + "scenario_id": scenario_id, + "suite_scenario": suite_scenario, + "suite_name": suite_name, + "docs": docs_content, + } + ) yield scenario_id, scenario_func else: logger.warning(f"Scenario {scenario_id} not found in registry.") @@ -99,6 +131,9 @@ def data_files(scenario_id: ScenarioId) -> Generator[DataFiles, None, None]: # Merge suite overrides with base config trajectory = suite_scenario.trajectory or config.data_files.trajectory flight_declaration = suite_scenario.flight_declaration or config.data_files.flight_declaration + flight_declaration_via_operational_intent = ( + suite_scenario.flight_declaration_via_operational_intent or config.data_files.flight_declaration_via_operational_intent + ) geo_fence = suite_scenario.geo_fence or config.data_files.geo_fence else: # Use base config @@ -109,6 +144,7 @@ def data_files(scenario_id: ScenarioId) -> Generator[DataFiles, None, None]: data = DataFiles( trajectory=trajectory, flight_declaration=flight_declaration, + flight_declaration_via_operational_intent=flight_declaration_via_operational_intent, geo_fence=geo_fence, ) yield data @@ -125,7 +161,9 @@ def app_config() -> Generator[AppConfig, None, None]: @dependency(FlightBlenderClient) -async def flight_blender_client(config: AppConfig) -> AsyncGenerator[FlightBlenderClient, None]: +async def flight_blender_client( + config: AppConfig, +) -> AsyncGenerator[FlightBlenderClient, None]: """Provides a FlightBlenderClient instance for dependency injection. Args: @@ -151,7 +189,9 @@ async def opensky_client(config: AppConfig) -> AsyncGenerator[OpenSkyClient, Non @dependency(AirTrafficClient) -async def air_traffic_client(config: AppConfig) -> AsyncGenerator[AirTrafficClient, None]: +async def air_traffic_client( + config: AppConfig, +) -> AsyncGenerator[AirTrafficClient, None]: """Provides an AirTrafficClient instance for dependency injection.""" settings = create_air_traffic_settings() async with AirTrafficClient(settings) as air_traffic_client: diff --git a/src/openutm_verification/core/execution/scenario_runner.py b/src/openutm_verification/core/execution/scenario_runner.py index aebe41a..5d96c2e 100644 --- a/src/openutm_verification/core/execution/scenario_runner.py +++ b/src/openutm_verification/core/execution/scenario_runner.py @@ -4,16 +4,36 @@ from dataclasses import dataclass, field from functools import wraps from pathlib import Path -from typing import Any, Awaitable, Callable, Coroutine, ParamSpec, Protocol, TypedDict, TypeVar, cast, overload +from typing import ( + Any, + Awaitable, + Callable, + Coroutine, + ParamSpec, + Protocol, + TypedDict, + TypeVar, + cast, + overload, +) from loguru import logger from uas_standards.astm.f3411.v22a.api import RIDAircraftState from openutm_verification.core.clients.opensky.base_client import OpenSkyError -from openutm_verification.core.reporting.reporting_models import ScenarioResult, Status, StepResult +from openutm_verification.core.reporting.reporting_models import ( + ScenarioResult, + Status, + StepResult, +) from openutm_verification.models import FlightBlenderError -from openutm_verification.simulator.models.declaration_models import FlightDeclaration -from openutm_verification.simulator.models.flight_data_types import FlightObservationSchema +from openutm_verification.simulator.models.declaration_models import ( + FlightDeclaration, + FlightDeclarationViaOperationalIntent, +) +from openutm_verification.simulator.models.flight_data_types import ( + FlightObservationSchema, +) T = TypeVar("T") P = ParamSpec("P") @@ -25,6 +45,7 @@ class ScenarioState: steps: list[StepResult[Any]] = field(default_factory=list) active: bool = False flight_declaration_data: FlightDeclaration | None = None + flight_declaration_via_operational_intent_data: FlightDeclarationViaOperationalIntent | None = None telemetry_data: list[RIDAircraftState] | None = None air_traffic_data: list[list[FlightObservationSchema]] = field(default_factory=list) @@ -65,6 +86,12 @@ def set_flight_declaration_data(cls, data: FlightDeclaration) -> None: if state and state.active: state.flight_declaration_data = data + @classmethod + def set_flight_declaration_via_operational_intent_data(cls, data: FlightDeclarationViaOperationalIntent) -> None: + state = _scenario_state.get() + if state and state.active: + state.flight_declaration_via_operational_intent_data = data + @classmethod def set_telemetry_data(cls, data: list[RIDAircraftState]) -> None: state = _scenario_state.get() @@ -91,6 +118,15 @@ def flight_declaration_data(self) -> FlightDeclaration | None: state = _scenario_state.get() return state.flight_declaration_data if state else None + @property + def flight_declaration_via_operational_intent_data( + self, + ) -> FlightDeclarationViaOperationalIntent | None: + if self._state: + return self._state.flight_declaration_via_operational_intent_data + state = _scenario_state.get() + return state.flight_declaration_via_operational_intent_data if state else None + @property def telemetry_data(self) -> list[RIDAircraftState] | None: if self._state: @@ -117,7 +153,9 @@ def __call__(self, func: Callable[P, Awaitable[Any]]) -> Callable[P, Coroutine[A def scenario_step(step_name: str) -> StepDecorator: - def decorator(func: Callable[P, Awaitable[Any]]) -> Callable[P, Coroutine[Any, Any, Any]]: + def decorator( + func: Callable[P, Awaitable[Any]], + ) -> Callable[P, Coroutine[Any, Any, Any]]: def handle_result(result: Any, start_time: float) -> StepResult[Any]: duration = time.time() - start_time logger.info(f"Step '{step_name}' successful in {duration:.2f} seconds.") @@ -125,7 +163,12 @@ def handle_result(result: Any, start_time: float) -> StepResult[Any]: if isinstance(result, StepResult): step_result = result else: - step_result = StepResult(name=step_name, status=Status.PASS, duration=duration, details=result) + step_result = StepResult( + name=step_name, + status=Status.PASS, + duration=duration, + details=result, + ) ScenarioContext.add_result(step_result) return step_result diff --git a/src/openutm_verification/core/reporting/reporting_models.py b/src/openutm_verification/core/reporting/reporting_models.py index fbad735..85237e4 100644 --- a/src/openutm_verification/core/reporting/reporting_models.py +++ b/src/openutm_verification/core/reporting/reporting_models.py @@ -9,8 +9,13 @@ from uas_standards.astm.f3411.v22a.api import RIDAircraftState from openutm_verification.core.execution.config_models import DeploymentDetails -from openutm_verification.simulator.models.declaration_models import FlightDeclaration -from openutm_verification.simulator.models.flight_data_types import FlightObservationSchema +from openutm_verification.simulator.models.declaration_models import ( + FlightDeclaration, + FlightDeclarationViaOperationalIntent, +) +from openutm_verification.simulator.models.flight_data_types import ( + FlightObservationSchema, +) class Status(StrEnum): @@ -47,6 +52,7 @@ class ScenarioResult(BaseModel): flight_declaration_filename: str | None = None telemetry_filename: str | None = None flight_declaration_data: FlightDeclaration | None = None + flight_declaration_via_operational_intent_data: FlightDeclarationViaOperationalIntent | None = None telemetry_data: list[RIDAircraftState] | None = None air_traffic_data: list[list[FlightObservationSchema]] | None = None visualization_2d_path: str | None = None diff --git a/src/openutm_verification/scenarios/common.py b/src/openutm_verification/scenarios/common.py index 6b45a21..396f1f0 100644 --- a/src/openutm_verification/scenarios/common.py +++ b/src/openutm_verification/scenarios/common.py @@ -6,7 +6,7 @@ from openutm_verification.simulator.flight_declaration import FlightDeclarationGenerator from openutm_verification.simulator.geo_json_telemetry import GeoJSONFlightsSimulator -from openutm_verification.simulator.models.declaration_models import FlightDeclaration +from openutm_verification.simulator.models.declaration_models import FlightDeclaration, FlightDeclarationViaOperationalIntent from openutm_verification.simulator.models.flight_data_types import ( GeoJSONFlightsSimulatorConfiguration, ) @@ -24,6 +24,16 @@ def generate_flight_declaration(config_path: str) -> FlightDeclaration: raise +def generate_flight_declaration_via_operational_intent(config_path: str) -> FlightDeclarationViaOperationalIntent: + """Generate a flight declaration via operational intent from the config file at the given path.""" + try: + generator = FlightDeclarationGenerator(bounds_path=Path(config_path)) + return generator.generate_via_operational_intent() + except Exception as e: + logger.error(f"Failed to generate flight declaration via operational intent from {config_path}: {e}") + raise + + def generate_telemetry(config_path: str, duration: int = DEFAULT_TELEMETRY_DURATION) -> list[RIDAircraftState]: """Generate telemetry states from the GeoJSON config file at the given path.""" try: diff --git a/src/openutm_verification/scenarios/registry.py b/src/openutm_verification/scenarios/registry.py index b39fa02..42b1b71 100644 --- a/src/openutm_verification/scenarios/registry.py +++ b/src/openutm_verification/scenarios/registry.py @@ -19,7 +19,10 @@ def run_my_scenario(client, scenario_id): from loguru import logger -from openutm_verification.core.execution.scenario_runner import ScenarioContext, ScenarioRegistry +from openutm_verification.core.execution.scenario_runner import ( + ScenarioContext, + ScenarioRegistry, +) from openutm_verification.core.reporting.reporting_models import ( ScenarioResult, Status, @@ -42,6 +45,7 @@ async def _run_scenario_simple_async(scenario_id: str, func: Callable, args, kwa steps = ctx.steps flight_declaration_data = ctx.flight_declaration_data + flight_declaration_via_operational_intent_data = ctx.flight_declaration_via_operational_intent_data telemetry_data = ctx.telemetry_data air_traffic_data = ctx.air_traffic_data @@ -53,6 +57,7 @@ async def _run_scenario_simple_async(scenario_id: str, func: Callable, args, kwa duration_seconds=total_duration, steps=steps, flight_declaration_data=flight_declaration_data, + flight_declaration_via_operational_intent_data=flight_declaration_via_operational_intent_data, telemetry_data=telemetry_data, air_traffic_data=air_traffic_data, ) @@ -64,7 +69,10 @@ async def _run_scenario_simple_async(scenario_id: str, func: Callable, args, kwa def register_scenario( scenario_id: str, -) -> Callable[[Callable[P, Coroutine[Any, Any, Any]]], Callable[P, Coroutine[Any, Any, ScenarioResult]]]: +) -> Callable[ + [Callable[P, Coroutine[Any, Any, Any]]], + Callable[P, Coroutine[Any, Any, ScenarioResult]], +]: """ A decorator to register a test scenario function. @@ -73,7 +81,9 @@ def register_scenario( This ID is used in the configuration file. """ - def decorator(func: Callable[P, Coroutine[Any, Any, Any]]) -> Callable[P, Coroutine[Any, Any, ScenarioResult]]: + def decorator( + func: Callable[P, Coroutine[Any, Any, Any]], + ) -> Callable[P, Coroutine[Any, Any, ScenarioResult]]: if scenario_id in SCENARIO_REGISTRY: raise ValueError(f"Scenario with ID '{scenario_id}' is already registered.") diff --git a/src/openutm_verification/scenarios/test_add_operational_intent.py b/src/openutm_verification/scenarios/test_add_operational_intent.py new file mode 100644 index 0000000..80a07e0 --- /dev/null +++ b/src/openutm_verification/scenarios/test_add_operational_intent.py @@ -0,0 +1,29 @@ +from openutm_verification.core.clients.flight_blender.flight_blender_client import FlightBlenderClient +from openutm_verification.core.execution.config_models import DataFiles +from openutm_verification.models import OperationState +from openutm_verification.scenarios.registry import register_scenario + + +@register_scenario("add_flight_declaration_via_operational_intent") +async def test_add_flight_declaration_via_operational_intent(fb_client: FlightBlenderClient, data_files: DataFiles) -> None: + """Runs the add flight declaration scenario. + + This scenario replicates the behavior of the add_flight_declaration.py importer: + 1. Upload flight declaration (handled by template). + 2. Wait 20 seconds. + 3. Set flight operation state to ACTIVATED. + 4. Submit telemetry data for 30 seconds. + 5. Set flight operation state to ENDED. + + Args: + fb_client: The FlightBlenderClient instance for API interaction. + data_files: The DataFiles instance containing file paths for telemetry, flight declaration, and geo-fence. + + Returns: + A ScenarioResult object containing the results of< the scenario execution. + """ + + async with fb_client.create_flight_declaration_via_operational_intent(data_files): + await fb_client.update_operation_state(new_state=OperationState.ACTIVATED, duration_seconds=5) + # await fb_client.submit_telemetry(duration_seconds=30) + await fb_client.update_operation_state(new_state=OperationState.ENDED) diff --git a/src/openutm_verification/simulator/flight_declaration.py b/src/openutm_verification/simulator/flight_declaration.py index 541d22b..957b6b0 100644 --- a/src/openutm_verification/simulator/flight_declaration.py +++ b/src/openutm_verification/simulator/flight_declaration.py @@ -7,14 +7,25 @@ import arrow from loguru import logger from shapely.geometry import Polygon +from uas_standards.astm.f3548.v21.api import ( + Altitude, + LatLngPoint, + Time, + Volume3D, + Volume4D, +) +from uas_standards.astm.f3548.v21.api import Polygon as ASTMPolygon from openutm_verification.simulator.models.declaration_models import ( BaseUpdates, + BaseUpdatesViaOperationalIntent, Feature, FeatureCollection, FlightDeclaration, FlightDeclarationBounds, FlightDeclarationOverrides, + FlightDeclarationViaOperationalIntent, + FlightDeclarationViaOperationalIntentOverrides, PolygonGeometry, ) @@ -22,6 +33,9 @@ END_TIME_OFFSET_S = 4 * 60 # Seconds from now for flight end DEFAULT_TEMPLATE_PATH: Final[Path] = Path(__file__).resolve().parent.parent / "assets" / "simulator_templates" / "flight_declaration_template.json" +OPERTATIONAL_INTENT_TEMPLATE_PATH: Final[Path] = ( + Path(__file__).resolve().parent.parent / "assets" / "simulator_templates" / "flight_declaration_via_operational_intent_template.json" +) BOUNDS_FILE_PATH: Final[Path] = Path(__file__).resolve().parents[3] / "config" / "bern" / "flight_declaration.json" @@ -32,10 +46,22 @@ def __init__( self, *, template_path: Path = DEFAULT_TEMPLATE_PATH, + operational_intent_template_path: Path = OPERTATIONAL_INTENT_TEMPLATE_PATH, bounds_path: Path = BOUNDS_FILE_PATH, ): self.bounds = self._load_bounds(bounds_path) self.template = self._load_template(template_path) + self.flight_declaration_via_operational_intent_template = self._load_flight_declaration_via_operational_intent_template( + operational_intent_template_path + ) + + @staticmethod + def _validate_flight_declaration_via_operational_intent_template_path( + operational_intent_template_path: Path, + ) -> Path: + if not operational_intent_template_path.is_file(): + raise FileNotFoundError(f"Operational Intent Template not found: {operational_intent_template_path}") + return operational_intent_template_path @staticmethod def _validate_template_path(template_path: Path) -> Path: @@ -43,6 +69,13 @@ def _validate_template_path(template_path: Path) -> Path: raise FileNotFoundError(f"Template not found: {template_path}") return template_path + def _load_flight_declaration_via_operational_intent_template(self, template_path: Path) -> dict[str, Any]: + """Load template as raw dict.""" + path = self._validate_flight_declaration_via_operational_intent_template_path(template_path) + logger.debug(f"Loading flight declaration template from {path}") + with path.open("r", encoding="utf-8") as f: + return json.load(f) + def _load_template(self, template_path: Path) -> dict[str, Any]: """Load template as raw dict.""" path = self._validate_template_path(template_path) @@ -74,7 +107,10 @@ def _build_geojson_bbox(self, bbox: Polygon) -> FeatureCollection: geometry = PolygonGeometry(coordinates=coordinates) # TODO: verify if this should be somewhere else # Add required properties for Flight Blender API - properties = {"min_altitude": {"meters": 50, "datum": "w84"}, "max_altitude": {"meters": 120, "datum": "w84"}} + properties = { + "min_altitude": {"meters": 50, "datum": "w84"}, + "max_altitude": {"meters": 120, "datum": "w84"}, + } feature = Feature(geometry=geometry, properties=properties) return FeatureCollection(features=[feature]) @@ -97,6 +133,51 @@ def generate(self) -> FlightDeclaration: # Validate and return as FlightDeclaration model return FlightDeclaration.model_validate(final_payload) + def _build_volume4d_operational_intent(self, bbox: Polygon) -> list[dict[str, Any]]: + coordinates = self._normalize_coordinates(bbox) + geometry = PolygonGeometry(coordinates=coordinates) + all_verticies = [] + WGS84_OFFSET = 200 # between the WGS1984 ellipsoid and terrain surface + for polygon in geometry.coordinates: + for lat_lng in polygon: + all_verticies.append(LatLngPoint(lat=lat_lng[1], lng=lat_lng[0])) + polygon = ASTMPolygon(vertices=all_verticies) + volume3d = Volume3D( + outline_polygon=polygon, + altitude_lower=Altitude(value=WGS84_OFFSET + 50, reference="w84", units="m"), + altitude_upper=Altitude(value=WGS84_OFFSET + 120, reference="w84", units="m"), + ) + start_time = Time( + value=arrow.utcnow().shift(seconds=START_TIME_OFFSET_S).timestamp(), + format="RFC3339", + ) + end_time = Time( + value=arrow.utcnow().shift(seconds=END_TIME_OFFSET_S).timestamp(), + format="RFC3339", + ) + volume4d = Volume4D(volume=volume3d, time_start=start_time, time_end=end_time) + + return [json.loads(json.dumps(volume4d))] + + def generate_via_operational_intent(self) -> FlightDeclarationViaOperationalIntent: + """Generate validated flight declaration via operational intent as pydantic model.""" + bbox = self.bounds.polygon + logger.info(f"Diagonal: {self.bounds.diagonal_length_m:.2f} m") + _volume4ds = self._build_volume4d_operational_intent(bbox) + now = arrow.utcnow() + base_updates = BaseUpdatesViaOperationalIntent( + start_datetime=now.shift(seconds=START_TIME_OFFSET_S).isoformat(), + end_datetime=now.shift(seconds=END_TIME_OFFSET_S).isoformat(), + flight_declaration_operational_intent=_volume4ds, + ) + + final_payload = self.template.copy() + final_payload.update(base_updates.model_dump()) + final_payload.update(FlightDeclarationViaOperationalIntentOverrides().model_dump()) + + # Validate and return as FlightDeclaration model + return FlightDeclarationViaOperationalIntent.model_validate(final_payload) + def get_flight_declaration_model(self) -> FlightDeclaration: """Helper function to get output as FlightDeclaration model.""" return self.generate() diff --git a/src/openutm_verification/simulator/models/declaration_models.py b/src/openutm_verification/simulator/models/declaration_models.py index c1c9eb3..6660951 100644 --- a/src/openutm_verification/simulator/models/declaration_models.py +++ b/src/openutm_verification/simulator/models/declaration_models.py @@ -5,6 +5,7 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator from pyproj import Geod from shapely.geometry import Polygon, box +from uas_standards.astm.f3548.v21.api import Volume4D MINIMUM_DIAGONAL_LENGTH_M = 500 # Minimum bounding box diagonal in meters @@ -19,6 +20,16 @@ class FlightDeclaration(BaseModel): model_config = ConfigDict(extra="allow") +class FlightDeclarationViaOperationalIntent(BaseModel): + """Final validated flight declaration model.""" + + start_datetime: str + end_datetime: str + operational_intent_volume4ds: dict + + model_config = ConfigDict(arbitrary_types_allowed=True) + + class FeatureCollection(BaseModel): type: Literal["FeatureCollection"] = "FeatureCollection" features: list[Feature] @@ -80,6 +91,16 @@ class BaseUpdates(BaseModel): flight_declaration_geo_json: "FeatureCollection" +class BaseUpdatesViaOperationalIntent(BaseModel): + """Model for base updates in flight declaration generation.""" + + start_datetime: str + end_datetime: str + flight_declaration_operational_intent: list[dict[Any, Any]] + + model_config = ConfigDict(arbitrary_types_allowed=True) + + class FlightDeclarationOverrides(BaseModel): """Model for overrides in flight declaration generation.""" @@ -92,3 +113,17 @@ class FlightDeclarationOverrides(BaseModel): # destination: str = "POINT(3.0 4.0)" model_config = ConfigDict(extra="allow") + + +class FlightDeclarationViaOperationalIntentOverrides(BaseModel): + """Model for overrides in flight declaration generation.""" + + flight_id: str = "FL123" + operator_id: str = "OP456" + # TODO: verify if these fields are needed + # departure_time: str = "2024-01-01T10:00:00Z" + # arrival_time: str = "2024-01-01T10:30:00Z" + # origin: str = "POINT(1.0 2.0)" + # destination: str = "POINT(3.0 4.0)" + + model_config = ConfigDict(extra="allow") diff --git a/uv.lock b/uv.lock index 97ee105..587bcb3 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" [[package]] @@ -430,16 +430,16 @@ wheels = [ [[package]] name = "implicitdict" -version = "3.0.0" +version = "4.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "arrow" }, { name = "jsonschema" }, { name = "pytimeparse" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/83/834fc20d08fc4551868b7062bd266f05d74e17cf1b854f594822cad9dd9b/implicitdict-3.0.0.tar.gz", hash = "sha256:11b9ea6d849a727b8473e9b41e7a21ebb02611c014a2b0c9e1c597c8764e631e", size = 28131, upload-time = "2025-03-05T18:22:51.345Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/92/335c435d15e2d198a679c3fbb56cd20907f204c386c1b8facdc06cea8256/implicitdict-4.0.1.tar.gz", hash = "sha256:6413da2faeb03f1d20a537f357c05a58cef8ab32e8923eae4e79ee2396546074", size = 57876, upload-time = "2025-10-01T15:07:54.467Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/10/396c1f9c22ee9a723f6751090710c32d302d8849c83921ee40a4e9c881ec/implicitdict-3.0.0-py3-none-any.whl", hash = "sha256:c2dc106d148825295c722a9edc6f766fd86cd565923b10fac5015174d3bcb073", size = 17351, upload-time = "2025-03-05T18:22:50.353Z" }, + { url = "https://files.pythonhosted.org/packages/02/ce/d947f5995b160d56a2e4a2ba48eacc59884cc2cabfb9eafe4ac49d80575d/implicitdict-4.0.1-py3-none-any.whl", hash = "sha256:71e16dc977a944c0279f5f383db63b44a14a9961b32b535e011fbba782e88a70", size = 13658, upload-time = "2025-10-01T15:07:53.603Z" }, ] [[package]] @@ -867,7 +867,7 @@ requires-dist = [ { name = "http-message-signatures", specifier = "==0.5.0" }, { name = "http-sfv", specifier = "==0.9.9" }, { name = "httpx", specifier = ">=0.27.0" }, - { name = "implicitdict", specifier = "==3.0.0" }, + { name = "implicitdict", specifier = "==4.0.1" }, { name = "ipywidgets", specifier = ">=8.1.7" }, { name = "jinja2", specifier = ">=3.1.6" }, { name = "jwcrypto", specifier = "==1.5.6" }, @@ -887,7 +887,7 @@ requires-dist = [ { name = "pyyaml", specifier = ">=6.0.2" }, { name = "redis", specifier = "==6.0.0" }, { name = "shapely", specifier = "==2.1.0" }, - { name = "uas-standards", specifier = "==3.5.0" }, + { name = "uas-standards", specifier = "==4.2.0" }, { name = "walrus", specifier = "==0.9.4" }, { name = "websocket-client", specifier = "==1.9.0" }, ] @@ -1779,14 +1779,14 @@ wheels = [ [[package]] name = "uas-standards" -version = "3.5.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "implicitdict" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/71/0e/1ca14186983c3c9dfb1253acb6a463851f937d33cc851de6214f3f952e0b/uas_standards-3.5.0.tar.gz", hash = "sha256:2646be72442c987ec992b017d70ef305affa49d736991d4f90568c96840a37f0", size = 87685, upload-time = "2025-03-13T08:33:55.078Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/f9/23a2857405c72c9bb24ca06ef55317ef4ae03e8e43a905be7bc29fa8c1e0/uas_standards-4.2.0.tar.gz", hash = "sha256:5ef61a8a163422eac8525461a950f3f4fb443563ed5b610343da625c1eb5d95f", size = 126658, upload-time = "2025-10-30T16:07:39.238Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/11/24750cdcf875bedc0bf91bcde55393921ab3eaaae8a71fbcfd1b6d2490a7/uas_standards-3.5.0-py3-none-any.whl", hash = "sha256:e699246bc9b05e667123b052b7a67a8978bb746d626f7b1be7b9e0c22adaabfc", size = 86108, upload-time = "2025-03-13T08:33:53.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6d/ac2ce5c67185b4d63e11815c705902432980dd4fb208130d110adb9a0803/uas_standards-4.2.0-py3-none-any.whl", hash = "sha256:732e2b293cfde7f75132d9abfddd63a97347273221ef2534f03676a1f0f623d5", size = 88533, upload-time = "2025-10-30T16:07:38.003Z" }, ] [[package]] From 14d6da99f7f9b924d60144190b02750125a4be38 Mon Sep 17 00:00:00 2001 From: Hrishikesh Ballal Date: Tue, 30 Dec 2025 11:57:03 +0000 Subject: [PATCH 02/12] Updated to fix creation of operational intent --- ...ation_via_operational_intent_template.json | 5 +- .../flight_blender/flight_blender_client.py | 1 + .../simulator/flight_declaration.py | 6 +-- .../simulator/models/declaration_models.py | 47 +++++++++++++++++-- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/openutm_verification/assets/simulator_templates/flight_declaration_via_operational_intent_template.json b/src/openutm_verification/assets/simulator_templates/flight_declaration_via_operational_intent_template.json index 25e18ff..089447e 100644 --- a/src/openutm_verification/assets/simulator_templates/flight_declaration_via_operational_intent_template.json +++ b/src/openutm_verification/assets/simulator_templates/flight_declaration_via_operational_intent_template.json @@ -13,9 +13,8 @@ "expect_telemetry": true, "originating_party": "Medicine Delivery Company", "contact_url": "https://utm.originatingparty.com/contact?5a7f3377-b991-4cc8-af2d-379d57f786d1", - "type_of_operation": 0, + "type_of_operation": 1, "vehicle_id": "157de9bb-6b49-496b-bf3f-0b768ce6a3b6", "operator_id": "4a725cb5-02d2-4f78-888f-b93088d324be", - "flight_declaration_geo_json": { - } + "operational_intent_volume4ds": [] } diff --git a/src/openutm_verification/core/clients/flight_blender/flight_blender_client.py b/src/openutm_verification/core/clients/flight_blender/flight_blender_client.py index 9422659..6790094 100644 --- a/src/openutm_verification/core/clients/flight_blender/flight_blender_client.py +++ b/src/openutm_verification/core/clients/flight_blender/flight_blender_client.py @@ -849,6 +849,7 @@ async def setup_flight_declaration_via_operational_intent( """Generates data and uploads flight declaration via Operational Intent.""" flight_declaration = generate_flight_declaration_via_operational_intent(flight_declaration_via_operational_intent_path) + telemetry_states = generate_telemetry(trajectory_path) self.telemetry_states = telemetry_states diff --git a/src/openutm_verification/simulator/flight_declaration.py b/src/openutm_verification/simulator/flight_declaration.py index 957b6b0..c47cf04 100644 --- a/src/openutm_verification/simulator/flight_declaration.py +++ b/src/openutm_verification/simulator/flight_declaration.py @@ -165,17 +165,17 @@ def generate_via_operational_intent(self) -> FlightDeclarationViaOperationalInte logger.info(f"Diagonal: {self.bounds.diagonal_length_m:.2f} m") _volume4ds = self._build_volume4d_operational_intent(bbox) now = arrow.utcnow() + base_updates = BaseUpdatesViaOperationalIntent( start_datetime=now.shift(seconds=START_TIME_OFFSET_S).isoformat(), end_datetime=now.shift(seconds=END_TIME_OFFSET_S).isoformat(), - flight_declaration_operational_intent=_volume4ds, + operational_intent_volume4ds=_volume4ds, ) - final_payload = self.template.copy() + final_payload = self.flight_declaration_via_operational_intent_template.copy() final_payload.update(base_updates.model_dump()) final_payload.update(FlightDeclarationViaOperationalIntentOverrides().model_dump()) - # Validate and return as FlightDeclaration model return FlightDeclarationViaOperationalIntent.model_validate(final_payload) def get_flight_declaration_model(self) -> FlightDeclaration: diff --git a/src/openutm_verification/simulator/models/declaration_models.py b/src/openutm_verification/simulator/models/declaration_models.py index 6660951..f3bf147 100644 --- a/src/openutm_verification/simulator/models/declaration_models.py +++ b/src/openutm_verification/simulator/models/declaration_models.py @@ -1,5 +1,6 @@ from __future__ import annotations +from enum import Enum from typing import Any, ClassVar, Literal from pydantic import BaseModel, ConfigDict, Field, model_validator @@ -20,14 +21,52 @@ class FlightDeclaration(BaseModel): model_config = ConfigDict(extra="allow") +class FlightBlenderOperationState(int, Enum): + """Enumeration for FlightBlender operation state values.""" + + NOT_SUBMITTED = 0 + ACCEPTED = 1 + ACTIVATED = 2 + NONCONFORMING = 3 + CONTINGENT = 4 + ENDED = 5 + WITHDRAWN = 6 + CANCELLED = 7 + REJECTED = 8 + + +class TypeOfOperation(int, Enum): + """Enumeration for type of operation values.""" + + UNKNOWN = 0 + VISUAL_LINE_OF_SIGHT = 1 + BEYOND_VISUAL_LINE_OF_SIGHT = 2 + CREWED = 3 + + class FlightDeclarationViaOperationalIntent(BaseModel): """Final validated flight declaration model.""" start_datetime: str end_datetime: str - operational_intent_volume4ds: dict - - model_config = ConfigDict(arbitrary_types_allowed=True) + operational_intent_volume4ds: list[dict] + exchange_type: str + aircraft_id: str + flight_id: str + plan_id: str + flight_state: FlightBlenderOperationState + flight_approved: bool + sequence_number: int + version: str + purpose: str + expect_telemetry: bool + originating_party: str + contact_url: str + type_of_operation: TypeOfOperation + vehicle_id: str + operator_id: str + + model_config = ConfigDict(arbitrary_types_allowed=True, use_enum_values=True) class FeatureCollection(BaseModel): @@ -96,7 +135,7 @@ class BaseUpdatesViaOperationalIntent(BaseModel): start_datetime: str end_datetime: str - flight_declaration_operational_intent: list[dict[Any, Any]] + operational_intent_volume4ds: list[dict[Any, Any]] model_config = ConfigDict(arbitrary_types_allowed=True) From 5b49b840a3b9bfc3c225afb8b76f7d079cf7abcb Mon Sep 17 00:00:00 2001 From: Hrishikesh Ballal Date: Tue, 30 Dec 2025 11:57:46 +0000 Subject: [PATCH 03/12] Update flight_blender_client.py --- .../core/clients/flight_blender/flight_blender_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/openutm_verification/core/clients/flight_blender/flight_blender_client.py b/src/openutm_verification/core/clients/flight_blender/flight_blender_client.py index 6790094..cada6c6 100644 --- a/src/openutm_verification/core/clients/flight_blender/flight_blender_client.py +++ b/src/openutm_verification/core/clients/flight_blender/flight_blender_client.py @@ -907,7 +907,8 @@ async def create_flight_declaration_via_operational_intent(self, data_files: Dat ) assert data_files.trajectory is not None, "Trajectory file path must be provided" await self.setup_flight_declaration_via_operational_intent( - flight_declaration_via_operational_intent_path=data_files.flight_declaration_via_operational_intent, trajectory_path=data_files.trajectory + flight_declaration_via_operational_intent_path=data_files.flight_declaration_via_operational_intent, + trajectory_path=data_files.trajectory, ) try: yield From ebc9c5b6e100284057f72df67ec27449c5d82f1a Mon Sep 17 00:00:00 2001 From: Hrishikesh Ballal Date: Tue, 30 Dec 2025 12:42:43 +0000 Subject: [PATCH 04/12] Updated default config --- config/default.yaml | 1 + .../scenarios/test_add_operational_intent.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/config/default.yaml b/config/default.yaml index c120df5..84318b4 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -34,6 +34,7 @@ air_traffic_simulator_settings: data_files: trajectory: "config/bern/trajectory_f1.json" # Path to flight declarations JSON file flight_declaration: "config/bern/flight_declaration.json" # Path to flight declarations JSON file + flight_declaration_via_operational_intent: "config/bern/flight_declaration_via_operational_intent.json" # Path to flight declaration via operational intent JSON file # geo_fence: "config/geo_fences.json" # Path to geo-fences # List of test scenario IDs to execute diff --git a/src/openutm_verification/scenarios/test_add_operational_intent.py b/src/openutm_verification/scenarios/test_add_operational_intent.py index 80a07e0..e25a47b 100644 --- a/src/openutm_verification/scenarios/test_add_operational_intent.py +++ b/src/openutm_verification/scenarios/test_add_operational_intent.py @@ -24,6 +24,7 @@ async def test_add_flight_declaration_via_operational_intent(fb_client: FlightBl """ async with fb_client.create_flight_declaration_via_operational_intent(data_files): + pass await fb_client.update_operation_state(new_state=OperationState.ACTIVATED, duration_seconds=5) - # await fb_client.submit_telemetry(duration_seconds=30) + await fb_client.wait_x_seconds(20) await fb_client.update_operation_state(new_state=OperationState.ENDED) From 327d1cb6a0cdb7ccf912e1593b77356efde28fba Mon Sep 17 00:00:00 2001 From: Hrishikesh Ballal Date: Tue, 30 Dec 2025 12:44:44 +0000 Subject: [PATCH 05/12] Update test_add_operational_intent.py --- .../scenarios/test_add_operational_intent.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/openutm_verification/scenarios/test_add_operational_intent.py b/src/openutm_verification/scenarios/test_add_operational_intent.py index e25a47b..8459b4b 100644 --- a/src/openutm_verification/scenarios/test_add_operational_intent.py +++ b/src/openutm_verification/scenarios/test_add_operational_intent.py @@ -24,7 +24,6 @@ async def test_add_flight_declaration_via_operational_intent(fb_client: FlightBl """ async with fb_client.create_flight_declaration_via_operational_intent(data_files): - pass await fb_client.update_operation_state(new_state=OperationState.ACTIVATED, duration_seconds=5) - await fb_client.wait_x_seconds(20) + await fb_client.wait_x_seconds(10) await fb_client.update_operation_state(new_state=OperationState.ENDED) From ba9c9a4c0ef8cc8cfd1d54302d2d980e425fb35d Mon Sep 17 00:00:00 2001 From: Hrishikesh Ballal Date: Tue, 30 Dec 2025 12:52:57 +0000 Subject: [PATCH 06/12] Update src/openutm_verification/simulator/flight_declaration.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/openutm_verification/simulator/flight_declaration.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/openutm_verification/simulator/flight_declaration.py b/src/openutm_verification/simulator/flight_declaration.py index c47cf04..3905490 100644 --- a/src/openutm_verification/simulator/flight_declaration.py +++ b/src/openutm_verification/simulator/flight_declaration.py @@ -136,12 +136,12 @@ def generate(self) -> FlightDeclaration: def _build_volume4d_operational_intent(self, bbox: Polygon) -> list[dict[str, Any]]: coordinates = self._normalize_coordinates(bbox) geometry = PolygonGeometry(coordinates=coordinates) - all_verticies = [] + all_vertices = [] WGS84_OFFSET = 200 # between the WGS1984 ellipsoid and terrain surface for polygon in geometry.coordinates: for lat_lng in polygon: - all_verticies.append(LatLngPoint(lat=lat_lng[1], lng=lat_lng[0])) - polygon = ASTMPolygon(vertices=all_verticies) + all_vertices.append(LatLngPoint(lat=lat_lng[1], lng=lat_lng[0])) + polygon = ASTMPolygon(vertices=all_vertices) volume3d = Volume3D( outline_polygon=polygon, altitude_lower=Altitude(value=WGS84_OFFSET + 50, reference="w84", units="m"), From 88c415f3d824657ac43477cf23af245424859c71 Mon Sep 17 00:00:00 2001 From: Hrishikesh Ballal Date: Tue, 30 Dec 2025 12:53:30 +0000 Subject: [PATCH 07/12] Update src/openutm_verification/assets/simulator_templates/flight_declaration_template.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../assets/simulator_templates/flight_declaration_template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openutm_verification/assets/simulator_templates/flight_declaration_template.json b/src/openutm_verification/assets/simulator_templates/flight_declaration_template.json index fa01dbc..25e18ff 100644 --- a/src/openutm_verification/assets/simulator_templates/flight_declaration_template.json +++ b/src/openutm_verification/assets/simulator_templates/flight_declaration_template.json @@ -16,6 +16,6 @@ "type_of_operation": 0, "vehicle_id": "157de9bb-6b49-496b-bf3f-0b768ce6a3b6", "operator_id": "4a725cb5-02d2-4f78-888f-b93088d324be", - "operational_intent_volume4ds": { + "flight_declaration_geo_json": { } } From b13483102dc9e4828b54ae10c3409b2ec5ae8e11 Mon Sep 17 00:00:00 2001 From: Hrishikesh Ballal Date: Tue, 30 Dec 2025 12:54:46 +0000 Subject: [PATCH 08/12] Update flight_declaration.py --- src/openutm_verification/simulator/flight_declaration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/openutm_verification/simulator/flight_declaration.py b/src/openutm_verification/simulator/flight_declaration.py index 3905490..0467ccf 100644 --- a/src/openutm_verification/simulator/flight_declaration.py +++ b/src/openutm_verification/simulator/flight_declaration.py @@ -33,7 +33,7 @@ END_TIME_OFFSET_S = 4 * 60 # Seconds from now for flight end DEFAULT_TEMPLATE_PATH: Final[Path] = Path(__file__).resolve().parent.parent / "assets" / "simulator_templates" / "flight_declaration_template.json" -OPERTATIONAL_INTENT_TEMPLATE_PATH: Final[Path] = ( +OPERATIONAL_INTENT_TEMPLATE_PATH: Final[Path] = ( Path(__file__).resolve().parent.parent / "assets" / "simulator_templates" / "flight_declaration_via_operational_intent_template.json" ) BOUNDS_FILE_PATH: Final[Path] = Path(__file__).resolve().parents[3] / "config" / "bern" / "flight_declaration.json" @@ -46,7 +46,7 @@ def __init__( self, *, template_path: Path = DEFAULT_TEMPLATE_PATH, - operational_intent_template_path: Path = OPERTATIONAL_INTENT_TEMPLATE_PATH, + operational_intent_template_path: Path = OPERATIONAL_INTENT_TEMPLATE_PATH, bounds_path: Path = BOUNDS_FILE_PATH, ): self.bounds = self._load_bounds(bounds_path) From 403ba5ce6e6bd55ad979adf87c02f15f3875439e Mon Sep 17 00:00:00 2001 From: Hrishikesh Ballal Date: Tue, 30 Dec 2025 12:56:26 +0000 Subject: [PATCH 09/12] Update src/openutm_verification/scenarios/test_add_operational_intent.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../scenarios/test_add_operational_intent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openutm_verification/scenarios/test_add_operational_intent.py b/src/openutm_verification/scenarios/test_add_operational_intent.py index 8459b4b..d80ed72 100644 --- a/src/openutm_verification/scenarios/test_add_operational_intent.py +++ b/src/openutm_verification/scenarios/test_add_operational_intent.py @@ -20,7 +20,7 @@ async def test_add_flight_declaration_via_operational_intent(fb_client: FlightBl data_files: The DataFiles instance containing file paths for telemetry, flight declaration, and geo-fence. Returns: - A ScenarioResult object containing the results of< the scenario execution. + A ScenarioResult object containing the results of the scenario execution. """ async with fb_client.create_flight_declaration_via_operational_intent(data_files): From 89ce209c0a33510e2f085c44742a81cdd47e094e Mon Sep 17 00:00:00 2001 From: Hrishikesh Ballal Date: Tue, 30 Dec 2025 12:56:37 +0000 Subject: [PATCH 10/12] Update src/openutm_verification/core/clients/flight_blender/flight_blender_client.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../core/clients/flight_blender/flight_blender_client.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/openutm_verification/core/clients/flight_blender/flight_blender_client.py b/src/openutm_verification/core/clients/flight_blender/flight_blender_client.py index cada6c6..1bd41f3 100644 --- a/src/openutm_verification/core/clients/flight_blender/flight_blender_client.py +++ b/src/openutm_verification/core/clients/flight_blender/flight_blender_client.py @@ -842,12 +842,11 @@ async def setup_flight_declaration_via_operational_intent( flight_declaration_via_operational_intent_path: str, trajectory_path: str, ) -> None: + """Generates data and uploads flight declaration via Operational Intent.""" from openutm_verification.scenarios.common import ( generate_flight_declaration_via_operational_intent, generate_telemetry, ) - - """Generates data and uploads flight declaration via Operational Intent.""" flight_declaration = generate_flight_declaration_via_operational_intent(flight_declaration_via_operational_intent_path) telemetry_states = generate_telemetry(trajectory_path) From 2d853d3395b0a982ec70e54b8f55b0668897d6ba Mon Sep 17 00:00:00 2001 From: Hrishikesh Ballal Date: Tue, 30 Dec 2025 12:56:44 +0000 Subject: [PATCH 11/12] Update pyproject.toml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dc215be..ede75b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ dependencies = [ "pydantic-settings>=2.10.1", "websocket-client==1.9.0", "markdown>=3.10", - "uas-standards==4.2.0" + "uas-standards==4.2.0", ] [project.scripts] From 005812a508d4ccd259723e6f18a4a2b56dc12eb7 Mon Sep 17 00:00:00 2001 From: Hrishikesh Ballal Date: Tue, 30 Dec 2025 12:56:52 +0000 Subject: [PATCH 12/12] Update src/openutm_verification/simulator/models/declaration_models.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/openutm_verification/simulator/models/declaration_models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/openutm_verification/simulator/models/declaration_models.py b/src/openutm_verification/simulator/models/declaration_models.py index f3bf147..2c496f2 100644 --- a/src/openutm_verification/simulator/models/declaration_models.py +++ b/src/openutm_verification/simulator/models/declaration_models.py @@ -6,7 +6,6 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator from pyproj import Geod from shapely.geometry import Polygon, box -from uas_standards.astm.f3548.v21.api import Volume4D MINIMUM_DIAGONAL_LENGTH_M = 500 # Minimum bounding box diagonal in meters