ezmsg-sigproc 1.2.2__py3-none-any.whl → 1.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. ezmsg/sigproc/__init__.py +1 -1
  2. ezmsg/sigproc/__version__.py +16 -1
  3. ezmsg/sigproc/activation.py +75 -0
  4. ezmsg/sigproc/affinetransform.py +234 -0
  5. ezmsg/sigproc/aggregate.py +158 -0
  6. ezmsg/sigproc/bandpower.py +74 -0
  7. ezmsg/sigproc/base.py +38 -0
  8. ezmsg/sigproc/butterworthfilter.py +102 -11
  9. ezmsg/sigproc/decimate.py +7 -4
  10. ezmsg/sigproc/downsample.py +95 -51
  11. ezmsg/sigproc/ewmfilter.py +38 -16
  12. ezmsg/sigproc/filter.py +108 -20
  13. ezmsg/sigproc/filterbank.py +278 -0
  14. ezmsg/sigproc/math/__init__.py +0 -0
  15. ezmsg/sigproc/math/abs.py +28 -0
  16. ezmsg/sigproc/math/clip.py +30 -0
  17. ezmsg/sigproc/math/difference.py +60 -0
  18. ezmsg/sigproc/math/invert.py +29 -0
  19. ezmsg/sigproc/math/log.py +32 -0
  20. ezmsg/sigproc/math/scale.py +31 -0
  21. ezmsg/sigproc/messages.py +2 -3
  22. ezmsg/sigproc/sampler.py +259 -224
  23. ezmsg/sigproc/scaler.py +173 -0
  24. ezmsg/sigproc/signalinjector.py +64 -0
  25. ezmsg/sigproc/slicer.py +133 -0
  26. ezmsg/sigproc/spectral.py +6 -132
  27. ezmsg/sigproc/spectrogram.py +86 -0
  28. ezmsg/sigproc/spectrum.py +259 -0
  29. ezmsg/sigproc/synth.py +299 -105
  30. ezmsg/sigproc/wavelets.py +167 -0
  31. ezmsg/sigproc/window.py +254 -116
  32. ezmsg_sigproc-1.3.1.dist-info/METADATA +59 -0
  33. ezmsg_sigproc-1.3.1.dist-info/RECORD +35 -0
  34. {ezmsg_sigproc-1.2.2.dist-info → ezmsg_sigproc-1.3.1.dist-info}/WHEEL +1 -2
  35. ezmsg_sigproc-1.2.2.dist-info/METADATA +0 -36
  36. ezmsg_sigproc-1.2.2.dist-info/RECORD +0 -17
  37. ezmsg_sigproc-1.2.2.dist-info/top_level.txt +0 -1
  38. {ezmsg_sigproc-1.2.2.dist-info → ezmsg_sigproc-1.3.1.dist-info/licenses}/LICENSE.txt +0 -0
