ezmsg-sigproc 1.8.2__tar.gz → 2.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_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/PKG-INFO +2 -1
- ezmsg_sigproc-2.1.0/docs/ProcessorsBase.md +173 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/pyproject.toml +14 -16
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/src/ezmsg/sigproc/__version__.py +2 -2
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/activation.py +84 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/adaptive_lattice_notch.py +231 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/affinetransform.py +239 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/aggregate.py +215 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/bandpower.py +88 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/base.py +1284 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/src/ezmsg/sigproc/butterworthfilter.py +37 -33
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/src/ezmsg/sigproc/cheby.py +29 -17
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/combfilter.py +163 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/src/ezmsg/sigproc/decimate.py +19 -10
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/detrend.py +29 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/diff.py +81 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/downsample.py +120 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/ewma.py +197 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/extract_axis.py +41 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/filter.py +315 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/filterbank.py +329 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/math/abs.py +29 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/math/clip.py +40 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/src/ezmsg/sigproc/math/difference.py +34 -30
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/math/invert.py +23 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/math/log.py +47 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/math/scale.py +32 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/quantize.py +71 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/resample.py +298 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/sampler.py +308 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/scaler.py +127 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/signalinjector.py +81 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/slicer.py +158 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/spectrogram.py +97 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/spectrum.py +298 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/synth.py +774 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/transpose.py +131 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/util/asio.py +156 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/util/message.py +31 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/src/ezmsg/sigproc/util/profile.py +55 -12
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/util/typeresolution.py +83 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/wavelets.py +196 -0
- ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/window.py +380 -0
- ezmsg_sigproc-2.1.0/tests/conftest.py +4 -0
- ezmsg_sigproc-2.1.0/tests/helpers/__init__.py +0 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/tests/helpers/util.py +84 -8
- ezmsg_sigproc-2.1.0/tests/integration/bytewax/test_spectrum_bytewax.py +68 -0
- ezmsg_sigproc-2.1.0/tests/integration/bytewax/test_window_bytewax.py +59 -0
- ezmsg_sigproc-1.8.2/tests/test_butterworth.py → ezmsg_sigproc-2.1.0/tests/integration/ezmsg/test_butterworth_system.py +2 -2
- ezmsg_sigproc-1.8.2/tests/test_decimate.py → ezmsg_sigproc-2.1.0/tests/integration/ezmsg/test_decimate_system.py +1 -1
- ezmsg_sigproc-1.8.2/tests/test_downsample.py → ezmsg_sigproc-2.1.0/tests/integration/ezmsg/test_downsample_system.py +3 -72
- {ezmsg_sigproc-1.8.2/tests → ezmsg_sigproc-2.1.0/tests/integration/ezmsg}/test_filter_system.py +1 -1
- ezmsg_sigproc-1.8.2/tests/test_sampler.py → ezmsg_sigproc-2.1.0/tests/integration/ezmsg/test_sampler_system.py +5 -92
- ezmsg_sigproc-1.8.2/tests/test_scaler.py → ezmsg_sigproc-2.1.0/tests/integration/ezmsg/test_scaler_system.py +2 -74
- ezmsg_sigproc-2.1.0/tests/integration/ezmsg/test_spectrum_system.py +104 -0
- ezmsg_sigproc-1.8.2/tests/test_synth.py → ezmsg_sigproc-2.1.0/tests/integration/ezmsg/test_synth_system.py +2 -143
- ezmsg_sigproc-2.1.0/tests/integration/ezmsg/test_window_system.py +192 -0
- ezmsg_sigproc-2.1.0/tests/test_profile.py +191 -0
- ezmsg_sigproc-2.1.0/tests/unit/test_adaptive_lattice_notch.py +159 -0
- {ezmsg_sigproc-1.8.2/tests → ezmsg_sigproc-2.1.0/tests/unit}/test_affine_transform.py +3 -3
- {ezmsg_sigproc-1.8.2/tests → ezmsg_sigproc-2.1.0/tests/unit}/test_aggregate.py +25 -1
- {ezmsg_sigproc-1.8.2/tests → ezmsg_sigproc-2.1.0/tests/unit}/test_bandpower.py +6 -2
- ezmsg_sigproc-2.1.0/tests/unit/test_base.py +1183 -0
- {ezmsg_sigproc-1.8.2/tests → ezmsg_sigproc-2.1.0/tests/unit}/test_butter.py +95 -0
- ezmsg_sigproc-2.1.0/tests/unit/test_combfilter.py +332 -0
- ezmsg_sigproc-2.1.0/tests/unit/test_diff.py +127 -0
- ezmsg_sigproc-2.1.0/tests/unit/test_downsample.py +77 -0
- ezmsg_sigproc-2.1.0/tests/unit/test_ewma.py +51 -0
- ezmsg_sigproc-2.1.0/tests/unit/test_extract_axis.py +65 -0
- {ezmsg_sigproc-1.8.2/tests → ezmsg_sigproc-2.1.0/tests/unit}/test_filterbank.py +1 -9
- ezmsg_sigproc-2.1.0/tests/unit/test_quantize.py +89 -0
- ezmsg_sigproc-2.1.0/tests/unit/test_resample.py +145 -0
- ezmsg_sigproc-2.1.0/tests/unit/test_sampler.py +94 -0
- ezmsg_sigproc-2.1.0/tests/unit/test_scaler.py +73 -0
- {ezmsg_sigproc-1.8.2/tests → ezmsg_sigproc-2.1.0/tests/unit}/test_slicer.py +1 -1
- {ezmsg_sigproc-1.8.2/tests → ezmsg_sigproc-2.1.0/tests/unit}/test_spectrogram.py +7 -4
- {ezmsg_sigproc-1.8.2/tests → ezmsg_sigproc-2.1.0/tests/unit}/test_spectrum.py +1 -96
- ezmsg_sigproc-2.1.0/tests/unit/test_synth.py +150 -0
- ezmsg_sigproc-2.1.0/tests/unit/test_transpose.py +64 -0
- ezmsg_sigproc-2.1.0/tests/unit/test_util.py +121 -0
- {ezmsg_sigproc-1.8.2/tests → ezmsg_sigproc-2.1.0/tests/unit}/test_wavelets.py +1 -9
- ezmsg_sigproc-2.1.0/tests/unit/test_window.py +219 -0
- ezmsg_sigproc-2.1.0/uv.lock +729 -0
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/activation.py +0 -87
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/affinetransform.py +0 -233
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/aggregate.py +0 -183
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/bandpower.py +0 -76
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/base.py +0 -42
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/downsample.py +0 -126
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/filter.py +0 -199
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/filterbank.py +0 -281
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/math/abs.py +0 -34
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/math/clip.py +0 -40
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/math/invert.py +0 -35
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/math/log.py +0 -52
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/math/scale.py +0 -40
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/sampler.py +0 -326
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/scaler.py +0 -290
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/signalinjector.py +0 -72
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/slicer.py +0 -166
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/spectrogram.py +0 -95
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/spectrum.py +0 -263
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/synth.py +0 -621
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/wavelets.py +0 -195
- ezmsg_sigproc-1.8.2/src/ezmsg/sigproc/window.py +0 -322
- ezmsg_sigproc-1.8.2/tests/conftest.py +0 -4
- ezmsg_sigproc-1.8.2/tests/test_window.py +0 -466
- ezmsg_sigproc-1.8.2/uv.lock +0 -716
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/.github/workflows/python-publish-ezmsg-sigproc.yml +0 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/.github/workflows/python-tests.yml +0 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/.gitignore +0 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/.pre-commit-config.yaml +0 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/LICENSE.txt +0 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/README.md +0 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/src/ezmsg/sigproc/__init__.py +0 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/src/ezmsg/sigproc/ewmfilter.py +0 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/src/ezmsg/sigproc/math/__init__.py +0 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/src/ezmsg/sigproc/messages.py +0 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/src/ezmsg/sigproc/spectral.py +0 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/src/ezmsg/sigproc/util/__init__.py +0 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/src/ezmsg/sigproc/util/sparse.py +0 -0
- {ezmsg_sigproc-1.8.2/tests/helpers → ezmsg_sigproc-2.1.0/tests}/__init__.py +0 -0
- {ezmsg_sigproc-1.8.2 → ezmsg_sigproc-2.1.0}/tests/resources/xform.csv +0 -0
- {ezmsg_sigproc-1.8.2/tests → ezmsg_sigproc-2.1.0/tests/unit}/test_activation.py +0 -0
- {ezmsg_sigproc-1.8.2/tests → ezmsg_sigproc-2.1.0/tests/unit}/test_math.py +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ezmsg-sigproc
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Timeseries signal processing implementations in ezmsg
|
|
5
5
|
Author-email: Griffin Milsap <griffin.milsap@gmail.com>, Preston Peranich <pperanich@gmail.com>, Chadwick Boulay <chadwick.boulay@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
License-File: LICENSE.txt
|
|
8
8
|
Requires-Python: >=3.10.15
|
|
9
|
+
Requires-Dist: array-api-compat>=1.11.1
|
|
9
10
|
Requires-Dist: ezmsg>=3.6.0
|
|
10
11
|
Requires-Dist: numba>=0.61.0
|
|
11
12
|
Requires-Dist: numpy>=1.26.0
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
## Processor Base Classes
|
|
2
|
+
|
|
3
|
+
The `ezmsg.sigproc.base` module contains the base classes for the signal processors. The base classes are designed to allow users to create custom signal processors with minimal errors and minimal repetition of boilerplate code.
|
|
4
|
+
|
|
5
|
+
> The information below was written at the time of a major refactor to `ezmsg.sigproc.base` to help collate the design decisions and to help with future refactoring. However, it may be outdated or incomplete. Please refer to the source code for the most accurate information.
|
|
6
|
+
|
|
7
|
+
> Updates: Added CompositeProducer, BaseProcessorUnit.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Generic TypeVars
|
|
11
|
+
|
|
12
|
+
| Idx | Class | Description |
|
|
13
|
+
|-----|-----------------------|----------------------------------------------------------------------------|
|
|
14
|
+
| 1 | `MessageInType` (Mi) | for messages passed to a consumer, processor, or transformer |
|
|
15
|
+
| 2 | `MessageOutType` (Mo) | for messages returned by a producer, processor, or transformer |
|
|
16
|
+
| 3 | `SettingsType` | bound to ez.Settings |
|
|
17
|
+
| 4 | `StateType` (St) | bound to ProcessorState which is simply ez.State with a `hash: int` field. |
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Protocols
|
|
21
|
+
|
|
22
|
+
| Idx | Class | Parent | State | `__call__` sig | @state | partial_fit |
|
|
23
|
+
|-----|-----------------------|--------|-------|--------------------------|--------|-------------|
|
|
24
|
+
| 1 | `Processor` | - | No | Any -> Any | - | - |
|
|
25
|
+
| 2 | `Producer` | - | No | None -> Mo | - | - |
|
|
26
|
+
| 3 | `Consumer` | 1 | No | Mi -> None | - | - |
|
|
27
|
+
| 4 | `Transformer` | 1 | No | Mi -> Mo | - | - |
|
|
28
|
+
| 5 | `StatefulProcessor` | - | Yes | Any -> Any | Y | - |
|
|
29
|
+
| 6 | `StatefulProducer` | - | Yes | None -> Mo | Y | - |
|
|
30
|
+
| 7 | `StatefulConsumer` | 5 | Yes | Mi -> None | Y | - |
|
|
31
|
+
| 8 | `StatefulTransformer` | 5 | Yes | Mi -> Mo | Y | - |
|
|
32
|
+
| 9 | `AdaptiveTransformer` | 8 | Yes | Mi -> Mo | Y | Y |
|
|
33
|
+
|
|
34
|
+
Note: `__call__` and `partial_fit` both have asynchronous alternatives: `__acall__` and `apartial_fit` respectively.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
### Abstract implementations (Base Classes) for standalone processors
|
|
38
|
+
|
|
39
|
+
| Idx | Class | Parent | Protocol | Features |
|
|
40
|
+
|-----|---------------------------|--------|----------|--------------------------------------------------------------------------------------------|
|
|
41
|
+
| 1 | `BaseProcessor` | - | 1 | `__init__` for settings; `__call__` (alias: `send`) wraps abstract `_process`. |
|
|
42
|
+
| 2 | `BaseProducer` | - | 2 | Similar to `BaseProcessor`; `next`/`anext` instead of `send`/`asend` aliases. async first! |
|
|
43
|
+
| 3 | `BaseConsumer` | 1 | 3 | Overrides return type to None |
|
|
44
|
+
| 4 | `BaseTransformer` | 1 | 4 | Overrides input and return types |
|
|
45
|
+
| 5 | `BaseStatefulProcessor` | 1 | 5 | `state` setter unpickles arg; `stateful_op` wraps `__call__` |
|
|
46
|
+
| 6 | `BaseStatefulProducer` | 2 | 6 | `state` setter and getter; `stateful_op` wraps `__call__` which runs `__acall__`. |
|
|
47
|
+
| 7 | `BaseStatefulConsumer` | 5 | 7 | Overrides return type to None |
|
|
48
|
+
| 8 | `BaseStatefulTransformer` | 5 | 8 | Overrides input and return types |
|
|
49
|
+
| 9 | `BaseAdaptiveTransformer` | 8 | 9 | Implements `partial_fit`. `__call__` may call `partial_fit` if message has `.trigger`. |
|
|
50
|
+
| 10 | `BaseAsyncTransformer` | 8 | 8 | `__acall__` wraps abstract `_aprocess`; `__call__` runs `__acall__`. |
|
|
51
|
+
| 11 | `CompositeProcessor` | 1 | 5 | Methods iterate over sequence of processors created in `_initialize_processors`. |
|
|
52
|
+
| 12 | `CompositeProducer` | 2 | 6 | Similar to `CompositeProcessor`, but first processor must be a producer. |
|
|
53
|
+
|
|
54
|
+
NOTES:
|
|
55
|
+
1. Producers do not inherit from `BaseProcessor`, so concrete implementations should subclass `BaseProducer` or `BaseStatefulProducer`.
|
|
56
|
+
2. For concrete implementations of non-producer processors, inherit from the base subclasses of `BaseProcessor` (eg. `BaseConsumer`, `BaseTransformer`) and from base subclasses of `BaseStatefulProcessor`. These two processor classes are primarily used for efficient abstract base class construction.
|
|
57
|
+
3. For most base classes, the async methods simply call the synchronous methods where the processor logic is expected. Exceptions are `BaseProducer` (and its children) and `BaseAsyncTransformer` which are async-first and should be strongly considered for operations that are I/O bound.
|
|
58
|
+
4. For async-first classes, the logic is implemented in the async methods and the sync methods are thin wrappers around the async methods. The wrapper uses a helper method called `run_coroutine_sync` to run the async method in a synchronous context, but this adds some noticeable processing overhead.
|
|
59
|
+
5. If you need to call your processor outside ezmsg (which uses async), and you cannot easily add an async context* in your processing, then you might want to consider duplicating the processor logic in the sync methods. __Note__: Jupyter notebooks are async by default, so you can await async code in a notebook without any extra setup.
|
|
60
|
+
6. `CompositeProcessor` and `CompositeProducer` are stateful, and structurally subclass the `StatefulProcessor` and `StatefulProducer` protocols, but they
|
|
61
|
+
do not inherit from `BaseStatefulProcessor` and `BaseStatefulProducer`. They accomplish statefulness by inheriting from the mixin abstract base class `CompositeStateful`, which implements the state related methods: `get_state_type`, `state.setter`, `state.getter`, `_hash_message`, `_reset_state`, and `stateful_op` (as well as composite processor chain related methods). However, `BaseStatefulProcessor`, `BaseStatefulProducer` implement `stateful_op` method for a single processor in an incompatible way to what is required for composite chains of processors.
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
### Generic TypeVars for ezmsg Units
|
|
65
|
+
|
|
66
|
+
| Idx | Class | Description |
|
|
67
|
+
|-----|---------------------------|------------------------------------------------------------------------------------------------------------------|
|
|
68
|
+
| 5 | `ProducerType` | bound to `BaseProducer` (hence, also `BaseStatefulProducer`, `CompositeProducer`) |
|
|
69
|
+
| 6 | `ConsumerType` | bound to `BaseConsumer`, `BaseStatefulConsumer` |
|
|
70
|
+
| 7 | `TransformerType` | bound to `BaseTransformer`, `BaseStatefulTransformer`, `CompositeProcessor` (hence, also `BaseAsyncTransformer`) |
|
|
71
|
+
| 8 | `AdaptiveTransformerType` | bound to `BaseAdaptiveTransformer` |
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
### Abstract implementations (Base Classes) for ezmsg Units using processors:
|
|
75
|
+
|
|
76
|
+
| Idx | Class | Parents | Expected TypeVars |
|
|
77
|
+
|-----|-------------------------------|---------|---------------------------|
|
|
78
|
+
| 1 | `BaseProcessorUnit` | - | - |
|
|
79
|
+
| 2 | `BaseProducerUnit` | - | `ProducerType` |
|
|
80
|
+
| 3 | `BaseConsumerUnit` | 1 | `ConsumerType` |
|
|
81
|
+
| 4 | `BaseTransformerUnit` | 1 | `TransformerType` |
|
|
82
|
+
| 5 | `BaseAdaptiveTransformerUnit` | 1 | `AdaptiveTransformerType` |
|
|
83
|
+
|
|
84
|
+
Note, it is strongly recommended to use `BaseConsumerUnit`, `BaseTransformerUnit`, or `BaseAdaptiveTransformerUnit` for implementing concrete subclasses rather than `BaseProcessorUnit`.
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
## Implementing a custom standalone processor
|
|
88
|
+
|
|
89
|
+
1. Create a new settings dataclass: `class MySettings(ez.Settings):`
|
|
90
|
+
2. Create a new state dataclass:
|
|
91
|
+
```
|
|
92
|
+
@processor_state
|
|
93
|
+
class MyState:
|
|
94
|
+
```
|
|
95
|
+
3. Decide on your base processor class, considering the protocol, whether it should be async-first, and other factors.
|
|
96
|
+
|
|
97
|
+
```mermaid
|
|
98
|
+
flowchart TD
|
|
99
|
+
AMP{Multiple Processors?};
|
|
100
|
+
AMP -->|no| ARI{Receives Input?};
|
|
101
|
+
AMP -->|yes| ACB{Single Chain / Branching?}
|
|
102
|
+
ARI -->|no| P(Producer);
|
|
103
|
+
ARI -->|yes| APO{Produces Output?};
|
|
104
|
+
ACB -->|branching| NBC[no base class];
|
|
105
|
+
ACB -->|single chain| ACRI{Receives Input?};
|
|
106
|
+
P --> PS{Stateful?};
|
|
107
|
+
APO -->|no| C(Consumer);
|
|
108
|
+
APO -->|yes| T(Transformer);
|
|
109
|
+
ACRI -->|no| CompositeProducer;
|
|
110
|
+
ACRI -->|yes| CompositeProcessor;
|
|
111
|
+
PS -->|no| BaseProducer;
|
|
112
|
+
PS -->|yes| BaseStatefulProducer;
|
|
113
|
+
C --> CS{Stateful?};
|
|
114
|
+
T --> TS{Stateful?};
|
|
115
|
+
CS -->|no| BaseConsumer;
|
|
116
|
+
CS -->|yes| BaseStatefulConsumer;
|
|
117
|
+
TS -->|no| BaseTransformer;
|
|
118
|
+
TS -->|yes| TSA{Adaptive?};
|
|
119
|
+
TSA -->|no| TSAF{Async First?};
|
|
120
|
+
TSA -->|yes| BaseAdaptiveTransformer;
|
|
121
|
+
TSAF -->|no| BaseStatefulTransformer;
|
|
122
|
+
TSAF -->|yes| BaseAsyncTransformer;
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
4. Implement the child class.
|
|
126
|
+
* The minimum implementation is `_process` for sync processors, `_aprocess` for async processors, and `_produce` for producers.
|
|
127
|
+
* For any stateful processor, implement `_reset_state`.
|
|
128
|
+
* For stateful processors that need to respond to a change in the incoming data, implement `_hash_message`.
|
|
129
|
+
* For adaptive transformers, implement `partial_fit`.
|
|
130
|
+
* For chains of processors (`CompositeProcessor`/ `CompositeProducer`), need to implement `_initialize_processors`.
|
|
131
|
+
* See processors in `ezmsg.sigproc` for examples.
|
|
132
|
+
5. Override non-abstract methods if you need special behaviour. For example:
|
|
133
|
+
* `WindowTransformer` overrides `__init__` to do some sanity checks on the provided settings.
|
|
134
|
+
* `TransposeTransformer` and `WindowTransformer` override `__call__` to provide a passthrough shortcut when the settings allow for it.
|
|
135
|
+
* `ClockProducer` overrides `__call__` in order to provide a synchronous call bypassing the default async behaviour.
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
## Implementing a custom ezmsg Unit
|
|
139
|
+
|
|
140
|
+
1. Create and test custom standalone processor as above.
|
|
141
|
+
2. Decide which base unit to implement.
|
|
142
|
+
* Use the "Generic TypeVars for ezmsg Units" table above to determine the expected TypeVar.
|
|
143
|
+
* Fine the Expected TypeVar in the "ezmsg Units" table.
|
|
144
|
+
3. Create the derived class.
|
|
145
|
+
|
|
146
|
+
Often, all that is required is the following (e.g., for a custom transformer):
|
|
147
|
+
|
|
148
|
+
```Python
|
|
149
|
+
import ezmsg.core as ez
|
|
150
|
+
from ezmsg.util.messages.axisarray import AxisArray
|
|
151
|
+
from ezmsg.sigproc.base import BaseTransformer, BaseTransformerUnit
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class CustomTransformerSettings(ez.Settings):
|
|
155
|
+
...
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class CustomTransformer(BaseTransformer[CustomTransformerSettings, AxisArray, AxisArray]):
|
|
159
|
+
def _process(self, message: AxisArray) -> AxisArray:
|
|
160
|
+
# Your processing code here...
|
|
161
|
+
return message
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class CustomUnit(BaseTransformerUnit[
|
|
165
|
+
CustomTransformerSettings, # SettingsType
|
|
166
|
+
AxisArray, # MessageInType
|
|
167
|
+
AxisArray, # MessageOutType
|
|
168
|
+
CustomTransformer, # TransformerType
|
|
169
|
+
]):
|
|
170
|
+
SETTINGS = CustomTransformerSettings
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
__Note__, the type of ProcessorUnit is based on the internal processor and not the input or output of the unit. Input streams are allowed in ProducerUnits and output streams in ConsumerUnits. For an example of such a use case, see `BaseCounterFirstProducerUnit` and its subclasses. `BaseCounterFirstProducerUnit` has an input stream that receives a flag signal from a clock that triggers a call to the internal producer.
|
|
@@ -4,28 +4,29 @@ description = "Timeseries signal processing implementations in ezmsg"
|
|
|
4
4
|
authors = [
|
|
5
5
|
{ name = "Griffin Milsap", email = "griffin.milsap@gmail.com" },
|
|
6
6
|
{ name = "Preston Peranich", email = "pperanich@gmail.com" },
|
|
7
|
-
{ name = "Chadwick Boulay", email = "chadwick.boulay@gmail.com" }
|
|
7
|
+
{ name = "Chadwick Boulay", email = "chadwick.boulay@gmail.com" },
|
|
8
8
|
]
|
|
9
9
|
license = "MIT"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.10.15"
|
|
12
12
|
dynamic = ["version"]
|
|
13
13
|
dependencies = [
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
"array-api-compat>=1.11.1",
|
|
15
|
+
"ezmsg>=3.6.0",
|
|
16
|
+
"numba>=0.61.0",
|
|
17
|
+
"numpy>=1.26.0",
|
|
18
|
+
"pywavelets>=1.6.0",
|
|
19
|
+
"scipy>=1.13.1",
|
|
20
|
+
"sparse>=0.15.4",
|
|
20
21
|
]
|
|
21
22
|
|
|
22
23
|
[project.optional-dependencies]
|
|
23
24
|
test = [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
"flake8>=7.1.1",
|
|
26
|
+
"frozendict>=2.4.4",
|
|
27
|
+
"pytest-asyncio>=0.24.0",
|
|
28
|
+
"pytest-cov>=5.0.0",
|
|
29
|
+
"pytest>=8.3.3",
|
|
29
30
|
]
|
|
30
31
|
|
|
31
32
|
[build-system]
|
|
@@ -42,10 +43,7 @@ version-file = "src/ezmsg/sigproc/__version__.py"
|
|
|
42
43
|
packages = ["src/ezmsg"]
|
|
43
44
|
|
|
44
45
|
[tool.uv]
|
|
45
|
-
dev-dependencies = [
|
|
46
|
-
"pre-commit>=3.8.0",
|
|
47
|
-
"ruff>=0.6.7",
|
|
48
|
-
]
|
|
46
|
+
dev-dependencies = ["pre-commit>=3.8.0", "ruff>=0.6.7"]
|
|
49
47
|
|
|
50
48
|
[tool.pytest.ini_options]
|
|
51
49
|
norecursedirs = "tests/helpers"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import scipy.special
|
|
2
|
+
import ezmsg.core as ez
|
|
3
|
+
from ezmsg.util.messages.axisarray import AxisArray
|
|
4
|
+
from ezmsg.util.messages.util import replace
|
|
5
|
+
|
|
6
|
+
from .spectral import OptionsEnum
|
|
7
|
+
from .base import BaseTransformer, BaseTransformerUnit
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ActivationFunction(OptionsEnum):
|
|
11
|
+
"""Activation (transformation) function."""
|
|
12
|
+
|
|
13
|
+
NONE = "none"
|
|
14
|
+
"""None."""
|
|
15
|
+
|
|
16
|
+
SIGMOID = "sigmoid"
|
|
17
|
+
""":obj:`scipy.special.expit`"""
|
|
18
|
+
|
|
19
|
+
EXPIT = "expit"
|
|
20
|
+
""":obj:`scipy.special.expit`"""
|
|
21
|
+
|
|
22
|
+
LOGIT = "logit"
|
|
23
|
+
""":obj:`scipy.special.logit`"""
|
|
24
|
+
|
|
25
|
+
LOGEXPIT = "log_expit"
|
|
26
|
+
""":obj:`scipy.special.log_expit`"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
ACTIVATIONS = {
|
|
30
|
+
ActivationFunction.NONE: lambda x: x,
|
|
31
|
+
ActivationFunction.SIGMOID: scipy.special.expit,
|
|
32
|
+
ActivationFunction.EXPIT: scipy.special.expit,
|
|
33
|
+
ActivationFunction.LOGIT: scipy.special.logit,
|
|
34
|
+
ActivationFunction.LOGEXPIT: scipy.special.log_expit,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ActivationSettings(ez.Settings):
|
|
39
|
+
function: str | ActivationFunction = ActivationFunction.NONE
|
|
40
|
+
"""An enum value from ActivationFunction or a string representing the activation function.
|
|
41
|
+
Possible values are: SIGMOID, EXPIT, LOGIT, LOGEXPIT, "sigmoid", "expit", "logit", "log_expit".
|
|
42
|
+
SIGMOID and EXPIT are equivalent. See :obj:`scipy.special.expit` for more details."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ActivationTransformer(BaseTransformer[ActivationSettings, AxisArray, AxisArray]):
|
|
46
|
+
def _process(self, message: AxisArray) -> AxisArray:
|
|
47
|
+
if type(self.settings.function) is ActivationFunction:
|
|
48
|
+
func = ACTIVATIONS[self.settings.function]
|
|
49
|
+
else:
|
|
50
|
+
# str type handling
|
|
51
|
+
function = self.settings.function.lower()
|
|
52
|
+
if function not in ActivationFunction.options():
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"Unrecognized activation function {function}. Must be one of {ACTIVATIONS.keys()}"
|
|
55
|
+
)
|
|
56
|
+
function = list(ACTIVATIONS.keys())[
|
|
57
|
+
ActivationFunction.options().index(function)
|
|
58
|
+
]
|
|
59
|
+
func = ACTIVATIONS[function]
|
|
60
|
+
|
|
61
|
+
return replace(message, data=func(message.data))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Activation(
|
|
65
|
+
BaseTransformerUnit[ActivationSettings, AxisArray, AxisArray, ActivationTransformer]
|
|
66
|
+
):
|
|
67
|
+
SETTINGS = ActivationSettings
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def activation(
|
|
71
|
+
function: str | ActivationFunction,
|
|
72
|
+
) -> ActivationTransformer:
|
|
73
|
+
"""
|
|
74
|
+
Transform the data with a simple activation function.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
function: An enum value from ActivationFunction or a string representing the activation function.
|
|
78
|
+
Possible values are: SIGMOID, EXPIT, LOGIT, LOGEXPIT, "sigmoid", "expit", "logit", "log_expit".
|
|
79
|
+
SIGMOID and EXPIT are equivalent. See :obj:`scipy.special.expit` for more details.
|
|
80
|
+
|
|
81
|
+
Returns: :obj:`ActivationTransformer`
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
return ActivationTransformer(ActivationSettings(function=function))
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import numpy.typing as npt
|
|
3
|
+
import scipy.signal
|
|
4
|
+
import ezmsg.core as ez
|
|
5
|
+
from ezmsg.util.messages.axisarray import AxisArray, CoordinateAxis
|
|
6
|
+
from ezmsg.util.messages.util import replace
|
|
7
|
+
|
|
8
|
+
from .base import processor_state, BaseStatefulTransformer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AdaptiveLatticeNotchFilterSettings(ez.Settings):
|
|
12
|
+
"""Settings for the Adaptive Lattice Notch Filter."""
|
|
13
|
+
|
|
14
|
+
gamma: float = 0.995
|
|
15
|
+
"""Pole-zero contraction factor"""
|
|
16
|
+
mu: float = 0.99
|
|
17
|
+
"""Smoothing factor"""
|
|
18
|
+
eta: float = 0.99
|
|
19
|
+
"""Forgetting factor"""
|
|
20
|
+
axis: str = "time"
|
|
21
|
+
"""Axis to apply filter to"""
|
|
22
|
+
init_notch_freq: float | None = None
|
|
23
|
+
"""Initial notch frequency. Should be < nyquist."""
|
|
24
|
+
chunkwise: bool = False
|
|
25
|
+
"""Speed up processing by updating the target freq once per chunk only."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@processor_state
|
|
29
|
+
class AdaptiveLatticeNotchFilterState:
|
|
30
|
+
"""State for the Adaptive Lattice Notch Filter."""
|
|
31
|
+
|
|
32
|
+
s_history: npt.NDArray | None = None
|
|
33
|
+
"""Historical `s` values for the adaptive filter."""
|
|
34
|
+
|
|
35
|
+
p: npt.NDArray | None = None
|
|
36
|
+
"""Accumulated product for reflection coefficient update"""
|
|
37
|
+
|
|
38
|
+
q: npt.NDArray | None = None
|
|
39
|
+
"""Accumulated product for reflection coefficient update"""
|
|
40
|
+
|
|
41
|
+
k1: npt.NDArray | None = None
|
|
42
|
+
"""Reflection coefficient"""
|
|
43
|
+
|
|
44
|
+
freq_template: CoordinateAxis | None = None
|
|
45
|
+
"""Template for the frequency axis on the output"""
|
|
46
|
+
|
|
47
|
+
zi: npt.NDArray | None = None
|
|
48
|
+
"""Initial conditions for the filter, updated after every chunk"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AdaptiveLatticeNotchFilterTransformer(
|
|
52
|
+
BaseStatefulTransformer[
|
|
53
|
+
AdaptiveLatticeNotchFilterSettings,
|
|
54
|
+
AxisArray,
|
|
55
|
+
AxisArray,
|
|
56
|
+
AdaptiveLatticeNotchFilterState,
|
|
57
|
+
]
|
|
58
|
+
):
|
|
59
|
+
"""
|
|
60
|
+
Adaptive Lattice Notch Filter implementation as a stateful transformer.
|
|
61
|
+
|
|
62
|
+
https://biomedical-engineering-online.biomedcentral.com/articles/10.1186/1475-925X-13-170
|
|
63
|
+
|
|
64
|
+
The filter automatically tracks and removes frequency components from the input signal.
|
|
65
|
+
It outputs the estimated frequency (in Hz) and the filtered sample.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def _hash_message(self, message: AxisArray) -> int:
|
|
69
|
+
ax_idx = message.get_axis_idx(self.settings.axis)
|
|
70
|
+
sample_shape = message.data.shape[:ax_idx] + message.data.shape[ax_idx + 1 :]
|
|
71
|
+
return hash((message.key, message.axes[self.settings.axis].gain, sample_shape))
|
|
72
|
+
|
|
73
|
+
def _reset_state(self, message: AxisArray) -> None:
|
|
74
|
+
ax_idx = message.get_axis_idx(self.settings.axis)
|
|
75
|
+
sample_shape = message.data.shape[:ax_idx] + message.data.shape[ax_idx + 1 :]
|
|
76
|
+
|
|
77
|
+
fs = 1 / message.axes[self.settings.axis].gain
|
|
78
|
+
init_f = (
|
|
79
|
+
self.settings.init_notch_freq
|
|
80
|
+
if self.settings.init_notch_freq is not None
|
|
81
|
+
else 0.07178314656435313 * fs
|
|
82
|
+
)
|
|
83
|
+
init_omega = init_f * (2 * np.pi) / fs
|
|
84
|
+
init_k1 = -np.cos(init_omega)
|
|
85
|
+
|
|
86
|
+
"""Reset filter state to initial values."""
|
|
87
|
+
self._state = AdaptiveLatticeNotchFilterState()
|
|
88
|
+
self._state.s_history = np.zeros((2,) + sample_shape, dtype=float)
|
|
89
|
+
self._state.p = np.zeros(sample_shape, dtype=float)
|
|
90
|
+
self._state.q = np.zeros(sample_shape, dtype=float)
|
|
91
|
+
self._state.k1 = init_k1 + np.zeros(sample_shape, dtype=float)
|
|
92
|
+
self._state.freq_template = CoordinateAxis(
|
|
93
|
+
data=np.zeros((0,) + sample_shape, dtype=float),
|
|
94
|
+
dims=[self.settings.axis]
|
|
95
|
+
+ message.dims[:ax_idx]
|
|
96
|
+
+ message.dims[ax_idx + 1 :],
|
|
97
|
+
unit="Hz",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Initialize the initial conditions for the filter
|
|
101
|
+
self._state.zi = np.zeros((2, np.prod(sample_shape)), dtype=float)
|
|
102
|
+
# Note: we could calculate it properly, but as long as we are initializing s_history with zeros,
|
|
103
|
+
# it will always be zero.
|
|
104
|
+
# a = [1, init_k1 * (1 + self.settings.gamma), self.settings.gamma]
|
|
105
|
+
# b = [1]
|
|
106
|
+
# s = np.reshape(self._state.s_history, (2, -1))
|
|
107
|
+
# for feat_ix in range(np.prod(sample_shape)):
|
|
108
|
+
# self._state.zi[:, feat_ix] = scipy.signal.lfiltic(b, a, s[::-1, feat_ix], x=None)
|
|
109
|
+
|
|
110
|
+
def _process(self, message: AxisArray) -> AxisArray:
|
|
111
|
+
x_data = message.data
|
|
112
|
+
ax_idx = message.get_axis_idx(self.settings.axis)
|
|
113
|
+
|
|
114
|
+
# TODO: Time should be moved to -1th axis, not the 0th axis
|
|
115
|
+
if message.dims[0] != self.settings.axis:
|
|
116
|
+
x_data = np.moveaxis(x_data, ax_idx, 0)
|
|
117
|
+
|
|
118
|
+
# Access settings once
|
|
119
|
+
gamma = self.settings.gamma
|
|
120
|
+
eta = self.settings.eta
|
|
121
|
+
mu = self.settings.mu
|
|
122
|
+
fs = 1 / message.axes[self.settings.axis].gain
|
|
123
|
+
|
|
124
|
+
# Pre-compute constants
|
|
125
|
+
one_minus_eta = 1 - eta
|
|
126
|
+
one_minus_mu = 1 - mu
|
|
127
|
+
gamma_plus_1 = 1 + gamma
|
|
128
|
+
omega_scale = fs / (2 * np.pi)
|
|
129
|
+
|
|
130
|
+
# For the lattice filter with constant k1:
|
|
131
|
+
# s_n = x_n - k1*(1+gamma)*s_n_1 - gamma*s_n_2
|
|
132
|
+
# This is equivalent to an IIR filter with b=1, a=[1, k1*(1+gamma), gamma]
|
|
133
|
+
|
|
134
|
+
# For the output filter:
|
|
135
|
+
# y_n = s_n + 2*k1*s_n_1 + s_n_2
|
|
136
|
+
# We can treat this as a direct-form FIR filter applied to s_out
|
|
137
|
+
|
|
138
|
+
if self.settings.chunkwise:
|
|
139
|
+
# Process each chunk using current filter parameters
|
|
140
|
+
# Reshape input and prepare output arrays
|
|
141
|
+
_s = self._state.s_history.reshape((2, -1))
|
|
142
|
+
_x = x_data.reshape((x_data.shape[0], -1))
|
|
143
|
+
s_n = np.zeros_like(_x)
|
|
144
|
+
y_out = np.zeros_like(_x)
|
|
145
|
+
|
|
146
|
+
# Apply static filter for each feature dimension
|
|
147
|
+
for ix, k in enumerate(self._state.k1.flatten()):
|
|
148
|
+
# Filter to get s_n (notch filter state)
|
|
149
|
+
a_s = [1, k * gamma_plus_1, gamma]
|
|
150
|
+
s_n[:, ix], self._state.zi[:, ix] = scipy.signal.lfilter(
|
|
151
|
+
[1], a_s, _x[:, ix], zi=self._state.zi[:, ix]
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Apply output filter to get y_out
|
|
155
|
+
b_y = [1, 2 * k, 1]
|
|
156
|
+
y_out[:, ix] = scipy.signal.lfilter(b_y, [1], s_n[:, ix])
|
|
157
|
+
|
|
158
|
+
# Update filter parameters using final values from the chunk
|
|
159
|
+
s_n_reshaped = s_n.reshape((s_n.shape[0],) + x_data.shape[1:])
|
|
160
|
+
s_final = s_n_reshaped[-1] # Current s_n
|
|
161
|
+
s_final_1 = s_n_reshaped[-2] # s_n_1
|
|
162
|
+
s_final_2 = (
|
|
163
|
+
s_n_reshaped[-3] if len(s_n_reshaped) > 2 else self._state.s_history[0]
|
|
164
|
+
) # s_n_2
|
|
165
|
+
|
|
166
|
+
# Update p and q using final values
|
|
167
|
+
self._state.p = eta * self._state.p + one_minus_eta * (
|
|
168
|
+
s_final_1 * (s_final + s_final_2)
|
|
169
|
+
)
|
|
170
|
+
self._state.q = eta * self._state.q + one_minus_eta * (
|
|
171
|
+
2 * (s_final_1 * s_final_1)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Update reflection coefficient
|
|
175
|
+
new_k1 = -self._state.p / (self._state.q + 1e-8) # Avoid division by zero
|
|
176
|
+
new_k1 = np.clip(new_k1, -1, 1) # Clip to prevent instability
|
|
177
|
+
self._state.k1 = mu * self._state.k1 + one_minus_mu * new_k1 # Smoothed
|
|
178
|
+
|
|
179
|
+
# Calculate frequency from updated k1 value
|
|
180
|
+
omega_n = np.arccos(-self._state.k1)
|
|
181
|
+
freq = omega_n * omega_scale
|
|
182
|
+
freq_out = np.full_like(x_data.reshape(x_data.shape), freq)
|
|
183
|
+
|
|
184
|
+
# Update s_history for next chunk
|
|
185
|
+
self._state.s_history = s_n_reshaped[-2:].reshape((2,) + x_data.shape[1:])
|
|
186
|
+
|
|
187
|
+
# Reshape y_out back to original dimensions
|
|
188
|
+
y_out = y_out.reshape(x_data.shape)
|
|
189
|
+
|
|
190
|
+
else:
|
|
191
|
+
# Perform filtering, sample-by-sample
|
|
192
|
+
y_out = np.zeros_like(x_data)
|
|
193
|
+
freq_out = np.zeros_like(x_data)
|
|
194
|
+
for sample_ix, x_n in enumerate(x_data):
|
|
195
|
+
s_n_1 = self._state.s_history[-1]
|
|
196
|
+
s_n_2 = self._state.s_history[-2]
|
|
197
|
+
|
|
198
|
+
s_n = x_n - self._state.k1 * gamma_plus_1 * s_n_1 - gamma * s_n_2
|
|
199
|
+
y_out[sample_ix] = s_n + 2 * self._state.k1 * s_n_1 + s_n_2
|
|
200
|
+
|
|
201
|
+
# Update filter parameters
|
|
202
|
+
self._state.p = eta * self._state.p + one_minus_eta * (
|
|
203
|
+
s_n_1 * (s_n + s_n_2)
|
|
204
|
+
)
|
|
205
|
+
self._state.q = eta * self._state.q + one_minus_eta * (
|
|
206
|
+
2 * (s_n_1 * s_n_1)
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Update reflection coefficient
|
|
210
|
+
new_k1 = -self._state.p / (
|
|
211
|
+
self._state.q + 1e-8
|
|
212
|
+
) # Avoid division by zero
|
|
213
|
+
new_k1 = np.clip(new_k1, -1, 1) # Clip to prevent instability
|
|
214
|
+
self._state.k1 = mu * self._state.k1 + one_minus_mu * new_k1 # Smoothed
|
|
215
|
+
|
|
216
|
+
# Compute normalized angular frequency using equation 13 from the paper
|
|
217
|
+
omega_n = np.arccos(-self._state.k1)
|
|
218
|
+
freq_out[sample_ix] = omega_n * omega_scale # As Hz
|
|
219
|
+
|
|
220
|
+
# Update for next iteration
|
|
221
|
+
self._state.s_history[-2] = s_n_1
|
|
222
|
+
self._state.s_history[-1] = s_n
|
|
223
|
+
|
|
224
|
+
return replace(
|
|
225
|
+
message,
|
|
226
|
+
data=y_out,
|
|
227
|
+
axes={
|
|
228
|
+
**message.axes,
|
|
229
|
+
"freq": replace(self._state.freq_template, data=freq_out),
|
|
230
|
+
},
|
|
231
|
+
)
|