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.
- ezmsg/sigproc/__version__.py +9 -4
- ezmsg/sigproc/bandpower.py +1 -0
- ezmsg/sigproc/base.py +3 -0
- ezmsg/sigproc/filterbank.py +2 -1
- ezmsg/sigproc/sampler.py +3 -0
- ezmsg/sigproc/signalinjector.py +3 -0
- ezmsg/sigproc/spectrogram.py +6 -1
- ezmsg/sigproc/util/__init__.py +0 -0
- ezmsg/sigproc/util/profile.py +131 -0
- ezmsg/sigproc/util/sparse.py +29 -0
- ezmsg/sigproc/wavelets.py +4 -3
- ezmsg/sigproc/window.py +57 -25
- {ezmsg_sigproc-1.7.0.dist-info → ezmsg_sigproc-1.7.1.dist-info}/METADATA +5 -3
- {ezmsg_sigproc-1.7.0.dist-info → ezmsg_sigproc-1.7.1.dist-info}/RECORD +16 -13
- {ezmsg_sigproc-1.7.0.dist-info → ezmsg_sigproc-1.7.1.dist-info}/WHEEL +1 -1
- {ezmsg_sigproc-1.7.0.dist-info → ezmsg_sigproc-1.7.1.dist-info}/licenses/LICENSE.txt +0 -0
ezmsg/sigproc/__version__.py
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
# file generated by
|
|
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
|
|
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.
|
|
16
|
-
__version_tuple__ = version_tuple = (1, 7,
|
|
20
|
+
__version__ = version = '1.7.1'
|
|
21
|
+
__version_tuple__ = version_tuple = (1, 7, 1)
|
ezmsg/sigproc/bandpower.py
CHANGED
|
@@ -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)
|
ezmsg/sigproc/filterbank.py
CHANGED
|
@@ -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
|
-
#
|
|
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:
|
ezmsg/sigproc/signalinjector.py
CHANGED
|
@@ -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
|
ezmsg/sigproc/spectrogram.py
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
147
|
-
# Adjust so first _new_ sample at index 0
|
|
148
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
216
|
+
win_offset = buffer_tvec[-window_samples]
|
|
185
217
|
elif buffer.shape[axis_idx] >= window_samples:
|
|
186
218
|
# Deterministic window shifts.
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: ezmsg-sigproc
|
|
3
|
-
Version: 1.7.
|
|
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=
|
|
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=
|
|
7
|
-
ezmsg/sigproc/base.py,sha256=
|
|
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=
|
|
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=
|
|
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
|
|
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=
|
|
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=
|
|
25
|
-
ezmsg/sigproc/window.py,sha256=
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
ezmsg_sigproc-1.7.
|
|
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,,
|
|
File without changes
|