#!/usr/bin/env python3 """ hybrid_observables_plot_sample.py Reads a GNSS-SDR hybrid observables raw dump and plots pseudorange, carrier phase, Doppler, and PRN values. ----------------------------------------------------------------------------- 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.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", type=Path, default=Path("."), help="Directory containing the observables dump (default: .).", ) parser.add_argument( "--file-prefix", default=None, help="GNSS-SDR Observables.dump_filename value. May include a " "directory and extension; the matching .dat file is read, " "resolved against --input-path. Defaults to Observables.dump_filename " "from --conf, or observables.dat.", ) parser.add_argument( "-o", "--fig-path", type=Path, default=Path("plots/hybrid-observables"), help="Directory where plots are saved.", ) parser.add_argument( "--channels", type=int, 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", action="store_true", help="Display figures interactively after saving them.", ) add_output_format_argument(parser) 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_.count value is required " "to infer --channels." ) args.channels = conf.total_channels else: args.channels = DEFAULT_CHANNELS def first_valid_observable(gnss_observables, channels): min_tow_idx = None obs_idx = 0 for channel in range(channels): valid_indices = np.where(np.array(gnss_observables["valid"][channel]) > 0)[0] if len(valid_indices) == 0: continue idx = valid_indices[0] if min_tow_idx is None or idx < min_tow_idx: min_tow_idx = idx obs_idx = channel if min_tow_idx is None: raise ValueError("No valid observables found in the dump.") return min_tow_idx, obs_idx def save_figure(fig_path, name, show, output_format): plt.tight_layout() plt.savefig(fig_path / f"{name}.{output_format}") # Close unless it will be shown; main() triggers a single plt.show() at the # end. Avoids repeated show()/close() cycles, which can crash interactive # matplotlib backends (e.g. macOS) on window close. if not show: plt.close() def main(): args = parse_args() args.fig_path.mkdir(parents=True, exist_ok=True) apply_publication_style() directory, base = resolve_dump_prefix(args.file_prefix, args.input_path) observables_file = directory / f"{base}.dat" gnss_observables = read_hybrid_observables_dump(args.channels, observables_file) min_tow_idx, obs_idx = first_valid_observable(gnss_observables, args.channels) time_start = gnss_observables["RX_time"][obs_idx][min_tow_idx] - 100 time_end = gnss_observables["RX_time"][obs_idx][-1] + 100 plt.figure() plt.title("Pseudorange") for channel in range(args.channels): plt.scatter( gnss_observables["RX_time"][channel][min_tow_idx:], gnss_observables["Pseudorange_m"][channel][min_tow_idx:], s=1, label=f"Channel {channel}", ) plt.xlim(time_start, time_end) plt.grid(True) plt.xlabel("TOW [s]") plt.ylabel("Pseudorange [m]") plt.legend() plt.gcf().canvas.manager.set_window_title("Pseudorange.png") save_figure(args.fig_path, "Pseudorange", args.show, args.output_format) plt.figure() plt.title("Carrier Phase") for channel in range(args.channels): plt.scatter( gnss_observables["RX_time"][channel][min_tow_idx:], gnss_observables["Carrier_phase_hz"][channel][min_tow_idx:], s=1, label=f"Channel {channel}", ) plt.xlim(time_start, time_end) plt.xlabel("TOW [s]") plt.ylabel("Accumulated Carrier Phase [cycles]") plt.grid(True) plt.legend() plt.gcf().canvas.manager.set_window_title("AccumulatedCarrierPhase.png") save_figure(args.fig_path, "AccumulatedCarrierPhase", args.show, args.output_format) plt.figure() plt.title("Doppler Effect") for channel in range(args.channels): plt.scatter( gnss_observables["RX_time"][channel][min_tow_idx:], gnss_observables["Carrier_Doppler_hz"][channel][min_tow_idx:], s=1, label=f"Channel {channel}", ) plt.xlim(time_start, time_end) plt.xlabel("TOW [s]") plt.ylabel("Doppler Frequency [Hz]") plt.grid(True) plt.legend() plt.gcf().canvas.manager.set_window_title("DopplerFrequency.png") save_figure(args.fig_path, "DopplerFrequency", args.show, args.output_format) plt.figure() plt.title("GNSS Channels captured") for channel in range(args.channels): label = "unknown" for prn in gnss_observables["PRN"][channel][min_tow_idx:]: if int(prn) != 0: label = str(int(prn)) break plt.scatter( gnss_observables["RX_time"][channel][min_tow_idx:], gnss_observables["PRN"][channel][min_tow_idx:], s=1, label=f"PRN {channel} = {label}", ) plt.xlim(time_start, time_end) plt.xlabel("TOW [s]") plt.ylabel("PRN") plt.grid(True) plt.legend() plt.gcf().canvas.manager.set_window_title("PRNs.png") save_figure(args.fig_path, "PRNs", args.show, args.output_format) # 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, ValueError) as exc: raise SystemExit(f"Error: {exc}")