ezmsg-baseproc 1.0.3__py3-none-any.whl → 1.1.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 +23 -0
- ezmsg/baseproc/__version__.py +2 -2
- ezmsg/baseproc/clock.py +109 -0
- ezmsg/baseproc/counter.py +128 -0
- {ezmsg_baseproc-1.0.3.dist-info → ezmsg_baseproc-1.1.0.dist-info}/METADATA +19 -22
- {ezmsg_baseproc-1.0.3.dist-info → ezmsg_baseproc-1.1.0.dist-info}/RECORD +8 -6
- {ezmsg_baseproc-1.0.3.dist-info → ezmsg_baseproc-1.1.0.dist-info}/WHEEL +0 -0
- {ezmsg_baseproc-1.0.3.dist-info → ezmsg_baseproc-1.1.0.dist-info}/licenses/LICENSE +0 -0
ezmsg/baseproc/__init__.py
CHANGED
|
@@ -7,6 +7,14 @@ 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
|
+
|
|
10
18
|
# Composite processor classes
|
|
11
19
|
from .composite import (
|
|
12
20
|
CompositeProcessor,
|
|
@@ -14,6 +22,12 @@ from .composite import (
|
|
|
14
22
|
CompositeStateful,
|
|
15
23
|
_get_processor_message_type,
|
|
16
24
|
)
|
|
25
|
+
from .counter import (
|
|
26
|
+
Counter,
|
|
27
|
+
CounterSettings,
|
|
28
|
+
CounterTransformer,
|
|
29
|
+
CounterTransformerState,
|
|
30
|
+
)
|
|
17
31
|
|
|
18
32
|
# Base processor classes (non-stateful)
|
|
19
33
|
from .processor import (
|
|
@@ -152,4 +166,13 @@ __all__ = [
|
|
|
152
166
|
# Type utilities
|
|
153
167
|
"check_message_type_compatibility",
|
|
154
168
|
"resolve_typevar",
|
|
169
|
+
# Clock and Counter
|
|
170
|
+
"Clock",
|
|
171
|
+
"ClockProducer",
|
|
172
|
+
"ClockSettings",
|
|
173
|
+
"ClockState",
|
|
174
|
+
"Counter",
|
|
175
|
+
"CounterSettings",
|
|
176
|
+
"CounterTransformer",
|
|
177
|
+
"CounterTransformerState",
|
|
155
178
|
]
|
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.1.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 1, 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,128 @@
|
|
|
1
|
+
"""Counter generator for sample counting and timing."""
|
|
2
|
+
|
|
3
|
+
import ezmsg.core as ez
|
|
4
|
+
import numpy as np
|
|
5
|
+
from ezmsg.util.messages.axisarray import AxisArray, replace
|
|
6
|
+
|
|
7
|
+
from .protocols import processor_state
|
|
8
|
+
from .stateful import BaseStatefulTransformer
|
|
9
|
+
from .units import BaseTransformerUnit
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CounterSettings(ez.Settings):
|
|
13
|
+
"""Settings for :obj:`Counter` and :obj:`CounterTransformer`."""
|
|
14
|
+
|
|
15
|
+
fs: float
|
|
16
|
+
"""Sampling rate in Hz."""
|
|
17
|
+
|
|
18
|
+
n_time: int | None = None
|
|
19
|
+
"""
|
|
20
|
+
Samples per block.
|
|
21
|
+
- If specified: fixed chunk size (clock gain is ignored)
|
|
22
|
+
- If None: derived from clock gain (fs * clock.gain), with fractional sample tracking
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
mod: int | None = None
|
|
26
|
+
"""If set, counter values rollover at this modulus."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@processor_state
|
|
30
|
+
class CounterTransformerState:
|
|
31
|
+
"""State for :obj:`CounterTransformer`."""
|
|
32
|
+
|
|
33
|
+
counter: int = 0
|
|
34
|
+
"""Current counter value (next sample index)."""
|
|
35
|
+
|
|
36
|
+
fractional_samples: float = 0.0
|
|
37
|
+
"""Accumulated fractional samples for variable chunk mode."""
|
|
38
|
+
|
|
39
|
+
template: AxisArray | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CounterTransformer(
|
|
43
|
+
BaseStatefulTransformer[CounterSettings, AxisArray.LinearAxis, AxisArray, CounterTransformerState]
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Transforms clock ticks (LinearAxis) into AxisArray counter values.
|
|
47
|
+
|
|
48
|
+
Each clock tick produces a block of counter values. The block size is either
|
|
49
|
+
fixed (n_time setting) or derived from the clock's gain (fs * gain).
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def _reset_state(self, message: AxisArray.LinearAxis) -> None:
|
|
53
|
+
"""Reset state - counter transformer state is simple, just reset values."""
|
|
54
|
+
self._state.counter = 0
|
|
55
|
+
self._state.fractional_samples = 0.0
|
|
56
|
+
self._state.template = AxisArray(
|
|
57
|
+
data=np.array([], dtype=int),
|
|
58
|
+
dims=["time"],
|
|
59
|
+
axes={
|
|
60
|
+
"time": AxisArray.TimeAxis(fs=self.settings.fs, offset=message.offset),
|
|
61
|
+
},
|
|
62
|
+
key="counter",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def _hash_message(self, message: AxisArray.LinearAxis) -> int:
|
|
66
|
+
# Return constant hash - counter state should never reset based on message content.
|
|
67
|
+
# The counter maintains continuity regardless of clock rate changes.
|
|
68
|
+
return 0
|
|
69
|
+
|
|
70
|
+
def _process(self, clock_tick: AxisArray.LinearAxis) -> AxisArray | None:
|
|
71
|
+
"""Transform a clock tick into counter AxisArray."""
|
|
72
|
+
# Determine number of samples for this block
|
|
73
|
+
if self.settings.n_time is not None:
|
|
74
|
+
# Fixed chunk size mode
|
|
75
|
+
n_samples = self.settings.n_time
|
|
76
|
+
# Use wall clock or synthetic offset based on clock gain
|
|
77
|
+
if clock_tick.gain == 0.0:
|
|
78
|
+
# AFAP mode - synthetic offset
|
|
79
|
+
offset = self.state.counter / self.settings.fs
|
|
80
|
+
else:
|
|
81
|
+
# Use clock's timestamp
|
|
82
|
+
offset = clock_tick.offset
|
|
83
|
+
else:
|
|
84
|
+
# Variable chunk size mode - derive from clock gain
|
|
85
|
+
if clock_tick.gain == 0.0:
|
|
86
|
+
# AFAP with no fixed n_time - this is an error
|
|
87
|
+
raise ValueError("Cannot use clock with gain=0 (AFAP) without specifying n_time")
|
|
88
|
+
|
|
89
|
+
# Calculate samples including fractional accumulation
|
|
90
|
+
# Add small epsilon to avoid floating point truncation errors (e.g., 0.9999999 -> 0)
|
|
91
|
+
samples_float = self.settings.fs * clock_tick.gain + self.state.fractional_samples
|
|
92
|
+
n_samples = int(samples_float + 1e-9)
|
|
93
|
+
self.state.fractional_samples = samples_float - n_samples
|
|
94
|
+
|
|
95
|
+
if n_samples == 0:
|
|
96
|
+
# Not enough samples accumulated yet
|
|
97
|
+
# TODO: Return empty array. What should offset be?
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
# Use clock's timestamp for offset
|
|
101
|
+
offset = clock_tick.offset
|
|
102
|
+
|
|
103
|
+
# Generate counter data
|
|
104
|
+
block_samp = np.arange(self.state.counter, self.state.counter + n_samples)
|
|
105
|
+
if self.settings.mod is not None:
|
|
106
|
+
block_samp = block_samp % self.settings.mod
|
|
107
|
+
|
|
108
|
+
# Create output AxisArray
|
|
109
|
+
result = replace(
|
|
110
|
+
self._state.template,
|
|
111
|
+
data=block_samp,
|
|
112
|
+
axes={"time": replace(self._state.template.axes["time"], offset=offset)},
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Update state
|
|
116
|
+
self.state.counter += n_samples
|
|
117
|
+
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Counter(BaseTransformerUnit[CounterSettings, AxisArray.LinearAxis, AxisArray, CounterTransformer]):
|
|
122
|
+
"""
|
|
123
|
+
Transforms clock ticks into monotonically increasing counter values as AxisArray.
|
|
124
|
+
|
|
125
|
+
Receives timing from INPUT_SIGNAL (LinearAxis from Clock) and outputs AxisArray.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
SETTINGS = CounterSettings
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ezmsg-baseproc
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.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
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
ezmsg/baseproc/__init__.py,sha256=
|
|
2
|
-
ezmsg/baseproc/__version__.py,sha256=
|
|
1
|
+
ezmsg/baseproc/__init__.py,sha256=OLpOqiGBjQSrLPjAtWEHdCS8Sm_2tJ55I7bpCtXntYY,4243
|
|
2
|
+
ezmsg/baseproc/__version__.py,sha256=ePNVzJOkxR8FY5bezqKQ_fgBRbzH1G7QTaRDHvGQRAY,704
|
|
3
|
+
ezmsg/baseproc/clock.py,sha256=jRJGxaWWBek503OcGRYsW4Qc7Lt4m-tVl1wn_v-qCIk,3254
|
|
3
4
|
ezmsg/baseproc/composite.py,sha256=Lin4K_rmS2Tnxt-m8daP-PUyeeqL4id5JkVh-AUNrQw,14901
|
|
5
|
+
ezmsg/baseproc/counter.py,sha256=3JR4AQ_wwZhdjKQdPlLhjhNVLHE5fj9ggwUPtJhWDS4,4601
|
|
4
6
|
ezmsg/baseproc/processor.py,sha256=Ir9FtNuVG4yc-frwNxoYrlld99ff1mXwwGWaHxEJ6tY,8056
|
|
5
7
|
ezmsg/baseproc/protocols.py,sha256=O3Qp0ymE9Ovlmh8t22v-lMmFzuWK0D93REAYMnJV3xA,5106
|
|
6
8
|
ezmsg/baseproc/stateful.py,sha256=-jjAZIyJA5eiTECi1fSfazfqgv__RtyqPp1ZvLFFIDI,11424
|
|
@@ -10,7 +12,7 @@ ezmsg/baseproc/util/asio.py,sha256=0sF5oDc58DSLlcEgoUpNiqjjcbqnZhjSpQrXn6IdosM,4
|
|
|
10
12
|
ezmsg/baseproc/util/message.py,sha256=l_b1b6bXX8N6VF9RbUELzsHs73cKkDURBdIr0lt3CY0,909
|
|
11
13
|
ezmsg/baseproc/util/profile.py,sha256=MOQDsFsW6ddXT0uAOgytW3aK_AZW5ieA16Pz2hWuE2o,6189
|
|
12
14
|
ezmsg/baseproc/util/typeresolution.py,sha256=WCHHYIrMMZ1CfwJWVlJPQgFyY2gnGRNFJVQynAsee7Y,3113
|
|
13
|
-
ezmsg_baseproc-1.0.
|
|
14
|
-
ezmsg_baseproc-1.0.
|
|
15
|
-
ezmsg_baseproc-1.0.
|
|
16
|
-
ezmsg_baseproc-1.0.
|
|
15
|
+
ezmsg_baseproc-1.1.0.dist-info/METADATA,sha256=zv5si0HBsR2t8lzzmwzeuLiik4SeanZvmV-JNWqOnKg,3415
|
|
16
|
+
ezmsg_baseproc-1.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
17
|
+
ezmsg_baseproc-1.1.0.dist-info/licenses/LICENSE,sha256=BDD8rfac1Ur7mp0_3izEdr6fHgSA3Or6U1Kb0ZAWsow,1066
|
|
18
|
+
ezmsg_baseproc-1.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|