ezmsg-sigproc 2.11.0__tar.gz → 2.13.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 (169) hide show
  1. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/PKG-INFO +1 -1
  2. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/__version__.py +2 -2
  3. ezmsg_sigproc-2.13.0/src/ezmsg/sigproc/affinetransform.py +507 -0
  4. ezmsg_sigproc-2.13.0/src/ezmsg/sigproc/merge.py +358 -0
  5. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/quantize.py +9 -8
  6. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/rollingscaler.py +28 -20
  7. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/scaler.py +10 -4
  8. ezmsg_sigproc-2.13.0/tests/unit/test_affine_transform.py +783 -0
  9. ezmsg_sigproc-2.13.0/tests/unit/test_merge.py +369 -0
  10. ezmsg_sigproc-2.11.0/src/ezmsg/sigproc/affinetransform.py +0 -235
  11. ezmsg_sigproc-2.11.0/tests/unit/test_affine_transform.py +0 -136
  12. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/.github/workflows/docs.yml +0 -0
  13. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/.github/workflows/python-publish.yml +0 -0
  14. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/.github/workflows/python-tests.yml +0 -0
  15. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/.gitignore +0 -0
  16. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/.pre-commit-config.yaml +0 -0
  17. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/LICENSE +0 -0
  18. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/README.md +0 -0
  19. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/Makefile +0 -0
  20. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/make.bat +0 -0
  21. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/_templates/autosummary/module.rst +0 -0
  22. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/api/index.rst +0 -0
  23. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/conf.py +0 -0
  24. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/HybridBuffer.md +0 -0
  25. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/explanations/array_api.rst +0 -0
  26. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/explanations/sigproc.rst +0 -0
  27. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/how-tos/signalprocessing/adaptive.rst +0 -0
  28. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/how-tos/signalprocessing/checkpoint.rst +0 -0
  29. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/how-tos/signalprocessing/composite.rst +0 -0
  30. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/how-tos/signalprocessing/content-signalprocessing.rst +0 -0
  31. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/how-tos/signalprocessing/processor.rst +0 -0
  32. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/how-tos/signalprocessing/standalone.rst +0 -0
  33. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/how-tos/signalprocessing/stateful.rst +0 -0
  34. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/how-tos/signalprocessing/unit.rst +0 -0
  35. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/img/HybridBufferBasic.svg +0 -0
  36. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/img/HybridBufferOverflow.svg +0 -0
  37. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/sigproc/architecture.rst +0 -0
  38. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/sigproc/base.rst +0 -0
  39. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/sigproc/content-sigproc.rst +0 -0
  40. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/sigproc/processors.rst +0 -0
  41. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/sigproc/units.rst +0 -0
  42. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/guides/tutorials/signalprocessing.rst +0 -0
  43. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/docs/source/index.md +0 -0
  44. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/pyproject.toml +0 -0
  45. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/__init__.py +0 -0
  46. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/activation.py +0 -0
  47. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/adaptive_lattice_notch.py +0 -0
  48. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/aggregate.py +0 -0
  49. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/bandpower.py +0 -0
  50. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/base.py +0 -0
  51. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/butterworthfilter.py +0 -0
  52. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/butterworthzerophase.py +0 -0
  53. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/cheby.py +0 -0
  54. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/combfilter.py +0 -0
  55. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/coordinatespaces.py +0 -0
  56. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/decimate.py +0 -0
  57. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/denormalize.py +0 -0
  58. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/detrend.py +0 -0
  59. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/diff.py +0 -0
  60. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/downsample.py +0 -0
  61. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/ewma.py +0 -0
  62. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/ewmfilter.py +0 -0
  63. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/extract_axis.py +0 -0
  64. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/fbcca.py +0 -0
  65. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/filter.py +0 -0
  66. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/filterbank.py +0 -0
  67. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/filterbankdesign.py +0 -0
  68. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/fir_hilbert.py +0 -0
  69. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/fir_pmc.py +0 -0
  70. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/firfilter.py +0 -0
  71. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/gaussiansmoothing.py +0 -0
  72. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/kaiser.py +0 -0
  73. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/linear.py +0 -0
  74. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/math/__init__.py +0 -0
  75. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/math/abs.py +0 -0
  76. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/math/add.py +0 -0
  77. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/math/clip.py +0 -0
  78. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/math/difference.py +0 -0
  79. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/math/invert.py +0 -0
  80. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/math/log.py +0 -0
  81. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/math/pow.py +0 -0
  82. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/math/scale.py +0 -0
  83. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/messages.py +0 -0
  84. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/resample.py +0 -0
  85. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/sampler.py +0 -0
  86. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/signalinjector.py +0 -0
  87. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/singlebandpow.py +0 -0
  88. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/slicer.py +0 -0
  89. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/spectral.py +0 -0
  90. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/spectrogram.py +0 -0
  91. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/spectrum.py +0 -0
  92. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/transpose.py +0 -0
  93. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/util/__init__.py +0 -0
  94. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/util/asio.py +0 -0
  95. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/util/axisarray_buffer.py +0 -0
  96. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/util/buffer.py +0 -0
  97. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/util/message.py +0 -0
  98. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/util/profile.py +0 -0
  99. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/util/sparse.py +0 -0
  100. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/util/typeresolution.py +0 -0
  101. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/wavelets.py +0 -0
  102. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/src/ezmsg/sigproc/window.py +0 -0
  103. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/__init__.py +0 -0
  104. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/conftest.py +0 -0
  105. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/helpers/__init__.py +0 -0
  106. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/helpers/synth.py +0 -0
  107. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/helpers/util.py +0 -0
  108. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/bytewax/test_spectrum_bytewax.py +0 -0
  109. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/bytewax/test_window_bytewax.py +0 -0
  110. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/ezmsg/test_add_system.py +0 -0
  111. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/ezmsg/test_butterworth_system.py +0 -0
  112. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/ezmsg/test_butterworthzerophase_system.py +0 -0
  113. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/ezmsg/test_coordinatespaces_system.py +0 -0
  114. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/ezmsg/test_decimate_system.py +0 -0
  115. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/ezmsg/test_difference_system.py +0 -0
  116. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/ezmsg/test_downsample_system.py +0 -0
  117. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/ezmsg/test_filter_system.py +0 -0
  118. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/ezmsg/test_fir_hilbert_system.py +0 -0
  119. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/ezmsg/test_fir_pmc_system.py +0 -0
  120. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/ezmsg/test_rollingscaler_system.py +0 -0
  121. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/ezmsg/test_sampler_system.py +0 -0
  122. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/ezmsg/test_scaler_system.py +0 -0
  123. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/ezmsg/test_spectrum_system.py +0 -0
  124. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/integration/ezmsg/test_window_system.py +0 -0
  125. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/resources/xform.csv +0 -0
  126. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/test_profile.py +0 -0
  127. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/buffer/test_axisarray_buffer.py +0 -0
  128. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/buffer/test_buffer.py +0 -0
  129. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/buffer/test_buffer_overflow.py +0 -0
  130. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_activation.py +0 -0
  131. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_adaptive_lattice_notch.py +0 -0
  132. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_aggregate.py +0 -0
  133. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_bandpower.py +0 -0
  134. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_base.py +0 -0
  135. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_butter.py +0 -0
  136. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_butterworthzerophase.py +0 -0
  137. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_combfilter.py +0 -0
  138. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_coordinatespaces.py +0 -0
  139. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_denormalize.py +0 -0
  140. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_diff.py +0 -0
  141. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_downsample.py +0 -0
  142. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_ewma.py +0 -0
  143. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_extract_axis.py +0 -0
  144. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_fbcca.py +0 -0
  145. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_filter.py +0 -0
  146. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_filterbank.py +0 -0
  147. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_filterbankdesign.py +0 -0
  148. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_fir_hilbert.py +0 -0
  149. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_fir_pmc.py +0 -0
  150. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_firfilter.py +0 -0
  151. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_gaussian_smoothing_filter.py +0 -0
  152. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_kaiser.py +0 -0
  153. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_linear.py +0 -0
  154. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_math.py +0 -0
  155. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_math_add.py +0 -0
  156. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_math_difference.py +0 -0
  157. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_quantize.py +0 -0
  158. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_resample.py +0 -0
  159. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_rollingscaler.py +0 -0
  160. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_sampler.py +0 -0
  161. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_scaler.py +0 -0
  162. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_singlebandpow.py +0 -0
  163. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_slicer.py +0 -0
  164. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_spectrogram.py +0 -0
  165. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_spectrum.py +0 -0
  166. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_transpose.py +0 -0
  167. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_util.py +0 -0
  168. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.0}/tests/unit/test_wavelets.py +0 -0
  169. {ezmsg_sigproc-2.11.0 → ezmsg_sigproc-2.13.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.11.0
3
+ Version: 2.13.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.11.0'
32
- __version_tuple__ = version_tuple = (2, 11, 0)
31
+ __version__ = version = '2.13.0'
32
+ __version_tuple__ = version_tuple = (2, 13, 0)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -0,0 +1,507 @@
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
+
11
+ import os
12
+ from pathlib import Path
13
+
14
+ import ezmsg.core as ez
15
+ import numpy as np
16
+ import numpy.typing as npt
17
+ from array_api_compat import get_namespace
18
+ from ezmsg.baseproc import (
19
+ BaseStatefulTransformer,
20
+ BaseTransformerUnit,
21
+ processor_state,
22
+ )
23
+ from ezmsg.util.messages.axisarray import AxisArray, AxisBase
24
+ from ezmsg.util.messages.util import replace
25
+
26
+
27
+ def _find_block_diagonal_clusters(weights: np.ndarray) -> list[tuple[np.ndarray, np.ndarray]] | None:
28
+ """Detect block-diagonal structure in a weight matrix.
29
+
30
+ Finds connected components in the bipartite graph of non-zero weights,
31
+ where input channels and output channels are separate node sets.
32
+
33
+ Args:
34
+ weights: 2-D weight matrix of shape (n_in, n_out).
35
+
36
+ Returns:
37
+ List of (input_indices, output_indices) tuples, one per block, or
38
+ None if the matrix is not block-diagonal (single connected component).
39
+ """
40
+ if weights.ndim != 2:
41
+ return None
42
+
43
+ n_in, n_out = weights.shape
44
+ if n_in + n_out <= 2:
45
+ return None
46
+
47
+ from scipy.sparse import coo_matrix
48
+ from scipy.sparse.csgraph import connected_components
49
+
50
+ rows, cols = np.nonzero(weights)
51
+ if len(rows) == 0:
52
+ return None
53
+
54
+ # Bipartite graph: input nodes [0, n_in), output nodes [n_in, n_in + n_out)
55
+ shifted_cols = cols + n_in
56
+ adj_rows = np.concatenate([rows, shifted_cols])
57
+ adj_cols = np.concatenate([shifted_cols, rows])
58
+ adj_data = np.ones(len(adj_rows), dtype=bool)
59
+ n_nodes = n_in + n_out
60
+ adj = coo_matrix((adj_data, (adj_rows, adj_cols)), shape=(n_nodes, n_nodes))
61
+
62
+ n_components, labels = connected_components(adj, directed=False)
63
+
64
+ if n_components <= 1:
65
+ return None
66
+
67
+ clusters = []
68
+ for comp in range(n_components):
69
+ members = np.where(labels == comp)[0]
70
+ in_idx = np.sort(members[members < n_in])
71
+ out_idx = np.sort(members[members >= n_in] - n_in)
72
+ if len(in_idx) > 0 and len(out_idx) > 0:
73
+ clusters.append((in_idx, out_idx))
74
+
75
+ return clusters if len(clusters) > 1 else None
76
+
77
+
78
+ def _max_cross_cluster_weight(weights: np.ndarray, clusters: list[tuple[np.ndarray, np.ndarray]]) -> float:
79
+ """Return the maximum absolute weight between different clusters."""
80
+ mask = np.zeros(weights.shape, dtype=bool)
81
+ for in_idx, out_idx in clusters:
82
+ mask[np.ix_(in_idx, out_idx)] = True
83
+ cross = np.abs(weights[~mask])
84
+ return float(cross.max()) if cross.size > 0 else 0.0
85
+
86
+
87
+ def _merge_small_clusters(
88
+ clusters: list[tuple[np.ndarray, np.ndarray]], min_size: int
89
+ ) -> list[tuple[np.ndarray, np.ndarray]]:
90
+ """Merge clusters smaller than *min_size* into combined groups.
91
+
92
+ Small clusters are greedily concatenated until each merged group has
93
+ at least *min_size* channels (measured as ``max(n_in, n_out)``).
94
+ Any leftover small clusters that don't reach the threshold are
95
+ combined into a final group.
96
+
97
+ The merged group's sub-weight-matrix will contain the original small
98
+ diagonal blocks with zeros between them — a dense matmul on that
99
+ sub-matrix is cheaper than iterating over many tiny matmuls.
100
+ """
101
+ if min_size <= 1:
102
+ return clusters
103
+
104
+ large = []
105
+ small = []
106
+ for cluster in clusters:
107
+ in_idx, out_idx = cluster
108
+ if max(len(in_idx), len(out_idx)) >= min_size:
109
+ large.append(cluster)
110
+ else:
111
+ small.append(cluster)
112
+
113
+ if not small:
114
+ return clusters
115
+
116
+ current_in: list[np.ndarray] = []
117
+ current_out: list[np.ndarray] = []
118
+ current_in_size = 0
119
+ current_out_size = 0
120
+ for in_idx, out_idx in small:
121
+ current_in.append(in_idx)
122
+ current_out.append(out_idx)
123
+ current_in_size += len(in_idx)
124
+ current_out_size += len(out_idx)
125
+ if max(current_in_size, current_out_size) >= min_size:
126
+ large.append((np.sort(np.concatenate(current_in)), np.sort(np.concatenate(current_out))))
127
+ current_in = []
128
+ current_out = []
129
+ current_in_size = 0
130
+ current_out_size = 0
131
+
132
+ if current_in:
133
+ large.append((np.sort(np.concatenate(current_in)), np.sort(np.concatenate(current_out))))
134
+
135
+ return large
136
+
137
+
138
+ class AffineTransformSettings(ez.Settings):
139
+ """
140
+ Settings for :obj:`AffineTransform`.
141
+ """
142
+
143
+ weights: np.ndarray | str | Path
144
+ """An array of weights or a path to a file with weights compatible with np.loadtxt."""
145
+
146
+ axis: str | None = None
147
+ """The name of the axis to apply the transformation to. Defaults to the leading (0th) axis in the array."""
148
+
149
+ right_multiply: bool = True
150
+ """Set False to transpose the weights before applying."""
151
+
152
+ channel_clusters: list[list[int]] | None = None
153
+ """Optional explicit input channel cluster specification for block-diagonal optimization.
154
+
155
+ Each element is a list of input channel indices forming one cluster. The
156
+ corresponding output indices are derived automatically from the non-zero
157
+ columns of the weight matrix for those input rows.
158
+
159
+ When provided, the weight matrix is decomposed into per-cluster sub-matrices
160
+ and multiplied separately, which is faster when cross-cluster weights are zero.
161
+
162
+ If None, block-diagonal structure is auto-detected from the zero pattern
163
+ of the weights."""
164
+
165
+ min_cluster_size: int = 32
166
+ """Minimum number of channels per cluster for the block-diagonal optimization.
167
+ Clusters smaller than this are greedily merged together to avoid excessive
168
+ Python loop overhead. Set to 1 to disable merging."""
169
+
170
+
171
+ @processor_state
172
+ class AffineTransformState:
173
+ weights: npt.NDArray | None = None
174
+ new_axis: AxisBase | None = None
175
+ n_out: int = 0
176
+ clusters: list | None = None
177
+ """list of (in_indices_xp, out_indices_xp, sub_weights_xp) tuples when block-diagonal."""
178
+
179
+
180
+ class AffineTransformTransformer(
181
+ BaseStatefulTransformer[AffineTransformSettings, AxisArray, AxisArray, AffineTransformState]
182
+ ):
183
+ """Apply affine transformation via matrix multiplication: y = Ax or y = Ax + B.
184
+
185
+ Use this transformer when you need full matrix transformations that mix
186
+ channels (off-diagonal weights), such as spatial filters or projections.
187
+
188
+ For simple per-channel scaling and offset where each output channel depends
189
+ only on its corresponding input channel (diagonal weight matrix), use
190
+ :obj:`LinearTransformTransformer` instead, which is more efficient.
191
+
192
+ The weights matrix can include an offset row (stacked as [A|B]) where the
193
+ input is automatically augmented with a column of ones to compute y = Ax + B.
194
+ """
195
+
196
+ def __call__(self, message: AxisArray) -> AxisArray:
197
+ # Override __call__ so we can shortcut if weights are None.
198
+ if self.settings.weights is None or (
199
+ isinstance(self.settings.weights, str) and self.settings.weights == "passthrough"
200
+ ):
201
+ return message
202
+ return super().__call__(message)
203
+
204
+ def _hash_message(self, message: AxisArray) -> int:
205
+ return hash(message.key)
206
+
207
+ def _reset_state(self, message: AxisArray) -> None:
208
+ weights = self.settings.weights
209
+ if isinstance(weights, str):
210
+ weights = Path(os.path.abspath(os.path.expanduser(weights)))
211
+ if isinstance(weights, Path):
212
+ weights = np.loadtxt(weights, delimiter=",")
213
+ if not self.settings.right_multiply:
214
+ weights = weights.T
215
+ if weights is not None:
216
+ weights = np.ascontiguousarray(weights)
217
+
218
+ self._state.weights = weights
219
+
220
+ # Note: If weights were scipy.sparse BSR then maybe we could use automate this next part.
221
+ # However, that would break compatibility with Array API.
222
+
223
+ # --- Block-diagonal cluster detection ---
224
+ # Clusters are a list of (input_indices, output_indices) tuples.
225
+ n_in, n_out = weights.shape
226
+ if self.settings.channel_clusters is not None:
227
+ # Validate input index bounds
228
+ all_in = np.concatenate([np.asarray(group) for group in self.settings.channel_clusters])
229
+ if np.any((all_in < 0) | (all_in >= n_in)):
230
+ raise ValueError(
231
+ "channel_clusters contains out-of-range input indices " f"(valid range: 0..{n_in - 1})"
232
+ )
233
+
234
+ # Derive output indices from non-zero weights for each input cluster
235
+ clusters = []
236
+ for group in self.settings.channel_clusters:
237
+ in_idx = np.asarray(group)
238
+ out_idx = np.where(np.any(weights[in_idx, :] != 0, axis=0))[0]
239
+ clusters.append((in_idx, out_idx))
240
+
241
+ max_cross = _max_cross_cluster_weight(weights, clusters)
242
+ if max_cross > 0:
243
+ ez.logger.warning(
244
+ f"Non-zero cross-cluster weights detected (max abs: {max_cross:.2e}). "
245
+ "These will be ignored in block-diagonal multiplication."
246
+ )
247
+ else:
248
+ clusters = _find_block_diagonal_clusters(weights)
249
+ if clusters is not None:
250
+ ez.logger.info(
251
+ f"Auto-detected {len(clusters)} block-diagonal clusters "
252
+ f"(sizes: {[(len(i), len(o)) for i, o in clusters]})"
253
+ )
254
+
255
+ # Merge small clusters to avoid excessive loop overhead
256
+ if clusters is not None:
257
+ clusters = _merge_small_clusters(clusters, self.settings.min_cluster_size)
258
+
259
+ if clusters is not None and len(clusters) > 1:
260
+ self._state.n_out = n_out
261
+ self._state.clusters = [
262
+ (in_idx, out_idx, np.ascontiguousarray(weights[np.ix_(in_idx, out_idx)]))
263
+ for in_idx, out_idx in clusters
264
+ ]
265
+ self._state.weights = None
266
+ else:
267
+ self._state.clusters = None
268
+
269
+ # --- Axis label handling (for non-square transforms, non-cluster path) ---
270
+ axis = self.settings.axis or message.dims[-1]
271
+ if axis in message.axes and hasattr(message.axes[axis], "data") and n_in != n_out:
272
+ in_labels = message.axes[axis].data
273
+ new_labels = []
274
+ if len(in_labels) != n_in:
275
+ ez.logger.warning(f"Received {len(in_labels)} for {n_in} inputs. Check upstream labels.")
276
+ else:
277
+ b_filled_outputs = np.any(weights, axis=0)
278
+ b_used_inputs = np.any(weights, axis=1)
279
+ if np.all(b_used_inputs) and np.all(b_filled_outputs):
280
+ new_labels = []
281
+ elif np.all(b_used_inputs):
282
+ in_ix = 0
283
+ new_labels = []
284
+ for out_ix in range(n_out):
285
+ if b_filled_outputs[out_ix]:
286
+ new_labels.append(in_labels[in_ix])
287
+ in_ix += 1
288
+ else:
289
+ new_labels.append("")
290
+ elif np.all(b_filled_outputs):
291
+ new_labels = np.array(in_labels)[b_used_inputs]
292
+
293
+ self._state.new_axis = replace(message.axes[axis], data=np.array(new_labels))
294
+
295
+ # Convert to match message.data namespace for efficient operations in _process
296
+ xp = get_namespace(message.data)
297
+ if self._state.weights is not None:
298
+ self._state.weights = xp.asarray(self._state.weights)
299
+ if self._state.clusters is not None:
300
+ self._state.clusters = [
301
+ (xp.asarray(in_idx), xp.asarray(out_idx), xp.asarray(sub_w))
302
+ for in_idx, out_idx, sub_w in self._state.clusters
303
+ ]
304
+
305
+ def _block_diagonal_matmul(self, xp, data, axis_idx):
306
+ """Perform matmul using block-diagonal decomposition.
307
+
308
+ For each cluster, gathers input channels via ``xp.take``, performs a
309
+ matmul with the cluster's sub-weight matrix, and writes the result
310
+ directly into the pre-allocated output at the cluster's output indices.
311
+ Omitted output channels naturally remain zero.
312
+ """
313
+ needs_permute = axis_idx not in [-1, data.ndim - 1]
314
+ if needs_permute:
315
+ dim_perm = list(range(data.ndim))
316
+ dim_perm.append(dim_perm.pop(axis_idx))
317
+ data = xp.permute_dims(data, dim_perm)
318
+
319
+ # Pre-allocate output (omitted channels stay zero)
320
+ out_shape = data.shape[:-1] + (self._state.n_out,)
321
+ result = xp.zeros(out_shape, dtype=data.dtype)
322
+
323
+ for in_idx, out_idx, sub_weights in self._state.clusters:
324
+ chunk = xp.take(data, in_idx, axis=data.ndim - 1)
325
+ result[..., out_idx] = xp.matmul(chunk, sub_weights)
326
+
327
+ if needs_permute:
328
+ inv_dim_perm = list(range(result.ndim))
329
+ inv_dim_perm.insert(axis_idx, inv_dim_perm.pop(-1))
330
+ result = xp.permute_dims(result, inv_dim_perm)
331
+
332
+ return result
333
+
334
+ def _process(self, message: AxisArray) -> AxisArray:
335
+ xp = get_namespace(message.data)
336
+ axis = self.settings.axis or message.dims[-1]
337
+ axis_idx = message.get_axis_idx(axis)
338
+ data = message.data
339
+
340
+ if self._state.clusters is not None:
341
+ data = self._block_diagonal_matmul(xp, data, axis_idx)
342
+ else:
343
+ if data.shape[axis_idx] == (self._state.weights.shape[0] - 1):
344
+ # The weights are stacked A|B where A is the transform and B is a single row
345
+ # in the equation y = Ax + B. This supports NeuroKey's weights matrices.
346
+ sample_shape = data.shape[:axis_idx] + (1,) + data.shape[axis_idx + 1 :]
347
+ data = xp.concat((data, xp.ones(sample_shape, dtype=data.dtype)), axis=axis_idx)
348
+
349
+ if axis_idx in [-1, len(message.dims) - 1]:
350
+ data = xp.matmul(data, self._state.weights)
351
+ else:
352
+ perm = list(range(data.ndim))
353
+ perm.append(perm.pop(axis_idx))
354
+ data = xp.permute_dims(data, perm)
355
+ data = xp.matmul(data, self._state.weights)
356
+ inv_perm = list(range(data.ndim))
357
+ inv_perm.insert(axis_idx, inv_perm.pop(-1))
358
+ data = xp.permute_dims(data, inv_perm)
359
+
360
+ replace_kwargs = {"data": data}
361
+ if self._state.new_axis is not None:
362
+ replace_kwargs["axes"] = {**message.axes, axis: self._state.new_axis}
363
+
364
+ return replace(message, **replace_kwargs)
365
+
366
+
367
+ class AffineTransform(BaseTransformerUnit[AffineTransformSettings, AxisArray, AxisArray, AffineTransformTransformer]):
368
+ SETTINGS = AffineTransformSettings
369
+
370
+
371
+ def affine_transform(
372
+ weights: np.ndarray | str | Path,
373
+ axis: str | None = None,
374
+ right_multiply: bool = True,
375
+ channel_clusters: list[list[int]] | None = None,
376
+ min_cluster_size: int = 32,
377
+ ) -> AffineTransformTransformer:
378
+ """
379
+ Perform affine transformations on streaming data.
380
+
381
+ Args:
382
+ weights: An array of weights or a path to a file with weights compatible with np.loadtxt.
383
+ axis: The name of the axis to apply the transformation to. Defaults to the leading (0th) axis in the array.
384
+ right_multiply: Set False to transpose the weights before applying.
385
+ channel_clusters: Optional explicit channel cluster specification. See
386
+ :attr:`AffineTransformSettings.channel_clusters`.
387
+ min_cluster_size: Minimum channels per cluster; smaller clusters are merged. See
388
+ :attr:`AffineTransformSettings.min_cluster_size`.
389
+
390
+ Returns:
391
+ :obj:`AffineTransformTransformer`.
392
+ """
393
+ return AffineTransformTransformer(
394
+ AffineTransformSettings(
395
+ weights=weights,
396
+ axis=axis,
397
+ right_multiply=right_multiply,
398
+ channel_clusters=channel_clusters,
399
+ min_cluster_size=min_cluster_size,
400
+ )
401
+ )
402
+
403
+
404
+ class CommonRereferenceSettings(ez.Settings):
405
+ """
406
+ Settings for :obj:`CommonRereference`
407
+ """
408
+
409
+ mode: str = "mean"
410
+ """The statistical mode to apply -- either "mean" or "median"."""
411
+
412
+ axis: str | None = None
413
+ """The name of the axis to apply the transformation to."""
414
+
415
+ include_current: bool = True
416
+ """Set False to exclude each channel from participating in the calculation of its reference."""
417
+
418
+ channel_clusters: list[list[int]] | None = None
419
+ """Optional channel clusters for per-cluster rereferencing. Each element is a
420
+ list of channel indices forming one cluster. The common reference is computed
421
+ independently within each cluster. If None, all channels form a single cluster."""
422
+
423
+
424
+ @processor_state
425
+ class CommonRereferenceState:
426
+ clusters: list | None = None
427
+ """list of xp arrays of channel indices, one per cluster."""
428
+
429
+
430
+ class CommonRereferenceTransformer(
431
+ BaseStatefulTransformer[CommonRereferenceSettings, AxisArray, AxisArray, CommonRereferenceState]
432
+ ):
433
+ def _hash_message(self, message: AxisArray) -> int:
434
+ axis = self.settings.axis or message.dims[-1]
435
+ axis_idx = message.get_axis_idx(axis)
436
+ return hash((message.key, message.data.shape[axis_idx]))
437
+
438
+ def _reset_state(self, message: AxisArray) -> None:
439
+ xp = get_namespace(message.data)
440
+ axis = self.settings.axis or message.dims[-1]
441
+ axis_idx = message.get_axis_idx(axis)
442
+ n_chans = message.data.shape[axis_idx]
443
+
444
+ if self.settings.channel_clusters is not None:
445
+ self._state.clusters = [xp.asarray(group) for group in self.settings.channel_clusters]
446
+ else:
447
+ self._state.clusters = [xp.arange(n_chans)]
448
+
449
+ def _process(self, message: AxisArray) -> AxisArray:
450
+ if self.settings.mode == "passthrough":
451
+ return message
452
+
453
+ xp = get_namespace(message.data)
454
+ axis = self.settings.axis or message.dims[-1]
455
+ axis_idx = message.get_axis_idx(axis)
456
+ func = {"mean": xp.mean, "median": np.median}[self.settings.mode]
457
+
458
+ # Use result_type to match dtype promotion from data - float operations.
459
+ out_dtype = np.result_type(message.data.dtype, np.float64)
460
+ output = xp.zeros(message.data.shape, dtype=out_dtype)
461
+
462
+ for cluster_idx in self._state.clusters:
463
+ cluster_data = xp.take(message.data, cluster_idx, axis=axis_idx)
464
+ ref_data = func(cluster_data, axis=axis_idx, keepdims=True)
465
+
466
+ if not self.settings.include_current:
467
+ N = cluster_data.shape[axis_idx]
468
+ ref_data = (N / (N - 1)) * ref_data - cluster_data / (N - 1)
469
+
470
+ # Write per-cluster result into output at the correct axis position
471
+ idx = [slice(None)] * output.ndim
472
+ idx[axis_idx] = cluster_idx
473
+ output[tuple(idx)] = cluster_data - ref_data
474
+
475
+ return replace(message, data=output)
476
+
477
+
478
+ class CommonRereference(
479
+ BaseTransformerUnit[CommonRereferenceSettings, AxisArray, AxisArray, CommonRereferenceTransformer]
480
+ ):
481
+ SETTINGS = CommonRereferenceSettings
482
+
483
+
484
+ def common_rereference(
485
+ mode: str = "mean",
486
+ axis: str | None = None,
487
+ include_current: bool = True,
488
+ channel_clusters: list[list[int]] | None = None,
489
+ ) -> CommonRereferenceTransformer:
490
+ """
491
+ Perform common average referencing (CAR) on streaming data.
492
+
493
+ Args:
494
+ mode: The statistical mode to apply -- either "mean" or "median"
495
+ axis: The name of the axis to apply the transformation to.
496
+ include_current: Set False to exclude each channel from participating in the calculation of its reference.
497
+ channel_clusters: Optional channel clusters for per-cluster rereferencing. See
498
+ :attr:`CommonRereferenceSettings.channel_clusters`.
499
+
500
+ Returns:
501
+ :obj:`CommonRereferenceTransformer`
502
+ """
503
+ return CommonRereferenceTransformer(
504
+ CommonRereferenceSettings(
505
+ mode=mode, axis=axis, include_current=include_current, channel_clusters=channel_clusters
506
+ )
507
+ )