ezmsg-baseproc 1.0.3__tar.gz → 1.1.0__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.
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/PKG-INFO +19 -22
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/README.md +18 -21
- ezmsg_baseproc-1.1.0/docs/source/index.md +18 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/src/ezmsg/baseproc/__init__.py +23 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/src/ezmsg/baseproc/__version__.py +2 -2
- ezmsg_baseproc-1.1.0/src/ezmsg/baseproc/clock.py +109 -0
- ezmsg_baseproc-1.1.0/src/ezmsg/baseproc/counter.py +128 -0
- ezmsg_baseproc-1.1.0/tests/test_clock.py +83 -0
- ezmsg_baseproc-1.1.0/tests/test_counter.py +180 -0
- ezmsg_baseproc-1.0.3/docs/source/index.rst +0 -87
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/.github/workflows/docs.yml +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/.github/workflows/python-publish.yml +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/.github/workflows/python-tests.yml +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/.gitignore +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/.pre-commit-config.yaml +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/LICENSE +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/Makefile +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/make.bat +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/_templates/autosummary/module.rst +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/api/index.rst +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/conf.py +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/guides/ProcessorsBase.md +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/guides/how-tos/processors/adaptive.rst +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/guides/how-tos/processors/checkpoint.rst +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/guides/how-tos/processors/composite.rst +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/guides/how-tos/processors/content-processors.rst +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/guides/how-tos/processors/processor.rst +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/guides/how-tos/processors/standalone.rst +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/guides/how-tos/processors/stateful.rst +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/guides/how-tos/processors/unit.rst +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/pyproject.toml +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/src/ezmsg/baseproc/composite.py +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/src/ezmsg/baseproc/processor.py +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/src/ezmsg/baseproc/protocols.py +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/src/ezmsg/baseproc/stateful.py +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/src/ezmsg/baseproc/units.py +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/src/ezmsg/baseproc/util/__init__.py +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/src/ezmsg/baseproc/util/asio.py +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/src/ezmsg/baseproc/util/message.py +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/src/ezmsg/baseproc/util/profile.py +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/src/ezmsg/baseproc/util/typeresolution.py +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/tests/test_baseproc.py +0 -0
- {ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/tests/test_profile.py +0 -0
|
@@ -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,6 @@
|
|
|
1
1
|
# ezmsg-baseproc
|
|
2
2
|
|
|
3
|
-
Base processor classes and protocols for building
|
|
3
|
+
Base processor classes and protocols for building message-processing components in [ezmsg](https://github.com/ezmsg-org/ezmsg).
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,30 +8,26 @@ Base processor classes and protocols for building signal processing pipelines in
|
|
|
8
8
|
pip install ezmsg-baseproc
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Or install the latest development version:
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
```bash
|
|
14
|
+
pip install git+https://github.com/ezmsg-org/ezmsg-baseproc@dev
|
|
15
|
+
```
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
- **Base Classes** - Abstract base classes for building stateless and stateful processors
|
|
17
|
-
- **Composite Processors** - Classes for chaining processors into pipelines
|
|
18
|
-
- **Unit Wrappers** - ezmsg Unit base classes that wrap processors for graph integration
|
|
17
|
+
## Overview
|
|
19
18
|
|
|
20
|
-
|
|
19
|
+
``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:
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
├── profile.py # Profiling decorators
|
|
33
|
-
└── typeresolution.py # Type resolution helpers
|
|
34
|
-
```
|
|
21
|
+
* **Protocols** - Type definitions for processors, transformers, consumers, and producers
|
|
22
|
+
* **Processors** - Transform input messages to output messages
|
|
23
|
+
* **Producers** - Generate output messages without requiring input
|
|
24
|
+
* **Consumers** - Accept input messages without producing output
|
|
25
|
+
* **Transformers** - A specific type of processor with typed input/output
|
|
26
|
+
* **Stateful variants** - Processors that maintain state across invocations
|
|
27
|
+
* **Adaptive transformers** - Transformers that can be trained via ``partial_fit``
|
|
28
|
+
* **Composite processors** - Chain multiple processors together efficiently
|
|
29
|
+
|
|
30
|
+
All base classes support both synchronous and asynchronous operation, making them suitable for offline analysis and real-time streaming applications.
|
|
35
31
|
|
|
36
32
|
## Usage
|
|
37
33
|
|
|
@@ -88,6 +84,7 @@ We use [`uv`](https://docs.astral.sh/uv/getting-started/installation/) for devel
|
|
|
88
84
|
2. Clone and cd into the repository
|
|
89
85
|
3. Run `uv sync` to create a `.venv` and install dependencies
|
|
90
86
|
4. Run `uv run pytest tests` to run tests
|
|
87
|
+
5. (Optional) Install pre-commit hooks: `uv run pre-commit install`
|
|
91
88
|
|
|
92
89
|
## License
|
|
93
90
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
```{include} ../../README.md
|
|
2
|
+
```
|
|
3
|
+
|
|
4
|
+
## Documentation
|
|
5
|
+
|
|
6
|
+
```{toctree}
|
|
7
|
+
:maxdepth: 2
|
|
8
|
+
:caption: Contents:
|
|
9
|
+
|
|
10
|
+
guides/ProcessorsBase
|
|
11
|
+
guides/how-tos/processors/content-processors
|
|
12
|
+
api/index
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Indices and tables
|
|
16
|
+
|
|
17
|
+
- {ref}`genindex`
|
|
18
|
+
- {ref}`modindex`
|
|
@@ -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
|
|
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
|
|
@@ -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
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Unit tests for ezmsg.baseproc.clock module."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pytest
|
|
8
|
+
from ezmsg.util.messages.axisarray import AxisArray
|
|
9
|
+
|
|
10
|
+
from ezmsg.baseproc import ClockProducer, ClockSettings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.mark.parametrize("dispatch_rate", [math.inf, 1.0, 2.0, 5.0, 10.0, 20.0])
|
|
14
|
+
def test_clock_producer_sync(dispatch_rate: float):
|
|
15
|
+
"""Test synchronous ClockProducer via __call__."""
|
|
16
|
+
run_time = 1.0
|
|
17
|
+
n_target = 100 if math.isinf(dispatch_rate) else int(np.ceil(dispatch_rate * run_time))
|
|
18
|
+
|
|
19
|
+
producer = ClockProducer(ClockSettings(dispatch_rate=dispatch_rate))
|
|
20
|
+
|
|
21
|
+
results = []
|
|
22
|
+
t_start = time.monotonic()
|
|
23
|
+
while len(results) < n_target:
|
|
24
|
+
results.append(producer())
|
|
25
|
+
t_elapsed = time.monotonic() - t_start
|
|
26
|
+
|
|
27
|
+
# All results should be LinearAxis
|
|
28
|
+
assert all(isinstance(r, AxisArray.LinearAxis) for r in results)
|
|
29
|
+
|
|
30
|
+
# Check gain values
|
|
31
|
+
if math.isinf(dispatch_rate):
|
|
32
|
+
assert all(r.gain == 0.0 for r in results)
|
|
33
|
+
else:
|
|
34
|
+
expected_gain = 1.0 / dispatch_rate
|
|
35
|
+
assert all(abs(r.gain - expected_gain) < 1e-10 for r in results)
|
|
36
|
+
|
|
37
|
+
# Offsets (timestamps) should be monotonically increasing
|
|
38
|
+
offsets = [r.offset for r in results]
|
|
39
|
+
assert all(offsets[i] <= offsets[i + 1] for i in range(len(offsets) - 1))
|
|
40
|
+
|
|
41
|
+
# Check timing
|
|
42
|
+
if math.isfinite(dispatch_rate):
|
|
43
|
+
assert (run_time - 1 / dispatch_rate) < t_elapsed < (run_time + 0.2)
|
|
44
|
+
else:
|
|
45
|
+
# 100 usec per iteration is pretty generous for AFAP
|
|
46
|
+
assert t_elapsed < (n_target * 1e-4)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.parametrize("dispatch_rate", [math.inf, 2.0, 20.0])
|
|
50
|
+
@pytest.mark.asyncio
|
|
51
|
+
async def test_clock_producer_async(dispatch_rate: float):
|
|
52
|
+
"""Test asynchronous ClockProducer via __acall__."""
|
|
53
|
+
run_time = 1.0
|
|
54
|
+
n_target = 100 if math.isinf(dispatch_rate) else int(np.ceil(dispatch_rate * run_time))
|
|
55
|
+
|
|
56
|
+
producer = ClockProducer(ClockSettings(dispatch_rate=dispatch_rate))
|
|
57
|
+
|
|
58
|
+
results = []
|
|
59
|
+
t_start = time.monotonic()
|
|
60
|
+
while len(results) < n_target:
|
|
61
|
+
results.append(await producer.__acall__())
|
|
62
|
+
t_elapsed = time.monotonic() - t_start
|
|
63
|
+
|
|
64
|
+
# All results should be LinearAxis
|
|
65
|
+
assert all(isinstance(r, AxisArray.LinearAxis) for r in results)
|
|
66
|
+
|
|
67
|
+
# Check gain values
|
|
68
|
+
if math.isinf(dispatch_rate):
|
|
69
|
+
assert all(r.gain == 0.0 for r in results)
|
|
70
|
+
else:
|
|
71
|
+
expected_gain = 1.0 / dispatch_rate
|
|
72
|
+
assert all(abs(r.gain - expected_gain) < 1e-10 for r in results)
|
|
73
|
+
|
|
74
|
+
# Offsets (timestamps) should be monotonically increasing
|
|
75
|
+
offsets = [r.offset for r in results]
|
|
76
|
+
assert all(offsets[i] <= offsets[i + 1] for i in range(len(offsets) - 1))
|
|
77
|
+
|
|
78
|
+
# Check timing
|
|
79
|
+
if math.isfinite(dispatch_rate):
|
|
80
|
+
assert (run_time - 1.1 / dispatch_rate) < t_elapsed < (run_time + 0.1)
|
|
81
|
+
else:
|
|
82
|
+
# 100 usec per iteration is pretty generous for AFAP
|
|
83
|
+
assert t_elapsed < (n_target * 1e-4)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Unit tests for ezmsg.baseproc.counter module."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
from ezmsg.util.messages.axisarray import AxisArray
|
|
6
|
+
|
|
7
|
+
from ezmsg.baseproc import (
|
|
8
|
+
CounterSettings,
|
|
9
|
+
CounterTransformer,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestCounterTransformer:
|
|
14
|
+
"""Tests for CounterTransformer."""
|
|
15
|
+
|
|
16
|
+
def test_fixed_n_time_mode(self):
|
|
17
|
+
"""Test transformer with fixed n_time."""
|
|
18
|
+
transformer = CounterTransformer(CounterSettings(fs=1000.0, n_time=100, mod=None))
|
|
19
|
+
|
|
20
|
+
# Create clock tick with gain = 0.1 (10 Hz dispatch rate)
|
|
21
|
+
clock_tick = AxisArray.LinearAxis(gain=0.1, offset=1.0)
|
|
22
|
+
|
|
23
|
+
result = transformer(clock_tick)
|
|
24
|
+
|
|
25
|
+
assert isinstance(result, AxisArray)
|
|
26
|
+
assert result.data.shape == (100,)
|
|
27
|
+
assert result.dims == ["time"]
|
|
28
|
+
# TimeAxis has gain = 1/fs
|
|
29
|
+
assert result.axes["time"].gain == 1 / 1000.0
|
|
30
|
+
assert result.axes["time"].offset == 1.0 # Uses clock's offset
|
|
31
|
+
np.testing.assert_array_equal(result.data, np.arange(100))
|
|
32
|
+
|
|
33
|
+
def test_fixed_n_time_with_afap_clock(self):
|
|
34
|
+
"""Test transformer with fixed n_time and AFAP clock (gain=0)."""
|
|
35
|
+
transformer = CounterTransformer(CounterSettings(fs=1000.0, n_time=50, mod=None))
|
|
36
|
+
|
|
37
|
+
# AFAP clock has gain=0
|
|
38
|
+
clock_tick = AxisArray.LinearAxis(gain=0.0, offset=123.456)
|
|
39
|
+
|
|
40
|
+
result = transformer(clock_tick)
|
|
41
|
+
|
|
42
|
+
assert isinstance(result, AxisArray)
|
|
43
|
+
assert result.data.shape == (50,)
|
|
44
|
+
# With AFAP clock, offset is synthetic (counter / fs)
|
|
45
|
+
assert result.axes["time"].offset == 0.0 # First block starts at 0
|
|
46
|
+
|
|
47
|
+
# Second call
|
|
48
|
+
result2 = transformer(clock_tick)
|
|
49
|
+
assert result2.axes["time"].offset == 50 / 1000.0 # 0.05 seconds
|
|
50
|
+
|
|
51
|
+
def test_variable_n_time_mode(self):
|
|
52
|
+
"""Test transformer with n_time derived from clock gain."""
|
|
53
|
+
transformer = CounterTransformer(CounterSettings(fs=1000.0, n_time=None, mod=None))
|
|
54
|
+
|
|
55
|
+
# Clock at 10 Hz with fs=1000 -> 100 samples per tick
|
|
56
|
+
clock_tick = AxisArray.LinearAxis(gain=0.1, offset=5.0)
|
|
57
|
+
|
|
58
|
+
result = transformer(clock_tick)
|
|
59
|
+
|
|
60
|
+
assert isinstance(result, AxisArray)
|
|
61
|
+
assert result.data.shape == (100,) # 1000 * 0.1 = 100
|
|
62
|
+
assert result.axes["time"].offset == 5.0
|
|
63
|
+
|
|
64
|
+
def test_variable_n_time_fractional_accumulation(self):
|
|
65
|
+
"""Test fractional sample accumulation in variable mode."""
|
|
66
|
+
transformer = CounterTransformer(CounterSettings(fs=1000.0, n_time=None, mod=None))
|
|
67
|
+
|
|
68
|
+
# Clock at 3 Hz with fs=1000 -> 333.33... samples per tick
|
|
69
|
+
# Over 3 ticks we should get exactly 1000 samples (333 + 333 + 334)
|
|
70
|
+
clock_tick = AxisArray.LinearAxis(gain=1.0 / 3.0, offset=0.0)
|
|
71
|
+
|
|
72
|
+
# First tick: 333.33 -> 333 samples, 0.33 fractional
|
|
73
|
+
result1 = transformer(clock_tick)
|
|
74
|
+
assert result1.data.shape == (333,)
|
|
75
|
+
|
|
76
|
+
# Second tick: 333.33 + 0.33 = 666.66 -> 333 samples, 0.66 fractional
|
|
77
|
+
result2 = transformer(clock_tick)
|
|
78
|
+
assert result2.data.shape == (333,)
|
|
79
|
+
|
|
80
|
+
# Third tick: 333.33 + 0.66 = 999.99... ≈ 1000 -> 334 samples
|
|
81
|
+
result3 = transformer(clock_tick)
|
|
82
|
+
assert result3.data.shape == (334,)
|
|
83
|
+
|
|
84
|
+
# Verify total is exactly 1000 (3 ticks * 333.33... = 1000)
|
|
85
|
+
total_samples = sum(r.data.shape[0] for r in [result1, result2, result3])
|
|
86
|
+
assert total_samples == 1000
|
|
87
|
+
|
|
88
|
+
# Fourth tick starts fresh cycle: 333 samples
|
|
89
|
+
result4 = transformer(clock_tick)
|
|
90
|
+
assert result4.data.shape == (333,)
|
|
91
|
+
|
|
92
|
+
def test_variable_n_time_returns_none_when_no_samples(self):
|
|
93
|
+
"""Test that transformer returns None when not enough samples accumulated."""
|
|
94
|
+
transformer = CounterTransformer(
|
|
95
|
+
CounterSettings(fs=10.0, n_time=None, mod=None) # Low fs
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Clock at 100 Hz with fs=10 -> 0.1 samples per tick
|
|
99
|
+
clock_tick = AxisArray.LinearAxis(gain=0.01, offset=0.0)
|
|
100
|
+
|
|
101
|
+
# Need 10 ticks to accumulate 1 sample
|
|
102
|
+
for _ in range(9):
|
|
103
|
+
result = transformer(clock_tick)
|
|
104
|
+
assert result is None
|
|
105
|
+
|
|
106
|
+
# 10th tick should produce 1 sample
|
|
107
|
+
result = transformer(clock_tick)
|
|
108
|
+
assert result is not None
|
|
109
|
+
assert result.data.shape == (1,)
|
|
110
|
+
|
|
111
|
+
def test_variable_n_time_afap_raises_error(self):
|
|
112
|
+
"""Test that variable mode with AFAP clock raises error."""
|
|
113
|
+
transformer = CounterTransformer(CounterSettings(fs=1000.0, n_time=None, mod=None))
|
|
114
|
+
|
|
115
|
+
clock_tick = AxisArray.LinearAxis(gain=0.0, offset=0.0)
|
|
116
|
+
|
|
117
|
+
with pytest.raises(ValueError, match="Cannot use clock with gain=0"):
|
|
118
|
+
transformer(clock_tick)
|
|
119
|
+
|
|
120
|
+
def test_mod_rollover(self):
|
|
121
|
+
"""Test counter rollover with mod."""
|
|
122
|
+
transformer = CounterTransformer(CounterSettings(fs=100.0, n_time=10, mod=8))
|
|
123
|
+
|
|
124
|
+
clock_tick = AxisArray.LinearAxis(gain=0.1, offset=0.0)
|
|
125
|
+
|
|
126
|
+
result = transformer(clock_tick)
|
|
127
|
+
np.testing.assert_array_equal(result.data, [0, 1, 2, 3, 4, 5, 6, 7, 0, 1])
|
|
128
|
+
|
|
129
|
+
def test_continuity_across_calls(self):
|
|
130
|
+
"""Test counter continuity across multiple calls."""
|
|
131
|
+
transformer = CounterTransformer(CounterSettings(fs=100.0, n_time=5, mod=None))
|
|
132
|
+
|
|
133
|
+
clock_tick = AxisArray.LinearAxis(gain=0.05, offset=0.0)
|
|
134
|
+
|
|
135
|
+
results = [transformer(clock_tick) for _ in range(4)]
|
|
136
|
+
agg = AxisArray.concatenate(*results, dim="time")
|
|
137
|
+
np.testing.assert_array_equal(agg.data, np.arange(20))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TestCounterTransformerExternalClock:
|
|
141
|
+
"""Tests for external clock mode patterns."""
|
|
142
|
+
|
|
143
|
+
def test_external_clock_with_fixed_n_time(self):
|
|
144
|
+
"""Test external clock mode with fixed n_time."""
|
|
145
|
+
transformer = CounterTransformer(CounterSettings(fs=1000.0, n_time=100, mod=None))
|
|
146
|
+
|
|
147
|
+
# Simulate external clock ticks
|
|
148
|
+
timestamps = [1.0, 1.1, 1.2, 1.3, 1.4]
|
|
149
|
+
clock_ticks = [AxisArray.LinearAxis(gain=0.1, offset=ts) for ts in timestamps]
|
|
150
|
+
|
|
151
|
+
results = [transformer(tick) for tick in clock_ticks]
|
|
152
|
+
|
|
153
|
+
# Verify offsets match clock timestamps
|
|
154
|
+
offsets = [r.axes["time"].offset for r in results]
|
|
155
|
+
np.testing.assert_array_equal(offsets, timestamps)
|
|
156
|
+
|
|
157
|
+
# Verify data continuity
|
|
158
|
+
agg = AxisArray.concatenate(*results, dim="time")
|
|
159
|
+
np.testing.assert_array_equal(agg.data, np.arange(500))
|
|
160
|
+
|
|
161
|
+
def test_external_clock_variable_chunk_sizes(self):
|
|
162
|
+
"""Test external clock mode with variable chunk sizes."""
|
|
163
|
+
transformer = CounterTransformer(CounterSettings(fs=1000.0, n_time=None, mod=None))
|
|
164
|
+
|
|
165
|
+
# Clock with varying rates
|
|
166
|
+
clock_ticks = [
|
|
167
|
+
AxisArray.LinearAxis(gain=0.1, offset=0.0), # 100 samples
|
|
168
|
+
AxisArray.LinearAxis(gain=0.05, offset=0.1), # 50 samples
|
|
169
|
+
AxisArray.LinearAxis(gain=0.2, offset=0.15), # 200 samples
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
results = [transformer(tick) for tick in clock_ticks]
|
|
173
|
+
|
|
174
|
+
assert results[0].data.shape == (100,)
|
|
175
|
+
assert results[1].data.shape == (50,)
|
|
176
|
+
assert results[2].data.shape == (200,)
|
|
177
|
+
|
|
178
|
+
# Verify data continuity
|
|
179
|
+
agg = AxisArray.concatenate(*results, dim="time")
|
|
180
|
+
np.testing.assert_array_equal(agg.data, np.arange(350))
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
ezmsg.baseproc
|
|
2
|
-
==============
|
|
3
|
-
|
|
4
|
-
Base processor classes and utilities for building message-processing components in the `ezmsg <https://www.ezmsg.org>`_ framework.
|
|
5
|
-
|
|
6
|
-
Overview
|
|
7
|
-
--------
|
|
8
|
-
|
|
9
|
-
``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:
|
|
10
|
-
|
|
11
|
-
* **Processors** - Transform input messages to output messages
|
|
12
|
-
* **Producers** - Generate output messages without requiring input
|
|
13
|
-
* **Consumers** - Accept input messages without producing output
|
|
14
|
-
* **Transformers** - A specific type of processor with typed input/output
|
|
15
|
-
* **Stateful variants** - Processors that maintain state across invocations
|
|
16
|
-
* **Adaptive transformers** - Transformers that can be trained via ``partial_fit``
|
|
17
|
-
* **Composite processors** - Chain multiple processors together efficiently
|
|
18
|
-
|
|
19
|
-
All base classes support both synchronous and asynchronous operation, making them suitable for offline analysis and real-time streaming applications.
|
|
20
|
-
|
|
21
|
-
Installation
|
|
22
|
-
------------
|
|
23
|
-
|
|
24
|
-
Install from PyPI:
|
|
25
|
-
|
|
26
|
-
.. code-block:: bash
|
|
27
|
-
|
|
28
|
-
pip install ezmsg-baseproc
|
|
29
|
-
|
|
30
|
-
Or install the latest development version:
|
|
31
|
-
|
|
32
|
-
.. code-block:: bash
|
|
33
|
-
|
|
34
|
-
pip install git+https://github.com/ezmsg-org/ezmsg-baseproc@main
|
|
35
|
-
|
|
36
|
-
Dependencies
|
|
37
|
-
^^^^^^^^^^^^
|
|
38
|
-
|
|
39
|
-
Core dependencies:
|
|
40
|
-
|
|
41
|
-
* ``ezmsg`` - Core messaging framework
|
|
42
|
-
* ``typing-extensions`` - Extended typing support
|
|
43
|
-
|
|
44
|
-
Quick Start
|
|
45
|
-
-----------
|
|
46
|
-
|
|
47
|
-
For general ezmsg tutorials and guides, visit `ezmsg.org <https://www.ezmsg.org>`_.
|
|
48
|
-
|
|
49
|
-
Here's a simple example of creating a custom transformer:
|
|
50
|
-
|
|
51
|
-
.. code-block:: python
|
|
52
|
-
|
|
53
|
-
import ezmsg.core as ez
|
|
54
|
-
from ezmsg.baseproc import BaseTransformer, BaseTransformerUnit
|
|
55
|
-
|
|
56
|
-
class MySettings(ez.Settings):
|
|
57
|
-
scale: float = 1.0
|
|
58
|
-
|
|
59
|
-
class MyTransformer(BaseTransformer[MySettings, float, float]):
|
|
60
|
-
def _process(self, message: float) -> float:
|
|
61
|
-
return message * self.settings.scale
|
|
62
|
-
|
|
63
|
-
# Use standalone
|
|
64
|
-
transformer = MyTransformer(scale=2.0)
|
|
65
|
-
result = transformer(5.0) # Returns 10.0
|
|
66
|
-
|
|
67
|
-
# Or wrap in an ezmsg Unit
|
|
68
|
-
class MyUnit(BaseTransformerUnit[MySettings, float, float, MyTransformer]):
|
|
69
|
-
SETTINGS = MySettings
|
|
70
|
-
|
|
71
|
-
Documentation
|
|
72
|
-
-------------
|
|
73
|
-
|
|
74
|
-
.. toctree::
|
|
75
|
-
:maxdepth: 2
|
|
76
|
-
:caption: Contents:
|
|
77
|
-
|
|
78
|
-
guides/ProcessorsBase
|
|
79
|
-
guides/how-tos/processors/content-processors
|
|
80
|
-
api/index
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
Indices and tables
|
|
84
|
-
------------------
|
|
85
|
-
|
|
86
|
-
* :ref:`genindex`
|
|
87
|
-
* :ref:`modindex`
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/guides/how-tos/processors/adaptive.rst
RENAMED
|
File without changes
|
{ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/guides/how-tos/processors/checkpoint.rst
RENAMED
|
File without changes
|
{ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/guides/how-tos/processors/composite.rst
RENAMED
|
File without changes
|
|
File without changes
|
{ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/guides/how-tos/processors/processor.rst
RENAMED
|
File without changes
|
{ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/guides/how-tos/processors/standalone.rst
RENAMED
|
File without changes
|
{ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/guides/how-tos/processors/stateful.rst
RENAMED
|
File without changes
|
{ezmsg_baseproc-1.0.3 → ezmsg_baseproc-1.1.0}/docs/source/guides/how-tos/processors/unit.rst
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|