ezmsg-sigproc 1.6.0__tar.gz → 1.7.0__tar.gz

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 (64) hide show
  1. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/PKG-INFO +1 -1
  2. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/__version__.py +2 -2
  3. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/decimate.py +28 -4
  4. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/downsample.py +16 -7
  5. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_decimate.py +7 -6
  6. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_downsample.py +14 -12
  7. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/uv.lock +1 -1
  8. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/.github/workflows/python-publish-ezmsg-sigproc.yml +0 -0
  9. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/.github/workflows/python-tests.yml +0 -0
  10. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/.gitignore +0 -0
  11. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/.pre-commit-config.yaml +0 -0
  12. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/LICENSE.txt +0 -0
  13. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/README.md +0 -0
  14. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/pyproject.toml +0 -0
  15. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/__init__.py +0 -0
  16. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/activation.py +0 -0
  17. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/affinetransform.py +0 -0
  18. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/aggregate.py +0 -0
  19. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/bandpower.py +0 -0
  20. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/base.py +0 -0
  21. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/butterworthfilter.py +0 -0
  22. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/cheby.py +0 -0
  23. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/ewmfilter.py +0 -0
  24. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/filter.py +0 -0
  25. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/filterbank.py +0 -0
  26. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/math/__init__.py +0 -0
  27. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/math/abs.py +0 -0
  28. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/math/clip.py +0 -0
  29. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/math/difference.py +0 -0
  30. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/math/invert.py +0 -0
  31. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/math/log.py +0 -0
  32. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/math/scale.py +0 -0
  33. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/messages.py +0 -0
  34. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/sampler.py +0 -0
  35. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/scaler.py +0 -0
  36. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/signalinjector.py +0 -0
  37. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/slicer.py +0 -0
  38. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/spectral.py +0 -0
  39. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/spectrogram.py +0 -0
  40. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/spectrum.py +0 -0
  41. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/synth.py +0 -0
  42. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/wavelets.py +0 -0
  43. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/src/ezmsg/sigproc/window.py +0 -0
  44. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/conftest.py +0 -0
  45. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/helpers/__init__.py +0 -0
  46. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/helpers/util.py +0 -0
  47. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/resources/xform.csv +0 -0
  48. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_activation.py +0 -0
  49. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_affine_transform.py +0 -0
  50. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_aggregate.py +0 -0
  51. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_bandpower.py +0 -0
  52. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_butter.py +0 -0
  53. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_butterworth.py +0 -0
  54. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_filter_system.py +0 -0
  55. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_filterbank.py +0 -0
  56. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_math.py +0 -0
  57. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_sampler.py +0 -0
  58. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_scaler.py +0 -0
  59. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_slicer.py +0 -0
  60. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_spectrogram.py +0 -0
  61. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_spectrum.py +0 -0
  62. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_synth.py +0 -0
  63. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_wavelets.py +0 -0
  64. {ezmsg_sigproc-1.6.0 → ezmsg_sigproc-1.7.0}/tests/test_window.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ezmsg-sigproc
3
- Version: 1.6.0
3
+ Version: 1.7.0
4
4
  Summary: Timeseries signal processing implementations in ezmsg
5
5
  Author-email: Griffin Milsap <griffin.milsap@gmail.com>, Preston Peranich <pperanich@gmail.com>, Chadwick Boulay <chadwick.boulay@gmail.com>
6
6
  License: MIT
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.6.0'
16
- __version_tuple__ = version_tuple = (1, 6, 0)
15
+ __version__ = version = '1.7.0'
16
+ __version_tuple__ = version_tuple = (1, 7, 0)
@@ -1,8 +1,31 @@
1
+ import typing
2
+
1
3
  import ezmsg.core as ez
2
4
  from ezmsg.util.messages.axisarray import AxisArray
3
5
 
4
6
  from .cheby import ChebyshevFilter, ChebyshevFilterSettings
5
7
  from .downsample import Downsample, DownsampleSettings
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
6
29
 
7
30
 
8
31
  class Decimate(ez.Collection):
@@ -16,17 +39,18 @@ class Decimate(ez.Collection):
16
39
  INPUT_SIGNAL = ez.InputStream(AxisArray)
17
40
  OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
18
41
 
19
- FILTER = ChebyshevFilter()
42
+ FILTER = ChebyForDecimate()
20
43
  DOWNSAMPLE = Downsample()
21
44
 
22
45
  def configure(self) -> None:
