spectro-kernel 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (303) hide show
  1. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/CHANGELOG.md +134 -0
  2. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/PKG-INFO +1 -1
  3. spectro_kernel-0.5.0/src/spectro_kernel/algorithms/continuum/normalize_polynomial.py +242 -0
  4. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/kinematics/rotation_curve.py +76 -25
  5. spectro_kernel-0.5.0/src/spectro_kernel/algorithms/rv/cross_correlate.py +404 -0
  6. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/rv/measure.py +41 -12
  7. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/rv/precision_bouchy.py +21 -8
  8. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/timeseries/lomb_scargle.py +58 -12
  9. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/version.py +1 -1
  10. spectro_kernel-0.5.0/tests/unit/test_continuum.py +125 -0
  11. spectro_kernel-0.5.0/tests/unit/test_cross_correlate_extension.py +165 -0
  12. spectro_kernel-0.5.0/tests/unit/test_line_profiles.py +115 -0
  13. spectro_kernel-0.5.0/tests/unit/test_rotation_curve.py +133 -0
  14. spectro_kernel-0.5.0/tests/unit/test_timeseries.py +79 -0
  15. spectro_kernel-0.4.0/src/spectro_kernel/algorithms/continuum/normalize_polynomial.py +0 -62
  16. spectro_kernel-0.4.0/src/spectro_kernel/algorithms/rv/cross_correlate.py +0 -151
  17. spectro_kernel-0.4.0/tests/unit/test_continuum.py +0 -40
  18. spectro_kernel-0.4.0/tests/unit/test_line_profiles.py +0 -54
  19. spectro_kernel-0.4.0/tests/unit/test_rotation_curve.py +0 -68
  20. spectro_kernel-0.4.0/tests/unit/test_timeseries.py +0 -32
  21. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/.gitignore +0 -0
  22. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/LICENSE +0 -0
  23. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/README.md +0 -0
  24. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/concepts/algorithms.md +0 -0
  25. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/concepts/architecture.md +0 -0
  26. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/concepts/data-types.md +0 -0
  27. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/concepts/pipelines.md +0 -0
  28. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/contributing.md +0 -0
  29. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/cookbook/bess-dashboard.md +0 -0
  30. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/cookbook/multi-star-viewer.md +0 -0
  31. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/cookbook/web-playground.md +0 -0
  32. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/gen_catalogue.py +0 -0
  33. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/getting-started.md +0 -0
  34. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_cyg_ew_timeseries.png +0 -0
  35. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_cyg_halpha_phase_stack.png +0 -0
  36. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_cyg_halpha_timeseries.png +0 -0
  37. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_cyg_overlay.png +0 -0
  38. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_cyg_pc1_vs_phase.png +0 -0
  39. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_cyg_pca_2d.png +0 -0
  40. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_cyg_pca_3d.png +0 -0
  41. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_cyg_periodogram.png +0 -0
  42. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_cyg_phase_folded.png +0 -0
  43. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_cyg_rv_timeseries.png +0 -0
  44. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_cyg_similarity.png +0 -0
  45. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_cyg_similarity_date.png +0 -0
  46. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_dra_halpha_phase_stack.png +0 -0
  47. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_dra_pc1_vs_phase.png +0 -0
  48. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_dra_pca_2d.png +0 -0
  49. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_dra_pca_3d.png +0 -0
  50. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_dra_periodogram.png +0 -0
  51. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_dra_phase_folded.png +0 -0
  52. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_dra_rv_timeseries.png +0 -0
  53. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_dra_similarity_date.png +0 -0
  54. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/images/notebooks/alpha_dra_similarity_phase.png +0 -0
  55. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/index.md +0 -0
  56. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/notebooks/alpha-cyg-time-series.md +0 -0
  57. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/notebooks/alpha-dra-binary-period.md +0 -0
  58. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/notebooks/aurora-line-monitor.md +0 -0
  59. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/notebooks/be-star-variability.md +0 -0
  60. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/notebooks/claude-mcp-end-to-end.md +0 -0
  61. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/notebooks/exoplanet-transit-rv.md +0 -0
  62. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/notebooks/full-reduction-walkthrough.md +0 -0
  63. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/notebooks/native-vs-easyspec-showdown.md +0 -0
  64. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/notebooks/your-first-sb2.md +0 -0
  65. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/reference.md +0 -0
  66. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/tutorials/add-an-algorithm.md +0 -0
  67. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/tutorials/analyse-a-spectrum.md +0 -0
  68. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/tutorials/discover-the-catalogue.md +0 -0
  69. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/tutorials/first-spectrum.md +0 -0
  70. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/tutorials/long-slit-reduction.md +0 -0
  71. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/usage/cli.md +0 -0
  72. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/usage/library.md +0 -0
  73. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/usage/mcp.md +0 -0
  74. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/docs/why.md +0 -0
  75. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/playground/README.md +0 -0
  76. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/pyproject.toml +0 -0
  77. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/__init__.py +0 -0
  78. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/adapters/__init__.py +0 -0
  79. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/adapters/easyspec.py +0 -0
  80. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/__init__.py +0 -0
  81. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/_common.py +0 -0
  82. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/advanced/__init__.py +0 -0
  83. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/advanced/aperture_photometry.py +0 -0
  84. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/advanced/disentangle_sb2.py +0 -0
  85. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/advanced/doppler_tomogram.py +0 -0
  86. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/catalogs/__init__.py +0 -0
  87. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/catalogs/gaia.py +0 -0
  88. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/catalogs/simbad.py +0 -0
  89. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/catalogs/vizier.py +0 -0
  90. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/classification/__init__.py +0 -0
  91. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/classification/classify_template_chi2.py +0 -0
  92. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/classification/pickles_atlas.py +0 -0
  93. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/continuum/__init__.py +0 -0
  94. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/continuum/compare_normalisations.py +0 -0
  95. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/continuum/normalize_edges.py +0 -0
  96. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/continuum/normalize_max.py +0 -0
  97. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/continuum/normalize_percentile.py +0 -0
  98. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/continuum/normalize_region.py +0 -0
  99. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/continuum/subtract_continuum.py +0 -0
  100. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/corrections/__init__.py +0 -0
  101. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/corrections/air_vacuum.py +0 -0
  102. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/corrections/barycentric.py +0 -0
  103. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/corrections/doppler_shift.py +0 -0
  104. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/corrections/extinction_correct_easyspec.py +0 -0
  105. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/corrections/fit_telluric_scaling.py +0 -0
  106. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/corrections/flux_calibrate_easyspec.py +0 -0
  107. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/corrections/remove_telluric.py +0 -0
  108. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/corrections/response_from_standard.py +0 -0
  109. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/corrections/synth_telluric.py +0 -0
  110. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/embedding/__init__.py +0 -0
  111. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/embedding/embed_band_power.py +0 -0
  112. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/embedding/embed_continuum_subtracted.py +0 -0
  113. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/embedding/embed_lick_indices.py +0 -0
  114. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/embedding/embed_log_lambda.py +0 -0
  115. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/embedding/embed_pretrained.py +0 -0
  116. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/embedding/embed_remote.py +0 -0
  117. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/embedding/embed_spectrum.py +0 -0
  118. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/embedding/embed_wavelets.py +0 -0
  119. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/exports/__init__.py +0 -0
  120. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/exports/export_csv.py +0 -0
  121. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/exports/export_fits.py +0 -0
  122. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/exports/export_fits_bess.py +0 -0
  123. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/exports/export_hdf5.py +0 -0
  124. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/exports/export_votable.py +0 -0
  125. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/extraction/__init__.py +0 -0
  126. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/extraction/boxcar.py +0 -0
  127. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/extraction/detect_trace.py +0 -0
  128. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/extraction/easyspec_extract.py +0 -0
  129. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/extraction/optimal.py +0 -0
  130. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/extraction/sky_lateral_bands.py +0 -0
  131. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/io/__init__.py +0 -0
  132. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/io/read_ascii.py +0 -0
  133. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/io/read_echelle.py +0 -0
  134. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/io/read_fits.py +0 -0
  135. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/io/read_sdss.py +0 -0
  136. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/io/read_votable.py +0 -0
  137. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/kinematics/__init__.py +0 -0
  138. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/lines/__init__.py +0 -0
  139. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/lines/_line_flux.py +0 -0
  140. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/lines/_profiles.py +0 -0
  141. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/lines/catalogs.py +0 -0
  142. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/lines/compare_line_fits.py +0 -0
  143. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/lines/detect.py +0 -0
  144. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/lines/equivalent_width.py +0 -0
  145. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/lines/fit_gaussian.py +0 -0
  146. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/lines/fit_lorentzian.py +0 -0
  147. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/lines/fit_voigt.py +0 -0
  148. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/lines/vr_ratio.py +0 -0
  149. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/nebular/__init__.py +0 -0
  150. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/nebular/bpt_line_ratios.py +0 -0
  151. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/nebular/oiii_electron_temperature.py +0 -0
  152. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/nebular/sii_electron_density.py +0 -0
  153. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/quality/__init__.py +0 -0
  154. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/quality/compare_snr_methods.py +0 -0
  155. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/quality/snr_der.py +0 -0
  156. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/quality/snr_edge.py +0 -0
  157. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/quality/snr_linear_fit.py +0 -0
  158. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/__init__.py +0 -0
  159. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/_easyspec_apply.py +0 -0
  160. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/_easyspec_helpers.py +0 -0
  161. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/bias_combine.py +0 -0
  162. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/clip_cosmic_rays.py +0 -0
  163. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/dark_combine.py +0 -0
  164. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/dark_subtract.py +0 -0
  165. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/denoise_2d.py +0 -0
  166. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/easyspec_bias.py +0 -0
  167. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/easyspec_cosmic_ray.py +0 -0
  168. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/easyspec_dark.py +0 -0
  169. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/easyspec_flat.py +0 -0
  170. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/easyspec_flat_normalize.py +0 -0
  171. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/easyspec_subtract_bias.py +0 -0
  172. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/easyspec_subtract_dark.py +0 -0
  173. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/extract_spectrum_sum.py +0 -0
  174. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/flat_combine.py +0 -0
  175. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/flat_normalize.py +0 -0
  176. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/geometry.py +0 -0
  177. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/subtract_sky_2d.py +0 -0
  178. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/reduction/wavelength_calibrate.py +0 -0
  179. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/rv/__init__.py +0 -0
  180. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/rv/fit_keplerian_orbit.py +0 -0
  181. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/rv/redshift_lines.py +0 -0
  182. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/smoothing/__init__.py +0 -0
  183. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/smoothing/compare_smoothings.py +0 -0
  184. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/smoothing/smooth_gaussian.py +0 -0
  185. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/smoothing/smooth_savgol.py +0 -0
  186. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/stacking/__init__.py +0 -0
  187. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/stacking/merge_echelle_orders.py +0 -0
  188. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/stacking/stack.py +0 -0
  189. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/timeseries/__init__.py +0 -0
  190. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/timeseries/phase_fold.py +0 -0
  191. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/transforms/__init__.py +0 -0
  192. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/transforms/clip_sigma.py +0 -0
  193. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/transforms/combine_arithmetic.py +0 -0
  194. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/transforms/extract_region.py +0 -0
  195. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/transforms/mask_range.py +0 -0
  196. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/transforms/resample.py +0 -0
  197. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/transforms/resample_flux_conserving.py +0 -0
  198. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/viz/__init__.py +0 -0
  199. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/viz/plot_3d_surface.py +0 -0
  200. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/viz/plot_animation.py +0 -0
  201. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/viz/plot_dynamic_spectrum.py +0 -0
  202. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/viz/plot_plotly.py +0 -0
  203. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/wavelength_calibration/__init__.py +0 -0
  204. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/wavelength_calibration/arc_geometry.py +0 -0
  205. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/wavelength_calibration/easyspec_wavelength.py +0 -0
  206. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/wavelength_calibration/in_situ.py +0 -0
  207. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/wavelength_calibration/lamp_atlas.py +0 -0
  208. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/wavelength_calibration/match_lamp.py +0 -0
  209. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/algorithms/wavelength_calibration/solar.py +0 -0
  210. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/base.py +0 -0
  211. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/cli.py +0 -0
  212. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/embeddings.py +0 -0
  213. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/errors.py +0 -0
  214. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/io/__init__.py +0 -0
  215. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/io/ascii.py +0 -0
  216. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/io/fits.py +0 -0
  217. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/io/votable.py +0 -0
  218. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/pipeline.py +0 -0
  219. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/presets/__init__.py +0 -0
  220. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/presets/catalog/analysis/balmer_quick.yaml +0 -0
  221. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/presets/catalog/analysis/embed_quick.yaml +0 -0
  222. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/presets/catalog/analysis/quality_report.yaml +0 -0
  223. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/presets/catalog/analysis/rv_quick.yaml +0 -0
  224. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/presets/catalog/analysis/snr_check.yaml +0 -0
  225. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/presets/catalog/analysis/time_series_overview.yaml +0 -0
  226. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/presets/catalog/reduction/full_reduction_easyspec.yaml +0 -0
  227. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/presets/loader.py +0 -0
  228. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/py.typed +0 -0
  229. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/registry.py +0 -0
  230. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/types/__init__.py +0 -0
  231. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/types/catalog.py +0 -0
  232. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/types/context.py +0 -0
  233. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/types/enums.py +0 -0
  234. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/types/history.py +0 -0
  235. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/types/image.py +0 -0
  236. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/types/line.py +0 -0
  237. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/types/spectrum.py +0 -0
  238. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_kernel/types/timeseries.py +0 -0
  239. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_mcp/__init__.py +0 -0
  240. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_mcp/__main__.py +0 -0
  241. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_mcp/auth.py +0 -0
  242. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_mcp/auto_tools.py +0 -0
  243. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_mcp/observability.py +0 -0
  244. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_mcp/py.typed +0 -0
  245. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_mcp/server.py +0 -0
  246. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_mcp/session.py +0 -0
  247. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/src/spectro_mcp/url_safety.py +0 -0
  248. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/conftest.py +0 -0
  249. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/reference/conftest.py +0 -0
  250. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/reference/data/.gitkeep +0 -0
  251. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/reference/data/README.md +0 -0
  252. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/reference/data/sun/sun_reference_stis_002.fits +0 -0
  253. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/reference/data/vega/alpha_lyr_stis_011.fits +0 -0
  254. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/reference/test_known_answers.py +0 -0
  255. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/reference/test_sun.py +0 -0
  256. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/reference/test_vega.py +0 -0
  257. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_base.py +0 -0
  258. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_classify_template_chi2.py +0 -0
  259. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_cli.py +0 -0
  260. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_combine.py +0 -0
  261. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_combine_arithmetic.py +0 -0
  262. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_corrections.py +0 -0
  263. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_dark_flat_combine.py +0 -0
  264. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_denoise_2d.py +0 -0
  265. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_detect_lines_blind.py +0 -0
  266. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_detect_trace.py +0 -0
  267. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_doppler_tomogram.py +0 -0
  268. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_easyspec_apply_staging.py +0 -0
  269. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_easyspec_wrappers.py +0 -0
  270. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_embedding.py +0 -0
  271. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_embedding_pretrained.py +0 -0
  272. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_embedding_remote_lick.py +0 -0
  273. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_embedding_tier1.py +0 -0
  274. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_export_fits_bess.py +0 -0
  275. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_extract_boxcar.py +0 -0
  276. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_extract_optimal.py +0 -0
  277. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_geometry_corrections.py +0 -0
  278. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_io.py +0 -0
  279. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_lamp_atlas_match.py +0 -0
  280. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_lines.py +0 -0
  281. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_mcp.py +0 -0
  282. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_mcp_production.py +0 -0
  283. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_mcp_security.py +0 -0
  284. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_measure_arc_geometry.py +0 -0
  285. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_misc_algorithms.py +0 -0
  286. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_nebular_diagnostics.py +0 -0
  287. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_no_circular_imports.py +0 -0
  288. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_normalize_to_region.py +0 -0
  289. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_pipeline.py +0 -0
  290. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_quality.py +0 -0
  291. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_read_sdss.py +0 -0
  292. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_redshift_lines.py +0 -0
  293. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_registry.py +0 -0
  294. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_response_from_standard.py +0 -0
  295. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_sky_lateral_bands.py +0 -0
  296. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_smoothing.py +0 -0
  297. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_transforms.py +0 -0
  298. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_types.py +0 -0
  299. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_viz.py +0 -0
  300. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_vr_ratio.py +0 -0
  301. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_wavelength_calibration_in_situ.py +0 -0
  302. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/tests/unit/test_wavelength_calibration_solar.py +0 -0
  303. {spectro_kernel-0.4.0 → spectro_kernel-0.5.0}/website/README.md +0 -0
