spectro-kernel 0.1.2__tar.gz → 0.1.3__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 (241) hide show
  1. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/CHANGELOG.md +36 -0
  2. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/PKG-INFO +1 -1
  3. spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_ew_timeseries.png +0 -0
  4. spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_halpha_phase_stack.png +0 -0
  5. spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_halpha_timeseries.png +0 -0
  6. spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_overlay.png +0 -0
  7. spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_pc1_vs_phase.png +0 -0
  8. spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_pca_2d.png +0 -0
  9. spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_pca_3d.png +0 -0
  10. spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_periodogram.png +0 -0
  11. spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_phase_folded.png +0 -0
  12. spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_rv_timeseries.png +0 -0
  13. spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_similarity.png +0 -0
  14. spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_similarity_date.png +0 -0
  15. spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_halpha_phase_stack.png +0 -0
  16. spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_pc1_vs_phase.png +0 -0
  17. spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_pca_2d.png +0 -0
  18. spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_pca_3d.png +0 -0
  19. spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_periodogram.png +0 -0
  20. spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_phase_folded.png +0 -0
  21. spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_rv_timeseries.png +0 -0
  22. spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_similarity_date.png +0 -0
  23. spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_similarity_phase.png +0 -0
  24. spectro_kernel-0.1.3/docs/notebooks/alpha-cyg-time-series.md +155 -0
  25. spectro_kernel-0.1.3/docs/notebooks/alpha-dra-binary-period.md +210 -0
  26. spectro_kernel-0.1.3/src/spectro_kernel/algorithms/__init__.py +17 -0
  27. spectro_kernel-0.1.3/src/spectro_kernel/algorithms/io/read_sdss.py +121 -0
  28. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/registry.py +25 -4
  29. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/version.py +1 -1
  30. spectro_kernel-0.1.3/tests/unit/test_no_circular_imports.py +89 -0
  31. spectro_kernel-0.1.3/tests/unit/test_read_sdss.py +76 -0
  32. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_registry.py +1 -0
  33. spectro_kernel-0.1.2/src/spectro_kernel/algorithms/__init__.py +0 -20
  34. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/.gitignore +0 -0
  35. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/LICENSE +0 -0
  36. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/README.md +0 -0
  37. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/concepts/algorithms.md +0 -0
  38. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/concepts/architecture.md +0 -0
  39. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/concepts/data-types.md +0 -0
  40. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/concepts/pipelines.md +0 -0
  41. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/contributing.md +0 -0
  42. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/cookbook/bess-dashboard.md +0 -0
  43. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/cookbook/multi-star-viewer.md +0 -0
  44. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/cookbook/web-playground.md +0 -0
  45. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/gen_catalogue.py +0 -0
  46. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/getting-started.md +0 -0
  47. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/index.md +0 -0
  48. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/notebooks/aurora-line-monitor.md +0 -0
  49. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/notebooks/be-star-variability.md +0 -0
  50. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/notebooks/claude-mcp-end-to-end.md +0 -0
  51. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/notebooks/exoplanet-transit-rv.md +0 -0
  52. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/notebooks/full-reduction-walkthrough.md +0 -0
  53. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/notebooks/native-vs-easyspec-showdown.md +0 -0
  54. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/notebooks/your-first-sb2.md +0 -0
  55. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/reference.md +0 -0
  56. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/tutorials/add-an-algorithm.md +0 -0
  57. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/tutorials/analyse-a-spectrum.md +0 -0
  58. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/tutorials/discover-the-catalogue.md +0 -0
  59. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/tutorials/first-spectrum.md +0 -0
  60. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/usage/cli.md +0 -0
  61. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/usage/library.md +0 -0
  62. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/usage/mcp.md +0 -0
  63. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/why.md +0 -0
  64. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/playground/README.md +0 -0
  65. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/pyproject.toml +0 -0
  66. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/__init__.py +0 -0
  67. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/adapters/__init__.py +0 -0
  68. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/adapters/easyspec.py +0 -0
  69. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/_common.py +0 -0
  70. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/advanced/__init__.py +0 -0
  71. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/advanced/aperture_photometry.py +0 -0
  72. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/advanced/disentangle_sb2.py +0 -0
  73. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/catalogs/__init__.py +0 -0
  74. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/catalogs/gaia.py +0 -0
  75. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/catalogs/simbad.py +0 -0
  76. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/catalogs/vizier.py +0 -0
  77. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/continuum/__init__.py +0 -0
  78. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/continuum/compare_normalisations.py +0 -0
  79. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/continuum/normalize_edges.py +0 -0
  80. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/continuum/normalize_max.py +0 -0
  81. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/continuum/normalize_percentile.py +0 -0
  82. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/continuum/normalize_polynomial.py +0 -0
  83. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/continuum/subtract_continuum.py +0 -0
  84. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/__init__.py +0 -0
  85. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/air_vacuum.py +0 -0
  86. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/barycentric.py +0 -0
  87. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/doppler_shift.py +0 -0
  88. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/extinction_correct_easyspec.py +0 -0
  89. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/fit_telluric_scaling.py +0 -0
  90. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/flux_calibrate_easyspec.py +0 -0
  91. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/remove_telluric.py +0 -0
  92. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/synth_telluric.py +0 -0
  93. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/__init__.py +0 -0
  94. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/embed_band_power.py +0 -0
  95. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/embed_continuum_subtracted.py +0 -0
  96. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/embed_lick_indices.py +0 -0
  97. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/embed_log_lambda.py +0 -0
  98. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/embed_pretrained.py +0 -0
  99. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/embed_remote.py +0 -0
  100. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/embed_spectrum.py +0 -0
  101. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/embed_wavelets.py +0 -0
  102. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/exports/__init__.py +0 -0
  103. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/exports/export_csv.py +0 -0
  104. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/exports/export_fits.py +0 -0
  105. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/exports/export_hdf5.py +0 -0
  106. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/exports/export_votable.py +0 -0
  107. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/extraction/__init__.py +0 -0
  108. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/extraction/easyspec_extract.py +0 -0
  109. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/io/__init__.py +0 -0
  110. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/io/read_ascii.py +0 -0
  111. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/io/read_echelle.py +0 -0
  112. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/io/read_fits.py +0 -0
  113. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/io/read_votable.py +0 -0
  114. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/__init__.py +0 -0
  115. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/_profiles.py +0 -0
  116. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/catalogs.py +0 -0
  117. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/compare_line_fits.py +0 -0
  118. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/detect.py +0 -0
  119. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/equivalent_width.py +0 -0
  120. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/fit_gaussian.py +0 -0
  121. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/fit_lorentzian.py +0 -0
  122. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/fit_voigt.py +0 -0
  123. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/quality/__init__.py +0 -0
  124. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/quality/compare_snr_methods.py +0 -0
  125. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/quality/snr_der.py +0 -0
  126. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/quality/snr_edge.py +0 -0
  127. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/quality/snr_linear_fit.py +0 -0
  128. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/__init__.py +0 -0
  129. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/_easyspec_apply.py +0 -0
  130. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/_easyspec_helpers.py +0 -0
  131. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/bias_combine.py +0 -0
  132. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/clip_cosmic_rays.py +0 -0
  133. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/dark_subtract.py +0 -0
  134. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/easyspec_bias.py +0 -0
  135. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/easyspec_cosmic_ray.py +0 -0
  136. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/easyspec_dark.py +0 -0
  137. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/easyspec_flat.py +0 -0
  138. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/easyspec_flat_normalize.py +0 -0
  139. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/easyspec_subtract_bias.py +0 -0
  140. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/easyspec_subtract_dark.py +0 -0
  141. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/extract_spectrum_sum.py +0 -0
  142. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/flat_normalize.py +0 -0
  143. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/subtract_sky_2d.py +0 -0
  144. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/wavelength_calibrate.py +0 -0
  145. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/rv/__init__.py +0 -0
  146. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/rv/cross_correlate.py +0 -0
  147. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/rv/fit_keplerian_orbit.py +0 -0
  148. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/rv/measure.py +0 -0
  149. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/rv/precision_bouchy.py +0 -0
  150. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/smoothing/__init__.py +0 -0
  151. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/smoothing/compare_smoothings.py +0 -0
  152. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/smoothing/smooth_gaussian.py +0 -0
  153. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/smoothing/smooth_savgol.py +0 -0
  154. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/stacking/__init__.py +0 -0
  155. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/stacking/merge_echelle_orders.py +0 -0
  156. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/stacking/stack.py +0 -0
  157. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/timeseries/__init__.py +0 -0
  158. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/timeseries/lomb_scargle.py +0 -0
  159. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/timeseries/phase_fold.py +0 -0
  160. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/transforms/__init__.py +0 -0
  161. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/transforms/clip_sigma.py +0 -0
  162. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/transforms/extract_region.py +0 -0
  163. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/transforms/mask_range.py +0 -0
  164. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/transforms/resample.py +0 -0
  165. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/transforms/resample_flux_conserving.py +0 -0
  166. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/viz/__init__.py +0 -0
  167. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/viz/plot_3d_surface.py +0 -0
  168. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/viz/plot_animation.py +0 -0
  169. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/viz/plot_dynamic_spectrum.py +0 -0
  170. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/viz/plot_plotly.py +0 -0
  171. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/wavelength_calibration/__init__.py +0 -0
  172. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/wavelength_calibration/easyspec_wavelength.py +0 -0
  173. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/base.py +0 -0
  174. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/cli.py +0 -0
  175. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/embeddings.py +0 -0
  176. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/errors.py +0 -0
  177. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/io/__init__.py +0 -0
  178. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/io/ascii.py +0 -0
  179. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/io/fits.py +0 -0
  180. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/io/votable.py +0 -0
  181. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/pipeline.py +0 -0
  182. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/__init__.py +0 -0
  183. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/catalog/analysis/balmer_quick.yaml +0 -0
  184. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/catalog/analysis/embed_quick.yaml +0 -0
  185. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/catalog/analysis/quality_report.yaml +0 -0
  186. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/catalog/analysis/rv_quick.yaml +0 -0
  187. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/catalog/analysis/snr_check.yaml +0 -0
  188. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/catalog/analysis/time_series_overview.yaml +0 -0
  189. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/catalog/reduction/full_reduction_easyspec.yaml +0 -0
  190. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/loader.py +0 -0
  191. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/py.typed +0 -0
  192. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/__init__.py +0 -0
  193. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/catalog.py +0 -0
  194. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/context.py +0 -0
  195. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/enums.py +0 -0
  196. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/history.py +0 -0
  197. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/image.py +0 -0
  198. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/line.py +0 -0
  199. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/spectrum.py +0 -0
  200. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/timeseries.py +0 -0
  201. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_mcp/__init__.py +0 -0
  202. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_mcp/__main__.py +0 -0
  203. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_mcp/auth.py +0 -0
  204. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_mcp/auto_tools.py +0 -0
  205. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_mcp/observability.py +0 -0
  206. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_mcp/py.typed +0 -0
  207. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_mcp/server.py +0 -0
  208. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_mcp/session.py +0 -0
  209. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/conftest.py +0 -0
  210. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/reference/conftest.py +0 -0
  211. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/reference/data/.gitkeep +0 -0
  212. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/reference/data/README.md +0 -0
  213. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/reference/data/sun/sun_reference_stis_002.fits +0 -0
  214. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/reference/data/vega/alpha_lyr_stis_011.fits +0 -0
  215. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/reference/test_known_answers.py +0 -0
  216. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/reference/test_sun.py +0 -0
  217. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/reference/test_vega.py +0 -0
  218. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_base.py +0 -0
  219. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_cli.py +0 -0
  220. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_combine.py +0 -0
  221. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_continuum.py +0 -0
  222. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_corrections.py +0 -0
  223. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_easyspec_wrappers.py +0 -0
  224. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_embedding.py +0 -0
  225. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_embedding_pretrained.py +0 -0
  226. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_embedding_remote_lick.py +0 -0
  227. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_embedding_tier1.py +0 -0
  228. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_io.py +0 -0
  229. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_line_profiles.py +0 -0
  230. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_lines.py +0 -0
  231. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_mcp.py +0 -0
  232. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_mcp_production.py +0 -0
  233. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_misc_algorithms.py +0 -0
  234. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_pipeline.py +0 -0
  235. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_quality.py +0 -0
  236. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_smoothing.py +0 -0
  237. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_timeseries.py +0 -0
  238. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_transforms.py +0 -0
  239. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_types.py +0 -0
  240. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_viz.py +0 -0
  241. {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/website/README.md +0 -0
@@ -6,6 +6,42 @@ Until `1.0.0` the public API may change between minor versions.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.3] — 2026-06-09
10
+
11
+ ### Fixed — circular import that prevented `from spectro_kernel.embeddings import …`
12
+
13
+ In 0.1.2, `spectro_kernel.algorithms.__init__` ran an eager submodule
14
+ walk at import time. That walk imported every algorithm, including
15
+ `embed_continuum_subtracted`, which itself did a top-level
16
+ `from ...embeddings import VALID_NORM_METHODS, VALID_STRATEGIES,
17
+ embed_flux`. But `embeddings` itself imports
18
+ `spectro_kernel.algorithms._common` before defining `VALID_*` /
19
+ `embed_flux` — so when an external consumer wrote
20
+ `from spectro_kernel.embeddings import embed_flux` as its very first
21
+ contact with the kernel, Python's import machinery saw a partially
22
+ initialised module and raised `ImportError`.
23
+
24
+ Reported by a downstream library whose service worker crashed
25
+ deterministically on the same PyPI 0.1.2 wheel.
26
+
27
+ The fix moves discovery into `registry.ensure_discovered()`, which was
28
+ already the lazy entry point — every registry getter (`list_algorithms`,
29
+ `get_algorithm`, `run_algorithm`, …) calls it before answering.
30
+ Importing `spectro_kernel.algorithms` is now a true no-op, so pulling a
31
+ helper out of `algorithms._common` from inside `embeddings` no longer
32
+ triggers a transitive cycle. Discovery now silently skips any
33
+ optional-extra algorithm whose dependency is missing, instead of
34
+ breaking the entire import chain.
35
+
36
+ Behaviour preservation: existing callers that go through the registry
37
+ see the same catalogue at the same time (the first registry call
38
+ populates it, exactly as before). The only change is **when** discovery
39
+ runs — it's properly lazy now.
40
+
41
+ Added 4 regression tests in `test_no_circular_imports.py` that run the
42
+ exact cold-start imports the bug reporter hit, in fresh subprocesses,
43
+ so a future re-introduction of the cycle is caught immediately by CI.
44
+
9
45
  ## [0.1.2] — 2026-06-05
