1"""A collection of methods for filtering contorl amplitudes"""
2
3import typing
4import functools
5
6from typing import Callable
7
8import numpy as np
9import tensorflow as tf
10
11import scipy.special
12
13from . import composition
14
[docs]
15def get_fixed_filter(n_pieces_in_spline: int,
16 piece_length: float,
17 samples_per_piece: int,
18 low_pass_time_constant: float,
19 high_pass_time_constant: float,
20 low_pass_order: float,
21 spline_order: int,
22 high_pass_order: float
23 ) -> Callable[[np.ndarray[np.complex128]], typing.Any]:
24 r"""
25 Generates a Callable that applies the defined filter to the input signal
26 represented by a spline. The filter has a transfer function of the form
27 $$
28 T(\omega)=\frac{(i\omega\tau')^m}{(1+i\omega\tau)^n}.
29 $$
30 where $\tau'$ corresponds to `high_pass_time_constant`, $m$ corresponds to
31 `high_pass_order`, $\tau$ corresponds to `low_pass_time_constant`, and $n$
32 corresponds to `low_pass_order`.
33
34 Consider the spline
35
36 $$
37 \begin{aligned}
38 f\left(t\right)&\coloneqq\sum_{i=0}^L\sqcap_i\left(t\right)\sum_{j=0}^N
39 f^-_{ij}\left(t-t_i\right)^j\quad\textrm{where }\sqcap_i\left(t\right)
40 \coloneqq\begin{cases}
41 1&t_i\le t< t_{i+1},\\
42 0&\textrm{otherwise},
43 \end{cases}\\
44 &\equiv\sum_{i=0}^L\sqcap_i\left(t\right)\sum_{j=0}^N
45 f^+_{ij}\left(t-t_{i+1}\right)^j\quad
46 \textrm{where }f^\pm_{ij}\equiv\sum_{k=j}^N
47 {k\choose j}\left(\pm[t_{i+1}-t_i]\right)^{j-k}f^\mp_{ik}
48 \end{aligned}
49 $$
50
51 where $L$ correspodns to `n_pieces_in_spline`, $N$ corresponds to
52 `spline_order`, and $t_{i+1}-t_i$ corresponds to `piece_length`.
53
54 If we apply the transfer function $T(\omega)$ to the spline $f(t)$ we find
55 the filtered spline is given by
56
57 $$
58 f'\left(t\right)=\tau^{-n}\tau'^{m}\sum_{i=0}^L\sum_{j=0}^N\Delta f_{ij}
59 \Theta\left(t-t_i\right)\frac{\Gamma\left(j+1\right)
60 \left(t-t_i\right)^{n-m+j}}{\Gamma\left(n-m+j+1\right)}
61 {_1F_1}\left[\begin{matrix}
62 n\\
63 n-m+j+1
64 \end{matrix}\:; -\left(t-t_i\right)\tau^{-1}\right]
65 $$
66 where $\Delta f_{ij}\coloneqq f^-_{ij}-f^+_{\left(i-1\right)j}$, $\Theta$ is
67 the Heaviside step function, $\Gamma$ is the Gamma function, and ${_1F_1}$
68 is the confluent hypergeometric function.
69
70 Parameters
71 ----------
72 n_pieces_in_spline : int
73 The number of pieces in the spline
74 piece_length : float
75 The length of each piece in the spline
76 samples_per_piece : int
77 The number of samples per piece in the spline
78 low_pass_time_constant : float
79 The time constant of the low pass type filter. To apply a pure low
80 pass filter set ``high_pass_order_constant = 0``.
81 high_pass_time_constant : float
82 The time constant of the high pass type filter. To apply a pure high
83 pass filter `high_pass_time_constant` should equal
84 ``low_pass_time_constant``.
85 low_pass_order : float
86 The order of the low pass type filter. To apply a pure low pass
87 filter set ``high_pass_order = 0``.
88 spline_orders : NDArray[Shape[n_orders], int]
89 The orders of the spline. 0 corresponds to piecewise constant, 1
90 corresponds to piecewise linear, etc.
91 high_pass_order : float
92 The order of the high pass type filter. To apply a pure high pass
93 filter `high_pass_time_constant` should equal ``low_pass_time_constant``
94 and ``high_pass_order`` should equal ``low_pass_order``.
95
96 Returns
97 -------
98 Callable[[NDArray[np.complex128]], Any]
99 A callable that takes a spline and returns the filtered spline.
100
101 PARAMETERS:
102 * **time_spline_matrix** (*NDArray[np.complex128]*) —
103 This matrix defines the spline and corresponds to $\Delta f_{ij}$.
104
105 RETURNS:
106 The filtered spline $f'(t)$ evaluated at ``samples_per_piece``
107 equally spaced points per piece of the splice. The shape will be
108 ``(..., samples_per_piece * n_peices_in_spline)``.
109
110 RETURN TYPE:
111 `TensorFlow <https://www.tensorflow.org>`__ Tensor
112 """
113 piece_sample_times = np.linspace(0, piece_length, samples_per_piece+1)[:-1]
114 piece_points = piece_length*np.arange(n_pieces_in_spline)
115 sample_times = np.add.outer(piece_sample_times, piece_points)
116 time_response = get_time_response(sample_times,
117 low_pass_time_constant,
118 high_pass_time_constant,
119 low_pass_order,
120 spline_order,
121 high_pass_order)
122 frequency_response = get_frequency_response(time_response)
123 return functools.partial(apply_filtering_transform,
124 frequency_response=frequency_response)
125
191
[docs]
192def get_time_response(sample_times: np.ndarray[np.float64],
193 low_pass_time_constant: float,
194 high_pass_time_constant: float,
195 low_pass_order: int,
196 mononomial_order: int,
197 high_pass_order: int
198 ) -> np.ndarray[np.float64]:
199 r"""Generates the response of the transfer function
200
201 $$
202 T(\omega)=
203 \frac{(i\omega\tau')^m}{(1+i\omega\tau)^n}.
204 $$
205
206 to a Heaviside step function multiplied by a mononomial of order
207 `mononomial_order`
208
209 $$
210 g\left(t\right)=\tau^{-n}\tau'^{m}\Theta\left(t\right)
211 \frac{\Gamma\left(j+1\right)t^{n-m+j}}{\Gamma\left(n-m+j+1\right)}
212 {_1F_1}\left[\begin{matrix}
213 n\\
214 n-m+j+1
215 \end{matrix}\:; -t\tau^{-1}\right].
216 $$
217
218 where $\tau$ corresponds to `low_pass_time_constant`, $\tau'$ corresponds to
219 `high_pass_time_constant`, $m$ corresponds to `high_pass_order`, $n$
220 corresponds to `low_pass_order`, $j$ corresponds to
221 `mononomial_order`, $\Theta$ is
222 the Heaviside step function, $\Gamma$ is the Gamma function, and ${_1F_1}$
223 is the confluent hypergeometric function.
224
225 Parameters
226 ----------
227 sample_times : NDArray[Shape[n_samples], np.float64]
228 The times at which the response function is evaluated.
229 low_pass_time_constant : float
230 The time constant of the low pass type filter. To apply a pure low
231 pass filter set ``high_pass_order_constant = 0``.
232 high_pass_time_constant : float
233 The time constant of the high pass type filter. To apply a pure high
234 pass filter `high_pass_time_constant` should equal
235 ``low_pass_time_constant``.
236 low_pass_order : int
237 The order of the low pass type filter. To apply a pure low pass
238 filter set ``high_pass_order = 0``.
239 mononomial_order : NDArray[Shape[n_orders], int]
240 The order of the mononomial. 0 corresponds constant, 1 linear, etc.
241 high_pass_order : int
242 The order of the high pass type filter. To apply a pure high pass
243 filter ``high_pass_time_constant`` should equal
244 ``low_pass_time_constant`` and ``high_pass_order`` should equal
245 ``low_pass_order``.
246
247 Returns
248 -------
249 NDArray[Shape[n_orders, n_samples], np.float64]
250 The response function evaluated at ``sample_times``.
251 """
252 sample_times = np.array(sample_times)
253 tndim = sample_times.ndim
254 mononomial_order = np.array(mononomial_order)
255 jndim = mononomial_order.ndim
256 for _ in range(jndim): sample_times = np.expand_dims(sample_times, axis=0)
257 for _ in range(tndim):
258 mononomial_order = np.expand_dims(mononomial_order, axis=-1)
259 order = (low_pass_order - high_pass_order) + mononomial_order
260 heaviside_step = (sample_times >= 0)
261 polynomial = np.power(sample_times, order)
262 pre_factor = (scipy.special.factorial(mononomial_order)
263 /scipy.special.factorial(order)) \
264 * np.power(high_pass_time_constant, high_pass_order) \
265 / np.power(low_pass_time_constant, low_pass_order)
266 decay = scipy.special.hyp1f1(low_pass_order,
267 order+1,
268 -sample_times/low_pass_time_constant)
269 return pre_factor*heaviside_step*polynomial*decay
270
[docs]
271def get_frequency_response(time_response: np.ndarray) -> typing.Any:
272 r"""Adds zeros as padding to a time signal before performing descrete
273 Fourier transform. The zero padding allows the discrete convolution theorem
274 to be used.
275
276 Parameters
277 ----------
278 time_response : NDArray[Shape[..., n_samples], np.float64]
279 The time response function to be transformed.
280
281 Returns
282 -------
283 NDArray[Shape[..., ``2*n_samples``], np.complex128]
284 The frequency response function.
285 """
286 time_response = tf.cast(time_response, tf.complex128)
287 time_response = tf.concat([time_response,
288 tf.zeros_like(time_response)],
289 axis=-1)
290 return tf.signal.fft(time_response)
291
[docs]
292def const_spline_time_spline_matrix(points: np.ndarray) -> typing.Any:
293 """
294 Generates a spline matrix of a piecewise constant spline from an array
295 of `points` that represent each of the value of each of the constant pieces
296 of the piecewise constant spline.
297
298 Parameters
299 ----------
300 points : NDArray[Shape[n_pieces, ...], np.float64]
301 The values of the piecewise constant spline.
302
303 Returns
304 -------
305 NDArray[Shape[n_pieces, n_orders, ...], np.float64]
306 The spline matrix of the piecewise constant spline.
307 """
308 return tf.expand_dims(points-composition.pad(points, right=False)[:-1],
309 axis=1)