spectro-kernel 0.2.0__tar.gz → 0.3.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 (276) hide show
  1. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/CHANGELOG.md +188 -0
  2. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/PKG-INFO +1 -1
  3. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/tutorials/long-slit-reduction.md +5 -2
  4. spectro_kernel-0.3.0/src/spectro_kernel/algorithms/corrections/response_from_standard.py +291 -0
  5. spectro_kernel-0.3.0/src/spectro_kernel/algorithms/extraction/boxcar.py +343 -0
  6. spectro_kernel-0.3.0/src/spectro_kernel/algorithms/extraction/detect_trace.py +286 -0
  7. spectro_kernel-0.3.0/src/spectro_kernel/algorithms/extraction/optimal.py +403 -0
  8. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/_easyspec_apply.py +39 -3
  9. spectro_kernel-0.3.0/src/spectro_kernel/algorithms/reduction/dark_combine.py +174 -0
  10. spectro_kernel-0.3.0/src/spectro_kernel/algorithms/reduction/flat_combine.py +165 -0
  11. spectro_kernel-0.3.0/src/spectro_kernel/algorithms/wavelength_calibration/arc_geometry.py +350 -0
  12. spectro_kernel-0.3.0/src/spectro_kernel/algorithms/wavelength_calibration/lamp_atlas.py +170 -0
  13. spectro_kernel-0.3.0/src/spectro_kernel/algorithms/wavelength_calibration/match_lamp.py +488 -0
  14. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/version.py +1 -1
  15. spectro_kernel-0.3.0/tests/unit/test_dark_flat_combine.py +188 -0
  16. spectro_kernel-0.3.0/tests/unit/test_detect_trace.py +104 -0
  17. spectro_kernel-0.3.0/tests/unit/test_easyspec_apply_staging.py +156 -0
  18. spectro_kernel-0.3.0/tests/unit/test_extract_boxcar.py +171 -0
  19. spectro_kernel-0.3.0/tests/unit/test_extract_optimal.py +178 -0
  20. spectro_kernel-0.3.0/tests/unit/test_lamp_atlas_match.py +219 -0
  21. spectro_kernel-0.3.0/tests/unit/test_measure_arc_geometry.py +152 -0
  22. spectro_kernel-0.3.0/tests/unit/test_response_from_standard.py +129 -0
  23. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/.gitignore +0 -0
  24. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/LICENSE +0 -0
  25. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/README.md +0 -0
  26. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/concepts/algorithms.md +0 -0
  27. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/concepts/architecture.md +0 -0
  28. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/concepts/data-types.md +0 -0
  29. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/concepts/pipelines.md +0 -0
  30. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/contributing.md +0 -0
  31. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/cookbook/bess-dashboard.md +0 -0
  32. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/cookbook/multi-star-viewer.md +0 -0
  33. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/cookbook/web-playground.md +0 -0
  34. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/gen_catalogue.py +0 -0
  35. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/getting-started.md +0 -0
  36. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_cyg_ew_timeseries.png +0 -0
  37. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_cyg_halpha_phase_stack.png +0 -0
  38. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_cyg_halpha_timeseries.png +0 -0
  39. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_cyg_overlay.png +0 -0
  40. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_cyg_pc1_vs_phase.png +0 -0
  41. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_cyg_pca_2d.png +0 -0
  42. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_cyg_pca_3d.png +0 -0
  43. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_cyg_periodogram.png +0 -0
  44. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_cyg_phase_folded.png +0 -0
  45. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_cyg_rv_timeseries.png +0 -0
  46. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_cyg_similarity.png +0 -0
  47. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_cyg_similarity_date.png +0 -0
  48. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_dra_halpha_phase_stack.png +0 -0
  49. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_dra_pc1_vs_phase.png +0 -0
  50. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_dra_pca_2d.png +0 -0
  51. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_dra_pca_3d.png +0 -0
  52. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_dra_periodogram.png +0 -0
  53. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_dra_phase_folded.png +0 -0
  54. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_dra_rv_timeseries.png +0 -0
  55. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_dra_similarity_date.png +0 -0
  56. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/images/notebooks/alpha_dra_similarity_phase.png +0 -0
  57. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/index.md +0 -0
  58. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/notebooks/alpha-cyg-time-series.md +0 -0
  59. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/notebooks/alpha-dra-binary-period.md +0 -0
  60. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/notebooks/aurora-line-monitor.md +0 -0
  61. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/notebooks/be-star-variability.md +0 -0
  62. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/notebooks/claude-mcp-end-to-end.md +0 -0
  63. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/notebooks/exoplanet-transit-rv.md +0 -0
  64. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/notebooks/full-reduction-walkthrough.md +0 -0
  65. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/notebooks/native-vs-easyspec-showdown.md +0 -0
  66. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/notebooks/your-first-sb2.md +0 -0
  67. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/reference.md +0 -0
  68. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/tutorials/add-an-algorithm.md +0 -0
  69. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/tutorials/analyse-a-spectrum.md +0 -0
  70. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/tutorials/discover-the-catalogue.md +0 -0
  71. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/tutorials/first-spectrum.md +0 -0
  72. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/usage/cli.md +0 -0
  73. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/usage/library.md +0 -0
  74. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/usage/mcp.md +0 -0
  75. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/docs/why.md +0 -0
  76. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/playground/README.md +0 -0
  77. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/pyproject.toml +0 -0
  78. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/__init__.py +0 -0
  79. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/adapters/__init__.py +0 -0
  80. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/adapters/easyspec.py +0 -0
  81. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/__init__.py +0 -0
  82. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/_common.py +0 -0
  83. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/advanced/__init__.py +0 -0
  84. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/advanced/aperture_photometry.py +0 -0
  85. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/advanced/disentangle_sb2.py +0 -0
  86. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/catalogs/__init__.py +0 -0
  87. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/catalogs/gaia.py +0 -0
  88. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/catalogs/simbad.py +0 -0
  89. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/catalogs/vizier.py +0 -0
  90. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/continuum/__init__.py +0 -0
  91. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/continuum/compare_normalisations.py +0 -0
  92. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/continuum/normalize_edges.py +0 -0
  93. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/continuum/normalize_max.py +0 -0
  94. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/continuum/normalize_percentile.py +0 -0
  95. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/continuum/normalize_polynomial.py +0 -0
  96. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/continuum/normalize_region.py +0 -0
  97. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/continuum/subtract_continuum.py +0 -0
  98. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/corrections/__init__.py +0 -0
  99. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/corrections/air_vacuum.py +0 -0
  100. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/corrections/barycentric.py +0 -0
  101. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/corrections/doppler_shift.py +0 -0
  102. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/corrections/extinction_correct_easyspec.py +0 -0
  103. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/corrections/fit_telluric_scaling.py +0 -0
  104. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/corrections/flux_calibrate_easyspec.py +0 -0
  105. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/corrections/remove_telluric.py +0 -0
  106. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/corrections/synth_telluric.py +0 -0
  107. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/embedding/__init__.py +0 -0
  108. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/embedding/embed_band_power.py +0 -0
  109. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/embedding/embed_continuum_subtracted.py +0 -0
  110. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/embedding/embed_lick_indices.py +0 -0
  111. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/embedding/embed_log_lambda.py +0 -0
  112. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/embedding/embed_pretrained.py +0 -0
  113. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/embedding/embed_remote.py +0 -0
  114. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/embedding/embed_spectrum.py +0 -0
  115. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/embedding/embed_wavelets.py +0 -0
  116. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/exports/__init__.py +0 -0
  117. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/exports/export_csv.py +0 -0
  118. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/exports/export_fits.py +0 -0
  119. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/exports/export_fits_bess.py +0 -0
  120. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/exports/export_hdf5.py +0 -0
  121. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/exports/export_votable.py +0 -0
  122. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/extraction/__init__.py +0 -0
  123. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/extraction/easyspec_extract.py +0 -0
  124. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/extraction/sky_lateral_bands.py +0 -0
  125. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/io/__init__.py +0 -0
  126. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/io/read_ascii.py +0 -0
  127. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/io/read_echelle.py +0 -0
  128. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/io/read_fits.py +0 -0
  129. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/io/read_sdss.py +0 -0
  130. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/io/read_votable.py +0 -0
  131. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/lines/__init__.py +0 -0
  132. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/lines/_profiles.py +0 -0
  133. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/lines/catalogs.py +0 -0
  134. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/lines/compare_line_fits.py +0 -0
  135. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/lines/detect.py +0 -0
  136. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/lines/equivalent_width.py +0 -0
  137. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/lines/fit_gaussian.py +0 -0
  138. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/lines/fit_lorentzian.py +0 -0
  139. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/lines/fit_voigt.py +0 -0
  140. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/quality/__init__.py +0 -0
  141. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/quality/compare_snr_methods.py +0 -0
  142. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/quality/snr_der.py +0 -0
  143. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/quality/snr_edge.py +0 -0
  144. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/quality/snr_linear_fit.py +0 -0
  145. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/__init__.py +0 -0
  146. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/_easyspec_helpers.py +0 -0
  147. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/bias_combine.py +0 -0
  148. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/clip_cosmic_rays.py +0 -0
  149. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/dark_subtract.py +0 -0
  150. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/denoise_2d.py +0 -0
  151. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/easyspec_bias.py +0 -0
  152. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/easyspec_cosmic_ray.py +0 -0
  153. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/easyspec_dark.py +0 -0
  154. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/easyspec_flat.py +0 -0
  155. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/easyspec_flat_normalize.py +0 -0
  156. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/easyspec_subtract_bias.py +0 -0
  157. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/easyspec_subtract_dark.py +0 -0
  158. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/extract_spectrum_sum.py +0 -0
  159. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/flat_normalize.py +0 -0
  160. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/geometry.py +0 -0
  161. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/subtract_sky_2d.py +0 -0
  162. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/reduction/wavelength_calibrate.py +0 -0
  163. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/rv/__init__.py +0 -0
  164. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/rv/cross_correlate.py +0 -0
  165. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/rv/fit_keplerian_orbit.py +0 -0
  166. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/rv/measure.py +0 -0
  167. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/rv/precision_bouchy.py +0 -0
  168. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/smoothing/__init__.py +0 -0
  169. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/smoothing/compare_smoothings.py +0 -0
  170. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/smoothing/smooth_gaussian.py +0 -0
  171. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/smoothing/smooth_savgol.py +0 -0
  172. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/stacking/__init__.py +0 -0
  173. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/stacking/merge_echelle_orders.py +0 -0
  174. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/stacking/stack.py +0 -0
  175. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/timeseries/__init__.py +0 -0
  176. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/timeseries/lomb_scargle.py +0 -0
  177. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/timeseries/phase_fold.py +0 -0
  178. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/transforms/__init__.py +0 -0
  179. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/transforms/clip_sigma.py +0 -0
  180. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/transforms/combine_arithmetic.py +0 -0
  181. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/transforms/extract_region.py +0 -0
  182. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/transforms/mask_range.py +0 -0
  183. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/transforms/resample.py +0 -0
  184. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/transforms/resample_flux_conserving.py +0 -0
  185. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/viz/__init__.py +0 -0
  186. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/viz/plot_3d_surface.py +0 -0
  187. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/viz/plot_animation.py +0 -0
  188. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/viz/plot_dynamic_spectrum.py +0 -0
  189. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/viz/plot_plotly.py +0 -0
  190. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/wavelength_calibration/__init__.py +0 -0
  191. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/wavelength_calibration/easyspec_wavelength.py +0 -0
  192. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/wavelength_calibration/in_situ.py +0 -0
  193. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/algorithms/wavelength_calibration/solar.py +0 -0
  194. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/base.py +0 -0
  195. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/cli.py +0 -0
  196. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/embeddings.py +0 -0
  197. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/errors.py +0 -0
  198. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/io/__init__.py +0 -0
  199. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/io/ascii.py +0 -0
  200. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/io/fits.py +0 -0
  201. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/io/votable.py +0 -0
  202. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/pipeline.py +0 -0
  203. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/presets/__init__.py +0 -0
  204. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/presets/catalog/analysis/balmer_quick.yaml +0 -0
  205. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/presets/catalog/analysis/embed_quick.yaml +0 -0
  206. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/presets/catalog/analysis/quality_report.yaml +0 -0
  207. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/presets/catalog/analysis/rv_quick.yaml +0 -0
  208. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/presets/catalog/analysis/snr_check.yaml +0 -0
  209. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/presets/catalog/analysis/time_series_overview.yaml +0 -0
  210. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/presets/catalog/reduction/full_reduction_easyspec.yaml +0 -0
  211. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/presets/loader.py +0 -0
  212. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/py.typed +0 -0
  213. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/registry.py +0 -0
  214. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/types/__init__.py +0 -0
  215. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/types/catalog.py +0 -0
  216. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/types/context.py +0 -0
  217. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/types/enums.py +0 -0
  218. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/types/history.py +0 -0
  219. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/types/image.py +0 -0
  220. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/types/line.py +0 -0
  221. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/types/spectrum.py +0 -0
  222. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_kernel/types/timeseries.py +0 -0
  223. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_mcp/__init__.py +0 -0
  224. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_mcp/__main__.py +0 -0
  225. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_mcp/auth.py +0 -0
  226. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_mcp/auto_tools.py +0 -0
  227. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_mcp/observability.py +0 -0
  228. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_mcp/py.typed +0 -0
  229. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_mcp/server.py +0 -0
  230. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_mcp/session.py +0 -0
  231. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/src/spectro_mcp/url_safety.py +0 -0
  232. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/conftest.py +0 -0
  233. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/reference/conftest.py +0 -0
  234. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/reference/data/.gitkeep +0 -0
  235. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/reference/data/README.md +0 -0
  236. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/reference/data/sun/sun_reference_stis_002.fits +0 -0
  237. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/reference/data/vega/alpha_lyr_stis_011.fits +0 -0
  238. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/reference/test_known_answers.py +0 -0
  239. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/reference/test_sun.py +0 -0
  240. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/reference/test_vega.py +0 -0
  241. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_base.py +0 -0
  242. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_cli.py +0 -0
  243. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_combine.py +0 -0
  244. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_combine_arithmetic.py +0 -0
  245. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_continuum.py +0 -0
  246. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_corrections.py +0 -0
  247. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_denoise_2d.py +0 -0
  248. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_easyspec_wrappers.py +0 -0
  249. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_embedding.py +0 -0
  250. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_embedding_pretrained.py +0 -0
  251. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_embedding_remote_lick.py +0 -0
  252. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_embedding_tier1.py +0 -0
  253. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_export_fits_bess.py +0 -0
  254. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_geometry_corrections.py +0 -0
  255. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_io.py +0 -0
  256. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_line_profiles.py +0 -0
  257. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_lines.py +0 -0
  258. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_mcp.py +0 -0
  259. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_mcp_production.py +0 -0
  260. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_mcp_security.py +0 -0
  261. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_misc_algorithms.py +0 -0
  262. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_no_circular_imports.py +0 -0
  263. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_normalize_to_region.py +0 -0
  264. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_pipeline.py +0 -0
  265. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_quality.py +0 -0
  266. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_read_sdss.py +0 -0
  267. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_registry.py +0 -0
  268. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_sky_lateral_bands.py +0 -0
  269. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_smoothing.py +0 -0
  270. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_timeseries.py +0 -0
  271. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_transforms.py +0 -0
  272. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_types.py +0 -0
  273. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_viz.py +0 -0
  274. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_wavelength_calibration_in_situ.py +0 -0
  275. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/tests/unit/test_wavelength_calibration_solar.py +0 -0
  276. {spectro_kernel-0.2.0 → spectro_kernel-0.3.0}/website/README.md +0 -0
