ezmsg-sigproc 2.10.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.
Files changed (167) hide show
  1. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/PKG-INFO +1 -1
  2. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/__version__.py +2 -2
  3. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/affinetransform.py +21 -8
  4. ezmsg_sigproc-2.12.0/src/ezmsg/sigproc/math/pow.py +43 -0
  5. ezmsg_sigproc-2.12.0/src/ezmsg/sigproc/merge.py +358 -0
  6. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/quantize.py +9 -8
  7. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/rollingscaler.py +28 -20
  8. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/scaler.py +65 -34
  9. ezmsg_sigproc-2.12.0/src/ezmsg/sigproc/singlebandpow.py +116 -0
  10. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_scaler_system.py +10 -6
  11. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_base.py +19 -21
  12. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_math.py +15 -0
  13. ezmsg_sigproc-2.12.0/tests/unit/test_merge.py +369 -0
  14. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_scaler.py +6 -3
  15. ezmsg_sigproc-2.12.0/tests/unit/test_singlebandpow.py +180 -0
  16. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/.github/workflows/docs.yml +0 -0
  17. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/.github/workflows/python-publish.yml +0 -0
  18. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/.github/workflows/python-tests.yml +0 -0
  19. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/.gitignore +0 -0
  20. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/.pre-commit-config.yaml +0 -0
  21. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/LICENSE +0 -0
  22. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/README.md +0 -0
  23. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/Makefile +0 -0
  24. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/make.bat +0 -0
  25. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/_templates/autosummary/module.rst +0 -0
  26. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/api/index.rst +0 -0
  27. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/conf.py +0 -0
  28. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/HybridBuffer.md +0 -0
  29. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/explanations/array_api.rst +0 -0
  30. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/explanations/sigproc.rst +0 -0
  31. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/how-tos/signalprocessing/adaptive.rst +0 -0
  32. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/how-tos/signalprocessing/checkpoint.rst +0 -0
  33. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/how-tos/signalprocessing/composite.rst +0 -0
  34. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/how-tos/signalprocessing/content-signalprocessing.rst +0 -0
  35. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/how-tos/signalprocessing/processor.rst +0 -0
  36. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/how-tos/signalprocessing/standalone.rst +0 -0
  37. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/how-tos/signalprocessing/stateful.rst +0 -0
  38. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/how-tos/signalprocessing/unit.rst +0 -0
  39. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/img/HybridBufferBasic.svg +0 -0
  40. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/img/HybridBufferOverflow.svg +0 -0
  41. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/sigproc/architecture.rst +0 -0
  42. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/sigproc/base.rst +0 -0
  43. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/sigproc/content-sigproc.rst +0 -0
  44. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/sigproc/processors.rst +0 -0
  45. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/sigproc/units.rst +0 -0
  46. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/guides/tutorials/signalprocessing.rst +0 -0
  47. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/docs/source/index.md +0 -0
  48. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/pyproject.toml +0 -0
  49. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/__init__.py +0 -0
  50. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/activation.py +0 -0
  51. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/adaptive_lattice_notch.py +0 -0
  52. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/aggregate.py +0 -0
  53. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/bandpower.py +0 -0
  54. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/base.py +0 -0
  55. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/butterworthfilter.py +0 -0
  56. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/butterworthzerophase.py +0 -0
  57. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/cheby.py +0 -0
  58. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/combfilter.py +0 -0
  59. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/coordinatespaces.py +0 -0
  60. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/decimate.py +0 -0
  61. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/denormalize.py +0 -0
  62. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/detrend.py +0 -0
  63. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/diff.py +0 -0
  64. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/downsample.py +0 -0
  65. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/ewma.py +0 -0
  66. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/ewmfilter.py +0 -0
  67. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/extract_axis.py +0 -0
  68. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/fbcca.py +0 -0
  69. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/filter.py +0 -0
  70. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/filterbank.py +0 -0
  71. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/filterbankdesign.py +0 -0
  72. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/fir_hilbert.py +0 -0
  73. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/fir_pmc.py +0 -0
  74. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/firfilter.py +0 -0
  75. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/gaussiansmoothing.py +0 -0
  76. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/kaiser.py +0 -0
  77. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/linear.py +0 -0
  78. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/__init__.py +0 -0
  79. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/abs.py +0 -0
  80. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/add.py +0 -0
  81. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/clip.py +0 -0
  82. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/difference.py +0 -0
  83. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/invert.py +0 -0
  84. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/log.py +0 -0
  85. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/math/scale.py +0 -0
  86. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/messages.py +0 -0
  87. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/resample.py +0 -0
  88. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/sampler.py +0 -0
  89. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/signalinjector.py +0 -0
  90. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/slicer.py +0 -0
  91. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/spectral.py +0 -0
  92. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/spectrogram.py +0 -0
  93. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/spectrum.py +0 -0
  94. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/transpose.py +0 -0
  95. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/util/__init__.py +0 -0
  96. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/util/asio.py +0 -0
  97. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/util/axisarray_buffer.py +0 -0
  98. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/util/buffer.py +0 -0
  99. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/util/message.py +0 -0
  100. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/util/profile.py +0 -0
  101. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/util/sparse.py +0 -0
  102. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/util/typeresolution.py +0 -0
  103. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/wavelets.py +0 -0
  104. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/src/ezmsg/sigproc/window.py +0 -0
  105. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/__init__.py +0 -0
  106. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/conftest.py +0 -0
  107. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/helpers/__init__.py +0 -0
  108. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/helpers/synth.py +0 -0
  109. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/helpers/util.py +0 -0
  110. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/bytewax/test_spectrum_bytewax.py +0 -0
  111. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/bytewax/test_window_bytewax.py +0 -0
  112. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_add_system.py +0 -0
  113. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_butterworth_system.py +0 -0
  114. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_butterworthzerophase_system.py +0 -0
  115. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_coordinatespaces_system.py +0 -0
  116. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_decimate_system.py +0 -0
  117. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_difference_system.py +0 -0
  118. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_downsample_system.py +0 -0
  119. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_filter_system.py +0 -0
  120. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_fir_hilbert_system.py +0 -0
  121. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_fir_pmc_system.py +0 -0
  122. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_rollingscaler_system.py +0 -0
  123. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_sampler_system.py +0 -0
  124. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_spectrum_system.py +0 -0
  125. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/integration/ezmsg/test_window_system.py +0 -0
  126. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/resources/xform.csv +0 -0
  127. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/test_profile.py +0 -0
  128. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/buffer/test_axisarray_buffer.py +0 -0
  129. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/buffer/test_buffer.py +0 -0
  130. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/buffer/test_buffer_overflow.py +0 -0
  131. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_activation.py +0 -0
  132. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_adaptive_lattice_notch.py +0 -0
  133. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_affine_transform.py +0 -0
  134. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_aggregate.py +0 -0
  135. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_bandpower.py +0 -0
  136. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_butter.py +0 -0
  137. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_butterworthzerophase.py +0 -0
  138. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_combfilter.py +0 -0
  139. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_coordinatespaces.py +0 -0
  140. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_denormalize.py +0 -0
  141. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_diff.py +0 -0
  142. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_downsample.py +0 -0
  143. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_ewma.py +0 -0
  144. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_extract_axis.py +0 -0
  145. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_fbcca.py +0 -0
  146. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_filter.py +0 -0
  147. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_filterbank.py +0 -0
  148. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_filterbankdesign.py +0 -0
  149. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_fir_hilbert.py +0 -0
  150. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_fir_pmc.py +0 -0
  151. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_firfilter.py +0 -0
  152. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_gaussian_smoothing_filter.py +0 -0
  153. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_kaiser.py +0 -0
  154. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_linear.py +0 -0
  155. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_math_add.py +0 -0
  156. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_math_difference.py +0 -0
  157. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_quantize.py +0 -0
  158. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_resample.py +0 -0
  159. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_rollingscaler.py +0 -0
  160. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_sampler.py +0 -0
  161. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_slicer.py +0 -0
  162. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_spectrogram.py +0 -0
  163. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_spectrum.py +0 -0
  164. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_transpose.py +0 -0
  165. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_util.py +0 -0
  166. {ezmsg_sigproc-2.10.0 → ezmsg_sigproc-2.12.0}/tests/unit/test_wavelets.py +0 -0
  167. {ezmsg_sigproc-2.10.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.10.0
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.10.0'
32
- __version_tuple__ = version_tuple = (2, 10, 0)
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 = np.concatenate((data, np.ones(sample_shape).astype(data.dtype)), axis=axis_idx)
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 = np.matmul(data, self._state.weights)
133
+ data = xp.matmul(data, self._state.weights)
127
134
  else:
128
- data = np.moveaxis(data, axis_idx, -1)
129
- data = np.matmul(data, self._state.weights)
130
- data = np.moveaxis(data, -1, axis_idx)
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: npt.NDArray, **ignore_kwargs) -> npt.NDArray:
165
- return np.zeros_like(data)
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": np.mean, "median": np.median, "passthrough": zeros_for_noop}[self.settings.mode]
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,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))
@@ -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
- import numpy as np
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 = np.uint8
45
+ dtype = xp.uint8
45
46
  elif self.settings.bits <= 16:
