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.
@@ -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
  ]
@@ -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.3'
32
- __version_tuple__ = version_tuple = (1, 0, 3)
31
+ __version__ = version = '1.1.0'
32
+ __version_tuple__ = version_tuple = (1, 1, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -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
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 signal processing pipelines in [ezmsg](https://github.com/ezmsg-org/ezmsg).
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
- ## Overview
23
+ Or install the latest development version:
24
24
 
25
- This package provides the foundational processor architecture for ezmsg signal processing:
25
+ ```bash
26
+ pip install git+https://github.com/ezmsg-org/ezmsg-baseproc@dev
27
+ ```
26
28
 
27
- - **Protocols** - Type definitions for processors, transformers, consumers, and producers
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
- ## Module Structure
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
- ezmsg.baseproc/
36
- ├── protocols.py # Protocol definitions and type variables
37
- ├── processor.py # Base non-stateful processors
38
- ├── stateful.py # Stateful processor base classes
39
- ├── composite.py # CompositeProcessor and CompositeProducer
40
- ├── units.py # ezmsg Unit wrappers
41
- └── util/
42
- ├── asio.py # Async/sync utilities
43
- ├── message.py # SampleMessage definitions
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=zmhdrRTnj3-ilBitu6zBniV04HJXc_fM_tKYiapFGko,3830
2
- ezmsg/baseproc/__version__.py,sha256=l8k828IdTfzXAlmx4oT8GsiIf2eeMAlFDALjoYk-jrU,704
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.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,,
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,,