spectro-kernel 0.1.2__tar.gz → 0.1.4__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 (243) hide show
  1. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/CHANGELOG.md +145 -0
  2. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/PKG-INFO +1 -1
  3. spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_ew_timeseries.png +0 -0
  4. spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_halpha_phase_stack.png +0 -0
  5. spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_halpha_timeseries.png +0 -0
  6. spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_overlay.png +0 -0
  7. spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_pc1_vs_phase.png +0 -0
  8. spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_pca_2d.png +0 -0
  9. spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_pca_3d.png +0 -0
  10. spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_periodogram.png +0 -0
  11. spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_phase_folded.png +0 -0
  12. spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_rv_timeseries.png +0 -0
  13. spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_similarity.png +0 -0
  14. spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_similarity_date.png +0 -0
  15. spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_halpha_phase_stack.png +0 -0
  16. spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_pc1_vs_phase.png +0 -0
  17. spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_pca_2d.png +0 -0
  18. spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_pca_3d.png +0 -0
  19. spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_periodogram.png +0 -0
  20. spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_phase_folded.png +0 -0
  21. spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_rv_timeseries.png +0 -0
  22. spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_similarity_date.png +0 -0
  23. spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_similarity_phase.png +0 -0
  24. spectro_kernel-0.1.4/docs/notebooks/alpha-cyg-time-series.md +155 -0
  25. spectro_kernel-0.1.4/docs/notebooks/alpha-dra-binary-period.md +210 -0
  26. spectro_kernel-0.1.4/src/spectro_kernel/algorithms/__init__.py +17 -0
  27. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/embed_lick_indices.py +42 -5
  28. spectro_kernel-0.1.4/src/spectro_kernel/algorithms/io/read_sdss.py +121 -0
  29. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/registry.py +25 -4
  30. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/version.py +1 -1
  31. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_mcp/__main__.py +63 -16
  32. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_mcp/auto_tools.py +49 -14
  33. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_mcp/observability.py +9 -2
  34. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_mcp/session.py +80 -5
  35. spectro_kernel-0.1.4/src/spectro_mcp/url_safety.py +155 -0
  36. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_embedding_remote_lick.py +63 -0
  37. spectro_kernel-0.1.4/tests/unit/test_mcp_security.py +169 -0
  38. spectro_kernel-0.1.4/tests/unit/test_no_circular_imports.py +89 -0
  39. spectro_kernel-0.1.4/tests/unit/test_read_sdss.py +76 -0
  40. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_registry.py +1 -0
  41. spectro_kernel-0.1.2/src/spectro_kernel/algorithms/__init__.py +0 -20
  42. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/.gitignore +0 -0
  43. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/LICENSE +0 -0
  44. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/README.md +0 -0
  45. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/concepts/algorithms.md +0 -0
  46. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/concepts/architecture.md +0 -0
  47. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/concepts/data-types.md +0 -0
  48. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/concepts/pipelines.md +0 -0
  49. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/contributing.md +0 -0
  50. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/cookbook/bess-dashboard.md +0 -0
  51. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/cookbook/multi-star-viewer.md +0 -0
  52. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/cookbook/web-playground.md +0 -0
  53. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/gen_catalogue.py +0 -0
  54. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/getting-started.md +0 -0
  55. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/index.md +0 -0
  56. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/notebooks/aurora-line-monitor.md +0 -0
  57. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/notebooks/be-star-variability.md +0 -0
  58. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/notebooks/claude-mcp-end-to-end.md +0 -0
  59. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/notebooks/exoplanet-transit-rv.md +0 -0
  60. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/notebooks/full-reduction-walkthrough.md +0 -0
  61. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/notebooks/native-vs-easyspec-showdown.md +0 -0
  62. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/notebooks/your-first-sb2.md +0 -0
  63. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/reference.md +0 -0
  64. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/tutorials/add-an-algorithm.md +0 -0
  65. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/tutorials/analyse-a-spectrum.md +0 -0
  66. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/tutorials/discover-the-catalogue.md +0 -0
  67. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/tutorials/first-spectrum.md +0 -0
  68. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/usage/cli.md +0 -0
  69. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/usage/library.md +0 -0
  70. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/usage/mcp.md +0 -0
  71. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/why.md +0 -0
  72. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/playground/README.md +0 -0
  73. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/pyproject.toml +0 -0
  74. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/__init__.py +0 -0
  75. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/adapters/__init__.py +0 -0
  76. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/adapters/easyspec.py +0 -0
  77. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/_common.py +0 -0
  78. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/advanced/__init__.py +0 -0
  79. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/advanced/aperture_photometry.py +0 -0
  80. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/advanced/disentangle_sb2.py +0 -0
  81. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/catalogs/__init__.py +0 -0
  82. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/catalogs/gaia.py +0 -0
  83. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/catalogs/simbad.py +0 -0
  84. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/catalogs/vizier.py +0 -0
  85. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/continuum/__init__.py +0 -0
  86. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/continuum/compare_normalisations.py +0 -0
  87. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/continuum/normalize_edges.py +0 -0
  88. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/continuum/normalize_max.py +0 -0
  89. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/continuum/normalize_percentile.py +0 -0
  90. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/continuum/normalize_polynomial.py +0 -0
  91. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/continuum/subtract_continuum.py +0 -0
  92. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/__init__.py +0 -0
  93. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/air_vacuum.py +0 -0
  94. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/barycentric.py +0 -0
  95. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/doppler_shift.py +0 -0
  96. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/extinction_correct_easyspec.py +0 -0
  97. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/fit_telluric_scaling.py +0 -0
  98. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/flux_calibrate_easyspec.py +0 -0
  99. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/remove_telluric.py +0 -0
  100. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/synth_telluric.py +0 -0
  101. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/__init__.py +0 -0
  102. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/embed_band_power.py +0 -0
  103. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/embed_continuum_subtracted.py +0 -0
  104. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/embed_log_lambda.py +0 -0
  105. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/embed_pretrained.py +0 -0
  106. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/embed_remote.py +0 -0
  107. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/embed_spectrum.py +0 -0
  108. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/embed_wavelets.py +0 -0
  109. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/exports/__init__.py +0 -0
  110. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/exports/export_csv.py +0 -0
  111. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/exports/export_fits.py +0 -0
  112. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/exports/export_hdf5.py +0 -0
  113. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/exports/export_votable.py +0 -0
  114. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/extraction/__init__.py +0 -0
  115. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/extraction/easyspec_extract.py +0 -0
  116. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/io/__init__.py +0 -0
  117. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/io/read_ascii.py +0 -0
  118. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/io/read_echelle.py +0 -0
  119. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/io/read_fits.py +0 -0
  120. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/io/read_votable.py +0 -0
  121. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/__init__.py +0 -0
  122. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/_profiles.py +0 -0
  123. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/catalogs.py +0 -0
  124. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/compare_line_fits.py +0 -0
  125. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/detect.py +0 -0
  126. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/equivalent_width.py +0 -0
  127. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/fit_gaussian.py +0 -0
  128. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/fit_lorentzian.py +0 -0
  129. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/fit_voigt.py +0 -0
  130. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/quality/__init__.py +0 -0
  131. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/quality/compare_snr_methods.py +0 -0
  132. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/quality/snr_der.py +0 -0
  133. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/quality/snr_edge.py +0 -0
  134. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/quality/snr_linear_fit.py +0 -0
  135. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/__init__.py +0 -0
  136. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/_easyspec_apply.py +0 -0
  137. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/_easyspec_helpers.py +0 -0
  138. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/bias_combine.py +0 -0
  139. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/clip_cosmic_rays.py +0 -0
  140. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/dark_subtract.py +0 -0
  141. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/easyspec_bias.py +0 -0
  142. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/easyspec_cosmic_ray.py +0 -0
  143. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/easyspec_dark.py +0 -0
  144. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/easyspec_flat.py +0 -0
  145. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/easyspec_flat_normalize.py +0 -0
  146. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/easyspec_subtract_bias.py +0 -0
  147. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/easyspec_subtract_dark.py +0 -0
  148. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/extract_spectrum_sum.py +0 -0
  149. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/flat_normalize.py +0 -0
  150. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/subtract_sky_2d.py +0 -0
  151. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/wavelength_calibrate.py +0 -0
  152. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/rv/__init__.py +0 -0
  153. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/rv/cross_correlate.py +0 -0
  154. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/rv/fit_keplerian_orbit.py +0 -0
  155. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/rv/measure.py +0 -0
  156. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/rv/precision_bouchy.py +0 -0
  157. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/smoothing/__init__.py +0 -0
  158. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/smoothing/compare_smoothings.py +0 -0
  159. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/smoothing/smooth_gaussian.py +0 -0
  160. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/smoothing/smooth_savgol.py +0 -0
  161. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/stacking/__init__.py +0 -0
  162. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/stacking/merge_echelle_orders.py +0 -0
  163. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/stacking/stack.py +0 -0
  164. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/timeseries/__init__.py +0 -0
  165. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/timeseries/lomb_scargle.py +0 -0
  166. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/timeseries/phase_fold.py +0 -0
  167. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/transforms/__init__.py +0 -0
  168. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/transforms/clip_sigma.py +0 -0
  169. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/transforms/extract_region.py +0 -0
  170. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/transforms/mask_range.py +0 -0
  171. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/transforms/resample.py +0 -0
  172. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/transforms/resample_flux_conserving.py +0 -0
  173. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/viz/__init__.py +0 -0
  174. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/viz/plot_3d_surface.py +0 -0
  175. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/viz/plot_animation.py +0 -0
  176. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/viz/plot_dynamic_spectrum.py +0 -0
  177. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/viz/plot_plotly.py +0 -0
  178. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/wavelength_calibration/__init__.py +0 -0
  179. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/wavelength_calibration/easyspec_wavelength.py +0 -0
  180. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/base.py +0 -0
  181. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/cli.py +0 -0
  182. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/embeddings.py +0 -0
  183. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/errors.py +0 -0
  184. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/io/__init__.py +0 -0
  185. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/io/ascii.py +0 -0
  186. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/io/fits.py +0 -0
  187. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/io/votable.py +0 -0
  188. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/pipeline.py +0 -0
  189. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/__init__.py +0 -0
  190. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/catalog/analysis/balmer_quick.yaml +0 -0
  191. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/catalog/analysis/embed_quick.yaml +0 -0
  192. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/catalog/analysis/quality_report.yaml +0 -0
  193. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/catalog/analysis/rv_quick.yaml +0 -0
  194. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/catalog/analysis/snr_check.yaml +0 -0
  195. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/catalog/analysis/time_series_overview.yaml +0 -0
  196. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/catalog/reduction/full_reduction_easyspec.yaml +0 -0
  197. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/loader.py +0 -0
  198. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/py.typed +0 -0
  199. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/__init__.py +0 -0
  200. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/catalog.py +0 -0
  201. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/context.py +0 -0
  202. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/enums.py +0 -0
  203. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/history.py +0 -0
  204. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/image.py +0 -0
  205. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/line.py +0 -0
  206. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/spectrum.py +0 -0
  207. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/timeseries.py +0 -0
  208. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_mcp/__init__.py +0 -0
  209. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_mcp/auth.py +0 -0
  210. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_mcp/py.typed +0 -0
  211. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_mcp/server.py +0 -0
  212. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/conftest.py +0 -0
  213. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/reference/conftest.py +0 -0
  214. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/reference/data/.gitkeep +0 -0
  215. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/reference/data/README.md +0 -0
  216. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/reference/data/sun/sun_reference_stis_002.fits +0 -0
  217. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/reference/data/vega/alpha_lyr_stis_011.fits +0 -0
  218. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/reference/test_known_answers.py +0 -0
  219. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/reference/test_sun.py +0 -0
  220. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/reference/test_vega.py +0 -0
  221. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_base.py +0 -0
  222. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_cli.py +0 -0
  223. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_combine.py +0 -0
  224. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_continuum.py +0 -0
  225. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_corrections.py +0 -0
  226. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_easyspec_wrappers.py +0 -0
  227. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_embedding.py +0 -0
  228. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_embedding_pretrained.py +0 -0
  229. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_embedding_tier1.py +0 -0
  230. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_io.py +0 -0
  231. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_line_profiles.py +0 -0
  232. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_lines.py +0 -0
  233. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_mcp.py +0 -0
  234. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_mcp_production.py +0 -0
  235. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_misc_algorithms.py +0 -0
  236. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_pipeline.py +0 -0
  237. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_quality.py +0 -0
  238. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_smoothing.py +0 -0
  239. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_timeseries.py +0 -0
  240. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_transforms.py +0 -0
  241. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_types.py +0 -0
  242. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_viz.py +0 -0
  243. {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/website/README.md +0 -0
@@ -6,6 +6,151 @@ Until `1.0.0` the public API may change between minor versions.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.4] — 2026-06-10
10
+
11
+ ### Fixed — Lick atomic equivalent-width formula (CRITICAL)
12
+
13
+ `embed_lick_indices` was computing
14
+ ``EW = (λ_hi − λ_lo) × (1 − mean(F/Fc))`` for atomic indices instead of
15
+ the canonical Worthey-1994 form ``EW = ∫(1 − F/Fc) dλ``. The two only
16
+ agree when pixel edges fall exactly on the band limits — which never
17
+ happens on real data — so every atomic Lick value produced by ≤ 0.1.3
18
+ was systematically **inflated by ~1–2 Å**. The molecular-magnitude form
19
+ was correct.
20
+
21
+ Algorithm version bumped 1.0.0 → 2.0.0; any indexed Lick embedding from
22
+ ≤ 0.1.3 should be **re-computed**. Companion fix in ``_band_mean_flux``:
23
+ the band mean now uses the actual covered pixel span (not the nominal
24
+ ``hi − lo``) and refuses bands less than half-covered by the spectrum.
25
+
26
+ ### Fixed — SSRF on `load_spectrum` / `load_spectrum_from_url` (CRITICAL)
27
+
28
+ The MCP file-loading tools forwarded the user-supplied URL directly to
29
+ ``urllib.request.urlretrieve`` with no host validation, no redirect
30
+ control, no timeout and no size cap. An attacker could fetch instance
31
+ metadata (``http://169.254.169.254/latest/meta-data/iam/...``), scan the
32
+ VPC interior, or stream a 10 GB file to exhaust the worker.
33
+
34
+ New module ``spectro_mcp/url_safety.py`` gates every URL coming from
35
+ MCP through:
36
+ - scheme allowlist (``http``/``https`` only);
37
+ - ``localhost`` and DNS-resolved IP validation (refuses loopback,
38
+ RFC1918, link-local, multicast, reserved, unspecified);
39
+ - a no-redirect HTTP handler (a friendly upstream can't 302 to
40
+ ``169.254.169.254`` after the hostname guard passes);
41
+ - 60-second timeout plus 256 MiB byte cap on downloads.
42
+
43
+ ### Fixed — Path traversal on the HTTP MCP server (HIGH)
44
+
45
+ ``load_spectrum(session_id, path)`` accepted any local filesystem path,
46
+ including ``/etc/passwd`` and ``/proc/self/environ`` (which leaks
47
+ ``SPECTRO_MCP_API_KEY``, ``AWS_*``, ``SPECTRO_MCP_REDIS_URL``, …).
48
+
49
+ Local paths are now refused when the new env var
50
+ ``SPECTRO_MCP_DENY_LOCAL_PATHS=1`` is set, and the ``--http`` entry
51
+ point sets it automatically. ``--stdio`` mode (the local default) is
52
+ unaffected — the user's own machine is trusted by definition.
53
+
54
+ ### Fixed — Pickle deserialisation from Redis (HIGH)
55
+
56
+ ``RedisSessionManager.get()`` called ``pickle.loads`` on whatever
57
+ arbitrary bytes Redis returned. Any path that lets an attacker write to
58
+ the Redis backing store (compromised Redis, MITM with no TLS, leaked
59
+ ``SPECTRO_MCP_REDIS_URL``) → arbitrary code execution on the next
60
+ ``get``.
61
+
62
+ Payloads are now wrapped with an HMAC-SHA256 signature derived from
63
+ ``SPECTRO_MCP_SESSION_SECRET`` (env). Without that env var the server
64
+ generates a one-shot random secret at boot (sessions don't survive a
65
+ restart in that case — a clear stderr warning is emitted). Unsigned or
66
+ mis-signed blobs raise ``ValueError`` before ``pickle.loads`` runs.
67
+
68
+ ### Fixed — MCP stdio corruption from boot-time stdout prints (HIGH)
69
+
70
+ In stdio mode, stdout is the JSON-RPC wire to the agent. Two paths
71
+ emitted bytes on stdout that corrupted the very first handshake:
72
+
73
+ 1. Algorithm discovery imported ``astroquery`` (Gaia DR4 banner, ~390 B)
74
+ and ``easyspec`` (version banner, ~30 B). FD-level writes that
75
+ ``contextlib.redirect_stdout`` doesn't catch.
76
+ 2. ``configure_logger`` attached the audit handler to ``sys.stdout``;
77
+ every tool call therefore emitted a JSON record on the wire.
78
+
79
+ Fix: a new ``_silence_fd_stdout`` context manager in ``__main__``
80
+ ``dup2``'s FD 1 to FD 2 for the boot phase (catches even C-level
81
+ writes), and the audit logger now targets ``sys.stderr``. HTTP mode is
82
+ unchanged.
83
+
84
+ ### Hardened — Dockerfile non-root user (MED)
85
+
86
+ Default container user was root. The Dockerfile now creates an
87
+ unprivileged ``app`` (UID 10001) and drops to it before ``CMD``. Any
88
+ future vulnerability that reaches the filesystem is confined to the
89
+ app user's home.
90
+
91
+ ### Hardened — Discovery silent error swallow (LOW)
92
+
93
+ ``registry.ensure_discovered`` silently dropped any submodule that
94
+ failed to import. The intent (gracefully skip algorithms whose optional
95
+ extra is missing) is preserved, but a bug-induced import failure used
96
+ to vanish without trace. Discovery now logs every dropped submodule
97
+ via the ``spectro_kernel.registry`` logger.
98
+
99
+ ### Added
100
+
101
+ - ``tests/unit/test_mcp_security.py`` — 15 new tests covering every
102
+ v0.1.4 finding (SSRF target families, path-traversal env-var gate,
103
+ HMAC roundtrip + tamper + wrong-key, RedisSessionManager round-trip
104
+ via fakeredis, ``_resolve_redis_secret`` env-vs-random behaviour).
105
+ - 2 new Lick regression tests (``test_lick_atomic_ew_matches_worthey_integral``,
106
+ ``test_lick_band_mean_uses_actual_pixel_span``).
107
+
108
+ ### Migration notes
109
+
110
+ - Re-index any vectors produced by ``embed_lick_indices`` ≤ 0.1.3
111
+ (values changed by 1–2 Å on atomic indices).
112
+ - For Redis-backed deployments: set ``SPECTRO_MCP_SESSION_SECRET`` to
113
+ a stable 32-byte secret in the environment. Otherwise sessions don't
114
+ survive a restart.
115
+ - For HTTP deployments where you want users to send local paths anyway
116
+ (uncommon), unset ``SPECTRO_MCP_DENY_LOCAL_PATHS`` explicitly.
117
+
118
+ ## [0.1.3] — 2026-06-09
119
+
120
+ ### Fixed — circular import that prevented `from spectro_kernel.embeddings import …`
121
+
122
+ In 0.1.2, `spectro_kernel.algorithms.__init__` ran an eager submodule
123
+ walk at import time. That walk imported every algorithm, including
124
+ `embed_continuum_subtracted`, which itself did a top-level
125
+ `from ...embeddings import VALID_NORM_METHODS, VALID_STRATEGIES,
126
+ embed_flux`. But `embeddings` itself imports
127
+ `spectro_kernel.algorithms._common` before defining `VALID_*` /
128
+ `embed_flux` — so when an external consumer wrote
129
+ `from spectro_kernel.embeddings import embed_flux` as its very first
130
+ contact with the kernel, Python's import machinery saw a partially
131
+ initialised module and raised `ImportError`.
132
+
133
+ Reported by a downstream library whose service worker crashed
134
+ deterministically on the same PyPI 0.1.2 wheel.
135
+
136
+ The fix moves discovery into `registry.ensure_discovered()`, which was
137
+ already the lazy entry point — every registry getter (`list_algorithms`,
138
+ `get_algorithm`, `run_algorithm`, …) calls it before answering.
139
+ Importing `spectro_kernel.algorithms` is now a true no-op, so pulling a
140
+ helper out of `algorithms._common` from inside `embeddings` no longer
141
+ triggers a transitive cycle. Discovery now silently skips any
142
+ optional-extra algorithm whose dependency is missing, instead of
143
+ breaking the entire import chain.
144
+
145
+ Behaviour preservation: existing callers that go through the registry
146
+ see the same catalogue at the same time (the first registry call
147
+ populates it, exactly as before). The only change is **when** discovery
148
+ runs — it's properly lazy now.
149
+
150
+ Added 4 regression tests in `test_no_circular_imports.py` that run the
151
+ exact cold-start imports the bug reporter hit, in fresh subprocesses,
152
+ so a future re-introduction of the cycle is caught immediately by CI.
153
+
9
154
  ## [0.1.2] — 2026-06-05
