ezmsg-baseproc 1.0.3__py3-none-any.whl → 1.2.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.
- ezmsg/baseproc/__init__.py +42 -0
- ezmsg/baseproc/__version__.py +2 -2
- ezmsg/baseproc/clock.py +109 -0
- ezmsg/baseproc/clockdriven.py +179 -0
- ezmsg/baseproc/counter.py +67 -0
- ezmsg/baseproc/units.py +48 -1
- ezmsg/baseproc/util/typeresolution.py +9 -1
- {ezmsg_baseproc-1.0.3.dist-info → ezmsg_baseproc-1.2.0.dist-info}/METADATA +19 -22
- ezmsg_baseproc-1.2.0.dist-info/RECORD +19 -0
- ezmsg_baseproc-1.0.3.dist-info/RECORD +0 -16
- {ezmsg_baseproc-1.0.3.dist-info → ezmsg_baseproc-1.2.0.dist-info}/WHEEL +0 -0
- {ezmsg_baseproc-1.0.3.dist-info → ezmsg_baseproc-1.2.0.dist-info}/licenses/LICENSE +0 -0
ezmsg/baseproc/__init__.py
CHANGED
|
@@ -7,6 +7,22 @@ signal processing pipelines in ezmsg.
|
|
|
7
7
|
|
|
8
8
|
from .__version__ import __version__ as __version__
|
|
9
9
|
|
|
10
|
+
# Clock and Counter
|
|
11
|
+
from .clock import (
|
|
12
|
+
Clock,
|
|
13
|
+
ClockProducer,
|
|
14
|
+
ClockSettings,
|
|
15
|
+
ClockState,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Clock-driven producers
|
|
19
|
+
from .clockdriven import (
|
|
20
|
+
BaseClockDrivenProducer,
|
|
21
|
+
ClockDrivenSettings,
|
|
22
|
+
ClockDrivenSettingsType,
|
|
23
|
+
ClockDrivenState,
|
|
24
|
+
)
|
|
25
|
+
|
|
10
26
|
# Composite processor classes
|
|
11
27
|
from .composite import (
|
|
12
28
|
CompositeProcessor,
|
|
@@ -14,6 +30,12 @@ from .composite import (
|
|
|
14
30
|
CompositeStateful,
|
|
15
31
|
_get_processor_message_type,
|
|
16
32
|
)
|
|
33
|
+
from .counter import (
|
|
34
|
+
Counter,
|
|
35
|
+
CounterSettings,
|
|
36
|
+
CounterTransformer,
|
|
37
|
+
CounterTransformerState,
|
|
38
|
+
)
|
|
17
39
|
|
|
18
40
|
# Base processor classes (non-stateful)
|
|
19
41
|
from .processor import (
|
|
@@ -60,15 +82,18 @@ from .stateful import (
|
|
|
60
82
|
from .units import (
|
|
61
83
|
AdaptiveTransformerType,
|
|
62
84
|
BaseAdaptiveTransformerUnit,
|
|
85
|
+
BaseClockDrivenProducerUnit,
|
|
63
86
|
BaseConsumerUnit,
|
|
64
87
|
BaseProcessorUnit,
|
|
65
88
|
BaseProducerUnit,
|
|
66
89
|
BaseTransformerUnit,
|
|
90
|
+
ClockDrivenProducerType,
|
|
67
91
|
ConsumerType,
|
|
68
92
|
GenAxisArray,
|
|
69
93
|
ProducerType,
|
|
70
94
|
TransformerType,
|
|
71
95
|
get_base_adaptive_transformer_type,
|
|
96
|
+
get_base_clockdriven_producer_type,
|
|
72
97
|
get_base_consumer_type,
|
|
73
98
|
get_base_producer_type,
|
|
74
99
|
get_base_transformer_type,
|
|
@@ -102,6 +127,7 @@ __all__ = [
|
|
|
102
127
|
"ConsumerType",
|
|
103
128
|
"TransformerType",
|
|
104
129
|
"AdaptiveTransformerType",
|
|
130
|
+
"ClockDrivenProducerType",
|
|
105
131
|
# Decorators
|
|
106
132
|
"processor_state",
|
|
107
133
|
# Base processor classes
|
|
@@ -117,6 +143,11 @@ __all__ = [
|
|
|
117
143
|
"BaseStatefulTransformer",
|
|
118
144
|
"BaseAdaptiveTransformer",
|
|
119
145
|
"BaseAsyncTransformer",
|
|
146
|
+
# Clock-driven producers
|
|
147
|
+
"BaseClockDrivenProducer",
|
|
148
|
+
"ClockDrivenSettings",
|
|
149
|
+
"ClockDrivenSettingsType",
|
|
150
|
+
"ClockDrivenState",
|
|
120
151
|
# Composite classes
|
|
121
152
|
"CompositeStateful",
|
|
122
153
|
"CompositeProcessor",
|
|
@@ -127,12 +158,14 @@ __all__ = [
|
|
|
127
158
|
"BaseConsumerUnit",
|
|
128
159
|
"BaseTransformerUnit",
|
|
129
160
|
"BaseAdaptiveTransformerUnit",
|
|
161
|
+
"BaseClockDrivenProducerUnit",
|
|
130
162
|
"GenAxisArray",
|
|
131
163
|
# Type resolution helpers
|
|
132
164
|
"get_base_producer_type",
|
|
133
165
|
"get_base_consumer_type",
|
|
134
166
|
"get_base_transformer_type",
|
|
135
167
|
"get_base_adaptive_transformer_type",
|
|
168
|
+
"get_base_clockdriven_producer_type",
|
|
136
169
|
"_get_base_processor_settings_type",
|
|
137
170
|
"_get_base_processor_message_in_type",
|
|
138
171
|
"_get_base_processor_message_out_type",
|
|
@@ -152,4 +185,13 @@ __all__ = [
|
|
|
152
185
|
# Type utilities
|
|
153
186
|
"check_message_type_compatibility",
|
|
154
187
|
"resolve_typevar",
|
|
188
|
+
# Clock and Counter
|
|
189
|
+
"Clock",
|
|
190
|
+
"ClockProducer",
|
|
191
|
+
"ClockSettings",
|
|
192
|
+
"ClockState",
|
|
193
|
+
"Counter",
|
|
194
|
+
"CounterSettings",
|
|
195
|
+
"CounterTransformer",
|
|
196
|
+
"CounterTransformerState",
|
|
155
197
|
]
|
ezmsg/baseproc/__version__.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.0
|
|
32
|
-
__version_tuple__ = version_tuple = (1,
|
|
31
|
+
__version__ = version = '1.2.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 2, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
ezmsg/baseproc/clock.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Clock generator for timing control."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import math
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import field
|
|
7
|
+
|
|
8
|
+
import ezmsg.core as ez
|
|
9
|
+
from ezmsg.util.messages.axisarray import AxisArray
|
|
10
|
+
|
|
11
|
+
from .protocols import processor_state
|
|
12
|
+
from .stateful import BaseStatefulProducer
|
|
13
|
+
from .units import BaseProducerUnit
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ClockSettings(ez.Settings):
|
|
17
|
+
"""Settings for :obj:`ClockProducer`."""
|
|
18
|
+
|
|
19
|
+
dispatch_rate: float = math.inf
|
|
20
|
+
"""
|
|
21
|
+
Dispatch rate in Hz.
|
|
22
|
+
- Finite value (e.g., 100.0): Dispatch 100 times per second
|
|
23
|
+
- math.inf: Dispatch as fast as possible (no sleep)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@processor_state
|
|
28
|
+
class ClockState:
|
|
29
|
+
"""State for :obj:`ClockProducer`."""
|
|
30
|
+
|
|
31
|
+
t_0: float = field(default_factory=time.monotonic)
|
|
32
|
+
"""Start time (monotonic)."""
|
|
33
|
+
|
|
34
|
+
n_dispatch: int = 0
|
|
35
|
+
"""Number of dispatches since reset."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ClockProducer(BaseStatefulProducer[ClockSettings, AxisArray.LinearAxis, ClockState]):
|
|
39
|
+
"""
|
|
40
|
+
Produces clock ticks at a specified rate.
|
|
41
|
+
|
|
42
|
+
Each tick outputs a :obj:`AxisArray.LinearAxis` containing:
|
|
43
|
+
- ``gain``: 1/dispatch_rate (seconds per tick), or 0.0 if dispatch_rate is infinite
|
|
44
|
+
- ``offset``: Wall clock timestamp (time.monotonic)
|
|
45
|
+
|
|
46
|
+
This output type allows downstream components (like Counter) to know both
|
|
47
|
+
the timing of the tick and the nominal dispatch rate.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def _reset_state(self) -> None:
|
|
51
|
+
"""Reset internal state."""
|
|
52
|
+
self._state.t_0 = time.monotonic()
|
|
53
|
+
self._state.n_dispatch = 0
|
|
54
|
+
|
|
55
|
+
def _make_output(self, timestamp: float) -> AxisArray.LinearAxis:
|
|
56
|
+
"""Create LinearAxis output with gain and offset."""
|
|
57
|
+
if math.isinf(self.settings.dispatch_rate):
|
|
58
|
+
gain = 0.0
|
|
59
|
+
else:
|
|
60
|
+
gain = 1.0 / self.settings.dispatch_rate
|
|
61
|
+
return AxisArray.LinearAxis(gain=gain, offset=timestamp)
|
|
62
|
+
|
|
63
|
+
def __call__(self) -> AxisArray.LinearAxis:
|
|
64
|
+
"""Synchronous clock production."""
|
|
65
|
+
if self._hash == -1:
|
|
66
|
+
self._reset_state()
|
|
67
|
+
self._hash = 0
|
|
68
|
+
|
|
69
|
+
now = time.monotonic()
|
|
70
|
+
if math.isfinite(self.settings.dispatch_rate):
|
|
71
|
+
target_time = self.state.t_0 + (self.state.n_dispatch + 1) / self.settings.dispatch_rate
|
|
72
|
+
if target_time > now:
|
|
73
|
+
time.sleep(target_time - now)
|
|
74
|
+
else:
|
|
75
|
+
target_time = now
|
|
76
|
+
|
|
77
|
+
self.state.n_dispatch += 1
|
|
78
|
+
return self._make_output(target_time)
|
|
79
|
+
|
|
80
|
+
async def _produce(self) -> AxisArray.LinearAxis:
|
|
81
|
+
"""Generate next clock tick."""
|
|
82
|
+
now = time.monotonic()
|
|
83
|
+
if math.isfinite(self.settings.dispatch_rate):
|
|
84
|
+
target_time = self.state.t_0 + (self.state.n_dispatch + 1) / self.settings.dispatch_rate
|
|
85
|
+
if target_time > now:
|
|
86
|
+
await asyncio.sleep(target_time - now)
|
|
87
|
+
else:
|
|
88
|
+
target_time = now
|
|
89
|
+
|
|
90
|
+
self.state.n_dispatch += 1
|
|
91
|
+
return self._make_output(target_time)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Clock(
|
|
95
|
+
BaseProducerUnit[
|
|
96
|
+
ClockSettings,
|
|
97
|
+
AxisArray.LinearAxis,
|
|
98
|
+
ClockProducer,
|
|
99
|
+
]
|
|
100
|
+
):
|
|
101
|
+
"""
|
|
102
|
+
Clock unit that produces ticks at a specified rate.
|
|
103
|
+
|
|
104
|
+
Output is a :obj:`AxisArray.LinearAxis` with:
|
|
105
|
+
- ``gain``: 1/dispatch_rate (seconds per tick)
|
|
106
|
+
- ``offset``: Wall clock timestamp
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
SETTINGS = ClockSettings
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Clock-driven producer base classes for generating data synchronized to clock ticks."""
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
|
|
6
|
+
import ezmsg.core as ez
|
|
7
|
+
from ezmsg.util.messages.axisarray import AxisArray, LinearAxis
|
|
8
|
+
|
|
9
|
+
from .protocols import StateType, processor_state
|
|
10
|
+
from .stateful import BaseStatefulProcessor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ClockDrivenSettings(ez.Settings):
|
|
14
|
+
"""
|
|
15
|
+
Base settings for clock-driven producers.
|
|
16
|
+
|
|
17
|
+
Subclass this to add your own settings while inheriting fs and n_time.
|
|
18
|
+
|
|
19
|
+
Example::
|
|
20
|
+
|
|
21
|
+
class SinGeneratorSettings(ClockDrivenSettings):
|
|
22
|
+
freq: float = 1.0
|
|
23
|
+
amp: float = 1.0
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
fs: float
|
|
27
|
+
"""Output sampling rate in Hz."""
|
|
28
|
+
|
|
29
|
+
n_time: int | None = None
|
|
30
|
+
"""
|
|
31
|
+
Samples per block.
|
|
32
|
+
- If specified: fixed chunk size (clock gain is ignored for determining chunk size)
|
|
33
|
+
- If None: derived from clock gain (fs * clock.gain), with fractional sample tracking
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Type variable for settings that extend ClockDrivenSettings
|
|
38
|
+
ClockDrivenSettingsType = typing.TypeVar("ClockDrivenSettingsType", bound=ClockDrivenSettings)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@processor_state
|
|
42
|
+
class ClockDrivenState:
|
|
43
|
+
"""
|
|
44
|
+
Internal state for clock-driven producers.
|
|
45
|
+
|
|
46
|
+
Tracks sample counting and fractional sample accumulation.
|
|
47
|
+
Subclasses should extend this if they need additional state.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
counter: int = 0
|
|
51
|
+
"""Current sample counter (total samples produced)."""
|
|
52
|
+
|
|
53
|
+
fractional_samples: float = 0.0
|
|
54
|
+
"""Accumulated fractional samples for variable chunk mode."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class BaseClockDrivenProducer(
|
|
58
|
+
BaseStatefulProcessor[ClockDrivenSettingsType, AxisArray.LinearAxis, AxisArray, StateType],
|
|
59
|
+
typing.Generic[ClockDrivenSettingsType, StateType],
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Base class for clock-driven data producers.
|
|
63
|
+
|
|
64
|
+
Accepts clock ticks (LinearAxis) as input and produces AxisArray output.
|
|
65
|
+
Handles all the timing/counter logic internally, so subclasses only need
|
|
66
|
+
to implement the data generation logic.
|
|
67
|
+
|
|
68
|
+
This eliminates the need for the Clock → Counter → Generator pattern
|
|
69
|
+
by combining the Counter functionality into the generator base class.
|
|
70
|
+
|
|
71
|
+
Subclasses must implement:
|
|
72
|
+
- ``_reset_state(time_axis)``: Initialize any state needed for production
|
|
73
|
+
- ``_produce(n_samples, time_axis)``: Generate the actual output data
|
|
74
|
+
|
|
75
|
+
Example::
|
|
76
|
+
|
|
77
|
+
@processor_state
|
|
78
|
+
class SinState(ClockDrivenState):
|
|
79
|
+
ang_freq: float = 0.0
|
|
80
|
+
|
|
81
|
+
class SinProducer(BaseClockDrivenProducer[SinSettings, SinState]):
|
|
82
|
+
def _reset_state(self, time_axis: AxisArray.TimeAxis) -> None:
|
|
83
|
+
self._state.ang_freq = 2 * np.pi * self.settings.fs
|
|
84
|
+
|
|
85
|
+
def _produce(self, n_samples: int, time_axis: AxisArray.TimeAxis) -> AxisArray:
|
|
86
|
+
t = (np.arange(n_samples) + self._state.counter) * time_axis.gain
|
|
87
|
+
data = np.sin(self._state.ang_freq * t)
|
|
88
|
+
return AxisArray(data=data, dims=["time"], axes={"time": time_axis})
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def _hash_message(self, message: AxisArray.LinearAxis) -> int:
|
|
92
|
+
# Return constant hash - state should not reset based on clock rate changes.
|
|
93
|
+
# The producer maintains continuity regardless of clock rate changes.
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
def _compute_samples_and_offset(self, clock_tick: AxisArray.LinearAxis) -> tuple[int, float] | None:
|
|
97
|
+
"""
|
|
98
|
+
Compute number of samples and time offset from a clock tick.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Tuple of (n_samples, offset) or None if no samples to produce yet.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: If clock gain is 0 (AFAP mode) and n_time is not specified.
|
|
105
|
+
"""
|
|
106
|
+
if self.settings.n_time is not None:
|
|
107
|
+
# Fixed chunk size mode
|
|
108
|
+
n_samples = self.settings.n_time
|
|
109
|
+
if clock_tick.gain == 0.0:
|
|
110
|
+
# AFAP mode - synthetic offset based on counter
|
|
111
|
+
offset = self._state.counter / self.settings.fs
|
|
112
|
+
else:
|
|
113
|
+
# Use clock's timestamp
|
|
114
|
+
offset = clock_tick.offset
|
|
115
|
+
else:
|
|
116
|
+
# Variable chunk size mode - derive from clock gain
|
|
117
|
+
if clock_tick.gain == 0.0:
|
|
118
|
+
raise ValueError("Cannot use clock with gain=0 (AFAP) without specifying n_time")
|
|
119
|
+
|
|
120
|
+
# Calculate samples including fractional accumulation
|
|
121
|
+
samples_float = self.settings.fs * clock_tick.gain + self._state.fractional_samples
|
|
122
|
+
n_samples = int(samples_float + 1e-9)
|
|
123
|
+
self._state.fractional_samples = samples_float - n_samples
|
|
124
|
+
|
|
125
|
+
if n_samples == 0:
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
offset = clock_tick.offset
|
|
129
|
+
|
|
130
|
+
return n_samples, offset
|
|
131
|
+
|
|
132
|
+
@abstractmethod
|
|
133
|
+
def _reset_state(self, time_axis: LinearAxis) -> None:
|
|
134
|
+
"""
|
|
135
|
+
Reset/initialize state for production.
|
|
136
|
+
|
|
137
|
+
Called once before the first call to _produce, or when state needs resetting.
|
|
138
|
+
Use this to pre-compute values, create templates, etc.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
time_axis: TimeAxis with the output sampling rate (fs) and initial offset.
|
|
142
|
+
"""
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
@abstractmethod
|
|
146
|
+
def _produce(self, n_samples: int, time_axis: LinearAxis) -> AxisArray:
|
|
147
|
+
"""
|
|
148
|
+
Generate output data for this chunk.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
n_samples: Number of samples to generate.
|
|
152
|
+
time_axis: TimeAxis with correct offset and gain (1/fs) for this chunk.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
AxisArray containing the generated data. The time axis should use
|
|
156
|
+
the provided time_axis or one derived from it.
|
|
157
|
+
"""
|
|
158
|
+
...
|
|
159
|
+
|
|
160
|
+
def _process(self, clock_tick: LinearAxis) -> AxisArray | None:
|
|
161
|
+
"""
|
|
162
|
+
Process a clock tick and produce output.
|
|
163
|
+
|
|
164
|
+
Handles all the counter/timing logic internally, then calls _produce.
|
|
165
|
+
"""
|
|
166
|
+
result = self._compute_samples_and_offset(clock_tick)
|
|
167
|
+
if result is None:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
n_samples, offset = result
|
|
171
|
+
time_axis = AxisArray.TimeAxis(fs=self.settings.fs, offset=offset)
|
|
172
|
+
|
|
173
|
+
# Call subclass production method
|
|
174
|
+
output = self._produce(n_samples, time_axis)
|
|
175
|
+
|
|
176
|
+
# Update counter
|
|
177
|
+
self._state.counter += n_samples
|
|
178
|
+
|
|
179
|
+
return output
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Counter generator for sample counting and timing."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ezmsg.util.messages.axisarray import AxisArray, LinearAxis, replace
|
|
5
|
+
|
|
6
|
+
from .clockdriven import (
|
|
7
|
+
BaseClockDrivenProducer,
|
|
8
|
+
ClockDrivenSettings,
|
|
9
|
+
ClockDrivenState,
|
|
10
|
+
)
|
|
11
|
+
from .protocols import processor_state
|
|
12
|
+
from .units import BaseClockDrivenProducerUnit
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CounterSettings(ClockDrivenSettings):
|
|
16
|
+
"""Settings for :obj:`Counter` and :obj:`CounterTransformer`."""
|
|
17
|
+
|
|
18
|
+
mod: int | None = None
|
|
19
|
+
"""If set, counter values rollover at this modulus."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@processor_state
|
|
23
|
+
class CounterTransformerState(ClockDrivenState):
|
|
24
|
+
"""State for :obj:`CounterTransformer`."""
|
|
25
|
+
|
|
26
|
+
template: AxisArray | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CounterTransformer(BaseClockDrivenProducer[CounterSettings, CounterTransformerState]):
|
|
30
|
+
"""
|
|
31
|
+
Transforms clock ticks (LinearAxis) into AxisArray counter values.
|
|
32
|
+
|
|
33
|
+
Each clock tick produces a block of counter values. The block size is either
|
|
34
|
+
fixed (n_time setting) or derived from the clock's gain (fs * gain).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def _reset_state(self, time_axis: LinearAxis) -> None:
|
|
38
|
+
"""Reset state - initialize template for counter output."""
|
|
39
|
+
self._state.template = AxisArray(
|
|
40
|
+
data=np.array([], dtype=int),
|
|
41
|
+
dims=["time"],
|
|
42
|
+
axes={"time": time_axis},
|
|
43
|
+
key="counter",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def _produce(self, n_samples: int, time_axis: LinearAxis) -> AxisArray:
|
|
47
|
+
"""Generate counter values for this chunk."""
|
|
48
|
+
# Generate counter data (using pre-increment counter value)
|
|
49
|
+
block_samp = np.arange(self._state.counter, self._state.counter + n_samples)
|
|
50
|
+
if self.settings.mod is not None:
|
|
51
|
+
block_samp = block_samp % self.settings.mod
|
|
52
|
+
|
|
53
|
+
return replace(
|
|
54
|
+
self._state.template,
|
|
55
|
+
data=block_samp,
|
|
56
|
+
axes={"time": time_axis},
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Counter(BaseClockDrivenProducerUnit[CounterSettings, CounterTransformer]):
|
|
61
|
+
"""
|
|
62
|
+
Transforms clock ticks into monotonically increasing counter values as AxisArray.
|
|
63
|
+
|
|
64
|
+
Receives timing from INPUT_CLOCK (LinearAxis from Clock) and outputs AxisArray.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
SETTINGS = CounterSettings
|
ezmsg/baseproc/units.py
CHANGED
|
@@ -7,8 +7,9 @@ from abc import ABC, abstractmethod
|
|
|
7
7
|
|
|
8
8
|
import ezmsg.core as ez
|
|
9
9
|
from ezmsg.util.generator import GenState
|
|
10
|
-
from ezmsg.util.messages.axisarray import AxisArray
|
|
10
|
+
from ezmsg.util.messages.axisarray import AxisArray, LinearAxis
|
|
11
11
|
|
|
12
|
+
from .clockdriven import BaseClockDrivenProducer
|
|
12
13
|
from .composite import CompositeProcessor
|
|
13
14
|
from .processor import BaseConsumer, BaseProducer, BaseTransformer
|
|
14
15
|
from .protocols import MessageInType, MessageOutType, SettingsType
|
|
@@ -25,6 +26,7 @@ TransformerType = typing.TypeVar(
|
|
|
25
26
|
bound=BaseTransformer | BaseStatefulTransformer | CompositeProcessor,
|
|
26
27
|
)
|
|
27
28
|
AdaptiveTransformerType = typing.TypeVar("AdaptiveTransformerType", bound=BaseAdaptiveTransformer)
|
|
29
|
+
ClockDrivenProducerType = typing.TypeVar("ClockDrivenProducerType", bound=BaseClockDrivenProducer)
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
def get_base_producer_type(cls: type) -> type:
|
|
@@ -43,6 +45,10 @@ def get_base_adaptive_transformer_type(cls: type) -> type:
|
|
|
43
45
|
return resolve_typevar(cls, AdaptiveTransformerType)
|
|
44
46
|
|
|
45
47
|
|
|
48
|
+
def get_base_clockdriven_producer_type(cls: type) -> type:
|
|
49
|
+
return resolve_typevar(cls, ClockDrivenProducerType)
|
|
50
|
+
|
|
51
|
+
|
|
46
52
|
# --- Base classes for ezmsg Unit with specific processing capabilities ---
|
|
47
53
|
class BaseProducerUnit(ez.Unit, ABC, typing.Generic[SettingsType, MessageOutType, ProducerType]):
|
|
48
54
|
"""
|
|
@@ -240,6 +246,47 @@ class BaseAdaptiveTransformerUnit(
|
|
|
240
246
|
await self.processor.apartial_fit(msg)
|
|
241
247
|
|
|
242
248
|
|
|
249
|
+
class BaseClockDrivenProducerUnit(
|
|
250
|
+
BaseProcessorUnit[SettingsType],
|
|
251
|
+
ABC,
|
|
252
|
+
typing.Generic[SettingsType, ClockDrivenProducerType],
|
|
253
|
+
):
|
|
254
|
+
"""
|
|
255
|
+
Base class for clock-driven producer units.
|
|
256
|
+
|
|
257
|
+
These units receive clock ticks (LinearAxis) and produce AxisArray output.
|
|
258
|
+
This simplifies the Clock → Counter → Generator pattern by combining
|
|
259
|
+
the counter functionality into the generator.
|
|
260
|
+
|
|
261
|
+
Implement a new Unit as follows::
|
|
262
|
+
|
|
263
|
+
class SinGeneratorUnit(BaseClockDrivenProducerUnit[
|
|
264
|
+
SinGeneratorSettings, # SettingsType (must extend ClockDrivenSettings)
|
|
265
|
+
SinProducer, # ClockDrivenProducerType
|
|
266
|
+
]):
|
|
267
|
+
SETTINGS = SinGeneratorSettings
|
|
268
|
+
|
|
269
|
+
Where SinGeneratorSettings extends ClockDrivenSettings and SinProducer
|
|
270
|
+
extends BaseClockDrivenProducer.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
INPUT_CLOCK = ez.InputStream(LinearAxis)
|
|
274
|
+
OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
|
|
275
|
+
|
|
276
|
+
def create_processor(self) -> None:
|
|
277
|
+
"""Create the clock-driven producer instance from settings."""
|
|
278
|
+
producer_type = get_base_clockdriven_producer_type(self.__class__)
|
|
279
|
+
self.processor = producer_type(settings=self.SETTINGS)
|
|
280
|
+
|
|
281
|
+
@ez.subscriber(INPUT_CLOCK, zero_copy=True)
|
|
282
|
+
@ez.publisher(OUTPUT_SIGNAL)
|
|
283
|
+
@profile_subpub(trace_oldest=False)
|
|
284
|
+
async def on_clock(self, clock_tick: LinearAxis) -> typing.AsyncGenerator:
|
|
285
|
+
result = await self.processor.__acall__(clock_tick)
|
|
286
|
+
if result is not None:
|
|
287
|
+
yield self.OUTPUT_SIGNAL, result
|
|
288
|
+
|
|
289
|
+
|
|
243
290
|
# Legacy class
|
|
244
291
|
class GenAxisArray(ez.Unit):
|
|
245
292
|
STATE = GenState
|
|
@@ -11,6 +11,10 @@ def resolve_typevar(cls: type, target_typevar: typing.TypeVar) -> type:
|
|
|
11
11
|
and checks the original bases of each class in the MRO for the TypeVar.
|
|
12
12
|
If the TypeVar is found, it returns the concrete type bound to it.
|
|
13
13
|
If the TypeVar is not found, it raises a TypeError.
|
|
14
|
+
|
|
15
|
+
If the resolved type is itself a TypeVar, this function recursively
|
|
16
|
+
resolves it until a concrete type is found.
|
|
17
|
+
|
|
14
18
|
Args:
|
|
15
19
|
cls (type): The class to inspect.
|
|
16
20
|
target_typevar (typing.TypeVar): The TypeVar to resolve.
|
|
@@ -30,7 +34,11 @@ def resolve_typevar(cls: type, target_typevar: typing.TypeVar) -> type:
|
|
|
30
34
|
index = params.index(target_typevar)
|
|
31
35
|
args = typing.get_args(orig_base)
|
|
32
36
|
try:
|
|
33
|
-
|
|
37
|
+
resolved = args[index]
|
|
38
|
+
# If the resolved type is itself a TypeVar, resolve it recursively
|
|
39
|
+
if isinstance(resolved, typing.TypeVar):
|
|
40
|
+
return resolve_typevar(cls, resolved)
|
|
41
|
+
return resolved
|
|
34
42
|
except IndexError:
|
|
35
43
|
pass
|
|
36
44
|
raise TypeError(f"Could not resolve {target_typevar} in {cls}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ezmsg-baseproc
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Base processor classes and protocols for ezmsg signal processing pipelines
|
|
5
5
|
Author-email: Griffin Milsap <griffin.milsap@gmail.com>, Preston Peranich <pperanich@gmail.com>, Chadwick Boulay <chadwick.boulay@gmail.com>, Kyle McGraw <kmcgraw@blackrockneuro.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -12,7 +12,7 @@ Description-Content-Type: text/markdown
|
|
|
12
12
|
|
|
13
13
|
# ezmsg-baseproc
|
|
14
14
|
|
|
15
|
-
Base processor classes and protocols for building
|
|
15
|
+
Base processor classes and protocols for building message-processing components in [ezmsg](https://github.com/ezmsg-org/ezmsg).
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
@@ -20,30 +20,26 @@ Base processor classes and protocols for building signal processing pipelines in
|
|
|
20
20
|
pip install ezmsg-baseproc
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Or install the latest development version:
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
```bash
|
|
26
|
+
pip install git+https://github.com/ezmsg-org/ezmsg-baseproc@dev
|
|
27
|
+
```
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
- **Base Classes** - Abstract base classes for building stateless and stateful processors
|
|
29
|
-
- **Composite Processors** - Classes for chaining processors into pipelines
|
|
30
|
-
- **Unit Wrappers** - ezmsg Unit base classes that wrap processors for graph integration
|
|
29
|
+
## Overview
|
|
31
30
|
|
|
32
|
-
|
|
31
|
+
``ezmsg-baseproc`` provides abstract base classes for creating message processors that can be used both standalone and within ezmsg pipelines. The package offers a consistent pattern for building:
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
├── profile.py # Profiling decorators
|
|
45
|
-
└── typeresolution.py # Type resolution helpers
|
|
46
|
-
```
|
|
33
|
+
* **Protocols** - Type definitions for processors, transformers, consumers, and producers
|
|
34
|
+
* **Processors** - Transform input messages to output messages
|
|
35
|
+
* **Producers** - Generate output messages without requiring input
|
|
36
|
+
* **Consumers** - Accept input messages without producing output
|
|
37
|
+
* **Transformers** - A specific type of processor with typed input/output
|
|
38
|
+
* **Stateful variants** - Processors that maintain state across invocations
|
|
39
|
+
* **Adaptive transformers** - Transformers that can be trained via ``partial_fit``
|
|
40
|
+
* **Composite processors** - Chain multiple processors together efficiently
|
|
41
|
+
|
|
42
|
+
All base classes support both synchronous and asynchronous operation, making them suitable for offline analysis and real-time streaming applications.
|
|
47
43
|
|
|
48
44
|
## Usage
|
|
49
45
|
|
|
@@ -100,6 +96,7 @@ We use [`uv`](https://docs.astral.sh/uv/getting-started/installation/) for devel
|
|
|
100
96
|
2. Clone and cd into the repository
|
|
101
97
|
3. Run `uv sync` to create a `.venv` and install dependencies
|
|
102
98
|
4. Run `uv run pytest tests` to run tests
|
|
99
|
+
5. (Optional) Install pre-commit hooks: `uv run pre-commit install`
|
|
103
100
|
|
|
104
101
|
## License
|
|
105
102
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
ezmsg/baseproc/__init__.py,sha256=cSkyXkfPvIiAxe6maHiZvJpFbdRw3xTEitBClXeehec,4755
|
|
2
|
+
ezmsg/baseproc/__version__.py,sha256=-uLONazCO1SzFfcY-K6A1keL--LIVfTYccGX6ciADac,704
|
|
3
|
+
ezmsg/baseproc/clock.py,sha256=jRJGxaWWBek503OcGRYsW4Qc7Lt4m-tVl1wn_v-qCIk,3254
|
|
4
|
+
ezmsg/baseproc/clockdriven.py,sha256=ckPZSVHZfYfjFRHDCERUWjDwyQgOu-aRTNsJ3EDFBaI,6161
|
|
5
|
+
ezmsg/baseproc/composite.py,sha256=Lin4K_rmS2Tnxt-m8daP-PUyeeqL4id5JkVh-AUNrQw,14901
|
|
6
|
+
ezmsg/baseproc/counter.py,sha256=kcBPiVxMPULp4ojnVESNw7mn_4v0xSODfASHrL83GtM,2168
|
|
7
|
+
ezmsg/baseproc/processor.py,sha256=Ir9FtNuVG4yc-frwNxoYrlld99ff1mXwwGWaHxEJ6tY,8056
|
|
8
|
+
ezmsg/baseproc/protocols.py,sha256=O3Qp0ymE9Ovlmh8t22v-lMmFzuWK0D93REAYMnJV3xA,5106
|
|
9
|
+
ezmsg/baseproc/stateful.py,sha256=-jjAZIyJA5eiTECi1fSfazfqgv__RtyqPp1ZvLFFIDI,11424
|
|
10
|
+
ezmsg/baseproc/units.py,sha256=byFijVLEZFO145HE74sZk1_qpCu6nFjB8-vSYz9Grds,12077
|
|
11
|
+
ezmsg/baseproc/util/__init__.py,sha256=hvMUJOBuqioER50GZ5-GZiQbQ9NtQYEze13ZlR2jbMA,37
|
|
12
|
+
ezmsg/baseproc/util/asio.py,sha256=0sF5oDc58DSLlcEgoUpNiqjjcbqnZhjSpQrXn6IdosM,4960
|
|
13
|
+
ezmsg/baseproc/util/message.py,sha256=l_b1b6bXX8N6VF9RbUELzsHs73cKkDURBdIr0lt3CY0,909
|
|
14
|
+
ezmsg/baseproc/util/profile.py,sha256=MOQDsFsW6ddXT0uAOgytW3aK_AZW5ieA16Pz2hWuE2o,6189
|
|
15
|
+
ezmsg/baseproc/util/typeresolution.py,sha256=5on4QcrYd1rxsRoDEqivNjuWT5BkU-Wg7XdTNaOircI,3485
|
|
16
|
+
ezmsg_baseproc-1.2.0.dist-info/METADATA,sha256=H2jTn5VSw0pEwQWLdd3Hu0G1OCbbKM-On4AmKyyyfm4,3415
|
|
17
|
+
ezmsg_baseproc-1.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
18
|
+
ezmsg_baseproc-1.2.0.dist-info/licenses/LICENSE,sha256=BDD8rfac1Ur7mp0_3izEdr6fHgSA3Or6U1Kb0ZAWsow,1066
|
|
19
|
+
ezmsg_baseproc-1.2.0.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
ezmsg/baseproc/__init__.py,sha256=zmhdrRTnj3-ilBitu6zBniV04HJXc_fM_tKYiapFGko,3830
|
|
2
|
-
ezmsg/baseproc/__version__.py,sha256=l8k828IdTfzXAlmx4oT8GsiIf2eeMAlFDALjoYk-jrU,704
|
|
3
|
-
ezmsg/baseproc/composite.py,sha256=Lin4K_rmS2Tnxt-m8daP-PUyeeqL4id5JkVh-AUNrQw,14901
|
|
4
|
-
ezmsg/baseproc/processor.py,sha256=Ir9FtNuVG4yc-frwNxoYrlld99ff1mXwwGWaHxEJ6tY,8056
|
|
5
|
-
ezmsg/baseproc/protocols.py,sha256=O3Qp0ymE9Ovlmh8t22v-lMmFzuWK0D93REAYMnJV3xA,5106
|
|
6
|
-
ezmsg/baseproc/stateful.py,sha256=-jjAZIyJA5eiTECi1fSfazfqgv__RtyqPp1ZvLFFIDI,11424
|
|
7
|
-
ezmsg/baseproc/units.py,sha256=TRhjDKw0lqUUst7BHYJKP3AhGpRd6mvcdKxULfeWjA0,10283
|
|
8
|
-
ezmsg/baseproc/util/__init__.py,sha256=hvMUJOBuqioER50GZ5-GZiQbQ9NtQYEze13ZlR2jbMA,37
|
|
9
|
-
ezmsg/baseproc/util/asio.py,sha256=0sF5oDc58DSLlcEgoUpNiqjjcbqnZhjSpQrXn6IdosM,4960
|
|
10
|
-
ezmsg/baseproc/util/message.py,sha256=l_b1b6bXX8N6VF9RbUELzsHs73cKkDURBdIr0lt3CY0,909
|
|
11
|
-
ezmsg/baseproc/util/profile.py,sha256=MOQDsFsW6ddXT0uAOgytW3aK_AZW5ieA16Pz2hWuE2o,6189
|
|
12
|
-
ezmsg/baseproc/util/typeresolution.py,sha256=WCHHYIrMMZ1CfwJWVlJPQgFyY2gnGRNFJVQynAsee7Y,3113
|
|
13
|
-
ezmsg_baseproc-1.0.3.dist-info/METADATA,sha256=nOK4zkV08Nx1azdXd7js_OT0kQqomMq0Z2s8AoPSPd4,3320
|
|
14
|
-
ezmsg_baseproc-1.0.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
15
|
-
ezmsg_baseproc-1.0.3.dist-info/licenses/LICENSE,sha256=BDD8rfac1Ur7mp0_3izEdr6fHgSA3Or6U1Kb0ZAWsow,1066
|
|
16
|
-
ezmsg_baseproc-1.0.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|