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

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