|
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: '[email protected]', |
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: '[email protected]', |
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