Compare commits

..

No commits in common. "main" and "zfp-oss" have entirely different histories.

45 changed files with 1297 additions and 2224 deletions

3
.gitignore vendored
View File

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

View File

@ -1,24 +1,5 @@
# Changelog
## [0.1.7] - 2026-05-26
### Added
- **Human-readable agent names**`ria-agent register` now generates a default `adjective-colour-animal` name (e.g. `swift-teal-falcon`) via the new `namegen` module when `--name` is omitted, instead of registering with an empty string.
- **Structured registration error messages**`ria-agent register` translates hub responses into actionable English for the known failure reasons (`invalid_key`, `expired`, `revoked`, `already_consumed`) and rate-limit (`HTTP 429`) responses, instead of surfacing raw `HTTP 4xx` text.
### Changed
- **`ria-agent register` `--api-key` help** — now describes the personal `ria_reg_*` registration key flow (minted from **Settings → RIA Agents** on the hub, shown once at mint time). The legacy shared `[wac] API_KEY` is still accepted by the hub for back-compat, but the CLI documents the per-user flow as preferred.
- **`ria-agent register` success output** — now prints both the hub-assigned agent ID and the chosen name: `Registered agent: <id> (<name>)`.
### Fixed
- **`ria-agent register` blocked by Cloudflare on hubs behind it** — set an explicit `User-Agent` (`ria-agent/<package-version> (+https://riahub.ai/qoherent/ria-toolkit-oss)`) so the request isn't rejected as `Python-urllib/<ver>` (Cloudflare Browser Integrity Check returns HTTP 403, edge error code 1010). Version is read from package metadata so it tracks releases automatically.
- **`ria-agent register` could hang indefinitely** — added a 15-second timeout to the hub request; previously `urllib`'s default of no timeout meant a stuck hub would block the CLI forever.
---
## [0.1.0] - 2026-02-20
### Added

View File

