SigMF


NameSigMF JSON
Version 1.2.2 PyPI version JSON
download
home_pageNone
SummaryEasily interact with Signal Metadata Format (SigMF) recordings.
upload_time2024-06-06 20:54:11
maintainerNone
docs_urlNone
authorNone
requires_python>=3.7
licenseNone
keywords gnuradio radio
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            <p align="center"><img src="https://github.com/sigmf/SigMF/blob/v1.2.0/logo/sigmf_logo.png" alt="Rendered SigMF Logo"/></p>

This python module makes it easy to interact with Signal Metadata Format
(SigMF) recordings. This module works with Python 3.7+ 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

# 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: dt.datetime.utcnow().isoformat()+'Z',
})

# 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
'<c8'
```

Another supported mode is the case where you might have an archive that *is not
on disk* but instead is simply `bytes` in a python variable.
Instead of needing to write this out to a temporary file before being able to
read it, this can be done "in mid air" or "without touching the ground (disk)".

```python
>>> 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.

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "SigMF",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.7",
    "maintainer_email": null,
    "keywords": "gnuradio, radio",
    "author": null,
    "author_email": null,
    "download_url": "https://files.pythonhosted.org/packages/9e/ba/a938e6df9e90a3278fcce05d784d32fbfc93d900f6188818cabd8e5457cd/sigmf-1.2.2.tar.gz",
    "platform": null,
    "description": "<p align=\"center\"><img src=\"https://github.com/sigmf/SigMF/blob/v1.2.0/logo/sigmf_logo.png\" alt=\"Rendered SigMF Logo\"/></p>\n\nThis python module makes it easy to interact with Signal Metadata Format\n(SigMF) recordings. This module works with Python 3.7+ and is distributed\nfreely under the terms GNU Lesser GPL v3 License.\n\nThis 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).\n\n# Installation\n\nTo install the latest PyPi release, install from pip:\n\n```bash\npip install sigmf\n```\n\nTo install the latest git release, build from source:\n\n```bash\ngit clone https://github.com/sigmf/sigmf-python.git\ncd sigmf-python\npip install .\n```\n\nTesting can be run with a variety of tools:\n\n```bash\n# pytest and coverage run locally\npytest\ncoverage run\n# run coverage in a venv\ntox run\n# other useful tools\npylint sigmf tests\npytype\nblack\nflake8\n```\n\n# Examples\n\n### Load a SigMF archive; read all samples & metadata\n\n```python\nimport sigmf\nhandle = sigmf.sigmffile.fromfile('example.sigmf')\nhandle.read_samples() # returns all timeseries data\nhandle.get_global_info() # returns 'global' dictionary\nhandle.get_captures() # returns list of 'captures' dictionaries\nhandle.get_annotations() # returns list of all annotations\n```\n\n### Verify SigMF dataset integrity & compliance\n\n```bash\nsigmf_validate example.sigmf\n```\n\n### Load a SigMF dataset; read its annotation, metadata, and samples\n\n```python\nfrom sigmf import SigMFFile, sigmffile\n\n# Load a dataset\nfilename = 'logo/sigmf_logo' # extension is optional\nsignal = sigmffile.fromfile(filename)\n\n# Get some metadata and all annotations\nsample_rate = signal.get_global_field(SigMFFile.SAMPLE_RATE_KEY)\nsample_count = signal.sample_count\nsignal_duration = sample_count / sample_rate\nannotations = signal.get_annotations()\n\n# Iterate over annotations\nfor adx, annotation in enumerate(annotations):\n    annotation_start_idx = annotation[SigMFFile.START_INDEX_KEY]\n    annotation_length = annotation[SigMFFile.LENGTH_INDEX_KEY]\n    annotation_comment = annotation.get(SigMFFile.COMMENT_KEY, \"[annotation {}]\".format(adx))\n\n    # Get capture info associated with the start of annotation\n    capture = signal.get_capture_info(annotation_start_idx)\n    freq_center = capture.get(SigMFFile.FREQUENCY_KEY, 0)\n    freq_min = freq_center - 0.5*sample_rate\n    freq_max = freq_center + 0.5*sample_rate\n\n    # Get frequency edges of annotation (default to edges of capture)\n    freq_start = annotation.get(SigMFFile.FLO_KEY)\n    freq_stop = annotation.get(SigMFFile.FHI_KEY)\n\n    # Get the samples corresponding to annotation\n    samples = signal.read_samples(annotation_start_idx, annotation_length)\n```\n\n### Create and save a Collection of SigMF Recordings from numpy arrays\n\nFirst, create a single SigMF Recording and save it to disk\n\n```python\nimport datetime as dt\nimport numpy as np\nimport sigmf\nfrom sigmf import SigMFFile\nfrom sigmf.utils import get_data_type_str\n\n# suppose we have an complex timeseries signal\ndata = np.zeros(1024, dtype=np.complex64)\n\n# write those samples to file in cf32_le\ndata.tofile('example_cf32.sigmf-data')\n\n# create the metadata\nmeta = SigMFFile(\n    data_file='example_cf32.sigmf-data', # extension is optional\n    global_info = {\n        SigMFFile.DATATYPE_KEY: get_data_type_str(data),  # in this case, 'cf32_le'\n        SigMFFile.SAMPLE_RATE_KEY: 48000,\n        SigMFFile.AUTHOR_KEY: 'jane.doe@domain.org',\n        SigMFFile.DESCRIPTION_KEY: 'All zero complex float32 example file.',\n    }\n)\n\n# create a capture key at time index 0\nmeta.add_capture(0, metadata={\n    SigMFFile.FREQUENCY_KEY: 915000000,\n    SigMFFile.DATETIME_KEY: dt.datetime.utcnow().isoformat()+'Z',\n})\n\n# add an annotation at sample 100 with length 200 & 10 KHz width\nmeta.add_annotation(100, 200, metadata = {\n    SigMFFile.FLO_KEY: 914995000.0,\n    SigMFFile.FHI_KEY: 915005000.0,\n    SigMFFile.COMMENT_KEY: 'example annotation',\n})\n\n# check for mistakes & write to disk\nmeta.tofile('example_cf32.sigmf-meta') # extension is optional\n```\n\nNow lets add another SigMF Recording and associate them with a SigMF Collection:\n\n```python\nfrom sigmf import SigMFCollection\n\ndata_ci16 = np.zeros(1024, dtype=np.complex64)\n\n#rescale and save as a complex int16 file:\ndata_ci16 *= pow(2, 15)\ndata_ci16.view(np.float32).astype(np.int16).tofile('example_ci16.sigmf-data')\n\n# create the metadata for the second file\nmeta_ci16 = SigMFFile(\n    data_file='example_ci16.sigmf-data', # extension is optional\n    global_info = {\n        SigMFFile.DATATYPE_KEY: 'ci16_le', # get_data_type_str() is only valid for numpy types\n        SigMFFile.SAMPLE_RATE_KEY: 48000,\n        SigMFFile.DESCRIPTION_KEY: 'All zero complex int16 file.',\n    }\n)\nmeta_ci16.add_capture(0, metadata=meta.get_capture_info(0))\nmeta_ci16.tofile('example_ci16.sigmf-meta')\n\ncollection = SigMFCollection(['example_cf32.sigmf-meta', 'example_ci16.sigmf-meta'],\n        metadata = {'collection': {\n            SigMFCollection.AUTHOR_KEY: 'sigmf@sigmf.org',\n            SigMFCollection.DESCRIPTION_KEY: 'Collection of two all zero files.',\n        }\n    }\n)\nstreams = collection.get_stream_names()\nsigmf = [collection.get_SigMFFile(stream) for stream in streams]\ncollection.tofile('example_zeros.sigmf-collection')\n```\n\nThe SigMF Collection and its associated Recordings can now be loaded like this:\n\n```python\nfrom sigmf import sigmffile\ncollection = sigmffile.fromfile('example_zeros')\nci16_sigmffile = collection.get_SigMFFile(stream_name='example_ci16')\ncf32_sigmffile = collection.get_SigMFFile(stream_name='example_cf32')\n```\n\n### Load a SigMF Archive and slice its data without untaring it\n\nSince an *archive* is merely a tarball (uncompressed), and since there any many\nexcellent tools for manipulating tar files, it's fairly straightforward to\naccess the *data* part of a SigMF archive without un-taring it. This is a\ncompelling feature because __1__ archives make it harder for the `-data` and\nthe `-meta` to get separated, and __2__ some datasets are so large that it can\nbe impractical (due to available disk space, or slow network speeds if the\narchive file resides on a network file share) or simply obnoxious to untar it\nfirst.\n\n```python\n>>> import sigmf\n>>> arc = sigmf.SigMFArchiveReader('/src/LTE.sigmf')\n>>> arc.shape\n(15379532,)\n>>> arc.ndim\n1\n>>> arc[:10]\narray([-20.+11.j, -21. -6.j, -17.-20.j, -13.-52.j,   0.-75.j,  22.-58.j,\n        48.-44.j,  49.-60.j,  31.-56.j,  23.-47.j], dtype=complex64)\n```\n\nThe preceeding example exhibits another feature of this approach; the archive\n`LTE.sigmf` is actually `complex-int16`'s on disk, for which there is no\ncorresponding type in `numpy`. However, the `.sigmffile` member keeps track of\nthis, and converts the data to `numpy.complex64` *after* slicing it, that is,\nafter reading it from disk.\n\n```python\n>>> arc.sigmffile.get_global_field(sigmf.SigMFFile.DATATYPE_KEY)\n'ci16_le'\n\n>>> arc.sigmffile._memmap.dtype\ndtype('int16')\n\n>>> arc.sigmffile._return_type\n'<c8'\n```\n\nAnother supported mode is the case where you might have an archive that *is not\non disk* but instead is simply `bytes` in a python variable.\nInstead of needing to write this out to a temporary file before being able to\nread it, this can be done \"in mid air\" or \"without touching the ground (disk)\".\n\n```python\n>>> import sigmf, io\n>>> sigmf_bytes = io.BytesIO(open('/src/LTE.sigmf', 'rb').read())\n>>> arc = sigmf.SigMFArchiveReader(archive_buffer=sigmf_bytes)\n>>> arc[:10]\narray([-20.+11.j, -21. -6.j, -17.-20.j, -13.-52.j,   0.-75.j,  22.-58.j,\n        48.-44.j,  49.-60.j,  31.-56.j,  23.-47.j], dtype=complex64)\n```\n\n# Frequently Asked Questions\n\n### Is this a GNU Radio effort?\n\n*No*, this is not a GNU Radio-specific effort.\nThis effort first emerged from a group of GNU Radio core\ndevelopers, but the goal of the project to provide a standard that will be\nuseful to anyone and everyone, regardless of tool or workflow.\n\n### Is this specific to wireless communications?\n\n*No*, similar to the response, above, the goal is to create something that is\ngenerally applicable to _signal processing_, regardless of whether or not the\napplication is communications related.\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "Easily interact with Signal Metadata Format (SigMF) recordings.",
    "version": "1.2.2",
    "project_urls": {
        "repository": "https://github.com/sigmf/sigmf-python"
    },
    "split_keywords": [
        "gnuradio",
        " radio"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "b296814a31e105c2b4126b5908e3269ef4981b52b1c625da87e953f29f2bad11",
                "md5": "b6842ed559eea259e1ae66ccacdedf22",
                "sha256": "6cd5470d1294b34f4ad4d0f0502330f5712aa8629f912b913500c67ed1e0d7d4"
            },
            "downloads": -1,
            "filename": "SigMF-1.2.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "b6842ed559eea259e1ae66ccacdedf22",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.7",
            "size": 39558,
            "upload_time": "2024-06-06T20:54:09",
            "upload_time_iso_8601": "2024-06-06T20:54:09.330959Z",
            "url": "https://files.pythonhosted.org/packages/b2/96/814a31e105c2b4126b5908e3269ef4981b52b1c625da87e953f29f2bad11/SigMF-1.2.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "9ebaa938e6df9e90a3278fcce05d784d32fbfc93d900f6188818cabd8e5457cd",
                "md5": "4d1d0f20ef0a3dfa811bd74da80758b6",
                "sha256": "9d2eeea6e3c69f90ad83c05e8faa7f5caabd24ce79e809e75ff5cc62d6765d5f"
            },
            "downloads": -1,
            "filename": "sigmf-1.2.2.tar.gz",
            "has_sig": false,
            "md5_digest": "4d1d0f20ef0a3dfa811bd74da80758b6",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.7",
            "size": 42100,
            "upload_time": "2024-06-06T20:54:11",
            "upload_time_iso_8601": "2024-06-06T20:54:11.010748Z",
            "url": "https://files.pythonhosted.org/packages/9e/ba/a938e6df9e90a3278fcce05d784d32fbfc93d900f6188818cabd8e5457cd/sigmf-1.2.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-06-06 20:54:11",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "sigmf",
    "github_project": "sigmf-python",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "sigmf"
}
        
Elapsed time: 9.17990s