Skip to content

Commit 3b3c113

Browse files
committed
python: Start of re-working thor and util functions for integer math.
1 parent eccf3d0 commit 3b3c113

2 files changed

Lines changed: 144 additions & 11 deletions

File tree

python/digital_rf/util.py

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import ast
1313
import datetime
14+
import fractions
1415

1516
import dateutil.parser
1617
import numpy as np
@@ -32,12 +33,141 @@
3233
epoch = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc)
3334

3435

36+
def get_samplerate_frac(sr_or_numerator, denominator=None):
37+
"""Convert argument sample rate to a rational Fraction.
38+
39+
Arguments are passed directly to the fractions.Fraction class, and the denominator
40+
of the result is limited to 32 bits.
41+
42+
Parameters
43+
----------
44+
sr_or_numerator : int | float | numpy.number | Rational | Decimal | str
45+
Sample rate in Hz, or the numerator of the sample rate if `denominator` is
46+
given. Most numeric types are accepted, falling back to evaluating the argument
47+
as a string if passing directly to fractions.Fraction fails. String arguments
48+
can represent the sample rate or a rational expression like "123/456".
49+
50+
denominator: int | Rational, optional
51+
Denominator of the sample rate in Hz, if not None. Must be an integer or
52+
a Rational type, as expected by the `denominator` argument of
53+
fractions.Fraction.
54+
55+
56+
Returns
57+
-------
58+
frac : fractions.Fraction
59+
Rational representation of the sample rate.
60+
61+
"""
62+
try:
63+
frac = fractions.Fraction(sr_or_numerator, denominator)
64+
except TypeError:
65+
# try converting sr to str, then to fraction (works for np.longdouble)
66+
sr_frac = fractions.Fraction(str(sr_or_numerator))
67+
frac = fractions.Fraction(sr_frac, denominator)
68+
return frac.limit_denominator(2 ** 32)
69+
70+
71+
def time_to_sample_ceil(timedelta, samples_per_second):
72+
"""Convert a timedelta into a number of samples using a given sample rate.
73+
74+
Ceiling rounding is used so that the value is the whole number of samples
75+
that spans *at least* the given `timedelta` but no more than
76+
``timedelta + 1 / samples_per_second``. This complements the flooring in
77+
`sample_to_time_floor`, so that::
78+
79+
time_to_sample_ceil(sample_to_time_floor(sample, sps), sps) == sample
80+
81+
82+
Parameters
83+
----------
84+
timedelta : (second, picosecond) tuple | np.timedelta64 | datetime.timedelta | float
85+
Time span to convert to a number of samples. To represent large time spans
86+
with high accuracy, pass a 2-tuple of ints containing the number of whole
87+
seconds and additional picoseconds. Float values are interpreted as a
88+
number of seconds.
89+
90+
samples_per_second : fractions.Fraction | arg tuple for ``get_samplerate_frac``
91+
Sample rate in Hz.
92+
93+
94+
Returns
95+
-------
96+
nsamples : int
97+
Number of samples in the `timedelta` time span at a rate of
98+
`samples_per_second`, using ceiling rounding (up to the next whole sample).
99+
100+
"""
101+
if isinstance(timedelta, tuple):
102+
t_sec, t_psec = timedelta
103+
elif isinstance(timedelta, np.timedelta64):
104+
onesec = np.timedelta64(1, "s")
105+
t_sec = timedelta // onesec
106+
t_psec = (timedelta % onesec) // np.timedelta64(1, "ps")
107+
elif isinstance(timedelta, datetime.timedelta):
108+
t_sec = int(timedelta.total_seconds())
109+
t_psec = 1000000 * timedelta.microseconds
110+
else:
111+
t_sec = int(timedelta)
112+
t_psec = int(np.round((timedelta % 1) * 1e12))
113+
# ensure that samples_per_seconds is a fractions.Fraction
114+
if not isinstance(samples_per_second, fractions.Fraction):
115+
samples_per_second = get_samplerate_frac(*samples_per_second)
116+
# calculate rational values for the second and picosecond parts
117+
s_frac = t_sec * samples_per_second + t_psec * samples_per_second / 10 ** 12
118+
# get an integer value through ceiling rounding
119+
return int(s_frac) + ((s_frac % 1) != 0)
120+
121+
122+
def sample_to_time_floor(nsamples, samples_per_second):
123+
"""Convert a number of samples into a timedelta using a given sample rate.
124+
125+
Floor rounding is used so that the given whole number of samples spans
126+
*at least* the returned amount of time, accurate to the picosecond.
127+
This complements the ceiling rounding in `time_to_sample_ceil`, so that::
128+
129+
time_to_sample_ceil(sample_to_time_floor(sample, sps), sps) == sample
130+
131+
132+
Parameters
133+
----------
134+
nsamples : int
135+
Whole number of samples to convert into a span of time.
136+
137+
samples_per_second : fractions.Fraction | arg tuple for ``get_samplerate_frac``
138+
Sample rate in Hz.
139+
140+
141+
Returns
142+
-------
143+
seconds : int
144+
Number of whole seconds in the time span covered by `nsamples` at a rate
145+
of `samples_per_second`.
146+
147+
picoseconds : int
148+
Number of additional picoseconds in the time span covered by `nsamples`,
149+
using floor rounding (down to the previous whole number of picoseconds).
150+
151+
"""
152+
nsamples = int(nsamples)
153+
# ensure that samples_per_seconds is a fractions.Fraction
154+
if not isinstance(samples_per_second, fractions.Fraction):
155+
samples_per_second = get_samplerate_frac(*samples_per_second)
156+
157+
# get the timedelta as a Fraction
158+
t_frac = nsamples / samples_per_second
159+
160+
seconds = int(t_frac)
161+
picoseconds = int((t_frac % 1) * 10 ** 12)
162+
163+
return (seconds, picoseconds)
164+
165+
35166
def time_to_sample(time, samples_per_second):
36167
"""Get a sample index from a time using a given sample rate.
37168
38169
Parameters
39170
----------
40-
41171
time : datetime | float
42172
Time corresponding to the desired sample index. If not given as a
43173
datetime object, the numeric value is interpreted as a UTC timestamp
@@ -49,7 +179,6 @@ def time_to_sample(time, samples_per_second):
49179
50180
Returns
51181
-------
52-
53182
sample_index : int
54183
Index to the identified sample given in the number of samples since
55184
the epoch (time_since_epoch*sample_per_second).
@@ -80,6 +209,11 @@ def sample_to_datetime(sample, samples_per_second):
80209
samples_per_second : np.longdouble
81210
Sample rate in Hz.
82211
212+
epoch : datetime, optional
213+
Epoch time for converting absolute `time` values to a number of seconds
214+
since `epoch`. If None, the Digital RF default (the Unix epoch,
215+
January 1, 1970) is used.
216+
83217
84218
Returns
85219
-------

python/tools/thor.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -543,9 +543,9 @@ def _usrp_setup(self):
543543
# (integer division of clock rate)
544544
cr = op.clock_rates[0]
545545
srdec = int(round(cr / samplerate))
546+
op.samplerate_frac = Fraction(cr).limit_denominator(2 ** 32) / srdec
546547
samplerate_ld = np.longdouble(cr) / srdec
547548
op.samplerate = samplerate_ld
548-
op.samplerate_frac = Fraction(cr).limit_denominator() / srdec
549549

550550
# set per-channel options
551551
# set command time so settings are synced
@@ -667,7 +667,7 @@ def _usrp_setup(self):
667667
info["lo_off"] = op.lo_offsets[ch_num]
668668
info["lo_source"] = op.lo_sources[ch_num]
669669
info["lo_export"] = op.lo_exports[ch_num]
670-
info["sr"] = op.samplerate
670+
info["sr"] = op.samplerate_frac
671671
print(chinfo.format(**info))
672672
print("-" * 78)
673673

@@ -683,19 +683,18 @@ def _finalize_options(self):
683683
op.channelizer_filter_taps = []
684684
op.channelizer_filter_delays = []
685685
for ko, (osr, nsc) in enumerate(zip(op.ch_samplerates, op.ch_nsubchannels)):
686-
# get output sample rate fraction
686+
# get output resampling ratio
687687
# (op.samplerate_frac final value is set in _usrp_setup
688688
# so can't get output sample rate until after that is done)
689689
if osr is None:
690-
ch_samplerate_frac = op.samplerate_frac
690+
ratio = Fraction(1)
691691
else:
692-
ch_samplerate_frac = Fraction(osr).limit_denominator()
693-
op.ch_samplerates_frac.append(ch_samplerate_frac)
694-
695-
# get resampling ratio
696-
ratio = ch_samplerate_frac / op.samplerate_frac
692+
ratio = (Fraction(osr) / op.samplerate_frac).limit_denominator(2 ** 16)
697693
op.resampling_ratios.append(ratio)
698694

695+
# get output samplerate fraction
696+
op.ch_samplerates_frac.append(op.samplerate_frac * ratio)
697+
699698
# get resampling low-pass filter taps
700699
if ratio == 1:
701700
op.resampling_filter_taps.append(np.zeros(0))

0 commit comments

Comments
 (0)