From 14b7786e33fa2ff01c46b2c12e8f5fe1c05f5d0e Mon Sep 17 00:00:00 2001 From: Teque5 Date: Wed, 8 Jan 2025 11:59:52 -0800 Subject: [PATCH 1/4] add ReadtheDocs config; officially support python3.13 --- .github/workflows/main.yml | 2 +- .gitignore | 15 ++++++--- .readthedocs.yaml | 26 ++++++++++++++ docs/Makefile | 20 +++++++++++ docs/make.bat | 35 +++++++++++++++++++ docs/requirements.txt | 2 ++ docs/source/api.rst | 7 ++++ docs/source/conf.py | 53 +++++++++++++++++++++++++++++ docs/source/index.rst | 20 +++++++++++ docs/source/usage.rst | 69 ++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 11 files changed, 244 insertions(+), 7 deletions(-) create mode 100644 .readthedocs.yaml create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/requirements.txt create mode 100644 docs/source/api.rst create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100644 docs/source/usage.rst diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e9f7109..e688093 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - python-version: ["3.7", "3.9", "3.12"] + python-version: ["3.7", "3.9", "3.11", "3.13"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} diff --git a/.gitignore b/.gitignore index 5c9c73d..8f29b01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,14 @@ # temp files +__pycache__/ *.swp -*.pyc +*.py[cod] .cache -# setuptools related -dist/* -build/* -.eggs/* +# packaging related +dist/ +build/ +eggs/ +.eggs/ SigMF.egg-info/* # test related @@ -16,3 +18,6 @@ SigMF.egg-info/* coverage.xml pytest.xml htmlcov/* + +# docs related +docs/_build/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..9ee2227 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,26 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version, and other tools you might need +build: + os: ubuntu-24.04 + tools: + python: "3.13" + +# declare the Python requirements required to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - method: pip + path: . + extra_requirements: + - test + - apps + - requirements: docs/requirements.txt + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/source/conf.py diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..6247f7e --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..53fc1f3 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx==7.1.2 +sphinx-rtd-theme==1.3.0rc1 diff --git a/docs/source/api.rst b/docs/source/api.rst new file mode 100644 index 0000000..a693f61 --- /dev/null +++ b/docs/source/api.rst @@ -0,0 +1,7 @@ +API +=== + +.. autosummary:: + :toctree: generated + + sigmf diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..0524fd0 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,53 @@ +# Copyright: Multiple Authors +# +# This file is part of sigmf-python. https://github.com/sigmf/sigmf-python +# +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Configuration file for the Sphinx documentation builder.""" + +import datetime +import re +from pathlib import Path + +# parse info from project files + +root = Path(__file__).parent.parent.parent +with open(root / "sigmf" / "__init__.py", "r") as handle: + init = handle.read() + toolversion = re.search(r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]', init).group(1) + specversion = re.search(r'__specification__\s*=\s*[\'"]([^\'"]*)[\'"]', init).group(1) +print("DBUG", toolversion, specversion) + +# -- Project information + +project = "sigmf" +author = "Multiple Authors" +copyright = f"2017-{datetime.date.today().year}, {author}" + +release = toolversion +version = toolversion + +# -- General configuration + +extensions = [ + "sphinx.ext.duration", + "sphinx.ext.doctest", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.intersphinx", +] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3/", None), + "sphinx": ("https://www.sphinx-doc.org/en/master/", None), +} +intersphinx_disabled_domains = ["std"] + +templates_path = ["_templates"] + +# -- Options for HTML output + +html_theme = "sphinx_rtd_theme" + +# -- Options for EPUB output +epub_show_urls = "footnote" diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..5050d21 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,20 @@ +Welcome to SigMF! +================= + +**sigmf** is a Python library for working with radio recordings as specified in the `SigMF `_ standard. +It offers a *simple* and *intuitive* API for python developers. + +Check out the :doc:`usage` section for further information, including +how to :ref:`installation` the project. + +.. note:: + + This project is under active development. + +Contents +-------- + +.. toctree:: + + usage + api diff --git a/docs/source/usage.rst b/docs/source/usage.rst new file mode 100644 index 0000000..4d42aac --- /dev/null +++ b/docs/source/usage.rst @@ -0,0 +1,69 @@ +Usage +===== + +.. _installation: + +Installation +------------ + +To install the latest PyPi release, install from pip: + +.. code-block:: console + + $ pip install sigmf + +To install the latest git release, build from source: + +.. code-block:: console + + $ git clone https://github.com/sigmf/sigmf-python.git + $ cd sigmf-python + $ pip install . + +Testing +------- + +Testing can be run locally: + +.. code-block:: console + + $ coverage run + +Run coverage on multiple python versions: + +.. code-block:: console + + $ tox run + +Other tools developers may want to use: + +.. code-block:: console + + $ pytest -rA tests/test_archive.py # test one file verbosely + $ pylint sigmf tests # lint entire project + $ black sigmf tests # autoformat entire project + $ isort sigmf tests # format imports for entire project + +Examples +-------- + +Load a SigMF archive; read all samples & metadata +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + import sigmf + handle = sigmf.sigmffile.fromfile("example.sigmf") + handle.read_samples() # returns all timeseries data + handle.get_global_info() # returns 'global' dictionary + handle.get_captures() # returns list of 'captures' dictionaries + handle.get_annotations() # returns list of all annotations + +Verify SigMF dataset integrity & compliance +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: console + + $ sigmf_validate example.sigmf + +TODO: Insert more examples from `README.md`. diff --git a/pyproject.toml b/pyproject.toml index 563e7ce..afe9c18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,12 +7,12 @@ 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", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering", "Topic :: Communications :: Ham Radio", ] From 5b1b4a907d36b6ccc6c5b54ec45ec62e98a4fd89 Mon Sep 17 00:00:00 2001 From: Teque5 Date: Wed, 8 Jan 2025 16:04:15 -0800 Subject: [PATCH 2/4] add shields for docs, downloads, workflow status --- .gitignore | 1 + README.md | 6 +- docs/requirements.txt | 5 +- .../_templates/custom-class-template.rst | 34 ++++++++++ .../_templates/custom-module-template.rst | 66 +++++++++++++++++++ docs/source/api.rst | 4 +- docs/source/conf.py | 12 +++- docs/source/usage.rst | 4 +- pyproject.toml | 7 ++ 9 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 docs/source/_templates/custom-class-template.rst create mode 100644 docs/source/_templates/custom-module-template.rst diff --git a/.gitignore b/.gitignore index 8f29b01..85bad5a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ htmlcov/* # docs related docs/_build/ +docs/source/_autosummary/ diff --git a/README.md b/README.md index 2f4d4d5..51f73c9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -

Rendered SigMF Logo

+![Rendered SigMF Logo](https://raw.githubusercontent.com/sigmf/SigMF/refs/heads/main/logo/sigmf_logo.png) + +[![Documentation Shield](https://img.shields.io/readthedocs/sigmf)](https://sigmf.readthedocs.io/en/latest/) +[![Build Status Shield](https://img.shields.io/github/actions/workflow/status/sigmf/sigmf-python/main.yml)](https://github.com/sigmf/sigmf-python/actions?query=branch%3Amain) +[![PyPI Downloads Shield](https://img.shields.io/pypi/dm/sigmf)](https://pypi.org/project/SigMF/) This python module makes it easy to interact with Signal Metadata Format (SigMF) recordings. This module works with Python 3.7+ and is distributed diff --git a/docs/requirements.txt b/docs/requirements.txt index 53fc1f3..1016144 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ -sphinx==7.1.2 -sphinx-rtd-theme==1.3.0rc1 +# pinned 2025-01-15 +sphinx==8.1.3 +sphinx-rtd-theme==3.0.2 diff --git a/docs/source/_templates/custom-class-template.rst b/docs/source/_templates/custom-class-template.rst new file mode 100644 index 0000000..f73eda5 --- /dev/null +++ b/docs/source/_templates/custom-class-template.rst @@ -0,0 +1,34 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} + :members: + :show-inheritance: + :inherited-members: + :special-members: __call__, __add__, __mul__ + + {% block methods %} + {% if methods %} + .. rubric:: {{ _('Methods') }} + + .. autosummary:: + :nosignatures: + {% for item in methods %} + {%- if not item.startswith('_') %} + ~{{ name }}.{{ item }} + {%- endif -%} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block attributes %} + {% if attributes %} + .. rubric:: {{ _('Attributes') }} + + .. autosummary:: + {% for item in attributes %} + ~{{ name }}.{{ item }} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/docs/source/_templates/custom-module-template.rst b/docs/source/_templates/custom-module-template.rst new file mode 100644 index 0000000..d066d0e --- /dev/null +++ b/docs/source/_templates/custom-module-template.rst @@ -0,0 +1,66 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + + {% block attributes %} + {% if attributes %} + .. rubric:: Module attributes + + .. autosummary:: + :toctree: + {% for item in attributes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block functions %} + {% if functions %} + .. rubric:: {{ _('Functions') }} + + .. autosummary:: + :toctree: + :nosignatures: + {% for item in functions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block classes %} + {% if classes %} + .. rubric:: {{ _('Classes') }} + + .. autosummary:: + :toctree: + :template: custom-class-template.rst + :nosignatures: + {% for item in classes %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block exceptions %} + {% if exceptions %} + .. rubric:: {{ _('Exceptions') }} + + .. autosummary:: + :toctree: + {% for item in exceptions %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + +{% block modules %} +{% if modules %} +.. autosummary:: + :toctree: + :template: custom-module-template.rst + :recursive: +{% for item in modules %} + {{ item }} +{%- endfor %} +{% endif %} +{% endblock %} diff --git a/docs/source/api.rst b/docs/source/api.rst index a693f61..4d602fd 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -2,6 +2,8 @@ API === .. autosummary:: - :toctree: generated + :toctree: _autosummary + :template: custom-module-template.rst + :recursive: sigmf diff --git a/docs/source/conf.py b/docs/source/conf.py index 0524fd0..d63c31b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -7,6 +7,7 @@ import datetime import re +import sys from pathlib import Path # parse info from project files @@ -16,7 +17,9 @@ init = handle.read() toolversion = re.search(r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]', init).group(1) specversion = re.search(r'__specification__\s*=\s*[\'"]([^\'"]*)[\'"]', init).group(1) -print("DBUG", toolversion, specversion) + +# autodoc needs special pathing +sys.path.append(str(root)) # -- Project information @@ -30,11 +33,12 @@ # -- General configuration extensions = [ - "sphinx.ext.duration", - "sphinx.ext.doctest", "sphinx.ext.autodoc", "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx.ext.duration", "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", # allows numpy-style docstrings ] intersphinx_mapping = { @@ -48,6 +52,8 @@ # -- Options for HTML output html_theme = "sphinx_rtd_theme" +html_favicon = "https://raw.githubusercontent.com/wiki/sigmf/SigMF/logo/logo-icon-32-folder.png" +html_logo = "https://raw.githubusercontent.com/sigmf/SigMF/refs/heads/main/logo/sigmf_logo.svg" # -- Options for EPUB output epub_show_urls = "footnote" diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 4d42aac..7991117 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -29,13 +29,13 @@ Testing can be run locally: $ coverage run -Run coverage on multiple python versions: +Run tests within a temporary environment: .. code-block:: console $ tox run -Other tools developers may want to use: +Tools developers may want to use: .. code-block:: console diff --git a/pyproject.toml b/pyproject.toml index afe9c18..56bb7d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ 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", @@ -24,6 +25,12 @@ dependencies = [ ] [project.urls] repository = "https://github.com/sigmf/sigmf-python" + documentation = "https://sigmf.readthedocs.io/en/latest/" + issues = "https://github.com/sigmf/sigmf-python/issues" + "Specification (HTML)" = "https://sigmf.org/" + "Specification (PDF)" = "https://sigmf.github.io/SigMF/sigmf-spec.pdf" + "Specification (Repo)" = "https://github.com/sigmf/SigMF" + [project.scripts] sigmf_validate = "sigmf.validate:main" sigmf_convert_wav = "sigmf.apps.convert_wav:main [apps]" From ac1f558dcfe9d4a4722496c81d1618ba3db7760c Mon Sep 17 00:00:00 2001 From: Teque5 Date: Wed, 15 Jan 2025 14:37:23 -0800 Subject: [PATCH 3/4] move most of README to rst; split examples into quickstart & advanced --- README.md | 247 +------------------------------------ docs/source/advanced.rst | 197 +++++++++++++++++++++++++++++ docs/source/api.rst | 5 +- docs/source/conf.py | 12 ++ docs/source/developers.rst | 60 +++++++++ docs/source/faq.rst | 28 +++++ docs/source/index.rst | 40 ++++-- docs/source/quickstart.rst | 82 ++++++++++++ docs/source/usage.rst | 69 ----------- 9 files changed, 415 insertions(+), 325 deletions(-) create mode 100644 docs/source/advanced.rst create mode 100644 docs/source/developers.rst create mode 100644 docs/source/faq.rst create mode 100644 docs/source/quickstart.rst delete mode 100644 docs/source/usage.rst diff --git a/README.md b/README.md index 51f73c9..5eedc3f 100644 --- a/README.md +++ b/README.md @@ -4,251 +4,10 @@ [![Build Status Shield](https://img.shields.io/github/actions/workflow/status/sigmf/sigmf-python/main.yml)](https://github.com/sigmf/sigmf-python/actions?query=branch%3Amain) [![PyPI Downloads Shield](https://img.shields.io/pypi/dm/sigmf)](https://pypi.org/project/SigMF/) -This python module makes it easy to interact with Signal Metadata Format -(SigMF) recordings. This module works with Python 3.7+ and is distributed +The `sigmf` module makes it easy to interact with Signal Metadata Format +(SigMF) recordings. This module works with Python 3.7-3.13 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). -# Installation - -To install the latest PyPi release, install from pip: - -```bash -pip install sigmf -``` - -To install the latest git release, build from source: - -```bash -git clone https://github.com/sigmf/sigmf-python.git -cd sigmf-python -pip install . -``` - -Testing can be run with a variety of tools: - -```bash -# pytest and coverage run locally -pytest -coverage run -# run coverage in a venv -tox run -# other useful tools -pylint sigmf tests -pytype -black -flake8 -``` - -# Examples - -### Load a SigMF archive; read all samples & metadata - -```python -import sigmf -handle = sigmf.sigmffile.fromfile('example.sigmf') -handle.read_samples() # returns all timeseries data -handle.get_global_info() # returns 'global' dictionary -handle.get_captures() # returns list of 'captures' dictionaries -handle.get_annotations() # returns list of all annotations -``` - -### Verify SigMF dataset integrity & compliance - -```bash -sigmf_validate example.sigmf -``` - -### Load a SigMF dataset; read its annotation, metadata, and samples - -```python -from sigmf import SigMFFile, sigmffile - -# Load a dataset -filename = 'logo/sigmf_logo' # extension is optional -signal = sigmffile.fromfile(filename) - -# Get some metadata and all annotations -sample_rate = signal.get_global_field(SigMFFile.SAMPLE_RATE_KEY) -sample_count = signal.sample_count -signal_duration = sample_count / sample_rate -annotations = signal.get_annotations() - -# Iterate over annotations -for adx, annotation in enumerate(annotations): - annotation_start_idx = annotation[SigMFFile.START_INDEX_KEY] - annotation_length = annotation[SigMFFile.LENGTH_INDEX_KEY] - annotation_comment = annotation.get(SigMFFile.COMMENT_KEY, "[annotation {}]".format(adx)) - - # Get capture info associated with the start of annotation - capture = signal.get_capture_info(annotation_start_idx) - freq_center = capture.get(SigMFFile.FREQUENCY_KEY, 0) - freq_min = freq_center - 0.5*sample_rate - freq_max = freq_center + 0.5*sample_rate - - # Get frequency edges of annotation (default to edges of capture) - freq_start = annotation.get(SigMFFile.FLO_KEY) - freq_stop = annotation.get(SigMFFile.FHI_KEY) - - # Get the samples corresponding to annotation - samples = signal.read_samples(annotation_start_idx, annotation_length) -``` - -### Create and save a Collection of SigMF Recordings from numpy arrays - -First, create a single SigMF Recording and save it to disk - -```python -import datetime as dt -import numpy as np -import sigmf -from sigmf import SigMFFile -from sigmf.utils import get_data_type_str, get_sigmf_iso8601_datetime_now - -# suppose we have an complex timeseries signal -data = np.zeros(1024, dtype=np.complex64) - -# write those samples to file in cf32_le -data.tofile('example_cf32.sigmf-data') - -# create the metadata -meta = SigMFFile( - data_file='example_cf32.sigmf-data', # extension is optional - global_info = { - SigMFFile.DATATYPE_KEY: get_data_type_str(data), # in this case, 'cf32_le' - SigMFFile.SAMPLE_RATE_KEY: 48000, - SigMFFile.AUTHOR_KEY: 'jane.doe@domain.org', - SigMFFile.DESCRIPTION_KEY: 'All zero complex float32 example file.', - } -) - -# create a capture key at time index 0 -meta.add_capture(0, metadata={ - SigMFFile.FREQUENCY_KEY: 915000000, - SigMFFile.DATETIME_KEY: get_sigmf_iso8601_datetime_now(), -}) - -# add an annotation at sample 100 with length 200 & 10 KHz width -meta.add_annotation(100, 200, metadata = { - SigMFFile.FLO_KEY: 914995000.0, - SigMFFile.FHI_KEY: 915005000.0, - SigMFFile.COMMENT_KEY: 'example annotation', -}) - -# check for mistakes & write to disk -meta.tofile('example_cf32.sigmf-meta') # extension is optional -``` - -Now lets add another SigMF Recording and associate them with a SigMF Collection: - -```python -from sigmf import SigMFCollection - -data_ci16 = np.zeros(1024, dtype=np.complex64) - -#rescale and save as a complex int16 file: -data_ci16 *= pow(2, 15) -data_ci16.view(np.float32).astype(np.int16).tofile('example_ci16.sigmf-data') - -# create the metadata for the second file -meta_ci16 = SigMFFile( - data_file='example_ci16.sigmf-data', # extension is optional - global_info = { - SigMFFile.DATATYPE_KEY: 'ci16_le', # get_data_type_str() is only valid for numpy types - SigMFFile.SAMPLE_RATE_KEY: 48000, - SigMFFile.DESCRIPTION_KEY: 'All zero complex int16 file.', - } -) -meta_ci16.add_capture(0, metadata=meta.get_capture_info(0)) -meta_ci16.tofile('example_ci16.sigmf-meta') - -collection = SigMFCollection(['example_cf32.sigmf-meta', 'example_ci16.sigmf-meta'], - metadata = {'collection': { - SigMFCollection.AUTHOR_KEY: 'sigmf@sigmf.org', - SigMFCollection.DESCRIPTION_KEY: 'Collection of two all zero files.', - } - } -) -streams = collection.get_stream_names() -sigmf = [collection.get_SigMFFile(stream) for stream in streams] -collection.tofile('example_zeros.sigmf-collection') -``` - -The SigMF Collection and its associated Recordings can now be loaded like this: - -```python -from sigmf import sigmffile -collection = sigmffile.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 its data without untaring it - -Since an *archive* is merely a tarball (uncompressed), and since there any many -excellent tools for manipulating tar files, it's fairly straightforward to -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. - -```python ->>> import sigmf ->>> arc = sigmf.SigMFArchiveReader('/src/LTE.sigmf') ->>> arc.shape -(15379532,) ->>> arc.ndim -1 ->>> arc[:10] -array([-20.+11.j, -21. -6.j, -17.-20.j, -13.-52.j, 0.-75.j, 22.-58.j, - 48.-44.j, 49.-60.j, 31.-56.j, 23.-47.j], dtype=complex64) -``` - -The preceeding example exhibits another feature of this approach; the archive -`LTE.sigmf` is actually `complex-int16`'s on disk, for which there is no -corresponding type in `numpy`. However, the `.sigmffile` member keeps track of -this, and converts the data to `numpy.complex64` *after* slicing it, that is, -after reading it from disk. - -```python ->>> arc.sigmffile.get_global_field(sigmf.SigMFFile.DATATYPE_KEY) -'ci16_le' - ->>> arc.sigmffile._memmap.dtype -dtype('int16') - ->>> arc.sigmffile._return_type -'>> import sigmf, io ->>> sigmf_bytes = io.BytesIO(open('/src/LTE.sigmf', 'rb').read()) ->>> arc = sigmf.SigMFArchiveReader(archive_buffer=sigmf_bytes) ->>> arc[:10] -array([-20.+11.j, -21. -6.j, -17.-20.j, -13.-52.j, 0.-75.j, 22.-58.j, - 48.-44.j, 49.-60.j, 31.-56.j, 23.-47.j], dtype=complex64) -``` - -# Frequently Asked Questions - -### Is this a GNU Radio effort? - -*No*, this is not a GNU Radio-specific effort. -This effort first emerged from a group of GNU Radio core -developers, but the goal of the project to provide a standard that will be -useful to anyone and everyone, regardless of tool or workflow. - -### Is this specific to wireless communications? - -*No*, similar to the response, above, the goal is to create something that is -generally applicable to _signal processing_, regardless of whether or not the -application is communications related. +[Please visit the sigmf-python documentation for install, examples, & more info.](https://sigmf.readthedocs.io/en/latest/) diff --git a/docs/source/advanced.rst b/docs/source/advanced.rst new file mode 100644 index 0000000..c167c18 --- /dev/null +++ b/docs/source/advanced.rst @@ -0,0 +1,197 @@ +======== +Advanced +======== + +Here we discuss more advanced techniques for working with **collections** and +**archives**. + +------------------------------ +Iterate over SigMF Annotations +------------------------------ + +Here we will load a SigMF dataset and iterate over the annotations. You can get +the recording of the SigMF logo used in this example `from the specification +`_. + +.. code-block:: python + + from sigmf import SigMFFile, sigmffile + + # Load a dataset + path = 'logo/sigmf_logo' # extension is optional + signal = sigmffile.fromfile(path) + + # Get some metadata and all annotations + sample_rate = signal.get_global_field(SigMFFile.SAMPLE_RATE_KEY) + sample_count = signal.sample_count + signal_duration = sample_count / sample_rate + annotations = signal.get_annotations() + + # Iterate over annotations + for adx, annotation in enumerate(annotations): + annotation_start_idx = annotation[SigMFFile.START_INDEX_KEY] + annotation_length = annotation[SigMFFile.LENGTH_INDEX_KEY] + annotation_comment = annotation.get(SigMFFile.COMMENT_KEY, "[annotation {}]".format(adx)) + + # Get capture info associated with the start of annotation + capture = signal.get_capture_info(annotation_start_idx) + freq_center = capture.get(SigMFFile.FREQUENCY_KEY, 0) + freq_min = freq_center - 0.5*sample_rate + freq_max = freq_center + 0.5*sample_rate + + # Get frequency edges of annotation (default to edges of capture) + freq_start = annotation.get(SigMFFile.FLO_KEY) + freq_stop = annotation.get(SigMFFile.FHI_KEY) + + # Get the samples corresponding to annotation + samples = signal.read_samples(annotation_start_idx, annotation_length) + + # Do something with the samples & metadata for each annotation here + +------------------------------------- +Save a Collection of SigMF Recordings +------------------------------------- + +First, create a single SigMF Recording and save it to disk: + +.. code-block:: python + + import datetime as dt + import numpy as np + import sigmf + from sigmf import SigMFFile + from sigmf.utils import get_data_type_str, get_sigmf_iso8601_datetime_now + + # suppose we have a complex timeseries signal + data = np.zeros(1024, dtype=np.complex64) + + # write those samples to file in cf32_le + data.tofile('example_cf32.sigmf-data') + + # create the metadata + meta = SigMFFile( + data_file='example_cf32.sigmf-data', # extension is optional + global_info = { + SigMFFile.DATATYPE_KEY: get_data_type_str(data), # in this case, 'cf32_le' + SigMFFile.SAMPLE_RATE_KEY: 48000, + SigMFFile.AUTHOR_KEY: 'jane.doe@domain.org', + SigMFFile.DESCRIPTION_KEY: 'All zero complex float32 example file.', + } + ) + + # create a capture key at time index 0 + meta.add_capture(0, metadata={ + SigMFFile.FREQUENCY_KEY: 915000000, + SigMFFile.DATETIME_KEY: get_sigmf_iso8601_datetime_now(), + }) + + # add an annotation at sample 100 with length 200 & 10 KHz width + meta.add_annotation(100, 200, metadata = { + SigMFFile.FLO_KEY: 914995000.0, + SigMFFile.FHI_KEY: 915005000.0, + SigMFFile.COMMENT_KEY: 'example annotation', + }) + + # check for mistakes & write to disk + meta.tofile('example_cf32.sigmf-meta') # extension is optional + +Now lets add another SigMF Recording and associate them with a SigMF Collection: + +.. code-block:: python + + from sigmf import SigMFCollection + + data_ci16 = np.zeros(1024, dtype=np.complex64) + + #rescale and save as a complex int16 file: + data_ci16 *= pow(2, 15) + data_ci16.view(np.float32).astype(np.int16).tofile('example_ci16.sigmf-data') + + # create the metadata for the second file + meta_ci16 = SigMFFile( + data_file='example_ci16.sigmf-data', # extension is optional + global_info = { + SigMFFile.DATATYPE_KEY: 'ci16_le', # get_data_type_str() is only valid for numpy types + SigMFFile.SAMPLE_RATE_KEY: 48000, + SigMFFile.DESCRIPTION_KEY: 'All zero complex int16 file.', + } + ) + meta_ci16.add_capture(0, metadata=meta.get_capture_info(0)) + meta_ci16.tofile('example_ci16.sigmf-meta') + + collection = SigMFCollection(['example_cf32.sigmf-meta', 'example_ci16.sigmf-meta'], + metadata = {'collection': { + SigMFCollection.AUTHOR_KEY: 'sigmf@sigmf.org', + SigMFCollection.DESCRIPTION_KEY: 'Collection of two all zero files.', + } + } + ) + streams = collection.get_stream_names() + sigmf = [collection.get_SigMFFile(stream) for stream in streams] + collection.tofile('example_zeros.sigmf-collection') + +The SigMF Collection and its associated Recordings can now be loaded like this: + +.. code-block:: python + + from sigmf import sigmffile + collection = sigmffile.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 merely a tarball (uncompressed), and since there any many +excellent tools for manipulating tar files, it's fairly straightforward to +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 + >>> arc = sigmf.SigMFArchiveReader('/src/LTE.sigmf') + >>> arc.shape + (15379532,) + >>> arc.ndim + 1 + >>> arc[:10] + array([-20.+11.j, -21. -6.j, -17.-20.j, -13.-52.j, 0.-75.j, 22.-58.j, + 48.-44.j, 49.-60.j, 31.-56.j, 23.-47.j], dtype=complex64) + +The preceeding example exhibits another feature of this approach; the archive +``LTE.sigmf`` is actually ``complex-int16``'s on disk, for which there is no +corresponding type in ``numpy``. However, the ``.sigmffile`` member keeps track of +this, and converts the data to ``numpy.complex64`` *after* slicing it, that is, +after reading it from disk. + +:: + + >>> arc.sigmffile.get_global_field(sigmf.SigMFFile.DATATYPE_KEY) + 'ci16_le' + + >>> arc.sigmffile._memmap.dtype + dtype('int16') + + >>> arc.sigmffile._return_type + '>> import sigmf, io + >>> sigmf_bytes = io.BytesIO(open('/src/LTE.sigmf', 'rb').read()) + >>> arc = sigmf.SigMFArchiveReader(archive_buffer=sigmf_bytes) + >>> arc[:10] + array([-20.+11.j, -21. -6.j, -17.-20.j, -13.-52.j, 0.-75.j, 22.-58.j, + 48.-44.j, 49.-60.j, 31.-56.j, 23.-47.j], dtype=complex64) \ No newline at end of file diff --git a/docs/source/api.rst b/docs/source/api.rst index 4d602fd..295a2dc 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -1,5 +1,6 @@ -API -=== +========= +SigMF API +========= .. autosummary:: :toctree: _autosummary diff --git a/docs/source/conf.py b/docs/source/conf.py index d63c31b..2c1c9f4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -56,4 +56,16 @@ html_logo = "https://raw.githubusercontent.com/sigmf/SigMF/refs/heads/main/logo/sigmf_logo.svg" # -- Options for EPUB output + epub_show_urls = "footnote" + +# Method to use variables within rst files +# https://stackoverflow.com/a/69211912/760099 + +variables_to_export = [ + "toolversion", + "specversion", +] +frozen_locals = dict(locals()) +rst_epilog = '\n'.join(map(lambda x: f".. |{x}| replace:: {frozen_locals[x]}", variables_to_export)) +del frozen_locals \ No newline at end of file diff --git a/docs/source/developers.rst b/docs/source/developers.rst new file mode 100644 index 0000000..2d10cf5 --- /dev/null +++ b/docs/source/developers.rst @@ -0,0 +1,60 @@ +========== +Developers +========== + +This page is for developers of the ``sigmf-python`` module. + +------- +Install +------- + +To install the latest git release, build from source: + +.. code-block:: console + + $ git clone https://github.com/sigmf/sigmf-python.git + $ cd sigmf-python + $ pip install . + +------- +Testing +------- + +This library contains many tests in the ``tests/`` folder. These can all be run locally: + +.. code-block:: console + + $ coverage run + +Or tests can be run within a temporary environment on all supported python versions: + +.. code-block:: console + + $ tox run + +To run a single (perhaps new) test that may be needed verbosely: + +.. code-block:: console + + $ pytest -rA tests/test_archive.py + +To lint the entire project and get suggested changes: + +.. code-block:: console + + $ pylint sigmf tests + +To autoformat the entire project according to our coding standard: + +.. code-block:: console + + $ black sigmf tests # autoformat entire project + $ isort sigmf tests # format imports for entire project + +------ +Issues +------ + +Issues can be addressed by opening an `issue +`_ or by forking the project and +submitting a `pull request `_. diff --git a/docs/source/faq.rst b/docs/source/faq.rst new file mode 100644 index 0000000..370a84c --- /dev/null +++ b/docs/source/faq.rst @@ -0,0 +1,28 @@ +========================== +Frequently Asked Questions +========================== + +.. contents:: + :local: + +.. + Frequently asked questions should be questions that actually got asked. + Formulate them as a question and an answer. + Consider that the answer is best as a reference to another place in the documentation. + +--------------------------- +Is this a GNU Radio effort? +--------------------------- + +*No*, this is not a GNU Radio-specific effort. +This effort first emerged from a group of GNU Radio core +developers, but the goal of the project to provide a standard that will be +useful to anyone and everyone, regardless of tool or workflow. + +-------------------------------------------- +Is this specific to wireless communications? +-------------------------------------------- + +*No*, similar to the response, above, the goal is to create something that is +generally applicable to *signal processing*, regardless of whether or not the +application is communications related. diff --git a/docs/source/index.rst b/docs/source/index.rst index 5050d21..a2ca1af 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,20 +1,40 @@ +================= Welcome to SigMF! ================= -**sigmf** is a Python library for working with radio recordings as specified in the `SigMF `_ standard. -It offers a *simple* and *intuitive* API for python developers. +**sigmf** is a Python library for working with radio recordings captured in +``.sigmf`` format according to the `SigMF standard `_. It +offers a *simple* and *intuitive* API for python developers. + +.. + Note below toolversion & specversion are replaced dynamically during build. -Check out the :doc:`usage` section for further information, including -how to :ref:`installation` the project. +This version |toolversion| of the library coincides with version |specversion| +of the specification. -.. note:: +Check out the :doc:`quickstart` section for further information, including +how to :ref:`install` the project. - This project is under active development. -Contents --------- +.. toctree:: + :maxdepth: 1 + :hidden: + :caption: SigMF + + quickstart + advanced + developers + +.. toctree:: + :maxdepth: 1 + :hidden: + :caption: Community + + faq .. toctree:: + :maxdepth: 1 + :hidden: + :caption: API Reference - usage - api + api \ No newline at end of file diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst new file mode 100644 index 0000000..ba4d093 --- /dev/null +++ b/docs/source/quickstart.rst @@ -0,0 +1,82 @@ +========== +Quickstart +========== + +Here we discuss how to do all basic operations with SigMF. + +.. _install: + +------- +Install +------- + +To install the latest PyPi release, install from pip: + +.. code-block:: console + + $ pip install sigmf + +---------------------- +Read a SigMF Recording +---------------------- + +.. code-block:: python + + import sigmf + handle = sigmf.sigmffile.fromfile("example.sigmf") + handle.read_samples() # returns all timeseries data + handle.get_global_info() # returns 'global' dictionary + handle.get_captures() # returns list of 'captures' dictionaries + handle.get_annotations() # returns list of all annotations + handle[10:50] # return memory slice of samples 10 through 50 + +----------------------------------- +Verify SigMF Integrity & Compliance +----------------------------------- + +.. code-block:: console + + $ sigmf_validate example.sigmf + +--------------------------------------- +Save a Numpy array as a SigMF Recording +--------------------------------------- + +.. code-block:: python + + import numpy as np + from sigmf import SigMFFile + from sigmf.utils import get_data_type_str, get_sigmf_iso8601_datetime_now + + # suppose we have a complex timeseries signal + data = np.zeros(1024, dtype=np.complex64) + + # write those samples to file in cf32_le + data.tofile('example_cf32.sigmf-data') + + # create the metadata + meta = SigMFFile( + data_file='example_cf32.sigmf-data', # extension is optional + global_info = { + SigMFFile.DATATYPE_KEY: get_data_type_str(data), # in this case, 'cf32_le' + SigMFFile.SAMPLE_RATE_KEY: 48000, + SigMFFile.AUTHOR_KEY: 'jane.doe@domain.org', + SigMFFile.DESCRIPTION_KEY: 'All zero complex float32 example file.', + } + ) + + # create a capture key at time index 0 + meta.add_capture(0, metadata={ + SigMFFile.FREQUENCY_KEY: 915000000, + SigMFFile.DATETIME_KEY: get_sigmf_iso8601_datetime_now(), + }) + + # add an annotation at sample 100 with length 200 & 10 KHz width + meta.add_annotation(100, 200, metadata = { + SigMFFile.FLO_KEY: 914995000.0, + SigMFFile.FHI_KEY: 915005000.0, + SigMFFile.COMMENT_KEY: 'example annotation', + }) + + # check for mistakes & write to disk + meta.tofile('example_cf32.sigmf-meta') # extension is optional \ No newline at end of file diff --git a/docs/source/usage.rst b/docs/source/usage.rst deleted file mode 100644 index 7991117..0000000 --- a/docs/source/usage.rst +++ /dev/null @@ -1,69 +0,0 @@ -Usage -===== - -.. _installation: - -Installation ------------- - -To install the latest PyPi release, install from pip: - -.. code-block:: console - - $ pip install sigmf - -To install the latest git release, build from source: - -.. code-block:: console - - $ git clone https://github.com/sigmf/sigmf-python.git - $ cd sigmf-python - $ pip install . - -Testing -------- - -Testing can be run locally: - -.. code-block:: console - - $ coverage run - -Run tests within a temporary environment: - -.. code-block:: console - - $ tox run - -Tools developers may want to use: - -.. code-block:: console - - $ pytest -rA tests/test_archive.py # test one file verbosely - $ pylint sigmf tests # lint entire project - $ black sigmf tests # autoformat entire project - $ isort sigmf tests # format imports for entire project - -Examples --------- - -Load a SigMF archive; read all samples & metadata -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: python - - import sigmf - handle = sigmf.sigmffile.fromfile("example.sigmf") - handle.read_samples() # returns all timeseries data - handle.get_global_info() # returns 'global' dictionary - handle.get_captures() # returns list of 'captures' dictionaries - handle.get_annotations() # returns list of all annotations - -Verify SigMF dataset integrity & compliance -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. code-block:: console - - $ sigmf_validate example.sigmf - -TODO: Insert more examples from `README.md`. From e1d2dd774545eacbd9c21868a4be69fec4b06d44 Mon Sep 17 00:00:00 2001 From: Teque5 Date: Wed, 15 Jan 2025 14:44:43 -0800 Subject: [PATCH 4/4] increment patch --- README.md | 16 ++++++++++++---- sigmf/__init__.py | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5eedc3f..2dca188 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,21 @@ ![Rendered SigMF Logo](https://raw.githubusercontent.com/sigmf/SigMF/refs/heads/main/logo/sigmf_logo.png) -[![Documentation Shield](https://img.shields.io/readthedocs/sigmf)](https://sigmf.readthedocs.io/en/latest/) +[![PyPI Version Shield](https://img.shields.io/pypi/v/sigmf)](https://pypi.org/project/SigMF/) [![Build Status Shield](https://img.shields.io/github/actions/workflow/status/sigmf/sigmf-python/main.yml)](https://github.com/sigmf/sigmf-python/actions?query=branch%3Amain) +[![License Shield](https://img.shields.io/pypi/l/sigmf)](https://en.wikipedia.org/wiki/GNU_Lesser_General_Public_License) +[![Documentation Shield](https://img.shields.io/readthedocs/sigmf)](https://sigmf.readthedocs.io/en/latest/) [![PyPI Downloads Shield](https://img.shields.io/pypi/dm/sigmf)](https://pypi.org/project/SigMF/) -The `sigmf` module makes it easy to interact with Signal Metadata Format -(SigMF) recordings. This module works with Python 3.7-3.13 and is distributed +The `sigmf` library makes it easy to interact with Signal Metadata Format +(SigMF) recordings. This library is compatible with Python 3.7-3.13 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). -[Please visit the sigmf-python documentation for install, examples, & more info.](https://sigmf.readthedocs.io/en/latest/) +To install the latest PyPI release, install from pip: + +```bash +pip install sigmf +``` + +**[Please visit the documentation for examples & more info.](https://sigmf.readthedocs.io/en/latest/)** diff --git a/sigmf/__init__.py b/sigmf/__init__.py index 98fc58a..060a9e5 100644 --- a/sigmf/__init__.py +++ b/sigmf/__init__.py @@ -5,7 +5,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later # version of this python module -__version__ = "1.2.6" +__version__ = "1.2.7" # matching version of the SigMF specification __specification__ = "1.2.3"