ezmsg-sigproc 2.8.0__tar.gz → 2.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/PKG-INFO +1 -1
  2. ezmsg_sigproc-2.10.0/docs/source/guides/explanations/array_api.rst +156 -0
  3. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/sigproc/content-sigproc.rst +3 -1
  4. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/tutorials/signalprocessing.rst +5 -24
  5. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/__version__.py +2 -2
  6. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/aggregate.py +9 -0
  7. ezmsg_sigproc-2.10.0/src/ezmsg/sigproc/butterworthzerophase.py +305 -0
  8. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/coordinatespaces.py +22 -5
  9. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/diff.py +15 -4
  10. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/linear.py +7 -5
  11. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/math/abs.py +11 -6
  12. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/math/add.py +1 -2
  13. ezmsg_sigproc-2.10.0/src/ezmsg/sigproc/math/clip.py +48 -0
  14. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/math/difference.py +9 -3
  15. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/math/invert.py +8 -3
  16. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/math/log.py +19 -8
  17. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/math/scale.py +8 -3
  18. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/transpose.py +22 -7
  19. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_add_system.py +2 -2
  20. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_butterworthzerophase_system.py +34 -6
  21. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_difference_system.py +3 -3
  22. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_sampler_system.py +1 -1
  23. ezmsg_sigproc-2.10.0/tests/unit/test_butterworthzerophase.py +545 -0
  24. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_math.py +6 -6
  25. ezmsg_sigproc-2.8.0/src/ezmsg/sigproc/butterworthzerophase.py +0 -123
  26. ezmsg_sigproc-2.8.0/src/ezmsg/sigproc/math/clip.py +0 -43
  27. ezmsg_sigproc-2.8.0/tests/unit/test_butterworthzerophase.py +0 -164
  28. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/.github/workflows/docs.yml +0 -0
  29. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/.github/workflows/python-publish.yml +0 -0
  30. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/.github/workflows/python-tests.yml +0 -0
  31. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/.gitignore +0 -0
  32. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/.pre-commit-config.yaml +0 -0
  33. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/LICENSE +0 -0
  34. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/README.md +0 -0
  35. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/Makefile +0 -0
  36. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/make.bat +0 -0
  37. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/_templates/autosummary/module.rst +0 -0
  38. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/api/index.rst +0 -0
  39. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/conf.py +0 -0
  40. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/HybridBuffer.md +0 -0
  41. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/explanations/sigproc.rst +0 -0
  42. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/how-tos/signalprocessing/adaptive.rst +0 -0
  43. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/how-tos/signalprocessing/checkpoint.rst +0 -0
  44. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/how-tos/signalprocessing/composite.rst +0 -0
  45. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/how-tos/signalprocessing/content-signalprocessing.rst +0 -0
  46. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/how-tos/signalprocessing/processor.rst +0 -0
  47. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/how-tos/signalprocessing/standalone.rst +0 -0
  48. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/how-tos/signalprocessing/stateful.rst +0 -0
  49. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/how-tos/signalprocessing/unit.rst +0 -0
  50. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/img/HybridBufferBasic.svg +0 -0
  51. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/img/HybridBufferOverflow.svg +0 -0
  52. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/sigproc/architecture.rst +0 -0
  53. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/sigproc/base.rst +0 -0
  54. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/sigproc/processors.rst +0 -0
  55. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/guides/sigproc/units.rst +0 -0
  56. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/docs/source/index.md +2 -2
  57. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/pyproject.toml +0 -0
  58. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/__init__.py +0 -0
  59. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/activation.py +0 -0
  60. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/adaptive_lattice_notch.py +0 -0
  61. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/affinetransform.py +0 -0
  62. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/bandpower.py +0 -0
  63. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/base.py +0 -0
  64. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/butterworthfilter.py +0 -0
  65. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/cheby.py +0 -0
  66. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/combfilter.py +0 -0
  67. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/decimate.py +0 -0
  68. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/denormalize.py +0 -0
  69. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/detrend.py +0 -0
  70. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/downsample.py +0 -0
  71. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/ewma.py +0 -0
  72. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/ewmfilter.py +0 -0
  73. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/extract_axis.py +0 -0
  74. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/fbcca.py +0 -0
  75. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/filter.py +0 -0
  76. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/filterbank.py +0 -0
  77. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/filterbankdesign.py +0 -0
  78. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/fir_hilbert.py +0 -0
  79. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/fir_pmc.py +0 -0
  80. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/firfilter.py +0 -0
  81. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/gaussiansmoothing.py +0 -0
  82. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/kaiser.py +0 -0
  83. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/math/__init__.py +0 -0
  84. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/messages.py +0 -0
  85. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/quantize.py +0 -0
  86. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/resample.py +0 -0
  87. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/rollingscaler.py +0 -0
  88. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/sampler.py +0 -0
  89. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/scaler.py +0 -0
  90. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/signalinjector.py +0 -0
  91. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/slicer.py +0 -0
  92. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/spectral.py +0 -0
  93. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/spectrogram.py +0 -0
  94. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/spectrum.py +0 -0
  95. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/util/__init__.py +0 -0
  96. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/util/asio.py +0 -0
  97. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/util/axisarray_buffer.py +0 -0
  98. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/util/buffer.py +0 -0
  99. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/util/message.py +0 -0
  100. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/util/profile.py +0 -0
  101. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/util/sparse.py +0 -0
  102. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/util/typeresolution.py +0 -0
  103. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/wavelets.py +0 -0
  104. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/src/ezmsg/sigproc/window.py +0 -0
  105. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/__init__.py +0 -0
  106. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/conftest.py +0 -0
  107. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/helpers/__init__.py +0 -0
  108. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/helpers/synth.py +0 -0
  109. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/helpers/util.py +0 -0
  110. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/bytewax/test_spectrum_bytewax.py +0 -0
  111. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/bytewax/test_window_bytewax.py +0 -0
  112. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_butterworth_system.py +0 -0
  113. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_coordinatespaces_system.py +0 -0
  114. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_decimate_system.py +0 -0
  115. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_downsample_system.py +0 -0
  116. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_filter_system.py +0 -0
  117. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_fir_hilbert_system.py +0 -0
  118. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_fir_pmc_system.py +0 -0
  119. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_rollingscaler_system.py +0 -0
  120. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_scaler_system.py +0 -0
  121. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_spectrum_system.py +0 -0
  122. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/integration/ezmsg/test_window_system.py +0 -0
  123. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/resources/xform.csv +0 -0
  124. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/test_profile.py +0 -0
  125. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/buffer/test_axisarray_buffer.py +0 -0
  126. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/buffer/test_buffer.py +0 -0
  127. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/buffer/test_buffer_overflow.py +0 -0
  128. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_activation.py +0 -0
  129. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_adaptive_lattice_notch.py +0 -0
  130. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_affine_transform.py +0 -0
  131. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_aggregate.py +0 -0
  132. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_bandpower.py +0 -0
  133. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_base.py +0 -0
  134. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_butter.py +0 -0
  135. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_combfilter.py +0 -0
  136. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_coordinatespaces.py +0 -0
  137. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_denormalize.py +0 -0
  138. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_diff.py +0 -0
  139. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_downsample.py +0 -0
  140. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_ewma.py +0 -0
  141. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_extract_axis.py +0 -0
  142. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_fbcca.py +0 -0
  143. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_filter.py +0 -0
  144. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_filterbank.py +0 -0
  145. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_filterbankdesign.py +0 -0
  146. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_fir_hilbert.py +0 -0
  147. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_fir_pmc.py +0 -0
  148. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_firfilter.py +0 -0
  149. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_gaussian_smoothing_filter.py +0 -0
  150. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_kaiser.py +0 -0
  151. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_linear.py +0 -0
  152. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_math_add.py +0 -0
  153. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_math_difference.py +0 -0
  154. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_quantize.py +0 -0
  155. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_resample.py +0 -0
  156. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_rollingscaler.py +0 -0
  157. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_sampler.py +0 -0
  158. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_scaler.py +0 -0
  159. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_slicer.py +0 -0
  160. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_spectrogram.py +0 -0
  161. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_spectrum.py +0 -0
  162. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_transpose.py +0 -0
  163. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_util.py +0 -0
  164. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_wavelets.py +0 -0
  165. {ezmsg_sigproc-2.8.0 → ezmsg_sigproc-2.10.0}/tests/unit/test_window.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ezmsg-sigproc
