Skip to content
Merged
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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,15 @@ import numpy as np
import sigmf

data = np.array([0.1 + 0.2j, 0.3 + 0.4j], dtype=np.complex64)
meta = sigmf.fromarray(data, sample_rate=48000)
meta = sigmf.fromarray(data)
# optional additional metadata
meta.sample_rate = 8000
meta.description = "sample recording"
meta.add_capture(start_index=0, metadata={sigmf.FREQUENCY_KEY: 915e6})
# creates recording.sigmf-data and recording.sigmf-meta
meta.tofile("recording")
# or create compressed archive
meta.tofile("recording.sigmf.gz")
```

### Docs
Expand Down
7 changes: 6 additions & 1 deletion docs/source/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ Save a Numpy array as a SigMF Recording
data = np.zeros(1024, dtype=np.complex64)

# create SigMFFile from array — datatype is inferred from the numpy array
meta = sigmf.fromarray(data, sample_rate=48000, frequency=915e6)
meta = sigmf.fromarray(data)

# optional additional metadata
meta.sample_rate = 48000
meta.description = "an example recording"
meta.add_capture(start_index=0, metadata={sigmf.FREQUENCY_KEY: 915e6})

# write to separate .sigmf-meta and .sigmf-data files
meta.tofile("example")
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.0"
__version__ = "1.11.1"
# matching version of the SigMF specification
__specification__ = "1.2.6"

Expand Down
39 changes: 11 additions & 28 deletions sigmf/sigmffile.py
Original file line number Diff line number Diff line change
Expand Up @@ -1322,12 +1322,12 @@ def get_dataset_filename_from_metadata(meta_fn, metadata=None):
return None


def fromarray(data, sample_rate, frequency=None, global_info=None):
def fromarray(data):
Comment thread
777arc marked this conversation as resolved.
"""
Create a SigMFFile from a numpy array.

Convenience function that infers the SigMF datatype from the numpy dtype,
creates an in-memory SigMFFile with a single capture at index 0. The
Convenience function that infers the SigMF datatype from the numpy dtype
and creates an in-memory SigMFFile with a single capture at index 0. The
returned object can then be written to disk using ``tofile()`` or
``archive()``. For full control over captures, annotations, and global
fields, use ``SigMFFile`` directly.
Expand All @@ -1336,12 +1336,6 @@ def fromarray(data, sample_rate, frequency=None, global_info=None):
----------
data : np.ndarray
Signal samples.
sample_rate : float
Sample rate in Hz.
frequency : float, optional
Center frequency in Hz for the capture.
global_info : dict, optional
Additional global metadata fields to include.

Returns
-------
Expand All @@ -1351,37 +1345,26 @@ def fromarray(data, sample_rate, frequency=None, global_info=None):
Examples
--------
>>> import numpy as np
>>> import tempfile
>>> import sigmf, tempfile
>>> from pathlib import Path
>>> data = np.random.randn(1000) + 1j * np.random.randn(1000)
>>> meta = fromarray(data, sample_rate=1e6, frequency=915e6) # returns SigMFFile
>>> data = (np.random.randn(16) + 1j * np.random.randn(16))
>>> meta = sigmf.fromarray(data)
>>> meta.sample_rate = 1e6 # set global fields via attribute
>>> meta.add_capture(0, metadata={sigmf.FREQUENCY_KEY: 915e6}) # add capture metadata
>>> meta.add_annotation(0, length=len(data), metadata={sigmf.LABEL_KEY: 'example'}) # add annotation
>>> tmpdir = Path(tempfile.mkdtemp())
>>> meta.tofile(tmpdir / 'recording') # creates recording.sigmf-meta and recording.sigmf-data
>>> meta.tofile(tmpdir / 'recording.sigmf') # creates recording.sigmf archive
"""
import io

# create in-memory data buffer
data_buffer = io.BytesIO()
data_buffer.write(data.tobytes())
data_buffer.seek(0)

# build metadata
info = {
keys.DATATYPE_KEY: get_data_type_str(data),
keys.SAMPLE_RATE_KEY: sample_rate,
}
if global_info is not None:
info.update(global_info)

capture_meta = None
if frequency is not None:
capture_meta = {keys.FREQUENCY_KEY: frequency}

# create sigmffile object with in-memory buffer
meta = SigMFFile(global_info=info)
meta = SigMFFile(global_info={keys.DATATYPE_KEY: get_data_type_str(data)})
meta.set_data_file(data_buffer=data_buffer)
meta.add_capture(0, metadata=capture_meta)
meta.add_capture(0)

return meta

Expand Down
33 changes: 33 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

"""Provides pytest fixtures for other tests."""

import os
import tempfile
from pathlib import Path

import pytest

Expand All @@ -16,6 +18,37 @@
from .testdata import TEST_FLOAT32_DATA, TEST_METADATA


def get_nonsigmf_path() -> Path:
"""Get path to example_nonsigmf_recordings repo or skip test"""
nonsigmf_env = "EXAMPLE_NONSIGMF_RECORDINGS_PATH"
recordings_path = Path(os.getenv(nonsigmf_env, "nopath"))
if not recordings_path.is_dir():
pytest.skip(
Comment thread
777arc marked this conversation as resolved.
f"Set {nonsigmf_env} environment variable to path non-SigMF recordings repository to run test."
f" Available at https://github.com/sigmf/example_nonsigmf_recordings"
)
return recordings_path


def validate_ncd(meta: SigMFFile, target_path: Path):
"""Validate that a SigMF object is a properly structured non-conforming dataset (NCD)."""
assert str(meta.data_file) == str(target_path), "Auto-detected NCD should point to original file"
assert isinstance(meta, SigMFFile)
Comment thread
777arc marked this conversation as resolved.

global_info = meta.get_global_info()
capture_info = meta.get_captures()

# validate NCD SigMF spec compliance
assert len(capture_info) > 0, "Should have at least one capture"
assert "core:header_bytes" in capture_info[0]
if target_path.suffix != ".iq":
# skip for Signal Hound
assert capture_info[0]["core:header_bytes"] > 0, "Should have non-zero core:header_bytes field"
assert "core:trailing_bytes" in global_info, "Should have core:trailing_bytes field."
assert "core:dataset" in global_info, "Should have core:dataset field."
assert "core:metadata_only" not in global_info, "Should NOT have core:metadata_only field."


@pytest.fixture
def test_data_file():
"""when called, yields temporary dataset"""
Expand Down
9 changes: 8 additions & 1 deletion tests/test_archivereader.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@

import numpy as np

from sigmf import DATATYPE_KEY, NUM_CHANNELS_KEY, SigMFArchiveReader, SigMFFile, __specification__, fromfile
from sigmf import (
DATATYPE_KEY,
NUM_CHANNELS_KEY,
SigMFArchiveReader,
SigMFFile,
__specification__,
fromfile,
)


class TestArchiveReader(unittest.TestCase):
Expand Down
10 changes: 5 additions & 5 deletions tests/test_convert_blue.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from sigmf.convert.blue import TYPE_MAP, blue_to_sigmf
from sigmf.utils import SIGMF_DATETIME_ISO8601_FMT

from .testdata import get_nonsigmf_path, validate_ncd
from .conftest import get_nonsigmf_path, validate_ncd


class TestBlueConverter(unittest.TestCase):
Expand Down Expand Up @@ -152,7 +152,7 @@ def test_blue_to_sigmf_ncd(self) -> None:
for form, atol in self.format_tolerance:
self.write_minimal(form.encode())
meta = blue_to_sigmf(self.blue_path)
validate_ncd(self, meta, self.blue_path)
validate_ncd(meta, self.blue_path)
self.check_data_and_metadata(meta, form, atol)

def test_pair_overwrite_protection(self) -> None:
Expand Down Expand Up @@ -202,7 +202,7 @@ def setUp(self) -> None:
"""setup paths to blue files"""
self.tmp_dir = tempfile.TemporaryDirectory()
self.tmp_path = Path(self.tmp_dir.name)
nonsigmf_path = get_nonsigmf_path(self)
nonsigmf_path = get_nonsigmf_path()
# glob all files in blue/ directory
blue_dir = nonsigmf_path / "blue"
self.blue_paths = []
Expand Down Expand Up @@ -240,7 +240,7 @@ def test_create_ncd(self):
"""test direct NCD conversion"""
for blue_path in self.blue_paths:
meta = blue_to_sigmf(blue_path=blue_path)
validate_ncd(self, meta, blue_path)
validate_ncd(meta, blue_path)
if len(meta):
# check sample read consistency
np.testing.assert_allclose(meta.read_samples(count=10), meta[0:10], atol=1e-6)
Expand All @@ -249,4 +249,4 @@ def test_fromfile_ncd(self):
"""test automatic NCD conversion with fromfile()"""
for blue_path in self.blue_paths:
meta = sigmf.fromfile(blue_path)
validate_ncd(self, meta, blue_path)
validate_ncd(meta, blue_path)
10 changes: 5 additions & 5 deletions tests/test_convert_signalhound.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import sigmf
from sigmf.convert.signalhound import signalhound_to_sigmf

from .testdata import get_nonsigmf_path, validate_ncd
from .conftest import get_nonsigmf_path, validate_ncd


class TestSignalHoundConverter(unittest.TestCase):
Expand Down Expand Up @@ -116,7 +116,7 @@ def test_signalhound_to_sigmf_ncd(self):
"""Test Signal Hound to SigMF conversion as Non-Conforming Dataset."""
meta = signalhound_to_sigmf(signalhound_path=self.xml_path, create_ncd=True)
target_path = self.iq_path
validate_ncd(self, meta, target_path)
validate_ncd(meta, target_path)
self._verify(meta)


Expand All @@ -127,7 +127,7 @@ def setUp(self) -> None:
"""Find a non-SigMF dataset for testing."""
self.tmp_dir = tempfile.TemporaryDirectory()
self.tmp_path = Path(self.tmp_dir.name)
nonsigmf_path = get_nonsigmf_path(self)
nonsigmf_path = get_nonsigmf_path()
# glob all files in signal hound directory
hound_dir = nonsigmf_path / "signal_hound"
self.hound_paths = []
Expand Down Expand Up @@ -164,7 +164,7 @@ def test_create_ncd(self):
for hound_path in self.hound_paths:
meta = signalhound_to_sigmf(signalhound_path=hound_path)
target_path = hound_path.with_suffix(".iq")
validate_ncd(self, meta, target_path)
validate_ncd(meta, target_path)
if len(meta):
# check sample read consistency
np.testing.assert_array_equal(meta.read_samples(count=10), meta[0:10])
Expand All @@ -174,4 +174,4 @@ def test_fromfile_ncd(self):
for hound_path in self.hound_paths:
meta = sigmf.fromfile(hound_path)
target_path = hound_path.with_suffix(".iq")
validate_ncd(self, meta, target_path)
validate_ncd(meta, target_path)
15 changes: 7 additions & 8 deletions tests/test_convert_wav.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import sigmf
from sigmf.convert.wav import wav_to_sigmf

from .testdata import get_nonsigmf_path, validate_ncd
from .conftest import get_nonsigmf_path, validate_ncd


class TestWAVConverter(unittest.TestCase):
Expand Down Expand Up @@ -93,7 +93,7 @@ def test_wav_to_sigmf_archive(self) -> None:
def test_wav_to_sigmf_ncd(self) -> None:
"""test wav to sigmf conversion as Non-Conforming Dataset"""
meta = wav_to_sigmf(wav_path=self.wav_path, create_ncd=True)
validate_ncd(self, meta, self.wav_path)
validate_ncd(meta, self.wav_path)
self._verify(meta)

# test overwrite protection when creating NCD with output path
Expand All @@ -115,7 +115,7 @@ def setUp(self) -> None:
"""setup paths to example wav files"""
self.tmp_dir = tempfile.TemporaryDirectory()
self.tmp_path = Path(self.tmp_dir.name)
nonsigmf_path = get_nonsigmf_path(self)
nonsigmf_path = get_nonsigmf_path()
# glob all files in wav/ directory
wav_dir = nonsigmf_path / "wav"
self.wav_paths = []
Expand Down Expand Up @@ -152,13 +152,12 @@ def test_create_ncd(self) -> None:
"""test direct NCD conversion"""
for wav_path in self.wav_paths:
meta = wav_to_sigmf(wav_path=wav_path)
validate_ncd(self, meta, wav_path)

# test file read
_ = meta.read_samples(count=10)
validate_ncd(meta, wav_path)
# check sample read consistency
np.testing.assert_array_equal(meta.read_samples(count=10), meta[0:10])

def test_autodetect_ncd(self) -> None:
"""test automatic NCD conversion"""
for wav_path in self.wav_paths:
meta = sigmf.fromfile(wav_path)
validate_ncd(self, meta, wav_path)
validate_ncd(meta, wav_path)
2 changes: 1 addition & 1 deletion tests/test_hashing.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import numpy as np

import sigmf
from sigmf import SigMFFile, TRAILING_BYTES_KEY, hashing
from sigmf import TRAILING_BYTES_KEY, SigMFFile, hashing

from .testdata import TEST_FLOAT32_DATA, TEST_METADATA

Expand Down
Loading
Loading