Compare commits

..

17 Commits

Author SHA1 Message Date
c15b79b43f Merge pull request 'docs: improve getting_started and installation readability' (#30) from docs/sdr-guides-update into main
Some checks failed
Build Sphinx Docs Set / Build Docs (push) Successful in 36s
Build Project / Build Project (3.10) (push) Successful in 13m31s
Build Project / Build Project (3.12) (push) Successful in 13m29s
Build Project / Build Project (3.11) (push) Successful in 13m35s
Test with tox / Test with tox (3.11) (push) Successful in 14m39s
Test with tox / Test with tox (3.12) (push) Successful in 14m23s
Test with tox / Test with tox (3.10) (push) Failing after 14m56s
Reviewed-on: #30
Reviewed-by: madrigal <madrigal@qoherent.ai>
2026-05-12 14:05:24 -04:00
7ef3fe8fb1 Revert "Drop Python 3.10 support, minimum is now 3.11"
Some checks failed
Test with tox / Test with tox (3.11) (pull_request) Successful in 15m58s
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 18m3s
Build Project / Build Project (3.10) (pull_request) Successful in 21m51s
Test with tox / Test with tox (3.10) (pull_request) Failing after 21m59s
Build Project / Build Project (3.11) (pull_request) Successful in 22m37s
Build Project / Build Project (3.12) (pull_request) Successful in 22m35s
Test with tox / Test with tox (3.12) (pull_request) Successful in 6m53s
This reverts commit 70c790cadd.
2026-05-12 13:39:05 -04:00
57d1d6e55e Revert "Fix duplicate 3.11 key in tox gh-actions config"
This reverts commit 657dd0d499.
2026-05-12 13:39:05 -04:00
657dd0d499 Fix duplicate 3.11 key in tox gh-actions config
Some checks failed
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 32s
Test with tox / Test with tox (3.11) (pull_request) Failing after 39s
Test with tox / Test with tox (3.12) (pull_request) Failing after 20s
Build Project / Build Project (3.12) (pull_request) Successful in 1m16s
Build Project / Build Project (3.11) (pull_request) Successful in 1m18s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 13:32:52 -04:00
70c790cadd Drop Python 3.10 support, minimum is now 3.11
Some checks failed
Test with tox / Test with tox (3.11) (pull_request) Failing after 6m28s
Test with tox / Test with tox (3.12) (pull_request) Failing after 6m27s
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 10m49s
Build Project / Build Project (3.11) (pull_request) Successful in 11m27s
Build Project / Build Project (3.12) (pull_request) Successful in 11m29s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 13:22:08 -04:00
F fordg1
f23bac08a1 Fix flake8 E501 and C901 violations in annotate.py and annotation classes
Some checks failed
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 48s
Build Project / Build Project (3.12) (pull_request) Successful in 4m10s
Build Project / Build Project (3.10) (pull_request) Successful in 4m24s
Build Project / Build Project (3.11) (pull_request) Successful in 4m29s
Test with tox / Test with tox (3.11) (pull_request) Successful in 4m23s
Test with tox / Test with tox (3.10) (pull_request) Failing after 4m44s
Test with tox / Test with tox (3.12) (pull_request) Successful in 4m45s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 13:10:40 -04:00
F fordg1
3b8b55ae7a Fix flake8 E501 line too long in annotate.py separate() signature
Some checks failed
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 1m43s
Build Project / Build Project (3.10) (pull_request) Successful in 23m32s
Build Project / Build Project (3.12) (pull_request) Successful in 23m41s
Test with tox / Test with tox (3.10) (pull_request) Failing after 23m43s
Build Project / Build Project (3.11) (pull_request) Successful in 23m50s
Test with tox / Test with tox (3.11) (pull_request) Successful in 24m31s
Test with tox / Test with tox (3.12) (pull_request) Successful in 23m9s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 12:19:00 -04:00
F fordg1
c2dc2e6d43 docs: add generated viewer images for getting_started examples
Some checks failed
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 23m0s
Test with tox / Test with tox (3.11) (pull_request) Successful in 29m19s
Build Project / Build Project (3.10) (pull_request) Successful in 30m13s
Build Project / Build Project (3.12) (pull_request) Successful in 30m17s
Build Project / Build Project (3.11) (pull_request) Successful in 30m24s
Test with tox / Test with tox (3.12) (pull_request) Successful in 8m30s
Test with tox / Test with tox (3.10) (pull_request) Failing after 5m13s
2026-05-05 14:35:41 -04:00
F fordg1
18666d95ee docs: expand getting_started with real command output, examples, and images
- 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
2026-05-05 14:31:42 -04:00
04099fdbac Merge branch 'docs/sdr-guides-update' of https://riahub.ai/qoherent/ria-toolkit-oss into docs/sdr-guides-update
Some checks failed
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 1m31s
Test with tox / Test with tox (3.11) (pull_request) Successful in 12m45s
Build Project / Build Project (3.12) (pull_request) Successful in 15m4s
Test with tox / Test with tox (3.10) (pull_request) Failing after 15m7s
Build Project / Build Project (3.10) (pull_request) Successful in 15m14s
Build Project / Build Project (3.11) (pull_request) Successful in 15m17s
Test with tox / Test with tox (3.12) (pull_request) Successful in 14m32s
2026-04-30 10:31:42 -04:00
4ce42fa71a Formatting, updated lock file
All checks were successful
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 34s
Build Project / Build Project (3.10) (pull_request) Successful in 1m42s
Build Project / Build Project (3.11) (pull_request) Successful in 1m41s
Build Project / Build Project (3.12) (pull_request) Successful in 1m46s
Test with tox / Test with tox (3.11) (pull_request) Successful in 2m47s
Test with tox / Test with tox (3.12) (pull_request) Successful in 2m32s
Test with tox / Test with tox (3.10) (pull_request) Successful in 3m1s
2026-04-29 09:55:29 -04:00
0a1bef8453 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
2026-04-28 16:31:35 -04:00
e5a3d327e5 refactor: unify signal viewer styling and update docs screenshots
Some checks failed
Test with tox / Test with tox (3.11) (pull_request) Successful in 3m37s
Test with tox / Test with tox (3.12) (pull_request) Successful in 3m44s
Build Project / Build Project (3.10) (pull_request) Successful in 5m55s
Build Project / Build Project (3.11) (pull_request) Successful in 5m35s
Build Project / Build Project (3.12) (pull_request) Successful in 6m27s
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 6m22s
Test with tox / Test with tox (3.10) (pull_request) Failing after 5m33s
- Align view_simple and view_full on background colour (#161616), title
  size (25pt), subtitle size (15pt), base font/tick/label sizes, grid
  style (alpha=0.2), and legend fontsize (10pt)
- Spectrogram placed above IQ plot in view_simple; subplot renamed from
  "Time Series" to "IQ Sample Plot"
- Frequency and spectrogram Y-axes formatted in MHz across both viewers
- Added xlabel/ylabel, subtle grids, and IQ legend to view_full subplots
- Fixed spectrogram right-side clipping in view_simple by syncing xlim
  from specgram output rather than total signal duration
- Updated getting_started.rst to reference both simple and full viewer
  screenshots; replaced doc images with latest renders
2026-04-28 14:08:44 -04:00
4c94f6ae94 Changed datasets to data to match utils 2026-04-28 12:49:43 -04:00
F fordg1
9a304faa00 docs: enhance getting started guide with example output and image reference
Some checks failed
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 22s
Build Project / Build Project (3.10) (pull_request) Failing after 3m19s
Test with tox / Test with tox (3.11) (pull_request) Failing after 6m16s
Test with tox / Test with tox (3.10) (pull_request) Failing after 11m32s
Build Project / Build Project (3.11) (pull_request) Successful in 12m4s
Build Project / Build Project (3.12) (pull_request) Successful in 12m2s
Test with tox / Test with tox (3.12) (pull_request) Successful in 12m26s
2026-04-28 11:27:47 -04:00
a4cd158b2a Merge branch 'main' into docs/sdr-guides-update
All checks were successful
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 6m36s
Build Project / Build Project (3.10) (pull_request) Successful in 3m50s
Build Project / Build Project (3.12) (pull_request) Successful in 3m38s
Build Project / Build Project (3.11) (pull_request) Successful in 4m0s
Test with tox / Test with tox (3.10) (pull_request) Successful in 5m16s
Test with tox / Test with tox (3.11) (pull_request) Successful in 5m33s
Test with tox / Test with tox (3.12) (pull_request) Successful in 5m24s
2026-04-24 14:36:53 -04:00
2c1fba75da docs: improve getting_started and installation readability
All checks were successful
Build Sphinx Docs Set / Build Docs (pull_request) Successful in 35s
Test with tox / Test with tox (3.11) (pull_request) Successful in 14m53s
Build Project / Build Project (3.12) (pull_request) Successful in 21m54s
Test with tox / Test with tox (3.10) (pull_request) Successful in 22m0s
Build Project / Build Project (3.11) (pull_request) Successful in 22m7s
Build Project / Build Project (3.10) (pull_request) Successful in 22m16s
Test with tox / Test with tox (3.12) (pull_request) Successful in 22m42s
- 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
2026-04-24 14:34:11 -04:00
30 changed files with 1394 additions and 878 deletions

3
.gitignore vendored
View File

@ -99,4 +99,5 @@ cython_debug/
*.sigmf-meta
*.blue
*.wav
images/
/images/
!docs/source/images/**

View File

@ -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 <https://pypi.org/project/ria-toolkit-oss/>`_
* `RIA Hub Conda package <https://riahub.ai/qoherent/-/packages/conda/ria-toolkit-oss>`_
.. 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 <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 <cmd-discover>`
- Probe SDR drivers and enumerate attached hardware
* - :ref:`init <cmd-init>`
- Create and manage user metadata defaults
* - :ref:`capture <cmd-capture>`
- Record IQ samples from a connected SDR
* - :ref:`view <cmd-view>`
- Generate visualizations from IQ files
* - :ref:`annotate <cmd-annotate>`
- Label signal regions manually or with auto-detection (group)
* - :ref:`convert <cmd-convert>`
- Convert between IQ file formats
* - :ref:`split <cmd-split>`
- Split, trim, or extract recordings
* - :ref:`combine <cmd-combine>`
- Merge multiple recordings by concatenation or addition
* - :ref:`generate / synth <cmd-generate>`
- Generate synthetic IQ signals (group; ``synth`` is an alias)
* - :ref:`transform <cmd-transform>`
- Apply augmentations or impairments to recordings (group)
* - :ref:`transmit <cmd-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 <subcommand> ...
**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> [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

View File

@ -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;
}

BIN
docs/source/images/recordings/qam64_35-full.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/source/images/recordings/qam64_35.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/source/images/recordings/sample_recording3-full.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/source/images/recordings/sample_recording3.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

BIN
docs/source/images/recordings/sample_recording3_awgn.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/source/images/recordings/sample_recording3_cusum.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -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 <sdr_guides>` section of the documentation for addition setup instructions.
:ref:`SDR Guides <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.

View File

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

381
poetry.lock generated
View File

@ -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]

View File

@ -38,7 +38,13 @@ 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

View File

@ -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,13 @@ 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 +369,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

View File

@ -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,13 @@ 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 +432,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

View File

@ -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)

View File

@ -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:

View File

@ -42,6 +42,7 @@ classification or demodulation stages.
"""
import json
import warnings
from typing import Optional
import numpy as np
@ -216,11 +217,22 @@ 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 +249,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 +314,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-

View File

@ -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:

View File

@ -32,16 +32,15 @@ def extract_metadata_fields(metadata):
def set_path(output_path):
split_path = output_path.split("/")
path = pathlib.Path(output_path)
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])
# 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

View File

@ -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,26 @@ 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 +268,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 +296,10 @@ 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,14 @@ 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 +350,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 +399,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

View File

@ -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,25 @@ 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,
)
yticks = ax2.get_yticks()
ax2.set_yticklabels([f"{y / 1e6:.1f}" for y in yticks])
ax2.yaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(lambda x, _: f"{x / 1e6:.1f}"))
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 +338,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 +350,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 +366,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

View File

@ -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,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("--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 +519,7 @@ def energy(
nfft,
obw_power,
annotation_type,
sample_rate,
output,
overwrite,
quiet,
@ -539,8 +542,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 +554,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 +590,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 +616,14 @@ 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 +635,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 +646,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 +675,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 +706,14 @@ 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 +723,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 +737,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 +767,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 +786,30 @@ 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 _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):
"""
Auto-detect parallel frequency-offset signals and split into sub-bands.
@ -768,6 +835,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 +844,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)
@ -783,17 +861,7 @@ def separate(input, indices, nfft, noise_threshold_db, min_component_bw, output,
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)
@ -821,8 +889,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")