ezmsg-sigproc 2.10.0__tar.gz → 2.11.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.10.0 → ezmsg_sigproc-2.11.0}/PKG-INFO +1 -1
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/__version__.py +2 -2
- ezmsg_sigproc-2.11.0/src/ezmsg/sigproc/math/pow.py +43 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/scaler.py +55 -30
- ezmsg_sigproc-2.11.0/src/ezmsg/sigproc/singlebandpow.py +116 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/ezmsg/test_scaler_system.py +10 -6
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_base.py +19 -21
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_math.py +15 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_scaler.py +6 -3
- ezmsg_sigproc-2.11.0/tests/unit/test_singlebandpow.py +180 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/.github/workflows/docs.yml +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/.github/workflows/python-publish.yml +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/.github/workflows/python-tests.yml +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/.gitignore +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/.pre-commit-config.yaml +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/LICENSE +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/README.md +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/Makefile +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/make.bat +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/_templates/autosummary/module.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/api/index.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/conf.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/HybridBuffer.md +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/explanations/array_api.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/explanations/sigproc.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/how-tos/signalprocessing/adaptive.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/how-tos/signalprocessing/checkpoint.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/how-tos/signalprocessing/composite.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/how-tos/signalprocessing/content-signalprocessing.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/how-tos/signalprocessing/processor.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/how-tos/signalprocessing/standalone.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/how-tos/signalprocessing/stateful.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/how-tos/signalprocessing/unit.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/img/HybridBufferBasic.svg +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/img/HybridBufferOverflow.svg +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/sigproc/architecture.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/sigproc/base.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/sigproc/content-sigproc.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/sigproc/processors.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/sigproc/units.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/guides/tutorials/signalprocessing.rst +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/docs/source/index.md +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/pyproject.toml +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/__init__.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/activation.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/adaptive_lattice_notch.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/affinetransform.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/aggregate.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/bandpower.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/base.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/butterworthfilter.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/butterworthzerophase.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/cheby.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/combfilter.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/coordinatespaces.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/decimate.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/denormalize.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/detrend.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/diff.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/downsample.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/ewma.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/ewmfilter.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/extract_axis.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/fbcca.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/filter.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/filterbank.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/filterbankdesign.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/fir_hilbert.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/fir_pmc.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/firfilter.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/gaussiansmoothing.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/kaiser.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/linear.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/math/__init__.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/math/abs.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/math/add.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/math/clip.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/math/difference.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/math/invert.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/math/log.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/math/scale.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/messages.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/quantize.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/resample.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/rollingscaler.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/sampler.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/signalinjector.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/slicer.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/spectral.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/spectrogram.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/spectrum.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/transpose.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/util/__init__.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/util/asio.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/util/axisarray_buffer.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/util/buffer.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/util/message.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/util/profile.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/util/sparse.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/util/typeresolution.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/wavelets.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/src/ezmsg/sigproc/window.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/__init__.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/conftest.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/helpers/__init__.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/helpers/synth.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/helpers/util.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/bytewax/test_spectrum_bytewax.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/bytewax/test_window_bytewax.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/ezmsg/test_add_system.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/ezmsg/test_butterworth_system.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/ezmsg/test_butterworthzerophase_system.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/ezmsg/test_coordinatespaces_system.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/ezmsg/test_decimate_system.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/ezmsg/test_difference_system.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/ezmsg/test_downsample_system.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/ezmsg/test_filter_system.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/ezmsg/test_fir_hilbert_system.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/ezmsg/test_fir_pmc_system.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/ezmsg/test_rollingscaler_system.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/ezmsg/test_sampler_system.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/ezmsg/test_spectrum_system.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/integration/ezmsg/test_window_system.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/resources/xform.csv +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/test_profile.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/buffer/test_axisarray_buffer.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/buffer/test_buffer.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/buffer/test_buffer_overflow.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_activation.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_adaptive_lattice_notch.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_affine_transform.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_aggregate.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_bandpower.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_butter.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_butterworthzerophase.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_combfilter.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_coordinatespaces.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_denormalize.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_diff.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_downsample.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_ewma.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_extract_axis.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_fbcca.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_filter.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_filterbank.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_filterbankdesign.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_fir_hilbert.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_fir_pmc.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_firfilter.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_gaussian_smoothing_filter.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_kaiser.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_linear.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_math_add.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_math_difference.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_quantize.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_resample.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_rollingscaler.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_sampler.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_slicer.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_spectrogram.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_spectrum.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_transpose.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_util.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.0}/tests/unit/test_wavelets.py +0 -0
- {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.11.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.11.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.11.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 11, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Element-wise power of the data.
|
|
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
|
+
|
|
9
|
+
import ezmsg.core as ez
|
|
10
|
+
from array_api_compat import get_namespace
|
|
11
|
+
from ezmsg.baseproc import BaseTransformer, BaseTransformerUnit
|
|
12
|
+
from ezmsg.util.messages.axisarray import AxisArray
|
|
13
|
+
from ezmsg.util.messages.util import replace
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PowSettings(ez.Settings):
|
|
17
|
+
exponent: float = 2.0
|
|
18
|
+
"""The exponent to raise the data to. Default is 2.0 (squaring)."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PowTransformer(BaseTransformer[PowSettings, AxisArray, AxisArray]):
|
|
22
|
+
def _process(self, message: AxisArray) -> AxisArray:
|
|
23
|
+
xp = get_namespace(message.data)
|
|
24
|
+
return replace(message, data=xp.pow(message.data, self.settings.exponent))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Pow(BaseTransformerUnit[PowSettings, AxisArray, AxisArray, PowTransformer]):
|
|
28
|
+
SETTINGS = PowSettings
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def pow(
|
|
32
|
+
exponent: float = 2.0,
|
|
33
|
+
) -> PowTransformer:
|
|
34
|
+
"""
|
|
35
|
+
Raise the data to an element-wise power. See :obj:`xp.pow` for more details.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
exponent: The exponent to raise the data to. Default is 2.0.
|
|
39
|
+
|
|
40
|
+
Returns: :obj:`PowTransformer`.
|
|
41
|
+
|
|
42
|
+
"""
|
|
43
|
+
return PowTransformer(PowSettings(exponent=exponent))
|
|
@@ -7,7 +7,6 @@ from ezmsg.baseproc import (
|
|
|
7
7
|
BaseTransformerUnit,
|
|
8
8
|
processor_state,
|
|
9
9
|
)
|
|
10
|
-
from ezmsg.util.generator import consumer
|
|
11
10
|
from ezmsg.util.messages.axisarray import AxisArray
|
|
12
11
|
from ezmsg.util.messages.util import replace
|
|
13
12
|
|
|
@@ -18,50 +17,69 @@ from .ewma import _tau_from_alpha as _tau_from_alpha
|
|
|
18
17
|
from .ewma import ewma_step as ewma_step
|
|
19
18
|
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
class RiverAdaptiveStandardScalerSettings(ez.Settings):
|
|
21
|
+
time_constant: float = 1.0
|
|
22
|
+
"""Decay constant ``tau`` in seconds."""
|
|
23
|
+
|
|
24
|
+
axis: str | None = None
|
|
25
|
+
"""The name of the axis to accumulate statistics over."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@processor_state
|
|
29
|
+
class RiverAdaptiveStandardScalerState:
|
|
30
|
+
scaler: typing.Any = None
|
|
31
|
+
axis: str | None = None
|
|
32
|
+
axis_idx: int = 0
|
|
26
33
|
|
|
27
|
-
Args:
|
|
28
|
-
time_constant: Decay constant `tau` in seconds.
|
|
29
|
-
axis: The name of the axis to accumulate statistics over.
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
class RiverAdaptiveStandardScalerTransformer(
|
|
36
|
+
BaseStatefulTransformer[
|
|
37
|
+
RiverAdaptiveStandardScalerSettings,
|
|
38
|
+
AxisArray,
|
|
39
|
+
AxisArray,
|
|
40
|
+
RiverAdaptiveStandardScalerState,
|
|
41
|
+
]
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
Apply the adaptive standard scaler from
|
|
45
|
+
`river <https://riverml.xyz/latest/api/preprocessing/AdaptiveStandardScaler/>`_.
|
|
46
|
+
|
|
47
|
+
This processes data sample-by-sample using River's online learning
|
|
48
|
+
implementation. For a vectorized EWMA-based alternative, see
|
|
49
|
+
:class:`AdaptiveStandardScalerTransformer`.
|
|
34
50
|
"""
|
|
35
|
-
from river import preprocessing
|
|
36
51
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
data = msg_in.data
|
|
52
|
+
def _reset_state(self, message: AxisArray) -> None:
|
|
53
|
+
from river import preprocessing
|
|
54
|
+
|
|
55
|
+
axis = self.settings.axis
|
|
42
56
|
if axis is None:
|
|
43
|
-
axis =
|
|
44
|
-
axis_idx = 0
|
|
57
|
+
axis = message.dims[0]
|
|
58
|
+
self._state.axis_idx = 0
|
|
45
59
|
else:
|
|
46
|
-
axis_idx =
|
|
47
|
-
|
|
48
|
-
|
|
60
|
+
self._state.axis_idx = message.get_axis_idx(axis)
|
|
61
|
+
self._state.axis = axis
|
|
62
|
+
|
|
63
|
+
alpha = _alpha_from_tau(self.settings.time_constant, message.axes[axis].gain)
|
|
64
|
+
self._state.scaler = preprocessing.AdaptiveStandardScaler(fading_factor=alpha)
|
|
49
65
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
66
|
+
def _process(self, message: AxisArray) -> AxisArray:
|
|
67
|
+
data = message.data
|
|
68
|
+
axis_idx = self._state.axis_idx
|
|
69
|
+
if axis_idx != 0:
|
|
70
|
+
data = np.moveaxis(data, axis_idx, 0)
|
|
53
71
|
|
|
54
72
|
result = []
|
|
55
73
|
for sample in data:
|
|
56
74
|
x = {k: v for k, v in enumerate(sample.flatten().tolist())}
|
|
57
|
-
|
|
58
|
-
y =
|
|
75
|
+
self._state.scaler.learn_one(x)
|
|
76
|
+
y = self._state.scaler.transform_one(x)
|
|
59
77
|
k = sorted(y.keys())
|
|
60
78
|
result.append(np.array([y[_] for _ in k]).reshape(sample.shape))
|
|
61
79
|
|
|
62
80
|
result = np.stack(result)
|
|
63
81
|
result = np.moveaxis(result, 0, axis_idx)
|
|
64
|
-
|
|
82
|
+
return replace(message, data=result)
|
|
65
83
|
|
|
66
84
|
|
|
67
85
|
class AdaptiveStandardScalerSettings(EWMASettings): ...
|
|
@@ -158,7 +176,14 @@ class AdaptiveStandardScaler(
|
|
|
158
176
|
self.processor.settings = msg
|
|
159
177
|
|
|
160
178
|
|
|
161
|
-
#
|
|
179
|
+
# Convenience functions to support deprecated generator API
|
|
180
|
+
def scaler(time_constant: float = 1.0, axis: str | None = None) -> RiverAdaptiveStandardScalerTransformer:
|
|
181
|
+
"""Create a :class:`RiverAdaptiveStandardScalerTransformer` with the given parameters."""
|
|
182
|
+
return RiverAdaptiveStandardScalerTransformer(
|
|
183
|
+
settings=RiverAdaptiveStandardScalerSettings(time_constant=time_constant, axis=axis)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
162
187
|
def scaler_np(time_constant: float = 1.0, axis: str | None = None) -> AdaptiveStandardScalerTransformer:
|
|
163
188
|
return AdaptiveStandardScalerTransformer(
|
|
164
189
|
settings=AdaptiveStandardScalerSettings(time_constant=time_constant, axis=axis)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Time-domain single-band power estimation.
|
|
3
|
+
|
|
4
|
+
Two methods are provided:
|
|
5
|
+
|
|
6
|
+
1. **RMS Band Power** — Bandpass filter, square, window into bins, take the mean, optionally take the square root.
|
|
7
|
+
2. **Square-Law + LPF Band Power** — Bandpass filter, square, lowpass filter (smoothing), downsample.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from dataclasses import field
|
|
11
|
+
|
|
12
|
+
import ezmsg.core as ez
|
|
13
|
+
from ezmsg.baseproc import (
|
|
14
|
+
BaseProcessor,
|
|
15
|
+
BaseStatefulProcessor,
|
|
16
|
+
BaseTransformerUnit,
|
|
17
|
+
CompositeProcessor,
|
|
18
|
+
)
|
|
19
|
+
from ezmsg.util.messages.axisarray import AxisArray
|
|
20
|
+
from ezmsg.util.messages.modify import modify_axis
|
|
21
|
+
|
|
22
|
+
from .aggregate import AggregateSettings, AggregateTransformer, AggregationFunction
|
|
23
|
+
from .butterworthfilter import ButterworthFilterSettings, ButterworthFilterTransformer
|
|
24
|
+
from .downsample import DownsampleSettings, DownsampleTransformer
|
|
25
|
+
from .math.pow import PowSettings, PowTransformer
|
|
26
|
+
from .window import WindowTransformer
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RMSBandPowerSettings(ez.Settings):
|
|
30
|
+
"""Settings for :obj:`RMSBandPowerTransformer`."""
|
|
31
|
+
|
|
32
|
+
bandpass: ButterworthFilterSettings = field(
|
|
33
|
+
default_factory=lambda: ButterworthFilterSettings(order=4, coef_type="sos")
|
|
34
|
+
)
|
|
35
|
+
"""Butterworth bandpass filter settings. Set ``cuton`` and ``cutoff`` to define the band."""
|
|
36
|
+
|
|
37
|
+
bin_duration: float = 0.05
|
|
38
|
+
"""Duration of each non-overlapping bin in seconds."""
|
|
39
|
+
|
|
40
|
+
apply_sqrt: bool = True
|
|
41
|
+
"""If True, output is RMS (root-mean-square). If False, output is mean-square power."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class RMSBandPowerTransformer(CompositeProcessor[RMSBandPowerSettings, AxisArray, AxisArray]):
|
|
45
|
+
"""
|
|
46
|
+
RMS band power estimation.
|
|
47
|
+
|
|
48
|
+
Pipeline: bandpass -> square -> window(bins) -> mean(time) -> rename bin->time -> [sqrt]
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _initialize_processors(
|
|
53
|
+
settings: RMSBandPowerSettings,
|
|
54
|
+
) -> dict[str, BaseProcessor | BaseStatefulProcessor]:
|
|
55
|
+
procs: dict[str, BaseProcessor | BaseStatefulProcessor] = {
|
|
56
|
+
"bandpass": ButterworthFilterTransformer(settings.bandpass),
|
|
57
|
+
"square": PowTransformer(PowSettings(exponent=2.0)),
|
|
58
|
+
"window": WindowTransformer(
|
|
59
|
+
axis="time",
|
|
60
|
+
newaxis="bin",
|
|
61
|
+
window_dur=settings.bin_duration,
|
|
62
|
+
window_shift=settings.bin_duration,
|
|
63
|
+
zero_pad_until="none",
|
|
64
|
+
),
|
|
65
|
+
"aggregate": AggregateTransformer(AggregateSettings(axis="time", operation=AggregationFunction.MEAN)),
|
|
66
|
+
"rename": modify_axis(name_map={"bin": "time"}),
|
|
67
|
+
}
|
|
68
|
+
if settings.apply_sqrt:
|
|
69
|
+
procs["sqrt"] = PowTransformer(PowSettings(exponent=0.5))
|
|
70
|
+
return procs
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class RMSBandPower(BaseTransformerUnit[RMSBandPowerSettings, AxisArray, AxisArray, RMSBandPowerTransformer]):
|
|
74
|
+
SETTINGS = RMSBandPowerSettings
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class SquareLawBandPowerSettings(ez.Settings):
|
|
78
|
+
"""Settings for :obj:`SquareLawBandPowerTransformer`."""
|
|
79
|
+
|
|
80
|
+
bandpass: ButterworthFilterSettings = field(
|
|
81
|
+
default_factory=lambda: ButterworthFilterSettings(order=4, coef_type="sos")
|
|
82
|
+
)
|
|
83
|
+
"""Butterworth bandpass filter settings. Set ``cuton`` and ``cutoff`` to define the band."""
|
|
84
|
+
|
|
85
|
+
lowpass: ButterworthFilterSettings = field(
|
|
86
|
+
default_factory=lambda: ButterworthFilterSettings(order=4, coef_type="sos")
|
|
87
|
+
)
|
|
88
|
+
"""Butterworth lowpass filter settings for smoothing the squared signal."""
|
|
89
|
+
|
|
90
|
+
downsample: DownsampleSettings = field(default_factory=DownsampleSettings)
|
|
91
|
+
"""Downsample settings for rate reduction after lowpass smoothing."""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SquareLawBandPowerTransformer(CompositeProcessor[SquareLawBandPowerSettings, AxisArray, AxisArray]):
|
|
95
|
+
"""
|
|
96
|
+
Square-law + LPF band power estimation.
|
|
97
|
+
|
|
98
|
+
Pipeline: bandpass -> square -> lowpass -> downsample
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _initialize_processors(
|
|
103
|
+
settings: SquareLawBandPowerSettings,
|
|
104
|
+
) -> dict[str, BaseProcessor | BaseStatefulProcessor]:
|
|
105
|
+
return {
|
|
106
|
+
"bandpass": ButterworthFilterTransformer(settings.bandpass),
|
|
107
|
+
"square": PowTransformer(PowSettings(exponent=2.0)),
|
|
108
|
+
"lowpass": ButterworthFilterTransformer(settings.lowpass),
|
|
109
|
+
"downsample": DownsampleTransformer(settings.downsample),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class SquareLawBandPower(
|
|
114
|
+
BaseTransformerUnit[SquareLawBandPowerSettings, AxisArray, AxisArray, SquareLawBandPowerTransformer]
|
|
115
|
+
):
|
|
116
|
+
SETTINGS = SquareLawBandPowerSettings
|
|
@@ -8,7 +8,11 @@ from ezmsg.util.messages.axisarray import AxisArray
|
|
|
8
8
|
from ezmsg.util.terminate import TerminateOnTotal, TerminateOnTotalSettings
|
|
9
9
|
from frozendict import frozendict
|
|
10
10
|
|
|
11
|
-
from ezmsg.sigproc.scaler import
|
|
11
|
+
from ezmsg.sigproc.scaler import (
|
|
12
|
+
AdaptiveStandardScaler,
|
|
13
|
+
AdaptiveStandardScalerSettings,
|
|
14
|
+
AdaptiveStandardScalerTransformer,
|
|
15
|
+
)
|
|
12
16
|
from tests.helpers.synth import Counter, CounterSettings
|
|
13
17
|
from tests.helpers.util import get_test_fn
|
|
14
18
|
|
|
@@ -20,10 +24,10 @@ def test_scaler_system(
|
|
|
20
24
|
test_name: str | None = None,
|
|
21
25
|
):
|
|
22
26
|
"""
|
|
23
|
-
For this test, we assume that Counter and
|
|
24
|
-
The purpose of this test is exclusively to test that the AdaptiveStandardScaler
|
|
25
|
-
|
|
26
|
-
This test passing should only be considered a success if
|
|
27
|
+
For this test, we assume that Counter and AdaptiveStandardScalerTransformer are functioning properly.
|
|
28
|
+
The purpose of this test is exclusively to test that the AdaptiveStandardScaler Unit
|
|
29
|
+
correctly wraps AdaptiveStandardScalerTransformer and exposes its parameters.
|
|
30
|
+
This test passing should only be considered a success if test_scaler also passed.
|
|
27
31
|
"""
|
|
28
32
|
block_size: int = 4
|
|
29
33
|
test_filename = get_test_fn(test_name)
|
|
@@ -68,6 +72,6 @@ def test_scaler_system(
|
|
|
68
72
|
dims=["ch", "time"],
|
|
69
73
|
axes=frozendict({"time": AxisArray.TimeAxis(fs=fs)}),
|
|
70
74
|
)
|
|
71
|
-
_scaler =
|
|
75
|
+
_scaler = AdaptiveStandardScalerTransformer(settings=AdaptiveStandardScalerSettings(time_constant=tau, axis="time"))
|
|
72
76
|
expected_output = _scaler.send(expected_input)
|
|
73
77
|
assert np.allclose(expected_output.data.squeeze(), data)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import pickle
|
|
3
3
|
from types import NoneType
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Any
|
|
5
5
|
from unittest.mock import MagicMock
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
@@ -26,7 +26,6 @@ from ezmsg.baseproc import (
|
|
|
26
26
|
_get_processor_message_type,
|
|
27
27
|
processor_state,
|
|
28
28
|
)
|
|
29
|
-
from ezmsg.util.generator import consumer
|
|
30
29
|
|
|
31
30
|
from ezmsg.sigproc.cheby import ChebyshevFilterTransformer
|
|
32
31
|
from ezmsg.sigproc.filter import FilterByDesignState
|
|
@@ -251,18 +250,18 @@ class ChainedCompositeProcessorWithDeepProcessors(CompositeProcessor[MockSetting
|
|
|
251
250
|
}
|
|
252
251
|
|
|
253
252
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
253
|
+
class MockGeneratorTransformer(BaseTransformer[MockSettings, MockMessageA, MockMessageA]):
|
|
254
|
+
"""A mock transformer replacing the legacy @consumer generator."""
|
|
255
|
+
|
|
256
|
+
def _process(self, message: MockMessageA) -> MockMessageA:
|
|
257
|
+
return MockMessageA()
|
|
259
258
|
|
|
260
259
|
|
|
261
260
|
class MockGeneratorCompositeProcessor(CompositeProcessor[MockSettings, MockMessageA, MockMessageB]):
|
|
262
261
|
@staticmethod
|
|
263
262
|
def _initialize_processors(settings):
|
|
264
263
|
return {
|
|
265
|
-
"generator":
|
|
264
|
+
"generator": MockGeneratorTransformer(settings=settings),
|
|
266
265
|
"stateful_processor": MockStatefulProcessor(settings=settings),
|
|
267
266
|
}
|
|
268
267
|
|
|
@@ -333,27 +332,26 @@ class ChainedCompositeProducerWithDeepProcessors(CompositeProducer[MockSettings,
|
|
|
333
332
|
}
|
|
334
333
|
|
|
335
334
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
335
|
+
class MockGeneratorProducer(BaseProducer[MockSettings, MockMessageA]):
|
|
336
|
+
"""A mock producer replacing the legacy @consumer producer generator."""
|
|
337
|
+
|
|
338
|
+
async def _produce(self) -> MockMessageA:
|
|
339
|
+
return MockMessageA()
|
|
340
|
+
|
|
341
341
|
|
|
342
|
+
class MockGeneratorPassthroughTransformer(BaseTransformer[MockSettings, MockMessageA, MockMessageA]):
|
|
343
|
+
"""A mock transformer replacing the legacy unprimed generator."""
|
|
342
344
|
|
|
343
|
-
def
|
|
344
|
-
|
|
345
|
-
output = MockMessageA()
|
|
346
|
-
while True:
|
|
347
|
-
input = yield output
|
|
348
|
-
output = input or output
|
|
345
|
+
def _process(self, message: MockMessageA) -> MockMessageA:
|
|
346
|
+
return message or MockMessageA()
|
|
349
347
|
|
|
350
348
|
|
|
351
349
|
class MockGeneratorCompositeProducer(CompositeProducer[MockSettings, MockMessageB]):
|
|
352
350
|
@staticmethod
|
|
353
351
|
def _initialize_processors(settings):
|
|
354
352
|
return {
|
|
355
|
-
"generator":
|
|
356
|
-
"
|
|
353
|
+
"generator": MockGeneratorProducer(settings=settings),
|
|
354
|
+
"mock_generator_passthrough": MockGeneratorPassthroughTransformer(settings=settings),
|
|
357
355
|
"stateful_processor": MockStatefulProcessor(settings=settings),
|
|
358
356
|
}
|
|
359
357
|
|
|
@@ -7,6 +7,7 @@ from ezmsg.sigproc.math.clip import ClipSettings, ClipTransformer
|
|
|
7
7
|
from ezmsg.sigproc.math.difference import ConstDifferenceSettings, ConstDifferenceTransformer
|
|
8
8
|
from ezmsg.sigproc.math.invert import InvertTransformer
|
|
9
9
|
from ezmsg.sigproc.math.log import LogSettings, LogTransformer
|
|
10
|
+
from ezmsg.sigproc.math.pow import PowSettings, PowTransformer
|
|
10
11
|
from ezmsg.sigproc.math.scale import ScaleSettings, ScaleTransformer
|
|
11
12
|
|
|
12
13
|
|
|
@@ -85,3 +86,17 @@ def test_scale(scale_factor: float):
|
|
|
85
86
|
|
|
86
87
|
assert msg_out.data.shape == (n_times, n_chans)
|
|
87
88
|
assert np.array_equal(msg_out.data, in_dat * scale_factor)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.mark.parametrize("exponent", [0.5, 2.0, 3.0])
|
|
92
|
+
def test_pow(exponent: float):
|
|
93
|
+
n_times = 130
|
|
94
|
+
n_chans = 255
|
|
95
|
+
in_dat = np.abs(np.arange(n_times * n_chans).reshape(n_times, n_chans)).astype(float) + 1.0
|
|
96
|
+
msg_in = AxisArray(in_dat, dims=["time", "ch"])
|
|
97
|
+
|
|
98
|
+
xformer = PowTransformer(PowSettings(exponent=exponent))
|
|
99
|
+
msg_out = xformer(msg_in)
|
|
100
|
+
|
|
101
|
+
assert msg_out.data.shape == (n_times, n_chans)
|
|
102
|
+
assert np.allclose(msg_out.data, in_dat**exponent)
|
|
@@ -10,7 +10,8 @@ from frozendict import frozendict
|
|
|
10
10
|
from ezmsg.sigproc.scaler import (
|
|
11
11
|
AdaptiveStandardScalerSettings,
|
|
12
12
|
AdaptiveStandardScalerTransformer,
|
|
13
|
-
|
|
13
|
+
RiverAdaptiveStandardScalerSettings,
|
|
14
|
+
RiverAdaptiveStandardScalerTransformer,
|
|
14
15
|
)
|
|
15
16
|
from tests.helpers.util import assert_messages_equal
|
|
16
17
|
|
|
@@ -39,8 +40,10 @@ def test_adaptive_standard_scaler_river(fixture_arrays):
|
|
|
39
40
|
# The River example used alpha = 0.6
|
|
40
41
|
# tau = -gain / np.log(1 - alpha) and here we're using gain = 0.01
|
|
41
42
|
tau = 0.010913566679372915
|
|
42
|
-
_scaler =
|
|
43
|
-
|
|
43
|
+
_scaler = RiverAdaptiveStandardScalerTransformer(
|
|
44
|
+
settings=RiverAdaptiveStandardScalerSettings(time_constant=tau, axis="time")
|
|
45
|
+
)
|
|
46
|
+
output = _scaler(test_input)
|
|
44
47
|
assert np.allclose(output.data[0], expected_result, atol=1e-3)
|
|
45
48
|
assert_messages_equal([test_input], backup)
|
|
46
49
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from ezmsg.util.messages.axisarray import AxisArray
|
|
3
|
+
|
|
4
|
+
from ezmsg.sigproc.butterworthfilter import ButterworthFilterSettings
|
|
5
|
+
from ezmsg.sigproc.downsample import DownsampleSettings
|
|
6
|
+
from ezmsg.sigproc.singlebandpow import (
|
|
7
|
+
RMSBandPowerSettings,
|
|
8
|
+
RMSBandPowerTransformer,
|
|
9
|
+
SquareLawBandPowerSettings,
|
|
10
|
+
SquareLawBandPowerTransformer,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _make_sinusoid(
|
|
15
|
+
freq: float = 50.0,
|
|
16
|
+
amplitude: float = 1.0,
|
|
17
|
+
fs: float = 1000.0,
|
|
18
|
+
duration: float = 2.0,
|
|
19
|
+
n_channels: int = 2,
|
|
20
|
+
) -> AxisArray:
|
|
21
|
+
"""Generate a multi-channel sinusoid as an AxisArray."""
|
|
22
|
+
n_samples = int(fs * duration)
|
|
23
|
+
t = np.arange(n_samples) / fs
|
|
24
|
+
signal = amplitude * np.sin(2 * np.pi * freq * t)
|
|
25
|
+
data = np.column_stack([signal] * n_channels)
|
|
26
|
+
return AxisArray(
|
|
27
|
+
data,
|
|
28
|
+
dims=["time", "ch"],
|
|
29
|
+
axes={"time": AxisArray.LinearAxis(gain=1.0 / fs, offset=0.0)},
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_rms_bandpower():
|
|
34
|
+
"""RMS band power of a sinusoid should approximate A / sqrt(2)."""
|
|
35
|
+
freq = 50.0
|
|
36
|
+
amplitude = 2.0
|
|
37
|
+
fs = 1000.0
|
|
38
|
+
duration = 2.0
|
|
39
|
+
bin_duration = 0.1
|
|
40
|
+
n_channels = 2
|
|
41
|
+
|
|
42
|
+
msg_in = _make_sinusoid(freq=freq, amplitude=amplitude, fs=fs, duration=duration, n_channels=n_channels)
|
|
43
|
+
|
|
44
|
+
xformer = RMSBandPowerTransformer(
|
|
45
|
+
RMSBandPowerSettings(
|
|
46
|
+
bandpass=ButterworthFilterSettings(order=4, coef_type="sos", cuton=30.0, cutoff=70.0),
|
|
47
|
+
bin_duration=bin_duration,
|
|
48
|
+
apply_sqrt=True,
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Process in chunks to exercise stateful behavior
|
|
53
|
+
chunk_size = 100
|
|
54
|
+
outputs = []
|
|
55
|
+
for i in range(0, msg_in.data.shape[0], chunk_size):
|
|
56
|
+
chunk_data = msg_in.data[i : i + chunk_size]
|
|
57
|
+
chunk = AxisArray(
|
|
58
|
+
chunk_data,
|
|
59
|
+
dims=["time", "ch"],
|
|
60
|
+
axes={"time": AxisArray.LinearAxis(gain=1.0 / fs, offset=i / fs)},
|
|
61
|
+
)
|
|
62
|
+
result = xformer(chunk)
|
|
63
|
+
if result.data.size > 0:
|
|
64
|
+
outputs.append(result)
|
|
65
|
+
|
|
66
|
+
assert len(outputs) > 0
|
|
67
|
+
|
|
68
|
+
all_data = np.concatenate([o.data for o in outputs], axis=0)
|
|
69
|
+
|
|
70
|
+
# Output should have dims (time, ch)
|
|
71
|
+
assert all_data.ndim == 2
|
|
72
|
+
assert all_data.shape[1] == n_channels
|
|
73
|
+
|
|
74
|
+
# Check the output axis is "time" (renamed from "bin")
|
|
75
|
+
assert "time" in outputs[-1].dims
|
|
76
|
+
|
|
77
|
+
# After the filter settles, RMS of sinusoid should be ~ A / sqrt(2)
|
|
78
|
+
expected_rms = amplitude / np.sqrt(2)
|
|
79
|
+
# Use the second half of the output to let the filter settle
|
|
80
|
+
settled = all_data[all_data.shape[0] // 2 :]
|
|
81
|
+
mean_rms = np.mean(settled)
|
|
82
|
+
assert abs(mean_rms - expected_rms) < 0.15 * expected_rms, f"Expected RMS ~{expected_rms:.3f}, got {mean_rms:.3f}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_rms_bandpower_no_sqrt():
|
|
86
|
+
"""With apply_sqrt=False, output should be mean-square power ~ A^2 / 2."""
|
|
87
|
+
freq = 50.0
|
|
88
|
+
amplitude = 2.0
|
|
89
|
+
fs = 1000.0
|
|
90
|
+
duration = 2.0
|
|
91
|
+
bin_duration = 0.1
|
|
92
|
+
|
|
93
|
+
msg_in = _make_sinusoid(freq=freq, amplitude=amplitude, fs=fs, duration=duration, n_channels=1)
|
|
94
|
+
|
|
95
|
+
xformer = RMSBandPowerTransformer(
|
|
96
|
+
RMSBandPowerSettings(
|
|
97
|
+
bandpass=ButterworthFilterSettings(order=4, coef_type="sos", cuton=30.0, cutoff=70.0),
|
|
98
|
+
bin_duration=bin_duration,
|
|
99
|
+
apply_sqrt=False,
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
chunk_size = 100
|
|
104
|
+
outputs = []
|
|
105
|
+
for i in range(0, msg_in.data.shape[0], chunk_size):
|
|
106
|
+
chunk_data = msg_in.data[i : i + chunk_size]
|
|
107
|
+
chunk = AxisArray(
|
|
108
|
+
chunk_data,
|
|
109
|
+
dims=["time", "ch"],
|
|
110
|
+
axes={"time": AxisArray.LinearAxis(gain=1.0 / fs, offset=i / fs)},
|
|
111
|
+
)
|
|
112
|
+
result = xformer(chunk)
|
|
113
|
+
if result.data.size > 0:
|
|
114
|
+
outputs.append(result)
|
|
115
|
+
|
|
116
|
+
assert len(outputs) > 0
|
|
117
|
+
|
|
118
|
+
all_data = np.concatenate([o.data for o in outputs], axis=0)
|
|
119
|
+
|
|
120
|
+
# Mean-square power of sinusoid: A^2 / 2
|
|
121
|
+
expected_ms = amplitude**2 / 2
|
|
122
|
+
settled = all_data[all_data.shape[0] // 2 :]
|
|
123
|
+
mean_ms = np.mean(settled)
|
|
124
|
+
assert (
|
|
125
|
+
abs(mean_ms - expected_ms) < 0.15 * expected_ms
|
|
126
|
+
), f"Expected mean-square ~{expected_ms:.3f}, got {mean_ms:.3f}"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_squarelaw_bandpower():
|
|
130
|
+
"""Square-law band power should track signal power and downsample correctly."""
|
|
131
|
+
freq = 50.0
|
|
132
|
+
amplitude = 3.0
|
|
133
|
+
fs = 1000.0
|
|
134
|
+
duration = 2.0
|
|
135
|
+
target_rate = 100.0
|
|
136
|
+
n_channels = 2
|
|
137
|
+
|
|
138
|
+
msg_in = _make_sinusoid(freq=freq, amplitude=amplitude, fs=fs, duration=duration, n_channels=n_channels)
|
|
139
|
+
|
|
140
|
+
xformer = SquareLawBandPowerTransformer(
|
|
141
|
+
SquareLawBandPowerSettings(
|
|
142
|
+
bandpass=ButterworthFilterSettings(order=4, coef_type="sos", cuton=30.0, cutoff=70.0),
|
|
143
|
+
lowpass=ButterworthFilterSettings(order=4, coef_type="sos", cutoff=10.0),
|
|
144
|
+
downsample=DownsampleSettings(target_rate=target_rate),
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
chunk_size = 100
|
|
149
|
+
outputs = []
|
|
150
|
+
for i in range(0, msg_in.data.shape[0], chunk_size):
|
|
151
|
+
chunk_data = msg_in.data[i : i + chunk_size]
|
|
152
|
+
chunk = AxisArray(
|
|
153
|
+
chunk_data,
|
|
154
|
+
dims=["time", "ch"],
|
|
155
|
+
axes={"time": AxisArray.LinearAxis(gain=1.0 / fs, offset=i / fs)},
|
|
156
|
+
)
|
|
157
|
+
result = xformer(chunk)
|
|
158
|
+
if result.data.size > 0:
|
|
159
|
+
outputs.append(result)
|
|
160
|
+
|
|
161
|
+
assert len(outputs) > 0
|
|
162
|
+
|
|
163
|
+
all_data = np.concatenate([o.data for o in outputs], axis=0)
|
|
164
|
+
|
|
165
|
+
# Output should have dims (time, ch) and be downsampled
|
|
166
|
+
assert all_data.ndim == 2
|
|
167
|
+
assert all_data.shape[1] == n_channels
|
|
168
|
+
|
|
169
|
+
# Check output rate: should be approximately target_rate
|
|
170
|
+
out_axis = outputs[-1].get_axis("time")
|
|
171
|
+
out_rate = 1.0 / out_axis.gain
|
|
172
|
+
assert abs(out_rate - target_rate) < 1.0, f"Expected rate ~{target_rate}, got {out_rate}"
|
|
173
|
+
|
|
174
|
+
# After settling, the mean power should track A^2/2
|
|
175
|
+
expected_ms = amplitude**2 / 2
|
|
176
|
+
settled = all_data[all_data.shape[0] // 2 :]
|
|
177
|
+
mean_power = np.mean(settled)
|
|
178
|
+
assert (
|
|
179
|
+
abs(mean_power - expected_ms) < 0.25 * expected_ms
|
|
180
|
+
), f"Expected power ~{expected_ms:.3f}, got {mean_power:.3f}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|