46
+
23
47
  cheby_settings = ChebyshevFilterSettings(
24
- order=8 if self.SETTINGS.factor > 1 else 0,
48
+ order=8,
25
49
  ripple_tol=0.05,
26
- Wn=0.8 / self.SETTINGS.factor if self.SETTINGS.factor > 1 else None,
50
+ Wn=0.4 * self.SETTINGS.target_rate,
27
51
  btype="lowpass",
28
52
  axis=self.SETTINGS.axis,
29
- wn_hz=False,
53
+ wn_hz=True,
30
54
  )
31
55
  self.FILTER.apply_settings(cheby_settings)
32
56
  self.DOWNSAMPLE.apply_settings(self.SETTINGS)
@@ -14,7 +14,7 @@ from .base import GenAxisArray
14
14
 
15
15
  @consumer
16
16
  def downsample(
17
- axis: str | None = 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
@@ -97,7 +106,7 @@ class DownsampleSettings(ez.Settings):
97
106
  """
98
107
 
99
108
  axis: str | None = None
100
- factor: int = 1
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
  )
@@ -13,8 +13,8 @@ from ezmsg.sigproc.synth import EEGSynth
13
13
  from util import get_test_fn
14
14
 
15
15
 
16
- @pytest.mark.parametrize("factor", [5.0, 1.0])
17
- def test_decimate_system(factor: float):
16
+ @pytest.mark.parametrize("target_rate", [100.0, 500.0])
17
+ def test_decimate_system(target_rate: float):
18
18
  test_filename = get_test_fn()
19
19
  test_filename_raw = test_filename.parent / (
20
20
  test_filename.stem + "raw" + test_filename.suffix
@@ -27,7 +27,7 @@ def test_decimate_system(factor: float):
27
27
 
28
28
  comps = {
29
29
  "SRC": EEGSynth(n_time=n_time, fs=fs, n_ch=n_ch, alpha_freq=10.5),
30
- "DECIMATE": Decimate(axis="time", factor=factor),
30
+ "DECIMATE": Decimate(axis="time", target_rate=target_rate),
31
31
  "LOGRAW": MessageLogger(output=test_filename_raw),
32
32
  "LOGFILT": MessageLogger(output=test_filename),
33
33
  "TERM": TerminateOnTotal(n_total),
@@ -47,13 +47,14 @@ def test_decimate_system(factor: float):
47
47
  inputs = AxisArray.concatenate(*inputs, dim="time")
48
48
  outputs = AxisArray.concatenate(*messages, dim="time")
49
49
 
50
- if factor == 1:
50
+ expected_factor: int = int(fs // target_rate)
51
+ if expected_factor == 1:
51
52
  expected = inputs.data
52
53
  else:
53
- b, a = scipy.signal.cheby1(8, 0.05, 0.8 / factor)
54
+ b, a = scipy.signal.cheby1(8, 0.05, 0.8 / expected_factor)
54
55
  b, a = scipy.signal.normalize(b, a)
55
56
  zi = scipy.signal.lfilter_zi(b, a)[:, None]
56
57
  antialiased, _ = scipy.signal.lfilter(b, a, inputs.data, axis=0, zi=zi)
57
- expected = antialiased[:: int(factor)]
58
+ expected = antialiased[:: int(expected_factor)]
58
59
 
59
60
  assert np.allclose(outputs.data, expected)
@@ -20,8 +20,8 @@ from ezmsg.util.debuglog import DebugLog
20
20
 
21
21
 
22
22
  @pytest.mark.parametrize("block_size", [1, 5, 10, 20])
23
- @pytest.mark.parametrize("factor", [1, 2, 3])
24
- def test_downsample_core(block_size: int, factor: int):
23
+ @pytest.mark.parametrize("target_rate", [19.0, 9.5, 6.3])
24
+ def test_downsample_core(block_size: int, target_rate: float):
25
25
  in_fs = 19.0
26
26
  test_dur = 4.0
27
27
  n_channels = 2
@@ -60,7 +60,7 @@ def test_downsample_core(block_size: int, factor: int):
60
60
  in_msgs = list(msg_generator())
61
61
  backup = [copy.deepcopy(msg) for msg in in_msgs]
62
62
 
63
- proc = downsample(axis="time", factor=factor)
63
+ proc = downsample(axis="time", target_rate=target_rate)
64
64
  out_msgs = []
65
65
  for msg in in_msgs:
66
66
  res = proc.send(msg)
@@ -70,18 +70,19 @@ def test_downsample_core(block_size: int, factor: int):
70
70
  assert_messages_equal(in_msgs, backup)
71
71
 
72
72
  # Assert correctness of gain
73
- assert all(msg.axes["time"].gain == factor / in_fs for msg in out_msgs)
73
+ expected_factor: int = int(in_fs // target_rate)
74
+ assert all(msg.axes["time"].gain == expected_factor / in_fs for msg in out_msgs)
74
75
 
75
76
  # Assert messages have the correct timestamps
76
77
  expected_offsets = (
77
- np.cumsum([0] + [_.data.shape[0] for _ in out_msgs]) * factor / in_fs
78
+ np.cumsum([0] + [_.data.shape[0] for _ in out_msgs]) * expected_factor / in_fs
78
79
  )
79
80
  actual_offsets = np.array([_.axes["time"].offset for _ in out_msgs])
80
81
  assert np.allclose(actual_offsets, expected_offsets[:-1])
81
82
 
82
83
  # Compare returned values to expected values.
83
84
  allres_msg = AxisArray.concatenate(*out_msgs, dim="time")
84
- assert np.array_equal(allres_msg.data, sig[::factor])
85
+ assert np.array_equal(allres_msg.data, sig[::expected_factor])
85
86
 
86
87
 
87
88
  class DownsampleSystemSettings(ez.Settings):
@@ -130,8 +131,8 @@ class DownsampleSystem(ez.Collection):
130
131
 
131
132
 
132
133
  @pytest.mark.parametrize("block_size", [10])
133
- @pytest.mark.parametrize("factor", [3])
134
- def test_downsample_system(block_size: int, factor: int, test_name: str | None = None):
134
+ @pytest.mark.parametrize("target_rate", [6.3])
135
+ def test_downsample_system(block_size: int, target_rate: float, test_name: str | None = None):
135
136
  in_fs = 19.0
136
137
  num_msgs = int(4.0 / (block_size / in_fs)) # Ensure 4 seconds of data
137
138
 
@@ -145,7 +146,7 @@ def test_downsample_system(block_size: int, factor: int, test_name: str | None =
145
146
  fs=in_fs,
146
147
  dispatch_rate=20.0,
147
148
  ),
148
- down_settings=DownsampleSettings(factor=factor),
149
+ down_settings=DownsampleSettings(target_rate=target_rate),
149
150
  log_settings=MessageLoggerSettings(output=test_filename),
150
151
  term_settings=TerminateTestSettings(time=1.0),
151
152
  )
@@ -159,7 +160,8 @@ def test_downsample_system(block_size: int, factor: int, test_name: str | None =
159
160
  ez.logger.info(f"Analyzing recording of { len( messages ) } messages...")
160
161
 
161
162
  # Check fs
162
- out_fs = in_fs / factor
163
+ expected_factor: int = int(in_fs // target_rate)
164
+ out_fs = in_fs / expected_factor
163
165
  assert np.allclose(
164
166
  np.array([1 / msg.axes["time"].gain for msg in messages]),
165
167
  np.ones(
@@ -173,7 +175,7 @@ def test_downsample_system(block_size: int, factor: int, test_name: str | None =
173
175
  # Check data
174
176
  time_ax_idx = messages[0].get_axis_idx("time")
175
177
  data = np.concatenate([_.data for _ in messages], axis=time_ax_idx)
176
- expected_data = np.arange(data.shape[time_ax_idx]) * factor
178
+ expected_data = np.arange(data.shape[time_ax_idx]) * expected_factor
177
179
  assert np.array_equal(data, expected_data[:, None])
178
180
 
179
181
  # Grab first sample from each message. We will use their values to get the offsets.
@@ -185,7 +187,7 @@ def test_downsample_system(block_size: int, factor: int, test_name: str | None =
185
187
 
186
188
  # Check offsets
187
189
  first_samps = np.concatenate(first_samps, axis=time_ax_idx)
188
- expected_offsets = first_samps.squeeze() / out_fs / factor
190
+ expected_offsets = first_samps.squeeze() / out_fs / expected_factor
189
191
  assert np.allclose(
190
192
  np.array([msg.axes["time"].offset for msg in messages]), expected_offsets
191
193
  )
@@ -115,7 +115,7 @@ wheels = [
115
115
 
116
116
  [[package]]
117
117
  name = "ezmsg-sigproc"
118
- version = "1.4.3.dev34+gbfb0671.d20241122"
118
+ version = "1.6.0"
119
119
  source = { editable = "." }
120
120
  dependencies = [
121
121
  { name = "ezmsg" },
File without changes
File without changes
File without changes