10
155
 
11
156
  ### Added — embedding category (8 algorithms total)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spectro-kernel
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: A shared catalogue of astronomical spectroscopy algorithms, composable into reproducible pipelines, usable as a Python library, a CLI, or an MCP server.
5
5
  Project-URL: Homepage, https://github.com/matthieulel/spectro-kernel
6
6
  Project-URL: Documentation, https://matthieulel.github.io/spectro-kernel/
@@ -0,0 +1,155 @@
1
+ # α Cyg — 124 amateur spectra of a pulsating supergiant
2
+
3
+ **A second real-world walkthrough of `spectro-kernel`, complementary to the
4
+ [α Dra binary case](alpha-dra-binary-period.md).** Deneb (α Cyg, A2 Ia) is
5
+ not a binary — it's the **prototype of the α-Cygni variables**: a
6
+ non-radial pulsating supergiant whose H-α profile is also shaped by an
7
+ ionised stellar wind. Where α Dra gave a single clean orbital period, α
8
+ Cyg exhibits low-amplitude, **multi-periodic, sometimes irregular**
9
+ variability on timescales of weeks. This notebook applies the same kernel
10
+ pipeline as the α Dra case and **honestly reports** what amateur data of a
11
+ genuinely harder target can and can't deliver.
12
+
13
+ The complete script is at
14
+ [`playground/analyse_alpha_cyg.py`](https://github.com/matthieulel/spectro-kernel/blob/main/playground/analyse_alpha_cyg.py).
15
+
16
+ ## Dataset provenance
17
+
18
+ The 124 FITS files come from an amateur monitoring campaign of Deneb run
19
+ between **2020 and 2024** (~12 contributing observers, slit spectrographs
20
+ typically R ≈ 5 000–20 000 centred on H-α). The data is **not bundled
21
+ with this repository** — drop your own FITS into
22
+ `playground/datas/alphacygni/` and the script picks them up automatically.
23
+
24
+ > If you contributed spectra to or reuse spectra from a public archive,
25
+ > please cite the relevant campaign. Personal observer attributions have
26
+ > been omitted from this notebook — the kernel showcases the analysis
27
+ > pipeline, not the source of any specific measurement.
28
+
29
+ ## The pipeline
30
+
31
+ Identical to the α Dra one — same algorithms, same defaults:
32
+
33
+ ```python
34
+ from spectro_kernel import WorkContext, run_algorithm
35
+ from spectro_kernel.io import read_fits
36
+
37
+ ctx = WorkContext(spectrum=read_fits(path))
38
+ run_algorithm("snr_der", ctx)
39
+ run_algorithm("normalize_polynomial", ctx, {"order": 3})
40
+ run_algorithm("fit_gaussian_line", ctx,
41
+ {"line_center_angstrom": 6562.81, "window_angstrom": 30.0})
42
+ # Embedding restricted to H-α ± 50 Å for similarity comparison
43
+ region = ctx.copy()
44
+ run_algorithm("extract_region", region,
45
+ {"wavelength_min": 6512.81, "wavelength_max": 6612.81})
46
+ run_algorithm("embed_spectrum", region, {"dim": 128, "strategy": "dct"})
47
+ ```
48
+
49
+ 124/124 spectra pass the basic SNR/RV sanity filter — Deneb is bright
50
+ enough that even modest amateur setups give usable spectra.
51
+
52
+ ## Result 1 — RV vs time
53
+
54
+ Centroid of the H-α Gaussian fit, converted to radial velocity (km/s),
55
+ median-subtracted:
56
+
57
+ ![α Cyg RV time series](../images/notebooks/alpha_cyg_rv_timeseries.png)
58
+
59
+ The series shows **two main observing campaigns** (a dense 2023 cluster
60
+ around MJD 60 100–60 250, and a 2024 cluster around MJD 60 400–60 550)
61
+ plus a few isolated 2020 / 2023-summer points. Within each campaign, RV
62
+ scatter is **~±20 km/s**, consistent with α Cyg-variable pulsation
63
+ amplitudes documented in the literature. Between campaigns, there is a
64
+ **systematic offset** (the 2024 mean RV sits ~15 km/s above the 2023 mean)
65
+ that could be a real long-term modulation OR an instrument-mix effect
66
+ between observers — the kernel can't tell those apart.
67
+
68
+ ## Result 2 — Periodogram (and an honest caveat)
69
+
70
+ Lomb-Scargle on the (time, RV) pairs:
71
+
72
+ ![α Cyg periodogram](../images/notebooks/alpha_cyg_periodogram.png)
73
+
74
+ The dominant peak sits at **~553 days**, but **this is largely a sampling
75
+ artefact** rather than a stellar period: it corresponds to the spacing
76
+ between the two observing campaigns. The genuine α Cyg-variable
77
+ pulsations have published timescales of ~11–100 days; the periodogram
78
+ shows enhanced power in that range too (the bumpy plateau between 30 and
79
+ 100 days) but no single sharp peak — exactly what's expected from a
80
+ multi-periodic, semi-coherent pulsator observed at heterogeneous cadence.
81
+
82
+ > **Take-away.** A periodogram on amateur data is a *measurement*, not a
83
+ > *truth*. The kernel reports what's in the data without lying; turning
84
+ > that into astrophysics requires a critical eye on the sampling. For α
85
+ > Dra (single clean binary period, ~4 cycles densely sampled) the
86
+ > periodogram nails it. For α Cyg (multi-periodic, unevenly sampled), the
87
+ > answer is "yes there's variability on 10–100 day scales, no single
88
+ > period dominates".
89
+
90
+ ## Result 3 — H-α profile gallery
91
+
92
+ The H-α region of every spectrum, overlaid:
93
+
94
+ ![α Cyg H-α overlay](../images/notebooks/alpha_cyg_overlay.png)
95
+
96
+ Most spectra share the same characteristic A-supergiant H-α absorption
97
+ profile, with the small profile variations expected for α Cyg-variables.
98
+ A handful of low-SNR observations and a few obvious mis-calibrations stand
99
+ out — visible without any per-file labelling, just from the spread of the
100
+ flux trace.
101
+
102
+ ## Result 4 — Embedding latent space
103
+
104
+ Each H-α region is embedded to a 128-d vector via `embed_spectrum`
105
+ (`strategy="dct"`); the 124 vectors are then PCA-projected to 2D, with
106
+ each point coloured by its phase at the periodogram's best period:
107
+
108
+ ![α Cyg PCA latent space coloured by phase](../images/notebooks/alpha_cyg_pca_2d.png)
109
+
110
+ There **is** a colour gradient (early phases at right, mid phases at
111
+ centre, late phases scattered at top) but it's far less clean than the α
112
+ Dra version — because the underlying period isn't really 553 days, it's a
113
+ mix of shorter-scale pulsations and a sampling artefact. **The
114
+ embedding's latent axes still capture the most variable directions of the
115
+ dataset** — but with α Cyg those don't align with one single phase like
116
+ they did with α Dra's clean orbit.
117
+
118
+ ## What you actually learn from this dataset
119
+
120
+ - **Sub-day RV scatter of ~20 km/s** within each observing campaign,
121
+ consistent with low-amplitude pulsations of an A Ia supergiant.
122
+ - **A long-term offset between 2023 and 2024 campaigns** that requires
123
+ inter-instrument cross-calibration to interpret astrophysically.
124
+ - **No single dominant pulsation period** — α Cyg-variables are known to
125
+ be multi-periodic / quasi-periodic, and a 4-year amateur monitoring is
126
+ consistent with that.
127
+ - **The kernel pipeline reports all of the above without any
128
+ Deneb-specific tuning** — same six algorithm calls as the α Dra binary
129
+ notebook, applied to 124 files instead of 240.
130
+
131
+ ## Reproducing this
132
+
133
+ ```bash
134
+ cd playground
135
+ python3.12 -m venv venv && source venv/bin/activate
136
+ pip install -e "..[viz]" plotly kaleido
137
+ # drop your own alphacyg_*.fits files in playground/datas/alphacygni/
138
+ python analyse_alpha_cyg.py
139
+ # → output/alpha_cyg_*.png + alpha_cyg_summary.csv
140
+ ```
141
+
142
+ ## Takeaway compared to α Dra
143
+
144
+ | | α Dra (binary) | α Cyg (supergiant pulsator) |
145
+ |---|---|---|
146
+ | Underlying physics | single Keplerian orbit | multi-periodic pulsations + wind |
147
+ | Expected RV signal | clean ~120 km/s sinusoid at 51 d | semi-coherent ~±20 km/s on 10–100 d |
148
+ | Periodogram outcome | single sharp peak at 51.6 d | broad plateau + sampling-driven peak |
149
+ | Phase-folded curve | clean asymmetric loop (eccentric orbit) | mostly noise at any candidate period |
150
+ | Embedding latent space | PCA axis = orbital phase | PCA axis = mixed variability + outliers |
151
+
152
+ These are the **right** behaviours given the underlying physics — the
153
+ kernel doesn't try to force a periodicity on a star that doesn't have a
154
+ clean one. The same pipeline tells two different stories from two
155
+ different datasets, faithfully.
@@ -0,0 +1,210 @@
1
+ # α Dra — recovering a binary period from 240 amateur spectra
2
+
3
+ **A second real-world walkthrough of `spectro-kernel`, this time on a known
4
+ spectroscopic binary.** Alpha Draconis (Thuban) has a published orbital
5
+ period of **51.42 days** and a non-negligible eccentricity (e ≈ 0.42; see
6
+ Bischoff et al. 2017, A&A). The dataset below — 240 H-α spectra collected
7
+ by amateur observers between May 2022 and August 2023 — is enough to
8
+ **independently recover the period to within 0.3 %** and **reveal the
9
+ eccentric character of the orbit**.
10
+
11
+ The full script is at
12
+ [`playground/analyse_alpha_dra.py`](https://github.com/matthieulel/scal-kernel/blob/main/playground/analyse_alpha_dra.py).
13
+
14
+ ## Dataset provenance
15
+
16
+ The 240 FITS files come from an amateur monitoring campaign of α Dra
17
+ between **May 2022 and August 2023** (14-month baseline, ~10 contributing
18
+ observers, slit spectrographs typically R ≈ 5 000 – 20 000 centred on
19
+ H-α). The dense 2023 sub-campaign covers ~4 full binary orbits — enough
20
+ for a clean periodogram.
21
+
22
+ The data is **not bundled with this repository** — drop your own
23
+ `alphadra_*.fits` files into `playground/datas/alphadra/` and the script
24
+ picks them up automatically.
25
+
26
+ > If you contributed spectra to or reuse spectra from a public archive,
27
+ > please cite the relevant campaign. Personal observer attributions have
28
+ > been omitted from this notebook — the kernel showcases the analysis
29
+ > pipeline, not the source of any specific measurement.
30
+
31
+ ## The pipeline
32
+
33
+ Identical to the α Cyg one — same algorithms, same defaults — except for
34
+ two adaptations to the binary case:
35
+
36
+ 1. The Gaussian-fit window is **40 Å** (vs. 30 Å for α Cyg) because the
37
+ H-α line itself drifts by several Å across the orbit due to the
38
+ primary's Doppler motion.
39
+ 2. We record the **line CENTROID** (not just the equivalent width), and
40
+ convert wavelength shift → radial velocity:
41
+ `RV [km/s] = c × (λ_fit − λ_rest) / λ_rest`.
42
+
43
+ ```python
44
+ from spectro_kernel import WorkContext, run_algorithm
45
+ from spectro_kernel.io import read_fits
46
+
47
+ ctx = WorkContext(spectrum=read_fits(path))
48
+ run_algorithm("snr_der", ctx)
49
+ run_algorithm("normalize_polynomial", ctx, {"order": 3})
50
+ run_algorithm("fit_gaussian_line", ctx,
51
+ {"line_center_angstrom": 6562.81, "window_angstrom": 40.0})
52
+ fit = next(iter(ctx.line_fits.values()))
53
+ rv_kms = 299792.458 * (fit.line_center_angstrom - 6562.81) / 6562.81
54
+ ```
55
+
56
+ That's the per-spectrum extraction. The periodogram and phase fold then
57
+ operate on the **(time, RV)** sample set, agnostic to the kernel — they're
58
+ just `astropy.timeseries.LombScargle` calls.
59
+
60
+ ## Result 1 — Radial velocity vs time
61
+
62
+ After fitting all 240 spectra and dropping the bad-SNR / unphysical-RV
63
+ outliers, the median-subtracted RV time series:
64
+
65
+ ![α Dra RV time series](../images/notebooks/alpha_dra_rv_timeseries.png)
66
+
67
+ The 2022 epoch is sparse (a handful of points), but the 2023 campaign
68
+ already shows **the binary signal by eye**: ~4 visible peaks corresponding
69
+ to ~4 orbital cycles. Peak-to-peak amplitude ~120 km/s, which sets the
70
+ order of magnitude of the binary motion.
71
+
72
+ ## Result 2 — Lomb-Scargle periodogram
73
+
74
+ Run on the unevenly-sampled (time, RV) pairs, no pre-detrending other
75
+ than median subtraction:
76
+
77
+ ![α Dra periodogram](../images/notebooks/alpha_dra_periodogram.png)
78
+
79
+ A **single dominant peak at 51.59 days** towers above a noisy background.
80
+
81
+ | Quantity | Our value | Literature (Bischoff+ 2017) | Relative error |
82
+ |---|---|---|---|
83
+ | Orbital period (days) | **51.59** | 51.42 | **0.33 %** |
84
+
85
+ The smaller bump around 25-27 d is a harmonic — expected for an eccentric
86
+ orbit (the asymmetric RV curve has power at the 2nd harmonic of the
87
+ fundamental). The high-frequency noise around 1 day is the sampling
88
+ cadence (1 spectrum per night).
89
+
90
+ ## Result 3 — Phase-folded RV curve
91
+
92
+ Folding the 240 RV points at the best period:
93
+
94
+ ![α Dra phase folded](../images/notebooks/alpha_dra_phase_folded.png)
95
+
96
+ **The folded curve is asymmetric** — not the gentle sinusoid you'd expect
97
+ from a circular orbit. It shows a fast positive peak around phase 0.1
98
+ (reaching ~+50 km/s), then a slow descent through phase 0.3 – 0.8 (around
99
+ −15 km/s), then a slow rise back to zero. This is the canonical signature
100
+ of an **eccentric Keplerian orbit** — the primary spends more time at
101
+ apastron (slow tail) and zips through periastron (sharp peak).
102
+
103
+ The literature value for α Dra is **e ≈ 0.42**, which would produce exactly
104
+ this kind of shape. We didn't fit an explicit Kepler orbit (we could —
105
+ `fit_keplerian_orbit` exists in the catalogue), but the *qualitative*
106
+ asymmetry is unmistakable from the folded raw measurements.
107
+
108
+ ## Result 4 — The orbit is visible in the raw spectra
109
+
110
+ Before talking about latent spaces, the most direct evidence the binary
111
+ motion is in the data: bin the 240 spectra by orbital phase, average each
112
+ bin, plot the means side-by-side around H-α.
113
+
114
+ ![α Dra H-α profile stacked by phase](../images/notebooks/alpha_dra_halpha_phase_stack.png)
115
+
116
+ **The absorption notch slides left/right of the rest wavelength** (the
117
+ black dotted line at 6562.81 Å) as the orbital phase advances:
118
+
119
+ - Phase 0.5-0.67 (cyan): the line is **blueshifted** — the primary is moving toward us.
120
+ - Phase 0.0-0.17 (red, bottom) and 0.17-0.33 (yellow): the line drifts **back through
121
+ the rest wavelength and into the red** — the primary's RV reverses through
122
+ periastron.
123
+
124
+ You don't need an algorithm to see this; you just need 240 well-calibrated
125
+ amateur spectra and a phase-binning. This is the *raw spectroscopic
126
+ signature* of a spectroscopic binary.
127
+
128
+ ## Result 5 — The embedding latent space *is* the orbit
129
+
130
+ Now the algorithmic claim: each of the 240 H-α regions is embedded to a
131
+ 128-d vector via `embed_spectrum(strategy="dct")`. **The Doppler shift we
132
+ just saw by eye gets automatically encoded as the principal direction of
133
+ the embedding cloud.** Plot it the simple way — the first principal
134
+ component vs. orbital phase:
135
+
136
+ ![α Dra PC1 vs orbital phase](../images/notebooks/alpha_dra_pc1_vs_phase.png)
137
+
138
+ The shape is unmistakable: PC1 follows a clear (asymmetric) cycle in
139
+ phase, with the **same morphology** as the RV phase-fold from Result 3 —
140
+ sharp positive peak at phase 0.1, slow descent to a plateau around
141
+ phase 0.4-0.7, slow rise back. A pure sinusoid here would mean a
142
+ circular orbit; this asymmetric shape echoes the eccentricity again.
143
+
144
+ In other words, **PCA on the embedding cloud rediscovers the radial
145
+ velocity**, with no input other than "flux → DCT → truncate → L2".
146
+
147
+ For completeness, the 2D PCA scatter coloured by phase tells the same
148
+ story in two dimensions:
149
+
150
+ ![α Dra PCA latent space coloured by phase](../images/notebooks/alpha_dra_pca_2d.png)
151
+
152
+ A horizontal colour gradient (blue on the left for late phases, red on
153
+ the right for early phases) shows that PC1 carries the phase axis. The
154
+ 2D plot is busier than the 1D PC1-vs-phase view above, but it confirms
155
+ the structure isn't a fluke of one direction.
156
+
157
+ We did not tell the embedding what to look for. We just gave it
158
+ ``flux → DCT → truncate → L2 normalise`` and let it run. The Doppler shift
159
+ of H-α across the orbit ends up encoded in one direction of the latent
160
+ space — which is exactly the property a similarity layer or a clustering
161
+ algorithm needs to leverage.
162
+
163
+ A small caveat: a handful of low-SNR / mis-fit spectra would otherwise
164
+ dominate the leading PCs and hide the phase structure of the bulk. The
165
+ plot above uses the inner 95 % of the embedding cloud (15 outliers
166
+ dropped on a robust Euclidean-distance criterion). The dropped points
167
+ are real — they correspond to spectra where the H-α profile fit fails or
168
+ where the data quality is too poor — but their structure is independent
169
+ of orbital phase, so removing them lets the phase signal become visible.
170
+
171
+ For completeness, the pairwise cosine-similarity matrix re-ordered by
172
+ phase tells the same story in a different form:
173
+
174
+ ![α Dra similarity by phase](../images/notebooks/alpha_dra_similarity_phase.png)
175
+
176
+ (At 240 × 240 the per-cell labels are unreadable in this preview — the
177
+ full-resolution version is in the script's output folder.) Spectra near
178
+ the same orbital phase are slightly more similar to each other than to
179
+ spectra at the opposite phase, which gives a faint block-diagonal pattern
180
+ in the third decimal of the cosine similarity.
181
+
182
+ ## Reproducing this
183
+
184
+ ```bash
185
+ cd playground
186
+ python3.12 -m venv venv && source venv/bin/activate
187
+ pip install -e "..[viz]" plotly kaleido
188
+ # drop your own alphadra_*.fits files in playground/datas/alphadra/
189
+ python analyse_alpha_dra.py
190
+ # → output/alpha_dra_*.png + alpha_dra_summary.csv
191
+ ```
192
+
193
+ ## Takeaways
194
+
195
+ - **A 70-year-old published orbital period drops out of 240 amateur
196
+ spectra**, recovered to better than 1 % from the same kernel pipeline
197
+ that loads, normalises, and fits H-α on every file.
198
+ - **The eccentric character of the orbit is visible** in the shape of the
199
+ phase-folded curve, without ever explicitly fitting a Kepler model.
200
+ - **The principal axis of the embedding latent space *is* the orbital
201
+ phase**. Project the 240 vectors onto their first two PCs, colour by
202
+ phase: you get a smooth gradient. The DCT recipe, given nothing but
203
+ flux, ended up encoding the Doppler shift as a vector dimension — a
204
+ free property to exploit for clustering and similarity search.
205
+ - This is also the **first concrete demonstration** that amateur
206
+ spectroscopy at moderate resolution is more than sufficient to do real
207
+ binary-star science when the data are reduced through a consistent,
208
+ reproducible pipeline. The kernel didn't need any α-Dra-specific
209
+ tuning — it's the same `snr_der → normalize → fit_gaussian_line` recipe
210
+ used on α Cyg, run on 240 files instead of 18.
@@ -0,0 +1,17 @@
1
+ """The algorithm catalogue.
2
+
3
+ Importing this package is a no-op: it does **not** trigger algorithm
4
+ discovery on its own. Discovery is driven from
5
+ :func:`spectro_kernel.registry.ensure_discovered`, which is called
6
+ lazily by every registry getter (``list_algorithms``, ``get_algorithm``,
7
+ ``run_algorithm``, …). This keeps importing helpers from inside the
8
+ ``algorithms.*`` tree (for example
9
+ ``spectro_kernel.algorithms._common``) free of any circular-import
10
+ hazard.
11
+
12
+ Adding a new algorithm is still as simple as dropping a file under
13
+ ``algorithms/<category>/`` with a ``@register_algorithm`` decorator —
14
+ the lazy walk picks it up the first time the registry is queried.
15
+ """
16
+
17
+ from __future__ import annotations