@@ -6,6 +6,140 @@ Until `1.0.0` the public API may change between minor versions.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.0] — 2026-06-26
10
+
11
+ Second-pass enrichment driven by the STAROS dashboard's delegation
12
+ checklist (`stuff/spectro_kernel_requests_25_06_26.md`). Six
13
+ focused improvements across the radial-velocity, kinematics,
14
+ continuum, and time-series stacks — two correctness fixes that
15
+ warrant a MAJOR bump on the individual algorithm, four ergonomic
16
+ extensions, and a reciprocal documentation cross-reference.
17
+
18
+ Public API additions only — no parameter renamed or removed. The
19
+ two algorithms whose internal numerics change (`cross_correlate_rv`,
20
+ `rotation_curve`) ship explicit opt-outs that preserve the v0.4.1
21
+ behaviour bit-for-bit.
22
+
23
+ ### Changed — `cross_correlate_rv` v1.2.0 → v2.0.0
24
+
25
+ - **Continuum subtraction is now ON by default.** Tonry & Davis
26
+ 1979 §III require continuum-normalised inputs ; the previous
27
+ mean-only centring biased the CCF on raw stellar fluxes with a
28
+ sloped SED (verified empirically : on a synthetic G-type-like
29
+ template shifted by +100 km/s the recovered RV climbed from
30
+ ~+40 km/s to ~+100 km/s with the new default).
31
+ - New parameters ``continuum_subtract`` (default ``True``) and
32
+ ``continuum_window`` (default 101 pixels, running median).
33
+ - Set ``continuum_subtract=False`` to recover the v1.x behaviour
34
+ bit-for-bit (covered by a regression test).
35
+ - Long description now cross-references `rv_precision_bouchy` for
36
+ the photon-noise floor — pair the empirical CCF error with the
37
+ Bouchy floor to read your RV systematics budget.
38
+
39
+ ### Changed — `rotation_curve` v1.0.0 → v2.0.0
40
+
41
+ - **Per-spectrum traceability.** The brick used to drop failed or
42
+ missing-offset spectra silently from the points list ; STAROS
43
+ needs to surface those rejections on the dashboard. The
44
+ ``extras["points"]`` artefact is now a ``list[dict]`` with one
45
+ entry per input spectrum (success OR failure) carrying
46
+ ``{spectrum_id, slit_offset_arcsec, v_los_kms,
47
+ line_fit_center_aa, note}``. Notes : ``"ok"``,
48
+ ``"missing slit_offset"``, ``"Hα out of range"``, ``"fit failed"``,
49
+ ``"empty spectrum"``.
50
+ - ``spectrum_id`` is round-tripped from
51
+ ``spec.meta["spectrum_id"]`` or falls back to the input index.
52
+ - Successful entries are sorted by slit offset ; missing-offset
53
+ entries are appended at the tail.
54
+ - When fewer than 3 fits succeed the algorithm still fails, but
55
+ the per-spectrum breakdown is exposed in ``extras["points"]`` so
56
+ the dashboard can report which spectra are blocking the fit.
57
+
58
+ ### Changed — `normalize_polynomial` v1.0.0 → v1.1.0
59
+
60
+ - New ``sigma_low`` and ``sigma_high`` parameters for **asymmetric
61
+ sigma-clipping** (IRAF ``low_reject`` / ``high_reject`` heritage,
62
+ Tody 1986, Proc. SPIE 627, 733). Amateur stellar spectra are
63
+ dominated by absorption ; a symmetric clip wrongly treats those
64
+ lines as noise and biases the continuum downwards. Pass
65
+ ``sigma_low=1.5, sigma_high=3.0`` to protect the continuum from
66
+ absorption-line population.
67
+ - New ``windows`` parameter — list of ``(λ_lo, λ_hi)`` intervals
68
+ restricting the fit to user-specified continuum windows (IRAF
69
+ ``sample`` parameter).
70
+ - When ``sigma_low``, ``sigma_high`` and ``windows`` are all left
71
+ at their defaults the brick routes through the legacy helper —
72
+ v1.0.0 callers stay bit-identical.
73
+
74
+ ### Changed — `measure_radial_velocity` v1.0.0 → v1.1.0
75
+
76
+ - New ``profile`` parameter ∈ ``{"gaussian", "lorentzian", "voigt"}``
77
+ forwarded to the existing ``fit_line`` helper. Default ``"gaussian"``
78
+ preserves v1.0.0 behaviour. Use ``"lorentzian"`` for damped wings
79
+ (Hα in cool stars) and ``"voigt"`` when neither pure Doppler nor
80
+ pure collisional broadening fits.
81
+
82
+ ### Changed — `lomb_scargle` v1.0.0 → v1.1.0
83
+
84
+ - New ``normalization`` parameter ∈
85
+ ``{"standard", "model", "log", "psd"}`` forwarded verbatim to
86
+ ``astropy.timeseries.LombScargle`` (default ``"standard"``
87
+ preserves v1.0.0 behaviour). Use ``"psd"`` to stitch with
88
+ classical Fourier-domain tooling ; the peak frequency is
89
+ invariant under the choice of normalisation.
90
+ - The chosen normalisation is echoed in
91
+ ``periodogram.meta["normalization"]``.
92
+ - Doc note about composing with the existing ``phase_fold`` brick :
93
+ feed ``ctx.metrics["best_period"]`` to ``phase_fold`` to render
94
+ the folded light curve.
95
+
96
+ ### Changed — `rv_precision_bouchy` v1.0.0 → v1.0.1
97
+
98
+ - Documentation only. Long description and references now point at
99
+ `cross_correlate_rv` reciprocally — pair the photon-noise floor
100
+ (Bouchy 2001) with the empirical Tonry & Davis CCF error to
101
+ characterise the RV systematics budget.
102
+
103
+ ### Numbers
104
+
105
+ 113 algorithms (unchanged). 343 tests pass (12 new), ruff clean.
106
+
107
+ ## [0.4.1] — 2026-06-26
108
+
109
+ Backwards-compatible extension of `cross_correlate_rv` so downstream
110
+ RV consumers can delegate to the kernel without losing the
111
+ peak-confidence / error / CCF-curve / template-provenance surface
112
+ they currently maintain in-house.
113
+
114
+ ### Changed — `cross_correlate_rv` v1.1.0 → v1.2.0
115
+
116
+ - The CCF is now **Pearson-normalised** so the peak height reads
117
+ directly as a similarity score bounded in ``[-1, 1]``.
118
+ - New ``metrics.peak_strength`` (clamped to ``[0, 1]``) — UI-friendly
119
+ confidence indicator (1 ⇒ template-identical observed spectrum).
120
+ - New ``metrics.radial_velocity_error_kms`` — the canonical
121
+ **Tonry & Davis 1979** §III formula
122
+ ``σ_v = 3 · w / (8 · (1 + r))`` with
123
+ ``r = h / (√2 · σ_a)`` (peak height over noise antisymmetry RMS)
124
+ and ``w`` the CCF peak FWHM. Reports ``NaN`` when the CCF shape
125
+ forbids a clean estimate (no resolved peak, no antisymmetric
126
+ noise sample).
127
+ - New ``extras["ccf"] = {"lags_kms": [...], "ccf": [...]}`` — the CCF
128
+ curve restricted to the velocity search window, for downstream
129
+ plotting.
130
+ - New ``extras["template_id"]`` — best-effort label of the template
131
+ used (file basename for ``template_path``, key name for
132
+ ``template_key``).
133
+ - Pre-existing output ``metrics.radial_velocity_kms`` is preserved
134
+ bit-exact ; the ``v1.2.0`` bump reflects the *additions*. A
135
+ regression test (`tests/reference/test_known_answers.py`) covers
136
+ the existing 50 km/s recovery.
137
+
138
+ ### Numbers
139
+
140
+ 113 algorithms (unchanged). 331 tests pass (6 new), ruff clean,
141
+ mkdocs strict OK, twine strict OK.
142
+
9
143
  ## [0.4.0] — 2026-06-26
