diff --git a/utils/python/README.md b/utils/python/README.md index 8e8b0f8ef..9c80300e6 100644 --- a/utils/python/README.md +++ b/utils/python/README.md @@ -45,11 +45,11 @@ repeat them with the default value that applies to each tool. Any directory in `--file-prefix` is resolved relative to `--input-path` (an absolute `--file-prefix` path overrides it). - When `--conf` is provided to the generic tracking and acquisition plotters, - a multi-signal configuration expands to every enabled signal. Pass + When `--conf` is provided to the generic tracking and acquisition plotters, a + multi-signal configuration expands to every enabled signal. Pass `--signal-type` to restrict plots to one signal. Single-file modes and - signal-specific scripts still require or prefer one signal. Channel ranges - are computed using GNSS-SDR's receiver order: + signal-specific scripts still require or prefer one signal. Channel ranges are + computed using GNSS-SDR's receiver order: `1C, 2S, L5, 1B, 5X, E6, 1G, 2G, B1, B3, 7X, J1, J5`. - `-o`, `--fig-path`: directory where plots are saved. Defaults to a per-script @@ -92,25 +92,30 @@ Output PNGs (`_2D` and `_3D`): /___ch___sat__3D.png ``` -| Option | Default | Description | -| ---------------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------- | -| `-i`, `--input-path` | `.` | Directory containing the acquisition `.mat` dumps. | -| `--conf` | unset | GNSS-SDR configuration used to infer acquisition dump prefixes and signal selectors. | +Each figure title uses a readable signal name and PRN, for example +`GPS L1 C/A PRN 3 (channel 0, execution 1)`. + +| Option | Default | Description | +| ---------------------------------------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `-i`, `--input-path` | `.` | Directory containing the acquisition `.mat` dumps. | +| `--conf` | unset | GNSS-SDR configuration used to infer acquisition dump prefixes and signal selectors. | | `--file-prefix` | `--conf` or `acquisition` | `Acquisition.dump_filename` value (may include a directory/extension). | -| `-o`, `--fig-path` | `plots/acquisition` | Output directory for PNGs. | -| `--output-basename`, `--output-prefix` | `--file-prefix` basename | Base name for the saved PNGs (directory and extension stripped). | -| `--all-files` / `--single-file` | `--all-files` | Plot every matching dump, or a single dump chosen by the selectors below. | -| `--positive-only` / `--include-negative` | `--positive-only` | Plot only positive acquisitions, or positive and negative. | -| `--lite-view` / `--full-view` | `--lite-view` | Interpolated light grid, or the raw acquisition grid. | -| `--sat` | `1` in single-file mode | Satellite PRN. Filters in all-files mode; selects the dump in single-file mode. | -| `--channel` | `0` in single-file mode | Acquisition channel number. Filter / selector. | -| `--execution` | `1` in single-file mode | Acquisition dump execution index. Filter / selector. | -| `--signal-type CODE` | `--conf` or `1C` | GNSS-SDR signal code (see table below); case-insensitive. Filters in all-files mode; selects in single-file mode. | -| `--samples-per-chip` | `3` | Samples per chip used by the light-grid interpolation. | -| `--samples-per-code` | `25000` | Samples per code used for the 2D normalization. | -| `--input-power` | `100.0` | Input power used for the 2D normalization. | -| `--show` | off | Display figures after saving. | -| `--format` | `png` | Saved figure format: `png` (default), `pdf`, `eps`, `svg`, `jpg`. Vector formats embed fonts (publication-ready). | +| `-o`, `--fig-path` | `plots/acquisition` | Output directory for PNGs. | +| `--output-basename`, `--output-prefix` | `--file-prefix` basename | Base name for the saved PNGs (directory and extension stripped). | +| `--all-files` / `--single-file` | `--all-files` | Plot every matching dump, or a single dump chosen by the selectors below. | +| `--positive-only` / `--include-negative` | `--positive-only` | Plot only positive acquisitions, or positive and negative. | +| `--lite-view` / `--full-view` | `--lite-view` | Interpolated light grid, or the raw acquisition grid. | +| `--sat` | `1` in single-file mode | Satellite PRN. Filters in all-files mode; selects the dump in single-file mode. | +| `--channel` | `0` in single-file mode | Acquisition channel number. Filter / selector. | +| `--execution` | `1` in single-file mode | Acquisition dump execution index. Filter / selector. | +| `--signal-type CODE` | `--conf` or `1C` | GNSS-SDR signal code (see table below); case-insensitive. Filters in all-files mode; selects in single-file mode. | +| `--samples-per-chip` | `3` | Samples per chip used by the light-grid interpolation. | +| `--samples-per-doppler-step` | `5` | Interpolated samples per Doppler step used by the light-grid interpolation. | +| `--surface-smoothing` | `2.0` | Gaussian smoothing sigma for the lite 3D surface; use `0` to disable. | +| `--samples-per-code` | `25000` | Samples per code used for the 2D normalization. | +| `--input-power` | `100.0` | Input power used for the 2D normalization. | +| `--show` | off | Display figures after saving. | +| `--format` | `png` | Saved figure format: `png` (default), `pdf`, `eps`, `svg`, `jpg`. Vector formats embed fonts (publication-ready). | Signal codes accepted by `--signal-type`, using the same nomenclature as the GNSS-SDR configuration and dump filenames (e.g. `--signal-type=E6`): @@ -145,7 +150,7 @@ python3 plot_acq_grid.py --input-path ./out --fig-path ./plots/acquisition python3 plot_acq_grid.py --file-prefix hackrf_l1_acq_dump --channel 0 python3 plot_acq_grid.py --file-prefix hackrf_l1_acq_dump --sat 26 python3 plot_acq_grid.py --signal-type E6 -python3 plot_acq_grid.py --conf conf/File_input/Beidou/gnss-sdr_BDS_B3I_byte.conf --signal-type B3 --input-path ./out +python3 plot_acq_grid.py --conf gnss-sdr_BDS_B3I_byte.conf python3 plot_acq_grid.py --single-file --signal-type J1 --sat 193 --channel 0 --execution 1 python3 plot_acq_grid.py --single-file --sat 3 --channel 0 --execution 1 --full-view python3 plot_acq_grid.py --include-negative --output-basename my_run @@ -158,20 +163,20 @@ Reads per-channel tracking `.dat` dumps and plots VEML tracking diagnostics Input pattern: `/.dat`. -| Option | Default | Description | -| ---------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| `-i`, `--input-path` | `.` | Directory containing the tracking `.dat` dumps. | -| `--conf` | unset | GNSS-SDR configuration used to infer tracking dump prefix, sampling frequency, and channel range. | -| `--file-prefix` | `--conf` or `track_ch` | `Tracking.dump_filename` value (may include a directory/extension). | -| `-o`, `--fig-path` | `plots/dll-pll-veml-tracking` | Output directory for PNGs. | -| `--sampling-frequency` | `--conf` or `3000000.0` | Signal sampling frequency in Hz (sets the RX-time axis). | -| `--channels` | `--conf` or `5` | Number of channels to read. | -| `--first-channel` | `--conf` or `0` | First channel number in the dump filenames. | +| Option | Default | Description | +| ---------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `-i`, `--input-path` | `.` | Directory containing the tracking `.dat` dumps. | +| `--conf` | unset | GNSS-SDR configuration used to infer tracking dump prefix, sampling frequency, and channel range. | +| `--file-prefix` | `--conf` or `track_ch` | `Tracking.dump_filename` value (may include a directory/extension). | +| `-o`, `--fig-path` | `plots/dll-pll-veml-tracking` | Output directory for PNGs. | +| `--sampling-frequency` | `--conf` or `3000000.0` | Signal sampling frequency in Hz (sets the RX-time axis). | +| `--channels` | `--conf` or `5` | Number of channels to read. | +| `--first-channel` | `--conf` or `0` | First channel number in the dump filenames. | | `--signal-type CODE` | inferred if needed | GNSS-SDR signal code used to restrict channel ranges and dump filenames from `--conf`; omitted means all configured signals. | -| `--plot-last-outputs` | `0` | Only plot the last N outputs; `0` plots all of them. | -| `--no-doppler` | Doppler plots on | Skip the extra Doppler-only plots. | -| `--show` | off | Display figures after saving. | -| `--format` | `png` | Saved figure format: `png` (default), `pdf`, `eps`, `svg`, `jpg`. Vector formats embed fonts (publication-ready). | +| `--plot-last-outputs` | `0` | Only plot the last N outputs; `0` plots all of them. | +| `--no-doppler` | Doppler plots on | Skip the extra Doppler-only plots. | +| `--show` | off | Display figures after saving. | +| `--format` | `png` | Saved figure format: `png` (default), `pdf`, `eps`, `svg`, `jpg`. Vector formats embed fonts (publication-ready). | Examples: @@ -189,19 +194,19 @@ Reads per-channel Kalman-filter tracking `.dat` dumps produced by the Input pattern: `/.dat`. -| Option | Default | Description | -| ---------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------- | -| `-i`, `--input-path` | `.` | Directory containing the tracking `.dat` dumps. | -| `--conf` | unset | GNSS-SDR configuration used to infer tracking dump prefix, sampling frequency, and channel range. | -| `--file-prefix` | `--conf` or `track_ch` | `Tracking.dump_filename` value (may include a directory/extension). | -| `-o`, `--fig-path` | `plots/kf-tracking` | Output directory for PNGs. | -| `--sampling-frequency` | `--conf` or `4000000.0` | Signal sampling frequency in Hz. | -| `--channels` | `--conf` or `5` | Number of channels to read. | -| `--first-channel` | `--conf` or `0` | First channel number in the dump filenames. | -| `--signal-type CODE` | inferred if needed | GNSS-SDR signal code used to select channel ranges and dump filenames from `--conf`. | -| `--code-period` | `0.001` | Code period in seconds. | -| `--show` | off | Display figures after saving. | -| `--format` | `png` | Saved figure format: `png` (default), `pdf`, `eps`, `svg`, `jpg`. Vector formats embed fonts (publication-ready). | +| Option | Default | Description | +| ---------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `-i`, `--input-path` | `.` | Directory containing the tracking `.dat` dumps. | +| `--conf` | unset | GNSS-SDR configuration used to infer tracking dump prefix, sampling frequency, and channel range. | +| `--file-prefix` | `--conf` or `track_ch` | `Tracking.dump_filename` value (may include a directory/extension). | +| `-o`, `--fig-path` | `plots/kf-tracking` | Output directory for PNGs. | +| `--sampling-frequency` | `--conf` or `4000000.0` | Signal sampling frequency in Hz. | +| `--channels` | `--conf` or `5` | Number of channels to read. | +| `--first-channel` | `--conf` or `0` | First channel number in the dump filenames. | +| `--signal-type CODE` | inferred if needed | GNSS-SDR signal code used to select channel ranges and dump filenames from `--conf`. | +| `--code-period` | `0.001` | Code period in seconds. | +| `--show` | off | Display figures after saving. | +| `--format` | `png` | Saved figure format: `png` (default), `pdf`, `eps`, `svg`, `jpg`. Vector formats embed fonts (publication-ready). | > The dumps must come from the `GPS_L1_CA_KF_Tracking` block; standard DLL/PLL > tracking dumps have a different record layout and will be misread. @@ -222,22 +227,22 @@ figures. Input pattern: `/.dat`. -| Option | Default | Description | -| -------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | -| `-i`, `--input-path` | `.` | Directory containing the tracking `.dat` dumps. | -| `--conf` | unset | GNSS-SDR configuration used to infer tracking dump prefix, sampling frequency, signal type, and channel range. | -| `--file-prefix` | `--conf` or `track_ch` | `Tracking.dump_filename` value (may include a directory/extension). | -| `-o`, `--fig-path` | `plots/tracking-quality` | Output directory for PNGs. | -| `--sampling-frequency` | `--conf` or `4000000.0` | Signal sampling frequency in Hz. | -| `--channels` | `--conf` or `5` | Number of channels to read. | -| `--first-channel` | `--conf` or `0` | First channel number in the dump filenames. | -| `--signal-type` | `--conf` or `1C` | GNSS-SDR signal code used to restrict channel ranges and label traces; omitted with multi-signal `--conf` means all configured signals. | -| `--show` | off | Display figures after saving. | -| `--format` | `png` | Saved figure format: `png` (default), `pdf`, `eps`, `svg`, `jpg`. Vector formats embed fonts (publication-ready). | +| Option | Default | Description | +| ---------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | +| `-i`, `--input-path` | `.` | Directory containing the tracking `.dat` dumps. | +| `--conf` | unset | GNSS-SDR configuration used to infer tracking dump prefix, sampling frequency, signal type, and channel range. | +| `--file-prefix` | `--conf` or `track_ch` | `Tracking.dump_filename` value (may include a directory/extension). | +| `-o`, `--fig-path` | `plots/tracking-quality` | Output directory for PNGs. | +| `--sampling-frequency` | `--conf` or `4000000.0` | Signal sampling frequency in Hz. | +| `--channels` | `--conf` or `5` | Number of channels to read. | +| `--first-channel` | `--conf` or `0` | First channel number in the dump filenames. | +| `--signal-type` | `--conf` or `1C` | GNSS-SDR signal code used to restrict channel ranges and label traces; omitted with multi-signal `--conf` means all configured signals. | +| `--show` | off | Display figures after saving. | +| `--format` | `png` | Saved figure format: `png` (default), `pdf`, `eps`, `svg`, `jpg`. Vector formats embed fonts (publication-ready). | The C/N0 legend labels each trace as ` PRN ` (e.g. -`1C PRN 9`); the carrier-lock legend labels traces as `SV PRN -`. +`1C PRN 9`); the carrier-lock legend labels traces as +`SV PRN `. Examples: @@ -255,19 +260,19 @@ channels. Input pattern: `/.dat`. -| Option | Default | Description | -| -------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------- | -| `-i`, `--input-path` | `.` | Directory containing the telemetry `.dat` dumps. | -| `--conf` | unset | GNSS-SDR configuration used to infer telemetry dump prefix and channel range. | -| `--file-prefix` | `--conf` or `telemetry` | `TelemetryDecoder.dump_filename` value (may include a directory/extension). | -| `-o`, `--fig-path` | `plots/telemetry` | Output directory for PNGs. | -| `--channels` | `--conf` or `18` | Number of channels to try reading; missing channels are skipped. | -| `--first-channel` | `--conf` or `0` | First channel number in the dump filenames. | -| `--signal-type CODE` | inferred if needed | GNSS-SDR signal code used to select channel ranges and dump filenames from `--conf`. | -| `--channel-a` | `--conf` or `0` | First channel number to plot. | -| `--channel-b` | `--conf` or `5` | Second channel number to plot. | -| `--show` | off | Display figures after saving. | -| `--format` | `png` | Saved figure format: `png` (default), `pdf`, `eps`, `svg`, `jpg`. Vector formats embed fonts (publication-ready). | +| Option | Default | Description | +| -------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `-i`, `--input-path` | `.` | Directory containing the telemetry `.dat` dumps. | +| `--conf` | unset | GNSS-SDR configuration used to infer telemetry dump prefix and channel range. | +| `--file-prefix` | `--conf` or `telemetry` | `TelemetryDecoder.dump_filename` value (may include a directory/extension). | +| `-o`, `--fig-path` | `plots/telemetry` | Output directory for PNGs. | +| `--channels` | `--conf` or `18` | Number of channels to try reading; missing channels are skipped. | +| `--first-channel` | `--conf` or `0` | First channel number in the dump filenames. | +| `--signal-type CODE` | inferred if needed | GNSS-SDR signal code used to select channel ranges and dump filenames from `--conf`. | +| `--channel-a` | `--conf` or `0` | First channel number to plot. | +| `--channel-b` | `--conf` or `5` | Second channel number to plot. | +| `--show` | off | Display figures after saving. | +| `--format` | `png` | Saved figure format: `png` (default), `pdf`, `eps`, `svg`, `jpg`. Vector formats embed fonts (publication-ready). | Examples: @@ -284,15 +289,15 @@ phase, Doppler frequency, and captured PRNs across channels. Input pattern: `/.dat` (single file). -| Option | Default | Description | -| -------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------- | -| `-i`, `--input-path` | `.` | Directory containing the observables dump. | -| `--conf` | unset | GNSS-SDR configuration used to infer observables dump filename and total channel count. | -| `--file-prefix` | `--conf` or `observables.dat` | `Observables.dump_filename` value (may include a directory/extension). | -| `-o`, `--fig-path` | `plots/hybrid-observables` | Output directory for PNGs. | -| `--channels` | `--conf` or `5` | Number of observable channels in the dump. | -| `--show` | off | Display figures after saving. | -| `--format` | `png` | Saved figure format: `png` (default), `pdf`, `eps`, `svg`, `jpg`. Vector formats embed fonts (publication-ready). | +| Option | Default | Description | +| -------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `-i`, `--input-path` | `.` | Directory containing the observables dump. | +| `--conf` | unset | GNSS-SDR configuration used to infer observables dump filename and total channel count. | +| `--file-prefix` | `--conf` or `observables.dat` | `Observables.dump_filename` value (may include a directory/extension). | +| `-o`, `--fig-path` | `plots/hybrid-observables` | Output directory for PNGs. | +| `--channels` | `--conf` or `5` | Number of observable channels in the dump. | +| `--show` | off | Display figures after saving. | +| `--format` | `png` | Saved figure format: `png` (default), `pdf`, `eps`, `svg`, `jpg`. Vector formats embed fonts (publication-ready). | Examples: @@ -311,20 +316,20 @@ given. Input pattern: `/.dat` (single file). -| Option | Default | Description | -| ----------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------- | -| `-i`, `--input-path` | `.` | Directory containing the PVT dump. | -| `--conf` | unset | GNSS-SDR configuration used to infer PVT dump filename and navigation solution period. | -| `--file-prefix` | `--conf` or `PVT.dat` | `PVT.dump_filename` value (may include a directory/extension). | -| `-o`, `--fig-path` | `plots/pvt` | Output directory for PNGs and maps. | -| `--nav-sol-period` | `--conf` or `10.0` | Navigation solution period in milliseconds. | -| `--true-position E_UTM N_UTM U_UTM` | unset (NaN) | Reference receiver position in UTM, used for error plots. | -| `--plot-skyplot` | off | Try to generate the sky-plot panel. | -| `--no-position` | position plot on | Skip the position/map diagnostic (avoids the `folium` dependency). | -| `--one-vs-time NAME` | `X_vel`, `Tot_Vel` | `navSolutions` variable to plot versus time. Repeatable. | -| `--show` | off | Display figures after saving. | -| `--format` | `png` | Saved figure format: `png` (default), `pdf`, `eps`, `svg`, `jpg`. Vector formats embed fonts (publication-ready). | -| `--open-maps` | off | Open the generated folium map HTML files in a browser. | +| Option | Default | Description | +| ----------------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------- | +| `-i`, `--input-path` | `.` | Directory containing the PVT dump. | +| `--conf` | unset | GNSS-SDR configuration used to infer PVT dump filename and navigation solution period. | +| `--file-prefix` | `--conf` or `PVT.dat` | `PVT.dump_filename` value (may include a directory/extension). | +| `-o`, `--fig-path` | `plots/pvt` | Output directory for PNGs and maps. | +| `--nav-sol-period` | `--conf` or `10.0` | Navigation solution period in milliseconds. | +| `--true-position E_UTM N_UTM U_UTM` | unset (NaN) | Reference receiver position in UTM, used for error plots. | +| `--plot-skyplot` | off | Try to generate the sky-plot panel. | +| `--no-position` | position plot on | Skip the position/map diagnostic (avoids the `folium` dependency). | +| `--one-vs-time NAME` | `X_vel`, `Tot_Vel` | `navSolutions` variable to plot versus time. Repeatable. | +| `--show` | off | Display figures after saving. | +| `--format` | `png` | Saved figure format: `png` (default), `pdf`, `eps`, `svg`, `jpg`. Vector formats embed fonts (publication-ready). | +| `--open-maps` | off | Open the generated folium map HTML files in a browser. | Examples: diff --git a/utils/python/dll_pll_veml_plot_sample.py b/utils/python/dll_pll_veml_plot_sample.py index 3099e6732..455ae4027 100755 --- a/utils/python/dll_pll_veml_plot_sample.py +++ b/utils/python/dll_pll_veml_plot_sample.py @@ -33,6 +33,7 @@ from lib.gnss_sdr_conf import ( SIGNAL_TYPES, add_conf_argument, load_gnss_sdr_conf, + signal_pretty_name, ) from lib.plot_format import add_output_format_argument, apply_publication_style from lib.plotVEMLTracking import plotVEMLTracking @@ -216,6 +217,11 @@ def read_tracking_dumps(args): return dumps +def prn_label(prns): + prns = dict.fromkeys(int(prn) for prn in prns if int(prn)) + return "/".join(str(prn) for prn in prns) or "unknown" + + def main(): args = parse_args() args.fig_path.mkdir(parents=True, exist_ok=True) @@ -270,6 +276,14 @@ def main(): / args.sampling_frequency ), } + signal_name = signal_pretty_name( + tracking.get("_signal_type") or DEFAULT_SIGNAL_TYPE + ) + track_result["signalName"] = signal_name + track_result["prnLabel"] = prn_label(track_result["PRN"]) + track_result["plotTitle"] = ( + f"{signal_name} PRN {track_result['prnLabel']}" + ) track_results.append(track_result) plotVEMLTracking(index, track_results, settings) @@ -281,9 +295,12 @@ def main(): track_result["prn_start_time_s"], [x / 1000 for x in tracking["carrier_doppler_hz"][start_sample:]], ) - plt.xlabel("Time(s)") - plt.ylabel("Doppler(KHz)") - plt.title(f"Doppler frequency channel {channel}") + plt.xlabel("Time (s)") + plt.ylabel("Doppler (kHz)") + plt.title( + f"Doppler frequency - {track_result['plotTitle']} " + f"(channel {channel})" + ) plt.savefig( args.fig_path / f"Doppler_freq_ch_{channel}.{args.output_format}" diff --git a/utils/python/lib/gnss_sdr_conf.py b/utils/python/lib/gnss_sdr_conf.py index 1b1c1bc28..36b6719a0 100644 --- a/utils/python/lib/gnss_sdr_conf.py +++ b/utils/python/lib/gnss_sdr_conf.py @@ -39,25 +39,51 @@ SIGNAL_ORDER = ( "J5", ) +@dataclass(frozen=True) +class SignalType: + system: str + n_chips: int + pretty_name: str + + def __iter__(self): + # Keep compatibility with callers that unpack SIGNAL_TYPES entries as + # (system, n_chips). + yield self.system + yield self.n_chips + + # GNSS-SDR signal nomenclature: each signal code maps to the system character -# used in acquisition dump filenames and to its primary code length in chips. +# used in acquisition dump filenames, its primary code length in chips, and a +# human-friendly signal name for plot labels. SIGNAL_TYPES = { - "1C": ("G", 1023), # GPS L1 C/A - "2S": ("G", 10230), # GPS L2C - "L5": ("G", 10230), # GPS L5 - "1B": ("E", 4092), # Galileo E1B - "5X": ("E", 10230), # Galileo E5a - "7X": ("E", 10230), # Galileo E5b - "E6": ("E", 5115), # Galileo E6 - "1G": ("R", 511), # GLONASS L1 - "2G": ("R", 511), # GLONASS L2 - "B1": ("C", 2046), # BeiDou B1I - "B3": ("C", 10230), # BeiDou B3I - "J1": ("J", 1023), # QZSS L1 C/A - "J5": ("J", 10230), # QZSS L5 + "1C": SignalType("G", 1023, "GPS L1 C/A"), + "2S": SignalType("G", 10230, "GPS L2C"), + "L5": SignalType("G", 10230, "GPS L5"), + "1B": SignalType("E", 4092, "Galileo E1B"), + "5X": SignalType("E", 10230, "Galileo E5a"), + "7X": SignalType("E", 10230, "Galileo E5b"), + "E6": SignalType("E", 5115, "Galileo E6"), + "1G": SignalType("R", 511, "GLONASS L1 C/A"), + "2G": SignalType("R", 511, "GLONASS L2 C/A"), + "B1": SignalType("C", 2046, "BeiDou B1I"), + "B3": SignalType("C", 10230, "BeiDou B3I"), + "J1": SignalType("J", 1023, "QZSS L1 C/A"), + "J5": SignalType("J", 10230, "QZSS L5"), } -N_CHIPS = {(system, code): nc for code, (system, nc) in SIGNAL_TYPES.items()} +N_CHIPS = { + (signal_type.system, code): signal_type.n_chips + for code, signal_type in SIGNAL_TYPES.items() +} + + +def signal_pretty_name(signal_type): + if signal_type is None: + return "" + metadata = SIGNAL_TYPES.get(signal_type.upper()) + if metadata is None: + return signal_type + return metadata.pretty_name class ConfigError(ValueError): diff --git a/utils/python/lib/plotVEMLTracking.py b/utils/python/lib/plotVEMLTracking.py index a4f4ffb47..8736b9377 100644 --- a/utils/python/lib/plotVEMLTracking.py +++ b/utils/python/lib/plotVEMLTracking.py @@ -46,12 +46,17 @@ def plotVEMLTracking(channelNr, trackResults, settings): # Protection - if the list contains incorrect channel numbers if channelNr in list(range(1,settings["numberOfChannels"]+1)): + track_result = trackResults[channelNr-1] + prn_label = track_result.get('prnLabel', str(track_result["PRN"][0])) + plot_title = track_result.get('plotTitle', f'PRN {prn_label}') + plt.figure(figsize=(1920 / 120, 1080 / 120)) plt.clf() plt.gcf().canvas.manager.set_window_title( - f'Channel {display_channel} (PRN ' - f'{trackResults[channelNr-1]["PRN"][0]}) results') - plt.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.1, + f'Channel {display_channel} - {plot_title} results') + plt.suptitle(f'{plot_title} (channel {display_channel})', + fontweight='bold') + plt.subplots_adjust(left=0.1, right=0.9, top=0.86, bottom=0.1, hspace=0.4, wspace=0.4) # Extract timeAxis and time_label diff --git a/utils/python/plot_acq_grid.py b/utils/python/plot_acq_grid.py index ee1bff8b6..78e596fa6 100755 --- a/utils/python/plot_acq_grid.py +++ b/utils/python/plot_acq_grid.py @@ -33,6 +33,7 @@ from lib.gnss_sdr_conf import ( SIGNAL_TYPES, add_conf_argument, load_gnss_sdr_conf, + signal_pretty_name, ) from lib.plot_format import add_output_format_argument, apply_publication_style @@ -46,6 +47,8 @@ DEFAULT_SAT = 1 DEFAULT_CHANNEL = 0 DEFAULT_EXECUTION = 1 DEFAULT_SIGNAL_TYPE = "1C" +MAX_LITE_SURFACE_CODE_POINTS = 3200 +MAX_LITE_SURFACE_DOPPLER_POINTS = 250 @dataclass(frozen=True) @@ -176,6 +179,20 @@ def parse_args(): default=3, help="Samples per chip used by the light grid interpolation.", ) + parser.add_argument( + "--samples-per-doppler-step", + type=int, + default=5, + help="Interpolated samples per Doppler step used by the light grid " + "interpolation.", + ) + parser.add_argument( + "--surface-smoothing", + type=float, + default=2.0, + help="Gaussian smoothing sigma, in interpolated surface samples, " + "applied only to the lite 3D view. Use 0 to disable.", + ) parser.add_argument( "--samples-per-code", type=int, @@ -310,6 +327,52 @@ def matching_dump_prefixes(input_path): return sorted(prefixes) +def configured_doppler_axis(doppler_max, doppler_step, n_dop_bins, file_path): + import numpy as np + + doppler_max = abs(float(doppler_max)) + doppler_step = abs(float(doppler_step)) + if doppler_step == 0: + raise ValueError(f"{file_path}: doppler_step must be non-zero") + + freq = np.arange(-doppler_max, doppler_max + doppler_step / 2, doppler_step) + if len(freq) == n_dop_bins: + return freq + + legacy_freq = np.arange(n_dop_bins) * doppler_step - doppler_max + if len(freq) - 1 == n_dop_bins: + print( + f"Warning: {file_path.name}: acq_grid has {n_dop_bins} Doppler " + f"bins, one fewer than the inclusive configured axis from " + f"{-doppler_max:g} Hz to {doppler_max:g} Hz in " + f"{doppler_step:g} Hz steps. Plotting the available bins from " + f"{legacy_freq[0]:g} Hz to {legacy_freq[-1]:g} Hz." + ) + return legacy_freq + + print( + f"Warning: {file_path.name}: configured Doppler axis has {len(freq)} " + f"bins from {-doppler_max:g} Hz to {doppler_max:g} Hz in " + f"{doppler_step:g} Hz steps, but acq_grid has {n_dop_bins} columns. " + "Plotting the available columns over the configured Doppler range." + ) + return np.linspace(-doppler_max, doppler_max, n_dop_bins) + + +def expected_doppler_bin_counts(doppler_max, doppler_step): + import numpy as np + + doppler_max = abs(float(doppler_max)) + doppler_step = abs(float(doppler_step)) + if doppler_step == 0: + return set() + + inclusive_count = len( + np.arange(-doppler_max, doppler_max + doppler_step / 2, doppler_step) + ) + return {inclusive_count, max(1, inclusive_count - 1)} + + def read_acquisition_dump(file_path, n_chips, positive_only): import h5py import numpy as np @@ -323,21 +386,77 @@ def read_acquisition_dump(file_path, n_chips, positive_only): if positive_acq != 1: return None - acq_grid = data["acq_grid"][:] - n_fft, n_dop_bins = acq_grid.shape - d_max, f_max = np.unravel_index(np.argmax(acq_grid), acq_grid.shape) doppler_step = data["doppler_step"][0] doppler_max = data["doppler_max"][0] - freq = np.arange(n_dop_bins) * doppler_step - doppler_max + acq_grid = data["acq_grid"][:] + expected_doppler_bins = expected_doppler_bin_counts( + doppler_max, + doppler_step, + ) + if ( + acq_grid.shape[1] not in expected_doppler_bins + and acq_grid.shape[0] in expected_doppler_bins + ): + acq_grid = acq_grid.T + + n_fft, n_dop_bins = acq_grid.shape + freq = configured_doppler_axis( + doppler_max, + doppler_step, + n_dop_bins, + file_path, + ) + d_max, f_max = np.unravel_index(np.argmax(acq_grid), acq_grid.shape) delay = np.arange(n_fft) / n_fft * n_chips return acq_grid, n_fft, d_max, f_max, doppler_step, doppler_max, freq, delay -def plot_dump(file_path, fig_path, image_name_root, n_chips, args): +def acquisition_plot_title(metadata): + signal_name = signal_pretty_name(metadata["signal"]) + return ( + f"{signal_name} PRN {metadata['sat']} " + f"(channel {metadata['channel']}, execution {metadata['execution']})" + ) + + +def interpolated_acquisition_grid(delay, freq, acq_grid, args, n_chips): + import numpy as np + from scipy.ndimage import gaussian_filter + from scipy.interpolate import RectBivariateSpline + + if len(delay) < 2 or len(freq) < 2: + return delay, freq, acq_grid + + samples_per_chip = max(1, args.samples_per_chip) + samples_per_doppler_step = max(1, args.samples_per_doppler_step) + delay_points = min( + samples_per_chip * n_chips, + MAX_LITE_SURFACE_CODE_POINTS, + ) + freq_points = min( + (len(freq) - 1) * samples_per_doppler_step + 1, + MAX_LITE_SURFACE_DOPPLER_POINTS, + ) + delay_interp = np.linspace(delay[0], delay[-1], max(2, delay_points)) + freq_interp = np.linspace(freq[0], freq[-1], max(2, freq_points)) + + kx = min(3, len(delay) - 1) + ky = min(3, len(freq) - 1) + spline = RectBivariateSpline(delay, freq, acq_grid, kx=kx, ky=ky) + grid_interp = spline(delay_interp, freq_interp) + if args.surface_smoothing > 0: + grid_interp = gaussian_filter( + grid_interp, + sigma=args.surface_smoothing, + mode="nearest", + ) + return delay_interp, freq_interp, grid_interp + + +def plot_dump(file_path, fig_path, image_name_root, n_chips, args, metadata=None): import matplotlib.pyplot as plt import numpy as np - from scipy.interpolate import CubicSpline dump = read_acquisition_dump(file_path, n_chips, args.plot_positive_acqs) if dump is None: @@ -354,28 +473,65 @@ def plot_dump(file_path, fig_path, image_name_root, n_chips, args): delay, ) = dump + if metadata is None: + metadata = parse_dump_name(file_path) + plot_title = acquisition_plot_title(metadata) if metadata else str(file_path) + fig = plt.figure() plt.gcf().canvas.manager.set_window_title(str(file_path)) if not args.lite_view: ax = fig.add_subplot(111, projection="3d") - x_axis, y_axis = np.meshgrid(freq, delay) - ax.plot_surface(x_axis, y_axis, acq_grid, cmap="viridis") + x_axis, y_axis = np.meshgrid(freq, delay, indexing="xy") + ax.plot_surface( + x_axis, + y_axis, + acq_grid, + cmap="viridis", + linewidth=0, + edgecolor="none", + antialiased=False, + ) ax.set_ylim([min(delay), max(delay)]) else: - delay_interp = ( - np.arange(args.samples_per_chip * n_chips) / args.samples_per_chip + delay_interp, freq_interp, grid_interp = interpolated_acquisition_grid( + delay, + freq, + acq_grid, + args, + n_chips, ) - spline = CubicSpline(delay, acq_grid) - grid_interp = spline(delay_interp) ax = fig.add_subplot(111, projection="3d") - x_axis, y_axis = np.meshgrid(freq, delay_interp) - ax.plot_surface(x_axis, y_axis, grid_interp, cmap="inferno") + x_axis, y_axis = np.meshgrid(freq_interp, delay_interp, indexing="xy") + ax.plot_surface( + x_axis, + y_axis, + grid_interp, + cmap="inferno", + rstride=1, + cstride=1, + linewidth=0, + edgecolor="none", + antialiased=False, + ) ax.set_ylim([min(delay_interp), max(delay_interp)]) + peak_doppler = freq[f_max] + peak_delay = delay[d_max] + peak_value = acq_grid[d_max, f_max] + ax.scatter( + [peak_doppler], + [peak_delay], + [peak_value], + color="red", + s=30, + label="Peak", + ) ax.set_xlabel("Doppler shift (Hz)") ax.set_xlim([min(freq), max(freq)]) ax.set_ylabel("Code delay (chips)") ax.set_zlabel("Test Statistics") + ax.set_title(plot_title) + ax.legend(loc="upper right") plt.tight_layout() plt.savefig(fig_path / f"{image_name_root}_3D.{args.output_format}") if not args.show: @@ -383,24 +539,25 @@ def plot_dump(file_path, fig_path, image_name_root, n_chips, args): fig2, axes = plt.subplots(2, 1, figsize=(8, 6)) plt.gcf().canvas.manager.set_window_title(str(file_path)) + fig2.suptitle(plot_title) axes[0].plot(freq, acq_grid[d_max, :]) + axes[0].axvline(peak_doppler, color="red", linestyle="--", linewidth=0.8) axes[0].set_xlim([min(freq), max(freq)]) axes[0].set_xlabel("Doppler shift (Hz)") axes[0].set_ylabel("Test statistics") - axes[0].set_title( - f"Fixed code delay to {(d_max - 1) / n_fft * n_chips} chips" - ) + axes[0].set_title(f"Doppler cut at code delay = {peak_delay:g} chips") normalization = (args.samples_per_code ** 4) * args.input_power axes[1].plot(delay, acq_grid[:, f_max] / normalization) + axes[1].axvline(peak_delay, color="red", linestyle="--", linewidth=0.8) axes[1].set_xlim([min(delay), max(delay)]) axes[1].set_xlabel("Code delay (chips)") axes[1].set_ylabel("Test statistics") axes[1].set_title( - f"Doppler wipe-off = {(f_max - 1) * doppler_step - doppler_max} Hz" + f"Code-delay cut at Doppler wipe-off = {peak_doppler:g} Hz" ) - plt.tight_layout() + fig2.tight_layout(rect=(0, 0, 1, 0.95)) plt.savefig(fig_path / f"{image_name_root}_2D.{args.output_format}") if not args.show: plt.close(fig2) @@ -425,17 +582,15 @@ def single_file_path(args): f"{base}_{system}_{signal}_ch_{channel}_" f"{execution}_sat_{sat}.mat" ) - image_name_root = image_name_from_metadata( - args.output_basename, - { - "system": system, - "signal": signal, - "channel": channel, - "execution": execution, - "sat": sat, - }, - ) - return directory / filename, image_name_root, n_chips + metadata = { + "system": system, + "signal": signal, + "channel": channel, + "execution": execution, + "sat": sat, + } + image_name_root = image_name_from_metadata(args.output_basename, metadata) + return directory / filename, image_name_root, n_chips, metadata def image_name_from_metadata(output_basename, metadata): @@ -486,10 +641,17 @@ def main(): apply_publication_style() if not args.plot_all_files: - file_path, image_name_root, n_chips = single_file_path(args) + file_path, image_name_root, n_chips, metadata = single_file_path(args) if not file_path.exists(): raise FileNotFoundError(f"dump file not found: {file_path}") - plot_dump(file_path, args.fig_path, image_name_root, n_chips, args) + plot_dump( + file_path, + args.fig_path, + image_name_root, + n_chips, + args, + metadata, + ) show_figures(args) return @@ -530,7 +692,14 @@ def main(): args.output_basename, metadata, ) - if plot_dump(file_path, args.fig_path, image_name_root, n_chips, args): + if plot_dump( + file_path, + args.fig_path, + image_name_root, + n_chips, + args, + metadata, + ): plotted += 1 if not found_paths: