1111
1212import ast
1313import datetime
14+ import fractions
1415
1516import dateutil .parser
1617import numpy as np
3233epoch = 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+
35166def 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 -------
0 commit comments