Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 19 additions & 2 deletions example_data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Examples days:

Paths in the example configs should parse on Mac/Linux or Windows. However, the paths are relative, so scripts need to be run from the repository root.

**Clock conventions:** MXAK AIS timestamps are UTC-naive; NVSPL and ship-visit times are site-local naive (e.g. GLBALSTL ≈ `America/Juneau`).
**Clock conventions:** MXAK AIS CSV timestamps are UTC-naive; `load_tracks` converts them to site-local naive when a deployment microphone is provided (same clock as NVSPL and ship visits). ADS-B TSV Unix epochs are decoded to naive datetimes and used on the NVSPL clock without a site timezone shift (ground-truthing convention). Legacy pre-2020 ADS-B `.txt` (`EarlyAdsb`) is already naive local. NVSPL and ship-visit times are site-local naive (e.g. GLBALSTL ≈ `America/Juneau`). GPS overflights DB times (`ak_datetime`) are already site-local.

## Layout

Expand Down Expand Up @@ -51,7 +51,8 @@ Paths in the example configs should parse on Mac/Linux or Windows. However, the
| `nvspl_archive/.../NVSPL_DENATRLA_2025_06_23_*.txt` | DENATRLA | Hourly SPL for 2025-06-23 | Offline dev (`DENA_example.config`) |

## Local ground truthing (from repo root)
Running the ground truthing script is a great way to view vessel/flight tracks alongside NVSPL data. The following commands should work out of the box after setting up the project virtual environment.

Running the ground truthing script is a great way to view vessel/flight tracks alongside NVSPL data. The following commands should work out of the box after setting up the project virtual environment.

**GLBALSTL (AIS):** `-e GLBA_example`

Expand All @@ -68,3 +69,19 @@ python -m nps_active_space.scripts.run_ground_truthing \
```

To rebuild `faa/MASTER_DENATRLA_20250623_sample.txt` after changing the example ADS-B day, extract rows from the full FAA `MASTER.txt` for the unique `ICAO_address` values in that day's TSV (see `FAAReleasable` in `utils/models.py`).

## Local viz (from repo root)

**GLBALSTL (AIS):** `-e GLBA_example`

```bash
python -m nps_active_space.scripts.viz GLBALSTL2024 -e GLBA_example \
--track-source AIS --start-date 2024-05-24 --end-date 2024-05-24 -m 100
```

**DENATRLA (ADS-B):** `-e DENA_example`

```bash
python -m nps_active_space.scripts.viz DENATRLA2025 -e DENA_example \
--track-source ADSB --start-date 2025-06-23 --end-date 2025-06-23 -m 100
```
17 changes: 14 additions & 3 deletions nps_active_space/active_space/layered_active_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,22 @@ def __init__(self, designator, layer_dirs, study_area, gain=None, crs="epsg:4326
self.set_gain(gain)
self.fit_pbar = None

if not self.layer_dirs:
raise FileNotFoundError(
f"No active space layer directories provided for {designator}."
)

# determine min and max gain - import here to avoid circular import
from nps_active_space.utils.helpers import omni_to_gain
first_layer_dir = list(self.layer_dirs.values())[0]
active_names = glob.glob(os.path.join(first_layer_dir, "*_O_*.geojson"))
gains = list(map(lambda f: omni_to_gain(f), active_names))
active_names = []
for layer_dir in self.layer_dirs.values():
active_names.extend(glob.glob(os.path.join(layer_dir, "*_O_*.geojson")))
if not active_names:
raise FileNotFoundError(
f"No *_O_*.geojson active space files found for {designator} in:\n"
+ "\n".join(f" {d}" for d in self.layer_dirs.values())
)
gains = [omni_to_gain(path) for path in active_names]
self.min_gain = min(gains)
self.max_gain = max(gains)

Expand Down
94 changes: 0 additions & 94 deletions nps_active_space/ground_truthing/load_tracks.py

This file was deleted.

72 changes: 67 additions & 5 deletions nps_active_space/ground_truthing/segments.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,65 @@
import datetime as dt

import geopandas as gpd
import numpy as np
from shapely.geometry import LineString, Point

# Mutable [start, end] pair on the time_audible axis (lists are updated in place).
AudibleRange = list[dt.datetime]

# Splines are 1 Hz; storing every vertex makes GeoJSON huge. Reload uses start_dt/end_dt only.
ANNOTATION_MAX_VERTICES = 64
COORD_DECIMALS = 6
Z_DECIMALS = 1


def _round_coord(coord: tuple) -> tuple:
"""Round WGS84 xy and elevation for compact GeoJSON."""
if len(coord) < 3:
return (round(coord[0], COORD_DECIMALS), round(coord[1], COORD_DECIMALS))
return (
round(coord[0], COORD_DECIMALS),
round(coord[1], COORD_DECIMALS),
round(coord[2], Z_DECIMALS),
)


def downsample_linestring(
line: LineString,
max_vertices: int = ANNOTATION_MAX_VERTICES,
) -> LineString:
"""Reduce vertex count for annotation storage and viz (keeps endpoints)."""
if line.is_empty:
return line
coords = list(line.coords)
if len(coords) <= max_vertices:
return LineString([_round_coord(c) for c in coords])
indices = np.unique(np.linspace(0, len(coords) - 1, max_vertices, dtype=int))
sampled = [_round_coord(coords[i]) for i in indices]
return LineString(sampled)


def storage_geometry_from_points(geometries: list) -> Point | LineString:
"""Build a compact Point or downsampled LineString for GeoJSON export."""
if len(geometries) == 1:
geom = geometries[0]
if isinstance(geom, Point):
return Point(_round_coord(geom.coords[0]))
return downsample_linestring(LineString([geom]))
return downsample_linestring(LineString(geometries))


def compact_geometry(
geom: Point | LineString,
max_vertices: int = ANNOTATION_MAX_VERTICES,
) -> Point | LineString:
"""Downsample and round a stored annotation geometry."""
if isinstance(geom, Point):
return Point(_round_coord(geom.coords[0]))
if isinstance(geom, LineString):
return downsample_linestring(geom, max_vertices=max_vertices)
return geom


def collapse_audible_ranges(ranges: list[AudibleRange]) -> list[AudibleRange]:
"""Collapse overlapping audible ranges into single audible ranges.
Expand Down Expand Up @@ -85,8 +139,10 @@ def build_annotation_segments(
'valid': valid,
'audible': False,
'note': note,
'geometry': points.geometry.iat[0] if points.shape[0] == 1
else LineString(points.geometry.tolist())
'geometry': storage_geometry_from_points(
[points.geometry.iat[0]] if points.shape[0] == 1
else list(points.geometry)
)
})

else:
Expand All @@ -112,7 +168,9 @@ def build_annotation_segments(
'valid': True,
'audible': False,
'note': note,
'geometry': LineString(first_inaudible_segment.geometry.tolist())}
'geometry': storage_geometry_from_points(
list(first_inaudible_segment.geometry)
)}
)

# add segments for each audible range and the inaudible range following it
Expand All @@ -126,7 +184,9 @@ def build_annotation_segments(
'valid': True,
'audible': True,
'note': note,
'geometry': LineString(audible_segment.geometry.tolist())}
'geometry': storage_geometry_from_points(
list(audible_segment.geometry)
)}
)

if i+1 < len(audible_ranges):
Expand All @@ -143,7 +203,9 @@ def build_annotation_segments(
'valid': True,
'audible': False,
'note': note,
'geometry': LineString(inaudible_segment.geometry.tolist())}
'geometry': storage_geometry_from_points(
list(inaudible_segment.geometry)
)}
)

gdf = gpd.GeoDataFrame(segments, geometry='geometry', crs=points.crs)
Expand Down
25 changes: 22 additions & 3 deletions nps_active_space/scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,15 +453,18 @@ This script is used to visualize select geospatial objects relevant to the `nps_
| command-line arg | description |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `deployment` (no flag) | **required.**<br/>The deployment name, e.g., DENACATH2018 |
| `-e`, `--environment` | **required.**<br/>The configuration environment to use. _Ex_: To use `production.config` pass `-e production` |
| `-e`, `--environment` | Config environment name (e.g. `production` for `production.config`). Default: `DENA_streamline` |
| `-g`, `--gain` | Active space gain, if not the optimal default found in `fits.csv` |
| `-s`, `--active-space` | If included, load and plot the active space |
| `-a`, `--annotations` | If included, load and plot annotations |
| `-t`, `--audible-transits` | If included, load and plot audible transits |
| `--all` | Load and plot all geospatial objects (shorthand for `--active-space --annotations --audible-transits`) |
| `-m`, `--max-tracks` | **_default 500_**<br>Maximum number of annotation tracks or audible transits to show |
| `--track-source` | Load and plot tracks from **{GPS, ADSB, AIS}**. Uses the same config paths as ground truthing (`[data]` ais/adsb, overflights DB for GPS). Not included in `--all`. Former `-v`/`--vessels` (AIS only) still works but is deprecated. |
| `--all` | Load and plot active space, annotations, and audible transits (does **not** include `--track-source`) |
| `-m`, `--max-tracks` | **_default 500_**<br>Maximum number of annotation tracks, audible transits, or causal tracks to show |
| `--annotation-file` | **_default to deployment dir_**<br/>Path to .geojson file from which to load annotations |
| `--transits-pkl` | **_default to deployment dir_**<br/>Path to .pkl file from which to load audible transits |
| `--start-date` | Track query start date (YYYY-MM-DD). **Requires `--track-source`.** Default: Jan 1 of deployment year. |
| `--end-date` | Track query end date (YYYY-MM-DD). **Requires `--track-source`.** Default: Dec 31 of deployment year. |
| `--terraced` | If included, render the active space as a terraced surface instead of contours |
| `--fill-layers` | If included, fill the interior of each active space contour polygon |

