1
0
mirror of https://github.com/gnss-sdr/gnss-sdr synced 2026-07-02 09:28:50 +00:00
Files
gnss-sdr/utils/python/dll_pll_veml_plot_sample.py
T
2026-06-26 08:59:15 +02:00

322 lines
11 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 = 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):
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}")