Skip to content

Commit

Permalink
Merge pull request #116 from the-aerospace-corporation/release/public
Browse files Browse the repository at this point in the history
ordered metadata; improved setup; better use cases in readme
  • Loading branch information
bhilburn authored Oct 7, 2020
2 parents 20a9e42 + f147aff commit 12202a4
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 53 deletions.
71 changes: 52 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,22 @@ maintained for posterity.
Anyone is welcome to get involved - indeed, the more people involved in the
discussions, the more useful the standard is likely to be.

## Installation
After cloning, simply run the setup script for a static installation.
## Getting Started

This module can be installed the typical way:
```bash
pip install .
```
python setup.py
```

Alternatively, install the module in developer mode if you plan to experiment
with your own changes.

```
python setup.py develop
```
## Use Cases
### Load a SigMF dataset; read its annotation, metadata, and samples

## Usage example
#### Load a SigMF dataset; read its annotation, metadata, and samples
```python
from sigmf import SigMFFile, sigmffile

# Load a dataset
sigmf_filename = 'datasets/my_dataset.sigmf-meta' # extension is optional
signal = sigmffile.fromfile(sigmf_filename)
filename = 'example.sigmf-meta' # extension is optional
signal = sigmffile.fromfile(filename)

# Get some metadata and all annotations
sample_rate = signal.get_global_field(SigMFFile.SAMPLE_RATE_KEY)
Expand All @@ -69,11 +63,10 @@ signal_duration = sample_count / sample_rate
annotations = signal.get_annotations()

# Iterate over annotations
for annotation_idx, annotation in enumerate(annotations):
for adx, annotation in enumerate(annotations):
annotation_start_idx = annotation[SigMFFile.START_INDEX_KEY]
annotation_length = annotation[SigMFFile.LENGTH_INDEX_KEY]
annotation_comment = annotation.get(SigMFFile.COMMENT_KEY,
"[annotation {}]".format(annotation_idx))
annotation_comment = annotation.get(SigMFFile.COMMENT_KEY, "[annotation {}]".format(adx))

# Get capture info associated with the start of annotation
capture = signal.get_capture_info(annotation_start_idx)
Expand All @@ -82,14 +75,54 @@ for annotation_idx, annotation in enumerate(annotations):
freq_max = freq_center + 0.5*sample_rate

# Get frequency edges of annotation (default to edges of capture)
freq_start = annotation.get(SigMFFile.FLO_KEY, f_min)
freq_stop = annotation.get(SigMFFile.FHI_KEY, f_max)
freq_start = annotation.get(SigMFFile.FLO_KEY)
freq_stop = annotation.get(SigMFFile.FHI_KEY)

# Get the samples corresponding to annotation
samples = signal.read_samples(annotation_start_idx, annotation_length)
```

### Write a SigMF file from a numpy array

```python
import datetime as dt
from sigmf import SigMFFile

# suppose we have an complex timeseries signal
data = np.zeros(1024, dtype=np.complex64)

# write those samples to file in cf32_le
data.tofile('example.sigmf-data')

# create the metadata
meta = SigMFFile(
data_file='example.sigmf-data', # extension is optional
global_info = {
SigMFFile.DATATYPE_KEY: 'cf32_le',
SigMFFile.SAMPLE_RATE_KEY: 48000,
SigMFFile.AUTHOR_KEY: '[email protected]',
SigMFFile.DESCRIPTION_KEY: 'All zero example file.',
SigMFFile.VERSION_KEY: sigmf.__version__,
}
)

# create a capture key at time index 0
meta.add_capture(0, metadata={
SigMFFile.FREQUENCY_KEY: 915000000,
SigMFFile.DATETIME_KEY: dt.datetime.utcnow().isoformat()+'Z',
})

# add an annotation at sample 100 with length 200 & 10 KHz width
meta.add_annotation(100, 200, metadata = {
SigMFFile.FLO_KEY: 914995000.0,
SigMFFile.FHI_KEY: 915005000.0,
SigMFFile.COMMENT_KEY: 'example annotation',
})