@@ -6,6 +6,194 @@ Until `1.0.0` the public API may change between minor versions.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.0] — 2026-06-09
10
+
11
+ A focused expansion driven by the LineMill stellar-spectroscopy work
12
+ (Alpy 600 / Star'Ex). **Seven new algorithms + one package data
13
+ module**, all genuinely literature-based, none duplicating existing
14
+ bricks. The minor-version bump signals a meaningful catalogue
15
+ expansion (105 algorithms, up from 98), not an API break — every
16
+ existing brick remains untouched.
17
+
18
+ ### Added — automatic wavelength calibration
19
+
20
+ - **`lamp_atlas`** (package data, not an algorithm) — curated
21
+ persistent-line lists for `Ne`, `Ar`, `NeAr` and `ThAr` arc lamps,
22
+ sourced from the **NIST Atomic Spectra Database** (Kramida, Ralchenko,
23
+ Reader, NIST ASD Team). Embedded — no network at runtime. Sibling of
24
+ the existing ``lines.catalogs`` module. API: ``get_atlas(lamp,
25
+ wave_min=None, wave_max=None) -> ndarray`` of ``(wavelength_angstrom,
26
+ relative_intensity)``.
27
+ - **`match_lamp_lines`** — automatic identification of arc-line peaks
28
+ against the bundled atlas. IRAF ``autoidentify`` heritage (Tody 1986
29
+ SPIE 627 733 ; Tody 1993 ASP Conf 52 173), with the robust ThAr-style
30
+ σ-clipped polynomial refinement (Murphy et al. 2007 MNRAS 378 221).
31
+ Writes ``ctx.extras["lamp_identifications"]`` in the exact shape
32
+ ``wavelength_calibrate_polynomial`` consumes — the old
33
+ hand-identified ``(pixel, λ)`` pairs become optional. Linear seed by
34
+ global anchor-pair brute-force (the local-greedy method's known
35
+ failure mode on Alpy 600 plein visible), then σ-clipped polyfit.
36
+
37
+ ### Added — native master-frame combiners
38
+
39
+ - **`dark_combine`** and **`flat_combine`** — pure-numpy companions to
40
+ the existing native ``bias_combine``. Required by cloud workers that
41
+ source frames from S3 / R2 / memory queues, where the existing
42
+ ``*_easyspec`` variants (file round-trip) don't fit. Median (robust
43
+ default) or σ-clipped mean. ``dark_combine`` enforces uniform EXPTIME
44
+ by default (so the downstream ``dark_subtract(scale_by_exptime=True)``
45
+ has a well-defined factor). ``flat_combine`` produces the **raw**
46
+ master flat — the existing ``flat_normalize`` keeps the
47
+ normalisation responsibility. Howell 2006 ch. 4.
48
+
49
+ ### Added — flux calibration
50
+
51
+ - **`response_from_standard`** — derive the instrumental response
52
+ curve from a standard-star observation. Observed/catalogue ratio,
53
+ Balmer + telluric masking, low-frequency spline (or polynomial) fit.
54
+ Output goes to ``ctx.extras["reference_spectrum"]`` so the existing
55
+ ``combine_spectra_arithmetic(operation="div")`` consumes it as the
56
+ "second spectrum" — same idiom as ``remove_telluric_division``.
57
+ References: Oke 1990 AJ 99 1621 ; Bessell 1999 PASP 111 1426 ; Hamuy
58
+ 1992/1994 ; CALSPEC Bohlin et al. 2014 PASP 126 711 ; Hayes & Latham
59
+ 1975 ApJ 197 593.
60
+
61
+ ### Added — optimal extraction (Horne 1986)
62
+
63
+ - **`extract_spectrum_optimal`** — full Horne 1986 estimator: empirical
64
+ spatial profile (smoothed à la Marsh 1989), per-pixel variance model
65
+ ``V = signal/gain + RON²``, inverse-variance weighting, iterative
66
+ σ-clip cosmic-ray rejection, propagated 1-σ ``Spectrum1D.uncertainty``.
67
+
68
+ **This is NOT a replacement of ``extract_spectrum_boxcar``**. The
69
+ boxcar (with ``extraction_weights="gaussian"``) is a *profile-only*
70
+ estimator without variance model, cosmic rejection or uncertainty
71
+ propagation — it remains the right tool for high-SNR sources, wide
72
+ apertures, extended emission (aurora, nebulae). Pick
73
+ ``extract_spectrum_optimal`` when SNR matters most (faint, RN-limited
74
+ targets), when residual cosmics survive ``clip_cosmic_rays``, or
75
+ when downstream code needs ``Spectrum1D.uncertainty``. Cross-refs
76
+ added to both bricks' ``long_description`` to make the choice
77
+ explicit. References: Horne 1986 PASP 98 609 ; Marsh 1989 PASP 101
78
+ 1032 ; Tody 1986 IRAF apall.
79
+
80
+ ### Added — automation bricks
81
+
82
+ - **`detect_trace`** — pure-numpy trace detection that **exposes** the
83
+ trace as a product on ``ctx.extras["trace"] = {"center_row",
84
+ "poly_coef", "fwhm_px", "snr", "rms_px"}``. Today every
85
+ ``extract_spectrum_*`` recomputes the trace internally and throws the
86
+ result away ; downstream bricks (``subtract_sky_2d``,
87
+ ``correct_smile_polynomial`` reference_row, ``correct_slant_affine``
88
+ pivot_row) take it as a hand-tuned parameter. ``detect_trace`` makes
89
+ the automatic-reduction preset feasible. Tody 1986 SPIE 627 733
90
+ (IRAF ``apall`` / ``aptrace``).
91
+ - **`measure_arc_geometry`** — measure ``smile_radius`` and
92
+ ``slant_deg`` directly from an arc-lamp 2-D pose. The two geometry
93
+ correctors (``correct_smile_polynomial``, ``correct_slant_affine``)
94
+ consume them but until now had no measurement step ; they were
95
+ hand-tuned. The brick tracks each detected arc line row-by-row
96
+ across the slit, then fits one common Schroeder parabola + linear
97
+ tilt slope. Pairs with ``detect_trace`` (uses the science trace as
98
+ the reference row ``y₀``). IRAF ``identify/reidentify/fitcoords``
99
+ heritage (Tody 1986) ; PypeIt wavelength/tilts module (Prochaska et
100
+ al. 2020 JOSS 5 2308) ; Schroeder 2000 ch. 15 §15.3 for the smile
101
+ parametrisation.
102
+
103
+ ### Improved — cross-references in extraction docstrings
104
+
105
+ The ``long_description`` of both ``extract_spectrum_boxcar`` and
106
+ ``extract_spectrum_optimal`` now point at each other, so users can
107
+ choose between them with the precise scientific criterion (SNR
108
+ regime, need for uncertainty propagation, cosmic-ray residuals)
109
+ spelled out in the catalogue.
110
+
111
+ ### Numbers
112
+
113
+ **105 algorithms** registered after this release (was 98). **294
114
+ tests** pass (35 new), 2 skipped. ``ruff`` clean, ``mkdocs build
115
+ --strict`` clean, ``twine check --strict`` clean.
116
+
117
+ ## [0.2.1] — 2026-06-09
118
+
119
+ End-to-end aurora-parity patch. Diagnosed and validated bit-exactly on
120
+ real M42 long-slit data: with these two changes a downstream aurora
121
+ reduction pipeline running entirely on spectro-kernel matches its
122
+ science-core baseline at **Pearson 0.99996** (median |Δ| 0.82 %),
123
+ where v0.2.0 alone scored 0.107 (output silently shifted by +32768 ADU
124
+ on every step).
125
+
126
+ ### Fixed — uint16 ``BZERO``/``BSCALE`` leak in the easyspec staging helpers (BUG)
127
+
128
+ The shared helper ``stage_target`` in
129
+ [`reduction/_easyspec_apply.py`](src/spectro_kernel/algorithms/reduction/_easyspec_apply.py)
130
+ wrote ``ctx.image.data`` as float64 on a new ``PrimaryHDU`` and copied
131
+ the source header verbatim. When the source frame originated from a
132
+ **uint16 raw file** (``BITPIX=16``, ``BZERO=32768``, ``BSCALE=1`` —
133
+ astropy's convention), the scaling keywords landed on the float64 HDU
134
+ and astropy then re-applied them on read-back, **silently adding 32768
135
+ ADU to every pixel**. The companion ``build_corrected_imageframe``
136
+ re-attached the same scaling block to the corrected ``ImageFrame``, so
137
+ the next pipeline step that re-staged it inherited the leak.
138
+
139
+ The fix strips a small set of reserved / scaling keywords —
140
+ ``SIMPLE, BITPIX, EXTEND, BZERO, BSCALE, BLANK, END, NAXIS*, PCOUNT,
141
+ GCOUNT, XTENSION`` — before they reach either the staged HDU or the
142
+ header re-attached downstream. astropy re-derives the structural keys
143
+ from the data itself; the non-structural metadata (``EXPTIME``,
144
+ ``OBSERVER``, etc.) survives.
145
+
146
+ Affects all four easyspec apply wrappers in one shot:
147
+ ``subtract_bias_easyspec``, ``subtract_dark_easyspec``,
148
+ ``flat_normalize_easyspec``, ``extract_spectrum_easyspec``. Native-numpy
149
+ algorithms (``dark_subtract``, ``flat_normalize``, ``subtract_sky_2d``,
150
+ the v0.2.0 geometry / denoising suite) were never affected — they
151
+ operate directly on ``ctx.image.data`` without round-tripping through a
152
+ FITS file.
153
+
154
+ 5 regression tests added in
155
+ [`tests/unit/test_easyspec_apply_staging.py`](tests/unit/test_easyspec_apply_staging.py).
156
+
157
+ ### Added — ``extract_spectrum_boxcar`` (literature aperture extraction)
158
+
159
+ New algorithm in
160
+ [`algorithms/extraction/boxcar.py`](src/spectro_kernel/algorithms/extraction/boxcar.py).
161
+ Same easyspec tracing step as ``extract_spectrum_easyspec``, but
162
+ extraction is a **pure-numpy aperture sum** (clamped to the detector
163
+ bounds on every column).
164
+
165
+ Why a new brick: ``extract_spectrum_easyspec`` delegates the extraction
166
+ to ``easyspec.extracting``, whose per-column internal sky window
167
+ crashes with ``ValueError: broadcast (W,) vs (S,)`` when the aperture
168
+ half-width is wide (≥ a few hundred px) and the trace sits near a
169
+ detector edge — the typical aurora / extended-source geometry. The new
170
+ brick keeps easyspec's argmax tracing (the trace itself is fine) but
171
+ performs the aperture sum in numpy, with ``max(0, …)`` / ``min(rows,
172
+ …)`` clipping on every column so it is **edge-safe by construction**.
173
+
174
+ Supports two weightings:
175
+
176
+ - ``"tophat"`` — unweighted box sum (standard IRAF ``apall``).
177
+ - ``"gaussian"`` — profile-weighted Horne 1986 estimator (the average
178
+ column profile is fit with ``astropy.modeling`` to derive the
179
+ weights).
180
+
181
+ ``shift_y_pixels = 0`` disables the background subtraction (pure
182
+ column sum); a positive value samples sky from two symmetric flanking
183
+ windows. References: **Horne 1986, PASP 98, 609** ; **Tody 1986, Proc.
184
+ SPIE 627, 733** (IRAF ``apall`` / ``aptrace``). easyspec is imported
185
+ lazily inside ``run()`` (v0.1.5+ pattern).
186
+
187
+ 6 tests in
188
+ [`tests/unit/test_extract_boxcar.py`](tests/unit/test_extract_boxcar.py),
189
+ including the wide-aperture edge case that motivated the brick.
190
+
191
+ ### Numbers
192
+
193
+ **98 algorithms** registered (was 97). 254 tests pass (11 new), 3
194
+ skipped. ``ruff check`` clean, ``mkdocs build --strict`` clean,
195
+ ``twine check --strict`` clean.
196
+
9
197
  ## [0.2.0] — 2026-06-09
10
198
 
11
199
  A library expansion focused on long-slit reduction (aurora, stellar,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spectro-kernel
3
- Version: 0.2.0
3
+ Version: 0.3.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/
@@ -63,8 +63,11 @@ steps:
63
63
  band_below_hi: 160
64
64
  - algorithm: subtract_sky_2d
65
65
  params: {trace_row: 120, trace_half_width: 6, sky_offset: 4, sky_half_width: 10}
66
- - algorithm: extract_spectrum_sum
67
- params: {trace_row: 120, half_width: 6}
66
+ # extract_spectrum_boxcar is the edge-safe boxcar for extended sources
67
+ # (aurora, nebulae). For a stellar point source, extract_spectrum_sum
68
+ # or extract_spectrum_easyspec are also valid choices.
69
+ - algorithm: extract_spectrum_boxcar
70
+ params: {trace_method: "argmax", trace_half_width: 6, extraction_weights: "tophat", shift_y_pixels: 0}
68
71
 
69
72
  # ── 4. In-situ wavelength calibration from the simultaneously-acquired
70
73
  # sky reference (no arc lamp needed).
@@ -0,0 +1,291 @@
1
+ """Algorithm: build the instrumental-response curve from a standard star.
2
+
3
+ Closes the flux-calibration loop in the native (non-easyspec) chain.
4
+ The recipe is the textbook standard-star method (Oke 1990, Bessell
5
+ 1999, Hamuy 1992/1994) :
6
+
7
+ 1. Take the observed standard ``ctx.spectrum`` — already wavelength-
8
+ calibrated, ideally already extinction-corrected.
9
+ 2. Take the catalogue spectrum from ``ctx.extras["catalog_spectrum"]``
10
+ (e.g. a CALSPEC / Oke star, loaded via the existing ``read_*``
11
+ readers or fetched manually upstream).
12
+ 3. Resample the catalogue to the observed wavelength grid and compute
13
+ the ratio ``observed / catalog``. This is the **raw** sensitivity
14
+ curve (instrument + telescope + atmosphere combined).
15
+ 4. Mask Balmer + telluric windows (configurable via
16
+ ``exclude_regions``). These features are intrinsic to the source or
17
+ the atmosphere — keeping them in the fit would imprint their wings
18
+ into the response.
19
+ 5. Fit a low-frequency smoother (spline or polynomial) to the masked
20
+ ratio. The smoother captures the broad sensitivity envelope without
21
+ chasing line residuals.
22
+ 6. Optionally apply an extinction correction first, when the user
23
+ supplies an ``airmass``. The default is to assume the input has
24
+ already been extinction-corrected (the spec's recommendation; pair
25
+ with ``extinction_correct_easyspec`` upstream).
26
+
27
+ The resulting response is written to
28
+ ``ctx.extras["reference_spectrum"]`` so the existing
29
+ ``combine_spectra_arithmetic(operation="div")`` brick consumes it
30
+ directly as the "second spectrum" — same idiom as
31
+ ``remove_telluric_division``.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from typing import Any
37
+
38
+ import numpy as np
39
+ from scipy.interpolate import UnivariateSpline
40
+
41
+ from ...base import AlgorithmOutput, BaseAlgorithm
42
+ from ...errors import InvalidParameterError
43
+ from ...registry import register_algorithm
44
+ from ...types import AlgorithmCategory, Spectrum1D, WorkContext
45
+
46
+ # Default exclude windows: Balmer lines + main visible telluric bands.
47
+ # These are conservative widths suitable for amateur resolutions
48
+ # (Alpy 600 ~ 11 Å, Star'Ex LR ~ 4 Å). User can override entirely.
49
+ _DEFAULT_EXCLUDE_REGIONS_ANGSTROM: tuple[tuple[float, float], ...] = (
50
+ (3960.0, 3985.0), # Ca II H + Hε blend
51
+ (3925.0, 3945.0), # Ca II K
52
+ (4090.0, 4115.0), # Hδ
53
+ (4330.0, 4355.0), # Hγ
54
+ (4850.0, 4875.0), # Hβ
55
+ (5870.0, 5900.0), # Na D
56
+ (6540.0, 6585.0), # Hα (+ [N II])
57
+ (6860.0, 6960.0), # O₂ B-band telluric
58
+ (7150.0, 7350.0), # H₂O telluric
59
+ (7580.0, 7700.0), # O₂ A-band telluric
60
+ )
61
+
62
+ _OKE = "Oke 1990, AJ 99, 1621 — flux calibration with secondary standards."
63
+ _BESSELL = "Bessell 1999, PASP 111, 1426 — UBVRI flux standards review."
64
+ _HAMUY = (
65
+ "Hamuy et al. 1992 PASP 104, 533 + 1994 PASP 106, 566 — Southern "
66
+ "spectrophotometric standards."
67
+ )
68
+ _BOHLIN = "Bohlin et al. 2014, PASP 126, 711 — CALSPEC HST standard stars."
69
+ _HAYES_LATHAM = "Hayes & Latham 1975, ApJ 197, 593 — mean atmospheric extinction."
70
+
71
+
72
+ def _build_mask(
73
+ wavelength: np.ndarray, exclude_regions: list[tuple[float, float]],
74
+ ) -> np.ndarray:
75
+ """Boolean keep-mask : True where the sample is NOT in any exclude window."""
76
+ keep = np.ones(wavelength.shape, dtype=bool)
77
+ for lo, hi in exclude_regions:
78
+ keep &= ~((wavelength >= float(lo)) & (wavelength <= float(hi)))
79
+ return keep
80
+
81
+
82
+ def _sigma_clip_residuals(
83
+ x: np.ndarray, y: np.ndarray, fit_values: np.ndarray, sigma: float,
84
+ ) -> np.ndarray:
85
+ """Keep samples whose residual lies within ±σ of the fit."""
86
+ res = y - fit_values
87
+ rms = float(np.sqrt(np.nanmean(res ** 2)))
88
+ if rms == 0.0:
89
+ return np.ones(x.shape, dtype=bool)
90
+ return np.abs(res) <= sigma * rms
91
+
92
+
93
+ @register_algorithm(
94
+ "response_from_standard",
95
+ category=AlgorithmCategory.FLUX_CALIBRATION,
96
+ version="1.0.0",
97
+ )
98
+ class ResponseFromStandard(BaseAlgorithm):
99
+ """Derive the instrumental response curve from a standard-star observation.
100
+
101
+ Input contract :
102
+
103
+ - ``ctx.spectrum`` — the observed standard, wavelength-calibrated in
104
+ Ångström. Recommended : already extinction-corrected (via
105
+ ``extinction_correct_easyspec`` or equivalent).
106
+ - ``ctx.extras["catalog_spectrum"]`` — the catalogue spectrum of
107
+ the same standard (CALSPEC / Oke / MILES …), as a
108
+ :class:`Spectrum1D`. The wavelength grid does not need to match
109
+ the observed grid ; the algorithm resamples by linear
110
+ interpolation.
111
+
112
+ Output : ``ctx.extras["reference_spectrum"]`` — the smoothed
113
+ sensitivity curve on the observed wavelength grid. Feeds directly
114
+ into ``combine_spectra_arithmetic(operation="div")``.
115
+ """
116
+
117
+ backend = "scipy"
118
+ references = [_OKE, _BESSELL, _HAMUY, _BOHLIN, _HAYES_LATHAM]
119
+ long_description = (
120
+ "Computes observed / catalog (after resampling the catalogue to "
121
+ "the observed grid), masks Balmer + telluric windows, σ-clips, "
122
+ "fits a low-frequency spline (default) or polynomial. The "
123
+ "spline knot count controls the smoothness — too many knots "
124
+ "carve real features into the response, too few miss instrument "
125
+ "structure. Sensible defaults : 20 knots for the full visible. "
126
+ "Catalogue spectrum lives in ctx.extras['catalog_spectrum'], "
127
+ "consistent with the second-spectrum idiom used by "
128
+ "remove_telluric_division and combine_spectra_arithmetic."
129
+ )
130
+ default_params: dict[str, Any] = {
131
+ "fit": "spline",
132
+ "spline_knots": 20,
133
+ "poly_order": 5,
134
+ "exclude_regions": None, # None = use _DEFAULT_EXCLUDE_REGIONS_ANGSTROM
135
+ "sigma_clip": 3.0,
136
+ "max_iter": 3,
137
+ "catalog_key": "catalog_spectrum",
138
+ "output_key": "reference_spectrum",
139
+ }
140
+ required_params: list[str] = []
141
+ param_descriptions = {
142
+ "fit": "'spline' (default low-frequency UnivariateSpline) or 'polynomial'.",
143
+ "spline_knots": (
144
+ "Number of interior knots for the spline fit. More knots ⇒ "
145
+ "tighter fit (risks eating real features) ; fewer ⇒ smoother."
146
+ ),
147
+ "poly_order": "Polynomial order when fit='polynomial'.",
148
+ "exclude_regions": (
149
+ "List of (wave_lo, wave_hi) windows (Å) to exclude from the "
150
+ "fit. None ⇒ a default set covering Balmer + visible "
151
+ "telluric bands."
152
+ ),
153
+ "sigma_clip": "σ threshold for residual clipping during the fit (≥ 1).",
154
+ "max_iter": "Maximum σ-clip iterations.",
155
+ "catalog_key": "ctx.extras key holding the catalogue Spectrum1D.",
156
+ "output_key": "ctx.extras key receiving the fitted response curve.",
157
+ }
158
+ input_requirements = ["spectrum"]
159
+ output_produces = ["extras.reference_spectrum", "metrics.response_rms"]
160
+
161
+ def run(self, ctx: WorkContext, params: dict[str, Any]) -> AlgorithmOutput:
162
+ if ctx.spectrum is None:
163
+ return AlgorithmOutput.fail("No ctx.spectrum (the observed standard).")
164
+ catalog_key = str(params["catalog_key"])
165
+ catalog = ctx.extras.get(catalog_key)
166
+ if not isinstance(catalog, Spectrum1D):
167
+ return AlgorithmOutput.fail(
168
+ f"Catalogue spectrum missing at ctx.extras[{catalog_key!r}]."
169
+ )
170
+ fit = str(params["fit"])
171
+ if fit not in ("spline", "polynomial"):
172
+ raise InvalidParameterError("fit must be 'spline' or 'polynomial'.")
173
+
174
+ observed = ctx.spectrum
175
+ wave = np.asarray(observed.wavelength, dtype=np.float64)
176
+ obs_flux = np.asarray(observed.flux, dtype=np.float64)
177
+ if wave.size < 16:
178
+ return AlgorithmOutput.fail(
179
+ "Observed spectrum too short to build a response."
180
+ )
181
+
182
+ # Resample catalogue to the observed grid (NaN outside its domain).
183
+ cat_flux = np.interp(
184
+ wave,
185
+ np.asarray(catalog.wavelength, dtype=np.float64),
186
+ np.asarray(catalog.flux, dtype=np.float64),
187
+ left=np.nan, right=np.nan,
188
+ )
189
+
190
+ # Raw response = observed / catalog. NaN/zero-safe.
191
+ with np.errstate(divide="ignore", invalid="ignore"):
192
+ raw_response = obs_flux / cat_flux
193
+ valid = np.isfinite(raw_response) & (cat_flux > 0.0)
194
+
195
+ # Apply user-supplied exclude regions on top of validity mask.
196
+ excludes = params.get("exclude_regions")
197
+ if excludes is None:
198
+ excludes = list(_DEFAULT_EXCLUDE_REGIONS_ANGSTROM)
199
+ excludes = [(float(lo), float(hi)) for lo, hi in excludes]
200
+ keep = valid & _build_mask(wave, excludes)
201
+ if int(keep.sum()) < max(int(params["spline_knots"]) + 4, 8):
202
+ return AlgorithmOutput.fail(
203
+ f"Only {int(keep.sum())} valid samples after masking — "
204
+ "loosen exclude_regions or lower spline_knots."
205
+ )
206
+
207
+ sigma_clip = float(params["sigma_clip"])
208
+ max_iter = int(params["max_iter"])
209
+
210
+ if fit == "spline":
211
+ knots = max(4, int(params["spline_knots"]))
212
+ for _ in range(max_iter):
213
+ x_fit = wave[keep]
214
+ y_fit = raw_response[keep]
215
+ try:
216
+ spline = UnivariateSpline(
217
+ x_fit, y_fit, k=3, s=len(x_fit),
218
+ )
219
+ # Choose knot count by clamping the smoothing factor: a
220
+ # smaller s gives more knots. We tune s so the spline
221
+ # uses roughly `knots` internal knots.
222
+ span = float(np.var(y_fit)) * len(x_fit)
223
+ spline.set_smoothing_factor(max(span / max(knots, 1), 1e-9))
224
+ except Exception as exc: # noqa: BLE001
225
+ return AlgorithmOutput.fail(
226
+ f"Spline fit failed: {type(exc).__name__}: {exc}"
227
+ )
228
+ fit_values = spline(x_fit)
229
+ new_keep_in = _sigma_clip_residuals(
230
+ x_fit, y_fit, fit_values, sigma_clip,
231
+ )
232
+ if int(new_keep_in.sum()) <= knots + 4:
233
+ break
234
+ # Update outer mask in-place (mask is reduced, never grown).
235
+ inner = np.where(keep)[0]
236
+ keep = np.zeros_like(keep)
237
+ keep[inner[new_keep_in]] = True
238
+ response = spline(wave)
239
+ else: # polynomial
240
+ order = int(params["poly_order"])
241
+ for _ in range(max_iter):
242
+ x_fit = wave[keep]
243
+ y_fit = raw_response[keep]
244
+ coef = np.polyfit(x_fit, y_fit, order)
245
+ fit_values = np.polyval(coef, x_fit)
246
+ new_keep_in = _sigma_clip_residuals(
247
+ x_fit, y_fit, fit_values, sigma_clip,
248
+ )
249
+ if int(new_keep_in.sum()) <= order + 1:
250
+ break
251
+ inner = np.where(keep)[0]
252
+ keep = np.zeros_like(keep)
253
+ keep[inner[new_keep_in]] = True
254
+ response = np.polyval(coef, wave)
255
+
256
+ # Build the response Spectrum1D, marked as such for downstream.
257
+ reference = Spectrum1D(
258
+ wavelength=wave,
259
+ flux=response.astype(np.float64),
260
+ flux_unit="response",
261
+ wavelength_unit=observed.wavelength_unit,
262
+ meta={
263
+ "kind": "instrumental_response",
264
+ "derived_from": "response_from_standard",
265
+ "fit": fit,
266
+ "n_fit_samples": int(keep.sum()),
267
+ "exclude_regions_angstrom": excludes,
268
+ },
269
+ )
270
+ output_key = str(params["output_key"])
271
+ ctx.extras[output_key] = reference
272
+
273
+ residuals = raw_response[keep] - response[keep]
274
+ rms = float(np.sqrt(np.nanmean(residuals ** 2))) if residuals.size else 0.0
275
+ ctx.metrics["response_rms"] = rms
276
+ return AlgorithmOutput.ok(
277
+ metrics={
278
+ "response_rms": rms,
279
+ "n_fit_samples": float(int(keep.sum())),
280
+ "median_response": float(np.nanmedian(response)),
281
+ },
282
+ artifacts={
283
+ "fit_method": fit,
284
+ "exclude_regions_angstrom": excludes,
285
+ "output_key": output_key,
286
+ },
287
+ message=(
288
+ f"Response derived ({fit}, {int(keep.sum())} samples used, "
289
+ f"RMS={rms:.4g})."
290
+ ),
291
+ )