mirror of
https://github.com/gnss-sdr/gnss-sdr
synced 2026-10-13 19:21:50 +00:00
Add possibility to infer Python plot defaults from GNSS-SDR configuration files
This commit is contained in:
+59
-26
@@ -26,6 +26,11 @@ repeat them with the default value that applies to each tool.
|
||||
- `-h`, `--help`: print the option list and exit.
|
||||
- `-i`, `--input-path`: directory containing the dump files (default: current
|
||||
directory).
|
||||
- `--conf`: optional GNSS-SDR configuration file. When provided, scripts infer
|
||||
missing options such as `--file-prefix`, `--channels`, `--first-channel`,
|
||||
`--sampling-frequency`, `--signal-type`, and `--nav-sol-period` from the
|
||||
configuration. Explicit command-line options always take precedence over
|
||||
values inferred from `--conf`.
|
||||
- `--file-prefix`: the value of the block's `dump_filename` configuration
|
||||
parameter in GNSS-SDR. It may include a directory and an extension; each
|
||||
script reconstructs the actual dump filenames from it exactly the way GNSS-SDR
|
||||
@@ -40,6 +45,13 @@ 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
|
||||
`--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:
|
||||
`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
|
||||
subdirectory under `./plots/`.
|
||||
- `--show`: display plots interactively after saving them. By default, scripts
|
||||
@@ -83,7 +95,8 @@ Output PNGs (`_2D` and `_3D`):
|
||||
| Option | Default | Description |
|
||||
| ---------------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `-i`, `--input-path` | `.` | Directory containing the acquisition `.mat` dumps. |
|
||||
| `--file-prefix` | `acquisition` | `Acquisition.dump_filename` value (may include a directory/extension). |
|
||||
| `--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. |
|
||||
@@ -92,7 +105,7 @@ Output PNGs (`_2D` and `_3D`):
|
||||
| `--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` | `1C` (GPS L1 C/A) | GNSS-SDR signal code (see table below); case-insensitive. Filters in all-files mode; selects in single-file mode. |
|
||||
| `--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. |
|
||||
@@ -119,8 +132,10 @@ GNSS-SDR configuration and dump filenames (e.g. `--signal-type=E6`):
|
||||
| `J5` | QZSS | L5 | 10230 |
|
||||
|
||||
In all-files mode `--sat`, `--channel`, `--execution`, and `--signal-type`
|
||||
filter the matching dumps; if a `--file-prefix` matches no files, the available
|
||||
dump prefixes found in the directory are listed.
|
||||
filter the matching dumps. If `--conf` enables several signals and
|
||||
`--file-prefix` is not set, all configured acquisition dump prefixes are
|
||||
scanned. If a `--file-prefix` matches no files, the available dump prefixes
|
||||
found in the directory are listed.
|
||||
|
||||
Examples:
|
||||
|
||||
@@ -130,6 +145,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 --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
|
||||
@@ -145,11 +161,13 @@ Input pattern: `<input-path>/<file-prefix><channel>.dat`.
|
||||
| Option | Default | Description |
|
||||
| ---------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `-i`, `--input-path` | `.` | Directory containing the tracking `.dat` dumps. |
|
||||
| `--file-prefix` | `track_ch` | `Tracking.dump_filename` value (may include a directory/extension). |
|
||||
| `--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` | `3000000.0` | Signal sampling frequency in Hz (sets the RX-time axis). |
|
||||
| `--channels` | `5` | Number of channels to read. |
|
||||
| `--first-channel` | `0` | First channel number in the dump filenames. |
|
||||
| `--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. |
|
||||
@@ -160,6 +178,7 @@ Examples:
|
||||
```bash
|
||||
python3 dll_pll_veml_plot_sample.py --input-path ./out
|
||||
python3 dll_pll_veml_plot_sample.py --file-prefix tracking_ch_ --channels 8
|
||||
python3 dll_pll_veml_plot_sample.py --conf conf/File_input/Galileo/gnss-sdr_Galileo_E1_nsr.conf --signal-type 1B --input-path ./out
|
||||
python3 dll_pll_veml_plot_sample.py --plot-last-outputs 5000 --no-doppler
|
||||
```
|
||||
|
||||
@@ -173,11 +192,13 @@ Input pattern: `<input-path>/<file-prefix><channel>.dat`.
|
||||
| Option | Default | Description |
|
||||
| ---------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `-i`, `--input-path` | `.` | Directory containing the tracking `.dat` dumps. |
|
||||
| `--file-prefix` | `track_ch` | `Tracking.dump_filename` value (may include a directory/extension). |
|
||||
| `--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` | `4000000.0` | Signal sampling frequency in Hz. |
|
||||
| `--channels` | `5` | Number of channels to read. |
|
||||
| `--first-channel` | `0` | First channel number in the dump filenames. |
|
||||
| `--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). |
|
||||
@@ -190,6 +211,7 @@ Examples:
|
||||
```bash
|
||||
python3 gps_l1_ca_kf_plot_sample.py --input-path ./out
|
||||
python3 gps_l1_ca_kf_plot_sample.py --file-prefix track_ch --channels 8
|
||||
python3 gps_l1_ca_kf_plot_sample.py --conf conf/File_input/MultiCons/gnss-sdr_labsat_kf.conf --signal-type 1C --input-path ./out
|
||||
python3 gps_l1_ca_kf_plot_sample.py --sampling-frequency 4000000 --code-period 0.001
|
||||
```
|
||||
|
||||
@@ -203,22 +225,26 @@ Input pattern: `<input-path>/<file-prefix><channel>.dat`.
|
||||
| Option | Default | Description |
|
||||
| -------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `-i`, `--input-path` | `.` | Directory containing the tracking `.dat` dumps. |
|
||||
| `--file-prefix` | `track_ch` | `Tracking.dump_filename` value (may include a directory/extension). |
|
||||
| `--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. |
|
||||
| `--channels` | `5` | Number of channels to read. |
|
||||
| `--first-channel` | `0` | First channel number in the dump filenames. |
|
||||
| `--signal-type` | `1C` | GNSS-SDR signal code used as the C/N0 legend label (case-insensitive). The tracking dump stores only the PRN, so the signal is supplied here. |
|
||||
| `--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 `<signal-type> PRN <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 <signal-type> PRN
|
||||
<PRN>`.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
python3 plot_tracking_quality_indicators.py --input-path ./out
|
||||
python3 plot_tracking_quality_indicators.py --file-prefix hackrf_l1_tracking_ch_ --channels 8
|
||||
python3 plot_tracking_quality_indicators.py --conf conf/RealTime_input/gnss-sdr_GPS_L1_USRP_realtime.conf --input-path ./out
|
||||
python3 plot_tracking_quality_indicators.py --file-prefix hackrf_l1_tracking_ch_ --channels 8 --signal-type 1C
|
||||
```
|
||||
|
||||
@@ -232,12 +258,14 @@ Input pattern: `<input-path>/<file-prefix><channel>.dat`.
|
||||
| Option | Default | Description |
|
||||
| -------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `-i`, `--input-path` | `.` | Directory containing the telemetry `.dat` dumps. |
|
||||
| `--file-prefix` | `telemetry` | `TelemetryDecoder.dump_filename` value (may include a directory/extension). |
|
||||
| `--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` | `18` | Number of channels to try reading; missing channels are skipped. |
|
||||
| `--first-channel` | `0` | First channel number in the dump filenames. |
|
||||
| `--channel-a` | `0` | First channel number to plot. |
|
||||
| `--channel-b` | `5` | Second channel number to plot. |
|
||||
| `--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). |
|
||||
|
||||
@@ -245,6 +273,7 @@ Examples:
|
||||
|
||||
```bash
|
||||
python3 gps_l1_ca_telemetry_plot_sample.py --input-path ./out
|
||||
python3 gps_l1_ca_telemetry_plot_sample.py --conf conf/gnss-sdr.conf --input-path ./out
|
||||
python3 gps_l1_ca_telemetry_plot_sample.py --file-prefix hackrf_l1_tlm_ch_ --channel-a 0 --channel-b 5
|
||||
```
|
||||
|
||||
@@ -258,9 +287,10 @@ Input pattern: `<input-path>/<file-prefix>.dat` (single file).
|
||||
| Option | Default | Description |
|
||||
| -------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `-i`, `--input-path` | `.` | Directory containing the observables dump. |
|
||||
| `--file-prefix` | `observables.dat` | `Observables.dump_filename` value (may include a directory/extension). |
|
||||
| `--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` | `5` | Number of observable channels in the dump. |
|
||||
| `--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). |
|
||||
|
||||
@@ -269,6 +299,7 @@ Examples:
|
||||
```bash
|
||||
python3 hybrid_observables_plot_sample.py
|
||||
python3 hybrid_observables_plot_sample.py --input-path ./out --channels 8
|
||||
python3 hybrid_observables_plot_sample.py --conf conf/gnss-sdr.conf --input-path ./out
|
||||
python3 hybrid_observables_plot_sample.py --file-prefix hackrf_l1_observables.dat
|
||||
```
|
||||
|
||||
@@ -283,9 +314,10 @@ Input pattern: `<input-path>/<file-prefix>.dat` (single file).
|
||||
| Option | Default | Description |
|
||||
| ----------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `-i`, `--input-path` | `.` | Directory containing the PVT dump. |
|
||||
| `--file-prefix` | `PVT.dat` | `PVT.dump_filename` value (may include a directory/extension). |
|
||||
| `--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` | `10.0` | Navigation solution period in milliseconds. |
|
||||
| `--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). |
|
||||
@@ -299,6 +331,7 @@ Examples:
|
||||
```bash
|
||||
python3 gps_l1_ca_pvt_raw_plot_sample.py
|
||||
python3 gps_l1_ca_pvt_raw_plot_sample.py --input-path ./out --file-prefix PVT.dat
|
||||
python3 gps_l1_ca_pvt_raw_plot_sample.py --conf conf/gnss-sdr.conf --input-path ./out
|
||||
python3 gps_l1_ca_pvt_raw_plot_sample.py --true-position 431234.0 4587654.0 120.0
|
||||
python3 gps_l1_ca_pvt_raw_plot_sample.py --one-vs-time X_vel --one-vs-time Y_vel
|
||||
python3 gps_l1_ca_pvt_raw_plot_sample.py --no-position
|
||||
|
||||
@@ -27,14 +27,28 @@ import numpy as np
|
||||
|
||||
from lib.dll_pll_veml_read_tracking_dump import dll_pll_veml_read_tracking_dump
|
||||
from lib.dump_filename import resolve_dump_prefix
|
||||
from lib.gnss_sdr_conf import (
|
||||
ChannelDump,
|
||||
ConfigError,
|
||||
SIGNAL_TYPES,
|
||||
add_conf_argument,
|
||||
load_gnss_sdr_conf,
|
||||
)
|
||||
from lib.plot_format import add_output_format_argument, apply_publication_style
|
||||
from lib.plotVEMLTracking import plotVEMLTracking
|
||||
|
||||
DEFAULT_FILE_PREFIX = "track_ch"
|
||||
DEFAULT_SAMPLING_FREQUENCY = 3000000.0
|
||||
DEFAULT_CHANNELS = 5
|
||||
DEFAULT_FIRST_CHANNEL = 0
|
||||
DEFAULT_SIGNAL_TYPE = "1C"
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Plot GNSS-SDR DLL/PLL VEML tracking dumps."
|
||||
)
|
||||
add_conf_argument(parser)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--input-path",
|
||||
@@ -51,28 +65,42 @@ def parse_args():
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file-prefix",
|
||||
default="track_ch",
|
||||
help="GNSS-SDR Tracking.dump_filename value (default: track_ch). May "
|
||||
"include a directory and extension; the matching <prefix><channel>.dat "
|
||||
"files are read, resolved against --input-path.",
|
||||
default=None,
|
||||
help="GNSS-SDR Tracking.dump_filename value. May include a directory "
|
||||
"and extension; the matching <prefix><channel>.dat files are read, "
|
||||
"resolved against --input-path. Defaults to the configured Tracking "
|
||||
"dump filename or filenames from --conf, or track_ch.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sampling-frequency",
|
||||
type=float,
|
||||
default=3000000.0,
|
||||
help="Signal sampling frequency in Hz.",
|
||||
default=None,
|
||||
help="Signal sampling frequency in Hz. Defaults to "
|
||||
"GNSS-SDR.internal_fs_sps from --conf, or 3000000.0.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--channels",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Number of channels to read.",
|
||||
default=None,
|
||||
help="Number of channels to read. Defaults to all selected channels "
|
||||
"from --conf, or 5.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--first-channel",
|
||||
type=int,
|
||||
default=0,
|
||||
help="First channel number in the dump filenames.",
|
||||
default=None,
|
||||
help="First channel number in the dump filenames. Defaults to the "
|
||||
"first selected absolute channel from --conf, or 0.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--signal-type",
|
||||
type=str.upper,
|
||||
choices=sorted(SIGNAL_TYPES),
|
||||
default=None,
|
||||
metavar="CODE",
|
||||
help="GNSS-SDR signal code used to restrict channel ranges and dump "
|
||||
"filenames from --conf (e.g. 1C, 5X, 7X, E6, J1). Omitted with a "
|
||||
"multi-signal --conf means all configured signals.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--plot-last-outputs",
|
||||
@@ -93,15 +121,98 @@ def parse_args():
|
||||
)
|
||||
add_output_format_argument(parser)
|
||||
parser.set_defaults(plot_doppler=True)
|
||||
return parser.parse_args()
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
apply_conf_defaults(args)
|
||||
except ConfigError as exc:
|
||||
parser.error(str(exc))
|
||||
return args
|
||||
|
||||
|
||||
def apply_conf_defaults(args):
|
||||
conf = load_gnss_sdr_conf(args.conf) if args.conf else None
|
||||
signals = []
|
||||
explicit_range = args.channels is not None or args.first_channel is not None
|
||||
|
||||
if conf is not None:
|
||||
signals = conf.select_signals(args.signal_type)
|
||||
if args.signal_type is not None:
|
||||
args.signal_type = signals[0].signal
|
||||
|
||||
if args.file_prefix is None:
|
||||
if conf is None or (len(signals) == 1 and explicit_range):
|
||||
signal = signals[0] if signals else None
|
||||
args.file_prefix = (
|
||||
signal.tracking_dump_filename
|
||||
if signal is not None and signal.tracking_dump_filename
|
||||
else DEFAULT_FILE_PREFIX
|
||||
)
|
||||
if args.sampling_frequency is None:
|
||||
if conf is not None:
|
||||
if conf.internal_fs_sps is None:
|
||||
raise ConfigError(
|
||||
"GNSS-SDR.internal_fs_sps is required to infer "
|
||||
"--sampling-frequency."
|
||||
)
|
||||
args.sampling_frequency = conf.internal_fs_sps
|
||||
else:
|
||||
args.sampling_frequency = DEFAULT_SAMPLING_FREQUENCY
|
||||
|
||||
if conf is not None and not explicit_range:
|
||||
args.channel_plan = conf.channel_dump_plan(
|
||||
signals=signals,
|
||||
dump_filename_attr="tracking_dump_filename",
|
||||
default_file_prefix=DEFAULT_FILE_PREFIX,
|
||||
override_file_prefix=args.file_prefix,
|
||||
)
|
||||
args.channels = len(args.channel_plan)
|
||||
args.first_channel = args.channel_plan[0].channel
|
||||
args.channel_ids = [entry.channel for entry in args.channel_plan]
|
||||
if len(signals) == 1:
|
||||
args.file_prefix = args.channel_plan[0].file_prefix
|
||||
return
|
||||
|
||||
if args.channels is None:
|
||||
args.channels = signals[0].count if signals else DEFAULT_CHANNELS
|
||||
|
||||
if args.first_channel is None:
|
||||
args.first_channel = (
|
||||
signals[0].first_channel if signals else DEFAULT_FIRST_CHANNEL
|
||||
)
|
||||
|
||||
if args.file_prefix is None:
|
||||
args.file_prefix = DEFAULT_FILE_PREFIX
|
||||
|
||||
manual_signal = args.signal_type or (
|
||||
signals[0].signal if signals else DEFAULT_SIGNAL_TYPE
|
||||
)
|
||||
args.channel_plan = [
|
||||
ChannelDump(
|
||||
channel=channel,
|
||||
signal=manual_signal,
|
||||
file_prefix=args.file_prefix,
|
||||
)
|
||||
for channel in range(args.first_channel, args.first_channel + args.channels)
|
||||
]
|
||||
args.channel_ids = [entry.channel for entry in args.channel_plan]
|
||||
|
||||
|
||||
def read_tracking_dumps(args):
|
||||
directory, base = resolve_dump_prefix(args.file_prefix, args.input_path)
|
||||
dumps = []
|
||||
for channel in range(args.first_channel, args.first_channel + args.channels):
|
||||
for entry in args.channel_plan:
|
||||
directory, base = resolve_dump_prefix(entry.file_prefix, args.input_path)
|
||||
channel = entry.channel
|
||||
tracking_log_path = directory / f"{base}{channel}.dat"
|
||||
dumps.append(dll_pll_veml_read_tracking_dump(tracking_log_path))
|
||||
if not tracking_log_path.exists():
|
||||
print(f"Skipping channel {channel}: missing {tracking_log_path}")
|
||||
continue
|
||||
tracking = dll_pll_veml_read_tracking_dump(tracking_log_path)
|
||||
if not tracking["PRN"]:
|
||||
print(f"Skipping channel {channel}: no samples in {tracking_log_path}")
|
||||
continue
|
||||
tracking["_channel"] = channel
|
||||
tracking["_signal_type"] = entry.signal
|
||||
dumps.append(tracking)
|
||||
return dumps
|
||||
|
||||
|
||||
@@ -112,9 +223,15 @@ def main():
|
||||
apply_publication_style()
|
||||
|
||||
gnss_tracking = read_tracking_dumps(args)
|
||||
if not gnss_tracking:
|
||||
raise SystemExit("Error: no channel dumps found to plot.")
|
||||
|
||||
plotted_channel_ids = [tracking["_channel"] for tracking in gnss_tracking]
|
||||
track_results = []
|
||||
settings = {
|
||||
"numberOfChannels": args.channels,
|
||||
"numberOfChannels": len(gnss_tracking),
|
||||
"firstChannel": plotted_channel_ids[0],
|
||||
"channelIds": plotted_channel_ids,
|
||||
"fig_path": args.fig_path,
|
||||
"show": args.show,
|
||||
"output_format": args.output_format,
|
||||
@@ -158,7 +275,7 @@ def main():
|
||||
plotVEMLTracking(index, track_results, settings)
|
||||
|
||||
if args.plot_doppler:
|
||||
channel = args.first_channel + index - 1
|
||||
channel = tracking["_channel"]
|
||||
plt.figure()
|
||||
plt.plot(
|
||||
track_result["prn_start_time_s"],
|
||||
|
||||
@@ -21,20 +21,34 @@
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
from lib.dump_filename import resolve_dump_prefix
|
||||
from lib.gnss_sdr_conf import (
|
||||
ConfigError,
|
||||
SIGNAL_TYPES,
|
||||
add_conf_argument,
|
||||
load_gnss_sdr_conf,
|
||||
)
|
||||
from lib.gps_l1_ca_kf_read_tracking_dump import gps_l1_ca_kf_read_tracking_dump
|
||||
from lib.plot_format import add_output_format_argument, apply_publication_style
|
||||
from lib.plotKalman import plotKalman
|
||||
from lib.plotTracking import plotTracking
|
||||
|
||||
DEFAULT_FILE_PREFIX = "track_ch"
|
||||
DEFAULT_SAMPLING_FREQUENCY = 4000000.0
|
||||
DEFAULT_CHANNELS = 5
|
||||
DEFAULT_FIRST_CHANNEL = 0
|
||||
DEFAULT_CODE_PERIOD = 0.001
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Plot GPS L1 C/A Kalman-filter tracking dumps."
|
||||
)
|
||||
add_conf_argument(parser)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--input-path",
|
||||
@@ -51,33 +65,46 @@ def parse_args():
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file-prefix",
|
||||
default="track_ch",
|
||||
help="GNSS-SDR Tracking.dump_filename value (default: track_ch). May "
|
||||
"include a directory and extension; the matching <prefix><channel>.dat "
|
||||
"files are read, resolved against --input-path.",
|
||||
default=None,
|
||||
help="GNSS-SDR Tracking.dump_filename value. May include a directory "
|
||||
"and extension; the matching <prefix><channel>.dat files are read, "
|
||||
"resolved against --input-path. Defaults to the selected Tracking "
|
||||
"dump filename from --conf, or track_ch.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sampling-frequency",
|
||||
type=float,
|
||||
default=4000000.0,
|
||||
help="Signal sampling frequency in Hz.",
|
||||
default=None,
|
||||
help="Signal sampling frequency in Hz. Defaults to "
|
||||
"GNSS-SDR.internal_fs_sps from --conf, or 4000000.0.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--channels",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Number of channels to read.",
|
||||
default=None,
|
||||
help="Number of channels to read. Defaults to the selected signal's "
|
||||
"channel count from --conf, or 5.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--first-channel",
|
||||
type=int,
|
||||
default=0,
|
||||
help="First channel number in the dump filenames.",
|
||||
default=None,
|
||||
help="First channel number in the dump filenames. Defaults to the "
|
||||
"selected signal's first absolute channel from --conf, or 0.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--signal-type",
|
||||
type=str.upper,
|
||||
choices=sorted(SIGNAL_TYPES),
|
||||
default=None,
|
||||
metavar="CODE",
|
||||
help="GNSS-SDR signal code used to select channel ranges and dump "
|
||||
"filenames from --conf (default: infer from --conf, otherwise 1C).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--code-period",
|
||||
type=float,
|
||||
default=0.001,
|
||||
default=None,
|
||||
help="Code period in seconds.",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -86,7 +113,69 @@ def parse_args():
|
||||
help="Display figures interactively after saving them.",
|
||||
)
|
||||
add_output_format_argument(parser)
|
||||
return parser.parse_args()
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
apply_conf_defaults(args)
|
||||
except ConfigError as exc:
|
||||
parser.error(str(exc))
|
||||
return args
|
||||
|
||||
|
||||
def apply_conf_defaults(args):
|
||||
conf = load_gnss_sdr_conf(args.conf) if args.conf else None
|
||||
signal = None
|
||||
if conf is not None and (
|
||||
args.signal_type is not None
|
||||
or args.file_prefix is None
|
||||
or args.channels is None
|
||||
or args.first_channel is None
|
||||
):
|
||||
signal = conf.select_signal(
|
||||
args.signal_type,
|
||||
default_signal="1C",
|
||||
prefer_default_on_ambiguous=True,
|
||||
)
|
||||
args.signal_type = signal.signal
|
||||
|
||||
if (
|
||||
signal.tracking_implementation
|
||||
and "KF" not in signal.tracking_implementation.upper()
|
||||
):
|
||||
print(
|
||||
"Warning: selected tracking implementation "
|
||||
f"{signal.tracking_implementation!r} does not look like a "
|
||||
"Kalman-filter tracker; the dump layout may be incompatible.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if args.file_prefix is None:
|
||||
args.file_prefix = (
|
||||
signal.tracking_dump_filename
|
||||
if signal is not None and signal.tracking_dump_filename
|
||||
else DEFAULT_FILE_PREFIX
|
||||
)
|
||||
|
||||
if args.sampling_frequency is None:
|
||||
if conf is not None:
|
||||
if conf.internal_fs_sps is None:
|
||||
raise ConfigError(
|
||||
"GNSS-SDR.internal_fs_sps is required to infer "
|
||||
"--sampling-frequency."
|
||||
)
|
||||
args.sampling_frequency = conf.internal_fs_sps
|
||||
else:
|
||||
args.sampling_frequency = DEFAULT_SAMPLING_FREQUENCY
|
||||
|
||||
if args.channels is None:
|
||||
args.channels = signal.count if signal is not None else DEFAULT_CHANNELS
|
||||
|
||||
if args.first_channel is None:
|
||||
args.first_channel = (
|
||||
signal.first_channel if signal is not None else DEFAULT_FIRST_CHANNEL
|
||||
)
|
||||
|
||||
if args.code_period is None:
|
||||
args.code_period = DEFAULT_CODE_PERIOD
|
||||
|
||||
|
||||
def read_tracking_dumps(args):
|
||||
@@ -145,6 +234,7 @@ def main():
|
||||
|
||||
settings = {
|
||||
"numberOfChannels": args.channels,
|
||||
"firstChannel": args.first_channel,
|
||||
"msToProcess": len(tracking["E"]),
|
||||
"codePeriod": args.code_period,
|
||||
"timeStartInSeconds": 0,
|
||||
|
||||
@@ -21,13 +21,22 @@ from pathlib import Path
|
||||
import numpy as np
|
||||
|
||||
from lib.dump_filename import resolve_dump_prefix
|
||||
from lib.gnss_sdr_conf import (
|
||||
ConfigError,
|
||||
add_conf_argument,
|
||||
load_gnss_sdr_conf,
|
||||
)
|
||||
from lib.plot_format import add_output_format_argument, apply_publication_style
|
||||
|
||||
DEFAULT_FILE_PREFIX = "PVT.dat"
|
||||
DEFAULT_NAV_SOL_PERIOD = 10.0
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Plot GNSS-SDR GPS L1 C/A PVT raw dump data."
|
||||
)
|
||||
add_conf_argument(parser)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--input-path",
|
||||
@@ -37,10 +46,10 @@ def parse_args():
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file-prefix",
|
||||
default="PVT.dat",
|
||||
help="GNSS-SDR PVT.dump_filename value (default: PVT.dat). May include "
|
||||
"a directory and extension; the matching <prefix>.dat file is read, "
|
||||
"resolved against --input-path.",
|
||||
default=None,
|
||||
help="GNSS-SDR PVT.dump_filename value. May include a directory and "
|
||||
"extension; the matching <prefix>.dat file is read, resolved against "
|
||||
"--input-path. Defaults to PVT.dump_filename from --conf, or PVT.dat.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
@@ -52,8 +61,9 @@ def parse_args():
|
||||
parser.add_argument(
|
||||
"--nav-sol-period",
|
||||
type=float,
|
||||
default=10.0,
|
||||
help="Navigation solution period in milliseconds.",
|
||||
default=None,
|
||||
help="Navigation solution period in milliseconds. Defaults to "
|
||||
"PVT.output_rate_ms from --conf, or 10.0.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--true-position",
|
||||
@@ -94,7 +104,30 @@ def parse_args():
|
||||
)
|
||||
add_output_format_argument(parser)
|
||||
parser.set_defaults(plot_position=True)
|
||||
return parser.parse_args()
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
apply_conf_defaults(args)
|
||||
except ConfigError as exc:
|
||||
parser.error(str(exc))
|
||||
return args
|
||||
|
||||
|
||||
def apply_conf_defaults(args):
|
||||
conf = load_gnss_sdr_conf(args.conf) if args.conf else None
|
||||
|
||||
if args.file_prefix is None:
|
||||
args.file_prefix = (
|
||||
conf.pvt_dump_filename
|
||||
if conf is not None and conf.pvt_dump_filename
|
||||
else DEFAULT_FILE_PREFIX
|
||||
)
|
||||
|
||||
if args.nav_sol_period is None:
|
||||
args.nav_sol_period = (
|
||||
conf.pvt_output_rate_ms
|
||||
if conf is not None and conf.pvt_output_rate_ms is not None
|
||||
else DEFAULT_NAV_SOL_PERIOD
|
||||
)
|
||||
|
||||
|
||||
def add_utm_coordinates(nav_solutions):
|
||||
|
||||
@@ -25,14 +25,27 @@ from pathlib import Path
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from lib.dump_filename import resolve_dump_prefix
|
||||
from lib.gnss_sdr_conf import (
|
||||
ConfigError,
|
||||
SIGNAL_TYPES,
|
||||
add_conf_argument,
|
||||
load_gnss_sdr_conf,
|
||||
)
|
||||
from lib.gps_l1_ca_read_telemetry_dump import gps_l1_ca_read_telemetry_dump
|
||||
from lib.plot_format import add_output_format_argument, apply_publication_style
|
||||
|
||||
DEFAULT_FILE_PREFIX = "telemetry"
|
||||
DEFAULT_CHANNELS = 18
|
||||
DEFAULT_FIRST_CHANNEL = 0
|
||||
DEFAULT_CHANNEL_A = 0
|
||||
DEFAULT_CHANNEL_B = 5
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Plot GNSS-SDR GPS L1 C/A telemetry dumps."
|
||||
)
|
||||
add_conf_argument(parser)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--input-path",
|
||||
@@ -49,34 +62,49 @@ def parse_args():
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file-prefix",
|
||||
default="telemetry",
|
||||
help="GNSS-SDR TelemetryDecoder.dump_filename value (default: "
|
||||
"telemetry). May include a directory and extension; the matching "
|
||||
"<prefix><channel>.dat files are read, resolved against --input-path.",
|
||||
default=None,
|
||||
help="GNSS-SDR TelemetryDecoder.dump_filename value. May include a "
|
||||
"directory and extension; the matching <prefix><channel>.dat files "
|
||||
"are read, resolved against --input-path. Defaults to the selected "
|
||||
"TelemetryDecoder dump filename from --conf, or telemetry.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--channels",
|
||||
type=int,
|
||||
default=18,
|
||||
help="Number of channels to try reading.",
|
||||
default=None,
|
||||
help="Number of channels to try reading. Defaults to the selected "
|
||||
"signal's channel count from --conf, or 18.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--first-channel",
|
||||
type=int,
|
||||
default=0,
|
||||
help="First channel number in the dump filenames.",
|
||||
default=None,
|
||||
help="First channel number in the dump filenames. Defaults to the "
|
||||
"selected signal's first absolute channel from --conf, or 0.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--signal-type",
|
||||
type=str.upper,
|
||||
choices=sorted(SIGNAL_TYPES),
|
||||
default=None,
|
||||
metavar="CODE",
|
||||
help="GNSS-SDR signal code used to select channel ranges and dump "
|
||||
"filenames from --conf (default: infer from --conf, preferring 1C "
|
||||
"when available).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--channel-a",
|
||||
type=int,
|
||||
default=0,
|
||||
help="First channel number to plot.",
|
||||
default=None,
|
||||
help="First channel number to plot. Defaults to the first selected "
|
||||
"channel from --conf, or 0.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--channel-b",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Second channel number to plot.",
|
||||
default=None,
|
||||
help="Second channel number to plot. Defaults to the second selected "
|
||||
"channel from --conf, or 5.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--show",
|
||||
@@ -84,7 +112,59 @@ def parse_args():
|
||||
help="Display figures interactively after saving them.",
|
||||
)
|
||||
add_output_format_argument(parser)
|
||||
return parser.parse_args()
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
apply_conf_defaults(args)
|
||||
except ConfigError as exc:
|
||||
parser.error(str(exc))
|
||||
return args
|
||||
|
||||
|
||||
def apply_conf_defaults(args):
|
||||
conf = load_gnss_sdr_conf(args.conf) if args.conf else None
|
||||
signal = None
|
||||
if conf is not None and (
|
||||
args.signal_type is not None
|
||||
or args.file_prefix is None
|
||||
or args.channels is None
|
||||
or args.first_channel is None
|
||||
or args.channel_a is None
|
||||
or args.channel_b is None
|
||||
):
|
||||
signal = conf.select_signal(
|
||||
args.signal_type,
|
||||
default_signal="1C",
|
||||
prefer_default_on_ambiguous=True,
|
||||
)
|
||||
args.signal_type = signal.signal
|
||||
|
||||
if args.file_prefix is None:
|
||||
args.file_prefix = (
|
||||
signal.telemetry_dump_filename
|
||||
if signal is not None and signal.telemetry_dump_filename
|
||||
else DEFAULT_FILE_PREFIX
|
||||
)
|
||||
|
||||
if args.channels is None:
|
||||
args.channels = signal.count if signal is not None else DEFAULT_CHANNELS
|
||||
|
||||
if args.first_channel is None:
|
||||
args.first_channel = (
|
||||
signal.first_channel if signal is not None else DEFAULT_FIRST_CHANNEL
|
||||
)
|
||||
|
||||
if args.channel_a is None:
|
||||
args.channel_a = (
|
||||
signal.first_channel if signal is not None else DEFAULT_CHANNEL_A
|
||||
)
|
||||
|
||||
if args.channel_b is None:
|
||||
if signal is not None and signal.count > 1:
|
||||
args.channel_b = signal.first_channel + 1
|
||||
elif signal is not None:
|
||||
args.channel_b = signal.first_channel
|
||||
else:
|
||||
args.channel_b = DEFAULT_CHANNEL_B
|
||||
|
||||
|
||||
def read_telemetry_dumps(args):
|
||||
|
||||
@@ -23,14 +23,23 @@ import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from lib.dump_filename import resolve_dump_prefix
|
||||
from lib.gnss_sdr_conf import (
|
||||
ConfigError,
|
||||
add_conf_argument,
|
||||
load_gnss_sdr_conf,
|
||||
)
|
||||
from lib.plot_format import add_output_format_argument, apply_publication_style
|
||||
from lib.read_hybrid_observables_dump import read_hybrid_observables_dump
|
||||
|
||||
DEFAULT_FILE_PREFIX = "observables.dat"
|
||||
DEFAULT_CHANNELS = 5
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Plot GNSS-SDR hybrid observables dump data."
|
||||
)
|
||||
add_conf_argument(parser)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--input-path",
|
||||
@@ -40,10 +49,11 @@ def parse_args():
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file-prefix",
|
||||
default="observables.dat",
|
||||
help="GNSS-SDR Observables.dump_filename value (default: "
|
||||
"observables.dat). May include a directory and extension; the "
|
||||
"matching <prefix>.dat file is read, resolved against --input-path.",
|
||||
default=None,
|
||||
help="GNSS-SDR Observables.dump_filename value. May include a "
|
||||
"directory and extension; the matching <prefix>.dat file is read, "
|
||||
"resolved against --input-path. Defaults to Observables.dump_filename "
|
||||
"from --conf, or observables.dat.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
@@ -55,8 +65,9 @@ def parse_args():
|
||||
parser.add_argument(
|
||||
"--channels",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Number of observable channels in the dump.",
|
||||
default=None,
|
||||
help="Number of observable channels in the dump. Defaults to the "
|
||||
"total configured channel count from --conf, or 5.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--show",
|
||||
@@ -64,7 +75,34 @@ def parse_args():
|
||||
help="Display figures interactively after saving them.",
|
||||
)
|
||||
add_output_format_argument(parser)
|
||||
return parser.parse_args()
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
apply_conf_defaults(args)
|
||||
except ConfigError as exc:
|
||||
parser.error(str(exc))
|
||||
return args
|
||||
|
||||
|
||||
def apply_conf_defaults(args):
|
||||
conf = load_gnss_sdr_conf(args.conf) if args.conf else None
|
||||
|
||||
if args.file_prefix is None:
|
||||
args.file_prefix = (
|
||||
conf.observables_dump_filename
|
||||
if conf is not None and conf.observables_dump_filename
|
||||
else DEFAULT_FILE_PREFIX
|
||||
)
|
||||
|
||||
if args.channels is None:
|
||||
if conf is not None:
|
||||
if conf.total_channels <= 0:
|
||||
raise ConfigError(
|
||||
"At least one Channels_<signal>.count value is required "
|
||||
"to infer --channels."
|
||||
)
|
||||
args.channels = conf.total_channels
|
||||
else:
|
||||
args.channels = DEFAULT_CHANNELS
|
||||
|
||||
|
||||
def first_valid_observable(gnss_observables, channels):
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
"""
|
||||
gnss_sdr_conf.py
|
||||
|
||||
Helpers to read GNSS-SDR configuration files from the Python plotting
|
||||
utilities.
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
GNSS-SDR is a Global Navigation Satellite System software-defined receiver.
|
||||
This file is part of GNSS-SDR.
|
||||
|
||||
Copyright (C) 2026 (see AUTHORS file for a list of contributors)
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# Keep this order in sync with src/core/receiver/gnss_block_factory.cc. GNSS-SDR
|
||||
# assigns absolute channel IDs by walking the configured signal counts in this
|
||||
# order.
|
||||
SIGNAL_ORDER = (
|
||||
"1C",
|
||||
"2S",
|
||||
"L5",
|
||||
"1B",
|
||||
"5X",
|
||||
"E6",
|
||||
"1G",
|
||||
"2G",
|
||||
"B1",
|
||||
"B3",
|
||||
"7X",
|
||||
"J1",
|
||||
"J5",
|
||||
)
|
||||
|
||||
# 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.
|
||||
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
|
||||
}
|
||||
|
||||
N_CHIPS = {(system, code): nc for code, (system, nc) in SIGNAL_TYPES.items()}
|
||||
|
||||
|
||||
class ConfigError(ValueError):
|
||||
"""Raised when a configuration file cannot provide required metadata."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SignalConfig:
|
||||
signal: str
|
||||
system: str
|
||||
n_chips: int
|
||||
count: int
|
||||
first_channel: int
|
||||
acquisition_dump_filename: Optional[str]
|
||||
tracking_dump_filename: Optional[str]
|
||||
telemetry_dump_filename: Optional[str]
|
||||
acquisition_implementation: Optional[str]
|
||||
tracking_implementation: Optional[str]
|
||||
telemetry_implementation: Optional[str]
|
||||
|
||||
@property
|
||||
def last_channel(self):
|
||||
if self.count <= 0:
|
||||
return None
|
||||
return self.first_channel + self.count - 1
|
||||
|
||||
@property
|
||||
def channels(self):
|
||||
return range(self.first_channel, self.first_channel + self.count)
|
||||
|
||||
def range_label(self):
|
||||
if self.count == 0:
|
||||
return "none"
|
||||
if self.count == 1:
|
||||
return str(self.first_channel)
|
||||
return f"{self.first_channel}..{self.last_channel}"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ChannelDump:
|
||||
channel: int
|
||||
signal: str
|
||||
file_prefix: str
|
||||
|
||||
|
||||
def add_conf_argument(parser):
|
||||
parser.add_argument(
|
||||
"--conf",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="GNSS-SDR configuration file. When provided, missing plotting "
|
||||
"options such as dump filename, channel range, signal type, sampling "
|
||||
"frequency, and PVT period are inferred from it.",
|
||||
)
|
||||
|
||||
|
||||
def load_gnss_sdr_conf(path):
|
||||
return GnssSdrConfig(Path(path))
|
||||
|
||||
|
||||
class GnssSdrConfig:
|
||||
def __init__(self, path):
|
||||
self.path = Path(path)
|
||||
self.values = _parse_config_file(self.path)
|
||||
self._signals = None
|
||||
|
||||
def get_str(self, key, default=None):
|
||||
return self.values.get(_normalize_key(key), default)
|
||||
|
||||
def get_bool(self, key, default=None):
|
||||
value = self.get_str(key)
|
||||
if value is None:
|
||||
return default
|
||||
normalized = value.strip().lower()
|
||||
if normalized in ("true", "yes", "on", "1"):
|
||||
return True
|
||||
if normalized in ("false", "no", "off", "0"):
|
||||
return False
|
||||
return default
|
||||
|
||||
def get_int(self, key, default=None):
|
||||
value = self.get_str(key)
|
||||
if value is None:
|
||||
return default
|
||||
try:
|
||||
return int(value, 0)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
def get_float(self, key, default=None):
|
||||
value = self.get_str(key)
|
||||
if value is None:
|
||||
return default
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
@property
|
||||
def internal_fs_sps(self):
|
||||
return self.get_float(
|
||||
"GNSS-SDR.internal_fs_sps",
|
||||
self.get_float("GNSS-SDR.internal_fs_hz"),
|
||||
)
|
||||
|
||||
@property
|
||||
def pvt_output_rate_ms(self):
|
||||
return self.get_float("PVT.output_rate_ms")
|
||||
|
||||
@property
|
||||
def observables_dump_filename(self):
|
||||
return self.get_str("Observables.dump_filename")
|
||||
|
||||
@property
|
||||
def pvt_dump_filename(self):
|
||||
return self.get_str("PVT.dump_filename")
|
||||
|
||||
@property
|
||||
def signals(self):
|
||||
if self._signals is None:
|
||||
self._signals = self._build_signals()
|
||||
return self._signals
|
||||
|
||||
@property
|
||||
def enabled_signals(self):
|
||||
return [signal for signal in self.signals if signal.count > 0]
|
||||
|
||||
@property
|
||||
def total_channels(self):
|
||||
return sum(signal.count for signal in self.signals)
|
||||
|
||||
def signal(self, signal_type):
|
||||
requested = signal_type.upper()
|
||||
for signal in self.signals:
|
||||
if signal.signal == requested:
|
||||
return signal
|
||||
return None
|
||||
|
||||
def require_enabled_signal(self, signal_type):
|
||||
signal = self.signal(signal_type)
|
||||
if signal is None:
|
||||
choices = ", ".join(SIGNAL_ORDER)
|
||||
raise ConfigError(
|
||||
f"Unsupported signal type {signal_type!r}. Choices: {choices}."
|
||||
)
|
||||
if signal.count <= 0:
|
||||
raise ConfigError(
|
||||
f"Configuration {self.path} enables no {signal.signal} "
|
||||
"channels."
|
||||
)
|
||||
return signal
|
||||
|
||||
def select_signal(
|
||||
self,
|
||||
requested_signal=None,
|
||||
*,
|
||||
default_signal=None,
|
||||
prefer_default_on_ambiguous=False,
|
||||
):
|
||||
if requested_signal is not None:
|
||||
return self.require_enabled_signal(requested_signal)
|
||||
|
||||
enabled = self.enabled_signals
|
||||
if len(enabled) == 1:
|
||||
return enabled[0]
|
||||
|
||||
if (
|
||||
default_signal is not None
|
||||
and prefer_default_on_ambiguous
|
||||
and self.signal(default_signal) is not None
|
||||
and self.signal(default_signal).count > 0
|
||||
):
|
||||
return self.signal(default_signal)
|
||||
|
||||
if not enabled:
|
||||
raise ConfigError(
|
||||
f"Configuration {self.path} enables no GNSS-SDR channels."
|
||||
)
|
||||
|
||||
available = ", ".join(
|
||||
f"{signal.signal} (channels {signal.range_label()})"
|
||||
for signal in enabled
|
||||
)
|
||||
raise ConfigError(
|
||||
f"Configuration {self.path} enables multiple signals: {available}. "
|
||||
"Pass --signal-type to select one."
|
||||
)
|
||||
|
||||
def select_signals(self, requested_signal=None):
|
||||
if requested_signal is not None:
|
||||
return [self.require_enabled_signal(requested_signal)]
|
||||
|
||||
enabled = self.enabled_signals
|
||||
if not enabled:
|
||||
raise ConfigError(
|
||||
f"Configuration {self.path} enables no GNSS-SDR channels."
|
||||
)
|
||||
return enabled
|
||||
|
||||
def channel_dump_plan(
|
||||
self,
|
||||
*,
|
||||
signals,
|
||||
dump_filename_attr,
|
||||
default_file_prefix,
|
||||
override_file_prefix=None,
|
||||
):
|
||||
plan = []
|
||||
for signal in signals:
|
||||
file_prefix = (
|
||||
override_file_prefix
|
||||
or getattr(signal, dump_filename_attr)
|
||||
or default_file_prefix
|
||||
)
|
||||
for channel in signal.channels:
|
||||
plan.append(ChannelDump(channel, signal.signal, file_prefix))
|
||||
return plan
|
||||
|
||||
def summary_lines(self):
|
||||
lines = [f"Configuration: {self.path}"]
|
||||
if self.internal_fs_sps is not None:
|
||||
lines.append(f"internal_fs_sps: {self.internal_fs_sps:g}")
|
||||
lines.append("signals:")
|
||||
for signal in self.enabled_signals:
|
||||
parts = [
|
||||
f" {signal.signal}: channels {signal.range_label()}",
|
||||
]
|
||||
if signal.tracking_dump_filename:
|
||||
parts.append(f"tracking dump {signal.tracking_dump_filename}")
|
||||
if signal.acquisition_dump_filename:
|
||||
parts.append(
|
||||
f"acquisition dump {signal.acquisition_dump_filename}"
|
||||
)
|
||||
lines.append(", ".join(parts))
|
||||
if self.observables_dump_filename:
|
||||
lines.append(f"observables dump: {self.observables_dump_filename}")
|
||||
if self.pvt_dump_filename:
|
||||
lines.append(f"PVT dump: {self.pvt_dump_filename}")
|
||||
return lines
|
||||
|
||||
def _build_signals(self):
|
||||
signals = []
|
||||
first_channel = 0
|
||||
for signal_type in SIGNAL_ORDER:
|
||||
system, n_chips = SIGNAL_TYPES[signal_type]
|
||||
count = self.get_int(f"Channels_{signal_type}.count", 0)
|
||||
if count is None:
|
||||
count = 0
|
||||
if count < 0:
|
||||
raise ConfigError(
|
||||
f"Configuration {self.path} has a negative channel count "
|
||||
f"for Channels_{signal_type}.count."
|
||||
)
|
||||
signals.append(
|
||||
SignalConfig(
|
||||
signal=signal_type,
|
||||
system=system,
|
||||
n_chips=n_chips,
|
||||
count=count,
|
||||
first_channel=first_channel,
|
||||
acquisition_dump_filename=self._block_value(
|
||||
"Acquisition", signal_type, "dump_filename"
|
||||
),
|
||||
tracking_dump_filename=self._block_value(
|
||||
"Tracking", signal_type, "dump_filename"
|
||||
),
|
||||
telemetry_dump_filename=self._block_value(
|
||||
"TelemetryDecoder", signal_type, "dump_filename"
|
||||
),
|
||||
acquisition_implementation=self._block_value(
|
||||
"Acquisition", signal_type, "implementation"
|
||||
),
|
||||
tracking_implementation=self._block_value(
|
||||
"Tracking", signal_type, "implementation"
|
||||
),
|
||||
telemetry_implementation=self._block_value(
|
||||
"TelemetryDecoder", signal_type, "implementation"
|
||||
),
|
||||
)
|
||||
)
|
||||
first_channel += count
|
||||
return signals
|
||||
|
||||
def _block_value(self, role, signal_type, option):
|
||||
return self.get_str(
|
||||
f"{role}_{signal_type}.{option}",
|
||||
self.get_str(f"{role}.{option}"),
|
||||
)
|
||||
|
||||
|
||||
def _parse_config_file(path):
|
||||
if not path.exists():
|
||||
raise ConfigError(f"Configuration file not found: {path}")
|
||||
|
||||
flat_values = {}
|
||||
gnss_values = {}
|
||||
has_gnss_section = False
|
||||
active_section = None
|
||||
|
||||
try:
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
except UnicodeDecodeError:
|
||||
lines = path.read_text(encoding="latin-1").splitlines()
|
||||
|
||||
for line_no, raw_line in enumerate(lines, start=1):
|
||||
if raw_line.lstrip().startswith("#"):
|
||||
continue
|
||||
line = _strip_inline_comment(raw_line).strip()
|
||||
if not line:
|
||||
continue
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
|
||||
if line.startswith("["):
|
||||
if not line.endswith("]"):
|
||||
raise ConfigError(
|
||||
f"{path}:{line_no}: malformed section header."
|
||||
)
|
||||
active_section = line[1:-1].strip().lower()
|
||||
if active_section == "gnss-sdr":
|
||||
has_gnss_section = True
|
||||
continue
|
||||
|
||||
if "=" not in line:
|
||||
raise ConfigError(f"{path}:{line_no}: expected key=value.")
|
||||
|
||||
key, value = line.split("=", 1)
|
||||
key = _normalize_key(key)
|
||||
value = value.strip()
|
||||
if active_section is None:
|
||||
flat_values[key] = value
|
||||
elif active_section == "gnss-sdr":
|
||||
gnss_values[key] = value
|
||||
|
||||
values = gnss_values if has_gnss_section else flat_values
|
||||
if not values:
|
||||
raise ConfigError(f"No GNSS-SDR configuration values found in {path}.")
|
||||
return values
|
||||
|
||||
|
||||
def _normalize_key(key):
|
||||
return key.strip().lower()
|
||||
|
||||
|
||||
def _strip_inline_comment(line):
|
||||
return line.split(";", 1)[0]
|
||||
@@ -43,6 +43,11 @@ def plotKalman(channelNr, trackResults, settings):
|
||||
np.arange(1, settings['numberOfChannels'] + 1))
|
||||
|
||||
for channelNr in channelNr:
|
||||
channel_ids = settings.get('channelIds')
|
||||
if channel_ids is not None:
|
||||
display_channel = channel_ids[channelNr - 1]
|
||||
else:
|
||||
display_channel = settings.get('firstChannel', 1) + channelNr - 1
|
||||
time_start = settings['timeStartInSeconds']
|
||||
time_axis_in_seconds = np.arange(1, settings['msToProcess']+1)/1000
|
||||
|
||||
@@ -50,7 +55,7 @@ def plotKalman(channelNr, trackResults, settings):
|
||||
plt.figure(figsize=(1920 / 100, 1080 / 100))
|
||||
plt.clf()
|
||||
plt.gcf().canvas.manager.set_window_title(
|
||||
f'Channel {channelNr} (PRN '
|
||||
f'Channel {display_channel} (PRN '
|
||||
f'{str(trackResults[channelNr-1]["PRN"][-2])}) results')
|
||||
plt.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.1,
|
||||
hspace=0.4, wspace=0.4)
|
||||
@@ -137,7 +142,7 @@ def plotKalman(channelNr, trackResults, settings):
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(os.path.join(fig_path,
|
||||
f'kalman_ch{channelNr}_PRN_'
|
||||
f'kalman_ch{display_channel}_PRN_'
|
||||
f'{trackResults[channelNr - 1]["PRN"][-1]}'
|
||||
f'.{output_format}'))
|
||||
# Close unless it will be shown; the caller triggers a single
|
||||
|
||||
@@ -40,11 +40,16 @@ def plotTracking(channelNr, trackResults, settings):
|
||||
|
||||
# Protection - if the list contains incorrect channel numbers
|
||||
if channelNr in list(range(1,settings["numberOfChannels"]+1)):
|
||||
channel_ids = settings.get('channelIds')
|
||||
if channel_ids is not None:
|
||||
display_channel = channel_ids[channelNr - 1]
|
||||
else:
|
||||
display_channel = settings.get('firstChannel', 1) + channelNr - 1
|
||||
|
||||
plt.figure(figsize=(1920 / 120, 1080 / 120))
|
||||
plt.clf()
|
||||
plt.gcf().canvas.manager.set_window_title(
|
||||
f'Channel {channelNr} (PRN '
|
||||
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,
|
||||
hspace=0.4, wspace=0.4)
|
||||
@@ -184,7 +189,7 @@ def plotTracking(channelNr, trackResults, settings):
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(os.path.join(fig_path,
|
||||
f'trk_dump_ch{channelNr}_PRN_'
|
||||
f'trk_dump_ch{display_channel}_PRN_'
|
||||
f'{trackResults[channelNr - 1]["PRN"][-1]}'
|
||||
f'.{output_format}'))
|
||||
# Close unless it will be shown; the caller triggers a single
|
||||
|
||||
@@ -38,13 +38,18 @@ def plotVEMLTracking(channelNr, trackResults, settings):
|
||||
if not os.path.exists(fig_path):
|
||||
os.makedirs(fig_path)
|
||||
|
||||
channel_ids = settings.get('channelIds')
|
||||
if channel_ids is not None:
|
||||
display_channel = channel_ids[channelNr - 1]
|
||||
else:
|
||||
display_channel = settings.get('firstChannel', 1) + channelNr - 1
|
||||
|
||||
# Protection - if the list contains incorrect channel numbers
|
||||
if channelNr in list(range(1,settings["numberOfChannels"]+1)):
|
||||
|
||||
plt.figure(figsize=(1920 / 120, 1080 / 120))
|
||||
plt.clf()
|
||||
plt.gcf().canvas.manager.set_window_title(
|
||||
f'Channel {channelNr} (PRN '
|
||||
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,
|
||||
hspace=0.4, wspace=0.4)
|
||||
@@ -165,7 +170,7 @@ def plotVEMLTracking(channelNr, trackResults, settings):
|
||||
plt.title('Filtered DLL discriminator',fontweight='bold')
|
||||
|
||||
plt.savefig(os.path.join(fig_path,
|
||||
f'Ch{channelNr}_PRN'
|
||||
f'Ch{display_channel}_PRN'
|
||||
f'{trackResults[channelNr-1]["PRN"][0]}'
|
||||
f'_results.{output_format}'))
|
||||
# Close the figure unless it will be shown; the caller triggers a single
|
||||
|
||||
+151
-50
@@ -22,9 +22,18 @@
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from lib.dump_filename import resolve_dump_prefix
|
||||
from lib.gnss_sdr_conf import (
|
||||
ConfigError,
|
||||
N_CHIPS,
|
||||
SIGNAL_TYPES,
|
||||
add_conf_argument,
|
||||
load_gnss_sdr_conf,
|
||||
)
|
||||
from lib.plot_format import add_output_format_argument, apply_publication_style
|
||||
|
||||
# Heavy numerical and plotting libraries (h5py, numpy, matplotlib, scipy) are
|
||||
@@ -32,36 +41,25 @@ from lib.plot_format import add_output_format_argument, apply_publication_style
|
||||
# argument-error invocations fast and free of matplotlib font-cache warnings.
|
||||
|
||||
|
||||
# GNSS-SDR signal nomenclature: each two-character signal code maps to its
|
||||
# system character (as written in the dump filename) and primary code length
|
||||
# in chips. The signal code matches the value used in GNSS-SDR configuration
|
||||
# (e.g. Galileo.implementation signals) and in the dump filename.
|
||||
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
|
||||
}
|
||||
N_CHIPS = {(system, code): nc for code, (system, nc) in SIGNAL_TYPES.items()}
|
||||
DEFAULT_FILE_PREFIX = "acquisition"
|
||||
DEFAULT_SAT = 1
|
||||
DEFAULT_CHANNEL = 0
|
||||
DEFAULT_EXECUTION = 1
|
||||
DEFAULT_SIGNAL_TYPE = "1C"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AcquisitionDumpPlan:
|
||||
signal: Optional[str]
|
||||
system: Optional[str]
|
||||
file_prefix: str
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Plot GNSS-SDR acquisition dump grids."
|
||||
)
|
||||
add_conf_argument(parser)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--input-path",
|
||||
@@ -78,11 +76,12 @@ def parse_args():
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file-prefix",
|
||||
default="acquisition",
|
||||
help="GNSS-SDR Acquisition.dump_filename value (default: "
|
||||
"acquisition). May include a directory and extension; matching "
|
||||
default=None,
|
||||
help="GNSS-SDR Acquisition.dump_filename value. May include a "
|
||||
"directory and extension; matching "
|
||||
"<prefix>_<sys>_<sig>_ch_<ch>_<exec>_sat_<PRN>.mat files are read, "
|
||||
"resolved against --input-path.",
|
||||
"resolved against --input-path. Defaults to the configured "
|
||||
"Acquisition dump filename or filenames from --conf, or acquisition.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-basename",
|
||||
@@ -167,8 +166,9 @@ def parse_args():
|
||||
default=None,
|
||||
metavar="CODE",
|
||||
help="GNSS-SDR signal code (e.g. 1C, 5X, 7X, E6, J1). Filters "
|
||||
"all-files mode; defaults to 1C (GPS L1 C/A) in single-file mode. "
|
||||
"Choices: " + ", ".join(sorted(SIGNAL_TYPES)) + ".",
|
||||
"all-files mode; omitted with a multi-signal --conf scans all "
|
||||
"configured signals. Defaults to 1C (GPS L1 C/A) in single-file "
|
||||
"mode. Choices: " + ", ".join(sorted(SIGNAL_TYPES)) + ".",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--samples-per-chip",
|
||||
@@ -196,13 +196,84 @@ def parse_args():
|
||||
add_output_format_argument(parser)
|
||||
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
apply_conf_defaults(args)
|
||||
except ConfigError as exc:
|
||||
parser.error(str(exc))
|
||||
if args.output_basename is None:
|
||||
# Default the saved-plot basename to the dump basename, dropping any
|
||||
# directory and extension carried by --file-prefix.
|
||||
_, args.output_basename = resolve_dump_prefix(args.file_prefix)
|
||||
# directory and extension carried by --file-prefix. A multi-signal
|
||||
# configuration may use several prefixes, so leave the basename empty
|
||||
# and let the system/signal/channel fields identify each output.
|
||||
if getattr(args, "acquisition_plans", None) and args.file_prefix is None:
|
||||
args.output_basename = None
|
||||
else:
|
||||
_, args.output_basename = resolve_dump_prefix(args.file_prefix)
|
||||
return args
|
||||
|
||||
|
||||
def apply_conf_defaults(args):
|
||||
conf = load_gnss_sdr_conf(args.conf) if args.conf else None
|
||||
signal = None
|
||||
signals = []
|
||||
args.acquisition_plans = []
|
||||
|
||||
if conf is not None:
|
||||
if args.signal_type is not None:
|
||||
signal = conf.require_enabled_signal(args.signal_type)
|
||||
signals = [signal]
|
||||
elif args.plot_all_files and args.file_prefix is None:
|
||||
signals = conf.select_signals()
|
||||
if len(signals) == 1:
|
||||
signal = signals[0]
|
||||
args.signal_type = signal.signal
|
||||
elif not args.plot_all_files:
|
||||
signal = conf.select_signal()
|
||||
signals = [signal]
|
||||
args.signal_type = signal.signal
|
||||
|
||||
if conf is not None and args.plot_all_files and args.file_prefix is None:
|
||||
args.acquisition_plans = [
|
||||
AcquisitionDumpPlan(
|
||||
signal=selected.signal,
|
||||
system=selected.system,
|
||||
file_prefix=(
|
||||
selected.acquisition_dump_filename
|
||||
or DEFAULT_FILE_PREFIX
|
||||
),
|
||||
)
|
||||
for selected in signals
|
||||
]
|
||||
if len(args.acquisition_plans) == 1:
|
||||
args.file_prefix = args.acquisition_plans[0].file_prefix
|
||||
|
||||
if args.file_prefix is None and not args.acquisition_plans:
|
||||
args.file_prefix = (
|
||||
signal.acquisition_dump_filename
|
||||
if signal is not None and signal.acquisition_dump_filename
|
||||
else DEFAULT_FILE_PREFIX
|
||||
)
|
||||
|
||||
if args.signal_type is None and not args.plot_all_files:
|
||||
args.signal_type = DEFAULT_SIGNAL_TYPE
|
||||
|
||||
|
||||
def acquisition_plans_for_args(args):
|
||||
if getattr(args, "acquisition_plans", None):
|
||||
return args.acquisition_plans
|
||||
|
||||
system = None
|
||||
if args.signal_type is not None:
|
||||
system, _ = SIGNAL_TYPES[args.signal_type]
|
||||
return [
|
||||
AcquisitionDumpPlan(
|
||||
signal=args.signal_type,
|
||||
system=system,
|
||||
file_prefix=args.file_prefix,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def positive_acq_value(data):
|
||||
for key in ("positive_acq", "d_positive_acq"):
|
||||
value = data.get(key)
|
||||
@@ -389,6 +460,12 @@ def metadata_matches_filters(metadata, args):
|
||||
return True
|
||||
|
||||
|
||||
def metadata_matches_plan(metadata, plan):
|
||||
if plan.signal is None:
|
||||
return True
|
||||
return metadata["system"] == plan.system and metadata["signal"] == plan.signal
|
||||
|
||||
|
||||
def show_figures(args):
|
||||
# Display every saved figure with a single plt.show() call. Showing once
|
||||
# (instead of once per figure) keeps the GUI event loop to a single entry
|
||||
@@ -417,32 +494,56 @@ def main():
|
||||
return
|
||||
|
||||
plotted = 0
|
||||
directory, base = resolve_dump_prefix(args.file_prefix, args.input_path)
|
||||
file_paths = sorted(directory.glob(f"{base}*.mat"))
|
||||
for file_path in file_paths:
|
||||
metadata = parse_dump_name(file_path)
|
||||
if metadata is None:
|
||||
print(f"Skipping {file_path.name}: unexpected filename format")
|
||||
continue
|
||||
found_paths = set()
|
||||
handled_paths = set()
|
||||
searched_prefixes = []
|
||||
for plan in acquisition_plans_for_args(args):
|
||||
directory, base = resolve_dump_prefix(plan.file_prefix, args.input_path)
|
||||
searched_prefixes.append((directory, base))
|
||||
file_paths = sorted(directory.glob(f"{base}*.mat"))
|
||||
for file_path in file_paths:
|
||||
found_paths.add(file_path)
|
||||
if file_path in handled_paths:
|
||||
continue
|
||||
|
||||
if not metadata_matches_filters(metadata, args):
|
||||
continue
|
||||
metadata = parse_dump_name(file_path)
|
||||
if metadata is None:
|
||||
print(f"Skipping {file_path.name}: unexpected filename format")
|
||||
continue
|
||||
|
||||
n_chips = N_CHIPS.get((metadata["system"], metadata["signal"]))
|
||||
if n_chips is None:
|
||||
print(
|
||||
f"Unknown system/signal {metadata['system']}/"
|
||||
f"{metadata['signal']}, skipping: {file_path.name}"
|
||||
if not metadata_matches_plan(metadata, plan):
|
||||
continue
|
||||
|
||||
if not metadata_matches_filters(metadata, args):
|
||||
continue
|
||||
|
||||
handled_paths.add(file_path)
|
||||
n_chips = N_CHIPS.get((metadata["system"], metadata["signal"]))
|
||||
if n_chips is None:
|
||||
print(
|
||||
f"Unknown system/signal {metadata['system']}/"
|
||||
f"{metadata['signal']}, skipping: {file_path.name}"
|
||||
)
|
||||
continue
|
||||
|
||||
image_name_root = image_name_from_metadata(
|
||||
args.output_basename,
|
||||
metadata,
|
||||
)
|
||||
continue
|
||||
if plot_dump(file_path, args.fig_path, image_name_root, n_chips, args):
|
||||
plotted += 1
|
||||
|
||||
image_name_root = image_name_from_metadata(args.output_basename, metadata)
|
||||
if plot_dump(file_path, args.fig_path, image_name_root, n_chips, args):
|
||||
plotted += 1
|
||||
|
||||
if not file_paths:
|
||||
print(f"No files matching {base}*.mat in {directory}")
|
||||
prefixes = matching_dump_prefixes(directory)
|
||||
if not found_paths:
|
||||
for directory, base in searched_prefixes:
|
||||
print(f"No files matching {base}*.mat in {directory}")
|
||||
directories = sorted(
|
||||
{directory for directory, _ in searched_prefixes},
|
||||
key=str,
|
||||
)
|
||||
prefixes = []
|
||||
for directory in directories:
|
||||
prefixes.extend(matching_dump_prefixes(directory))
|
||||
prefixes = sorted(set(prefixes))
|
||||
if prefixes:
|
||||
print("Available acquisition dump prefixes:")
|
||||
for prefix in prefixes:
|
||||
|
||||
@@ -34,13 +34,27 @@ import numpy as np
|
||||
|
||||
from lib.dll_pll_veml_read_tracking_dump import dll_pll_veml_read_tracking_dump
|
||||
from lib.dump_filename import resolve_dump_prefix
|
||||
from lib.gnss_sdr_conf import (
|
||||
ChannelDump,
|
||||
ConfigError,
|
||||
SIGNAL_TYPES,
|
||||
add_conf_argument,
|
||||
load_gnss_sdr_conf,
|
||||
)
|
||||
from lib.plot_format import add_output_format_argument, apply_publication_style
|
||||
|
||||
DEFAULT_FILE_PREFIX = "track_ch"
|
||||
DEFAULT_SAMPLING_FREQUENCY = 4000000.0
|
||||
DEFAULT_CHANNELS = 5
|
||||
DEFAULT_FIRST_CHANNEL = 0
|
||||
DEFAULT_SIGNAL_TYPE = "1C"
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Plot tracking quality indicators from GNSS-SDR dumps."
|
||||
)
|
||||
add_conf_argument(parser)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--input-path",
|
||||
@@ -57,37 +71,42 @@ def parse_args():
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file-prefix",
|
||||
default="track_ch",
|
||||
help="GNSS-SDR Tracking.dump_filename value (default: track_ch). May "
|
||||
"include a directory and extension; the matching <prefix><channel>.dat "
|
||||
"files are read, resolved against --input-path.",
|
||||
default=None,
|
||||
help="GNSS-SDR Tracking.dump_filename value. May include a directory "
|
||||
"and extension; the matching <prefix><channel>.dat files are read, "
|
||||
"resolved against --input-path. Defaults to the configured Tracking "
|
||||
"dump filename or filenames from --conf, or track_ch.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sampling-frequency",
|
||||
type=float,
|
||||
default=4000000.0,
|
||||
help="Signal sampling frequency in Hz.",
|
||||
default=None,
|
||||
help="Signal sampling frequency in Hz. Defaults to "
|
||||
"GNSS-SDR.internal_fs_sps from --conf, or 4000000.0.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--channels",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Number of channels to read.",
|
||||
default=None,
|
||||
help="Number of channels to read. Defaults to all selected channels "
|
||||
"from --conf, or 5.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--first-channel",
|
||||
type=int,
|
||||
default=0,
|
||||
help="First channel number in the dump filenames.",
|
||||
default=None,
|
||||
help="First channel number in the dump filenames. Defaults to the "
|
||||
"first selected absolute channel from --conf, or 0.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--signal-type",
|
||||
type=str.upper,
|
||||
default="1C",
|
||||
choices=sorted(SIGNAL_TYPES),
|
||||
default=None,
|
||||
metavar="CODE",
|
||||
help="GNSS-SDR signal code (e.g. 1C, 5X, 7X, E6, J1) used as the "
|
||||
"C/N0 legend label. The tracking dump stores only the PRN, so the "
|
||||
"signal is supplied here (default: 1C).",
|
||||
help="GNSS-SDR signal code (e.g. 1C, 5X, 7X, E6, J1) used to restrict "
|
||||
"channel ranges and label traces. Omitted with a multi-signal --conf "
|
||||
"means all configured signals.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--style",
|
||||
@@ -109,24 +128,107 @@ def parse_args():
|
||||
help="Display figures interactively after saving them.",
|
||||
)
|
||||
add_output_format_argument(parser)
|
||||
return parser.parse_args()
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
apply_conf_defaults(args)
|
||||
except ConfigError as exc:
|
||||
parser.error(str(exc))
|
||||
return args
|
||||
|
||||
|
||||
def apply_conf_defaults(args):
|
||||
conf = load_gnss_sdr_conf(args.conf) if args.conf else None
|
||||
signals = []
|
||||
explicit_range = args.channels is not None or args.first_channel is not None
|
||||
if conf is not None:
|
||||
signals = conf.select_signals(args.signal_type)
|
||||
if args.signal_type is not None:
|
||||
args.signal_type = signals[0].signal
|
||||
elif args.signal_type is None:
|
||||
args.signal_type = DEFAULT_SIGNAL_TYPE
|
||||
|
||||
if args.file_prefix is None:
|
||||
if conf is None or (len(signals) == 1 and explicit_range):
|
||||
signal = signals[0] if signals else None
|
||||
args.file_prefix = (
|
||||
signal.tracking_dump_filename
|
||||
if signal is not None and signal.tracking_dump_filename
|
||||
else DEFAULT_FILE_PREFIX
|
||||
)
|
||||
|
||||
if args.sampling_frequency is None:
|
||||
if conf is not None:
|
||||
if conf.internal_fs_sps is None:
|
||||
raise ConfigError(
|
||||
"GNSS-SDR.internal_fs_sps is required to infer "
|
||||
"--sampling-frequency."
|
||||
)
|
||||
args.sampling_frequency = conf.internal_fs_sps
|
||||
else:
|
||||
args.sampling_frequency = DEFAULT_SAMPLING_FREQUENCY
|
||||
|
||||
if conf is not None and not explicit_range:
|
||||
args.channel_plan = conf.channel_dump_plan(
|
||||
signals=signals,
|
||||
dump_filename_attr="tracking_dump_filename",
|
||||
default_file_prefix=DEFAULT_FILE_PREFIX,
|
||||
override_file_prefix=args.file_prefix,
|
||||
)
|
||||
args.channels = len(args.channel_plan)
|
||||
args.first_channel = args.channel_plan[0].channel
|
||||
args.channel_ids = [entry.channel for entry in args.channel_plan]
|
||||
if len(signals) == 1:
|
||||
args.file_prefix = args.channel_plan[0].file_prefix
|
||||
return
|
||||
|
||||
if args.channels is None:
|
||||
args.channels = signals[0].count if signals else DEFAULT_CHANNELS
|
||||
|
||||
if args.first_channel is None:
|
||||
args.first_channel = (
|
||||
signals[0].first_channel if signals else DEFAULT_FIRST_CHANNEL
|
||||
)
|
||||
|
||||
if args.file_prefix is None:
|
||||
args.file_prefix = DEFAULT_FILE_PREFIX
|
||||
|
||||
manual_signal = args.signal_type or (
|
||||
signals[0].signal if signals else DEFAULT_SIGNAL_TYPE
|
||||
)
|
||||
args.channel_plan = [
|
||||
ChannelDump(
|
||||
channel=channel,
|
||||
signal=manual_signal,
|
||||
file_prefix=args.file_prefix,
|
||||
)
|
||||
for channel in range(args.first_channel, args.first_channel + args.channels)
|
||||
]
|
||||
args.channel_ids = [entry.channel for entry in args.channel_plan]
|
||||
|
||||
|
||||
def read_tracking_dumps(args):
|
||||
directory, base = resolve_dump_prefix(args.file_prefix, args.input_path)
|
||||
dumps = []
|
||||
for channel in range(args.first_channel, args.first_channel + args.channels):
|
||||
for entry in args.channel_plan:
|
||||
directory, base = resolve_dump_prefix(entry.file_prefix, args.input_path)
|
||||
channel = entry.channel
|
||||
tracking_log_path = directory / f"{base}{channel}.dat"
|
||||
dumps.append(dll_pll_veml_read_tracking_dump(tracking_log_path))
|
||||
tracking = dll_pll_veml_read_tracking_dump(tracking_log_path)
|
||||
tracking["_channel"] = channel
|
||||
tracking["_signal_type"] = entry.signal
|
||||
dumps.append(tracking)
|
||||
return dumps
|
||||
|
||||
|
||||
def prn_label(tracking):
|
||||
# Distinct PRNs the channel tracked, in order of appearance. A channel that
|
||||
# is reassigned mid-run (lost lock and re-acquired) tracks more than one
|
||||
# satellite;
|
||||
# satellite.
|
||||
prns = dict.fromkeys(int(p) for p in tracking["PRN"] if int(p))
|
||||
return "/".join(str(p) for p in prns)
|
||||
signal = tracking.get("_signal_type")
|
||||
prn_list = "/".join(str(p) for p in prns) or "unknown"
|
||||
if signal:
|
||||
return f"{signal} PRN {prn_list}"
|
||||
return prn_list
|
||||
|
||||
|
||||
def time_seconds(tracking, sampling_frequency):
|
||||
@@ -134,28 +236,38 @@ def time_seconds(tracking, sampling_frequency):
|
||||
|
||||
|
||||
def series_by_prn(tracking_list, key, sampling_frequency):
|
||||
# Merge each PRN's samples across all channels, sorted by time, so a
|
||||
# satellite handed between channels becomes one continuous series.
|
||||
# Assumes no two channels track the same PRN simultaneously
|
||||
# Merge each signal/PRN series across all channels, sorted by time, so a
|
||||
# satellite handed between channels becomes one continuous series. Assumes
|
||||
# no two channels track the same signal/PRN simultaneously.
|
||||
parts = {}
|
||||
for tracking in tracking_list:
|
||||
time_s = time_seconds(tracking, sampling_frequency)
|
||||
prn = np.asarray(tracking["PRN"])
|
||||
values = np.asarray(tracking[key], dtype=float)
|
||||
signal = tracking.get("_signal_type", "")
|
||||
for p in np.unique(prn):
|
||||
if not p:
|
||||
continue
|
||||
sel = prn == p
|
||||
parts.setdefault(int(p), []).append((time_s[sel], values[sel]))
|
||||
parts.setdefault((signal, int(p)), []).append(
|
||||
(time_s[sel], values[sel])
|
||||
)
|
||||
series = {}
|
||||
for p, chunks in parts.items():
|
||||
for key, chunks in parts.items():
|
||||
time_s = np.concatenate([c[0] for c in chunks])
|
||||
values = np.concatenate([c[1] for c in chunks])
|
||||
order = np.argsort(time_s, kind="stable")
|
||||
series[p] = (time_s[order], values[order])
|
||||
series[key] = (time_s[order], values[order])
|
||||
return series
|
||||
|
||||
|
||||
def satellite_title(series_key):
|
||||
signal, prn = series_key
|
||||
if signal:
|
||||
return f"{signal} PRN {prn}"
|
||||
return f"PRN {prn}"
|
||||
|
||||
|
||||
def plot_per_satellite(tracking_list, spec, args):
|
||||
series = series_by_prn(tracking_list, spec["key"], args.sampling_frequency)
|
||||
prns = sorted(series)
|
||||
@@ -175,7 +287,7 @@ def plot_per_satellite(tracking_list, spec, args):
|
||||
ax.set_visible(True)
|
||||
time_s, values = series[p]
|
||||
ax.plot(time_s, values, linewidth=0.8)
|
||||
ax.set_title(f"PRN {p}", fontsize=9)
|
||||
ax.set_title(satellite_title(p), fontsize=9)
|
||||
ax.grid(alpha=0.3)
|
||||
fig.suptitle(f"{spec['ylabel']} per satellite")
|
||||
fig.supxlabel("Time(s)")
|
||||
@@ -195,7 +307,7 @@ def plot_per_channel(tracking_list, spec, args):
|
||||
plt.plot(
|
||||
time_s,
|
||||
tracking[spec["key"]],
|
||||
label=f"{spec['legend_prefix']} {prn_label(tracking)}",
|
||||
label=spec["label"](tracking),
|
||||
)
|
||||
plt.xlabel("Time(s)")
|
||||
plt.ylabel(spec["ylabel"])
|
||||
@@ -217,13 +329,13 @@ def main():
|
||||
"key": "carrier_lock_test",
|
||||
"ylabel": "Carrier lock test",
|
||||
"stem": "carrier_lock_test",
|
||||
"legend_prefix": "SV",
|
||||
"label": lambda tracking: f"SV {prn_label(tracking)}",
|
||||
},
|
||||
{
|
||||
"key": "CN0_SNV_dB_Hz",
|
||||
"ylabel": "C/N0 (dB-Hz)",
|
||||
"stem": "CN0_SNV_dB_Hz",
|
||||
"legend_prefix": f"{args.signal_type} PRN",
|
||||
"label": lambda tracking: prn_label(tracking),
|
||||
},
|
||||
]
|
||||
for spec in specs:
|
||||
|
||||
Reference in New Issue
Block a user