@ -185,7 +185,7 @@ RIA Toolkit OSS is developed and maintained by [Qoherent](https://qoherent.ai/),
If you are doing research with RIA Toolkit OSS, please cite the project:
```
[1] Qoherent Inc., "Radio Intelligence Apps Toolkit OSS," 2026. [Online]. Available: https://riahub.ai/qoherent/ria-toolkit-oss
[1] Qoherent Inc., "Radio Intelligence Apps Toolkit OSS," 2025. [Online]. Available: https://riahub.ai/qoherent/ria-toolkit-oss
```
If you like what we're doing, don't forget to give the project a star! ⭐

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 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
* Installation and setup
* End-to-end CLI workflows
* Full command reference for CLI features
* Brief scripting section
**Official resources:**
@ -18,15 +18,76 @@ 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
==========================
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.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
1.1 SDR driver prerequisites
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
-----------------------------
Toolkit package install does not install all system SDR drivers. Install vendor/runtime
@ -34,22 +95,11 @@ dependencies for the hardware you use.
Examples (depends on device and 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
* 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.
@ -69,34 +119,18 @@ Top-level CLI follows this model:
**Top-level commands:**
.. 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
* ``discover``
* ``init``
* ``capture``
* ``view``
* ``annotate`` (group)
* ``convert``
* ``split``
* ``combine``
* ``generate`` (group)
* ``transform`` (group)
* ``transmit``
* ``synth`` (alias of ``generate`` in command bindings)
3) Quick End-to-End Workflow
@ -124,8 +158,10 @@ provenance fields.
.. code-block:: bash
ria init
ria init --author "Jane Doe" --project "rf-campaign-1" --location "Lab-A" # non-interactive
ria init --show # show config
# or non-interactive
ria init --author "Jane Doe" --project "rf-campaign-1" --location "Lab-A"
# show config
ria init --show
3.3 Capture IQ
@ -191,14 +227,13 @@ 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
ria transmit -d hackrf --generate lfm --continuous # generated waveform
# or generated waveform
ria transmit -d hackrf --generate lfm --continuous
4) Command Reference
=====================
.. _cmd-discover:
4.1 ``discover``
-----------------
@ -228,8 +263,6 @@ Replay recorded or synthesized IQ through a transmit-capable SDR.
hidden in default output.
.. _cmd-init:
4.2 ``init``
-------------
@ -276,8 +309,6 @@ Replay recorded or synthesized IQ through a transmit-capable SDR.
generate metadata, and YAML config loading paths).
.. _cmd-capture:
4.3 ``capture``
----------------
@ -351,8 +382,6 @@ Device selection (``--device``) is optional if only one device is detected. Exac
ria capture -c capture_config.yaml
.. _cmd-view:
4.4 ``view``
-------------
@ -413,21 +442,7 @@ 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
-----------------------
@ -444,30 +459,8 @@ Device selection (``--device``) is optional if only one device is detected. Exac
ria annotate <subcommand> ...
**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
**Subcommands:** ``list``, ``add``, ``remove``, ``clear``, ``energy``, ``cusum``,
``threshold``, ``separate``
**General behavior:**
@ -594,16 +587,8 @@ 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``
----------------
@ -644,8 +629,6 @@ inferred from the output file extension.
ria convert old.npy --format sigmf --legacy --overwrite
.. _cmd-split:
4.7 ``split``
--------------
@ -687,8 +670,6 @@ Choose exactly one operation per invocation:
ria split annotated.sigmf-data --extract-annotations --annotation-label payload
.. _cmd-combine:
4.8 ``combine``
----------------
@ -736,8 +717,6 @@ 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)
---------------------------------------------
@ -749,34 +728,15 @@ Choose exactly one operation per invocation:
``ria synth ...`` is an alias for ``ria generate ...``.
**Usage:**
**Shape:**
.. code-block:: bash
ria generate <subcommand> [subcommand options] [common options]
**Available subcommands:**
.. 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
``tone``, ``noise``, ``chirp``, ``square``, ``sawtooth``, ``qam``, ``apsk``, ``pam``,
``fsk``, ``ook``, ``oqpsk``, ``gmsk``, ``psk``
**Common options shared across all generators:**
@ -800,16 +760,22 @@ 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``
~~~~~~~~~~~
@ -860,8 +826,6 @@ symbol transition sharpness).
ria synth psk -s 2e6 -r 100e3 -M 8 -N 8000 -o psk8.npy
.. _cmd-transform:
4.10 ``transform`` group
-------------------------
@ -870,7 +834,7 @@ symbol transition sharpness).
* Apply algorithmic transforms to existing recordings.
* Run reusable augmentations/impairments for dataset diversity and robustness testing.
**Usage:**
**Shape:**
.. code-block:: bash
@ -931,8 +895,6 @@ 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``
------------------
@ -1031,7 +993,17 @@ experiment-specific fields on the CLI.
ria generate noise --config generate.yaml
6) Version Notes
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
=================
These notes are based on the current implementation and should be re-validated against future
@ -1044,19 +1016,18 @@ releases.
3. Multiple non-CLI modules still import ``utils.*``, which can create runtime dependency
coupling when using only ``ria-toolkit-oss`` in isolation.
.. tip::
If you observe unexpected import errors after install, check the package version and
changelog, then test ``ria --help`` in a clean virtual environment.
If you observe unexpected import errors after install, check the package version and
changelog, then test ``ria --help`` in a clean virtual environment.
7) Brief Scripting (Python) Preview
8) Brief Scripting (Python) Preview
=====================================
For quick non-CLI use:
.. code-block:: python
from ria_toolkit_oss.data import Recording
from ria_toolkit_oss.datatypes import Recording
from ria_toolkit_oss.io import load_recording, to_sigmf
from ria_toolkit_oss.transforms import iq_augmentations, iq_impairments
@ -1066,3 +1037,47 @@ 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,7 +1,6 @@
/* Change the hex values below to customize heading colours */
.rst-content { color: #e0e0e0; }
.rst-content h1 { color: #ffffff; }
.rst-content h1 { color: #2c3e50; }
.rst-content h2,
.rst-content h2 a { color: #ffffff !important; font-size: 22px !important; }
@ -23,20 +22,8 @@
.rst-content .admonition.warning p {
color: #ffffff !important;
}
.rst-content h4 { color: #cccccc; }
.rst-content h4 { color: #404040; }
.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;
}

View File

@ -12,9 +12,9 @@ sys.path.insert(0, os.path.abspath(os.path.join('..', '..')))
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'ria-toolkit-oss'
copyright = '2026, Qoherent Inc'
copyright = '2025, Qoherent Inc'
author = 'Qoherent Inc.'
release = '0.1.7'
release = '0.1.5'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View File

@ -1,4 +1,4 @@
.. _sdr_examples:
.. _examples:
############
SDR Examples

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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,26 +4,7 @@ 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 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
:ref:`SDR Guides <sdr_guides>` section of the documentation for addition setup instructions.
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``.
@ -103,22 +84,12 @@ Please follow the steps below to install RIA Toolkit OSS using pip:
python -m venv venv
venv\Scripts\activate
2. Upgrade pip and install RIA Toolkit OSS:
2. Install RIA Toolkit OSS from PyPI with pip:
.. 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.
@ -148,6 +119,3 @@ 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 @@
Data Package (ria_toolkit_oss.data)
=======================================
Datatypes Package (ria_toolkit_oss.data)
=============================================
.. |br| raw:: html

View File

@ -1,87 +1,77 @@
.. _blade:
BladeRF
=======
The BladeRF is a versatile software-defined radio (SDR) platform developed by Nuand. It is designed for a wide
range of applications, from wireless communication research to field deployments. BladeRF devices are known
for their high performance, flexibility, and extensive open-source support, making them suitable for both
hobbyists and professionals. The BladeRF is based on the Analog Devices AD9361 RF transceiver, which provides
wide frequency coverage and high bandwidth.
Supported Models
----------------
- **BladeRF 2.0 Micro xA4:** A compact model with a 49 kLE FPGA, ideal for portable applications.
- **BladeRF 2.0 Micro xA9:** A higher-end version of the Micro with a 115 kLE FPGA, offering more processing power in a small form factor.
Key Features
------------
- **Frequency Range:** Typically from 47 MHz to 6 GHz, covering a wide range of wireless communication bands.
- **Bandwidth:** Up to 56 MHz, allowing for wideband signal processing.
- **FPGA:** Integrated FPGA (varies by model) for real-time processing and custom logic development.
- **Connectivity:** USB 3.0 interface for high-speed data transfer, with options for GPIO, SPI, and other I/O.
Hackability
-----------
- **Expansion:** The BladeRF features GPIO, expansion headers, and add-on boards, allowing users to extend the
functionality of the device for specific applications, such as additional RF front ends.
- **Frequency and Bandwidth Modification:** Advanced users can modify the BladeRF's settings and firmware to
explore different frequency bands and optimize the bandwidth for their specific use cases.
Limitations
-----------
- The complexity of FPGA development may present a steep learning curve for users unfamiliar with hardware
description languages (HDL).
- Bandwidth is capped at 56 MHz, which might not be sufficient for ultra-wideband applications.
- USB 3.0 connectivity is required for optimal performance; using USB 2.0 will significantly limit data
transfer rates.
Set up instructions (Linux)
---------------------------
No additional Python packages are required for BladeRF beyond the base RIA Toolkit OSS installation.
1. Install the system library:
.. code-block:: bash
sudo apt install libbladerf-dev
For a more complete installation including CLI tools and FPGA images, use the Nuand PPA:
.. code-block:: bash
sudo add-apt-repository ppa:nuandllc/bladerf
sudo apt-get update
sudo apt-get install bladerf libbladerf-dev
sudo apt-get install bladerf-fpga-hostedxa4 # Necessary for BladeRF 2.0 Micro xA4
2. Install udev rules:
For most users:
.. code-block:: bash
sudo udevadm control --reload
sudo udevadm trigger
For **Radioconda** users, create symlinks from your conda environment instead:
.. code-block:: bash
sudo ln -s $CONDA_PREFIX/lib/udev/rules.d/88-nuand-bladerf1.rules /etc/udev/rules.d/88-radioconda-nuand-bladerf1.rules
sudo ln -s $CONDA_PREFIX/lib/udev/rules.d/88-nuand-bladerf2.rules /etc/udev/rules.d/88-radioconda-nuand-bladerf2.rules
sudo ln -s $CONDA_PREFIX/lib/udev/rules.d/88-nuand-bootloader.rules /etc/udev/rules.d/88-radioconda-nuand-bootloader.rules
sudo udevadm control --reload
sudo udevadm trigger
Further Information
-------------------
- `Official BladeRF Website <https://www.nuand.com/>`_
- `BladeRF GitHub Repository <https://github.com/Nuand/bladeRF>`_
- `BladeRF Setup with Radioconda <https://github.com/radioconda/radioconda-installer?tab=readme-ov-file#bladerf>`_
.. _blade:
BladeRF
=======
The BladeRF is a versatile software-defined radio (SDR) platform developed by Nuand. It is designed for a wide
range of applications, from wireless communication research to field deployments. BladeRF devices are known
for their high performance, flexibility, and extensive open-source support, making them suitable for both
hobbyists and professionals. The BladeRF is based on the Analog Devices AD9361 RF transceiver, which provides
wide frequency coverage and high bandwidth.
Supported Models
----------------
- **BladeRF 2.0 Micro xA4:** A compact model with a 49 kLE FPGA, ideal for portable applications.
- **BladeRF 2.0 Micro xA9:** A higher-end version of the Micro with a 115 kLE FPGA, offering more processing power in a small form factor.
Key Features
------------
- **Frequency Range:** Typically from 47 MHz to 6 GHz, covering a wide range of wireless communication bands.
- **Bandwidth:** Up to 56 MHz, allowing for wideband signal processing.
- **FPGA:** Integrated FPGA (varies by model) for real-time processing and custom logic development.
- **Connectivity:** USB 3.0 interface for high-speed data transfer, with options for GPIO, SPI, and other I/O.
Hackability
-----------
- **Expansion:** The BladeRF features GPIO, expansion headers, and add-on boards, allowing users to extend the
functionality of the device for specific applications, such as additional RF front ends.
- **Frequency and Bandwidth Modification:** Advanced users can modify the BladeRF's settings and firmware to
explore different frequency bands and optimize the bandwidth for their specific use cases.
Limitations
-----------
- The complexity of FPGA development may present a steep learning curve for users unfamiliar with hardware
description languages (HDL).
- Bandwidth is capped at 56 MHz, which might not be sufficient for ultra-wideband applications.
- USB 3.0 connectivity is required for optimal performance; using USB 2.0 will significantly limit data
transfer rates.
Set up instructions (Linux, Radioconda)
---------------------------------------
1. Activate your Radioconda environment.
.. code-block:: bash
conda activate <your-env-name>
2. Install the base dependencies and drivers (*Easy method*):
.. code-block:: bash
sudo add-apt-repository ppa:nuandllc/bladerf
sudo apt-get update
sudo apt-get install bladerf
sudo apt-get install libbladerf-dev
sudo apt-get install bladerf-fpga-hostedxa4 # Necessary for installation of bladeRF 2.0 Micro A4.
3. Install a ``udev`` rule by creating a link into your Radioconda installation:
.. code-block:: bash
sudo ln -s $CONDA_PREFIX/lib/udev/rules.d/88-nuand-bladerf1.rules /etc/udev/rules.d/88-radioconda-nuand-bladerf1.rules
sudo ln -s $CONDA_PREFIX/lib/udev/rules.d/88-nuand-bladerf2.rules /etc/udev/rules.d/88-radioconda-nuand-bladerf2.rules
sudo ln -s $CONDA_PREFIX/lib/udev/rules.d/88-nuand-bootloader.rules /etc/udev/rules.d/88-radioconda-nuand-bootloader.rules
sudo udevadm control --reload
sudo udevadm trigger
Further Information
-------------------
- `Official BladeRF Website <https://www.nuand.com/>`_
- `BladeRF GitHub Repository <https://github.com/Nuand/bladeRF>`_
- `BladeRF Setup with Radioconda <https://github.com/radioconda/radioconda-installer?tab=readme-ov-file#bladerf>`_

View File

@ -1,88 +1,83 @@
.. _hackrf:
HackRF
======
The HackRF One is a portable and affordable software-defined radio developed by Great Scott Gadgets. It is an
open source hardware platform that is designed to enable test and development of modern and next generation
radio technologies.
The HackRF is based on the Analog Devices MAX2839 transceiver chip, which supports both transmission and
reception of signals across a wide frequency range, combined with a MAX5864 RF front-end chip and a
RFFC5072 wideband synthesizer/VCO.
Supported models
----------------
- **HackRF One:** The standard model with a frequency range of 1 MHz to 6 GHz and a bandwidth of up to 20 MHz.
- **Opera Cake for HackRF:** An antenna switching add-on board for HackRF One that is configured with command-line software.
Key features
------------
- **Frequency Range:** 1 MHz to 6 GHz.
- **Bandwidth:** 2 MHz to 20 MHz.
- **Connectivity:** USB 2.0 interface with support for power, data, and firmware updates.
- **Software Support:** Compatible with GNU Radio, SDR#, and other SDR frameworks.
- **Onboard Processing:** ARM-based LPC4320 processor for digital signal processing and interfacing over USB.
Hackability
-----------
.. todo::
Add information regarding HackRF hackability
Limitations
-----------
- Bandwidth is limited to 20 MHz.
- USB 2.0 connectivity might limit data transfer rates compared to USB 3.0 or Ethernet-based SDRs.
Set up instructions (Linux)
---------------------------
HackRF is supported out of the box after installing RIA Toolkit OSS.
1. Ensure ``libhackrf`` is installed at the system level. On most Ubuntu installations this is already
present. If not:
.. code-block:: bash
sudo apt install libhackrf-dev
2. Install udev rules to allow non-root device access:
For most users:
.. code-block:: bash
sudo udevadm control --reload
sudo udevadm trigger
For **Radioconda** users, create a symlink from your conda environment instead:
.. code-block:: bash
sudo ln -s $CONDA_PREFIX/lib/udev/rules.d/53-hackrf.rules /etc/udev/rules.d/53-radioconda-hackrf.rules
sudo udevadm control --reload
sudo udevadm trigger
Make sure your user account belongs to the ``plugdev`` group in order to access your device:
.. code-block:: bash
sudo usermod -a -G plugdev <user>
.. note::
You may have to restart your system for group membership changes to take effect.
Further information
-------------------
- `Official HackRF Website <https://greatscottgadgets.com/hackrf/>`_
- `HackRF Project Documentation <https://hackrf.readthedocs.io/en/latest/>`_
- `HackRF Software Installation Guide <https://hackrf.readthedocs.io/en/latest/installing_hackrf_software.html>`_
- `HackRF GitHub Repository <https://github.com/greatscottgadgets/hackrf>`_
- `HackRF Setup with Radioconda <https://github.com/radioconda/radioconda-installer?tab=readme-ov-file#hackrf>`_
.. _hackrf:
HackRF
======
The HackRF One is a portable and affordable software-defined radio developed by Great Scott Gadgets. It is an
open source hardware platform that is designed to enable test and development of modern and next generation
radio technologies.
The HackRF is based on the Analog Devices MAX2839 transceiver chip, which supports both transmission and
reception of signals across a wide frequency range, combined with a MAX5864 RF front-end chip and a
RFFC5072 wideband synthesizer/VCO.
Supported models
----------------
- **HackRF One:** The standard model with a frequency range of 1 MHz to 6 GHz and a bandwidth of up to 20 MHz.
- **Opera Cake for HackRF:** An antenna switching add-on board for HackRF One that is configured with command-line software.
Key features
------------
- **Frequency Range:** 1 MHz to 6 GHz.
- **Bandwidth:** 2 MHz to 20 MHz.
- **Connectivity:** USB 2.0 interface with support for power, data, and firmware updates.
- **Software Support:** Compatible with GNU Radio, SDR#, and other SDR frameworks.
- **Onboard Processing:** ARM-based LPC4320 processor for digital signal processing and interfacing over USB.
Hackability
-----------
.. todo::
Add information regarding HackRF hackability
Limitations
-----------
- Bandwidth is limited to 20 MHz.
- USB 2.0 connectivity might limit data transfer rates compared to USB 3.0 or Ethernet-based SDRs.
Set up instructions (Linux, Radioconda)
---------------------------------------
1. Activate your Radioconda environment:
.. code-block:: bash
conda activate <your-env-name>
2. Install the System Package (Ubuntu / Debian):
.. code-block:: bash
sudo apt-get update
sudo apt-get install hackrf
3. Install a ``udev`` rule by creating a link into your Radioconda installation:
.. code-block:: bash
sudo ln -s $CONDA_PREFIX/lib/udev/rules.d/53-hackrf.rules /etc/udev/rules.d/53-radioconda-hackrf.rules
sudo udevadm control --reload
sudo udevadm trigger
Make sure your user account belongs to the plugdev group in order to access your device:
.. code-block:: bash
sudo usermod -a -G plugdev <user>
.. note::
You may have to restart your system for changes to take effect.
Further information
-------------------
- `Official HackRF Website <https://greatscottgadgets.com/hackrf/>`_
- `HackRF Project Documentation <https://hackrf.readthedocs.io/en/latest/>`_
- `HackRF Software Installation Guide <https://hackrf.readthedocs.io/en/latest/installing_hackrf_software.html>`_
- `HackRF GitHub Repository <https://github.com/greatscottgadgets/hackrf>`_
- `HackRF Setup with Radioconda <https://github.com/radioconda/radioconda-installer?tab=readme-ov-file#hackrf>`_

View File

@ -1,123 +1,116 @@
.. _pluto:
PlutoSDR
========
The ADALM-PLUTO (PlutoSDR) is a portable and affordable software-defined radio developed by Analog Devices.
It is designed for learning, experimenting, and prototyping in the field of wireless communication. The PlutoSDR
is popular among students, educators, and hobbyists due to its versatility and ease of use.
The PlutoSDR is based on the AD9363 transceiver chip, which supports both transmission and reception of signals
across a wide frequency range. The device is supported by a robust open-source ecosystem, making it ideal for
hands-on learning and rapid prototyping.
Supported models
----------------
- **ADALM-PLUTO:** The standard model with a frequency range of 325 MHz to 3.8 GHz and a bandwidth of up to 20 MHz.
- **Modified ADALM-PLUTO:** Some users modify their PlutoSDR to extend the frequency range to approximately 70 MHz
to 6 GHz by applying firmware patches with unqualified RF performance.
Key features
------------
- **Frequency Range:** 325 MHz to 3.8 GHz (standard), expandable with modifications.
- **Bandwidth:** Up to 20 MHz, can be increased to 56 MHz with firmware modifications.
- **Connectivity:** USB 2.0 interface with support for power, data, and firmware updates.
- **Software Support:** Compatible with GNU Radio, MATLAB, Simulink, and other SDR frameworks.
- **Onboard Processing:** Integrated ARM Cortex-A9 processor for custom applications and signal processing.
Hackability
------------
- **Frequency Range and Bandwidth:** The default frequency range of 325 MHz to 3.8 GHz can be expanded to
approximately 70 MHz to 6 GHz, and the bandwidth can be increased from 20 MHz to 56 MHz by modifying
the device's firmware.
- **2x2 MIMO:** On Rev C models, users can unlock 2x2 MIMO (Multiple Input Multiple Output) functionality by
wiring UFL to SMA connectors to the device's PCB, effectively turning the device into a dual-channel SDR.
Limitations
-----------
- Bandwidth is limited to 20 MHz by default, but can be increased to 56 MHz with modifications, which may
affect stability.
- USB 2.0 connectivity might limit data transfer rates compared to USB 3.0 or Ethernet-based SDRs.
Set up instructions (Linux)
---------------------------
The PlutoSDR is supported out of the box after installing RIA Toolkit OSS. The required Python package
(``pyadi-iio``) is included in the toolkit's dependencies.
1. Ensure ``libiio`` is installed at the system level. On most Ubuntu installations this is already present.
If not:
.. code-block:: bash
sudo apt install libiio-dev libiio-utils libiio0
.. note::
PlutoSDR devices are discoverable over both USB and network (mDNS). Network discovery uses Avahi — if
``avahi-daemon`` is not running, network discovery will be skipped but USB discovery still works.
2. Install a ``udev`` rule to allow non-root device access:
For most users:
.. code-block:: bash
sudo udevadm control --reload
sudo udevadm trigger
For **Radioconda** users, create a symlink from your conda environment instead:
.. code-block:: bash
sudo ln -s $CONDA_PREFIX/lib/udev/rules.d/90-libiio.rules /etc/udev/rules.d/90-radioconda-libiio.rules
sudo udevadm control --reload
sudo udevadm trigger
Once you can communicate with the hardware, you may want to perform the post-install steps detailed on
the `PlutoSDR Documentation <https://wiki.analog.com/university/tools/pluto>`_.
3. (Optional) Building ``libiio`` or ``libad9361-iio`` from source:
This step is only required if you need a version not available via ``apt``. First install build
dependencies:
.. code-block:: bash
sudo apt-get install -y build-essential git libxml2-dev bison flex libcdk5-dev cmake \
libusb-1.0-0-dev libavahi-client-dev libavahi-common-dev libaio-dev
.. code-block:: bash
# Build libiio from source
cd ~
git clone --branch v0.23 https://github.com/analogdevicesinc/libiio.git
cd libiio
mkdir -p build
cd build
cmake -DPYTHON_BINDINGS=ON ..
make -j"$(nproc)"
sudo make install
sudo ldconfig
.. code-block:: bash
# Build libad9361-iio from source
cd ~
git clone https://github.com/analogdevicesinc/libad9361-iio.git
cd libad9361-iio
mkdir -p build
cd build
cmake ..
make -j"$(nproc)"
sudo make install
Further information
-------------------
- `PlutoSDR Documentation <https://wiki.analog.com/university/tools/pluto>`_
- `PlutoSDR Setup with Radioconda <https://github.com/radioconda/radioconda-installer?tab=readme-ov-file#iio-pluto-sdr>`_
.. _pluto:
PlutoSDR
========
The ADALM-PLUTO (PlutoSDR) is a portable and affordable software-defined radio developed by Analog Devices.
It is designed for learning, experimenting, and prototyping in the field of wireless communication. The PlutoSDR
is popular among students, educators, and hobbyists due to its versatility and ease of use.
The PlutoSDR is based on the AD9363 transceiver chip, which supports both transmission and reception of signals
across a wide frequency range. The device is supported by a robust open-source ecosystem, making it ideal for
hands-on learning and rapid prototyping.
Supported models
----------------
- **ADALM-PLUTO:** The standard model with a frequency range of 325 MHz to 3.8 GHz and a bandwidth of up to 20 MHz.
- **Modified ADALM-PLUTO:** Some users modify their PlutoSDR to extend the frequency range to approximately 70 MHz
to 6 GHz by applying firmware patches with unqualified RF performance.
Key features
------------
- **Frequency Range:** 325 MHz to 3.8 GHz (standard), expandable with modifications.
- **Bandwidth:** Up to 20 MHz, can be increased to 56 MHz with firmware modifications.
- **Connectivity:** USB 2.0 interface with support for power, data, and firmware updates.
- **Software Support:** Compatible with GNU Radio, MATLAB, Simulink, and other SDR frameworks.
- **Onboard Processing:** Integrated ARM Cortex-A9 processor for custom applications and signal processing.
Hackability
------------
- **Frequency Range and Bandwidth:** The default frequency range of 325 MHz to 3.8 GHz can be expanded to
approximately 70 MHz to 6 GHz, and the bandwidth can be increased from 20 MHz to 56 MHz by modifying
the device's firmware.
- **2x2 MIMO:** On Rev C models, users can unlock 2x2 MIMO (Multiple Input Multiple Output) functionality by
wiring UFL to SMA connectors to the device's PCB, effectively turning the device into a dual-channel SDR.
Limitations
-----------
- Bandwidth is limited to 20 MHz by default, but can be increased to 56 MHz with modifications, which may
affect stability.
- USB 2.0 connectivity might limit data transfer rates compared to USB 3.0 or Ethernet-based SDRs.
Set up instructions (Linux, Radioconda)
---------------------------------------
1. Activate your Radioconda environment:
.. code-block:: bash
conda activate <your-env-name>
2. Install system dependencies:
.. code-block:: bash
sudo apt-get update
sudo apt-get install -y \
build-essential \
git \
libxml2-dev \
bison \
flex \
libcdk5-dev \
cmake \
libusb-1.0-0-dev \
libavahi-client-dev \
libavahi-common-dev \
libaio-dev
3. Install a ``udev`` rule by creating a link into your Radioconda installation:
.. code-block:: bash
sudo ln -s $CONDA_PREFIX/lib/udev/rules.d/90-libiio.rules /etc/udev/rules.d/90-radioconda-libiio.rules
sudo udevadm control --reload
sudo udevadm trigger
Once you can talk to the hardware, you may want to perform the post-install steps detailed on the `PlutoSDR Documentation <https://wiki.analog.com/university/tools/pluto>`_.
4. (Optional) Building ``libiio`` or ``libad9361-iio`` from source:
This step is only required if you want the latest version of these libraries not provided in Radioconda.
.. code-block:: bash
# Build libiio from source
cd ~
git clone --branch v0.23 https://github.com/analogdevicesinc/libiio.git
cd libiio
mkdir -p build
cd build
cmake -DPYTHON_BINDINGS=ON ..
make -j"$(nproc)"
sudo make install
sudo ldconfig
.. code-block:: bash
# Build libad9361-iio from source
cd ~
git clone https://github.com/analogdevicesinc/libad9361-iio.git
cd libad9361-iio
mkdir -p build
cd build
cmake ..
make -j"$(nproc)"
sudo make install
Further information
-------------------
- `PlutoSDR Documentation <https://wiki.analog.com/university/tools/pluto>`_
- `PlutoSDR Setup with Radioconda <https://github.com/radioconda/radioconda-installer?tab=readme-ov-file#iio-pluto-sdr>`_

View File

@ -30,111 +30,71 @@ Limitations
- Sensitivity and performance can vary depending on the specific model and components.
- Requires external software for signal processing and analysis.
Set up instructions (Linux)
---------------------------
Set up instructions (Linux, Radioconda)
---------------------------------------
1. If you previously had RTL-SDR drivers installed, purge them first:
1. Activate your Radioconda environment:
.. code-block:: bash
conda activate <your-env-name>
2. Purge drivers:
If you already have other drivers installed, purge them from your system.
.. code-block:: bash
sudo apt purge ^librtlsdr
sudo rm -rvf /usr/lib/librtlsdr*
sudo rm -rvf /usr/include/rtl-sdr*
sudo rm -rvf /usr/local/lib/librtlsdr*
sudo rm -rvf /usr/local/include/rtl-sdr*
sudo rm -rvf /usr/local/include/rtl_*
sudo rm -rvf /usr/lib/librtlsdr*
sudo rm -rvf /usr/include/rtl-sdr*
sudo rm -rvf /usr/local/lib/librtlsdr*
sudo rm -rvf /usr/local/include/rtl-sdr*
sudo rm -rvf /usr/local/include/rtl_*
sudo rm -rvf /usr/local/bin/rtl_*
2. Install build dependencies:
3. Install RTL-SDR Blog drivers:
.. code-block:: bash
sudo apt install libusb-1.0-0-dev git cmake pkg-config build-essential
3. Build ``librtlsdr`` from source:
The standard ``librtlsdr`` package available via ``apt`` is missing symbols required by the Python
bindings. Build from the **rtl-sdr-blog fork**:
.. code-block:: bash
git clone https://github.com/rtlsdrblog/rtl-sdr-blog.git
cd rtl-sdr-blog
mkdir build && cd build
cmake .. -DINSTALL_UDEV_RULES=ON
sudo apt-get install libusb-1.0-0-dev git cmake pkg-config build-essential
git clone https://github.com/osmocom/rtl-sdr
cd rtl-sdr
mkdir build
cd build
cmake ../ -DINSTALL_UDEV_RULES=ON
make
sudo make install
sudo cp ../rtl-sdr.rules /etc/udev/rules.d/
sudo ldconfig
.. important::
Do not use the osmocom ``rtl-sdr`` repository or the Ubuntu ``librtlsdr-dev`` apt package. Neither
provides the ``rtlsdr_set_dithering`` symbol that the Python bindings require.
4. Blacklist the kernel DVB driver:
The kernel DVB-T driver (``dvb_usb_rtl28xxu``) claims the RTL-SDR device and prevents ``librtlsdr``
from accessing it.
For most users:
4. Blacklist the DVB-T modules that would otherwise claim the device:
.. code-block:: bash
echo 'blacklist dvb_usb_rtl28xxu' | sudo tee /etc/modprobe.d/blacklist-rtlsdr.conf
sudo modprobe -r dvb_usb_rtl28xxu
For **Radioconda** users, a blacklist configuration is already provided in your conda environment:
.. code-block:: bash
sudo ln -s $CONDA_PREFIX/etc/modprobe.d/rtl-sdr-blacklist.conf /etc/modprobe.d/radioconda-rtl-sdr-blacklist.conf
sudo modprobe -r $(cat $CONDA_PREFIX/etc/modprobe.d/rtl-sdr-blacklist.conf | sed -n -e 's/^blacklist //p')
If ``modprobe -r`` fails with "Module is in use", unplug the RTL-SDR dongle, run the command again,
then plug it back in. Alternatively, reboot — the blacklist takes effect on next boot.
.. note::
.. note::
In addition to the Radioconda blacklist file, some systems also require
manually blacklisting the following DVB-T modules to prevent them from
claiming the device:
Some systems also require blacklisting additional DVB-T modules. Add these entries to your
blacklist configuration if needed:
- ``dvb_usb_rtl28xxu``
- ``rtl2832``
- ``rtl2830``
- ``rtl2832``
- ``rtl2830``
Add these entries to ``rtlsdr.conf`` (or create the file at
``/etc/modprobe.d/rtlsdr.conf``) if they are not already present.
5. Reload udev rules:
For most users (rules are installed by the build step above):
5. Install a udev rule by creating a link into your radioconda installation:
.. code-block:: bash
sudo udevadm control --reload
sudo udevadm trigger
For **Radioconda** users, create a symlink from your conda environment instead:
.. code-block:: bash
sudo ln -s $CONDA_PREFIX/lib/udev/rules.d/rtl-sdr.rules /etc/udev/rules.d/radioconda-rtl-sdr.rules
sudo udevadm control --reload
sudo udevadm trigger
6. Install Python packages:
.. code-block:: bash
pip install pyrtlsdr==0.3.0
pip install setuptools==69.5.1
.. note::
``pyrtlsdr`` 0.4.0 references a ``rtlsdr_set_dithering`` symbol not present in standard
``librtlsdr`` builds. Version 0.3.0 works correctly.
``pyrtlsdr`` 0.3.0 depends on ``pkg_resources``, which was removed in ``setuptools`` >= 82.
Pinning to 69.5.1 ensures ``pkg_resources`` is available.
Further Information
-------------------
- `RTL-SDR Official Website <https://www.rtl-sdr.com/>`_
- `RTL-SDR Documentation <https://www.rtl-sdr.com/rtl-sdr-quick-start-guide/>`_
- `RTL-SDR Documentation <https://www.rtl-sdr.com/rtl-sdr-quick-start-guide/>`_

View File

@ -39,48 +39,18 @@ Limitations
Set up instructions (Linux)
---------------------------------
ThinkRF devices require the ``pyrf`` package, which is written in Python 2 syntax and must be patched
after installation to work with Python 3.
.. note::
``lib2to3`` was fully removed in Python 3.13. ThinkRF support is currently limited to
**Python 3.12 and below**.
1. Install ``lib2to3``:
On some distributions (including Ubuntu 24.04+), ``lib2to3`` is not included by default:
Install PyRF
.. code-block:: bash
sudo apt install python3-lib2to3
pip install 'pyrf>=2.8.0'
2. Install ``pyrf``:
Convert PyRF scripts to Python 3
.. code-block:: bash
pip install pyrf
3. Patch ``pyrf`` for Python 3:
The ``pyrf`` package contains Python 2 syntax throughout (e.g., ``dict.iteritems()``, ``print``
statements). Run the following to automatically convert the entire package to Python 3:
.. code-block:: bash
python -c "
from lib2to3.refactor import RefactoringTool, get_fixers_from_package
import pyrf, os
pyrf_path = os.path.dirname(pyrf.__file__)
fixers = get_fixers_from_package('lib2to3.fixes')
tool = RefactoringTool(fixers)
tool.refactor_dir(pyrf_path, write=True)
print('Done')
"
.. note::
This patches the entire ``pyrf`` package in place, which is required for the driver to fully load.
cd ../scripts
./convert_pyrf_to_python3.sh
Further Information
-------------------

View File

@ -1,155 +1,92 @@
.. _usrp:
USRP
====
The USRP (Universal Software Radio Peripheral) product line is a series of software-defined radios (SDRs)
developed by Ettus Research. These devices are widely used in academia, industry, and research for various
wireless communication applications, ranging from simple experimentation to complex signal processing tasks.
USRP devices offer a flexible platform that can be used with various software frameworks, including GNU Radio
and the USRP Hardware Driver (UHD). The product line includes both entry-level models for hobbyists and
advanced models for professional and research use.
Supported models
----------------
- **USRP B200/B210:** Compact, single-board, full-duplex, with a wide frequency range.
- **USRP N200/N210:** High-performance models with increased bandwidth and connectivity options.
- **USRP X300/X310:** High-end models featuring large bandwidth, multiple MIMO channels, and support for GPSDO.
- **USRP E310/E320:** Embedded devices with onboard processing capabilities.
- **USRP B200mini:** Ultra-compact model for portable and embedded applications.
Key features
------------
- **Frequency Range:** Typically covers from DC to 6 GHz, depending on the model and daughter boards used.
- **Bandwidth:** Varies by model, up to 160 MHz in some high-end versions.
- **Connectivity:** Includes USB 3.0, Ethernet, and PCIe interfaces depending on the model.
- **Software Support:** Compatible with UHD, GNU Radio, and other SDR frameworks.
Hackability
-----------
- The UHD library is fully open source and can be modified to meet user untention.
- Certain USRP models have "RFNoC" which streamlines the inclusion of custom FPGA processing in a USRP.
Limitations
-----------
- Some models may have limited bandwidth or processing capabilities.
- Compatibility with certain software tools may vary depending on the version of the UHD.
- Price range can be a consideration, especially for high-end models.
Set up instructions (Linux)
---------------------------
USRP devices require the UHD (USRP Hardware Driver) library with Python bindings. There is no pip-installable
UHD package — it must either be installed via conda or built from source.
**Option A: Install via conda (recommended for conda environments)**
.. code-block:: bash
conda install conda-forge::uhd
**Option B: Build from source (required for pip/venv environments)**
The Python bindings must target the same Python version used in your virtual environment.
1. Install build dependencies:
.. code-block:: bash
sudo apt install cmake build-essential libboost-all-dev libusb-1.0-0-dev \
python3-dev python3-numpy libncurses-dev
2. Install the Mako template library into your virtual environment (used by UHD's build system):
.. code-block:: bash
pip install mako
3. Clone and build UHD with your virtual environment activated:
.. code-block:: bash
git clone https://github.com/EttusResearch/uhd.git
cd uhd
git checkout v4.7.0.0
cd host
mkdir build && cd build
cmake -DENABLE_PYTHON_API=ON -DPYTHON_EXECUTABLE=$(which python3) ..
make -j$(nproc)
sudo make install
sudo ldconfig
.. important::
Run the ``cmake`` command with your virtual environment activated so ``$(which python3)`` points
to the correct interpreter. Before running ``make``, verify the cmake output includes::
-- * LibUHD - Python API → must say "Enabling"
-- Python interpreter: .../your-venv/bin/python3
If "LibUHD - Python API" is not listed under enabled components, the Python bindings will not be
built. The build typically takes 1030 minutes.
4. Copy the Python bindings into your virtual environment if ``import uhd`` fails after installation:
.. code-block:: bash
cp -r ~/uhd/host/build/python/uhd ~/.venv/lib/python3.XX/site-packages/
Replace ``python3.XX`` with your Python version (e.g., ``python3.12``).
.. note::
If you have a pre-existing UHD installation built against a different Python version, you will see
a circular import error. The bindings must match the Python version in your virtual environment exactly.
**After either installation method:**
1. Download UHD FPGA/firmware images:
.. code-block:: bash
uhd_images_downloader
2. Verify device access:
.. code-block:: bash
uhd_find_devices
For USB devices (e.g. B-series), install a ``udev`` rule.
For most users:
.. code-block:: bash
sudo udevadm control --reload
sudo udevadm trigger
For **Radioconda** users, create a symlink from your conda environment instead:
.. code-block:: bash
sudo ln -s $CONDA_PREFIX/lib/uhd/utils/uhd-usrp.rules /etc/udev/rules.d/radioconda-uhd-usrp.rules
sudo udevadm control --reload
sudo udevadm trigger
3. (Optional) Update firmware/FPGA images:
.. code-block:: bash
uhd_usrp_probe
This will ensure your device is running the latest firmware and FPGA versions.
Further information
-------------------
- `Official USRP Website <https://www.ettus.com/>`_
- `USRP Documentation <https://kb.ettus.com/USRP_Hardware_Driver_and_Interfaces>`_
- `USRP Setup with Radioconda <https://github.com/radioconda/radioconda-installer?tab=readme-ov-file#uhd-ettus-usrp>`_
.. _usrp:
USRP
====
The USRP (Universal Software Radio Peripheral) product line is a series of software-defined radios (SDRs)
developed by Ettus Research. These devices are widely used in academia, industry, and research for various
wireless communication applications, ranging from simple experimentation to complex signal processing tasks.
USRP devices offer a flexible platform that can be used with various software frameworks, including GNU Radio
and the USRP Hardware Driver (UHD). The product line includes both entry-level models for hobbyists and
advanced models for professional and research use.
Supported models
----------------
- **USRP B200/B210:** Compact, single-board, full-duplex, with a wide frequency range.
- **USRP N200/N210:** High-performance models with increased bandwidth and connectivity options.
- **USRP X300/X310:** High-end models featuring large bandwidth, multiple MIMO channels, and support for GPSDO.
- **USRP E310/E320:** Embedded devices with onboard processing capabilities.
- **USRP B200mini:** Ultra-compact model for portable and embedded applications.
Key features
------------
- **Frequency Range:** Typically covers from DC to 6 GHz, depending on the model and daughter boards used.
- **Bandwidth:** Varies by model, up to 160 MHz in some high-end versions.
- **Connectivity:** Includes USB 3.0, Ethernet, and PCIe interfaces depending on the model.
- **Software Support:** Compatible with UHD, GNU Radio, and other SDR frameworks.
Hackability
-----------
- The UHD library is fully open source and can be modified to meet user untention.
- Certain USRP models have "RFNoC" which streamlines the inclusion of custom FPGA processing in a USRP.
Limitations
-----------
- Some models may have limited bandwidth or processing capabilities.
- Compatibility with certain software tools may vary depending on the version of the UHD.
- Price range can be a consideration, especially for high-end models.
Set up instructions (Linux, Radioconda)
---------------------------------------
1. Activate your Radioconda environment:
.. code-block:: bash
conda activate <your-env-name>
2. Install UHD and Python bindings:
.. code-block:: bash
conda install conda-forge::uhd
3. Download UHD images:
.. code-block:: bash
uhd_images_downloader
4. Verify access to your device:
.. code-block:: bash
uhd_find_devices
For USB devices only (e.g. B series), install a ``udev`` rule by creating a link into your Radioconda installation.
.. code-block:: bash
sudo ln -s $CONDA_PREFIX/lib/uhd/utils/uhd-usrp.rules /etc/udev/rules.d/radioconda-uhd-usrp.rules
sudo udevadm control --reload
sudo udevadm trigger
5. (Optional) Update firmware/FPGA images:
.. code-block:: bash
uhd_usrp_probe
This will ensure your device is running the latest firmware and FPGA versions.
Further information
-------------------
- `Official USRP Website <https://www.ettus.com/>`_
- `USRP Documentation <https://kb.ettus.com/USRP_Hardware_Driver_and_Interfaces>`_
- `USRP Setup with Radioconda <https://github.com/radioconda/radioconda-installer?tab=readme-ov-file#uhd-ettus-usrp>`_

379
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]]
name = "alabaster"
@ -242,14 +242,14 @@ files = [
[[package]]
name = "certifi"
version = "2026.4.22"
version = "2026.2.25"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.7"
groups = ["agent", "docs", "test"]
files = [
{file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"},
{file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"},
{file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"},
{file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"},
]
[[package]]
@ -491,14 +491,14 @@ files = [
[[package]]
name = "click"
version = "8.3.3"
version = "8.3.2"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.10"
groups = ["main", "dev", "docs", "server", "test"]
files = [
{file = "click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613"},
{file = "click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2"},
{file = "click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d"},
{file = "click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5"},
]
[package.dependencies]
@ -690,61 +690,61 @@ test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist"
[[package]]
name = "cryptography"
version = "47.0.0"
version = "46.0.7"
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-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"},
{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"},
]
[package.dependencies]
@ -752,7 +752,14 @@ 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"
@ -843,14 +850,14 @@ test = ["pytest (>=6)"]
[[package]]
name = "fastapi"
version = "0.136.1"
version = "0.136.0"
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.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f"},
{file = "fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f"},
{file = "fastapi-0.136.0-py3-none-any.whl", hash = "sha256:8793d44ec7378e2be07f8a013cf7f7aa47d6327d0dfe9804862688ec4541a6b4"},
{file = "fastapi-0.136.0.tar.gz", hash = "sha256:cf08e067cc66e106e102d9ba659463abfac245200752f8a5b7b1e813de4ff73e"},
]
[package.dependencies]
@ -1154,18 +1161,18 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "idna"
version = "3.13"
version = "3.11"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.8"
groups = ["agent", "docs", "server", "test"]
files = [
{file = "idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3"},
{file = "idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242"},
{file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"},
{file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"},
]
[package.extras]
all = ["mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
[[package]]
name = "imagesize"
@ -1515,67 +1522,67 @@ files = [
[[package]]
name = "matplotlib"
version = "3.10.9"
version = "3.10.8"
description = "Python plotting package"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{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"},
{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"},
]
[package.dependencies]
@ -1590,7 +1597,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,<10)"]
dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7)"]
[[package]]
name = "mccabe"
@ -1604,6 +1611,25 @@ 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"
@ -1691,37 +1717,37 @@ markers = {server = "python_version >= \"3.11\"", test = "python_version >= \"3.
[[package]]
name = "onnxruntime"
version = "1.25.1"
version = "1.24.4"
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.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"},
{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"},
]
[package.dependencies]
@ -1729,21 +1755,18 @@ flatbuffers = "*"
numpy = ">=1.21.6"
packaging = "*"
protobuf = "*"
[package.extras]
quantization = ["ml_dtypes"]
symbolic = ["sympy"]
sympy = "*"
[[package]]
name = "packaging"
version = "26.2"
version = "26.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["main", "dev", "docs", "server", "test"]
files = [
{file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"},
{file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"},
{file = "packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f"},
{file = "packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de"},
]
markers = {server = "python_version >= \"3.11\""}
@ -1870,20 +1893,21 @@ gssapi = ["gssapi (>=1.4.1) ; platform_system != \"Windows\"", "pyasn1 (>=0.1.7)
[[package]]
name = "pathspec"
version = "1.1.1"
version = "1.0.4"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189"},
{file = "pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a"},
{file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"},
{file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"},
]
[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"
@ -2950,14 +2974,14 @@ test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis
[[package]]
name = "sigmf"
version = "1.9.0"
version = "1.8.0"
description = "Easily interact with Signal Metadata Format (SigMF) recordings."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "sigmf-1.9.0-py3-none-any.whl", hash = "sha256:902e694894e61f8cdb75b0d69ae8c407f82f35435c3c5e4c1b586b313f77b89b"},
{file = "sigmf-1.9.0.tar.gz", hash = "sha256:95e4b28156b2182035ecca5f5852108fb3cdef5f20b0cd48919bb0fc5f293d0e"},
{file = "sigmf-1.8.0-py3-none-any.whl", hash = "sha256:f233ab04344fa3e42170926a646f7e53edd7edc65fcda42eb3d7efaf8a2e8263"},
{file = "sigmf-1.8.0.tar.gz", hash = "sha256:91e10cb046499639e5f961d66a24c17a33ff76fc98df892eab0953cc9d659a50"},
]
[package.dependencies]
@ -3205,6 +3229,25 @@ 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"
@ -3346,14 +3389,14 @@ typing-extensions = ">=4.12.0"
[[package]]
name = "tzdata"
version = "2026.2"
version = "2026.1"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
groups = ["main"]
files = [
{file = "tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7"},
{file = "tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10"},
{file = "tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9"},
{file = "tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98"},
]
[[package]]
@ -3376,14 +3419,14 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
[[package]]
name = "uvicorn"
version = "0.46.0"
version = "0.44.0"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.10"
groups = ["docs", "server", "test"]
files = [
{file = "uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048"},
{file = "uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d"},
{file = "uvicorn-0.44.0-py3-none-any.whl", hash = "sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89"},
{file = "uvicorn-0.44.0.tar.gz", hash = "sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e"},
]
[package.dependencies]
@ -3468,14 +3511,14 @@ test = ["aiohttp (>=3.10.5)", "flake8 (>=6.1,<7.0)", "mypy (>=0.800)", "psutil",
[[package]]
name = "virtualenv"
version = "21.3.0"
version = "21.2.4"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.8"
groups = ["test"]
files = [
{file = "virtualenv-21.3.0-py3-none-any.whl", hash = "sha256:4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7"},
{file = "virtualenv-21.3.0.tar.gz", hash = "sha256:733750db978ec95c2d8eb4feadaa57091002bce404cb39ba69899cf7bd28944e"},
{file = "virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac"},
{file = "virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada"},
]
[package.dependencies]

View File

@ -1,6 +1,6 @@
[project]
name = "ria-toolkit-oss"
version = "0.1.7"
version = "0.1.5"
description = "An open-source version of the RIA Toolkit, including the fundamental tools to get started developing, testing, and deploying radio intelligence applications"
license = { text = "AGPL-3.0-only" }
readme = "README.md"

View File

@ -5,11 +5,8 @@ Subcommands:
- ``ria-agent run [legacy args]`` legacy long-poll NodeAgent (unchanged).
- ``ria-agent stream`` new WebSocket-based IQ streamer.
- ``ria-agent detect`` print SDR drivers whose modules import cleanly.
- ``ria-agent register --hub URL --api-key KEY`` register with the hub
using a personal registration key (minted from **Settings RIA Agents**
on the hub, shown once at mint time) and save credentials (and optional
TX interlocks) to ``~/.ria/agent.json``. The hub also accepts the legacy
shared ``[wac] API_KEY`` for back-compat, but that path is deprecated.
- ``ria-agent register --hub URL --api-key KEY`` register with the hub and
save credentials (and optional TX interlocks) to ``~/.ria/agent.json``.
Invoking ``ria-agent`` with no subcommand falls through to the legacy
long-poll behavior for back-compatibility with existing deployments.
@ -31,79 +28,6 @@ from .namegen import generate_agent_name
_LEGACY_ALIASES = {"--hub", "--key", "--name", "--device", "--insecure", "--log-level", "--config"}
def _user_agent() -> str:
"""Build the User-Agent header for hub requests.
Set explicitly so we don't fall back to Python's default `Python-urllib/<ver>`,
which is blocked by Cloudflare's Browser Integrity Check on `riahub.ai`
(HTTP 403 edge code 1010). Version is read from package metadata so it
tracks releases instead of going stale.
"""
from importlib.metadata import PackageNotFoundError, version
try:
pkg_version = version("ria-toolkit-oss")
except PackageNotFoundError:
pkg_version = "unknown"
return f"ria-agent/{pkg_version} (+https://riahub.ai/qoherent/ria-toolkit-oss)"
# How long to wait on the hub before giving up. The register endpoint is a
# small DB lookup + insert; anything past this is a stuck hub, not a slow one.
_REGISTER_TIMEOUT_S = 15
REGISTRATION_REASON_MESSAGES = {
"invalid_key": (
"Registration key not recognized. Generate a fresh key from "
"Settings → RIA Agents on the hub."
),
"expired": (
"This registration key has expired. Generate a new one from "
"Settings → RIA Agents on the hub."
),
"revoked": (
"This registration key was revoked. Generate a new one from "
"Settings → RIA Agents on the hub."
),
"already_consumed": (
"This single-use registration key has already been used. "
"Generate a new one, or mint a reusable key instead."
),
}
def _explain_registration_failure(status: int, body: bytes) -> str:
"""Return a human-readable explanation for a failed register call."""
try:
parsed = json.loads(body) if body else None
except ValueError:
parsed = None
if status == 429:
# 429 carries a plain string detail, never a reason code.
if isinstance(parsed, dict) and parsed.get("detail"):
detail = parsed["detail"]
else:
detail = body.decode("utf-8", "replace") or "rate limited"
return f"Registration rate-limited by the hub: {detail}"
if not isinstance(parsed, dict):
text = body.decode("utf-8", "replace")
return f"HTTP {status}: {text or 'no body'}"
detail = parsed.get("detail")
if isinstance(detail, dict):
reason = detail.get("reason")
if reason in REGISTRATION_REASON_MESSAGES:
return REGISTRATION_REASON_MESSAGES[reason]
if reason:
return f"Registration rejected ({reason})"
if isinstance(detail, str) and detail:
return f"Registration rejected: {detail}"
return f"HTTP {status}: {parsed}"
def _cmd_detect(_args: argparse.Namespace) -> int:
devices = available_devices()
if not devices:
@ -115,7 +39,6 @@ def _cmd_detect(_args: argparse.Namespace) -> int:
def _cmd_register(args: argparse.Namespace) -> int:
import urllib.error
import urllib.request
hub_url = args.hub.rstrip("/")
@ -128,20 +51,11 @@ def _cmd_register(args: argparse.Namespace) -> int:
headers={
"Content-Type": "application/json",
"X-API-Key": args.api_key,
"User-Agent": _user_agent(),
},
)
try:
with urllib.request.urlopen(req, timeout=_REGISTER_TIMEOUT_S) as resp:
with urllib.request.urlopen(req) as resp:
data = json.loads(resp.read())
except urllib.error.HTTPError as e:
try:
err_body = e.read()
except Exception:
err_body = b""
msg = _explain_registration_failure(e.code, err_body)
print(f"error: registration failed: {msg}", file=sys.stderr)
return 1
except Exception as e:
print(f"error: registration failed: {e}", file=sys.stderr)
return 1
@ -166,7 +80,7 @@ def _cmd_register(args: argparse.Namespace) -> int:
cfg.tx_allowed_freq_ranges = [[float(lo), float(hi)] for lo, hi in freq_ranges]
path = _config.save(cfg)
print(f"Registered agent: {agent_id} ({name})")
print(f"Registered agent: {agent_id}")
if cfg.tx_enabled:
caps: list[str] = []
if cfg.tx_max_gain_db is not None:
@ -227,16 +141,7 @@ def main() -> None:
p_reg = sub.add_parser("register", help="Register agent with RIA Hub and save credentials")
p_reg.add_argument("--hub", required=True, help="RIA Hub URL (e.g. http://whitehorse:3005)")
p_reg.add_argument(
"--api-key",
dest="api_key",
required=True,
help=(
"Personal registration key from the RIA Agents page on the hub "
"(format: ria_reg_...). Shown once when generated; save it then. "
"The legacy shared API key is also accepted but deprecated."
),
)
p_reg.add_argument("--api-key", dest="api_key", required=True, help="Hub API key")
p_reg.add_argument("--name", default=None, help="Human-friendly agent name")
p_reg.add_argument("--insecure", action="store_true", help="Skip TLS verification")
p_reg.add_argument(

View File

@ -38,13 +38,7 @@ def annotate_with_cusum(
:type annotation_type: str
"""
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."
)
sample_rate = recording.metadata["sample_rate"]
center_frequency = recording.metadata.get("center_frequency", 0)
# Create an object of the time segmenter

View File

@ -6,7 +6,6 @@ and occupied bandwidth calculation following ITU-R SM.328 standard.
"""
import json
import warnings
from typing import Tuple
import numpy as np
@ -120,17 +119,6 @@ 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:
@ -147,13 +135,7 @@ def detect_signals_energy(
merged_boundaries.append((start, length))
# Create annotations from detected boundaries
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."
)
sample_rate = recording.metadata["sample_rate"]
center_frequency = recording.metadata.get("center_frequency", 0)
# Validate frequency method
@ -369,12 +351,7 @@ def annotate_with_obw(
>>> annotated = annotate_with_obw(recording, label="signal_obw")
"""
signal = recording.data[0]
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."
)
sample_rate = recording.metadata["sample_rate"]
center_freq = recording.metadata.get("center_frequency", 0)
# Calculate OBW

View File

@ -49,7 +49,6 @@ allowing splitting of overlapping signals into separate training samples.
"""
import json
import warnings
from typing import List, Optional, Tuple
import numpy as np
@ -402,13 +401,7 @@ def split_recording_annotations(
return recording
signal = recording.data[0]
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."
)
sample_rate = recording.metadata["sample_rate"]
center_frequency = recording.metadata.get("center_frequency", 0.0)
# Build new annotation list
@ -432,11 +425,8 @@ def split_recording_annotations(
else:
# No components found, keep original
new_annotations.append(anno)
except Exception as e:
warnings.warn(
f"split_recording_annotations: failed to split annotation at index {i} ({e}); keeping original.",
stacklevel=2,
)
except Exception:
# Split failed for any reason, keep original
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):
for i in range((len(recording.data[0]) // slice_length) - 1):
start_index = slice_length * i
end_index = slice_length * (i + 1)

View File

@ -35,24 +35,17 @@ 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=sample_rate,
sample_rate=recording.metadata["sample_rate"],
shift_frequency=-1 * anno_base_center_freq,
)
# filter
if isolation_bw < sample_rate - 1:
if isolation_bw < recording.metadata["sample_rate"] - 1:
filtered_signal = apply_complex_lowpass_filter(
signal=shifted_signal_slice, cutoff_frequency=isolation_bw, sample_rate=sample_rate
signal=shifted_signal_slice, cutoff_frequency=isolation_bw, sample_rate=recording.metadata["sample_rate"]
)
else:

View File

@ -42,7 +42,6 @@ classification or demodulation stages.
"""
import json
import warnings
from typing import Optional
import numpy as np
@ -217,22 +216,11 @@ def threshold_qualifier(
"""
# Extract signal and metadata
sample_data = recording.data[channel]
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."
)
sample_rate = recording.metadata["sample_rate"]
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)
@ -249,12 +237,6 @@ 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)
@ -314,7 +296,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 = min(max(window_size * 16, int(sample_rate * 0.02)), max(window_size * 2, n_samples // 4))
macro_window_size = max(window_size * 16, int(sample_rate * 0.02))
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,15 +175,6 @@ 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

@ -58,7 +58,7 @@ class HackRF(SDR):
:param channel: The channel the HackRF is set to. (Not actually used)
:type channel: int
:param gain_mode: 'absolute' passes gain directly to the sdr,
'relative' means that gain should be a negative value, and it will be subtracted from the max gain (40).
'relative' means that gain should be a negative value, and it will be subtracted from the max gain (40).
:type gain_mode: str
"""
print("Initializing RX")

View File

@ -54,7 +54,7 @@ class USRP(SDR):
:param channel: The channel the USRP is set to.
:type channel: int
:param gain_mode: 'absolute' passes gain directly to the sdr,
'relative' means that gain should be a negative value, and it will be subtracted from the max gain.
'relative' means that gain should be a negative value, and it will be subtracted from the max gain.
:type gain_mode: str
:param rx_buffer_size: Internal buffer size for receiving samples. Defaults to 960000.
:type rx_buffer_size: int
@ -285,7 +285,7 @@ class USRP(SDR):
:param channel: The channel the USRP is set to.
:type channel: int
:param gain_mode: 'absolute' passes gain directly to the sdr,
'relative' means that gain should be a negative value, and it will be subtracted from the max gain.
'relative' means that gain should be a negative value, and it will be subtracted from the max gain.
:type gain_mode: str
"""

View File

@ -32,15 +32,16 @@ def extract_metadata_fields(metadata):
def set_path(output_path):
path = pathlib.Path(output_path)
split_path = output_path.split("/")
# If only filename provided (no directory), use default 'images' folder
if len(path.parts) == 1:
folder = pathlib.Path("images")
file = path.name
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])
else:
folder = path.parent
file = path.name
folder, file = split_path
split_file = file.split(".")
if len(split_file) == 2:
@ -52,5 +53,5 @@ def set_path(output_path):
extension = "png"
file = file + ".png"
folder.mkdir(parents=True, exist_ok=True)
return str(folder / file), extension
pathlib.Path(folder).mkdir(parents=True, exist_ok=True)
return "/".join([folder, file]), extension

View File

@ -3,12 +3,11 @@ import os
import textwrap
from typing import Optional
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import gridspec, ticker
from matplotlib import gridspec
from matplotlib.patches import Patch
from PIL import Image, UnidentifiedImageError
from PIL import Image
from scipy.fft import fft, fftshift
from scipy.signal import spectrogram
from scipy.signal.windows import hann
@ -186,7 +185,7 @@ def view_sig(
logo: Optional[bool] = True,
dark: Optional[bool] = True,
spines: Optional[bool] = False,
title_fontsize: Optional[int] = 25,
title_fontsize: Optional[int] = 35,
subtitle_fontsize: Optional[int] = 15,
) -> None:
"""
@ -231,26 +230,11 @@ def view_sig(
complex_signal = recording.data[0]
sample_rate, center_frequency, _ = extract_metadata_fields(recording.metadata)
subplot_height = 3 * (plot_spectrogram) + 2 * (iq + frequency) + 3 * (constellation or metadata or logo)
subplot_height = 2 * (plot_spectrogram + 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")
@ -268,8 +252,8 @@ def view_sig(
plot_x_indx = 0
if plot_spectrogram:
spec_ax = plt.subplot(gs[plot_y_indx : plot_y_indx + 3, :])
plot_y_indx = plot_y_indx + 3
spec_ax = plt.subplot(gs[plot_y_indx : plot_y_indx + 2, :])
plot_y_indx = plot_y_indx + 2
fft_size = get_fft_size(plot_length=plot_length)
_, t_spec, Sxx = spectrogram(
@ -296,10 +280,7 @@ def view_sig(
)
set_spines(spec_ax, spines)
spec_ax.set_title("Spectrogram", loc="left", fontsize=subtitle_fontsize)
spec_ax.set_xlabel("Time (s)")
spec_ax.set_ylabel("Frequency (MHz)")
spec_ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, _: f"{x / 1e6:.1f}"))
spec_ax.set_title("Spectrogram", loc="center", fontsize=subtitle_fontsize)
if iq:
iq_ax = plt.subplot(gs[plot_y_indx : plot_y_indx + 2, :])
@ -310,13 +291,12 @@ 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(True, alpha=0.2, linewidth=0.5)
iq_ax.grid(False)
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", loc="left", fontsize=subtitle_fontsize)
iq_ax.legend(loc="upper right", fontsize=10)
iq_ax.set_title("IQ Sample Plot", fontsize=subtitle_fontsize)
set_spines(iq_ax, spines)
if frequency:
@ -330,14 +310,10 @@ def view_sig(
# Convert to dB
spectrum_db = 20 * np.log10(spectrum + 1e-12) # 20*log for magnitude
freqs = (
np.linspace(-sample_rate / 2, sample_rate / 2, len(complex_signal[:plot_length])) + center_frequency
) / 1e6
freqs = np.linspace(-sample_rate / 2, sample_rate / 2, len(complex_signal[:plot_length])) + center_frequency
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.grid(True, alpha=0.2, linewidth=0.5)
freq_ax.set_title("Frequency Spectrum (Windowed FFT)", loc="left", fontsize=subtitle_fontsize)
freq_ax.set_title("Frequency Spectrum (Windowed FFT)", fontsize=subtitle_fontsize)
set_spines(freq_ax, spines)
if constellation:
@ -350,7 +326,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", loc="left", fontsize=subtitle_fontsize)
const_ax.set_title("Constellation", fontsize=subtitle_fontsize)
const_ax.set_aspect("equal")
if not spines:
@ -399,8 +375,8 @@ def view_sig(
image = Image.open(logo_path) # Open the PNG image using PIL
logo_ax.imshow(image)
except (FileNotFoundError, UnidentifiedImageError, OSError) as exc:
print(f"Warning, could not load logo image: {logo_path}. Reason: {exc}")
except FileNotFoundError:
print(f"Warning, {logo_path} not found.")
fig.subplots_adjust(
left=0.1, # Left margin

View File

@ -119,19 +119,24 @@ def setup_style(*, labels_mode: bool = False, compact_mode: bool = False) -> Non
label_font = 14
else:
base_font = 10
title_font = 15
title_font = 12
label_font = 10
matplotlib.rcParams.update(
{
"figure.facecolor": "#161616",
"axes.facecolor": "#161616",
"savefig.facecolor": "#161616",
"savefig.edgecolor": "#161616",
"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,
"font.size": base_font,
"axes.titlesize": title_font,
"axes.labelsize": label_font,
"figure.titlesize": title_font + 4,
"figure.titlesize": title_font + 2,
"legend.frameon": False,
"legend.facecolor": "none",
"xtick.labelsize": base_font,
@ -189,7 +194,7 @@ def view_simple_sig(
constellation_mode: Optional[bool] = False,
labels_mode: Optional[bool] = False,
slice: Optional[tuple] = None,
title: Optional[str] = "Signal Plot",
title: Optional[str] = "Signal",
):
"""
Create a simple plot of various signal visualizations as a png or svg image.
@ -232,7 +237,7 @@ def view_simple_sig(
spec_signal = signal
if compact_mode:
fig, (ax2, ax1) = plt.subplots(2, 1, figsize=(12, 6), gridspec_kw={"height_ratios": [5, 1]})
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 6), gridspec_kw={"height_ratios": [1, 5]})
show_title = False
show_labels = False
ax_constellation = ax_psd = None
@ -248,24 +253,25 @@ def view_simple_sig(
ax_psd = None
else:
if constellation_mode:
fig, ((ax2, ax1), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12))
ax_constellation, ax_psd = ax3, ax4
else:
fig, (ax2, ax1) = plt.subplots(2, 1, figsize=(14, 10))
fig, (ax1, ax2) = 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=25)
fig.patch.set_facecolor(matplotlib.rcParams["figure.facecolor"])
fig.suptitle(title, fontsize=16, color=COLORS["light"], y=0.96)
fig.patch.set_facecolor("#0f172a")
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.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)
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)
nfft, overlap = _get_nfft_size(signal=signal, fast_mode=fast_mode)
@ -279,7 +285,7 @@ def view_simple_sig(
)
ax2.set_ylim(center_freq_hz - sample_rate_hz / 2, center_freq_hz + sample_rate_hz / 2)
ax1.set_xlim(ax2.get_xlim())
ax2.set_xlim(0, total_duration_s)
if show_labels:
if horizontal_mode:
@ -288,25 +294,20 @@ def view_simple_sig(
ax2.set_xlabel("Time (s)")
ax1.set_ylabel("Amplitude")
ax1.set_title(f"IQ Sample Plot - {sdr} SDR", loc="left", pad=10, fontsize=15)
ax1.legend(loc="upper right", fontsize=10)
ax1.set_title(f"Time Series - {sdr} SDR", loc="left", pad=10)
ax1.legend(loc="upper right")
ax2.set_ylabel("Frequency (MHz)")
ax2.set_ylabel("Frequency (Hz)")
ax2.set_title(
f"Spectrogram - {center_freq_hz / 1e6:.1f} MHz ± {sample_rate_hz / 2e6:.1f} MHz",
loc="left",
pad=10,
fontsize=15,
f"Spectrogram - {center_freq_hz / 1e6:.1f} MHz ± {sample_rate_hz / 2e6:.1f} MHz", loc="left", pad=10
)
ax2.yaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(lambda x, _: f"{x / 1e6:.1f}"))
yticks = ax2.get_yticks()
ax2.set_yticklabels([f"{y / 1e6:.1f}" for y in yticks])
elif not compact_mode:
ax1.set_title("IQ Sample Plot", loc="left", pad=10, fontsize=15)
ax1.legend(loc="upper right", fontsize=10)
ax1.set_title("Time Series", loc="left", pad=10)
ax1.legend(loc="upper right", fontsize=8)
ax2.set_xlabel("Time (s)")
ax2.set_ylabel("Frequency (MHz)")
ax2.set_title("Spectrogram", loc="left", pad=10, fontsize=15)
ax2.yaxis.set_major_formatter(matplotlib.ticker.FuncFormatter(lambda x, _: f"{x / 1e6:.1f}"))
ax2.set_title("Spectrogram", loc="left", pad=10)
_add_annotations(
annotations=annotations,
@ -338,8 +339,8 @@ def view_simple_sig(
)
ax_constellation.set_xlabel("In-phase (I)")
ax_constellation.set_ylabel("Quadrature (Q)")
ax_constellation.set_title("Constellation", loc="left", fontsize=15)
ax_constellation.grid(True, alpha=0.2, linewidth=0.5)
ax_constellation.set_title("Constellation")
ax_constellation.grid(True, alpha=0.3)
ax_constellation.set_aspect("equal")
if ax_psd is not None:
@ -350,11 +351,11 @@ def view_simple_sig(
freqs = freqs + center_freq_hz
spectrum_db = 10 * np.log10(spectrum + 1e-12)
ax_psd.plot(freqs / 1e6, spectrum_db, color=COLORS["accent"], linewidth=0.8)
ax_psd.plot(freqs / 1e6, spectrum_db, color=COLORS["accent"], linewidth=1.0)
ax_psd.set_xlabel("Frequency (MHz)")
ax_psd.set_ylabel("Power (dB)")
ax_psd.set_title("Power Spectral Density", loc="left", fontsize=15)
ax_psd.grid(True, alpha=0.2, linewidth=0.5)
ax_psd.set_title("Power Spectral Density")
ax_psd.grid(True, alpha=0.3)
if compact_mode:
ax1.set_xticks([])
@ -366,20 +367,13 @@ def view_simple_sig(
else:
plt.tight_layout()
if show_title:
plt.subplots_adjust(top=0.9)
plt.subplots_adjust(top=0.92)
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",
pad_inches=0.3,
facecolor=matplotlib.rcParams["savefig.facecolor"],
edgecolor=matplotlib.rcParams["savefig.edgecolor"],
)
plt.savefig(output_path, dpi=dpi_value, bbox_inches="tight", facecolor="#0f172a", edgecolor="none")
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, overwrite):
def determine_output_path(input_path, output_path, fmt, quiet, overwrite):
input_path = Path(input_path)
input_is_annotated = input_path.stem.endswith("_annotated")
@ -63,20 +63,24 @@ def determine_output_path(input_path, output_path, fmt, overwrite):
else:
target = input_path.with_name(f"{input_path.stem}_annotated{input_path.suffix}")
final_path = normalize_sigmf_path(target) if fmt == "sigmf" else target
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}")
if final_path.exists() and not overwrite:
raise click.ClickException(f"{final_path} already exists. Use --overwrite to replace it.")
# 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
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.
@ -86,16 +90,11 @@ 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, overwrite=overwrite
input_path=input_path, output_path=output_path, fmt=fmt, quiet=quiet, 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
@ -313,8 +312,6 @@ 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:
@ -366,9 +363,12 @@ def add(input, start, count, label, freq_lower, freq_upper, comment, annotation_
if comment:
click.echo(f" Comment: {comment}")
save_recording_auto(recording, output, input, quiet, overwrite)
if not quiet:
click.echo(" ✓ Saved")
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}")
# ============================================================================
@ -466,6 +466,8 @@ 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:
@ -501,10 +503,6 @@ 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")
@ -519,7 +517,6 @@ def energy(
nfft,
obw_power,
annotation_type,
sample_rate,
output,
overwrite,
quiet,
@ -542,11 +539,8 @@ 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:
@ -554,15 +548,6 @@ 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:")
@ -590,13 +575,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
@ -616,14 +601,10 @@ def energy(
default="standalone",
help="Annotation type",
)
@click.option(
"--sample-rate", type=float, default=None,
help="Sample rate in Hz (overrides metadata; required if not in file)"
)
@click.option("--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, sample_rate, output, overwrite, quiet):
def cusum(input, label, min_duration, window_size, tolerance, annotation_type, output, overwrite, quiet):
"""Auto-detect segments using CUSUM method.
Detects signal state changes (on/off, amplitude transitions). Best for
@ -635,10 +616,7 @@ def cusum(input, label, min_duration, window_size, tolerance, annotation_type, s
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:
@ -646,15 +624,6 @@ def cusum(input, label, min_duration, window_size, tolerance, annotation_type, s
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")
@ -675,13 +644,13 @@ def cusum(input, label, min_duration, window_size, tolerance, annotation_type, s
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
@ -706,14 +675,10 @@ def cusum(input, label, min_duration, window_size, tolerance, annotation_type, s
help="Annotation type",
)
@click.option("--channel", type=int, default=0, help="Channel index to annotate (default: 0)")
@click.option(
"--sample-rate", type=float, default=None,
help="Sample rate in Hz (overrides metadata; required if not in file)"
)
@click.option("--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, sample_rate, output, overwrite, quiet):
def threshold(input, threshold, label, window_size, annotation_type, channel, output, overwrite, quiet):
"""Auto-detect signals using threshold method.
Detects samples above a percentage of maximum magnitude. Best for simple
@ -723,13 +688,10 @@ def threshold(input, threshold, label, window_size, annotation_type, channel, sa
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:
@ -737,21 +699,11 @@ def threshold(input, threshold, label, window_size, annotation_type, channel, sa
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)
@ -767,13 +719,13 @@ def threshold(input, threshold, label, window_size, annotation_type, channel, sa
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)
@ -786,30 +738,11 @@ def threshold(input, threshold, label, window_size, annotation_type, channel, sa
@click.option("--nfft", type=int, default=65536, help="FFT size for spectral analysis")
@click.option("--noise-threshold-db", type=float, help="Noise floor threshold in dB (auto-estimated if not specified)")
@click.option("--min-component-bw", type=float, default=50e3, help="Min component bandwidth in Hz")
@click.option(
"--sample-rate", type=float, default=None,
help="Sample rate in Hz (overrides metadata; required if not in file)"
)
@click.option("--output", "-o", type=click.Path(), help="Output file path")
@click.option("--overwrite", is_flag=True, help="Overwrite input file (non-SigMF only)")
@click.option("--quiet", is_flag=True, help="Quiet mode")
@click.option("--verbose", is_flag=True, help="Verbose output (show detected components)")
def _log_separate_start(quiet, recording, indices_list, nfft, noise_threshold_db, min_component_bw):
if not quiet:
click.echo("\nSplitting annotations by frequency components...")
click.echo(f" Input annotations: {len(recording.annotations)}")
if indices_list:
click.echo(f" Splitting indices: {indices_list}")
click.echo(f" FFT size: {nfft}")
if noise_threshold_db is not None:
click.echo(f" Noise threshold: {noise_threshold_db} dB")
else:
click.echo(" Noise threshold: auto-estimated")
click.echo(f" Min component BW: {format_frequency(min_component_bw)}")
def separate(
input, indices, nfft, noise_threshold_db, min_component_bw, sample_rate, output, overwrite, quiet, verbose):
def separate(input, indices, nfft, noise_threshold_db, min_component_bw, output, overwrite, quiet, verbose):
"""
Auto-detect parallel frequency-offset signals and split into sub-bands.
@ -835,8 +768,6 @@ def separate(
ria annotate separate signal.npy --min-component-bw 100000
"""
check_output_available(input, output, overwrite)
try:
recording = load_recording(input)
if not quiet:
@ -844,15 +775,6 @@ def separate(
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)
@ -861,7 +783,17 @@ def separate(
click.echo("No annotations to split")
return
_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)}")
try:
initial_count = len(recording.annotations)
@ -889,9 +821,8 @@ def separate(
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")

View File

@ -1,142 +0,0 @@
"""Structured error reporting for `ria-agent register` (T2)."""
from __future__ import annotations
import json
import sys
import urllib.error
from io import BytesIO
from unittest.mock import patch
import pytest
from ria_toolkit_oss.agent import cli as agent_cli
def _structured(reason: str) -> bytes:
return json.dumps({"detail": {"reason": reason}}).encode()
@pytest.mark.parametrize(
"reason",
["invalid_key", "expired", "revoked", "already_consumed"],
)
def test_explain_maps_known_reasons(reason):
msg = agent_cli._explain_registration_failure(403, _structured(reason))
assert msg == agent_cli.REGISTRATION_REASON_MESSAGES[reason]
def test_explain_unknown_reason_falls_through_with_code():
msg = agent_cli._explain_registration_failure(403, _structured("brand_new_thing"))
assert "brand_new_thing" in msg
assert "rejected" in msg.lower()
def test_explain_string_detail():
body = json.dumps({"detail": "Forbidden"}).encode()
msg = agent_cli._explain_registration_failure(403, body)
assert msg == "Registration rejected: Forbidden"
def test_explain_429_with_string_detail():
body = json.dumps({"detail": "Too many attempts; try again shortly"}).encode()
msg = agent_cli._explain_registration_failure(429, body)
assert "rate-limited" in msg
assert "Too many attempts" in msg
def test_explain_429_with_no_body():
msg = agent_cli._explain_registration_failure(429, b"")
assert "rate-limited" in msg
def test_explain_malformed_json():
msg = agent_cli._explain_registration_failure(500, b"<html>boom</html>")
assert msg.startswith("HTTP 500")
assert "boom" in msg
def test_explain_empty_body():
msg = agent_cli._explain_registration_failure(502, b"")
assert msg == "HTTP 502: no body"
def _http_error(status: int, body: bytes) -> urllib.error.HTTPError:
return urllib.error.HTTPError(
url="http://hub/screens/agents/register",
code=status,
msg="",
hdrs=None, # type: ignore[arg-type]
fp=BytesIO(body),
)
def test_user_agent_is_set_and_not_python_default():
"""Cloudflare on `riahub.ai` returns 403 code 1010 to `Python-urllib/*`.
Guarding the UA explicitly is the entire point of the register-flow fix;
if this test ever breaks, the production bug is back.
"""
ua = agent_cli._user_agent()
assert ua, "User-Agent must not be empty"
assert not ua.lower().startswith("python-urllib"), (
f"User-Agent must not be Python's default (got {ua!r}) — Cloudflare blocks it"
)
assert ua.startswith("ria-agent/")
def test_register_request_carries_explicit_user_agent(tmp_path):
"""Capture the outbound urllib Request and verify the UA header is set."""
cfg_path = tmp_path / "agent.json"
captured: dict = {}
def _fake_urlopen(req, *args, **kwargs):
# urllib normalizes header names; get_header takes the title-cased form.
captured["ua"] = req.get_header("User-agent")
captured["api_key"] = req.get_header("X-api-key")
captured["timeout"] = kwargs.get("timeout")
raise urllib.error.HTTPError(
url=req.full_url, code=403, msg="", hdrs=None, # type: ignore[arg-type]
fp=BytesIO(_structured("invalid_key")),
)
with (
patch.dict("os.environ", {"RIA_AGENT_CONFIG": str(cfg_path)}, clear=False),
patch("urllib.request.urlopen", side_effect=_fake_urlopen),
patch.object(
sys,
"argv",
["ria-agent", "register", "--hub", "http://hub:3005", "--api-key", "ria_reg_x"],
),
):
with pytest.raises(SystemExit):
agent_cli.main()
assert captured["ua"], "User-Agent header was not sent"
assert not captured["ua"].lower().startswith("python-urllib")
assert captured["api_key"] == "ria_reg_x"
assert captured["timeout"] is not None, "register must pass a timeout to urlopen"
def test_register_surfaces_reason_on_http_error(tmp_path, capsys):
cfg_path = tmp_path / "agent.json"
err = _http_error(403, _structured("revoked"))
with (
patch.dict("os.environ", {"RIA_AGENT_CONFIG": str(cfg_path)}, clear=False),
patch("urllib.request.urlopen", side_effect=err),
patch.object(
sys,
"argv",
["ria-agent", "register", "--hub", "http://hub:3005", "--api-key", "ria_reg_x"],
),
):
with pytest.raises(SystemExit) as exc:
agent_cli.main()
assert exc.value.code == 1
captured = capsys.readouterr()
assert "revoked" in captured.err.lower()
assert "Settings → RIA Agents" in captured.err
# Config must NOT be written on failure.
assert not cfg_path.exists()