10
46
 
11
47
  ### 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.3
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
@@ -0,0 +1,121 @@
1
+ """Algorithm: read an SDSS spectrum (loglam-based binary table)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any
7
+
8
+ import numpy as np
9
+ from astropy.io import fits
10
+
11
+ from ...base import AlgorithmOutput, BaseAlgorithm
12
+ from ...registry import register_algorithm
13
+ from ...types import AlgorithmCategory, Spectrum1D, WorkContext
14
+
15
+
16
+ @register_algorithm("read_sdss_spectrum", category=AlgorithmCategory.IO, version="1.0.0")
17
+ class ReadSdssSpectrum(BaseAlgorithm):
18
+ """Read an SDSS-format spectrum (``spec-*.fits``) into ``ctx.spectrum``.
19
+
20
+ SDSS spectra store the wavelength axis as ``loglam`` (log10 of the
21
+ wavelength in Å) and the flux as ``flux`` in a binary-table extension.
22
+ This is incompatible with the generic ``read_fits`` algorithm (which
23
+ looks for a column named ``wavelength``/``wave``/``lambda``), so we
24
+ handle it explicitly here.
25
+
26
+ The reader:
27
+
28
+ 1. Opens the file (local path or http(s) URL).
29
+ 2. Locates the spectrum extension — typically HDU 1 (``COADD``), with
30
+ ``flux``, ``loglam``, ``ivar`` columns.
31
+ 3. Converts ``loglam`` → linear Å.
32
+ 4. Converts inverse variance ``ivar`` → 1σ uncertainty.
33
+ 5. Returns a ``Spectrum1D`` in the kernel's canonical form.
34
+
35
+ Flux units in SDSS are 10⁻¹⁷ erg/s/cm²/Å — stored as-is in
36
+ ``Spectrum1D.flux_unit`` for downstream sanity.
37
+ """
38
+
39
+ backend = "astropy"
40
+ references = [
41
+ "York et al. 2000, AJ, 120, 1579 — Sloan Digital Sky Survey overview.",
42
+ "Smee et al. 2013, AJ, 146, 32 — SDSS BOSS spectrograph and data format.",
43
+ "Bolton et al. 2012, AJ, 144, 144 — DR9 spectroscopic data release "
44
+ "documenting the spec-*.fits layout.",
45
+ ]
46
+ long_description = (
47
+ "Reading ``loglam``-based spectra is the one big gap of the generic "
48
+ "FITS reader — this algorithm closes it. Compatible with SDSS DR9 "
49
+ "through the current DR (the ``COADD`` HDU layout has been stable "
50
+ "since DR9)."
51
+ )
52
+ default_params = {"path": None}
53
+ required_params = ["path"]
54
+ param_descriptions = {
55
+ "path": "Local path or http(s) URL of an SDSS spec-*.fits file."
56
+ }
57
+ input_requirements: list[str] = []
58
+ output_produces = ["spectrum"]
59
+
60
+ def run(self, ctx: WorkContext, params: dict[str, Any]) -> AlgorithmOutput:
61
+ path = params["path"]
62
+ with fits.open(path) as hdulist:
63
+ hdu = _find_spectrum_hdu(hdulist)
64
+ if hdu is None:
65
+ return AlgorithmOutput.fail(
66
+ f"No SDSS spectrum extension found in {os.path.basename(str(path))}; "
67
+ "expected an extension with 'flux' + 'loglam' columns."
68
+ )
69
+ data = hdu.data
70
+ loglam = np.asarray(data["loglam"], dtype=np.float64).ravel()
71
+ flux = np.asarray(data["flux"], dtype=np.float64).ravel()
72
+ wavelength = np.power(10.0, loglam)
73
+ uncertainty: np.ndarray | None = None
74
+ if "ivar" in data.names:
75
+ ivar = np.asarray(data["ivar"], dtype=np.float64).ravel()
76
+ with np.errstate(divide="ignore", invalid="ignore"):
77
+ uncertainty = np.where(ivar > 0.0, 1.0 / np.sqrt(ivar), np.nan)
78
+
79
+ mask: np.ndarray | None = None
80
+ if "and_mask" in data.names:
81
+ mask = np.asarray(data["and_mask"], dtype=np.int64).ravel() != 0
82
+
83
+ meta: dict[str, Any] = {"source": str(path), "format": "sdss"}
84
+ # Carry the standard SDSS keywords if present (helpful for downstream tools).
85
+ with fits.open(path) as hdulist:
86
+ primary = hdulist[0].header
87
+ for key in ("PLUG_RA", "PLUG_DEC", "PLATE", "MJD", "FIBERID", "OBJTYPE"):
88
+ if key in primary:
89
+ meta[key.lower()] = primary[key]
90
+
91
+ ctx.spectrum = Spectrum1D(
92
+ wavelength=wavelength,
93
+ flux=flux,
94
+ uncertainty=uncertainty,
95
+ mask=mask,
96
+ wavelength_unit="Angstrom",
97
+ flux_unit="1e-17 erg/s/cm2/Angstrom",
98
+ meta=meta,
99
+ )
100
+ return AlgorithmOutput.ok(
101
+ metrics={
102
+ "npix": float(wavelength.size),
103
+ "wavelength_min": float(wavelength.min()),
104
+ "wavelength_max": float(wavelength.max()),
105
+ },
106
+ message=(
107
+ f"Loaded SDSS spectrum: {wavelength.size} pixels, "
108
+ f"{wavelength.min():.0f}-{wavelength.max():.0f} Å."
109
+ ),
110
+ )
111
+
112
+
113
+ def _find_spectrum_hdu(hdulist: fits.HDUList) -> fits.BinTableHDU | None:
114
+ """Return the first BinTableHDU that has 'flux' + 'loglam' columns."""
115
+ for hdu in hdulist:
116
+ if not isinstance(hdu, fits.BinTableHDU) or hdu.data is None:
117
+ continue
118
+ names = {n.lower() for n in hdu.data.names}
119
+ if "flux" in names and "loglam" in names:
120
+ return hdu
121
+ return None
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  import importlib
12
12
  import inspect
13
+ import pkgutil
13
14
  from collections.abc import Callable
14
15
  from dataclasses import dataclass
15
16
  from pathlib import Path
@@ -152,15 +153,35 @@ def register_algorithm(
152
153
 
153
154
 
154
155
  def ensure_discovered() -> None:
155
- """Import the ``algorithms`` package once so every algorithm self-registers.
156
-
157
- Called automatically by the discovery functions below; safe to call repeatedly.
156
+ """Import every algorithm submodule once so each ``@register_algorithm`` runs.
157
+
158
+ Called automatically by the registry getter functions below; safe to call
159
+ repeatedly. Discovery is **lazy** on purpose: importing
160
+ ``spectro_kernel.algorithms`` (the package) used to trigger eager
161
+ submodule discovery via the package's ``__init__``, which created a
162
+ circular-import hazard for any module that pulled even one helper out of
163
+ the ``algorithms`` tree before its own top-level definitions had run
164
+ (notably ``spectro_kernel.embeddings`` which uses
165
+ ``algorithms._common.fit_polynomial_continuum_asymmetric``).
166
+ Triggering discovery here, only when something actively asks the
167
+ registry for an algorithm, sidesteps that hazard entirely.
158
168
  """
159
169
  global _discovered
160
170
  if _discovered:
161
171
  return
162
- importlib.import_module("spectro_kernel.algorithms")
172
+ # Mark *before* the walk so any algorithm that re-enters the registry
173
+ # during its own import (rare but possible) doesn't recurse infinitely.
163
174
  _discovered = True
175
+ pkg = importlib.import_module("spectro_kernel.algorithms")
176
+ for module_info in pkgutil.walk_packages(pkg.__path__, prefix=pkg.__name__ + "."):
177
+ try:
178
+ importlib.import_module(module_info.name)
179
+ except Exception: # noqa: BLE001 - optional algorithms may need extras
180
+ # An algorithm whose dependencies aren't installed should not break
181
+ # discovery of the others. The algorithm will simply not appear in
182
+ # the registry; users who actually try to call it get a clear
183
+ # ImportError from inside its own module.
184
+ continue
164
185
 
165
186
 
166
187
  def has_algorithm(name: str) -> bool:
@@ -1,3 +1,3 @@
1
1
  """Single source of truth for the package version."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.1.3"