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/hybrid_observables_plot_sample.py
T

231 lines
7.4 KiB
Python
Executable File

#!/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 <prefix>.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_<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):
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}")