Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[mock_uss] Clean up mock_uss flight planning #311

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
258 changes: 236 additions & 22 deletions monitoring/mock_uss/f3548v21/flight_planning.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import uuid
from datetime import datetime
from typing import Optional, List, Callable

import arrow

from monitoring.mock_uss import webapp
from monitoring.mock_uss.config import KEY_BASE_URL
from monitoring.monitorlib.clients.flight_planning.flight_info import (
FlightInfo,
AirspaceUsageState,
UasState,
)
from monitoring.uss_qualifier.resources.overrides import apply_overrides
from uas_standards.astm.f3548.v21 import api as f3548_v21
from uas_standards.astm.f3548.v21.api import OperationalIntentDetails, OperationalIntent
from uas_standards.astm.f3548.v21.constants import OiMaxVertices, OiMaxPlanHorizonDays
from uas_standards.interuss.automated_testing.scd.v1 import api as scd_api

from monitoring.mock_uss.flights.database import FlightRecord
from monitoring.mock_uss.f3548v21 import utm_client
from monitoring.mock_uss.flights.database import FlightRecord, db
from monitoring.monitorlib.clients import scd as scd_client
from monitoring.monitorlib.geotemporal import Volume4DCollection
from monitoring.monitorlib.locality import Locality
from uas_standards.interuss.automated_testing.scd.v1.api import OperationalIntentState


class PlanningError(Exception):
Expand All @@ -24,7 +32,6 @@ def validate_request(req_body: scd_api.InjectFlightRequest) -> None:

Args:
req_body: Information about the requested flight.
locality: Jurisdictional requirements which the mock_uss should follow.
"""
# Validate max number of vertices
nb_vertices = 0
Expand Down Expand Up @@ -67,7 +74,7 @@ def validate_request(req_body: scd_api.InjectFlightRequest) -> None:

# Validate intent is currently active if in Activated state
# I.e. at least one volume has start time in the past and end time in the future
if req_body.operational_intent.state == OperationalIntentState.Activated:
if req_body.operational_intent.state == scd_api.OperationalIntentState.Activated:
now = arrow.utcnow().datetime
active_volume = Volume4DCollection.from_interuss_scd_api(
req_body.operational_intent.volumes
Expand All @@ -80,7 +87,7 @@ def validate_request(req_body: scd_api.InjectFlightRequest) -> None:


def check_for_disallowed_conflicts(
req_body: scd_api.InjectFlightRequest,
new_op_intent: f3548_v21.OperationalIntent,
existing_flight: Optional[FlightRecord],
op_intents: List[f3548_v21.OperationalIntent],
locality: Locality,
Expand All @@ -89,7 +96,7 @@ def check_for_disallowed_conflicts(
"""Raise a PlannerError if there are any disallowed conflicts.

