ezmsg-baseproc 1.2.0__tar.gz → 1.2.1__tar.gz

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.
Files changed (49) hide show
  1. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/PKG-INFO +1 -1
  2. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/docs/source/guides/ProcessorsBase.md +2 -2
  3. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/docs/source/guides/how-tos/processors/clockdriven.rst +2 -2
  4. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/src/ezmsg/baseproc/__init__.py +2 -2
  5. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/src/ezmsg/baseproc/__version__.py +2 -2
  6. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/src/ezmsg/baseproc/counter.py +2 -2
  7. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/src/ezmsg/baseproc/units.py +2 -2
  8. ezmsg_baseproc-1.2.1/tests/conftest.py +18 -0
  9. ezmsg_baseproc-1.2.1/tests/helpers/__init__.py +0 -0
  10. ezmsg_baseproc-1.2.1/tests/helpers/util.py +24 -0
  11. ezmsg_baseproc-1.2.1/tests/test_clock_counter_system.py +229 -0
  12. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/.github/workflows/docs.yml +0 -0
  13. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/.github/workflows/python-publish.yml +0 -0
  14. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/.github/workflows/python-tests.yml +0 -0
  15. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/.gitignore +0 -0
  16. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/.pre-commit-config.yaml +0 -0
  17. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/LICENSE +0 -0
  18. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/README.md +0 -0
  19. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/docs/Makefile +0 -0
  20. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/docs/make.bat +0 -0
  21. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/docs/source/_templates/autosummary/module.rst +0 -0
  22. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/docs/source/api/index.rst +0 -0
  23. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/docs/source/conf.py +0 -0
  24. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/docs/source/guides/how-tos/processors/adaptive.rst +0 -0
  25. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/docs/source/guides/how-tos/processors/checkpoint.rst +0 -0
  26. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/docs/source/guides/how-tos/processors/composite.rst +0 -0
  27. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/docs/source/guides/how-tos/processors/content-processors.rst +0 -0
  28. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/docs/source/guides/how-tos/processors/processor.rst +0 -0
  29. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/docs/source/guides/how-tos/processors/standalone.rst +0 -0
  30. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/docs/source/guides/how-tos/processors/stateful.rst +0 -0
  31. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/docs/source/guides/how-tos/processors/unit.rst +0 -0
  32. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/docs/source/index.md +0 -0
  33. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/pyproject.toml +0 -0
  34. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/src/ezmsg/baseproc/clock.py +0 -0
  35. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/src/ezmsg/baseproc/clockdriven.py +0 -0
  36. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/src/ezmsg/baseproc/composite.py +0 -0
  37. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/src/ezmsg/baseproc/processor.py +0 -0
  38. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/src/ezmsg/baseproc/protocols.py +0 -0
  39. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/src/ezmsg/baseproc/stateful.py +0 -0
  40. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/src/ezmsg/baseproc/util/__init__.py +0 -0
  41. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/src/ezmsg/baseproc/util/asio.py +0 -0
  42. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/src/ezmsg/baseproc/util/message.py +0 -0
  43. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/src/ezmsg/baseproc/util/profile.py +0 -0
  44. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/src/ezmsg/baseproc/util/typeresolution.py +0 -0
  45. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/tests/test_baseproc.py +0 -0
  46. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/tests/test_clock.py +0 -0
  47. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/tests/test_clockdriven.py +0 -0
  48. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/tests/test_counter.py +0 -0
  49. {ezmsg_baseproc-1.2.0 → ezmsg_baseproc-1.2.1}/tests/test_profile.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ezmsg-baseproc
3
- Version: 1.2.0
3
+ Version: 1.2.1
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
@@ -80,9 +80,9 @@ do not inherit from `BaseStatefulProcessor` and `BaseStatefulProducer`. They acc
80
80
  | 3 | `BaseConsumerUnit` | 1 | `ConsumerType` |
81
81
  | 4 | `BaseTransformerUnit` | 1 | `TransformerType` |
82
82
  | 5 | `BaseAdaptiveTransformerUnit` | 1 | `AdaptiveTransformerType` |
83
- | 6 | `BaseClockDrivenProducerUnit` | 1 | `ClockDrivenProducerType` |
83
+ | 6 | `BaseClockDrivenUnit` | 1 | `ClockDrivenProducerType` |
84
84
 
85
- Note, it is strongly recommended to use `BaseConsumerUnit`, `BaseTransformerUnit`, `BaseAdaptiveTransformerUnit`, or `BaseClockDrivenProducerUnit` for implementing concrete subclasses rather than `BaseProcessorUnit`.
85
+ Note, it is strongly recommended to use `BaseConsumerUnit`, `BaseTransformerUnit`, `BaseAdaptiveTransformerUnit`, or `BaseClockDrivenUnit` for implementing concrete subclasses rather than `BaseProcessorUnit`.
86
86
 
87
87
 
88
88
  ## Implementing a custom standalone processor
@@ -42,7 +42,7 @@ Here's a complete example of a sine wave generator:
42
42
 
43
43
  from ezmsg.baseproc import (
44
44
  BaseClockDrivenProducer,
45
- BaseClockDrivenProducerUnit,
45
+ BaseClockDrivenUnit,
46
46
  ClockDrivenSettings,
47
47
  ClockDrivenState,
48
48
  processor_state,
@@ -124,7 +124,7 @@ Here's a complete example of a sine wave generator:
124
124
 
125
125
 
126
126
  class SinGeneratorUnit(
127
- BaseClockDrivenProducerUnit[SinGeneratorSettings, SinGenerator]
127
+ BaseClockDrivenUnit[SinGeneratorSettings, SinGenerator]
128
128
  ):
129
129
  """
130
130
  ezmsg Unit wrapper for SinGenerator.
@@ -82,7 +82,7 @@ from .stateful import (
82
82
  from .units import (
83
83
  AdaptiveTransformerType,
84
84
  BaseAdaptiveTransformerUnit,
85
- BaseClockDrivenProducerUnit,
85
+ BaseClockDrivenUnit,
86
86
  BaseConsumerUnit,
87
87
  BaseProcessorUnit,
88
88
  BaseProducerUnit,
@@ -158,7 +158,7 @@ __all__ = [
158
158
  "BaseConsumerUnit",
159
159
  "BaseTransformerUnit",
160
160
  "BaseAdaptiveTransformerUnit",
161
- "BaseClockDrivenProducerUnit",
161
+ "BaseClockDrivenUnit",
162
162
  "GenAxisArray",
163
163
  # Type resolution helpers
164
164
  "get_base_producer_type",
@@ -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.2.0'
32
- __version_tuple__ = version_tuple = (1, 2, 0)
31
+ __version__ = version = '1.2.1'
32
+ __version_tuple__ = version_tuple = (1, 2, 1)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -9,7 +9,7 @@ from .clockdriven import (
9
9
  ClockDrivenState,
10
10
  )
11
11
  from .protocols import processor_state
12
- from .units import BaseClockDrivenProducerUnit
12
+ from .units import BaseClockDrivenUnit
13
13
 
14
14
 
15
15
  class CounterSettings(ClockDrivenSettings):
@@ -57,7 +57,7 @@ class CounterTransformer(BaseClockDrivenProducer[CounterSettings, CounterTransfo
57
57
  )
58
58
 
59
59
 
60
- class Counter(BaseClockDrivenProducerUnit[CounterSettings, CounterTransformer]):
60
+ class Counter(BaseClockDrivenUnit[CounterSettings, CounterTransformer]):
61
61
  """
62
62
  Transforms clock ticks into monotonically increasing counter values as AxisArray.
63
63
 
@@ -246,7 +246,7 @@ class BaseAdaptiveTransformerUnit(
246
246
  await self.processor.apartial_fit(msg)
247
247
 
248
248
 
249
- class BaseClockDrivenProducerUnit(
249
+ class BaseClockDrivenUnit(
250
250
  BaseProcessorUnit[SettingsType],
251
251
  ABC,
252
252
  typing.Generic[SettingsType, ClockDrivenProducerType],
@@ -260,7 +260,7 @@ class BaseClockDrivenProducerUnit(
260
260
 
261
261
  Implement a new Unit as follows::
262
262
 
263
- class SinGeneratorUnit(BaseClockDrivenProducerUnit[
263
+ class SinGeneratorUnit(BaseClockDrivenUnit[
264
264
  SinGeneratorSettings, # SettingsType (must extend ClockDrivenSettings)
265
265
  SinProducer, # ClockDrivenProducerType
266
266
  ]):
@@ -0,0 +1,18 @@
1
+ """pytest configuration for ezmsg-baseproc tests."""
2
+
3
+ import os
4
+ import sys
5
+
6
+ import pytest
7
+
8
+ # Add tests directory to path so 'tests.helpers' can be imported
9
+ _tests_dir = os.path.dirname(__file__)
10
+ _parent_dir = os.path.dirname(_tests_dir)
11
+ if _parent_dir not in sys.path:
12
+ sys.path.insert(0, _parent_dir)
13
+
14
+
15
+ @pytest.fixture
16
+ def test_name(request):
17
+ """Provide the test name to test functions."""
18
+ return request.node.name
File without changes
@@ -0,0 +1,24 @@
1
+ import os
2
+ import tempfile
3
+ from pathlib import Path
4
+
5
+
6
+ def get_test_fn(test_name: str | None = None, extension: str = "txt") -> Path:
7
+ """PYTEST compatible temporary test file creator"""
8
+
9
+ # Get current test name if we can..
10
+ if test_name is None:
11
+ test_name = os.environ.get("PYTEST_CURRENT_TEST")
12
+ if test_name is not None:
13
+ test_name = test_name.split(":")[-1].split(" ")[0]
14
+ else:
15
+ test_name = __name__
16
+
17
+ file_path = Path(tempfile.gettempdir())
18
+ file_path = file_path / Path(f"{test_name}.{extension}")
19
+
20
+ # Create the file
21
+ with open(file_path, "w"):
22
+ pass
23
+
24
+ return file_path
@@ -0,0 +1,229 @@
1
+ """Integration tests for Clock and Counter ezmsg systems."""
2
+
3
+ import math
4
+ import os
5
+ from dataclasses import field
6
+
7
+ import ezmsg.core as ez
8
+ import numpy as np
9
+ import pytest
10
+ from ezmsg.util.messagecodec import message_log
11
+ from ezmsg.util.messagelogger import MessageLogger, MessageLoggerSettings
12
+ from ezmsg.util.messages.axisarray import AxisArray
13
+ from ezmsg.util.terminate import TerminateOnTotal, TerminateOnTotalSettings
14
+
15
+ from ezmsg.baseproc import (
16
+ Clock,
17
+ ClockSettings,
18
+ Counter,
19
+ CounterSettings,
20
+ )
21
+ from tests.helpers.util import get_test_fn
22
+
23
+
24
+ class ClockTestSystemSettings(ez.Settings):
25
+ clock_settings: ClockSettings
26
+ log_settings: MessageLoggerSettings
27
+ term_settings: TerminateOnTotalSettings = field(default_factory=TerminateOnTotalSettings)
28
+
29
+
30
+ class ClockTestSystem(ez.Collection):
31
+ SETTINGS = ClockTestSystemSettings
32
+
33
+ CLOCK = Clock()
34
+ LOG = MessageLogger()
35
+ TERM = TerminateOnTotal()
36
+
37
+ def configure(self) -> None:
38
+ self.CLOCK.apply_settings(self.SETTINGS.clock_settings)
39
+ self.LOG.apply_settings(self.SETTINGS.log_settings)
40
+ self.TERM.apply_settings(self.SETTINGS.term_settings)
41
+
42
+ def network(self) -> ez.NetworkDefinition:
43
+ return (
44
+ (self.CLOCK.OUTPUT_SIGNAL, self.LOG.INPUT_MESSAGE),
45
+ (self.LOG.OUTPUT_MESSAGE, self.TERM.INPUT_MESSAGE),
46
+ )
47
+
48
+
49
+ @pytest.mark.parametrize("dispatch_rate", [math.inf, 2.0, 20.0])
50
+ def test_clock_system(
51
+ dispatch_rate: float,
52
+ test_name: str | None = None,
53
+ ):
54
+ run_time = 1.0
55
+ n_target = 100 if math.isinf(dispatch_rate) else int(np.ceil(dispatch_rate * run_time))
56
+ test_filename = get_test_fn(test_name)
57
+ ez.logger.info(test_filename)
58
+ settings = ClockTestSystemSettings(
59
+ clock_settings=ClockSettings(dispatch_rate=dispatch_rate),
60
+ log_settings=MessageLoggerSettings(output=test_filename),
61
+ term_settings=TerminateOnTotalSettings(total=n_target),
62
+ )
63
+ system = ClockTestSystem(settings)
64
+ ez.run(SYSTEM=system)
65
+
66
+ # Collect result
67
+ messages = list(message_log(test_filename))
68
+ os.remove(test_filename)
69
+
70
+ # Clock produces LinearAxis with gain and offset
71
+ assert all(isinstance(m, AxisArray.LinearAxis) for m in messages)
72
+ assert len(messages) >= n_target
73
+
74
+
75
+ class CounterTestSystemSettings(ez.Settings):
76
+ clock_settings: ClockSettings
77
+ counter_settings: CounterSettings
78
+ log_settings: MessageLoggerSettings
79
+ term_settings: TerminateOnTotalSettings = field(default_factory=TerminateOnTotalSettings)
80
+
81
+
82
+ class CounterTestSystem(ez.Collection):
83
+ """Counter must be driven by Clock in the new architecture."""
84
+
85
+ SETTINGS = CounterTestSystemSettings
86
+
87
+ CLOCK = Clock()
88
+ COUNTER = Counter()
89
+ LOG = MessageLogger()
90
+ TERM = TerminateOnTotal()
91
+
92
+ def configure(self) -> None:
93
+ self.CLOCK.apply_settings(self.SETTINGS.clock_settings)
94
+ self.COUNTER.apply_settings(self.SETTINGS.counter_settings)
95
+ self.LOG.apply_settings(self.SETTINGS.log_settings)
96
+ self.TERM.apply_settings(self.SETTINGS.term_settings)
97
+
98
+ def network(self) -> ez.NetworkDefinition:
99
+ return (
100
+ (self.CLOCK.OUTPUT_SIGNAL, self.COUNTER.INPUT_CLOCK),
101
+ (self.COUNTER.OUTPUT_SIGNAL, self.LOG.INPUT_MESSAGE),
102
+ (self.LOG.OUTPUT_MESSAGE, self.TERM.INPUT_MESSAGE),
103
+ )
104
+
105
+
106
+ @pytest.mark.parametrize(
107
+ "n_time, fs, dispatch_rate, mod",
108
+ [
109
+ (1, 10.0, math.inf, None), # AFAP mode
110
+ (20, 1000.0, 50.0, None), # Realtime mode (50 Hz dispatch = 20 samples/tick @ 1000 Hz)
111
+ (1, 1000.0, 100.0, 2**3), # 100 Hz dispatch with mod
112
+ (10, 10.0, 10.0, 2**3), # 10 Hz dispatch with mod
113
+ ],
114
+ )
115
+ def test_counter_system(
116
+ n_time: int,
117
+ fs: float,
118
+ dispatch_rate: float,
119
+ mod: int | None,
120
+ test_name: str | None = None,
121
+ ):
122
+ target_dur = 2.6 # 2.6 seconds per test
123
+ if math.isinf(dispatch_rate):
124
+ # AFAP mode - runs as fast as possible
125
+ target_messages = 100 # Fixed target for AFAP
126
+ else:
127
+ target_messages = int(target_dur * dispatch_rate)
128
+
129
+ test_filename = get_test_fn(test_name)
130
+ ez.logger.info(test_filename)
131
+ settings = CounterTestSystemSettings(
132
+ clock_settings=ClockSettings(dispatch_rate=dispatch_rate),
133
+ counter_settings=CounterSettings(
134
+ n_time=n_time,
135
+ fs=fs,
136
+ mod=mod,
137
+ ),
138
+ log_settings=MessageLoggerSettings(
139
+ output=test_filename,
140
+ ),
141
+ term_settings=TerminateOnTotalSettings(
142
+ total=target_messages,
143
+ ),
144
+ )
145
+ system = CounterTestSystem(settings)
146
+ ez.run(SYSTEM=system)
147
+
148
+ # Collect result
149
+ messages: list[AxisArray] = [_ for _ in message_log(test_filename)]
150
+ os.remove(test_filename)
151
+
152
+ if math.isinf(dispatch_rate):
153
+ # The number of messages depends on how fast the computer is
154
+ target_messages = len(messages)
155
+ # This should be an equivalence assertion (==) but the use of TerminateOnTotal does
156
+ # not guarantee that MessageLogger will exit before an additional message is received.
157
+ # Let's just clip the last message if we exceed the target messages.
158
+ if len(messages) > target_messages:
159
+ messages = messages[:target_messages]
160
+ assert len(messages) >= target_messages
161
+
162
+ # Just do one quick data check (Counter now outputs 1D array)
163
+ agg = AxisArray.concatenate(*messages, dim="time")
164
+ target_samples = n_time * target_messages
165
+ expected_data = np.arange(target_samples)
166
+ if mod is not None:
167
+ expected_data = expected_data % mod
168
+ assert np.array_equal(agg.data, expected_data)
169
+
170
+
171
+ @pytest.mark.parametrize(
172
+ "clock_rate, fs, n_time",
173
+ [
174
+ (10.0, 1000.0, 100), # 10 Hz clock, fs=1000, n_time=100 (fixed)
175
+ (20.0, 500.0, None), # 20 Hz clock, fs=500, n_time derived (25 samples per tick)
176
+ (5.0, 1000.0, None), # 5 Hz clock, fs=1000, n_time derived (200 samples per tick)
177
+ ],
178
+ )
179
+ def test_counter_with_external_clock(
180
+ clock_rate: float,
181
+ fs: float,
182
+ n_time: int | None,
183
+ test_name: str | None = None,
184
+ ):
185
+ """Test Counter driven by external Clock (now the standard pattern)."""
186
+ target_messages = 20
187
+ test_filename = get_test_fn(test_name)
188
+ ez.logger.info(test_filename)
189
+
190
+ # This now uses the same CounterTestSystem since all counters need clocks
191
+ settings = CounterTestSystemSettings(
192
+ clock_settings=ClockSettings(dispatch_rate=clock_rate),
193
+ counter_settings=CounterSettings(
194
+ fs=fs,
195
+ n_time=n_time,
196
+ ),
197
+ log_settings=MessageLoggerSettings(output=test_filename),
198
+ term_settings=TerminateOnTotalSettings(total=target_messages),
199
+ )
200
+ system = CounterTestSystem(settings)
201
+ ez.run(SYSTEM=system)
202
+
203
+ # Collect result
204
+ messages: list[AxisArray] = [_ for _ in message_log(test_filename)]
205
+ os.remove(test_filename)
206
+
207
+ assert len(messages) >= target_messages
208
+
209
+ # Verify each message has correct sample rate (gain = 1/fs)
210
+ for msg in messages:
211
+ assert msg.axes["time"].gain == 1.0 / fs
212
+
213
+ # Verify data continuity
214
+ messages = messages[:target_messages] # Trim to target
215
+ agg = AxisArray.concatenate(*messages, dim="time")
216
+
217
+ # Expected samples per tick
218
+ if n_time is not None:
219
+ expected_samples_per_tick = n_time
220
+ else:
221
+ expected_samples_per_tick = int(fs / clock_rate)
222
+
223
+ expected_total = expected_samples_per_tick * target_messages
224
+ # Allow for fractional sample accumulation variance
225
+ assert abs(len(agg.data) - expected_total) <= target_messages
226
+
227
+ # Counter values should be sequential (0, 1, 2, ...)
228
+ expected_data = np.arange(len(agg.data))
229
+ assert np.array_equal(agg.data, expected_data)
File without changes
File without changes