ezmsg-sigproc 1.4.2__py3-none-any.whl → 1.5.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.4.2'
16
- __version_tuple__ = version_tuple = (1, 4, 2)
15
+ __version__ = version = '1.5.0'
16
+ __version_tuple__ = version_tuple = (1, 5, 0)
@@ -1,10 +1,9 @@
1
- from dataclasses import replace
2
1
  import typing
3
2
 
4
3
  import numpy as np
5
4
  import scipy.special
6
5
  import ezmsg.core as ez
7
- from ezmsg.util.messages.axisarray import AxisArray
6
+ from ezmsg.util.messages.axisarray import AxisArray, replace
8
7
  from ezmsg.util.generator import consumer
9
8
 
10
9
  from .spectral import OptionsEnum
@@ -1,4 +1,3 @@
1
- from dataclasses import replace
2
1
  import os
3
2
  from pathlib import Path
4
3
  import typing
@@ -6,7 +5,7 @@ import typing
6
5
  import numpy as np
7
6
  import numpy.typing as npt
8
7
  import ezmsg.core as ez
9
- from ezmsg.util.messages.axisarray import AxisArray
8
+ from ezmsg.util.messages.axisarray import AxisArray, AxisBase, replace
10
9
  from ezmsg.util.generator import consumer
11
10
 
12
11
  from .base import GenAxisArray
