Compare commits

..

2 Commits

Author SHA1 Message Date
e5a3d327e5 refactor: unify signal viewer styling and update docs screenshots
Some checks failed
Test with tox / Test with tox (3.11) (pull_request) Successful in 3m37s
Test with tox / Test with tox (3.12) (pull_request) Successful in 3m44s
Build Project / Build Project (3.10) (pull_request) Successful in 5m55s
Build Project / Build Project (3.11) (pull_request) Successful in 5m35s
Build Project / Build Project (3.12) (pull_request) Successful in 6m27s
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 6m22s
Test with tox / Test with tox (3.10) (pull_request) Failing after 5m33s
- Align view_simple and view_full on background colour (#161616), title
  size (25pt), subtitle size (15pt), base font/tick/label sizes, grid
  style (alpha=0.2), and legend fontsize (10pt)
- Spectrogram placed above IQ plot in view_simple; subplot renamed from
  "Time Series" to "IQ Sample Plot"
- Frequency and spectrogram Y-axes formatted in MHz across both viewers
- Added xlabel/ylabel, subtle grids, and IQ legend to view_full subplots
- Fixed spectrogram right-side clipping in view_simple by syncing xlim
  from specgram output rather than total signal duration
- Updated getting_started.rst to reference both simple and full viewer
  screenshots; replaced doc images with latest renders
2026-04-28 14:08:44 -04:00
4c94f6ae94 Changed datasets to data to match utils 2026-04-28 12:49:43 -04:00
6 changed files with 90 additions and 55 deletions

View File

@ -414,12 +414,18 @@ Device selection (``--device``) is optional if only one device is detected. Exac
ria view capture.npy --show --no-save ria view capture.npy --show --no-save
ria view old.npy --legacy --type simple ria view old.npy --legacy --type simple
ria view recordings\qam64_35.npy --type simple ria view recordings\qam64_35.npy --type simple
ria view recordings\qam64_35.npy --type full
.. figure:: ../images/qam64_35.png .. figure:: ../images/recordings/qam64_35.png
:alt: Example output of ria view recordings\qam64_35.npy --type simple :alt: Example output of ria view recordings\qam64_35.npy --type simple
Output of ``ria view recordings\qam64_35.npy --type simple`` Output of ``ria view recordings\qam64_35.npy --type simple``
.. figure:: ../images/recordings/qam64_35-full.png
:alt: Example output of ria view recordings\qam64_35.npy --type full
Output of ``ria view recordings\qam64_35.npy --type full``
.. _cmd-annotate: .. _cmd-annotate:

View File

@ -1,5 +1,5 @@
Datatypes Package (ria_toolkit_oss.data) Data Package (ria_toolkit_oss.data)
============================================= =======================================
.. |br| raw:: html .. |br| raw:: html

View File

@ -3,11 +3,12 @@ import os
import textwrap import textwrap
from typing import Optional from typing import Optional
import matplotlib
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
from matplotlib import gridspec from matplotlib import gridspec, ticker
from matplotlib.patches import Patch from matplotlib.patches import Patch
from PIL import Image from PIL import Image, UnidentifiedImageError
from scipy.fft import fft, fftshift from scipy.fft import fft, fftshift
from scipy.signal import spectrogram from scipy.signal import spectrogram
from scipy.signal.windows import hann from scipy.signal.windows import hann
@ -185,7 +186,7 @@ def view_sig(
logo: Optional[bool] = True, logo: Optional[bool] = True,
dark: Optional[bool] = True, dark: Optional[bool] = True,
spines: Optional[bool] = False, spines: Optional[bool] = False,
title_fontsize: Optional[int] = 35, title_fontsize: Optional[int] = 25,
subtitle_fontsize: Optional[int] = 15, subtitle_fontsize: Optional[int] = 15,
) -> None: ) -> None:
""" """
@ -230,11 +231,24 @@ def view_sig(
complex_signal = recording.data[0] complex_signal = recording.data[0]
sample_rate, center_frequency, _ = extract_metadata_fields(recording.metadata) sample_rate, center_frequency, _ = extract_metadata_fields(recording.metadata)
subplot_height = 2 * (plot_spectrogram + iq + frequency) + 3 * (constellation or metadata or logo) subplot_height = 3 * (plot_spectrogram) + 2 * (iq + frequency) + 3 * (constellation or metadata or logo)
subplot_width = max((constellation + metadata or 1), logo * 3) subplot_width = max((constellation + metadata or 1), logo * 3)
if dark: if dark:
plt.style.use("dark_background") plt.style.use("dark_background")
matplotlib.rcParams.update({
"figure.facecolor": "#161616",
"axes.facecolor": "#161616",
"savefig.facecolor": "#161616",
"savefig.edgecolor": "#161616",
"font.size": 10,
"axes.titlesize": 15,
"axes.labelsize": 10,
"xtick.labelsize": 10,
"ytick.labelsize": 10,
"legend.frameon": False,
"legend.facecolor": "none",
})
logo_path = os.path.dirname(__file__) + "/graphics/Qoherent-logo-white-transparent.png" logo_path = os.path.dirname(__file__) + "/graphics/Qoherent-logo-white-transparent.png"
else: else:
plt.style.use("default") plt.style.use("default")
@ -252,8 +266,8 @@ def view_sig(
plot_x_indx = 0 plot_x_indx = 0
if plot_spectrogram: if plot_spectrogram:
spec_ax = plt.subplot(gs[plot_y_indx : plot_y_indx + 2, :]) spec_ax = plt.subplot(gs[plot_y_indx : plot_y_indx + 3, :])
plot_y_indx = plot_y_indx + 2 plot_y_indx = plot_y_indx + 3
fft_size = get_fft_size(plot_length=plot_length) fft_size = get_fft_size(plot_length=plot_length)
_, t_spec, Sxx = spectrogram( _, t_spec, Sxx = spectrogram(
@ -280,7 +294,12 @@ def view_sig(
) )
set_spines(spec_ax, spines) set_spines(spec_ax, spines)
spec_ax.set_title("Spectrogram", loc="center", fontsize=subtitle_fontsize) spec_ax.set_title("Spectrogram", loc="left", fontsize=subtitle_fontsize)
spec_ax.set_xlabel("Time (s)")
spec_ax.set_ylabel("Frequency (MHz)")
spec_ax.yaxis.set_major_formatter(
ticker.FuncFormatter(lambda x, _: f"{x / 1e6:.1f}")
)
if iq: if iq:
iq_ax = plt.subplot(gs[plot_y_indx : plot_y_indx + 2, :]) iq_ax = plt.subplot(gs[plot_y_indx : plot_y_indx + 2, :])
@ -291,12 +310,13 @@ def view_sig(
iq_ax.plot(t, plot_iq.real, color=COLORS["purple"], linewidth=0.6, alpha=0.8, label="I") iq_ax.plot(t, plot_iq.real, color=COLORS["purple"], linewidth=0.6, alpha=0.8, label="I")
iq_ax.plot(t, plot_iq.imag, color=COLORS["magenta"], linewidth=0.6, alpha=0.8, label="Q") iq_ax.plot(t, plot_iq.imag, color=COLORS["magenta"], linewidth=0.6, alpha=0.8, label="Q")
iq_ax.grid(False) iq_ax.grid(True, alpha=0.2, linewidth=0.5)
iq_ax.set_ylabel("Amplitude") iq_ax.set_ylabel("Amplitude")
iq_ax.set_xlim([min(t), max(t)]) iq_ax.set_xlim([min(t), max(t)])
iq_ax.set_xlabel("Time (s)") iq_ax.set_xlabel("Time (s)")
iq_ax.set_title("IQ Sample Plot", fontsize=subtitle_fontsize) iq_ax.set_title("IQ Sample Plot", loc="left", fontsize=subtitle_fontsize)
iq_ax.legend(loc="upper right", fontsize=10)
set_spines(iq_ax, spines) set_spines(iq_ax, spines)
if frequency: if frequency:
@ -310,10 +330,12 @@ def view_sig(
# Convert to dB # Convert to dB
spectrum_db = 20 * np.log10(spectrum + 1e-12) # 20*log for magnitude spectrum_db = 20 * np.log10(spectrum + 1e-12) # 20*log for magnitude
freqs = np.linspace(-sample_rate / 2, sample_rate / 2, len(complex_signal[:plot_length])) + center_frequency freqs = (np.linspace(-sample_rate / 2, sample_rate / 2, len(complex_signal[:plot_length])) + center_frequency) / 1e6
freq_ax.plot(freqs, spectrum_db, color=COLORS["accent"], linewidth=0.8) freq_ax.plot(freqs, spectrum_db, color=COLORS["accent"], linewidth=0.8)
freq_ax.set_xlabel("Frequency (MHz)")
freq_ax.set_ylabel("Magnitude (dB)") freq_ax.set_ylabel("Magnitude (dB)")
freq_ax.set_title("Frequency Spectrum (Windowed FFT)", fontsize=subtitle_fontsize) freq_ax.grid(True, alpha=0.2, linewidth=0.5)
freq_ax.set_title("Frequency Spectrum (Windowed FFT)", loc="left", fontsize=subtitle_fontsize)
set_spines(freq_ax, spines) set_spines(freq_ax, spines)
if constellation: if constellation:
@ -326,7 +348,7 @@ def view_sig(
const_ax.set_ylim([-1 * dimension, dimension]) const_ax.set_ylim([-1 * dimension, dimension])
const_ax.set_xlabel("In-phase (I)") const_ax.set_xlabel("In-phase (I)")
const_ax.set_ylabel("Quadrature (Q)") const_ax.set_ylabel("Quadrature (Q)")
const_ax.set_title("Constellation", fontsize=subtitle_fontsize) const_ax.set_title("Constellation", loc="left", fontsize=subtitle_fontsize)
const_ax.set_aspect("equal") const_ax.set_aspect("equal")
if not spines: if not spines:
@ -375,8 +397,8 @@ def view_sig(
image = Image.open(logo_path) # Open the PNG image using PIL image = Image.open(logo_path) # Open the PNG image using PIL
logo_ax.imshow(image) logo_ax.imshow(image)
except FileNotFoundError: except (FileNotFoundError, UnidentifiedImageError, OSError) as exc:
print(f"Warning, {logo_path} not found.") print(f"Warning, could not load logo image: {logo_path}. Reason: {exc}")
fig.subplots_adjust( fig.subplots_adjust(
left=0.1, # Left margin left=0.1, # Left margin

View File

@ -119,24 +119,19 @@ def setup_style(*, labels_mode: bool = False, compact_mode: bool = False) -> Non
label_font = 14 label_font = 14
else: else:
base_font = 10 base_font = 10
title_font = 12 title_font = 15
label_font = 10 label_font = 10
matplotlib.rcParams.update( matplotlib.rcParams.update(
{ {
"figure.facecolor": "#0f172a", "figure.facecolor": "#161616",
"axes.facecolor": "#1e293b", "axes.facecolor": "#161616",
"axes.edgecolor": COLORS["muted"], "savefig.facecolor": "#161616",
"axes.labelcolor": COLORS["light"], "savefig.edgecolor": "#161616",
"text.color": COLORS["light"],
"xtick.color": COLORS["muted"],
"ytick.color": COLORS["muted"],
"grid.color": COLORS["muted"],
"grid.alpha": 0.3,
"font.size": base_font, "font.size": base_font,
"axes.titlesize": title_font, "axes.titlesize": title_font,
"axes.labelsize": label_font, "axes.labelsize": label_font,
"figure.titlesize": title_font + 2, "figure.titlesize": title_font + 4,
"legend.frameon": False, "legend.frameon": False,
"legend.facecolor": "none", "legend.facecolor": "none",
"xtick.labelsize": base_font, "xtick.labelsize": base_font,
@ -194,7 +189,7 @@ def view_simple_sig(
constellation_mode: Optional[bool] = False, constellation_mode: Optional[bool] = False,
labels_mode: Optional[bool] = False, labels_mode: Optional[bool] = False,
slice: Optional[tuple] = None, slice: Optional[tuple] = None,
title: Optional[str] = "Signal", title: Optional[str] = "Signal Plot",
): ):
""" """
Create a simple plot of various signal visualizations as a png or svg image. Create a simple plot of various signal visualizations as a png or svg image.
@ -237,7 +232,7 @@ def view_simple_sig(
spec_signal = signal spec_signal = signal
if compact_mode: if compact_mode:
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 6), gridspec_kw={"height_ratios": [1, 5]}) fig, (ax2, ax1) = plt.subplots(2, 1, figsize=(12, 6), gridspec_kw={"height_ratios": [5, 1]})
show_title = False show_title = False
show_labels = False show_labels = False
ax_constellation = ax_psd = None ax_constellation = ax_psd = None
@ -253,25 +248,24 @@ def view_simple_sig(
ax_psd = None ax_psd = None
else: else:
if constellation_mode: if constellation_mode:
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12)) fig, ((ax2, ax1), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
ax_constellation, ax_psd = ax3, ax4 ax_constellation, ax_psd = ax3, ax4
else: else:
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10)) fig, (ax2, ax1) = plt.subplots(2, 1, figsize=(14, 10))
ax_constellation = ax_psd = None ax_constellation = ax_psd = None
show_title = True show_title = True
show_labels = labels_mode show_labels = labels_mode
if show_title: if show_title:
fig.suptitle(title, fontsize=16, color=COLORS["light"], y=0.96) fig.suptitle(title, fontsize=25)
fig.patch.set_facecolor("#0f172a") fig.patch.set_facecolor(matplotlib.rcParams["figure.facecolor"])
total_duration_s = len(signal) / sample_rate_hz if sample_rate_hz else 0.0 total_duration_s = len(signal) / sample_rate_hz if sample_rate_hz else 0.0
t_s = np.linspace(0, total_duration_s, len(display_signal)) if len(display_signal) else np.array([]) t_s = np.linspace(0, total_duration_s, len(display_signal)) if len(display_signal) else np.array([])
ax1.plot(t_s, display_signal.real, color=COLORS["purple"], linewidth=0.8, alpha=0.8, label="I") ax1.plot(t_s, display_signal.real, color=COLORS["purple"], linewidth=0.6, alpha=0.8, label="I")
ax1.plot(t_s, display_signal.imag, color=COLORS["magenta"], linewidth=0.8, alpha=0.8, label="Q") ax1.plot(t_s, display_signal.imag, color=COLORS["magenta"], linewidth=0.6, alpha=0.8, label="Q")
ax1.set_xlim(0, total_duration_s) ax1.grid(True, alpha=0.2, linewidth=0.5)
ax1.grid(True, alpha=0.3)
nfft, overlap = _get_nfft_size(signal=signal, fast_mode=fast_mode) nfft, overlap = _get_nfft_size(signal=signal, fast_mode=fast_mode)
@ -285,7 +279,7 @@ def view_simple_sig(
) )
ax2.set_ylim(center_freq_hz - sample_rate_hz / 2, center_freq_hz + sample_rate_hz / 2) ax2.set_ylim(center_freq_hz - sample_rate_hz / 2, center_freq_hz + sample_rate_hz / 2)
ax2.set_xlim(0, total_duration_s) ax1.set_xlim(ax2.get_xlim())
if show_labels: if show_labels:
if horizontal_mode: if horizontal_mode:
@ -294,20 +288,26 @@ def view_simple_sig(
ax2.set_xlabel("Time (s)") ax2.set_xlabel("Time (s)")
ax1.set_ylabel("Amplitude") ax1.set_ylabel("Amplitude")
ax1.set_title(f"Time Series - {sdr} SDR", loc="left", pad=10) ax1.set_title(f"IQ Sample Plot - {sdr} SDR", loc="left", pad=10, fontsize=15)
ax1.legend(loc="upper right") ax1.legend(loc="upper right", fontsize=10)
ax2.set_ylabel("Frequency (Hz)") ax2.set_ylabel("Frequency (MHz)")
ax2.set_title( ax2.set_title(
f"Spectrogram - {center_freq_hz / 1e6:.1f} MHz ± {sample_rate_hz / 2e6:.1f} MHz", loc="left", pad=10 f"Spectrogram - {center_freq_hz / 1e6:.1f} MHz ± {sample_rate_hz / 2e6:.1f} MHz", loc="left", pad=10, fontsize=15
)
ax2.yaxis.set_major_formatter(
matplotlib.ticker.FuncFormatter(lambda x, _: f"{x / 1e6:.1f}")
) )
yticks = ax2.get_yticks()
ax2.set_yticklabels([f"{y / 1e6:.1f}" for y in yticks])
elif not compact_mode: elif not compact_mode:
ax1.set_title("Time Series", loc="left", pad=10) ax1.set_title("IQ Sample Plot", loc="left", pad=10, fontsize=15)
ax1.legend(loc="upper right", fontsize=8) ax1.legend(loc="upper right", fontsize=10)
ax2.set_title("Spectrogram", loc="left", pad=10) ax2.set_xlabel("Time (s)")
ax2.set_ylabel("Frequency (MHz)")
ax2.set_title("Spectrogram", loc="left", pad=10, fontsize=15)
ax2.yaxis.set_major_formatter(
matplotlib.ticker.FuncFormatter(lambda x, _: f"{x / 1e6:.1f}")
)
_add_annotations( _add_annotations(
annotations=annotations, annotations=annotations,
@ -339,8 +339,8 @@ def view_simple_sig(
) )
ax_constellation.set_xlabel("In-phase (I)") ax_constellation.set_xlabel("In-phase (I)")
ax_constellation.set_ylabel("Quadrature (Q)") ax_constellation.set_ylabel("Quadrature (Q)")
ax_constellation.set_title("Constellation") ax_constellation.set_title("Constellation", loc="left", fontsize=15)
ax_constellation.grid(True, alpha=0.3) ax_constellation.grid(True, alpha=0.2, linewidth=0.5)
ax_constellation.set_aspect("equal") ax_constellation.set_aspect("equal")
if ax_psd is not None: if ax_psd is not None:
@ -351,11 +351,11 @@ def view_simple_sig(
freqs = freqs + center_freq_hz freqs = freqs + center_freq_hz
spectrum_db = 10 * np.log10(spectrum + 1e-12) spectrum_db = 10 * np.log10(spectrum + 1e-12)
ax_psd.plot(freqs / 1e6, spectrum_db, color=COLORS["accent"], linewidth=1.0) ax_psd.plot(freqs / 1e6, spectrum_db, color=COLORS["accent"], linewidth=0.8)
ax_psd.set_xlabel("Frequency (MHz)") ax_psd.set_xlabel("Frequency (MHz)")
ax_psd.set_ylabel("Power (dB)") ax_psd.set_ylabel("Power (dB)")
ax_psd.set_title("Power Spectral Density") ax_psd.set_title("Power Spectral Density", loc="left", fontsize=15)
ax_psd.grid(True, alpha=0.3) ax_psd.grid(True, alpha=0.2, linewidth=0.5)
if compact_mode: if compact_mode:
ax1.set_xticks([]) ax1.set_xticks([])
@ -367,13 +367,20 @@ def view_simple_sig(
else: else:
plt.tight_layout() plt.tight_layout()
if show_title: if show_title:
plt.subplots_adjust(top=0.92) plt.subplots_adjust(top=0.9)
if saveplot: if saveplot:
output_path, extension = set_path(output_path=output_path) output_path, extension = set_path(output_path=output_path)
dpi_value = _set_dpi(fast_mode=fast_mode, labels_mode=labels_mode, extension=extension) dpi_value = _set_dpi(fast_mode=fast_mode, labels_mode=labels_mode, extension=extension)
plt.savefig(output_path, dpi=dpi_value, bbox_inches="tight", facecolor="#0f172a", edgecolor="none") plt.savefig(
output_path,
dpi=dpi_value,
bbox_inches="tight",
pad_inches=0.3,
facecolor=matplotlib.rcParams["savefig.facecolor"],
edgecolor=matplotlib.rcParams["savefig.edgecolor"],
)
print(f"Saved signal plot to {output_path}") print(f"Saved signal plot to {output_path}")
return output_path return output_path