Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: ["3.7", "3.9", "3.11", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![PyPI Downloads Shield](https://img.shields.io/pypi/dm/sigmf)](https://pypi.org/project/SigMF/)

The `sigmf` library makes it easy to interact with Signal Metadata Format
(SigMF) recordings. This library is compatible with Python 3.7-3.14 and is distributed
(SigMF) recordings. This library is compatible with Python 3.10-3.14 and is distributed
freely under the terms GNU Lesser GPL v3 License.

This module follows the SigMF specification [html](https://sigmf.org/)/[pdf](https://sigmf.github.io/SigMF/sigmf-spec.pdf) from the [spec repository](https://github.com/sigmf/SigMF).
Expand Down
180 changes: 99 additions & 81 deletions docs/source/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,105 @@ Advanced
Here we discuss more advanced techniques for working with **collections** and
**archives**.

-----------------------------------------------
Load a SigMF Archive and slice without untaring
-----------------------------------------------

Since an *archive* is a tarball (uncompressed by default), you can access the
*data* part of a SigMF archive without un-taring it. This is a compelling
feature because **1** archives make it harder for the ``-data`` and the
``-meta`` to get separated, and **2** some datasets are so large that it can be
impractical (due to available disk space, or slow network speeds if the archive
file resides on a network file share) or simply obnoxious to untar it first.

::

>>> import sigmf
>>> signal = sigmf.fromfile('/src/LTE.sigmf')
>>> signal.shape
(15379532,)
>>> signal.ndim
1
>>> signal[:10]
array([-0.023+0.012j, -0.021-0.006j, -0.017-0.020j, -0.013-0.052j,
0.000-0.075j, 0.022-0.058j, 0.048-0.044j, 0.049-0.060j,
0.031-0.056j, 0.023-0.047j], dtype=complex64)

Archives can contain fixed-point data types like ``complex-int16`` (``ci16``),
which have no direct ``numpy`` equivalent. By default, this data is automatically
scaled to floating-point values in the range ``[-1.0, 1.0]`` and returned as
``numpy.complex64``:

::

>>> signal.get_global_field(sigmf.DATATYPE_KEY)
'ci16_le'

------------------------------
Compressed SigMF Archives
------------------------------

SigMF archives can be compressed using gzip, xz, or zip.
The file extension determines the archive format:

+---------------------+-------------+
| Extension | Format |
+=====================+=============+
| ``.sigmf`` | uncompressed|
+---------------------+-------------+
| ``.sigmf.gz`` | gzip tar |
+---------------------+-------------+
| ``.sigmf.xz`` | xz tar |
+---------------------+-------------+
| ``.sigmf.zip`` | zip archive |
+---------------------+-------------+

**Writing compressed archives:**

::

>>> import sigmf
>>> signal = sigmf.sigmffile.fromfile('recording.sigmf-meta')

# extension determines format
>>> signal.tofile('recording.sigmf.xz')
>>> signal.archive('recording.sigmf.gz')

# compression parameter creates archive with correct extension
>>> signal.tofile('recording', compression='xz') # → recording.sigmf.xz
>>> signal.archive('recording', compression='gz') # → recording.sigmf.gz

**Reading compressed archives:**

::

>>> signal = sigmf.fromfile('recording.sigmf.xz')
>>> signal[:10]
array([-0.023+0.012j, -0.021-0.006j, ...], dtype=complex64)

**Memory behavior:**

Uncompressed ``.sigmf`` archives use ``numpy.memmap`` for zero-copy access.
Compressed archives must decompress into RAM before access.

--------------------------------
Control Fixed-Point Data Scaling
--------------------------------

You can control whether fixed-point samples are automatically scaled:

.. code-block:: python

import sigmf

# Default: autoscale fixed-point data to [-1.0, 1.0] range
handle = sigmf.fromfile("fixed_point_data.sigmf")
samples = handle.read_samples() # Returns float32/complex64

# Disable autoscaling to access raw integer values
handle_raw = sigmf.fromfile("fixed_point_data.sigmf", autoscale=False)
raw_samples = handle_raw.read_samples() # Returns original integer types

------------------------------
Iterate over SigMF Annotations
------------------------------
Expand Down Expand Up @@ -150,84 +249,3 @@ The SigMF Collection and its associated Recordings can now be loaded like this:
collection = sigmf.fromfile("example_zeros")
ci16_sigmffile = collection.get_SigMFFile(stream_name="example_ci16")
cf32_sigmffile = collection.get_SigMFFile(stream_name="example_cf32")

-----------------------------------------------
Load a SigMF Archive and slice without untaring
-----------------------------------------------

Since an *archive* is a tarball (uncompressed by default), you can access the
*data* part of a SigMF archive without un-taring it. This is a compelling
feature because **1** archives make it harder for the ``-data`` and the
``-meta`` to get separated, and **2** some datasets are so large that it can be
impractical (due to available disk space, or slow network speeds if the archive
file resides on a network file share) or simply obnoxious to untar it first.

::

>>> import sigmf
>>> signal = sigmf.fromarchive('/src/LTE.sigmf')
>>> signal.shape
(15379532,)
>>> signal.ndim
1
>>> signal[:10]
array([-0.023+0.012j, -0.021-0.006j, -0.017-0.020j, -0.013-0.052j,
0.000-0.075j, 0.022-0.058j, 0.048-0.044j, 0.049-0.060j,
0.031-0.056j, 0.023-0.047j], dtype=complex64)

Archives can contain fixed-point data types like ``complex-int16`` (``ci16``),
which have no direct ``numpy`` equivalent. By default, this data is automatically
scaled to floating-point values in the range ``[-1.0, 1.0]`` and returned as
``numpy.complex64``:

::

>>> signal.get_global_field(sigmf.DATATYPE_KEY)
'ci16_le'

------------------------------
Compressed SigMF Archives
------------------------------

SigMF archives can be compressed using gzip, xz, or zip.
The file extension determines the archive format:

+---------------------+-------------+
| Extension | Format |
+=====================+=============+
| ``.sigmf`` | uncompressed|
+---------------------+-------------+
| ``.sigmf.gz`` | gzip tar |
+---------------------+-------------+
| ``.sigmf.xz`` | xz tar |
+---------------------+-------------+
| ``.sigmf.zip`` | zip archive |
+---------------------+-------------+

**Writing compressed archives:**

::

>>> import sigmf
>>> signal = sigmf.sigmffile.fromfile('recording.sigmf-meta')

# extension determines format
>>> signal.tofile('recording.sigmf.xz')
>>> signal.archive('recording.sigmf.gz')

# compression parameter creates archive with correct extension
>>> signal.tofile('recording', compression='xz') # → recording.sigmf.xz
>>> signal.archive('recording', compression='gz') # → recording.sigmf.gz

**Reading compressed archives:**

::

>>> signal = sigmf.fromfile('recording.sigmf.xz')
>>> signal[:10]
array([-0.023+0.012j, -0.021-0.006j, ...], dtype=complex64)

**Memory behavior:**

Uncompressed ``.sigmf`` archives use ``numpy.memmap`` for zero-copy access.
Compressed archives must decompress into RAM before access.
26 changes: 26 additions & 0 deletions docs/source/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,32 @@ Frequently Asked Questions
Formulate them as a question and an answer.
Consider that the answer is best as a reference to another place in the documentation.

----------------------------------------
Can I use my own custom metadata fields?
----------------------------------------

*Yes*, you can add arbitrary fields to the ``global``, ``captures``, and
``annotations`` objects. However, we recommend defining custom fields in a
SigMF extension and listing that extension in ``core:extensions`` so that other
tools can understand your metadata:

.. code-block:: json

"global": {
"core:extensions": [
{
"name": "my-extension",
"version": "0.0.1",
"optional": true
}
],
"my-extension:my_field": "some value"
}

If you think your extension will be useful to others, consider publishing it or
submitting it to the `SigMF Community Extensions repository
<https://github.com/sigmf/community-extensions>`_.

---------------------------
Is this a GNU Radio effort?
---------------------------
Expand Down
2 changes: 1 addition & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ It offers a *simple* and *intuitive* API for Python developers.
This documentation is for version |toolversion| of the library, which is
compatible with version |specversion| of the SigMF specification.

To get started, see `quickstart`.
To get started, see :doc:`quickstart`.

-----

Expand Down
21 changes: 0 additions & 21 deletions docs/source/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,24 +164,3 @@ method-based approach.
Only core **global** fields support attribute access. Capture and annotation
fields must still be accessed using the traditional ``get_captures()`` and
``get_annotations()`` methods.

--------------------------------
Control Fixed-Point Data Scaling
--------------------------------

For fixed-point datasets, you can control whether samples are automatically scaled to floating-point values:

.. code-block:: python

import sigmf

# Default behavior: autoscale fixed-point data to [-1.0, 1.0] range
handle = sigmf.fromfile("fixed_point_data.sigmf")
samples = handle.read_samples() # Returns float32/complex64

# Disable autoscaling to access raw integer values
handle_raw = sigmf.fromfile("fixed_point_data.sigmf", autoscale=False)
raw_samples = handle_raw.read_samples() # Returns original integer types

# Both slicing and read_samples() respect the autoscale setting
assert handle[0:10].dtype == handle.read_samples(count=10).dtype
7 changes: 2 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ classifiers = [
"License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand All @@ -20,7 +17,7 @@ classifiers = [
"Topic :: Communications :: Ham Radio",
]
dynamic = ["version", "readme"]
requires-python = ">=3.7"
requires-python = ">=3.10"
dependencies = [
"numpy", # for vector math
"jsonschema", # for spec validation
Expand Down Expand Up @@ -106,7 +103,7 @@ line-length = 120
legacy_tox_ini = '''
[tox]
skip_missing_interpreters = True
envlist = py{37,38,39,310,311,312,313,314}
envlist = py{310,311,312,313,314}

[testenv]
usedevelop = True
Expand Down
2 changes: 1 addition & 1 deletion sigmf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# SPDX-License-Identifier: LGPL-3.0-or-later

# version of this python module
__version__ = "1.11.2"
__version__ = "1.12.0"
# matching version of the SigMF specification
__specification__ = "1.2.6"

Expand Down
4 changes: 2 additions & 2 deletions sigmf/convert/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ def detect_converter(file_path: Path):
return "signalhound"
else:
raise SigMFConversionError(
f"Unsupported XML file format. Root element: {expanded_magic_bytes}. "
f"Unsupported XML file format. Root element: {expanded_magic_bytes.decode('utf-8', errors='replace')}. "
f"Expected SignalHoundIQFile for Signal Hound Spike files."
)

else:
raise SigMFConversionError(
f"Unsupported file format. Magic bytes: {magic_bytes}. "
f"Unsupported file format. Magic bytes: {magic_bytes.decode('utf-8', errors='replace')}. "
f"Supported formats for conversion are WAV, BLUE/Platinum, and Signal Hound Spike."
)
Loading