Skip to content

Commit c2b61c3

Browse files
[uss_qualifier/reports] Add KML visualization of RID test queries to sequence view artifact (#1382)
1 parent 902b1d8 commit c2b61c3

File tree

7 files changed

+232
-28
lines changed

7 files changed

+232
-28
lines changed

monitoring/monitorlib/kml/generation.py

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import math
2+
from datetime import datetime
23

34
import s2sphere
45
from pykml.factory import KML_ElementMaker as kml
@@ -48,6 +49,37 @@ def _distance_value_of(distance: Altitude | Radius) -> float:
4849
raise NotImplementedError(f"Distance units {distance.units} not yet supported")
4950

5051

52+
def make_basic_placemark(
53+
name: str | None = None,
54+
style_url: str | None = None,
55+
description: str | None = None,
56+
time_start: datetime | None = None,
57+
time_end: datetime | None = None,
58+
):
59+
# Create placemark
60+
args = []
61+
if name is not None:
62+
args.append(kml.name(name))
63+
if style_url is not None:
64+
args.append(kml.styleUrl(style_url))
65+
placemark = kml.Placemark(*args)
66+
if description:
67+
placemark.append(kml.description(description))
68+
69+
# Set time range
70+
timespan = None
71+
if time_start:
72+
timespan = kml.TimeSpan(kml.begin(time_start.isoformat()))
73+
if time_end:
74+
if timespan is None:
75+
timespan = kml.TimeSpan()
76+
timespan.append(kml.end(time_end.isoformat()))
77+
if timespan is not None:
78+
placemark.append(timespan)
79+
80+
return placemark
81+
82+
5183
def make_placemark_from_volume(
5284
v4: Volume4D,
5385
name: str | None = None,
@@ -74,25 +106,15 @@ def make_placemark_from_volume(
74106
raise NotImplementedError("No vertices found")
75107

76108
# Create placemark
77-
args = []
78-
if name is not None:
79-
args.append(kml.name(name))
80-
if style_url is not None:
81-
args.append(kml.styleUrl(style_url))
82-
placemark = kml.Placemark(*args)
83-
if description:
84-
placemark.append(kml.description(description))
85-
86-
# Set time range
87-
timespan = None
88-
if "time_start" in v4 and v4.time_start:
89-
timespan = kml.TimeSpan(kml.begin(v4.time_start.datetime.isoformat()))
90-
if "time_end" in v4 and v4.time_end:
91-
if timespan is None:
92-
timespan = kml.TimeSpan()
93-
timespan.append(kml.end(v4.time_end.datetime.isoformat()))
94-
if timespan is not None:
95-
placemark.append(timespan)
109+
placemark = make_basic_placemark(
110+
name=name,
111+
style_url=style_url,
112+
description=description,
113+
time_start=v4.time_start.datetime
114+
if "time_start" in v4 and v4.time_start
115+
else None,
116+
time_end=v4.time_end.datetime if "time_end" in v4 and v4.time_end else None,
117+
)
96118

97119
# Create top and bottom of the volume
98120
avg = s2sphere.LatLng.from_degrees(
@@ -192,6 +214,23 @@ def make_placemark_from_volume(
192214
return placemark
193215

194216

217+
def add_point(
218+
placemark,
219+
lat_degrees: float,
220+
lng_degrees: float,
221+
) -> None:
222+
placemark.append(kml.Point(kml.coordinates(f"{lng_degrees},{lat_degrees},0")))
223+
224+
225+
def add_linestring(
226+
placemark,
227+
lng_lat_alt: list[tuple[float, float, float]],
228+
):
229+
# Create the point
230+
coord_string = " ".join(f"{lng},{lat},{alt}" for (lng, lat, alt) in lng_lat_alt)
231+
placemark.append(kml.LineString(kml.tessellate(1), kml.coordinates(coord_string)))
232+
233+
195234
def query_styles() -> list:
196235
"""Provides KML styles for query areas."""
197236
return [

monitoring/monitorlib/kml/rid.py

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
from datetime import timedelta
2+
3+
from pykml.factory import KML_ElementMaker as kml
4+
from uas_standards.astm.f3411.v22a.api import GetFlightsResponse
5+
from uas_standards.interuss.automated_testing.rid.v1.injection import (
6+
ChangeTestResponse,
7+
CreateTestParameters,
8+
)
9+
10+
from monitoring.monitorlib.kml.generation import (
11+
add_linestring,
12+
add_point,
13+
make_basic_placemark,
14+
)
15+
16+
17+
def create_test(req: CreateTestParameters, resp: ChangeTestResponse) -> list:
18+
result = []
19+
for test_flight in resp.injected_flights:
20+
folder = kml.Folder(kml.name(f"Test flight {test_flight.injection_id}"))
21+
requested_flight = None
22+
for flight in req.requested_flights:
23+
if flight.injection_id == test_flight.injection_id:
24+
requested_flight = flight
25+
for i, telemetry in enumerate(test_flight.telemetry):
26+
if (
27+
"position" in telemetry
28+
and telemetry.position
29+
and "lat" in telemetry.position
30+
and telemetry.position.lat
31+
and "lng" in telemetry.position
32+
and telemetry.position.lng
33+
):
34+
style_url = "#modifiedtelemetry"
35+
if requested_flight and len(requested_flight.telemetry) > i:
36+
rt = requested_flight.telemetry[i]
37+
if (
38+
"position" in rt
39+
and rt.position
40+
and "lat" in rt.position
41+
and rt.position.lat == telemetry.position.lat
42+
and "lng" in rt.position
43+
and rt.position.lng == telemetry.position.lng
44+
):
45+
style_url = "#unmodifiedtelemetry"
46+
if "timestamp" in telemetry and telemetry.timestamp:
47+
time_start = telemetry.timestamp.datetime
48+
if i < len(test_flight.telemetry) - 1:
49+
tt = test_flight.telemetry[i + 1]
50+
if "timestamp" in tt and tt.timestamp:
51+
time_end = tt.timestamp.datetime
52+
else:
53+
time_end = time_start + timedelta(seconds=1)
54+
else:
55+
time_end = time_start + timedelta(seconds=1)
56+
else:
57+
time_start = None
58+
time_end = None
59+
point = make_basic_placemark(
60+
name=f"Telemetry {i}",
61+
style_url=style_url,
62+
time_start=time_start,
63+
time_end=time_end,
64+
)
65+
add_point(point, telemetry.position.lat, telemetry.position.lng)
66+
folder.append(point)
67+
result.append(folder)
68+
return result
69+
70+
71+
def get_flights_v22a(url: str, resp: GetFlightsResponse) -> list:
72+
result = []
73+
# TODO: render request bounding box from query parameter in url
74+
for flight in resp.flights or [] if "flights" in resp else []:
75+
folder = kml.Folder(kml.name(flight.id))
76+
if (
77+
"current_state" in flight
78+
and flight.current_state
79+
and "lat" in flight.current_state.position
80+
and flight.current_state.position.lat
81+
and "lng" in flight.current_state.position
82+
and flight.current_state.position.lng
83+
):
84+
point = make_basic_placemark(
85+
name=f"{flight.aircraft_type.name} {flight.id}{' SIMULATED' if flight.simulated else ''}",
86+
style_url="#aircraft",
87+
)
88+
add_point(
89+
point,
90+
flight.current_state.position.lat,
91+
flight.current_state.position.lng,
92+
)
93+
folder.append(point)
94+
if "recent_positions" in flight and flight.recent_positions:
95+
tail = make_basic_placemark(
96+
name=f"{flight.id} recent positions", style_url="#ridtail"
97+
)
98+
add_linestring(
99+
tail,
100+
[
101+
(rp.position.lng, rp.position.lat, 0)
102+
for rp in flight.recent_positions
103+
if "lng" in rp.position
104+
and rp.position.lng
105+
and "lat" in rp.position
106+
and rp.position.lat
107+
],
108+
)
109+
folder.append(tail)
110+
111+
result.append(folder)
112+
return result
113+
114+
115+
def rid_styles() -> list:
116+
"""Provides KML styles used by RID visualizations above."""
117+
return [
118+
kml.Style(
119+
kml.IconStyle(
120+
kml.scale(1.4),
121+
kml.Icon(
122+
kml.href(
123+
"http://maps.google.com/mapfiles/kml/shapes/placemark_circle.png"
124+
)
125+
),
126+
),
127+
id="unmodifiedtelemetry",
128+
),
129+
kml.Style(
130+
kml.IconStyle(
131+
kml.scale(1.4),
132+
kml.Icon(
133+
kml.href(
134+
"http://maps.google.com/mapfiles/kml/shapes/placemark_square.png"
135+
)
136+
),
137+
),
138+
id="modifiedtelemetry",
139+
),
140+
kml.Style(
141+
kml.IconStyle(
142+
kml.scale(1.4),
143+
kml.Icon(
144+
kml.href("http://maps.google.com/mapfiles/kml/shapes/airports.png")
145+
),
146+
),
147+
id="aircraft",
148+
),
149+
]

monitoring/uss_qualifier/reports/sequence_view/kml.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
from typing import Protocol, get_type_hints
33

44
from implicitdict import ImplicitDict
5-
from loguru import logger
65
from lxml import etree
76
from pykml.factory import KML_ElementMaker as kml
87
from pykml.util import format_xml_with_cdata
8+
from uas_standards.astm.f3411.v22a import api as f3411v22a
99
from uas_standards.astm.f3548.v21.api import (
1010
GetOperationalIntentDetailsResponse,
1111
QueryOperationalIntentReferenceParameters,
@@ -15,6 +15,7 @@
1515
UpsertFlightPlanRequest,
1616
UpsertFlightPlanResponse,
1717
)
18+
from uas_standards.interuss.automated_testing.rid.v1 import injection as rid_injection
1819

1920
from monitoring.monitorlib.errors import stacktrace_string
2021
from monitoring.monitorlib.fetch import Query, QueryType
@@ -28,6 +29,7 @@
2829
upsert_flight_plan,
2930
)
3031
from monitoring.monitorlib.kml.generation import query_styles
32+
from monitoring.monitorlib.kml.rid import create_test, get_flights_v22a, rid_styles
3133
from monitoring.uss_qualifier.reports.sequence_view.summary_types import TestedScenario
3234

3335

@@ -125,7 +127,6 @@ def make_scenario_kml(scenario: TestedScenario) -> str:
125127
)
126128
except ValueError as e:
127129
msg = f"Error parsing request into {render_info.request_type.__name__}"
128-
logger.warning(msg)
129130
query_folder.append(
130131
kml.Folder(
131132
kml.name(msg),
@@ -144,7 +145,6 @@ def make_scenario_kml(scenario: TestedScenario) -> str:
144145
)
145146
except ValueError as e:
146147
msg = f"Error parsing response into {render_info.response_type.__name__}"
147-
logger.warning(msg)
148148
query_folder.append(
149149
kml.Folder(
150150
kml.name(msg),
@@ -164,7 +164,11 @@ def make_scenario_kml(scenario: TestedScenario) -> str:
164164
)
165165
doc = kml.kml(
166166
kml.Document(
167-
*query_styles(), *f3548v21_styles(), *flight_planning_styles(), top_folder
167+
*query_styles(),
168+
*f3548v21_styles(),
169+
*flight_planning_styles(),
170+
*rid_styles(),
171+
top_folder,
168172
)
169173
)
170174
return etree.tostring(format_xml_with_cdata(doc), pretty_print=True).decode("utf-8")
@@ -188,3 +192,15 @@ def render_flight_planning_upsert_flight_plan(
188192
req: UpsertFlightPlanRequest, resp: UpsertFlightPlanResponse
189193
):
190194
return [upsert_flight_plan(req, resp)]
195+
196+
197+
@query_kml_renderer(QueryType.InterussRIDAutomatedTestingV1CreateTest) # pyright: ignore[reportArgumentType]
198+
def render_rid_injection_create_test(
199+
req: rid_injection.CreateTestParameters, resp: rid_injection.ChangeTestResponse
200+
):
201+
return create_test(req, resp)
202+
203+
204+
@query_kml_renderer(QueryType.F3411v22aUSSSearchFlights) # pyright: ignore[reportArgumentType]
205+
def render_f3411_22a_search_flights(query: Query, resp: f3411v22a.GetFlightsResponse):
206+
return get_flights_v22a(query.request.url, resp)

monitoring/uss_qualifier/scenarios/astm/netrid/v22a/display_data_evaluator_flight_presence.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ The timestamps of the injected telemetry usually start in the future. If a flig
1010

1111
**[astm.f3411.v22a.NET0610](../../../../requirements/astm/f3411/v22a.md)** requires that SPs make all UAS operations discoverable over the duration of the flight plus *NetMaxNearRealTimeDataPeriod*, so each injected flight should be observable during this time. If a flight is not observed during its appropriate time period, this check will fail.
1212

13-
**[astm.f3411.v22a.NET0710,1](../../../../requirements/astm/f3411/v22a.md)** and **[astm.f3411.v22a.NET0340](../../../../requirements/astm/f3411/v22a.md) require a Service Provider to implement the GET flights endpoint. This check will also fail if uss_qualifier cannot query that endpoint (specified in the ISA present in the DSS) successfully.
13+
**[astm.f3411.v22a.NET0710,1](../../../../requirements/astm/f3411/v22a.md)** and **[astm.f3411.v22a.NET0340](../../../../requirements/astm/f3411/v22a.md)** require a Service Provider to implement the GET flights endpoint. This check will also fail if uss_qualifier cannot query that endpoint (specified in the ISA present in the DSS) successfully.
1414

15-
The identity of flights is determined by precisely matching the known injected positions. If the flight can be found, the USS may not have met **[astm.f3411.v22a.NET0260,Table1,10](../../../../requirements/astm/f3411/v22a.md)** or **[astm.f3411.v22a.NET0260,Table1,11](../../../../requirements/astm/f3411/v22a.md)** prescribing provision of position data consistent with the common data dictionary.
15+
The identity of flights is determined by precisely matching the known injected positions. If the flight cannot be found, the USS may not have met **[astm.f3411.v22a.NET0260,Table1,10](../../../../requirements/astm/f3411/v22a.md)** or **[astm.f3411.v22a.NET0260,Table1,11](../../../../requirements/astm/f3411/v22a.md)** prescribing provision of position data consistent with the common data dictionary.
1616

1717
## ⚠️ Lingering flight check
1818

monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@
428428
<tr>
429429
<td><a href="../../../requirements/astm/f3411/v22a.md">NET0340</a></td>
430430
<td>Implemented</td>
431-
<td><a href="../../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
431+
<td><a href="../../../scenarios/astm/netrid/v22a/networked_uas_disconnect.md">ASTM NetRID networked UAS disconnection</a><br><a href="../../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
432432
</tr>
433433
<tr>
434434
<td><a href="../../../requirements/astm/f3411/v22a.md">NET0420</a></td>

monitoring/uss_qualifier/suites/uspace/network_identification.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@
420420
<tr>
421421
<td><a href="../../requirements/astm/f3411/v22a.md">NET0340</a></td>
422422
<td>Implemented</td>
423-
<td><a href="../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
423+
<td><a href="../../scenarios/astm/netrid/v22a/networked_uas_disconnect.md">ASTM NetRID networked UAS disconnection</a><br><a href="../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
424424
</tr>
425425
<tr>
426426
<td><a href="../../requirements/astm/f3411/v22a.md">NET0420</a></td>

monitoring/uss_qualifier/suites/uspace/required_services.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@
421421
<tr>
422422
<td><a href="../../requirements/astm/f3411/v22a.md">NET0340</a></td>
423423
<td>Implemented</td>
424-
<td><a href="../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
424+
<td><a href="../../scenarios/astm/netrid/v22a/networked_uas_disconnect.md">ASTM NetRID networked UAS disconnection</a><br><a href="../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
425425
</tr>
426426
<tr>
427427
<td><a href="../../requirements/astm/f3411/v22a.md">NET0420</a></td>

0 commit comments

Comments
 (0)