ezmsg-sigproc 1.5.0__py3-none-any.whl → 1.7.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.
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.5.0'
16
- __version_tuple__ = version_tuple = (1, 5, 0)
15
+ __version__ = version = '1.7.0'
16
+ __version_tuple__ = version_tuple = (1, 7, 0)
@@ -3,7 +3,8 @@ import typing
3
3
  import numpy as np
4
4
  import scipy.special
5
5
  import ezmsg.core as ez
6
- from ezmsg.util.messages.axisarray import AxisArray, replace
6
+ from ezmsg.util.messages.axisarray import AxisArray
7
+ from ezmsg.util.messages.util import replace
7
8
  from ezmsg.util.generator import consumer
8
9
 
9
10
  from .spectral import OptionsEnum
@@ -40,7 +41,7 @@ ACTIVATIONS = {
40
41
 
41
42
  @consumer
42
43
  def activation(
43
- function: typing.Union[str, ActivationFunction],
44
+ function: str | ActivationFunction,
44
45
  ) -> typing.Generator[AxisArray, AxisArray, None]:
45
46
  """
46
47
  Transform the data with a simple activation function.
@@ -5,7 +5,8 @@ import typing
5
5
  import numpy as np
6
6
  import numpy.typing as npt
7
7
  import ezmsg.core as ez
8
- from ezmsg.util.messages.axisarray import AxisArray, AxisBase, replace
8
+ from ezmsg.util.messages.axisarray import AxisArray, AxisBase
9
+ from ezmsg.util.messages.util import replace
9
10
  from ezmsg.util.generator import consumer
10
11
 
11
12
  from .base import GenAxisArray
@@ -13,8 +14,8 @@ from .base import GenAxisArray
13
14
 
14
15
  @consumer
15
16
  def affine_transform(
16
- weights: typing.Union[np.ndarray, str, Path],
17
- axis: typing.Optional[str] = None,
17
+ weights: np.ndarray | str | Path,
18
+ axis: str | None = None,
18
19
  right_multiply: bool = True,
19
20
  ) -> typing.Generator[AxisArray, AxisArray, None]:
20
21
  """
@@ -46,7 +47,7 @@ def affine_transform(
46
47
 
47
48
  # State variables
48
49
  # New axis with transformed labels, if required
49
- new_axis: typing.Optional[AxisBase] = None
50
+ new_axis: AxisBase | None = None
50
51
 
51
52
  # Reset if any of these change.
52
53
  check_input = {"key": None}
@@ -132,8 +133,8 @@ class AffineTransformSettings(ez.Settings):
132
133
  See :obj:`affine_transform` for argument details.
133
134
  """
134
135
 
135
- weights: typing.Union[np.ndarray, str, Path]
136
- axis: typing.Optional[str] = None
136
+ weights: np.ndarray | str | Path
137
+ axis: str | None = None
137
138
  right_multiply: bool = True
138
139
 
139
140
 
@@ -156,7 +157,7 @@ def zeros_for_noop(data: npt.NDArray, **ignore_kwargs) -> npt.NDArray:
156
157
 
157
158
  @consumer
158
159
  def common_rereference(
159
- mode: str = "mean", axis: typing.Optional[str] = None, include_current: bool = True
160
+ mode: str = "mean", axis: str | None = None, include_current: bool = True
160
161
  ) -> typing.Generator[AxisArray, AxisArray, None]:
161
162
  """
162
163
  Perform common average referencing (CAR) on streaming data.
@@ -213,7 +214,7 @@ class CommonRereferenceSettings(ez.Settings):
213
214
  """
214
215
 
215
216
  mode: str = "mean"
216
- axis: typing.Optional[str] = None
217
+ axis: str | None = None
217
218
  include_current: bool = True
218
219
 
219
220
 
@@ -56,8 +56,8 @@ AGGREGATORS = {
56
56
 
57
57
  @consumer
58
58
  def ranged_aggregate(
59
- axis: typing.Optional[str] = None,
60
- bands: typing.Optional[typing.List[typing.Tuple[float, float]]] = None,
59
+ axis: str | None = None,
60
+ bands: list[tuple[float, float]] | None = None,
61
61
  operation: AggregationFunction = AggregationFunction.MEAN,
62
62
  ):
63
63
  """
@@ -75,9 +75,9 @@ def ranged_aggregate(
75
75
  msg_out = AxisArray(np.array([]), dims=[""])
76
76
 
77
77
  # State variables
78
- slices: typing.Optional[typing.List[typing.Tuple[typing.Any, ...]]] = None
79
- out_axis: typing.Optional[AxisBase] = None
80
- ax_vec: typing.Optional[npt.NDArray] = None
78
+ slices: list[tuple[typing.Any, ...]] | None = None
79
+ out_axis: AxisBase | None = None
80
+ ax_vec: npt.NDArray | None = None
81
81
 
82
82
  # Reset if any of these changes. Key not checked because continuity between chunks not required.
83
83
  check_inputs = {"gain": None, "offset": None, "len": None, "key": None}
@@ -163,8 +163,8 @@ class RangedAggregateSettings(ez.Settings):
163
163
  See :obj:`ranged_aggregate` for details.
164
164
  """
165
165
 
166
- axis: typing.Optional[str] = None
167
- bands: typing.Optional[typing.List[typing.Tuple[float, float]]] = None
166
+ axis: str | None = None
167
+ bands: list[tuple[float, float]] | None = None
168
168
  operation: AggregationFunction = AggregationFunction.MEAN
169
169
 
170
170
 
@@ -14,7 +14,7 @@ from .base import GenAxisArray
14
14
  @consumer
15
15
  def bandpower(
16
16
  spectrogram_settings: SpectrogramSettings,
17
- bands: typing.Optional[typing.List[typing.Tuple[float, float]]] = [
17
+ bands: list[tuple[float, float]] | None = [
18
18
  (17, 30),
19
19
  (70, 170),
20
20
  ],
@@ -58,7 +58,7 @@ class BandPowerSettings(ez.Settings):
58
58
  spectrogram_settings: SpectrogramSettings = field(
59
59
  default_factory=SpectrogramSettings
60
60
  )
61
- bands: typing.Optional[typing.List[typing.Tuple[float, float]]] = field(
61
+ bands: list[tuple[float, float]] | None = field(
62
62
  default_factory=lambda: [(17, 30), (70, 170)]
63
63
  )
64
64
 
@@ -1,15 +1,19 @@
1
+ import functools
1
2
  import typing
2
3
 
3
- import ezmsg.core as ez
4
4
  import scipy.signal
5
- import numpy as np
6
5
  from ezmsg.util.messages.axisarray import AxisArray
7
- from ezmsg.util.generator import consumer
6
+ from scipy.signal import normalize
8
7
 
9
- from .filter import filtergen, Filter, FilterState, FilterSettingsBase
8
+ from .filter import (
9
+ FilterBaseSettings,
10
+ FilterCoefsMultiType,
11
+ FilterBase,
12
+ filter_gen_by_design,
13
+ )
10
14
 
11
15
 
12
- class ButterworthFilterSettings(FilterSettingsBase):
16
+ class ButterworthFilterSettings(FilterBaseSettings):
13
17
  """Settings for :obj:`ButterworthFilter`."""
14
18
 
15
19
  order: int = 0
@@ -17,25 +21,28 @@ class ButterworthFilterSettings(FilterSettingsBase):
17
21
  Filter order
18
22
  """
19
23
 
20
- cuton: typing.Optional[float] = None
24
+ cuton: float | None = None
21
25
  """
22
26
  Cuton frequency (Hz). If `cutoff` is not specified then this is the highpass corner. Otherwise,
23
27
  if this is lower than `cutoff` then this is the beginning of the bandpass
24
28
  or if this is greater than `cutoff` then this is the end of the bandstop.
25
29
  """
26
30
 
27
- cutoff: typing.Optional[float] = None
31
+ cutoff: float | None = None
28
32
  """
29
33
  Cutoff frequency (Hz). If `cuton` is not specified then this is the lowpass corner. Otherwise,
30
34
  if this is greater than `cuton` then this is the end of the bandpass,
31
35
  or if this is less than `cuton` then this is the beginning of the bandstop.
32
36
  """
33
37
 
38
+ wn_hz: bool = True
39
+ """
40
+ Set False if provided Wn are normalized from 0 to 1, where 1 is the Nyquist frequency
41
+ """
42
+
34
43
  def filter_specs(
35
44
  self,
36
- ) -> typing.Optional[
37
- typing.Tuple[str, typing.Union[float, typing.Tuple[float, float]]]
38
- ]:
45
+ ) -> tuple[str, float | tuple[float, float]] | None:
39
46
  """
40
47
  Determine the filter type given the corner frequencies.
41
48
 
@@ -58,15 +65,74 @@ class ButterworthFilterSettings(FilterSettingsBase):
58
65
  return "bandstop", (self.cutoff, self.cuton)
59
66
 
60
67
 
61
- @consumer
68
+ def butter_design_fun(
69
+ fs: float,
70
+ order: int = 0,
71
+ cuton: float | None = None,
72
+ cutoff: float | None = None,
73
+ coef_type: str = "ba",
74
+ wn_hz: bool = True,
75
+ ) -> FilterCoefsMultiType | None:
76
+ """
77
+ See :obj:`ButterworthFilterSettings.filter_specs` for an explanation of specifying different
78
+ filter types (lowpass, highpass, bandpass, bandstop) from the parameters.
79
+ You are likely to want to use this function with :obj:`filter_by_design`, which only passes `fs` to the design
80
+ function (this), meaning that you should wrap this function with a lambda or prepare with functools.partial.
81
+
82
+ Args:
83
+ fs: The sampling frequency of the data in Hz.
84
+ order: Filter order.
85
+ cuton: Corner frequency of the filter in Hz.
86
+ cutoff: Corner frequency of the filter in Hz.
87
+ coef_type: "ba", "sos", or "zpk"
88
+ wn_hz: Set False if provided Wn are normalized from 0 to 1, where 1 is the Nyquist frequency
89
+
90
+ Returns:
91
+ The filter coefficients as a tuple of (b, a) for coef_type "ba", or as a single ndarray for "sos",
92
+ or (z, p, k) for "zpk".
93
+
94
+ """
95
+ coefs = None
96
+ if order > 0:
97
+ btype, cutoffs = ButterworthFilterSettings(
98
+ order=order, cuton=cuton, cutoff=cutoff
99
+ ).filter_specs()
100
+ coefs = scipy.signal.butter(
101
+ order,
102
+ Wn=cutoffs,
103
+ btype=btype,
104
+ fs=fs if wn_hz else None,
105
+ output=coef_type,
106
+ )
107
+ if coefs is not None and coef_type == "ba":
108
+ coefs = normalize(*coefs)
109
+ return coefs
110
+
111
+
112
+ class ButterworthFilter(FilterBase):
113
+ SETTINGS = ButterworthFilterSettings
114
+
115
+ def design_filter(
116
+ self,
117
+ ) -> typing.Callable[[float], FilterCoefsMultiType | None]:
118
+ return functools.partial(
119
+ butter_design_fun,
120
+ order=self.SETTINGS.order,
121
+ cuton=self.SETTINGS.cuton,
122
+ cutoff=self.SETTINGS.cutoff,
123
+ coef_type=self.SETTINGS.coef_type,
124
+ )
125
+
126
+
62
127
  def butter(
63
- axis: typing.Optional[str],
128
+ axis: str | None,
64
129
  order: int = 0,
65
- cuton: typing.Optional[float] = None,
66
- cutoff: typing.Optional[float] = None,
130
+ cuton: float | None = None,
131
+ cutoff: float | None = None,
67
132
  coef_type: str = "ba",
68
133
  ) -> typing.Generator[AxisArray, AxisArray, None]:
69
134
  """
135
+ Convenience generator wrapping filter_gen_by_design for Butterworth filters.
70
136
  Apply Butterworth filter to streaming data. Uses :obj:`scipy.signal.butter` to design the filter.
71
137
  See :obj:`ButterworthFilterSettings.filter_specs` for an explanation of specifying different
72
138
  filter types (lowpass, highpass, bandpass, bandstop) from the parameters.
@@ -84,79 +150,11 @@ def butter(
84
150
  and yields an :obj:`AxisArray` with filtered data.
85
151
 
86
152
  """
87
- # IO
88
- msg_out = AxisArray(np.array([]), dims=[""])
89
-
90
- # Check parameters
91
- btype, cutoffs = ButterworthFilterSettings(
92
- order=order, cuton=cuton, cutoff=cutoff
93
- ).filter_specs()
94
-
95
- # State variables
96
- # Initialize filtergen as passthrough until we can calculate coefs.
97
- filter_gen = filtergen(axis, None, coef_type)
98
-
99
- # Reset if these change.
100
- check_input = {"gain": None}
101
- # Key not checked because filter_gen will handle resetting if .key changes.
102
-
103
- while True:
104
- msg_in: AxisArray = yield msg_out
105
- axis = axis or msg_in.dims[0]
106
-
107
- b_reset = msg_in.axes[axis].gain != check_input["gain"]
108
- b_reset = b_reset and order > 0 # Not passthrough
109
- if b_reset:
110
- check_input["gain"] = msg_in.axes[axis].gain
111
- coefs = scipy.signal.butter(
112
- order,
113
- Wn=cutoffs,
114
- btype=btype,
115
- fs=1 / msg_in.axes[axis].gain,
116
- output=coef_type,
117
- )
118
- filter_gen = filtergen(axis, coefs, coef_type)
119
-
120
- msg_out = filter_gen.send(msg_in)
121
-
122
-
123
- class ButterworthFilterState(FilterState):
124
- design: ButterworthFilterSettings
125
-
126
-
127
- class ButterworthFilter(Filter):
128
- """:obj:`Unit` for :obj:`butterworth`"""
129
-
130
- SETTINGS = ButterworthFilterSettings
131
- STATE = ButterworthFilterState
132
-
133
- INPUT_FILTER = ez.InputStream(ButterworthFilterSettings)
134
-
135
- async def initialize(self) -> None:
136
- self.STATE.design = self.SETTINGS
137
- self.STATE.filt_designed = True
138
- await super().initialize()
139
-
140
- def design_filter(self) -> typing.Optional[typing.Tuple[np.ndarray, np.ndarray]]:
141
- specs = self.STATE.design.filter_specs()
142
- if self.STATE.design.order > 0 and specs is not None:
143
- btype, cut = specs
144
- return scipy.signal.butter(
145
- self.STATE.design.order,
146
- Wn=cut,
147
- btype=btype,
148
- fs=self.STATE.fs,
149
- output="ba",
150
- )
151
- else:
152
- return None
153
-
154
- @ez.subscriber(INPUT_FILTER)
155
- async def redesign(self, message: ButterworthFilterSettings) -> None:
156
- if type(message) is not ButterworthFilterSettings:
157
- return
158
-
159
- if self.STATE.design.order != message.order:
160
- self.STATE.zi = None
161
- self.STATE.design = message
162
- self.update_filter()
153
+ design_fun = functools.partial(
154
+ butter_design_fun,
155
+ order=order,
156
+ cuton=cuton,
157
+ cutoff=cutoff,
158
+ coef_type=coef_type,
159
+ )
160
+ return filter_gen_by_design(axis, coef_type, design_fun)
ezmsg/sigproc/cheby.py ADDED
@@ -0,0 +1,119 @@
1
+ import functools
2
+ import typing
3
+
4
+ import scipy.signal
5
+ from scipy.signal import normalize
6
+
7
+ from .filter import (
8
+ FilterBaseSettings,
9
+ FilterCoefsMultiType,
10
+ FilterBase,
11
+ )
12
+
13
+
14
+ class ChebyshevFilterSettings(FilterBaseSettings):
15
+ """Settings for :obj:`ButterworthFilter`."""
16
+
17
+ order: int = 0
18
+ """
19
+ Filter order
20
+ """
21
+
22
+ ripple_tol: float | None = None
23
+ """
24
+ The maximum ripple allowed below unity gain in the passband. Specified in decibels, as a positive number.
25
+ """
26
+
27
+ Wn: float | tuple[float, float] | None = None
28
+ """
29
+ A scalar or length-2 sequence giving the critical frequencies.
30
+ For Type I filters, this is the point in the transition band at which the gain first drops below -rp.
31
+ For digital filters, Wn are in the same units as fs unless wn_hz is False.
32
+ For analog filters, Wn is an angular frequency (e.g., rad/s).
33
+ """
34
+
35
+ btype: str = "lowpass"
36
+ """
37
+ {‘lowpass’, ‘highpass’, ‘bandpass’, ‘bandstop’}
38
+ """
39
+
40
+ analog: bool = False
41
+ """
42
+ When True, return an analog filter, otherwise a digital filter is returned.
43
+ """
44
+
45
+ cheby_type: str = "cheby1"
46
+ """
47
+ Which type of Chebyshev filter to design. Either "cheby1" or "cheby2".
48
+ """
49
+
50
+ wn_hz: bool = True
51
+ """
52
+ Set False if provided Wn are normalized from 0 to 1, where 1 is the Nyquist frequency
53
+ """
54
+
55
+
56
+ def cheby_design_fun(
57
+ fs: float,
58
+ order: int = 0,
59
+ ripple_tol: float | None = None,
60
+ Wn: float | tuple[float, float] | None = None,
61
+ btype: str = "lowpass",
62
+ analog: bool = False,
63
+ coef_type: str = "ba",
64
+ cheby_type: str = "cheby1",
65
+ wn_hz: bool = True,
66
+ ) -> FilterCoefsMultiType:
67
+ """
68
+ Chebyshev type I and type II digital and analog filter design.
69
+ Design an `order`th-order digital or analog Chebyshev type I or type II filter and return the filter coefficients.
70
+ See :obj:`ChebyFilterSettings` for argument description.
71
+
72
+ Returns:
73
+ The filter coefficients as a tuple of (b, a) for coef_type "ba", or as a single ndarray for "sos",
74
+ or (z, p, k) for "zpk".
75
+ """
76
+ coefs = None
77
+ if order > 0:
78
+ if cheby_type == "cheby1":
79
+ coefs = scipy.signal.cheby1(
80
+ order,
81
+ ripple_tol,
82
+ Wn,
83
+ btype=btype,
84
+ analog=analog,
85
+ output=coef_type,
86
+ fs=fs if wn_hz else None,
87
+ )
88
+ elif cheby_type == "cheby2":
89
+ coefs = scipy.signal.cheby2(
90
+ order,
91
+ ripple_tol,
92
+ Wn,
93
+ btype=btype,
94
+ analog=analog,
95
+ output=coef_type,
96
+ fs=fs,
97
+ )
98
+ if coefs is not None and coef_type == "ba":
99
+ coefs = normalize(*coefs)
100
+ return coefs
101
+
102
+
103
+ class ChebyshevFilter(FilterBase):
104
+ SETTINGS = ChebyshevFilterSettings
105
+
106
+ def design_filter(
107
+ self,
108
+ ) -> typing.Callable[[float], FilterCoefsMultiType | None]:
109
+ return functools.partial(
110
+ cheby_design_fun,
111
+ order=self.SETTINGS.order,
112
+ ripple_tol=self.SETTINGS.ripple_tol,
113
+ Wn=self.SETTINGS.Wn,
114
+ btype=self.SETTINGS.btype,
115
+ analog=self.SETTINGS.analog,
116
+ coef_type=self.SETTINGS.coef_type,
117
+ cheby_type=self.SETTINGS.cheby_type,
118
+ wn_hz=self.SETTINGS.wn_hz,
119
+ )
ezmsg/sigproc/decimate.py CHANGED
@@ -1,9 +1,31 @@
1
- import scipy.signal
1
+ import typing
2
+
2
3
  import ezmsg.core as ez
3
4
  from ezmsg.util.messages.axisarray import AxisArray
4
5
 
6
+ from .cheby import ChebyshevFilter, ChebyshevFilterSettings
5
7
  from .downsample import Downsample, DownsampleSettings
6
- from .filter import Filter, FilterCoefficients, FilterSettings
8
+ from .filter import FilterCoefsMultiType
9
+
10
+
11
+ class ChebyForDecimate(ChebyshevFilter):
12
+ """
13
+ A :obj:`ChebyshevFilter` node with a design filter method that additionally accepts a target sampling rate,
14
+ and if the target rate cannot be achieved it returns None, else it returns the filter coefficients.
15
+ """
16
+
17
+ def design_filter(
18
+ self,
19
+ ) -> typing.Callable[[float], FilterCoefsMultiType | None]:
20
+ def cheby_opt_design_fun(fs: float) -> FilterCoefsMultiType | None:
21
+ if fs is None:
22
+ return None
23
+ ds_factor = int(fs / (2.5 * self.SETTINGS.Wn))
24
+ if ds_factor < 2:
25
+ return None
26
+ partial_fun = super(ChebyForDecimate, self).design_filter()
27
+ return partial_fun(fs)
28
+ return cheby_opt_design_fun
7
29
 
8
30
 
9
31
  class Decimate(ez.Collection):
@@ -17,23 +39,21 @@ class Decimate(ez.Collection):
17
39
  INPUT_SIGNAL = ez.InputStream(AxisArray)
18
40
  OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
19
41
 
20
- FILTER = Filter()
42
+ FILTER = ChebyForDecimate()
21
43
  DOWNSAMPLE = Downsample()
22
44
 
23
45
  def configure(self) -> None:
24
- self.DOWNSAMPLE.apply_settings(self.SETTINGS)
25
46
 
26
- if self.SETTINGS.factor < 1:
27
- raise ValueError("Decimation factor must be >= 1 (no decimation")
28
- elif self.SETTINGS.factor == 1:
29
- filt = FilterCoefficients()
30
- else:
31
- # See scipy.signal.decimate for IIR Filter Condition
32
- b, a = scipy.signal.cheby1(8, 0.05, 0.8 / self.SETTINGS.factor)
33
- system = scipy.signal.dlti(b, a)
34
- filt = FilterCoefficients(b=system.num, a=system.den) # type: ignore
35
-
36
- self.FILTER.apply_settings(FilterSettings(filt=filt))
47
+ cheby_settings = ChebyshevFilterSettings(
48
+ order=8,
49
+ ripple_tol=0.05,
50
+ Wn=0.4 * self.SETTINGS.target_rate,
51
+ btype="lowpass",
52
+ axis=self.SETTINGS.axis,
53
+ wn_hz=True,
54
+ )
55
+ self.FILTER.apply_settings(cheby_settings)
56
+ self.DOWNSAMPLE.apply_settings(self.SETTINGS)
37
57
 
38
58
  def network(self) -> ez.NetworkDefinition:
39
59
  return (
@@ -14,7 +14,7 @@ from .base import GenAxisArray
14
14
 
15
15
  @consumer
16
16
  def downsample(
17
- axis: typing.Optional[str] = None, factor: int = 1
17
+ axis: str | None = None, target_rate: float | None = None
18
18
  ) -> typing.Generator[AxisArray, AxisArray, None]:
19
19
  """
20
20
  Construct a generator that yields a downsampled version of the data .send() to it.
@@ -26,7 +26,8 @@ def downsample(
26
26
  Args:
27
27
  axis: The name of the axis along which to downsample.
28
28
  Note: The axis must exist in the message .axes and be of type AxisArray.LinearAxis.
29
- factor: Downsampling factor.
29
+ target_rate: Desired rate after downsampling. The actual rate will be the nearest integer factor of the
30
+ input rate that is the same or higher than the target rate.
30
31
 
31
32
  Returns:
32
33
  A primed generator object ready to receive an :obj:`AxisArray` via `.send(axis_array)`
@@ -37,10 +38,8 @@ def downsample(
37
38
  """
38
39
  msg_out = AxisArray(np.array([]), dims=[""])
39
40
 
40
- if factor < 1:
41
- raise ValueError("Downsample factor must be at least 1 (no downsampling)")
42
-
43
41
  # state variables
42
+ factor: int = 0 # The integer downsampling factor. It will be determined based on the target rate.
44
43
  s_idx: int = 0 # Index of the next msg's first sample into the virtual rotating ds_factor counter.
45
44
 
46
45
  check_input = {"gain": None, "key": None}
@@ -62,6 +61,16 @@ def downsample(
62
61
  check_input["key"] = msg_in.key
63
62
  # Reset state variables
64
63
  s_idx = 0
64
+ if target_rate is None:
65
+ factor = 1
66
+ else:
67
+ factor = int(1 / (axis_info.gain * target_rate))
68
+ if factor < 1:
69
+ ez.logger.warning(
70
+ f"Target rate {target_rate} cannot be achieved with input rate of {1/axis_info.gain}."
71
+ "Setting factor to 1."
72
+ )
73
+ factor = 1
65
74
 
66
75
  n_samples = msg_in.data.shape[axis_idx]
67
76
  samples = np.arange(s_idx, s_idx + n_samples) % factor
@@ -96,8 +105,8 @@ class DownsampleSettings(ez.Settings):
96
105
  See :obj:`downsample` documentation for a description of the parameters.
97
106
  """
98
107
 
99
- axis: typing.Optional[str] = None
100
- factor: int = 1
108
+ axis: str | None = None
109
+ target_rate: float | None = None
101
110
 
102
111
 
103
112
  class Downsample(GenAxisArray):
@@ -107,5 +116,5 @@ class Downsample(GenAxisArray):
107
116
 
108
117
  def construct_generator(self):
109
118
  self.STATE.gen = downsample(
110
- axis=self.SETTINGS.axis, factor=self.SETTINGS.factor
119
+ axis=self.SETTINGS.axis, target_rate=self.SETTINGS.target_rate
111
120
  )
@@ -2,14 +2,15 @@ import asyncio
2
2
  import typing
3
3
 
4
4
  import ezmsg.core as ez
5
- from ezmsg.util.messages.axisarray import AxisArray, replace
5
+ from ezmsg.util.messages.axisarray import AxisArray
6
+ from ezmsg.util.messages.util import replace
6
7
  import numpy as np
7
8
 
8
9
  from .window import Window, WindowSettings
9
10
 
10
11
 
11
12
  class EWMSettings(ez.Settings):
12
- axis: typing.Optional[str] = None
13
+ axis: str | None = None
13
14
  """Name of the axis to accumulate."""
14
15
 
15
16
  zero_offset: bool = True
@@ -23,7 +24,8 @@ class EWMState(ez.State):
23
24
 
24
25
  class EWM(ez.Unit):
25
26
  """
26
- Exponentially Weighted Moving Average Standardization
27
+ Exponentially Weighted Moving Average Standardization.
28
+ This is deprecated. Please use :obj:`ezmsg.sigproc.scaler.AdaptiveStandardScaler` instead.
27
29
 
28
30
  References https://stackoverflow.com/a/42926270
29
31
  """
@@ -36,6 +38,9 @@ class EWM(ez.Unit):
36
38
  OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
37
39
 
38
40
  async def initialize(self) -> None:
41
+ ez.logger.warning(
42
+ "EWM/EWMFilter is deprecated and will be removed in a future version. Use AdaptiveStandardScaler instead."
43
+ )
39
44
  self.STATE.signal_queue = asyncio.Queue()
40
45
  self.STATE.buffer_queue = asyncio.Queue()
41
46
 
@@ -99,7 +104,7 @@ class EWMFilterSettings(ez.Settings):
99
104
  history_dur: float
100
105
  """Previous data to accumulate for standardization."""
101
106
 
102
- axis: typing.Optional[str] = None
107
+ axis: str | None = None
103
108
  """Name of the axis to accumulate."""
104
109
 
105
110
  zero_offset: bool = True
@@ -112,7 +117,7 @@ class EWMFilter(ez.Collection):
112
117
  leads to :obj:`Window` which then feeds into :obj:`EWM` 's INPUT_BUFFER
113
118
  and another branch that feeds directly into :obj:`EWM` 's INPUT_SIGNAL.
114
119
 
115
- Consider :obj:`scaler` for a more efficient alternative.
120
+ This is deprecated. Please use :obj:`ezmsg.sigproc.scaler.AdaptiveStandardScaler` instead.
116
121
  """
117
122
 
118
123
  SETTINGS = EWMFilterSettings