ezmsg-sigproc 2.7.0__tar.gz → 2.9.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.7.0 → ezmsg_sigproc-2.9.0}/PKG-INFO +1 -1
  2. ezmsg_sigproc-2.9.0/docs/source/guides/explanations/array_api.rst +156 -0
  3. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/explanations/sigproc.rst +1 -1
  4. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/sigproc/content-sigproc.rst +3 -1
  5. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/tutorials/signalprocessing.rst +1 -1
  6. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/__version__.py +2 -2
  7. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/affinetransform.py +23 -1
  8. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/aggregate.py +9 -0
  9. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/butterworthzerophase.py +1 -1
  10. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/coordinatespaces.py +22 -5
  11. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/denormalize.py +3 -4
  12. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/detrend.py +1 -1
  13. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/diff.py +18 -8
  14. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/ewma.py +44 -7
  15. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/extract_axis.py +1 -2
  16. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/filter.py +3 -4
  17. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/fir_hilbert.py +1 -1
  18. ezmsg_sigproc-2.9.0/src/ezmsg/sigproc/linear.py +120 -0
  19. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/math/abs.py +11 -6
  20. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/math/add.py +1 -2
  21. ezmsg_sigproc-2.9.0/src/ezmsg/sigproc/math/clip.py +48 -0
  22. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/math/difference.py +9 -3
  23. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/math/invert.py +8 -3
  24. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/math/log.py +19 -8
  25. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/math/scale.py +8 -3
  26. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/rollingscaler.py +4 -4
  27. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/scaler.py +52 -3
  28. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/transpose.py +22 -7
  29. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_add_system.py +2 -2
  30. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_difference_system.py +3 -3
  31. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_sampler_system.py +1 -1
  32. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_activation.py +8 -3
  33. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_affine_transform.py +33 -26
  34. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_aggregate.py +18 -13
  35. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_bandpower.py +16 -9
  36. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_base.py +3 -3
  37. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_butter.py +29 -28
  38. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_butterworthzerophase.py +2 -2
  39. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_coordinatespaces.py +12 -12
  40. ezmsg_sigproc-2.9.0/tests/unit/test_ewma.py +162 -0
  41. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_filterbank.py +3 -3
  42. ezmsg_sigproc-2.9.0/tests/unit/test_linear.py +307 -0
  43. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_math.py +26 -26
  44. ezmsg_sigproc-2.9.0/tests/unit/test_scaler.py +233 -0
  45. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_slicer.py +17 -18
  46. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_spectrogram.py +1 -1
  47. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_spectrum.py +17 -14
  48. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_wavelets.py +10 -8
  49. ezmsg_sigproc-2.7.0/src/ezmsg/sigproc/math/clip.py +0 -43
  50. ezmsg_sigproc-2.7.0/tests/unit/test_ewma.py +0 -47
  51. ezmsg_sigproc-2.7.0/tests/unit/test_scaler.py +0 -70
  52. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/.github/workflows/docs.yml +0 -0
  53. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/.github/workflows/python-publish.yml +0 -0
  54. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/.github/workflows/python-tests.yml +0 -0
  55. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/.gitignore +0 -0
  56. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/.pre-commit-config.yaml +0 -0
  57. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/LICENSE +0 -0
  58. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/README.md +0 -0
  59. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/Makefile +0 -0
  60. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/make.bat +0 -0
  61. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/_templates/autosummary/module.rst +0 -0
  62. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/api/index.rst +0 -0
  63. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/conf.py +0 -0
  64. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/HybridBuffer.md +0 -0
  65. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/how-tos/signalprocessing/adaptive.rst +0 -0
  66. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/how-tos/signalprocessing/checkpoint.rst +0 -0
  67. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/how-tos/signalprocessing/composite.rst +0 -0
  68. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/how-tos/signalprocessing/content-signalprocessing.rst +0 -0
  69. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/how-tos/signalprocessing/processor.rst +0 -0
  70. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/how-tos/signalprocessing/standalone.rst +0 -0
  71. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/how-tos/signalprocessing/stateful.rst +0 -0
  72. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/how-tos/signalprocessing/unit.rst +0 -0
  73. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/img/HybridBufferBasic.svg +0 -0
  74. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/img/HybridBufferOverflow.svg +0 -0
  75. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/sigproc/architecture.rst +0 -0
  76. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/sigproc/base.rst +0 -0
  77. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/sigproc/processors.rst +0 -0
  78. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/guides/sigproc/units.rst +0 -0
  79. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/docs/source/index.md +0 -0
  80. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/pyproject.toml +0 -0
  81. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/__init__.py +0 -0
  82. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/activation.py +0 -0
  83. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/adaptive_lattice_notch.py +0 -0
  84. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/bandpower.py +0 -0
  85. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/base.py +0 -0
  86. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/butterworthfilter.py +0 -0
  87. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/cheby.py +0 -0
  88. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/combfilter.py +0 -0
  89. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/decimate.py +0 -0
  90. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/downsample.py +0 -0
  91. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/ewmfilter.py +0 -0
  92. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/fbcca.py +0 -0
  93. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/filterbank.py +0 -0
  94. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/filterbankdesign.py +0 -0
  95. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/fir_pmc.py +0 -0
  96. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/firfilter.py +0 -0
  97. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/gaussiansmoothing.py +0 -0
  98. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/kaiser.py +0 -0
  99. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/math/__init__.py +0 -0
  100. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/messages.py +0 -0
  101. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/quantize.py +0 -0
  102. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/resample.py +0 -0
  103. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/sampler.py +0 -0
  104. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/signalinjector.py +0 -0
  105. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/slicer.py +0 -0
  106. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/spectral.py +0 -0
  107. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/spectrogram.py +0 -0
  108. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/spectrum.py +0 -0
  109. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/util/__init__.py +0 -0
  110. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/util/asio.py +0 -0
  111. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/util/axisarray_buffer.py +0 -0
  112. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/util/buffer.py +0 -0
  113. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/util/message.py +0 -0
  114. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/util/profile.py +0 -0
  115. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/util/sparse.py +0 -0
  116. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/util/typeresolution.py +0 -0
  117. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/wavelets.py +0 -0
  118. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/src/ezmsg/sigproc/window.py +0 -0
  119. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/__init__.py +0 -0
  120. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/conftest.py +0 -0
  121. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/helpers/__init__.py +0 -0
  122. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/helpers/synth.py +0 -0
  123. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/helpers/util.py +0 -0
  124. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/bytewax/test_spectrum_bytewax.py +0 -0
  125. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/bytewax/test_window_bytewax.py +0 -0
  126. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_butterworth_system.py +0 -0
  127. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_butterworthzerophase_system.py +0 -0
  128. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_coordinatespaces_system.py +0 -0
  129. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_decimate_system.py +0 -0
  130. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_downsample_system.py +0 -0
  131. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_filter_system.py +0 -0
  132. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_fir_hilbert_system.py +0 -0
  133. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_fir_pmc_system.py +0 -0
  134. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_rollingscaler_system.py +0 -0
  135. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_scaler_system.py +0 -0
  136. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_spectrum_system.py +0 -0
  137. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/integration/ezmsg/test_window_system.py +0 -0
  138. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/resources/xform.csv +0 -0
  139. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/test_profile.py +0 -0
  140. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/buffer/test_axisarray_buffer.py +0 -0
  141. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/buffer/test_buffer.py +0 -0
  142. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/buffer/test_buffer_overflow.py +0 -0
  143. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_adaptive_lattice_notch.py +0 -0
  144. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_combfilter.py +0 -0
  145. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_denormalize.py +0 -0
  146. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_diff.py +0 -0
  147. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_downsample.py +0 -0
  148. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_extract_axis.py +0 -0
  149. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_fbcca.py +0 -0
  150. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_filter.py +0 -0
  151. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_filterbankdesign.py +0 -0
  152. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_fir_hilbert.py +0 -0
  153. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_fir_pmc.py +0 -0
  154. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_firfilter.py +0 -0
  155. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_gaussian_smoothing_filter.py +0 -0
  156. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_kaiser.py +0 -0
  157. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_math_add.py +0 -0
  158. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_math_difference.py +0 -0
  159. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_quantize.py +0 -0
  160. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_resample.py +0 -0
  161. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_rollingscaler.py +0 -0
  162. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_sampler.py +0 -0
  163. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_transpose.py +0 -0
  164. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.0}/tests/unit/test_util.py +0 -0
  165. {ezmsg_sigproc-2.7.0 → ezmsg_sigproc-2.9.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.7.0
3
+ Version: 2.9.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
@@ -329,7 +329,7 @@ Often, all that is required is the following (e.g., for a custom transformer):
329
329
 
330
330
  import ezmsg.core as ez
331
331
  from ezmsg.util.messages.axisarray import AxisArray
332
- from ezmsg.sigproc.base import BaseTransformer, BaseTransformerUnit
332
+ from ezmsg.baseproc import BaseTransformer, BaseTransformerUnit
333
333
 
334
334
 
335
335
  class CustomTransformerSettings(ez.Settings):
@@ -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
@@ -132,7 +132,7 @@ Add the following import statements to the top of the `downsample.py` file:
132
132
  )