Args:
req_body: Information about the requested flight.
new_op_intent: The prospective operational intent.
existing_flight: The existing state of the flight (to be changed by the request), or None if this request is to
create a new flight.
op_intents: Full information for all potentially-relevant operational intents.
Expand All @@ -99,14 +106,14 @@ def check_for_disallowed_conflicts(
if log is None:
log = lambda msg: None

if req_body.operational_intent.state not in (
OperationalIntentState.Accepted,
OperationalIntentState.Activated,
if new_op_intent.reference.state not in (
scd_api.OperationalIntentState.Accepted,
scd_api.OperationalIntentState.Activated,
):
# No conflicts are disallowed if the flight is not nominal
return

v1 = Volume4DCollection.from_interuss_scd_api(req_body.operational_intent.volumes)
v1 = Volume4DCollection.from_interuss_scd_api(new_op_intent.details.volumes)

for op_intent in op_intents:
if (
Expand All @@ -117,16 +124,14 @@ def check_for_disallowed_conflicts(
f"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:
if new_op_intent.details.priority > op_intent.details.priority:
log(
f"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
)
new_op_intent.details.priority == op_intent.details.priority
and locality.allows_same_priority_intersections(op_intent.details.priority)
):
log(
f"intersection with {op_intent.reference.id} not considered: intersection with same-priority operational intents (if allowed)"
Expand All @@ -141,8 +146,7 @@ def check_for_disallowed_conflicts(
existing_flight
and existing_flight.op_intent.reference.state
== scd_api.OperationalIntentState.Activated
and req_body.operational_intent.state
== scd_api.OperationalIntentState.Activated
and op_intent.reference.state == scd_api.OperationalIntentState.Activated
)
if modifying_activated:
preexisting_conflict = Volume4DCollection.from_interuss_scd_api(
Expand All @@ -156,7 +160,7 @@ def check_for_disallowed_conflicts(

if v1.intersects_vol4s(v2):
raise PlanningError(
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})"
f"Requested flight (priority {new_op_intent.details.priority}) intersected {op_intent.reference.manager}'s operational intent {op_intent.reference.id} (priority {op_intent.details.priority})"
)


Expand Down Expand Up @@ -217,14 +221,66 @@ def op_intent_transition_valid(
return False


def op_intent_from_flightrecord(flight: FlightRecord, method: str) -> OperationalIntent:
def op_intent_from_flightinfo(
flight_info: FlightInfo, flight_id: str
) -> f3548_v21.OperationalIntent:
volumes = [v.to_f3548v21() for v in flight_info.basic_information.area]
off_nominal_volumes = []
usage_state = flight_info.basic_information.usage_state
if usage_state == AirspaceUsageState.Planned:
state = f3548_v21.OperationalIntentState.Accepted
elif usage_state == AirspaceUsageState.InUse:
uas_state = flight_info.basic_information.uas_state
if uas_state == UasState.Nominal:
state = f3548_v21.OperationalIntentState.Activated
elif uas_state == UasState.OffNominal:
state = f3548_v21.OperationalIntentState.Nonconforming
off_nominal_volumes = volumes
volumes = []
elif uas_state == UasState.Contingent:
state = f3548_v21.OperationalIntentState.Contingent
off_nominal_volumes = volumes
volumes = []
else:
raise ValueError(f"Unknown uas_state '{uas_state}'")
else:
raise ValueError(f"Unknown usage_state '{usage_state}'")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this seems duplicate with

volumes = [v.to_interuss_scd_api() for v in info.basic_information.area]
if info.basic_information.usage_state == AirspaceUsageState.Planned:
state = scd_api.OperationalIntentState.Accepted
off_nominal_volumes = []
elif info.basic_information.usage_state == AirspaceUsageState.InUse:
if info.basic_information.uas_state == UasState.Nominal:
state = scd_api.OperationalIntentState.Activated
off_nominal_volumes = []
elif info.basic_information.uas_state == UasState.OffNominal:
state = scd_api.OperationalIntentState.Nonconforming
off_nominal_volumes = volumes
volumes = []
elif info.basic_information.uas_state == UasState.Contingent:
state = scd_api.OperationalIntentState.Contingent
off_nominal_volumes = volumes
volumes = []
else:
raise ValueError(
f"Unrecognized uas_state '{info.basic_information.uas_state}'"
)
else:
raise ValueError(
f"Unrecognized usage_state '{info.basic_information.usage_state}'"
)

Factor those away?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good -- they were different enough I didn't notice, but I think I have some common logic that simplifies both places now.


v4c = Volume4DCollection(volumes=flight_info.basic_information.area)

reference = f3548_v21.OperationalIntentReference(
id=f3548_v21.EntityID(flight_id),
manager="UNKNOWN",
uss_availability=f3548_v21.UssAvailabilityState.Unknown,
version=0,
state=state,
ovn="UNKNOWN",
time_start=v4c.time_start.to_f3548v21(),
time_end=v4c.time_end.to_f3548v21(),
uss_base_url="{}/mock/scd".format(webapp.config[KEY_BASE_URL]),
subscription_id="UNKNOWN",
)
details = f3548_v21.OperationalIntentDetails(
volumes=volumes,
off_nominal_volumes=off_nominal_volumes,
priority=flight_info.astm_f3548_21.priority,
)
return f3548_v21.OperationalIntent(
reference=reference,
details=details,
)


def op_intent_from_flightrecord(
flight: FlightRecord, method: str
) -> f3548_v21.OperationalIntent:
ref = flight.op_intent.reference
details = OperationalIntentDetails(
details = f3548_v21.OperationalIntentDetails(
volumes=flight.op_intent.details.volumes,
off_nominal_volumes=flight.op_intent.details.off_nominal_volumes,
priority=flight.op_intent.details.priority,
)
op_intent = OperationalIntent(reference=ref, details=details)
op_intent = f3548_v21.OperationalIntent(reference=ref, details=details)
if flight.mod_op_sharing_behavior:
mod_op_sharing_behavior = flight.mod_op_sharing_behavior
if mod_op_sharing_behavior.modify_sharing_methods is not None:
Expand All @@ -235,3 +291,161 @@ def op_intent_from_flightrecord(flight: FlightRecord, method: str) -> Operationa
)

return op_intent


def query_operational_intents(
area_of_interest: f3548_v21.Volume4D,
) -> List[f3548_v21.OperationalIntent]:
"""Retrieve a complete set of operational intents in an area, including details.

:param area_of_interest: Area where intersecting operational intents must be discovered
:return: Full definition for every operational intent discovered
"""
op_intent_refs = scd_client.query_operational_intent_references(
utm_client, area_of_interest
)
tx = db.value
get_details_for = []
own_flights = {f.op_intent.reference.id: f for f in tx.flights.values() if f}
result = []
for op_intent_ref in op_intent_refs:
if op_intent_ref.id in own_flights:
# This is our own flight
result.append(
op_intent_from_flightrecord(own_flights[op_intent_ref.id], "GET")
)
elif (
op_intent_ref.id in tx.cached_operations
and tx.cached_operations[op_intent_ref.id].reference.version
== op_intent_ref.version
):
# We have a current version of this op intent cached
result.append(tx.cached_operations[op_intent_ref.id])
else:
# We need to get the details for this op intent
get_details_for.append(op_intent_ref)

updated_op_intents = []
for op_intent_ref in get_details_for:
op_intent, _ = scd_client.get_operational_intent_details(
utm_client, op_intent_ref.uss_base_url, op_intent_ref.id
)
updated_op_intents.append(op_intent)
result.extend(updated_op_intents)

with db as tx:
for op_intent in updated_op_intents:
tx.cached_operations[op_intent.reference.id] = op_intent

return result


def check_op_intent(
new_flight: FlightRecord,
existing_flight: Optional[FlightRecord],
locality: Locality,
log: Callable[[str], None],
) -> List[f3548_v21.EntityOVN]:
# Check the transition is valid
state_transition_from = (
f3548_v21.OperationalIntentState(existing_flight.op_intent.reference.state)
if existing_flight
else None
)
state_transition_to = f3548_v21.OperationalIntentState(
new_flight.op_intent.reference.state
)
if not op_intent_transition_valid(state_transition_from, state_transition_to):
raise PlanningError(
f"Operational intent state transition from {state_transition_from} to {state_transition_to} is invalid"
)

if new_flight.op_intent.reference.state in (
f3548_v21.OperationalIntentState.Accepted,
f3548_v21.OperationalIntentState.Activated,
):
# Check for intersections if the flight is nominal

# Check for operational intents in the DSS
log("Obtaining latest operational intent information")
v1 = Volume4DCollection.from_interuss_scd_api(
new_flight.op_intent.details.volumes
+ new_flight.op_intent.details.off_nominal_volumes
)
vol4 = v1.bounding_volume.to_f3548v21()
op_intents = query_operational_intents(vol4)

# Check for intersections
log(
f"Checking for intersections with {', '.join(op_intent.reference.id for op_intent in op_intents)}"
)
check_for_disallowed_conflicts(
new_flight.op_intent, existing_flight, op_intents, locality, log
)

key = [f3548_v21.EntityOVN(op.reference.ovn) for op in op_intents]
else:
# Flight is not nominal and therefore doesn't need to check intersections
key = []

return key


def share_op_intent(
new_flight: FlightRecord,
existing_flight: Optional[FlightRecord],
key: List[f3548_v21.EntityOVN],
log: Callable[[str], None],
):
# Create operational intent in DSS
log("Sharing operational intent with DSS")
base_url = new_flight.op_intent.reference.uss_base_url
req = f3548_v21.PutOperationalIntentReferenceParameters(
extents=new_flight.op_intent.details.volumes
+ new_flight.op_intent.details.off_nominal_volumes,
key=key,
state=new_flight.op_intent.reference.state,
uss_base_url=base_url,
new_subscription=f3548_v21.ImplicitSubscriptionParameters(
uss_base_url=base_url
),
)
if existing_flight:
id = existing_flight.op_intent.reference.id
log(f"Updating existing operational intent {id} in DSS")
result = scd_client.update_operational_intent_reference(
utm_client,
id,
existing_flight.op_intent.reference.ovn,
req,
)
else:
id = str(uuid.uuid4())
log(f"Creating new operational intent {id} in DSS")
result = scd_client.create_operational_intent_reference(utm_client, id, req)

# Notify subscribers
true_op_intent = f3548_v21.OperationalIntent(
reference=result.operational_intent_reference,
details=new_flight.op_intent.details,
)
record = FlightRecord(
op_intent=true_op_intent,
flight_info=new_flight.flight_info,
mod_op_sharing_behavior=new_flight.mod_op_sharing_behavior,
)
operational_intent = op_intent_from_flightrecord(record, "POST")
for subscriber in result.subscribers:
if subscriber.uss_base_url == base_url:
# Do not notify ourselves
continue
update = f3548_v21.PutOperationalIntentDetailsParameters(
operational_intent_id=result.operational_intent_reference.id,
operational_intent=operational_intent,
subscriptions=subscriber.subscriptions,
)
log(f"Notifying subscriber at {subscriber.uss_base_url}")
scd_client.notify_operational_intent_details_changed(
utm_client, subscriber.uss_base_url, update
)
return record
Loading
Loading