@@ -47,7 +46,7 @@ def affine_transform(
47
46
 
48
47
  # State variables
49
48
  # New axis with transformed labels, if required
50
- new_axis: typing.Optional[AxisArray.Axis] = None
49
+ new_axis: typing.Optional[AxisBase] = None
51
50
 
52
51
  # Reset if any of these change.
53
52
  check_input = {"key": None}
@@ -71,10 +70,10 @@ def affine_transform(
71
70
  # Determine if we need to modify the transformed axis.
72
71
  if (
73
72
  axis in msg_in.axes
74
- and hasattr(msg_in.axes[axis], "labels")
73
+ and hasattr(msg_in.axes[axis], "data")
75
74
  and weights.shape[0] != weights.shape[1]
76
75
  ):
77
- in_labels = msg_in.axes[axis].labels
76
+ in_labels = msg_in.axes[axis].data
78
77
  new_labels = []
79
78
  n_in, n_out = weights.shape
80
79
  if len(in_labels) != n_in:
@@ -101,8 +100,8 @@ def affine_transform(
101
100
  new_labels.append("")
102
101
  elif np.all(b_filled_outputs):
103
102
  # Transform is dropping some of the inputs.
104
- new_labels = np.array(in_labels)[b_used_inputs].tolist()
105
- new_axis = replace(msg_in.axes[axis], labels=new_labels)
103
+ new_labels = np.array(in_labels)[b_used_inputs]
104
+ new_axis = replace(msg_in.axes[axis], data=np.array(new_labels))
106
105
 
107
106
  data = msg_in.data
108
107
 
@@ -1,11 +1,15 @@
1
- from dataclasses import replace
2
1
  import typing
3
2
 
4
3
  import numpy as np
5
4
  import numpy.typing as npt
6
5
  import ezmsg.core as ez
7
6
  from ezmsg.util.generator import consumer
8
- from ezmsg.util.messages.axisarray import AxisArray, slice_along_axis
7
+ from ezmsg.util.messages.axisarray import (
8
+ AxisArray,
9
+ slice_along_axis,
10
+ AxisBase,
11
+ replace,
12
+ )
9
13
 
10
14
  from .spectral import OptionsEnum
11
15
  from .base import GenAxisArray
@@ -72,11 +76,11 @@ def ranged_aggregate(
72
76
 
73
77
  # State variables
74
78
  slices: typing.Optional[typing.List[typing.Tuple[typing.Any, ...]]] = None
75
- out_axis: typing.Optional[AxisArray.Axis] = None
79
+ out_axis: typing.Optional[AxisBase] = None
76
80
  ax_vec: typing.Optional[npt.NDArray] = None
77
81
 
78
82
  # Reset if any of these changes. Key not checked because continuity between chunks not required.
79
- check_inputs = {"gain": None, "offset": None}
83
+ check_inputs = {"gain": None, "offset": None, "len": None, "key": None}
80
84
 
81
85
  while True:
82
86
  msg_in: AxisArray = yield msg_out
@@ -86,35 +90,52 @@ def ranged_aggregate(
86
90
  axis = axis or msg_in.dims[0]
87
91
  target_axis = msg_in.get_axis(axis)
88
92
 
89
- b_reset = target_axis.gain != check_inputs["gain"]
90
- b_reset = b_reset or target_axis.offset != check_inputs["offset"]
93
+ # Check if we need to reset state
94
+ b_reset = msg_in.key != check_inputs["key"]
95
+ if hasattr(target_axis, "data"):
96
+ b_reset = b_reset or len(target_axis.data) != check_inputs["len"]
97
+ elif isinstance(target_axis, AxisArray.LinearAxis):
98
+ b_reset = b_reset or target_axis.gain != check_inputs["gain"]
99
+ b_reset = b_reset or target_axis.offset != check_inputs["offset"]
100
+
91
101
  if b_reset:
92
- check_inputs["gain"] = target_axis.gain
93
- check_inputs["offset"] = target_axis.offset
102
+ # Update check variables
103
+ check_inputs["key"] = msg_in.key
104
+ if hasattr(target_axis, "data"):
105
+ check_inputs["len"] = len(target_axis.data)
106
+ else:
107
+ check_inputs["gain"] = target_axis.gain
108
+ check_inputs["offset"] = target_axis.offset
94
109
 
95
110
  # If the axis we are operating on has changed (e.g., "time" or "win" axis always changes),
96
111
  # or the key has changed, then recalculate slices.
97
112
 
98
113
  ax_idx = msg_in.get_axis_idx(axis)
99
114
 
100
- ax_vec = (
101
- target_axis.offset
102
- + np.arange(msg_in.data.shape[ax_idx]) * target_axis.gain
103
- )
115
+ if hasattr(target_axis, "data"):
116
+ ax_vec = target_axis.data
117
+ else:
118
+ ax_vec = target_axis.value(np.arange(msg_in.data.shape[ax_idx]))
119
+
104
120
  slices = []
105
- mids = []
121
+ ax_dat = []
106
122
  for start, stop in bands:
107
123
  inds = np.where(np.logical_and(ax_vec >= start, ax_vec <= stop))[0]
108
- mids.append(np.mean(inds) * target_axis.gain + target_axis.offset)
109
124
  slices.append(np.s_[inds[0] : inds[-1] + 1])
110
- out_ax_kwargs = {
111
- "unit": target_axis.unit,
112
- "offset": mids[0],
113
- "gain": (mids[1] - mids[0]) if len(mids) > 1 else 1.0,
114
- }
115
- if hasattr(target_axis, "labels"):
116
- out_ax_kwargs["labels"] = [f"{_[0]} - {_[1]}" for _ in bands]
117
- out_axis = replace(target_axis, **out_ax_kwargs)
125
+ if hasattr(target_axis, "data"):
126
+ if ax_vec.dtype.type is np.str_:
127
+ sl_dat = f"{ax_vec[start]} - {ax_vec[stop]}"
128
+ else:
129
+ sl_dat = ax_dat.append(np.mean(ax_vec[inds]))
130
+ else:
131
+ sl_dat = target_axis.value(np.mean(inds))
132
+ ax_dat.append(sl_dat)
133
+
134
+ out_axis = AxisArray.CoordinateAxis(
135
+ data=np.array(ax_dat),
136
+ dims=[axis],
137
+ unit=target_axis.unit,
138
+ )
118
139
 
119
140
  agg_func = AGGREGATORS[operation]
120
141
  out_data = [
@@ -73,6 +73,7 @@ def butter(
73
73
 
74
74
  Args:
75
75
  axis: The name of the axis to filter.
76
+ Note: The axis must be represented in the message .axes and be of type AxisArray.LinearAxis.
76
77
  order: Filter order.
77
78
  cuton: Corner frequency of the filter in Hz.
78
79
  cutoff: Corner frequency of the filter in Hz.
@@ -1,8 +1,11 @@
1
- from dataclasses import replace
2
1
  import typing
3
2
 
4
3
  import numpy as np
5
- from ezmsg.util.messages.axisarray import AxisArray, slice_along_axis
4
+ from ezmsg.util.messages.axisarray import (
5
+ AxisArray,
6
+ slice_along_axis,
7
+ replace,
8
+ )
6
9
  from ezmsg.util.generator import consumer
7
10
  import ezmsg.core as ez
8
11
 
@@ -22,6 +25,7 @@ def downsample(
22
25
 
23
26
  Args:
24
27
  axis: The name of the axis along which to downsample.
28
+ Note: The axis must exist in the message .axes and be of type AxisArray.LinearAxis.
25
29
  factor: Downsampling factor.
26
30
 
27
31
  Returns:
@@ -1,9 +1,8 @@
1
1
  import asyncio
2
- from dataclasses import replace
3
2
  import typing
4
3
 
5
4
  import ezmsg.core as ez
6
- from ezmsg.util.messages.axisarray import AxisArray
5
+ from ezmsg.util.messages.axisarray import AxisArray, replace
7
6
  import numpy as np
8
7
 
9
8
  from .window import Window, WindowSettings
ezmsg/sigproc/filter.py CHANGED
@@ -1,9 +1,9 @@
1
1
  import asyncio
2
- from dataclasses import dataclass, replace, field
2
+ from dataclasses import dataclass, field
3
3
  import typing
4
4
 
5
5
  import ezmsg.core as ez
6
- from ezmsg.util.messages.axisarray import AxisArray
6
+ from ezmsg.util.messages.axisarray import AxisArray, replace
7
7
  from ezmsg.util.generator import consumer
8
8
  import numpy as np
9
9
  import numpy.typing as npt
@@ -1,4 +1,3 @@
1
- from dataclasses import replace
2
1
  import functools
3
2
  import math
4
3
  import typing
@@ -9,7 +8,7 @@ import scipy.fft as sp_fft
9
8
  from scipy.special import lambertw
10
9
  import numpy.typing as npt
11
10
  import ezmsg.core as ez
12
- from ezmsg.util.messages.axisarray import AxisArray
11
+ from ezmsg.util.messages.axisarray import AxisArray, replace
13
12
  from ezmsg.util.generator import consumer
14
13
 
15
14
  from .base import GenAxisArray
ezmsg/sigproc/math/abs.py CHANGED
@@ -1,10 +1,9 @@
1
- from dataclasses import replace
2
1
  import typing
3
2
 
4
3
  import numpy as np
5
4
  import ezmsg.core as ez
6
5
  from ezmsg.util.generator import consumer
7
- from ezmsg.util.messages.axisarray import AxisArray
6
+ from ezmsg.util.messages.axisarray import AxisArray, replace
8
7
 
9
8
  from ..base import GenAxisArray
10
9
 
@@ -1,10 +1,9 @@
1
- from dataclasses import replace
2
1
  import typing
3
2
 
4
3
  import numpy as np
5
4
  import ezmsg.core as ez
6
5
  from ezmsg.util.generator import consumer
7
- from ezmsg.util.messages.axisarray import AxisArray
6
+ from ezmsg.util.messages.axisarray import AxisArray, replace
8
7
 
9
8
  from ..base import GenAxisArray
10
9
 
@@ -1,10 +1,9 @@
1
- from dataclasses import replace
2
1
  import typing
3
2
 
4
3
  import numpy as np
5
4
  import ezmsg.core as ez
6
5
  from ezmsg.util.generator import consumer
7
- from ezmsg.util.messages.axisarray import AxisArray
6
+ from ezmsg.util.messages.axisarray import AxisArray, replace
8
7
 
9
8
  from ..base import GenAxisArray
10
9
 
@@ -1,10 +1,9 @@
1
- from dataclasses import replace
2
1
  import typing
3
2
 
4
3
  import numpy as np
5
4
  import ezmsg.core as ez
6
5
  from ezmsg.util.generator import consumer
7
- from ezmsg.util.messages.axisarray import AxisArray
6
+ from ezmsg.util.messages.axisarray import AxisArray, replace
8
7
 
9
8
  from ..base import GenAxisArray
10
9
 
ezmsg/sigproc/math/log.py CHANGED
@@ -1,10 +1,9 @@
1
- from dataclasses import replace
2
1
  import typing
3
2
 
4
3
  import numpy as np
5
4
  import ezmsg.core as ez
6
5
  from ezmsg.util.generator import consumer
7
- from ezmsg.util.messages.axisarray import AxisArray
6
+ from ezmsg.util.messages.axisarray import AxisArray, replace
8
7
 
9
8
  from ..base import GenAxisArray
10
9
 
@@ -1,10 +1,9 @@
1
- from dataclasses import replace
2
1
  import typing
3
2
 
4
3
  import numpy as np
5
4
  import ezmsg.core as ez
6
5
  from ezmsg.util.generator import consumer
7
- from ezmsg.util.messages.axisarray import AxisArray
6
+ from ezmsg.util.messages.axisarray import AxisArray, replace
8
7
 
9
8
  from ..base import GenAxisArray
10
9
 
ezmsg/sigproc/messages.py CHANGED
@@ -26,5 +26,5 @@ def TSMessage(
26
26
  dims[time_dim] = "time"
27
27
  offset = time.time() if timestamp is None else timestamp
28
28
  offset_adj = data.shape[time_dim] / fs # offset corresponds to idx[0] on time_dim
29
- axis = AxisArray.Axis.TimeAxis(fs, offset=offset - offset_adj)
29
+ axis = AxisArray.TimeAxis(fs, offset=offset - offset_adj)
30
30
  return AxisArray(data, dims=dims, axes=dict(time=axis))
ezmsg/sigproc/sampler.py CHANGED
@@ -1,13 +1,17 @@
1
1
  import asyncio # Dev/test apparatus
2
2
  from collections import deque
3
- from dataclasses import dataclass, replace, field
3
+ from dataclasses import dataclass, field
4
4
  import time
5
5
  import typing
6
6
 
7
7
  import numpy as np
8
8
  import numpy.typing as npt
9
9
  import ezmsg.core as ez
10
- from ezmsg.util.messages.axisarray import AxisArray, slice_along_axis
10
+ from ezmsg.util.messages.axisarray import (
11
+ AxisArray,
12
+ slice_along_axis,
13
+ replace,
14
+ )
11
15
  from ezmsg.util.generator import consumer
12
16
 
13
17
 
@@ -52,6 +56,7 @@ def sampler(
52
56
  need a buffer of 0.5 + (1.5 - -1.0) = 3.0 seconds. It is best to at least double your estimate if memory allows.
53
57
  axis: The axis along which to sample the data.
54
58
  None (default) will choose the first axis in the first input.
59
+ Note: (for now) the axis must exist in the msg .axes and be of type AxisArray.LinearAxis
55
60
  period: The period in seconds during which to sample the data.
56
61
  Defaults to None. Only used if not None and the trigger message does not define its own period.
57
62
  value: The value to sample. Defaults to None.
ezmsg/sigproc/scaler.py CHANGED
@@ -1,10 +1,9 @@
1
- from dataclasses import replace
2
1
  import typing
3
2
 
4
3
  import numpy as np
5
4
  import numpy.typing as npt
6
5
  import ezmsg.core as ez
7
- from ezmsg.util.messages.axisarray import AxisArray
6
+ from ezmsg.util.messages.axisarray import AxisArray, replace
8
7
  from ezmsg.util.generator import consumer
9
8
 
10
9
  from .base import GenAxisArray
@@ -87,6 +86,7 @@ def scaler_np(
87
86
  Args:
88
87
  time_constant: Decay constant `tau` in seconds.
89
88
  axis: The name of the axis to accumulate statistics over.
89
+ Note: The axis must be in the msg.axes and be of type AxisArray.LinearAxis.
90
90
 
91
91
  Returns:
92
92
  A primed generator object that expects to be sent a :obj:`AxisArray` via `.send(axis_array)`
@@ -1,8 +1,7 @@
1
- from dataclasses import replace
2
1
  import typing
3
2
 
4
3
  import ezmsg.core as ez
5
- from ezmsg.util.messages.axisarray import AxisArray
4
+ from ezmsg.util.messages.axisarray import AxisArray, replace
6
5
  import numpy as np
7
6
  import numpy.typing as npt
8
7
 
ezmsg/sigproc/slicer.py CHANGED
@@ -1,10 +1,14 @@
1
- from dataclasses import replace
2
1
  import typing
3
2
 
4
3
  import numpy as np
5
4
  import numpy.typing as npt
6
5
  import ezmsg.core as ez
7
- from ezmsg.util.messages.axisarray import AxisArray, slice_along_axis
6
+ from ezmsg.util.messages.axisarray import (
7
+ AxisArray,
8
+ slice_along_axis,
9
+ AxisBase,
10
+ replace,
11
+ )
8
12
  from ezmsg.util.generator import consumer
9
13
 
10
14
  from .base import GenAxisArray
@@ -15,7 +19,10 @@ Slicer:Select a subset of data along a particular axis.
15
19
  """
16
20
 
17
21
 
18
- def parse_slice(s: str) -> typing.Tuple[typing.Union[slice, int], ...]:
22
+ def parse_slice(
23
+ s: str,
24
+ axinfo: typing.Optional[AxisArray.CoordinateAxis] = None,
25
+ ) -> typing.Tuple[typing.Union[slice, int], ...]:
19
26
  """
20
27
  Parses a string representation of a slice and returns a tuple of slice objects.
21
28
 
@@ -26,9 +33,13 @@ def parse_slice(s: str) -> typing.Tuple[typing.Union[slice, int], ...]:
26
33
  - "5" (or any integer) -> (5,). Take only that item.
27
34
  applying this to a ndarray or AxisArray will drop the dimension.
28
35
  - A comma-separated list of the above -> a tuple of slices | ints
36
+ - A comma-separated list of values and axinfo is provided and is a CoordinateAxis -> a tuple of ints
29
37
 
30
38
  Args:
31
39
  s: The string representation of the slice.
40
+ axinfo: (Optional) If provided, and of type CoordinateAxis,
41
+ and `s` is a comma-separated list of values, then the values
42
+ in s will be checked against the values in axinfo.data.
32
43
 
33
44
  Returns:
34
45
  A tuple of slice objects and/or ints.
@@ -38,9 +49,15 @@ def parse_slice(s: str) -> typing.Tuple[typing.Union[slice, int], ...]:
38
49
  if "," not in s:
39
50
  parts = [part.strip() for part in s.split(":")]
40
51
  if len(parts) == 1:
52
+ if (
53
+ axinfo is not None
54
+ and hasattr(axinfo, "data")
55
+ and parts[0] in axinfo.data
56
+ ):
57
+ return tuple(np.where(axinfo.data == parts[0])[0])
41
58
  return (int(parts[0]),)
42
59
  return (slice(*(int(part.strip()) if part else None for part in parts)),)
43
- suplist = [parse_slice(_) for _ in s.split(",")]
60
+ suplist = [parse_slice(_, axinfo=axinfo) for _ in s.split(",")]
44
61
  return tuple([item for sublist in suplist for item in sublist])
45
62
 
46
63
 
@@ -64,7 +81,7 @@ def slicer(
64
81
 
65
82
  # State variables
66
83
  _slice: typing.Optional[typing.Union[slice, npt.NDArray]] = None
67
- new_axis: typing.Optional[AxisArray.Axis] = None
84
+ new_axis: typing.Optional[AxisBase] = None
68
85
  b_change_dims: bool = False # If number of dimensions changes when slicing
69
86
 
70
87
  # Reset if input changes
@@ -92,7 +109,7 @@ def slicer(
92
109
  b_change_dims = False
93
110
 
94
111
  # Calculate the slice
95
- _slices = parse_slice(selection)
112
+ _slices = parse_slice(selection, msg_in.axes.get(axis, None))
96
113
  if len(_slices) == 1:
97
114
  _slice = _slices[0]
98
115
  # Do we drop the sliced dimension?
@@ -107,12 +124,15 @@ def slicer(
107
124
  # Create the output axis.
108
125
  if (
109
126
  axis in msg_in.axes
110
- and hasattr(msg_in.axes[axis], "labels")
111
- and len(msg_in.axes[axis].labels) > 0
127
+ and hasattr(msg_in.axes[axis], "data")
128
+ and len(msg_in.axes[axis].data) > 0
112
129
  ):
113
- in_labels = np.array(msg_in.axes[axis].labels)
114
- new_labels = in_labels[_slice].tolist()
115
- new_axis = replace(msg_in.axes[axis], labels=new_labels)
130
+ in_data = np.array(msg_in.axes[axis].data)
131
+ if b_change_dims:
132
+ out_data = in_data[_slice : _slice + 1]
133
+ else:
134
+ out_data = in_data[_slice]
135
+ new_axis = replace(msg_in.axes[axis], data=out_data)
116
136
 
117
137
  replace_kwargs = {}
118
138
  if b_change_dims:
ezmsg/sigproc/spectrum.py CHANGED
@@ -1,11 +1,14 @@
1
- from dataclasses import replace
2
1
  import enum
3
2
  from functools import partial
4
3
  import typing
5
4
 
6
5
  import numpy as np
7
6
  import ezmsg.core as ez
8
- from ezmsg.util.messages.axisarray import AxisArray, slice_along_axis
7
+ from ezmsg.util.messages.axisarray import (
8
+ AxisArray,
9
+ slice_along_axis,
10
+ replace,
11
+ )
9
12
  from ezmsg.util.generator import consumer
10
13
 
11
14
  from .base import GenAxisArray
@@ -79,6 +82,7 @@ def spectrum(
79
82
 
80
83
  Args:
81
84
  axis: The name of the axis on which to calculate the spectrum.
85
+ Note: The axis must have an .axes entry of type LinearAxis, not CoordinateAxis.
82
86
  out_axis: The name of the new axis. Defaults to "freq".
83
87
  window: The :obj:`WindowFunction` to apply to the data slice prior to calculating the spectrum.
84
88
  transform: The :obj:`SpectralTransform` to apply to the spectral magnitude.
@@ -101,7 +105,7 @@ def spectrum(
101
105
  apply_window = window != WindowFunction.NONE
102
106
  do_fftshift &= output == SpectralOutput.FULL
103
107
  f_sl = slice(None)
104
- freq_axis: typing.Optional[AxisArray.Axis] = None
108
+ freq_axis: typing.Optional[AxisArray.LinearAxis] = None
105
109
  fftfun: typing.Optional[typing.Callable] = None
106
110
  f_transform: typing.Optional[typing.Callable] = None
107
111
  new_dims: typing.Optional[typing.List[str]] = None
@@ -174,7 +178,7 @@ def spectrum(
174
178
  freqs = np.fft.fftshift(freqs, axes=-1)
175
179
  freqs = freqs[f_sl]
176
180
  freqs = freqs.tolist() # To please type checking
177
- freq_axis = AxisArray.Axis(
181
+ freq_axis = AxisArray.LinearAxis(
178
182
  unit="Hz", gain=freqs[1] - freqs[0], offset=freqs[0]
179
183
  )
180
184
  if out_axis is None:
ezmsg/sigproc/synth.py CHANGED
@@ -1,12 +1,12 @@
1
1
  import asyncio
2
- from dataclasses import replace, field
2
+ from dataclasses import field
3
3
  import time
4
4
  from typing import Optional, Generator, AsyncGenerator, Union
5
5
 
6
6
  import numpy as np
7
7
  import ezmsg.core as ez
8
8
  from ezmsg.util.generator import consumer
9
- from ezmsg.util.messages.axisarray import AxisArray
9
+ from ezmsg.util.messages.axisarray import AxisArray, replace
10
10
 
11
11
  from .butterworthfilter import ButterworthFilter, ButterworthFilterSettings
12
12
  from .base import GenAxisArray
@@ -138,6 +138,17 @@ async def acounter(
138
138
 
139
139
  n_sent: int = 0 # It is convenient to know how many samples we have sent.
140
140
  clock_zero: float = time.time() # time associated with first sample
141
+ template = AxisArray(
142
+ data=np.array([[]]),
143
+ dims=["time", "ch"],
144
+ axes={
145
+ "time": AxisArray.TimeAxis(fs=fs),
146
+ "ch": AxisArray.CoordinateAxis(
147
+ data=np.array([f"Ch{_}" for _ in range(n_ch)]), dims=["ch"]
148
+ ),
149
+ },
150
+ key="acounter",
151
+ )
141
152
 
142
153
  while True:
143
154
  # 1. Sleep, if necessary, until we are at the end of the current block
@@ -167,10 +178,13 @@ async def acounter(
167
178
  # offset += clock_zero # ??
168
179
 
169
180
  # 4. yield output
170
- yield AxisArray(
171
- block_samp,
172
- dims=["time", "ch"],
173
- axes={"time": AxisArray.Axis.TimeAxis(fs=fs, offset=offset)},
181
+ yield replace(
182
+ template,
183
+ data=block_samp,
184
+ axes={
185
+ "time": replace(template.axes["time"], offset=offset),
186
+ "ch": template.axes["ch"],
187
+ },
174
188
  )
175
189
 
176
190
  # 5. Update state for next iteration (after next yield)
@@ -273,6 +287,7 @@ def sin(
273
287
 
274
288
  Args:
275
289
  axis: The name of the axis over which the sinusoid passes.
290
+ Note: The axis must exist in the msg.axes and be of type AxisArray.LinearAxis.
276
291
  freq: The frequency of the sinusoid, in Hz.
277
292
  amp: The amplitude of the sinusoid.
278
293
  phase: The initial phase of the sinusoid, in radians.
ezmsg/sigproc/wavelets.py CHANGED
@@ -1,11 +1,10 @@
1
- from dataclasses import replace
2
1
  import typing
3
2
 
4
3
  import numpy as np
5
4
  import numpy.typing as npt
6
5
  import pywt
7
6
  import ezmsg.core as ez
8
- from ezmsg.util.messages.axisarray import AxisArray
7
+ from ezmsg.util.messages.axisarray import AxisArray, replace
9
8
  from ezmsg.util.generator import consumer
10
9
 
11
10
  from .base import GenAxisArray
@@ -24,11 +23,12 @@ def cwt(
24
23
  The function is equivalent to the :obj:`pywt.cwt` function, but is designed to work with streaming data.
25
24
 
26
25
  Args:
27
- scales: The wavelet scales to use.
26
+ scales: The wavelet scales to use. Note: Scales will be sorted from largest to smallest.
28
27
  wavelet: Wavelet object or name of wavelet to use.
29
28
  min_phase: See filterbank MinPhaseMode for details.
30
29
  axis: The target axis for operation. Note that this will be moved to the -1th dimension
31
30
  because fft and matrix multiplication is much faster on the last axis.
31
+ This axis must be in the msg.axes and it must be of type AxisArray.LinearAxis.
32
32
 
33
33
  Returns:
34
34
  A primed Generator object that expects an :obj:`AxisArray` via `.send(axis_array)` of continuous data
@@ -37,7 +37,7 @@ def cwt(
37
37
  msg_out: typing.Optional[AxisArray] = None
38
38
 
39
39
  # Check parameters
40
- scales = np.array(scales)
40
+ scales = np.sort(scales)[::-1]
41
41
  assert np.all(scales > 0), "Scales must be positive."
42
42
  assert scales.ndim == 1, "Scales must be a 1D list, tuple, or array."
43
43
  if not isinstance(wavelet, (pywt.ContinuousWavelet, pywt.Wavelet)):
@@ -103,7 +103,6 @@ def cwt(
103
103
  pywt.scale2frequency(wavelet, scales, precision)
104
104
  / msg_in.axes[axis].gain
105
105
  )
106
- fstep = (freqs[1] - freqs[0]) if len(freqs) > 1 else 1.0
107
106
  # Create output template
108
107
  dummy_shape = in_shape + (len(scales), 0)
109
108
  template = AxisArray(
@@ -113,7 +112,9 @@ def cwt(
113
112
  dims=msg_in.dims[:ax_idx] + msg_in.dims[ax_idx + 1 :] + ["freq", axis],
114
113
  axes={
115
114
  **msg_in.axes,
116
- "freq": AxisArray.Axis("Hz", offset=freqs[0], gain=fstep),
115
+ "freq": AxisArray.CoordinateAxis(
116
+ unit="Hz", data=freqs, dims=["freq"]
117
+ ),
117
118
  },
118
119
  key=msg_in.key,
119
120
  )
ezmsg/sigproc/window.py CHANGED
@@ -1,4 +1,3 @@
1
- from dataclasses import replace
2
1
  import traceback
3
2
  import typing
4
3
 
@@ -9,6 +8,7 @@ from ezmsg.util.messages.axisarray import (
9
8
  AxisArray,
10
9
  slice_along_axis,
11
10
  sliding_win_oneaxis,
11
+ replace,
12
12
  )
13
13
  from ezmsg.util.generator import consumer
14
14
 
@@ -31,6 +31,7 @@ def windowing(
31
31
  Args:
32
32
  axis: The axis along which to segment windows.
33
33
  If None, defaults to the first dimension of the first seen AxisArray.
34
+ Note: The windowed axis must be an AxisArray.LinearAxis, not an AxisArray.CoordinateAxis.
34
35
  newaxis: New axis on which windows are delimited, immediately
35
36
  preceding the target windowed axis. The data length along newaxis may be 0 if
36
37
  this most recent push did not provide enough data for a new window.
@@ -78,7 +79,7 @@ def windowing(
78
79
  shift_deficit: int = 0
79
80
  b_1to1 = window_shift is None
80
81
  newaxis_warned: bool = b_1to1
81
- out_newaxis: typing.Optional[AxisArray.Axis] = None
82
+ out_newaxis: typing.Optional[AxisArray.LinearAxis] = None
82
83
  out_dims: typing.Optional[typing.List[str]] = None
83
84
 
84
85
  check_inputs = {"samp_shape": None, "fs": None, "key": None}
@@ -1,12 +1,11 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: ezmsg-sigproc
3
- Version: 1.4.2
3
+ Version: 1.5.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
- License-Expression: MIT
7
- License-File: LICENSE.txt
6
+ License: MIT
8
7
  Requires-Python: >=3.9
9
- Requires-Dist: ezmsg>=3.5.0
8
+ Requires-Dist: ezmsg>=3.6.0
10
9
  Requires-Dist: numpy>=1.26.0
11
10
  Requires-Dist: pywavelets>=1.6.0
12
11
  Requires-Dist: scipy>=1.13.1
@@ -0,0 +1,35 @@
1
+ ezmsg/sigproc/__init__.py,sha256=8K4IcOA3-pfzadoM6s2Sfg5460KlJUocGgyTJTJl96U,52
2
+ ezmsg/sigproc/__version__.py,sha256=OYzqgMEgfFG0au4hzbEdgYI-c7Hxo3wdBtrpEjK1RoY,411
3
+ ezmsg/sigproc/activation.py,sha256=4oDn_xPYRqG04-AEZPGXG7975WeKyljqTMAp86k6-X8,2619
4
+ ezmsg/sigproc/affinetransform.py,sha256=UGVV9FWrvegBigPujtBNa4uziOzhN3HKNCpf7fVFHoI,8778
5
+ ezmsg/sigproc/aggregate.py,sha256=DTFVzDHZRk-cdN3yjz3UmU5nG5dcyO_gWOqlIW9Z5AE,6347
6
+ ezmsg/sigproc/bandpower.py,sha256=QPktf3v7sojDHhwUVcK3UNPU-Dm4sf2QQw3qmYWZ1r0,2228
7
+ ezmsg/sigproc/base.py,sha256=vut0BLjgc0mxYRbs7tDd9XzwRFA2_GcsgXZmIYovR0Y,1248
8
+ ezmsg/sigproc/butterworthfilter.py,sha256=bKfObvceOAU8ThebEtWA8DOpC0E6dspDmrGINBS_zfU,5450
9
+ ezmsg/sigproc/decimate.py,sha256=zNxJXauE4Z54Yy7tpN9_oJuW0I0ppQRWhSpwb3Ri2gc,1473
10
+ ezmsg/sigproc/downsample.py,sha256=gvGOc05wJz6oicEXSf3BUJ6Kpc2seQ4UQioe4PA-LaI,3474
11
+ ezmsg/sigproc/ewmfilter.py,sha256=NRrHQmqODu0nre42mRCZWYEcG29UeAzWn9OdE_f1elE,4493
12
+ ezmsg/sigproc/filter.py,sha256=wzpz8ABPdCt_gN37wkdoBAmrMnO80kcwoPZ5Zxmv4sg,8301
13
+ ezmsg/sigproc/filterbank.py,sha256=NFGzetlpTPuzEBPmZSM83sZUXEjRoKNrojDdCvIhCUo,12420
14
+ ezmsg/sigproc/messages.py,sha256=4GKPJrXrybLoFCBxKv87Qz5e2ykq2loPOq0vxDMhhW0,950
15
+ ezmsg/sigproc/sampler.py,sha256=sHTj6eRwFc-JrGDpstHjJDClKdPrFVGLUGTz08QwYUc,12851
16
+ ezmsg/sigproc/scaler.py,sha256=8GUE1FEjW96h0CEyZWzXCR2vqUWK2rOObia2RYK_Xhg,6009
17
+ ezmsg/sigproc/signalinjector.py,sha256=yKWiL2C_qEhvcbMVp-0hvXs8SDfTUFB2xrvYPmPtSu8,2621
18
+ ezmsg/sigproc/slicer.py,sha256=SMBYJNjBR5BzFdbdXBeZ0q35_utVhqy0TG2B4daE6Xw,5814
19
+ ezmsg/sigproc/spectral.py,sha256=_2qO6as4Nesmc9V1WW2EXNMH5pPz8aVTEcIPOi4-g2o,322
20
+ ezmsg/sigproc/spectrogram.py,sha256=16By4VWln7UvZ8IV_TdNAFaJDgkzvVKXWpnsoZoiW1M,3146
21
+ ezmsg/sigproc/spectrum.py,sha256=Wwz2z6IZznzxeH8fS_pFgYxd45ctFp1RdmE7TWyUcDw,9513
22
+ ezmsg/sigproc/synth.py,sha256=5XUSqgdaSmulonmI7wk3r0C1FDYbeL_PfKYyy4KyPjg,18830
23
+ ezmsg/sigproc/wavelets.py,sha256=pusFCvIMxhCAg-6PQWp1bwvKcyyJLKwaOWhKCLqOTSE,6711
24
+ ezmsg/sigproc/window.py,sha256=7TaOI3Z-oS-3_dYT2Pt1WE0_wU2JhARZKpAqnYUr7bw,13183
25
+ ezmsg/sigproc/math/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ ezmsg/sigproc/math/abs.py,sha256=P4fVsBePU_lEO1mxKP-0ueGQmAuMKXFckviLzAqQkoE,895
27
+ ezmsg/sigproc/math/clip.py,sha256=uRPyyFIaaWc-JBtgb-eUxQnL5sbCql2FghtPOeNx4QA,1108
28
+ ezmsg/sigproc/math/difference.py,sha256=AlFicTeEOrUYvdbi-fm-YglrRCMNB8bMKEJdC_2ZN9A,2160
29
+ ezmsg/sigproc/math/invert.py,sha256=--pldt1XRHHPEY2gtbTFXVP1Q5rsjtht-BhkkFMFibE,859
30
+ ezmsg/sigproc/math/log.py,sha256=dTahVnsXqc-NYRBE149SzV3eNV637kLuoMOs6KHAjjg,1536
31
+ ezmsg/sigproc/math/scale.py,sha256=cGXzkiIW-TPHIgvNf8pimEF-e2zzTbRPnpMSuD37Kq4,1027
32
+ ezmsg_sigproc-1.5.0.dist-info/METADATA,sha256=ZvTUyFuKvj4h0ezxKgTFcY_71IHEaxFm2F9QGlZa644,2339
33
+ ezmsg_sigproc-1.5.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
34
+ ezmsg_sigproc-1.5.0.dist-info/licenses/LICENSE.txt,sha256=seu0tKhhAMPCUgc1XpXGGaCxY1YaYvFJwqFuQZAl2go,1100
35
+ ezmsg_sigproc-1.5.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.25.0
2
+ Generator: hatchling 1.26.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,35 +0,0 @@
1
- ezmsg/sigproc/__init__.py,sha256=8K4IcOA3-pfzadoM6s2Sfg5460KlJUocGgyTJTJl96U,52
2
- ezmsg/sigproc/__version__.py,sha256=tX44urJsC0QN5SJ51xsM0HeojA_8Xx7NXFEGAkYlNE4,411
3
- ezmsg/sigproc/activation.py,sha256=amvVjO-1BcChgK0VQi44i7-Tmz_2FK1xb-BTC52_DO0,2642
4
- ezmsg/sigproc/affinetransform.py,sha256=a5xt6LSymlV3gNSdCjWdtUoi9kOkWiYx6oeiGEIphYI,8802
5
- ezmsg/sigproc/aggregate.py,sha256=Wnvz1xXDQhdVkTfVPOmhdmjgIhR_YQhioo8Dwh4BaoE,5615
6
- ezmsg/sigproc/bandpower.py,sha256=QPktf3v7sojDHhwUVcK3UNPU-Dm4sf2QQw3qmYWZ1r0,2228
7
- ezmsg/sigproc/base.py,sha256=vut0BLjgc0mxYRbs7tDd9XzwRFA2_GcsgXZmIYovR0Y,1248
8
- ezmsg/sigproc/butterworthfilter.py,sha256=vTESZAAcQhWLI8yIMOApsGAYgjv2ekCZsTIliFjEkc8,5345
9
- ezmsg/sigproc/decimate.py,sha256=zNxJXauE4Z54Yy7tpN9_oJuW0I0ppQRWhSpwb3Ri2gc,1473
10
- ezmsg/sigproc/downsample.py,sha256=0TOhWkg7B9g3FDcewHINZiNuo28JyXRtgTYvZ7C8ImM,3384
11
- ezmsg/sigproc/ewmfilter.py,sha256=XzTmouRa7AnPjTiI32-oumwm4ZfOl6uPPUSldExNpAs,4516
12
- ezmsg/sigproc/filter.py,sha256=x3ytFowxg4O0QFQeMDFmQieV86H-6Q3Mkt3ThRIOh60,8301
13
- ezmsg/sigproc/filterbank.py,sha256=Rhs7AKWy36lrMlzftEbYPXPW-FwZZ-QUCEp_QFG9mFU,12443
14
- ezmsg/sigproc/messages.py,sha256=KpYCWWRD5itVAq0-Lf8qd7ErEE1A9qdm2p7O8Y4zEFA,955
15
- ezmsg/sigproc/sampler.py,sha256=Zv5eq9CLpE8ui4HyZzBEqxwqBf--IbexS7zBx3qKp3w,12733
16
- ezmsg/sigproc/scaler.py,sha256=2Hi4XWWvUtPOVOfIyX0Dr0cZEdbTobZOoMXKh9Vszlk,5944
17
- ezmsg/sigproc/signalinjector.py,sha256=T1yvigk9WYujYqubd15UAF6hNmSqS9J7rCzaPYENoEA,2644
18
- ezmsg/sigproc/slicer.py,sha256=ffawSqhO06lC5TXg_Ujg0eP2EuF98McQiUdlwxn3psI,5072
19
- ezmsg/sigproc/spectral.py,sha256=_2qO6as4Nesmc9V1WW2EXNMH5pPz8aVTEcIPOi4-g2o,322
20
- ezmsg/sigproc/spectrogram.py,sha256=16By4VWln7UvZ8IV_TdNAFaJDgkzvVKXWpnsoZoiW1M,3146
21
- ezmsg/sigproc/spectrum.py,sha256=a75d6q6o3dAuqX6ZuFv6AfHYDOMuhA01OVCugzFMUSQ,9415
22
- ezmsg/sigproc/synth.py,sha256=ygJRI9Seqhrg13lUhcA6XSpxcEr3LL1NupviSQLrC9o,18351
23
- ezmsg/sigproc/wavelets.py,sha256=_3jI52NzksD1WNnHx03F2p4V2uGN_MtBbLsJGRjeyoE,6596
24
- ezmsg/sigproc/window.py,sha256=A697XG2YyR9ywmzfxJIXovi6wPwDhBI9UUeShNJFKgM,13094
25
- ezmsg/sigproc/math/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- ezmsg/sigproc/math/abs.py,sha256=hqS_sbLqAKSlguj3_rsfeWzOe6233tMxXfjDPg1fi2g,918
27
- ezmsg/sigproc/math/clip.py,sha256=xIfc6fEfiNh7ILG3IYxcuCd5VBqmWXmDliGi_dKVJpM,1131
28
- ezmsg/sigproc/math/difference.py,sha256=s3al1WzUZXsG2XYzOprRcKP-VD9J5FwYx578_SKG0vg,2183
29
- ezmsg/sigproc/math/invert.py,sha256=H8df1u8OVb5TOkRC8p5EW7R17kHdLOYqXVH_axy5bkI,882
30
- ezmsg/sigproc/math/log.py,sha256=oot0tT0wVvdI0UlPOqj1tQLDn_gIxf8em_b4NfJRmW0,1559
31
- ezmsg/sigproc/math/scale.py,sha256=iIQuJAr5pzCiMqzi2YdzLc1agQBKBOcngdNZlLO3X_o,1050
32
- ezmsg_sigproc-1.4.2.dist-info/METADATA,sha256=islU0_cvDjX0iqp202X5_eD-Qu37_9wKsBOo_cKcV8c,2376
33
- ezmsg_sigproc-1.4.2.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
34
- ezmsg_sigproc-1.4.2.dist-info/licenses/LICENSE.txt,sha256=seu0tKhhAMPCUgc1XpXGGaCxY1YaYvFJwqFuQZAl2go,1100
35
- ezmsg_sigproc-1.4.2.dist-info/RECORD,,