ezmsg-sigproc 2.11.0__tar.gz → 2.12.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.11.0 → ezmsg_sigproc-2.12.0}/PKG-INFO +1 -1
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/__version__.py +2 -2
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/affinetransform.py +21 -8
- ezmsg_sigproc-2.12.0/src/ezmsg/sigproc/merge.py +358 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/quantize.py +9 -8
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/rollingscaler.py +28 -20
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/scaler.py +10 -4
- ezmsg_sigproc-2.12.0/tests/unit/test_merge.py +369 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/.github/workflows/docs.yml +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/.github/workflows/python-publish.yml +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/.github/workflows/python-tests.yml +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/.gitignore +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/.pre-commit-config.yaml +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/LICENSE +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/README.md +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/Makefile +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/make.bat +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/_templates/autosummary/module.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/api/index.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/conf.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/HybridBuffer.md +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/explanations/array_api.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/explanations/sigproc.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/how-tos/signalprocessing/adaptive.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/how-tos/signalprocessing/checkpoint.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/how-tos/signalprocessing/composite.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/how-tos/signalprocessing/content-signalprocessing.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/how-tos/signalprocessing/processor.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/how-tos/signalprocessing/standalone.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/how-tos/signalprocessing/stateful.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/how-tos/signalprocessing/unit.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/img/HybridBufferBasic.svg +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/img/HybridBufferOverflow.svg +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/sigproc/architecture.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/sigproc/base.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/sigproc/content-sigproc.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/sigproc/processors.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/sigproc/units.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/tutorials/signalprocessing.rst +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/docs/source/index.md +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/pyproject.toml +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/__init__.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/activation.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/adaptive_lattice_notch.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/aggregate.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/bandpower.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/base.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/butterworthfilter.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/butterworthzerophase.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/cheby.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/combfilter.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/coordinatespaces.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/decimate.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/denormalize.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/detrend.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/diff.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/downsample.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/ewma.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/ewmfilter.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/extract_axis.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/fbcca.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/filter.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/filterbank.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/filterbankdesign.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/fir_hilbert.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/fir_pmc.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/firfilter.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/gaussiansmoothing.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/kaiser.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/linear.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/__init__.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/abs.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/add.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/clip.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/difference.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/invert.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/log.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/pow.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/scale.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/messages.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/resample.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/sampler.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/signalinjector.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/singlebandpow.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/slicer.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/spectral.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/spectrogram.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/spectrum.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/transpose.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/util/__init__.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/util/asio.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/util/axisarray_buffer.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/util/buffer.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/util/message.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/util/profile.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/util/sparse.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/util/typeresolution.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/wavelets.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/window.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/__init__.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/conftest.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/helpers/__init__.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/helpers/synth.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/helpers/util.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/bytewax/test_spectrum_bytewax.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/bytewax/test_window_bytewax.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_add_system.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_butterworth_system.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_butterworthzerophase_system.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_coordinatespaces_system.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_decimate_system.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_difference_system.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_downsample_system.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_filter_system.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_fir_hilbert_system.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_fir_pmc_system.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_rollingscaler_system.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_sampler_system.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_scaler_system.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_spectrum_system.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_window_system.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/resources/xform.csv +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/test_profile.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/buffer/test_axisarray_buffer.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/buffer/test_buffer.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/buffer/test_buffer_overflow.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_activation.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_adaptive_lattice_notch.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_affine_transform.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_aggregate.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_bandpower.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_base.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_butter.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_butterworthzerophase.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_combfilter.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_coordinatespaces.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_denormalize.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_diff.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_downsample.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_ewma.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_extract_axis.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_fbcca.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_filter.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_filterbank.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_filterbankdesign.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_fir_hilbert.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_fir_pmc.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_firfilter.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_gaussian_smoothing_filter.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_kaiser.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_linear.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_math.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_math_add.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_math_difference.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_quantize.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_resample.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_rollingscaler.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_sampler.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_scaler.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_singlebandpow.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_slicer.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_spectrogram.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_spectrum.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_transpose.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_util.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_wavelets.py +0 -0
- {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.12.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.12.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
|
|
@@ -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.12.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 12, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -14,6 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
import ezmsg.core as ez
|
|
15
15
|
import numpy as np
|
|
16
16
|
import numpy.typing as npt
|
|
17
|
+
from array_api_compat import get_namespace
|
|
17
18
|
from ezmsg.baseproc import (
|
|
18
19
|
BaseStatefulTransformer,
|
|
19
20
|
BaseTransformer,
|
|
@@ -111,7 +112,13 @@ class AffineTransformTransformer(
|
|
|
111
112
|
|
|
112
113
|
self._state.new_axis = replace(message.axes[axis], data=np.array(new_labels))
|
|
113
114
|
|
|
115
|
+
# Convert weights to match message.data namespace for efficient operations in _process
|
|
116
|
+
xp = get_namespace(message.data)
|
|
117
|
+
if self._state.weights is not None:
|
|
118
|
+
self._state.weights = xp.asarray(self._state.weights)
|
|
119
|
+
|
|
114
120
|
def _process(self, message: AxisArray) -> AxisArray:
|
|
121
|
+
xp = get_namespace(message.data)
|
|
115
122
|
axis = self.settings.axis or message.dims[-1]
|
|
116
123
|
axis_idx = message.get_axis_idx(axis)
|
|
117
124
|
data = message.data
|
|
@@ -120,14 +127,18 @@ class AffineTransformTransformer(
|
|
|
120
127
|
# The weights are stacked A|B where A is the transform and B is a single row
|
|
121
128
|
# in the equation y = Ax + B. This supports NeuroKey's weights matrices.
|
|
122
129
|
sample_shape = data.shape[:axis_idx] + (1,) + data.shape[axis_idx + 1 :]
|
|
123
|
-
data =
|
|
130
|
+
data = xp.concat((data, xp.ones(sample_shape, dtype=data.dtype)), axis=axis_idx)
|
|
124
131
|
|
|
125
132
|
if axis_idx in [-1, len(message.dims) - 1]:
|
|
126
|
-
data =
|
|
133
|
+
data = xp.matmul(data, self._state.weights)
|
|
127
134
|
else:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
data =
|
|
135
|
+
perm = list(range(data.ndim))
|
|
136
|
+
perm.append(perm.pop(axis_idx))
|
|
137
|
+
data = xp.permute_dims(data, perm)
|
|
138
|
+
data = xp.matmul(data, self._state.weights)
|
|
139
|
+
inv_perm = list(range(data.ndim))
|
|
140
|
+
inv_perm.insert(axis_idx, inv_perm.pop(-1))
|
|
141
|
+
data = xp.permute_dims(data, inv_perm)
|
|
131
142
|
|
|
132
143
|
replace_kwargs = {"data": data}
|
|
133
144
|
if self._state.new_axis is not None:
|
|
@@ -161,8 +172,9 @@ def affine_transform(
|
|
|
161
172
|
)
|
|
162
173
|
|
|
163
174
|
|
|
164
|
-
def zeros_for_noop(data
|
|
165
|
-
|
|
175
|
+
def zeros_for_noop(data, **ignore_kwargs):
|
|
176
|
+
xp = get_namespace(data)
|
|
177
|
+
return xp.zeros_like(data)
|
|
166
178
|
|
|
167
179
|
|
|
168
180
|
class CommonRereferenceSettings(ez.Settings):
|
|
@@ -185,10 +197,11 @@ class CommonRereferenceTransformer(BaseTransformer[CommonRereferenceSettings, Ax
|
|
|
185
197
|
if self.settings.mode == "passthrough":
|
|
186
198
|
return message
|
|
187
199
|
|
|
200
|
+
xp = get_namespace(message.data)
|
|
188
201
|
axis = self.settings.axis or message.dims[-1]
|
|
189
202
|
axis_idx = message.get_axis_idx(axis)
|
|
190
203
|
|
|
191
|
-
func = {"mean":
|
|
204
|
+
func = {"mean": xp.mean, "median": np.median, "passthrough": zeros_for_noop}[self.settings.mode]
|
|
192
205
|
|
|
193
206
|
ref_data = func(message.data, axis=axis_idx, keepdims=True)
|
|
194
207
|
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""Time-aligned merge of two AxisArray streams along a non-time axis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typing
|
|
6
|
+
|
|
7
|
+
import ezmsg.core as ez
|
|
8
|
+
import numpy as np
|
|
9
|
+
from array_api_compat import get_namespace
|
|
10
|
+
from ezmsg.baseproc.protocols import processor_state
|
|
11
|
+
from ezmsg.baseproc.stateful import BaseStatefulTransformer
|
|
12
|
+
from ezmsg.baseproc.units import BaseProcessorUnit
|
|
13
|
+
from ezmsg.util.messages.axisarray import AxisArray, CoordinateAxis
|
|
14
|
+
from ezmsg.util.messages.util import replace
|
|
15
|
+
|
|
16
|
+
from .util.axisarray_buffer import HybridAxisArrayBuffer
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MergeSettings(ez.Settings):
|
|
20
|
+
axis: str = "ch"
|
|
21
|
+
"""Axis along which to concatenate the two signals."""
|
|
22
|
+
|
|
23
|
+
align_axis: str | None = "time"
|
|
24
|
+
"""Axis used for alignment. If None, defaults to the first dimension."""
|
|
25
|
+
|
|
26
|
+
buffer_dur: float = 10.0
|
|
27
|
+
"""Buffer duration in seconds for each input stream."""
|
|
28
|
+
|
|
29
|
+
relabel_axis: bool = True
|
|
30
|
+
"""Whether to relabel coordinate axis labels to ensure uniqueness."""
|
|
31
|
+
|
|
32
|
+
label_a: str = "_a"
|
|
33
|
+
"""Suffix appended to signal A labels when relabel_axis is True."""
|
|
34
|
+
|
|
35
|
+
label_b: str = "_b"
|
|
36
|
+
"""Suffix appended to signal B labels when relabel_axis is True."""
|
|
37
|
+
|
|
38
|
+
new_key: str | None = None
|
|
39
|
+
"""Output AxisArray key. If None, uses the key from signal A."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@processor_state
|
|
43
|
+
class MergeState:
|
|
44
|
+
# Common state
|
|
45
|
+
gain: float | None = None
|
|
46
|
+
align_axis: str | None = None
|
|
47
|
+
aligned: bool = False
|
|
48
|
+
merged_concat_axis: CoordinateAxis | None = None
|
|
49
|
+
|
|
50
|
+
# A state
|
|
51
|
+
buf_a: HybridAxisArrayBuffer | None = None
|
|
52
|
+
concat_axis_a: CoordinateAxis | None = None
|
|
53
|
+
a_concat_dim: int | None = None
|
|
54
|
+
a_other_dims: tuple[int, ...] | None = None
|
|
55
|
+
|
|
56
|
+
# B state
|
|
57
|
+
buf_b: HybridAxisArrayBuffer | None = None
|
|
58
|
+
concat_axis_b: CoordinateAxis | None = None
|
|
59
|
+
b_concat_dim: int | None = None
|
|
60
|
+
b_other_dims: tuple[int, ...] | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class MergeProcessor(BaseStatefulTransformer[MergeSettings, AxisArray, AxisArray | None, MergeState]):
|
|
64
|
+
"""Processor that time-aligns two AxisArray streams and concatenates them.
|
|
65
|
+
|
|
66
|
+
Input A flows through the standard ``__call__`` / ``_process`` path,
|
|
67
|
+
getting automatic ``_hash_message`` / ``_reset_state`` handling from
|
|
68
|
+
:class:`BaseStatefulTransformer`. Input B flows through :meth:`push_b`,
|
|
69
|
+
which independently tracks its own structure.
|
|
70
|
+
|
|
71
|
+
Invalidation rules:
|
|
72
|
+
|
|
73
|
+
- Gain mismatch (either input vs stored common gain) → full reset.
|
|
74
|
+
- Concat-axis dimensionality change → per-input buffer reset +
|
|
75
|
+
alignment and merged-axis cache invalidation.
|
|
76
|
+
- Non-align/non-concat axis shape change → per-input buffer reset +
|
|
77
|
+
alignment invalidation.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
# -- Structural extraction helpers ---------------------------------------
|
|
81
|
+
|
|
82
|
+
def _extract_gain(self, message: AxisArray) -> float | None:
|
|
83
|
+
"""Extract the align-axis gain from a message."""
|
|
84
|
+
align_name = self.settings.align_axis or message.dims[0]
|
|
85
|
+
ax = message.axes.get(align_name)
|
|
86
|
+
if ax is not None and hasattr(ax, "gain"):
|
|
87
|
+
return ax.gain
|
|
88
|
+
if ax is not None and hasattr(ax, "data") and len(ax.data) > 1:
|
|
89
|
+
return float(ax.data[-1] - ax.data[0]) / (len(ax.data) - 1)
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
# -- Reset helpers -------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def _full_reset(self, align_axis: str) -> None:
|
|
95
|
+
"""Reset all state — both inputs and common merge state."""
|
|
96
|
+
self._state.align_axis = align_axis
|
|
97
|
+
self._state.buf_a = HybridAxisArrayBuffer(duration=self.settings.buffer_dur, axis=align_axis)
|
|
98
|
+
self._state.buf_b = HybridAxisArrayBuffer(duration=self.settings.buffer_dur, axis=align_axis)
|
|
99
|
+
self._state.gain = None
|
|
100
|
+
self._state.aligned = False
|
|
101
|
+
self._state.concat_axis_a = None
|
|
102
|
+
self._state.concat_axis_b = None
|
|
103
|
+
self._state.merged_concat_axis = None
|
|
104
|
+
self._state.a_concat_dim = None
|
|
105
|
+
self._state.a_other_dims = None
|
|
106
|
+
self._state.b_concat_dim = None
|
|
107
|
+
self._state.b_other_dims = None
|
|
108
|
+
|
|
109
|
+
def _reset_a_state(self) -> None:
|
|
110
|
+
"""Reset input-A buffer and concat-axis cache."""
|
|
111
|
+
self._state.buf_a = HybridAxisArrayBuffer(duration=self.settings.buffer_dur, axis=self._state.align_axis)
|
|
112
|
+
self._state.concat_axis_a = None
|
|
113
|
+
|
|
114
|
+
def _reset_b_state(self) -> None:
|
|
115
|
+
"""Reset input-B buffer and concat-axis cache."""
|
|
116
|
+
self._state.buf_b = HybridAxisArrayBuffer(duration=self.settings.buffer_dur, axis=self._state.align_axis)
|
|
117
|
+
self._state.concat_axis_b = None
|
|
118
|
+
|
|
119
|
+
# -- BaseStatefulTransformer interface ------------------------------------
|
|
120
|
+
|
|
121
|
+
def _hash_message(self, message: AxisArray) -> int:
|
|
122
|
+
"""Hash the align-axis gain only.
|
|
123
|
+
|
|
124
|
+
Gain changes trigger a full reset via ``_reset_state``. Concat-axis
|
|
125
|
+
and non-merge dimension changes are handled as partial resets inside
|
|
126
|
+
``_process`` and ``push_b``.
|
|
127
|
+
"""
|
|
128
|
+
return hash(self._extract_gain(message))
|
|
129
|
+
|
|
130
|
+
def _reset_state(self, message: AxisArray) -> None:
|
|
131
|
+
"""Full reset — called by the base class when gain changes."""
|
|
132
|
+
align_axis = self.settings.align_axis or message.dims[0]
|
|
133
|
+
self._full_reset(align_axis)
|
|
134
|
+
|
|
135
|
+
def _process(self, message: AxisArray) -> AxisArray | None:
|
|
136
|
+
"""Process input A: detect structural changes, buffer, try merge."""
|
|
137
|
+
# Detect per-input structural changes.
|
|
138
|
+
align_idx = message.dims.index(self._state.align_axis)
|
|
139
|
+
concat_idx = message.dims.index(self.settings.axis) if self.settings.axis in message.dims else None
|
|
140
|
+
concat_dim = message.data.shape[concat_idx] if concat_idx is not None else None
|
|
141
|
+
other_dims = tuple(s for i, s in enumerate(message.data.shape) if i != align_idx and i != concat_idx)
|
|
142
|
+
|
|
143
|
+
if self._state.a_concat_dim is not None and concat_dim != self._state.a_concat_dim:
|
|
144
|
+
self._reset_a_state()
|
|
145
|
+
self._state.aligned = False
|
|
146
|
+
self._state.merged_concat_axis = None
|
|
147
|
+
elif self._state.a_other_dims is not None and other_dims != self._state.a_other_dims:
|
|
148
|
+
self._reset_a_state()
|
|
149
|
+
self._state.aligned = False
|
|
150
|
+
|
|
151
|
+
self._state.a_concat_dim = concat_dim
|
|
152
|
+
self._state.a_other_dims = other_dims
|
|
153
|
+
|
|
154
|
+
self._state.buf_a.write(message)
|
|
155
|
+
if self._state.gain is None:
|
|
156
|
+
self._state.gain = self._state.buf_a.axis_gain
|
|
157
|
+
self._update_concat_axis(message, "a")
|
|
158
|
+
return self._try_merge()
|
|
159
|
+
|
|
160
|
+
# -- Input B entry point ------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def push_b(self, message: AxisArray) -> AxisArray | None:
|
|
163
|
+
"""Process input B: check gain, detect structural changes, buffer, try merge."""
|
|
164
|
+
align_axis = self.settings.align_axis or message.dims[0]
|
|
165
|
+
|
|
166
|
+
# Gain compatibility check.
|
|
167
|
+
b_gain = self._extract_gain(message)
|
|
168
|
+
if self._state.gain is not None and b_gain != self._state.gain:
|
|
169
|
+
self._full_reset(align_axis)
|
|
170
|
+
# Set the base-class hash so the next compatible A goes straight
|
|
171
|
+
# to _process instead of triggering another full reset.
|
|
172
|
+
self._hash = self._hash_message(message)
|
|
173
|
+
|
|
174
|
+
# Lazy-create buf_b if B arrives before A.
|
|
175
|
+
if self._state.buf_b is None:
|
|
176
|
+
if self._state.align_axis is None:
|
|
177
|
+
self._state.align_axis = align_axis
|
|
178
|
+
self._state.buf_b = HybridAxisArrayBuffer(duration=self.settings.buffer_dur, axis=align_axis)
|
|
179
|
+
|
|
180
|
+
# Detect per-input structural changes.
|
|
181
|
+
align_idx = message.dims.index(align_axis)
|
|
182
|
+
concat_idx = message.dims.index(self.settings.axis) if self.settings.axis in message.dims else None
|
|
183
|
+
concat_dim = message.data.shape[concat_idx] if concat_idx is not None else None
|
|
184
|
+
other_dims = tuple(s for i, s in enumerate(message.data.shape) if i != align_idx and i != concat_idx)
|
|
185
|
+
|
|
186
|
+
if self._state.b_concat_dim is not None and concat_dim != self._state.b_concat_dim:
|
|
187
|
+
self._reset_b_state()
|
|
188
|
+
self._state.aligned = False
|
|
189
|
+
self._state.merged_concat_axis = None
|
|
190
|
+
elif self._state.b_other_dims is not None and other_dims != self._state.b_other_dims:
|
|
191
|
+
self._reset_b_state()
|
|
192
|
+
self._state.aligned = False
|
|
193
|
+
|
|
194
|
+
self._state.b_concat_dim = concat_dim
|
|
195
|
+
self._state.b_other_dims = other_dims
|
|
196
|
+
|
|
197
|
+
self._state.buf_b.write(message)
|
|
198
|
+
if self._state.gain is None:
|
|
199
|
+
self._state.gain = self._state.buf_b.axis_gain
|
|
200
|
+
self._update_concat_axis(message, "b")
|
|
201
|
+
return self._try_merge()
|
|
202
|
+
|
|
203
|
+
# -- Concat-axis caching ------------------------------------------------
|
|
204
|
+
|
|
205
|
+
def _update_concat_axis(self, message: AxisArray, which: str) -> None:
|
|
206
|
+
"""Track each input's concat-axis labels; invalidate cache on change."""
|
|
207
|
+
concat_dim = self.settings.axis
|
|
208
|
+
if concat_dim not in message.axes:
|
|
209
|
+
return
|
|
210
|
+
ax = message.axes[concat_dim]
|
|
211
|
+
if not hasattr(ax, "data"):
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
if which == "a":
|
|
215
|
+
if self._state.concat_axis_a is None or not np.array_equal(self._state.concat_axis_a.data, ax.data):
|
|
216
|
+
self._state.concat_axis_a = ax
|
|
217
|
+
self._state.merged_concat_axis = None
|
|
218
|
+
else:
|
|
219
|
+
if self._state.concat_axis_b is None or not np.array_equal(self._state.concat_axis_b.data, ax.data):
|
|
220
|
+
self._state.concat_axis_b = ax
|
|
221
|
+
self._state.merged_concat_axis = None
|
|
222
|
+
|
|
223
|
+
def _build_merged_concat_axis(self) -> CoordinateAxis | None:
|
|
224
|
+
"""Build the merged CoordinateAxis from the two cached per-input axes."""
|
|
225
|
+
if self._state.concat_axis_a is None or self._state.concat_axis_b is None:
|
|
226
|
+
return None
|
|
227
|
+
if self.settings.relabel_axis:
|
|
228
|
+
labels_a = np.array([str(lbl) + self.settings.label_a for lbl in self._state.concat_axis_a.data])
|
|
229
|
+
labels_b = np.array([str(lbl) + self.settings.label_b for lbl in self._state.concat_axis_b.data])
|
|
230
|
+
else:
|
|
231
|
+
labels_a = self._state.concat_axis_a.data
|
|
232
|
+
labels_b = self._state.concat_axis_b.data
|
|
233
|
+
return CoordinateAxis(
|
|
234
|
+
data=np.concatenate([labels_a, labels_b]),
|
|
235
|
+
dims=self._state.concat_axis_a.dims,
|
|
236
|
+
unit=self._state.concat_axis_a.unit,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# -- Core merge logic ---------------------------------------------------
|
|
240
|
+
|
|
241
|
+
def _try_merge(self) -> AxisArray | None:
|
|
242
|
+
"""Align and read from both buffers, returning the merged result.
|
|
243
|
+
|
|
244
|
+
Initial alignment is performed once. After the first successful
|
|
245
|
+
merge the two streams are assumed to share a common clock and
|
|
246
|
+
never drop samples, so we simply read
|
|
247
|
+
``min(available_a, available_b)`` on every subsequent call.
|
|
248
|
+
"""
|
|
249
|
+
if self._state.buf_a is None or self._state.buf_b is None:
|
|
250
|
+
return None
|
|
251
|
+
if self._state.buf_a.is_empty() or self._state.buf_b.is_empty():
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
gain = self._state.gain
|
|
255
|
+
|
|
256
|
+
# --- Initial alignment (runs only until the first successful merge) ---
|
|
257
|
+
if not self._state.aligned:
|
|
258
|
+
first_a = self._state.buf_a.axis_first_value
|
|
259
|
+
final_a = self._state.buf_a.axis_final_value
|
|
260
|
+
first_b = self._state.buf_b.axis_first_value
|
|
261
|
+
final_b = self._state.buf_b.axis_final_value
|
|
262
|
+
|
|
263
|
+
overlap_start = max(first_a, first_b)
|
|
264
|
+
overlap_end = min(final_a, final_b)
|
|
265
|
+
|
|
266
|
+
if overlap_end < overlap_start - gain / 2:
|
|
267
|
+
if final_a < first_b:
|
|
268
|
+
self._state.buf_a.seek(self._state.buf_a.available())
|
|
269
|
+
elif final_b < first_a:
|
|
270
|
+
self._state.buf_b.seek(self._state.buf_b.available())
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
if first_a < overlap_start - gain / 2:
|
|
274
|
+
self._state.buf_a.seek(int(round((overlap_start - first_a) / gain)))
|
|
275
|
+
if first_b < overlap_start - gain / 2:
|
|
276
|
+
self._state.buf_b.seek(int(round((overlap_start - first_b) / gain)))
|
|
277
|
+
|
|
278
|
+
# --- Read aligned samples ---
|
|
279
|
+
n_read = min(self._state.buf_a.available(), self._state.buf_b.available())
|
|
280
|
+
if n_read <= 0:
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
aa_a = self._state.buf_a.read(n_read)
|
|
284
|
+
aa_b = self._state.buf_b.read(n_read)
|
|
285
|
+
if aa_a is None or aa_b is None:
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
if not self._state.aligned:
|
|
289
|
+
axis_a = aa_a.axes.get(self._state.align_axis)
|
|
290
|
+
axis_b = aa_b.axes.get(self._state.align_axis)
|
|
291
|
+
if axis_a is not None and axis_b is not None:
|
|
292
|
+
off_a = axis_a.value(0) if hasattr(axis_a, "value") else None
|
|
293
|
+
off_b = axis_b.value(0) if hasattr(axis_b, "value") else None
|
|
294
|
+
if off_a is not None and off_b is not None:
|
|
295
|
+
if not np.isclose(off_a, off_b, atol=abs(gain) * 1e-6):
|
|
296
|
+
raise RuntimeError(
|
|
297
|
+
f"Offset mismatch after alignment: " f"off_a={off_a}, off_b={off_b}, gain={gain}"
|
|
298
|
+
)
|
|
299
|
+
self._state.aligned = True
|
|
300
|
+
|
|
301
|
+
return self._concat(aa_a, aa_b)
|
|
302
|
+
|
|
303
|
+
def _concat(self, a: AxisArray, b: AxisArray) -> AxisArray:
|
|
304
|
+
"""Concatenate *a* and *b* along the configured merge axis."""
|
|
305
|
+
merge_dim = self.settings.axis
|
|
306
|
+
|
|
307
|
+
# If the merge dim doesn't exist in an input, add it as a trailing axis.
|
|
308
|
+
if merge_dim not in a.dims:
|
|
309
|
+
xp = get_namespace(a.data)
|
|
310
|
+
a = replace(a, data=xp.expand_dims(a.data, axis=-1), dims=[*a.dims, merge_dim])
|
|
311
|
+
if merge_dim not in b.dims:
|
|
312
|
+
xp = get_namespace(b.data)
|
|
313
|
+
b = replace(b, data=xp.expand_dims(b.data, axis=-1), dims=[*b.dims, merge_dim])
|
|
314
|
+
|
|
315
|
+
# Use the cached merged axis (rebuilt lazily when labels change).
|
|
316
|
+
if self._state.merged_concat_axis is None:
|
|
317
|
+
self._state.merged_concat_axis = self._build_merged_concat_axis()
|
|
318
|
+
|
|
319
|
+
key = self.settings.new_key if self.settings.new_key is not None else a.key
|
|
320
|
+
result = AxisArray.concatenate(a, b, dim=merge_dim, axis=self._state.merged_concat_axis)
|
|
321
|
+
if key != result.key:
|
|
322
|
+
result = replace(result, key=key)
|
|
323
|
+
return result
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class Merge(BaseProcessorUnit[MergeSettings]):
|
|
327
|
+
"""Merge two AxisArray streams by time-aligning and concatenating along a non-time axis.
|
|
328
|
+
|
|
329
|
+
Input A routes through the processor's ``__acall__`` (triggering
|
|
330
|
+
hash-based reset when the stream structure changes). Input B
|
|
331
|
+
routes through ``push_b`` which independently tracks its own structure.
|
|
332
|
+
|
|
333
|
+
Inherits ``INPUT_SETTINGS`` and ``on_settings`` → ``create_processor``
|
|
334
|
+
from :class:`BaseProcessorUnit`.
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
SETTINGS = MergeSettings
|
|
338
|
+
|
|
339
|
+
INPUT_SIGNAL_A = ez.InputStream(AxisArray)
|
|
340
|
+
INPUT_SIGNAL_B = ez.InputStream(AxisArray)
|
|
341
|
+
OUTPUT_SIGNAL = ez.OutputStream(AxisArray)
|
|
342
|
+
|
|
343
|
+
def create_processor(self) -> None:
|
|
344
|
+
self.processor = MergeProcessor(settings=self.SETTINGS)
|
|
345
|
+
|
|
346
|
+
@ez.subscriber(INPUT_SIGNAL_A, zero_copy=True)
|
|
347
|
+
@ez.publisher(OUTPUT_SIGNAL)
|
|
348
|
+
async def on_a(self, msg: AxisArray) -> typing.AsyncGenerator:
|
|
349
|
+
result = await self.processor.__acall__(msg)
|
|
350
|
+
if result is not None:
|
|
351
|
+
yield self.OUTPUT_SIGNAL, result
|
|
352
|
+
|
|
353
|
+
@ez.subscriber(INPUT_SIGNAL_B, zero_copy=True)
|
|
354
|
+
@ez.publisher(OUTPUT_SIGNAL)
|
|
355
|
+
async def on_b(self, msg: AxisArray) -> typing.AsyncGenerator:
|
|
356
|
+
result = self.processor.push_b(msg)
|
|
357
|
+
if result is not None:
|
|
358
|
+
yield self.OUTPUT_SIGNAL, result
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ezmsg.core as ez
|
|
2
|
-
|
|
2
|
+
from array_api_compat import get_namespace
|
|
3
3
|
from ezmsg.baseproc import BaseTransformer, BaseTransformerUnit
|
|
4
4
|
from ezmsg.util.messages.axisarray import AxisArray, replace
|
|
5
5
|
|
|
@@ -33,32 +33,33 @@ class QuantizeTransformer(BaseTransformer[QuantizeSettings, AxisArray, AxisArray
|
|
|
33
33
|
self,
|
|
34
34
|
message: AxisArray,
|
|
35
35
|
) -> AxisArray:
|
|
36
|
+
xp = get_namespace(message.data)
|
|
36
37
|
expected_range = self.settings.max_val - self.settings.min_val
|
|
37
38
|
scale_factor = 2**self.settings.bits - 1
|
|
38
39
|
clip_max = self.settings.max_val
|
|
39
40
|
|
|
40
41
|
# Determine appropriate integer type based on bits
|
|
41
42
|
if self.settings.bits <= 1:
|
|
42
|
-
dtype = bool
|
|
43
|
+
dtype = xp.bool
|
|
43
44
|
elif self.settings.bits <= 8:
|
|
44
|
-
dtype =
|
|
45
|
+
dtype = xp.uint8
|
|
45
46
|
elif self.settings.bits <= 16:
|
|
46
|
-
dtype =
|
|
47
|
+
dtype = xp.uint16
|
|
47
48
|
elif self.settings.bits <= 32:
|
|
48
|
-
dtype =
|
|
49
|
+
dtype = xp.uint32
|
|
49
50
|
else:
|
|
50
|
-
dtype =
|
|
51
|
+
dtype = xp.uint64
|
|
51
52
|
if self.settings.bits == 64:
|
|
52
53
|
# The practical upper bound before converting to int is: 2**64 - 1025
|
|
53
54
|
# Anything larger will wrap around to 0.
|
|
54
55
|
#
|
|
55
56
|
clip_max *= 1 - 2e-16
|
|
56
57
|
|
|
57
|
-
data = message.data
|
|
58
|
+
data = xp.clip(message.data, self.settings.min_val, clip_max)
|
|
58
59
|
data = (data - self.settings.min_val) / expected_range
|
|
59
60
|
|
|
60
61
|
# Scale to the quantized range [0, 2^bits - 1]
|
|
61
|
-
data =
|
|
62
|
+
data = xp.round(scale_factor * data).astype(dtype)
|
|
62
63
|
|
|
63
64
|
# Create a new AxisArray with the quantized data
|
|
64
65
|
return replace(message, data=data)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import math
|
|
1
2
|
from collections import deque
|
|
2
3
|
|
|
3
4
|
import ezmsg.core as ez
|
|
4
|
-
import numpy as np
|
|
5
5
|
import numpy.typing as npt
|
|
6
|
+
from array_api_compat import get_namespace
|
|
6
7
|
from ezmsg.baseproc import (
|
|
7
8
|
BaseAdaptiveTransformer,
|
|
8
9
|
BaseAdaptiveTransformerUnit,
|
|
@@ -111,12 +112,13 @@ class RollingScalerProcessor(BaseAdaptiveTransformer[RollingScalerSettings, Axis
|
|
|
111
112
|
return hash((message.key, samp_shape, gain))
|
|
112
113
|
|
|
113
114
|
def _reset_state(self, message: AxisArray) -> None:
|
|
115
|
+
xp = get_namespace(message.data)
|
|
114
116
|
ch = message.data.shape[-1]
|
|
115
|
-
self._state.mean =
|
|
117
|
+
self._state.mean = xp.zeros(ch, dtype=xp.float64)
|
|
116
118
|
self._state.N = 0
|
|
117
|
-
self._state.M2 =
|
|
119
|
+
self._state.M2 = xp.zeros(ch, dtype=xp.float64)
|
|
118
120
|
self._state.k_samples = (
|
|
119
|
-
|
|
121
|
+
math.ceil(self.settings.window_size / message.axes[self.settings.axis].gain)
|
|
120
122
|
if self.settings.window_size is not None
|
|
121
123
|
else self.settings.k_samples
|
|
122
124
|
)
|
|
@@ -127,7 +129,7 @@ class RollingScalerProcessor(BaseAdaptiveTransformer[RollingScalerSettings, Axis
|
|
|
127
129
|
ez.logger.warning("k_samples is None; z-score accumulation will be unbounded.")
|
|
128
130
|
self._state.samples = deque(maxlen=self._state.k_samples)
|
|
129
131
|
self._state.min_samples = (
|
|
130
|
-
|
|
132
|
+
math.ceil(self.settings.min_seconds / message.axes[self.settings.axis].gain)
|
|
131
133
|
if self.settings.window_size is not None
|
|
132
134
|
else self.settings.min_samples
|
|
133
135
|
)
|
|
@@ -136,10 +138,11 @@ class RollingScalerProcessor(BaseAdaptiveTransformer[RollingScalerSettings, Axis
|
|
|
136
138
|
self._state.min_samples = self._state.k_samples
|
|
137
139
|
|
|
138
140
|
def _add_batch_stats(self, x: npt.NDArray) -> None:
|
|
139
|
-
|
|
141
|
+
xp = get_namespace(x)
|
|
142
|
+
x = xp.asarray(x, dtype=xp.float64)
|
|
140
143
|
n_b = x.shape[0]
|
|
141
|
-
mean_b =
|
|
142
|
-
M2_b =
|
|
144
|
+
mean_b = xp.mean(x, axis=0)
|
|
145
|
+
M2_b = xp.sum((x - mean_b) ** 2, axis=0)
|
|
143
146
|
|
|
144
147
|
if self._state.k_samples is not None and len(self._state.samples) == self._state.k_samples:
|
|
145
148
|
n_old, mean_old, M2_old = self._state.samples.popleft()
|
|
@@ -148,8 +151,8 @@ class RollingScalerProcessor(BaseAdaptiveTransformer[RollingScalerSettings, Axis
|
|
|
148
151
|
|
|
149
152
|
if N_new <= 0:
|
|
150
153
|
self._state.N = 0
|
|
151
|
-
self._state.mean =
|
|
152
|
-
self._state.M2 =
|
|
154
|
+
self._state.mean = xp.zeros_like(self._state.mean)
|
|
155
|
+
self._state.M2 = xp.zeros_like(self._state.M2)
|
|
153
156
|
else:
|
|
154
157
|
delta = mean_old - self._state.mean
|
|
155
158
|
self._state.N = N_new
|
|
@@ -170,32 +173,37 @@ class RollingScalerProcessor(BaseAdaptiveTransformer[RollingScalerSettings, Axis
|
|
|
170
173
|
self._add_batch_stats(x)
|
|
171
174
|
|
|
172
175
|
def _process(self, message: AxisArray) -> AxisArray:
|
|
176
|
+
xp = get_namespace(message.data)
|
|
177
|
+
|
|
173
178
|
if self._state.N == 0 or self._state.N < self._state.min_samples:
|
|
174
179
|
if self.settings.update_with_signal:
|
|
175
180
|
x = message.data
|
|
176
181
|
if self.settings.artifact_z_thresh is not None and self._state.N > 0:
|
|
177
182
|
varis = self._state.M2 / self._state.N
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
raw_std = varis**0.5
|
|
184
|
+
std = xp.where(xp.isnan(raw_std), raw_std, xp.clip(raw_std, min=1e-8))
|
|
185
|
+
z = xp.abs((x - self._state.mean) / std)
|
|
186
|
+
mask = xp.any(z > self.settings.artifact_z_thresh, axis=1)
|
|
181
187
|
x = x[~mask]
|
|
182
188
|
if x.size > 0:
|
|
183
189
|
self._add_batch_stats(x)
|
|
184
190
|
return message
|
|
185
191
|
|
|
186
192
|
varis = self._state.M2 / self._state.N
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
result =
|
|
193
|
+
raw_std = varis**0.5
|
|
194
|
+
# Preserve NaN from negative variance (will be caught below), clip positive std to floor
|
|
195
|
+
std = xp.where(xp.isnan(raw_std), raw_std, xp.clip(raw_std, min=1e-8))
|
|
196
|
+
result = (message.data - self._state.mean) / std
|
|
197
|
+
# Replace NaN/inf with 0 (equivalent to nan_to_num with nan=0, posinf=0, neginf=0)
|
|
198
|
+
result = xp.where(xp.isfinite(result), result, xp.asarray(0.0, dtype=result.dtype))
|
|
191
199
|
if self.settings.clip is not None:
|
|
192
|
-
result =
|
|
200
|
+
result = xp.clip(result, -self.settings.clip, self.settings.clip)
|
|
193
201
|
|
|
194
202
|
if self.settings.update_with_signal:
|
|
195
203
|
x = message.data
|
|
196
204
|
if self.settings.artifact_z_thresh is not None:
|
|
197
|
-
z_scores =
|
|
198
|
-
mask =
|
|
205
|
+
z_scores = xp.abs((x - self._state.mean) / std)
|
|
206
|
+
mask = xp.any(z_scores > self.settings.artifact_z_thresh, axis=1)
|
|
199
207
|
x = x[~mask]
|
|
200
208
|
if x.size > 0:
|
|
201
209
|
self._add_batch_stats(x)
|
|
@@ -2,6 +2,7 @@ import typing
|
|
|
2
2
|
|
|
3
3
|
import ezmsg.core as ez
|
|
4
4
|
import numpy as np
|
|
5
|
+
from array_api_compat import get_namespace
|
|
5
6
|
from ezmsg.baseproc import (
|
|
6
7
|
BaseStatefulTransformer,
|
|
7
8
|
BaseTransformerUnit,
|
|
@@ -132,15 +133,20 @@ class AdaptiveStandardScalerTransformer(
|
|
|
132
133
|
self._state.vars_sq_ewma.settings = replace(self._state.vars_sq_ewma.settings, accumulate=value)
|
|
133
134
|
|
|
134
135
|
def _process(self, message: AxisArray) -> AxisArray:
|
|
136
|
+
xp = get_namespace(message.data)
|
|
137
|
+
|
|
135
138
|
# Update step (respects accumulate setting via child EWMAs)
|
|
136
139
|
mean_message = self._state.samps_ewma(message)
|
|
137
140
|
var_sq_message = self._state.vars_sq_ewma(replace(message, data=message.data**2))
|
|
138
141
|
|
|
139
|
-
# Get step
|
|
142
|
+
# Get step: safe division avoids warnings from zero/negative variance
|
|
140
143
|
varis = var_sq_message.data - mean_message.data**2
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
+
std = varis**0.5
|
|
145
|
+
mask = std > 0
|
|
146
|
+
safe_std = xp.where(mask, std, xp.asarray(1.0, dtype=std.dtype))
|
|
147
|
+
result = xp.where(
|
|
148
|
+
mask, (message.data - mean_message.data) / safe_std, xp.asarray(0.0, dtype=message.data.dtype)
|
|
149
|
+
)
|
|
144
150
|
return replace(message, data=result)
|
|
145
151
|
|
|
146
152
|
|