Skip to content

Commit bd190c0

Browse files
Merge branch 'main' into feature/rohdeschwarz
2 parents 6d2977f + a3db459 commit bd190c0

33 files changed

Lines changed: 866 additions & 672 deletions

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ jobs:
1414
matrix:
1515
python-version: ["3.7", "3.9", "3.11", "3.13", "3.14"]
1616
steps:
17-
- uses: actions/checkout@v3
17+
- uses: actions/checkout@v4
1818
- name: Set up Python ${{ matrix.python-version }}
19-
uses: actions/setup-python@v4
19+
uses: actions/setup-python@v5
2020
with:
2121
python-version: ${{ matrix.python-version }}
2222
- name: Install dependencies

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,15 @@ import numpy as np
5050
import sigmf
5151

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

5864
### Docs

docs/source/advanced.rst

Lines changed: 38 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -22,28 +22,28 @@ the recording of the SigMF logo used in this example `from the specification
2222
signal = sigmffile.fromfile(path)
2323
2424
# Get some metadata and all annotations
25-
sample_rate = signal.get_global_field(SigMFFile.SAMPLE_RATE_KEY)
25+
sample_rate = signal.get_global_field(sigmf.SAMPLE_RATE_KEY)
2626
sample_count = signal.sample_count
2727
signal_duration = sample_count / sample_rate
2828
annotations = signal.get_annotations()
2929
3030
# Iterate over annotations
3131
for adx, annotation in enumerate(annotations):
32-
annotation_start_idx = annotation[SigMFFile.START_INDEX_KEY]
33-
annotation_length = annotation[SigMFFile.LENGTH_INDEX_KEY]
32+
annotation_start_idx = annotation[sigmf.SAMPLE_START_KEY]
33+
annotation_length = annotation[sigmf.SAMPLE_COUNT_KEY]
3434
annotation_comment = annotation.get(
35-
SigMFFile.COMMENT_KEY, "[annotation {}]".format(adx)
35+
sigmf.COMMENT_KEY, "[annotation {}]".format(adx)
3636
)
3737
3838
# Get capture info associated with the start of annotation
3939
capture = signal.get_capture_info(annotation_start_idx)
40-
freq_center = capture.get(SigMFFile.FREQUENCY_KEY, 0)
40+
freq_center = capture.get(sigmf.FREQUENCY_KEY, 0)
4141
freq_min = freq_center - 0.5 * sample_rate
4242
freq_max = freq_center + 0.5 * sample_rate
4343
4444
# Get frequency edges of annotation (default to edges of capture)
45-
freq_start = annotation.get(SigMFFile.FLO_KEY)
46-
freq_stop = annotation.get(SigMFFile.FHI_KEY)
45+
freq_start = annotation.get(sigmf.FREQ_LOWER_EDGE_KEY)
46+
freq_stop = annotation.get(sigmf.FREQ_UPPER_EDGE_KEY)
4747
4848
# Get the samples corresponding to annotation
4949
samples = signal.read_samples(annotation_start_idx, annotation_length)
@@ -74,19 +74,19 @@ First, create a single SigMF Recording and save it to disk:
7474
meta = SigMFFile(
7575
data_file="example_cf32.sigmf-data", # extension is optional
7676
global_info={
77-
SigMFFile.DATATYPE_KEY: get_data_type_str(data), # in this case, 'cf32_le'
78-
SigMFFile.SAMPLE_RATE_KEY: 48000,
79-
SigMFFile.AUTHOR_KEY: "jane.doe@domain.org",
80-
SigMFFile.DESCRIPTION_KEY: "All zero complex float32 example file.",
77+
sigmf.DATATYPE_KEY: get_data_type_str(data), # in this case, 'cf32_le'
78+
sigmf.SAMPLE_RATE_KEY: 48000,
79+
sigmf.AUTHOR_KEY: "jane.doe@domain.org",
80+
sigmf.DESCRIPTION_KEY: "All zero complex float32 example file.",
8181
},
8282
)
8383
8484
# create a capture key at time index 0
8585
meta.add_capture(
8686
0,
8787
metadata={
88-
SigMFFile.FREQUENCY_KEY: 915000000,
89-
SigMFFile.DATETIME_KEY: get_sigmf_iso8601_datetime_now(),
88+
sigmf.FREQUENCY_KEY: 915000000,
89+
sigmf.DATETIME_KEY: get_sigmf_iso8601_datetime_now(),
9090
},
9191
)
9292
@@ -95,9 +95,9 @@ First, create a single SigMF Recording and save it to disk:
9595
100,
9696
200,
9797
metadata={
98-
SigMFFile.FLO_KEY: 914995000.0,
99-
SigMFFile.FHI_KEY: 915005000.0,
100-
SigMFFile.COMMENT_KEY: "example annotation",
98+
sigmf.FREQ_LOWER_EDGE_KEY: 914995000.0,
99+
sigmf.FREQ_UPPER_EDGE_KEY: 915005000.0,
100+
sigmf.COMMENT_KEY: "example annotation",
101101
},
102102
)
103103
@@ -120,9 +120,9 @@ Now lets add another SigMF Recording and associate them with a SigMF Collection:
120120
meta_ci16 = SigMFFile(
121121
data_file="example_ci16.sigmf-data", # extension is optional
122122
global_info={
123-
SigMFFile.DATATYPE_KEY: "ci16_le", # get_data_type_str() is only valid for numpy types
124-
SigMFFile.SAMPLE_RATE_KEY: 48000,
125-
SigMFFile.DESCRIPTION_KEY: "All zero complex int16 file.",
123+
sigmf.DATATYPE_KEY: "ci16_le", # get_data_type_str() is only valid for numpy types
124+
sigmf.SAMPLE_RATE_KEY: 48000,
125+
sigmf.DESCRIPTION_KEY: "All zero complex int16 file.",
126126
},
127127
)
128128
meta_ci16.add_capture(0, metadata=meta.get_capture_info(0))
@@ -155,59 +155,36 @@ The SigMF Collection and its associated Recordings can now be loaded like this:
155155
Load a SigMF Archive and slice without untaring
156156
-----------------------------------------------
157157

158-
Since an *archive* is a tarball (uncompressed by default), and since there are many
159-
excellent tools for manipulating tar files, it's fairly straightforward to
160-
access the *data* part of a SigMF archive without un-taring it. This is a
161-
compelling feature because **1** archives make it harder for the ``-data`` and
162-
the ``-meta`` to get separated, and **2** some datasets are so large that it
163-
can be impractical (due to available disk space, or slow network speeds if the
164-
archive file resides on a network file share) or simply obnoxious to untar it
165-
first.
158+
Since an *archive* is a tarball (uncompressed by default), you can access the
159+
*data* part of a SigMF archive without un-taring it. This is a compelling
160+
feature because **1** archives make it harder for the ``-data`` and the
161+
``-meta`` to get separated, and **2** some datasets are so large that it can be
162+
impractical (due to available disk space, or slow network speeds if the archive
163+
file resides on a network file share) or simply obnoxious to untar it first.
166164

167165
::
168166

169167
>>> import sigmf
170-
>>> arc = sigmf.SigMFArchiveReader('/src/LTE.sigmf')
171-
>>> arc.shape
168+
>>> signal = sigmf.fromarchive('/src/LTE.sigmf')
169+
>>> signal.shape
172170
(15379532,)
173-
>>> arc.ndim
171+
>>> signal.ndim
174172
1
175-
>>> arc[:10]
176-
array([-20.+11.j, -21. -6.j, -17.-20.j, -13.-52.j, 0.-75.j, 22.-58.j,
177-
48.-44.j, 49.-60.j, 31.-56.j, 23.-47.j], dtype=complex64)
173+
>>> signal[:10]
174+
array([-0.023+0.012j, -0.021-0.006j, -0.017-0.020j, -0.013-0.052j,
175+
0.000-0.075j, 0.022-0.058j, 0.048-0.044j, 0.049-0.060j,
176+
0.031-0.056j, 0.023-0.047j], dtype=complex64)
178177

179-
The preceeding example exhibits another feature of this approach; the archive
180-
``LTE.sigmf`` is actually ``complex-int16``'s on disk, for which there is no
181-
corresponding type in ``numpy``. However, the ``.sigmffile`` member keeps track of
182-
this, and converts the data to ``numpy.complex64`` *after* slicing it, that is,
183-
after reading it from disk.
178+
Archives can contain fixed-point data types like ``complex-int16`` (``ci16``),
179+
which have no direct ``numpy`` equivalent. By default, this data is automatically
180+
scaled to floating-point values in the range ``[-1.0, 1.0]`` and returned as
181+
``numpy.complex64``:
184182

185183
::
186184

187-
>>> arc.sigmffile.get_global_field(sigmf.SigMFFile.DATATYPE_KEY)
185+
>>> signal.get_global_field(sigmf.DATATYPE_KEY)
188186
'ci16_le'
189187

190-
>>> arc.sigmffile._memmap.dtype
191-
dtype('int16')
192-
193-
>>> arc.sigmffile._return_type
194-
'<c8'
195-
196-
Another supported mode is the case where you might have an archive that *is not
197-
on disk* but instead is simply ``bytes`` in a python variable.
198-
199-
Instead of needing to write this out to a temporary file before being able to
200-
read it, this can be done "in mid air" or "without touching the ground (disk)".
201-
202-
::
203-
204-
>>> import sigmf, io
205-
>>> sigmf_bytes = io.BytesIO(open('/src/LTE.sigmf', 'rb').read())
206-
>>> arc = sigmf.SigMFArchiveReader(archive_buffer=sigmf_bytes)
207-
>>> arc[:10]
208-
array([-20.+11.j, -21. -6.j, -17.-20.j, -13.-52.j, 0.-75.j, 22.-58.j,
209-
48.-44.j, 49.-60.j, 31.-56.j, 23.-47.j], dtype=complex64)
210-
211188
------------------------------
212189
Compressed SigMF Archives
213190
------------------------------
@@ -248,7 +225,7 @@ The file extension determines the archive format:
248225

249226
>>> signal = sigmf.fromfile('recording.sigmf.xz')
250227
>>> signal[:10]
251-
array([-20.+11.j, ...], dtype=complex64)
228+
array([-0.023+0.012j, -0.021-0.006j, ...], dtype=complex64)
252229

253230
**Memory behavior:**
254231

docs/source/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,5 @@
6767
"specversion",
6868
]
6969
frozen_locals = dict(locals())
70-
rst_epilog = '\n'.join(map(lambda x: f".. |{x}| replace:: {frozen_locals[x]}", variables_to_export))
71-
del frozen_locals
70+
rst_epilog = "\n".join(map(lambda x: f".. |{x}| replace:: {frozen_locals[x]}", variables_to_export))
71+
del frozen_locals

docs/source/developers.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,13 @@ To lint the entire project and get suggested changes:
4242

4343
.. code-block:: console
4444
45-
$ pylint sigmf tests
45+
$ ruff check
4646
4747
To autoformat the entire project according to our coding standard:
4848

4949
.. code-block:: console
5050
51-
$ black sigmf tests # autoformat entire project
52-
$ isort sigmf tests # format imports for entire project
51+
$ ruff format
5352
5453
----
5554
Docs

docs/source/quickstart.rst

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,12 @@ Save a Numpy array as a SigMF Recording
6262
data = np.zeros(1024, dtype=np.complex64)
6363
6464
# create SigMFFile from array — datatype is inferred from the numpy array
65-
meta = sigmf.fromarray(data, sample_rate=48000, frequency=915e6)
65+
meta = sigmf.fromarray(data)
66+
67+
# optional additional metadata
68+
meta.sample_rate = 48000
69+
meta.description = "an example recording"
70+
meta.add_capture(start_index=0, metadata={sigmf.FREQUENCY_KEY: 915e6})
6671
6772
# write to separate .sigmf-meta and .sigmf-data files
6873
meta.tofile("example")
@@ -98,19 +103,19 @@ For full control over global fields, captures, and annotations:
98103
meta = SigMFFile(
99104
data_file="example.sigmf-data", # extension is optional
100105
global_info={
101-
SigMFFile.DATATYPE_KEY: get_data_type_str(data), # in this case, "cf32_le"
102-
SigMFFile.SAMPLE_RATE_KEY: 48000,
103-
SigMFFile.AUTHOR_KEY: "jane.doe@domain.org",
104-
SigMFFile.DESCRIPTION_KEY: "All zero complex float32 example file.",
106+
sigmf.DATATYPE_KEY: get_data_type_str(data), # in this case, "cf32_le"
107+
sigmf.SAMPLE_RATE_KEY: 48000,
108+
sigmf.AUTHOR_KEY: "jane.doe@domain.org",
109+
sigmf.DESCRIPTION_KEY: "All zero complex float32 example file.",
105110
},
106111
)
107112
108113
# create a capture key at time index 0
109114
meta.add_capture(
110115
0,
111116
metadata={
112-
SigMFFile.FREQUENCY_KEY: 915000000,
113-
SigMFFile.DATETIME_KEY: get_sigmf_iso8601_datetime_now(),
117+
sigmf.FREQUENCY_KEY: 915000000,
118+
sigmf.DATETIME_KEY: get_sigmf_iso8601_datetime_now(),
114119
},
115120
)
116121
@@ -119,9 +124,9 @@ For full control over global fields, captures, and annotations:
119124
100,
120125
200,
121126
metadata={
122-
SigMFFile.FLO_KEY: 914995000.0,
123-
SigMFFile.FHI_KEY: 915005000.0,
124-
SigMFFile.COMMENT_KEY: "example annotation",
127+
sigmf.FREQ_LOWER_EDGE_KEY: 914995000.0,
128+
sigmf.FREQ_UPPER_EDGE_KEY: 915005000.0,
129+
sigmf.COMMENT_KEY: "example annotation",
125130
},
126131
)
127132

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ dependencies = [
3939
sigmf_convert = "sigmf.convert.__main__:main"
4040
[project.optional-dependencies]
4141
test = [
42-
"pylint",
42+
"ruff",
4343
"pytest",
4444
"pytest-cov",
4545
"hypothesis", # next-gen testing framework
@@ -99,6 +99,9 @@ line-length = 120
9999
[tool.isort]
100100
profile = "black"
101101

102+
[tool.ruff]
103+
line-length = 120
104+
102105
[tool.tox]
103106
legacy_tox_ini = '''
104107
[tox]

sigmf/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
# SPDX-License-Identifier: LGPL-3.0-or-later
66

77
# version of this python module
8-
__version__ = "1.10.0"
8+
__version__ = "1.11.2"
99
# matching version of the SigMF specification
1010
__specification__ = "1.2.6"
1111

1212
from . import (
1313
archive,
1414
archivereader,
1515
error,
16+
keys,
1617
schema,
1718
siggen,
1819
sigmffile,
@@ -21,5 +22,6 @@
2122
)
2223
from .archive import SigMFArchive
2324
from .archivereader import SigMFArchiveReader
25+
from .keys import * # noqa: F401, F403
2426
from .siggen import SigMFGenerator
2527
from .sigmffile import SigMFCollection, SigMFFile, fromarchive, fromarray, fromfile

sigmf/archive.py

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,13 @@
1414
from pathlib import Path
1515

1616
from .error import SigMFFileError, SigMFFileExistsError
17-
18-
SIGMF_ARCHIVE_EXT = ".sigmf"
19-
SIGMF_METADATA_EXT = ".sigmf-meta"
20-
SIGMF_DATASET_EXT = ".sigmf-data"
21-
SIGMF_COLLECTION_EXT = ".sigmf-collection"
22-
23-
SIGMF_COMPRESSED_EXTS = {
24-
# compression type -> unique compound extension
25-
"gz": ".sigmf.gz",
26-
"xz": ".sigmf.xz",
27-
"zip": ".sigmf.zip",
28-
}
29-
30-
# all recognized archive extensions (uncompressed + compressed)
31-
SIGMF_ARCHIVE_EXTS = {SIGMF_ARCHIVE_EXT} | set(SIGMF_COMPRESSED_EXTS.values())
17+
from .keys import (
18+
SIGMF_ARCHIVE_EXT,
19+
SIGMF_ARCHIVE_EXTS,
20+
SIGMF_COMPRESSED_EXTS,
21+
SIGMF_DATASET_EXT,
22+
SIGMF_METADATA_EXT,
23+
)
3224

3325

3426
def _detect_compression(path):
@@ -152,7 +144,6 @@ def __init__(self, sigmffile, name=None, fileobj=None, compression=None, overwri
152144
with open(meta_path, "w") as handle:
153145
self.sigmffile.dump(handle)
154146
if isinstance(self.sigmffile.data_buffer, io.BytesIO):
155-
self.sigmffile.data_file = data_path
156147
with open(data_path, "wb") as handle:
157148
handle.write(self.sigmffile.data_buffer.getbuffer())
158149
else:

0 commit comments

Comments
 (0)