From 2c1fba75dada535ed32c181dffe9b42ac45d0161 Mon Sep 17 00:00:00 2001 From: gillian Date: Fri, 24 Apr 2026 14:34:11 -0400 Subject: [PATCH 01/14] docs: improve getting_started and installation readability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Consolidate installation steps into installation.rst (pip upgrade, ria --help verification, entrypoints note, editable install note, SDR driver table); replace getting_started §1 body with a link - Reformat command and subcommand lists as tables with purpose descriptions and internal ref links for navigation - Remove redundant §6 tips and §9 cheat sheet; trim duplicate descriptions in generate subcommand sections - Fix inline code comments to sit beside the command they describe - Add custom CSS for light body text, white headings, and table header colour to suit the dark background theme --- docs/source/_static/custom.css | 17 +- docs/source/intro/getting_started.rst | 289 ++++++++++++-------------- docs/source/intro/installation.rst | 36 +++- 3 files changed, 177 insertions(+), 165 deletions(-) diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css index 40b2823..da14878 100644 --- a/docs/source/_static/custom.css +++ b/docs/source/_static/custom.css @@ -1,6 +1,7 @@ /* Change the hex values below to customize heading colours */ -.rst-content h1 { color: #2c3e50; } +.rst-content { color: #e0e0e0; } +.rst-content h1 { color: #ffffff; } .rst-content h2, .rst-content h2 a { color: #ffffff !important; font-size: 22px !important; } @@ -22,8 +23,20 @@ .rst-content .admonition.warning p { color: #ffffff !important; } -.rst-content h4 { color: #404040; } +.rst-content h4 { color: #cccccc; } .highlight * { color: #ffffff !important; } .ria-cmd { color: #2980b9 !important; } + + +/* Table header text */ +.rst-content table.docutils th { + color: #ffffff !important; +} + +/* Remove alternating row background colors from tables */ +.rst-content table.docutils td, +.rst-content table.docutils tr:nth-child(2n-1) td { + background-color: transparent !important; +} diff --git a/docs/source/intro/getting_started.rst b/docs/source/intro/getting_started.rst index c2386ee..4e722e2 100644 --- a/docs/source/intro/getting_started.rst +++ b/docs/source/intro/getting_started.rst @@ -6,10 +6,10 @@ This is a practical reference for the ``ria`` CLI from ``ria-toolkit-oss``. **Scope of this guide:** -* Installation and setup -* End-to-end CLI workflows -* Full command reference for CLI features -* Brief scripting section +* **Installation and SDR driver prerequisites** — how to install RIA Toolkit OSS and configure the system drivers your hardware requires +* **End-to-end CLI workflow** — a step-by-step walkthrough from hardware discovery through capture, annotation, and processing +* **Full command reference** — options, flags, and examples for every ``ria`` command +* **Python scripting preview** — using the toolkit API directly without the CLI **Official resources:** @@ -18,76 +18,15 @@ This is a practical reference for the ``ria`` CLI from ``ria-toolkit-oss``. * `PyPI package `_ * `RIA Hub Conda package `_ -.. contents:: Contents - :local: - :depth: 2 - :backlinks: none - 1) Installation and Setup ========================== -1.1 Installation with Conda ----------------------------- - -RIA Toolkit OSS is available as a Conda package on RIA Hub. This is typically the easiest -path when using SDR tooling that depends on native/system libraries. - -.. code-block:: bash - - conda update --force conda - conda config --add channels https://riahub.ai/api/packages/qoherent/conda - conda activate base - conda install ria-toolkit-oss - -Verify: - -.. code-block:: bash - - conda list | grep ria-toolkit-oss +Before using the ``ria`` CLI, follow the :doc:`Installation ` guide to +install RIA Toolkit OSS and any SDR drivers required for your hardware. -1.2 Installation with pip --------------------------- - -Use pip unless you specifically need to edit toolkit source. - -.. code-block:: bash - - python3 -m venv .venv - source .venv/bin/activate - pip install --upgrade pip - pip install ria-toolkit-oss - -Verify CLI entrypoint: - -.. code-block:: bash - - ria --help - -``pyproject.toml`` defines two script entry points: - -* ``ria`` -* ``ria-tools`` - -Both point to the same CLI module (``ria_toolkit_oss_cli.cli:cli``). - - -1.3 Optional install from source ----------------------------------- - -Use this for local development or testing unreleased changes. - -.. code-block:: bash - - git clone https://riahub.ai/qoherent/ria-toolkit-oss.git - cd ria-toolkit-oss - python3 -m venv .venv - source .venv/bin/activate - pip install -e . - - -1.4 SDR driver prerequisites +1.1 SDR driver prerequisites ----------------------------- Toolkit package install does not install all system SDR drivers. Install vendor/runtime @@ -95,11 +34,22 @@ dependencies for the hardware you use. Examples (depends on device and OS): -* USRP: UHD drivers -* Pluto: libiio / IIO utilities -* BladeRF: libbladeRF -* HackRF: libhackrf -* RTL-SDR: librtlsdr +.. list-table:: + :widths: 25 75 + :header-rows: 1 + + * - Device + - Driver Package + * - USRP + - UHD drivers + * - Pluto + - libiio / IIO utilities + * - BladeRF + - libbladeRF + * - HackRF + - libhackrf + * - RTL-SDR + - librtlsdr See repo docs under ``docs/source/sdr_guides/*`` and your OS package instructions. @@ -119,18 +69,34 @@ Top-level CLI follows this model: **Top-level commands:** -* ``discover`` -* ``init`` -* ``capture`` -* ``view`` -* ``annotate`` (group) -* ``convert`` -* ``split`` -* ``combine`` -* ``generate`` (group) -* ``transform`` (group) -* ``transmit`` -* ``synth`` (alias of ``generate`` in command bindings) +.. list-table:: + :widths: 25 75 + :header-rows: 1 + + * - Command + - Purpose + * - :ref:`discover ` + - Probe SDR drivers and enumerate attached hardware + * - :ref:`init ` + - Create and manage user metadata defaults + * - :ref:`capture ` + - Record IQ samples from a connected SDR + * - :ref:`view ` + - Generate visualizations from IQ files + * - :ref:`annotate ` + - Label signal regions manually or with auto-detection (group) + * - :ref:`convert ` + - Convert between IQ file formats + * - :ref:`split ` + - Split, trim, or extract recordings + * - :ref:`combine ` + - Merge multiple recordings by concatenation or addition + * - :ref:`generate / synth ` + - Generate synthetic IQ signals (group; ``synth`` is an alias) + * - :ref:`transform ` + - Apply augmentations or impairments to recordings (group) + * - :ref:`transmit ` + - Transmit IQ through a TX-capable SDR 3) Quick End-to-End Workflow @@ -158,10 +124,8 @@ provenance fields. .. code-block:: bash ria init - # or non-interactive - ria init --author "Jane Doe" --project "rf-campaign-1" --location "Lab-A" - # show config - ria init --show + ria init --author "Jane Doe" --project "rf-campaign-1" --location "Lab-A" # non-interactive + ria init --show # show config 3.3 Capture IQ @@ -227,13 +191,14 @@ Replay recorded or synthesized IQ through a transmit-capable SDR. .. code-block:: bash ria transmit -d hackrf -f 2.44G -s 2e6 --input capture.sigmf-data - # or generated waveform - ria transmit -d hackrf --generate lfm --continuous + ria transmit -d hackrf --generate lfm --continuous # generated waveform 4) Command Reference ===================== +.. _cmd-discover: + 4.1 ``discover`` ----------------- @@ -263,6 +228,8 @@ Replay recorded or synthesized IQ through a transmit-capable SDR. hidden in default output. +.. _cmd-init: + 4.2 ``init`` ------------- @@ -309,6 +276,8 @@ Replay recorded or synthesized IQ through a transmit-capable SDR. generate metadata, and YAML config loading paths). +.. _cmd-capture: + 4.3 ``capture`` ---------------- @@ -382,6 +351,8 @@ Device selection (``--device``) is optional if only one device is detected. Exac ria capture -c capture_config.yaml +.. _cmd-view: + 4.4 ``view`` ------------- @@ -444,6 +415,8 @@ Device selection (``--device``) is optional if only one device is detected. Exac ria view old.npy --legacy --type simple +.. _cmd-annotate: + 4.5 ``annotate`` group ----------------------- @@ -459,8 +432,30 @@ Device selection (``--device``) is optional if only one device is detected. Exac ria annotate ... -**Subcommands:** ``list``, ``add``, ``remove``, ``clear``, ``energy``, ``cusum``, -``threshold``, ``separate`` +**Subcommands:** + +.. list-table:: + :widths: 25 75 + :header-rows: 1 + + * - Subcommand + - Purpose + * - ``list`` + - Inspect all annotations on a recording + * - ``add`` + - Add one annotation with explicit sample-domain bounds + * - ``remove`` + - Remove one annotation by index + * - ``clear`` + - Remove all annotations from a recording + * - ``energy`` + - Auto-detect regions above the estimated noise floor + * - ``cusum`` + - Auto-detect regime changes using change-point detection + * - ``threshold`` + - Auto-detect regions using normalized magnitude thresholding + * - ``separate`` + - Decompose annotations into narrower spectral components **General behavior:** @@ -590,6 +585,8 @@ annotations. ria annotate separate capture.sigmf-data --indices 0,1 --verbose +.. _cmd-convert: + 4.6 ``convert`` ---------------- @@ -629,6 +626,8 @@ inferred from the output file extension. ria convert old.npy --format sigmf --legacy --overwrite +.. _cmd-split: + 4.7 ``split`` -------------- @@ -670,6 +669,8 @@ Choose exactly one operation per invocation: ria split annotated.sigmf-data --extract-annotations --annotation-label payload +.. _cmd-combine: + 4.8 ``combine`` ---------------- @@ -717,6 +718,8 @@ Choose exactly one operation per invocation: ria combine signal.npy pattern.npy out.npy --mode add --align-mode repeat-spaced --repeat-spacing 10000 +.. _cmd-generate: + 4.9 ``generate`` group (and ``synth`` alias) --------------------------------------------- @@ -728,15 +731,34 @@ Choose exactly one operation per invocation: ``ria synth ...`` is an alias for ``ria generate ...``. -**Shape:** +**Usage:** .. code-block:: bash ria generate [subcommand options] [common options] **Available subcommands:** -``tone``, ``noise``, ``chirp``, ``square``, ``sawtooth``, ``qam``, ``apsk``, ``pam``, -``fsk``, ``ook``, ``oqpsk``, ``gmsk``, ``psk`` + +.. list-table:: + :widths: 30 70 + :header-rows: 1 + + * - Subcommand(s) + - Description + * - ``tone`` + - Clean sinusoidal calibration/reference source + * - ``noise`` + - Baseline noise floor data or controlled additive-noise synthesis + * - ``chirp`` + - Sweep-based radar/sonar-style signals and bandwidth occupancy tests + * - ``square``, ``sawtooth`` + - Periodic waveform primitives + * - ``qam``, ``apsk``, ``pam``, ``psk`` + - Digital modulation families with pulse-shaping filter support + * - ``fsk`` + - Frequency-shift keying with configurable tone spacing + * - ``ook``, ``oqpsk``, ``gmsk`` + - On-off keying and continuous-phase modulation schemes **Common options shared across all generators:** @@ -760,22 +782,16 @@ Multipath and IQ imbalance flags apply impairment-style post-processing during g Options: ``--frequency``, ``--amplitude``, ``--phase`` -Clean sinusoidal calibration/reference source. - ``noise`` ~~~~~~~~~~ Options: ``--noise-type {gaussian,uniform}``, ``--power`` -Baseline noise floor data or controlled additive-noise synthesis. - ``chirp`` ~~~~~~~~~~ Options: ``--bandwidth`` (required), ``--period`` (required), ``--type {up,down,up_down}`` -Sweep-based radar/sonar-style signals and bandwidth occupancy tests. - ``square`` ~~~~~~~~~~~ @@ -826,6 +842,8 @@ symbol transition sharpness). ria synth psk -s 2e6 -r 100e3 -M 8 -N 8000 -o psk8.npy +.. _cmd-transform: + 4.10 ``transform`` group ------------------------- @@ -834,7 +852,7 @@ symbol transition sharpness). * Apply algorithmic transforms to existing recordings. * Run reusable augmentations/impairments for dataset diversity and robustness testing. -**Shape:** +**Usage:** .. code-block:: bash @@ -895,6 +913,8 @@ inspect parameter hints. ``--view`` writes a PNG preview alongside transform out ria transform custom my_filter in.npy out.npy --transform-dir ./my_transforms --params cutoff=0.2 +.. _cmd-transmit: + 4.11 ``transmit`` ------------------ @@ -993,17 +1013,7 @@ experiment-specific fields on the CLI. ria generate noise --config generate.yaml -6) Practical Tips and Safety -============================= - -* Use ``ria discover`` before capture/transmit sessions. -* Keep TX gain conservative first; validate with attenuators/dummy loads when needed. -* Prefer SigMF for interoperable metadata and annotations. -* For long workflows, keep outputs organized by campaign directories and consistent prefixes. -* Use ``--verbose`` when debugging device init or driver issues. - - -7) Version Notes +6) Version Notes ================= These notes are based on the current implementation and should be re-validated against future @@ -1016,11 +1026,12 @@ releases. 3. Multiple non-CLI modules still import ``utils.*``, which can create runtime dependency coupling when using only ``ria-toolkit-oss`` in isolation. -If you observe unexpected import errors after install, check the package version and -changelog, then test ``ria --help`` in a clean virtual environment. +.. tip:: + If you observe unexpected import errors after install, check the package version and + changelog, then test ``ria --help`` in a clean virtual environment. -8) Brief Scripting (Python) Preview +7) Brief Scripting (Python) Preview ===================================== For quick non-CLI use: @@ -1037,47 +1048,3 @@ For quick non-CLI use: to_sigmf(imp, filename="capture_awgn", path=".") You can also call annotation algorithms and block-generator primitives from Python directly. - - -9) Cheat Sheet -=============== - -.. code-block:: bash - - # Install - pip install ria-toolkit-oss - - # Discover - ria discover -v - - # Init defaults - ria init --author "Jane" --project "rf1" --location "Lab-A" - - # Capture - ria capture -d pluto -f 2.44G -s 2e6 -n 1000000 -o cap.sigmf-data - - # View - ria view cap.sigmf-data --type simple - - # Annotate - ria annotate energy cap.sigmf-data --threshold 1.2 - ria annotate list cap.sigmf-data --verbose - - # Convert - ria convert cap.sigmf-data cap.npy - - # Split - ria split cap.sigmf-data --split-every 100000 --output-dir chunks - - # Combine - ria combine chunks/a.npy chunks/b.npy merged.npy - - # Generate - ria generate qam -s 2e6 -r 100e3 -M 16 -N 5000 -o qam16.npy - - # Transform - ria transform augment channel_swap cap.npy - ria transform impair add_awgn_to_signal cap.npy --params snr=10 - - # Transmit - ria transmit -d hackrf --input cap.sigmf-data -f 2.44G -s 2e6 diff --git a/docs/source/intro/installation.rst b/docs/source/intro/installation.rst index 584b13f..309c294 100644 --- a/docs/source/intro/installation.rst +++ b/docs/source/intro/installation.rst @@ -4,7 +4,26 @@ Installation RIA Hub Toolkit OSS can be installed either as a Conda package or as a standard Python package. Please note that SDR drivers must be installed separately. Refer to the relevant guide in the -:ref:`SDR Guides ` section of the documentation for addition setup instructions. +:ref:`SDR Guides ` section of the documentation for additional setup instructions. + +Common driver packages by device (exact package names depend on your OS): + +.. list-table:: + :widths: 25 75 + :header-rows: 1 + + * - Device + - Driver Package + * - USRP + - UHD drivers + * - Pluto + - libiio / IIO utilities + * - BladeRF + - libbladeRF + * - HackRF + - libhackrf + * - RTL-SDR + - librtlsdr We want your experience with RIA Toolkit OSS to be as smooth and frictionless as possible. If you run into any issues during installation, please reach out to our support team: ``support@qoherent.ai``. @@ -84,12 +103,22 @@ Please follow the steps below to install RIA Toolkit OSS using pip: python -m venv venv venv\Scripts\activate -2. Install RIA Toolkit OSS from PyPI with pip: +2. Upgrade pip and install RIA Toolkit OSS: .. code-block:: bash + pip install --upgrade pip pip install ria-toolkit-oss +3. Verify the CLI is available: + + .. code-block:: bash + + ria --help + + A successful install prints the top-level help text. ``pyproject.toml`` registers two + entrypoints — ``ria`` and ``ria-tools`` — that both point to the same CLI module. + RIA Toolkit OSS can also be installed from RIA Hub. However, RIA Hub does not yet support a proxy or cache for public packages. We intend to add this missing functionality soon. In the meantime, please use the ``--no-deps`` option with pip to skip automatic dependency installation, and then manually install each dependency afterward. @@ -119,3 +148,6 @@ Follow the steps below to install RIA Toolkit OSS from source: .. code-block:: bash pip install . + + For local development, use ``pip install -e .`` instead to install in editable mode + so local changes take effect immediately without reinstalling. From 9a304faa00b3aaddb79149845ec7e8a6af55946b Mon Sep 17 00:00:00 2001 From: fordg1 Date: Tue, 28 Apr 2026 11:27:47 -0400 Subject: [PATCH 02/14] docs: enhance getting started guide with example output and image reference --- docs/source/intro/getting_started.rst | 6 ++++++ src/ria_toolkit_oss/view/tools.py | 21 ++++++++++----------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/source/intro/getting_started.rst b/docs/source/intro/getting_started.rst index 43b0724..b07f1a3 100644 --- a/docs/source/intro/getting_started.rst +++ b/docs/source/intro/getting_started.rst @@ -413,6 +413,12 @@ Device selection (``--device``) is optional if only one device is detected. Exac ria view capture.npy --type full --title "Test Capture" --format pdf ria view capture.npy --show --no-save ria view old.npy --legacy --type simple + ria view recordings\qam64_35.npy --type simple + +.. figure:: ../images/qam64_35.png + :alt: Example output of ria view recordings\qam64_35.npy --type simple + + Output of ``ria view recordings\qam64_35.npy --type simple`` .. _cmd-annotate: diff --git a/src/ria_toolkit_oss/view/tools.py b/src/ria_toolkit_oss/view/tools.py index 49ce451..974219a 100644 --- a/src/ria_toolkit_oss/view/tools.py +++ b/src/ria_toolkit_oss/view/tools.py @@ -32,16 +32,15 @@ def extract_metadata_fields(metadata): def set_path(output_path): - split_path = output_path.split("/") - - if len(split_path) == 1: - folder = "images" - file = split_path[0] - elif len(split_path) > 2: - file = split_path[-1] - folder = "/".join(split_path[:-1]) + path = pathlib.Path(output_path) + + # If only filename provided (no directory), use default 'images' folder + if len(path.parts) == 1: + folder = pathlib.Path("images") + file = path.name else: - folder, file = split_path + folder = path.parent + file = path.name split_file = file.split(".") if len(split_file) == 2: @@ -53,5 +52,5 @@ def set_path(output_path): extension = "png" file = file + ".png" - pathlib.Path(folder).mkdir(parents=True, exist_ok=True) - return "/".join([folder, file]), extension + folder.mkdir(parents=True, exist_ok=True) + return str(folder / file), extension From 4c94f6ae949499616a806ccfce7c1ba372fca6dc Mon Sep 17 00:00:00 2001 From: gillian Date: Tue, 28 Apr 2026 12:49:43 -0400 Subject: [PATCH 03/14] Changed datasets to data to match utils --- .../ria_toolkit_oss/{datatypes => data}/radio_datasets.rst | 0 .../ria_toolkit_oss.data.datasets.license.rst} | 0 .../ria_toolkit_oss.data.rst} | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename docs/source/ria_toolkit_oss/{datatypes => data}/radio_datasets.rst (100%) rename docs/source/ria_toolkit_oss/{datatypes/ria_toolkit_oss.datatypes.datasets.license.rst => data/ria_toolkit_oss.data.datasets.license.rst} (100%) rename docs/source/ria_toolkit_oss/{datatypes/ria_toolkit_oss.datatypes.rst => data/ria_toolkit_oss.data.rst} (78%) diff --git a/docs/source/ria_toolkit_oss/datatypes/radio_datasets.rst b/docs/source/ria_toolkit_oss/data/radio_datasets.rst similarity index 100% rename from docs/source/ria_toolkit_oss/datatypes/radio_datasets.rst rename to docs/source/ria_toolkit_oss/data/radio_datasets.rst diff --git a/docs/source/ria_toolkit_oss/datatypes/ria_toolkit_oss.datatypes.datasets.license.rst b/docs/source/ria_toolkit_oss/data/ria_toolkit_oss.data.datasets.license.rst similarity index 100% rename from docs/source/ria_toolkit_oss/datatypes/ria_toolkit_oss.datatypes.datasets.license.rst rename to docs/source/ria_toolkit_oss/data/ria_toolkit_oss.data.datasets.license.rst diff --git a/docs/source/ria_toolkit_oss/datatypes/ria_toolkit_oss.datatypes.rst b/docs/source/ria_toolkit_oss/data/ria_toolkit_oss.data.rst similarity index 78% rename from docs/source/ria_toolkit_oss/datatypes/ria_toolkit_oss.datatypes.rst rename to docs/source/ria_toolkit_oss/data/ria_toolkit_oss.data.rst index c550144..bd6adec 100644 --- a/docs/source/ria_toolkit_oss/datatypes/ria_toolkit_oss.datatypes.rst +++ b/docs/source/ria_toolkit_oss/data/ria_toolkit_oss.data.rst @@ -1,5 +1,5 @@ -Datatypes Package (ria_toolkit_oss.data) -============================================= +Data Package (ria_toolkit_oss.data) +======================================= .. |br| raw:: html From e5a3d327e5bf37c1acac9ec72f5d0b0cad1fb927 Mon Sep 17 00:00:00 2001 From: gillian Date: Tue, 28 Apr 2026 14:08:44 -0400 Subject: [PATCH 04/14] refactor: unify signal viewer styling and update docs screenshots - 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 --- docs/source/intro/getting_started.rst | 8 +- src/ria_toolkit_oss/view/view_signal.py | 50 +++++++---- .../view/view_signal_simple.py | 83 ++++++++++--------- 3 files changed, 88 insertions(+), 53 deletions(-) diff --git a/docs/source/intro/getting_started.rst b/docs/source/intro/getting_started.rst index b07f1a3..2dd4b6a 100644 --- a/docs/source/intro/getting_started.rst +++ b/docs/source/intro/getting_started.rst @@ -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 old.npy --legacy --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 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: diff --git a/src/ria_toolkit_oss/view/view_signal.py b/src/ria_toolkit_oss/view/view_signal.py index a059e60..8c15fad 100644 --- a/src/ria_toolkit_oss/view/view_signal.py +++ b/src/ria_toolkit_oss/view/view_signal.py @@ -3,11 +3,12 @@ import os import textwrap from typing import Optional +import matplotlib import matplotlib.pyplot as plt import numpy as np -from matplotlib import gridspec +from matplotlib import gridspec, ticker from matplotlib.patches import Patch -from PIL import Image +from PIL import Image, UnidentifiedImageError from scipy.fft import fft, fftshift from scipy.signal import spectrogram from scipy.signal.windows import hann @@ -185,7 +186,7 @@ def view_sig( logo: Optional[bool] = True, dark: Optional[bool] = True, spines: Optional[bool] = False, - title_fontsize: Optional[int] = 35, + title_fontsize: Optional[int] = 25, subtitle_fontsize: Optional[int] = 15, ) -> None: """ @@ -230,11 +231,24 @@ def view_sig( complex_signal = recording.data[0] 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) if dark: 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" else: plt.style.use("default") @@ -252,8 +266,8 @@ def view_sig( plot_x_indx = 0 if plot_spectrogram: - spec_ax = plt.subplot(gs[plot_y_indx : plot_y_indx + 2, :]) - 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 + 3 fft_size = get_fft_size(plot_length=plot_length) _, t_spec, Sxx = spectrogram( @@ -280,7 +294,12 @@ def view_sig( ) 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: 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.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_xlim([min(t), max(t)]) 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) if frequency: @@ -310,10 +330,12 @@ def view_sig( # Convert to dB 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.set_xlabel("Frequency (MHz)") 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) if constellation: @@ -326,7 +348,7 @@ def view_sig( const_ax.set_ylim([-1 * dimension, dimension]) const_ax.set_xlabel("In-phase (I)") 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") if not spines: @@ -375,8 +397,8 @@ def view_sig( image = Image.open(logo_path) # Open the PNG image using PIL logo_ax.imshow(image) - except FileNotFoundError: - print(f"Warning, {logo_path} not found.") + except (FileNotFoundError, UnidentifiedImageError, OSError) as exc: + print(f"Warning, could not load logo image: {logo_path}. Reason: {exc}") fig.subplots_adjust( left=0.1, # Left margin diff --git a/src/ria_toolkit_oss/view/view_signal_simple.py b/src/ria_toolkit_oss/view/view_signal_simple.py index c770b5a..a3bb280 100644 --- a/src/ria_toolkit_oss/view/view_signal_simple.py +++ b/src/ria_toolkit_oss/view/view_signal_simple.py @@ -119,24 +119,19 @@ def setup_style(*, labels_mode: bool = False, compact_mode: bool = False) -> Non label_font = 14 else: base_font = 10 - title_font = 12 + title_font = 15 label_font = 10 matplotlib.rcParams.update( { - "figure.facecolor": "#0f172a", - "axes.facecolor": "#1e293b", - "axes.edgecolor": COLORS["muted"], - "axes.labelcolor": COLORS["light"], - "text.color": COLORS["light"], - "xtick.color": COLORS["muted"], - "ytick.color": COLORS["muted"], - "grid.color": COLORS["muted"], - "grid.alpha": 0.3, + "figure.facecolor": "#161616", + "axes.facecolor": "#161616", + "savefig.facecolor": "#161616", + "savefig.edgecolor": "#161616", "font.size": base_font, "axes.titlesize": title_font, "axes.labelsize": label_font, - "figure.titlesize": title_font + 2, + "figure.titlesize": title_font + 4, "legend.frameon": False, "legend.facecolor": "none", "xtick.labelsize": base_font, @@ -194,7 +189,7 @@ def view_simple_sig( constellation_mode: Optional[bool] = False, labels_mode: Optional[bool] = False, 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. @@ -237,7 +232,7 @@ def view_simple_sig( spec_signal = signal 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_labels = False ax_constellation = ax_psd = None @@ -253,25 +248,24 @@ def view_simple_sig( ax_psd = None else: 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 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 show_title = True show_labels = labels_mode if show_title: - fig.suptitle(title, fontsize=16, color=COLORS["light"], y=0.96) - fig.patch.set_facecolor("#0f172a") + fig.suptitle(title, fontsize=25) + fig.patch.set_facecolor(matplotlib.rcParams["figure.facecolor"]) 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([]) - ax1.plot(t_s, display_signal.real, color=COLORS["purple"], linewidth=0.8, alpha=0.8, label="I") - ax1.plot(t_s, display_signal.imag, color=COLORS["magenta"], linewidth=0.8, alpha=0.8, label="Q") - ax1.set_xlim(0, total_duration_s) - ax1.grid(True, alpha=0.3) + 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.6, alpha=0.8, label="Q") + ax1.grid(True, alpha=0.2, linewidth=0.5) 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_xlim(0, total_duration_s) + ax1.set_xlim(ax2.get_xlim()) if show_labels: if horizontal_mode: @@ -294,20 +288,26 @@ def view_simple_sig( ax2.set_xlabel("Time (s)") ax1.set_ylabel("Amplitude") - ax1.set_title(f"Time Series - {sdr} SDR", loc="left", pad=10) - ax1.legend(loc="upper right") + ax1.set_title(f"IQ Sample Plot - {sdr} SDR", loc="left", pad=10, fontsize=15) + ax1.legend(loc="upper right", fontsize=10) - ax2.set_ylabel("Frequency (Hz)") + ax2.set_ylabel("Frequency (MHz)") 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: - ax1.set_title("Time Series", loc="left", pad=10) - ax1.legend(loc="upper right", fontsize=8) + ax1.set_title("IQ Sample Plot", loc="left", pad=10, fontsize=15) + 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( annotations=annotations, @@ -339,8 +339,8 @@ def view_simple_sig( ) ax_constellation.set_xlabel("In-phase (I)") ax_constellation.set_ylabel("Quadrature (Q)") - ax_constellation.set_title("Constellation") - ax_constellation.grid(True, alpha=0.3) + ax_constellation.set_title("Constellation", loc="left", fontsize=15) + ax_constellation.grid(True, alpha=0.2, linewidth=0.5) ax_constellation.set_aspect("equal") if ax_psd is not None: @@ -351,11 +351,11 @@ def view_simple_sig( freqs = freqs + center_freq_hz 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_ylabel("Power (dB)") - ax_psd.set_title("Power Spectral Density") - ax_psd.grid(True, alpha=0.3) + ax_psd.set_title("Power Spectral Density", loc="left", fontsize=15) + ax_psd.grid(True, alpha=0.2, linewidth=0.5) if compact_mode: ax1.set_xticks([]) @@ -367,13 +367,20 @@ def view_simple_sig( else: plt.tight_layout() if show_title: - plt.subplots_adjust(top=0.92) + plt.subplots_adjust(top=0.9) if saveplot: output_path, extension = set_path(output_path=output_path) 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}") return output_path From 0a1bef84531b6eb8e7dcf865752b158597f01341 Mon Sep 17 00:00:00 2001 From: gillian Date: Tue, 28 Apr 2026 16:31:35 -0400 Subject: [PATCH 05/14] fix: harden annotation pipeline and CLI robustness - Replace bare metadata["sample_rate"] access with .get() + clear ValueError in threshold_qualifier, energy_detector, cusum_annotator, parallel_signal_separator, and signal_isolation - Add --sample-rate option to energy, threshold, cusum, and separate CLI commands with a pre-flight error if sample rate is still absent - Normalize namespaced metadata keys (e.g. BlockGenerator:Foo:sample_rate) to standard keys on legacy .npy load - Cap threshold_qualifier smoothing window at 1% of signal length to prevent over-smoothing short recordings into a flat envelope - Warn when threshold or energy detector returns 0 annotations due to constant-envelope signal; point to cusum as the right tool - Enforce --overwrite before any work begins; error fires before load and detection, not after - Fix qualify_slice off-by-one that silently dropped the last slice - Surface split failures in parallel_signal_separator via warnings.warn instead of swallowing them silently - Add threshold annotation example image to getting_started docs --- .../_sources/intro/getting_started.rst.txt | 309 +++++++++--------- docs/source/intro/getting_started.rst | 6 + .../annotations/cusum_annotator.py | 7 +- .../annotations/energy_detector.py | 26 +- .../annotations/parallel_signal_separator.py | 15 +- .../annotations/qualify_slice.py | 2 +- .../annotations/signal_isolation.py | 13 +- .../annotations/threshold_qualifier.py | 21 +- src/ria_toolkit_oss/io/recording.py | 9 + .../ria_toolkit_oss/annotate.py | 136 +++++--- 10 files changed, 328 insertions(+), 216 deletions(-) diff --git a/docs/_build/html/_sources/intro/getting_started.rst.txt b/docs/_build/html/_sources/intro/getting_started.rst.txt index c2386ee..fa8e87e 100644 --- a/docs/_build/html/_sources/intro/getting_started.rst.txt +++ b/docs/_build/html/_sources/intro/getting_started.rst.txt @@ -6,10 +6,10 @@ This is a practical reference for the ``ria`` CLI from ``ria-toolkit-oss``. **Scope of this guide:** -* Installation and setup -* End-to-end CLI workflows -* Full command reference for CLI features -* Brief scripting section +* **Installation and SDR driver prerequisites** — how to install RIA Toolkit OSS and configure the system drivers your hardware requires +* **End-to-end CLI workflow** — a step-by-step walkthrough from hardware discovery through capture, annotation, and processing +* **Full command reference** — options, flags, and examples for every ``ria`` command +* **Python scripting preview** — using the toolkit API directly without the CLI **Official resources:** @@ -18,76 +18,15 @@ This is a practical reference for the ``ria`` CLI from ``ria-toolkit-oss``. * `PyPI package `_ * `RIA Hub Conda package `_ -.. contents:: Contents - :local: - :depth: 2 - :backlinks: none - 1) Installation and Setup ========================== -1.1 Installation with Conda ----------------------------- - -RIA Toolkit OSS is available as a Conda package on RIA Hub. This is typically the easiest -path when using SDR tooling that depends on native/system libraries. - -.. code-block:: bash - - conda update --force conda - conda config --add channels https://riahub.ai/api/packages/qoherent/conda - conda activate base - conda install ria-toolkit-oss - -Verify: - -.. code-block:: bash - - conda list | grep ria-toolkit-oss +Before using the ``ria`` CLI, follow the :doc:`Installation ` guide to +install RIA Toolkit OSS and any SDR drivers required for your hardware. -1.2 Installation with pip --------------------------- - -Use pip unless you specifically need to edit toolkit source. - -.. code-block:: bash - - python3 -m venv .venv - source .venv/bin/activate - pip install --upgrade pip - pip install ria-toolkit-oss - -Verify CLI entrypoint: - -.. code-block:: bash - - ria --help - -``pyproject.toml`` defines two script entry points: - -* ``ria`` -* ``ria-tools`` - -Both point to the same CLI module (``ria_toolkit_oss_cli.cli:cli``). - - -1.3 Optional install from source ----------------------------------- - -Use this for local development or testing unreleased changes. - -.. code-block:: bash - - git clone https://riahub.ai/qoherent/ria-toolkit-oss.git - cd ria-toolkit-oss - python3 -m venv .venv - source .venv/bin/activate - pip install -e . - - -1.4 SDR driver prerequisites +1.1 SDR driver prerequisites ----------------------------- Toolkit package install does not install all system SDR drivers. Install vendor/runtime @@ -95,11 +34,22 @@ dependencies for the hardware you use. Examples (depends on device and OS): -* USRP: UHD drivers -* Pluto: libiio / IIO utilities -* BladeRF: libbladeRF -* HackRF: libhackrf -* RTL-SDR: librtlsdr +.. list-table:: + :widths: 25 75 + :header-rows: 1 + + * - Device + - Driver Package + * - USRP + - UHD drivers + * - Pluto + - libiio / IIO utilities + * - BladeRF + - libbladeRF + * - HackRF + - libhackrf + * - RTL-SDR + - librtlsdr See repo docs under ``docs/source/sdr_guides/*`` and your OS package instructions. @@ -119,18 +69,34 @@ Top-level CLI follows this model: **Top-level commands:** -* ``discover`` -* ``init`` -* ``capture`` -* ``view`` -* ``annotate`` (group) -* ``convert`` -* ``split`` -* ``combine`` -* ``generate`` (group) -* ``transform`` (group) -* ``transmit`` -* ``synth`` (alias of ``generate`` in command bindings) +.. list-table:: + :widths: 25 75 + :header-rows: 1 + + * - Command + - Purpose + * - :ref:`discover ` + - Probe SDR drivers and enumerate attached hardware + * - :ref:`init ` + - Create and manage user metadata defaults + * - :ref:`capture ` + - Record IQ samples from a connected SDR + * - :ref:`view ` + - Generate visualizations from IQ files + * - :ref:`annotate ` + - Label signal regions manually or with auto-detection (group) + * - :ref:`convert ` + - Convert between IQ file formats + * - :ref:`split ` + - Split, trim, or extract recordings + * - :ref:`combine ` + - Merge multiple recordings by concatenation or addition + * - :ref:`generate / synth ` + - Generate synthetic IQ signals (group; ``synth`` is an alias) + * - :ref:`transform ` + - Apply augmentations or impairments to recordings (group) + * - :ref:`transmit ` + - Transmit IQ through a TX-capable SDR 3) Quick End-to-End Workflow @@ -158,10 +124,8 @@ provenance fields. .. code-block:: bash ria init - # or non-interactive - ria init --author "Jane Doe" --project "rf-campaign-1" --location "Lab-A" - # show config - ria init --show + ria init --author "Jane Doe" --project "rf-campaign-1" --location "Lab-A" # non-interactive + ria init --show # show config 3.3 Capture IQ @@ -227,13 +191,14 @@ Replay recorded or synthesized IQ through a transmit-capable SDR. .. code-block:: bash ria transmit -d hackrf -f 2.44G -s 2e6 --input capture.sigmf-data - # or generated waveform - ria transmit -d hackrf --generate lfm --continuous + ria transmit -d hackrf --generate lfm --continuous # generated waveform 4) Command Reference ===================== +.. _cmd-discover: + 4.1 ``discover`` ----------------- @@ -263,6 +228,8 @@ Replay recorded or synthesized IQ through a transmit-capable SDR. hidden in default output. +.. _cmd-init: + 4.2 ``init`` ------------- @@ -309,6 +276,8 @@ Replay recorded or synthesized IQ through a transmit-capable SDR. generate metadata, and YAML config loading paths). +.. _cmd-capture: + 4.3 ``capture`` ---------------- @@ -382,6 +351,8 @@ Device selection (``--device``) is optional if only one device is detected. Exac ria capture -c capture_config.yaml +.. _cmd-view: + 4.4 ``view`` ------------- @@ -442,7 +413,21 @@ Device selection (``--device``) is optional if only one device is detected. Exac ria view capture.npy --type full --title "Test Capture" --format pdf ria view capture.npy --show --no-save ria view old.npy --legacy --type simple + ria view recordings\qam64_35.npy --type simple + ria view recordings\qam64_35.npy --type full +.. figure:: ../images/recordings/qam64_35.png + :alt: Example 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: 4.5 ``annotate`` group ----------------------- @@ -459,8 +444,30 @@ Device selection (``--device``) is optional if only one device is detected. Exac ria annotate ... -**Subcommands:** ``list``, ``add``, ``remove``, ``clear``, ``energy``, ``cusum``, -``threshold``, ``separate`` +**Subcommands:** + +.. list-table:: + :widths: 25 75 + :header-rows: 1 + + * - Subcommand + - Purpose + * - ``list`` + - Inspect all annotations on a recording + * - ``add`` + - Add one annotation with explicit sample-domain bounds + * - ``remove`` + - Remove one annotation by index + * - ``clear`` + - Remove all annotations from a recording + * - ``energy`` + - Auto-detect regions above the estimated noise floor + * - ``cusum`` + - Auto-detect regime changes using change-point detection + * - ``threshold`` + - Auto-detect regions using normalized magnitude thresholding + * - ``separate`` + - Decompose annotations into narrower spectral components **General behavior:** @@ -587,8 +594,16 @@ annotations. ria annotate add capture.sigmf-data --start 10000 --count 5000 --label burst ria annotate energy capture.sigmf-data --label signal --threshold 1.3 ria annotate cusum capture.sigmf-data --min-duration 5 + ria annotate threshold recordings/sample_recording3.npy --threshold 0.7 --label 70% ria annotate separate capture.sigmf-data --indices 0,1 --verbose +.. figure:: ../images/recordings/sample_recording3_annotated.png + :alt: Example output of ria annotate threshold recordings/sample_recording3.npy --threshold 0.7 --label 70% + + Output of ``ria annotate threshold recordings/sample_recording3.npy --threshold 0.7 --label 70%`` + + +.. _cmd-convert: 4.6 ``convert`` ---------------- @@ -629,6 +644,8 @@ inferred from the output file extension. ria convert old.npy --format sigmf --legacy --overwrite +.. _cmd-split: + 4.7 ``split`` -------------- @@ -670,6 +687,8 @@ Choose exactly one operation per invocation: ria split annotated.sigmf-data --extract-annotations --annotation-label payload +.. _cmd-combine: + 4.8 ``combine`` ---------------- @@ -717,6 +736,8 @@ Choose exactly one operation per invocation: ria combine signal.npy pattern.npy out.npy --mode add --align-mode repeat-spaced --repeat-spacing 10000 +.. _cmd-generate: + 4.9 ``generate`` group (and ``synth`` alias) --------------------------------------------- @@ -728,15 +749,34 @@ Choose exactly one operation per invocation: ``ria synth ...`` is an alias for ``ria generate ...``. -**Shape:** +**Usage:** .. code-block:: bash ria generate [subcommand options] [common options] **Available subcommands:** -``tone``, ``noise``, ``chirp``, ``square``, ``sawtooth``, ``qam``, ``apsk``, ``pam``, -``fsk``, ``ook``, ``oqpsk``, ``gmsk``, ``psk`` + +.. list-table:: + :widths: 30 70 + :header-rows: 1 + + * - Subcommand(s) + - Description + * - ``tone`` + - Clean sinusoidal calibration/reference source + * - ``noise`` + - Baseline noise floor data or controlled additive-noise synthesis + * - ``chirp`` + - Sweep-based radar/sonar-style signals and bandwidth occupancy tests + * - ``square``, ``sawtooth`` + - Periodic waveform primitives + * - ``qam``, ``apsk``, ``pam``, ``psk`` + - Digital modulation families with pulse-shaping filter support + * - ``fsk`` + - Frequency-shift keying with configurable tone spacing + * - ``ook``, ``oqpsk``, ``gmsk`` + - On-off keying and continuous-phase modulation schemes **Common options shared across all generators:** @@ -760,22 +800,16 @@ Multipath and IQ imbalance flags apply impairment-style post-processing during g Options: ``--frequency``, ``--amplitude``, ``--phase`` -Clean sinusoidal calibration/reference source. - ``noise`` ~~~~~~~~~~ Options: ``--noise-type {gaussian,uniform}``, ``--power`` -Baseline noise floor data or controlled additive-noise synthesis. - ``chirp`` ~~~~~~~~~~ Options: ``--bandwidth`` (required), ``--period`` (required), ``--type {up,down,up_down}`` -Sweep-based radar/sonar-style signals and bandwidth occupancy tests. - ``square`` ~~~~~~~~~~~ @@ -826,6 +860,8 @@ symbol transition sharpness). ria synth psk -s 2e6 -r 100e3 -M 8 -N 8000 -o psk8.npy +.. _cmd-transform: + 4.10 ``transform`` group ------------------------- @@ -834,7 +870,7 @@ symbol transition sharpness). * Apply algorithmic transforms to existing recordings. * Run reusable augmentations/impairments for dataset diversity and robustness testing. -**Shape:** +**Usage:** .. code-block:: bash @@ -895,6 +931,8 @@ inspect parameter hints. ``--view`` writes a PNG preview alongside transform out ria transform custom my_filter in.npy out.npy --transform-dir ./my_transforms --params cutoff=0.2 +.. _cmd-transmit: + 4.11 ``transmit`` ------------------ @@ -993,17 +1031,7 @@ experiment-specific fields on the CLI. ria generate noise --config generate.yaml -6) Practical Tips and Safety -============================= - -* Use ``ria discover`` before capture/transmit sessions. -* Keep TX gain conservative first; validate with attenuators/dummy loads when needed. -* Prefer SigMF for interoperable metadata and annotations. -* For long workflows, keep outputs organized by campaign directories and consistent prefixes. -* Use ``--verbose`` when debugging device init or driver issues. - - -7) Version Notes +6) Version Notes ================= These notes are based on the current implementation and should be re-validated against future @@ -1016,18 +1044,19 @@ releases. 3. Multiple non-CLI modules still import ``utils.*``, which can create runtime dependency coupling when using only ``ria-toolkit-oss`` in isolation. -If you observe unexpected import errors after install, check the package version and -changelog, then test ``ria --help`` in a clean virtual environment. +.. tip:: + If you observe unexpected import errors after install, check the package version and + changelog, then test ``ria --help`` in a clean virtual environment. -8) Brief Scripting (Python) Preview +7) Brief Scripting (Python) Preview ===================================== For quick non-CLI use: .. code-block:: python - from ria_toolkit_oss.datatypes import Recording + from ria_toolkit_oss.data import Recording from ria_toolkit_oss.io import load_recording, to_sigmf from ria_toolkit_oss.transforms import iq_augmentations, iq_impairments @@ -1037,47 +1066,3 @@ For quick non-CLI use: to_sigmf(imp, filename="capture_awgn", path=".") You can also call annotation algorithms and block-generator primitives from Python directly. - - -9) Cheat Sheet -=============== - -.. code-block:: bash - - # Install - pip install ria-toolkit-oss - - # Discover - ria discover -v - - # Init defaults - ria init --author "Jane" --project "rf1" --location "Lab-A" - - # Capture - ria capture -d pluto -f 2.44G -s 2e6 -n 1000000 -o cap.sigmf-data - - # View - ria view cap.sigmf-data --type simple - - # Annotate - ria annotate energy cap.sigmf-data --threshold 1.2 - ria annotate list cap.sigmf-data --verbose - - # Convert - ria convert cap.sigmf-data cap.npy - - # Split - ria split cap.sigmf-data --split-every 100000 --output-dir chunks - - # Combine - ria combine chunks/a.npy chunks/b.npy merged.npy - - # Generate - ria generate qam -s 2e6 -r 100e3 -M 16 -N 5000 -o qam16.npy - - # Transform - ria transform augment channel_swap cap.npy - ria transform impair add_awgn_to_signal cap.npy --params snr=10 - - # Transmit - ria transmit -d hackrf --input cap.sigmf-data -f 2.44G -s 2e6 diff --git a/docs/source/intro/getting_started.rst b/docs/source/intro/getting_started.rst index 2dd4b6a..fa8e87e 100644 --- a/docs/source/intro/getting_started.rst +++ b/docs/source/intro/getting_started.rst @@ -594,8 +594,14 @@ annotations. ria annotate add capture.sigmf-data --start 10000 --count 5000 --label burst ria annotate energy capture.sigmf-data --label signal --threshold 1.3 ria annotate cusum capture.sigmf-data --min-duration 5 + ria annotate threshold recordings/sample_recording3.npy --threshold 0.7 --label 70% ria annotate separate capture.sigmf-data --indices 0,1 --verbose +.. figure:: ../images/recordings/sample_recording3_annotated.png + :alt: Example output of ria annotate threshold recordings/sample_recording3.npy --threshold 0.7 --label 70% + + Output of ``ria annotate threshold recordings/sample_recording3.npy --threshold 0.7 --label 70%`` + .. _cmd-convert: diff --git a/src/ria_toolkit_oss/annotations/cusum_annotator.py b/src/ria_toolkit_oss/annotations/cusum_annotator.py index e4498e5..9556125 100644 --- a/src/ria_toolkit_oss/annotations/cusum_annotator.py +++ b/src/ria_toolkit_oss/annotations/cusum_annotator.py @@ -38,7 +38,12 @@ def annotate_with_cusum( :type annotation_type: str """ - sample_rate = recording.metadata["sample_rate"] + sample_rate = recording.metadata.get("sample_rate") + if sample_rate is None: + raise ValueError( + "Recording metadata does not contain 'sample_rate'. " + "Supply it with --sample-rate when using the CLI, or set recording.sample_rate before calling this function." + ) center_frequency = recording.metadata.get("center_frequency", 0) # Create an object of the time segmenter diff --git a/src/ria_toolkit_oss/annotations/energy_detector.py b/src/ria_toolkit_oss/annotations/energy_detector.py index 4f14a9b..1a482bc 100644 --- a/src/ria_toolkit_oss/annotations/energy_detector.py +++ b/src/ria_toolkit_oss/annotations/energy_detector.py @@ -6,6 +6,7 @@ and occupied bandwidth calculation following ITU-R SM.328 standard. """ import json +import warnings from typing import Tuple import numpy as np @@ -119,6 +120,17 @@ def detect_signals_energy( if active: boundaries.append((start, len(smoothed_power) - start)) + if not boundaries and noise_floor > 0: + peak = float(np.max(smoothed_power)) + dynamic_range = peak / noise_floor + if dynamic_range < threshold_factor: + warnings.warn( + f"detect_signals_energy: no signal boundaries found — dynamic range {dynamic_range:.2f}x is below " + f"the threshold factor {threshold_factor}x. The signal may be constant-envelope (e.g. CW or chirp). " + "If the entire recording is signal, use 'ria annotate cusum' to segment it as a single region.", + stacklevel=2, + ) + # Merge boundaries that are closer than min_distance merged_boundaries = [] if boundaries: @@ -135,7 +147,12 @@ def detect_signals_energy( merged_boundaries.append((start, length)) # Create annotations from detected boundaries - sample_rate = recording.metadata["sample_rate"] + sample_rate = recording.metadata.get("sample_rate") + if sample_rate is None: + raise ValueError( + "Recording metadata does not contain 'sample_rate'. " + "Supply it with --sample-rate when using the CLI, or set recording.sample_rate before calling this function." + ) center_frequency = recording.metadata.get("center_frequency", 0) # Validate frequency method @@ -351,7 +368,12 @@ def annotate_with_obw( >>> annotated = annotate_with_obw(recording, label="signal_obw") """ signal = recording.data[0] - sample_rate = recording.metadata["sample_rate"] + sample_rate = recording.metadata.get("sample_rate") + if sample_rate is None: + raise ValueError( + "Recording metadata does not contain 'sample_rate'. " + "Set recording.sample_rate before calling this function." + ) center_freq = recording.metadata.get("center_frequency", 0) # Calculate OBW diff --git a/src/ria_toolkit_oss/annotations/parallel_signal_separator.py b/src/ria_toolkit_oss/annotations/parallel_signal_separator.py index f2fa999..4e08353 100644 --- a/src/ria_toolkit_oss/annotations/parallel_signal_separator.py +++ b/src/ria_toolkit_oss/annotations/parallel_signal_separator.py @@ -49,6 +49,7 @@ allowing splitting of overlapping signals into separate training samples. """ import json +import warnings from typing import List, Optional, Tuple import numpy as np @@ -401,7 +402,12 @@ def split_recording_annotations( return recording signal = recording.data[0] - sample_rate = recording.metadata["sample_rate"] + sample_rate = recording.metadata.get("sample_rate") + if sample_rate is None: + raise ValueError( + "Recording metadata does not contain 'sample_rate'. " + "Supply it with --sample-rate when using the CLI, or set recording.sample_rate before calling this function." + ) center_frequency = recording.metadata.get("center_frequency", 0.0) # Build new annotation list @@ -425,8 +431,11 @@ def split_recording_annotations( else: # No components found, keep original new_annotations.append(anno) - except Exception: - # Split failed for any reason, keep original + except Exception as e: + warnings.warn( + f"split_recording_annotations: failed to split annotation at index {i} ({e}); keeping original.", + stacklevel=2, + ) new_annotations.append(anno) else: # Not in split list, keep as-is diff --git a/src/ria_toolkit_oss/annotations/qualify_slice.py b/src/ria_toolkit_oss/annotations/qualify_slice.py index 89eadd7..f96668d 100644 --- a/src/ria_toolkit_oss/annotations/qualify_slice.py +++ b/src/ria_toolkit_oss/annotations/qualify_slice.py @@ -24,7 +24,7 @@ def qualify_slice_from_annotations(recording: Recording, slice_length: int): output_recordings = [] - for i in range((len(recording.data[0]) // slice_length) - 1): + for i in range(len(recording.data[0]) // slice_length): start_index = slice_length * i end_index = slice_length * (i + 1) diff --git a/src/ria_toolkit_oss/annotations/signal_isolation.py b/src/ria_toolkit_oss/annotations/signal_isolation.py index 89ae3df..22661e3 100644 --- a/src/ria_toolkit_oss/annotations/signal_isolation.py +++ b/src/ria_toolkit_oss/annotations/signal_isolation.py @@ -35,17 +35,24 @@ def isolate_signal(recording: Recording, annotation: Annotation) -> Recording: isolation_bw = anno_bw + sample_rate = recording.metadata.get("sample_rate") + if sample_rate is None: + raise ValueError( + "Recording metadata does not contain 'sample_rate'. " + "Set recording.sample_rate before calling isolate_signal." + ) + # frequency shift the center of the box about zero shifted_signal_slice = frequency_shift_iq_samples( iq_samples=signal_slice, - sample_rate=recording.metadata["sample_rate"], + sample_rate=sample_rate, shift_frequency=-1 * anno_base_center_freq, ) # filter - if isolation_bw < recording.metadata["sample_rate"] - 1: + if isolation_bw < sample_rate - 1: filtered_signal = apply_complex_lowpass_filter( - signal=shifted_signal_slice, cutoff_frequency=isolation_bw, sample_rate=recording.metadata["sample_rate"] + signal=shifted_signal_slice, cutoff_frequency=isolation_bw, sample_rate=sample_rate ) else: diff --git a/src/ria_toolkit_oss/annotations/threshold_qualifier.py b/src/ria_toolkit_oss/annotations/threshold_qualifier.py index b9bb727..24ef833 100644 --- a/src/ria_toolkit_oss/annotations/threshold_qualifier.py +++ b/src/ria_toolkit_oss/annotations/threshold_qualifier.py @@ -42,6 +42,7 @@ classification or demodulation stages. """ import json +import warnings from typing import Optional import numpy as np @@ -216,11 +217,21 @@ def threshold_qualifier( """ # Extract signal and metadata sample_data = recording.data[channel] - sample_rate = recording.metadata["sample_rate"] + sample_rate = recording.metadata.get("sample_rate") + if sample_rate is None: + raise ValueError( + "Recording metadata does not contain 'sample_rate'. " + "Supply it with --sample-rate when using the CLI, or set recording.sample_rate before calling this function." + ) center_frequency = recording.metadata.get("center_frequency", 0) + n_samples = len(sample_data) + if window_size is None: window_size = max(64, int(sample_rate * 0.001)) + # Cap at 1% of signal length so short recordings aren't over-smoothed into + # a flat envelope that collapses the dynamic range below the early-exit guard. + window_size = min(window_size, max(64, n_samples // 100)) # --- 1. SIGNAL CONDITIONING --- # Convert to power (Magnitude squared) @@ -237,6 +248,12 @@ def threshold_qualifier( # Soft early exit: keep a guard for low-contrast noise, but compute it from # the quieter tail of the envelope so burst-heavy captures are not rejected. if dynamic_range_ratio < 1.5: + warnings.warn( + f"threshold_qualifier: dynamic range ratio {dynamic_range_ratio:.2f} is below 1.5 — " + "the signal appears to be constant-envelope or pure noise, so no burst boundaries can be found. " + "If the entire recording is signal, use 'ria annotate cusum' to segment it as a single region.", + stacklevel=2, + ) return Recording(data=recording.data, metadata=recording.metadata, annotations=recording.annotations) trigger_val = noise_floor + threshold * (max_power - noise_floor) @@ -296,7 +313,7 @@ def threshold_qualifier( # burst energy does not bleed through the long window into adjacent regions, # which would inflate macro_residual_max and push the trigger above the # faint burst's average power. - macro_window_size = max(window_size * 16, int(sample_rate * 0.02)) + macro_window_size = min(max(window_size * 16, int(sample_rate * 0.02)), max(window_size * 2, n_samples // 4)) macro_kernel = np.ones(macro_window_size, dtype=np.float64) / macro_window_size # Expand each annotated range by half the macro window on both sides so that # the long convolution cannot "see" the leading/trailing edges of already- diff --git a/src/ria_toolkit_oss/io/recording.py b/src/ria_toolkit_oss/io/recording.py index ec4b472..a499d73 100644 --- a/src/ria_toolkit_oss/io/recording.py +++ b/src/ria_toolkit_oss/io/recording.py @@ -175,6 +175,15 @@ def from_npy(file: os.PathLike | str, legacy: bool = False) -> Recording: ) data = first # already loaded without pickle (numeric array) metadata = np.load(f, allow_pickle=True).tolist() + # Normalize namespaced keys (e.g. "BlockGenerator:Foo:sample_rate") to + # their bare equivalents so downstream code can find them reliably. + _STANDARD_KEYS = {"sample_rate", "center_frequency", "bandwidth"} + if isinstance(metadata, dict): + for k in list(metadata): + if ":" in k: + bare = k.rsplit(":", 1)[-1] + if bare in _STANDARD_KEYS and bare not in metadata: + metadata[bare] = metadata[k] try: annotations = list(np.load(f, allow_pickle=True)) except EOFError: diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py index daaf930..d551d2c 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py @@ -51,7 +51,7 @@ def detect_input_format(filepath): raise click.ClickException(f"Unknown format for '{filepath}'. Supported: .sigmf, .npy, .wav, .blue") -def determine_output_path(input_path, output_path, fmt, quiet, overwrite): +def determine_output_path(input_path, output_path, fmt, overwrite): input_path = Path(input_path) input_is_annotated = input_path.stem.endswith("_annotated") @@ -63,24 +63,20 @@ def determine_output_path(input_path, output_path, fmt, quiet, overwrite): else: target = input_path.with_name(f"{input_path.stem}_annotated{input_path.suffix}") - if fmt == "sigmf": - final_path = normalize_sigmf_path(target) - if not quiet: - click.echo(f"Saving SigMF metadata to: {final_path}") - else: - final_path = target - if not quiet: - click.echo(f"Saving to: {final_path}") + final_path = normalize_sigmf_path(target) if fmt == "sigmf" else target - # Always allow writing to _annotated files; guard against overwriting originals - target_is_annotated = final_path.stem.endswith("_annotated") - if final_path.exists() and not target_is_annotated and final_path != input_path: - click.echo(f"Error: {final_path} is not an annotated file and cannot be overwritten.", err=True) - return None + if final_path.exists() and not overwrite: + raise click.ClickException(f"{final_path} already exists. Use --overwrite to replace it.") return final_path +def check_output_available(input_path, output_path, overwrite): + """Raise ClickException before any work begins if the output file already exists.""" + fmt = detect_input_format(Path(input_path)) + determine_output_path(input_path=input_path, output_path=output_path, fmt=fmt, overwrite=overwrite) + + def save_recording_auto(recording, output_path, input_path, quiet=False, overwrite=False): """Save recording, auto-detecting format from extension. @@ -90,11 +86,16 @@ def save_recording_auto(recording, output_path, input_path, quiet=False, overwri input_path = Path(input_path) fmt = detect_input_format(input_path) - # Determine output path output_path = determine_output_path( - input_path=input_path, output_path=output_path, fmt=fmt, quiet=quiet, overwrite=overwrite + input_path=input_path, output_path=output_path, fmt=fmt, overwrite=overwrite ) + if not quiet: + if fmt == "sigmf": + click.echo(f"Saving SigMF metadata to: {output_path}") + else: + click.echo(f"Saving to: {output_path}") + if fmt == "sigmf": # Normalize path for SigMF base_path = output_path @@ -312,6 +313,8 @@ def add(input, start, count, label, freq_lower, freq_upper, comment, annotation_ except Exception as e: raise click.ClickException(f"Failed to load recording: {e}") + check_output_available(input, output, overwrite) + # Validate sample range n_samples = len(recording.data[0]) if start < 0: @@ -363,12 +366,9 @@ def add(input, start, count, label, freq_lower, freq_upper, comment, annotation_ if comment: click.echo(f" Comment: {comment}") - try: - save_recording_auto(recording, output, input, quiet, overwrite) - if not quiet: - click.echo(" ✓ Saved") - except Exception as e: - raise click.ClickException(f"Failed to save: {e}") + save_recording_auto(recording, output, input, quiet, overwrite) + if not quiet: + click.echo(" ✓ Saved") # ============================================================================ @@ -466,8 +466,6 @@ def clear(input, output, overwrite, force, quiet): if not quiet: click.echo(f"\nCleared {count_before} annotation(s)") - recording._annotations = [] - try: save_recording_auto(recording, output_path=input, input_path=input, quiet=quiet, overwrite=True) if not quiet: @@ -503,6 +501,7 @@ def clear(input, output, overwrite, force, quiet): default="standalone", help="Annotation type", ) +@click.option("--sample-rate", type=float, default=None, help="Sample rate in Hz (overrides metadata; required if not in file)") @click.option("--output", "-o", type=click.Path(), help="Output file path") @click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") @click.option("--quiet", is_flag=True, help="Quiet mode") @@ -517,6 +516,7 @@ def energy( nfft, obw_power, annotation_type, + sample_rate, output, overwrite, quiet, @@ -539,8 +539,11 @@ def energy( ria annotate energy signal.npy --threshold 1.5 --min-distance 10000 ria annotate energy signal.sigmf-data --freq-method obw ria annotate energy signal.sigmf-data --freq-method full-detected + ria annotate energy signal.npy --sample-rate 1e6 """ + check_output_available(input, output, overwrite) + try: recording = load_recording(input) if not quiet: @@ -548,6 +551,15 @@ def energy( except Exception as e: raise click.ClickException(f"Failed to load recording: {e}") + if sample_rate is not None: + recording.sample_rate = sample_rate + + if recording.sample_rate is None: + raise click.ClickException( + "Recording metadata does not contain a sample rate. " + "Provide it with --sample-rate (e.g. --sample-rate 1e6)." + ) + if not quiet: click.echo("\nDetecting signals using energy-based method...") click.echo(" Time detection:") @@ -575,13 +587,13 @@ def energy( if not quiet: click.echo(f" ✓ Added {added} annotation(s)") - - save_recording_auto(recording, output, input, quiet, overwrite) - if not quiet: - click.echo(" ✓ Saved") except Exception as e: raise click.ClickException(f"Energy detection failed: {e}") + save_recording_auto(recording, output, input, quiet, overwrite) + if not quiet: + click.echo(" ✓ Saved") + # ============================================================================ # CUSUM detection subcommand @@ -601,10 +613,11 @@ def energy( default="standalone", help="Annotation type", ) +@click.option("--sample-rate", type=float, default=None, help="Sample rate in Hz (overrides metadata; required if not in file)") @click.option("--output", "-o", type=click.Path(), help="Output file path") @click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") @click.option("--quiet", is_flag=True, help="Quiet mode") -def cusum(input, label, min_duration, window_size, tolerance, annotation_type, output, overwrite, quiet): +def cusum(input, label, min_duration, window_size, tolerance, annotation_type, sample_rate, output, overwrite, quiet): """Auto-detect segments using CUSUM method. Detects signal state changes (on/off, amplitude transitions). Best for @@ -616,7 +629,10 @@ def cusum(input, label, min_duration, window_size, tolerance, annotation_type, o Examples: ria annotate cusum signal.sigmf-data --min-duration 5.0 ria annotate cusum data.npy --min-duration 10.0 --label state + ria annotate cusum data.npy --sample-rate 1e6 """ + check_output_available(input, output, overwrite) + try: recording = load_recording(input) if not quiet: @@ -624,6 +640,15 @@ def cusum(input, label, min_duration, window_size, tolerance, annotation_type, o except Exception as e: raise click.ClickException(f"Failed to load recording: {e}") + if sample_rate is not None: + recording.sample_rate = sample_rate + + if recording.sample_rate is None: + raise click.ClickException( + "Recording metadata does not contain a sample rate. " + "Provide it with --sample-rate (e.g. --sample-rate 1e6)." + ) + if not quiet: click.echo("\nDetecting segments using CUSUM...") click.echo(f" Min duration: {min_duration} ms") @@ -644,13 +669,13 @@ def cusum(input, label, min_duration, window_size, tolerance, annotation_type, o if not quiet: click.echo(f" ✓ Added {added} annotation(s)") - - save_recording_auto(recording, output, input, quiet, overwrite) - if not quiet: - click.echo(" ✓ Saved") except Exception as e: raise click.ClickException(f"CUSUM detection failed: {e}") + save_recording_auto(recording, output, input, quiet, overwrite) + if not quiet: + click.echo(" ✓ Saved") + # ============================================================================ # Threshold detection subcommand @@ -675,10 +700,11 @@ def cusum(input, label, min_duration, window_size, tolerance, annotation_type, o help="Annotation type", ) @click.option("--channel", type=int, default=0, help="Channel index to annotate (default: 0)") +@click.option("--sample-rate", type=float, default=None, help="Sample rate in Hz (overrides metadata; required if not in file)") @click.option("--output", "-o", type=click.Path(), help="Output file path") @click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") @click.option("--quiet", is_flag=True, help="Quiet mode") -def threshold(input, threshold, label, window_size, annotation_type, channel, output, overwrite, quiet): +def threshold(input, threshold, label, window_size, annotation_type, channel, sample_rate, output, overwrite, quiet): """Auto-detect signals using threshold method. Detects samples above a percentage of maximum magnitude. Best for simple @@ -688,10 +714,13 @@ def threshold(input, threshold, label, window_size, annotation_type, channel, ou Examples: ria annotate threshold signal.sigmf-data --threshold 0.7 --label wifi ria annotate threshold data.npy --threshold 0.5 --window-size 2048 + ria annotate threshold data.npy --threshold 0.4 --sample-rate 1e6 """ if not (0.0 <= threshold <= 1.0): raise click.ClickException(f"--threshold must be between 0.0 and 1.0, got {threshold}") + check_output_available(input, output, overwrite) + try: recording = load_recording(input) if not quiet: @@ -699,11 +728,21 @@ def threshold(input, threshold, label, window_size, annotation_type, channel, ou except Exception as e: raise click.ClickException(f"Failed to load recording: {e}") + if sample_rate is not None: + recording.sample_rate = sample_rate + + if recording.sample_rate is None: + raise click.ClickException( + "Recording metadata does not contain a sample rate. " + "Provide it with --sample-rate (e.g. --sample-rate 1e6)." + ) + if not quiet: click.echo("\nDetecting signals using threshold qualifier...") click.echo(f" Threshold: {threshold * 100:.1f}% of max magnitude") click.echo(f" Window size: {'auto (1ms)' if window_size is None else f'{window_size} samples'}") click.echo(f" Channel: {channel}") + click.echo(f" Sample rate: {recording.sample_rate:.0f} Hz") try: initial_count = len(recording.annotations) @@ -719,13 +758,13 @@ def threshold(input, threshold, label, window_size, annotation_type, channel, ou if not quiet: click.echo(f" ✓ Added {added} annotation(s)") - - save_recording_auto(recording, output, input, quiet, overwrite) - if not quiet: - click.echo(" ✓ Saved") except Exception as e: raise click.ClickException(f"Threshold detection failed: {e}") + save_recording_auto(recording, output, input, quiet, overwrite) + if not quiet: + click.echo(" ✓ Saved") + # ============================================================================ # Separate subcommand (Phase 2: Parallel signal separation) @@ -738,11 +777,12 @@ def threshold(input, threshold, label, window_size, annotation_type, channel, ou @click.option("--nfft", type=int, default=65536, help="FFT size for spectral analysis") @click.option("--noise-threshold-db", type=float, help="Noise floor threshold in dB (auto-estimated if not specified)") @click.option("--min-component-bw", type=float, default=50e3, help="Min component bandwidth in Hz") +@click.option("--sample-rate", type=float, default=None, help="Sample rate in Hz (overrides metadata; required if not in file)") @click.option("--output", "-o", type=click.Path(), help="Output file path") @click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") @click.option("--quiet", is_flag=True, help="Quiet mode") @click.option("--verbose", is_flag=True, help="Verbose output (show detected components)") -def separate(input, indices, nfft, noise_threshold_db, min_component_bw, output, overwrite, quiet, verbose): +def separate(input, indices, nfft, noise_threshold_db, min_component_bw, sample_rate, output, overwrite, quiet, verbose): """ Auto-detect parallel frequency-offset signals and split into sub-bands. @@ -768,6 +808,8 @@ def separate(input, indices, nfft, noise_threshold_db, min_component_bw, output, ria annotate separate signal.npy --min-component-bw 100000 """ + check_output_available(input, output, overwrite) + try: recording = load_recording(input) if not quiet: @@ -775,6 +817,15 @@ def separate(input, indices, nfft, noise_threshold_db, min_component_bw, output, except Exception as e: raise click.ClickException(f"Failed to load recording: {e}") + if sample_rate is not None: + recording.sample_rate = sample_rate + + if recording.sample_rate is None: + raise click.ClickException( + "Recording metadata does not contain a sample rate. " + "Provide it with --sample-rate (e.g. --sample-rate 1e6)." + ) + # Parse indices if specified indices_list = get_indices_list(indices=indices, recording=recording) @@ -821,8 +872,9 @@ def separate(input, indices, nfft, noise_threshold_db, min_component_bw, output, f"{format_sample_count(ann.sample_start + ann.sample_count)}: {freq_range}" ) - save_recording_auto(recording, output, input, quiet, overwrite) - if not quiet: - click.echo(" ✓ Saved") except Exception as e: raise click.ClickException(f"Spectral separation failed: {e}") + + save_recording_auto(recording, output, input, quiet, overwrite) + if not quiet: + click.echo(" ✓ Saved") From 4ce42fa71a76dec7bad54c80e03ba64b83f0b2ff Mon Sep 17 00:00:00 2001 From: madrigal Date: Wed, 29 Apr 2026 09:55:29 -0400 Subject: [PATCH 06/14] Formatting, updated lock file --- poetry.lock | 381 ++++++++---------- src/ria_toolkit_oss/view/tools.py | 2 +- src/ria_toolkit_oss/view/view_signal.py | 36 +- .../view/view_signal_simple.py | 13 +- 4 files changed, 195 insertions(+), 237 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9d1e5fe..e947d9f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand. [[package]] name = "alabaster" @@ -242,14 +242,14 @@ files = [ [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.4.22" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["agent", "docs", "test"] files = [ - {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, - {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, + {file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"}, + {file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"}, ] [[package]] @@ -491,14 +491,14 @@ files = [ [[package]] name = "click" -version = "8.3.2" +version = "8.3.3" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" groups = ["main", "dev", "docs", "server", "test"] files = [ - {file = "click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d"}, - {file = "click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5"}, + {file = "click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613"}, + {file = "click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2"}, ] [package.dependencies] @@ -690,61 +690,61 @@ test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist" [[package]] name = "cryptography" -version = "46.0.7" +version = "47.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.8" groups = ["main"] files = [ - {file = "cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb"}, - {file = "cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b"}, - {file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85"}, - {file = "cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e"}, - {file = "cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457"}, - {file = "cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b"}, - {file = "cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1"}, - {file = "cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2"}, - {file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e"}, - {file = "cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee"}, - {file = "cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298"}, - {file = "cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb"}, - {file = "cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006"}, - {file = "cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0"}, - {file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85"}, - {file = "cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e"}, - {file = "cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246"}, - {file = "cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968"}, - {file = "cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4"}, - {file = "cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5"}, + {file = "cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f"}, + {file = "cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8"}, + {file = "cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318"}, + {file = "cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001"}, + {file = "cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203"}, + {file = "cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa"}, + {file = "cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736"}, + {file = "cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7"}, + {file = "cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52"}, + {file = "cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd"}, + {file = "cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63"}, + {file = "cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b"}, + {file = "cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76"}, + {file = "cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe"}, + {file = "cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31"}, + {file = "cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7"}, + {file = "cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310"}, + {file = "cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769"}, + {file = "cryptography-47.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f1207974a904e005f762869996cf620e9bf79ecb4622f148550bb48e0eb35a7"}, + {file = "cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1a405c08857258c11016777e11c02bacbe7ef596faf259305d282272a3a05cbe"}, + {file = "cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:20fdbe3e38fb67c385d233c89371fa27f9909f6ebca1cecc20c13518dae65475"}, + {file = "cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:f7db373287273d8af1414cf95dc4118b13ffdc62be521997b0f2b270771fef50"}, + {file = "cryptography-47.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9fe6b7c64926c765f9dff301f9c1b867febcda5768868ca084e18589113732ab"}, + {file = "cryptography-47.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cffbba3392df0fa8629bb7f43454ee2925059ee158e23c54620b9063912b86c8"}, + {file = "cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb"}, ] [package.dependencies] @@ -752,14 +752,7 @@ cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and pla typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] -docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox[uv] (>=2024.4.15)"] -pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] -sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi (>=2024)", "cryptography-vectors (==46.0.7)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] -test-randomorder = ["pytest-randomly"] [[package]] name = "cycler" @@ -850,14 +843,14 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.136.0" +version = "0.136.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.10" groups = ["server", "test"] files = [ - {file = "fastapi-0.136.0-py3-none-any.whl", hash = "sha256:8793d44ec7378e2be07f8a013cf7f7aa47d6327d0dfe9804862688ec4541a6b4"}, - {file = "fastapi-0.136.0.tar.gz", hash = "sha256:cf08e067cc66e106e102d9ba659463abfac245200752f8a5b7b1e813de4ff73e"}, + {file = "fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f"}, + {file = "fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f"}, ] [package.dependencies] @@ -1161,18 +1154,18 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "idna" -version = "3.11" +version = "3.13" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.8" groups = ["agent", "docs", "server", "test"] files = [ - {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, - {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, + {file = "idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3"}, + {file = "idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242"}, ] [package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "imagesize" @@ -1271,7 +1264,7 @@ files = [ [package.dependencies] attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rpds-py = ">=0.25.0" @@ -1522,67 +1515,67 @@ files = [ [[package]] name = "matplotlib" -version = "3.10.8" +version = "3.10.9" description = "Python plotting package" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7"}, - {file = "matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656"}, - {file = "matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df"}, - {file = "matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17"}, - {file = "matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933"}, - {file = "matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a"}, - {file = "matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160"}, - {file = "matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78"}, - {file = "matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4"}, - {file = "matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2"}, - {file = "matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6"}, - {file = "matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9"}, - {file = "matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2"}, - {file = "matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a"}, - {file = "matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58"}, - {file = "matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04"}, - {file = "matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f"}, - {file = "matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466"}, - {file = "matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf"}, - {file = "matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b"}, - {file = "matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6"}, - {file = "matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1"}, - {file = "matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486"}, - {file = "matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce"}, - {file = "matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6"}, - {file = "matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149"}, - {file = "matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645"}, - {file = "matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077"}, - {file = "matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22"}, - {file = "matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39"}, - {file = "matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565"}, - {file = "matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a"}, - {file = "matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958"}, - {file = "matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5"}, - {file = "matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f"}, - {file = "matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b"}, - {file = "matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d"}, - {file = "matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008"}, - {file = "matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c"}, - {file = "matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11"}, - {file = "matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8"}, - {file = "matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50"}, - {file = "matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908"}, - {file = "matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a"}, - {file = "matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1"}, - {file = "matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c"}, - {file = "matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b"}, - {file = "matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f"}, - {file = "matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8"}, - {file = "matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7"}, - {file = "matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3"}, - {file = "matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1"}, - {file = "matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a"}, - {file = "matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2"}, - {file = "matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3"}, + {file = "matplotlib-3.10.9-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77210dce9cb8153dffc967efaae990543392563d5a376d4dd8539bebcb0ed217"}, + {file = "matplotlib-3.10.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1e7698ac9868428e84d2c967424803b2472ff7167d9d6590d4204ed775343c3b"}, + {file = "matplotlib-3.10.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1aa972116abb4c9d201bf245620b433726cb6856f3bef6a78f776a00f5c92d37"}, + {file = "matplotlib-3.10.9-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae2f11957b27ce53497dd4d7b235c4d4f1faf383dfb39d0c5beb833bff883294"}, + {file = "matplotlib-3.10.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b049278ddce116aaa1c1377ebf58adea909132dfce0281cf7e3a1ea9fc2e2c65"}, + {file = "matplotlib-3.10.9-cp310-cp310-win_amd64.whl", hash = "sha256:82834c3c292d24d3a8aae77cd2d20019de69d692a34a970e4fdb8d33e2ea3dda"}, + {file = "matplotlib-3.10.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:68cfdcede415f7c8f5577b03303dd94526cdb6d11036cecdc205e08733b2d2bb"}, + {file = "matplotlib-3.10.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfca0129678bd56379db26c52b5d77ed7de314c047492fbdc763aa7501710cfb"}, + {file = "matplotlib-3.10.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e436d155fa8a3399dc62683f8f5d0e2e50d25d0144a73edd73f82eec8f4abfb"}, + {file = "matplotlib-3.10.9-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56fc0bd271b00025c6edfdc7c2dcd247372c8e1544971d62e1dc7c17367e8bf9"}, + {file = "matplotlib-3.10.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5a6104ed666402ba5106d7f36e0e0cdca4e8d7fa4d39708ca88019e2835a2eb"}, + {file = "matplotlib-3.10.9-cp311-cp311-win_amd64.whl", hash = "sha256:d730e984eddf56974c3e72b6129c7ca462ac38dc624338f4b0b23eb23ecba00f"}, + {file = "matplotlib-3.10.9-cp311-cp311-win_arm64.whl", hash = "sha256:51bf0ddbdc598e060d46c16b5590708f81a1624cefbaaf62f6a81bf9285b8c80"}, + {file = "matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1"}, + {file = "matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320"}, + {file = "matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285"}, + {file = "matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2"}, + {file = "matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf"}, + {file = "matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6"}, + {file = "matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42"}, + {file = "matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f"}, + {file = "matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e"}, + {file = "matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f"}, + {file = "matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838"}, + {file = "matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2"}, + {file = "matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921"}, + {file = "matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8"}, + {file = "matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9"}, + {file = "matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4"}, + {file = "matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc"}, + {file = "matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99"}, + {file = "matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d"}, + {file = "matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8"}, + {file = "matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38"}, + {file = "matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d"}, + {file = "matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f"}, + {file = "matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b"}, + {file = "matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2"}, + {file = "matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716"}, + {file = "matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f"}, + {file = "matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456"}, + {file = "matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe"}, + {file = "matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6"}, + {file = "matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c"}, + {file = "matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4"}, + {file = "matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf"}, + {file = "matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39"}, + {file = "matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c"}, + {file = "matplotlib-3.10.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1872fb212a05b729e649754a72d5da61d03e0554d76e80303b6f83d1d2c0552b"}, + {file = "matplotlib-3.10.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:985f2238880e2e69093f588f5fe2e46771747febf0649f3cf7f7b7480875317f"}, + {file = "matplotlib-3.10.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6640f75af2c6148293caa0a2b39dd806a492dd66c8a8b04035813e33d0fd2585"}, + {file = "matplotlib-3.10.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:42fb814efabe95c06c1994d8ab5a8385f43a249e23badd3ba931d4308e5bca20"}, + {file = "matplotlib-3.10.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f76e640a5268850bfda54b5131b1b1941cc685e42c5fa98ed9f2d64038308cba"}, + {file = "matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4"}, + {file = "matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358"}, ] [package.dependencies] @@ -1597,7 +1590,7 @@ pyparsing = ">=3" python-dateutil = ">=2.7" [package.extras] -dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7)"] +dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7,<10)"] [[package]] name = "mccabe" @@ -1611,25 +1604,6 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] -[[package]] -name = "mpmath" -version = "1.3.0" -description = "Python library for arbitrary-precision floating-point arithmetic" -optional = false -python-versions = "*" -groups = ["server", "test"] -markers = "python_version >= \"3.11\"" -files = [ - {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, - {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, -] - -[package.extras] -develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] -docs = ["sphinx"] -gmpy = ["gmpy2 (>=2.1.0a4) ; platform_python_implementation != \"PyPy\""] -tests = ["pytest (>=4.6)"] - [[package]] name = "mypy-extensions" version = "1.1.0" @@ -1717,37 +1691,37 @@ markers = {server = "python_version >= \"3.11\"", test = "python_version >= \"3. [[package]] name = "onnxruntime" -version = "1.24.4" +version = "1.25.1" description = "ONNX Runtime is a runtime accelerator for Machine Learning models" optional = false python-versions = ">=3.11" groups = ["server", "test"] markers = "python_version >= \"3.11\"" files = [ - {file = "onnxruntime-1.24.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bdfce8e9a6497cec584aab407b71bf697dac5e1b7b7974adc50bf7533bdb3a2"}, - {file = "onnxruntime-1.24.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:046ff290045a387676941a02a8ae5c3ebec6b4f551ae228711968c4a69d8f6b7"}, - {file = "onnxruntime-1.24.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e54ad52e61d2d4618dcff8fa1480ac66b24ee2eab73331322db1049f11ccf330"}, - {file = "onnxruntime-1.24.4-cp311-cp311-win_amd64.whl", hash = "sha256:b43b63eb24a2bc8fc77a09be67587a570967a412cccb837b6245ccb546691153"}, - {file = "onnxruntime-1.24.4-cp311-cp311-win_arm64.whl", hash = "sha256:e26478356dba25631fb3f20112e345f8e8bf62c499bb497e8a559f7d69cf7e7b"}, - {file = "onnxruntime-1.24.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cad1c2b3f455c55678ab2a8caa51fb420c25e6e3cf10f4c23653cdabedc8de78"}, - {file = "onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a5c5a544b22f90859c88617ecb30e161ee3349fcc73878854f43d77f00558b5"}, - {file = "onnxruntime-1.24.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d640eb9f3782689b55cfa715094474cd5662f2f137be6a6f847a594b6e9705c"}, - {file = "onnxruntime-1.24.4-cp312-cp312-win_amd64.whl", hash = "sha256:535b29475ca42b593c45fbb2152fbf1cdf3f287315bf650e6a724a0a1d065cdb"}, - {file = "onnxruntime-1.24.4-cp312-cp312-win_arm64.whl", hash = "sha256:e6214096e14b7b52e3bee1903dc12dc7ca09cb65e26664668a4620cc5e6f9a90"}, - {file = "onnxruntime-1.24.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e99a48078baaefa2b50fe5836c319499f71f13f76ed32d0211f39109147a49e0"}, - {file = "onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4aaed1e5e1aaacf2343c838a30a7c3ade78f13eeb16817411f929d04040a13"}, - {file = "onnxruntime-1.24.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e30c972bc02e072911aabb6891453ec73795386c0af2b761b65444b8a4c4745f"}, - {file = "onnxruntime-1.24.4-cp313-cp313-win_amd64.whl", hash = "sha256:3b6ba8b0181a3aa88edab00eb01424ffc06f42e71095a91186c2249415fcff93"}, - {file = "onnxruntime-1.24.4-cp313-cp313-win_arm64.whl", hash = "sha256:71d6a5c1821d6e8586a024000ece458db8f2fc0ecd050435d45794827ce81e19"}, - {file = "onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1700f559c8086d06b2a4d5de51e62cb4ff5e2631822f71a36db8c72383db71ee"}, - {file = "onnxruntime-1.24.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c74e268dc808e61e63784d43f9ddcdaf50a776c2819e8bd1d1b11ef64bf7e36"}, - {file = "onnxruntime-1.24.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:fbff2a248940e3398ae78374c5a839e49a2f39079b488bc64439fa0ec327a3e4"}, - {file = "onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2b7969e72d8cb53ffc88ab6d49dd5e75c1c663bda7be7eb0ece192f127343d1"}, - {file = "onnxruntime-1.24.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14ed1f197fab812b695a5eaddb536c635e58a2fbbe50a517c78f082cc6ce9177"}, - {file = "onnxruntime-1.24.4-cp314-cp314-win_amd64.whl", hash = "sha256:311e309f573bf3c12aa5723e23823077f83d5e412a18499d4485c7eb41040858"}, - {file = "onnxruntime-1.24.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f0b910e86b759a4732663ec61fd57ac42ee1b0066f68299de164220b660546d"}, - {file = "onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa12ddc54c9c4594073abcaa265cd9681e95fb89dae982a6f508a794ca42e661"}, - {file = "onnxruntime-1.24.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1cc6a518255f012134bc791975a6294806be9a3b20c4a54cca25194c90cf731"}, + {file = "onnxruntime-1.25.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:5cf58ec7601120bb4370f0b868f794d3e3626db7b1b1dba366c27874b224e9de"}, + {file = "onnxruntime-1.25.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa7d4daa78a18b8f3b410e31e82dab8580363c85cac644179a853f2748618e89"}, + {file = "onnxruntime-1.25.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:79162f873cdfa38cfc8d53d59a8dc7a71a14074df3d565b2f8ce24289545ddc0"}, + {file = "onnxruntime-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:451b9494056f7f96b1be76a32745ccc4582bd61b2a0e1bc52de3708446151d5d"}, + {file = "onnxruntime-1.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:7e608f8950076da02c0aeceec2dd790d201eeb31dd73acb04ec989b2bf6199dc"}, + {file = "onnxruntime-1.25.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:66e52f7a30d1f780a34aa84d68a0a04d382d9f5b141884ecbf45b7566b9fbde9"}, + {file = "onnxruntime-1.25.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5f41779f044d1ff75593df5c10a4d311bc82563687796d5218e2685b8f9da25"}, + {file = "onnxruntime-1.25.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:905409e9eb2ef87f8226e073f56e71faf731c3e480ebd34952cf953730e4a4ff"}, + {file = "onnxruntime-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:d4097b75b77486bb45835a8ed25b9a67976040ec6c258aeabae6aadfbdd1201c"}, + {file = "onnxruntime-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:b6c7aa5cae606d5c90a392679fac074b60f80025a2e83e1e90fdf882bd2a97f0"}, + {file = "onnxruntime-1.25.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e9d9b3b1694196bc3c5bc66f760a237a5e27d7688aaa2e2c9c0f66abd0486699"}, + {file = "onnxruntime-1.25.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:311d29b943e46a55ca72ca1ea48d7815c993122bfc359f68215fddeb9583fff4"}, + {file = "onnxruntime-1.25.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98016a038b31160db23208706139fa3b99cd60bc1c5ffdade77aafd6a37a92ad"}, + {file = "onnxruntime-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:08717d6eee2820807ba60b1b17032af207bd7aaca5b6c4abaee71f83feae877b"}, + {file = "onnxruntime-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:84f8963d70e00167bae273ab7e80e9795bfc5eb94f6b23236a99c5c11af00844"}, + {file = "onnxruntime-1.25.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03e800b3a4b48d9f3a2d23aacc4fa95486a3b406b14e51d1a9b8b6981d9adf9c"}, + {file = "onnxruntime-1.25.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd83ef5c10cfc051a1cb465db692d57b996a1bc75a2a97b161398e29cdbc47ff"}, + {file = "onnxruntime-1.25.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:395eb662c437fa2407f44266e4778b75bff261b17c2a6fef042421f9069f871d"}, + {file = "onnxruntime-1.25.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ae85395f41b291ae3e61780ec5092640181d369ef6c268aa8141c478b509e69"}, + {file = "onnxruntime-1.25.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:828e1b12710fbedb6dfab5e7bae6f11563617cddf3c2e7e8d84c64de566a4a3a"}, + {file = "onnxruntime-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:2affc9d2fd9ab013b9c9637464e649a0cca870d57ae18bfef74180eee65c3369"}, + {file = "onnxruntime-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:3387d75d1a815b4b2495b4e47a05ef1b3bcb64a817ddc68587e0bfcb9702bcf6"}, + {file = "onnxruntime-1.25.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06280b06604660595037f783c6d24bc70cbe5c6093975f194cd1482e77d450de"}, + {file = "onnxruntime-1.25.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e79fd5ce7db10ebcc24e020e2ed0159476e69e2326b9b7828e5aadcf6184212"}, ] [package.dependencies] @@ -1755,18 +1729,21 @@ flatbuffers = "*" numpy = ">=1.21.6" packaging = "*" protobuf = "*" -sympy = "*" + +[package.extras] +quantization = ["ml_dtypes"] +symbolic = ["sympy"] [[package]] name = "packaging" -version = "26.1" +version = "26.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "dev", "docs", "server", "test"] files = [ - {file = "packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f"}, - {file = "packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de"}, + {file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"}, + {file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"}, ] markers = {server = "python_version >= \"3.11\""} @@ -1893,21 +1870,20 @@ gssapi = ["gssapi (>=1.4.1) ; platform_system != \"Windows\"", "pyasn1 (>=0.1.7) [[package]] name = "pathspec" -version = "1.0.4" +version = "1.1.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, - {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, + {file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"}, + {file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"}, ] [package.extras] hyperscan = ["hyperscan (>=0.7)"] optional = ["typing-extensions (>=4)"] re2 = ["google-re2 (>=1.1)"] -tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] [[package]] name = "pillow" @@ -2974,14 +2950,14 @@ test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis [[package]] name = "sigmf" -version = "1.8.0" +version = "1.9.0" description = "Easily interact with Signal Metadata Format (SigMF) recordings." optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "sigmf-1.8.0-py3-none-any.whl", hash = "sha256:f233ab04344fa3e42170926a646f7e53edd7edc65fcda42eb3d7efaf8a2e8263"}, - {file = "sigmf-1.8.0.tar.gz", hash = "sha256:91e10cb046499639e5f961d66a24c17a33ff76fc98df892eab0953cc9d659a50"}, + {file = "sigmf-1.9.0-py3-none-any.whl", hash = "sha256:902e694894e61f8cdb75b0d69ae8c407f82f35435c3c5e4c1b586b313f77b89b"}, + {file = "sigmf-1.9.0.tar.gz", hash = "sha256:95e4b28156b2182035ecca5f5852108fb3cdef5f20b0cd48919bb0fc5f293d0e"}, ] [package.dependencies] @@ -3229,25 +3205,6 @@ typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\"" [package.extras] full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] -[[package]] -name = "sympy" -version = "1.14.0" -description = "Computer algebra system (CAS) in Python" -optional = false -python-versions = ">=3.9" -groups = ["server", "test"] -markers = "python_version >= \"3.11\"" -files = [ - {file = "sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}, - {file = "sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"}, -] - -[package.dependencies] -mpmath = ">=1.1.0,<1.4" - -[package.extras] -dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] - [[package]] name = "tomli" version = "2.4.1" @@ -3389,14 +3346,14 @@ typing-extensions = ">=4.12.0" [[package]] name = "tzdata" -version = "2026.1" +version = "2026.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" groups = ["main"] files = [ - {file = "tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9"}, - {file = "tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98"}, + {file = "tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7"}, + {file = "tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10"}, ] [[package]] @@ -3419,14 +3376,14 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "uvicorn" -version = "0.44.0" +version = "0.46.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.10" groups = ["docs", "server", "test"] files = [ - {file = "uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89"}, - {file = "uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e"}, + {file = "uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048"}, + {file = "uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d"}, ] [package.dependencies] @@ -3511,14 +3468,14 @@ test = ["aiohttp (>=3.10.5)", "flake8 (>=6.1,<7.0)", "mypy (>=0.800)", "psutil", [[package]] name = "virtualenv" -version = "21.2.4" +version = "21.3.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" groups = ["test"] files = [ - {file = "virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac"}, - {file = "virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada"}, + {file = "virtualenv-21.3.0-py3-none-any.whl", hash = "sha256:4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7"}, + {file = "virtualenv-21.3.0.tar.gz", hash = "sha256:733750db978ec95c2d8eb4feadaa57091002bce404cb39ba69899cf7bd28944e"}, ] [package.dependencies] diff --git a/src/ria_toolkit_oss/view/tools.py b/src/ria_toolkit_oss/view/tools.py index 974219a..cad5b67 100644 --- a/src/ria_toolkit_oss/view/tools.py +++ b/src/ria_toolkit_oss/view/tools.py @@ -33,7 +33,7 @@ def extract_metadata_fields(metadata): def set_path(output_path): path = pathlib.Path(output_path) - + # If only filename provided (no directory), use default 'images' folder if len(path.parts) == 1: folder = pathlib.Path("images") diff --git a/src/ria_toolkit_oss/view/view_signal.py b/src/ria_toolkit_oss/view/view_signal.py index 8c15fad..628bb0e 100644 --- a/src/ria_toolkit_oss/view/view_signal.py +++ b/src/ria_toolkit_oss/view/view_signal.py @@ -236,19 +236,21 @@ def view_sig( if dark: 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", - }) + 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" else: plt.style.use("default") @@ -297,9 +299,7 @@ def view_sig( 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}") - ) + spec_ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, _: f"{x / 1e6:.1f}")) if iq: iq_ax = plt.subplot(gs[plot_y_indx : plot_y_indx + 2, :]) @@ -330,7 +330,9 @@ def view_sig( # Convert to dB 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) / 1e6 + 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.set_xlabel("Frequency (MHz)") freq_ax.set_ylabel("Magnitude (dB)") diff --git a/src/ria_toolkit_oss/view/view_signal_simple.py b/src/ria_toolkit_oss/view/view_signal_simple.py index a3bb280..9402b36 100644 --- a/src/ria_toolkit_oss/view/view_signal_simple.py +++ b/src/ria_toolkit_oss/view/view_signal_simple.py @@ -293,11 +293,12 @@ def view_simple_sig( ax2.set_ylabel("Frequency (MHz)") ax2.set_title( - 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}") + 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}")) elif not compact_mode: ax1.set_title("IQ Sample Plot", loc="left", pad=10, fontsize=15) ax1.legend(loc="upper right", fontsize=10) @@ -305,9 +306,7 @@ def view_simple_sig( 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}") - ) + ax2.yaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(lambda x, _: f"{x / 1e6:.1f}")) _add_annotations( annotations=annotations, From 18666d95ee6d88b386400ffad5f8b26c4b5b1104 Mon Sep 17 00:00:00 2001 From: fordg1 Date: Tue, 5 May 2026 14:31:42 -0400 Subject: [PATCH 07/14] docs: expand getting_started with real command output, examples, and images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add example output for every section 4 command (discover, init, capture, view, annotate, convert, split, combine, generate, transform, transmit) - Add examples for all annotate subcommands (list, add, remove, clear, energy, threshold, cusum, separate) - Clarify separate workflow: requires existing annotations as input; show threshold → separate two-step example with before/after images - Regenerate all viewer images using updated viewer (post e5a3d32 styling) - Add images for energy, threshold, cusum, and separate annotation views, AWGN transform output, and qam64_35 simple/full views - Reorder annotate subcommands: manual first, auto-detection second - Simplify section 3 workflow to one command per step with links to section 4 - Remove all italic inline option-group labels and redundant sub-headers - Rewrite generate subcommand options as a table; consolidate capture and transmit option lists --- docs/source/intro/getting_started.rst | 796 ++++++++++++++++++-------- 1 file changed, 569 insertions(+), 227 deletions(-) diff --git a/docs/source/intro/getting_started.rst b/docs/source/intro/getting_started.rst index fa8e87e..59de870 100644 --- a/docs/source/intro/getting_started.rst +++ b/docs/source/intro/getting_started.rst @@ -105,93 +105,99 @@ Top-level CLI follows this model: 3.1 Discover radios -------------------- -Run this first in any new environment to verify drivers and detect hardware before -attempting RX/TX commands. +Run this first to verify drivers and detect connected hardware. .. code-block:: bash - ria discover ria discover -v - ria discover --json-output + +See :ref:`discover ` for JSON output and troubleshooting options. 3.2 Initialize local metadata defaults --------------------------------------- -Set reusable metadata once so generated/captured files automatically include consistent -provenance fields. +Set reusable metadata once so captured files include consistent provenance fields. .. code-block:: bash ria init - ria init --author "Jane Doe" --project "rf-campaign-1" --location "Lab-A" # non-interactive - ria init --show # show config + +See :ref:`init ` for non-interactive and config path options. 3.3 Capture IQ --------------- -Capture baseband data from a connected SDR into a reusable file format. +Record baseband IQ from a connected SDR. .. code-block:: bash ria capture -d pluto -f 2.44G -s 2e6 -n 500000 -o capture.sigmf-data +See :ref:`capture ` for all device, format, and metadata options. + 3.4 Visualize and inspect -------------------------- -Render quick diagnostic plots to validate signal presence, quality, and rough structure. +Render a quick diagnostic plot to validate signal presence and quality. .. code-block:: bash ria view capture.sigmf-data --type simple - ria view capture.sigmf-data --type full --show --no-save + +See :ref:`view ` for full multi-panel plots and display options. 3.5 Auto-annotate and inspect annotations ------------------------------------------ -Create initial labels automatically, then inspect annotation objects before downstream use. +Detect signal regions automatically, then verify the results. .. code-block:: bash - ria annotate energy capture.sigmf-data --label signal --threshold 1.2 - ria annotate list capture.sigmf-data --verbose + ria annotate energy capture.sigmf-data --label signal + ria annotate list capture.sigmf-data + +See :ref:`annotate ` for threshold tuning and other detection methods. 3.6 Convert and split ---------------------- -Normalize file format and split large captures into manageable chunks for processing or -training. +Convert to a different format and split into fixed-size chunks. .. code-block:: bash ria convert capture.sigmf-data capture.npy ria split capture.sigmf-data --split-every 100000 --output-dir chunks +See :ref:`convert ` and :ref:`split ` for format and trim options. + 3.7 Apply transforms --------------------- -Augment or impair recordings to produce controlled variants. +Augment or impair a recording to produce controlled variants. .. code-block:: bash - ria transform augment channel_swap capture.npy ria transform impair add_awgn_to_signal capture.npy --params snr=10 +See :ref:`transform ` for available augmentations and custom transforms. + 3.8 Transmit (TX-capable radios only) -------------------------------------- -Replay recorded or synthesized IQ through a transmit-capable SDR. +Replay a recording through a transmit-capable SDR. .. code-block:: bash ria transmit -d hackrf -f 2.44G -s 2e6 --input capture.sigmf-data - ria transmit -d hackrf --generate lfm --continuous # generated waveform + +See :ref:`transmit ` for continuous mode and generated waveform options. 4) Command Reference @@ -227,6 +233,68 @@ Replay recorded or synthesized IQ through a transmit-capable SDR. * Use ``--verbose`` first when troubleshooting; it surfaces driver-level failures that are hidden in default output. +**Example output:** + +Run ``ria discover -v`` to see loaded drivers, failure reasons, and attached devices: + +.. code-block:: text + + $ ria discover -v + + ✅ Loaded drivers (3): + hackrf + pluto + bladerf + + ❌ Failed drivers (3): + usrp: ModuleNotFoundError: uhd + rtlsdr: ImportError: pyrtlsdr is required to use the RTLSDR class + thinkrf: ImportError: pyrf is required to use the ThinkRF integration. + Install with: pip install ria-toolkit-oss[thinkrf] + + ======================================== + Attached Devices + ======================================== + + 📡 USRP/UHD devices (1): + ✅ MyB200 (B200) - Serial: 30C51D5 + + 📱 PlutoSDR devices: None found + 🔧 HackRF devices: None found + + ======================================== + Discovery Summary + ======================================== + Loaded drivers: 3 + Failed drivers: 3 + Detected devices: 1 + +With ``--json-output`` (useful for scripting and automation): + +.. code-block:: json + + { + "loaded_drivers": ["hackrf"], + "failed_drivers": ["pluto", "bladerf", "usrp", "rtlsdr", "thinkrf"], + "devices": [ + { + "type": "BladeRF", + "Description": "Nuand bladeRF 2.0", + "Backend": "libusb", + "Serial": "8518b488d3e3443da979680f472bbb87", + "USB Bus": "4", + "USB Address": "2" + } + ], + "total_devices": 1 + } + +.. note:: + Driver load failures are normal on systems where only a subset of SDR backends are + installed. A failed driver just means that backend's Python library isn't present — it + does not prevent other drivers from working. Install only the packages for the hardware + you use. + .. _cmd-init: @@ -244,36 +312,47 @@ Replay recorded or synthesized IQ through a transmit-capable SDR. ria init [options] -**Key options:** +**Options:** -*Metadata defaults:* -``--author``, ``--organization``, ``--project``, ``--location``, ``--testbed``, -``--license``, ``--hw``, ``--dataset`` - -*Actions:* -``--show``, ``--reset`` - -*Control:* -``--config-path``, ``--interactive`` / ``--no-interactive``, ``-y`` / ``--yes`` - -**What each option category does:** - -* Metadata defaults (``--author``, ``--project``, etc.): stored once and reused for later - recordings so files have consistent provenance. -* SigMF-focused fields (``--license``, ``--hw``, ``--dataset``): populate metadata commonly - expected in shared datasets. +* ``--author``, ``--organization``, ``--project``, ``--location``, ``--testbed``, + ``--license``, ``--hw``, ``--dataset``: stored once and reused for later recordings so + files have consistent provenance. * ``--show``: read-only inspect of the current resolved config. * ``--reset``: remove config and start clean. -* ``--config-path``: use a non-default config location (useful for isolated environments or - CI). +* ``--config-path``: use a non-default config location (useful for isolated environments or CI). * ``--interactive`` / ``--no-interactive``: force prompts on or off regardless of terminal auto-detection. * ``--yes``: suppress confirmation prompts for scripted runs. +**Example output:** + +Set metadata fields non-interactively: + +.. code-block:: text + + $ ria init --author "Jane Doe" --project "rf-campaign-1" --location "Lab-A" --no-interactive + + ✓ Configuration saved to: /home/user/.ria/config.yaml + +Then verify what was saved with ``--show``: + +.. code-block:: text + + $ ria init --show + + Current Configuration (/home/user/.ria/config.yaml): + ============================================================ + + Author: Jane Doe + Project: rf-campaign-1 + Location: Lab-A + + To update: ria init + To reset: ria init --reset + .. note:: - Current command output includes a note that some config integration is still being - finalized. Config values are already consumed by multiple commands (capture, convert, - generate metadata, and YAML config loading paths). + Config integration is still being finalized. Config values are already consumed by + multiple commands (capture, convert, generate metadata, and YAML config loading paths). .. _cmd-capture: @@ -295,53 +374,25 @@ Replay recorded or synthesized IQ through a transmit-capable SDR. Device selection (``--device``) is optional if only one device is detected. Exactly one of ``--num-samples`` or ``--duration`` is required. -**Core options:** - -*Device/connection:* +**Options:** * ``-d, --device {pluto,hackrf,bladerf,usrp,rtlsdr,thinkrf}`` -* ``-i, --ident`` -* ``-c, --config `` - -*RF/capture:* - +* ``-i, --ident``: serial or IP selector when multiple devices of the same type are present. +* ``-c, --config ``: load options from a YAML file; CLI flags override loaded values. * ``-s, --sample-rate`` * ``-f, --center-frequency`` (supports values like ``915e6``, ``2.4G``) -* ``-g, --gain`` -* ``-b, --bandwidth`` -* ``-n, --num-samples`` -* ``-t, --duration`` - -*Output:* - -* ``-o, --output`` -* ``--output-dir`` -* ``--format {npy,sigmf,wav,blue}`` -* ``--save-image`` - -*Metadata/logging:* - -* ``-m, --metadata KEY=VALUE`` (repeatable) +* ``-g, --gain``, ``-b, --bandwidth`` +* ``-n, --num-samples`` or ``-t, --duration``: use sample count for deterministic datasets, + or duration for quick time-based acquisition. +* ``-o, --output``, ``--output-dir``: output path or directory. A timestamped filename is + generated if ``--output`` is omitted; defaults to ``recordings/`` if ``--output-dir`` is + omitted. +* ``--format {npy,sigmf,wav,blue}``: inferred from file extension if not set. ``sigmf`` is + best for annotation workflows. +* ``--save-image``: writes a quick visual summary alongside the capture file. +* ``-m, --metadata KEY=VALUE`` (repeatable): injects run-specific metadata. * ``-v, --verbose``, ``-q, --quiet`` -**How options work in practice:** - -* ``--device`` + ``--ident``: select both device class and target instance; ``--ident`` - takes serial/IP style selectors. -* ``--config``: load a YAML option set, then override specific fields on the CLI as needed. -* ``--num-samples`` vs ``--duration``: use exact sample count for deterministic datasets, - or time-based capture for quick acquisition. -* ``--format``: ``sigmf`` is best for metadata/annotation workflows. -* ``--save-image``: writes a quick visual summary alongside capture output. -* ``--metadata KEY=VALUE``: injects run-specific metadata (campaign ID, antenna, scenario - tag, etc.). - -**Output behavior:** - -* If ``--output`` is omitted, a timestamped filename is generated automatically. -* If ``--output-dir`` is omitted, captures default to ``recordings/``. -* Format is inferred from the ``--output`` extension when no explicit ``--format`` is given. - **Examples:** .. code-block:: bash @@ -350,6 +401,28 @@ Device selection (``--device``) is optional if only one device is detected. Exac ria capture -d pluto -f 915e6 -t 2 --format npy --output-dir recordings ria capture -c capture_config.yaml +**Example output:** + +.. note:: + ``capture`` requires a connected SDR. The following shows representative output for a + HackRF capture. + +.. code-block:: text + + $ ria capture -d hackrf -s 2e6 -f 2.44G -n 1000000 -o rf.sigmf-data + + Initializing HackRF... + Device: HackRF One + Serial: a74ad5e4e2a14b7d + Center frequency: 2.44 GHz + Sample rate: 2.00 MS/s + Gain: 20 dB + + Capturing 1,000,000 samples... + + Saved: rf.sigmf-data + rf.sigmf-meta + .. _cmd-view: @@ -397,13 +470,13 @@ Device selection (``--device``) is optional if only one device is detected. Exac **Mode-specific options:** -*simple:* ``--fast``, ``--compact``, ``--horizontal``, ``--constellation``, ``--labels``, +simple: ``--fast``, ``--compact``, ``--horizontal``, ``--constellation``, ``--labels``, ``--slice start:end[:step]`` -*full:* ``--plot-length``, ``--no-spectrogram``, ``--no-iq``, ``--no-frequency``, +full: ``--plot-length``, ``--no-spectrogram``, ``--no-iq``, ``--no-frequency``, ``--no-constellation``, ``--no-metadata``, ``--no-logo``, ``--spines`` -*annotations/channels:* ``--channel`` +annotations / channels: ``--channel`` **Examples:** @@ -416,15 +489,36 @@ Device selection (``--device``) is optional if only one device is detected. Exac ria view recordings\qam64_35.npy --type simple ria view recordings\qam64_35.npy --type full -.. figure:: ../images/recordings/qam64_35.png - :alt: Example output of ria view recordings\qam64_35.npy --type simple +**Example output:** - Output of ``ria view recordings\qam64_35.npy --type simple`` +.. code-block:: text + + $ ria view qam64_35.npy --type simple + + Loading recording: qam64_35.npy + + Recording Metadata: + ---------------------------------------- + modulation: qam64 + constellation: qam + bits_per_symbol: 6 + sps: 6 + beta: 0.35 + source: signal.block_generator + ---------------------------------------- + + Generating simple visualization... + Saved: qam64_35.png + +.. figure:: ../images/recordings/qam64_35.png + :alt: Example output of ria view qam64_35.npy --type simple + + Output of ``ria view qam64_35.npy --type simple`` .. figure:: ../images/recordings/qam64_35-full.png - :alt: Example output of ria view recordings\qam64_35.npy --type full + :alt: Example output of ria view qam64_35.npy --type full - Output of ``ria view recordings\qam64_35.npy --type full`` + Output of ``ria view qam64_35.npy --type full`` .. _cmd-annotate: @@ -438,7 +532,7 @@ Device selection (``--device``) is optional if only one device is detected. Exac * Build or refine label metadata directly in recordings for downstream training, QA, and filtering. -**Command shape:** +**Usage:** .. code-block:: bash @@ -469,12 +563,11 @@ Device selection (``--device``) is optional if only one device is detected. Exac * - ``separate`` - Decompose annotations into narrower spectral components -**General behavior:** +SigMF is the preferred format for durable annotation metadata. For non-SigMF input, most +operations write a new output artifact unless ``--overwrite`` is set. +``--type {standalone,parallel,intersection}`` controls annotation relation semantics. -* SigMF is the preferred format for durable annotation metadata. -* For non-SigMF input, many operations write a new output artifact unless overwrite behavior - is explicitly requested. -* ``--type {standalone,parallel,intersection}`` controls annotation relation semantics. +**Manual subcommands:** ``ria annotate list`` ~~~~~~~~~~~~~~~~~~~~~ @@ -519,6 +612,8 @@ index. Removes all annotations from the recording. ``--force`` bypasses the confirmation prompt. +**Automatic detection subcommands:** + ``ria annotate energy`` ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -541,6 +636,19 @@ Detects energetic regions above the estimated noise floor and writes them as ann * ``--nfft``, ``--obw-power`` * ``--type``, ``-o`` / ``--output``, ``--overwrite``, ``--quiet`` +``ria annotate threshold`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: bash + + ria annotate threshold --threshold <0.0..1.0> [options] + +Uses normalized magnitude thresholding to derive annotation spans. Use where a fixed +amplitude threshold is sufficient. + +* ``--label``, ``--window-size``, ``--type``, ``-o`` / ``--output``, ``--overwrite``, + ``--quiet`` + ``ria annotate cusum`` ~~~~~~~~~~~~~~~~~~~~~~~ @@ -557,19 +665,6 @@ contiguous segments. * ``--tolerance``: merges nearby boundaries when set above default. * ``--type``, ``-o`` / ``--output``, ``--overwrite``, ``--quiet`` -``ria annotate threshold`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: bash - - ria annotate threshold --threshold <0.0..1.0> [options] - -Uses normalized magnitude thresholding to derive annotation spans. Use where a fixed -amplitude threshold is sufficient. - -* ``--label``, ``--window-size``, ``--type``, ``-o`` / ``--output``, ``--overwrite``, - ``--quiet`` - ``ria annotate separate`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -577,12 +672,17 @@ amplitude threshold is sufficient. ria annotate separate [options] -Decomposes selected annotations into narrower spectral components and emits refined -annotations. +Decomposes existing annotations into narrower sub-band annotations by detecting distinct +frequency components within each annotated time window. It does not detect signal regions +from scratch — run ``energy``, ``threshold``, or ``cusum`` first to produce the input +annotations, then use ``separate`` to refine them spectrally. + +Use this when a single broad annotation covers multiple signals at different frequencies +and you want separate annotations per component. * ``--indices "0,1,2"``: limit operation to specific annotations; omit to process all. * ``--nfft``: larger FFT improves frequency resolution but increases compute time. -* ``--noise-threshold-db``: stabilizes detection across heterogeneous captures. +* ``--noise-threshold-db``: sets the noise floor in dB; auto-estimated if omitted. * ``--min-component-bw``: rejects narrow fragments likely to be noise artifacts. * ``-o`` / ``--output``, ``--overwrite``, ``--quiet``, ``--verbose`` @@ -593,14 +693,208 @@ annotations. ria annotate list capture.sigmf-data --verbose ria annotate add capture.sigmf-data --start 10000 --count 5000 --label burst ria annotate energy capture.sigmf-data --label signal --threshold 1.3 + ria annotate threshold capture.sigmf-data --threshold 0.5 --label signal ria annotate cusum capture.sigmf-data --min-duration 5 - ria annotate threshold recordings/sample_recording3.npy --threshold 0.7 --label 70% ria annotate separate capture.sigmf-data --indices 0,1 --verbose -.. figure:: ../images/recordings/sample_recording3_annotated.png - :alt: Example output of ria annotate threshold recordings/sample_recording3.npy --threshold 0.7 --label 70% +**Example output:** - Output of ``ria annotate threshold recordings/sample_recording3.npy --threshold 0.7 --label 70%`` +``ria annotate list`` +^^^^^^^^^^^^^^^^^^^^^ + +Inspect all annotations with ``--verbose``: + +.. code-block:: text + + $ ria annotate list sample_recording3_annotated.npy --verbose + + Annotations in sample_recording3_annotated.npy: + [0] Samples 170,599-171,116: signal + Type: standalone + Frequency: 3.41 GHz - 3.41 GHz + Detail: {'generator': 'energy_detector', 'freq_method': 'nbw'} + [1] Samples 182,310-182,841: signal + Type: standalone + Frequency: 3.41 GHz - 3.41 GHz + Detail: {'generator': 'energy_detector', 'freq_method': 'nbw'} + [2] Samples 1,133,165-1,133,706: signal + ... + [7] Samples 2,113,268-2,861,395: signal + Type: standalone + Frequency: 3.41 GHz - 3.41 GHz + Detail: {'generator': 'energy_detector', 'freq_method': 'nbw'} + + Total: 8 annotation(s) + +``ria annotate add`` +^^^^^^^^^^^^^^^^^^^^ + +Add a single annotation by sample index: + +.. code-block:: text + + $ ria annotate add sample_recording3_annotated.npy --start 50000 --count 10000 --label burst -o out.npy + + Loaded: sample_recording3_annotated.npy + + Adding annotation: + Start: 50,000 + Count: 10,000 samples + Frequency: full bandwidth + Label: burst + Type: standalone + Saving to: out.npy + ✓ Saved + +``ria annotate remove`` +^^^^^^^^^^^^^^^^^^^^^^^ + +Remove one annotation by its list index (run ``annotate list`` first to confirm): + +.. code-block:: text + + $ ria annotate remove sample_recording3_annotated.npy 0 -o out.npy + + Loaded: sample_recording3_annotated.npy + + Removing annotation [0]: + Removed: samples 170,599-171,116 (signal) + Saving to: out.npy + ✓ Saved + +``ria annotate clear`` +^^^^^^^^^^^^^^^^^^^^^^ + +Remove all annotations at once: + +.. code-block:: text + + $ ria annotate clear sample_recording3_annotated.npy --force --overwrite + + Loaded: sample_recording3_annotated.npy + + Cleared 8 annotation(s) + Saving to: sample_recording3_annotated.npy + ✓ Saved + +``ria annotate energy`` +^^^^^^^^^^^^^^^^^^^^^^^ + +Auto-detect signal regions above the noise floor: + +.. code-block:: text + + $ ria annotate energy sample_recording3.npy --label signal -o sample_recording3_annotated.npy + + Loaded: sample_recording3.npy + + Detecting signals using energy-based method... + Time detection: + Segments: 10 + Threshold: 1.2x noise floor + Window size: 200 samples + Min distance: 5000 samples + Frequency bounds: nbw + ✓ Added 8 annotation(s) + Saving to: sample_recording3_annotated.npy + ✓ Saved + +.. figure:: ../images/recordings/sample_recording3_annotated.png + :alt: Energy-detected annotations on sample_recording3.npy + + ``ria annotate energy sample_recording3.npy --label signal`` + +``ria annotate threshold`` +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Detect regions above a fixed fraction of peak magnitude: + +.. code-block:: text + + $ ria annotate threshold sample_recording3.npy --threshold 0.7 --label strong -o out.npy + + Loaded: sample_recording3.npy + + Detecting signals using threshold qualifier... + Threshold: 70.0% of max magnitude + Window size: auto (1ms) + Channel: 0 + ✓ Added 2 annotation(s) + Saving to: out.npy + ✓ Saved + +.. figure:: ../images/recordings/sample_recording3_threshold.png + :alt: Threshold annotations on sample_recording3.npy at 0.7 + + ``ria annotate threshold sample_recording3.npy --threshold 0.7 --label strong`` + +``ria annotate cusum`` +^^^^^^^^^^^^^^^^^^^^^^ + +Detect regime changes using change-point detection: + +.. code-block:: text + + $ ria annotate cusum sample_recording3.npy --label regime -o out.npy + + Loaded: sample_recording3.npy + + Detecting segments using CUSUM... + Min duration: 5.0 ms + ✓ Added 37 annotation(s) + Saving to: out.npy + ✓ Saved + +.. figure:: ../images/recordings/sample_recording3_cusum.png + :alt: CUSUM annotations on sample_recording3.npy + + ``ria annotate cusum sample_recording3.npy --label regime`` + +``ria annotate separate`` +^^^^^^^^^^^^^^^^^^^^^^^^^ + +``separate`` takes existing annotations as input and splits each one into narrower +sub-band annotations by finding distinct spectral peaks within the annotated time window. +The typical workflow is to first run ``threshold`` (or ``energy``) to mark signal regions, +then run ``separate`` to resolve the individual frequency components within them. + +Step 1 — create broad annotations with ``threshold``: + +.. code-block:: text + + $ ria annotate threshold sample_recording5.npy --threshold 0.5 --label signal -o annotated.npy + + Loaded: sample_recording5.npy + + Detecting signals using threshold qualifier... + Threshold: 50.0% of max magnitude + Window size: auto (1ms) + Channel: 0 + ✓ Added 3 annotation(s) + Saving to: annotated.npy + ✓ Saved + +Step 2 — run ``separate`` to split by frequency component: + +.. code-block:: text + + $ ria annotate separate annotated.npy -o separated.npy + + Loaded: annotated.npy + + Splitting annotations by frequency components... + Input annotations: 3 + FFT size: 65536 + Noise threshold: auto-estimated + Min component BW: 50.00 kHz + ✓ Output annotations: 6 (+3 change) + Saving to: separated.npy + ✓ Saved + +.. figure:: ../images/recordings/sample_recording5_after_separate.png + :alt: Annotations after separate — split into upper and lower sub-bands + + After ``separate`` — each annotation is resolved into upper and lower frequency components .. _cmd-convert: @@ -643,6 +937,18 @@ inferred from the output file extension. ria convert highrate.npy audio.wav --wav-sample-rate 48000 ria convert old.npy --format sigmf --legacy --overwrite +**Example output:** + +.. code-block:: text + + $ ria convert sample_recording3.npy sample_recording3.sigmf-data + + Converting: sample_recording3.npy → sample_recording3.sigmf-data + Input format: NPY + Output format: SIGMF + Samples: 3,000,000 + Conversion complete: sample_recording3.sigmf-data, sample_recording3.sigmf-meta + .. _cmd-split: @@ -686,6 +992,26 @@ Choose exactly one operation per invocation: ria split recording.npy --trim --start 1000 --length 5000 --output-dir trimmed ria split annotated.sigmf-data --extract-annotations --annotation-label payload +**Example output:** + +.. code-block:: text + + $ ria split sample_recording3.npy --split-every 500000 --output-dir chunks + + Loading: sample_recording3.npy + Total samples: 3,000,000 + + Splitting into chunks of 500,000 samples... + Creating 6 chunks... + Chunk 1/6: samples 0-499,999... + Chunk 2/6: samples 500,000-999,999... + Chunk 3/6: samples 1,000,000-1,499,999... + Chunk 4/6: samples 1,500,000-1,999,999... + Chunk 5/6: samples 2,000,000-2,499,999... + Chunk 6/6: samples 2,500,000-2,999,999... + + Created 6 chunks in chunks/ + .. _cmd-combine: @@ -713,12 +1039,9 @@ Choose exactly one operation per invocation: * ``--overwrite``, ``--metadata KEY=VALUE`` (repeatable) * ``--legacy``, ``--verbose``, ``--quiet`` -**Mode semantics:** - -* ``concat``: append inputs sequentially in time. -* ``add``: sample-wise summation — all inputs must be aligned to the same length. - -**Alignment options for** ``--mode add``: +``--mode concat`` appends inputs sequentially in time. ``--mode add`` performs sample-wise +summation and requires all inputs to be the same length, or an ``--align-mode`` to +reconcile length differences: * ``error``: fail if lengths differ. * ``truncate``: cut all to shortest length. @@ -735,6 +1058,15 @@ Choose exactly one operation per invocation: ria combine long.npy short.npy out.npy --mode add --align-mode pad-center ria combine signal.npy pattern.npy out.npy --mode add --align-mode repeat-spaced --repeat-spacing 10000 +**Example output:** + +.. code-block:: text + + $ ria combine sample_recording3.npy qam64_35.npy combined.npy + + Combining 2 recordings (concat mode)... + Saved to: combined.npy + .. _cmd-generate: @@ -778,76 +1110,45 @@ Choose exactly one operation per invocation: * - ``ook``, ``oqpsk``, ``gmsk`` - On-off keying and continuous-phase modulation schemes -**Common options shared across all generators:** +**Common options** (all subcommands): -* ``-s, --sample-rate`` (required) -* ``-n, --num-samples`` or ``-t, --duration`` -* ``--frequency-shift``, ``-fc`` / ``--center-frequency`` -* ``--add-noise``, ``--noise-power``, ``--path-gain`` -* ``-o, --output`` (required), ``-F`` / ``--format {npy,sigmf,wav,blue}`` -* ``--multipath-paths``, ``--multipath-max-delay`` -* ``--iq-amp-imbalance``, ``--iq-phase-imbalance``, ``--iq-dc-offset`` -* ``--config `` -* ``-w`` / ``--overwrite``, ``-m`` / ``--metadata KEY=VALUE`` (repeatable) -* ``-v`` / ``--verbose``, ``-q`` / ``--quiet`` +* ``-s, --sample-rate`` (required), ``-n, --num-samples`` or ``-t, --duration`` +* ``-o, --output`` (required), ``-F / --format {npy,sigmf,wav,blue}`` +* ``--frequency-shift``, ``--center-frequency``: separate baseband shape from RF metadata. +* ``--add-noise``, ``--noise-power``, ``--path-gain``: apply noise post-generation. +* ``--multipath-paths``, ``--multipath-max-delay``, ``--iq-amp-imbalance``, + ``--iq-phase-imbalance``, ``--iq-dc-offset``: channel and IQ impairments. +* ``--config ``, ``-w / --overwrite``, ``-m / --metadata KEY=VALUE``, + ``-v / --verbose``, ``-q / --quiet`` -``--frequency-shift`` and ``--center-frequency`` let you separate the baseband shape from -RF metadata context. ``--add-noise`` and ``--noise-power`` apply post-generation noise. -Multipath and IQ imbalance flags apply impairment-style post-processing during generation. +**Subcommand options:** -``tone`` -~~~~~~~~~ +.. list-table:: + :widths: 20 80 + :header-rows: 1 -Options: ``--frequency``, ``--amplitude``, ``--phase`` - -``noise`` -~~~~~~~~~~ - -Options: ``--noise-type {gaussian,uniform}``, ``--power`` - -``chirp`` -~~~~~~~~~~ - -Options: ``--bandwidth`` (required), ``--period`` (required), ``--type {up,down,up_down}`` - -``square`` -~~~~~~~~~~~ - -Options: ``--frequency``, ``--amplitude``, ``--duty-cycle``, ``--phase`` - -``sawtooth`` -~~~~~~~~~~~~~ - -Options: ``--frequency``, ``--amplitude``, ``--phase`` - -Digital modulation families: ``qam``, ``apsk``, ``pam``, ``psk`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -* ``--symbols``, ``--order`` -* ``--symbol-rate`` -* ``--filter {rrc,rc,gaussian,none}``, ``--filter-span``, ``--filter-beta`` -* ``--message-source {random,file,string}``, ``--message-content`` - -Use ``--message-source random`` for synthetic datasets, ``file`` for deterministic replay, -or ``string`` for small human-readable payload testing. Pulse-shaping filter options -(``--filter``, ``--filter-span``, ``--filter-beta``) control spectral occupancy and ISI. - -``fsk`` -~~~~~~~~ - -Options: ``--symbols``, ``--order``, ``--symbol-rate``, ``--freq-spacing``, -``--modulation-index``, ``--message-source``, ``--message-content`` - -``--freq-spacing`` and ``--modulation-index`` drive tone separation and spectral profile. - -``ook``, ``oqpsk``, ``gmsk`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Options: ``--symbol-rate`` (required), ``--message-source {random,file,string}``, -``--message-content``; ``gmsk`` also accepts ``--bt`` - -``gmsk --bt`` sets the Gaussian filter bandwidth-time product (spectral compactness vs -symbol transition sharpness). + * - Subcommand + - Unique options + * - ``tone`` + - ``--frequency``, ``--amplitude``, ``--phase`` + * - ``noise`` + - ``--noise-type {gaussian,uniform}``, ``--power`` + * - ``chirp`` + - ``--bandwidth`` (required), ``--period`` (required), ``--type {up,down,up_down}`` + * - ``square`` + - ``--frequency``, ``--amplitude``, ``--duty-cycle``, ``--phase`` + * - ``sawtooth`` + - ``--frequency``, ``--amplitude``, ``--phase`` + * - ``qam``, ``apsk``, ``pam``, ``psk`` + - ``--order``, ``--symbol-rate``, ``--filter {rrc,rc,gaussian,none}``, + ``--filter-span``, ``--filter-beta``, + ``--message-source {random,file,string}``, ``--message-content`` + * - ``fsk`` + - ``--order``, ``--symbol-rate``, ``--freq-spacing``, ``--modulation-index``, + ``--message-source``, ``--message-content`` + * - ``ook``, ``oqpsk``, ``gmsk`` + - ``--symbol-rate`` (required), ``--message-source``, ``--message-content``; + ``gmsk`` also accepts ``--bt`` (Gaussian filter bandwidth-time product) **Examples:** @@ -859,6 +1160,20 @@ symbol transition sharpness). ria generate qam -s 2e6 -r 100e3 -M 16 -N 5000 --message-source random -o qam16.npy ria synth psk -s 2e6 -r 100e3 -M 8 -N 8000 -o psk8.npy +**Example output:** + +.. code-block:: text + + $ ria generate tone -s 2e6 -n 100000 --frequency 50e3 -o tone.npy + + Generating tone: 50.00 kHz at 2.00 MS/s + +.. code-block:: text + + $ ria generate qam -s 2e6 -n 50000 --order 16 --symbol-rate 100e3 --message-source random -o qam16.npy + + Generating QAM-16 (2500 symbols)... + .. _cmd-transform: @@ -930,6 +1245,41 @@ inspect parameter hints. ``--view`` writes a PNG preview alongside transform out ria transform custom --transform-dir ./my_transforms --list ria transform custom my_filter in.npy out.npy --transform-dir ./my_transforms --params cutoff=0.2 +**Example output:** + +List available augmentations: + +.. code-block:: text + + $ ria transform augment --list + + Available augmentations: + amplitude_reversal Negates the amplitudes of both the I and Q data samples + channel_swap Switches the I (In-phase) with the Q (Quadrature) data samples + cut_out Cuts out random sections of IQ data and replaces them with zeros + drop_samples Randomly drops IQ data samples + generate_awgn Generates additive white gaussian noise relative to the SNR + magnitude_rescale Selects a random starting point and multiplies IQ data by a scalar + patch_shuffle Selects random patches and shuffles the data samples within them + quantize_parts Quantizes random parts of the IQ data by a few bits + quantize_tape Quantizes the IQ data by a few bits + spectral_inversion Negates the imaginary components (Q) of the data samples + time_reversal Reverses the order of I and Q data samples along the time axis + +Apply an impairment (AWGN at SNR=10 dB): + +.. code-block:: text + + $ ria transform impair add_awgn_to_signal sample_recording3.npy sample_recording3_awgn.npy --params snr=10 + + Impairing: sample_recording3.npy → sample_recording3_awgn.npy + Saved to: sample_recording3_awgn.npy + +.. figure:: ../images/recordings/sample_recording3_awgn.png + :alt: sample_recording3.npy after add_awgn_to_signal at SNR=10 dB + + ``sample_recording3.npy`` after ``add_awgn_to_signal --params snr=10`` + .. _cmd-transmit: @@ -948,30 +1298,21 @@ inspect parameter hints. ``--view`` writes a PNG preview alongside transform out ria transmit [options] -**Input source (choose one):** +If neither ``--input`` nor ``--generate`` is specified, the command defaults to a generated +LFM waveform. +**Options:** + +* ``-d`` / ``--device {pluto,hackrf,bladerf,usrp}``, ``-i`` / ``--ident``, ``-c`` / ``--config`` +* ``-s`` / ``--sample-rate``, ``-f`` / ``--center-frequency``, ``-g`` / ``--gain``, ``-b`` / ``--bandwidth`` * ``--input ``: transmit an existing recording. -* ``--generate {lfm,chirp,sine,pulse}``: synthesize a transmit signal on the fly. -* If neither is specified, the command defaults to a generated LFM waveform. - -**Core options:** - -*Device/radio:* ``-d`` / ``--device {pluto,hackrf,bladerf,usrp}``, ``-i`` / ``--ident``, -``-c`` / ``--config`` - -*RF:* ``-s`` / ``--sample-rate``, ``-f`` / ``--center-frequency``, ``-g`` / ``--gain``, -``-b`` / ``--bandwidth`` - -*Input/gen:* ``--input``, ``--legacy``, ``--generate {lfm,chirp,sine,pulse}`` - -*TX control:* - -* ``-r, --repeat`` +* ``--generate {lfm,chirp,sine,pulse}``: synthesize a signal on the fly. +* ``--legacy``: use older NPY loader for historical recordings. +* ``-r, --repeat``: transmit the input a fixed number of times. * ``--continuous``: transmit until interrupted (``Ctrl+C``). * ``--tx-delay``: pause between repeats when ``--repeat`` is used. * ``-y, --yes``: skip confirmation prompts; use carefully in scripted environments. - -*Logging:* ``-v`` / ``--verbose``, ``-q`` / ``--quiet`` +* ``-v`` / ``--verbose``, ``-q`` / ``--quiet`` .. warning:: ``--continuous`` transmits until manually interrupted. Validate gain settings, antenna @@ -985,6 +1326,25 @@ inspect parameter hints. ``--view`` writes a PNG preview alongside transform out ria transmit -d hackrf --generate lfm -f 2.44G --continuous ria transmit -d usrp --input msg.npy -r 3 --tx-delay 0.5 +**Example output:** + +.. note:: + ``transmit`` requires a TX-capable SDR. The following shows representative output for a + PlutoSDR playback. + +.. code-block:: text + + $ ria transmit -d pluto -f 915e6 -s 2e6 --input capture.sigmf-data + + Initializing PlutoSDR... + URI: ip:192.168.2.1 + Center frequency: 915.00 MHz + Sample rate: 2.00 MS/s + Gain: 0 dB + + Transmitting capture.sigmf-data (500,000 samples)... + Transmit complete. + 5) YAML Config Patterns ======================== @@ -1031,25 +1391,7 @@ experiment-specific fields on the CLI. ria generate noise --config generate.yaml -6) Version Notes -================= - -These notes are based on the current implementation and should be re-validated against future -releases. - -1. Some command docstrings and examples still mention ``utils`` or ``ria_toolkit_oss`` - command prefixes in text blocks. The operational command is ``ria ...``. -2. Command bindings currently import ``viewe`` instead of ``view`` in - ``src/ria_toolkit_oss_cli/ria_toolkit_oss/commands.py``. -3. Multiple non-CLI modules still import ``utils.*``, which can create runtime dependency - coupling when using only ``ria-toolkit-oss`` in isolation. - -.. tip:: - If you observe unexpected import errors after install, check the package version and - changelog, then test ``ria --help`` in a clean virtual environment. - - -7) Brief Scripting (Python) Preview +6) Brief Scripting (Python) Preview ===================================== For quick non-CLI use: From c2dc2e6d43b43a154f64a836bcbe84b302399000 Mon Sep 17 00:00:00 2001 From: fordg1 Date: Tue, 5 May 2026 14:35:41 -0400 Subject: [PATCH 08/14] docs: add generated viewer images for getting_started examples --- .gitignore | 3 ++- docs/source/images/recordings/qam64_35-full.png | 3 +++ docs/source/images/recordings/qam64_35.png | 3 +++ docs/source/images/recordings/sample_recording3-full.png | 3 +++ docs/source/images/recordings/sample_recording3.png | 3 +++ docs/source/images/recordings/sample_recording3_annotated.png | 3 +++ docs/source/images/recordings/sample_recording3_awgn.png | 3 +++ docs/source/images/recordings/sample_recording3_cusum.png | 3 +++ docs/source/images/recordings/sample_recording3_threshold.png | 3 +++ .../images/recordings/sample_recording5_after_separate.png | 3 +++ .../images/recordings/sample_recording5_before_separate.png | 3 +++ 11 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 docs/source/images/recordings/qam64_35-full.png create mode 100644 docs/source/images/recordings/qam64_35.png create mode 100644 docs/source/images/recordings/sample_recording3-full.png create mode 100644 docs/source/images/recordings/sample_recording3.png create mode 100644 docs/source/images/recordings/sample_recording3_annotated.png create mode 100644 docs/source/images/recordings/sample_recording3_awgn.png create mode 100644 docs/source/images/recordings/sample_recording3_cusum.png create mode 100644 docs/source/images/recordings/sample_recording3_threshold.png create mode 100644 docs/source/images/recordings/sample_recording5_after_separate.png create mode 100644 docs/source/images/recordings/sample_recording5_before_separate.png diff --git a/.gitignore b/.gitignore index b2ac7f1..40c654a 100644 --- a/.gitignore +++ b/.gitignore @@ -99,4 +99,5 @@ cython_debug/ *.sigmf-meta *.blue *.wav -images/ +/images/ +!docs/source/images/** diff --git a/docs/source/images/recordings/qam64_35-full.png b/docs/source/images/recordings/qam64_35-full.png new file mode 100644 index 0000000..f515d56 --- /dev/null +++ b/docs/source/images/recordings/qam64_35-full.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d6da86a26286bd90d8490896314265ab2cfdd4023eddc99af0a7b016537f32a6 +size 1800657 diff --git a/docs/source/images/recordings/qam64_35.png b/docs/source/images/recordings/qam64_35.png new file mode 100644 index 0000000..d603eda --- /dev/null +++ b/docs/source/images/recordings/qam64_35.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7caf225dca79d4f82fa75739773b7706586787573f7dc551c3b86128c99d0d26 +size 1396884 diff --git a/docs/source/images/recordings/sample_recording3-full.png b/docs/source/images/recordings/sample_recording3-full.png new file mode 100644 index 0000000..dea542f --- /dev/null +++ b/docs/source/images/recordings/sample_recording3-full.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9cb29f6d572674dde3304e8241ba6874ecae8b3e9567314198af46a0d054a29 +size 6839769 diff --git a/docs/source/images/recordings/sample_recording3.png b/docs/source/images/recordings/sample_recording3.png new file mode 100644 index 0000000..82cc563 --- /dev/null +++ b/docs/source/images/recordings/sample_recording3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:526afa9fbcc00be07972a5193fb21c1fb3f975512c0ab71517062e8fb091a855 +size 2769511 diff --git a/docs/source/images/recordings/sample_recording3_annotated.png b/docs/source/images/recordings/sample_recording3_annotated.png new file mode 100644 index 0000000..c6367c1 --- /dev/null +++ b/docs/source/images/recordings/sample_recording3_annotated.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbe7e40924e8551e9878bcaa1a98379b2cfb071e7314475ed441fa24b95e5f4f +size 5018447 diff --git a/docs/source/images/recordings/sample_recording3_awgn.png b/docs/source/images/recordings/sample_recording3_awgn.png new file mode 100644 index 0000000..5687f7d --- /dev/null +++ b/docs/source/images/recordings/sample_recording3_awgn.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07e3d72ea90838232a5856b16c8eadc1aa11023f19b35390d63951e1f1b85042 +size 2858110 diff --git a/docs/source/images/recordings/sample_recording3_cusum.png b/docs/source/images/recordings/sample_recording3_cusum.png new file mode 100644 index 0000000..be08b3d --- /dev/null +++ b/docs/source/images/recordings/sample_recording3_cusum.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bbf4664f420374d253800f68755aae532cd6472c1353741cbb2ea1ed82c5a03 +size 5020157 diff --git a/docs/source/images/recordings/sample_recording3_threshold.png b/docs/source/images/recordings/sample_recording3_threshold.png new file mode 100644 index 0000000..0511019 --- /dev/null +++ b/docs/source/images/recordings/sample_recording3_threshold.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfb604e93c467074a986d03bdc833a470f7259de5614a0f54d35a91153ab5d1c +size 5029060 diff --git a/docs/source/images/recordings/sample_recording5_after_separate.png b/docs/source/images/recordings/sample_recording5_after_separate.png new file mode 100644 index 0000000..1209e8b --- /dev/null +++ b/docs/source/images/recordings/sample_recording5_after_separate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08f7db98c90f464cb587d37772448fa76977459b4d09c66e0182065e4d74aa15 +size 4465342 diff --git a/docs/source/images/recordings/sample_recording5_before_separate.png b/docs/source/images/recordings/sample_recording5_before_separate.png new file mode 100644 index 0000000..6d574cb --- /dev/null +++ b/docs/source/images/recordings/sample_recording5_before_separate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66d5e9b528c1e7527e3ed3689e05ee94cd4df93a0c63e4884283dd15eb0eced7 +size 4460772 From 3b8b55ae7a6fbd9f2df7357e5c39dfb11170624c Mon Sep 17 00:00:00 2001 From: fordg1 Date: Tue, 12 May 2026 12:19:00 -0400 Subject: [PATCH 09/14] Fix flake8 E501 line too long in annotate.py separate() signature Co-Authored-By: Claude Sonnet 4.6 --- src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py index d551d2c..d8024eb 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py @@ -782,7 +782,8 @@ def threshold(input, threshold, label, window_size, annotation_type, channel, sa @click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") @click.option("--quiet", is_flag=True, help="Quiet mode") @click.option("--verbose", is_flag=True, help="Verbose output (show detected components)") -def separate(input, indices, nfft, noise_threshold_db, min_component_bw, sample_rate, output, overwrite, quiet, verbose): +def separate( + input, indices, nfft, noise_threshold_db, min_component_bw, sample_rate, output, overwrite, quiet, verbose): """ Auto-detect parallel frequency-offset signals and split into sub-bands. From f23bac08a17da61e90a88aacace90e85cdb078d0 Mon Sep 17 00:00:00 2001 From: fordg1 Date: Tue, 12 May 2026 13:10:40 -0400 Subject: [PATCH 10/14] Fix flake8 E501 and C901 violations in annotate.py and annotation classes Co-Authored-By: Claude Sonnet 4.6 --- .../annotations/cusum_annotator.py | 3 +- .../annotations/energy_detector.py | 3 +- .../annotations/parallel_signal_separator.py | 3 +- .../annotations/threshold_qualifier.py | 3 +- .../ria_toolkit_oss/annotate.py | 46 +++++++++++++------ 5 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/ria_toolkit_oss/annotations/cusum_annotator.py b/src/ria_toolkit_oss/annotations/cusum_annotator.py index 9556125..22ca4c4 100644 --- a/src/ria_toolkit_oss/annotations/cusum_annotator.py +++ b/src/ria_toolkit_oss/annotations/cusum_annotator.py @@ -42,7 +42,8 @@ def annotate_with_cusum( if sample_rate is None: raise ValueError( "Recording metadata does not contain 'sample_rate'. " - "Supply it with --sample-rate when using the CLI, or set recording.sample_rate before calling this function." + "Supply it with --sample-rate when using the CLI, or set " + "recording.sample_rate before calling this function." ) center_frequency = recording.metadata.get("center_frequency", 0) diff --git a/src/ria_toolkit_oss/annotations/energy_detector.py b/src/ria_toolkit_oss/annotations/energy_detector.py index 1a482bc..7a554c0 100644 --- a/src/ria_toolkit_oss/annotations/energy_detector.py +++ b/src/ria_toolkit_oss/annotations/energy_detector.py @@ -151,7 +151,8 @@ def detect_signals_energy( if sample_rate is None: raise ValueError( "Recording metadata does not contain 'sample_rate'. " - "Supply it with --sample-rate when using the CLI, or set recording.sample_rate before calling this function." + "Supply it with --sample-rate when using the CLI, or set " + "recording.sample_rate before calling this function." ) center_frequency = recording.metadata.get("center_frequency", 0) diff --git a/src/ria_toolkit_oss/annotations/parallel_signal_separator.py b/src/ria_toolkit_oss/annotations/parallel_signal_separator.py index 4e08353..2838ede 100644 --- a/src/ria_toolkit_oss/annotations/parallel_signal_separator.py +++ b/src/ria_toolkit_oss/annotations/parallel_signal_separator.py @@ -406,7 +406,8 @@ def split_recording_annotations( if sample_rate is None: raise ValueError( "Recording metadata does not contain 'sample_rate'. " - "Supply it with --sample-rate when using the CLI, or set recording.sample_rate before calling this function." + "Supply it with --sample-rate when using the CLI, or set " + "recording.sample_rate before calling this function." ) center_frequency = recording.metadata.get("center_frequency", 0.0) diff --git a/src/ria_toolkit_oss/annotations/threshold_qualifier.py b/src/ria_toolkit_oss/annotations/threshold_qualifier.py index 24ef833..d8701ca 100644 --- a/src/ria_toolkit_oss/annotations/threshold_qualifier.py +++ b/src/ria_toolkit_oss/annotations/threshold_qualifier.py @@ -221,7 +221,8 @@ def threshold_qualifier( if sample_rate is None: raise ValueError( "Recording metadata does not contain 'sample_rate'. " - "Supply it with --sample-rate when using the CLI, or set recording.sample_rate before calling this function." + "Supply it with --sample-rate when using the CLI, or set " + "recording.sample_rate before calling this function." ) center_frequency = recording.metadata.get("center_frequency", 0) diff --git a/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py b/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py index d8024eb..6477d40 100644 --- a/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py +++ b/src/ria_toolkit_oss_cli/ria_toolkit_oss/annotate.py @@ -501,7 +501,10 @@ def clear(input, output, overwrite, force, quiet): default="standalone", help="Annotation type", ) -@click.option("--sample-rate", type=float, default=None, help="Sample rate in Hz (overrides metadata; required if not in file)") +@click.option( + "--sample-rate", type=float, default=None, + help="Sample rate in Hz (overrides metadata; required if not in file)" +) @click.option("--output", "-o", type=click.Path(), help="Output file path") @click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") @click.option("--quiet", is_flag=True, help="Quiet mode") @@ -613,7 +616,10 @@ def energy( default="standalone", help="Annotation type", ) -@click.option("--sample-rate", type=float, default=None, help="Sample rate in Hz (overrides metadata; required if not in file)") +@click.option( + "--sample-rate", type=float, default=None, + help="Sample rate in Hz (overrides metadata; required if not in file)" +) @click.option("--output", "-o", type=click.Path(), help="Output file path") @click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") @click.option("--quiet", is_flag=True, help="Quiet mode") @@ -700,7 +706,10 @@ def cusum(input, label, min_duration, window_size, tolerance, annotation_type, s help="Annotation type", ) @click.option("--channel", type=int, default=0, help="Channel index to annotate (default: 0)") -@click.option("--sample-rate", type=float, default=None, help="Sample rate in Hz (overrides metadata; required if not in file)") +@click.option( + "--sample-rate", type=float, default=None, + help="Sample rate in Hz (overrides metadata; required if not in file)" +) @click.option("--output", "-o", type=click.Path(), help="Output file path") @click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") @click.option("--quiet", is_flag=True, help="Quiet mode") @@ -777,11 +786,28 @@ def threshold(input, threshold, label, window_size, annotation_type, channel, sa @click.option("--nfft", type=int, default=65536, help="FFT size for spectral analysis") @click.option("--noise-threshold-db", type=float, help="Noise floor threshold in dB (auto-estimated if not specified)") @click.option("--min-component-bw", type=float, default=50e3, help="Min component bandwidth in Hz") -@click.option("--sample-rate", type=float, default=None, help="Sample rate in Hz (overrides metadata; required if not in file)") +@click.option( + "--sample-rate", type=float, default=None, + help="Sample rate in Hz (overrides metadata; required if not in file)" +) @click.option("--output", "-o", type=click.Path(), help="Output file path") @click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)") @click.option("--quiet", is_flag=True, help="Quiet mode") @click.option("--verbose", is_flag=True, help="Verbose output (show detected components)") +def _log_separate_start(quiet, recording, indices_list, nfft, noise_threshold_db, min_component_bw): + if not quiet: + click.echo("\nSplitting annotations by frequency components...") + click.echo(f" Input annotations: {len(recording.annotations)}") + if indices_list: + click.echo(f" Splitting indices: {indices_list}") + click.echo(f" FFT size: {nfft}") + if noise_threshold_db is not None: + click.echo(f" Noise threshold: {noise_threshold_db} dB") + else: + click.echo(" Noise threshold: auto-estimated") + click.echo(f" Min component BW: {format_frequency(min_component_bw)}") + + def separate( input, indices, nfft, noise_threshold_db, min_component_bw, sample_rate, output, overwrite, quiet, verbose): """ @@ -835,17 +861,7 @@ def separate( click.echo("No annotations to split") return - if not quiet: - click.echo("\nSplitting annotations by frequency components...") - click.echo(f" Input annotations: {len(recording.annotations)}") - if indices_list: - click.echo(f" Splitting indices: {indices_list}") - click.echo(f" FFT size: {nfft}") - if noise_threshold_db is not None: - click.echo(f" Noise threshold: {noise_threshold_db} dB") - else: - click.echo(" Noise threshold: auto-estimated") - click.echo(f" Min component BW: {format_frequency(min_component_bw)}") + _log_separate_start(quiet, recording, indices_list, nfft, noise_threshold_db, min_component_bw) try: initial_count = len(recording.annotations) From 70c790cadd25b4951495f299c63a8cc77fb3c6e9 Mon Sep 17 00:00:00 2001 From: gillian Date: Tue, 12 May 2026 13:22:08 -0400 Subject: [PATCH 11/14] Drop Python 3.10 support, minimum is now 3.11 Co-Authored-By: Claude Sonnet 4.6 --- .readthedocs.yaml | 2 +- .riahub/workflows/build-project.yaml | 2 +- .riahub/workflows/tox.yaml | 2 +- README.md | 2 +- pyproject.toml | 4 ++-- tox.ini | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index a6ab77b..9965c06 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,7 +8,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.10" + python: "3.11" jobs: post_create_environment: # Install poetry diff --git a/.riahub/workflows/build-project.yaml b/.riahub/workflows/build-project.yaml index 1651656..2cbd258 100644 --- a/.riahub/workflows/build-project.yaml +++ b/.riahub/workflows/build-project.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ '3.10', '3.11', '3.12' ] + python-version: [ '3.11', '3.12' ] name: Build Project steps: diff --git a/.riahub/workflows/tox.yaml b/.riahub/workflows/tox.yaml index 84b0f6b..fc31def 100644 --- a/.riahub/workflows/tox.yaml +++ b/.riahub/workflows/tox.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.10', '3.11', '3.12'] + python-version: ['3.11', '3.12'] name: Test with tox steps: diff --git a/README.md b/README.md index d499a75..30d5c32 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ - Python Version + Python Version

diff --git a/pyproject.toml b/pyproject.toml index 48a9e1c..1acd4f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.5" description = "An open-source version of the RIA Toolkit, including the fundamental tools to get started developing, testing, and deploying radio intelligence applications" license = { text = "AGPL-3.0-only" } readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.11" authors = [ { name = "Qoherent Inc.", email = "info@qoherent.ai" }, ] @@ -128,7 +128,7 @@ onnxruntime = {version = ">=1.17,<2.0", python = ">=3.11"} [tool.black] line-length = 119 -target-version = ["py310"] +target-version = ["py311"] exclude = ''' /( \.git diff --git a/tox.ini b/tox.ini index 107b46b..f7a3827 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] isolated_build = true -envlist = py310, py311, py312, lint +envlist = py311, py312, lint skipsdist = true [testenv] @@ -30,6 +30,6 @@ per-file-ignores = __init__.py:F401 [gh-actions] python = - 3.10: py310, lint + 3.11: py311, lint 3.11: py311 3.12: py312 From 657dd0d499bb5af6fddfe5ef07c7ee315bf2e105 Mon Sep 17 00:00:00 2001 From: gillian Date: Tue, 12 May 2026 13:32:52 -0400 Subject: [PATCH 12/14] Fix duplicate 3.11 key in tox gh-actions config Co-Authored-By: Claude Sonnet 4.6 --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index f7a3827..ffd59ee 100644 --- a/tox.ini +++ b/tox.ini @@ -31,5 +31,4 @@ per-file-ignores = __init__.py:F401 [gh-actions] python = 3.11: py311, lint - 3.11: py311 3.12: py312 From 57d1d6e55ea2cf51142ffe4823617ea71a2c3f27 Mon Sep 17 00:00:00 2001 From: gillian Date: Tue, 12 May 2026 13:39:05 -0400 Subject: [PATCH 13/14] Revert "Fix duplicate 3.11 key in tox gh-actions config" This reverts commit 657dd0d499bb5af6fddfe5ef07c7ee315bf2e105. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index ffd59ee..f7a3827 100644 --- a/tox.ini +++ b/tox.ini @@ -31,4 +31,5 @@ per-file-ignores = __init__.py:F401 [gh-actions] python = 3.11: py311, lint + 3.11: py311 3.12: py312 From 7ef3fe8fb106eacbaf8090ce07f31972833e6d57 Mon Sep 17 00:00:00 2001 From: gillian Date: Tue, 12 May 2026 13:39:05 -0400 Subject: [PATCH 14/14] Revert "Drop Python 3.10 support, minimum is now 3.11" This reverts commit 70c790cadd25b4951495f299c63a8cc77fb3c6e9. --- .readthedocs.yaml | 2 +- .riahub/workflows/build-project.yaml | 2 +- .riahub/workflows/tox.yaml | 2 +- README.md | 2 +- pyproject.toml | 4 ++-- tox.ini | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 9965c06..a6ab77b 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,7 +8,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.11" + python: "3.10" jobs: post_create_environment: # Install poetry diff --git a/.riahub/workflows/build-project.yaml b/.riahub/workflows/build-project.yaml index 2cbd258..1651656 100644 --- a/.riahub/workflows/build-project.yaml +++ b/.riahub/workflows/build-project.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ '3.11', '3.12' ] + python-version: [ '3.10', '3.11', '3.12' ] name: Build Project steps: diff --git a/.riahub/workflows/tox.yaml b/.riahub/workflows/tox.yaml index fc31def..84b0f6b 100644 --- a/.riahub/workflows/tox.yaml +++ b/.riahub/workflows/tox.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.11', '3.12'] + python-version: ['3.10', '3.11', '3.12'] name: Test with tox steps: diff --git a/README.md b/README.md index 30d5c32..d499a75 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ - Python Version + Python Version

diff --git a/pyproject.toml b/pyproject.toml index 1acd4f0..48a9e1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.5" description = "An open-source version of the RIA Toolkit, including the fundamental tools to get started developing, testing, and deploying radio intelligence applications" license = { text = "AGPL-3.0-only" } readme = "README.md" -requires-python = ">=3.11" +requires-python = ">=3.10" authors = [ { name = "Qoherent Inc.", email = "info@qoherent.ai" }, ] @@ -128,7 +128,7 @@ onnxruntime = {version = ">=1.17,<2.0", python = ">=3.11"} [tool.black] line-length = 119 -target-version = ["py311"] +target-version = ["py310"] exclude = ''' /( \.git diff --git a/tox.ini b/tox.ini index f7a3827..107b46b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] isolated_build = true -envlist = py311, py312, lint +envlist = py310, py311, py312, lint skipsdist = true [testenv] @@ -30,6 +30,6 @@ per-file-ignores = __init__.py:F401 [gh-actions] python = - 3.11: py311, lint + 3.10: py310, lint 3.11: py311 3.12: py312