Files
gnss-sdr/utils/python/dll_pll_veml_plot_sample.py
T
joebre a9c12954f3 python: fix signal-type label when overriding the plotted channel range
apply_conf_defaults() picked the plotted signal-type label as
signals[0].signal whenever --first-channel/--channels was passed
explicitly to override the conf-derived channel range. signals[0] is
just the first enabled signal in SIGNAL_ORDER for the given conf, so
it is only correct when the manually requested range happens to start
at channel 0 of that same signal; for any other range it mislabels
every plot with the wrong signal type, even though the correct .dat
files are read (since --file-prefix is typically also given
explicitly in that case).

For example, running
  --conf EVK1029_Galileo_E1_E5a.conf --file-prefix trk_GalileoE5a_ch \
  --first-channel 10 --channels 10
correctly reads the Galileo E5a tracking dumps but titled every plot
"Galileo E1B", the conf's first enabled signal.

Resolve each requested channel's signal type from the conf's own
per-signal channel ranges (SignalConfig.channels) instead, falling
back to the previous behavior only when no conf is available at all.

Signed-off-by: joebre <joel.brenner@saphyrion.ch>
2026-08-27 15:48:11 +02:00

337 lines
12 KiB
Python
Executable File

#!/usr/bin/env python3
"""
dll_pll_veml_plot_sample.py
Reads GNSS-SDR DLL/PLL VEML tracking dump binary files and plots internal
tracking variables.
File format:
{input_path}/{file_prefix}{channel}.dat
-----------------------------------------------------------------------------
GNSS-SDR is a Global Navigation Satellite System software-defined receiver.
This file is part of GNSS-SDR.
Copyright (C) 2022 (see AUTHORS file for a list of contributors)
SPDX-License-Identifier: GPL-3.0-or-later
-----------------------------------------------------------------------------
"""
import argparse
from pathlib import Path
import matplotlib.pyplot as plt
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,
signal_pretty_name,
)
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",
type=Path,
default=Path("."),
help="Directory containing tracking .dat dumps (default: .).",
)
parser.add_argument(
"-o",
"--fig-path",
type=Path,
default=Path("plots/dll-pll-veml-tracking"),
help="Directory where plots are saved.",
)
parser.add_argument(
"--file-prefix",
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=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=None,
help="Number of channels to read. Defaults to all selected channels "
"from --conf, or 5.",
)
parser.add_argument(
"--first-channel",
type=int,
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",
type=int,
default=0,
help="Only plot the last N outputs; 0 plots all outputs.",
)
parser.add_argument(
"--no-doppler",
dest="plot_doppler",
action="store_false",
help="Do not generate the extra Doppler-only plots.",
)
parser.add_argument(
"--show",
action="store_true",
help="Display figures interactively after saving them.",
)
add_output_format_argument(parser)
parser.set_defaults(plot_doppler=True)
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_fallback = args.signal_type or (
signals[0].signal if signals else DEFAULT_SIGNAL_TYPE
)
def signal_for_channel(channel):
# An explicit --first-channel/--channels range overrides the conf's
# auto-derived range, but the channel-to-signal mapping should still
# come from the conf when possible: signals[0] (first enabled signal
# in SIGNAL_ORDER) is not necessarily the signal actually occupying
# this channel index once the range has been overridden manually.
if args.signal_type is not None:
return args.signal_type
if conf is not None:
for signal in conf.signals:
if channel in signal.channels:
return signal.signal
return manual_signal_fallback
args.channel_plan = [
ChannelDump(
channel=channel,
signal=signal_for_channel(channel),
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):
dumps = []
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"
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
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)
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": 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,
}
for index, tracking in enumerate(gnss_tracking, start=1):
if 0 < args.plot_last_outputs < len(tracking["code_freq_hz"]):
start_sample = len(tracking["code_freq_hz"]) - args.plot_last_outputs
else:
start_sample = 0
track_result = {
"status": "T",
"codeFreq": np.copy(tracking["code_freq_hz"][start_sample:]),
"carrFreq": np.copy(tracking["carrier_doppler_hz"][start_sample:]),
"dllDiscr": np.copy(tracking["code_error"][start_sample:]),
"dllDiscrFilt": np.copy(tracking["code_nco"][start_sample:]),
"pllDiscr": np.copy(tracking["carr_error"][start_sample:]),
"pllDiscrFilt": np.copy(tracking["carr_nco"][start_sample:]),
"I_P": np.copy(tracking["P"][start_sample:]),
"Q_P": np.zeros(len(tracking["P"][start_sample:])),
"I_VE": np.copy(tracking["VE"][start_sample:]),
"I_E": np.copy(tracking["E"][start_sample:]),
"I_L": np.copy(tracking["L"][start_sample:]),
"I_VL": np.copy(tracking["VL"][start_sample:]),
"Q_VE": np.zeros(len(tracking["VE"][start_sample:])),
"Q_E": np.zeros(len(tracking["E"][start_sample:])),
"Q_L": np.zeros(len(tracking["L"][start_sample:])),
"Q_VL": np.zeros(len(tracking["VL"][start_sample:])),
"data_I": np.copy(tracking["prompt_I"][start_sample:]),
"data_Q": np.copy(tracking["prompt_Q"][start_sample:]),
"PRN": np.copy(tracking["PRN"][start_sample:]),
"CNo": np.copy(tracking["CN0_SNV_dB_Hz"][start_sample:]),
"prn_start_time_s": (
np.copy(tracking["PRN_start_sample"][start_sample:])
/ 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)
if args.plot_doppler:
channel = tracking["_channel"]
plt.figure()
plt.plot(
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 - {track_result['plotTitle']} "
f"(channel {channel})"
)
plt.savefig(
args.fig_path
/ f"Doppler_freq_ch_{channel}.{args.output_format}"
)
if not args.show:
plt.close()
# Show all saved figures with a single plt.show() to avoid the repeated
# show()/close() cycle that can crash interactive backends on macOS.
if args.show:
plt.show()
if __name__ == "__main__":
try:
main()
except OSError as exc:
raise SystemExit(f"Error: {exc}")