10
144
 
11
145
  A scientific catalogue expansion driven by a downstream
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spectro-kernel
3
- Version: 0.4.0
3
+ Version: 0.5.0
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,242 @@
1
+ """Algorithm: normalise a spectrum by a sigma-clipped polynomial continuum.
2
+
3
+ Iterative polynomial continuum fit with optional **asymmetric**
4
+ sigma-clipping (``sigma_low`` / ``sigma_high``) and optional explicit
5
+ **continuum windows** (``windows``). The defaults reproduce the
6
+ v1.0.0 symmetric-clip behaviour (``sigma_low = sigma_high = sigma``,
7
+ no windows).
8
+
9
+ References
10
+ ----------
11
+ - IRAF ``continuum`` task — symmetric and asymmetric ``low_reject`` /
12
+ ``high_reject``, ``sample`` (wavelength windows) ; **Tody 1986**,
13
+ Proc. SPIE 627, 733.
14
+ - Sigma-clipping iterative fit — standard robust-regression practice
15
+ in astronomical reduction pipelines.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any
21
+
22
+ import numpy as np
23
+
24
+ from ...base import AlgorithmOutput, BaseAlgorithm
25
+ from ...errors import InvalidParameterError
26
+ from ...registry import register_algorithm
27
+ from ...types import AlgorithmCategory, WorkContext
28
+ from .._common import fit_polynomial_continuum
29
+
30
+
31
+ def _windows_mask(wavelength: np.ndarray, windows: list[tuple[float, float]]) -> np.ndarray:
32
+ """Boolean mask : samples inside ANY of the (lo, hi) wavelength windows."""
33
+ mask = np.zeros_like(wavelength, dtype=bool)
34
+ for lo, hi in windows:
35
+ lo, hi = (float(lo), float(hi)) if lo <= hi else (float(hi), float(lo))
36
+ mask |= (wavelength >= lo) & (wavelength <= hi)
37
+ return mask
38
+
39
+
40
+ def _fit_polynomial_continuum_extended(
41
+ wavelength: np.ndarray,
42
+ flux: np.ndarray,
43
+ *,
44
+ order: int,
45
+ sigma_low: float,
46
+ sigma_high: float,
47
+ iterations: int,
48
+ windows: list[tuple[float, float]] | None,
49
+ ) -> np.ndarray:
50
+ """Continuum fit with asymmetric clip + optional wavelength windows.
51
+
52
+ Per-iteration:
53
+ 1. Fit a polynomial on the currently-kept samples.
54
+ 2. Compute residuals = flux − continuum.
55
+ 3. Keep samples whose residual ∈ ``[-sigma_low · σ, +sigma_high · σ]``.
56
+
57
+ ``windows`` (when given) restricts the initial-mask to samples inside
58
+ at least one user-specified wavelength interval — the polynomial is
59
+ then fitted **only on those samples**, but evaluated everywhere on
60
+ ``wavelength`` for the output (we still divide the whole spectrum
61
+ by the continuum model).
62
+ """
63
+ x = np.asarray(wavelength, dtype=np.float64)
64
+ y = np.asarray(flux, dtype=np.float64)
65
+ base_mask = np.isfinite(y)
66
+ if windows:
67
+ base_mask &= _windows_mask(x, windows)
68
+ mask = base_mask.copy()
69
+ if int(mask.sum()) <= order + 1:
70
+ # Not enough samples to fit; fall back to a flat continuum.
71
+ return np.full_like(y, float(np.nanmedian(y)))
72
+
73
+ for _ in range(max(1, iterations)):
74
+ model = np.polynomial.Polynomial.fit(x[mask], y[mask], deg=order)
75
+ residuals = y - model(x)
76
+ sigma = float(np.std(residuals[mask]))
77
+ if sigma == 0.0:
78
+ break
79
+ new_mask = (
80
+ base_mask
81
+ & (residuals > -sigma_low * sigma)
82
+ & (residuals < +sigma_high * sigma)
83
+ )
84
+ if int(new_mask.sum()) <= order + 1:
85
+ break
86
+ if np.array_equal(new_mask, mask):
87
+ mask = new_mask
88
+ break
89
+ mask = new_mask
90
+ final = np.polynomial.Polynomial.fit(x[mask], y[mask], deg=order)
91
+ return final(x)
92
+
93
+
94
+ @register_algorithm(
95
+ "normalize_polynomial",
96
+ category=AlgorithmCategory.CONTINUUM,
97
+ # 1.1.0: asymmetric sigma_low / sigma_high + explicit windows. The
98
+ # default behaviour (symmetric clip, full-range fit) is unchanged.
99
+ version="1.1.0",
100
+ )
101
+ class NormalizePolynomial(BaseAlgorithm):
102
+ """Normalise the continuum to unity with a sigma-clipped polynomial fit.
103
+
104
+ A polynomial of the requested order is fitted to the continuum
105
+ (lines rejected by iterative sigma clipping) ; the flux is divided
106
+ by it. The result is a continuum-normalised spectrum oscillating
107
+ around 1.0.
108
+
109
+ Two v1.1.0 additions match the IRAF ``continuum`` toolbox :
110
+
111
+ - **Asymmetric sigma-clip** via ``sigma_low`` and ``sigma_high``.
112
+ Amateur stellar spectra are dominated by *absorption* lines —
113
+ with the symmetric default they get clipped as if they were
114
+ noise, biasing the continuum downwards. ``sigma_low=1.5,
115
+ sigma_high=3.0`` is the conservative choice for absorption-rich
116
+ sources (the asymmetry says "I trust the +side more than the
117
+ −side for telling continuum from line"). When either is left at
118
+ ``None`` the brick falls back to the legacy symmetric clip with
119
+ ``sigma_clip`` on both sides.
120
+ - **Explicit continuum windows** via ``windows``. A list of
121
+ ``(λ_lo, λ_hi)`` intervals restricts the fit to the samples
122
+ inside at least one window — useful when the user knows where
123
+ the line-free regions are (Mg b avoidance, telluric avoidance).
124
+ Disabled by default ; the polynomial then sees the whole
125
+ spectrum and reduction is via the sigma-clip.
126
+ """
127
+
128
+ backend = "numpy"
129
+ references = [
130
+ "Tody 1986, Proc. SPIE 627, 733 — IRAF continuum task "
131
+ "(low_reject / high_reject / sample heritage).",
132
+ ]
133
+ long_description = (
134
+ "Use a low order (2-4) for a slowly varying continuum; higher "
135
+ "orders risk absorbing real spectral features. For "
136
+ "absorption-line-rich sources set sigma_low=1.5, sigma_high=3."
137
+ )
138
+ default_params = {
139
+ "order": 3,
140
+ "sigma_clip": 3.0,
141
+ "sigma_low": None,
142
+ "sigma_high": None,
143
+ "iterations": 3,
144
+ "windows": None,
145
+ }
146
+ param_descriptions = {
147
+ "order": "Polynomial degree of the continuum fit.",
148
+ "sigma_clip": (
149
+ "Symmetric clip threshold (default 3.0). Used as a fallback "
150
+ "for sigma_low / sigma_high when those are left at None."
151
+ ),
152
+ "sigma_low": (
153
+ "Asymmetric clip threshold on the LOW side (rejects "
154
+ "absorption lines). None ⇒ falls back to sigma_clip."
155
+ ),
156
+ "sigma_high": (
157
+ "Asymmetric clip threshold on the HIGH side (rejects "
158
+ "emission lines / cosmics). None ⇒ falls back to sigma_clip."
159
+ ),
160
+ "iterations": "Number of sigma-clipping iterations.",
161
+ "windows": (
162
+ "Optional list of (λ_lo, λ_hi) wavelength intervals ; when "
163
+ "given, the polynomial is fitted only on samples inside at "
164
+ "least one window (IRAF 'sample' parameter)."
165
+ ),
166
+ }
167
+ input_requirements = ["spectrum"]
168
+ output_produces = ["spectrum", "metrics.continuum_median"]
169
+
170
+ def run(self, ctx: WorkContext, params: dict[str, Any]) -> AlgorithmOutput:
171
+ spec = ctx.spectrum
172
+ order = int(params["order"])
173
+ sigma_clip = float(params["sigma_clip"])
174
+ iterations = int(params["iterations"])
175
+ if iterations < 1:
176
+ raise InvalidParameterError("iterations must be ≥ 1.")
177
+ sigma_low_raw = params.get("sigma_low")
178
+ sigma_high_raw = params.get("sigma_high")
179
+ sigma_low = float(sigma_low_raw) if sigma_low_raw is not None else sigma_clip
180
+ sigma_high = float(sigma_high_raw) if sigma_high_raw is not None else sigma_clip
181
+ if sigma_low <= 0.0 or sigma_high <= 0.0:
182
+ raise InvalidParameterError("sigma thresholds must be strictly positive.")
183
+
184
+ windows_param = params.get("windows")
185
+ windows: list[tuple[float, float]] | None = None
186
+ if windows_param:
187
+ windows = [(float(lo), float(hi)) for lo, hi in windows_param]
188
+
189
+ # When the defaults are in play AND no windows AND no asymmetry,
190
+ # we route through the legacy helper so v1.0.0 callers stay
191
+ # bit-identical (the iterative-mask convergence criterion
192
+ # differs subtly between the two helpers).
193
+ symmetric_default = (
194
+ windows is None
195
+ and sigma_low_raw is None
196
+ and sigma_high_raw is None
197
+ )
198
+ if symmetric_default:
199
+ continuum = fit_polynomial_continuum(
200
+ spec.wavelength,
201
+ spec.flux,
202
+ order=order,
203
+ sigma_clip=sigma_clip,
204
+ iterations=iterations,
205
+ )
206
+ else:
207
+ continuum = _fit_polynomial_continuum_extended(
208
+ np.asarray(spec.wavelength, dtype=np.float64),
209
+ np.asarray(spec.flux, dtype=np.float64),
210
+ order=order,
211
+ sigma_low=sigma_low,
212
+ sigma_high=sigma_high,
213
+ iterations=iterations,
214
+ windows=windows,
215
+ )
216
+ safe = np.where(continuum == 0.0, np.nan, continuum)
217
+ normalised = spec.flux / safe
218
+
219
+ out = spec.with_flux(normalised, flux_unit="normalized")
220
+ if spec.uncertainty is not None:
221
+ out.uncertainty = spec.uncertainty / np.abs(safe)
222
+ out.meta["continuum_method"] = "polynomial"
223
+ out.meta["continuum_sigma_low"] = sigma_low
224
+ out.meta["continuum_sigma_high"] = sigma_high
225
+ if windows:
226
+ out.meta["continuum_windows"] = windows
227
+ ctx.spectrum = out
228
+
229
+ metrics = {
230
+ "continuum_median": float(np.nanmedian(continuum)),
231
+ "normalized_rms": float(np.nanstd(normalised)),
232
+ "sigma_low": sigma_low,
233
+ "sigma_high": sigma_high,
234
+ }
235
+ return AlgorithmOutput.ok(
236
+ metrics=metrics,
237
+ message=(
238
+ "Continuum normalised to unity "
239
+ f"(σ_low={sigma_low:g}, σ_high={sigma_high:g}, "
240
+ f"{len(windows) if windows else 0} window(s))."
241
+ ),
242
+ )
@@ -101,7 +101,11 @@ def _slit_offset(spec: Spectrum1D, fallback_offsets: dict[int, float], idx: int)
101
101
  @register_algorithm(
102
102
  "rotation_curve",
103
103
  category=AlgorithmCategory.KINEMATICS,
104
- version="1.0.0",
104
+ # 2.0.0: ctx.extras["points"] schema changed from list[tuple] to
105
+ # list[dict] with per-spectrum spectrum_id + note (status); every
106
+ # input spectrum produces one entry even on failure, so callers
107
+ # can keep their per-spectrum traceability when delegating.
108
+ version="2.0.0",
105
109
  )
106
110
  class RotationCurve(BaseAlgorithm):
107
111
  """Projected long-slit rotation curve v_los(r) from Hα per slit offset.
@@ -111,18 +115,26 @@ class RotationCurve(BaseAlgorithm):
111
115
  arcsec from the galactic centre) from either
