ezmsg-sigproc 2.1.0__tar.gz → 2.2.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.
Files changed (106) hide show
  1. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/.github/workflows/python-publish-ezmsg-sigproc.yml +2 -2
  2. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/.github/workflows/python-tests.yml +4 -10
  3. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/.gitignore +2 -1
  4. ezmsg_sigproc-2.2.0/PKG-INFO +72 -0
  5. ezmsg_sigproc-2.2.0/README.md +55 -0
  6. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/pyproject.toml +11 -5
  7. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/__version__.py +2 -2
  8. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/filter.py +21 -20
  9. ezmsg_sigproc-2.2.0/src/ezmsg/sigproc/gaussiansmoothing.py +93 -0
  10. ezmsg_sigproc-2.2.0/src/ezmsg/sigproc/util/sparse.py +123 -0
  11. ezmsg_sigproc-2.2.0/tests/unit/test_filter.py +17 -0
  12. ezmsg_sigproc-2.2.0/tests/unit/test_gaussian_smoothing_filter.py +255 -0
  13. ezmsg_sigproc-2.1.0/PKG-INFO +0 -62
  14. ezmsg_sigproc-2.1.0/README.md +0 -39
  15. ezmsg_sigproc-2.1.0/src/ezmsg/sigproc/util/sparse.py +0 -29
  16. ezmsg_sigproc-2.1.0/uv.lock +0 -729
  17. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/.pre-commit-config.yaml +0 -0
  18. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/LICENSE.txt +0 -0
  19. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/docs/ProcessorsBase.md +0 -0
  20. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/__init__.py +0 -0
  21. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/activation.py +0 -0
  22. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/adaptive_lattice_notch.py +0 -0
  23. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/affinetransform.py +0 -0
  24. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/aggregate.py +0 -0
  25. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/bandpower.py +0 -0
  26. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/base.py +0 -0
  27. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/butterworthfilter.py +0 -0
  28. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/cheby.py +0 -0
  29. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/combfilter.py +0 -0
  30. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/decimate.py +0 -0
  31. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/detrend.py +0 -0
  32. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/diff.py +0 -0
  33. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/downsample.py +0 -0
  34. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/ewma.py +0 -0
  35. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/ewmfilter.py +0 -0
  36. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/extract_axis.py +0 -0
  37. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/filterbank.py +0 -0
  38. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/math/__init__.py +0 -0
  39. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/math/abs.py +0 -0
  40. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/math/clip.py +0 -0
  41. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/math/difference.py +0 -0
  42. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/math/invert.py +0 -0
  43. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/math/log.py +0 -0
  44. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/math/scale.py +0 -0
  45. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/messages.py +0 -0
  46. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/quantize.py +0 -0
  47. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/resample.py +0 -0
  48. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/sampler.py +0 -0
  49. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/scaler.py +0 -0
  50. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/signalinjector.py +0 -0
  51. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/slicer.py +0 -0
  52. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/spectral.py +0 -0
  53. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/spectrogram.py +0 -0
  54. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/spectrum.py +0 -0
  55. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/synth.py +0 -0
  56. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/transpose.py +0 -0
  57. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/util/__init__.py +0 -0
  58. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/util/asio.py +0 -0
  59. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/util/message.py +0 -0
  60. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/util/profile.py +0 -0
  61. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/util/typeresolution.py +0 -0
  62. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/wavelets.py +0 -0
  63. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/src/ezmsg/sigproc/window.py +0 -0
  64. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/__init__.py +0 -0
  65. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/conftest.py +0 -0
  66. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/helpers/__init__.py +0 -0
  67. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/helpers/util.py +0 -0
  68. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/integration/bytewax/test_spectrum_bytewax.py +0 -0
  69. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/integration/bytewax/test_window_bytewax.py +0 -0
  70. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/integration/ezmsg/test_butterworth_system.py +0 -0
  71. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/integration/ezmsg/test_decimate_system.py +0 -0
  72. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/integration/ezmsg/test_downsample_system.py +0 -0
  73. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/integration/ezmsg/test_filter_system.py +0 -0
  74. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/integration/ezmsg/test_sampler_system.py +0 -0
  75. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/integration/ezmsg/test_scaler_system.py +0 -0
  76. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/integration/ezmsg/test_spectrum_system.py +0 -0
  77. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/integration/ezmsg/test_synth_system.py +0 -0
  78. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/integration/ezmsg/test_window_system.py +0 -0
  79. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/resources/xform.csv +0 -0
  80. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/test_profile.py +0 -0
  81. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_activation.py +0 -0
  82. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_adaptive_lattice_notch.py +0 -0
  83. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_affine_transform.py +0 -0
  84. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_aggregate.py +0 -0
  85. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_bandpower.py +0 -0
  86. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_base.py +0 -0
  87. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_butter.py +0 -0
  88. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_combfilter.py +0 -0
  89. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_diff.py +0 -0
  90. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_downsample.py +0 -0
  91. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_ewma.py +0 -0
  92. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_extract_axis.py +0 -0
  93. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_filterbank.py +0 -0
  94. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_math.py +0 -0
  95. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_quantize.py +0 -0
  96. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_resample.py +0 -0
  97. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_sampler.py +0 -0
  98. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_scaler.py +0 -0
  99. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_slicer.py +0 -0
  100. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_spectrogram.py +0 -0
  101. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_spectrum.py +0 -0
  102. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_synth.py +0 -0
  103. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_transpose.py +0 -0
  104. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_util.py +0 -0
  105. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_wavelets.py +0 -0
  106. {ezmsg_sigproc-2.1.0 → ezmsg_sigproc-2.2.0}/tests/unit/test_window.py +0 -0
@@ -17,8 +17,8 @@ jobs:
17
17
  steps:
18
18
  - uses: actions/checkout@v4
19
19
 
20
- - name: Install uv
21
- uses: astral-sh/setup-uv@v2
20
+ - name: Install the latest version of uv
21
+ uses: astral-sh/setup-uv@v6
22
22
 
23
23
  - name: Build Package
24
24
  run: uv build
@@ -13,7 +13,7 @@ jobs:
13
13
  build:
14
14
  strategy:
15
15
  matrix:
16
- python-version: ["3.10", "3.11", "3.12"]
16
+ python-version: ["3.10.15", "3.11", "3.12", "3.13"]
17
17
  os:
18
18
  - "ubuntu-latest"
19
19
  - "windows-latest"
@@ -23,17 +23,11 @@ jobs:
23
23
  steps:
24
24
  - uses: actions/checkout@v4
25
25
 
26
- - name: Install uv
27
- uses: astral-sh/setup-uv@v2
28
- with:
29
- enable-cache: true
30
- cache-dependency-glob: "uv.lock"
31
-
32
- - name: Set up Python ${{ matrix.python-version }}
33
- run: uv python install ${{ matrix.python-version }}
26
+ - name: Install the latest version of uv
27
+ uses: astral-sh/setup-uv@v6
34
28
 
35
29
  - name: Install the project
36
- run: uv sync --all-extras --dev
30
+ run: uv sync --python ${{ matrix.python-version }}
37
31
 
38
32
  - name: Lint
39
33
  run:
@@ -142,4 +142,5 @@ cython_debug/
142
142
  # JetBrains
143
143
  .idea/
144
144
 
