specmod 0.3.0__tar.gz → 0.4.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 (270) hide show
  1. specmod-0.4.0/.release-please-manifest.json +3 -0
  2. {specmod-0.3.0 → specmod-0.4.0}/CHANGELOG.md +23 -0
  3. {specmod-0.3.0 → specmod-0.4.0}/PKG-INFO +1 -1
  4. {specmod-0.3.0 → specmod-0.4.0}/docs/REFACTOR_PLAN.md +99 -20
  5. {specmod-0.3.0 → specmod-0.4.0}/docs/choosing-a-transform.md +84 -0
  6. {specmod-0.3.0 → specmod-0.4.0}/docs/development.md +2 -1
  7. {specmod-0.3.0 → specmod-0.4.0}/docs/roadmap.md +27 -8
  8. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/config/sections.py +32 -4
  9. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/core/collection.py +103 -17
  10. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/pipeline.py +41 -0
  11. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/smoothing/__init__.py +14 -2
  12. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/smoothing/base.py +16 -1
  13. specmod-0.4.0/src/specmod/smoothing/log_window.py +215 -0
  14. specmod-0.4.0/src/specmod/smoothing/savitzky_golay.py +127 -0
  15. {specmod-0.3.0 → specmod-0.4.0}/tests/golden/motion_reference.json +140 -0
  16. {specmod-0.3.0 → specmod-0.4.0}/tests/golden/pipeline_reference.json +140 -0
  17. {specmod-0.3.0 → specmod-0.4.0}/tests/golden/window_reference.json +140 -0
  18. {specmod-0.3.0 → specmod-0.4.0}/tests/test_config.py +99 -1
  19. {specmod-0.3.0 → specmod-0.4.0}/tests/test_golden_reference.py +70 -0
  20. {specmod-0.3.0 → specmod-0.4.0}/tests/test_pipeline.py +123 -1
  21. {specmod-0.3.0 → specmod-0.4.0}/tests/test_release_config.py +88 -0
  22. specmod-0.4.0/tests/test_smoothing.py +431 -0
  23. specmod-0.4.0/tests/test_tools_track_the_api.py +125 -0
  24. {specmod-0.3.0 → specmod-0.4.0}/tools/make_golden.py +34 -4
  25. {specmod-0.3.0 → specmod-0.4.0}/tools/measure_docs.py +106 -6
  26. specmod-0.3.0/.release-please-manifest.json +0 -3
  27. specmod-0.3.0/tests/test_smoothing.py +0 -226
  28. {specmod-0.3.0 → specmod-0.4.0}/.git-blame-ignore-revs +0 -0
  29. {specmod-0.3.0 → specmod-0.4.0}/.github/workflows/build.yml +0 -0
  30. {specmod-0.3.0 → specmod-0.4.0}/.github/workflows/docs.yml +0 -0
  31. {specmod-0.3.0 → specmod-0.4.0}/.github/workflows/release.yml +0 -0
  32. {specmod-0.3.0 → specmod-0.4.0}/.github/workflows/test.yml +0 -0
  33. {specmod-0.3.0 → specmod-0.4.0}/.gitignore +0 -0
  34. {specmod-0.3.0 → specmod-0.4.0}/.pre-commit-config.yaml +0 -0
  35. {specmod-0.3.0 → specmod-0.4.0}/.readthedocs.yaml +0 -0
  36. {specmod-0.3.0 → specmod-0.4.0}/AGENTS.md +0 -0
  37. {specmod-0.3.0 → specmod-0.4.0}/CITATION.cff +0 -0
  38. {specmod-0.3.0 → specmod-0.4.0}/CLAUDE.md +0 -0
  39. {specmod-0.3.0 → specmod-0.4.0}/CONTRIBUTING.md +0 -0
  40. {specmod-0.3.0 → specmod-0.4.0}/LICENSE +0 -0
  41. {specmod-0.3.0 → specmod-0.4.0}/README.md +0 -0
  42. {specmod-0.3.0 → specmod-0.4.0}/datasets/magna_2020.toml +0 -0
  43. {specmod-0.3.0 → specmod-0.4.0}/datasets/pnr_2019.toml +0 -0
  44. {specmod-0.3.0 → specmod-0.4.0}/docs/_builders/_notebook.py +0 -0
  45. {specmod-0.3.0 → specmod-0.4.0}/docs/_builders/choosing_a_transform.py +0 -0
  46. {specmod-0.3.0 → specmod-0.4.0}/docs/_builders/specmod_tutorial.py +0 -0
  47. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/academic.css +0 -0
  48. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/fonts/OFL-fira-code.txt +0 -0
  49. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/fonts/OFL-merriweather.txt +0 -0
  50. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/fonts/OFL-open-sans.txt +0 -0
  51. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/fonts/README.md +0 -0
  52. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/fonts/fira-code-400-greek.woff2 +0 -0
  53. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/fonts/fira-code-400-latin-ext.woff2 +0 -0
  54. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/fonts/fira-code-400-latin.woff2 +0 -0
  55. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/fonts/merriweather-latin-ext.woff2 +0 -0
  56. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/fonts/merriweather-latin.woff2 +0 -0
  57. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/fonts/open-sans-greek.woff2 +0 -0
  58. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/fonts/open-sans-latin-ext.woff2 +0 -0
  59. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/fonts/open-sans-latin.woff2 +0 -0
  60. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/fonts.css +0 -0
  61. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/LICENSE +0 -0
  62. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/README.md +0 -0
  63. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
  64. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
  65. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
  66. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
  67. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
  68. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
  69. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
  70. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
  71. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
  72. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
  73. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
  74. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
  75. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
  76. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
  77. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
  78. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
  79. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
  80. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
  81. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
  82. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
  83. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/katex/katex.min.css +0 -0
  84. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/specmod-logo-academic-dark-mark.svg +0 -0
  85. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/specmod-logo-academic-dark.svg +0 -0
  86. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/specmod-logo-academic-mark.svg +0 -0
  87. {specmod-0.3.0 → specmod-0.4.0}/docs/_static/specmod-logo-academic.svg +0 -0
  88. {specmod-0.3.0 → specmod-0.4.0}/docs/api.md +0 -0
  89. {specmod-0.3.0 → specmod-0.4.0}/docs/branding.md +0 -0
  90. {specmod-0.3.0 → specmod-0.4.0}/docs/conf.py +0 -0
  91. {specmod-0.3.0 → specmod-0.4.0}/docs/contributing.md +0 -0
  92. {specmod-0.3.0 → specmod-0.4.0}/docs/documentation.md +0 -0
  93. {specmod-0.3.0 → specmod-0.4.0}/docs/getting-started.md +0 -0
  94. {specmod-0.3.0 → specmod-0.4.0}/docs/guides.md +0 -0
  95. {specmod-0.3.0 → specmod-0.4.0}/docs/index.md +0 -0
  96. {specmod-0.3.0 → specmod-0.4.0}/docs/notebooks/choosing-a-transform.ipynb +0 -0
  97. {specmod-0.3.0 → specmod-0.4.0}/docs/notes/api-audit.md +0 -0
  98. {specmod-0.3.0 → specmod-0.4.0}/docs/notes/window-position.md +0 -0
  99. {specmod-0.3.0 → specmod-0.4.0}/docs/pick-formats.md +0 -0
  100. {specmod-0.3.0 → specmod-0.4.0}/docs/processing.md +0 -0
  101. {specmod-0.3.0 → specmod-0.4.0}/docs/releasing-data.md +0 -0
  102. {specmod-0.3.0 → specmod-0.4.0}/docs/releasing.md +0 -0
  103. {specmod-0.3.0 → specmod-0.4.0}/docs/upgrading.md +0 -0
  104. {specmod-0.3.0 → specmod-0.4.0}/pyproject.toml +0 -0
  105. {specmod-0.3.0 → specmod-0.4.0}/release-please-config.json +0 -0
  106. {specmod-0.3.0 → specmod-0.4.0}/requirements.txt +0 -0
  107. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/__init__.py +0 -0
  108. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/_vendor/__init__.py +0 -0
  109. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/_vendor/qiinv.py +0 -0
  110. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/acquire.py +0 -0
  111. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/api.py +0 -0
  112. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/cli.py +0 -0
  113. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/config/__init__.py +0 -0
  114. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/config/layers.py +0 -0
  115. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/config/provenance.py +0 -0
  116. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/config/serialize.py +0 -0
  117. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/core/__init__.py +0 -0
  118. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/core/bandwidth.py +0 -0
  119. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/core/noise.py +0 -0
  120. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/core/scalogram.py +0 -0
  121. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/core/spectrum.py +0 -0
  122. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/core/units.py +0 -0
  123. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/datasets.py +0 -0
  124. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/distance.py +0 -0
  125. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/exceptions.py +0 -0
  126. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/fitting/__init__.py +0 -0
  127. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/fitting/base.py +0 -0
  128. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/fitting/event.py +0 -0
  129. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/fitting/guess.py +0 -0
  130. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/fitting/spectrum.py +0 -0
  131. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/io/__init__.py +0 -0
  132. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/io/backend.py +0 -0
  133. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/io/hdf5.py +0 -0
  134. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/io/layout.py +0 -0
  135. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/magnitude.py +0 -0
  136. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/picks/__init__.py +0 -0
  137. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/picks/base.py +0 -0
  138. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/picks/delimited.py +0 -0
  139. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/picks/events.py +0 -0
  140. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/picks/resolution.py +0 -0
  141. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/picks/snuffler.py +0 -0
  142. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/plotting/__init__.py +0 -0
  143. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/plotting/grid.py +0 -0
  144. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/plotting/pair.py +0 -0
  145. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/plotting/style.py +0 -0
  146. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/preprocess.py +0 -0
  147. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/smoothing/konno_ohmachi.py +0 -0
  148. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/smoothing/log_bins.py +0 -0
  149. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/sources/__init__.py +0 -0
  150. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/sources/attenuation.py +0 -0
  151. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/sources/composite.py +0 -0
  152. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/sources/motion.py +0 -0
  153. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/sources/source.py +0 -0
  154. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/spreading.py +0 -0
  155. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/staged.py +0 -0
  156. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/tables.py +0 -0
  157. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/transforms/__init__.py +0 -0
  158. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/transforms/base.py +0 -0
  159. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/transforms/cwt.py +0 -0
  160. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/transforms/fft.py +0 -0
  161. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/transforms/multitaper.py +0 -0
  162. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/transforms/prieto.py +0 -0
  163. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/transforms/quadratic.py +0 -0
  164. {specmod-0.3.0 → specmod-0.4.0}/src/specmod/utils.py +0 -0
  165. {specmod-0.3.0 → specmod-0.4.0}/stubs/README.md +0 -0
  166. {specmod-0.3.0 → specmod-0.4.0}/stubs/lmfit/__init__.pyi +0 -0
  167. {specmod-0.3.0 → specmod-0.4.0}/stubs/lmfit/model.pyi +0 -0
  168. {specmod-0.3.0 → specmod-0.4.0}/stubs/lmfit/parameter.pyi +0 -0
  169. {specmod-0.3.0 → specmod-0.4.0}/stubs/obspy/__init__.pyi +0 -0
  170. {specmod-0.3.0 → specmod-0.4.0}/stubs/obspy/clients/__init__.pyi +0 -0
  171. {specmod-0.3.0 → specmod-0.4.0}/stubs/obspy/clients/fdsn/__init__.pyi +0 -0
  172. {specmod-0.3.0 → specmod-0.4.0}/stubs/obspy/core/__init__.pyi +0 -0
  173. {specmod-0.3.0 → specmod-0.4.0}/stubs/obspy/core/event.pyi +0 -0
  174. {specmod-0.3.0 → specmod-0.4.0}/stubs/obspy/core/inventory.pyi +0 -0
  175. {specmod-0.3.0 → specmod-0.4.0}/stubs/obspy/core/stream.pyi +0 -0
  176. {specmod-0.3.0 → specmod-0.4.0}/stubs/obspy/core/trace.pyi +0 -0
  177. {specmod-0.3.0 → specmod-0.4.0}/stubs/obspy/core/utcdatetime.pyi +0 -0
  178. {specmod-0.3.0 → specmod-0.4.0}/stubs/obspy/core/util/__init__.pyi +0 -0
  179. {specmod-0.3.0 → specmod-0.4.0}/stubs/obspy/core/util/base.pyi +0 -0
  180. {specmod-0.3.0 → specmod-0.4.0}/stubs/obspy/geodetics/__init__.pyi +0 -0
  181. {specmod-0.3.0 → specmod-0.4.0}/stubs/obspy/signal/__init__.pyi +0 -0
  182. {specmod-0.3.0 → specmod-0.4.0}/stubs/obspy/signal/konnoohmachismoothing.pyi +0 -0
  183. {specmod-0.3.0 → specmod-0.4.0}/studies/magna_2020_paper.toml +0 -0
  184. {specmod-0.3.0 → specmod-0.4.0}/tests/__init__.py +0 -0
  185. {specmod-0.3.0 → specmod-0.4.0}/tests/conftest.py +0 -0
  186. {specmod-0.3.0 → specmod-0.4.0}/tests/test_acquire.py +0 -0
  187. {specmod-0.3.0 → specmod-0.4.0}/tests/test_ambient_state.py +0 -0
  188. {specmod-0.3.0 → specmod-0.4.0}/tests/test_api_surface.py +0 -0
  189. {specmod-0.3.0 → specmod-0.4.0}/tests/test_collection.py +0 -0
  190. {specmod-0.3.0 → specmod-0.4.0}/tests/test_cwt.py +0 -0
  191. {specmod-0.3.0 → specmod-0.4.0}/tests/test_datasets.py +0 -0
  192. {specmod-0.3.0 → specmod-0.4.0}/tests/test_docs_are_current.py +0 -0
  193. {specmod-0.3.0 → specmod-0.4.0}/tests/test_end_to_end.py +0 -0
  194. {specmod-0.3.0 → specmod-0.4.0}/tests/test_fitting_defaults.py +0 -0
  195. {specmod-0.3.0 → specmod-0.4.0}/tests/test_import.py +0 -0
  196. {specmod-0.3.0 → specmod-0.4.0}/tests/test_io_and_plotting.py +0 -0
  197. {specmod-0.3.0 → specmod-0.4.0}/tests/test_legacy_fixes.py +0 -0
  198. {specmod-0.3.0 → specmod-0.4.0}/tests/test_magnitude.py +0 -0
  199. {specmod-0.3.0 → specmod-0.4.0}/tests/test_make_golden.py +0 -0
  200. {specmod-0.3.0 → specmod-0.4.0}/tests/test_pick_plugins.py +0 -0
  201. {specmod-0.3.0 → specmod-0.4.0}/tests/test_pick_readers.py +0 -0
  202. {specmod-0.3.0 → specmod-0.4.0}/tests/test_picks.py +0 -0
  203. {specmod-0.3.0 → specmod-0.4.0}/tests/test_pipeline_smoke.py +0 -0
  204. {specmod-0.3.0 → specmod-0.4.0}/tests/test_preprocess.py +0 -0
  205. {specmod-0.3.0 → specmod-0.4.0}/tests/test_prieto.py +0 -0
  206. {specmod-0.3.0 → specmod-0.4.0}/tests/test_quadratic.py +0 -0
  207. {specmod-0.3.0 → specmod-0.4.0}/tests/test_sources.py +0 -0
  208. {specmod-0.3.0 → specmod-0.4.0}/tests/test_spectral_wiring.py +0 -0
  209. {specmod-0.3.0 → specmod-0.4.0}/tests/test_staged.py +0 -0
  210. {specmod-0.3.0 → specmod-0.4.0}/tests/test_stubs.py +0 -0
  211. {specmod-0.3.0 → specmod-0.4.0}/tests/test_transforms.py +0 -0
  212. {specmod-0.3.0 → specmod-0.4.0}/tests/test_tutorial.py +0 -0
  213. {specmod-0.3.0 → specmod-0.4.0}/tests/test_typing_backlog.py +0 -0
  214. {specmod-0.3.0 → specmod-0.4.0}/tests/test_utils.py +0 -0
  215. {specmod-0.3.0 → specmod-0.4.0}/tests/test_versioning.py +0 -0
  216. {specmod-0.3.0 → specmod-0.4.0}/tools/check_built_version.py +0 -0
  217. {specmod-0.3.0 → specmod-0.4.0}/tools/check_floors.py +0 -0
  218. {specmod-0.3.0 → specmod-0.4.0}/tools/make_logo.py +0 -0
  219. {specmod-0.3.0 → specmod-0.4.0}/tools/vendor_fonts.py +0 -0
  220. {specmod-0.3.0 → specmod-0.4.0}/tools/vendor_katex.py +0 -0
  221. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/picks/2019-08-26T07:30:47.000000Z.picks +0 -0
  222. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/picks/2019-08-26T07:30:47.000000Z.xml +0 -0
  223. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/stations/inventory.xml +0 -0
  224. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L001..HHE_2019-08-26T07:30:47.000000Z +0 -0
  225. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L001..HHN_2019-08-26T07:30:47.000000Z +0 -0
  226. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L001..HHZ_2019-08-26T07:30:47.000000Z +0 -0
  227. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L002..HHE_2019-08-26T07:30:47.000000Z +0 -0
  228. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L002..HHN_2019-08-26T07:30:47.000000Z +0 -0
  229. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L002..HHZ_2019-08-26T07:30:47.000000Z +0 -0
  230. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L006..HHE_2019-08-26T07:30:47.000000Z +0 -0
  231. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L006..HHN_2019-08-26T07:30:47.000000Z +0 -0
  232. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L006..HHZ_2019-08-26T07:30:47.000000Z +0 -0
  233. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L007..HHE_2019-08-26T07:30:47.000000Z +0 -0
  234. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L007..HHN_2019-08-26T07:30:47.000000Z +0 -0
  235. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L007..HHZ_2019-08-26T07:30:47.000000Z +0 -0
  236. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L008..HHE_2019-08-26T07:30:47.000000Z +0 -0
  237. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L008..HHN_2019-08-26T07:30:47.000000Z +0 -0
  238. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L008..HHZ_2019-08-26T07:30:47.000000Z +0 -0
  239. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L009..HHE_2019-08-26T07:30:47.000000Z +0 -0
  240. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L009..HHN_2019-08-26T07:30:47.000000Z +0 -0
  241. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.L009..HHZ_2019-08-26T07:30:47.000000Z +0 -0
  242. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.LD06..HH1_2019-08-26T07:30:47.000000Z +0 -0
  243. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.LD06..HH2_2019-08-26T07:30:47.000000Z +0 -0
  244. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/LV.LD06..HH3_2019-08-26T07:30:47.000000Z +0 -0
  245. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ01.00.HHE_2019-08-26T07:30:47.000000Z +0 -0
  246. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ01.00.HHN_2019-08-26T07:30:47.000000Z +0 -0
  247. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ01.00.HHZ_2019-08-26T07:30:47.000000Z +0 -0
  248. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ02.00.HHZ_2019-08-26T07:30:47.000000Z +0 -0
  249. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ03.00.HHE_2019-08-26T07:30:47.000000Z +0 -0
  250. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ03.00.HHN_2019-08-26T07:30:47.000000Z +0 -0
  251. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ03.00.HHZ_2019-08-26T07:30:47.000000Z +0 -0
  252. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ04.00.HHE_2019-08-26T07:30:47.000000Z +0 -0
  253. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ04.00.HHN_2019-08-26T07:30:47.000000Z +0 -0
  254. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ04.00.HHZ_2019-08-26T07:30:47.000000Z +0 -0
  255. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ05.00.HHE_2019-08-26T07:30:47.000000Z +0 -0
  256. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ05.00.HHN_2019-08-26T07:30:47.000000Z +0 -0
  257. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ05.00.HHZ_2019-08-26T07:30:47.000000Z +0 -0
  258. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ06.00.HHE_2019-08-26T07:30:47.000000Z +0 -0
  259. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ06.00.HHN_2019-08-26T07:30:47.000000Z +0 -0
  260. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ06.00.HHZ_2019-08-26T07:30:47.000000Z +0 -0
  261. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ07.00.HHE_2019-08-26T07:30:47.000000Z +0 -0
  262. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ07.00.HHN_2019-08-26T07:30:47.000000Z +0 -0
  263. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ07.00.HHZ_2019-08-26T07:30:47.000000Z +0 -0
  264. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ09.00.HHE_2019-08-26T07:30:47.000000Z +0 -0
  265. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ09.00.HHN_2019-08-26T07:30:47.000000Z +0 -0
  266. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ09.00.HHZ_2019-08-26T07:30:47.000000Z +0 -0
  267. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ10.00.HHE_2019-08-26T07:30:47.000000Z +0 -0
  268. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ10.00.HHN_2019-08-26T07:30:47.000000Z +0 -0
  269. {specmod-0.3.0 → specmod-0.4.0}/tutorial/data/events/2019-08-26T07:30:47.000000Z/waveforms/UR.AQ10.00.HHZ_2019-08-26T07:30:47.000000Z +0 -0
  270. {specmod-0.3.0 → specmod-0.4.0}/tutorial/specmod-tutorial.ipynb +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.4.0"
3
+ }
@@ -9,6 +9,29 @@ release could have this problem — every later one has a real predecessor tag
9
9
  so leave subsequent headings as generated. release-please only prepends to this
10
10
  file, so this edit survives. -->
11
11
 
12
+ ## [0.4.0](https://github.com/sgjholt/SpecMod/compare/v0.3.0...v0.4.0) (2026-09-16)
13
+
14
+
15
+ ### Features
16
+
17
+ * **smoothing:** wire [smoothing] method up, and add three more methods ([fff3ab1](https://github.com/sgjholt/SpecMod/commit/fff3ab1ef01a7f4d942fd776821d0418a0642a55))
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * **config:** refuse a [smoothing] method the pipeline does not apply ([1f18c68](https://github.com/sgjholt/SpecMod/commit/1f18c68a71794c2b1612a7cf0feedcfef39b625b))
23
+ * **test:** compare the roadmap against the published minor, not the proposed one ([1e34b4e](https://github.com/sgjholt/SpecMod/commit/1e34b4e5ab2bdef5d65f6fc0d620a42b9c3891eb))
24
+ * **tools:** call the preprocess API that 0.3.0 left behind ([8501c8e](https://github.com/sgjholt/SpecMod/commit/8501c8ed618e5d652b9eeffc36893115a58fd7d5))
25
+
26
+
27
+ ### Documentation
28
+
29
+ * keep the reasoning out of the docstrings, in comments where it belongs ([2684ec9](https://github.com/sgjholt/SpecMod/commit/2684ec923c8dcf59e882b2d11e9dc11ccbcf3766))
30
+ * say what tagging the frozen commit does and does not trigger ([2273219](https://github.com/sgjholt/SpecMod/commit/2273219e2a78ac1c1562118c2a1b3bce4df597dd))
31
+ * state what the smoothers are, not how they were arrived at ([4c97491](https://github.com/sgjholt/SpecMod/commit/4c97491d50aba5a1468304072ab3c6ed2904f2e0))
32
+ * the 0.3.0 work shipped a week ago, so stop calling it unreleased ([e2aaced](https://github.com/sgjholt/SpecMod/commit/e2aaced546a247b2c5967f01586c19928bcfe297))
33
+ * v0.1.1 is master's tip, which is what freezing it was for ([db50972](https://github.com/sgjholt/SpecMod/commit/db509729f3b7877a20b5691dddec8a6e139fb466))
34
+
12
35
  ## [0.3.0](https://github.com/sgjholt/SpecMod/compare/v0.2.3...v0.3.0) (2026-09-06)
13
36
 
14
37
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: specmod
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: A Python toolbox for processing and modelling seismic spectra
5
5
  Project-URL: Homepage, https://github.com/sgjholt/SpecMod
6
6
  Project-URL: Repository, https://github.com/sgjholt/SpecMod
@@ -1087,7 +1087,7 @@ and there are presumably more in your working directories.
1087
1087
  Two ways out, and the first is much better:
1088
1088
 
1089
1089
  1. **Convert in the old environment.** Phase 0 is already building a Docker image
1090
- where the 0.1.0 code runs (§6.7). Add a small
1090
+ where the 0.1.1 code runs (§6.7). Add a small
1091
1091
  `scripts/convert_legacy_spec.py` to it that loads `.spec` files with the old
1092
1092
  classes present and writes the new HDF5 format. This is a one-shot migration
1093
1093
  with no lasting cost to the codebase.
@@ -2742,13 +2742,30 @@ Two further problems with reproducing the published run from this repository:
2742
2742
  manuscript filename (`...SRL3`) suggests a third revision, which would also
2743
2743
  explain a 2021 code state behind a 2020 access date.
2744
2744
 
2745
- Practical consequence: **the candidate is `ba3f7ec` or later, not the
2745
+ The evidence narrows it: **the candidate is `ba3f7ec` or later, not the
2746
2746
  pre-November-2020 commits**, because the earlier code cannot perform the
2747
2747
  workflow the paper describes. Only `09f57b9` (removed cython) changes
2748
2748
  `specmod/` after that, so the realistic candidates are `ba3f7ec` and
2749
- `453c77c`. Pick one, record the reasoning, and move on — this is the
2750
- strongest possible argument for the `v0.1.0` tag (§6.7) and for `hatch-vcs`
2751
- (§6.4): it must never be this hard again.
2749
+ `453c77c`.
2750
+
2751
+ **Answered: `453c77c`** — the tip of `master`, 2021-08-12. On the author's
2752
+ word, and it is why `master` was frozen at that commit rather than at any
2753
+ other: the branch *is* the v0.1.1 record (§6.7). Which of the two the
2754
+ evidence above prefers was never the question it looked like.
2755
+
2756
+ What that settles and what it does not: the code is identified, so §5.2.6
2757
+ step 2 can be run against a known commit. The dating oddity is unchanged —
2758
+ the manuscript's access date still precedes the commit it cites, which the
2759
+ third revision (`...SRL3`) explains without needing the code to have moved.
2760
+ It is recorded rather than resolved, because nothing in the repository can
2761
+ resolve it.
2762
+
2763
+ This is still the strongest possible argument for tagging that commit (§6.7)
2764
+ and for `hatch-vcs` (§6.4): a version that takes a page of archaeology to
2765
+ identify must never happen again. **No `v0.1.1` tag exists yet.** Creating
2766
+ one is safe — `453c77c` is an ancestor of `main`, and every later release
2767
+ tag outranks it for `hatch-vcs`, so nothing derives a version from it — but
2768
+ it is repository state, not something a commit here can carry.
2752
2769
  2. **The full published pipeline is not in this repository.** The two-stage
2753
2770
  inversion (fit Ω/`f_c`/`t*` free, then fix event `f_c` to the
2754
2771
  inverse-hypocentral-distance-weighted mean of station `f_c` and refit) and the
@@ -3414,6 +3431,31 @@ been audited once is worth more than one that reads confidently throughout.
3414
3431
  | "`commitlint`-enforced" again, in §6.4 | The §7 instance was corrected and this one was left standing, in the same document. §6.4 corrected. |
3415
3432
  | `publish.yml` "GitHub Release published → PyPI" | It would never have run. release-please creates the release with the default `GITHUB_TOKEN`, and events triggered by that token do not start a workflow. The publish job moved into `release.yml`, gated on `release_created`. §6.5 corrected. |
3416
3433
 
3434
+ **A fourth of the same shape, found later** — the three above were corrected in
3435
+ the prose; this one was a live setting:
3436
+
3437
+ > `[smoothing] method` — "Name -> smoother, for resolving `SmoothingConfig.method`"
3438
+
3439
+ `SMOOTHERS` existed, `method` validated against its `Literal`, and **no code
3440
+ anywhere read it**. The comparison binned with `log_bin` unconditionally.
3441
+ Measured on the 28 PNR windows, `log_bins`, `konno_ohmachi` and `none` produced
3442
+ bit-identical output — `none` included, which reads as "leave my spectra
3443
+ unsmoothed" while every spectrum went on being binned exactly as before.
3444
+
3445
+ **Fixed rather than documented.** `SpectrumPair.compare` now takes a `smoother`,
3446
+ `[smoothing] method` resolves to one, and the registry gained `log_window`
3447
+ (constant-relative-bandwidth smoothing with a selectable window shape, and a
3448
+ median option) and `savitzky_golay` beside the two that were there.
3449
+ `tests/test_config.py::TestEverySmoothingMethodIsWiredUp` and
3450
+ `tests/test_pipeline.py::TestTheConfiguredSmootherReachesTheComparison` are the
3451
+ mechanism the comment claimed: each accepted value resolves, carries its
3452
+ section's parameters, and survives the whole path on real windows.
3453
+
3454
+ `log_bins` stays `core.collection.log_bin` rather than `smoothing.LogBinner` —
3455
+ the two differ in clamping and in what `n_bins` counts, and the difference is
3456
+ every committed golden number. The default path is unchanged, which the golden
3457
+ suite asserts and which is why this could land without moving a number.
3458
+
3417
3459
  **A claim whose mechanism is absent but whose property holds — for a different
3418
3460
  reason, which matters:**
3419
3461
 
@@ -3460,7 +3502,8 @@ of §5.2, and the two-stage fit API of §5.2.5.
3460
3502
  **`master` is frozen; `main` is the trunk.** `main` was branched from `master` at
3461
3503
  `453c77c` and is where all refactor work lands. `master` is never committed to
3462
3504
  again — it becomes the permanent, named record of the pre-refactor code, doing
3463
- the job a `v0.1.0` tag would have done.
3505
+ the job a `v0.1.1` tag would have done. Its tip, `453c77c`, **is** the v0.1.1
3506
+ the Magna paper cites (§5.2.5, §8).
3464
3507
 
3465
3508
  **Every pull request must target `sgjholt/SpecMod`, never `uofuseismo/SpecMod`.**
3466
3509
  This is not a matter of care — it is a GitHub default working against you.
@@ -3498,7 +3541,7 @@ Two consequences worth acting on:
3498
3541
  - [ ] Settings → Actions → confirm workflows are enabled
3499
3542
  - [ ] Settings → Branches → protect `master` (no pushes, no deletion) so the
3500
3543
  frozen record stays frozen
3501
- - [ ] `git tag -a v0.1.0 453c77c -m "pre-refactor snapshot" && git push origin v0.1.0`
3544
+ - [ ] `git tag -a v0.1.1 453c77c -m "the version the Magna paper cites" && git push origin v0.1.1`
3502
3545
 
3503
3546
  **No Claude session URLs in commit messages, PR bodies, or any other published
3504
3547
  artifact.** The repository is public and those links are private session state. A
@@ -3516,12 +3559,20 @@ needed and neither can be made from a git client:
3516
3559
  convention rather than a guarantee — one absent-minded `git push origin
3517
3560
  master` and the record is gone.
3518
3561
 
3519
- Optionally also tag `453c77c` as `v0.1.0` for good measure; belt and braces, and
3520
- it gives release-please an explicit floor to generate the first changelog from.
3521
- The two are complementary — the tag is immutable, the branch is discoverable.
3562
+ Also tag `453c77c` as `v0.1.1` — no longer optional now that §8 records it as
3563
+ the commit the paper cites. A branch is discoverable and a tag is immutable,
3564
+ and a citation should resolve to the immutable one.
3565
+
3566
+ Tagging it publishes nothing. No workflow triggers on tags; the PyPI upload is
3567
+ gated on release-please's `release_created`, which only a merged release PR on
3568
+ `main` can set; and `git describe` takes the nearest tag, which is a release
3569
+ far ahead of this one. **Do not create a GitHub Release from the tag**, which
3570
+ is the one action that would publish: Zenodo listens to the release webhook and
3571
+ would mint a DOI for the pre-refactor code. The commit has no `pyproject.toml`
3572
+ and is not packageable.
3522
3573
 
3523
3574
  > Note on tooling limits: tag pushes are blocked in the automation environment
3524
- > this plan was written in (HTTP 403 on `push origin v0.1.0`, while branch
3575
+ > this plan was written in (HTTP 403 on `push origin v0.1.1`, while branch
3525
3576
  > pushes to the same remote succeed), which is why `main` exists as a branch
3526
3577
  > rather than the state being pinned by a tag. **This does not affect the release
3527
3578
  > automation in §6.4** — release-please creates tags from inside GitHub Actions
@@ -3531,10 +3582,10 @@ The two are complementary — the tag is immutable, the branch is discoverable.
3531
3582
 
3532
3583
  Worth being precise about what any of this does and does not preserve: it
3533
3584
  preserves the **code**, but the code is not **runnable** — `mtspec` no longer
3534
- builds on any current toolchain (§2.1). Reproducing a 0.1.0 result needs three
3585
+ builds on any current toolchain (§2.1). Reproducing a 0.1.1 result needs three
3535
3586
  things, which is what Phase 0 delivers:
3536
3587
 
3537
- 1. `master` (and optionally `v0.1.0`) — the source.
3588
+ 1. `master` (and the `v0.1.1` tag on its tip) — the source.
3538
3589
  2. A `Dockerfile` pinning `gfortran`, `python 3.9`, `numpy<2` — the environment
3539
3590
  that can still build `mtspec`. Also the only place existing `.spec` pickles
3540
3591
  can be read (§4.6).
@@ -3565,7 +3616,7 @@ Each phase ends green on CI and is independently mergeable.
3565
3616
 
3566
3617
  | Phase | Work | Depends on | Rough size |
3567
3618
  |---|---|---|---|
3568
- | **0. Safety net** | Freeze `master`, default branch → `main`, optional `v0.1.0` tag (§6.7); reproducible legacy env (`Dockerfile`: gfortran + ObsPy 1.2.0 / SciPy 1.4.1 / NumPy 1.18 / pandas 1.0.0 (§5.2.6)); write `datasets/magna_2020.toml` and a first cut of `specmod.acquire`, publish the artifact as a `data-v1` release asset (§5.2); capture golden outputs for PNR **and** Magna; reproduce Table S2 / Figure 2 with 0.1.1 (§5.2.6 step 2); convert any `.spec` files (§4.6) | — | 1.5–2 days |
3619
+ | **0. Safety net** | Freeze `master`, default branch → `main`, optional `v0.1.1` tag (§6.7); reproducible legacy env (`Dockerfile`: gfortran + ObsPy 1.2.0 / SciPy 1.4.1 / NumPy 1.18 / pandas 1.0.0 (§5.2.6)); write `datasets/magna_2020.toml` and a first cut of `specmod.acquire`, publish the artifact as a `data-v1` release asset (§5.2); capture golden outputs for PNR **and** Magna; reproduce Table S2 / Figure 2 with 0.1.1 (§5.2.6 step 2); convert any `.spec` files (§4.6) | — | 1.5–2 days |
3569
3620
  | **1. Make it installable** | `pyproject.toml` + hatch-vcs, `src/` layout, `__init__.py`; ruff config, one-shot `ruff format` + `.git-blame-ignore-revs`, module renames to snake_case; mypy skeleton; pre-commit; `test`/`build` CI; `.gitignore`, `CITATION.cff`; fix the three hard breakages (§1) and the four `F821` bugs ruff finds (§2.5); delete `Tests/Tutorial/`, strip notebook outputs, subset the inventory (§5.1) | 0 | 3–4 days |
3570
3621
  | **2. De-globalise** | `config/` package per §4.8 — semantic groups, layer resolution, `config show`/`freeze`, provenance stamping; remove all module-level config reads (tracked by `PLW0603`); `Motion`/`AmplitudeKind` enums; `Spectrum` as a frozen dataclass with `duration`; mutable class attrs (`RUF012`); `isinstance` checks; `logging`. **Tag `v0.2.0`** | 1 | 3–4 days |
3571
3622
  | **2b. Release plumbing** ✅ | ~~Sphinx skeleton + `pydata-sphinx-theme` + autodoc/napoleon/intersphinx~~ ✅; ~~`docs.yml` → a published site~~ ✅ — Read the Docs rather than GH Pages, for versions (§6.3); ~~release-please + PyPI Trusted Publishing~~ ✅ — one `release.yml`, not two workflows (§6.5); ~~Zenodo webhook~~ ✅ documented. Five repository settings have to be turned on by hand: `docs/releasing.md` lists them. The `ci/` mirror that staged workflow files for hand-copying is gone — it existed because an agent token could not push `.github/workflows/`, and that permission is now granted. Parallel with 2 | 1 | 1–2 days |
@@ -3658,20 +3709,48 @@ changes anything shipped.
3658
3709
  ### Still open
3659
3710
 
3660
3711
  1. **Default estimator.** Multitaper (matching current behaviour) or FFT +
3661
- Konno–Ohmachi (faster, more conventional in engineering seismology)?
3712
+ Konno–Ohmachi (more conventional in engineering seismology)? **No longer
3713
+ blocked** — §6.6's dead `[smoothing] method` is wired up, so the second
3714
+ option is selectable — and now measured, on the 28 PNR windows with each
3715
+ station fitted freely:
3716
+
3717
+ | Against the shipped multitaper + `log_bins` | Median ΔMw | Per-station range | `fc` ratio |
3718
+ |---|---|---|---|
3719
+ | `fft` + `log_bins` | −0.039 | −0.85 to +1.90 | 0.003 to 5.0 |
3720
+ | `fft` + `konno_ohmachi` | +0.027 | −0.52 to +0.51 | 0.038 to 3.2 |
3721
+
3722
+ **The smoothing is what makes the switch defensible, not the estimator.**
3723
+ FFT with the shipped binner moves a station by up to 1.9 magnitude units,
3724
+ because `log_bins` reduces the axis without reducing the variance — 151 log
3725
+ bins over a few hundred Fourier samples is roughly one sample per bin.
3726
+ Pairing it with a real smoother halves the worst excursion and pulls the
3727
+ `fc` range in by a third. The median station barely moves either way.
3728
+
3729
+ What this event cannot settle: the *event* corner is unconstrained under all
3730
+ three (stage-one spread 895% to 1042% of the event value), so the ensemble
3731
+ number is not evidence. Deciding on the ensemble needs an event where it is
3732
+ constrained.
3733
+
3734
+ Speed, measured rather than assumed and far too small to decide on: the
3735
+ transform is 0.209 s against 0.323 s for the 28 windows, and the smoothing
3736
+ adds 0.14 s (`log_bins`) to 0.83 s (`konno_ohmachi`) on top.
3737
+
3662
3738
  2. **Python floor.** 3.11 is proposed. Any users stuck on 3.9/3.10?
3663
3739
  3. **History rewrite.** Deleting the 9.9 MB catalog and stripping the notebook
3664
3740
  (§5.1) shrinks the *working tree* but leaves both in history, so a fresh clone
3665
3741
  still pulls ~15 MiB. Rewriting history with `git-filter-repo` would recover it
3666
3742
  — but it rewrites every SHA, which moves what `master` and `main` currently
3667
- point at and invalidates any `v0.1.0` tag. Given the end state is a ~1 MB repo
3743
+ point at and invalidates any `v0.1.1` tag. Given the end state is a ~1 MB repo
3668
3744
  either way, **the recommendation is to leave history alone**: a one-time 15 MiB
3669
3745
  clone is a much smaller cost than an unrecoverable pre-refactor record. Only
3670
3746
  worth revisiting if the history genuinely becomes a burden.
3671
- 4. **Which commit is "v0.1.1"?** The paper cites SpecMod v0.1.1; no such tag
3672
- exists and the source carries no version string (§5.2.5). Reproducing the
3673
- published run needs that commit identified — by submission date against the
3674
- history, if nothing better is available. Only you can make that call.
3747
+ 4. ~~**Which commit is "v0.1.1"?**~~ **Resolved: `453c77c`**, the tip of
3748
+ `master`, 2021-08-12 — which is what freezing `master` at that commit was
3749
+ for (§6.7). The archaeology in §5.2.5 narrowed it to two candidates and
3750
+ could not choose between them; the answer did not come from the repository
3751
+ and was never going to. §5.2.6 step 2 now has a known commit to run. No
3752
+ `v0.1.1` tag exists yet, and creating one is repository state rather than
3753
+ something a commit can carry.
3675
3754
  5. ~~**Was noise rotation on for the published run?**~~ **Assumed on.** Not
3676
3755
  recalled, so `studies/magna_2020_paper.toml` starts from the shipped values —
3677
3756
  `ROTATE_NOISE = true`, `ROT_METHOD = 2`, `ROT_PARS = {inc = 0.05, space =
@@ -582,6 +582,90 @@ narrow at low frequency, wide at high. It preserves the frequency axis, which is
582
582
  what you want before fitting on the original grid. Bandwidth `b` is inverse:
583
583
  smaller smooths harder; 40 is conventional.
584
584
 
585
+ **`LogWindow`** is the same idea with the window shape exposed: a fixed width in
586
+ octaves, and `boxcar`, `bartlett` (triangular), `hann`, `hamming`, `blackman` or
587
+ `gaussian` across it. Use it when you want the constant-relative-bandwidth
588
+ behaviour of Konno–Ohmachi and a say in how hard the taper is — or simply
589
+ because it is faster, being a sliding window rather than a full weight matrix.
590
+
591
+ Which shape matters less than it looks. On an exact `f**-2` power law, which
592
+ every one of them has to return unchanged, the median residual runs from
593
+ `1.3e-8` dex (`blackman`) to `3.4e-4` dex (`boxcar`) — an ordering that tracks
594
+ the taper's smoothness and a spread far below anything a fit would notice.
595
+ **Pick the width first and the shape second.**
596
+
597
+ `statistic="median"` is the one option that is not an average: the window
598
+ selects the samples and the median decides, so the shape is ignored. It is
599
+ there for spectra carrying spikes — a power-line tone, a dropout, a telemetry
600
+ glitch — which an average spreads across the whole window. With a single 60×
601
+ spike dropped into that power law, the worst residual across the axis is
602
+ `1.7e-2` dex for the median against `3.8e-2` for the geometric mean and
603
+ `3.6e-1` for the arithmetic one. It will discard a genuine narrow peak just as
604
+ happily, which is the trade.
605
+
606
+ One detail in it is worth knowing about, because it is not what the textbook
607
+ description of fractional-octave smoothing gives you. A window symmetric in log
608
+ frequency is **not** a symmetric average over samples: a Fourier grid is uniform
609
+ in Hz, so every window holds more samples above its centre than below in log
610
+ terms, and a plain per-sample mean tilts. Weighting each sample by its share of
611
+ the log axis removes it. Smoothing an exact `f**-2` power law, which has to come
612
+ back unchanged, the median residual is `1.5e-3` dex per-sample against `3.2e-8`
613
+ dex weighted for `hann`. It is on by default; `log_measure=False` gives the
614
+ per-sample behaviour, which is what Konno–Ohmachi does.
615
+
616
+ **`SavitzkyGolay`** fits a sliding polynomial rather than averaging, so it
617
+ flattens a peak far less. It is fitted to `log10(amp)` against `log10(f)` on a
618
+ uniform log-frequency grid — a polynomial in raw amplitude against raw frequency
619
+ spends its degrees of freedom on the falloff and can return a negative
620
+ amplitude. Ringing near the corner and the Nyquist roll-off is what it costs.
621
+
622
+ ### What the choice actually changes
623
+
624
+ Less than it looks, and not where you would expect. **`[fitting] fit_bins` is
625
+ `false` by default, so the fit reads the unsmoothed spectrum.** The smoother's
626
+ whole influence on `Omega` is therefore indirect: it changes the signal-to-noise
627
+ ratio, which changes the band that passes the gate, which changes where the
628
+ plateau is read. The scatter column below is what the choice buys when
629
+ `fit_bins` is on, or when the reduced arrays are what you plot.
630
+
631
+ Measured on the 28 PNR windows, everything relative to the shipped default:
632
+
633
+ <!-- measured: smoothing_table -->
634
+ | Method | Banded | Band low (Hz) | Band (decades) | Δ plateau (dex) | Scatter (dex) |
635
+ |---|---|---|---|---|---|
636
+ | `log_bins` (default) | 28/28 | 0.94 | 1.55 | +0.000 | 0.317 |
637
+ | `konno_ohmachi`, b=40 | 28/28 | 0.93 | 1.68 | +0.067 | 0.093 |
638
+ | `log_window`, hann | 28/28 | 0.93 | 1.66 | +0.067 | 0.111 |
639
+ | `log_window`, bartlett | 28/28 | 0.93 | 1.66 | +0.067 | 0.102 |
640
+ | `log_window`, boxcar | 28/28 | 0.93 | 1.68 | +0.067 | 0.086 |
641
+ | `log_window`, gaussian | 28/28 | 0.93 | 1.65 | +0.067 | 0.147 |
642
+ | `log_window`, median | 28/28 | 0.93 | 1.69 | +0.067 | 0.091 |
643
+ | `savitzky_golay` | 28/28 | 0.93 | 1.67 | +0.067 | 0.063 |
644
+ | `none` | 28/28 | 0.94 | 1.51 | +0.000 | 0.318 |
645
+ <!-- /measured -->
646
+
647
+ Three things in that table are worth reading twice.
648
+
649
+ **The default barely smooths.** `log_bins` leaves the same scatter as `none`,
650
+ because 151 log bins over a window holding a few hundred Fourier samples puts
651
+ roughly one sample in most bins. It reduces the *axis*, not the noise.
652
+
653
+ **Every real smoother widens the band** by about a tenth of a decade. A smoothed
654
+ ratio crosses the threshold later, so more of the high-frequency tail survives
655
+ the gate — which is a gain if the tail is signal and a risk if it is not.
656
+
657
+ **The plateau moves by the same 0.067 dex under every one of them**, which is
658
+ 0.045 in magnitude. That is not seven methods agreeing about amplitudes; it is
659
+ the low band edge moving from 0.94 Hz to 0.93 Hz under all of them, and the
660
+ plateau being read from there. The amplitudes the fit sees never changed.
661
+
662
+ Cost, measured once at best of three for the whole comparison over the 28
663
+ windows: `none` 63 ms,
664
+ `savitzky_golay` 140 ms, `log_bins` 143 ms, `log_window` 313 ms,
665
+ `log_window` with `statistic="median"` 688 ms, `konno_ohmachi` 830 ms. The
666
+ spread is one machine's, and the shape of it — Konno–Ohmachi building a full
667
+ weight matrix where `LogWindow` slides a window — is what generalises.
668
+
585
669
  ---
586
670
 
587
671
  ## Reproducing the published Magna configuration
@@ -114,7 +114,8 @@ with `specmod[multitaper]` installed passes tests that CI, which installs only
114
114
 
115
115
  - **`main`** is the trunk. Everything lands here, and it is the default branch.
116
116
  - **`master`** is frozen: the permanent record of the pre-refactor code, doing
117
- the job a `v0.1.0` tag would have done. Never commit to it.
117
+ the job a `v0.1.1` tag would have done: its tip, `453c77c`, is the commit the
118
+ Magna paper cites. Never commit to it.
118
119
  - Feature branches are short-lived and merge into `main` via pull request.
119
120
 
120
121
  Every pull request must target **`sgjholt/SpecMod`**. See §6.7 of the plan for
@@ -35,7 +35,10 @@ The version before this one, `0.1.1`, is the pre-refactor code that produced
35
35
  the published Magna results. It is preserved unchanged on the
36
36
  [`master`](https://github.com/sgjholt/SpecMod/tree/master) branch, which is
37
37
  protected and frozen — it was never tagged or published to PyPI, so that
38
- branch is the only reference to it. Everything below is the delta between it
38
+ branch is the only reference to it. Its tip,
39
+ [`453c77c`](https://github.com/sgjholt/SpecMod/commit/453c77c), is the commit
40
+ the paper cites as v0.1.1; cite that rather than the branch name, which is a
41
+ moving target in principle even though this one will not move. Everything below is the delta between it
39
42
  and v0.2.0, which is why the first changelog is enormous and correctly so.
40
43
 
41
44
  ### An installable package
@@ -106,11 +109,11 @@ build**, so every figure and number on the page came from running the code
106
109
  being documented. A notebook that stops working fails the build rather than
107
110
  going quietly stale, which is why it was a 1.0 requirement.
108
111
 
109
- ## In progress — not yet released
112
+ ## Shipped in v0.3.0
110
113
 
111
- ### Finishing the decomposition
114
+ ### The decomposition, finished
112
115
 
113
- **Done, and unreleased.** This was the last item 1.0 was waiting on.
116
+ This was the last item 1.0 was waiting on.
114
117
 
115
118
  `io.py` and `plotting.py` are now packages, alongside the spectral core, the
116
119
  fitting layer and the pick readers; the type-checking backlog is empty. The
@@ -140,9 +143,9 @@ signposted from [Guides](guides.md) rather than split onto a page of their own.
140
143
 
141
144
  ### 1.0 — the API stops moving
142
145
 
143
- **Everything this was waiting on is now done** — the documentation, and the
144
- decomposition above with the signature changes it implied. What remains is the
145
- release itself, which says that names and signatures stop moving without a
146
+ **Everything this was waiting on shipped in v0.3.0** — the documentation, and
147
+ the decomposition above with the signature changes it implied. What remains is
148
+ the release itself, which says that names and signatures stop moving without a
146
149
  deprecation cycle.
147
150
 
148
151
  That promise is the whole content of the number, which is why it has to be a
@@ -151,6 +154,13 @@ is made, breaking changes bump the minor. The honest test before making it is
151
154
  whether a release goes by without one — the API is not settled because the
152
155
  backlog is empty, it is settled when it stops moving.
153
156
 
157
+ **By that test, 1.0 is not ready.** v0.3.0 is the release that emptied the
158
+ backlog and it carried two breaking changes itself: the ten renamed
159
+ `preprocess` functions, and the tutorial's page moving. So what 1.0 now waits
160
+ on is the next release going out without one — which is a thing that has to be
161
+ observed rather than declared, and cannot be brought forward by finishing
162
+ anything.
163
+
154
164
  ## After 1.0
155
165
 
156
166
  Designed but deliberately not on the path to 1.0, because each is blocked on
@@ -175,7 +185,16 @@ guess written from documentation cannot substitute for:
175
185
  Each shipped entry names the version that carries it, so a reader can install
176
186
  that version and check the claim. Work that is merged but unreleased sits under
177
187
  *In progress* and gets its version when a release goes out — merged is not
178
- shipped, and this page does not blur the two.
188
+ shipped, and this page does not blur the two. **There is no such section right
189
+ now**, because nothing is merged and unreleased; that is the state the page
190
+ should be in between releases, not a section that went missing.
191
+
192
+ The move at release time is the step this page got wrong once. The v0.3.0 work
193
+ sat under *In progress — not yet released* for a week after v0.3.0 was
194
+ released, because `release-please` writes the changelog and the manifest and
195
+ knows nothing about this file. `tests/test_release_config.py` now fails when
196
+ the released minor version is ahead of every version this page names, which is
197
+ the check that was missing rather than a rule that was.
179
198
 
180
199
  Before v0.2.0 this was a list of stages, because there was no released version
181
200
  to point at and "done" could only mean merged. The full changelog for every
@@ -140,12 +140,23 @@ class TransformConfig:
140
140
 
141
141
  @dataclass(frozen=True, slots=True)
142
142
  class SmoothingConfig:
143
- """Spectral smoothing and log-space binning."""
143
+ """What the signal-to-noise comparison smooths with, and how.
144
144
 
145
- method: Literal["log_bins", "konno_ohmachi", "none"] = "log_bins"
145
+ ``log_bins`` replaces the frequency axis with bin centres; every other
146
+ method keeps it. See :mod:`specmod.smoothing` for what each one does to a
147
+ seismic spectrum and ``docs/choosing-a-transform.md`` for the measured
148
+ comparison.
149
+ """
150
+
151
+ #: Resolved through `specmod.smoothing.SMOOTHERS`. Only the parameters
152
+ #: belonging to the chosen method are read.
153
+ method: Literal[
154
+ "log_bins", "konno_ohmachi", "log_window", "savitzky_golay", "none"
155
+ ] = "log_bins"
146
156
 
147
- #: Log bin edges. ``None`` derives them from the record: fmin from 1/T,
148
- #: fmax from Nyquist. The old code hardcoded 0.001-200 Hz regardless.
157
+ #: Log bin edges, for `log_bins`. ``None`` derives them from the record:
158
+ #: fmin from 1/T, fmax from Nyquist. The old code hardcoded 0.001-200 Hz
159
+ #: regardless.
149
160
  f_min: float | None = 0.001
150
161
  f_max: float | None = 200.0
151
162
  n_bins: int = 151
@@ -153,6 +164,23 @@ class SmoothingConfig:
153
164
  #: Konno-Ohmachi bandwidth ``b``. Smaller smooths harder.
154
165
  konno_ohmachi_bandwidth: float = 40.0
155
166
 
167
+ #: `log_window`: window width in octaves, its shape, and how the samples
168
+ #: it covers are combined. The shapes are listed in
169
+ #: `specmod.smoothing.WINDOWS`; `bartlett` is the triangular one.
170
+ #: `statistic = "median"` ignores the shape and rejects spikes instead of
171
+ #: averaging them in.
172
+ octave_fraction: float = 1.0 / 3.0
173
+ window: Literal["boxcar", "bartlett", "hann", "hamming", "blackman", "gaussian"] = (
174
+ "hann"
175
+ )
176
+ statistic: Literal["geometric", "mean", "median"] = "geometric"
177
+
178
+ #: `savitzky_golay`: the fit is done on a uniform log-frequency grid, so
179
+ #: the window is a fraction of a decade rather than a count of Fourier bins.
180
+ savgol_window_length: int = 41
181
+ savgol_polyorder: int = 3
182
+ savgol_points_per_decade: int = 200
183
+
156
184
 
157
185
  @dataclass(frozen=True, slots=True)
158
186
  class SnrConfig:
@@ -317,19 +317,26 @@ class SpectrumPair:
317
317
  max_nyquist_fraction: float | None = None,
318
318
  rotation_inc: float = 0.05,
319
319
  rotation_space: tuple[float, float] = (0.001, 1.001),
320
+ smoother: Any = None,
320
321
  meta: Mapping[str, Any] | None = None,
321
322
  ) -> SpectrumPair:
322
323
  """Pair the two and select the band.
323
324
 
324
325
  The order matters and is not arbitrary. The noise is rescaled and moved
325
- onto the signal's frequency axis *before* binning, which is what makes
326
- the two binned arrays share bin edges — the element-wise ratio below is
327
- only meaningful because of it, and it holds for every estimator
328
- including those whose native axes differ in length.
326
+ onto the signal's frequency axis *before* smoothing, which is what
327
+ makes the two reduced arrays share an axis — the element-wise ratio
328
+ below is only meaningful because of it, and it holds for every
329
+ estimator including those whose native axes differ in length.
329
330
 
330
331
  The floor is captured from the two spectra before the interpolation,
331
332
  because afterwards the noise carries the signal's axis and its own
332
333
  lowest resolvable frequency is unrecoverable.
334
+
335
+ ``smoother`` chooses what reduces the two spectra before the ratio.
336
+ ``None`` is :func:`log_bin` with the ``f_min``/``f_max``/``n_bins``
337
+ above. Anything else is a :mod:`specmod.smoothing` smoother, which
338
+ keeps the frequency axis rather than replacing it with bin centres, so
339
+ ``snr`` is then as long as the spectrum.
333
340
  """
334
341
  floor = max(_resolution_floor(signal), _resolution_floor(noise))
335
342
 
@@ -338,16 +345,32 @@ class SpectrumPair:
338
345
  noise_amp = noise_amp * parseval_scale(signal.amp.size, noise.amp.size)
339
346
  noise_amp = interpolate_onto(signal.freq, noise.freq, noise_amp)
340
347
 
341
- binned_signal = log_bin(
342
- signal.freq,
343
- np.asarray(signal.amp),
344
- f_min=f_min,
345
- f_max=f_max,
346
- n_bins=n_bins,
347
- )
348
- binned_noise = log_bin(
349
- signal.freq, noise_amp, f_min=f_min, f_max=f_max, n_bins=n_bins
350
- )
348
+ reducer = _resolve_smoother(smoother)
349
+
350
+ def reduce(amp: NDArray[np.float64], like: Spectrum) -> BinnedSpectrum:
351
+ if reducer is None:
352
+ return log_bin(
353
+ signal.freq, amp, f_min=f_min, f_max=f_max, n_bins=n_bins
354
+ )
355
+ smoothed = reducer.smooth(
356
+ Spectrum(
357
+ freq=signal.freq,
358
+ amp=amp,
359
+ motion=like.motion,
360
+ kind=like.kind,
361
+ duration=like.duration,
362
+ sampling_rate=like.sampling_rate,
363
+ meta=dict(like.meta),
364
+ )
365
+ )
366
+ return BinnedSpectrum(
367
+ freq=np.asarray(smoothed.freq, dtype=np.float64),
368
+ amp=np.asarray(smoothed.amp, dtype=np.float64),
369
+ )
370
+
371
+ binned_signal = reduce(np.asarray(signal.amp, dtype=np.float64), signal)
372
+ binned_noise = reduce(noise_amp, noise)
373
+ _require_shared_axis(binned_signal, binned_noise, reducer)
351
374
 
352
375
  if rotate_noise:
353
376
  # The factor is derived on the binned axis — that is where the
@@ -373,9 +396,7 @@ class SpectrumPair:
373
396
  noise_amp = noise_amp * interpolate_onto(
374
397
  signal.freq, binned_noise.freq, factor
375
398
  )
376
- binned_noise = log_bin(
377
- signal.freq, noise_amp, f_min=f_min, f_max=f_max, n_bins=n_bins
378
- )
399
+ binned_noise = reduce(noise_amp, noise)
379
400
 
380
401
  snr = binned_signal.amp / binned_noise.amp
381
402
  selector = _resolve_bandwidth(bandwidth)
@@ -416,6 +437,7 @@ class SpectrumPair:
416
437
  "max_nyquist_fraction": max_nyquist_fraction,
417
438
  "rotation_inc": rotation_inc,
418
439
  "rotation_space": rotation_space,
440
+ "smoother": _strategy_record(smoother),
419
441
  }
420
442
  # A band the ratio did not choose is marked as such, so a stored result
421
443
  # — or one read back a year later — still distinguishes a measurement
@@ -495,6 +517,70 @@ def _split_record(spec: Mapping[str, Any]) -> tuple[str, dict[str, Any]]:
495
517
  return str(spec["name"]), params
496
518
 
497
519
 
520
+ def _resolve_smoother(smoother: Any) -> Any:
521
+ """Turn a name, a record or an instance into a smoother, or ``None``.
522
+
523
+ ``None`` and ``"log_bins"`` both mean :func:`log_bin`, this module's own
524
+ binner. It is not :class:`specmod.smoothing.LogBinner`: ``log_bin`` clamps
525
+ the requested range to the record and counts ``n_bins`` as edges, where
526
+ ``LogBinner`` honours explicit edges exactly and can keep empty bins.
527
+ """
528
+ if smoother is None or smoother == "log_bins":
529
+ return None
530
+ # Imported here because `specmod.smoothing` imports `specmod.core`, and at
531
+ # module level that is a cycle.
532
+ from ..smoothing import SMOOTHERS # noqa: PLC0415
533
+
534
+ if isinstance(smoother, Mapping):
535
+ name, params = _split_record(smoother)
536
+ if name == "log_bins":
537
+ return None
538
+ return SMOOTHERS[name](**params)
539
+ if isinstance(smoother, str):
540
+ try:
541
+ return SMOOTHERS[smoother]()
542
+ except KeyError:
543
+ raise ValueError(
544
+ f"Unknown smoother {smoother!r}. Available: {sorted(SMOOTHERS)}."
545
+ ) from None
546
+ return smoother
547
+
548
+
549
+ def _require_shared_axis(
550
+ signal: BinnedSpectrum, noise: BinnedSpectrum, reducer: Any
551
+ ) -> None:
552
+ """The ratio is element-wise, so the two axes have to be the same one.
553
+
554
+ Only reachable with a smoother that re-grids, since every shipped one keeps
555
+ the axis it is given. ``LogBinner`` with derived edges is the case to know
556
+ about: it takes its range from each spectrum's own duration, and the noise
557
+ window is not the signal window.
558
+
559
+ The axes are compared rather than their lengths. Two log binnings with the
560
+ same ``n_bins`` and different edges give arrays of equal shape over
561
+ different frequencies, which divides cleanly and is wrong everywhere.
562
+ """
563
+ name = getattr(reducer, "name", reducer)
564
+ if signal.freq.shape != noise.freq.shape:
565
+ raise ValueError(
566
+ f"The smoother {name!r} produced {len(signal)} points for the "
567
+ f"signal and {len(noise)} for the noise, so the signal-to-noise "
568
+ f"ratio has no element-wise meaning. A smoother used here must "
569
+ f"leave the frequency axis alone, or pin its own axis explicitly "
570
+ f"(for LogBinner, by setting f_min and f_max rather than deriving "
571
+ f"them)."
572
+ )
573
+ if not np.allclose(signal.freq, noise.freq, rtol=1e-12, atol=0.0):
574
+ worst = int(np.argmax(np.abs(signal.freq - noise.freq)))
575
+ raise ValueError(
576
+ f"The smoother {name!r} put the signal and the noise on different "
577
+ f"frequencies — {signal.freq[worst]:.6g} Hz against "
578
+ f"{noise.freq[worst]:.6g} Hz at index {worst} — so the ratio has "
579
+ f"no element-wise meaning even though the two are the same length. "
580
+ f"Pin the axis explicitly (for LogBinner, set f_min and f_max)."
581
+ )
582
+
583
+
498
584
  def _resolve_noise_model(
499
585
  noise_model: str | NoiseModel | Mapping[str, Any], space: tuple[float, float]
500
586
  ) -> NoiseModel: