gnssrefl.tracks module

Build, manage, and tag multi-GNSS satellite tracks.

The on-disk tracks.json catalogs the periodic ground tracks visible at a station, keyed by track_id and grouped by (sat, freq). Each track holds a list of epochs describing the matching parameters (repeat interval, anchor MJD, mean azimuth, drift rate).

The module has two halves: a build side (build_tracks and helpers) that collects per-arc geometry and writes the JSON, and a runtime side (load_tracks_json, build_lookup_index, lookup_arc, attach_track_id) that tags arcs with their (track_id, track_epoch) at extract time. track_id is stable across every artifact derived from a tracks-shaped JSON (tracks.json, vwc_tracks.json, per-day phase files, vwc output files).

See docs/pages/tracks.md for the full design and workflow.

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}.txt once 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'], and meta['track_epoch'] on every arc. apriori_RH / track_azim are None on miss; track_id / track_epoch are -1 on miss. track_epoch is always 0 on 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) and vwc_tracks.json (the VWC-eligible filtered subset, which adds a per epoch apriori_RH field).

Each metadata dict gets these new keys:

track_id, track_epoch (both -1 on no match), track_azim (az_avg_minel of the matched epoch, or None), apriori_RH (matched epoch’s apriori_RH, or None; only present in vwc_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.json from build_tracks, or vwc_tracks.json from vwc_input).

  • year (int) – Year and day-of-year of the arcs (used together with each arc’s arc_timestamp to compute MJD for the lookup).

  • doy (int) – Year and day-of-year of the arcs (used together with each arc’s arc_timestamp to compute MJD for the lookup).

  • track_cache (dict, optional) – Path-keyed cache of prebuilt lookup indexes. Defaults to the module-level TRACK_INDEX_CACHE. Cache entries are never invalidated; restart the process if a tracks file is rewritten on disk.

Returns:

The same arcs list (modified in place), for chaining.

Return type:

list

gnssrefl.tracks.build_lookup_index(tracks_json)

Build a (sat, freq) -> [(track_id, track_epoch, def_dict), …] index.

Each def_dict carries the fields needed by lookup_arc:

rise, repeat_interval_d, anchor_mjd, az_avg_minel, az_drift_rate, first_mjd, last_mjd, epoch_type

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.

Two arc sources are supported, selected by source:

  • 'snr' walk SNR files via load_arcs (slow, covers every

    frequency in the SNR file).

  • 'results' read results/ + failQC/ via

    load_arcs(..., fast=True) (fast, covers only frequencies gnssir was run with).

  • 'auto' (default) prefer 'results' if any results/ dir is

    populated 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.

Returns columns (year, doy, sat, freq, mjd, azim, rise); the fast path additionally carries RH.

  • fast=False (default): walk SNR files day by day via extract_arcs_from_station, covering every frequency the SNR file contains.

  • 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, and 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.

Active matches require the query to fall inside the track’s [first_mjd, last_mjd] interval AND fit the periodic model within time_tol_min and az_tol.

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). Each entry_dict carries the epoch’s matching parameters (first_mjd, last_mjd, anchor_mjd, repeat_interval_d, az_avg_minel, az_drift_rate, epoch_type, ignored_ranges).

Returns:

(track_id, track_epoch, entry)(-1, -1, None) if no track def covers this arc. entry is the matched epoch dict from build_lookup_index (keys include az_avg_minel and apriori_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}.txt exists, print a -legacy T hint and exit.

Called from modern-path entry points when missing_file (e.g. vwc_tracks.json) is absent.

gnssrefl.tracks.write_tracks_json(tracks_json, f)

Write tracks_json to file handle f with one ignored_ranges pair per line.