145
- src/ezmsg/sigproc/__version__.py
145
+ src/ezmsg/sigproc/__version__.py
146
+ uv.lock
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: ezmsg-sigproc
3
+ Version: 2.2.0
4
+ Summary: Timeseries signal processing implementations in ezmsg
5
+ Author-email: Griffin Milsap <griffin.milsap@gmail.com>, Preston Peranich <pperanich@gmail.com>, Chadwick Boulay <chadwick.boulay@gmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE.txt
8
+ Requires-Python: >=3.10.15
9
+ Requires-Dist: array-api-compat>=1.11.1
10
+ Requires-Dist: ezmsg>=3.6.0
11
+ Requires-Dist: numba>=0.61.0
12
+ Requires-Dist: numpy>=1.26.0
13
+ Requires-Dist: pywavelets>=1.6.0
14
+ Requires-Dist: scipy>=1.13.1
15
+ Requires-Dist: sparse>=0.15.4
16
+ Description-Content-Type: text/markdown
17
+
18
+ # ezmsg.sigproc
19
+
20
+ ## Overview
21
+
22
+ ezmsg-sigproc offers timeseries signal‑processing primitives built atop the ezmsg message‑passing framework. Core dependencies include ezmsg, numpy, scipy, pywavelets, and sparse; the project itself is managed through hatchling and uses VCS hooks to populate __version__.py.
23
+
24
+ ## Installation
25
+
26
+ Install the latest release from pypi with: `pip install ezmsg-sigproc` (or `uv add ...` or `poetry add ...`).
27
+
28
+ You can install pre-release versions directly from GitHub:
29
+
30
+ * Using `pip`: `pip install git+https://github.com/ezmsg-org/ezmsg-sigproc.git@dev`
31
+ * Using `uv`: `uv add git+https://github.com/ezmsg-org/ezmsg-sigproc --branch dev`
32
+ * Using `poetry`: `poetry add "git+https://github.com/ezmsg-org/ezmsg-sigproc.git@dev"`
33
+
34
+ > See the [Development](#development) section below for installing with the intention of developing.
35
+
36
+ ## Source layout & key modules
37
+ * All source resides under src/ezmsg/sigproc, which contains a suite of processors (for example, filter.py, spectrogram.py, spectrum.py, sampler.py) and math and util subpackages.
38
+ * The framework’s backbone is base.py, defining standard protocols—Processor, Producer, Consumer, and Transformer—that enable both stateless and stateful processing chains.
39
+ * Filtering is implemented in filter.py, providing settings dataclasses and a stateful transformer that applies supplied coefficients to incoming data.
40
+ * Spectral analysis uses a composite spectrogram transformer chaining windowing, spectrum computation, and axis adjustments.
41
+
42
+ ## Operating styles: Standalone processors vs. ezmsg pipelines
43
+ While each processor is designed to be assembled into an ezmsg pipeline, the components are also well‑suited for offline, ad‑hoc analysis. You can instantiate processors directly in scripts or notebooks for quick prototyping or to validate results from other code. The companion Unit wrappers, however, are meant for assembling processors into a full ezmsg pipeline.
44
+
45
+ A fully defined ezmsg pipeline shines in online streaming scenarios where message routing, scheduling, and latency handling are crucial. Nevertheless, you can run the same pipeline offline—say, within a Jupyter notebook—if your analysis benefits from ezmsg’s structured execution model. Deciding between a standalone processor and a full pipeline comes down to the trade‑off between simplicity and the operational overhead of the pipeline:
46
+
47
+ * Standalone processors: Low overhead, ideal for one‑off or exploratory offline tasks.
48
+ * Pipeline + Unit wrappers: Additional setup cost but bring concurrency, standardized interfaces, and automatic message flow—useful when your offline experiment mirrors a live system or when you require fine‑grained pipeline behavior.
49
+
50
+ ## Documentation & tests
51
+ * `docs/ProcessorsBase.md` details the processor hierarchy and generic type patterns, providing a solid foundation for custom components.
52
+ * Unit tests (e.g., `tests/unit/test_sampler.py`) offer concrete examples of usage, showcasing sampler generation, windowing, and message handling.
53
+
54
+ ## Where to learn next
55
+ * Study docs/ProcessorsBase.md to master the processor architecture.
56
+ * Explore unit tests for hands‑on examples of composing processors and Units.
57
+ * Review the ezmsg framework in pyproject.toml to understand the surrounding ecosystem.
58
+ * Experiment with the code—try running processors standalone and then integrate them into a small pipeline to observe the trade‑offs firsthand.
59
+
60
+ This approach equips newcomers to choose the right level of abstraction—raw processor, Unit wrapper, or full pipeline—based on the demands of their analysis or streaming application.
61
+
62
+ ## Development
63
+
64
+ We use [`uv`](https://docs.astral.sh/uv/getting-started/installation/) for development. It is not strictly required, but if you intend to contribute to ezmsg-sigproc then using `uv` will lead to the smoothest collaboration.
65
+
66
+ 1. Install [`uv`](https://docs.astral.sh/uv/getting-started/installation/) if not already installed.
67
+ 2. Fork ezmsg-sigproc and clone your fork to your local computer.
68
+ 3. Open a terminal and `cd` to the cloned folder.
69
+ 4. `uv sync` to create a .venv and install dependencies.
70
+ 5. `uv run pre-commit install` to install pre-commit hooks to do linting and formatting.
71
+ 6. Run the test suite before finalizing your edits: `uv run pytest tests`
72
+ 7. Make a PR against the `dev` branch of the main repo.
@@ -0,0 +1,55 @@
1
+ # ezmsg.sigproc
2
+
3
+ ## Overview
4
+
5
+ ezmsg-sigproc offers timeseries signal‑processing primitives built atop the ezmsg message‑passing framework. Core dependencies include ezmsg, numpy, scipy, pywavelets, and sparse; the project itself is managed through hatchling and uses VCS hooks to populate __version__.py.
6
+
7
+ ## Installation
8
+
9
+ Install the latest release from pypi with: `pip install ezmsg-sigproc` (or `uv add ...` or `poetry add ...`).
10
+
11
+ You can install pre-release versions directly from GitHub:
12
+
13
+ * Using `pip`: `pip install git+https://github.com/ezmsg-org/ezmsg-sigproc.git@dev`
14
+ * Using `uv`: `uv add git+https://github.com/ezmsg-org/ezmsg-sigproc --branch dev`
15
+ * Using `poetry`: `poetry add "git+https://github.com/ezmsg-org/ezmsg-sigproc.git@dev"`
16
+
17
+ > See the [Development](#development) section below for installing with the intention of developing.
18
+
19
+ ## Source layout & key modules
20
+ * All source resides under src/ezmsg/sigproc, which contains a suite of processors (for example, filter.py, spectrogram.py, spectrum.py, sampler.py) and math and util subpackages.
21
+ * The framework’s backbone is base.py, defining standard protocols—Processor, Producer, Consumer, and Transformer—that enable both stateless and stateful processing chains.
22
+ * Filtering is implemented in filter.py, providing settings dataclasses and a stateful transformer that applies supplied coefficients to incoming data.
23
+ * Spectral analysis uses a composite spectrogram transformer chaining windowing, spectrum computation, and axis adjustments.
24
+
25
+ ## Operating styles: Standalone processors vs. ezmsg pipelines
26
+ While each processor is designed to be assembled into an ezmsg pipeline, the components are also well‑suited for offline, ad‑hoc analysis. You can instantiate processors directly in scripts or notebooks for quick prototyping or to validate results from other code. The companion Unit wrappers, however, are meant for assembling processors into a full ezmsg pipeline.
27
+
28
+ A fully defined ezmsg pipeline shines in online streaming scenarios where message routing, scheduling, and latency handling are crucial. Nevertheless, you can run the same pipeline offline—say, within a Jupyter notebook—if your analysis benefits from ezmsg’s structured execution model. Deciding between a standalone processor and a full pipeline comes down to the trade‑off between simplicity and the operational overhead of the pipeline:
29
+
30
+ * Standalone processors: Low overhead, ideal for one‑off or exploratory offline tasks.
31
+ * Pipeline + Unit wrappers: Additional setup cost but bring concurrency, standardized interfaces, and automatic message flow—useful when your offline experiment mirrors a live system or when you require fine‑grained pipeline behavior.
32
+
33
+ ## Documentation & tests
34
+ * `docs/ProcessorsBase.md` details the processor hierarchy and generic type patterns, providing a solid foundation for custom components.
35
+ * Unit tests (e.g., `tests/unit/test_sampler.py`) offer concrete examples of usage, showcasing sampler generation, windowing, and message handling.
36
+
37
+ ## Where to learn next
38
+ * Study docs/ProcessorsBase.md to master the processor architecture.
39
+ * Explore unit tests for hands‑on examples of composing processors and Units.
40
+ * Review the ezmsg framework in pyproject.toml to understand the surrounding ecosystem.
41
+ * Experiment with the code—try running processors standalone and then integrate them into a small pipeline to observe the trade‑offs firsthand.
42
+
43
+ This approach equips newcomers to choose the right level of abstraction—raw processor, Unit wrapper, or full pipeline—based on the demands of their analysis or streaming application.
44
+
45
+ ## Development
46
+
47
+ We use [`uv`](https://docs.astral.sh/uv/getting-started/installation/) for development. It is not strictly required, but if you intend to contribute to ezmsg-sigproc then using `uv` will lead to the smoothest collaboration.
48
+
49
+ 1. Install [`uv`](https://docs.astral.sh/uv/getting-started/installation/) if not already installed.
50
+ 2. Fork ezmsg-sigproc and clone your fork to your local computer.
51
+ 3. Open a terminal and `cd` to the cloned folder.
52
+ 4. `uv sync` to create a .venv and install dependencies.
53
+ 5. `uv run pre-commit install` to install pre-commit hooks to do linting and formatting.
54
+ 6. Run the test suite before finalizing your edits: `uv run pytest tests`
55
+ 7. Make a PR against the `dev` branch of the main repo.
@@ -20,9 +20,18 @@ dependencies = [
20
20
  "sparse>=0.15.4",
21
21
  ]
22
22
 
23
- [project.optional-dependencies]
23
+ [dependency-groups]
24
+ dev = [
25
+ "typer>=0.12.5",
26
+ "pre-commit>=4.2.0",
27
+ "jupyter>=1.1.1",
28
+ {include-group = "lint"},
29
+ {include-group = "test"},
30
+ ]
31
+ lint = [
32
+ "ruff"
33
+ ]
24
34
  test = [
25
- "flake8>=7.1.1",
26
35
  "frozendict>=2.4.4",
27
36
  "pytest-asyncio>=0.24.0",
28
37
  "pytest-cov>=5.0.0",
@@ -42,9 +51,6 @@ version-file = "src/ezmsg/sigproc/__version__.py"
42
51
  [tool.hatch.build.targets.wheel]
43
52
  packages = ["src/ezmsg"]
44
53
 
45
- [tool.uv]
46
- dev-dependencies = ["pre-commit>=3.8.0", "ruff>=0.6.7"]
47
-
48
54
  [tool.pytest.ini_options]
49
55
  norecursedirs = "tests/helpers"
50
56
  addopts = "-p no:warnings"
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.1.0'
21
- __version_tuple__ = version_tuple = (2, 1, 0)
20
+ __version__ = version = '2.2.0'
21
+ __version_tuple__ = version_tuple = (2, 2, 0)
@@ -32,17 +32,19 @@ FilterCoefsType = typing.TypeVar("FilterCoefsType", BACoeffs, SOSCoeffs)
32
32
 
33
33
 
34
34
  def _normalize_coefs(
35
- coefs: FilterCoefficients | tuple[npt.NDArray, npt.NDArray] | npt.NDArray,
36
- ) -> tuple[str, tuple[npt.NDArray, ...]]:
35
+ coefs: FilterCoefficients | tuple[npt.NDArray, npt.NDArray] | npt.NDArray | None,
36
+ ) -> tuple[str, tuple[npt.NDArray, ...] | None]:
37
37
  coef_type = "ba"
38
38
  if coefs is not None:
39
39
  # scipy.signal functions called with first arg `*coefs`.
40
40
  # Make sure we have a tuple of coefficients.
41
- if isinstance(coefs, npt.NDArray):
41
+ if isinstance(coefs, np.ndarray):
42
42
  coef_type = "sos"
43
43
  coefs = (coefs,) # sos funcs just want a single ndarray.
44
44
  elif isinstance(coefs, FilterCoefficients):
45
- coefs = (FilterCoefficients.b, FilterCoefficients.a)
45
+ coefs = (coefs.b, coefs.a)
46
+ elif not isinstance(coefs, tuple):
47
+ coefs = (coefs,)
46
48
  return coef_type, coefs
47
49
 
48
50
 
@@ -91,16 +93,20 @@ class FilterTransformer(
91
93
  axis = message.dims[0] if self.settings.axis is None else self.settings.axis
92
94
  axis_idx = message.get_axis_idx(axis)
93
95
  n_tail = message.data.ndim - axis_idx - 1
94
- coefs = (
95
- (self.settings.coefs,)
96
- if self.settings.coefs is not None
97
- and not isinstance(self.settings.coefs, tuple)
98
- else self.settings.coefs
99
- )
100
- zi_func = {"ba": scipy.signal.lfilter_zi, "sos": scipy.signal.sosfilt_zi}[
101
- self.settings.coef_type
102
- ]
103
- zi = zi_func(*coefs)
96
+ _, coefs = _normalize_coefs(self.settings.coefs)
97
+
98
+ if self.settings.coef_type == "ba":
99
+ b, a = coefs
100
+ if len(a) == 1 or np.allclose(a[1:], 0):
101
+ # For FIR filters, use lfiltic with zero initial conditions
102
+ zi = scipy.signal.lfiltic(b, a, [])
103
+ else:
104
+ # For IIR filters...
105
+ zi = scipy.signal.lfilter_zi(b, a)
106
+ else:
107
+ # For second-order sections (SOS) filters, use sosfilt_zi
108
+ zi = scipy.signal.sosfilt_zi(*coefs)
109
+
104
110
  zi_expand = (None,) * axis_idx + (slice(None),) + (None,) * n_tail
105
111
  n_tile = (
106
112
  message.data.shape[:axis_idx] + (1,) + message.data.shape[axis_idx + 1 :]
@@ -166,12 +172,7 @@ class FilterTransformer(
166
172
  if message.data.size > 0:
167
173
  axis = message.dims[0] if self.settings.axis is None else self.settings.axis
168
174
  axis_idx = message.get_axis_idx(axis)
169
- coefs = (
170
- (self.settings.coefs,)
171
- if self.settings.coefs is not None
172
- and not isinstance(self.settings.coefs, tuple)
173
- else self.settings.coefs
174
- )
175
+ _, coefs = _normalize_coefs(self.settings.coefs)
175
176
  filt_func = {"ba": scipy.signal.lfilter, "sos": scipy.signal.sosfilt}[
176
177
  self.settings.coef_type
177
178
  ]
@@ -0,0 +1,93 @@
1
+ from typing import Callable
2
+ import warnings
3
+
4
+ import numpy as np
5
+
6
+ from .filter import (
7
+ FilterBaseSettings,
8
+ BACoeffs,
9
+ FilterByDesignTransformer,
10
+ BaseFilterByDesignTransformerUnit,
11
+ )
12
+
13
+
14
+ class GaussianSmoothingSettings(FilterBaseSettings):
15
+ sigma: float | None = 1.0
16
+ """
17
+ sigma : float
18
+ Standard deviation of the Gaussian kernel.
19
+ """
20
+
21
+ width: int | None = 4
22
+ """
23
+ width : int
24
+ Number of standard deviations covered by the kernel window if kernel_size is not provided.
25
+ """
26
+
27
+ kernel_size: int | None = None
28
+ """
29
+ kernel_size : int | None
30
+ Length of the kernel in samples. If provided, overrides automatic calculation.
31
+ """
32
+
33
+
34
+ def gaussian_smoothing_filter_design(
35
+ sigma: float = 1.0,
36
+ width: int = 4,
37
+ kernel_size: int | None = None,
38
+ ) -> BACoeffs | None:
39
+ # Parameter checks
40
+ if sigma <= 0:
41
+ raise ValueError(f"sigma must be positive. Received: {sigma}")
42
+
43
+ if width <= 0:
44
+ raise ValueError(f"width must be positive. Received: {width}")
45
+
46
+ if kernel_size is not None:
47
+ if kernel_size < 1:
48
+ raise ValueError(f"kernel_size must be >= 1. Received: {kernel_size}")
49
+ else:
50
+ kernel_size = int(2 * width * sigma + 1)
51
+
52
+ # Warn if kernel_size is smaller than recommended but don't fail
53
+ expected_kernel_size = int(2 * width * sigma + 1)
54
+ if kernel_size < expected_kernel_size:
55
+ ## TODO: Either add a warning or determine appropriate kernel size and raise an error
56
+ warnings.warn(
57
+ f"Provided kernel_size {kernel_size} is smaller than recommended "
58
+ f"size {expected_kernel_size} for sigma={sigma} and width={width}. "
59
+ "The kernel may be truncated."
60
+ )
61
+
62
+ from scipy.signal.windows import gaussian
63
+
64
+ b = gaussian(kernel_size, std=sigma)
65
+ b /= np.sum(b) # Ensure normalization
66
+ a = np.array([1.0])
67
+
68
+ return b, a
69
+
70
+
71
+ class GaussianSmoothingFilterTransformer(
72
+ FilterByDesignTransformer[GaussianSmoothingSettings, BACoeffs]
73
+ ):
74
+ def get_design_function(
75
+ self,
76
+ ) -> Callable[[float], BACoeffs]:
77
+ # Create a wrapper function that ignores fs parameter since gaussian smoothing doesn't need it
78
+ def design_wrapper(fs: float) -> BACoeffs:
79
+ return gaussian_smoothing_filter_design(
80
+ sigma=self.settings.sigma,
81
+ width=self.settings.width,
82
+ kernel_size=self.settings.kernel_size,
83
+ )
84
+
85
+ return design_wrapper
86
+
87
+
88
+ class GaussianSmoothingFilter(
89
+ BaseFilterByDesignTransformerUnit[
90
+ GaussianSmoothingSettings, GaussianSmoothingFilterTransformer
91
+ ]
92
+ ):
93
+ SETTINGS = GaussianSmoothingSettings
@@ -0,0 +1,123 @@
1
+ import numpy as np
2
+ import sparse
3
+
4
+
5
+ def sliding_win_oneaxis_old(
6
+ s: sparse.SparseArray, nwin: int, axis: int, step: int = 1
7
+ ) -> sparse.SparseArray:
8
+ """
9
+ Like `ezmsg.util.messages.axisarray.sliding_win_oneaxis` but for sparse arrays.
10
+ This approach is about 4x slower than the version that uses coordinate arithmetic below.
11
+
12
+ Args:
13
+ s: The input sparse array.
14
+ nwin: The size of the sliding window.
15
+ axis: The axis along which the sliding window will be applied.
16
+ step: The size of the step between windows. If > 1, the strided window will be sliced with `slice_along_axis`.
17
+
18
+ Returns:
19
+
20
+ """
21
+ if -s.ndim <= axis < 0:
22
+ axis = s.ndim + axis
23
+ targ_slices = [slice(_, _ + nwin) for _ in range(0, s.shape[axis] - nwin + 1, step)]
24
+ s = s.reshape(s.shape[:axis] + (1,) + s.shape[axis:])
25
+ full_slices = (slice(None),) * s.ndim
26
+ full_slices = [
27
+ full_slices[: axis + 1] + (sl,) + full_slices[axis + 2 :] for sl in targ_slices
28
+ ]
29
+ result = sparse.concatenate([s[_] for _ in full_slices], axis=axis)
30
+ return result
31
+
32
+
33
+ def sliding_win_oneaxis(
34
+ s: sparse.SparseArray, nwin: int, axis: int, step: int = 1
35
+ ) -> sparse.SparseArray:
36
+ """
37
+ Generates a view-like sparse array using a sliding window of specified length along a specified axis.
38
+ Sparse analog of an optimized dense as_strided-based implementation with these properties:
39
+
40
+ - Accepts a single `nwin` and a single `axis`.
41
+ - Inserts a new 'win' axis immediately BEFORE the original target axis.
42
+ Output shape:
43
+ s.shape[:axis] + (W,) + (nwin,) + s.shape[axis+1:]
44
+ where W = s.shape[axis] - (nwin - 1).
45
+ - If `step > 1`, stepping is applied by slicing along the new windows axis (same observable behavior
46
+ as doing `slice_along_axis(result, slice(None, None, step), axis)` in the dense version).
47
+
48
+ Args:
49
+ s: Input sparse array (pydata/sparse COO-compatible).
50
+ nwin: Sliding window size (must be > 0).
51
+ axis: Axis of `s` along which the window slides (supports negative indexing).
52
+ step: Stride between windows. If > 1, applied by slicing the windows axis after construction.
53
+
54
+ Returns:
55
+ A sparse array with a new windows axis inserted before the original axis.
56
+
57
+ Notes:
58
+ - Mirrors the dense function’s known edge case: when nwin == shape[axis] + 1, W becomes 0 and
59
+ an empty windows axis is returned.
60
+ - Built by coordinate arithmetic; no per-window indexing or concatenation.
61
+ """
62
+ if -s.ndim <= axis < 0:
63
+ axis = s.ndim + axis
64
+ if not (0 <= axis < s.ndim):
65
+ raise ValueError(f"Invalid axis {axis} for array with {s.ndim} dimensions")
66
+ if nwin <= 0:
67
+ raise ValueError("nwin must be > 0")
68
+ dim = s.shape[axis]
69
+
70
+ last_win_start = dim - nwin
71
+ win_starts = list(range(0, last_win_start + 1, step))
72
+ n_win_out = len(win_starts)
73
+ if n_win_out <= 0:
74
+ # Return array with proper shape except empty along windows axis
75
+ return sparse.zeros(
76
+ s.shape[:axis] + (0,) + (nwin,) + s.shape[axis + 1 :], dtype=s.dtype
77
+ )
78
+
79
+ coo = s.asformat("coo")
80
+ coords = coo.coords # shape: (ndim, nnz)
81
+ data = coo.data # shape: (nnz,)
82
+ ia = coords[axis] # indices along sliding axis, shape: (nnz,)
83
+
84
+ # We emit contributions for each offset o in [0, nwin-1].
85
+ # For a nonzero at index i, it contributes to window start w = i - o when 0 <= w < W.
86
+ out_coords_blocks = []
87
+ out_data_blocks = []
88
+
89
+ # Small speed/memory tweak: reuse dtypes and pre-allocate o-array once per loop.
90
+ idx_dtype = coords.dtype
91
+
92
+ for win_ix, win_start in enumerate(win_starts):
93
+ w = ia - win_start
94
+ # Valid window starts are those within [0, nwin]
95
+ mask = (w >= 0) & (w < nwin)
96
+ if not mask.any():
97
+ continue
98
+
99
+ sel = np.nonzero(mask)[0]
100
+ w_sel = w[sel]
101
+
102
+ # Build new coords with windows axis inserted at `axis` and the original axis
103
+ # becoming the next axis with fixed offset value `o`.
104
+ # Output ndim = s.ndim + 1
105
+ before = coords[:axis, sel] # unchanged
106
+ after_other = coords[axis + 1 :, sel] # dims after original axis
107
+ win_idx_row = np.full((1, sel.size), win_ix, dtype=idx_dtype)
108
+
109
+ new_coords = np.vstack([before, win_idx_row, w_sel[None, :], after_other])
110
+
111
+ out_coords_blocks.append(new_coords)
112
+ out_data_blocks.append(data[sel])
113
+
114
+ if not out_coords_blocks:
115
+ return sparse.zeros(
116
+ s.shape[:axis] + (n_win_out,) + (nwin,) + s.shape[axis + 1 :], dtype=s.dtype
117
+ )
118
+
119
+ out_coords = np.hstack(out_coords_blocks)
120
+ out_data = np.hstack(out_data_blocks)
121
+ out_shape = s.shape[:axis] + (n_win_out,) + (nwin,) + s.shape[axis + 1 :]
122
+
123
+ return sparse.COO(out_coords, out_data, shape=out_shape)
@@ -0,0 +1,17 @@
1
+ import numpy as np
2
+ from ezmsg.sigproc.filter import FilterTransformer, FilterSettings, FilterCoefficients
3
+ from ezmsg.util.messages.axisarray import AxisArray
4
+
5
+
6
+ def test_filter_transformer_accepts_dataclass_coefficients():
7
+ data = np.arange(10.0)
8
+ msg = AxisArray(
9
+ data=data,
10
+ dims=["time"],
11
+ axes={"time": AxisArray.TimeAxis(fs=1.0, offset=0.0)},
12
+ key="test",
13
+ )
14
+ coefs = FilterCoefficients(b=np.array([1.0]), a=np.array([1.0, 0.0]))
15
+ transformer = FilterTransformer(settings=FilterSettings(axis="time", coefs=coefs))
16
+ out = transformer(msg)
17
+ assert np.allclose(out.data, data)