|
1 |
| -<p align="center"><img src="https://github.com/sigmf/SigMF/blob/v1.2.0/logo/sigmf_logo.png" alt="Rendered SigMF Logo"/></p> |
| 1 | + |
2 | 2 |
|
3 |
| -This python module makes it easy to interact with Signal Metadata Format |
4 |
| -(SigMF) recordings. This module works with Python 3.7+ and is distributed |
| 3 | +[](https://pypi.org/project/SigMF/) |
| 4 | +[](https://github.com/sigmf/sigmf-python/actions?query=branch%3Amain) |
| 5 | +[](https://en.wikipedia.org/wiki/GNU_Lesser_General_Public_License) |
| 6 | +[](https://sigmf.readthedocs.io/en/latest/) |
| 7 | +[](https://pypi.org/project/SigMF/) |
| 8 | + |
| 9 | +The `sigmf` library makes it easy to interact with Signal Metadata Format |
| 10 | +(SigMF) recordings. This library is compatible with Python 3.7-3.13 and is distributed |
5 | 11 | freely under the terms GNU Lesser GPL v3 License.
|
6 | 12 |
|
7 | 13 | 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).
|
8 | 14 |
|
9 |
| -# Installation |
10 |
| - |
11 |
| -To install the latest PyPi release, install from pip: |
| 15 | +To install the latest PyPI release, install from pip: |
12 | 16 |
|
13 | 17 | ```bash
|
14 | 18 | pip install sigmf
|
15 | 19 | ```
|
16 | 20 |
|
17 |
| -To install the latest git release, build from source: |
18 |
| - |
19 |
| -```bash |
20 |
| -git clone https://github.com/sigmf/sigmf-python.git |
21 |
| -cd sigmf-python |
22 |
| -pip install . |
23 |
| -``` |
24 |
| - |
25 |
| -Testing can be run with a variety of tools: |
26 |
| - |
27 |
| -```bash |
28 |
| -# pytest and coverage run locally |
29 |
| -pytest |
30 |
| -coverage run |
31 |
| -# run coverage in a venv |
32 |
| -tox run |
33 |
| -# other useful tools |
34 |
| -pylint sigmf tests |
35 |
| -pytype |
36 |
| -black |
37 |
| -flake8 |
38 |
| -``` |
39 |
| - |
40 |
| -# Examples |
41 |
| - |
42 |
| -### Load a SigMF archive; read all samples & metadata |
43 |
| - |
44 |
| -```python |
45 |
| -import sigmf |
46 |
| -handle = sigmf.sigmffile.fromfile('example.sigmf') |
47 |
| -handle.read_samples() # returns all timeseries data |
48 |
| -handle.get_global_info() # returns 'global' dictionary |
49 |
| -handle.get_captures() # returns list of 'captures' dictionaries |
50 |
| -handle.get_annotations() # returns list of all annotations |
51 |
| -``` |
52 |
| - |
53 |
| -### Verify SigMF dataset integrity & compliance |
54 |
| - |
55 |
| -```bash |
56 |
| -sigmf_validate example.sigmf |
57 |
| -``` |
58 |
| - |
59 |
| -### Load a SigMF dataset; read its annotation, metadata, and samples |
60 |
| - |
61 |
| -```python |
62 |
| -from sigmf import SigMFFile, sigmffile |
63 |
| - |
64 |
| -# Load a dataset |
65 |
| -filename = 'logo/sigmf_logo' # extension is optional |
66 |
| -signal = sigmffile.fromfile(filename) |
67 |
| - |
68 |
| -# Get some metadata and all annotations |
69 |
| -sample_rate = signal.get_global_field(SigMFFile.SAMPLE_RATE_KEY) |
70 |
| -sample_count = signal.sample_count |
71 |
| -signal_duration = sample_count / sample_rate |
72 |
| -annotations = signal.get_annotations() |
73 |
| - |
74 |
| -# Iterate over annotations |
75 |
| -for adx, annotation in enumerate(annotations): |
76 |
| - annotation_start_idx = annotation[SigMFFile.START_INDEX_KEY] |
77 |
| - annotation_length = annotation[SigMFFile.LENGTH_INDEX_KEY] |
78 |
| - annotation_comment = annotation.get(SigMFFile.COMMENT_KEY, "[annotation {}]".format(adx)) |
79 |
| - |
80 |
| - # Get capture info associated with the start of annotation |
81 |
| - capture = signal.get_capture_info(annotation_start_idx) |
82 |
| - freq_center = capture.get(SigMFFile.FREQUENCY_KEY, 0) |
83 |
| - freq_min = freq_center - 0.5*sample_rate |
84 |
| - freq_max = freq_center + 0.5*sample_rate |
85 |
| - |
86 |
| - # Get frequency edges of annotation (default to edges of capture) |
87 |
| - freq_start = annotation.get(SigMFFile.FLO_KEY) |
88 |
| - freq_stop = annotation.get(SigMFFile.FHI_KEY) |
89 |
| - |
90 |
| - # Get the samples corresponding to annotation |
91 |
| - samples = signal.read_samples(annotation_start_idx, annotation_length) |
92 |
| -``` |
93 |
| - |
94 |
| -### Create and save a Collection of SigMF Recordings from numpy arrays |
95 |
| - |
96 |
| -First, create a single SigMF Recording and save it to disk |
97 |
| - |
98 |
| -```python |
99 |
| -import datetime as dt |
100 |
| -import numpy as np |
101 |
| -import sigmf |
102 |
| -from sigmf import SigMFFile |
103 |
| -from sigmf.utils import get_data_type_str, get_sigmf_iso8601_datetime_now |
104 |
| - |
105 |
| -# suppose we have an complex timeseries signal |
106 |
| -data = np.zeros(1024, dtype=np.complex64) |
107 |
| - |
108 |
| -# write those samples to file in cf32_le |
109 |
| -data.tofile('example_cf32.sigmf-data') |
110 |
| - |
111 |
| -# create the metadata |
112 |
| -meta = SigMFFile( |
113 |
| - data_file='example_cf32.sigmf-data', # extension is optional |
114 |
| - global_info = { |
115 |
| - SigMFFile.DATATYPE_KEY: get_data_type_str(data), # in this case, 'cf32_le' |
116 |
| - SigMFFile.SAMPLE_RATE_KEY: 48000, |
117 |
| - SigMFFile.AUTHOR_KEY: 'jane.doe@domain.org', |
118 |
| - SigMFFile.DESCRIPTION_KEY: 'All zero complex float32 example file.', |
119 |
| - } |
120 |
| -) |
121 |
| - |
122 |
| -# create a capture key at time index 0 |
123 |
| -meta.add_capture(0, metadata={ |
124 |
| - SigMFFile.FREQUENCY_KEY: 915000000, |
125 |
| - SigMFFile.DATETIME_KEY: get_sigmf_iso8601_datetime_now(), |
126 |
| -}) |
127 |
| - |
128 |
| -# add an annotation at sample 100 with length 200 & 10 KHz width |
129 |
| -meta.add_annotation(100, 200, metadata = { |
130 |
| - SigMFFile.FLO_KEY: 914995000.0, |
131 |
| - SigMFFile.FHI_KEY: 915005000.0, |
132 |
| - SigMFFile.COMMENT_KEY: 'example annotation', |
133 |
| -}) |
134 |
| - |
135 |
| -# check for mistakes & write to disk |
136 |
| -meta.tofile('example_cf32.sigmf-meta') # extension is optional |
137 |
| -``` |
138 |
| - |
139 |
| -Now lets add another SigMF Recording and associate them with a SigMF Collection: |
140 |
| - |
141 |
| -```python |
142 |
| -from sigmf import SigMFCollection |
143 |
| - |
144 |
| -data_ci16 = np.zeros(1024, dtype=np.complex64) |
145 |
| - |
146 |
| -#rescale and save as a complex int16 file: |
147 |
| -data_ci16 *= pow(2, 15) |
148 |
| -data_ci16.view(np.float32).astype(np.int16).tofile('example_ci16.sigmf-data') |
149 |
| - |
150 |
| -# create the metadata for the second file |
151 |
| -meta_ci16 = SigMFFile( |
152 |
| - data_file='example_ci16.sigmf-data', # extension is optional |
153 |
| - global_info = { |
154 |
| - SigMFFile.DATATYPE_KEY: 'ci16_le', # get_data_type_str() is only valid for numpy types |
155 |
| - SigMFFile.SAMPLE_RATE_KEY: 48000, |
156 |
| - SigMFFile.DESCRIPTION_KEY: 'All zero complex int16 file.', |
157 |
| - } |
158 |
| -) |
159 |
| -meta_ci16.add_capture(0, metadata=meta.get_capture_info(0)) |
160 |
| -meta_ci16.tofile('example_ci16.sigmf-meta') |
161 |
| - |
162 |
| -collection = SigMFCollection(['example_cf32.sigmf-meta', 'example_ci16.sigmf-meta'], |
163 |
| - metadata = {'collection': { |
164 |
| - SigMFCollection.AUTHOR_KEY: 'sigmf@sigmf.org', |
165 |
| - SigMFCollection.DESCRIPTION_KEY: 'Collection of two all zero files.', |
166 |
| - } |
167 |
| - } |
168 |
| -) |
169 |
| -streams = collection.get_stream_names() |
170 |
| -sigmf = [collection.get_SigMFFile(stream) for stream in streams] |
171 |
| -collection.tofile('example_zeros.sigmf-collection') |
172 |
| -``` |
173 |
| - |
174 |
| -The SigMF Collection and its associated Recordings can now be loaded like this: |
175 |
| - |
176 |
| -```python |
177 |
| -from sigmf import sigmffile |
178 |
| -collection = sigmffile.fromfile('example_zeros') |
179 |
| -ci16_sigmffile = collection.get_SigMFFile(stream_name='example_ci16') |
180 |
| -cf32_sigmffile = collection.get_SigMFFile(stream_name='example_cf32') |
181 |
| -``` |
182 |
| - |
183 |
| -### Load a SigMF Archive and slice its data without untaring it |
184 |
| - |
185 |
| -Since an *archive* is merely a tarball (uncompressed), and since there any many |
186 |
| -excellent tools for manipulating tar files, it's fairly straightforward to |
187 |
| -access the *data* part of a SigMF archive without un-taring it. This is a |
188 |
| -compelling feature because __1__ archives make it harder for the `-data` and |
189 |
| -the `-meta` to get separated, and __2__ some datasets are so large that it can |
190 |
| -be impractical (due to available disk space, or slow network speeds if the |
191 |
| -archive file resides on a network file share) or simply obnoxious to untar it |
192 |
| -first. |
193 |
| - |
194 |
| -```python |
195 |
| ->>> import sigmf |
196 |
| ->>> arc = sigmf.SigMFArchiveReader('/src/LTE.sigmf') |
197 |
| ->>> arc.shape |
198 |
| -(15379532,) |
199 |
| ->>> arc.ndim |
200 |
| -1 |
201 |
| ->>> arc[:10] |
202 |
| -array([-20.+11.j, -21. -6.j, -17.-20.j, -13.-52.j, 0.-75.j, 22.-58.j, |
203 |
| - 48.-44.j, 49.-60.j, 31.-56.j, 23.-47.j], dtype=complex64) |
204 |
| -``` |
205 |
| - |
206 |
| -The preceeding example exhibits another feature of this approach; the archive |
207 |
| -`LTE.sigmf` is actually `complex-int16`'s on disk, for which there is no |
208 |
| -corresponding type in `numpy`. However, the `.sigmffile` member keeps track of |
209 |
| -this, and converts the data to `numpy.complex64` *after* slicing it, that is, |
210 |
| -after reading it from disk. |
211 |
| - |
212 |
| -```python |
213 |
| ->>> arc.sigmffile.get_global_field(sigmf.SigMFFile.DATATYPE_KEY) |
214 |
| -'ci16_le' |
215 |
| - |
216 |
| ->>> arc.sigmffile._memmap.dtype |
217 |
| -dtype('int16') |
218 |
| - |
219 |
| ->>> arc.sigmffile._return_type |
220 |
| -'<c8' |
221 |
| -``` |
222 |
| - |
223 |
| -Another supported mode is the case where you might have an archive that *is not |
224 |
| -on disk* but instead is simply `bytes` in a python variable. |
225 |
| -Instead of needing to write this out to a temporary file before being able to |
226 |
| -read it, this can be done "in mid air" or "without touching the ground (disk)". |
227 |
| - |
228 |
| -```python |
229 |
| ->>> import sigmf, io |
230 |
| ->>> sigmf_bytes = io.BytesIO(open('/src/LTE.sigmf', 'rb').read()) |
231 |
| ->>> arc = sigmf.SigMFArchiveReader(archive_buffer=sigmf_bytes) |
232 |
| ->>> arc[:10] |
233 |
| -array([-20.+11.j, -21. -6.j, -17.-20.j, -13.-52.j, 0.-75.j, 22.-58.j, |
234 |
| - 48.-44.j, 49.-60.j, 31.-56.j, 23.-47.j], dtype=complex64) |
235 |
| -``` |
236 |
| - |
237 |
| -# Frequently Asked Questions |
238 |
| - |
239 |
| -### Is this a GNU Radio effort? |
240 |
| - |
241 |
| -*No*, this is not a GNU Radio-specific effort. |
242 |
| -This effort first emerged from a group of GNU Radio core |
243 |
| -developers, but the goal of the project to provide a standard that will be |
244 |
| -useful to anyone and everyone, regardless of tool or workflow. |
245 |
| - |
246 |
| -### Is this specific to wireless communications? |
247 |
| - |
248 |
| -*No*, similar to the response, above, the goal is to create something that is |
249 |
| -generally applicable to _signal processing_, regardless of whether or not the |
250 |
| -application is communications related. |
| 21 | +**[Please visit the documentation for examples & more info.](https://sigmf.readthedocs.io/en/latest/)** |
0 commit comments