3
- Version: 2.8.0
3
+ Version: 2.10.0
4
4
  Summary: Timeseries signal processing implementations in ezmsg
5
5
  Author-email: Griffin Milsap <griffin.milsap@gmail.com>, Preston Peranich <pperanich@gmail.com>, Chadwick Boulay <chadwick.boulay@gmail.com>, Kyle McGraw <kmcgraw@blackrockneuro.com>
6
6
  License-Expression: MIT
@@ -0,0 +1,156 @@
1
+ Array API Support
2
+ =================
3
+
4
+ ezmsg-sigproc provides support for the `Python Array API standard
5
+ <https://data-apis.org/array-api/>`_, enabling many transformers to work with
6
+ arrays from different backends such as NumPy, CuPy, PyTorch, and JAX.
7
+
8
+ What is the Array API?
9
+ ----------------------
10
+
11
+ The Array API is a standardized interface for array operations across different
12
+ Python array libraries. By coding to this standard, ezmsg-sigproc transformers
13
+ can process data regardless of which array library created it, enabling:
14
+
15
+ - **GPU acceleration** via CuPy or PyTorch tensors
16
+ - **Framework interoperability** for integration with ML pipelines
17
+ - **Hardware flexibility** without code changes
18
+
19
+ How It Works
20
+ ------------
21
+
22
+ Compatible transformers use `array-api-compat <https://github.com/data-apis/array-api-compat>`_
23
+ to detect the input array's namespace and use the appropriate operations:
24
+
25
+ .. code-block:: python
26
+
27
+ from array_api_compat import get_namespace
28
+
29
+ def _process(self, message: AxisArray) -> AxisArray:
30
+ xp = get_namespace(message.data) # numpy, cupy, torch, etc.
31
+ result = xp.abs(message.data) # Uses the correct backend
32
+ return replace(message, data=result)
33
+
34
+ Usage Example
35
+ -------------
36
+
37
+ Using Array API compatible transformers with CuPy for GPU acceleration:
38
+
39
+ .. code-block:: python
40
+
41
+ import cupy as cp
42
+ from ezmsg.util.messages.axisarray import AxisArray
43
+ from ezmsg.sigproc.math.abs import AbsTransformer
44
+ from ezmsg.sigproc.math.clip import ClipTransformer, ClipSettings
45
+
46
+ # Create data on GPU
47
+ gpu_data = cp.random.randn(1000, 64).astype(cp.float32)
48
+ message = AxisArray(gpu_data, dims=["time", "ch"])
49
+
50
+ # Process entirely on GPU - no data transfer!
51
+ abs_transformer = AbsTransformer()
52
+ clip_transformer = ClipTransformer(ClipSettings(min=0.0, max=1.0))
53
+
54
+ result = clip_transformer(abs_transformer(message))
55
+ # result.data is still a CuPy array on GPU
56
+
57
+ Compatible Modules
58
+ ------------------
59
+
60
+ The following transformers fully support the Array API standard:
61
+
62
+ Math Operations
63
+ ^^^^^^^^^^^^^^^
64
+
65
+ .. list-table::
66
+ :header-rows: 1
67
+ :widths: 30 70
68
+
69
+ * - Module
70
+ - Description
71
+ * - :mod:`ezmsg.sigproc.math.abs`
72
+ - Absolute value
73
+ * - :mod:`ezmsg.sigproc.math.clip`
74
+ - Clip values to a range
75
+ * - :mod:`ezmsg.sigproc.math.log`
76
+ - Logarithm with configurable base
77
+ * - :mod:`ezmsg.sigproc.math.scale`
78
+ - Multiply by a constant
79
+ * - :mod:`ezmsg.sigproc.math.invert`
80
+ - Compute 1/x
81
+ * - :mod:`ezmsg.sigproc.math.difference`
82
+ - Subtract a constant (ConstDifferenceTransformer)
83
+
84
+ Signal Processing
85
+ ^^^^^^^^^^^^^^^^^
86
+
87
+ .. list-table::
88
+ :header-rows: 1
89
+ :widths: 30 70
90
+
91
+ * - Module
92
+ - Description
93
+ * - :mod:`ezmsg.sigproc.diff`
94
+ - Compute differences along an axis
95
+ * - :mod:`ezmsg.sigproc.transpose`
96
+ - Transpose/permute array dimensions
97
+ * - :mod:`ezmsg.sigproc.linear`
98
+ - Per-channel linear transform (scale + offset)
99
+ * - :mod:`ezmsg.sigproc.aggregate`
100
+ - Aggregate operations (AggregateTransformer only)
101
+
102
+ Coordinate Transforms
103
+ ^^^^^^^^^^^^^^^^^^^^^
104
+
105
+ .. list-table::
106
+ :header-rows: 1
107
+ :widths: 30 70
108
+
109
+ * - Module
110
+ - Description
111
+ * - :mod:`ezmsg.sigproc.coordinatespaces`
112
+ - Cartesian/polar coordinate conversions
113
+
114
+ Limitations
115
+ -----------
116
+
117
+ Some operations remain NumPy-only due to lack of Array API equivalents:
118
+
119
+ - **Random number generation**: Modules using ``np.random`` (e.g., ``denormalize``)
120
+ - **SciPy operations**: Filtering (``scipy.signal.lfilter``), FFT, wavelets
121
+ - **Advanced indexing**: Some slicing operations for metadata handling
122
+ - **Memory layout**: ``np.require`` for contiguous array optimization (NumPy only)
123
+
124
+ Metadata arrays (axis labels, coordinates) typically remain as NumPy arrays
125
+ since they are not performance-critical.
126
+
127
+ Adding Array API Support
128
+ ------------------------
129
+
130
+ When contributing new transformers, follow this pattern:
131
+
132
+ .. code-block:: python
133
+
134
+ from array_api_compat import get_namespace
135
+ from ezmsg.baseproc import BaseTransformer
136
+ from ezmsg.util.messages.axisarray import AxisArray
137
+ from ezmsg.util.messages.util import replace
138
+
139
+ class MyTransformer(BaseTransformer[MySettings, AxisArray, AxisArray]):
140
+ def _process(self, message: AxisArray) -> AxisArray:
141
+ xp = get_namespace(message.data)
142
+
143
+ # Use xp instead of np for array operations
144
+ result = xp.sqrt(xp.abs(message.data))
145
+
146
+ return replace(message, data=result)
147
+
148
+ Key guidelines:
149
+
150
+ 1. Call ``get_namespace(message.data)`` at the start of ``_process``
151
+ 2. Use ``xp.function_name`` instead of ``np.function_name``
152
+ 3. Note that some functions have different names:
153
+ - ``np.concatenate`` → ``xp.concat``
154
+ - ``np.transpose`` → ``xp.permute_dims``
155
+ 4. Keep metadata operations (axis labels, etc.) as NumPy
156
+ 5. Use in-place operations (``/=``, ``*=``) where possible for efficiency
@@ -4,7 +4,8 @@ ezmsg-sigproc
4
4
  Timeseries signal processing implementations in ezmsg, leveraging numpy and scipy.
