Source code for qugradlab.pulses.filtering

  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
[docs] 126def apply_filtering_transform(time_spline_matrix: np.ndarray[np.complex128], 127 frequency_response: np.ndarray[np.complex128] 128 ) -> np.ndarray[np.complex128]: 129 r""" 130 Filters a spline 131 132 $$ 133 \begin{aligned} 134 f\left(t\right)&\coloneqq\sum_{i=0}^L\sqcap_i\left(t\right)\sum_{j=0}^N 135 f^-_{ij}\left(t-t_i\right)^j\quad\textrm{where }\sqcap_i\left(t\right) 136 \coloneqq\begin{cases} 137 1&t_i\le t< t_{i+1},\\ 138 0&\textrm{otherwise}, 139 \end{cases}\\ 140 &\equiv\sum_{i=0}^L\sqcap_i\left(t\right)\sum_{j=0}^N 141 f^+_{ij}\left(t-t_{i+1}\right)^j\quad 142 \textrm{where }f^\pm_{ij}\equiv\sum_{k=j}^N 143 {k\choose j}\left(\pm[t_{i+1}-t_i]\right)^{j-k}f^\mp_{ik} 144 \end{aligned} 145 $$ 146 147 using the `frequency_response` to the transfer function 148 149 $$ 150 T(\omega)=\frac{(i\omega\tau')^m}{(1+i\omega\tau)^n}. 151 $$ 152 153 The filtered spline will be 154 $$ 155 f'\left(t\right)=\tau^{-n}\tau'^{m}\sum_{i=0}^L\sum_{j=0}^N\Delta f_{ij} 156 \Theta\left(t-t_i\right)\frac{\Gamma\left(j+1\right) 157 \left(t-t_i\right)^{n-m+j}}{\Gamma\left(n-m+j+1\right)} 158 {_1F_1}\left[\begin{matrix} 159 n\\ 160 n-m+j+1 161 \end{matrix}\:; -\left(t-t_i\right)\tau^{-1}\right] 162 $$ 163 where $\Delta f_{ij}\coloneqq f^-_{ij}-f^+_{\left(i-1\right)j}$ corresponds 164 to `time_spline_matrix`, $\Theta$ is 165 the Heaviside step function, $\Gamma$ is the Gamma function, and ${_1F_1}$ 166 is the confluent hypergeometric function. 167 168 Parameters 169 ---------- 170 time_spline_matrix : NDArray[Shape[n_pieces_in_spline, n_orders, ...], np.complex128] 171 This matrix defines the spline and corresponds to $\Delta f_{ij}$. 172 frequency_response : NDArray[Shape[n_orders, samples_per_piece, ``2*n_pieces_in_spline``], np.complex128] 173 The frequency response to the transfer function. This should be computed 174 using :func:`get_frequency_response`. 175 176 Returns 177 ------- 178 `TensorFlow <https://www.tensorflow.org>`__ Tensor 179 The filtered spline $f'(t)$ evaluated at ``samples_per_piece`` 180 equally spaced points per piece of the splice. The shape will be 181 ``(..., samples_per_piece * n_peices_in_spline)``. 182 """ 183 M = tf.shape(time_spline_matrix)[0] 184 remaining_shape = tf.shape(time_spline_matrix)[2:] 185 time_spline_matrix = tf.cast(time_spline_matrix, tf.complex128) 186 time_spline_matrix = tf.transpose(time_spline_matrix) 187 padding = tf.zeros_like(time_spline_matrix) 188 time_spline_matrix = tf.concat([padding, time_spline_matrix], axis=-1) 189 time_spline_matrix = tf.signal.fft(time_spline_matrix) 190 return tf.reshape(tf.transpose(tf.signal.ifft(tf.einsum("...jt,jdt->...dt", time_spline_matrix, frequency_response))[..., M:]), tf.concat([[-1], remaining_shape], axis=0))
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)