diff --git a/monitoring/mock_uss/scdsc/routes_injection.py b/monitoring/mock_uss/scdsc/routes_injection.py index 53058bda70..75008b9ef8 100644 --- a/monitoring/mock_uss/scdsc/routes_injection.py +++ b/monitoring/mock_uss/scdsc/routes_injection.py @@ -216,9 +216,11 @@ def inject_flight(flight_id: str, req_body: InjectFlightRequest) -> Tuple[dict, ) # Validate max planning horizon for creation - start_time = Volume4DCollection.from_interuss_scd_api( + v4c = Volume4DCollection.from_interuss_scd_api( req_body.operational_intent.volumes - ).time_start.datetime + + req_body.operational_intent.off_nominal_volumes + ) + start_time = v4c.time_start.datetime time_delta = start_time - datetime.now(tz=start_time.tzinfo) if ( time_delta.days > OiMaxPlanHorizonDays @@ -297,72 +299,79 @@ def inject_flight(flight_id: str, req_body: InjectFlightRequest) -> Tuple[dict, logger.debug( f"[inject_flight/{pid}:{flight_id}] Obtaining latest operational intent information" ) - vol4 = Volume4DCollection.from_interuss_scd_api( - req_body.operational_intent.volumes - ).bounding_volume.to_f3548v21() - op_intents = query_operational_intents(vol4) - - # Check for intersections - step_name = "checking for intersections" - logger.debug( - f"[inject_flight/{pid}:{flight_id}] Checking for intersections with {', '.join(op_intent.reference.id for op_intent in op_intents)}" - ) v1 = Volume4DCollection.from_interuss_scd_api( req_body.operational_intent.volumes + + req_body.operational_intent.off_nominal_volumes ) - for op_intent in op_intents: - if ( - existing_flight - and existing_flight.op_intent_reference.id == op_intent.reference.id - ): - logger.debug( - f"[inject_flight/{pid}:{flight_id}] intersection with {op_intent.reference.id} not considered: intersection with a past version of this flight" - ) - continue - if req_body.operational_intent.priority > op_intent.details.priority: - logger.debug( - f"[inject_flight/{pid}:{flight_id}] intersection with {op_intent.reference.id} not considered: intersection with lower-priority operational intents" - ) - continue - if ( - req_body.operational_intent.priority == op_intent.details.priority - and locality.allows_same_priority_intersections( - req_body.operational_intent.priority - ) - ): - logger.debug( - f"[inject_flight/{pid}:{flight_id}] intersection with {op_intent.reference.id} not considered: intersection with same-priority operational intents (if allowed)" - ) - continue + vol4 = v1.bounding_volume.to_f3548v21() + op_intents = query_operational_intents(vol4) - v2 = Volume4DCollection.from_interuss_scd_api( - op_intent.details.volumes + op_intent.details.off_nominal_volumes + if req_body.operational_intent.state in ( + OperationalIntentState.Nonconforming, + OperationalIntentState.Contingent, + ): + logger.debug( + f"[inject_flight/{pid}:{flight_id}] Skipping intersection check because flight is {req_body.operational_intent.state}" ) + else: + # Check for intersections + step_name = "checking for intersections" + logger.debug( + f"[inject_flight/{pid}:{flight_id}] Checking for intersections with {', '.join(op_intent.reference.id for op_intent in op_intents)}" + ) + for op_intent in op_intents: + if ( + existing_flight + and existing_flight.op_intent_reference.id == op_intent.reference.id + ): + logger.debug( + f"[inject_flight/{pid}:{flight_id}] intersection with {op_intent.reference.id} not considered: intersection with a past version of this flight" + ) + continue + if req_body.operational_intent.priority > op_intent.details.priority: + logger.debug( + f"[inject_flight/{pid}:{flight_id}] intersection with {op_intent.reference.id} not considered: intersection with lower-priority operational intents" + ) + continue + if ( + req_body.operational_intent.priority == op_intent.details.priority + and locality.allows_same_priority_intersections( + req_body.operational_intent.priority + ) + ): + logger.debug( + f"[inject_flight/{pid}:{flight_id}] intersection with {op_intent.reference.id} not considered: intersection with same-priority operational intents (if allowed)" + ) + continue - if ( - existing_flight - and existing_flight.op_intent_reference.state - == OperationalIntentState.Activated - and req_body.operational_intent.state - == OperationalIntentState.Activated - and Volume4DCollection.from_f3548v21( - existing_flight.op_intent_injection.volumes - ).intersects_vol4s(v2) - ): - logger.debug( - f"[inject_flight/{pid}:{flight_id}] intersection with {op_intent.reference.id} not considered: modification of Activated operational intent with a pre-existing conflict" + v2 = Volume4DCollection.from_interuss_scd_api( + op_intent.details.volumes + op_intent.details.off_nominal_volumes ) - continue - if v1.intersects_vol4s(v2): - notes = f"Requested flight (priority {req_body.operational_intent.priority}) intersected {op_intent.reference.manager}'s operational intent {op_intent.reference.id} (priority {op_intent.details.priority})" - return ( - InjectFlightResponse( - result=InjectFlightResponseResult.ConflictWithFlight, - notes=notes, - ), - 200, - ) + if ( + existing_flight + and existing_flight.op_intent_reference.state + == OperationalIntentState.Activated + and req_body.operational_intent.state + == OperationalIntentState.Activated + and Volume4DCollection.from_f3548v21( + existing_flight.op_intent_injection.volumes + ).intersects_vol4s(v2) + ): + logger.debug( + f"[inject_flight/{pid}:{flight_id}] intersection with {op_intent.reference.id} not considered: modification of Activated operational intent with a pre-existing conflict" + ) + continue + + if v1.intersects_vol4s(v2): + notes = f"Requested flight (priority {req_body.operational_intent.priority}) intersected {op_intent.reference.manager}'s operational intent {op_intent.reference.id} (priority {op_intent.details.priority})" + return ( + InjectFlightResponse( + result=InjectFlightResponseResult.ConflictWithFlight, + notes=notes, + ), + 200, + ) # Create operational intent in DSS step_name = "sharing operational intent in DSS" diff --git a/monitoring/monitorlib/clients/flight_planning/client_scd.py b/monitoring/monitorlib/clients/flight_planning/client_scd.py index 712e83a7a0..f5eb4643c7 100644 --- a/monitoring/monitorlib/clients/flight_planning/client_scd.py +++ b/monitoring/monitorlib/clients/flight_planning/client_scd.py @@ -77,15 +77,13 @@ def _inject( if uas_state == UasState.Nominal: volumes = [ - v.to_interuss_scd_api() - for v in flight_info.basic_information.area + v.to_interuss_scd_api() for v in flight_info.basic_information.area ] off_nominal_volumes = [] else: volumes = [] off_nominal_volumes = [ - v.to_scd_automated_testing_api() - for v in flight_info.basic_information.area + v.to_interuss_scd_api() for v in flight_info.basic_information.area ] if "astm_f3548_21" in flight_info and flight_info.astm_f3548_21: diff --git a/monitoring/uss_qualifier/resources/flight_planning/flight_planner.py b/monitoring/uss_qualifier/resources/flight_planning/flight_planner.py index 097afa0c39..fae5d9eb01 100644 --- a/monitoring/uss_qualifier/resources/flight_planning/flight_planner.py +++ b/monitoring/uss_qualifier/resources/flight_planning/flight_planner.py @@ -112,9 +112,15 @@ def request_flight( OperationalIntentState.Nonconforming: UasState.OffNominal, OperationalIntentState.Contingent: UasState.Contingent, } - if request.operational_intent.state in (OperationalIntentState.Accepted, OperationalIntentState.Activated) and request.operational_intent.off_nominal_volumes: + if ( + request.operational_intent.state + in (OperationalIntentState.Accepted, OperationalIntentState.Activated) + and request.operational_intent.off_nominal_volumes + ): # This invalid request can no longer be represented with a standard flight planning request; reject it at the client level instead - raise ValueError(f"Request for nominal {request.operational_intent.state} operational intent is invalid because it contains off-nominal volumes") + raise ValueError( + f"Request for nominal {request.operational_intent.state} operational intent is invalid because it contains off-nominal volumes" + ) v4c = Volume4DCollection.from_interuss_scd_api( request.operational_intent.volumes ) + Volume4DCollection.from_interuss_scd_api( @@ -138,13 +144,13 @@ def request_flight( ) if not flight_id: - flight_id = str(uuid.uuid4()) try: resp = self.scd_client.try_plan_flight( flight_info, ExecutionStyle.IfAllowed ) except PlanningActivityError as e: raise QueryError(str(e), e.queries) + flight_id = resp.flight_id else: try: resp = self.scd_client.try_update_flight( diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py b/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py index 1002c81156..a369a9a7e1 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/flight_intent_validation/flight_intent_validation.py @@ -247,7 +247,10 @@ def _validate_precision_intersection(self): self, "Attempt to plan flight conflicting by a tiny overlap", "Incorrectly planned", - {InjectFlightResponseResult.ConflictWithFlight}, + { + InjectFlightResponseResult.ConflictWithFlight, + InjectFlightResponseResult.Rejected, + }, {InjectFlightResponseResult.Failed: "Failure"}, self.tested_uss, self.valid_conflict_tiny_overlap.request, 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 938f66ec90..4f71ae140d 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 @@ -455,10 +455,6 @@ def _modify_activated_flight_preexisting_conflict( self.flight_1_activated_time_range_A.request ) - # TODO: the following call requires the control USS to support CMSA role, - # but as there is currently no explicit way of knowing if it is the case - # or not, we assume that a Rejected result means the USS does not - # support the CMSA role, in which case we interrupt the scenario. with OpIntentValidator( self, self.control_uss, @@ -473,15 +469,15 @@ def _modify_activated_flight_preexisting_conflict( "Successful transition to non-conforming state", { InjectFlightResponseResult.Planned, - InjectFlightResponseResult.Rejected, + InjectFlightResponseResult.NotSupported, }, {InjectFlightResponseResult.Failed: "Failure"}, self.control_uss, self.flight_2_equal_prio_nonconforming_time_range_A.request, self.flight_2_id, ) - if resp_flight_2.result == InjectFlightResponseResult.Rejected: - msg = f"{self.control_uss.config.participant_id} rejected transition to a Nonconforming state because it does not support CMSA role, execution of the scenario was stopped without failure" + if resp_flight_2.result == InjectFlightResponseResult.NotSupported: + msg = f"{self.control_uss.config.participant_id} does not support the transition to a Nonconforming state; execution of the scenario was stopped without failure" self.record_note("Control USS does not support CMSA role", msg) raise ScenarioCannotContinueError(msg) diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/prioritization_test_steps.py b/monitoring/uss_qualifier/scenarios/flight_planning/prioritization_test_steps.py index 01a1f6ab61..76271c3092 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/prioritization_test_steps.py +++ b/monitoring/uss_qualifier/scenarios/flight_planning/prioritization_test_steps.py @@ -38,7 +38,10 @@ def plan_priority_conflict_flight_intent( scenario, test_step, "Incorrectly planned", - {InjectFlightResponseResult.ConflictWithFlight}, + { + InjectFlightResponseResult.ConflictWithFlight, + InjectFlightResponseResult.Rejected, + }, {InjectFlightResponseResult.Failed: "Failure"}, flight_planner, flight_intent, @@ -67,7 +70,10 @@ def modify_planned_priority_conflict_flight_intent( scenario, test_step, "Incorrectly modified", - {InjectFlightResponseResult.ConflictWithFlight}, + { + InjectFlightResponseResult.ConflictWithFlight, + InjectFlightResponseResult.Rejected, + }, {InjectFlightResponseResult.Failed: "Failure"}, flight_planner, flight_intent, @@ -97,7 +103,10 @@ def activate_priority_conflict_flight_intent( scenario, test_step, "Incorrectly activated", - {InjectFlightResponseResult.ConflictWithFlight}, + { + InjectFlightResponseResult.ConflictWithFlight, + InjectFlightResponseResult.Rejected, + }, {InjectFlightResponseResult.Failed: "Failure"}, flight_planner, flight_intent, @@ -127,7 +136,10 @@ def modify_activated_priority_conflict_flight_intent( scenario, test_step, "Incorrectly modified", - {InjectFlightResponseResult.ConflictWithFlight}, + { + InjectFlightResponseResult.ConflictWithFlight, + InjectFlightResponseResult.Rejected, + }, {InjectFlightResponseResult.Failed: "Failure"}, flight_planner, flight_intent, @@ -156,7 +168,10 @@ def plan_conflict_flight_intent( scenario, test_step, "Incorrectly planned", - {InjectFlightResponseResult.ConflictWithFlight}, + { + InjectFlightResponseResult.ConflictWithFlight, + InjectFlightResponseResult.Rejected, + }, {InjectFlightResponseResult.Failed: "Failure"}, flight_planner, flight_intent, @@ -185,7 +200,10 @@ def modify_planned_conflict_flight_intent( scenario, test_step, "Incorrectly modified", - {InjectFlightResponseResult.ConflictWithFlight}, + { + InjectFlightResponseResult.ConflictWithFlight, + InjectFlightResponseResult.Rejected, + }, {InjectFlightResponseResult.Failed: "Failure"}, flight_planner, flight_intent, @@ -215,7 +233,10 @@ def activate_conflict_flight_intent( scenario, test_step, "Incorrectly activated", - {InjectFlightResponseResult.ConflictWithFlight}, + { + InjectFlightResponseResult.ConflictWithFlight, + InjectFlightResponseResult.Rejected, + }, {InjectFlightResponseResult.Failed: "Failure"}, flight_planner, flight_intent, @@ -245,7 +266,10 @@ def modify_activated_conflict_flight_intent( scenario, test_step, "Incorrectly modified", - {InjectFlightResponseResult.ConflictWithFlight}, + { + InjectFlightResponseResult.ConflictWithFlight, + InjectFlightResponseResult.Rejected, + }, {InjectFlightResponseResult.Failed: "Failure"}, flight_planner, flight_intent, diff --git a/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py b/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py index 69a1ccfcc8..36d742f56a 100644 --- a/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py +++ b/monitoring/uss_qualifier/scenarios/flight_planning/test_steps.py @@ -65,7 +65,9 @@ def clear_area( check.record_failed( summary="Area could not be cleared", severity=Severity.High, - details=f'Participant indicated "{resp.outcome.message}"' if "message" in resp.outcome else "See query", + details=f'Participant indicated "{resp.outcome.message}"' + if "message" in resp.outcome + else "See query", query_timestamps=[query.request.timestamp], ) @@ -385,7 +387,9 @@ def cleanup_flights( else: check.record_failed( summary="Failed to delete flight", - details=f"USS indicated: {resp.notes}" if "notes" in resp else "See query", + details=f"USS indicated: {resp.notes}" + if "notes" in resp + else "See query", severity=Severity.Medium, query_timestamps=[query.request.timestamp], )