# check for mistakes & write to disk
assert meta.validate()
meta.tofile('example.sigmf-meta') # extension is optional
```

## Frequently Asked Questions

Expand Down
8 changes: 4 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from setuptools import setup
import os
import re

shortdesc = "Signal Metadata Format Specification"
longdesc = """
Expand All @@ -10,13 +11,12 @@
samples, and features of the signal itself.
"""

# exec version.py to get __version__ (version.py is the single source of the version)
version_file = os.path.join(os.path.dirname(__file__), 'sigmf', 'version.py')
exec(open(version_file).read())
with open(os.path.join('sigmf', '__init__.py')) as handle:
version = re.search(r'__version__\s*=\s*[\'"]([^\'"]*)[\'"]', handle.read()).group(1)

setup(
name='SigMF',
version=__version__,
version=version,
description=shortdesc,
long_description=longdesc,
url='https://github.com/gnuradio/SigMF',
Expand Down
6 changes: 1 addition & 5 deletions sigmf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,8 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

# Use version.py to get the version
# Never define in the __init__.py and import it in setup.py because you can't
# import sigmf in setup.py because you won't have the dependencies yet.
# https://packaging.python.org/guides/single-sourcing-package-version/
__version__ = '0.0.2'

from .version import __version__
from .archive import SigMFArchive
from .sigmffile import SigMFFile

Expand Down
76 changes: 55 additions & 21 deletions sigmf/sigmffile.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from collections import OrderedDict
import codecs
import json
import tarfile
Expand Down Expand Up @@ -75,6 +75,7 @@ def __init__(
metadata=None,
data_file=None,
global_info=None,
skip_checksum=False,
):
self.version = None
self.schema = None
Expand All @@ -89,7 +90,7 @@ def __init__(
if global_info is not None:
self.set_global_info(global_info)
self.data_file = data_file
if self.data_file:
if self.data_file and not skip_checksum:
self.calculate_hash()
self._count_samples()

Expand Down Expand Up @@ -293,32 +294,65 @@ def validate(self):
self._metadata,
schema.get_schema(schema_version),
)

def dump(self, filep, pretty=False):
"""
def ordered_metadata(self):
'''
Get a nicer representation of _metadata. Will sort keys, but put the
top-level fields 'global', 'captures', 'annotations' in front.
Returns
-------
ordered_meta : OrderedDict
Cleaner representation of _metadata with top-level keys correctly
ordered and the rest of the keys sorted.
'''
ordered_meta = OrderedDict()
top_sort_order = ['global', 'captures', 'annotations']
for top_key in top_sort_order:
assert top_key in self._metadata
ordered_meta[top_key] = json.loads(json.dumps(self._metadata[top_key], sort_keys=True))
# If there are other top-level keys, they go later
# TODO: sort these `other` top-level keys
for oth_key, oth_val in self._metadata.items():
if oth_key not in top_sort_order:
ordered_meta[oth_key] = json.loads(json.dumps(oth_val, sort_keys=True))
return ordered_meta

def dump(self, filep, pretty=True):
'''
Write metadata to a file.
Parameters:
filep -- File pointer or something that json.dump() can handle
pretty -- If true, output will be formatted extra nicely.
"""
Parameters
----------
filep : object
File pointer or something that json.dump() can handle
pretty : bool, optional
Is true by default.
'''
json.dump(
self._metadata,
self.ordered_metadata(),
filep,
sort_keys=True if pretty else False,
indent=4 if pretty else None,
separators=(',', ': ') if pretty else None,
)

def dumps(self, pretty=False):
"""
Return a string representation of the metadata file.
Parameters:
pretty -- If true, output will be formatted extra nicely.
"""
def dumps(self, pretty=True):
'''
Get a string representation of the metadata.
Parameters
----------
filep : object
File pointer or something that json.dump() can handle
pretty : bool, optional
Is true by default.
Returns
-------
string
String representation of the metadata using json formatter.
'''
return json.dumps(
self._metadata,
self.ordered_metadata(),
indent=4 if pretty else None,
separators=(',', ': ') if pretty else None,
)
Expand Down Expand Up @@ -486,7 +520,7 @@ def fromarchive(archive_path, dir=None):

return SigMFFile(metadata=metadata, data_file=data_file)

def fromfile(filename):
def fromfile(filename, skip_checksum=False):
"""
Creates and returns a returns a SigMFFile instance with metadata loaded from the specified file.
The filename may be that of either a sigmf-meta file, a sigmf-data file, or a sigmf archive.
Expand All @@ -509,7 +543,7 @@ def fromfile(filename):
mdfile_reader = bytestream_reader(meta_fp)
metadata = json.load(mdfile_reader)
meta_fp.close()
return SigMFFile(metadata=metadata, data_file=data_fn)
return SigMFFile(metadata=metadata, data_file=data_fn, skip_checksum=skip_checksum)

def get_sigmf_filenames(filename):
"""
Expand Down
4 changes: 0 additions & 4 deletions sigmf/version.py

This file was deleted.

8 changes: 8 additions & 0 deletions tests/test_sigmffile.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,11 @@ def test_add_multiple_captures_and_annotations():
sigf = SigMFFile()
for idx in range(3):
simulate_capture(sigf, idx, 1024)

def test_ordered_metadata():
'''check to make sure the metadata is sorted as expected'''
sigf = SigMFFile()
top_sort_order = ['global', 'captures', 'annotations']
for kdx, key in enumerate(sigf.ordered_metadata()):
assert kdx == top_sort_order.index(key)

0 comments on commit 12202a4

Please sign in to comment.