ezmsg-sigproc 2.7.0__tar.gz → 2.9.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-2.7.0 → ezmsg_sigproc-2.9.0}/PKG-INFO +1 -1
- ezmsg_sigproc-2.9.0/docs/source/guides/explanations/array_api.rst +156 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/explanations/sigproc.rst +1 -1
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/sigproc/content-sigproc.rst +3 -1
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/tutorials/signalprocessing.rst +1 -1
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/__version__.py +2 -2
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/affinetransform.py +23 -1
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/aggregate.py +9 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/butterworthzerophase.py +1 -1
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/coordinatespaces.py +22 -5
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/denormalize.py +3 -4
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/detrend.py +1 -1
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/diff.py +18 -8
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/ewma.py +44 -7
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/extract_axis.py +1 -2
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/filter.py +3 -4
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/fir_hilbert.py +1 -1
- ezmsg_sigproc-2.9.0/src/ezmsg/sigproc/linear.py +120 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/math/abs.py +11 -6
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/math/add.py +1 -2
- ezmsg_sigproc-2.9.0/src/ezmsg/sigproc/math/clip.py +48 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/math/difference.py +9 -3
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/math/invert.py +8 -3
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/math/log.py +19 -8
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/math/scale.py +8 -3
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/rollingscaler.py +4 -4
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/scaler.py +52 -3
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/transpose.py +22 -7
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_add_system.py +2 -2
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_difference_system.py +3 -3
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_sampler_system.py +1 -1
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_activation.py +8 -3
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_affine_transform.py +33 -26
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_aggregate.py +18 -13
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_bandpower.py +16 -9
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_base.py +3 -3
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_butter.py +29 -28
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_butterworthzerophase.py +2 -2
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_coordinatespaces.py +12 -12
- ezmsg_sigproc-2.9.0/tests/unit/test_ewma.py +162 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_filterbank.py +3 -3
- ezmsg_sigproc-2.9.0/tests/unit/test_linear.py +307 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_math.py +26 -26
- ezmsg_sigproc-2.9.0/tests/unit/test_scaler.py +233 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_slicer.py +17 -18
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_spectrogram.py +1 -1
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_spectrum.py +17 -14
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_wavelets.py +10 -8
- ezmsg_sigproc-2.7.0/src/ezmsg/sigproc/math/clip.py +0 -43
- ezmsg_sigproc-2.7.0/tests/unit/test_ewma.py +0 -47
- ezmsg_sigproc-2.7.0/tests/unit/test_scaler.py +0 -70
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/.github/workflows/docs.yml +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/.github/workflows/python-publish.yml +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/.github/workflows/python-tests.yml +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/.gitignore +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/.pre-commit-config.yaml +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/LICENSE +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/README.md +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/Makefile +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/make.bat +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/_templates/autosummary/module.rst +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/api/index.rst +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/conf.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/HybridBuffer.md +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/how-tos/signalprocessing/adaptive.rst +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/how-tos/signalprocessing/checkpoint.rst +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/how-tos/signalprocessing/composite.rst +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/how-tos/signalprocessing/content-signalprocessing.rst +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/how-tos/signalprocessing/processor.rst +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/how-tos/signalprocessing/standalone.rst +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/how-tos/signalprocessing/stateful.rst +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/how-tos/signalprocessing/unit.rst +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/img/HybridBufferBasic.svg +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/img/HybridBufferOverflow.svg +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/sigproc/architecture.rst +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/sigproc/base.rst +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/sigproc/processors.rst +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/sigproc/units.rst +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/index.md +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/pyproject.toml +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/__init__.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/activation.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/adaptive_lattice_notch.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/bandpower.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/base.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/butterworthfilter.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/cheby.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/combfilter.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/decimate.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/downsample.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/ewmfilter.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/fbcca.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/filterbank.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/filterbankdesign.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/fir_pmc.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/firfilter.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/gaussiansmoothing.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/kaiser.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/math/__init__.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/messages.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/quantize.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/resample.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/sampler.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/signalinjector.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/slicer.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/spectral.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/spectrogram.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/spectrum.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/util/__init__.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/util/asio.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/util/axisarray_buffer.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/util/buffer.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/util/message.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/util/profile.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/util/sparse.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/util/typeresolution.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/wavelets.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/window.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/__init__.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/conftest.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/helpers/__init__.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/helpers/synth.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/helpers/util.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/bytewax/test_spectrum_bytewax.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/bytewax/test_window_bytewax.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_butterworth_system.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_butterworthzerophase_system.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_coordinatespaces_system.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_decimate_system.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_downsample_system.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_filter_system.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_fir_hilbert_system.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_fir_pmc_system.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_rollingscaler_system.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_scaler_system.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_spectrum_system.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_window_system.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/resources/xform.csv +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/test_profile.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/buffer/test_axisarray_buffer.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/buffer/test_buffer.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/buffer/test_buffer_overflow.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_adaptive_lattice_notch.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_combfilter.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_denormalize.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_diff.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_downsample.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_extract_axis.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_fbcca.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_filter.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_filterbankdesign.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_fir_hilbert.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_fir_pmc.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_firfilter.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_gaussian_smoothing_filter.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_kaiser.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_math_add.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_math_difference.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_quantize.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_resample.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_rollingscaler.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_sampler.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_transpose.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_util.py +0 -0
- {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_window.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ezmsg-sigproc
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.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>, Kyle McGraw <kmcgraw@blackrockneuro.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Array API Support
|
|
2
|
+
=================
|
|
3
|
+
|
|
4
|
+
ezmsg-sigproc provides support for the `Python Array API standard
|
|
5
|
+
<https://data-apis.org/array-api/>`_, enabling many transformers to work with
|
|
6
|
+
arrays from different backends such as NumPy, CuPy, PyTorch, and JAX.
|
|
7
|
+
|
|
8
|
+
What is the Array API?
|
|
9
|
+
----------------------
|
|
10
|
+
|
|
11
|
+
The Array API is a standardized interface for array operations across different
|
|
12
|
+
Python array libraries. By coding to this standard, ezmsg-sigproc transformers
|
|
13
|
+
can process data regardless of which array library created it, enabling:
|
|
14
|
+
|
|
15
|
+
- **GPU acceleration** via CuPy or PyTorch tensors
|
|
16
|
+
- **Framework interoperability** for integration with ML pipelines
|
|
17
|
+
- **Hardware flexibility** without code changes
|
|
18
|
+
|
|
19
|
+
How It Works
|
|
20
|
+
------------
|
|
21
|
+
|
|
22
|
+
Compatible transformers use `array-api-compat <https://github.com/data-apis/array-api-compat>`_
|
|
23
|
+
to detect the input array's namespace and use the appropriate operations:
|
|
24
|
+
|
|
25
|
+
.. code-block:: python
|
|
26
|
+
|
|
27
|
+
from array_api_compat import get_namespace
|
|
28
|
+
|
|
29
|
+
def _process(self, message: AxisArray) -> AxisArray:
|
|
30
|
+
xp = get_namespace(message.data) # numpy, cupy, torch, etc.
|
|
31
|
+
result = xp.abs(message.data) # Uses the correct backend
|
|
32
|
+
return replace(message, data=result)
|
|
33
|
+
|
|
34
|
+
Usage Example
|
|
35
|
+
-------------
|
|
36
|
+
|
|
37
|
+
Using Array API compatible transformers with CuPy for GPU acceleration:
|
|
38
|
+
|
|
39
|
+
.. code-block:: python
|
|
40
|
+
|
|
41
|
+
import cupy as cp
|
|
42
|
+
from ezmsg.util.messages.axisarray import AxisArray
|
|
43
|
+
from ezmsg.sigproc.math.abs import AbsTransformer
|
|
44
|
+
from ezmsg.sigproc.math.clip import ClipTransformer, ClipSettings
|
|
45
|
+
|
|
46
|
+
# Create data on GPU
|
|
47
|
+
gpu_data = cp.random.randn(1000, 64).astype(cp.float32)
|
|
48
|
+
message = AxisArray(gpu_data, dims=["time", "ch"])
|
|
49
|
+
|
|
50
|
+
# Process entirely on GPU - no data transfer!
|
|
51
|
+
abs_transformer = AbsTransformer()
|
|
52
|
+
clip_transformer = ClipTransformer(ClipSettings(min=0.0, max=1.0))
|
|
53
|
+
|
|
54
|
+
result = clip_transformer(abs_transformer(message))
|
|
55
|
+
# result.data is still a CuPy array on GPU
|
|
56
|
+
|
|
57
|
+
Compatible Modules
|
|
58
|
+
------------------
|
|
59
|
+
|
|
60
|
+
The following transformers fully support the Array API standard:
|
|
61
|
+
|
|
62
|
+
Math Operations
|
|
63
|
+
^^^^^^^^^^^^^^^
|
|
64
|
+
|
|
65
|
+
.. list-table::
|
|
66
|
+
:header-rows: 1
|
|
67
|
+
:widths: 30 70
|
|
68
|
+
|
|
69
|
+
* - Module
|
|
70
|
+
- Description
|
|
71
|
+
* - :mod:`ezmsg.sigproc.math.abs`
|
|
72
|
+
- Absolute value
|
|
73
|
+
* - :mod:`ezmsg.sigproc.math.clip`
|
|
74
|
+
- Clip values to a range
|
|
75
|
+
* - :mod:`ezmsg.sigproc.math.log`
|
|
76
|
+
- Logarithm with configurable base
|
|
77
|
+
* - :mod:`ezmsg.sigproc.math.scale`
|
|
78
|
+
- Multiply by a constant
|
|
79
|
+
* - :mod:`ezmsg.sigproc.math.invert`
|
|
80
|
+
- Compute 1/x
|
|
81
|
+
* - :mod:`ezmsg.sigproc.math.difference`
|
|
82
|
+
- Subtract a constant (ConstDifferenceTransformer)
|
|
83
|
+
|
|
84
|
+
Signal Processing
|
|
85
|
+
^^^^^^^^^^^^^^^^^
|
|
86
|
+
|
|
87
|
+
.. list-table::
|
|
88
|
+
:header-rows: 1
|
|
89
|
+
:widths: 30 70
|
|
90
|
+
|
|
91
|
+
* - Module
|
|
92
|
+
- Description
|
|
93
|
+
* - :mod:`ezmsg.sigproc.diff`
|
|
94
|
+
- Compute differences along an axis
|
|
95
|
+
* - :mod:`ezmsg.sigproc.transpose`
|
|
96
|
+
- Transpose/permute array dimensions
|
|
97
|
+
* - :mod:`ezmsg.sigproc.linear`
|
|
98
|
+
- Per-channel linear transform (scale + offset)
|
|
99
|
+
* - :mod:`ezmsg.sigproc.aggregate`
|
|
100
|
+
- Aggregate operations (AggregateTransformer only)
|
|
101
|
+
|
|
102
|
+
Coordinate Transforms
|
|
103
|
+
^^^^^^^^^^^^^^^^^^^^^
|
|
104
|
+
|
|
105
|
+
.. list-table::
|
|
106
|
+
:header-rows: 1
|
|
107
|
+
:widths: 30 70
|
|
108
|
+
|
|
109
|
+
* - Module
|
|
110
|
+
- Description
|
|
111
|
+
* - :mod:`ezmsg.sigproc.coordinatespaces`
|
|
112
|
+
- Cartesian/polar coordinate conversions
|
|
113
|
+
|
|
114
|
+
Limitations
|
|
115
|
+
-----------
|
|
116
|
+
|
|
117
|
+
Some operations remain NumPy-only due to lack of Array API equivalents:
|
|
118
|
+
|
|
119
|
+
- **Random number generation**: Modules using ``np.random`` (e.g., ``denormalize``)
|
|
120
|
+
- **SciPy operations**: Filtering (``scipy.signal.lfilter``), FFT, wavelets
|
|
121
|
+
- **Advanced indexing**: Some slicing operations for metadata handling
|
|
122
|
+
- **Memory layout**: ``np.require`` for contiguous array optimization (NumPy only)
|
|
123
|
+
|
|
124
|
+
Metadata arrays (axis labels, coordinates) typically remain as NumPy arrays
|
|
125
|
+
since they are not performance-critical.
|
|
126
|
+
|
|
127
|
+
Adding Array API Support
|
|
128
|
+
------------------------
|
|
129
|
+
|
|
130
|
+
When contributing new transformers, follow this pattern:
|
|
131
|
+
|
|
132
|
+
.. code-block:: python
|
|
133
|
+
|
|
134
|
+
from array_api_compat import get_namespace
|
|
135
|
+
from ezmsg.baseproc import BaseTransformer
|
|
136
|
+
from ezmsg.util.messages.axisarray import AxisArray
|
|
137
|
+
from ezmsg.util.messages.util import replace
|
|
138
|
+
|
|
139
|
+
class MyTransformer(BaseTransformer[MySettings, AxisArray, AxisArray]):
|
|
140
|
+
def _process(self, message: AxisArray) -> AxisArray:
|
|
141
|
+
xp = get_namespace(message.data)
|
|
142
|
+
|
|
143
|
+
# Use xp instead of np for array operations
|
|
144
|
+
result = xp.sqrt(xp.abs(message.data))
|
|
145
|
+
|
|
146
|
+
return replace(message, data=result)
|
|
147
|
+
|
|
148
|
+
Key guidelines:
|
|
149
|
+
|
|
150
|
+
1. Call ``get_namespace(message.data)`` at the start of ``_process``
|
|
151
|
+
2. Use ``xp.function_name`` instead of ``np.function_name``
|
|
152
|
+
3. Note that some functions have different names:
|
|
153
|
+
- ``np.concatenate`` → ``xp.concat``
|
|
154
|
+
- ``np.transpose`` → ``xp.permute_dims``
|
|
155
|
+
4. Keep metadata operations (axis labels, etc.) as NumPy
|
|
156
|
+
5. Use in-place operations (``/=``, ``*=``) where possible for efficiency
|
|
@@ -329,7 +329,7 @@ Often, all that is required is the following (e.g., for a custom transformer):
|
|
|
329
329
|
|
|
330
330
|
import ezmsg.core as ez
|
|
331
331
|
from ezmsg.util.messages.axisarray import AxisArray
|
|
332
|
-
from ezmsg.
|
|
332
|
+
from ezmsg.baseproc import BaseTransformer, BaseTransformerUnit
|
|
333
333
|
|
|
334
334
|
|
|
335
335
|
class CustomTransformerSettings(ez.Settings):
|
|
@@ -4,7 +4,8 @@ ezmsg-sigproc
|
|
|
4
4
|
Timeseries signal processing implementations in ezmsg, leveraging numpy and scipy.
|
|
5
5
|
Most of the methods and classes in this extension are intended to be used in building signal processing pipelines.
|
|
6
6
|
They use :class:`ezmsg.util.messages.axisarray.AxisArray` as the primary data structure for passing signals between components.
|
|
7
|
-
The message's data are
|
|
7
|
+
The message's data are typically NumPy arrays, though many transformers support the
|
|
8
|
+
:doc:`Array API standard <../explanations/array_api>` for use with CuPy, PyTorch, and other backends.
|
|
8
9
|
|
|
9
10
|
.. note:: Some generators might yield valid :class:`AxisArray` messages with ``.data`` size of 0.
|
|
10
11
|
This may occur when the generator receives inadequate data to produce a valid output, such as when windowing or buffering.
|
|
@@ -21,3 +22,4 @@ This may occur when the generator receives inadequate data to produce a valid ou
|
|
|
21
22
|
base
|
|
22
23
|
units
|
|
23
24
|
processors
|
|
25
|
+
../explanations/array_api
|
{ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/tutorials/signalprocessing.rst
RENAMED
|
@@ -132,7 +132,7 @@ Add the following import statements to the top of the `downsample.py` file:
|
|
|
132
132
|
)
|
|
133
133
|
import ezmsg.core as ez
|
|
134
134
|
|
|
135
|
-
from ezmsg.
|
|
135
|
+
from ezmsg.baseproc import (
|
|
136
136
|
BaseStatefulTransformer,
|
|
137
137
|
BaseTransformerUnit,
|
|
138
138
|
processor_state,
|
|
@@ -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 = '2.
|
|
32
|
-
__version_tuple__ = version_tuple = (2,
|
|
31
|
+
__version__ = version = '2.9.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 9, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
"""Affine transformations via matrix multiplication: y = Ax or y = Ax + B.
|
|
2
|
+
|
|
3
|
+
For full matrix transformations where channels are mixed (off-diagonal weights),
|
|
4
|
+
use :obj:`AffineTransformTransformer` or the `AffineTransform` unit.
|
|
5
|
+
|
|
6
|
+
For simple per-channel scaling and offset (diagonal weights only), use
|
|
7
|
+
:obj:`LinearTransformTransformer` from :mod:`ezmsg.sigproc.linear` instead,
|
|
8
|
+
which is more efficient as it avoids matrix multiplication.
|
|
9
|
+
"""
|
|
10
|
+
|
|
1
11
|
import os
|
|
2
12
|
from pathlib import Path
|
|
3
13
|
|
|
@@ -17,7 +27,6 @@ from ezmsg.util.messages.util import replace
|
|
|
17
27
|
class AffineTransformSettings(ez.Settings):
|
|
18
28
|
"""
|
|
19
29
|
Settings for :obj:`AffineTransform`.
|
|
20
|
-
See :obj:`affine_transform` for argument details.
|
|
21
30
|
"""
|
|
22
31
|
|
|
23
32
|
weights: np.ndarray | str | Path
|
|
@@ -39,6 +48,19 @@ class AffineTransformState:
|
|
|
39
48
|
class AffineTransformTransformer(
|
|
40
49
|
BaseStatefulTransformer[AffineTransformSettings, AxisArray, AxisArray, AffineTransformState]
|
|
41
50
|
):
|
|
51
|
+
"""Apply affine transformation via matrix multiplication: y = Ax or y = Ax + B.
|
|
52
|
+
|
|
53
|
+
Use this transformer when you need full matrix transformations that mix
|
|
54
|
+
channels (off-diagonal weights), such as spatial filters or projections.
|
|
55
|
+
|
|
56
|
+
For simple per-channel scaling and offset where each output channel depends
|
|
57
|
+
only on its corresponding input channel (diagonal weight matrix), use
|
|
58
|
+
:obj:`LinearTransformTransformer` instead, which is more efficient.
|
|
59
|
+
|
|
60
|
+
The weights matrix can include an offset row (stacked as [A|B]) where the
|
|
61
|
+
input is automatically augmented with a column of ones to compute y = Ax + B.
|
|
62
|
+
"""
|
|
63
|
+
|
|
42
64
|
def __call__(self, message: AxisArray) -> AxisArray:
|
|
43
65
|
# Override __call__ so we can shortcut if weights are None.
|
|
44
66
|
if self.settings.weights is None or (
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aggregation operations over arrays.
|
|
3
|
+
|
|
4
|
+
.. note::
|
|
5
|
+
:obj:`AggregateTransformer` supports the :doc:`Array API standard </guides/explanations/array_api>`,
|
|
6
|
+
enabling use with NumPy, CuPy, PyTorch, and other compatible array libraries.
|
|
7
|
+
:obj:`RangedAggregateTransformer` currently requires NumPy arrays.
|
|
8
|
+
"""
|
|
9
|
+
|
|
1
10
|
import typing
|
|
2
11
|
|
|
3
12
|
import ezmsg.core as ez
|
|
@@ -4,10 +4,10 @@ import typing
|
|
|
4
4
|
import ezmsg.core as ez
|
|
5
5
|
import numpy as np
|
|
6
6
|
import scipy.signal
|
|
7
|
+
from ezmsg.baseproc import SettingsType
|
|
7
8
|
from ezmsg.util.messages.axisarray import AxisArray
|
|
8
9
|
from ezmsg.util.messages.util import replace
|
|
9
10
|
|
|
10
|
-
from ezmsg.sigproc.base import SettingsType
|
|
11
11
|
from ezmsg.sigproc.butterworthfilter import ButterworthFilterSettings, butter_design_fun
|
|
12
12
|
from ezmsg.sigproc.filter import (
|
|
13
13
|
BACoeffs,
|
|
@@ -3,6 +3,10 @@ Coordinate space transformations for streaming data.
|
|
|
3
3
|
|
|
4
4
|
This module provides utilities and ezmsg nodes for transforming between
|
|
5
5
|
Cartesian (x, y) and polar (r, theta) coordinate systems.
|
|
6
|
+
|
|
7
|
+
.. note::
|
|
8
|
+
This module supports the :doc:`Array API standard </guides/explanations/array_api>`,
|
|
9
|
+
enabling use with NumPy, CuPy, PyTorch, and other compatible array libraries.
|
|
6
10
|
"""
|
|
7
11
|
|
|
8
12
|
from enum import Enum
|
|
@@ -11,6 +15,7 @@ from typing import Tuple
|
|
|
11
15
|
import ezmsg.core as ez
|
|
12
16
|
import numpy as np
|
|
13
17
|
import numpy.typing as npt
|
|
18
|
+
from array_api_compat import get_namespace, is_array_api_obj
|
|
14
19
|
from ezmsg.baseproc import (
|
|
15
20
|
BaseTransformer,
|
|
16
21
|
BaseTransformerUnit,
|
|
@@ -20,14 +25,24 @@ from ezmsg.util.messages.axisarray import AxisArray, replace
|
|
|
20
25
|
# -- Utility functions for coordinate transformations --
|
|
21
26
|
|
|
22
27
|
|
|
28
|
+
def _get_namespace_or_numpy(*args: npt.ArrayLike):
|
|
29
|
+
"""Get array namespace if any arg is an array, otherwise return numpy."""
|
|
30
|
+
for arg in args:
|
|
31
|
+
if is_array_api_obj(arg):
|
|
32
|
+
return get_namespace(arg)
|
|
33
|
+
return np
|
|
34
|
+
|
|
35
|
+
|
|
23
36
|
def polar2z(r: npt.ArrayLike, theta: npt.ArrayLike) -> npt.ArrayLike:
|
|
24
37
|
"""Convert polar coordinates to complex number representation."""
|
|
25
|
-
|
|
38
|
+
xp = _get_namespace_or_numpy(r, theta)
|
|
39
|
+
return r * xp.exp(1j * theta)
|
|
26
40
|
|
|
27
41
|
|
|
28
42
|
def z2polar(z: npt.ArrayLike) -> Tuple[npt.ArrayLike, npt.ArrayLike]:
|
|
29
43
|
"""Convert complex number to polar coordinates (r, theta)."""
|
|
30
|
-
|
|
44
|
+
xp = _get_namespace_or_numpy(z)
|
|
45
|
+
return xp.abs(z), xp.atan2(xp.imag(z), xp.real(z))
|
|
31
46
|
|
|
32
47
|
|
|
33
48
|
def cart2z(x: npt.ArrayLike, y: npt.ArrayLike) -> npt.ArrayLike:
|
|
@@ -37,7 +52,8 @@ def cart2z(x: npt.ArrayLike, y: npt.ArrayLike) -> npt.ArrayLike:
|
|
|
37
52
|
|
|
38
53
|
def z2cart(z: npt.ArrayLike) -> Tuple[npt.ArrayLike, npt.ArrayLike]:
|
|
39
54
|
"""Convert complex number to Cartesian coordinates (x, y)."""
|
|
40
|
-
|
|
55
|
+
xp = _get_namespace_or_numpy(z)
|
|
56
|
+
return xp.real(z), xp.imag(z)
|
|
41
57
|
|
|
42
58
|
|
|
43
59
|
def cart2pol(x: npt.ArrayLike, y: npt.ArrayLike) -> Tuple[npt.ArrayLike, npt.ArrayLike]:
|
|
@@ -90,6 +106,7 @@ class CoordinateSpacesTransformer(BaseTransformer[CoordinateSpacesSettings, Axis
|
|
|
90
106
|
"""
|
|
91
107
|
|
|
92
108
|
def _process(self, message: AxisArray) -> AxisArray:
|
|
109
|
+
xp = get_namespace(message.data)
|
|
93
110
|
axis = self.settings.axis or message.dims[-1]
|
|
94
111
|
axis_idx = message.get_axis_idx(axis)
|
|
95
112
|
|
|
@@ -116,9 +133,9 @@ class CoordinateSpacesTransformer(BaseTransformer[CoordinateSpacesSettings, Axis
|
|
|
116
133
|
out_a, out_b = pol2cart(component_a, component_b)
|
|
117
134
|
|
|
118
135
|
# Stack results back along the same axis
|
|
119
|
-
result =
|
|
136
|
+
result = xp.stack([out_a, out_b], axis=axis_idx)
|
|
120
137
|
|
|
121
|
-
# Update axis labels if present
|
|
138
|
+
# Update axis labels if present (use numpy for string labels)
|
|
122
139
|
axes = message.axes
|
|
123
140
|
if axis in axes and hasattr(axes[axis], "data"):
|
|
124
141
|
if self.settings.mode == CoordinateMode.CART2POL:
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import ezmsg.core as ez
|
|
2
2
|
import numpy as np
|
|
3
3
|
import numpy.typing as npt
|
|
4
|
-
from ezmsg.
|
|
5
|
-
from ezmsg.util.messages.util import replace
|
|
6
|
-
|
|
7
|
-
from ezmsg.sigproc.base import (
|
|
4
|
+
from ezmsg.baseproc import (
|
|
8
5
|
BaseStatefulTransformer,
|
|
9
6
|
BaseTransformerUnit,
|
|
10
7
|
processor_state,
|
|
11
8
|
)
|
|
9
|
+
from ezmsg.util.messages.axisarray import AxisArray
|
|
10
|
+
from ezmsg.util.messages.util import replace
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
class DenormalizeSettings(ez.Settings):
|
|
@@ -1,14 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compute differences along an axis.
|
|
3
|
+
|
|
4
|
+
.. note::
|
|
5
|
+
This module supports the :doc:`Array API standard </guides/explanations/array_api>`,
|
|
6
|
+
enabling use with NumPy, CuPy, PyTorch, and other compatible array libraries.
|
|
7
|
+
"""
|
|
8
|
+
|
|
1
9
|
import ezmsg.core as ez
|
|
2
10
|
import numpy as np
|
|
3
11
|
import numpy.typing as npt
|
|
4
|
-
from
|
|
5
|
-
from ezmsg.
|
|
6
|
-
|
|
7
|
-
from ezmsg.sigproc.base import (
|
|
12
|
+
from array_api_compat import get_namespace
|
|
13
|
+
from ezmsg.baseproc import (
|
|
8
14
|
BaseStatefulTransformer,
|
|
9
15
|
BaseTransformerUnit,
|
|
10
16
|
processor_state,
|
|
11
17
|
)
|
|
18
|
+
from ezmsg.util.messages.axisarray import AxisArray, slice_along_axis
|
|
19
|
+
from ezmsg.util.messages.util import replace
|
|
12
20
|
|
|
13
21
|
|
|
14
22
|
class DiffSettings(ez.Settings):
|
|
@@ -40,23 +48,25 @@ class DiffTransformer(BaseStatefulTransformer[DiffSettings, AxisArray, AxisArray
|
|
|
40
48
|
self.state.last_time = ax_info.data[0] - 0.001
|
|
41
49
|
|
|
42
50
|
def _process(self, message: AxisArray) -> AxisArray:
|
|
51
|
+
xp = get_namespace(message.data)
|
|
43
52
|
axis = self.settings.axis or message.dims[0]
|
|
44
53
|
ax_idx = message.get_axis_idx(axis)
|
|
45
54
|
|
|
46
|
-
diffs =
|
|
47
|
-
|
|
55
|
+
diffs = xp.diff(
|
|
56
|
+
xp.concat((self.state.last_dat, message.data), axis=ax_idx),
|
|
48
57
|
axis=ax_idx,
|
|
49
58
|
)
|
|
50
59
|
# Prepare last_dat for next iteration
|
|
51
60
|
self.state.last_dat = slice_along_axis(message.data, slice(-1, None), axis=ax_idx)
|
|
52
|
-
# Scale by fs if requested. This
|
|
61
|
+
# Scale by fs if requested. This converts the diff to a derivative. e.g., diff of position becomes velocity.
|
|
53
62
|
if self.settings.scale_by_fs:
|
|
54
63
|
ax_info = message.get_axis(axis)
|
|
55
64
|
if hasattr(ax_info, "data"):
|
|
65
|
+
# ax_info.data is typically numpy for metadata, so use np.diff here
|
|
56
66
|
dt = np.diff(np.concatenate(([self.state.last_time], ax_info.data)))
|
|
57
67
|
# Expand dt dims to match diffs
|
|
58
68
|
exp_sl = (None,) * ax_idx + (Ellipsis,) + (None,) * (message.data.ndim - ax_idx - 1)
|
|
59
|
-
diffs /= dt[exp_sl]
|
|
69
|
+
diffs /= xp.asarray(dt[exp_sl])
|
|
60
70
|
self.state.last_time = ax_info.data[-1] # For next iteration
|
|
61
71
|
else:
|
|
62
72
|
diffs /= ax_info.gain
|
|
@@ -139,8 +139,15 @@ class EWMA_Deprecated:
|
|
|
139
139
|
|
|
140
140
|
class EWMASettings(ez.Settings):
|
|
141
141
|
time_constant: float = 1.0
|
|
142
|
+
"""The amount of time for the smoothed response of a unit step function to reach 1 - 1/e approx-eq 63.2%."""
|
|
143
|
+
|
|
142
144
|
axis: str | None = None
|
|
143
145
|
|
|
146
|
+
accumulate: bool = True
|
|
147
|
+
"""If True, update the EWMA state with each sample. If False, only apply
|
|
148
|
+
the current EWMA estimate without updating state (useful for inference
|
|
149
|
+
periods where you don't want to adapt statistics)."""
|
|
150
|
+
|
|
144
151
|
|
|
145
152
|
@processor_state
|
|
146
153
|
class EWMAState:
|
|
@@ -166,15 +173,45 @@ class EWMATransformer(BaseStatefulTransformer[EWMASettings, AxisArray, AxisArray
|
|
|
166
173
|
return message
|
|
167
174
|
axis = self.settings.axis or message.dims[0]
|
|
168
175
|
axis_idx = message.get_axis_idx(axis)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
if self.settings.accumulate:
|
|
177
|
+
# Normal behavior: update state with new samples
|
|
178
|
+
expected, self._state.zi = sps.lfilter(
|
|
179
|
+
[self._state.alpha],
|
|
180
|
+
[1.0, self._state.alpha - 1.0],
|
|
181
|
+
message.data,
|
|
182
|
+
axis=axis_idx,
|
|
183
|
+
zi=self._state.zi,
|
|
184
|
+
)
|
|
185
|
+
else:
|
|
186
|
+
# Process-only: compute output without updating state
|
|
187
|
+
expected, _ = sps.lfilter(
|
|
188
|
+
[self._state.alpha],
|
|
189
|
+
[1.0, self._state.alpha - 1.0],
|
|
190
|
+
message.data,
|
|
191
|
+
axis=axis_idx,
|
|
192
|
+
zi=self._state.zi,
|
|
193
|
+
)
|
|
176
194
|
return replace(message, data=expected)
|
|
177
195
|
|
|
178
196
|
|
|
179
197
|
class EWMAUnit(BaseTransformerUnit[EWMASettings, AxisArray, AxisArray, EWMATransformer]):
|
|
180
198
|
SETTINGS = EWMASettings
|
|
199
|
+
|
|
200
|
+
@ez.subscriber(BaseTransformerUnit.INPUT_SETTINGS)
|
|
201
|
+
async def on_settings(self, msg: EWMASettings) -> None:
|
|
202
|
+
"""
|
|
203
|
+
Handle settings updates with smart reset behavior.
|
|
204
|
+
|
|
205
|
+
Only resets state if `axis` changes (structural change).
|
|
206
|
+
Changes to `time_constant` or `accumulate` are applied without
|
|
207
|
+
resetting accumulated state.
|
|
208
|
+
"""
|
|
209
|
+
old_axis = self.SETTINGS.axis
|
|
210
|
+
self.apply_settings(msg)
|
|
211
|
+
|
|
212
|
+
if msg.axis != old_axis:
|
|
213
|
+
# Axis changed - need full reset
|
|
214
|
+
self.create_processor()
|
|
215
|
+
else:
|
|
216
|
+
# Only accumulate or time_constant changed - keep state
|
|
217
|
+
self.processor.settings = msg
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import ezmsg.core as ez
|
|
2
2
|
import numpy as np
|
|
3
|
+
from ezmsg.baseproc import BaseTransformer, BaseTransformerUnit
|
|
3
4
|
from ezmsg.util.messages.axisarray import AxisArray, replace
|
|
4
5
|
|
|
5
|
-
from ezmsg.sigproc.base import BaseTransformer, BaseTransformerUnit
|
|
6
|
-
|
|
7
6
|
|
|
8
7
|
class ExtractAxisSettings(ez.Settings):
|
|
9
8
|
axis: str = "freq"
|
|
@@ -6,10 +6,7 @@ import ezmsg.core as ez
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import numpy.typing as npt
|
|
8
8
|
import scipy.signal
|
|
9
|
-
from ezmsg.
|
|
10
|
-
from ezmsg.util.messages.util import replace
|
|
11
|
-
|
|
12
|
-
from ezmsg.sigproc.base import (
|
|
9
|
+
from ezmsg.baseproc import (
|
|
13
10
|
BaseConsumerUnit,
|
|
14
11
|
BaseStatefulTransformer,
|
|
15
12
|
BaseTransformerUnit,
|
|
@@ -17,6 +14,8 @@ from ezmsg.sigproc.base import (
|
|
|
17
14
|
TransformerType,
|
|
18
15
|
processor_state,
|
|
19
16
|
)
|
|
17
|
+
from ezmsg.util.messages.axisarray import AxisArray
|
|
18
|
+
from ezmsg.util.messages.util import replace
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
@dataclass
|
|
@@ -4,10 +4,10 @@ import typing
|
|
|
4
4
|
import ezmsg.core as ez
|
|
5
5
|
import numpy as np
|
|
6
6
|
import scipy.signal as sps
|
|
7
|
+
from ezmsg.baseproc import BaseStatefulTransformer, processor_state
|
|
7
8
|
from ezmsg.util.messages.axisarray import AxisArray
|
|
8
9
|
from ezmsg.util.messages.util import replace
|
|
9
10
|
|
|
10
|
-
from ezmsg.sigproc.base import BaseStatefulTransformer, processor_state
|
|
11
11
|
from ezmsg.sigproc.filter import (
|
|
12
12
|
BACoeffs,
|
|
13
13
|
BaseFilterByDesignTransformerUnit,
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Apply a linear transformation: output = scale * input + offset.
|
|
3
|
+
|
|
4
|
+
Supports per-element scale and offset along a specified axis.
|
|
5
|
+
For full matrix transformations, use :obj:`AffineTransformTransformer` instead.
|
|
6
|
+
|
|
7
|
+
.. note::
|
|
8
|
+
This module supports the :doc:`Array API standard </guides/explanations/array_api>`,
|
|
9
|
+
enabling use with NumPy, CuPy, PyTorch, and other compatible array libraries.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import ezmsg.core as ez
|
|
13
|
+
import numpy as np
|
|
14
|
+
import numpy.typing as npt
|
|
15
|
+
from array_api_compat import get_namespace
|
|
16
|
+
from ezmsg.baseproc import BaseStatefulTransformer, BaseTransformerUnit, processor_state
|
|
17
|
+
from ezmsg.util.messages.axisarray import AxisArray
|
|
18
|
+
from ezmsg.util.messages.util import replace
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LinearTransformSettings(ez.Settings):
|
|
22
|
+
scale: float | list[float] | npt.ArrayLike = 1.0
|
|
23
|
+
"""Scale factor(s). Can be a scalar (applied to all elements) or an array
|
|
24
|
+
matching the size of the specified axis for per-element scaling."""
|
|
25
|
+
|
|
26
|
+
offset: float | list[float] | npt.ArrayLike = 0.0
|
|
27
|
+
"""Offset value(s). Can be a scalar (applied to all elements) or an array
|
|
28
|
+
matching the size of the specified axis for per-element offset."""
|
|
29
|
+
|
|
30
|
+
axis: str | None = None
|
|
31
|
+
"""Axis along which to apply per-element scale/offset. If None, scalar
|
|
32
|
+
scale/offset are broadcast to all elements."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@processor_state
|
|
36
|
+
class LinearTransformState:
|
|
37
|
+
scale: npt.NDArray = None
|
|
38
|
+
"""Prepared scale array for broadcasting."""
|
|
39
|
+
|
|
40
|
+
offset: npt.NDArray = None
|
|
41
|
+
"""Prepared offset array for broadcasting."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LinearTransformTransformer(
|
|
45
|
+
BaseStatefulTransformer[LinearTransformSettings, AxisArray, AxisArray, LinearTransformState]
|
|
46
|
+
):
|
|
47
|
+
"""Apply linear transformation: output = scale * input + offset.
|
|
48
|
+
|
|
49
|
+
This transformer is optimized for element-wise linear operations with
|
|
50
|
+
optional per-channel (or per-axis) coefficients. For full matrix
|
|
51
|
+
transformations, use :obj:`AffineTransformTransformer` instead.
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
# Uniform scaling and offset
|
|
55
|
+
>>> transformer = LinearTransformTransformer(LinearTransformSettings(scale=2.0, offset=1.0))
|
|
56
|
+
|
|
57
|
+
# Per-channel scaling (e.g., for 3-channel data along "ch" axis)
|
|
58
|
+
>>> transformer = LinearTransformTransformer(LinearTransformSettings(
|
|
59
|
+
... scale=[0.5, 1.0, 2.0],
|
|
60
|
+
... offset=[0.0, 0.1, 0.2],
|
|
61
|
+
... axis="ch"
|
|
62
|
+
... ))
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def _hash_message(self, message: AxisArray) -> int:
|
|
66
|
+
"""Hash based on shape and axis to detect when broadcast shapes need recalculation."""
|
|
67
|
+
axis = self.settings.axis
|
|
68
|
+
if axis is not None:
|
|
69
|
+
axis_idx = message.get_axis_idx(axis)
|
|
70
|
+
return hash((message.data.ndim, axis_idx, message.data.shape[axis_idx]))
|
|
71
|
+
return hash(message.data.ndim)
|
|
72
|
+
|
|
73
|
+
def _reset_state(self, message: AxisArray) -> None:
|
|
74
|
+
"""Prepare scale/offset arrays with proper broadcast shapes."""
|
|
75
|
+
xp = get_namespace(message.data)
|
|
76
|
+
ndim = message.data.ndim
|
|
77
|
+
|
|
78
|
+
scale = self.settings.scale
|
|
79
|
+
offset = self.settings.offset
|
|
80
|
+
|
|
81
|
+
# Convert settings to arrays
|
|
82
|
+
if isinstance(scale, (list, np.ndarray)):
|
|
83
|
+
scale = xp.asarray(scale, dtype=xp.float64)
|
|
84
|
+
else:
|
|
85
|
+
# Scalar: create a 0-d array
|
|
86
|
+
scale = xp.asarray(float(scale), dtype=xp.float64)
|
|
87
|
+
|
|
88
|
+
if isinstance(offset, (list, np.ndarray)):
|
|
89
|
+
offset = xp.asarray(offset, dtype=xp.float64)
|
|
90
|
+
else:
|
|
91
|
+
# Scalar: create a 0-d array
|
|
92
|
+
offset = xp.asarray(float(offset), dtype=xp.float64)
|
|
93
|
+
|
|
94
|
+
# If axis is specified and we have 1-d arrays, reshape for proper broadcasting
|
|
95
|
+
if self.settings.axis is not None and ndim > 0:
|
|
96
|
+
axis_idx = message.get_axis_idx(self.settings.axis)
|
|
97
|
+
|
|
98
|
+
if scale.ndim == 1:
|
|
99
|
+
# Create shape for broadcasting: all 1s except at axis_idx
|
|
100
|
+
broadcast_shape = [1] * ndim
|
|
101
|
+
broadcast_shape[axis_idx] = scale.shape[0]
|
|
102
|
+
scale = xp.reshape(scale, broadcast_shape)
|
|
103
|
+
|
|
104
|
+
if offset.ndim == 1:
|
|
105
|
+
broadcast_shape = [1] * ndim
|
|
106
|
+
broadcast_shape[axis_idx] = offset.shape[0]
|
|
107
|
+
offset = xp.reshape(offset, broadcast_shape)
|
|
108
|
+
|
|
109
|
+
self._state.scale = scale
|
|
110
|
+
self._state.offset = offset
|
|
111
|
+
|
|
112
|
+
def _process(self, message: AxisArray) -> AxisArray:
|
|
113
|
+
result = message.data * self._state.scale + self._state.offset
|
|
114
|
+
return replace(message, data=result)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class LinearTransform(BaseTransformerUnit[LinearTransformSettings, AxisArray, AxisArray, LinearTransformTransformer]):
|
|
118
|
+
"""Unit wrapper for LinearTransformTransformer."""
|
|
119
|
+
|
|
120
|
+
SETTINGS = LinearTransformSettings
|