@@ -0,0 +1,259 @@
1
+ from dataclasses import replace
2
+ import enum
3
+ from functools import partial
4
+ import typing
5
+
6
+ import numpy as np
7
+ import ezmsg.core as ez
8
+ from ezmsg.util.messages.axisarray import AxisArray, slice_along_axis
9
+ from ezmsg.util.generator import consumer
10
+
11
+ from .base import GenAxisArray
12
+
13
+
14
+ class OptionsEnum(enum.Enum):
15
+ @classmethod
16
+ def options(cls):
17
+ return list(map(lambda c: c.value, cls))
18
+
19
+
20
+ class WindowFunction(OptionsEnum):
21
+ """Windowing function prior to calculating spectrum."""
22
+
23
+ NONE = "None (Rectangular)"
24
+ """None."""
25
+
26
+ HAMMING = "Hamming"
27
+ """:obj:`numpy.hamming`"""
28
+
29
+ HANNING = "Hanning"
30
+ """:obj:`numpy.hanning`"""
31
+
32
+ BARTLETT = "Bartlett"
33
+ """:obj:`numpy.bartlett`"""
34
+
35
+ BLACKMAN = "Blackman"
36
+ """:obj:`numpy.blackman`"""
37
+
38
+
39
+ WINDOWS = {
40
+ WindowFunction.NONE: np.ones,
41
+ WindowFunction.HAMMING: np.hamming,
42
+ WindowFunction.HANNING: np.hanning,
43
+ WindowFunction.BARTLETT: np.bartlett,
44
+ WindowFunction.BLACKMAN: np.blackman,
45
+ }
46
+
47
+
48
+ class SpectralTransform(OptionsEnum):
49
+ """Additional transformation functions to apply to the spectral result."""
50
+
51
+ RAW_COMPLEX = "Complex FFT Output"
52
+ REAL = "Real Component of FFT"
53
+ IMAG = "Imaginary Component of FFT"
54
+ REL_POWER = "Relative Power"
55
+ REL_DB = "Log Power (Relative dB)"
56
+
57
+
58
+ class SpectralOutput(OptionsEnum):
59
+ """The expected spectral contents."""
60
+
61
+ FULL = "Full Spectrum"
62
+ POSITIVE = "Positive Frequencies"
63
+ NEGATIVE = "Negative Frequencies"
64
+
65
+
66
+ @consumer
67
+ def spectrum(
68
+ axis: typing.Optional[str] = None,
69
+ out_axis: typing.Optional[str] = "freq",
70
+ window: WindowFunction = WindowFunction.HANNING,
71
+ transform: SpectralTransform = SpectralTransform.REL_DB,
72
+ output: SpectralOutput = SpectralOutput.POSITIVE,
73
+ norm: typing.Optional[str] = "forward",
74
+ do_fftshift: bool = True,
75
+ nfft: typing.Optional[int] = None,
76
+ ) -> typing.Generator[AxisArray, AxisArray, None]:
77
+ """
78
+ Calculate a spectrum on a data slice.
79
+
80
+ Args:
81
+ axis: The name of the axis on which to calculate the spectrum.
82
+ out_axis: The name of the new axis. Defaults to "freq".
83
+ window: The :obj:`WindowFunction` to apply to the data slice prior to calculating the spectrum.
84
+ transform: The :obj:`SpectralTransform` to apply to the spectral magnitude.
85
+ output: The :obj:`SpectralOutput` format.
86
+ norm: Normalization mode. Default "forward" is best used when the inverse transform is not needed,
87
+ for example when the goal is to get spectral power. Use "backward" (equivalent to None) to not
88
+ scale the spectrum which is useful when the spectra will be manipulated and possibly inverse-transformed.
89
+ See numpy.fft.fft for details.
90
+ do_fftshift: Whether to apply fftshift to the output. Default is True. This value is ignored unless
91
+ output is SpectralOutput.FULL.
92
+ nfft: The number of points to use for the FFT. If None, the length of the input data is used.
93
+
94
+ Returns:
95
+ A primed generator object that expects `.send(axis_array)` of continuous data
96
+ and yields an AxisArray of spectral magnitudes or powers.
97
+ """
98
+ msg_out = AxisArray(np.array([]), dims=[""])
99
+
100
+ # State variables
101
+ apply_window = window != WindowFunction.NONE
102
+ do_fftshift &= output == SpectralOutput.FULL
103
+ f_sl = slice(None)
104
+ freq_axis: typing.Optional[AxisArray.Axis] = None
105
+ fftfun: typing.Optional[typing.Callable] = None
106
+ f_transform: typing.Optional[typing.Callable] = None
107
+ new_dims: typing.Optional[typing.List[str]] = None
108
+
109
+ # Reset if input changes substantially
110
+ check_input = {
111
+ "n_time": None, # Need to recalc windows
112
+ "ndim": None, # Input ndim changed: Need to recalc windows
113
+ "kind": None, # Input dtype changed: Need to re-init fft funcs
114
+ "ax_idx": None, # Axis index changed: Need to re-init fft funcs
115
+ "gain": None, # Gain changed: Need to re-calc freqs
116
+ # "key": None # There's no temporal continuity; we can ignore key changes
117
+ }
118
+
119
+ while True:
120
+ msg_in: AxisArray = yield msg_out
121
+
122
+ # Get signal properties
123
+ axis = axis or msg_in.dims[0]
124
+ ax_idx = msg_in.get_axis_idx(axis)
125
+ ax_info = msg_in.axes[axis]
126
+ targ_len = msg_in.data.shape[ax_idx]
127
+
128
+ # Check signal properties for change
129
+ b_reset = targ_len != check_input["n_time"]
130
+ b_reset = b_reset or msg_in.data.ndim != check_input["ndim"]
131
+ b_reset = b_reset or msg_in.data.dtype.kind != check_input["kind"]
132
+ b_reset = b_reset or ax_idx != check_input["ax_idx"]
133
+ b_reset = b_reset or ax_info.gain != check_input["gain"]
134
+ if b_reset:
135
+ check_input["n_time"] = targ_len
136
+ check_input["ndim"] = msg_in.data.ndim
137
+ check_input["kind"] = msg_in.data.dtype.kind
138
+ check_input["ax_idx"] = ax_idx
139
+ check_input["gain"] = ax_info.gain
140
+
141
+ nfft = nfft or targ_len
142
+
143
+ # Pre-calculate windowing
144
+ window = WINDOWS[window](targ_len)
145
+ window = window.reshape(
146
+ [1] * ax_idx
147
+ + [
148
+ len(window),
149
+ ]
150
+ + [1] * (msg_in.data.ndim - 1 - ax_idx)
151
+ )
152
+ if transform != SpectralTransform.RAW_COMPLEX and not (
153
+ transform == SpectralTransform.REAL
154
+ or transform == SpectralTransform.IMAG
155
+ ):
156
+ scale = np.sum(window**2.0) * ax_info.gain
157
+
158
+ # Pre-calculate frequencies and select our fft function.
159
+ b_complex = msg_in.data.dtype.kind == "c"
160
+ if (not b_complex) and output == SpectralOutput.POSITIVE:
161
+ # If input is not complex and desired output is SpectralOutput.POSITIVE, we can save some computation
162
+ # by using rfft and rfftfreq.
163
+ fftfun = partial(np.fft.rfft, n=nfft, axis=ax_idx, norm=norm)
164
+ freqs = np.fft.rfftfreq(nfft, d=ax_info.gain * targ_len / nfft)
165
+ else:
166
+ fftfun = partial(np.fft.fft, n=nfft, axis=ax_idx, norm=norm)
167
+ freqs = np.fft.fftfreq(nfft, d=ax_info.gain * targ_len / nfft)
168
+ if output == SpectralOutput.POSITIVE:
169
+ f_sl = slice(None, nfft // 2 + 1 - (nfft % 2))
170
+ elif output == SpectralOutput.NEGATIVE:
171
+ freqs = np.fft.fftshift(freqs, axes=-1)
172
+ f_sl = slice(None, nfft // 2 + 1)
173
+ elif do_fftshift: # and FULL
174
+ freqs = np.fft.fftshift(freqs, axes=-1)
175
+ freqs = freqs[f_sl]
176
+ freqs = freqs.tolist() # To please type checking
177
+ freq_axis = AxisArray.Axis(
178
+ unit="Hz", gain=freqs[1] - freqs[0], offset=freqs[0]
179
+ )
180
+ if out_axis is None:
181
+ out_axis = axis
182
+ new_dims = (
183
+ msg_in.dims[:ax_idx]
184
+ + [
185
+ out_axis,
186
+ ]
187
+ + msg_in.dims[ax_idx + 1 :]
188
+ )
189
+
190
+ def f_transform(x):
191
+ return x
192
+
193
+ if transform != SpectralTransform.RAW_COMPLEX:
194
+ if transform == SpectralTransform.REAL:
195
+
196
+ def f_transform(x):
197
+ return x.real
198
+ elif transform == SpectralTransform.IMAG:
199
+
200
+ def f_transform(x):
201
+ return x.imag
202
+ else:
203
+
204
+ def f1(x):
205
+ return (np.abs(x) ** 2.0) / scale
206
+
207
+ if transform == SpectralTransform.REL_DB:
208
+
209
+ def f_transform(x):
210
+ return 10 * np.log10(f1(x))
211
+ else:
212
+ f_transform = f1
213
+
214
+ new_axes = {k: v for k, v in msg_in.axes.items() if k not in [out_axis, axis]}
215
+ new_axes[out_axis] = freq_axis
216
+
217
+ if apply_window:
218
+ win_dat = msg_in.data * window
219
+ else:
220
+ win_dat = msg_in.data
221
+ spec = fftfun(win_dat, n=nfft, axis=ax_idx, norm=norm)
222
+ # Note: norm="forward" equivalent to `/ nfft`
223
+ if do_fftshift or output == SpectralOutput.NEGATIVE:
224
+ spec = np.fft.fftshift(spec, axes=ax_idx)
225
+ spec = f_transform(spec)
226
+ spec = slice_along_axis(spec, f_sl, ax_idx)
227
+
228
+ msg_out = replace(msg_in, data=spec, dims=new_dims, axes=new_axes)
229
+
230
+
231
+ class SpectrumSettings(ez.Settings):
232
+ """
233
+ Settings for :obj:`Spectrum.
234
+ See :obj:`spectrum` for a description of the parameters.
235
+ """
236
+
237
+ axis: typing.Optional[str] = None
238
+ # n: typing.Optional[int] = None # n parameter for fft
239
+ out_axis: typing.Optional[str] = "freq" # If none; don't change dim name
240
+ window: WindowFunction = WindowFunction.HAMMING
241
+ transform: SpectralTransform = SpectralTransform.REL_DB
242
+ output: SpectralOutput = SpectralOutput.POSITIVE
243
+
244
+
245
+ class Spectrum(GenAxisArray):
246
+ """Unit for :obj:`spectrum`"""
247
+
248
+ SETTINGS = SpectrumSettings
249
+
250
+ INPUT_SETTINGS = ez.InputStream(SpectrumSettings)
251
+
252
+ def construct_generator(self):
253
+ self.STATE.gen = spectrum(
254
+ axis=self.SETTINGS.axis,
255
+ out_axis=self.SETTINGS.out_axis,
256
+ window=self.SETTINGS.window,
257
+ transform=self.SETTINGS.transform,
258
+ output=self.SETTINGS.output,
259
+ )