46
- dtype = np.uint16
47
+ dtype = xp.uint16
47
48
  elif self.settings.bits <= 32:
48
- dtype = np.uint32
49
+ dtype = xp.uint32
49
50
  else:
50
- dtype = np.uint64
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.clip(self.settings.min_val, clip_max)
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 = np.rint(scale_factor * data).astype(dtype)
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 = np.zeros(ch)
117
+ self._state.mean = xp.zeros(ch, dtype=xp.float64)
116
118
  self._state.N = 0
117
- self._state.M2 = np.zeros(ch)
119
+ self._state.M2 = xp.zeros(ch, dtype=xp.float64)
118
120
  self._state.k_samples = (
119
- int(np.ceil(self.settings.window_size / message.axes[self.settings.axis].gain))
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
- int(np.ceil(self.settings.min_seconds / message.axes[self.settings.axis].gain))
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
- x = np.asarray(x, dtype=np.float64)
141
+ xp = get_namespace(x)
142
+ x = xp.asarray(x, dtype=xp.float64)
140
143
  n_b = x.shape[0]
141
- mean_b = np.mean(x, axis=0)
142
- M2_b = np.sum((x - mean_b) ** 2, axis=0)
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 = np.zeros_like(self._state.mean)
152
- self._state.M2 = np.zeros_like(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
- std = np.maximum(np.sqrt(varis), 1e-8)
179
- z = np.abs((x - self._state.mean) / std)
180
- mask = np.any(z > self.settings.artifact_z_thresh, axis=1)
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
- std = np.maximum(np.sqrt(varis), 1e-8)
188
- with np.errstate(divide="ignore", invalid="ignore"):
189
- result = (message.data - self._state.mean) / std
190
- result = np.nan_to_num(result, nan=0.0, posinf=0.0, neginf=0.0)
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 = np.clip(result, -self.settings.clip, self.settings.clip)
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 = np.abs((x - self._state.mean) / std)
198
- mask = np.any(z_scores > self.settings.artifact_z_thresh, axis=1)
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)