133
133
  import ezmsg.core as ez
134
134
 
135
- from ezmsg.sigproc.base import (
135
+ from ezmsg.baseproc import (
136
136
  BaseStatefulTransformer,
137
137
  BaseTransformerUnit,
138
138
  processor_state,
@@ -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.7.0'
32
- __version_tuple__ = version_tuple = (2, 7, 0)
31
+ __version__ = version = '2.9.0'
32
+ __version_tuple__ = version_tuple = (2, 9, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -1,3 +1,13 @@
1
+ """Affine transformations via matrix multiplication: y = Ax or y = Ax + B.
2
+
3
+ For full matrix transformations where channels are mixed (off-diagonal weights),
4
+ use :obj:`AffineTransformTransformer` or the `AffineTransform` unit.
5
+
6
+ For simple per-channel scaling and offset (diagonal weights only), use
7
+ :obj:`LinearTransformTransformer` from :mod:`ezmsg.sigproc.linear` instead,
8
+ which is more efficient as it avoids matrix multiplication.
9
+ """
10
+
1
11
  import os
2
12
  from pathlib import Path
3
13
 
@@ -17,7 +27,6 @@ from ezmsg.util.messages.util import replace
17
27
  class AffineTransformSettings(ez.Settings):
18
28
  """
19
29
  Settings for :obj:`AffineTransform`.
20
- See :obj:`affine_transform` for argument details.
21
30
  """
22
31
 
23
32
  weights: np.ndarray | str | Path
@@ -39,6 +48,19 @@ class AffineTransformState:
39
48
  class AffineTransformTransformer(
40
49
  BaseStatefulTransformer[AffineTransformSettings, AxisArray, AxisArray, AffineTransformState]
41
50
  ):
51
+ """Apply affine transformation via matrix multiplication: y = Ax or y = Ax + B.
52
+
53
+ Use this transformer when you need full matrix transformations that mix
54
+ channels (off-diagonal weights), such as spatial filters or projections.
55
+
56
+ For simple per-channel scaling and offset where each output channel depends
57
+ only on its corresponding input channel (diagonal weight matrix), use
58
+ :obj:`LinearTransformTransformer` instead, which is more efficient.
59
+
60
+ The weights matrix can include an offset row (stacked as [A|B]) where the
61
+ input is automatically augmented with a column of ones to compute y = Ax + B.
62
+ """
63
+
42
64
  def __call__(self, message: AxisArray) -> AxisArray:
43
65
  # Override __call__ so we can shortcut if weights are None.
44
66
  if self.settings.weights is None or (
@@ -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
@@ -4,10 +4,10 @@ import typing
4
4
  import ezmsg.core as ez
5
5
  import numpy as np
6
6
  import scipy.signal
7
+ from ezmsg.baseproc import SettingsType
7
8
  from ezmsg.util.messages.axisarray import AxisArray
8
9
  from ezmsg.util.messages.util import replace
9
10
 
10
- from ezmsg.sigproc.base import SettingsType
11
11
  from ezmsg.sigproc.butterworthfilter import ButterworthFilterSettings, butter_design_fun
12
12
  from ezmsg.sigproc.filter import (
13
13
  BACoeffs,
@@ -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:
@@ -1,14 +1,13 @@
1
1
  import ezmsg.core as ez
2
2
  import numpy as np
3
3
  import numpy.typing as npt
4
- from ezmsg.util.messages.axisarray import AxisArray
5
- from ezmsg.util.messages.util import replace
6
-
7
- from ezmsg.sigproc.base import (
4
+ from ezmsg.baseproc import (
8
5
  BaseStatefulTransformer,
9
6
  BaseTransformerUnit,
10
7
  processor_state,
11
8
  )
9
+ from ezmsg.util.messages.axisarray import AxisArray
10
+ from ezmsg.util.messages.util import replace
12
11
 
13
12
 
14
13
  class DenormalizeSettings(ez.Settings):
@@ -1,7 +1,7 @@
1
1
  import scipy.signal as sps
2
+ from ezmsg.baseproc import BaseTransformerUnit
2
3
  from ezmsg.util.messages.axisarray import AxisArray, replace
3
4
 
4
- from ezmsg.sigproc.base import BaseTransformerUnit
5
5
  from ezmsg.sigproc.ewma import EWMASettings, EWMATransformer
6
6
 
7
7
 
@@ -1,14 +1,22 @@
1
+ """
2
+ Compute differences along an axis.
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
+
1
9
  import ezmsg.core as ez
2
10
  import numpy as np
3
11
  import numpy.typing as npt
4
- from ezmsg.util.messages.axisarray import AxisArray, slice_along_axis
5
- from ezmsg.util.messages.util import replace
6
-
7
- from ezmsg.sigproc.base import (
12
+ from array_api_compat import get_namespace
13
+ from ezmsg.baseproc import (
8
14
  BaseStatefulTransformer,
9
15
  BaseTransformerUnit,
10
16
  processor_state,
11
17
  )
18
+ from ezmsg.util.messages.axisarray import AxisArray, slice_along_axis
19
+ from ezmsg.util.messages.util import replace
12
20
 
13
21
 
14
22
  class DiffSettings(ez.Settings):
@@ -40,23 +48,25 @@ class DiffTransformer(BaseStatefulTransformer[DiffSettings, AxisArray, AxisArray
40
48
  self.state.last_time = ax_info.data[0] - 0.001
41
49
 
42
50
  def _process(self, message: AxisArray) -> AxisArray:
51
+ xp = get_namespace(message.data)
43
52
  axis = self.settings.axis or message.dims[0]
44
53
  ax_idx = message.get_axis_idx(axis)
45
54
 
46
- diffs = np.diff(
47
- np.concatenate((self.state.last_dat, message.data), axis=ax_idx),
55
+ diffs = xp.diff(
56
+ xp.concat((self.state.last_dat, message.data), axis=ax_idx),
48
57
  axis=ax_idx,
49
58
  )
50
59
  # Prepare last_dat for next iteration
51
60
  self.state.last_dat = slice_along_axis(message.data, slice(-1, None), axis=ax_idx)
52
- # Scale by fs if requested. This convers the diff to a derivative. e.g., diff of position becomes velocity.
61
+ # Scale by fs if requested. This converts the diff to a derivative. e.g., diff of position becomes velocity.
53
62
  if self.settings.scale_by_fs:
54
63
  ax_info = message.get_axis(axis)
55
64
  if hasattr(ax_info, "data"):
65
+ # ax_info.data is typically numpy for metadata, so use np.diff here
56
66
  dt = np.diff(np.concatenate(([self.state.last_time], ax_info.data)))
57
67
  # Expand dt dims to match diffs
58
68
  exp_sl = (None,) * ax_idx + (Ellipsis,) + (None,) * (message.data.ndim - ax_idx - 1)
59
- diffs /= dt[exp_sl]
69
+ diffs /= xp.asarray(dt[exp_sl])
60
70
  self.state.last_time = ax_info.data[-1] # For next iteration
61
71
  else:
62
72
  diffs /= ax_info.gain
@@ -139,8 +139,15 @@ class EWMA_Deprecated:
139
139
 
140
140
  class EWMASettings(ez.Settings):
141
141
  time_constant: float = 1.0
142
+ """The amount of time for the smoothed response of a unit step function to reach 1 - 1/e approx-eq 63.2%."""
143
+
142
144
  axis: str | None = None
143
145
 
146
+ accumulate: bool = True
147
+ """If True, update the EWMA state with each sample. If False, only apply
148
+ the current EWMA estimate without updating state (useful for inference
149
+ periods where you don't want to adapt statistics)."""
150
+
144
151
 
145
152
  @processor_state
146
153
  class EWMAState:
@@ -166,15 +173,45 @@ class EWMATransformer(BaseStatefulTransformer[EWMASettings, AxisArray, AxisArray
166
173
  return message
167
174
  axis = self.settings.axis or message.dims[0]
168
175
  axis_idx = message.get_axis_idx(axis)
169
- expected, self._state.zi = sps.lfilter(
170
- [self._state.alpha],
171
- [1.0, self._state.alpha - 1.0],
172
- message.data,
173
- axis=axis_idx,
174
- zi=self._state.zi,
175
- )
176
+ if self.settings.accumulate:
177
+ # Normal behavior: update state with new samples
178
+ expected, self._state.zi = sps.lfilter(
179
+ [self._state.alpha],
180
+ [1.0, self._state.alpha - 1.0],
181
+ message.data,
182
+ axis=axis_idx,
183
+ zi=self._state.zi,
184
+ )
185
+ else:
186
+ # Process-only: compute output without updating state
187
+ expected, _ = sps.lfilter(
188
+ [self._state.alpha],
189
+ [1.0, self._state.alpha - 1.0],
190
+ message.data,
191
+ axis=axis_idx,
192
+ zi=self._state.zi,
193
+ )
176
194
  return replace(message, data=expected)
177
195
 
178
196
 
179
197
  class EWMAUnit(BaseTransformerUnit[EWMASettings, AxisArray, AxisArray, EWMATransformer]):
180
198
  SETTINGS = EWMASettings
199
+
200
+ @ez.subscriber(BaseTransformerUnit.INPUT_SETTINGS)
201
+ async def on_settings(self, msg: EWMASettings) -> None:
202
+ """
203
+ Handle settings updates with smart reset behavior.
204
+
205
+ Only resets state if `axis` changes (structural change).
206
+ Changes to `time_constant` or `accumulate` are applied without
207
+ resetting accumulated state.
208
+ """
209
+ old_axis = self.SETTINGS.axis
210
+ self.apply_settings(msg)
211
+
212
+ if msg.axis != old_axis:
213
+ # Axis changed - need full reset
214
+ self.create_processor()
215
+ else:
216
+ # Only accumulate or time_constant changed - keep state
217
+ self.processor.settings = msg
@@ -1,9 +1,8 @@
1
1
  import ezmsg.core as ez
2
2
  import numpy as np
3
+ from ezmsg.baseproc import BaseTransformer, BaseTransformerUnit
3
4
  from ezmsg.util.messages.axisarray import AxisArray, replace
4
5
 
5
- from ezmsg.sigproc.base import BaseTransformer, BaseTransformerUnit
6
-
7
6
 
8
7
  class ExtractAxisSettings(ez.Settings):
9
8
  axis: str = "freq"
@@ -6,10 +6,7 @@ import ezmsg.core as ez
6
6
  import numpy as np
7
7
  import numpy.typing as npt
8
8
  import scipy.signal
9
- from ezmsg.util.messages.axisarray import AxisArray
10
- from ezmsg.util.messages.util import replace
11
-
12
- from ezmsg.sigproc.base import (
9
+ from ezmsg.baseproc import (
13
10
  BaseConsumerUnit,
14
11
  BaseStatefulTransformer,
15
12
  BaseTransformerUnit,
@@ -17,6 +14,8 @@ from ezmsg.sigproc.base import (
17
14
  TransformerType,
18
15
  processor_state,
19
16
  )
17
+ from ezmsg.util.messages.axisarray import AxisArray
18
+ from ezmsg.util.messages.util import replace
20
19
 
21
20
 
22
21
  @dataclass
@@ -4,10 +4,10 @@ import typing
4
4
  import ezmsg.core as ez
5
5
  import numpy as np
6
6
  import scipy.signal as sps
7
+ from ezmsg.baseproc import BaseStatefulTransformer, processor_state
7
8
  from ezmsg.util.messages.axisarray import AxisArray
8
9
  from ezmsg.util.messages.util import replace
9
10
 
10
- from ezmsg.sigproc.base import BaseStatefulTransformer, processor_state
11
11
  from ezmsg.sigproc.filter import (
12
12
  BACoeffs,
13
13
  BaseFilterByDesignTransformerUnit,
@@ -0,0 +1,120 @@
1
+ """
2
+ Apply a linear transformation: output = scale * input + offset.
3
+
4
+ Supports per-element scale and offset along a specified axis.
5
+ For full matrix transformations, use :obj:`AffineTransformTransformer` instead.
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.
10
+ """
11
+
12
+ import ezmsg.core as ez
13
+ import numpy as np
14
+ import numpy.typing as npt
15
+ from array_api_compat import get_namespace
16
+ from ezmsg.baseproc import BaseStatefulTransformer, BaseTransformerUnit, processor_state
17
+ from ezmsg.util.messages.axisarray import AxisArray
18
+ from ezmsg.util.messages.util import replace
19
+
20
+
21
+ class LinearTransformSettings(ez.Settings):
22
+ scale: float | list[float] | npt.ArrayLike = 1.0
23
+ """Scale factor(s). Can be a scalar (applied to all elements) or an array
24
+ matching the size of the specified axis for per-element scaling."""
25
+
26
+ offset: float | list[float] | npt.ArrayLike = 0.0
27
+ """Offset value(s). Can be a scalar (applied to all elements) or an array
28
+ matching the size of the specified axis for per-element offset."""
29
+
30
+ axis: str | None = None
31
+ """Axis along which to apply per-element scale/offset. If None, scalar
32
+ scale/offset are broadcast to all elements."""
33
+
34
+
35
+ @processor_state
36
+ class LinearTransformState:
37
+ scale: npt.NDArray = None
38
+ """Prepared scale array for broadcasting."""
39
+
40
+ offset: npt.NDArray = None
41
+ """Prepared offset array for broadcasting."""
42
+
43
+
44
+ class LinearTransformTransformer(
45
+ BaseStatefulTransformer[LinearTransformSettings, AxisArray, AxisArray, LinearTransformState]
46
+ ):
47
+ """Apply linear transformation: output = scale * input + offset.
48
+
49
+ This transformer is optimized for element-wise linear operations with
50
+ optional per-channel (or per-axis) coefficients. For full matrix
51
+ transformations, use :obj:`AffineTransformTransformer` instead.
52
+
53
+ Examples:
54
+ # Uniform scaling and offset
55
+ >>> transformer = LinearTransformTransformer(LinearTransformSettings(scale=2.0, offset=1.0))
56
+
57
+ # Per-channel scaling (e.g., for 3-channel data along "ch" axis)
58
+ >>> transformer = LinearTransformTransformer(LinearTransformSettings(
59
+ ... scale=[0.5, 1.0, 2.0],
60
+ ... offset=[0.0, 0.1, 0.2],
61
+ ... axis="ch"
62
+ ... ))
63
+ """
64
+
65
+ def _hash_message(self, message: AxisArray) -> int:
66
+ """Hash based on shape and axis to detect when broadcast shapes need recalculation."""
67
+ axis = self.settings.axis
68
+ if axis is not None:
69
+ axis_idx = message.get_axis_idx(axis)
70
+ return hash((message.data.ndim, axis_idx, message.data.shape[axis_idx]))
71
+ return hash(message.data.ndim)
72
+
73
+ def _reset_state(self, message: AxisArray) -> None:
74
+ """Prepare scale/offset arrays with proper broadcast shapes."""
75
+ xp = get_namespace(message.data)
76
+ ndim = message.data.ndim
77
+
78
+ scale = self.settings.scale
79
+ offset = self.settings.offset
80
+
81
+ # Convert settings to arrays
82
+ if isinstance(scale, (list, np.ndarray)):
83
+ scale = xp.asarray(scale, dtype=xp.float64)
84
+ else:
85
+ # Scalar: create a 0-d array
86
+ scale = xp.asarray(float(scale), dtype=xp.float64)
87
+
88
+ if isinstance(offset, (list, np.ndarray)):
89
+ offset = xp.asarray(offset, dtype=xp.float64)
90
+ else:
91
+ # Scalar: create a 0-d array
92
+ offset = xp.asarray(float(offset), dtype=xp.float64)
93
+
94
+ # If axis is specified and we have 1-d arrays, reshape for proper broadcasting
95
+ if self.settings.axis is not None and ndim > 0:
96
+ axis_idx = message.get_axis_idx(self.settings.axis)
97
+
98
+ if scale.ndim == 1:
99
+ # Create shape for broadcasting: all 1s except at axis_idx
100
+ broadcast_shape = [1] * ndim
101
+ broadcast_shape[axis_idx] = scale.shape[0]
102
+ scale = xp.reshape(scale, broadcast_shape)
103
+
104
+ if offset.ndim == 1:
105
+ broadcast_shape = [1] * ndim
106
+ broadcast_shape[axis_idx] = offset.shape[0]
107
+ offset = xp.reshape(offset, broadcast_shape)
108
+
109
+ self._state.scale = scale
110
+ self._state.offset = offset
111
+
112
+ def _process(self, message: AxisArray) -> AxisArray:
113
+ result = message.data * self._state.scale + self._state.offset
114
+ return replace(message, data=result)
115
+
116
+
117
+ class LinearTransform(BaseTransformerUnit[LinearTransformSettings, AxisArray, AxisArray, LinearTransformTransformer]):
118
+ """Unit wrapper for LinearTransformTransformer."""
119
+
120
+ SETTINGS = LinearTransformSettings