gnssrefl.tracks module
tracks.py: multi-GNSS track ground-truth for gnssrefl.
This module owns both sides of the multi-GNSS tracks.json artifact:
Build side: build_tracks walks SNR files via gnssrefl.extract_arcs, folds arcs into per-(sat, freq) periodic ground tracks, fits a single epoch per track, and writes the JSON.
Runtime side: load_tracks_json, build_lookup_index, lookup_arc, and attach_track_id consume the JSON to tag arcs at extract time with their (track_id, track_epoch).
Why
gnssrefl currently identifies VWC tracks by clustering arcs on azimuth alone (+/-3 deg from cluster center). This works for GPS because GPS ground tracks repeat every sidereal day, so a sat’s daily passes have stable azimuths. For non-GPS the repeat period is multi-day:
GPS: 1 sidereal day (2 orbits/day) GLONASS: 8 sidereal days (17 orbits / 8 sid days) Galileo: 10 sidereal days (17 orbits / 10 sid days) BeiDou MEO: 7 sidereal days (13 orbits / 7 sid days)
Within one repeat period a single sat traces 10-17 distinct ground tracks across the sky. Pure az clustering can’t separate them (they cover the whole horizon), so the existing VWC code restricts to GPS. This module produces a clean ground-truth track set for the other constellations.
This is the minimal MVP variant: each track has exactly one stable epoch
(epoch_id == 0). The schema reserves epochs as a list so future
changepoint-detection work can add entries without breaking readers.
Identity scope
There are two layers of identity in the tracks system:
track_id is forever. The same id appears in every artifact derived from any tracks-shaped JSON (tracks.json, vwc_tracks.json, the per-day phase files, the vwc output files).
epoch_id equals the epoch’s list index (0..N-1) within one saved tracks_json. It is regenerated on every save_tracks and renumbered on every structural mutation (split_epoch, merge_epochs).
- gnssrefl.tracks.active_epoch_days(tracks_json)
Set of (year, doy) pairs spanning the union of active epoch windows.
- gnssrefl.tracks.assign_tracks(df_freq, T_candidates_solar)
Walk arcs forward in MJD and assign track ids.
Returns (df_sorted, track_ids, match_T) where match_T[i] is the candidate T (in solar days) used to extend arc i, or NaN if arc i seeded a new track.
- gnssrefl.tracks.attach_legacy_apriori(arcs, station, extension='')
Tag arcs with legacy GPS-only per-freq apriori_rh_{fr}.txt entries.
Groups arcs by frequency, loads
apriori_rh_{fr}.txtonce per freq, and matches each arc to a track by (satellite, circular azimuth distance <= 3 deg), the same rule used historically by the legacy VWC pipeline.Sets
meta['apriori_RH'],meta['track_azim'],meta['track_id'], andmeta['track_epoch']on every arc.apriori_RH/track_azimareNoneon miss;track_id/track_epochare-1on miss.track_epochis always0on match (the legacy path has only one epoch per track).
- gnssrefl.tracks.attach_track_id(arcs, track_file_path, year, doy, track_cache=None)
Tag each arc’s metadata with track info from tracks-shaped JSON.
Works against both
tracks.json(the station-wide catalog) andvwc_tracks.json(the VWC-eligible filtered subset, which adds a per epochapriori_RHfield).- Each
metadatadict gets these new keys: track_id,track_epoch(both -1 on no match),track_azim(az_avg_minelof the matched epoch, orNone),apriori_RH(matched epoch’sapriori_RH, orNone; only present invwc_tracks.json).
- Parameters:
arcs (list of (metadata, data) tuples) – Output of
extract_arcs, modified in place.track_file_path (path-like) – Path to a tracks-shaped JSON file (
tracks.jsonfrom build_tracks, orvwc_tracks.jsonfromvwc_input).year (int) – Year and day-of-year of the arcs (used together with each arc’s
arc_timestampto compute MJD for the lookup).doy (int) – Year and day-of-year of the arcs (used together with each arc’s
arc_timestampto compute MJD for the lookup).track_cache (dict, optional) – Path-keyed cache for reusing prebuilt lookup indexes across many calls. When omitted, a module-level default (TRACK_INDEX_CACHE) is used, so repeated calls transparently reuse the same index across stations, extensions, and tracks.json vs vwc_tracks.json. Cache entries are never invalidated; restart the process if a tracks file is rewritten on disk.
- Returns:
The same
arcslist (modified in place), for chaining.- Return type:
list
- Each
- gnssrefl.tracks.build_lookup_index(tracks_json)
Build a (sat, freq) -> [(track_id, track_epoch, def_dict), …] index.
- Each
def_dictcarries the fields needed by lookup_arc: rise, repeat_interval_d, anchor_mjd, az_avg_minel, az_drift_rate, first_mjd, last_mjd, epoch_type
- Each
- gnssrefl.tracks.build_tracks(station, year, year_end=None, extension='', snr_type=66, source='auto')
Build tracks.json for a station over [year .. year_end].
Collects per-arc geometry (sat, freq, mjd, azim, rise), folds arcs into periodic tracks, drops fragment tracks below the per-freq filter threshold (10 percent of per-freq median arcs per track), fits a single periodic epoch per surviving track, and writes the JSON via FileManagement.
Two arc sources are supported, selected by
source:'snr'walk SNR files via load_arcs: authoritative, slow,covers every frequency the SNR file contains.
'results'read results/ + failQC/ via load_arcs_from_results:fast, but only covers frequencies gnssir was run with.
'auto'(default) prefer'results'if any results/ dir ispopulated in range; else fall back to
'snr'.
- Parameters:
station (str) – 4-char station name (lowercase)
year (int) – Start year
year_end (int, optional) – End year inclusive. Defaults to
year.extension (str) – Strategy extension subdirectory. Default ‘’.
snr_type (int) – SNR file type for the SNR-walk path. Default 66.
source (str) – Arc source: ‘auto’ | ‘results’ | ‘snr’. Default ‘auto’.
- Returns:
tracks_json (dict) – In-memory tracks_json matching the on-disk JSON.
arcs_df (pandas.DataFrame or None) – Per-arc DataFrame in the extract_arcs_gnssir_results schema (mjd, azim, constellation, RH, match_T, track_id, track_epoch), or None when source=’snr’ (no RH available).
- gnssrefl.tracks.doy_hour_to_mjd(year, doy, hours)
Convert (year, doy, fractional hours UTC) to MJD.
- gnssrefl.tracks.fit_segment(arcs)
Fit T and azimuth model for a single track’s arcs.
Returns (T_fit, anchor_mjd, az_avg_minel, az_drift_rate). az_drift_rate is only nonzero for BeiDou (secular azimuth drift).
- gnssrefl.tracks.iso_to_mjd(iso_str)
ISO 8601 ‘YYYY-MM-DDTHH:MM:SSZ’ UTC string -> MJD float.
- gnssrefl.tracks.load_arcs(station, year, year_end, extension, snr_type=66, fast=False)
Collect per-arc geometry for station across [year..year_end] into a DataFrame.
Unified SNR-walk and results-walk entry point. Both paths return the same schema (year, doy, sat, freq, mjd, azim, rise); the results path also carries RH (useful for later stats).
fast=False(default): walk SNR files day by day via extract_arcs_from_station. Authoritative; covers every frequency the SNR file contains. Slow.fast=True: read the gnssir results/ + failQC/ artifacts via load_results_with_failqc. Orders of magnitude faster, but only covers frequencies gnssir was configured to run. Requires a prior gnssir run with save_failqc=True.
BeiDou GEO/IGSO PRNs in BEIDOU_NON_MEO_SATS are skipped here so the rest of the pipeline never sees them.
- gnssrefl.tracks.load_tracks_json(path)
Load a tracks.json file from disk and return the tracks_json dict.
- gnssrefl.tracks.lookup_arc(sat, freq, obs_time_mjd, obs_az_minel, track_lookup_index, az_tol=5.0, time_tol_min=30)
Look up the (track_id, track_epoch, epoch_entry) for a single arc.
- Parameters:
sat (int) – Satellite number and frequency code identifying the candidate list.
freq (int) – Satellite number and frequency code identifying the candidate list.
obs_time_mjd (float) – Arc observation time in MJD.
obs_az_minel (float) – Arc azimuth at minimum elevation (degrees). Compared against each candidate’s drift-corrected expected azimuth.
track_lookup_index (dict) – Pre-built (sat, freq) -> [(track_id, track_epoch, entry_dict), …] candidate index produced by
build_lookup_index(tracks_json). Eachentry_dictcarries the epoch’s matching parameters (first_mjd,last_mjd,anchor_mjd,repeat_interval_d,az_avg_minel,az_drift_rate,epoch_type,ignored_ranges).track's (Active matches require the query to fall inside the) –
``[first_mjd –
within (last_mjd]`` interval AND fit the periodic model) –
az_tol. (time_tol_min and) –
- Returns:
(track_id, track_epoch, entry) –
(-1, -1, None)if no track def covers this arc.entryis the matched epoch dict frombuild_lookup_index(keys includeaz_avg_minelandapriori_RH) when the match succeeds.- Return type:
tuple
- gnssrefl.tracks.mjd_to_iso_ceil(mjd)
MJD -> ISO 8601 Z UTC string, rounded UP to the nearest second.
- gnssrefl.tracks.mjd_to_iso_floor(mjd)
MJD -> ISO 8601 Z UTC string, rounded DOWN to the nearest second.
- gnssrefl.tracks.results_dir_has_files(station, year, year_end, extension)
True if any year in the range has a populated results/ dir for station.
- gnssrefl.tracks.unwrap_az(az)
Bring all azimuths into a single ±180° window centered on az[0].
- gnssrefl.tracks.warn_legacy_apriori_and_exit(station, missing_file, extension='')
If any GPS
apriori_rh_{fr}.txtexists, print a -legacy T hint and exit.Called from modern-path entry points when
missing_file(e.g.vwc_tracks.json) is absent, to nudge users who still have artifacts from a legacy GPS-only run toward passing-legacy T.
- gnssrefl.tracks.write_tracks_json(tracks_json, f)
Write tracks_json to file handle
fwith custom indentation for readability: indent=2 at the structural level, but each innerignored_rangespair[mjd_start, mjd_end]is collapsed onto a single line so the range reads as one atomic value.