Expand All @@ -471,10 +474,26 @@ Example executions:
$ python -u -W ignore nps_active_space/scripts/viz.py DENATRLA2024 -e production --all
```

```bash
$ python -u -W ignore nps_active_space/scripts/viz.py GLBALSTL2024 -e production -s -a --track-source AIS --terraced
```

```bash
$ python -u -W ignore nps_active_space/scripts/viz.py DENATRLA2024 -e production -g 15.0 -s -a -m 700 --terraced
```

```bash
python -m nps_active_space.scripts.viz GLBALSTL2024 -e GLBA_example \
--track-source AIS --start-date 2024-05-24 --end-date 2024-05-24 -m 100

python -m nps_active_space.scripts.viz DENATRLA2025 -e DENA_example \
--track-source ADSB --start-date 2025-06-23 --end-date 2025-06-23 -m 100
```

**Track plotting vs ground truthing:** viz uses the same `load_tracks` loader but draws raw point sequences (not annotation splines), does not apply clock-drift correction, and defaults to the full deployment year unless `--start-date` / `--end-date` are set. Ground truthing uses the NVSPL archive date span and drift files when present.

**Note:** In viz, `-t` means audible transits; in ground truthing, `-t` means `--track-source`.

----

### Generate Active Space Mesh
Expand Down
2 changes: 1 addition & 1 deletion nps_active_space/scripts/run_ground_truthing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import os
import iyore
import nps_active_space.ground_truthing as app
from nps_active_space.ground_truthing.load_tracks import load_tracks
from nps_active_space.utils.load_tracks import load_tracks
from nps_active_space.utils.enums import TrackSource
from nps_active_space.utils.models import Nvspl
import nps_active_space.utils.config as cfg
Expand Down
Loading