Skip to content

Commit 101c9be

Browse files
[uss_qualifier] Update verification approach for SCD0090 and SCD0095 (#1167)
1 parent 13344b3 commit 101c9be

File tree

25 files changed

+418
-326
lines changed

25 files changed

+418
-326
lines changed

monitoring/mock_uss/database.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from implicitdict import ImplicitDict, StringBasedDateTime, StringBasedTimeDelta
44

5-
from monitoring.monitorlib.clients.flight_planning.planning import UserNotification
65
from monitoring.monitorlib.errors import stacktrace_string
76
from monitoring.monitorlib.multiprocessing import SynchronizedValue
87

@@ -47,9 +46,6 @@ class Database(ImplicitDict):
4746
most_recent_periodic_check: StringBasedDateTime | None
4847
"""Timestamp of most recent time periodic task loop iterated"""
4948

50-
flight_planning_notifications: list[UserNotification]
51-
"""List of notifications send during flight planning operations"""
52-
5349

5450
db = SynchronizedValue(
5551
Database(

monitoring/mock_uss/f3548v21/flight_planning.py

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -90,47 +90,38 @@ def validate_request(op_intent: f3548_v21.OperationalIntent) -> None:
9090
)
9191

9292

93-
def check_for_local_conflits(
94-
op_intent: f3548_v21.OperationalIntent, flights: list[FlightRecord]
93+
def conflicts_with_flightrecords(
94+
op_intent: f3548_v21.OperationalIntent, flights: list[FlightRecord | None]
9595
) -> bool:
9696
"""
97-
Return true if an OperationalIntent conflict with any of existing flights
97+
Return true if the OperationalIntent conflicts with (intersects) any of the specified FlightRecords that do not
98+
correspond with op_intent.
9899
"""
99100

100-
if op_intent.reference.state not in (
101-
scd_api.OperationalIntentState.Accepted,
102-
scd_api.OperationalIntentState.Activated,
103-
):
104-
# No conflicts are disallowed if the flight is not nominal
105-
return False
106-
107-
v1 = Volume4DCollection.from_f3548v21(
101+
vc1 = Volume4DCollection.from_f3548v21(
108102
(op_intent.details.volumes or [])
109103
+ (op_intent.details.off_nominal_volumes or [])
110104
)
111105

112106
for other_flight in flights:
113-
if other_flight.op_intent.reference.state not in (
114-
scd_api.OperationalIntentState.Accepted,
115-
scd_api.OperationalIntentState.Activated,
116-
):
107+
if not other_flight:
117108
continue
118109

119110
if other_flight.op_intent.reference.id == op_intent.reference.id: # Same flight
120111
continue
121112

122-
v2 = Volume4DCollection.from_f3548v21(
113+
vc2 = Volume4DCollection.from_f3548v21(
123114
(other_flight.op_intent.details.volumes or [])
124115
+ (other_flight.op_intent.details.off_nominal_volumes or [])
125116
)
126117

127-
if v1.intersects_vol4s(v2):
118+
if vc1.intersects_vol4s(vc2):
128119
return True
129120

130121
return False
131122

132123

133-
def check_for_disallowed_conflicts(
124+
def check_for_conflicts(
134125
new_op_intent: f3548_v21.OperationalIntent,
135126
existing_flight: FlightRecord | None,
136127
op_intents: list[f3548_v21.OperationalIntent],
@@ -162,7 +153,7 @@ def log(msg):
162153

163154
v1 = Volume4DCollection.from_interuss_scd_api(new_op_intent.details.volumes)
164155

165-
found_conflict = False
156+
allowed_conflict = False
166157

167158
for op_intent in op_intents:
168159
if (
@@ -182,18 +173,18 @@ def log(msg):
182173
old_priority = priority_of(op_intent.details)
183174
if new_priority > old_priority:
184175
log(
185-
f"intersection with {op_intent.reference.id} not considered: intersection with lower-priority operational intents"
176+
f"intersection with {op_intent.reference.id} allowed: intersection with lower-priority operational intents"
186177
)
187178

188-
found_conflict |= v1.intersects_vol4s(v2)
179+
allowed_conflict |= v1.intersects_vol4s(v2)
189180
continue
190181
if new_priority == old_priority and locality.allows_same_priority_intersections(
191182
old_priority
192183
):
193184
log(
194-
f"intersection with {op_intent.reference.id} not considered: intersection with same-priority operational intents (if allowed)"
185+
f"intersection with {op_intent.reference.id} allowed: intersection with same-priority operational intents (if allowed)"
195186
)
196-
found_conflict |= v1.intersects_vol4s(v2)
187+
allowed_conflict |= v1.intersects_vol4s(v2)
197188
continue
198189

199190
modifying_activated = (
@@ -208,7 +199,7 @@ def log(msg):
208199
).intersects_vol4s(v2)
209200
if preexisting_conflict:
210201
log(
211-
f"intersection with {op_intent.reference.id} not considered: modification of Activated operational intent with a pre-existing conflict"
202+
f"intersection with {op_intent.reference.id} allowed: modification of Activated operational intent with a pre-existing conflict"
212203
)
213204
continue
214205

@@ -217,7 +208,7 @@ def log(msg):
217208
f"Requested flight (priority {new_priority}) intersected {op_intent.reference.manager}'s operational intent {op_intent.reference.id} (priority {old_priority})"
218209
)
219210

220-
return found_conflict
211+
return allowed_conflict
221212

222213

223214
def op_intent_transition_valid(
@@ -490,7 +481,7 @@ def get_down_uss_op_intent(
490481
491482
Note: This function will populate volumes (for accepted or activated states) and off_nominal_volumes (for contingent
492483
and non-conforming states) with the area of interest that was requested. The reason is that later on the function
493-
`check_for_disallowed_conflicts` will need to evaluate again those conflicts to determine pre-existing conflicts.
484+
`check_for_conflicts` will need to evaluate again those conflicts to determine pre-existing conflicts.
494485
TODO: A better approach to this issue would be to store the area in conflict when a flight is planned with a
495486
conflict, that way we can just retrieve the conflicting area instead of having to compute again the intersection
496487
between the flight to be planned and the conflicting operational intent.
@@ -586,7 +577,7 @@ def check_op_intent(
586577
log(
587578
f"Checking for intersections with {', '.join(op_intent.reference.id for op_intent in op_intents)}"
588579
)
589-
has_conflicts = check_for_disallowed_conflicts(
580+
has_conflicts = check_for_conflicts(
590581
new_flight.op_intent, existing_flight, op_intents, locality, log
591582
)
592583

@@ -707,11 +698,7 @@ def notify_subscribers(
707698
:return: Notification errors if any, by subscriber.
708699
"""
709700
notif_errors: dict[f3548_v21.SubscriptionUssBaseURL, Exception] = {}
710-
base_url = f"{webapp.config[KEY_BASE_URL]}/mock/scd"
711701
for subscriber in subscribers:
712-
if subscriber.uss_base_url == base_url:
713-
# Do not notify ourselves
714-
continue
715702
update = f3548_v21.PutOperationalIntentDetailsParameters(
716703
operational_intent_id=op_intent_id,
717704
operational_intent=op_intent,

monitoring/mock_uss/f3548v21/routes_scd.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,20 @@
1212
OperationalIntentState,
1313
PutOperationalIntentDetailsParameters,
1414
)
15-
from uas_standards.interuss.automated_testing.flight_planning.v1 import api
1615

1716
from monitoring.mock_uss import webapp
1817
from monitoring.mock_uss.auth import requires_scope
19-
from monitoring.mock_uss.database import db as core_db
2018
from monitoring.mock_uss.f3548v21.flight_planning import (
21-
check_for_local_conflits,
19+
conflicts_with_flightrecords,
2220
op_intent_from_flightrecord,
2321
)
2422
from monitoring.mock_uss.flights.database import FlightRecord, db
23+
from monitoring.mock_uss.user_interactions.notifications import (
24+
UserNotification,
25+
UserNotificationType,
26+
)
2527
from monitoring.monitorlib import scd
28+
from monitoring.monitorlib.clients.flight_planning.planning import Conflict
2629

2730

2831
@webapp.route("/mock/scd/uss/v1/operational_intents/<entityid>", methods=["GET"])
@@ -111,7 +114,7 @@ def scdsc_get_operational_intent_telemetry(entityid: str):
111114
def scdsc_notify_operational_intent_details_changed():
112115
"""Implements notifyOperationalIntentDetailsChanged in ASTM SCD API."""
113116

114-
# We check if there is a conflits with our own flights
117+
# Parse the notification payload
115118
try:
116119
op_intent_data: PutOperationalIntentDetailsParameters = ImplicitDict.parse(
117120
flask.request.json or {}, PutOperationalIntentDetailsParameters
@@ -123,19 +126,19 @@ def scdsc_notify_operational_intent_details_changed():
123126
)
124127

125128
if "operational_intent" in op_intent_data and op_intent_data.operational_intent:
129+
# An op intent is being created or modified; check if it conflicts with any flights we're managing
126130
with db as tx:
127-
if check_for_local_conflits(
131+
if conflicts_with_flightrecords(
128132
op_intent_data.operational_intent, tx.flights.values()
129133
):
130-
with core_db as core_tx:
131-
core_tx.flight_planning_notifications.append(
132-
api.UserNotification(
133-
observed_at=api.Time(
134-
value=StringBasedDateTime(arrow.utcnow().datetime)
135-
),
136-
conflicts="Single",
137-
)
134+
# Virtually notify user that another op intent conflicts with their flight
135+
tx.flight_planning_notifications.append(
136+
UserNotification(
137+
type=UserNotificationType.DetectedConflict,
138+
observed_at=StringBasedDateTime(arrow.utcnow().datetime),
139+
conflicts=Conflict.Single, # TODO: detect multiple conflicts
138140
)
141+
)
139142

140143
# Do nothing else because this USS is unsophisticated and polls the DSS for
141144
# every change in its operational intents

monitoring/mock_uss/flight_planning/routes.py

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,16 @@
44

55
import arrow
66
import flask
7-
from implicitdict import ImplicitDict, StringBasedDateTime
7+
from implicitdict import ImplicitDict
88
from loguru import logger
99
from uas_standards.interuss.automated_testing.flight_planning.v1 import api
1010
from uas_standards.interuss.automated_testing.flight_planning.v1.constants import Scope
1111

1212
from monitoring.mock_uss import require_config_value, webapp
1313
from monitoring.mock_uss.auth import requires_scope
1414
from monitoring.mock_uss.config import KEY_BASE_URL
15-
from monitoring.mock_uss.database import db
1615
from monitoring.mock_uss.f3548v21.flight_planning import op_intent_from_flightinfo
17-
from monitoring.mock_uss.flights.database import FlightRecord
16+
from monitoring.mock_uss.flights.database import FlightRecord, db
1817
from monitoring.mock_uss.scd_injection.routes_injection import (
1918
clear_area,
2019
delete_flight,
@@ -87,28 +86,6 @@ def log(msg: str) -> None:
8786

8887
inject_resp = inject_flight(flight_plan_id, new_flight, existing_flight)
8988

90-
if "has_conflict" in inject_resp and inject_resp.has_conflict:
91-
with db as tx:
92-
tx.flight_planning_notifications.append(
93-
api.UserNotification(
94-
observed_at=api.Time(
95-
value=StringBasedDateTime(arrow.utcnow().datetime)
96-
),
97-
conflicts="Single",
98-
)
99-
)
100-
101-
if "has_local_conflict" in inject_resp and inject_resp.has_local_conflict:
102-
with db as tx:
103-
tx.flight_planning_notifications.append(
104-
api.UserNotification(
105-
observed_at=api.Time(
106-
value=StringBasedDateTime(arrow.utcnow().datetime)
107-
),
108-
conflicts="Single",
109-
)
110-
)
111-
11289
finally:
11390
release_flight_lock(flight_plan_id, log)
11491

@@ -197,15 +174,16 @@ def flight_planning_v1_user_notifications() -> tuple[str, int]:
197174
400,
198175
)
199176

200-
final_list = []
177+
final_list: list[api.UserNotification] = []
201178

202179
for user_notification in db.value.flight_planning_notifications:
203-
if (
204-
after.datetime
205-
<= user_notification.observed_at.value.datetime
206-
<= before.datetime
207-
):
208-
final_list.append(user_notification)
180+
if after.datetime <= user_notification.observed_at.datetime <= before.datetime:
181+
final_list.append(
182+
api.UserNotification(
183+
observed_at=api.Time(value=user_notification.observed_at),
184+
conflicts=user_notification.conflicts.to_api(),
185+
)
186+
)
209187

210188
r = api.QueryUserNotificationsResponse(user_notifications=final_list)
211189

monitoring/mock_uss/flights/database.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from implicitdict import ImplicitDict
55
from uas_standards.astm.f3548.v21.api import OperationalIntent
66

7+
from monitoring.mock_uss.user_interactions.notifications import UserNotification
78
from monitoring.monitorlib.clients.flight_planning.flight_info import FlightInfo
89
from monitoring.monitorlib.clients.mock_uss.mock_uss_scd_injection_api import (
910
MockUssFlightBehavior,
@@ -28,6 +29,9 @@ class Database(ImplicitDict):
2829
flights: dict[str, FlightRecord | None] = {}
2930
cached_operations: dict[str, OperationalIntent] = {}
3031

32+
flight_planning_notifications: list[UserNotification] = []
33+
"""List of notifications sent during flight planning operations"""
34+
3135

3236
db = SynchronizedValue(
3337
Database(),

monitoring/mock_uss/scd_injection/routes_injection.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
from datetime import UTC, datetime, timedelta
33

4+
import arrow
45
import flask
56
import requests.exceptions
67
from implicitdict import ImplicitDict, StringBasedDateTime
@@ -23,7 +24,6 @@
2324
from monitoring.mock_uss.f3548v21 import utm_client
2425
from monitoring.mock_uss.f3548v21.flight_planning import (
2526
PlanningError,
26-
check_for_local_conflits,
2727
check_op_intent,
2828
delete_op_intent,
2929
op_intent_from_flightinfo,
@@ -36,6 +36,10 @@
3636
lock_flight,
3737
release_flight_lock,
3838
)
39+
from monitoring.mock_uss.user_interactions.notifications import (
40+
UserNotification,
41+
UserNotificationType,
42+
)
3943
from monitoring.mock_uss.uspace import flight_auth
4044
from monitoring.monitorlib import versioning
4145
from monitoring.monitorlib.clients import scd as scd_client
@@ -45,6 +49,7 @@
4549
)
4650
from monitoring.monitorlib.clients.flight_planning.planning import (
4751
ClearAreaResponse,
52+
Conflict,
4853
FlightPlanStatus,
4954
PlanningActivityResponse,
5055
PlanningActivityResult,
@@ -201,10 +206,15 @@ def unsuccessful(
201206
log("Storing flight in database")
202207
with db as tx:
203208
tx.flights[flight_id] = record
204-
205-
has_local_conflict = check_for_local_conflits(
206-
record.op_intent, tx.flights.values()
207-
)
209+
if has_conflict:
210+
# Record virtual user notification that this flight caused/has a conflict
211+
tx.flight_planning_notifications.append(
212+
UserNotification(
213+
type=UserNotificationType.CausedConflict,
214+
observed_at=StringBasedDateTime(arrow.utcnow().datetime),
215+
conflicts=Conflict.Single,
216+
)
217+
)
208218

209219
step_name = "returning final successful result"
210220
log("Complete.")
@@ -215,8 +225,6 @@ def unsuccessful(
215225
activity_result=PlanningActivityResult.Completed,
216226
flight_plan_status=FlightPlanStatus.from_flightinfo(record.flight_info),
217227
notes=notes,
218-
has_conflict=has_conflict,
219-
has_local_conflict=has_local_conflict,
220228
)
221229
except (ValueError, ConnectionError) as e:
222230
notes = (
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from enum import Enum
2+
3+
from implicitdict import ImplicitDict, StringBasedDateTime
4+
5+
from monitoring.monitorlib.clients.flight_planning.planning import Conflict
6+
7+
8+
class UserNotificationType(str, Enum):
9+
"""Type of notification the virtual user received"""
10+
11+
CausedConflict = "CausedConflict"
12+
"""User's flight caused or has a conflict with another flight"""
13+
14+
DetectedConflict = "DetectedConflict"
15+
"""Another flight created a conflict with one of the user's flights"""
16+
17+
18+
class UserNotification(ImplicitDict):
19+
type: UserNotificationType
20+
observed_at: StringBasedDateTime
21+
conflicts: Conflict

0 commit comments

Comments
 (0)