5
5
  Most of the methods and classes in this extension are intended to be used in building signal processing pipelines.
6
6
  They use :class:`ezmsg.util.messages.axisarray.AxisArray` as the primary data structure for passing signals between components.
7
- The message's data are expected to be a numpy array.
7
+ The message's data are typically NumPy arrays, though many transformers support the
8
+ :doc:`Array API standard <../explanations/array_api>` for use with CuPy, PyTorch, and other backends.
8
9
 
9
10
  .. note:: Some generators might yield valid :class:`AxisArray` messages with ``.data`` size of 0.
10
11
  This may occur when the generator receives inadequate data to produce a valid output, such as when windowing or buffering.
@@ -21,3 +22,4 @@ This may occur when the generator receives inadequate data to produce a valid ou
21
22
  base
22
23
  units
23
24
  processors
25
+ ../explanations/array_api
@@ -109,7 +109,7 @@ First, we need to install the `ezmsg-sigproc` package if we haven't already. Thi
109
109
 
110
110
  .. code-block:: bash
111
111
 
112
- pip install "ezmsg[sigproc]"
112
+ pip install "ezmsg-sigproc"
113
113
 
114
114
 
115
115
  |ezmsg_logo_small| Creating the `Downsample` signal processor
@@ -237,7 +237,7 @@ The first two methods deal with the state of the processor (and are only require
237
237
 
238
238
  In order to implement these methods, we need to understand our preferred message format: `AxisArray`. This is a flexible and powerful class for handling multi-dimensional arrays with named axes, which is particularly useful for signal processing applications. I have already used `AxisArray` in our code as the input message and output message types.
239
239
 
240
- A detailed explanation of the `AxisArray` class is beyond the scope of this tutorial, but you can refer to the :doc:`AxisArray explainer <../explanations/axisarray>` as well as the :doc:`API reference <../reference/API/axisarray>` for more information.
240
+ A detailed explanation of the `AxisArray` class is beyond the scope of this tutorial, but you can refer to the `AxisArray explainer <https://www.ezmsg.org/explanations/axisarray.html>`_ as well as the `API reference <https://www.ezmsg.org/ezmsg/reference/API/axisarray.html>`_ for more information.
241
241
 
242
242
  Brief Aside on AxisArray
243
243
  =================================
@@ -555,25 +555,6 @@ In a separate Python file in the same directory, you can test the `DownsampleTra
555
555
 
556
556
  Doing the above is very handy for unit testing your processor as well as for offline processing of data.
557
557
 
558
- .. note:: The `downsample` module in `ezmsg-sigproc` has a utility function for creating a `DownsampleTransformer` instance with the desired settings:
559
-
560
- .. code-block:: python
561
-
562
- def downsample(
563
- axis: str = "time",
564
- target_rate: float | None = None,
565
- factor: int | None = None,
566
- ) -> DownsampleTransformer:
567
- return DownsampleTransformer(
568
- DownsampleSettings(axis=axis, target_rate=target_rate, factor=factor)
569
- )
570
-
571
- After importing this utility function, lines 8 and 9 in our code above could now read:
572
-
573
- .. code-block:: python
574
-
575
- downsampler = downsample(axis="time", target_rate=50)
576
-
577
558
  Of course, the real power of `ezmsg` comes from integrating your processor into an `ezmsg` Unit and using it in a processing pipeline. We will see how to do this next.
578
559
 
579
560
 
@@ -600,15 +581,15 @@ A lot of the behind-the-scenes work is done for you by the `BaseTransformerUnit`
600
581
  SETTINGS = DownsampleSettings
601
582
 
602
583
 
603
- Connecting it to other `Component`\ s and initialising the transformer are accomplished in the same way that we did in the :doc:`pipeline tutorial <pipeline>`.
584
+ Connecting it to other `Component`\ s and initialising the transformer are accomplished in the same way that we did in the `Pipeline Tutorial <https://www.ezmsg.org/tutorials/pipeline.html>`_
604
585
 
605
586
 
606
587
  |ezmsg_logo_small| See Also
607
588
  ************************************
608
589
 
609
590
  - `Further examples <https://github.com/ezmsg-org/ezmsg/tree/master/examples>`_ can be found in the examples directory in `ezmsg`. These are examples of creating and using `ezmsg` Units and pipelines.
610
- - `ezmsg-sigproc` has a large number of already implemented signal processors. More information can be found at the :doc:`ezmsg-sigproc reference <../extensions/sigproc/content-sigproc>`.
611
- - `Downsample` class reference
591
+ - `ezmsg-sigproc` has a large number of already implemented signal processors. More information can be found at the :doc:`ezmsg-sigproc reference <../sigproc/content-sigproc>`.
592
+ - :doc:`Downsample class reference <../../api/generated/ezmsg.sigproc.downsample>`
612
593
 
613
594
  .. |ezmsg_logo_small| image:: ../_static/_images/ezmsg_logo.png
614
595
  :width: 40
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '2.8.0'
32
- __version_tuple__ = version_tuple = (2, 8, 0)
31
+ __version__ = version = '2.10.0'
32
+ __version_tuple__ = version_tuple = (2, 10, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -1,3 +1,12 @@
1
+ """
2
+ Aggregation operations over arrays.
3
+
4
+ .. note::
5
+ :obj:`AggregateTransformer` supports the :doc:`Array API standard </guides/explanations/array_api>`,
6
+ enabling use with NumPy, CuPy, PyTorch, and other compatible array libraries.
7
+ :obj:`RangedAggregateTransformer` currently requires NumPy arrays.
8
+ """
9
+
1
10
  import typing
2
11
 
3
12
  import ezmsg.core as ez
@@ -0,0 +1,305 @@
1
+ """
2
+ Streaming zero-phase Butterworth filter implemented as a two-stage composite processor.
3
+
4
+ Stage 1: Forward causal Butterworth filter (from ezmsg.sigproc.butterworthfilter)
5
+ Stage 2: Backward acausal filter with buffering (ButterworthBackwardFilterTransformer)
6
+
7
+ The output is delayed by `pad_length` samples to ensure the backward pass has sufficient
8
+ future context. The pad_length is computed analytically using scipy's heuristic.
9
+ """
10
+
11
+ import functools
12
+ import typing
13
+
14
+ import numpy as np
15
+ import scipy.signal
16
+ from ezmsg.baseproc import BaseTransformerUnit
17
+ from ezmsg.baseproc.composite import CompositeProcessor
18
+ from ezmsg.util.messages.axisarray import AxisArray
19
+ from ezmsg.util.messages.util import replace
20
+
21
+ from .butterworthfilter import (
22
+ ButterworthFilterSettings,
23
+ ButterworthFilterTransformer,
24
+ butter_design_fun,
25
+ )
26
+ from .filter import BACoeffs, FilterByDesignTransformer, SOSCoeffs
27
+ from .util.axisarray_buffer import HybridAxisArrayBuffer
28
+
29
+
30
+ class ButterworthZeroPhaseSettings(ButterworthFilterSettings):
31
+ """
32
+ Settings for :obj:`ButterworthZeroPhase`.
33
+
34
+ This implements a streaming zero-phase Butterworth filter using forward-backward
35
+ filtering. The output is delayed by `pad_length` samples to ensure the backward
36
+ pass has sufficient future context.
37
+
38
+ The pad_length is computed by finding where the filter's impulse response decays
39
+ to `settle_cutoff` fraction of its peak value. This accounts for the filter's
40
+ actual time constant rather than just its order.
41
+ """
42
+
43
+ # Inherits from ButterworthFilterSettings:
44
+ # axis, coef_type, order, cuton, cutoff, wn_hz
45
+
46
+ settle_cutoff: float = 0.01
47
+ """
48
+ Fraction of peak impulse response used to determine settling time.
49
+ The pad_length is set to the number of samples until the impulse response
50
+ decays to this fraction of its peak. Default is 0.01 (1% of peak).
51
+ """
52
+
53
+ max_pad_duration: float | None = None
54
+ """
55
+ Maximum pad duration in seconds. If set, the pad_length will be capped
56
+ at this value times the sampling rate. Use this to limit latency for
57
+ filters with very long impulse responses. Default is None (no limit).
58
+ """
59
+
60
+
61
+ class ButterworthBackwardFilterTransformer(FilterByDesignTransformer[ButterworthFilterSettings, BACoeffs | SOSCoeffs]):
62
+ """
63
+ Backward (acausal) Butterworth filter with buffering.
64
+
65
+ This transformer buffers its input and applies the filter in reverse,
66
+ outputting only the "settled" portion where transients have decayed.
67
+ This introduces a lag of ``pad_length`` samples.
68
+
69
+ Intended to be used as stage 2 in a zero-phase filter pipeline, receiving
70
+ forward-filtered data from a ButterworthFilterTransformer.
71
+ """
72
+
73
+ # Instance attributes (initialized in _reset_state)
74
+ _buffer: HybridAxisArrayBuffer | None
75
+ _coefs_cache: BACoeffs | SOSCoeffs | None
76
+ _zi_tiled: np.ndarray | None
77
+ _pad_length: int
78
+
79
+ def get_design_function(
80
+ self,
81
+ ) -> typing.Callable[[float], BACoeffs | SOSCoeffs | None]:
82
+ return functools.partial(
83
+ butter_design_fun,
84
+ order=self.settings.order,
85
+ cuton=self.settings.cuton,
86
+ cutoff=self.settings.cutoff,
87
+ coef_type=self.settings.coef_type,
88
+ wn_hz=self.settings.wn_hz,
89
+ )
90
+
91
+ def _compute_pad_length(self, fs: float) -> int:
92
+ """
93
+ Compute pad length based on the filter's impulse response settling time.
94
+
95
+ The pad_length is determined by finding where the impulse response decays
96
+ to `settle_cutoff` fraction of its peak value. This is then optionally
97
+ capped by `max_pad_duration`.
98
+
99
+ Args:
100
+ fs: Sampling frequency in Hz.
101
+
102
+ Returns:
103
+ Number of samples for the pad length.
104
+ """
105
+ # Design the filter to compute impulse response
106
+ coefs = self.get_design_function()(fs)
107
+ if coefs is None:
108
+ # Filter design failed or is disabled
109
+ return 0
110
+
111
+ # Generate impulse response - use a generous length initially
112
+ # Start with scipy's heuristic as minimum, then extend if needed
113
+ if self.settings.coef_type == "ba":
114
+ min_length = 3 * (self.settings.order + 1)
115
+ else:
116
+ n_sections = (self.settings.order + 1) // 2
117
+ min_length = 3 * n_sections * 2
118
+
119
+ # Use 10x the minimum as initial impulse length, or at least 10000 samples
120
+ # (10000 samples allows for ~333ms at 30kHz, covering most practical cases)
121
+ impulse_length = max(min_length * 10, 10000)
122
+
123
+ # Cap impulse length computation if max_pad_duration is set
124
+ if self.settings.max_pad_duration is not None:
125
+ max_samples = int(self.settings.max_pad_duration * fs)
126
+ impulse_length = min(impulse_length, max_samples + 1)
127
+
128
+ impulse = np.zeros(impulse_length)
129
+ impulse[0] = 1.0
130
+
131
+ if self.settings.coef_type == "ba":
132
+ b, a = coefs
133
+ h = scipy.signal.lfilter(b, a, impulse)
134
+ else:
135
+ h = scipy.signal.sosfilt(coefs, impulse)
136
+
137
+ # Find where impulse response settles to settle_cutoff of peak
138
+ abs_h = np.abs(h)
139
+ peak = abs_h.max()
140
+ if peak == 0:
141
+ return min_length
142
+
143
+ threshold = self.settings.settle_cutoff * peak
144
+ above_threshold = np.where(abs_h > threshold)[0]
145
+
146
+ if len(above_threshold) == 0:
147
+ pad_length = min_length
148
+ else:
149
+ pad_length = above_threshold[-1] + 1
150
+
151
+ # Ensure at least the scipy heuristic minimum
152
+ pad_length = max(pad_length, min_length)
153
+
154
+ # Apply max_pad_duration cap if set
155
+ if self.settings.max_pad_duration is not None:
156
+ max_samples = int(self.settings.max_pad_duration * fs)
157
+ pad_length = min(pad_length, max_samples)
158
+
159
+ return pad_length
160
+
161
+ def _reset_state(self, message: AxisArray) -> None:
162
+ """Reset filter state when stream changes."""
163
+ self._coefs_cache = None
164
+ self._zi_tiled = None
165
+ self._buffer = None
166
+ # Compute pad_length based on the message's sampling rate
167
+ axis = message.dims[0] if self.settings.axis is None else self.settings.axis
168
+ fs = 1 / message.axes[axis].gain
169
+ self._pad_length = self._compute_pad_length(fs)
170
+ self.state.needs_redesign = True
171
+
172
+ def _compute_zi_tiled(self, data: np.ndarray, ax_idx: int) -> None:
173
+ """Compute and cache the tiled zi for the given data shape.
174
+
175
+ Called once per stream (or after filter redesign). The result is
176
+ broadcast-ready for multiplication by the edge sample on each chunk.
177
+ """
178
+ if self.settings.coef_type == "ba":
179
+ b, a = self._coefs_cache
180
+ zi_base = scipy.signal.lfilter_zi(b, a)
181
+ else: # sos
182
+ zi_base = scipy.signal.sosfilt_zi(self._coefs_cache)
183
+
184
+ n_tail = data.ndim - ax_idx - 1
185
+
186
+ if self.settings.coef_type == "ba":
187
+ zi_expand = (None,) * ax_idx + (slice(None),) + (None,) * n_tail
188
+ n_tile = data.shape[:ax_idx] + (1,) + data.shape[ax_idx + 1 :]
189
+ else: # sos
190
+ zi_expand = (slice(None),) + (None,) * ax_idx + (slice(None),) + (None,) * n_tail
191
+ n_tile = (1,) + data.shape[:ax_idx] + (1,) + data.shape[ax_idx + 1 :]
192
+
193
+ self._zi_tiled = np.tile(zi_base[zi_expand], n_tile)
194
+
195
+ def _initialize_zi(self, data: np.ndarray, ax_idx: int) -> np.ndarray:
196
+ """Initialize filter state (zi) scaled by edge value."""
197
+ if self._zi_tiled is None:
198
+ self._compute_zi_tiled(data, ax_idx)
199
+ first_sample = np.take(data, [0], axis=ax_idx)
200
+ return self._zi_tiled * first_sample
201
+
202
+ def _process(self, message: AxisArray) -> AxisArray:
203
+ axis = message.dims[0] if self.settings.axis is None else self.settings.axis
204
+ ax_idx = message.get_axis_idx(axis)
205
+ fs = 1 / message.axes[axis].gain
206
+
207
+ # Check if we need to redesign filter
208
+ if self._coefs_cache is None or self.state.needs_redesign:
209
+ self._coefs_cache = self.get_design_function()(fs)
210
+ self._pad_length = self._compute_pad_length(fs)
211
+ self._zi_tiled = None # Invalidate; recomputed on next use.
212
+ self.state.needs_redesign = False
213
+
214
+ # Initialize buffer with duration based on pad_length
215
+ # Add some margin to handle variable chunk sizes
216
+ buffer_duration = (self._pad_length + 1) / fs
217
+ self._buffer = HybridAxisArrayBuffer(duration=buffer_duration, axis=axis)
218
+
219
+ # Early exit if filter is effectively disabled
220
+ if self._coefs_cache is None or self.settings.order <= 0 or message.data.size <= 0:
221
+ return message
222
+
223
+ # Write new data to buffer
224
+ self._buffer.write(message)
225
+ n_available = self._buffer.available()
226
+ n_output = n_available - self._pad_length
227
+
228
+ # If we don't have enough data yet, return empty
229
+ if n_output <= 0:
230
+ new_shape = list(message.data.shape)
231
+ new_shape[ax_idx] = 0
232
+ empty_data = np.empty(new_shape, dtype=message.data.dtype)
233
+ return replace(message, data=empty_data)
234
+
235
+ # Peek all available data from buffer
236
+ # Note: HybridAxisArrayBuffer moves the target axis to position 0
237
+ buffered = self._buffer.peek(n_available)
238
+ combined = buffered.data
239
+ buffer_ax_idx = 0 # Buffer always puts time axis at position 0
240
+
241
+ # Backward filter on reversed data
242
+ combined_rev = np.flip(combined, axis=buffer_ax_idx)
243
+ backward_zi = self._initialize_zi(combined_rev, buffer_ax_idx)
244
+
245
+ if self.settings.coef_type == "ba":
246
+ b, a = self._coefs_cache
247
+ y_bwd_rev, _ = scipy.signal.lfilter(b, a, combined_rev, axis=buffer_ax_idx, zi=backward_zi)
248
+ else: # sos
249
+ y_bwd_rev, _ = scipy.signal.sosfilt(self._coefs_cache, combined_rev, axis=buffer_ax_idx, zi=backward_zi)
250
+
251
+ # Reverse back to get output in correct time order
252
+ y_bwd = np.flip(y_bwd_rev, axis=buffer_ax_idx)
253
+
254
+ # Output the settled portion (first n_output samples)
255
+ y = y_bwd[:n_output]
256
+
257
+ # Advance buffer read head to discard output samples, keep pad_length
258
+ self._buffer.seek(n_output)
259
+
260
+ # Build output with adjusted time axis
261
+ # LinearAxis offset is already correct from the buffer
262
+ out_axis = buffered.axes[axis]
263
+
264
+ # Move axis back to original position if needed
265
+ if ax_idx != 0:
266
+ y = np.moveaxis(y, 0, ax_idx)
267
+
268
+ return replace(
269
+ message,
270
+ data=y,
271
+ axes={**message.axes, axis: out_axis},
272
+ )
273
+
274
+
275
+ class ButterworthZeroPhaseTransformer(CompositeProcessor[ButterworthZeroPhaseSettings, AxisArray, AxisArray]):
276
+ """
277
+ Streaming zero-phase Butterworth filter as a composite of two stages.
278
+
279
+ Stage 1 (forward): Standard causal Butterworth filter with state
280
+ Stage 2 (backward): Acausal Butterworth filter with buffering
281
+
282
+ The output is delayed by ``pad_length`` samples.
283
+ """
284
+
285
+ @staticmethod
286
+ def _initialize_processors(
287
+ settings: ButterworthZeroPhaseSettings,
288
+ ) -> dict[str, typing.Any]:
289
+ # Both stages use the same filter design settings
290
+ return {
291
+ "forward": ButterworthFilterTransformer(settings),
292
+ "backward": ButterworthBackwardFilterTransformer(settings),
293
+ }
294
+
295
+ @classmethod
296
+ def get_message_type(cls, dir: str) -> type[AxisArray]:
297
+ if dir in ("in", "out"):
298
+ return AxisArray
299
+ raise ValueError(f"Invalid direction: {dir}. Must be 'in' or 'out'.")
300
+
301
+
302
+ class ButterworthZeroPhase(
303
+ BaseTransformerUnit[ButterworthZeroPhaseSettings, AxisArray, AxisArray, ButterworthZeroPhaseTransformer]
304
+ ):
305
+ SETTINGS = ButterworthZeroPhaseSettings
@@ -3,6 +3,10 @@ Coordinate space transformations for streaming data.
3
3
 
4
4
  This module provides utilities and ezmsg nodes for transforming between
5
5
  Cartesian (x, y) and polar (r, theta) coordinate systems.
6
+
7
+ .. note::
8
+ This module supports the :doc:`Array API standard </guides/explanations/array_api>`,
9
+ enabling use with NumPy, CuPy, PyTorch, and other compatible array libraries.
6
10
  """
7
11
 
8
12
  from enum import Enum
@@ -11,6 +15,7 @@ from typing import Tuple
11
15
  import ezmsg.core as ez
12
16
  import numpy as np
13
17
  import numpy.typing as npt
18
+ from array_api_compat import get_namespace, is_array_api_obj
14
19
  from ezmsg.baseproc import (
15
20
  BaseTransformer,
16
21
  BaseTransformerUnit,
@@ -20,14 +25,24 @@ from ezmsg.util.messages.axisarray import AxisArray, replace
20
25
  # -- Utility functions for coordinate transformations --
21
26
 
22
27
 
28
+ def _get_namespace_or_numpy(*args: npt.ArrayLike):
29
+ """Get array namespace if any arg is an array, otherwise return numpy."""
30
+ for arg in args:
31
+ if is_array_api_obj(arg):
32
+ return get_namespace(arg)
33
+ return np
34
+
35
+
23
36
  def polar2z(r: npt.ArrayLike, theta: npt.ArrayLike) -> npt.ArrayLike:
24
37
  """Convert polar coordinates to complex number representation."""
25
- return r * np.exp(1j * theta)
38
+ xp = _get_namespace_or_numpy(r, theta)
39
+ return r * xp.exp(1j * theta)
26
40
 
27
41
 
28
42
  def z2polar(z: npt.ArrayLike) -> Tuple[npt.ArrayLike, npt.ArrayLike]:
29
43
  """Convert complex number to polar coordinates (r, theta)."""
30
- return np.abs(z), np.angle(z)
44
+ xp = _get_namespace_or_numpy(z)
45
+ return xp.abs(z), xp.atan2(xp.imag(z), xp.real(z))
31
46
 
32
47
 
33
48
  def cart2z(x: npt.ArrayLike, y: npt.ArrayLike) -> npt.ArrayLike:
@@ -37,7 +52,8 @@ def cart2z(x: npt.ArrayLike, y: npt.ArrayLike) -> npt.ArrayLike:
37
52
 
38
53
  def z2cart(z: npt.ArrayLike) -> Tuple[npt.ArrayLike, npt.ArrayLike]:
39
54
  """Convert complex number to Cartesian coordinates (x, y)."""
40
- return np.real(z), np.imag(z)
55
+ xp = _get_namespace_or_numpy(z)
56
+ return xp.real(z), xp.imag(z)
41
57
 
42
58
 
43
59
  def cart2pol(x: npt.ArrayLike, y: npt.ArrayLike) -> Tuple[npt.ArrayLike, npt.ArrayLike]:
@@ -90,6 +106,7 @@ class CoordinateSpacesTransformer(BaseTransformer[CoordinateSpacesSettings, Axis
90
106
  """
91
107
 
92
108
  def _process(self, message: AxisArray) -> AxisArray:
109
+ xp = get_namespace(message.data)
93
110
  axis = self.settings.axis or message.dims[-1]
94
111
  axis_idx = message.get_axis_idx(axis)
95
112
 
@@ -116,9 +133,9 @@ class CoordinateSpacesTransformer(BaseTransformer[CoordinateSpacesSettings, Axis
116
133
  out_a, out_b = pol2cart(component_a, component_b)
117
134
 
118
135
  # Stack results back along the same axis
119
- result = np.stack([out_a, out_b], axis=axis_idx)
136
+ result = xp.stack([out_a, out_b], axis=axis_idx)
120
137
 
121
- # Update axis labels if present
138
+ # Update axis labels if present (use numpy for string labels)
122
139
  axes = message.axes
123
140
  if axis in axes and hasattr(axes[axis], "data"):
124
141
  if self.settings.mode == CoordinateMode.CART2POL: