ezmsg-sigproc 2.8.0__tar.gz → 2.10.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.8.0 → ezmsg_sigproc-2.10.0}/PKG-INFO +1 -1
- ezmsg_sigproc-2.10.0/docs/source/guides/explanations/array_api.rst +156 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/sigproc/content-sigproc.rst +3 -1
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/tutorials/signalprocessing.rst +5 -24
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/__version__.py +2 -2
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/aggregate.py +9 -0
- ezmsg_sigproc-2.10.0/src/ezmsg/sigproc/butterworthzerophase.py +305 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/coordinatespaces.py +22 -5
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/diff.py +15 -4
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/linear.py +7 -5
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/math/abs.py +11 -6
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/math/add.py +1 -2
- ezmsg_sigproc-2.10.0/src/ezmsg/sigproc/math/clip.py +48 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/math/difference.py +9 -3
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/math/invert.py +8 -3
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/math/log.py +19 -8
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/math/scale.py +8 -3
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/transpose.py +22 -7
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_add_system.py +2 -2
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_butterworthzerophase_system.py +34 -6
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_difference_system.py +3 -3
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_sampler_system.py +1 -1
- ezmsg_sigproc-2.10.0/tests/unit/test_butterworthzerophase.py +545 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_math.py +6 -6
- ezmsg_sigproc-2.8.0/src/ezmsg/sigproc/butterworthzerophase.py +0 -123
- ezmsg_sigproc-2.8.0/src/ezmsg/sigproc/math/clip.py +0 -43
- ezmsg_sigproc-2.8.0/tests/unit/test_butterworthzerophase.py +0 -164
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/.github/workflows/docs.yml +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/.github/workflows/python-publish.yml +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/.github/workflows/python-tests.yml +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/.gitignore +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/.pre-commit-config.yaml +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/LICENSE +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/README.md +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/Makefile +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/make.bat +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/_templates/autosummary/module.rst +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/api/index.rst +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/conf.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/HybridBuffer.md +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/explanations/sigproc.rst +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/how-tos/signalprocessing/adaptive.rst +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/how-tos/signalprocessing/checkpoint.rst +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/how-tos/signalprocessing/composite.rst +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/how-tos/signalprocessing/content-signalprocessing.rst +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/how-tos/signalprocessing/processor.rst +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/how-tos/signalprocessing/standalone.rst +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/how-tos/signalprocessing/stateful.rst +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/how-tos/signalprocessing/unit.rst +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/img/HybridBufferBasic.svg +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/img/HybridBufferOverflow.svg +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/sigproc/architecture.rst +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/sigproc/base.rst +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/sigproc/processors.rst +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/sigproc/units.rst +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/index.md +2 -2
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/pyproject.toml +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/__init__.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/activation.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/adaptive_lattice_notch.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/affinetransform.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/bandpower.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/base.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/butterworthfilter.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/cheby.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/combfilter.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/decimate.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/denormalize.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/detrend.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/downsample.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/ewma.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/ewmfilter.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/extract_axis.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/fbcca.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/filter.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/filterbank.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/filterbankdesign.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/fir_hilbert.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/fir_pmc.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/firfilter.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/gaussiansmoothing.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/kaiser.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/math/__init__.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/messages.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/quantize.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/resample.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/rollingscaler.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/sampler.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/scaler.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/signalinjector.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/slicer.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/spectral.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/spectrogram.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/spectrum.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/util/__init__.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/util/asio.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/util/axisarray_buffer.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/util/buffer.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/util/message.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/util/profile.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/util/sparse.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/util/typeresolution.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/wavelets.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/window.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/__init__.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/conftest.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/helpers/__init__.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/helpers/synth.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/helpers/util.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/bytewax/test_spectrum_bytewax.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/bytewax/test_window_bytewax.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_butterworth_system.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_coordinatespaces_system.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_decimate_system.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_downsample_system.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_filter_system.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_fir_hilbert_system.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_fir_pmc_system.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_rollingscaler_system.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_scaler_system.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_spectrum_system.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_window_system.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/resources/xform.csv +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/test_profile.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/buffer/test_axisarray_buffer.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/buffer/test_buffer.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/buffer/test_buffer_overflow.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_activation.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_adaptive_lattice_notch.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_affine_transform.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_aggregate.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_bandpower.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_base.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_butter.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_combfilter.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_coordinatespaces.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_denormalize.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_diff.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_downsample.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_ewma.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_extract_axis.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_fbcca.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_filter.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_filterbank.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_filterbankdesign.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_fir_hilbert.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_fir_pmc.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_firfilter.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_gaussian_smoothing_filter.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_kaiser.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_linear.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_math_add.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_math_difference.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_quantize.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_resample.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_rollingscaler.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_sampler.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_scaler.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_slicer.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_spectrogram.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_spectrum.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_transpose.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_util.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_wavelets.py +0 -0
- {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.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.10.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
|
|
@@ -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.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/tutorials/signalprocessing.rst
RENAMED
|
@@ -109,7 +109,7 @@ First, we need to install the `ezmsg-sigproc` package if we haven't already. Thi
|
|
|
109
109
|
|
|
110
110
|
.. code-block:: bash
|
|
111
111
|
|
|
112
|
-
pip install "ezmsg
|
|
112
|
+
pip install "ezmsg-sigproc"
|
|
113
113
|
|
|
114
114
|
|
|
115
115
|
|ezmsg_logo_small| Creating the `Downsample` signal processor
|
|
@@ -237,7 +237,7 @@ The first two methods deal with the state of the processor (and are only require
|
|
|
237
237
|
|
|
238
238
|
In order to implement these methods, we need to understand our preferred message format: `AxisArray`. This is a flexible and powerful class for handling multi-dimensional arrays with named axes, which is particularly useful for signal processing applications. I have already used `AxisArray` in our code as the input message and output message types.
|
|
239
239
|
|
|
240
|
-
A detailed explanation of the `AxisArray` class is beyond the scope of this tutorial, but you can refer to the
|
|
240
|
+
A detailed explanation of the `AxisArray` class is beyond the scope of this tutorial, but you can refer to the `AxisArray explainer <https://www.ezmsg.org/explanations/axisarray.html>`_ as well as the `API reference <https://www.ezmsg.org/ezmsg/reference/API/axisarray.html>`_ for more information.
|
|
241
241
|
|
|
242
242
|
Brief Aside on AxisArray
|
|
243
243
|
=================================
|
|
@@ -555,25 +555,6 @@ In a separate Python file in the same directory, you can test the `DownsampleTra
|
|
|
555
555
|
|
|
556
556
|
Doing the above is very handy for unit testing your processor as well as for offline processing of data.
|
|
557
557
|
|
|
558
|
-
.. note:: The `downsample` module in `ezmsg-sigproc` has a utility function for creating a `DownsampleTransformer` instance with the desired settings:
|
|
559
|
-
|
|
560
|
-
.. code-block:: python
|
|
561
|
-
|
|
562
|
-
def downsample(
|
|
563
|
-
axis: str = "time",
|
|
564
|
-
target_rate: float | None = None,
|
|
565
|
-
factor: int | None = None,
|
|
566
|
-
) -> DownsampleTransformer:
|
|
567
|
-
return DownsampleTransformer(
|
|
568
|
-
DownsampleSettings(axis=axis, target_rate=target_rate, factor=factor)
|
|
569
|
-
)
|
|
570
|
-
|
|
571
|
-
After importing this utility function, lines 8 and 9 in our code above could now read:
|
|
572
|
-
|
|
573
|
-
.. code-block:: python
|
|
574
|
-
|
|
575
|
-
downsampler = downsample(axis="time", target_rate=50)
|
|
576
|
-
|
|
577
558
|
Of course, the real power of `ezmsg` comes from integrating your processor into an `ezmsg` Unit and using it in a processing pipeline. We will see how to do this next.
|
|
578
559
|
|
|
579
560
|
|
|
@@ -600,15 +581,15 @@ A lot of the behind-the-scenes work is done for you by the `BaseTransformerUnit`
|
|
|
600
581
|
SETTINGS = DownsampleSettings
|
|
601
582
|
|
|
602
583
|
|
|
603
|
-
Connecting it to other `Component`\ s and initialising the transformer are accomplished in the same way that we did in the
|
|
584
|
+
Connecting it to other `Component`\ s and initialising the transformer are accomplished in the same way that we did in the `Pipeline Tutorial <https://www.ezmsg.org/tutorials/pipeline.html>`_
|
|
604
585
|
|
|
605
586
|
|
|
606
587
|
|ezmsg_logo_small| See Also
|
|
607
588
|
************************************
|
|
608
589
|
|
|
609
590
|
- `Further examples <https://github.com/ezmsg-org/ezmsg/tree/master/examples>`_ can be found in the examples directory in `ezmsg`. These are examples of creating and using `ezmsg` Units and pipelines.
|
|
610
|
-
- `ezmsg-sigproc` has a large number of already implemented signal processors. More information can be found at the :doc:`ezmsg-sigproc reference <../
|
|
611
|
-
-
|
|
591
|
+
- `ezmsg-sigproc` has a large number of already implemented signal processors. More information can be found at the :doc:`ezmsg-sigproc reference <../sigproc/content-sigproc>`.
|
|
592
|
+
- :doc:`Downsample class reference <../../api/generated/ezmsg.sigproc.downsample>`
|
|
612
593
|
|
|
613
594
|
.. |ezmsg_logo_small| image:: ../_static/_images/ezmsg_logo.png
|
|
614
595
|
:width: 40
|
|
@@ -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.10.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 10, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -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
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Streaming zero-phase Butterworth filter implemented as a two-stage composite processor.
|
|
3
|
+
|
|
4
|
+
Stage 1: Forward causal Butterworth filter (from ezmsg.sigproc.butterworthfilter)
|
|
5
|
+
Stage 2: Backward acausal filter with buffering (ButterworthBackwardFilterTransformer)
|
|
6
|
+
|
|
7
|
+
The output is delayed by `pad_length` samples to ensure the backward pass has sufficient
|
|
8
|
+
future context. The pad_length is computed analytically using scipy's heuristic.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import functools
|
|
12
|
+
import typing
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import scipy.signal
|
|
16
|
+
from ezmsg.baseproc import BaseTransformerUnit
|
|
17
|
+
from ezmsg.baseproc.composite import CompositeProcessor
|
|
18
|
+
from ezmsg.util.messages.axisarray import AxisArray
|
|
19
|
+
from ezmsg.util.messages.util import replace
|
|
20
|
+
|
|
21
|
+
from .butterworthfilter import (
|
|
22
|
+
ButterworthFilterSettings,
|
|
23
|
+
ButterworthFilterTransformer,
|
|
24
|
+
butter_design_fun,
|
|
25
|
+
)
|
|
26
|
+
from .filter import BACoeffs, FilterByDesignTransformer, SOSCoeffs
|
|
27
|
+
from .util.axisarray_buffer import HybridAxisArrayBuffer
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ButterworthZeroPhaseSettings(ButterworthFilterSettings):
|
|
31
|
+
"""
|
|
32
|
+
Settings for :obj:`ButterworthZeroPhase`.
|
|
33
|
+
|
|
34
|
+
This implements a streaming zero-phase Butterworth filter using forward-backward
|
|
35
|
+
filtering. The output is delayed by `pad_length` samples to ensure the backward
|
|
36
|
+
pass has sufficient future context.
|
|
37
|
+
|
|
38
|
+
The pad_length is computed by finding where the filter's impulse response decays
|
|
39
|
+
to `settle_cutoff` fraction of its peak value. This accounts for the filter's
|
|
40
|
+
actual time constant rather than just its order.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# Inherits from ButterworthFilterSettings:
|
|
44
|
+
# axis, coef_type, order, cuton, cutoff, wn_hz
|
|
45
|
+
|
|
46
|
+
settle_cutoff: float = 0.01
|
|
47
|
+
"""
|
|
48
|
+
Fraction of peak impulse response used to determine settling time.
|
|
49
|
+
The pad_length is set to the number of samples until the impulse response
|
|
50
|
+
decays to this fraction of its peak. Default is 0.01 (1% of peak).
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
max_pad_duration: float | None = None
|
|
54
|
+
"""
|
|
55
|
+
Maximum pad duration in seconds. If set, the pad_length will be capped
|
|
56
|
+
at this value times the sampling rate. Use this to limit latency for
|
|
57
|
+
filters with very long impulse responses. Default is None (no limit).
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ButterworthBackwardFilterTransformer(FilterByDesignTransformer[ButterworthFilterSettings, BACoeffs | SOSCoeffs]):
|
|
62
|
+
"""
|
|
63
|
+
Backward (acausal) Butterworth filter with buffering.
|
|
64
|
+
|
|
65
|
+
This transformer buffers its input and applies the filter in reverse,
|
|
66
|
+
outputting only the "settled" portion where transients have decayed.
|
|
67
|
+
This introduces a lag of ``pad_length`` samples.
|
|
68
|
+
|
|
69
|
+
Intended to be used as stage 2 in a zero-phase filter pipeline, receiving
|
|
70
|
+
forward-filtered data from a ButterworthFilterTransformer.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
# Instance attributes (initialized in _reset_state)
|
|
74
|
+
_buffer: HybridAxisArrayBuffer | None
|
|
75
|
+
_coefs_cache: BACoeffs | SOSCoeffs | None
|
|
76
|
+
_zi_tiled: np.ndarray | None
|
|
77
|
+
_pad_length: int
|
|
78
|
+
|
|
79
|
+
def get_design_function(
|
|
80
|
+
self,
|
|
81
|
+
) -> typing.Callable[[float], BACoeffs | SOSCoeffs | None]:
|
|
82
|
+
return functools.partial(
|
|
83
|
+
butter_design_fun,
|
|
84
|
+
order=self.settings.order,
|
|
85
|
+
cuton=self.settings.cuton,
|
|
86
|
+
cutoff=self.settings.cutoff,
|
|
87
|
+
coef_type=self.settings.coef_type,
|
|
88
|
+
wn_hz=self.settings.wn_hz,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def _compute_pad_length(self, fs: float) -> int:
|
|
92
|
+
"""
|
|
93
|
+
Compute pad length based on the filter's impulse response settling time.
|
|
94
|
+
|
|
95
|
+
The pad_length is determined by finding where the impulse response decays
|
|
96
|
+
to `settle_cutoff` fraction of its peak value. This is then optionally
|
|
97
|
+
capped by `max_pad_duration`.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
fs: Sampling frequency in Hz.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Number of samples for the pad length.
|
|
104
|
+
"""
|
|
105
|
+
# Design the filter to compute impulse response
|
|
106
|
+
coefs = self.get_design_function()(fs)
|
|
107
|
+
if coefs is None:
|
|
108
|
+
# Filter design failed or is disabled
|
|
109
|
+
return 0
|
|
110
|
+
|
|
111
|
+
# Generate impulse response - use a generous length initially
|
|
112
|
+
# Start with scipy's heuristic as minimum, then extend if needed
|
|
113
|
+
if self.settings.coef_type == "ba":
|
|
114
|
+
min_length = 3 * (self.settings.order + 1)
|
|
115
|
+
else:
|
|
116
|
+
n_sections = (self.settings.order + 1) // 2
|
|
117
|
+
min_length = 3 * n_sections * 2
|
|
118
|
+
|
|
119
|
+
# Use 10x the minimum as initial impulse length, or at least 10000 samples
|
|
120
|
+
# (10000 samples allows for ~333ms at 30kHz, covering most practical cases)
|
|
121
|
+
impulse_length = max(min_length * 10, 10000)
|
|
122
|
+
|
|
123
|
+
# Cap impulse length computation if max_pad_duration is set
|
|
124
|
+
if self.settings.max_pad_duration is not None:
|
|
125
|
+
max_samples = int(self.settings.max_pad_duration * fs)
|
|
126
|
+
impulse_length = min(impulse_length, max_samples + 1)
|
|
127
|
+
|
|
128
|
+
impulse = np.zeros(impulse_length)
|
|
129
|
+
impulse[0] = 1.0
|
|
130
|
+
|
|
131
|
+
if self.settings.coef_type == "ba":
|
|
132
|
+
b, a = coefs
|
|
133
|
+
h = scipy.signal.lfilter(b, a, impulse)
|
|
134
|
+
else:
|
|
135
|
+
h = scipy.signal.sosfilt(coefs, impulse)
|
|
136
|
+
|
|
137
|
+
# Find where impulse response settles to settle_cutoff of peak
|
|
138
|
+
abs_h = np.abs(h)
|
|
139
|
+
peak = abs_h.max()
|
|
140
|
+
if peak == 0:
|
|
141
|
+
return min_length
|
|
142
|
+
|
|
143
|
+
threshold = self.settings.settle_cutoff * peak
|
|
144
|
+
above_threshold = np.where(abs_h > threshold)[0]
|
|
145
|
+
|
|
146
|
+
if len(above_threshold) == 0:
|
|
147
|
+
pad_length = min_length
|
|
148
|
+
else:
|
|
149
|
+
pad_length = above_threshold[-1] + 1
|
|
150
|
+
|
|
151
|
+
# Ensure at least the scipy heuristic minimum
|
|
152
|
+
pad_length = max(pad_length, min_length)
|
|
153
|
+
|
|
154
|
+
# Apply max_pad_duration cap if set
|
|
155
|
+
if self.settings.max_pad_duration is not None:
|
|
156
|
+
max_samples = int(self.settings.max_pad_duration * fs)
|
|
157
|
+
pad_length = min(pad_length, max_samples)
|
|
158
|
+
|
|
159
|
+
return pad_length
|
|
160
|
+
|
|
161
|
+
def _reset_state(self, message: AxisArray) -> None:
|
|
162
|
+
"""Reset filter state when stream changes."""
|
|
163
|
+
self._coefs_cache = None
|
|
164
|
+
self._zi_tiled = None
|
|
165
|
+
self._buffer = None
|
|
166
|
+
# Compute pad_length based on the message's sampling rate
|
|
167
|
+
axis = message.dims[0] if self.settings.axis is None else self.settings.axis
|
|
168
|
+
fs = 1 / message.axes[axis].gain
|
|
169
|
+
self._pad_length = self._compute_pad_length(fs)
|
|
170
|
+
self.state.needs_redesign = True
|
|
171
|
+
|
|
172
|
+
def _compute_zi_tiled(self, data: np.ndarray, ax_idx: int) -> None:
|
|
173
|
+
"""Compute and cache the tiled zi for the given data shape.
|
|
174
|
+
|
|
175
|
+
Called once per stream (or after filter redesign). The result is
|
|
176
|
+
broadcast-ready for multiplication by the edge sample on each chunk.
|
|
177
|
+
"""
|
|
178
|
+
if self.settings.coef_type == "ba":
|
|
179
|
+
b, a = self._coefs_cache
|
|
180
|
+
zi_base = scipy.signal.lfilter_zi(b, a)
|
|
181
|
+
else: # sos
|
|
182
|
+
zi_base = scipy.signal.sosfilt_zi(self._coefs_cache)
|
|
183
|
+
|
|
184
|
+
n_tail = data.ndim - ax_idx - 1
|
|
185
|
+
|
|
186
|
+
if self.settings.coef_type == "ba":
|
|
187
|
+
zi_expand = (None,) * ax_idx + (slice(None),) + (None,) * n_tail
|
|
188
|
+
n_tile = data.shape[:ax_idx] + (1,) + data.shape[ax_idx + 1 :]
|
|
189
|
+
else: # sos
|
|
190
|
+
zi_expand = (slice(None),) + (None,) * ax_idx + (slice(None),) + (None,) * n_tail
|
|
191
|
+
n_tile = (1,) + data.shape[:ax_idx] + (1,) + data.shape[ax_idx + 1 :]
|
|
192
|
+
|
|
193
|
+
self._zi_tiled = np.tile(zi_base[zi_expand], n_tile)
|
|
194
|
+
|
|
195
|
+
def _initialize_zi(self, data: np.ndarray, ax_idx: int) -> np.ndarray:
|
|
196
|
+
"""Initialize filter state (zi) scaled by edge value."""
|
|
197
|
+
if self._zi_tiled is None:
|
|
198
|
+
self._compute_zi_tiled(data, ax_idx)
|
|
199
|
+
first_sample = np.take(data, [0], axis=ax_idx)
|
|
200
|
+
return self._zi_tiled * first_sample
|
|
201
|
+
|
|
202
|
+
def _process(self, message: AxisArray) -> AxisArray:
|
|
203
|
+
axis = message.dims[0] if self.settings.axis is None else self.settings.axis
|
|
204
|
+
ax_idx = message.get_axis_idx(axis)
|
|
205
|
+
fs = 1 / message.axes[axis].gain
|
|
206
|
+
|
|
207
|
+
# Check if we need to redesign filter
|
|
208
|
+
if self._coefs_cache is None or self.state.needs_redesign:
|
|
209
|
+
self._coefs_cache = self.get_design_function()(fs)
|
|
210
|
+
self._pad_length = self._compute_pad_length(fs)
|
|
211
|
+
self._zi_tiled = None # Invalidate; recomputed on next use.
|
|
212
|
+
self.state.needs_redesign = False
|
|
213
|
+
|
|
214
|
+
# Initialize buffer with duration based on pad_length
|
|
215
|
+
# Add some margin to handle variable chunk sizes
|
|
216
|
+
buffer_duration = (self._pad_length + 1) / fs
|
|
217
|
+
self._buffer = HybridAxisArrayBuffer(duration=buffer_duration, axis=axis)
|
|
218
|
+
|
|
219
|
+
# Early exit if filter is effectively disabled
|
|
220
|
+
if self._coefs_cache is None or self.settings.order <= 0 or message.data.size <= 0:
|
|
221
|
+
return message
|
|
222
|
+
|
|
223
|
+
# Write new data to buffer
|
|
224
|
+
self._buffer.write(message)
|
|
225
|
+
n_available = self._buffer.available()
|
|
226
|
+
n_output = n_available - self._pad_length
|
|
227
|
+
|
|
228
|
+
# If we don't have enough data yet, return empty
|
|
229
|
+
if n_output <= 0:
|
|
230
|
+
new_shape = list(message.data.shape)
|
|
231
|
+
new_shape[ax_idx] = 0
|
|
232
|
+
empty_data = np.empty(new_shape, dtype=message.data.dtype)
|
|
233
|
+
return replace(message, data=empty_data)
|
|
234
|
+
|
|
235
|
+
# Peek all available data from buffer
|
|
236
|
+
# Note: HybridAxisArrayBuffer moves the target axis to position 0
|
|
237
|
+
buffered = self._buffer.peek(n_available)
|
|
238
|
+
combined = buffered.data
|
|
239
|
+
buffer_ax_idx = 0 # Buffer always puts time axis at position 0
|
|
240
|
+
|
|
241
|
+
# Backward filter on reversed data
|
|
242
|
+
combined_rev = np.flip(combined, axis=buffer_ax_idx)
|
|
243
|
+
backward_zi = self._initialize_zi(combined_rev, buffer_ax_idx)
|
|
244
|
+
|
|
245
|
+
if self.settings.coef_type == "ba":
|
|
246
|
+
b, a = self._coefs_cache
|
|
247
|
+
y_bwd_rev, _ = scipy.signal.lfilter(b, a, combined_rev, axis=buffer_ax_idx, zi=backward_zi)
|
|
248
|
+
else: # sos
|
|
249
|
+
y_bwd_rev, _ = scipy.signal.sosfilt(self._coefs_cache, combined_rev, axis=buffer_ax_idx, zi=backward_zi)
|
|
250
|
+
|
|
251
|
+
# Reverse back to get output in correct time order
|
|
252
|
+
y_bwd = np.flip(y_bwd_rev, axis=buffer_ax_idx)
|
|
253
|
+
|
|
254
|
+
# Output the settled portion (first n_output samples)
|
|
255
|
+
y = y_bwd[:n_output]
|
|
256
|
+
|
|
257
|
+
# Advance buffer read head to discard output samples, keep pad_length
|
|
258
|
+
self._buffer.seek(n_output)
|
|
259
|
+
|
|
260
|
+
# Build output with adjusted time axis
|
|
261
|
+
# LinearAxis offset is already correct from the buffer
|
|
262
|
+
out_axis = buffered.axes[axis]
|
|
263
|
+
|
|
264
|
+
# Move axis back to original position if needed
|
|
265
|
+
if ax_idx != 0:
|
|
266
|
+
y = np.moveaxis(y, 0, ax_idx)
|
|
267
|
+
|
|
268
|
+
return replace(
|
|
269
|
+
message,
|
|
270
|
+
data=y,
|
|
271
|
+
axes={**message.axes, axis: out_axis},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class ButterworthZeroPhaseTransformer(CompositeProcessor[ButterworthZeroPhaseSettings, AxisArray, AxisArray]):
|
|
276
|
+
"""
|
|
277
|
+
Streaming zero-phase Butterworth filter as a composite of two stages.
|
|
278
|
+
|
|
279
|
+
Stage 1 (forward): Standard causal Butterworth filter with state
|
|
280
|
+
Stage 2 (backward): Acausal Butterworth filter with buffering
|
|
281
|
+
|
|
282
|
+
The output is delayed by ``pad_length`` samples.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
@staticmethod
|
|
286
|
+
def _initialize_processors(
|
|
287
|
+
settings: ButterworthZeroPhaseSettings,
|
|
288
|
+
) -> dict[str, typing.Any]:
|
|
289
|
+
# Both stages use the same filter design settings
|
|
290
|
+
return {
|
|
291
|
+
"forward": ButterworthFilterTransformer(settings),
|
|
292
|
+
"backward": ButterworthBackwardFilterTransformer(settings),
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
@classmethod
|
|
296
|
+
def get_message_type(cls, dir: str) -> type[AxisArray]:
|
|
297
|
+
if dir in ("in", "out"):
|
|
298
|
+
return AxisArray
|
|
299
|
+
raise ValueError(f"Invalid direction: {dir}. Must be 'in' or 'out'.")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class ButterworthZeroPhase(
|
|
303
|
+
BaseTransformerUnit[ButterworthZeroPhaseSettings, AxisArray, AxisArray, ButterworthZeroPhaseTransformer]
|
|
304
|
+
):
|
|
305
|
+
SETTINGS = ButterworthZeroPhaseSettings
|
|
@@ -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:
|