ezmsg-sigproc 1.8.2__py3-none-any.whl → 2.1.0__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.
- ezmsg/sigproc/__version__.py +2 -2
- ezmsg/sigproc/activation.py +36 -39
- ezmsg/sigproc/adaptive_lattice_notch.py +231 -0
- ezmsg/sigproc/affinetransform.py +169 -163
- ezmsg/sigproc/aggregate.py +133 -101
- ezmsg/sigproc/bandpower.py +64 -52
- ezmsg/sigproc/base.py +1242 -0
- ezmsg/sigproc/butterworthfilter.py +37 -33
- ezmsg/sigproc/cheby.py +29 -17
- ezmsg/sigproc/combfilter.py +163 -0
- ezmsg/sigproc/decimate.py +19 -10
- ezmsg/sigproc/detrend.py +29 -0
- ezmsg/sigproc/diff.py +81 -0
- ezmsg/sigproc/downsample.py +78 -84
- ezmsg/sigproc/ewma.py +197 -0
- ezmsg/sigproc/extract_axis.py +41 -0
- ezmsg/sigproc/filter.py +257 -141
- ezmsg/sigproc/filterbank.py +247 -199
- ezmsg/sigproc/math/abs.py +17 -22
- ezmsg/sigproc/math/clip.py +24 -24
- ezmsg/sigproc/math/difference.py +34 -30
- ezmsg/sigproc/math/invert.py +13 -25
- ezmsg/sigproc/math/log.py +28 -33
- ezmsg/sigproc/math/scale.py +18 -26
- ezmsg/sigproc/quantize.py +71 -0
- ezmsg/sigproc/resample.py +298 -0
- ezmsg/sigproc/sampler.py +241 -259
- ezmsg/sigproc/scaler.py +55 -218
- ezmsg/sigproc/signalinjector.py +52 -43
- ezmsg/sigproc/slicer.py +81 -89
- ezmsg/sigproc/spectrogram.py +77 -75
- ezmsg/sigproc/spectrum.py +203 -168
- ezmsg/sigproc/synth.py +546 -393
- ezmsg/sigproc/transpose.py +131 -0
- ezmsg/sigproc/util/asio.py +156 -0
- ezmsg/sigproc/util/message.py +31 -0
- ezmsg/sigproc/util/profile.py +55 -12
- ezmsg/sigproc/util/typeresolution.py +83 -0
- ezmsg/sigproc/wavelets.py +154 -153
- ezmsg/sigproc/window.py +269 -211
- {ezmsg_sigproc-1.8.2.dist-info → ezmsg_sigproc-2.1.0.dist-info}/METADATA +2 -1
- ezmsg_sigproc-2.1.0.dist-info/RECORD +51 -0
- ezmsg_sigproc-1.8.2.dist-info/RECORD +0 -39
- {ezmsg_sigproc-1.8.2.dist-info → ezmsg_sigproc-2.1.0.dist-info}/WHEEL +0 -0
- {ezmsg_sigproc-1.8.2.dist-info → ezmsg_sigproc-2.1.0.dist-info}/licenses/LICENSE.txt +0 -0
ezmsg/sigproc/affinetransform.py
CHANGED
|
@@ -1,112 +1,112 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
import typing
|
|
4
3
|
|
|
5
4
|
import numpy as np
|
|
6
5
|
import numpy.typing as npt
|
|
7
6
|
import ezmsg.core as ez
|
|
8
7
|
from ezmsg.util.messages.axisarray import AxisArray, AxisBase
|
|
9
8
|
from ezmsg.util.messages.util import replace
|
|
10
|
-
from ezmsg.util.generator import consumer
|
|
11
9
|
|
|
12
|
-
from .base import
|
|
10
|
+
from .base import (
|
|
11
|
+
BaseStatefulTransformer,
|
|
12
|
+
BaseTransformerUnit,
|
|
13
|
+
BaseTransformer,
|
|
14
|
+
processor_state,
|
|
15
|
+
)
|
|
13
16
|
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
right_multiply: bool = True,
|
|
20
|
-
) -> typing.Generator[AxisArray, AxisArray, None]:
|
|
18
|
+
class AffineTransformSettings(ez.Settings):
|
|
19
|
+
"""
|
|
20
|
+
Settings for :obj:`AffineTransform`.
|
|
21
|
+
See :obj:`affine_transform` for argument details.
|
|
21
22
|
"""
|
|
22
|
-
Perform affine transformations on streaming data.
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
axis: The name of the axis to apply the transformation to. Defaults to the leading (0th) axis in the array.
|
|
27
|
-
right_multiply: Set False to transpose the weights before applying.
|
|
24
|
+
weights: np.ndarray | str | Path
|
|
25
|
+
"""An array of weights or a path to a file with weights compatible with np.loadtxt."""
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
:obj:`AxisArray` it receives via `send`.
|
|
32
|
-
"""
|
|
33
|
-
msg_out = AxisArray(np.array([]), dims=[""])
|
|
27
|
+
axis: str | None = None
|
|
28
|
+
"""The name of the axis to apply the transformation to. Defaults to the leading (0th) axis in the array."""
|
|
34
29
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
weights = np.loadtxt(weights, delimiter=",")
|
|
43
|
-
if not right_multiply:
|
|
44
|
-
weights = weights.T
|
|
45
|
-
if weights is not None:
|
|
46
|
-
weights = np.ascontiguousarray(weights)
|
|
47
|
-
|
|
48
|
-
# State variables
|
|
49
|
-
# New axis with transformed labels, if required
|
|
30
|
+
right_multiply: bool = True
|
|
31
|
+
"""Set False to transpose the weights before applying."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@processor_state
|
|
35
|
+
class AffineTransformState:
|
|
36
|
+
weights: npt.NDArray | None = None
|
|
50
37
|
new_axis: AxisBase | None = None
|
|
51
38
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if weights is None
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
39
|
+
|
|
40
|
+
class AffineTransformTransformer(
|
|
41
|
+
BaseStatefulTransformer[
|
|
42
|
+
AffineTransformSettings, AxisArray, AxisArray, AffineTransformState
|
|
43
|
+
]
|
|
44
|
+
):
|
|
45
|
+
def __call__(self, message: AxisArray) -> AxisArray:
|
|
46
|
+
# Override __call__ so we can shortcut if weights are None.
|
|
47
|
+
if self.settings.weights is None or (
|
|
48
|
+
isinstance(self.settings.weights, str)
|
|
49
|
+
and self.settings.weights == "passthrough"
|
|
50
|
+
):
|
|
51
|
+
return message
|
|
52
|
+
return super().__call__(message)
|
|
53
|
+
|
|
54
|
+
def _hash_message(self, message: AxisArray) -> int:
|
|
55
|
+
return hash(message.key)
|
|
56
|
+
|
|
57
|
+
def _reset_state(self, message: AxisArray) -> None:
|
|
58
|
+
weights = self.settings.weights
|
|
59
|
+
if isinstance(weights, str):
|
|
60
|
+
weights = Path(os.path.abspath(os.path.expanduser(weights)))
|
|
61
|
+
if isinstance(weights, Path):
|
|
62
|
+
weights = np.loadtxt(weights, delimiter=",")
|
|
63
|
+
if not self.settings.right_multiply:
|
|
64
|
+
weights = weights.T
|
|
65
|
+
if weights is not None:
|
|
66
|
+
weights = np.ascontiguousarray(weights)
|
|
67
|
+
|
|
68
|
+
self._state.weights = weights
|
|
69
|
+
|
|
70
|
+
axis = self.settings.axis or message.dims[-1]
|
|
71
|
+
if (
|
|
72
|
+
axis in message.axes
|
|
73
|
+
and hasattr(message.axes[axis], "data")
|
|
74
|
+
and weights.shape[0] != weights.shape[1]
|
|
75
|
+
):
|
|
76
|
+
in_labels = message.axes[axis].data
|
|
77
|
+
new_labels = []
|
|
78
|
+
n_in, n_out = weights.shape
|
|
79
|
+
if len(in_labels) != n_in:
|
|
80
|
+
ez.logger.warning(
|
|
81
|
+
f"Received {len(in_labels)} for {n_in} inputs. Check upstream labels."
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
b_filled_outputs = np.any(weights, axis=0)
|
|
85
|
+
b_used_inputs = np.any(weights, axis=1)
|
|
86
|
+
if np.all(b_used_inputs) and np.all(b_filled_outputs):
|
|
87
|
+
new_labels = []
|
|
88
|
+
elif np.all(b_used_inputs):
|
|
89
|
+
in_ix = 0
|
|
90
|
+
new_labels = []
|
|
91
|
+
for out_ix in range(n_out):
|
|
92
|
+
if b_filled_outputs[out_ix]:
|
|
93
|
+
new_labels.append(in_labels[in_ix])
|
|
94
|
+
in_ix += 1
|
|
95
|
+
else:
|
|
96
|
+
new_labels.append("")
|
|
97
|
+
elif np.all(b_filled_outputs):
|
|
98
|
+
new_labels = np.array(in_labels)[b_used_inputs]
|
|
99
|
+
|
|
100
|
+
self._state.new_axis = replace(
|
|
101
|
+
message.axes[axis], data=np.array(new_labels)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def _process(self, message: AxisArray) -> AxisArray:
|
|
105
|
+
axis = self.settings.axis or message.dims[-1]
|
|
106
|
+
axis_idx = message.get_axis_idx(axis)
|
|
107
|
+
data = message.data
|
|
108
|
+
|
|
109
|
+
if data.shape[axis_idx] == (self._state.weights.shape[0] - 1):
|
|
110
110
|
# The weights are stacked A|B where A is the transform and B is a single row
|
|
111
111
|
# in the equation y = Ax + B. This supports NeuroKey's weights matrices.
|
|
112
112
|
sample_shape = data.shape[:axis_idx] + (1,) + data.shape[axis_idx + 1 :]
|
|
@@ -114,82 +114,87 @@ def affine_transform(
|
|
|
114
114
|
(data, np.ones(sample_shape).astype(data.dtype)), axis=axis_idx
|
|
115
115
|
)
|
|
116
116
|
|
|
117
|
-
if axis_idx in [-1, len(
|
|
118
|
-
data = np.matmul(data, weights)
|
|
117
|
+
if axis_idx in [-1, len(message.dims) - 1]:
|
|
118
|
+
data = np.matmul(data, self._state.weights)
|
|
119
119
|
else:
|
|
120
120
|
data = np.moveaxis(data, axis_idx, -1)
|
|
121
|
-
data = np.matmul(data, weights)
|
|
121
|
+
data = np.matmul(data, self._state.weights)
|
|
122
122
|
data = np.moveaxis(data, -1, axis_idx)
|
|
123
123
|
|
|
124
124
|
replace_kwargs = {"data": data}
|
|
125
|
-
if new_axis is not None:
|
|
126
|
-
replace_kwargs["axes"] = {**
|
|
127
|
-
msg_out = replace(msg_in, **replace_kwargs)
|
|
125
|
+
if self._state.new_axis is not None:
|
|
126
|
+
replace_kwargs["axes"] = {**message.axes, axis: self._state.new_axis}
|
|
128
127
|
|
|
128
|
+
return replace(message, **replace_kwargs)
|
|
129
129
|
|
|
130
|
-
class AffineTransformSettings(ez.Settings):
|
|
131
|
-
"""
|
|
132
|
-
Settings for :obj:`AffineTransform`.
|
|
133
|
-
See :obj:`affine_transform` for argument details.
|
|
134
|
-
"""
|
|
135
130
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
class AffineTransform(
|
|
132
|
+
BaseTransformerUnit[
|
|
133
|
+
AffineTransformSettings, AxisArray, AxisArray, AffineTransformTransformer
|
|
134
|
+
]
|
|
135
|
+
):
|
|
136
|
+
SETTINGS = AffineTransformSettings
|
|
139
137
|
|
|
140
138
|
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
def affine_transform(
|
|
140
|
+
weights: np.ndarray | str | Path,
|
|
141
|
+
axis: str | None = None,
|
|
142
|
+
right_multiply: bool = True,
|
|
143
|
+
) -> AffineTransformTransformer:
|
|
144
|
+
"""
|
|
145
|
+
Perform affine transformations on streaming data.
|
|
143
146
|
|
|
144
|
-
|
|
147
|
+
Args:
|
|
148
|
+
weights: An array of weights or a path to a file with weights compatible with np.loadtxt.
|
|
149
|
+
axis: The name of the axis to apply the transformation to. Defaults to the leading (0th) axis in the array.
|
|
150
|
+
right_multiply: Set False to transpose the weights before applying.
|
|
145
151
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
152
|
+
Returns:
|
|
153
|
+
:obj:`AffineTransformTransformer`.
|
|
154
|
+
"""
|
|
155
|
+
return AffineTransformTransformer(
|
|
156
|
+
AffineTransformSettings(
|
|
157
|
+
weights=weights, axis=axis, right_multiply=right_multiply
|
|
151
158
|
)
|
|
159
|
+
)
|
|
152
160
|
|
|
153
161
|
|
|
154
162
|
def zeros_for_noop(data: npt.NDArray, **ignore_kwargs) -> npt.NDArray:
|
|
155
163
|
return np.zeros_like(data)
|
|
156
164
|
|
|
157
165
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
) -> typing.Generator[AxisArray, AxisArray, None]:
|
|
166
|
+
class CommonRereferenceSettings(ez.Settings):
|
|
167
|
+
"""
|
|
168
|
+
Settings for :obj:`CommonRereference`
|
|
162
169
|
"""
|
|
163
|
-
Perform common average referencing (CAR) on streaming data.
|
|
164
170
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
axis: The name of hte axis to apply the transformation to.
|
|
168
|
-
include_current: Set False to exclude each channel from participating in the calculation of its reference.
|
|
171
|
+
mode: str = "mean"
|
|
172
|
+
"""The statistical mode to apply -- either "mean" or "median"."""
|
|
169
173
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
for every :obj:`AxisArray` it receives via `send`.
|
|
173
|
-
"""
|
|
174
|
-
msg_out = AxisArray(np.array([]), dims=[""])
|
|
174
|
+
axis: str | None = None
|
|
175
|
+
"""The name of the axis to apply the transformation to."""
|
|
175
176
|
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
include_current: bool = True
|
|
178
|
+
"""Set False to exclude each channel from participating in the calculation of its reference."""
|
|
178
179
|
|
|
179
|
-
func = {"mean": np.mean, "median": np.median, "passthrough": zeros_for_noop}[mode]
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
class CommonRereferenceTransformer(
|
|
182
|
+
BaseTransformer[CommonRereferenceSettings, AxisArray, AxisArray]
|
|
183
|
+
):
|
|
184
|
+
def _process(self, message: AxisArray) -> AxisArray:
|
|
185
|
+
if self.settings.mode == "passthrough":
|
|
186
|
+
return message
|
|
183
187
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
axis = self.settings.axis or message.dims[-1]
|
|
189
|
+
axis_idx = message.get_axis_idx(axis)
|
|
190
|
+
|
|
191
|
+
func = {"mean": np.mean, "median": np.median, "passthrough": zeros_for_noop}[
|
|
192
|
+
self.settings.mode
|
|
193
|
+
]
|
|
189
194
|
|
|
190
|
-
ref_data = func(
|
|
195
|
+
ref_data = func(message.data, axis=axis_idx, keepdims=True)
|
|
191
196
|
|
|
192
|
-
if not include_current:
|
|
197
|
+
if not self.settings.include_current:
|
|
193
198
|
# Typical `CAR = x[0]/N + x[1]/N + ... x[i-1]/N + x[i]/N + x[i+1]/N + ... + x[N-1]/N`
|
|
194
199
|
# and is the same for all i, so it is calculated only once in `ref_data`.
|
|
195
200
|
# However, if we had excluded the current channel,
|
|
@@ -200,34 +205,35 @@ def common_rereference(
|
|
|
200
205
|
# from the current channel (i.e., `x[i] / (N-1)`)
|
|
201
206
|
# i.e., `CAR[i] = (N / (N-1)) * common_CAR - x[i]/(N-1)`
|
|
202
207
|
# We can use broadcasting subtraction instead of looping over channels.
|
|
203
|
-
N =
|
|
204
|
-
ref_data = (N / (N - 1)) * ref_data -
|
|
205
|
-
#
|
|
208
|
+
N = message.data.shape[axis_idx]
|
|
209
|
+
ref_data = (N / (N - 1)) * ref_data - message.data / (N - 1)
|
|
210
|
+
# Note: I profiled using AffineTransformTransformer; it's ~30x slower than this implementation.
|
|
206
211
|
|
|
207
|
-
|
|
212
|
+
return replace(message, data=message.data - ref_data)
|
|
208
213
|
|
|
209
214
|
|
|
210
|
-
class
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
mode: str = "mean"
|
|
217
|
-
axis: str | None = None
|
|
218
|
-
include_current: bool = True
|
|
215
|
+
class CommonRereference(
|
|
216
|
+
BaseTransformerUnit[
|
|
217
|
+
CommonRereferenceSettings, AxisArray, AxisArray, CommonRereferenceTransformer
|
|
218
|
+
]
|
|
219
|
+
):
|
|
220
|
+
SETTINGS = CommonRereferenceSettings
|
|
219
221
|
|
|
220
222
|
|
|
221
|
-
|
|
222
|
-
""
|
|
223
|
-
|
|
223
|
+
def common_rereference(
|
|
224
|
+
mode: str = "mean", axis: str | None = None, include_current: bool = True
|
|
225
|
+
) -> CommonRereferenceTransformer:
|
|
224
226
|
"""
|
|
227
|
+
Perform common average referencing (CAR) on streaming data.
|
|
225
228
|
|
|
226
|
-
|
|
229
|
+
Args:
|
|
230
|
+
mode: The statistical mode to apply -- either "mean" or "median"
|
|
231
|
+
axis: The name of hte axis to apply the transformation to.
|
|
232
|
+
include_current: Set False to exclude each channel from participating in the calculation of its reference.
|
|
227
233
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
+
Returns:
|
|
235
|
+
:obj:`CommonRereferenceTransformer`
|
|
236
|
+
"""
|
|
237
|
+
return CommonRereferenceTransformer(
|
|
238
|
+
CommonRereferenceSettings(mode=mode, axis=axis, include_current=include_current)
|
|
239
|
+
)
|