112
116
  ``spec.meta["slit_offset_arcsec"]`` or
113
117
  ``params["slit_offsets"]`` (an explicit ``{index: arcsec}`` mapping),
114
- fits Hα in each, and emits an offset-sorted ``v_los(r)`` rotation
115
- curve in ``ctx.extras["points"]``. The systemic velocity is the
116
- median of the surviving measurements.
117
-
118
- Validity : needs ≥ 3 spectra ; spectra without a slit offset, or
119
- where Hα cannot be fit, are silently dropped (the algorithm
120
- reports the count in ``metrics``). Fails if zero usable
121
- measurements remain.
122
-
123
- This is the **simple projected** rotation curve no inclination
124
- correction, no asymmetric drift, no 2-D model. See ``Sofue &
125
- Rubin 2001`` for the modelling extensions.
118
+ fits Hα in each, and emits a per-spectrum rotation curve in
119
+ ``ctx.extras["points"]``. The systemic velocity is the median of
120
+ the successful measurements.
121
+
122
+ Output shape (v2.0.0)
123
+ ---------------------
124
+ ``ctx.extras["points"] = [{spectrum_id, slit_offset_arcsec,
125
+ v_los_kms, line_fit_center_aa, note}]`` — **one entry per input
126
+ spectrum**, regardless of success. ``v_los_kms`` and
127
+ ``line_fit_center_aa`` are ``None`` on failure ; ``note`` is one
128
+ of ``"ok"`` / ``"missing slit_offset"`` / ``"Hα out of range"`` /
129
+ ``"fit failed"`` / ``"empty spectrum"``. Entries sort by slit
130
+ offset ; missing-offset entries land at the end. ``spectrum_id``
131
+ is read from ``spec.meta["spectrum_id"]`` when present, otherwise
132
+ the integer position in ``ctx.spectra`` is used.
133
+
134
+ Validity : needs ≥ 3 spectra ; fails if zero successful Hα fits
135
+ remain. This is the **simple projected** rotation curve — no
136
+ inclination correction, no asymmetric drift, no 2-D model. See
137
+ ``Sofue & Rubin 2001`` for the modelling extensions.
126
138
  """
127
139
 
128
140
  backend = "scipy"
@@ -172,47 +184,78 @@ class RotationCurve(BaseAlgorithm):
172
184
 
173
185
  lam_obs_rest = lam_rest * (1.0 + z)
174
186
 
175
- points: list[tuple[float, float, float]] = [] # (r_arcsec, v_los_kms, mu_aa)
187
+ # Per-spectrum dict one entry per input regardless of success.
188
+ # 'note' encodes the per-row status so downstream code can
189
+ # surface partial-success reductions instead of silently
190
+ # losing failed spectra.
191
+ points: list[dict[str, Any]] = []
176
192
  skipped_no_offset = 0
177
193
  skipped_no_fit = 0
178
194
  for idx, spec in enumerate(spectra):
195
+ spectrum_id = (
196
+ spec.meta.get("spectrum_id") if spec.meta else None
197
+ ) or idx
198
+ entry: dict[str, Any] = {
199
+ "spectrum_id": spectrum_id,
200
+ "slit_offset_arcsec": None,
201
+ "v_los_kms": None,
202
+ "line_fit_center_aa": None,
203
+ "note": "ok",
204
+ }
205
+
179
206
  offset = _slit_offset(spec, slit_offsets, idx)
180
207
  if offset is None:
181
208
  skipped_no_offset += 1
209
+ entry["note"] = "missing slit_offset"
210
+ points.append(entry)
182
211
  continue
212
+ entry["slit_offset_arcsec"] = float(offset)
213
+
183
214
  wave = np.asarray(spec.wavelength, dtype=np.float64)
184
215
  flux = np.asarray(spec.flux, dtype=np.float64)
185
216
  if wave.size == 0:
186
217
  skipped_no_fit += 1
218
+ entry["note"] = "empty spectrum"
219
+ points.append(entry)
187
220
  continue
188
221
  if lam_obs_rest < float(wave[0]) or lam_obs_rest > float(wave[-1]):
189
222
  skipped_no_fit += 1
223
+ entry["note"] = "Hα out of range"
224
+ points.append(entry)
190
225
  continue
191
226
  mu = _fit_halpha_centre(wave, flux, lam_obs_rest, fit_window)
192
227
  if mu is None:
193
228
  skipped_no_fit += 1
229
+ entry["note"] = "fit failed"
230
+ points.append(entry)
194
231
  continue
195
- v_los = _SPEED_OF_LIGHT_KMS * (mu / lam_obs_rest - 1.0)
196
- points.append((float(offset), float(v_los), float(mu)))
197
232
 
198
- if not points:
233
+ v_los = _SPEED_OF_LIGHT_KMS * (mu / lam_obs_rest - 1.0)
234
+ entry["v_los_kms"] = float(v_los)
235
+ entry["line_fit_center_aa"] = float(mu)
236
+ entry["note"] = "ok"
237
+ points.append(entry)
238
+
239
+ ok_rows = [p for p in points if p["note"] == "ok"]
240
+ if not ok_rows:
241
+ ctx.extras["points"] = self._sort_points(points)
199
242
  return AlgorithmOutput.fail(
200
243
  f"No usable rotation measurements — skipped "
201
244
  f"{skipped_no_offset} for missing slit_offset_arcsec and "
202
- f"{skipped_no_fit} for failed Hα fit."
245
+ f"{skipped_no_fit} for failed Hα fit. See "
246
+ "ctx.extras['points'] for the per-spectrum breakdown."
203
247
  )
204
248
 
205
- points.sort(key=lambda row: row[0])
206
- v_arr = np.asarray([row[1] for row in points], dtype=np.float64)
249
+ v_arr = np.asarray([p["v_los_kms"] for p in ok_rows], dtype=np.float64)
207
250
  v_sys = float(np.median(v_arr))
208
251
 
209
- ctx.extras["points"] = points
252
+ ctx.extras["points"] = self._sort_points(points)
210
253
  ctx.extras["method"] = (
211
254
  "Per-spectrum Hα fit; v_los = c·(μ−λ_obs_rest)/λ_obs_rest; "
212
255
  "v_systemic = median(v_los). No inclination correction."
213
256
  )
214
257
  ctx.metrics["v_systemic_km_s"] = v_sys
215
- ctx.metrics["n_points"] = float(len(points))
258
+ ctx.metrics["n_points"] = float(len(ok_rows))
216
259
  ctx.metrics["n_skipped_no_offset"] = float(skipped_no_offset)
217
260
  ctx.metrics["n_skipped_no_fit"] = float(skipped_no_fit)
218
261
 
@@ -221,15 +264,15 @@ class RotationCurve(BaseAlgorithm):
221
264
  return AlgorithmOutput.ok(
222
265
  metrics={
223
266
  "v_systemic_km_s": v_sys,
224
- "n_points": float(len(points)),
267
+ "n_points": float(len(ok_rows)),
225
268
  "n_skipped_no_offset": float(skipped_no_offset),
226
269
  "n_skipped_no_fit": float(skipped_no_fit),
227
270
  "v_min_km_s": v_min,
228
271
  "v_max_km_s": v_max,
229
272
  },
230
- artifacts={"points": points},
273
+ artifacts={"points": self._sort_points(points)},
231
274
  message=(
232
- f"Rotation curve: {len(points)} usable point(s), "
275
+ f"Rotation curve: {len(ok_rows)} usable point(s), "
233
276
  f"v_systemic = {v_sys:+.1f} km/s, "
234
277
  f"v ∈ [{v_min:+.1f}, {v_max:+.1f}] km/s "
235
278
  f"(skipped {skipped_no_offset} no-offset + "
@@ -237,5 +280,13 @@ class RotationCurve(BaseAlgorithm):
237
280
  ),
238
281
  )
239
282
 
283
+ @staticmethod
284
+ def _sort_points(points: list[dict[str, Any]]) -> list[dict[str, Any]]:
285
+ """Sort per-spectrum entries by slit_offset_arcsec; offset=None at the tail."""
286
+ with_offset = [p for p in points if p["slit_offset_arcsec"] is not None]
287
+ without_offset = [p for p in points if p["slit_offset_arcsec"] is None]
288
+ with_offset.sort(key=lambda p: p["slit_offset_arcsec"])
289
+ return with_offset + without_offset
290
+
240
291
 
241
292
  __all__ = ["RotationCurve"]