diff --git a/monitoring/uss_qualifier/configurations/dev/library/resources.yaml b/monitoring/uss_qualifier/configurations/dev/library/resources.yaml index 9db677f5ec..959f8ff398 100644 --- a/monitoring/uss_qualifier/configurations/dev/library/resources.yaml +++ b/monitoring/uss_qualifier/configurations/dev/library/resources.yaml @@ -134,7 +134,7 @@ che_conflicting_flights: file: path: file://./test_data/che/flight_intents/conflicting_flights.yaml # Note that this hash_sha512 field can be safely deleted if the content changes - hash_sha512: c35e3536d63b7dd521042cefa094dd1ecd2d3feaf31997ce6a2902361b85c42dec636bec62df853157e46e08d3fc811c00fedfd6dfe4b8bbd0506149cfeb4a17 + hash_sha512: 26ee66a5065e555512f8b1e354334678dfe1614c6fbba4898a1541e6306341620e96de8b48e4095c7b03ab6fd58d0aeeee9e69cf367e1b7346e0c5f287460792 che_invalid_flight_intents: $content_schema: monitoring/uss_qualifier/resources/definitions/ResourceDeclaration.json diff --git a/monitoring/uss_qualifier/resources/flight_planning/flight_intent_validation.py b/monitoring/uss_qualifier/resources/flight_planning/flight_intent_validation.py new file mode 100644 index 0000000000..2337680e0a --- /dev/null +++ b/monitoring/uss_qualifier/resources/flight_planning/flight_intent_validation.py @@ -0,0 +1,203 @@ +from dataclasses import dataclass +from datetime import timedelta +from typing import Optional, List, Dict, Iterator + +import arrow + +from monitoring.monitorlib.clients.flight_planning.flight_info import ( + AirspaceUsageState, + UasState, + FlightInfo, +) +from monitoring.monitorlib.clients.flight_planning.flight_info_template import ( + FlightInfoTemplate, +) +from monitoring.monitorlib.temporal import TimeDuringTest, Time +from monitoring.uss_qualifier.resources.flight_planning.flight_intent import ( + FlightIntentID, +) + +FlightIntentName = str + +MAX_TEST_RUN_DURATION = timedelta(minutes=30) +"""The longest a test run might take (to estimate flight intent timestamps prior to scenario execution)""" + + +@dataclass +class ExpectedFlightIntent(object): + intent_id: FlightIntentID + name: FlightIntentName + must_conflict_with: Optional[List[FlightIntentName]] = None + must_not_conflict_with: Optional[List[FlightIntentName]] = None + usage_state: Optional[AirspaceUsageState] = None + uas_state: Optional[UasState] = None + f3548v21_priority_higher_than: Optional[List[FlightIntentName]] = None + f3548v21_priority_equal_to: Optional[List[FlightIntentName]] = None + + +def validate_flight_intent_templates( + templates: Dict[FlightIntentID, FlightInfoTemplate], + expected_intents: List[ExpectedFlightIntent], +) -> None: + now = Time(arrow.utcnow().datetime) + times = { + TimeDuringTest.StartOfTestRun: now, + TimeDuringTest.StartOfScenario: now, + TimeDuringTest.TimeOfEvaluation: now, + } + flight_intents = {k: v.resolve(times) for k, v in templates.items()} + validate_flight_intents(flight_intents, expected_intents, now) + + later = Time(now.datetime + MAX_TEST_RUN_DURATION) + times = { + TimeDuringTest.StartOfTestRun: now, + TimeDuringTest.StartOfScenario: later, + TimeDuringTest.TimeOfEvaluation: later, + } + flight_intents = {k: v.resolve(times) for k, v in templates.items()} + validate_flight_intents(flight_intents, expected_intents, later) + + +def validate_flight_intents( + intents: Dict[FlightIntentID, FlightInfo], + expected_intents: List[ExpectedFlightIntent], + now: Time, +) -> None: + """Validate that `intents` contains all intents meeting all the criteria in `expected_intents`. + + Args: + intents: Flight intents we actually have. + expected_intents: Criteria that our flight intents are expected to meet. + now: Current time, for validation that in-use intents include this time. + + Raises: + * ValueError when a validation criterion is not met. + """ + + # Ensure all intents are present + for expected_intent in expected_intents: + if expected_intent.intent_id not in intents: + raise ValueError(f"Missing flight intent `{expected_intent.intent_id}`") + + for expected_intent in expected_intents: + intent = intents[expected_intent.intent_id] + + # Ensure in-use intent includes now + if intent.basic_information.usage_state == AirspaceUsageState.InUse: + start_time = intent.basic_information.area.time_start + if start_time is None: + raise ValueError( + f"At least one volume in `{expected_intent.intent_id}` is missing a start time" + ) + if now.datetime < start_time.datetime: + raise ValueError( + f"When evaluated at {now.datetime.isoformat()}, `{expected_intent.intent_id}`'s start time {start_time.datetime.isoformat()} is in the future even though the intent is indicated as InUse" + ) + end_time = intent.basic_information.area.time_end + if end_time is None: + raise ValueError( + f"At least one volume in `{expected_intent.intent_id}` is missing an end time" + ) + if now.datetime > end_time.datetime: + raise ValueError( + f"When evaluated at {now.datetime.isoformat()}, `{expected_intent.intent_id}`'s end time {end_time.datetime.isoformat()} is in the past even though the intent is indicated as InUse" + ) + + # Ensure not-in-use intent does not indicate an off-nominal UAS + if intent.basic_information.usage_state != AirspaceUsageState.InUse: + if intent.basic_information.uas_state != UasState.Nominal: + raise ValueError( + f"`{expected_intent.intent_id}` indicates the intent is not in use ({intent.basic_information.usage_state}), but the UAS state is specified as off-nominal ({intent.basic_information.uas_state})" + ) + + def named_intents( + name: FlightIntentName, + exclude: ExpectedFlightIntent, + no_matches_message: str, + ) -> Iterator[ExpectedFlightIntent]: + found = False + for expected_intent in expected_intents: + if expected_intent is exclude: + continue + if expected_intent.name != name: + continue + found = True + yield expected_intent + if not found: + raise ValueError(no_matches_message) + + # Ensure conflicts with other intents + if expected_intent.must_conflict_with: + for conflict_name in expected_intent.must_conflict_with: + msg = f"Invalid flight intent expectation: `{expected_intent.intent_id}` must conflict with intent name `{conflict_name}` but there are no expected flight intents with that name" + for other_expected_intent in named_intents( + conflict_name, expected_intent, msg + ): + other_intent = intents[other_expected_intent.intent_id] + if not intent.basic_information.area.intersects_vol4s( + other_intent.basic_information.area + ): + raise ValueError( + f"Flight intent `{expected_intent.intent_id}` must conflict with intent name `{conflict_name}` but there are no conflicts with `{other_expected_intent.intent_id}`" + ) + + # Ensure free of conflicts with other intents + if expected_intent.must_not_conflict_with: + for conflict_name in expected_intent.must_not_conflict_with: + msg = f"Invalid flight intent expectation: `{expected_intent.intent_id}` must not conflict with intent name `{conflict_name}` but there are no expected flight intents with that name" + for other_expected_intent in named_intents( + conflict_name, expected_intent, msg + ): + other_intent = intents[other_expected_intent.intent_id] + if intent.basic_information.area.intersects_vol4s( + other_intent.basic_information.area + ): + raise ValueError( + f"Flight intent `{expected_intent.intent_id}` must not conflict with intent name `{conflict_name}` but there is a conflict with `{other_expected_intent.intent_id}`" + ) + + # Ensure usage state + if expected_intent.usage_state: + if intent.basic_information.usage_state != expected_intent.usage_state: + raise ValueError( + f"Flight intent `{expected_intent.intent_id}` must have usage_state {expected_intent.usage_state}, but instead has usage_state {intent.basic_information.usage_state}" + ) + + # Ensure UAS state + if expected_intent.uas_state: + if intent.basic_information.uas_state != expected_intent.uas_state: + raise ValueError( + f"Flight intent `{expected_intent.intent_id}` must have uas_state {expected_intent.uas_state}, but instead has uas_state {intent.basic_information.uas_state}" + ) + + # Ensure ASTM F3548-21 priority higher than other intents + if expected_intent.f3548v21_priority_higher_than: + for priority_name in expected_intent.f3548v21_priority_higher_than: + msg = f"Invalid flight intent expectation: `{expected_intent.intent_id}` must be higher ASTM F3548-21 priority than intent `{priority_name}` but there are no expected flight intents with that name" + for other_expected_intent in named_intents( + priority_name, expected_intent, msg + ): + other_intent = intents[other_expected_intent.intent_id] + if ( + intent.astm_f3548_21.priority + <= other_intent.astm_f3548_21.priority + ): + raise ValueError( + f"Flight intent `{expected_intent.intent_id}` with ASTM F3548-21 priority {intent.astm_f3548_21.priority} must be higher priority than intent name `{priority_name}` but `{other_expected_intent.intent_id}` has priority {other_intent.astm_f3548_21.priority}" + ) + + # Ensure ASTM F3548-21 priority equal to other intents + if expected_intent.f3548v21_priority_equal_to: + for priority_name in expected_intent.f3548v21_priority_equal_to: + msg = f"Invalid flight intent expectation: `{expected_intent.intent_id}` must be equal ASTM F3548-21 priority to intent `{priority_name}` but there are no expected flight intents with that name" + for other_expected_intent in named_intents( + priority_name, expected_intent, msg + ): + other_intent = intents[other_expected_intent.intent_id] + if ( + intent.astm_f3548_21.priority + != other_intent.astm_f3548_21.priority + ): + raise ValueError( + f"Flight intent `{expected_intent.intent_id}` with ASTM F3548-21 priority {intent.astm_f3548_21.priority} must be equal priority to intent name `{priority_name}` but `{other_expected_intent.intent_id}` has priority {other_intent.astm_f3548_21.priority}" + ) diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/assets/make_assets.py b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/assets/make_assets.py index 69a57dc381..96c2b781ab 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/assets/make_assets.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/assets/make_assets.py @@ -259,21 +259,21 @@ def make_attempt_to_modify_activated_flight_into_conflict(): def make_modify_activated_flight_with_preexisting_conflict(): elements = [ svg.Polygon( - points=flight2_points, + points=flight1_points, stroke=outline, - fill=nonconforming, + fill=activated, fill_opacity=0.4, stroke_width=8, ), - svg.Text(x=60, y=90, class_=["heavy"], text="Flight 2"), + svg.Text(x=222, y=145, class_=["heavy"], text="Flight 1"), svg.Polygon( - points=flight1_points, + points=flight2_points, stroke=outline, - fill=activated, + fill=nonconforming, fill_opacity=0.4, stroke_width=8, ), - svg.Text(x=222, y=145, class_=["heavy"], text="Flight 1"), + svg.Text(x=60, y=90, class_=["heavy"], text="Flight 2"), svg.Polygon( points=translate(flight2_points, 440, 0), stroke=outline, diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/assets/modify_activated_flight_with_preexisting_conflict.svg b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/assets/modify_activated_flight_with_preexisting_conflict.svg index e4ac91baf1..a02b0c7a92 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/assets/modify_activated_flight_with_preexisting_conflict.svg +++ b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/assets/modify_activated_flight_with_preexisting_conflict.svg @@ -1 +1 @@ -Flight 2Flight 1Flight 2Flight 1m \ No newline at end of file +Flight 1Flight 2Flight 2Flight 1m \ No newline at end of file diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.md b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.md index 3596b8eefa..b4eae2886c 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.md +++ b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_equal_priority_not_permitted/conflict_equal_priority_not_permitted.md @@ -33,7 +33,7 @@ Otherwise, the FlightIntentsResource must provide the following flight intents: flight1_planned Flight 1 - Any (but all the same) + Any (but all the same) Accepted Flight 2 N/A @@ -60,10 +60,16 @@ Otherwise, the FlightIntentsResource must provide the following flight intents: flight1c_activated Activated + + equal_prio_flight2m_planned + Flight 2m + Planned + N/A + Flight 1 + equal_prio_flight2_planned Flight 2 - Planned Flight 1, Flight 1m Flight 1c @@ -82,11 +88,11 @@ test scenario. Additionally, their end time must leave sufficient time for the e sake of simplicity, it is recommended to set the start and end times of all the intents to the same range. ### tested_uss -FlightPlannerResource that is under test and will manage flight 1. +FlightPlannerResource that is under test and will manage Flight 1 and its variants. ### control_uss -FlightPlannerResource that will be used to inject conflicting flight 2. Note that this control USS needs to support the -CMSA role in order to transition some flight intents to the `Nonconforming` state. +FlightPlannerResource that will be used to inject conflicting Flight 2. Note that this control USS needs to support the +CMSA role in order to transition to the `Nonconforming` state in order to create a pre-existing conflict among equal-priority operational intents. ### dss DSSInstanceResource that provides access to a DSS instance where flight creation/sharing can be verified. @@ -95,99 +101,101 @@ DSSInstanceResource that provides access to a DSS instance where flight creation ## Attempt to plan flight into conflict test case ![Test case summary illustration](assets/attempt_to_plan_flight_into_conflict.svg) -### Plan flight 2 test step +### Plan Flight 2 test step -#### [Plan flight 2](../../../../flight_planning/plan_flight_intent.md) -Flight 2 on time range B should be successfully planned by the control USS. +#### [Plan Flight 2](../../../../flight_planning/plan_flight_intent.md) +Flight 2 should be successfully planned by the control USS. -### [Validate flight 2 sharing test step](../../validate_shared_operational_intent.md) +### [Validate Flight 2 sharing test step](../../validate_shared_operational_intent.md) -### [Activate flight 2 test step](../../../../flight_planning/activate_flight_intent.md) -Flight 2 on time range B should be successfully activated by the control USS. +### [Activate Flight 2 test step](../../../../flight_planning/activate_flight_intent.md) +Flight 2 should be successfully activated by the control USS. -### [Validate flight 2 sharing test step](../../validate_shared_operational_intent.md) -Validate that flight 2 is activated on time range B. +### [Validate Flight 2 sharing test step](../../validate_shared_operational_intent.md) -### [Attempt to plan flight 1 test step](../../../../flight_planning/plan_conflict_flight_intent.md) -The test driver attempts to plan the flight 1 on time range B via the tested USS. However, it conflicts with flight 2 +### [Attempt to plan Flight 1 test step](../../../../flight_planning/plan_conflict_flight_intent.md) +The test driver attempts to plan the Flight 1 via the tested USS. However, it conflicts with Flight 2 which is of equal priority but came first. As such it should be rejected per **[astm.f3548.v21.SCD0035](../../../../../requirements/astm/f3548/v21.md)**. -### [Validate flight 1 not shared test step](../../validate_not_shared_operational_intent.md) -Flight 1 should not have been planned. +### [Validate Flight 1 not shared test step](../../validate_not_shared_operational_intent.md) +Flight 1 should not have been shared with the interoperability ecosystem since it was rejected. ## Attempt to activate flight into conflict test case ![Test case summary illustration](assets/attempt_to_activate_flight_into_conflict.svg) -### [Attempt to directly activate conflicting flight 1 test step](../../../../flight_planning/activate_conflict_flight_intent.md) -The test driver attempts to activate directly flight 1 onto time range B, i.e. without the flight being planned before. -However, this conflicts with activated flight 2, which is of equal priority. As such it should be rejected +### [Attempt to directly activate conflicting Flight 1 test step](../../../../flight_planning/activate_conflict_flight_intent.md) +The test driver attempts to activate directly Flight 1, i.e. without the flight being planned before. +However, this conflicts with activated Flight 2, which is of equal priority. As such it should be rejected per **[astm.f3548.v21.SCD0045](../../../../../requirements/astm/f3548/v21.md)**. -### [Validate flight 1 not shared test step](../../validate_not_shared_operational_intent.md) -Flight 1 should not have been activated directly. +### [Validate Flight 1 not shared test step](../../validate_not_shared_operational_intent.md) +Flight 1 should not have been shared with the interoperability ecosystem since it was rejected. ## Attempt to modify planned flight into conflict test case ![Test case summary illustration](assets/attempt_to_modify_planned_flight_into_conflict.svg) -### [Plan flight 1 test step](../../../../flight_planning/plan_flight_intent.md) -Flight 1 on time range A should be successfully planned by the tested USS. +### [Plan Flight 1c test step](../../../../flight_planning/plan_flight_intent.md) +The smaller Flight 1c form (which doesn't conflict with Flight 2) should be successfully planned by the tested USS. -### [Validate flight 1 sharing test step](../../validate_shared_operational_intent.md) +### [Validate Flight 1c sharing test step](../../validate_shared_operational_intent.md) -### [Attempt to modify planned flight 1 into conflict test step](../../../../flight_planning/modify_planned_conflict_flight_intent.md) -The test driver attempts to modify flight 1 via the tested USS so that it becomes planned on time range B. -However, this conflicts with flight 2, which is of equal priority but was planned first. -As such it should be rejected per **[astm.f3548.v21.SCD0040](../../../../../requirements/astm/f3548/v21.md)**. +### [Attempt to modify planned Flight 1c into conflict test step](../../../../flight_planning/modify_planned_conflict_flight_intent.md) +The test driver attempts to enlarge Flight 1c so that it conflicts with Flight 2. +However, Flight 2 is of equal priority but was planned first. +As such the change to Flight 1c should be rejected per **[astm.f3548.v21.SCD0040](../../../../../requirements/astm/f3548/v21.md)**. -### [Validate flight 1 not modified test step](../../validate_shared_operational_intent.md) -Because the modification attempt was invalid, either Flight 1 should not have been modified (because the USS kept the +### [Validate Flight 1c not modified test step](../../validate_shared_operational_intent.md) +Because the modification attempt was invalid, either Flight 1c should not have been modified (because the USS kept the original accepted request), or it should have been removed (because the USS rejected the replacement plan provided). - ## Attempt to modify activated flight into conflict test case ![Test case summary illustration](assets/attempt_to_modify_activated_flight_into_conflict.svg) -### [Activate flight 1 test step](../../../../flight_planning/activate_flight_intent.md) -The test driver activates flight 1 onto its time range A, which should be done successfully. -Note that flight 1 could be either planned or non-existent before this step. In the latter case, the flight will be -directly activated without being planned beforehand. +### [Activate Flight 1c test step](../../../../flight_planning/activate_flight_intent.md) +The test driver activates the smaller Flight 1c form, which should be done successfully. +Note that Flight 1c could be either planned or non-existent before this step. In the latter case, the "user" will +directly activate the flight without planning it beforehand. -### [Validate flight 1 sharing test step](../../validate_shared_operational_intent.md) +### [Validate Flight 1c sharing test step](../../validate_shared_operational_intent.md) -### [Attempt to modify activated flight 1 into conflict test step](../../../../flight_planning/modify_activated_conflict_flight_intent.md) -The test driver attempts to modify flight 1 so that it becomes in conflict with flight 2 onto time range B. -Both flights are activated at that point. However, because the conflict did not exist when the modification was -initiated, it should be rejected per **[astm.f3548.v21.SCD0050](../../../../../requirements/astm/f3548/v21.md)**. +### [Attempt to modify activated Flight 1c into conflict test step](../../../../flight_planning/modify_activated_conflict_flight_intent.md) +The test driver attempts to enlarge Flight 1c so that it conflicts with Flight 2. +Both flights are activated at the point where that change is requested. However, because the conflict did not +exist when the modification was initiated, it should be rejected per **[astm.f3548.v21.SCD0050](../../../../../requirements/astm/f3548/v21.md)**. -### [Validate flight 1 not modified test step](../../validate_shared_operational_intent.md) -Because the modification attempt was invalid, either Flight 1 should not have been modified (because the USS kept the +### [Validate Flight 1c not modified test step](../../validate_shared_operational_intent.md) +Because the modification attempt was invalid, either Flight 1c should not have been modified (because the USS kept the original accepted request), or it should have been removed (because the USS rejected the replacement plan provided). +### [Delete Flight 2 test step](../../../../flight_planning/delete_flight_intent.md) +To prepare for the next test case, Flight 2 must be removed from the system. ## Modify activated flight with pre-existing conflict test case ![Test case summary illustration](assets/modify_activated_flight_with_preexisting_conflict.svg) -### [Activate flight 1 test step](../../../../flight_planning/activate_flight_intent.md) -The test driver activates flight 1 onto its time range A, which should be done successfully. -Note that flight 1 could be either already activated or non-existent before this step. In the former case, the call is -idempotent and nothing should change. In the latter case, the flight will be directly activated without being planned -beforehand. +### [Activate Flight 1 test step](../../../../flight_planning/activate_flight_intent.md) +The test driver activates the normal form of Flight 1, which should be done successfully since Flight 2 was removed at the end of the previous step. +Note that Flight 1 could be either already activated in the smaller Flight 1c form or non-existent before this step. In the former case, Flight 1c is enlarged to its normal size. In the latter case, the flight will be directly activated without being planned beforehand. + +### [Validate Flight 1 sharing test step](../../validate_shared_operational_intent.md) + +### [Plan Flight 2m test step](../../../../flight_planning/plan_flight_intent.md) +The smaller Flight 2 form should be successfully planned by the control USS because it does not conflict with Flight 1. -### [Validate flight 1 sharing test step](../../validate_shared_operational_intent.md) +### [Validate Flight 2m sharing test step](../../validate_shared_operational_intent.md) -### Declare flight 2 non-conforming test step -The test driver instructs the control USS to declare flight 2 as non-conforming with an off-nominal volume onto time -range A. This makes non-conforming flight 2 conflicts with activated flight 1. +### Declare Flight 2 non-conforming test step +The test driver instructs the control USS to declare Flight 2 as non-conforming. This makes non-conforming Flight 2 conflict with activated Flight 1 -- this same-priority conflict would not be allowed if Flight 2 were in a nominal state. Do note that executing this test step requires the control USS to support the CMSA role. As such, if the USS rejects the transition to non-conforming state, it will be assumed that the control USS does not support this role and the test execution will stop without failing. #### ℹ️ Successful transition to non-conforming state check -All flight intent data provided is correct and notably contains an off-nominal volume, therefore it should have been +All flight intent data provided is correct, therefore it should have been transitioned to non-conforming state by the USS per **[interuss.automated_testing.flight_planning.ExpectedBehavior](../../../../../requirements/interuss/automated_testing/flight_planning.md)**. If the USS indicates a conflict, this check will fail. If the USS indicates that the injection attempt failed, this check will fail. @@ -197,11 +205,11 @@ All flight intent data provided was complete and correct. It should have been pr to reject or accept the flight. If the USS indicates that the injection attempt failed, this check will fail per **[interuss.automated_testing.flight_planning.ExpectedBehavior](../../../../../requirements/interuss/automated_testing/flight_planning.md)**. -### [Validate flight 2 sharing test step](../../validate_shared_operational_intent.md) +### [Validate Flight 2 sharing test step](../../validate_shared_operational_intent.md) -### Attempt to modify activated flight 1 in conflict with nonconforming flight 2 test step -Before execution of this step, flight 1 is activated (onto time range A) and flight 2 is non-conforming (onto time range -A), and both are in conflict. The test driver modifies flight 1 in a way that still conflicts with flight 2 by extending +### Attempt to modify activated Flight 1 in conflict with nonconforming Flight 2 test step +Before execution of this step, Flight 1 is activated (onto time range A) and Flight 2 is non-conforming (onto time range +A), and both are in conflict. The test driver modifies Flight 1 in a way that still conflicts with Flight 2 by extending its time range A. This modification results in a conflict between the two equal priority flight that already existed before the modification was initiated. While this modification is expected to be accepted by the tested USS in general, the @@ -218,7 +226,7 @@ All flight intent data provided was complete and correct. It should have been pr to reject or accept the flight. If the USS indicates that the injection attempt failed, this check will fail per **[interuss.automated_testing.flight_planning.ExpectedBehavior](../../../../../requirements/interuss/automated_testing/flight_planning.md)**. -### [Validate flight 1 test step](../../validate_shared_operational_intent.md) +### [Validate Flight 1 test step](../../validate_shared_operational_intent.md) This step validates that the response of the USS is consistent with the flight shared, i.e. either it was properly modified, or the USS considered the attempt invalid. In the latter case, because the modification attempt was invalid, either Flight 1 should not have been modified (because the USS kept the original accepted request), or it should have 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 801d98e505..5c897ec6ff 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,13 +1,18 @@ from typing import Optional -import arrow - +from monitoring.monitorlib.clients.flight_planning.flight_info import ( + AirspaceUsageState, + UasState, +) +from monitoring.uss_qualifier.resources.flight_planning.flight_intent_validation import ( + validate_flight_intent_templates, + ExpectedFlightIntent, +) from monitoring.uss_qualifier.suites.suite import ExecutionContext from uas_standards.astm.f3548.v21.api import ( OperationalIntentReference, ) from monitoring.monitorlib.geotemporal import Volume4DCollection -from uas_standards.astm.f3548.v21.api import OperationalIntentState from monitoring.uss_qualifier.resources.astm.f3548.v21 import DSSInstanceResource from monitoring.uss_qualifier.resources.astm.f3548.v21.dss import DSSInstance @@ -41,6 +46,7 @@ cleanup_flights, activate_flight_intent, submit_flight_intent, + delete_flight_intent, ) from uas_standards.interuss.automated_testing.scd.v1.api import ( InjectFlightResponseResult, @@ -56,6 +62,7 @@ class ConflictEqualPriorityNotPermitted(TestScenario): flight1c_activated: FlightIntent flight2_id: Optional[str] = None + flight2m_planned: FlightIntent flight2_planned: FlightIntent flight2_activated: FlightIntent flight2_nonconforming: FlightIntent @@ -76,143 +83,93 @@ def __init__( self.control_uss = control_uss.flight_planner self.dss = dss.dss - if not flight_intents: - msg = f"No FlightIntentsResource was provided as input to this test, it is assumed that the jurisdiction of the tested USS ({self.tested_uss.config.participant_id}) does not allow any same priority conflicts, execution of the scenario was stopped without failure" - self.record_note( - "Jurisdiction of tested USS does not allow any same priority conflicts", - msg, + expected_flight_intents = [ + ExpectedFlightIntent( + "flight1_planned", + "Flight 1", + must_conflict_with=["Flight 2"], + usage_state=AirspaceUsageState.Planned, + uas_state=UasState.Nominal, + ), + ExpectedFlightIntent( + "flight1_activated", + "Flight 1", + must_conflict_with=["Flight 2"], + usage_state=AirspaceUsageState.InUse, + uas_state=UasState.Nominal, + ), + ExpectedFlightIntent( + "flight1m_activated", + "Flight 1m", + must_conflict_with=["Flight 2"], + usage_state=AirspaceUsageState.InUse, + uas_state=UasState.Nominal, + ), + ExpectedFlightIntent( + "flight1c_planned", + "Flight 1c", + must_not_conflict_with=["Flight 2"], + usage_state=AirspaceUsageState.Planned, + uas_state=UasState.Nominal, + ), + ExpectedFlightIntent( + "flight1c_activated", + "Flight 1c", + must_not_conflict_with=["Flight 2"], + usage_state=AirspaceUsageState.InUse, + uas_state=UasState.Nominal, + ), + ExpectedFlightIntent( + "equal_prio_flight2m_planned", + "Flight 2m", + must_not_conflict_with=["Flight 1"], + usage_state=AirspaceUsageState.Planned, + uas_state=UasState.Nominal, + ), + ExpectedFlightIntent( + "equal_prio_flight2_planned", + "Flight 2", + must_conflict_with=["Flight 1", "Flight 1m"], + must_not_conflict_with=["Flight 1c"], + usage_state=AirspaceUsageState.Planned, + uas_state=UasState.Nominal, + ), + ExpectedFlightIntent( + "equal_prio_flight2_activated", + "Flight 2", + must_conflict_with=["Flight 1", "Flight 1m"], + must_not_conflict_with=["Flight 1c"], + usage_state=AirspaceUsageState.InUse, + uas_state=UasState.Nominal, + ), + ExpectedFlightIntent( + "equal_prio_flight2_nonconforming", + "Flight 2", + must_conflict_with=["Flight 1", "Flight 1m"], + must_not_conflict_with=["Flight 1c"], + usage_state=AirspaceUsageState.InUse, + ), # Note: this intent expected to produce Nonconforming state, but this is hard to verify without telemetry. UAS state is not actually off-nominal. + ] + + templates = flight_intents.get_flight_intents() + try: + validate_flight_intent_templates(templates, expected_flight_intents) + except ValueError as e: + raise ValueError( + f"`{self.me()}` TestScenario requirements for flight_intents not met: {e}" ) - raise ScenarioCannotContinueError(msg) - - _flight_intents = { - k: FlightIntent.from_flight_info_template(v) - for k, v in flight_intents.get_flight_intents().items() - } extents = [] - for intent in _flight_intents.values(): + for efi in expected_flight_intents: + intent = FlightIntent.from_flight_info_template(templates[efi.intent_id]) extents.extend(intent.request.operational_intent.volumes) extents.extend(intent.request.operational_intent.off_nominal_volumes) + setattr(self, efi.intent_id.replace("equal_prio_", ""), intent) + self._intents_extent = Volume4DCollection.from_interuss_scd_api( extents ).bounding_volume.to_f3548v21() - try: - ( - self.flight1_planned, - self.flight1_activated, - self.flight1m_activated, - self.flight1c_planned, - self.flight1c_activated, - self.flight2_planned, - self.flight2_activated, - self.flight2_nonconforming, - ) = ( - _flight_intents["flight1_planned"], - _flight_intents["flight1_activated"], - _flight_intents["flight1m_activated"], - _flight_intents["flight1c_planned"], - _flight_intents["flight1c_activated"], - _flight_intents["equal_prio_flight2_planned"], - _flight_intents["equal_prio_flight2_activated"], - _flight_intents["equal_prio_flight2_nonconforming"], - ) - - now = arrow.utcnow().datetime - for intent_name, intent in _flight_intents.items(): - if ( - intent.request.operational_intent.state - == OperationalIntentState.Activated - ): - assert Volume4DCollection.from_interuss_scd_api( - intent.request.operational_intent.volumes - + intent.request.operational_intent.off_nominal_volumes - ).has_active_volume( - now - ), f"at least one volume of activated intent {intent_name} must be active now (now is {now})" - - assert ( - self.flight1_planned.request.operational_intent.state - == OperationalIntentState.Accepted - ), "flight1_planned must have state Accepted" - assert ( - self.flight1_activated.request.operational_intent.state - == OperationalIntentState.Activated - ), "flight1_activated must have state Activated" - assert ( - self.flight1m_activated.request.operational_intent.state - == OperationalIntentState.Activated - ), "flight1m_activated must have state Activated" - assert ( - self.flight1c_planned.request.operational_intent.state - == OperationalIntentState.Accepted - ), "flight1c_planned must have state Accepted" - assert ( - self.flight1c_activated.request.operational_intent.state - == OperationalIntentState.Activated - ), "flight1c_activated must have state Activated" - assert ( - self.flight2_planned.request.operational_intent.state - == OperationalIntentState.Accepted - ), "equal_prio_flight2_planned must have state Accepted" - assert ( - self.flight2_activated.request.operational_intent.state - == OperationalIntentState.Activated - ), "equal_prio_flight2_activated must have state Activated" - assert ( - self.flight2_nonconforming.request.operational_intent.state - == OperationalIntentState.Nonconforming - ), "equal_prio_flight2_nonconforming must have state Nonconforming" - - assert ( - self.flight2_planned.request.operational_intent.priority - == self.flight1_planned.request.operational_intent.priority - ), "flight_2 must have priority equal to flight_1" - assert not Volume4DCollection.from_interuss_scd_api( - self.flight1_planned.request.operational_intent.volumes - ).intersects_vol4s( - Volume4DCollection.from_interuss_scd_api( - self.flight2_planned.request.operational_intent.volumes - ) - ), "flight1_planned and equal_prio_flight2_planned must not intersect" - assert not Volume4DCollection.from_interuss_scd_api( - self.flight1_planned.request.operational_intent.volumes - ).intersects_vol4s( - Volume4DCollection.from_interuss_scd_api( - self.flight1c_activated.request.operational_intent.volumes - ) - ), "flight1_planned and flight1c_activated must not intersect" - assert Volume4DCollection.from_interuss_scd_api( - self.flight1c_activated.request.operational_intent.volumes - ).intersects_vol4s( - Volume4DCollection.from_interuss_scd_api( - self.flight2_activated.request.operational_intent.volumes - ) - ), "flight1c_activated and equal_prio_flight2_activated must intersect" - assert Volume4DCollection.from_interuss_scd_api( - self.flight1_activated.request.operational_intent.volumes - ).intersects_vol4s( - Volume4DCollection.from_interuss_scd_api( - self.flight2_nonconforming.request.operational_intent.off_nominal_volumes - ) - ), "flight1_activated.volumes and equal_prio_flight2_nonconforming.off_nominal_volumes must intersect" - - assert ( - len( - self.flight2_nonconforming.request.operational_intent.off_nominal_volumes - ) - > 0 - ), "equal_prio_flight2_nonconforming must have off-nominal volume" - - except KeyError as e: - raise ValueError( - f"`{self.me()}` TestScenario requirements for flight_intents not met: missing flight intent {e}" - ) - except AssertionError as e: - raise ValueError( - f"`{self.me()}` TestScenario requirements for flight_intents not met: {e}" - ) - def run(self, context: ExecutionContext): self.begin_test_scenario(context) @@ -226,7 +183,7 @@ def run(self, context: ExecutionContext): ) self.begin_test_case("Attempt to plan flight into conflict") - flight_2_oi_ref = self._attempt_plan_flight_conflict() + _ = self._attempt_plan_flight_conflict() self.end_test_case() self.begin_test_case("Attempt to activate flight into conflict") @@ -244,9 +201,7 @@ def run(self, context: ExecutionContext): self.end_test_case() self.begin_test_case("Modify activated flight with pre-existing conflict") - self._modify_activated_flight_preexisting_conflict( - flight_1_oi_ref, flight_2_oi_ref - ) + self._modify_activated_flight_preexisting_conflict(flight_1_oi_ref) self.end_test_case() self.end_test_scenario() @@ -256,12 +211,12 @@ def _attempt_plan_flight_conflict(self) -> OperationalIntentReference: self, self.control_uss, self.dss, - "Validate flight 2 sharing", + "Validate Flight 2 sharing", self._intents_extent, ) as validator: _, self.flight2_id = plan_flight_intent( self, - "Plan flight 2", + "Plan Flight 2", self.control_uss, self.flight2_planned.request, ) @@ -271,13 +226,13 @@ def _attempt_plan_flight_conflict(self) -> OperationalIntentReference: self, self.control_uss, self.dss, - "Validate flight 2 sharing", + "Validate Flight 2 sharing", self._intents_extent, flight_2_oi_ref, ) as validator: activate_flight_intent( self, - "Activate flight 2", + "Activate Flight 2", self.control_uss, self.flight2_activated.request, self.flight2_id, @@ -288,14 +243,14 @@ def _attempt_plan_flight_conflict(self) -> OperationalIntentReference: self, self.tested_uss, self.dss, - "Validate flight 1 not shared", + "Validate Flight 1 not shared", self._intents_extent, ) as validator: plan_conflict_flight_intent( self, - "Attempt to plan flight 1", + "Attempt to plan Flight 1", self.tested_uss, - self.flight1c_planned.request, + self.flight1_planned.request, ) validator.expect_not_shared() @@ -306,14 +261,14 @@ def _attempt_activate_flight_conflict(self): self, self.tested_uss, self.dss, - "Validate flight 1 not shared", + "Validate Flight 1 not shared", self._intents_extent, ) as validator: activate_conflict_flight_intent( self, - "Attempt to directly activate conflicting flight 1", + "Attempt to directly activate conflicting Flight 1", self.tested_uss, - self.flight1c_activated.request, + self.flight1_activated.request, self.flight1_id, ) validator.expect_not_shared() @@ -325,34 +280,34 @@ def _attempt_modify_planned_flight_conflict( self, self.tested_uss, self.dss, - "Validate flight 1 sharing", + "Validate Flight 1c sharing", self._intents_extent, ) as validator: _, self.flight1_id = plan_flight_intent( self, - "Plan flight 1", + "Plan Flight 1c", self.tested_uss, - self.flight1_planned.request, + self.flight1c_planned.request, ) - flight_1_oi_ref = validator.expect_shared(self.flight1_planned.request) + flight_1_oi_ref = validator.expect_shared(self.flight1c_planned.request) with OpIntentValidator( self, self.tested_uss, self.dss, - "Validate flight 1 not modified", + "Validate Flight 1c not modified", self._intents_extent, flight_1_oi_ref, ) as validator: modify_planned_conflict_flight_intent( self, - "Attempt to modify planned flight 1 into conflict", + "Attempt to modify planned Flight 1c into conflict", self.tested_uss, - self.flight1c_planned.request, + self.flight1_planned.request, self.flight1_id, ) flight_1_oi_ref = validator.expect_shared( - self.flight1_planned.request, skip_if_not_found=True + self.flight1c_planned.request, skip_if_not_found=True ) return flight_1_oi_ref @@ -364,56 +319,60 @@ def _attempt_modify_activated_flight_conflict( self, self.tested_uss, self.dss, - "Validate flight 1 sharing", + "Validate Flight 1c sharing", self._intents_extent, flight_1_oi_ref, ) as validator: activate_flight_intent( self, - "Activate flight 1", + "Activate Flight 1c", self.tested_uss, - self.flight1_activated.request, + self.flight1c_activated.request, self.flight1_id, ) - flight_1_oi_ref = validator.expect_shared(self.flight1_activated.request) + flight_1_oi_ref = validator.expect_shared(self.flight1c_activated.request) with OpIntentValidator( self, self.tested_uss, self.dss, - "Validate flight 1 not modified", + "Validate Flight 1c not modified", self._intents_extent, flight_1_oi_ref, ) as validator: modify_activated_conflict_flight_intent( self, - "Attempt to modify activated flight 1 into conflict", + "Attempt to modify activated Flight 1c into conflict", self.tested_uss, - self.flight1c_activated.request, + self.flight1_activated.request, self.flight1_id, ) flight_1_oi_ref = validator.expect_shared( - self.flight1_activated.request, skip_if_not_found=True + self.flight1c_activated.request, skip_if_not_found=True ) + _ = delete_flight_intent( + self, "Delete Flight 2", self.control_uss, self.flight2_id + ) + self.flight2_id = None + return flight_1_oi_ref def _modify_activated_flight_preexisting_conflict( self, flight_1_oi_ref: Optional[OperationalIntentReference], - flight_2_oi_ref: Optional[OperationalIntentReference], ): with OpIntentValidator( self, self.tested_uss, self.dss, - "Validate flight 1 sharing", + "Validate Flight 1 sharing", self._intents_extent, flight_1_oi_ref, ) as validator: activate_flight_intent( self, - "Activate flight 1", + "Activate Flight 1", self.tested_uss, self.flight1_activated.request, self.flight1_id, @@ -424,13 +383,28 @@ def _modify_activated_flight_preexisting_conflict( self, self.control_uss, self.dss, - "Validate flight 2 sharing", + "Validate Flight 2m sharing", + self._intents_extent, + ) as validator: + _, self.flight2_id = plan_flight_intent( + self, + "Plan Flight 2m", + self.control_uss, + self.flight2m_planned.request, + ) + flight_2_oi_ref = validator.expect_shared(self.flight2m_planned.request) + + with OpIntentValidator( + self, + self.control_uss, + self.dss, + "Validate Flight 2 sharing", self._intents_extent, flight_2_oi_ref, ) as validator: resp_flight_2, _ = submit_flight_intent( self, - "Declare flight 2 non-conforming", + "Declare Flight 2 non-conforming", "Successful transition to non-conforming state", { InjectFlightResponseResult.ReadyToFly, @@ -452,13 +426,13 @@ def _modify_activated_flight_preexisting_conflict( self, self.tested_uss, self.dss, - "Validate flight 1", + "Validate Flight 1", self._intents_extent, flight_1_oi_ref, ) as validator: resp_flight_1, _ = submit_flight_intent( self, - "Attempt to modify activated flight 1 in conflict with nonconforming flight 2", + "Attempt to modify activated Flight 1 in conflict with nonconforming Flight 2", "Successful modification or rejection", { InjectFlightResponseResult.ReadyToFly, diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.md b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.md index c443e793a5..fec8fa805a 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.md +++ b/monitoring/uss_qualifier/scenarios/astm/utm/nominal_planning/conflict_higher_priority/conflict_higher_priority.md @@ -52,7 +52,7 @@ FlightIntentsResource that provides the following flight intents: flight1c_activated Flight 1c Activated - Flight 2 + Flight 2m N/A @@ -80,10 +80,10 @@ test scenario. Additionally, their end time must leave sufficient time for the sake of simplicity, it is recommended to set the start and end times of all the intents to the same range. ### tested_uss -FlightPlannerResource that is under test and will manage flight 1. +FlightPlannerResource that is under test and will manage Flight 1. ### control_uss -FlightPlannerResource that will manage conflicting flight 2. +FlightPlannerResource that will manage conflicting Flight 2. ### dss DSSInstanceResource that provides access to a DSS instance where flight creation/sharing can be verified. @@ -91,40 +91,40 @@ DSSInstanceResource that provides access to a DSS instance where flight creation ## Attempt to plan flight in conflict test case ![Test case summary illustration](assets/attempt_to_plan_flight_into_conflict.svg) -### [Plan flight 2 test step](../../../../flight_planning/plan_flight_intent.md) +### [Plan Flight 2 test step](../../../../flight_planning/plan_flight_intent.md) The higher priority flight should be successfully planned by the control USS. -### [Validate flight 2 sharing test step](../../validate_shared_operational_intent.md) +### [Validate Flight 2 sharing test step](../../validate_shared_operational_intent.md) -### [Attempt to plan flight 1 test step](../../../../flight_planning/plan_priority_conflict_flight_intent.md) -The test driver attempts to plan the flight 1 via the tested USS. However, it conflicts with flight 2, which is of +### [Attempt to plan Flight 1 test step](../../../../flight_planning/plan_priority_conflict_flight_intent.md) +The test driver attempts to plan the Flight 1 via the tested USS. However, it conflicts with Flight 2, which is of higher priority. As such it should be rejected per **[astm.f3548.v21.SCD0015](../../../../../requirements/astm/f3548/v21.md)**. -### [Validate flight 1 not shared test step](../../validate_not_shared_operational_intent.md) +### [Validate Flight 1 not shared test step](../../validate_not_shared_operational_intent.md) -### [Delete flight 2 test step](../../../../flight_planning/delete_flight_intent.md) +### [Delete Flight 2 test step](../../../../flight_planning/delete_flight_intent.md) ## Attempt to modify planned flight in conflict test case ![Test case summary illustration](assets/attempt_to_modify_planned_flight_into_conflict.svg) -### [Plan flight 1 test step](../../../../flight_planning/plan_flight_intent.md) +### [Plan Flight 1 test step](../../../../flight_planning/plan_flight_intent.md) The first flight should be successfully planned by the tested USS. -### [Validate flight 1 sharing test step](../../validate_shared_operational_intent.md) +### [Validate Flight 1 sharing test step](../../validate_shared_operational_intent.md) -### [Plan flight 2 test step](../../../../flight_planning/plan_flight_intent.md) +### [Plan Flight 2 test step](../../../../flight_planning/plan_flight_intent.md) The second flight should be successfully planned by the control USS. -It conflicts with flight 1, but it has higher priority. +It conflicts with Flight 1, but it has higher priority. -### [Validate flight 2 sharing test step](../../validate_shared_operational_intent.md) +### [Validate Flight 2 sharing test step](../../validate_shared_operational_intent.md) -### [Attempt to modify planned flight 1 in conflict test step](../../../../flight_planning/modify_planned_priority_conflict_flight_intent.md) -The test driver attempts to modify flight 1 via the tested USS, which is planned. -However, it conflicts with flight 2, which is of higher priority and was planned in the meantime. +### [Attempt to modify planned Flight 1 in conflict test step](../../../../flight_planning/modify_planned_priority_conflict_flight_intent.md) +The test driver attempts to modify Flight 1 via the tested USS, which is planned. +However, it conflicts with Flight 2, which is of higher priority and was planned in the meantime. As such it should be rejected per **[astm.f3548.v21.SCD0020](../../../../../requirements/astm/f3548/v21.md)**. -### [Validate flight 1 not modified test step](../../validate_shared_operational_intent.md) +### [Validate Flight 1 not modified test step](../../validate_shared_operational_intent.md) Because the modification attempt was invalid, either Flight 1 should not have been modified (because the USS kept the original accepted request), or it should have been removed (because the USS rejected the replacement plan provided). @@ -132,12 +132,12 @@ original accepted request), or it should have been removed (because the USS reje ## Attempt to activate flight in conflict test case ![Test case summary illustration](assets/attempt_to_activate_flight_into_conflict.svg) -### [Attempt to activate conflicting flight 1 test step](../../../../flight_planning/activate_priority_conflict_flight_intent.md) -The test driver attempts to activate flight 1, however, it conflicts with flight 2, which is also planned and of -higher priority. Note that flight 1 could be either planned or non-existent before this step. +### [Attempt to activate conflicting Flight 1 test step](../../../../flight_planning/activate_priority_conflict_flight_intent.md) +The test driver attempts to activate Flight 1, however, it conflicts with Flight 2, which is also planned and of +higher priority. Note that Flight 1 could be either planned or non-existent before this step. As such it should be rejected per **[astm.f3548.v21.SCD0025](../../../../../requirements/astm/f3548/v21.md)**. -### [Validate flight 1 not activated test step](../../validate_shared_operational_intent.md) +### [Validate Flight 1 not activated test step](../../validate_shared_operational_intent.md) Because the modification attempt was invalid, either Flight 1 should not have been modified (because the USS kept the original accepted request), or it should have been removed (because the USS rejected the replacement plan provided). @@ -145,31 +145,31 @@ original accepted request), or it should have been removed (because the USS reje ## Modify activated flight with pre-existing conflict test case ![Test case summary illustration](assets/modify_activated_flight_with_preexisting_conflict.svg) -### [Delete flight 2 test step](../../../../flight_planning/delete_flight_intent.md) +### [Delete Flight 2 test step](../../../../flight_planning/delete_flight_intent.md) -### [Activate flight 1 test step](../../../../flight_planning/activate_flight_intent.md) -The test driver activates flight 1, which should be done successfully given that it is now the highest-priority flight. -Note that flight 1 could be either planned or non-existent before this step. In the latter case, the flight will be +### [Activate Flight 1 test step](../../../../flight_planning/activate_flight_intent.md) +The test driver activates Flight 1, which should be done successfully given that it is now the highest-priority flight. +Note that Flight 1 could be either planned or non-existent before this step. In the latter case, the flight will be directly activated without being planned beforehand. -### [Validate flight 1 sharing test step](../../validate_shared_operational_intent.md) +### [Validate Flight 1 sharing test step](../../validate_shared_operational_intent.md) -### [Plan flight 2 test step](../../../../flight_planning/plan_flight_intent.md) +### [Plan Flight 2 test step](../../../../flight_planning/plan_flight_intent.md) The second flight should be successfully planned by the control USS. -### [Validate flight 2 sharing test step](../../validate_shared_operational_intent.md) +### [Validate Flight 2 sharing test step](../../validate_shared_operational_intent.md) -### [Activate flight 2 test step](../../../../flight_planning/activate_flight_intent.md) -The test driver activates flight 2, which should be done successfully given that it is the highest-priority flight. +### [Activate Flight 2 test step](../../../../flight_planning/activate_flight_intent.md) +The test driver activates Flight 2, which should be done successfully given that it is the highest-priority flight. -### [Validate flight 2 sharing test step](../../validate_shared_operational_intent.md) +### [Validate Flight 2 sharing test step](../../validate_shared_operational_intent.md) -### [Modify activated flight 1 in conflict with activated flight 2 test step](../../../../flight_planning/modify_activated_flight_intent.md) +### [Modify activated Flight 1 in conflict with activated Flight 2 test step](../../../../flight_planning/modify_activated_flight_intent.md) Before execution of this step, flights 1 and 2 are activated and in conflict. Flight 2 is the highest-priority flight. -The test driver attempts to modify flight 1 in a way that still conflicts with flight 2. +The test driver attempts to modify Flight 1 in a way that still conflicts with Flight 2. The successful outcomes of the modification attempts: -1. Even though flight 2 is the highest-priority flight, because the conflict existed before the modification was +1. Even though Flight 2 is the highest-priority flight, because the conflict existed before the modification was initiated, an accepted modification is considered a success per **[astm.f3548.v21.SCD0030](../../../../../requirements/astm/f3548/v21.md)**. 2. Due to the conflict, the USS may decide to be more conservative and to not support the modification. This is considered a success as there is no positive requirement for the USS to accept the modification. @@ -184,29 +184,29 @@ In any case, whatever is the outcome of this step, there should not be any impac scenario. An intent should exist (this is checked in the next step) and it should be either the previous or the modified intent, both of which make no difference in the next steps. -### [Validate flight 1 sharing test step](../../validate_shared_operational_intent.md) -If the modification was accepted, flight 1 should have been modified. -If the modification was not supported, flight 1 should not have been modified. -If the modification was rejected, flight 1 should not have been modified and should still exist. If it does not exist, +### [Validate Flight 1 sharing test step](../../validate_shared_operational_intent.md) +If the modification was accepted, Flight 1 should have been modified. +If the modification was not supported, Flight 1 should not have been modified. +If the modification was rejected, Flight 1 should not have been modified and should still exist. If it does not exist, it means that there is an active flight without an operational intent, which is a failure to meet **[interuss.automated_testing.flight_planning.FlightCoveredByOperationalIntent](../../../../../requirements/interuss/automated_testing/flight_planning.md)**. ## Attempt to modify activated flight in conflict test case ![Test case summary illustration](assets/attempt_to_modify_activated_flight_into_conflict.svg) -### [Modify activated flight 2 to not conflict with activated flight 1 test step](../../../../flight_planning/modify_planned_flight_intent.md) -The test driver modifies (activated) flight 2 with the control USS so that it is not anymore in conflict with (activated) +### [Modify activated Flight 2 to not conflict with activated Flight 1 test step](../../../../flight_planning/modify_planned_flight_intent.md) +The test driver modifies (activated) Flight 2 with the control USS so that it is not anymore in conflict with (activated) flight of test USS. -As flight 2 is of higher priority, this should succeed and leave flight 1 clear of conflict. +As Flight 2 is of higher priority, this should succeed and leave Flight 1 clear of conflict. -### [Validate flight 2 sharing test step](../../validate_shared_operational_intent.md) +### [Validate Flight 2 sharing test step](../../validate_shared_operational_intent.md) -### [Attempt to modify activated flight 1 in conflict test step](../../../../flight_planning/modify_activated_priority_conflict_flight_intent.md) -The test driver attempts to modify flight 1 so that it becomes in conflict with flight 2. Both flights are activated at that point. +### [Attempt to modify activated Flight 1 in conflict test step](../../../../flight_planning/modify_activated_priority_conflict_flight_intent.md) +The test driver attempts to modify Flight 1 so that it becomes in conflict with Flight 2. Both flights are activated at that point. However, because the conflict did not exist when the modification was initiated, it should be rejected per **[astm.f3548.v21.SCD0030](../../../../../requirements/astm/f3548/v21.md)**. -### [Validate flight 1 not modified test step](../../validate_shared_operational_intent.md) +### [Validate Flight 1 not modified test step](../../validate_shared_operational_intent.md) Because the modification attempt was invalid, either Flight 1 should not have been modified (because the USS kept the original accepted request), or it should have been removed (because the USS rejected the replacement plan provided). 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 e26bd75ecd..591f7ad78c 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,14 +1,19 @@ from typing import Optional, Tuple -import arrow - +from monitoring.monitorlib.clients.flight_planning.flight_info import ( + AirspaceUsageState, + UasState, +) +from monitoring.uss_qualifier.resources.flight_planning.flight_intent_validation import ( + ExpectedFlightIntent, + validate_flight_intent_templates, +) from monitoring.uss_qualifier.suites.suite import ExecutionContext from uas_standards.astm.f3548.v21.api import ( OperationalIntentReference, ) -from monitoring.monitorlib.geotemporal import Volume4DCollection -from uas_standards.astm.f3548.v21.api import OperationalIntentState +from monitoring.monitorlib.geotemporal import Volume4DCollection, Volume4D from uas_standards.interuss.automated_testing.scd.v1.api import ( InjectFlightResponseResult, ) @@ -63,6 +68,8 @@ class ConflictHigherPriority(TestScenario): control_uss: FlightPlanner dss: DSSInstance + _intents_extent: Volume4D + def __init__( self, flight_intents: FlightIntentsResource, @@ -75,123 +82,90 @@ def __init__( self.control_uss = control_uss.flight_planner self.dss = dss.dss - _flight_intents = { - k: FlightIntent.from_flight_info_template(v) - for k, v in flight_intents.get_flight_intents().items() - } + expected_flight_intents = [ + ExpectedFlightIntent( + "flight1_planned", + "Flight 1", + must_conflict_with=["Flight 2"], + must_not_conflict_with=["Flight 2m"], + usage_state=AirspaceUsageState.Planned, + uas_state=UasState.Nominal, + ), + ExpectedFlightIntent( + "flight1_activated", + "Flight 1", + must_conflict_with=["Flight 2"], + must_not_conflict_with=["Flight 2m"], + usage_state=AirspaceUsageState.InUse, + uas_state=UasState.Nominal, + ), + ExpectedFlightIntent( + "flight1m_planned", + "Flight 1m", + must_conflict_with=["Flight 2"], + usage_state=AirspaceUsageState.Planned, + uas_state=UasState.Nominal, + ), + ExpectedFlightIntent( + "flight1m_activated", + "Flight 1m", + must_conflict_with=["Flight 2"], + usage_state=AirspaceUsageState.InUse, + uas_state=UasState.Nominal, + ), + ExpectedFlightIntent( + "flight1c_activated", + "Flight 1c", + must_conflict_with=["Flight 2m"], + usage_state=AirspaceUsageState.InUse, + uas_state=UasState.Nominal, + ), + ExpectedFlightIntent( + "flight2_planned", + "Flight 2", + must_conflict_with=["Flight 1"], + usage_state=AirspaceUsageState.Planned, + uas_state=UasState.Nominal, + f3548v21_priority_higher_than=["Flight 1"], + ), + ExpectedFlightIntent( + "flight2_activated", + name="Flight 2", + must_conflict_with=["Flight 1"], + usage_state=AirspaceUsageState.InUse, + uas_state=UasState.Nominal, + f3548v21_priority_higher_than=["Flight 1"], + ), + ExpectedFlightIntent( + "flight2m_activated", + name="Flight 2m", + must_conflict_with=["Flight 1c"], + must_not_conflict_with=["Flight 1"], + usage_state=AirspaceUsageState.InUse, + uas_state=UasState.Nominal, + f3548v21_priority_higher_than=["Flight 1"], + ), + ] + + templates = flight_intents.get_flight_intents() + try: + validate_flight_intent_templates(templates, expected_flight_intents) + except ValueError as e: + raise ValueError( + f"`{self.me()}` TestScenario requirements for flight_intents not met: {e}" + ) extents = [] - for intent in _flight_intents.values(): + for efi in expected_flight_intents: + intent = FlightIntent.from_flight_info_template(templates[efi.intent_id]) extents.extend(intent.request.operational_intent.volumes) extents.extend(intent.request.operational_intent.off_nominal_volumes) + setattr(self, efi.intent_id, intent) + self._intents_extent = Volume4DCollection.from_interuss_scd_api( extents ).bounding_volume.to_f3548v21() - try: - ( - self.flight1_planned, - self.flight1m_planned, - self.flight1_activated, - self.flight1m_activated, - self.flight1c_activated, - self.flight2_planned, - self.flight2_activated, - self.flight2m_activated, - ) = ( - _flight_intents["flight1_planned"], - _flight_intents["flight1m_planned"], - _flight_intents["flight1_activated"], - _flight_intents["flight1m_activated"], - _flight_intents["flight1c_activated"], - _flight_intents["flight2_planned"], - _flight_intents["flight2_activated"], - _flight_intents["flight2m_activated"], - ) - - now = arrow.utcnow().datetime - for intent_name, intent in _flight_intents.items(): - if ( - intent.request.operational_intent.state - == OperationalIntentState.Activated - ): - assert Volume4DCollection.from_interuss_scd_api( - intent.request.operational_intent.volumes - + intent.request.operational_intent.off_nominal_volumes - ).has_active_volume( - now - ), f"at least one volume of activated intent {intent_name} must be active now (now is {now})" - - assert ( - self.flight1_planned.request.operational_intent.state - == OperationalIntentState.Accepted - ), "flight1_planned must have state Accepted" - assert ( - self.flight1m_planned.request.operational_intent.state - == OperationalIntentState.Accepted - ), "flight1m_planned must have state Accepted" - assert ( - self.flight1_activated.request.operational_intent.state - == OperationalIntentState.Activated - ), "flight1_activated must have state Activated" - assert ( - self.flight1m_activated.request.operational_intent.state - == OperationalIntentState.Activated - ), "flight1m_activated must have state Activated" - assert ( - self.flight1c_activated.request.operational_intent.state - == OperationalIntentState.Activated - ), "flight1c_activated must have state Activated" - assert ( - self.flight2_planned.request.operational_intent.state - == OperationalIntentState.Accepted - ), "flight2_planned must have state Accepted" - assert ( - self.flight2_activated.request.operational_intent.state - == OperationalIntentState.Activated - ), "flight2_activated must have state Activated" - assert ( - self.flight2m_activated.request.operational_intent.state - == OperationalIntentState.Activated - ), "flight2m_activated must have state Activated" - - # TODO: check that flight data is the same across the different versions of the flight - - assert ( - self.flight2_planned.request.operational_intent.priority - > self.flight1_planned.request.operational_intent.priority - ), "flight_2 must have higher priority than flight_1" - assert Volume4DCollection.from_interuss_scd_api( - self.flight1_planned.request.operational_intent.volumes - ).intersects_vol4s( - Volume4DCollection.from_interuss_scd_api( - self.flight2_planned.request.operational_intent.volumes - ) - ), "flight1_planned and flight2_planned must intersect" - assert Volume4DCollection.from_interuss_scd_api( - self.flight1_planned.request.operational_intent.volumes - ).intersects_vol4s( - Volume4DCollection.from_interuss_scd_api( - self.flight1m_planned.request.operational_intent.volumes - ) - ), "flight1_planned and flight1m_planned must intersect" - assert not Volume4DCollection.from_interuss_scd_api( - self.flight1_planned.request.operational_intent.volumes - ).intersects_vol4s( - Volume4DCollection.from_interuss_scd_api( - self.flight1c_activated.request.operational_intent.volumes - ) - ), "flight1_planned and flight1c_activated must not intersect" - - except KeyError as e: - raise ValueError( - f"`{self.me()}` TestScenario requirements for flight_intents not met: missing flight intent {e}" - ) - except AssertionError as e: - raise ValueError( - f"`{self.me()}` TestScenario requirements for flight_intents not met: {e}" - ) - def run(self, context: ExecutionContext): self.begin_test_scenario(context) @@ -234,12 +208,12 @@ def _attempt_plan_flight_conflict(self): self, self.control_uss, self.dss, - "Validate flight 2 sharing", + "Validate Flight 2 sharing", self._intents_extent, ) as validator: resp_flight_2, self.flight2_id = plan_flight_intent( self, - "Plan flight 2", + "Plan Flight 2", self.control_uss, self.flight2_planned.request, ) @@ -249,19 +223,19 @@ def _attempt_plan_flight_conflict(self): self, self.tested_uss, self.dss, - "Validate flight 1 not shared", + "Validate Flight 1 not shared", self._intents_extent, ) as validator: _ = plan_priority_conflict_flight_intent( self, - "Attempt to plan flight 1", + "Attempt to plan Flight 1", self.tested_uss, self.flight1_planned.request, ) validator.expect_not_shared() _ = delete_flight_intent( - self, "Delete flight 2", self.control_uss, self.flight2_id + self, "Delete Flight 2", self.control_uss, self.flight2_id ) self.flight2_id = None @@ -272,12 +246,12 @@ def _attempt_modify_planned_flight_conflict( self, self.tested_uss, self.dss, - "Validate flight 1 sharing", + "Validate Flight 1 sharing", self._intents_extent, ) as validator: resp_flight_1, self.flight1_id = plan_flight_intent( self, - "Plan flight 1", + "Plan Flight 1", self.tested_uss, self.flight1_planned.request, ) @@ -287,12 +261,12 @@ def _attempt_modify_planned_flight_conflict( self, self.control_uss, self.dss, - "Validate flight 2 sharing", + "Validate Flight 2 sharing", self._intents_extent, ) as validator: resp_flight_2, self.flight2_id = plan_flight_intent( self, - "Plan flight 2", + "Plan Flight 2", self.control_uss, self.flight2_planned.request, ) @@ -302,13 +276,13 @@ def _attempt_modify_planned_flight_conflict( self, self.tested_uss, self.dss, - "Validate flight 1 not modified", + "Validate Flight 1 not modified", self._intents_extent, flight_1_oi_ref, ) as validator: _ = modify_planned_priority_conflict_flight_intent( self, - "Attempt to modify planned flight 1 in conflict", + "Attempt to modify planned Flight 1 in conflict", self.tested_uss, self.flight1m_planned.request, self.flight1_id, @@ -326,13 +300,13 @@ def _attempt_activate_flight_conflict( self, self.tested_uss, self.dss, - "Validate flight 1 not activated", + "Validate Flight 1 not activated", self._intents_extent, flight_1_oi_ref, ) as validator: _ = activate_priority_conflict_flight_intent( self, - "Attempt to activate conflicting flight 1", + "Attempt to activate conflicting Flight 1", self.tested_uss, self.flight1_activated.request, self.flight1_id, @@ -347,7 +321,7 @@ def _modify_activated_flight_conflict_preexisting( self, flight_1_oi_ref: Optional[OperationalIntentReference] ) -> Tuple[OperationalIntentReference, OperationalIntentReference]: _ = delete_flight_intent( - self, "Delete flight 2", self.control_uss, self.flight2_id + self, "Delete Flight 2", self.control_uss, self.flight2_id ) self.flight2_id = None @@ -355,13 +329,13 @@ def _modify_activated_flight_conflict_preexisting( self, self.tested_uss, self.dss, - "Validate flight 1 sharing", + "Validate Flight 1 sharing", self._intents_extent, flight_1_oi_ref, ) as validator: activate_flight_intent( self, - "Activate flight 1", + "Activate Flight 1", self.tested_uss, self.flight1_activated.request, self.flight1_id, @@ -372,12 +346,12 @@ def _modify_activated_flight_conflict_preexisting( self, self.control_uss, self.dss, - "Validate flight 2 sharing", + "Validate Flight 2 sharing", self._intents_extent, ) as validator: _, self.flight2_id = plan_flight_intent( self, - "Plan flight 2", + "Plan Flight 2", self.control_uss, self.flight2_planned.request, ) @@ -387,13 +361,13 @@ def _modify_activated_flight_conflict_preexisting( self, self.control_uss, self.dss, - "Validate flight 2 sharing", + "Validate Flight 2 sharing", self._intents_extent, flight_2_oi_ref, ) as validator: activate_flight_intent( self, - "Activate flight 2", + "Activate Flight 2", self.control_uss, self.flight2_activated.request, self.flight2_id, @@ -404,13 +378,13 @@ def _modify_activated_flight_conflict_preexisting( self, self.tested_uss, self.dss, - "Validate flight 1 sharing", + "Validate Flight 1 sharing", self._intents_extent, flight_1_oi_ref, ) as validator: resp = modify_activated_flight_intent( self, - "Modify activated flight 1 in conflict with activated flight 2", + "Modify activated Flight 1 in conflict with activated Flight 2", self.tested_uss, self.flight1m_activated.request, self.flight1_id, @@ -437,13 +411,13 @@ def _attempt_modify_activated_flight_conflict( self, self.control_uss, self.dss, - "Validate flight 2 sharing", + "Validate Flight 2 sharing", self._intents_extent, flight_2_oi_ref, ) as validator: modify_activated_flight_intent( self, - "Modify activated flight 2 to not conflict with activated flight 1", + "Modify activated Flight 2 to not conflict with activated Flight 1", self.control_uss, self.flight2m_activated.request, self.flight2_id, @@ -454,13 +428,13 @@ def _attempt_modify_activated_flight_conflict( self, self.tested_uss, self.dss, - "Validate flight 1 not modified", + "Validate Flight 1 not modified", self._intents_extent, flight_1_oi_ref, ) as validator: modify_activated_priority_conflict_flight_intent( self, - "Attempt to modify activated flight 1 in conflict", + "Attempt to modify activated Flight 1 in conflict", self.tested_uss, self.flight1c_activated.request, self.flight1_id, diff --git a/monitoring/uss_qualifier/test_data/che/flight_intents/conflicting_flights.yaml b/monitoring/uss_qualifier/test_data/che/flight_intents/conflicting_flights.yaml index 20a23fe332..bc8372cf56 100644 --- a/monitoring/uss_qualifier/test_data/che/flight_intents/conflicting_flights.yaml +++ b/monitoring/uss_qualifier/test_data/che/flight_intents/conflicting_flights.yaml @@ -27,12 +27,12 @@ intents: start_time: offset_from: starting_from: - time_during_test: StartOfTestRun + time_during_test: TimeOfEvaluation offset: -1s end_time: offset_from: starting_from: - time_during_test: StartOfTestRun + time_during_test: TimeOfEvaluation offset: 8m astm_f3548_21: @@ -131,12 +131,12 @@ intents: start_time: offset_from: starting_from: - time_during_test: StartOfTestRun + time_during_test: TimeOfEvaluation offset: -1s end_time: offset_from: starting_from: - time_during_test: StartOfTestRun + time_during_test: TimeOfEvaluation offset: 8m astm_f3548_21: @@ -178,7 +178,7 @@ intents: altitude_upper: value: 705 - equal_prio_flight2_planned: + equal_prio_flight2m_planned: delta: source: flight2m_activated mutation: @@ -186,19 +186,31 @@ intents: usage_state: Planned area: - altitude_lower: - value: 650.10 + value: 650.13 + astm_f3548_21: + priority: 0 + + equal_prio_flight2_planned: + delta: + source: flight2_planned + mutation: + basic_information: + area: + - altitude_lower: + value: 605.10 astm_f3548_21: priority: 0 equal_prio_flight2_activated: delta: - source: equal_prio_flight2_planned + source: flight2_activated mutation: basic_information: - usage_state: InUse area: - altitude_lower: - value: 650.11 + value: 605.11 + astm_f3548_21: + priority: 0 equal_prio_flight2_nonconforming: delta: