1"""Methods and classes for producing an array of sample points."""
2
3import numpy as np
4import tensorflow as tf
5
6import typing
7
[docs]
8def get_sample_points(T: int,
9 number_sample_points: int
10 ) -> tuple[np.ndarray[np.float64], np.float64]:
11 """Generates an array of `number_sample_points` equally spaced sample points
12 between 0 and `T` and also returns the step size.
13
14 Parameters
15 ----------
16 T : int
17 The final point.
18 number_sample_points : int
19 The number of sample points between 0 and `T`.
20
21 Returns
22 -------
23 tuple[NDArray[Shape[``number_sample_points``], np.float64], np.float64]
24 A tuple containing the array of sample points and the step size.
25 """
26 t = np.linspace(0, T, number_sample_points, dtype=np.float64)
27 return t, t[1]
28
[docs]
29class SampleTimes():
30 """Allows for easy generation of equally spaced sample times from a variety
31 of input data
32 """
33
34 _T: float
35 """The total time"""
36
37 _dt: float
38 """The time step"""
39
40 _number_sample_points: int
41 """The number of sample times between 0 and :attr:`T`"""
42
43 _t: np.ndarray[np.float64]
44 """An array of the sample times"""
45
46 _KEYWORDS = ["T", "dt", "number_sample_points"]
47 """The keywords that are allowed to be passed to the constructor"""
48
[docs]
49 def __init__(self, **kwargs):
50 """Initialises an instance of `SampleTimes`. Exactly two of the three
51 optional keyword arguments must be passed.
52
53 Parameters
54 ----------
55 T : float, optional
56 The total time.
57 dt : float, optional
58 The time step.
59 number_sample_points : int, optional
60 The number of sample times between 0 and `T`.
61
62 Raises
63 ------
64 TypeError
65 Raised when more or less than two keyword arguments are provided.
66 TypeError
67 Raised when a keyword arguement other than `T`, `dt`, or
68 `number_sample_points` is passed.
69 """
70 if len(kwargs) != 2:
71 raise TypeError("Must be exactly two arguments.")
72 for key, value in kwargs.items():
73 if key not in self._KEYWORDS:
74 raise TypeError(f"Only {self._KEYWORDS} are allowed arguments.")
75 setattr(self, "_"+key, value)
76 if "T" not in kwargs.keys():
77 self._T = self._dt*(self._number_sample_points-1)
78 self._t = np.linspace(0, self._T, self._number_sample_points)
79 elif "dt" not in kwargs.keys():
80 self._t, self._dt = get_sample_points(self._T, self._number_sample_points)
81 else:
82 assert np.floor(self._T/self._dt)*self._dt == self._T
83 self._number_sample_points = 1 + int(self._T/self._dt)
84 self._t = np.linspace(0, self._T, self._number_sample_points)
85 self._t.flags.writeable = False
86 @property
87 def T(self) -> float:
88 """
89 The total time. If updated, so is :attr:`dt` while
90 :attr:`number_sample_points` is kept constant.
91 """
92 return self._T
93 @T.setter
94 def T(self, value: float):
95 self._T = value
96 self._t, self._dt = get_sample_points(self._T, self._number_sample_points)
97 self._t.flags.writeable = False
98 @property
99 def dt(self) -> float:
100 """
101 The time step. If updated, so is :attr:`T` while
102 :attr:`number_sample_points` is kept constant.
103 """
104 return self._dt
105 @dt.setter
106 def dt(self, value: float):
107 self._dt = value
108 self._T = self._dt*(self._number_sample_points-1)
109 self._t = np.linspace(0, self._T, self._number_sample_points)
110 self._t.flags.writeable = False
111 @property
112 def number_sample_points(self) -> int:
113 """
114 The number of sample times between 0 and `T`. If updated, so is
115 :attr:`dt` while :attr:`T` is kept constant."""
116 return self._number_sample_points
117 @number_sample_points.setter
118 def number_sample_points(self, value: int):
119 self._number_sample_points = value
120 self._t, self._dt = get_sample_points(self._T, self._number_sample_points)
121 self._t.flags.writeable = False
122 @property
123 def t(self) -> np.ndarray[np.float64]:
124 """An array of the sample times"""
125 return self._t
126
[docs]
127def sample_from_piecewise_linear(signal: np.ndarray[complex],
128 fractional_indices: np.ndarray[float]
129 ) -> typing.Any:
130 """
131 Samples from a piecewise linear signal.
132
133 Parameters
134 ----------
135 signal : NDArray[Shape[n_points, Any_Shape], complex]
136 The piecewise linear signal to sample from
137 fractional_indices : NDArray[Shape[n_sample_points], complex]
138 Each entry will correspond to a sample. The integral part of each entry
139 corresponds to the index ``i`` in the signal and the fractional part
140 corresponds to the convex combination of the points at indices ``i`` and
141 ``i+1``.
142
143 Returns
144 -------
145 NDArray[Shape[n_sample_points, ``signal.shape[1:]``], complex]
146 The sampled signal.
147 """
148 ceil_indices = tf.cast(tf.math.ceil(fractional_indices), dtype=tf.int32)
149 floor_indices = tf.cast(tf.math.floor(fractional_indices), dtype=tf.int32)
150 sample_ceil = tf.gather(signal, ceil_indices)
151 sample_floor = tf.gather(signal, floor_indices)
152 return sample_floor \
153 + tf.einsum("i,i...->i...",
154 fractional_indices % 1,
155 sample_ceil - sample_floor)