Skip to content

Commit eb19378

Browse files
authored
Compression Support & New sigmf.fromarray method (#146)
Adds read/write support for compressed SigMF archives (`.sigmf.gz`, `.sigmf.xz`, `.sigmf.zip`) using only stdlib so no new dependencies are needed. Compression uses `tarfile` for gzip/xz and `zipfile` for zip. Also fixed performance issue where reading uncompressed `.sigmf` archives was extracting data to RAM — these are now numpy memmapped directly into the tar. Also added a `sigmf.fromarray()` convenience function to mirror `sigmf.fromfile()`. It infers the SigMF datatype from the numpy dtype. This can then be used with existing tofile() method: ```python data = np.array([<SOME_STUFF>], dtype=<SOME_DTYPE>) meta = sigmf.fromarray(data, sample_rate=48000) meta.tofile("x") # create `x.sigmf-data` and `x.sigmf-meta` pair meta.tofile("x.sigmf.gz") # create `x.sigmf.gz` compressed archive ```
1 parent 9a6c80b commit eb19378

17 files changed

Lines changed: 848 additions & 160 deletions

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,29 @@ meta = sigmf.fromfile("recording.sigmf-meta")
2828
samples = meta[0:1024] # get first 1024 samples
2929
sample_rate = meta.sample_rate # get sample rate
3030

31+
# read compressed SigMF archives
32+
meta = sigmf.fromfile("recording.sigmf.gz") # gzip-compressed
33+
meta = sigmf.fromfile("recording.sigmf.xz") # xz-compressed
34+
meta = sigmf.fromfile("recording.sigmf.zip") # zip archive
3135

3236
# read other formats containing RF time series as SigMF
3337
meta = sigmf.fromfile("recording.wav") # WAV
3438
meta = sigmf.fromfile("recording.cdif") # BLUE / Platinum
3539
meta = sigmf.fromfile("recording.xml") # Signal Hound Spike
3640
```
3741

42+
### Write SigMF
43+
44+
```python
45+
import numpy as np
46+
import sigmf
47+
48+
data = np.array([0.1 + 0.2j, 0.3 + 0.4j], dtype=np.complex64)
49+
meta = sigmf.fromarray(data, sample_rate=48000)
50+
# creates recording.sigmf-data and recording.sigmf-meta
51+
meta.tofile("recording")
52+
```
53+
3854
### Docs
3955

4056
**[Please visit our documentation for full API reference and more info.](https://sigmf.readthedocs.io/en/latest/)**

docs/source/advanced.rst

Lines changed: 98 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ the recording of the SigMF logo used in this example `from the specification
1818
from sigmf import SigMFFile, sigmffile
1919
2020
# Load a dataset
21-
path = 'logo/sigmf_logo' # extension is optional
21+
path = "logo/sigmf_logo" # extension is optional
2222
signal = sigmffile.fromfile(path)
2323
2424
# Get some metadata and all annotations
@@ -31,13 +31,15 @@ the recording of the SigMF logo used in this example `from the specification
3131
for adx, annotation in enumerate(annotations):
3232
annotation_start_idx = annotation[SigMFFile.START_INDEX_KEY]
3333
annotation_length = annotation[SigMFFile.LENGTH_INDEX_KEY]
34-
annotation_comment = annotation.get(SigMFFile.COMMENT_KEY, "[annotation {}]".format(adx))
34+
annotation_comment = annotation.get(
35+
SigMFFile.COMMENT_KEY, "[annotation {}]".format(adx)
36+
)
3537
3638
# Get capture info associated with the start of annotation
3739
capture = signal.get_capture_info(annotation_start_idx)
3840
freq_center = capture.get(SigMFFile.FREQUENCY_KEY, 0)
39-
freq_min = freq_center - 0.5*sample_rate
40-
freq_max = freq_center + 0.5*sample_rate
41+
freq_min = freq_center - 0.5 * sample_rate
42+
freq_max = freq_center + 0.5 * sample_rate
4143
4244
# Get frequency edges of annotation (default to edges of capture)
4345
freq_start = annotation.get(SigMFFile.FLO_KEY)
@@ -66,34 +68,41 @@ First, create a single SigMF Recording and save it to disk:
6668
data = np.zeros(1024, dtype=np.complex64)
6769
6870
# write those samples to file in cf32_le
69-
data.tofile('example_cf32.sigmf-data')
71+
data.tofile("example_cf32.sigmf-data")
7072
7173
# create the metadata
7274
meta = SigMFFile(
73-
data_file='example_cf32.sigmf-data', # extension is optional
74-
global_info = {
75+
data_file="example_cf32.sigmf-data", # extension is optional
76+
global_info={
7577
SigMFFile.DATATYPE_KEY: get_data_type_str(data), # in this case, 'cf32_le'
7678
SigMFFile.SAMPLE_RATE_KEY: 48000,
77-
SigMFFile.AUTHOR_KEY: 'jane.doe@domain.org',
78-
SigMFFile.DESCRIPTION_KEY: 'All zero complex float32 example file.',
79-
}
79+
SigMFFile.AUTHOR_KEY: "jane.doe@domain.org",
80+
SigMFFile.DESCRIPTION_KEY: "All zero complex float32 example file.",
81+
},
8082
)
8183
8284
# create a capture key at time index 0
83-
meta.add_capture(0, metadata={
84-
SigMFFile.FREQUENCY_KEY: 915000000,
85-
SigMFFile.DATETIME_KEY: get_sigmf_iso8601_datetime_now(),
86-
})
85+
meta.add_capture(
86+
0,
87+
metadata={
88+
SigMFFile.FREQUENCY_KEY: 915000000,
89+
SigMFFile.DATETIME_KEY: get_sigmf_iso8601_datetime_now(),
90+
},
91+
)
8792
8893
# add an annotation at sample 100 with length 200 & 10 KHz width
89-
meta.add_annotation(100, 200, metadata = {
90-
SigMFFile.FLO_KEY: 914995000.0,
91-
SigMFFile.FHI_KEY: 915005000.0,
92-
SigMFFile.COMMENT_KEY: 'example annotation',
93-
})
94+
meta.add_annotation(
95+
100,
96+
200,
97+
metadata={
98+
SigMFFile.FLO_KEY: 914995000.0,
99+
SigMFFile.FHI_KEY: 915005000.0,
100+
SigMFFile.COMMENT_KEY: "example annotation",
101+
},
102+
)
94103
95104
# check for mistakes & write to disk
96-
meta.tofile('example_cf32.sigmf-meta') # extension is optional
105+
meta.tofile("example_cf32.sigmf-meta") # extension is optional
97106
98107
Now lets add another SigMF Recording and associate them with a SigMF Collection:
99108

@@ -103,47 +112,50 @@ Now lets add another SigMF Recording and associate them with a SigMF Collection:
103112
104113
data_ci16 = np.zeros(1024, dtype=np.complex64)
105114
106-
#rescale and save as a complex int16 file:
115+
# rescale and save as a complex int16 file:
107116
data_ci16 *= pow(2, 15)
108-
data_ci16.view(np.float32).astype(np.int16).tofile('example_ci16.sigmf-data')
117+
data_ci16.view(np.float32).astype(np.int16).tofile("example_ci16.sigmf-data")
109118
110119
# create the metadata for the second file
111120
meta_ci16 = SigMFFile(
112-
data_file='example_ci16.sigmf-data', # extension is optional
113-
global_info = {
114-
SigMFFile.DATATYPE_KEY: 'ci16_le', # get_data_type_str() is only valid for numpy types
121+
data_file="example_ci16.sigmf-data", # extension is optional
122+
global_info={
123+
SigMFFile.DATATYPE_KEY: "ci16_le", # get_data_type_str() is only valid for numpy types
115124
SigMFFile.SAMPLE_RATE_KEY: 48000,
116-
SigMFFile.DESCRIPTION_KEY: 'All zero complex int16 file.',
117-
}
125+
SigMFFile.DESCRIPTION_KEY: "All zero complex int16 file.",
126+
},
118127
)
119128
meta_ci16.add_capture(0, metadata=meta.get_capture_info(0))
120-
meta_ci16.tofile('example_ci16.sigmf-meta')
121-
122-
collection = SigMFCollection(['example_cf32.sigmf-meta', 'example_ci16.sigmf-meta'],
123-
metadata = {'collection': {
124-
SigMFCollection.AUTHOR_KEY: 'sigmf@sigmf.org',
125-
SigMFCollection.DESCRIPTION_KEY: 'Collection of two all zero files.',
129+
meta_ci16.tofile("example_ci16.sigmf-meta")
130+
131+
collection = SigMFCollection(
132+
["example_cf32.sigmf-meta", "example_ci16.sigmf-meta"],
133+
metadata={
134+
"collection": {
135+
SigMFCollection.AUTHOR_KEY: "sigmf@sigmf.org",
136+
SigMFCollection.DESCRIPTION_KEY: "Collection of two all zero files.",
126137
}
127-
}
138+
},
128139
)
129140
streams = collection.get_stream_names()
130141
sigmf = [collection.get_SigMFFile(stream) for stream in streams]
131-
collection.tofile('example_zeros.sigmf-collection')
142+
collection.tofile("example_zeros.sigmf-collection")
132143
133144
The SigMF Collection and its associated Recordings can now be loaded like this:
134145

135146
.. code-block:: python
136147
137148
import sigmf
138-
collection = sigmf.fromfile('example_zeros')
139-
ci16_sigmffile = collection.get_SigMFFile(stream_name='example_ci16')
140-
cf32_sigmffile = collection.get_SigMFFile(stream_name='example_cf32')
149+
150+
collection = sigmf.fromfile("example_zeros")
151+
ci16_sigmffile = collection.get_SigMFFile(stream_name="example_ci16")
152+
cf32_sigmffile = collection.get_SigMFFile(stream_name="example_cf32")
141153
142154
-----------------------------------------------
143155
Load a SigMF Archive and slice without untaring
144156
-----------------------------------------------
145157

146-
Since an *archive* is merely a tarball (uncompressed), and since there any many
158+
Since an *archive* is a tarball (uncompressed by default), and since there are many
147159
excellent tools for manipulating tar files, it's fairly straightforward to
148160
access the *data* part of a SigMF archive without un-taring it. This is a
149161
compelling feature because **1** archives make it harder for the ``-data`` and
@@ -195,3 +207,50 @@ read it, this can be done "in mid air" or "without touching the ground (disk)".
195207
>>> arc[:10]
196208
array([-20.+11.j, -21. -6.j, -17.-20.j, -13.-52.j, 0.-75.j, 22.-58.j,
197209
48.-44.j, 49.-60.j, 31.-56.j, 23.-47.j], dtype=complex64)
210+
211+
------------------------------
212+
Compressed SigMF Archives
213+
------------------------------
214+
215+
SigMF archives can be compressed using gzip, xz, or zip.
216+
The file extension determines the archive format:
217+
218+
+---------------------+-------------+
219+
| Extension | Format |
220+
+=====================+=============+
221+
| ``.sigmf`` | uncompressed|
222+
+---------------------+-------------+
223+
| ``.sigmf.gz`` | gzip tar |
224+
+---------------------+-------------+
225+
| ``.sigmf.xz`` | xz tar |
226+
+---------------------+-------------+
227+
| ``.sigmf.zip`` | zip archive |
228+
+---------------------+-------------+
229+
230+
**Writing compressed archives:**
231+
232+
::
233+
234+
>>> import sigmf
235+
>>> signal = sigmf.sigmffile.fromfile('recording.sigmf-meta')
236+
237+
# extension determines format
238+
>>> signal.tofile('recording.sigmf.xz')
239+
>>> signal.archive('recording.sigmf.gz')
240+
241+
# compression parameter creates archive with correct extension
242+
>>> signal.tofile('recording', compression='xz') # → recording.sigmf.xz
243+
>>> signal.archive('recording', compression='gz') # → recording.sigmf.gz
244+
245+
**Reading compressed archives:**
246+
247+
::
248+
249+
>>> signal = sigmf.fromfile('recording.sigmf.xz')
250+
>>> signal[:10]
251+
array([-20.+11.j, ...], dtype=complex64)
252+
253+
**Memory behavior:**
254+
255+
Uncompressed ``.sigmf`` archives use ``numpy.memmap`` for zero-copy access.
256+
Compressed archives must decompress into RAM before access.

docs/source/converters.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ formats and reads without writing any output files:
2929
3030
# auto-detect and create NCD for any supported format
3131
meta = sigmf.fromfile("recording.cdif") # BLUE file
32-
meta = sigmf.fromfile("recording.wav") # WAV file
33-
meta = sigmf.fromfile("recording.xml") # Signal Hound Spike file
32+
meta = sigmf.fromfile("recording.wav") # WAV file
33+
meta = sigmf.fromfile("recording.xml") # Signal Hound Spike file
3434
meta = sigmf.fromfile("recording.sigmf") # SigMF archive
3535
3636
all_samples = meta.read_samples()

docs/source/quickstart.rst

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,16 @@ Read a SigMF Recording
2323
.. code-block:: python
2424
2525
import sigmf
26+
2627
handle = sigmf.fromfile("example.sigmf")
2728
# reading data
28-
handle.read_samples() # read all timeseries data
29-
handle[10:50] # read memory slice of samples 10 through 50
29+
handle.read_samples() # read all timeseries data
30+
handle[10:50] # read memory slice of samples 10 through 50
3031
# accessing metadata
31-
handle.sample_rate # get sample rate attribute
32-
handle.get_global_info() # returns 'global' dictionary
33-
handle.get_captures() # returns list of 'captures' dictionaries
34-
handle.get_annotations() # returns list of all annotations
32+
handle.sample_rate # get sample rate attribute
33+
handle.get_global_info() # returns 'global' dictionary
34+
handle.get_captures() # returns list of 'captures' dictionaries
35+
handle.get_annotations() # returns list of all annotations
3536
3637
-----------------------------------
3738
Verify SigMF Integrity & Compliance
@@ -45,6 +46,35 @@ Verify SigMF Integrity & Compliance
4546
Save a Numpy array as a SigMF Recording
4647
---------------------------------------
4748

49+
.. code-block:: python
50+
51+
import numpy as np
52+
import sigmf
53+
54+
# suppose we have a complex timeseries signal
55+
data = np.zeros(1024, dtype=np.complex64)
56+
57+
# create SigMFFile from array — datatype is inferred from the numpy array
58+
meta = sigmf.fromarray(data, sample_rate=48000, frequency=915e6)
59+
60+
# write to separate .sigmf-meta and .sigmf-data files
61+
meta.tofile("example")
62+
63+
# or write to a SigMF archive (example.sigmf)
64+
meta.tofile("example.sigmf")
65+
66+
# or write to a compressed archive (example.sigmf.xz)
67+
meta.tofile("example.sigmf.xz")
68+
69+
The ``SigMFFile`` object can be modified before writing to add additional
70+
captures, annotations, or global metadata fields.
71+
72+
---------------------------------------------------
73+
Save a Numpy array with Full Metadata (Advanced)
74+
---------------------------------------------------
75+
76+
For full control over global fields, captures, and annotations:
77+
4878
.. code-block:: python
4979
5080
import numpy as np
@@ -59,30 +89,37 @@ Save a Numpy array as a SigMF Recording
5989
6090
# create the metadata
6191
meta = SigMFFile(
62-
data_file="example.sigmf-data", # extension is optional
63-
global_info = {
92+
data_file="example.sigmf-data", # extension is optional
93+
global_info={
6494
SigMFFile.DATATYPE_KEY: get_data_type_str(data), # in this case, "cf32_le"
6595
SigMFFile.SAMPLE_RATE_KEY: 48000,
6696
SigMFFile.AUTHOR_KEY: "jane.doe@domain.org",
6797
SigMFFile.DESCRIPTION_KEY: "All zero complex float32 example file.",
68-
}
98+
},
6999
)
70100
71101
# create a capture key at time index 0
72-
meta.add_capture(0, metadata={
73-
SigMFFile.FREQUENCY_KEY: 915000000,
74-
SigMFFile.DATETIME_KEY: get_sigmf_iso8601_datetime_now(),
75-
})
102+
meta.add_capture(
103+
0,
104+
metadata={
105+
SigMFFile.FREQUENCY_KEY: 915000000,
106+
SigMFFile.DATETIME_KEY: get_sigmf_iso8601_datetime_now(),
107+
},
108+
)
76109
77110
# add an annotation at sample 100 with length 200 & 10 KHz width
78-
meta.add_annotation(100, 200, metadata = {
79-
SigMFFile.FLO_KEY: 914995000.0,
80-
SigMFFile.FHI_KEY: 915005000.0,
81-
SigMFFile.COMMENT_KEY: "example annotation",
82-
})
111+
meta.add_annotation(
112+
100,
113+
200,
114+
metadata={
115+
SigMFFile.FLO_KEY: 914995000.0,
116+
SigMFFile.FHI_KEY: 915005000.0,
117+
SigMFFile.COMMENT_KEY: "example annotation",
118+
},
119+
)
83120
84121
# validate & write to disk
85-
meta.tofile("example.sigmf-meta") # extension is optional
122+
meta.tofile("example.sigmf-meta") # extension is optional
86123
87124
----------------------------------
88125
Attribute Access for Global Fields

docs/source/siggen.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ A seed ensures reproducibility across runs.
6464
signal = SigMFGenerator(seed=0xDEADBEEF).generate()
6565
6666
# the number and type of components are randomly chosen
67-
print(signal.description) # e.g. "synthetic signal with 3 tones and 2 sweeps"
68-
print(signal.get_annotations()) # one annotation per component
67+
print(signal.description) # e.g. "synthetic signal with 3 tones and 2 sweeps"
68+
print(signal.get_annotations()) # one annotation per component
6969
7070
Without a seed, each call produces a different signal.
7171

sigmf/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# SPDX-License-Identifier: LGPL-3.0-or-later
66

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

@@ -22,4 +22,4 @@
2222
from .archive import SigMFArchive
2323
from .archivereader import SigMFArchiveReader
2424
from .siggen import SigMFGenerator
25-
from .sigmffile import SigMFCollection, SigMFFile, fromarchive, fromfile
25+
from .sigmffile import SigMFCollection, SigMFFile, fromarchive, fromarray, fromfile

0 commit comments

Comments
 (0)