Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions c/include/digital_rf.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ EXPORT int digital_rf_write_blocks_hdf5(
uint64_t, long double, int*, int*, int*, int*, int*, int*, uint64_t*);
extern "C" EXPORT int digital_rf_get_unix_time_rational(
uint64_t, uint64_t, uint64_t, int*, int*, int*, int*, int*, int*, uint64_t*);
extern "C" EXPORT int digital_rf_get_timestamp_floor(
uint64_t, uint64_t, uint64_t, uint64_t*, uint64_t*);
extern "C" EXPORT int digital_rf_get_sample_ceil(
uint64_t, uint64_t, uint64_t, uint64_t, uint64_t*);
extern "C" EXPORT Digital_rf_write_object * digital_rf_create_write_hdf5(
char*, hid_t, uint64_t, uint64_t, uint64_t, uint64_t, uint64_t, char *, int, int, int, int, int, int);
extern "C" EXPORT int digital_rf_write_hdf5(Digital_rf_write_object*, uint64_t, void*,uint64_t);
Expand All @@ -149,6 +153,10 @@ EXPORT int digital_rf_write_blocks_hdf5(
uint64_t sample_rate_numerator, uint64_t sample_rate_denominator,
int * year, int * month, int *day, int * hour, int * minute,
int * second, uint64_t * picosecond);
EXPORT int digital_rf_get_timestamp_floor(
uint64_t sample_index, uint64_t sample_rate_numerator, uint64_t sample_rate_denominator, uint64_t * second, uint64_t * picosecond);
EXPORT int digital_rf_get_sample_ceil(
uint64_t second, uint64_t picosecond, uint64_t sample_rate_numerator, uint64_t sample_rate_denominator, uint64_t * sample_index);
EXPORT Digital_rf_write_object * digital_rf_create_write_hdf5(
char * directory, hid_t dtype_id, uint64_t subdir_cadence_secs,
uint64_t file_cadence_millisecs, uint64_t global_start_sample,
Expand All @@ -166,10 +174,6 @@ EXPORT int digital_rf_write_blocks_hdf5(
#endif

/* Private method declarations */
int digital_rf_get_timestamp_floor(uint64_t sample_index, uint64_t sample_rate_numerator,
uint64_t sample_rate_denominator, uint64_t * second, uint64_t * picosecond);
int digital_rf_get_sample_ceil(uint64_t second, uint64_t picosecond,
uint64_t sample_rate_numerator, uint64_t sample_rate_denominator, uint64_t * sample_index);
int digital_rf_get_time_parts(time_t unix_second, int * year, int * month, int *day,
int * hour, int * minute, int * second);
int digital_rf_get_subdir_file(Digital_rf_write_object *hdf5_data_object, uint64_t global_sample,
Expand Down
2 changes: 1 addition & 1 deletion c/include/digital_rf_version.h
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// library version, increment to match package version when C interface changes
#define DIGITAL_RF_VERSION "2.6.0"
#define DIGITAL_RF_VERSION "2.7.0"
4 changes: 2 additions & 2 deletions conda-forge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@
# documentation on possible keys and values.

build_platform:
linux_aarch64: linux_64
#linux_ppc64le: linux_64
osx_arm64: osx_64
linux_aarch64: linux_64
clone_depth: 0
github_actions:
store_build_artifacts: true
provider:
linux_64: github_actions
linux_aarch64: github_actions
#linux_ppc64le: github_actions
osx_64: github_actions
win_64: github_actions
recipe_dir: recipes/conda
Expand Down
26 changes: 26 additions & 0 deletions news/integer_math_python.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
**Added:**

* The Digital RF and Digital Metadata reader objects now provide a ``sample_rate`` property that represents the rational sample rate as a ``fractions.Fraction`` object. The ``samples_per_second`` property of ```np.longdouble``` dtype still exists for backwards compatibility, but new code should use ``sample_rate`` instead.
* Add new ``digital_rf_get_timestamp_floor`` and ``digital_rf_get_sample_ceil`` C functions that can be used to convert between a timestamp and sample index, given the rational sample rate. These have been made available so that users can perform these calculations in a way that is consistent with what is done internally.
* Add new ``datetime_to_timedelta_tuple``, ``get_samplerate_frac``, ``sample_to_time_floor``, ``time_to_sample_ceil`` Python utility functions for datetime, timestamp, and sample index math. These match the new C functions.

**Changed:**

* All internal code has been updated so that sample rate calculations use a rational representation instead of a ``np.longdouble`` floating point.

**Deprecated:**

* Deprecate ``samples_to_timedelta`` utility function. Use ``sample_to_time_floor`` instead and create a timedelta object if necessary: ``datetime.timedelta(seconds=seconds, microseconds=picoseconds // 1000000)``.
* Deprecate ``time_to_sample`` utility function. Use ``time_to_sample_ceil`` instead in combination with ``datetime_to_timedelta_tuple`` if necessary.

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
28 changes: 21 additions & 7 deletions python/digital_rf/digital_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from six.moves import urllib, zip

# local imports
from . import list_drf
from . import list_drf, util
from ._version import __version__, __version_tuple__

try:
Expand Down Expand Up @@ -194,6 +194,10 @@ def __init__(
raise ValueError(errstr % str(sample_rate_denominator))
self._sample_rate_denominator = int(sample_rate_denominator)

self._sample_rate = util.get_samplerate_frac(
sample_rate_numerator, sample_rate_denominator
)

# have to go to uint64 before longdouble to ensure correct conversion
# from int
self._samples_per_second = np.longdouble(
Expand All @@ -209,6 +213,10 @@ def __init__(
self._fields = None # No data written yet
self._write_properties()

def get_sample_rate(self):
"""Return the sample rate in Hz as a fractions.Fraction."""
return self._sample_rate

def get_samples_per_second(self):
"""Return the sample rate in Hz as a np.longdouble."""
return self._samples_per_second
Expand Down Expand Up @@ -333,7 +341,7 @@ def _sample_group_generator(self, samples):
Digital Metadata file and takes its name from the sample index.

"""
samples_per_file = self._file_cadence_secs * self._samples_per_second
samples_per_file = self._file_cadence_secs * self._sample_rate
for file_idx, sample_group in itertools.groupby(
samples, lambda s: np.uint64(s / samples_per_file)
):
Expand Down Expand Up @@ -472,7 +480,7 @@ def __str__(self):
attr_list = (
"_subdir_cadence_secs",
"_file_cadence_secs",
"_samples_per_second",
"_sample_rate",
"_file_name",
)
for attr in attr_list:
Expand Down Expand Up @@ -587,6 +595,9 @@ def __init__(self, metadata_dir, accept_empty=True):
self._samples_per_second = np.longdouble(
np.uint64(self._sample_rate_numerator)
) / np.longdouble(np.uint64(self._sample_rate_denominator))
self._sample_rate = util.get_samplerate_frac(
self._sample_rate_numerator, self._sample_rate_denominator
)
fname = f.attrs["file_name"]
if isinstance(fname, bytes):
# for convenience and forward-compatibility with h5py>=2.9
Expand Down Expand Up @@ -714,6 +725,10 @@ def get_fields(self):
# _fields is an internal data structure, so make a copy for the user
return copy.deepcopy(self._fields)

def get_sample_rate(self):
"""Return the sample rate in Hz as a fractions.Fraction."""
return self._sample_rate

def get_sample_rate_numerator(self):
"""Return the numerator of the sample rate in Hz."""
return self._sample_rate_numerator
Expand Down Expand Up @@ -1020,9 +1035,8 @@ def _get_file_list(self, sample0, sample1):
scheme.

"""
# need to go through numpy uint64 to prevent conversion to float
start_ts = int(np.uint64(np.uint64(sample0) / self._samples_per_second))
end_ts = int(np.uint64(np.uint64(sample1) / self._samples_per_second))
start_ts, picoseconds = util.sample_to_time_floor(sample0, self._sample_rate)
end_ts, picoseconds = util.sample_to_time_floor(sample1, self._sample_rate)

# convert ts to be divisible by self._file_cadence_secs
start_ts = (start_ts // self._file_cadence_secs) * self._file_cadence_secs
Expand Down Expand Up @@ -1207,7 +1221,7 @@ def __str__(self):
attr_list = (
"_subdir_cadence_secs",
"_file_cadence_secs",
"_samples_per_second",
"_sample_rate",
"_file_name",
)
for attr in attr_list:
Expand Down
60 changes: 31 additions & 29 deletions python/digital_rf/digital_rf_hdf5.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

import collections
import datetime
import fractions
import glob
import os
import re
Expand All @@ -34,7 +33,7 @@
import six

# local imports
from . import _py_rf_write_hdf5, digital_metadata, list_drf
from . import _py_rf_write_hdf5, digital_metadata, list_drf, util
from ._version import __version__, __version_tuple__

__all__ = (
Expand Down Expand Up @@ -969,11 +968,11 @@ def read(self, start_sample, end_sample, channel_name, sub_channel=None):
# first get the names of all possible files with data
subdir_cadence_secs = file_properties["subdir_cadence_secs"]
file_cadence_millisecs = file_properties["file_cadence_millisecs"]
samples_per_second = file_properties["samples_per_second"]
sample_rate = file_properties["sample_rate"]
filepaths = self._get_file_list(
start_sample,
end_sample,
samples_per_second,
sample_rate,
subdir_cadence_secs,
file_cadence_millisecs,
)
Expand Down Expand Up @@ -1080,7 +1079,7 @@ def get_properties(self, channel_name, sample=None):
num_subchannels : int
sample_rate_numerator : int
sample_rate_denominator : int
samples_per_second : np.longdouble
samples_per_second : np.longdouble (don't rely on this!)
subdir_cadence_secs : int

The additional properties particular to each file are:
Expand All @@ -1102,12 +1101,12 @@ def get_properties(self, channel_name, sample=None):

subdir_cadence_secs = global_properties["subdir_cadence_secs"]
file_cadence_millisecs = global_properties["file_cadence_millisecs"]
samples_per_second = global_properties["samples_per_second"]
sample_rate = global_properties["sample_rate"]

file_list = self._get_file_list(
sample,
sample,
samples_per_second,
sample_rate,
subdir_cadence_secs,
file_cadence_millisecs,
)
Expand Down Expand Up @@ -1229,15 +1228,17 @@ def read_metadata(self, start_sample, end_sample, channel_name, method="ffill"):
For convenience, some pertinent metadata inherent to the Digital RF
channel is added to the Digital Metadata, including:

sample_rate : fractions.Fraction
sample_rate_numerator : int
sample_rate_denominator : int
samples_per_second : np.longdouble
samples_per_second : np.longdouble (don't rely on this!)

"""
properties = self.get_properties(channel_name)
added_metadata = {
key: properties[key]
for key in (
"sample_rate",
"sample_rate_numerator",
"sample_rate_denominator",
"samples_per_second",
Expand Down Expand Up @@ -1301,11 +1302,11 @@ def get_continuous_blocks(self, start_sample, end_sample, channel_name):
file_properties = self.get_properties(channel_name)
subdir_cadence_secs = file_properties["subdir_cadence_secs"]
file_cadence_millisecs = file_properties["file_cadence_millisecs"]
samples_per_second = file_properties["samples_per_second"]
sample_rate = file_properties["sample_rate"]
filepaths = self._get_file_list(
start_sample,
end_sample,
samples_per_second,
sample_rate,
subdir_cadence_secs,
file_cadence_millisecs,
)
Expand Down Expand Up @@ -1345,11 +1346,11 @@ def get_last_write(self, channel_name):
file_properties = self.get_properties(channel_name)
subdir_cadence_seconds = file_properties["subdir_cadence_secs"]
file_cadence_millisecs = file_properties["file_cadence_millisecs"]
samples_per_second = file_properties["samples_per_second"]
sample_rate = file_properties["sample_rate"]
file_list = self._get_file_list(
last_sample - 1,
last_sample,
samples_per_second,
sample_rate,
subdir_cadence_seconds,
file_cadence_millisecs,
)
Expand Down Expand Up @@ -1602,7 +1603,7 @@ def read_vector_c81d(
def _get_file_list(
sample0,
sample1,
samples_per_second,
sample_rate,
subdir_cadence_seconds,
file_cadence_millisecs,
):
Expand All @@ -1622,8 +1623,8 @@ def _get_file_list(
Sample index for end of read (inclusive), given in the number of
samples since the epoch (time_since_epoch*sample_rate).

samples_per_second : np.longdouble
Sample rate.
sample_rate : fractions.Fraction | first argument to ``util.get_samplerate_frac``
Sample rate in Hz.

subdir_cadence_secs : int
Number of seconds of data found in one subdir. For example, 3600
Expand All @@ -1644,13 +1645,10 @@ def _get_file_list(
if (sample1 - sample0) > 1e12:
warnstr = "Requested read size, %i samples, is very large"
warnings.warn(warnstr % (sample1 - sample0), RuntimeWarning)
sample0 = int(sample0)
sample1 = int(sample1)
# need to go through numpy uint64 to prevent conversion to float
start_ts = int(np.uint64(np.uint64(sample0) / samples_per_second))
end_ts = int(np.uint64(np.uint64(sample1) / samples_per_second)) + 1
start_msts = int(np.uint64(np.uint64(sample0) / samples_per_second * 1000))
end_msts = int(np.uint64(np.uint64(sample1) / samples_per_second * 1000))
start_ts, picoseconds = util.sample_to_time_floor(sample0, sample_rate)
start_msts = start_ts * 1000 + picoseconds // 1000000000
end_ts, picoseconds = util.sample_to_time_floor(sample1, sample_rate)
end_msts = end_ts * 1000 + picoseconds // 1000000000

# get subdirectory start and end ts
start_sub_ts = int(
Expand Down Expand Up @@ -1811,8 +1809,9 @@ def __init__(self, channel_name, top_level_dir_meta_list=None):
"""Create a new _channel_properties object.

This populates `self.properties`, which is a dictionary of
attributes found in the HDF5 files (eg, samples_per_second). It also
sets the attribute `max_samples_per_file`.
attributes found in the HDF5 files (e.g. sample_rate_numerator /
sample_rate_denominator). It also sets the attribute
`max_samples_per_file`.


Parameters
Expand All @@ -1830,10 +1829,11 @@ def __init__(self, channel_name, top_level_dir_meta_list=None):
self.top_level_dir_meta_list = top_level_dir_meta_list
self.properties = self._read_properties()
file_cadence_millisecs = self.properties["file_cadence_millisecs"]
samples_per_second = self.properties["samples_per_second"]
self.max_samples_per_file = int(
np.uint64(np.ceil(file_cadence_millisecs * samples_per_second / 1000))
)
sample_rate_numerator = self.properties["sample_rate_numerator"]
sample_rate_denominator = self.properties["sample_rate_denominator"]
num = file_cadence_millisecs * sample_rate_numerator
den = 1000 * sample_rate_denominator
self.max_samples_per_file = num // den + (num % den != 0)

def _read_properties(self):
"""Get a dict of the properties stored in the drf_properties.h5 file.
Expand Down Expand Up @@ -1981,13 +1981,15 @@ def _read_properties(self):
# if no sample_rate_numerator/sample_rate_denominator, then we must
# have an older version with samples_per_second as uint64
sps = ret_dict["samples_per_second"]
spsfrac = fractions.Fraction(sps).limit_denominator()
spsfrac = util.get_samplerate_frac(sps)
ret_dict["samples_per_second"] = np.longdouble(sps)
ret_dict["sample_rate_numerator"] = spsfrac.numerator
ret_dict["sample_rate_denominator"] = spsfrac.denominator
ret_dict["sample_rate"] = spsfrac
else:
sps = np.longdouble(np.uint64(srn)) / np.longdouble(np.uint64(srd))
ret_dict["samples_per_second"] = sps
ret_dict["sample_rate"] = util.get_samplerate_frac(srn, srd)

# success
return ret_dict
Expand Down
Loading
Loading