Files
gnss-sdr/utils/python/plot_acq_grid.py
T
Carles Fernandez fd274fb891 Make acquisition dump readers skip noise-reference columns
With set_doppler_num_bins(), acq_grid holds the Doppler candidate columns
followed by 0, 1 or 2 noise-reference columns placed at doppler_center
+/- the configured doppler_max, outside the linear Doppler axis. Nothing
in the dump said how many columns were candidates, so the readers either
rejected the file (Acquisition_Dump_Reader) or plotted the reference
columns on a wrong Doppler axis (plot_acq_grid.py, plot_acq_grid.m).

Write a new doppler_num_candidates variable to the dump and use it in
the readers to keep only the candidate columns. Dumps without it are
read as before, except that the reference column of older narrowed
dumps is also skipped. The Python and MATLAB scripts plot a
single-candidate (narrowed) grid as a code-delay line instead of an
empty surface.

Add a regression test that reads back a full-grid dump with two
reference columns and a narrowed N = 5 dump.
2026-09-23 12:46:37 +02:00

877 lines
28 KiB
Python
Executable File

#!/usr/bin/env python3
"""
plot_acq_grid.py
Reads GNSS-SDR Acquisition dump .mat files and plots acquisition grids.
Irene Perez Riega, 2023. iperrie@inta.es
Minhaj Uddin Ahmad, 2026. mahmad12@crimson.ua.edu
File format:
{path}/{file_prefix}_{system}_{signal}_ch_{channel}_{execution}_sat_{sat}.mat
-----------------------------------------------------------------------------
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 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,
signal_pretty_name,
)
from lib.plot_format import add_output_format_argument, apply_publication_style
# Heavy numerical and plotting libraries (h5py, numpy, matplotlib, scipy) are
# imported lazily inside the functions that need them. This keeps --help and
# argument-error invocations fast and free of matplotlib font-cache warnings.
DEFAULT_FILE_PREFIX = "acquisition"
DEFAULT_SAT = 1
DEFAULT_CHANNEL = 0
DEFAULT_EXECUTION = 1
DEFAULT_SIGNAL_TYPE = "1C"
MAX_LITE_SURFACE_CODE_POINTS = 3200
MAX_LITE_SURFACE_DOPPLER_POINTS = 250
@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",
type=Path,
default=Path("."),
help="Directory containing acquisition .mat dumps (default: .).",
)
parser.add_argument(
"-o",
"--fig-path",
type=Path,
default=Path("plots/acquisition"),
help="Directory where plots are saved (default: ./plots/acquisition).",
)
parser.add_argument(
"--file-prefix",
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. Defaults to the configured "
"Acquisition dump filename or filenames from --conf, or acquisition.",
)
parser.add_argument(
"--output-basename",
"--output-prefix",
dest="output_basename",
default=None,
help="Base name used for saved plot files (default: the --file-prefix "
"basename, without directory or extension).",
)
mode = parser.add_mutually_exclusive_group()
mode.add_argument(
"--all-files",
dest="plot_all_files",
action="store_true",
help="Plot all matching dump files (default).",
)
mode.add_argument(
"--single-file",
dest="plot_all_files",
action="store_false",
help="Plot one dump selected by --sat, --channel, --execution, and "
"--signal-type.",
)
parser.set_defaults(plot_all_files=True)
acq_filter = parser.add_mutually_exclusive_group()
acq_filter.add_argument(
"--positive-only",
dest="plot_positive_acqs",
action="store_true",
help="Only plot positive acquisitions (default).",
)
acq_filter.add_argument(
"--include-negative",
dest="plot_positive_acqs",
action="store_false",
help="Plot positive and negative acquisition dumps.",
)
parser.set_defaults(plot_positive_acqs=True)
view = parser.add_mutually_exclusive_group()
view.add_argument(
"--lite-view",
dest="lite_view",
action="store_true",
help="Use interpolated light grid representation (default).",
)
view.add_argument(
"--full-view",
dest="lite_view",
action="store_false",
help="Plot the raw acquisition grid.",
)
parser.set_defaults(lite_view=True)
parser.add_argument(
"--sat",
type=int,
default=None,
help="Satellite PRN. Filters all-files mode; defaults to 1 in "
"single-file mode.",
)
parser.add_argument(
"--channel",
type=int,
default=None,
help="Acquisition channel number. Filters all-files mode; defaults "
"to 0 in single-file mode.",
)
parser.add_argument(
"--execution",
type=int,
default=None,
help="Acquisition dump execution. Filters all-files mode; defaults "
"to 1 in single-file mode.",
)
parser.add_argument(
"--signal-type",
type=str.upper,
choices=sorted(SIGNAL_TYPES),
default=None,
metavar="CODE",
help="GNSS-SDR signal code (e.g. 1C, 5X, 7X, E6, J1). Filters "
"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",
type=int,
default=3,
help="Samples per chip used by the light grid interpolation.",
)
parser.add_argument(
"--samples-per-doppler-step",
type=int,
default=5,
help="Interpolated samples per Doppler step used by the light grid "
"interpolation.",
)
parser.add_argument(
"--surface-smoothing",
type=float,
default=2.0,
help="Gaussian smoothing sigma, in interpolated surface samples, "
"applied only to the lite 3D view. Use 0 to disable.",
)
parser.add_argument(
"--samples-per-code",
type=int,
default=25000,
help="Samples per code used for the 2D normalization.",
)
parser.add_argument(
"--input-power",
type=float,
default=100.0,
help="Input power used for the 2D normalization.",
)
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))
if args.output_basename is None:
# Default the saved-plot basename to the dump basename, dropping any
# 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 dump_scalar_value(data, key, default=None):
import numpy as np
value = data.get(key)
if value is None:
return default
return np.asarray(value[()]).reshape(-1)[0].item()
def positive_acq_value(data):
for key in ("positive_acq", "d_positive_acq"):
value = dump_scalar_value(data, key)
if value is not None:
return value
return None
def parse_dump_name(file_path):
parts = file_path.stem.split("_")
if len(parts) < 8:
return None
try:
if parts[-5] != "ch" or parts[-2] != "sat":
return None
return {
"file_prefix": "_".join(parts[:-7]),
"system": parts[-7],
"signal": parts[-6],
"channel": int(parts[-4]),
"execution": int(parts[-3]),
"sat": int(parts[-1]),
}
except ValueError:
return None
def matching_dump_prefixes(input_path):
prefixes = set()
for file_path in input_path.glob("*.mat"):
metadata = parse_dump_name(file_path)
if metadata is not None:
prefixes.add(metadata["file_prefix"])
return sorted(prefixes)
def configured_doppler_axis(
doppler_max,
doppler_step,
n_dop_bins,
file_path,
doppler_center=0.0,
doppler_narrowed=False,
exact=False,
):
import numpy as np
doppler_max = abs(float(doppler_max))
doppler_step = abs(float(doppler_step))
doppler_center = float(doppler_center)
if doppler_step == 0:
raise ValueError(f"{file_path}: doppler_step must be non-zero")
if exact or doppler_narrowed:
# Candidate column k decodes to
# doppler_center - doppler_max + k * doppler_step, the same rule
# pcps_acquisition uses. This is exact for dumps that store
# doppler_num_candidates, and for legacy narrowed dumps (a single
# candidate at doppler_center, stored with doppler_max = 0).
return (
doppler_center
- doppler_max
+ np.arange(n_dop_bins) * doppler_step
)
freq = doppler_center + np.arange(
-doppler_max,
doppler_max + doppler_step / 2,
doppler_step,
)
if len(freq) == n_dop_bins:
return freq
legacy_freq = (
doppler_center
+ np.arange(n_dop_bins) * doppler_step
- doppler_max
)
if len(freq) - 1 == n_dop_bins:
print(
f"Warning: {file_path.name}: acq_grid has {n_dop_bins} Doppler "
f"bins, one fewer than the inclusive configured axis from "
f"{doppler_center - doppler_max:g} Hz to "
f"{doppler_center + doppler_max:g} Hz in "
f"{doppler_step:g} Hz steps. Plotting the available bins from "
f"{legacy_freq[0]:g} Hz to {legacy_freq[-1]:g} Hz."
)
return legacy_freq
print(
f"Warning: {file_path.name}: configured Doppler axis has {len(freq)} "
f"bins from {doppler_center - doppler_max:g} Hz to "
f"{doppler_center + doppler_max:g} Hz in "
f"{doppler_step:g} Hz steps, but acq_grid has {n_dop_bins} columns. "
"Plotting the available columns over the configured Doppler range."
)
return np.linspace(
doppler_center - doppler_max,
doppler_center + doppler_max,
n_dop_bins,
)
def expected_doppler_bin_counts(doppler_max, doppler_step):
import numpy as np
doppler_max = abs(float(doppler_max))
doppler_step = abs(float(doppler_step))
if doppler_step == 0:
return set()
inclusive_count = len(
np.arange(-doppler_max, doppler_max + doppler_step / 2, doppler_step)
)
return {inclusive_count, max(1, inclusive_count - 1)}
def read_acquisition_dump(file_path, n_chips, positive_only):
import h5py
import numpy as np
with h5py.File(file_path, "r") as data:
positive_acq = positive_acq_value(data)
if positive_only:
if positive_acq is None:
print(f"Skipping {file_path.name}: no positive_acq variable")
return None
if positive_acq != 1:
return None
doppler_step = dump_scalar_value(data, "doppler_step")
doppler_max = dump_scalar_value(data, "doppler_max")
doppler_center = dump_scalar_value(data, "doppler_center", 0.0)
doppler_narrowed = bool(
dump_scalar_value(data, "doppler_narrowed", 0)
)
# Dumps that store doppler_num_candidates may carry up to two
# trailing noise-reference columns after the candidate columns.
# Older narrowed dumps have exactly one candidate plus one reference
# column; older full-grid dumps have no reference columns.
doppler_num_candidates = dump_scalar_value(
data, "doppler_num_candidates"
)
acq_grid = data["acq_grid"][:]
if doppler_num_candidates is not None:
doppler_num_candidates = int(doppler_num_candidates)
expected_doppler_bins = set(
range(doppler_num_candidates, doppler_num_candidates + 3)
)
elif doppler_narrowed:
expected_doppler_bins = {2}
else:
expected_doppler_bins = expected_doppler_bin_counts(
doppler_max, doppler_step
)
if (
acq_grid.shape[1] not in expected_doppler_bins
and acq_grid.shape[0] in expected_doppler_bins
):
acq_grid = acq_grid.T
n_fft, n_columns = acq_grid.shape
if doppler_num_candidates is not None:
n_dop_bins = max(1, min(doppler_num_candidates, n_columns))
elif doppler_narrowed:
n_dop_bins = 1
else:
n_dop_bins = n_columns
if n_dop_bins < n_columns:
# Noise-reference columns are not on the Doppler axis and are
# never acquisition candidates: leave them out of the plots.
acq_grid = acq_grid[:, :n_dop_bins]
freq = configured_doppler_axis(
doppler_max,
doppler_step,
n_dop_bins,
file_path,
doppler_center,
doppler_narrowed,
exact=doppler_num_candidates is not None,
)
d_max, f_max = np.unravel_index(np.argmax(acq_grid), acq_grid.shape)
delay = np.arange(n_fft) / n_fft * n_chips
return acq_grid, n_fft, d_max, f_max, doppler_step, doppler_max, freq, delay
def acquisition_plot_title(metadata):
signal_name = signal_pretty_name(metadata["signal"])
return (
f"{signal_name} PRN {metadata['sat']} "
f"(channel {metadata['channel']}, execution {metadata['execution']})"
)
def max_pooled_axis(axis_values, grid, n_points, axis):
# Block-max decimation: each output bin takes the maximum of its block of
# input samples. A correlation peak only a couple of samples wide survives
# this by construction, whereas sampling a spline at sparse points (or
# letting plot_surface decimate) can fall between the peak samples and
# miss it entirely.
import numpy as np
edges = np.linspace(0, len(axis_values), n_points + 1, dtype=int)
pooled_grid = np.maximum.reduceat(grid, edges[:-1], axis=axis)
pooled_axis = (axis_values[edges[:-1]] + axis_values[edges[1:] - 1]) / 2
return pooled_axis, pooled_grid
def interpolated_acquisition_grid(delay, freq, acq_grid, args, n_chips):
import numpy as np
from scipy.ndimage import gaussian_filter
from scipy.interpolate import RectBivariateSpline
if len(delay) < 2 or len(freq) < 2:
return delay, freq, acq_grid
samples_per_chip = max(1, args.samples_per_chip)
samples_per_doppler_step = max(1, args.samples_per_doppler_step)
delay_points = min(
samples_per_chip * n_chips,
MAX_LITE_SURFACE_CODE_POINTS,
)
freq_points = min(
(len(freq) - 1) * samples_per_doppler_step + 1,
MAX_LITE_SURFACE_DOPPLER_POINTS,
)
pooled_delay = len(delay) > delay_points
if pooled_delay:
delay, acq_grid = max_pooled_axis(delay, acq_grid, delay_points, 0)
pooled_freq = len(freq) > freq_points
if pooled_freq:
freq, acq_grid = max_pooled_axis(freq, acq_grid, freq_points, 1)
delay_interp = (
delay
if pooled_delay
else np.linspace(delay[0], delay[-1], max(2, delay_points))
)
freq_interp = (
freq
if pooled_freq
else np.linspace(freq[0], freq[-1], max(2, freq_points))
)
if pooled_delay and pooled_freq:
grid_interp = acq_grid
else:
kx = min(3, len(delay) - 1)
ky = min(3, len(freq) - 1)
spline = RectBivariateSpline(delay, freq, acq_grid, kx=kx, ky=ky)
grid_interp = spline(delay_interp, freq_interp)
if args.surface_smoothing > 0:
# Smoothing a pooled axis would flatten the peak it just preserved,
# so only smooth along axes that were spline-upsampled.
sigma = (
0.0 if pooled_delay else args.surface_smoothing,
0.0 if pooled_freq else args.surface_smoothing,
)
if any(sigma):
grid_interp = gaussian_filter(
grid_interp,
sigma=sigma,
mode="nearest",
)
return delay_interp, freq_interp, grid_interp
def plot_dump(file_path, fig_path, image_name_root, n_chips, args, metadata=None):
import matplotlib.pyplot as plt
import numpy as np
dump = read_acquisition_dump(file_path, n_chips, args.plot_positive_acqs)
if dump is None:
return False
(
acq_grid,
n_fft,
d_max,
f_max,
doppler_step,
doppler_max,
freq,
delay,
) = dump
if metadata is None:
metadata = parse_dump_name(file_path)
plot_title = acquisition_plot_title(metadata) if metadata else str(file_path)
# A narrowed search can have a single candidate Doppler bin: no surface
# can be drawn over one Doppler value, so draw it as a code-delay line.
single_doppler_bin = len(freq) < 2
if single_doppler_bin:
half_step = max(abs(float(doppler_step)), 1.0) / 2
freq_limits = [freq[0] - half_step, freq[0] + half_step]
else:
freq_limits = [min(freq), max(freq)]
fig = plt.figure()
plt.gcf().canvas.manager.set_window_title(str(file_path))
if single_doppler_bin:
ax = fig.add_subplot(111, projection="3d")
ax.plot(
np.full(len(delay), freq[0]),
delay,
acq_grid[:, 0],
linewidth=0.8,
)
ax.set_ylim([min(delay), max(delay)])
elif not args.lite_view:
ax = fig.add_subplot(111, projection="3d")
x_axis, y_axis = np.meshgrid(freq, delay, indexing="xy")
# rcount/ccount default to 50, which would silently decimate large
# grids and hide the correlation peak; render every sample instead.
ax.plot_surface(
x_axis,
y_axis,
acq_grid,
cmap="viridis",
rcount=acq_grid.shape[0],
ccount=acq_grid.shape[1],
linewidth=0,
edgecolor="none",
antialiased=False,
)
ax.set_ylim([min(delay), max(delay)])
else:
delay_interp, freq_interp, grid_interp = interpolated_acquisition_grid(
delay,
freq,
acq_grid,
args,
n_chips,
)
ax = fig.add_subplot(111, projection="3d")
x_axis, y_axis = np.meshgrid(freq_interp, delay_interp, indexing="xy")
ax.plot_surface(
x_axis,
y_axis,
grid_interp,
cmap="inferno",
rstride=1,
cstride=1,
linewidth=0,
edgecolor="none",
antialiased=False,
)
ax.set_ylim([min(delay_interp), max(delay_interp)])
peak_doppler = freq[f_max]
peak_delay = delay[d_max]
peak_value = acq_grid[d_max, f_max]
ax.scatter(
[peak_doppler],
[peak_delay],
[peak_value],
color="red",
s=30,
label="Peak",
)
ax.set_xlabel("Doppler shift (Hz)")
ax.set_xlim(freq_limits)
ax.set_ylabel("Code delay (chips)")
ax.set_zlabel("Test Statistics")
ax.set_title(plot_title)
ax.legend(loc="upper right")
plt.tight_layout()
plt.savefig(fig_path / f"{image_name_root}_3D.{args.output_format}")
if not args.show:
plt.close(fig)
fig2, axes = plt.subplots(2, 1, figsize=(8, 6))
plt.gcf().canvas.manager.set_window_title(str(file_path))
fig2.suptitle(plot_title)
axes[0].plot(
freq,
acq_grid[d_max, :],
marker="o" if single_doppler_bin else None,
)
axes[0].axvline(peak_doppler, color="red", linestyle="--", linewidth=0.8)
axes[0].set_xlim(freq_limits)
axes[0].set_xlabel("Doppler shift (Hz)")
axes[0].set_ylabel("Test statistics")
axes[0].set_title(f"Doppler cut at code delay = {peak_delay:g} chips")
normalization = (args.samples_per_code ** 4) * args.input_power
axes[1].plot(delay, acq_grid[:, f_max] / normalization)
axes[1].axvline(peak_delay, color="red", linestyle="--", linewidth=0.8)
axes[1].set_xlim([min(delay), max(delay)])
axes[1].set_xlabel("Code delay (chips)")
axes[1].set_ylabel("Test statistics")
axes[1].set_title(
f"Code-delay cut at Doppler wipe-off = {peak_doppler:g} Hz"
)
fig2.tight_layout(rect=(0, 0, 1, 0.95))
plt.savefig(fig_path / f"{image_name_root}_2D.{args.output_format}")
if not args.show:
plt.close(fig2)
return True
def single_file_path(args):
sat = args.sat if args.sat is not None else DEFAULT_SAT
channel = args.channel if args.channel is not None else DEFAULT_CHANNEL
execution = (
args.execution if args.execution is not None else DEFAULT_EXECUTION
)
signal_type = (
args.signal_type
if args.signal_type is not None
else DEFAULT_SIGNAL_TYPE
)
signal = signal_type
system, n_chips = SIGNAL_TYPES[signal_type]
directory, base = resolve_dump_prefix(args.file_prefix, args.input_path)
filename = (
f"{base}_{system}_{signal}_ch_{channel}_"
f"{execution}_sat_{sat}.mat"
)
metadata = {
"system": system,
"signal": signal,
"channel": channel,
"execution": execution,
"sat": sat,
}
image_name_root = image_name_from_metadata(args.output_basename, metadata)
return directory / filename, image_name_root, n_chips, metadata
def image_name_from_metadata(output_basename, metadata):
prefix = f"{output_basename}_" if output_basename else ""
return (
f"{prefix}{metadata['system']}_{metadata['signal']}_ch_"
f"{metadata['channel']}_{metadata['execution']}_sat_{metadata['sat']}"
)
def metadata_matches_filters(metadata, args):
if args.sat is not None and metadata["sat"] != args.sat:
return False
if args.channel is not None and metadata["channel"] != args.channel:
return False
if args.execution is not None and metadata["execution"] != args.execution:
return False
if args.signal_type is not None:
system, _ = SIGNAL_TYPES[args.signal_type]
if metadata["system"] != system or metadata["signal"] != args.signal_type:
return False
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
# and avoids closing figures afterwards, which prevents the segfault some
# macOS matplotlib backends hit on repeated show()/close() cycles. The
# figures stay open until the user closes all windows, then the process
# exits and the interpreter tears matplotlib down cleanly.
if not args.show:
return
import matplotlib.pyplot as plt
plt.show()
def main():
args = parse_args()
args.fig_path.mkdir(parents=True, exist_ok=True)
apply_publication_style()
if not args.plot_all_files:
file_path, image_name_root, n_chips, metadata = single_file_path(args)
if not file_path.exists():
raise FileNotFoundError(f"dump file not found: {file_path}")
plot_dump(
file_path,
args.fig_path,
image_name_root,
n_chips,
args,
metadata,
)
show_figures(args)
return
plotted = 0
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
metadata = parse_dump_name(file_path)
if metadata is None:
print(f"Skipping {file_path.name}: unexpected filename format")
continue
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,
)
if plot_dump(
file_path,
args.fig_path,
image_name_root,
n_chips,
args,
metadata,
):
plotted += 1
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:
print(f" {prefix}")
print(
"Use --file-prefix to select the input dump prefix. "
"--output-basename only changes saved plot filenames."
)
elif plotted == 0:
print("No acquisition dumps were plotted.")
show_figures(args)
if __name__ == "__main__":
try:
main()
except OSError as exc:
raise SystemExit(f"Error: {exc}")