ezmsg-sigproc 1.7.0__py3-none-any.whl → 1.7.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.
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
3
6
  TYPE_CHECKING = False
4
7
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
8
+ from typing import Tuple
9
+ from typing import Union
10
+
6
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
12
  else:
8
13
  VERSION_TUPLE = object
@@ -12,5 +17,5 @@ __version__: str
12
17
  __version_tuple__: VERSION_TUPLE
13
18
  version_tuple: VERSION_TUPLE
14
19
 
15
- __version__ = version = '1.7.0'
16
- __version_tuple__ = version_tuple = (1, 7, 0)
20
+ __version__ = version = '1.7.1'
21
+ __version_tuple__ = version_tuple = (1, 7, 1)
@@ -35,6 +35,7 @@ def bandpower(
35
35
  f_spec = spectrogram(
36
36
  window_dur=spectrogram_settings.window_dur,
37
37
  window_shift=spectrogram_settings.window_shift,
38
+ window_anchor=spectrogram_settings.window_anchor,
38
39
  window=spectrogram_settings.window,
39
40
  transform=spectrogram_settings.transform,
40
41
  output=spectrogram_settings.output,
ezmsg/sigproc/base.py CHANGED
@@ -6,6 +6,8 @@ import ezmsg.core as ez
6
6
  from ezmsg.util.messages.axisarray import AxisArray
7
7
  from ezmsg.util.generator import GenState
8
8
 
9
+ from .util.profile import profile_subpub
10
+
9
11
 
10
12
  class GenAxisArray(ez.Unit):
11
13
  STATE = GenState
@@ -28,6 +30,7 @@ class GenAxisArray(ez.Unit):
28
30
 
29
31
  @ez.subscriber(INPUT_SIGNAL, zero_copy=True)
30
32
  @ez.publisher(OUTPUT_SIGNAL)
33
+ @profile_subpub(trace_oldest=False)
31
34
  async def on_signal(self, message: AxisArray) -> typing.AsyncGenerator:
32
35
  try:
33
36
  ret = self.STATE.gen.send(message)
@@ -214,7 +214,8 @@ def filterbank(
214
214
  pad = np.zeros(dest_arr.shape[:-1] + (n_dest - dest_arr.shape[-1],))
215
215
  dest_arr = np.concatenate(dest_arr, pad, axis=-1)
216
216
  dest_arr.fill(0)
217
- # TODO: Parallelize this loop.
217
+ # Note: I tried several alternatives to this loop; all were slower than this.
218
+ # numba.jit; stride_tricks + np.einsum; threading. Latter might be better with Python 3.13.
218
219
  for k_ix, k in enumerate(kernels):
219
220
  n_out = in_dat.shape[-1] + k.shape[-1] - 1
220
221
  dest_arr[..., k_ix, :n_out] = np.apply_along_axis(
ezmsg/sigproc/sampler.py CHANGED
@@ -14,6 +14,8 @@ from ezmsg.util.messages.axisarray import (
14
14
  from ezmsg.util.messages.util import replace
15
15
  from ezmsg.util.generator import consumer
16
16
 
17
+ from .util.profile import profile_subpub
18
+
17
19
 
18
20
  @dataclass(unsafe_hash=True)
19
21
  class SampleTriggerMessage:
@@ -284,6 +286,7 @@ class Sampler(ez.Unit):
284
286
 
285
287
  @ez.subscriber(INPUT_SIGNAL, zero_copy=True)
286
288
  @ez.publisher(OUTPUT_SAMPLE)
289
+ @profile_subpub(trace_oldest=False)
287
290
  async def on_signal(self, msg: AxisArray) -> typing.AsyncGenerator:
288
291
  pub_samples = self.STATE.gen.send(msg)
289
292
  for sample in pub_samples:
@@ -6,6 +6,8 @@ from ezmsg.util.messages.util import replace
6
6
  import numpy as np
7
7
  import numpy.typing as npt
8
8
 
9
+ from .util.profile import profile_subpub
10
+
9
11
 
10
12
  class SignalInjectorSettings(ez.Settings):
11
13
  time_dim: str = "time" # Input signal needs a time dimension with units in sec.
@@ -50,6 +52,7 @@ class SignalInjector(ez.Unit):
50
52
 
51
53
  @ez.subscriber(INPUT_SIGNAL)
52
54
  @ez.publisher(OUTPUT_SIGNAL)
55
+ @profile_subpub(trace_oldest=False)
53
56
  async def inject(self, msg: AxisArray) -> typing.AsyncGenerator:
54
57
  if self.STATE.cur_shape != msg.shape:
55
58
  self.STATE.cur_shape = msg.shape
@@ -5,7 +5,7 @@ from ezmsg.util.messages.axisarray import AxisArray
5
5
  from ezmsg.util.generator import consumer, compose
6
6
  from ezmsg.util.messages.modify import modify_axis
7
7
 
8
- from .window import windowing
8
+ from .window import windowing, Anchor
9
9
  from .spectrum import spectrum, WindowFunction, SpectralTransform, SpectralOutput
10
10
  from .base import GenAxisArray
11
11
 
@@ -14,6 +14,7 @@ from .base import GenAxisArray
14
14
  def spectrogram(
15
15
  window_dur: float | None = None,
16
16
  window_shift: float | None = None,
17
+ window_anchor: str | Anchor = Anchor.BEGINNING,
17
18
  window: WindowFunction = WindowFunction.HANNING,
18
19
  transform: SpectralTransform = SpectralTransform.REL_DB,
19
20
  output: SpectralOutput = SpectralOutput.POSITIVE,
@@ -28,6 +29,7 @@ def spectrogram(
28
29
  Args:
29
30
  window_dur: See :obj:`ezmsg.sigproc.window.windowing`
30
31
  window_shift: See :obj:`ezmsg.sigproc.window.windowing`
32
+ window_anchor: See :obj:`ezmsg.sigproc.window.windowing`
31
33
  window: See :obj:`ezmsg.sigproc.spectrum.spectrum`
32
34
  transform: See :obj:`ezmsg.sigproc.spectrum.spectrum`
33
35
  output: See :obj:`ezmsg.sigproc.spectrum.spectrum`
@@ -44,6 +46,7 @@ def spectrogram(
44
46
  window_dur=window_dur,
45
47
  window_shift=window_shift,
46
48
  zero_pad_until="shift" if window_shift is not None else "input",
49
+ anchor=window_anchor,
47
50
  ),
48
51
  spectrum(axis="time", window=window, transform=transform, output=output),
49
52
  modify_axis(name_map={"win": "time"}),
@@ -66,6 +69,7 @@ class SpectrogramSettings(ez.Settings):
66
69
  window_dur: float | None = None # window duration in seconds
67
70
  window_shift: float | None = None
68
71
  """"window step in seconds. If None, window_shift == window_dur"""
72
+ window_anchor: str | Anchor = Anchor.BEGINNING
69
73
 
70
74
  # See SpectrumSettings for details of following settings:
71
75
  window: WindowFunction = WindowFunction.HAMMING
@@ -84,6 +88,7 @@ class Spectrogram(GenAxisArray):
84
88
  self.STATE.gen = spectrogram(
85
89
  window_dur=self.SETTINGS.window_dur,
86
90
  window_shift=self.SETTINGS.window_shift,
91
+ window_anchor=self.SETTINGS.window_anchor,
87
92
  window=self.SETTINGS.window,
88
93
  transform=self.SETTINGS.transform,
89
94
  output=self.SETTINGS.output,
File without changes
@@ -0,0 +1,131 @@
1
+ import functools
2
+ import logging
3
+ import os
4
+ from pathlib import Path
5
+ import time
6
+ import typing
7
+
8
+ import ezmsg.core as ez
9
+
10
+
11
+ def get_logger_path() -> Path:
12
+ # Retrieve the logfile name from the environment variable
13
+ logfile = os.environ.get("EZMSG_PROFILE", None)
14
+
15
+ # Determine the log file path, defaulting to "ezprofiler.log" if not set
16
+ logpath = Path(logfile or "ezprofiler.log")
17
+
18
+ # If the log path is not absolute, prepend it with the user's home directory and ".ezmsg/profile"
19
+ if not logpath.is_absolute():
20
+ logpath = Path.home() / ".ezmsg" / "profile" / logpath
21
+
22
+ return logpath
23
+
24
+
25
+ def _setup_logger(append: bool = False) -> logging.Logger:
26
+ logpath = get_logger_path()
27
+ logpath.parent.mkdir(parents=True, exist_ok=True)
28
+
29
+ if not append:
30
+ # Remove the file if it exists
31
+ logpath.unlink(missing_ok=True)
32
+
33
+ # Create a logger with the name "ezprofile"
34
+ _logger = logging.getLogger("ezprofile")
35
+
36
+ # Set the logger's level to EZMSG_LOGLEVEL env var value if it exists, otherwise INFO
37
+ _logger.setLevel(os.environ.get("EZMSG_LOGLEVEL", "INFO").upper())
38
+
39
+ # Create a file handler to write log messages to the log file
40
+ fh = logging.FileHandler(logpath)
41
+ fh.setLevel(logging.DEBUG) # Set the file handler log level to DEBUG
42
+
43
+ # Add the file handler to the logger
44
+ _logger.addHandler(fh)
45
+
46
+ # Add the first row without formatting.
47
+ _logger.debug(",".join(["Time", "Source", "Topic", "SampleTime", "PerfCounter", "Elapsed"]))
48
+
49
+ # Set the log message format
50
+ formatter = logging.Formatter(
51
+ "%(asctime)s,%(message)s",
52
+ datefmt="%Y-%m-%dT%H:%M:%S%z"
53
+ )
54
+ fh.setFormatter(formatter)
55
+
56
+ return _logger
57
+
58
+
59
+ logger = _setup_logger(append=True)
60
+
61
+
62
+ def _process_obj(obj, trace_oldest: bool = True):
63
+ samp_time = None
64
+ if hasattr(obj, "axes") and ("time" in obj.axes or "win" in obj.axes):
65
+ axis = "win" if "win" in obj.axes else "time"
66
+ ax = obj.get_axis(axis)
67
+ len = obj.data.shape[obj.get_axis_idx(axis)]
68
+ if len > 0:
69
+ idx = 0 if trace_oldest else (len - 1)
70
+ if hasattr(ax, "data"):
71
+ samp_time = ax.data[idx]
72
+ else:
73
+ samp_time = ax.value(idx)
74
+ if ax == "win" and "time" in obj.axes:
75
+ if hasattr(obj.axes["time"], "data"):
76
+ samp_time += obj.axes["time"].data[idx]
77
+ else:
78
+ samp_time += obj.axes["time"].value(idx)
79
+ return samp_time
80
+
81
+
82
+ def profile_method(trace_oldest: bool = True):
83
+ """
84
+ Decorator to profile a method by logging its execution time and other details.
85
+
86
+ Args:
87
+ trace_oldest (bool): If True, trace the oldest sample time; otherwise, trace the newest.
88
+
89
+ Returns:
90
+ Callable: The decorated function with profiling.
91
+ """
92
+ def profiling_decorator(func: typing.Callable):
93
+ @functools.wraps(func)
94
+ def wrapped_func(caller, *args, **kwargs):
95
+ start = time.perf_counter()
96
+ res = func(caller, *args, **kwargs)
97
+ stop = time.perf_counter()
98
+ source = '.'.join((caller.__class__.__module__, caller.__class__.__name__))
99
+ topic = f"{caller.address}"
100
+ samp_time = _process_obj(res, trace_oldest=trace_oldest)
101
+ logger.debug(",".join([source, topic, f"{samp_time}", f"{stop}", f"{(stop - start) * 1e3:0.4f}"]))
102
+ return res
103
+ return wrapped_func if logger.level == logging.DEBUG else func
104
+ return profiling_decorator
105
+
106
+
107
+ def profile_subpub(trace_oldest: bool = True):
108
+ """
109
+ Decorator to profile a subscriber-publisher method in an ezmsg Unit
110
+ by logging its execution time and other details.
111
+
112
+ Args:
113
+ trace_oldest (bool): If True, trace the oldest sample time; otherwise, trace the newest.
114
+
115
+ Returns:
116
+ Callable: The decorated async task with profiling.
117
+ """
118
+ def profiling_decorator(func: typing.Callable):
119
+ @functools.wraps(func)
120
+ async def wrapped_task(unit: ez.Unit, msg: typing.Any = None) -> None:
121
+ source = '.'.join((unit.__class__.__module__, unit.__class__.__name__))
122
+ topic = f"{unit.address}"
123
+ start = time.perf_counter()
124
+ async for stream, obj in func(unit, msg):
125
+ stop = time.perf_counter()
126
+ samp_time = _process_obj(obj, trace_oldest=trace_oldest)
127
+ logger.debug(",".join([source, topic, f"{samp_time}", f"{stop}", f"{(stop - start) * 1e3:0.4f}"]))
128
+ start = stop
129
+ yield stream, obj
130
+ return wrapped_task if logger.level == logging.DEBUG else func
131
+ return profiling_decorator
@@ -0,0 +1,29 @@
1
+ import sparse
2
+
3
+
4
+ def sliding_win_oneaxis(
5
+ s: sparse.SparseArray, nwin: int, axis: int, step: int = 1
6
+ ) -> sparse.SparseArray:
7
+ """
8
+ Like `ezmsg.util.messages.axisarray.sliding_win_oneaxis` but for sparse arrays.
9
+
10
+ Args:
11
+ s: The input sparse array.
12
+ nwin: The size of the sliding window.
13
+ axis: The axis along which the sliding window will be applied.
14
+ step: The size of the step between windows. If > 1, the strided window will be sliced with `slice_along_axis`.
15
+
16
+ Returns:
17
+
18
+ """
19
+ if -s.ndim <= axis < 0:
20
+ axis = s.ndim + axis
21
+ targ_slices = [slice(_, _ + nwin) for _ in range(0, s.shape[axis] - nwin + 1, step)]
22
+ s = s.reshape(s.shape[:axis] + (1,) + s.shape[axis:])
23
+ full_slices = (slice(None),) * s.ndim
24
+ full_slices = [
25
+ full_slices[: axis + 1] + (sl,) + full_slices[axis + 2 :] for sl in targ_slices
26
+ ]
27
+ result = sparse.concatenate([s[_] for _ in full_slices], axis=axis)
28
+ # TODO: Profile this approach vs modifying coords only.
29
+ return result
ezmsg/sigproc/wavelets.py CHANGED
@@ -171,11 +171,11 @@ class CWTSettings(ez.Settings):
171
171
  Settings for :obj:`CWT`
172
172
  See :obj:`cwt` for argument details.
173
173
  """
174
-
175
- scales: list | tuple | npt.NDArray
174
+ frequencies: list | tuple | npt.NDArray | None
176
175
  wavelet: str | pywt.ContinuousWavelet | pywt.Wavelet
177
176
  min_phase: MinPhaseMode = MinPhaseMode.NONE
178
177
  axis: str = "time"
178
+ scales: list | tuple | npt.NDArray | None = None
179
179
 
180
180
 
181
181
  class CWT(GenAxisArray):
@@ -187,8 +187,9 @@ class CWT(GenAxisArray):
187
187
 
188
188
  def construct_generator(self):
189
189
  self.STATE.gen = cwt(
190
- scales=self.SETTINGS.scales,
190
+ frequencies=self.SETTINGS.frequencies,
191
191
  wavelet=self.SETTINGS.wavelet,
192
192
  min_phase=self.SETTINGS.min_phase,
193
193
  axis=self.SETTINGS.axis,
194
+ scales=self.SETTINGS.scales,
194
195
  )
ezmsg/sigproc/window.py CHANGED
@@ -1,9 +1,11 @@
1
+ import enum
1
2
  import traceback
2
3
  import typing
3
4
 
4
5
  import ezmsg.core as ez
5
6
  import numpy as np
6
7
  import numpy.typing as npt
8
+ import sparse
7
9
  from ezmsg.util.messages.axisarray import (
8
10
  AxisArray,
9
11
  slice_along_axis,
@@ -13,6 +15,13 @@ from ezmsg.util.messages.axisarray import (
13
15
  from ezmsg.util.generator import consumer
14
16
 
15
17
  from .base import GenAxisArray
18
+ from .util.sparse import sliding_win_oneaxis as sparse_sliding_win_oneaxis
19
+
20
+
21
+ class Anchor(enum.Enum):
22
+ BEGINNING = "beginning"
23
+ END = "end"
24
+ MIDDLE = "middle"
16
25
 
17
26
 
18
27
  @consumer
@@ -22,6 +31,7 @@ def windowing(
22
31
  window_dur: float | None = None,
23
32
  window_shift: float | None = None,
24
33
  zero_pad_until: str = "input",
34
+ anchor: str | Anchor = Anchor.BEGINNING,
25
35
  ) -> typing.Generator[AxisArray, AxisArray, None]:
26
36
  """
27
37
  Apply a sliding window along the specified axis to input streaming data.
@@ -49,6 +59,8 @@ def windowing(
49
59
  - "shift" fills the buffer until `window_shift`.
50
60
  No outputs will be yielded until at least `window_shift` data has been seen.
51
61
  - "none" does not pad the buffer. No outputs will be yielded until at least `window_dur` data has been seen.
62
+ anchor: Determines the entry in `axis` that gets assigned `0`, which references the
63
+ value in `newaxis`. Can be of class :obj:`Anchor` or a string representation of an :obj:`Anchor`.
52
64
 
53
65
  Returns:
54
66
  A primed generator that accepts an :obj:`AxisArray` via `.send(axis_array)`
@@ -69,10 +81,15 @@ def windowing(
69
81
  "windowing is non-deterministic with `zero_pad_until='input'` as it depends on the size "
70
82
  "of the first input. We recommend using 'shift' when `window_shift` is float-valued."
71
83
  )
84
+ try:
85
+ anchor = Anchor(anchor)
86
+ except ValueError:
87
+ raise ValueError(f"Invalid anchor: {anchor}. Valid anchor are: {', '.join([e.value for e in Anchor])}")
88
+
72
89
  msg_out = AxisArray(np.array([]), dims=[""])
73
90
 
74
91
  # State variables
75
- buffer: npt.NDArray | None = None
92
+ buffer: npt.NDArray | sparse.SparseArray | None = None
76
93
  window_samples: int | None = None
77
94
  window_shift_samples: int | None = None
78
95
  # Number of incoming samples to ignore. Only relevant when shift > window.:
@@ -83,6 +100,8 @@ def windowing(
83
100
  out_dims: list[str] | None = None
84
101
 
85
102
  check_inputs = {"samp_shape": None, "fs": None, "key": None}
103
+ concat_fun = np.concatenate
104
+ sliding_win_fun = sliding_win_oneaxis
86
105
 
87
106
  while True:
88
107
  msg_in: AxisArray = yield msg_out
@@ -116,6 +135,15 @@ def windowing(
116
135
  check_inputs["fs"] = fs
117
136
  check_inputs["key"] = msg_in.key
118
137
 
138
+ if isinstance(msg_in.data, sparse.SparseArray):
139
+ concat_fun = sparse.concatenate
140
+ sliding_win_fun = sparse_sliding_win_oneaxis
141
+ zero_fun = sparse.zeros
142
+ else:
143
+ concat_fun = np.concatenate
144
+ sliding_win_fun = sliding_win_oneaxis # Requires updated signature in ezmsg dev.
145
+ zero_fun = np.zeros
146
+
119
147
  window_samples = int(window_dur * fs)
120
148
  if not b_1to1:
121
149
  window_shift_samples = int(window_shift * fs)
@@ -131,7 +159,7 @@ def windowing(
131
159
  + (n_zero,)
132
160
  + msg_in.data.shape[axis_idx + 1 :]
133
161
  )
134
- buffer = np.zeros(init_buffer_shape, dtype=msg_in.data.dtype)
162
+ buffer = zero_fun(init_buffer_shape, dtype=msg_in.data.dtype)
135
163
 
136
164
  # Add new data to buffer.
137
165
  # Currently, we concatenate the new time samples and clip the output.
@@ -140,21 +168,21 @@ def windowing(
140
168
  # is generally faster than np.roll and slicing anyway, but this could still
141
169
  # be a performance bottleneck for large memory arrays.
142
170
  # A circular buffer might be faster.
143
- buffer = np.concatenate((buffer, msg_in.data), axis=axis_idx)
171
+ buffer = concat_fun((buffer, msg_in.data), axis=axis_idx)
144
172
 
145
173
  # Create a vector of buffer timestamps to track axis `offset` in output(s)
146
- buffer_offset = np.arange(buffer.shape[axis_idx]).astype(float)
147
- # Adjust so first _new_ sample at index 0
148
- buffer_offset -= buffer_offset[-msg_in.data.shape[axis_idx]]
174
+ buffer_tvec = np.arange(buffer.shape[axis_idx]).astype(float)
175
+ # Adjust so first _new_ sample at index 0.
176
+ buffer_tvec -= buffer_tvec[-msg_in.data.shape[axis_idx]]
149
177
  # Convert form indices to 'units' (probably seconds).
150
- buffer_offset *= axis_info.gain
151
- buffer_offset += axis_info.offset
178
+ buffer_tvec *= axis_info.gain
179
+ buffer_tvec += axis_info.offset
152
180
 
153
181
  if not b_1to1 and shift_deficit > 0:
154
182
  n_skip = min(buffer.shape[axis_idx], shift_deficit)
155
183
  if n_skip > 0:
156
184
  buffer = slice_along_axis(buffer, slice(n_skip, None), axis_idx)
157
- buffer_offset = buffer_offset[n_skip:]
185
+ buffer_tvec = buffer_tvec[n_skip:]
158
186
  shift_deficit -= n_skip
159
187
 
160
188
  # Prepare reusable parts of output
@@ -171,8 +199,12 @@ def windowing(
171
199
  out_axes = {k: v for k, v in msg_in.axes.items() if k not in [newaxis, axis]}
172
200
 
173
201
  # Update targeted (windowed) axis so that its offset is relative to the new axis
174
- # TODO: If we have `anchor_newest=True` then offset should be -win_dur
175
- out_axes[axis] = replace(axis_info, offset=0.0)
202
+ if anchor == Anchor.BEGINNING:
203
+ out_axes[axis] = replace(axis_info, offset=0.0)
204
+ elif anchor == Anchor.END:
205
+ out_axes[axis] = replace(axis_info, offset=-window_dur)
206
+ elif anchor == Anchor.MIDDLE:
207
+ out_axes[axis] = replace(axis_info, offset=-window_dur / 2)
176
208
 
177
209
  # How we update .data and .axes[newaxis] depends on the windowing mode.
178
210
  if b_1to1:
@@ -181,19 +213,14 @@ def windowing(
181
213
  out_dat = buffer.reshape(
182
214
  buffer.shape[:axis_idx] + (1,) + buffer.shape[axis_idx:]
183
215
  )
184
- out_newaxis = replace(out_newaxis, offset=buffer_offset[-window_samples])
216
+ win_offset = buffer_tvec[-window_samples]
185
217
  elif buffer.shape[axis_idx] >= window_samples:
186
218
  # Deterministic window shifts.
187
- # Note: After https://github.com/ezmsg-org/ezmsg/pull/152, add `window_shift_samples` as the last arg
188
- # to `sliding_win_oneaxis` and remove the call to `slice_along_axis`.
189
- out_dat = sliding_win_oneaxis(buffer, window_samples, axis_idx)
190
- out_dat = slice_along_axis(
191
- out_dat, slice(None, None, window_shift_samples), axis_idx
192
- )
193
- offset_view = sliding_win_oneaxis(buffer_offset, window_samples, 0)[
219
+ out_dat = sliding_win_fun(buffer, window_samples, axis_idx, step=window_shift_samples)
220
+ offset_view = sliding_win_oneaxis(buffer_tvec, window_samples, 0)[
194
221
  ::window_shift_samples
195
222
  ]
196
- out_newaxis = replace(out_newaxis, offset=offset_view[0, 0])
223
+ win_offset = offset_view[0, 0]
197
224
 
198
225
  # Drop expired beginning of buffer and update shift_deficit
199
226
  multi_shift = window_shift_samples * out_dat.shape[axis_idx]
@@ -208,7 +235,13 @@ def windowing(
208
235
  )
209
236
  out_dat = np.zeros(empty_data_shape, dtype=msg_in.data.dtype)
210
237
  # out_newaxis will have first timestamp in input... but mostly meaningless because output is size-zero.
211
- out_newaxis = replace(out_newaxis, offset=axis_info.offset)
238
+ win_offset = axis_info.offset
239
+
240
+ if anchor == Anchor.END:
241
+ win_offset += window_dur
242
+ elif anchor == Anchor.MIDDLE:
243
+ win_offset += window_dur / 2
244
+ out_newaxis = replace(out_newaxis, offset=win_offset)
212
245
 
213
246
  msg_out = replace(
214
247
  msg_in, data=out_dat, dims=out_dims, axes={**out_axes, newaxis: out_newaxis}
@@ -221,6 +254,7 @@ class WindowSettings(ez.Settings):
221
254
  window_dur: float | None = None # Sec. passthrough if None
222
255
  window_shift: float | None = None # Sec. Use "1:1 mode" if None
223
256
  zero_pad_until: str = "full" # "full", "shift", "input", "none"
257
+ anchor: str | Anchor = Anchor.BEGINNING
224
258
 
225
259
 
226
260
  class WindowState(ez.State):
@@ -243,6 +277,7 @@ class Window(GenAxisArray):
243
277
  window_dur=self.SETTINGS.window_dur,
244
278
  window_shift=self.SETTINGS.window_shift,
245
279
  zero_pad_until=self.SETTINGS.zero_pad_until,
280
+ anchor=self.SETTINGS.anchor,
246
281
  )
247
282
 
248
283
  @ez.subscriber(INPUT_SIGNAL, zero_copy=True)
@@ -261,10 +296,7 @@ class Window(GenAxisArray):
261
296
  # We need to split out_msg into multiple yields, dropping newaxis.
262
297
  axis_idx = out_msg.get_axis_idx("win")
263
298
  win_axis = out_msg.axes["win"]
264
- offsets = (
265
- np.arange(out_msg.data.shape[axis_idx]) * win_axis.gain
266
- + win_axis.offset
267
- )
299
+ offsets = win_axis.value(np.arange(out_msg.data.shape[axis_idx]))
268
300
  for msg_ix in range(out_msg.data.shape[axis_idx]):
269
301
  # Need to drop 'win' and replace self.SETTINGS.axis from axes.
270
302
  _out_axes = {
@@ -1,14 +1,16 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: ezmsg-sigproc
3
- Version: 1.7.0
3
+ Version: 1.7.1
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
- License: MIT
6
+ License-Expression: MIT
7
+ License-File: LICENSE.txt
7
8
  Requires-Python: >=3.10.15
8
9
  Requires-Dist: ezmsg>=3.6.0
9
10
  Requires-Dist: numpy>=1.26.0
10
11
  Requires-Dist: pywavelets>=1.6.0
11
12
  Requires-Dist: scipy>=1.13.1
13
+ Requires-Dist: sparse>=0.15.4
12
14
  Provides-Extra: test
13
15
  Requires-Dist: flake8>=7.1.1; extra == 'test'
14
16
  Requires-Dist: frozendict>=2.4.4; extra == 'test'
@@ -1,28 +1,28 @@
1
1
  ezmsg/sigproc/__init__.py,sha256=8K4IcOA3-pfzadoM6s2Sfg5460KlJUocGgyTJTJl96U,52
2
- ezmsg/sigproc/__version__.py,sha256=2fEqxujmrV2dsREie2BmOYFLu66FowyHtZT2AoLuIzU,411
2
+ ezmsg/sigproc/__version__.py,sha256=rrNaU9VIsI9zhPU7cDbTjSQL51tjXJYrLUJilVd8_WE,511
3
3
  ezmsg/sigproc/activation.py,sha256=LM-MtaNvFvmBZ1_EPNu--4K3-wIzsb7CUQ00fvMM69g,2642
4
4
  ezmsg/sigproc/affinetransform.py,sha256=R2P1f5JdNSpr30x8Pxoqv-J-nAasGl_UZR8o7sVqlaQ,8740
5
5
  ezmsg/sigproc/aggregate.py,sha256=6BqAujWe7_zzGPDJz7yh4ofPzJTU5z2lrclX-IuqXDU,6235
6
- ezmsg/sigproc/bandpower.py,sha256=Mx8iUU-UMWKK5sgCXvxpUSA3522rW8Jg96asrT8MdpA,2180
7
- ezmsg/sigproc/base.py,sha256=vut0BLjgc0mxYRbs7tDd9XzwRFA2_GcsgXZmIYovR0Y,1248
6
+ ezmsg/sigproc/bandpower.py,sha256=wEb_iGRewn3EOAOVspnRvgz0QkcU6NhUXRF4dv_R3PY,2238
7
+ ezmsg/sigproc/base.py,sha256=1I0Dr8jz5u5mcdQu2SdrKAjujx2DQl05u9h0MO8yay0,1330
8
8
  ezmsg/sigproc/butterworthfilter.py,sha256=vZTY37FfLwa24bRZmeZGyO5323824wJosUrrZarb0_o,5402
9
9
  ezmsg/sigproc/cheby.py,sha256=yds5y1fOeBE1ljyH_EreBLxqFX4UetxB_3rwz3omHyc,3394
10
10
  ezmsg/sigproc/decimate.py,sha256=qxZoGmriviTNIUvTOA--U65CPWD1uTvQ9pij79-u00Q,2044
11
11
  ezmsg/sigproc/downsample.py,sha256=PYdrp7p6subP_qbGB2-3C14bd8W4tvC5oMKEmERQZmk,4049
12
12
  ezmsg/sigproc/ewmfilter.py,sha256=EPlocRdKORj575VV1YUzcNsVcq-pYgdEJ7_m9WfpVnY,4795
13
13
  ezmsg/sigproc/filter.py,sha256=n3_gColSPXe4pI-A2VDKfrdFHZgC-k4kqB8H6tnGIws,6969
14
- ezmsg/sigproc/filterbank.py,sha256=pySLNGppnG6Dx9r5jQpNSyWhd9qmvj6up2udG7RMNok,12410
14
+ ezmsg/sigproc/filterbank.py,sha256=HqyVrvpnp2ZhzYE3BNBXC0IWStKq4nFC9F9-Mj8nxCQ,12562
15
15
  ezmsg/sigproc/messages.py,sha256=y_twVPK7TxRj8ajmuSuBuxwvLTgyv9OF7Y7v9bw1tfs,926
16
- ezmsg/sigproc/sampler.py,sha256=ljZuToiC5p4Jx2cJPXJ4tOtv3nBOBGMEaz7ad7Nec8U,12735
16
+ ezmsg/sigproc/sampler.py,sha256=NNXJzuVsIrn3QK7Bs9D6P7_BoOhHIpL8aw8TJYsc1mk,12817
17
17
  ezmsg/sigproc/scaler.py,sha256=5uCgVbZHV9iCsAVnc9Pz7hKIXnaIiA9ssiTCce_QHlU,9550
18
- ezmsg/sigproc/signalinjector.py,sha256=-_cln7Ljny0WWrjBV3HZhhONLEkEphsgLS30AWlxcOE,2590
18
+ ezmsg/sigproc/signalinjector.py,sha256=nokCecnsM_61U7O-Gqo7WKb-vMQJPhQAmZQxad73GX0,2672
19
19
  ezmsg/sigproc/slicer.py,sha256=n6JGOkkY2ur5K1STsLQDSvFhEjT2ZLiSxmggK6qEmpc,5731
20
20
  ezmsg/sigproc/spectral.py,sha256=_2qO6as4Nesmc9V1WW2EXNMH5pPz8aVTEcIPOi4-g2o,322
21
- ezmsg/sigproc/spectrogram.py,sha256=Xc3zXKjVwG0-NLzKEsVKv253Sk50t2U1SlqEJ_ll36k,3086
21
+ ezmsg/sigproc/spectrogram.py,sha256=trbvZvJrC1xXu6jSn-bMHT6jTru-yCBg2pbxD93IWbA,3351
22
22
  ezmsg/sigproc/spectrum.py,sha256=ZhsWlxUiTaWc8qgNZ5NWqsU_fNmjb4V5532mNA8Bd5Y,9396
23
23
  ezmsg/sigproc/synth.py,sha256=kobTQwAyiNZLbpJIUNxI6SOiuKm0aca2G5pSjuB4K9E,18823
24
- ezmsg/sigproc/wavelets.py,sha256=bwlqyUESrOWAiI_gECbaKJj0PflnFMmuYtNridBTfNs,7955
25
- ezmsg/sigproc/window.py,sha256=eIxdtni6BwDmpko6m_WJnUDO3VHb5y7jM6nU9WyzIsM,13056
24
+ ezmsg/sigproc/wavelets.py,sha256=ZC92ZAK7TOlR5ub_gn5fZgNfSRXDlm1mSXXwMTgX2Qs,8070
25
+ ezmsg/sigproc/window.py,sha256=xigNWFy5GXhYR963onxW8ZcwGVsPoCPL4y3u8R2w5vU,14145
26
26
  ezmsg/sigproc/math/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  ezmsg/sigproc/math/abs.py,sha256=Hjnn2x8djWLUDVqvxRC4V7jOvgEtdU6CCHGpfVZ0uOY,931
28
28
  ezmsg/sigproc/math/clip.py,sha256=UpFLP_IkrY6bo4WcIpO4Czbttg7n6hybAihOGy-3nGs,1144
@@ -30,7 +30,10 @@ ezmsg/sigproc/math/difference.py,sha256=I96Md2pyfCU5TSw35u1uS6FaPJRv0JpHQVhrTHv1
30
30
  ezmsg/sigproc/math/invert.py,sha256=T6PEhBCquKRJQF6LfUgqjXKSBsS_tvi4BJAqJ_5VCjw,895
31
31
  ezmsg/sigproc/math/log.py,sha256=i-iF2WDH18RWapYwlhAsiDdhzvuJY9mDWrNtjfuiyL8,1572
32
32
  ezmsg/sigproc/math/scale.py,sha256=lQKF02Mv9nTWfgc4OcMpQMCx993p57GSN-AADeO2fjY,1063
33
- ezmsg_sigproc-1.7.0.dist-info/METADATA,sha256=Nnz-8hSZBErjldQf3kxBfZ7xEtr9Woi9boBBVFtZ3DI,2343
34
- ezmsg_sigproc-1.7.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
35
- ezmsg_sigproc-1.7.0.dist-info/licenses/LICENSE.txt,sha256=seu0tKhhAMPCUgc1XpXGGaCxY1YaYvFJwqFuQZAl2go,1100
36
- ezmsg_sigproc-1.7.0.dist-info/RECORD,,
33
+ ezmsg/sigproc/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
+ ezmsg/sigproc/util/profile.py,sha256=EZZ-GsvcgJEt1zP6bM26GfdKu7vhaqFtqFFGsM_AeC8,4670
35
+ ezmsg/sigproc/util/sparse.py,sha256=8Ke0jh3jRPi_TwIdLTwLdojQiaqPs6QV-Edqpx81VoI,1036
36
+ ezmsg_sigproc-1.7.1.dist-info/METADATA,sha256=XSNu-Ve1AEJhTezEEJ-VMdtnV82Gg_tCf6X9Z7RgB4s,2410
37
+ ezmsg_sigproc-1.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
38
+ ezmsg_sigproc-1.7.1.dist-info/licenses/LICENSE.txt,sha256=seu0tKhhAMPCUgc1XpXGGaCxY1YaYvFJwqFuQZAl2go,1100
39
+ ezmsg_sigproc-1.7.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.26.3
2
+ Generator: hatchling 1.27.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any