spectro-kernel 0.4.0__tar.gz → 0.4.1__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 (298) hide show
  1. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/CHANGELOG.md +36 -0
  2. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/PKG-INFO +1 -1
  3. spectro_kernel-0.4.1/src/spectro_kernel/algorithms/rv/cross_correlate.py +350 -0
  4. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/version.py +1 -1
  5. spectro_kernel-0.4.1/tests/unit/test_cross_correlate_extension.py +118 -0
  6. spectro_kernel-0.4.0/src/spectro_kernel/algorithms/rv/cross_correlate.py +0 -151
  7. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/.gitignore +0 -0
  8. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/LICENSE +0 -0
  9. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/README.md +0 -0
  10. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/concepts/algorithms.md +0 -0
  11. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/concepts/architecture.md +0 -0
  12. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/concepts/data-types.md +0 -0
  13. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/concepts/pipelines.md +0 -0
  14. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/contributing.md +0 -0
  15. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/cookbook/bess-dashboard.md +0 -0
  16. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/cookbook/multi-star-viewer.md +0 -0
  17. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/cookbook/web-playground.md +0 -0
  18. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/gen_catalogue.py +0 -0
  19. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/getting-started.md +0 -0
  20. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_ew_timeseries.png +0 -0
  21. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_halpha_phase_stack.png +0 -0
  22. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_halpha_timeseries.png +0 -0
  23. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_overlay.png +0 -0
  24. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_pc1_vs_phase.png +0 -0
  25. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_pca_2d.png +0 -0
  26. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_pca_3d.png +0 -0
  27. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_periodogram.png +0 -0
  28. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_phase_folded.png +0 -0
  29. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_rv_timeseries.png +0 -0
  30. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_similarity.png +0 -0
  31. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_similarity_date.png +0 -0
  32. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_halpha_phase_stack.png +0 -0
  33. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_pc1_vs_phase.png +0 -0
  34. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_pca_2d.png +0 -0
  35. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_pca_3d.png +0 -0
  36. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_periodogram.png +0 -0
  37. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_phase_folded.png +0 -0
  38. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_rv_timeseries.png +0 -0
  39. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_similarity_date.png +0 -0
  40. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_similarity_phase.png +0 -0
  41. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/index.md +0 -0
  42. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/alpha-cyg-time-series.md +0 -0
  43. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/alpha-dra-binary-period.md +0 -0
  44. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/aurora-line-monitor.md +0 -0
  45. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/be-star-variability.md +0 -0
  46. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/claude-mcp-end-to-end.md +0 -0
  47. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/exoplanet-transit-rv.md +0 -0
  48. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/full-reduction-walkthrough.md +0 -0
  49. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/native-vs-easyspec-showdown.md +0 -0
  50. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/your-first-sb2.md +0 -0
  51. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/reference.md +0 -0
  52. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/tutorials/add-an-algorithm.md +0 -0
  53. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/tutorials/analyse-a-spectrum.md +0 -0
  54. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/tutorials/discover-the-catalogue.md +0 -0
  55. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/tutorials/first-spectrum.md +0 -0
  56. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/tutorials/long-slit-reduction.md +0 -0
  57. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/usage/cli.md +0 -0
  58. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/usage/library.md +0 -0
  59. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/usage/mcp.md +0 -0
  60. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/why.md +0 -0
  61. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/playground/README.md +0 -0
  62. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/pyproject.toml +0 -0
  63. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/__init__.py +0 -0
  64. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/adapters/__init__.py +0 -0
  65. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/adapters/easyspec.py +0 -0
  66. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/__init__.py +0 -0
  67. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/_common.py +0 -0
  68. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/advanced/__init__.py +0 -0
  69. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/advanced/aperture_photometry.py +0 -0
  70. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/advanced/disentangle_sb2.py +0 -0
  71. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/advanced/doppler_tomogram.py +0 -0
  72. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/catalogs/__init__.py +0 -0
  73. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/catalogs/gaia.py +0 -0
  74. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/catalogs/simbad.py +0 -0
  75. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/catalogs/vizier.py +0 -0
  76. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/classification/__init__.py +0 -0
  77. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/classification/classify_template_chi2.py +0 -0
  78. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/classification/pickles_atlas.py +0 -0
  79. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/continuum/__init__.py +0 -0
  80. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/continuum/compare_normalisations.py +0 -0
  81. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/continuum/normalize_edges.py +0 -0
  82. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/continuum/normalize_max.py +0 -0
  83. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/continuum/normalize_percentile.py +0 -0
  84. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/continuum/normalize_polynomial.py +0 -0
  85. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/continuum/normalize_region.py +0 -0
  86. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/continuum/subtract_continuum.py +0 -0
  87. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/__init__.py +0 -0
  88. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/air_vacuum.py +0 -0
  89. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/barycentric.py +0 -0
  90. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/doppler_shift.py +0 -0
  91. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/extinction_correct_easyspec.py +0 -0
  92. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/fit_telluric_scaling.py +0 -0
  93. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/flux_calibrate_easyspec.py +0 -0
  94. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/remove_telluric.py +0 -0
  95. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/response_from_standard.py +0 -0
  96. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/synth_telluric.py +0 -0
  97. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/__init__.py +0 -0
  98. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/embed_band_power.py +0 -0
  99. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/embed_continuum_subtracted.py +0 -0
  100. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/embed_lick_indices.py +0 -0
  101. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/embed_log_lambda.py +0 -0
  102. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/embed_pretrained.py +0 -0
  103. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/embed_remote.py +0 -0
  104. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/embed_spectrum.py +0 -0
  105. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/embed_wavelets.py +0 -0
  106. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/exports/__init__.py +0 -0
  107. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/exports/export_csv.py +0 -0
  108. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/exports/export_fits.py +0 -0
  109. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/exports/export_fits_bess.py +0 -0
  110. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/exports/export_hdf5.py +0 -0
  111. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/exports/export_votable.py +0 -0
  112. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/extraction/__init__.py +0 -0
  113. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/extraction/boxcar.py +0 -0
  114. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/extraction/detect_trace.py +0 -0
  115. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/extraction/easyspec_extract.py +0 -0
  116. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/extraction/optimal.py +0 -0
  117. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/extraction/sky_lateral_bands.py +0 -0
  118. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/io/__init__.py +0 -0
  119. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/io/read_ascii.py +0 -0
  120. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/io/read_echelle.py +0 -0
  121. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/io/read_fits.py +0 -0
  122. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/io/read_sdss.py +0 -0
  123. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/io/read_votable.py +0 -0
  124. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/kinematics/__init__.py +0 -0
  125. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/kinematics/rotation_curve.py +0 -0
  126. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/__init__.py +0 -0
  127. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/_line_flux.py +0 -0
  128. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/_profiles.py +0 -0
  129. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/catalogs.py +0 -0
  130. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/compare_line_fits.py +0 -0
  131. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/detect.py +0 -0
  132. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/equivalent_width.py +0 -0
  133. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/fit_gaussian.py +0 -0
  134. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/fit_lorentzian.py +0 -0
  135. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/fit_voigt.py +0 -0
  136. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/vr_ratio.py +0 -0
  137. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/nebular/__init__.py +0 -0
  138. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/nebular/bpt_line_ratios.py +0 -0
  139. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/nebular/oiii_electron_temperature.py +0 -0
  140. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/nebular/sii_electron_density.py +0 -0
  141. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/quality/__init__.py +0 -0
  142. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/quality/compare_snr_methods.py +0 -0
  143. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/quality/snr_der.py +0 -0
  144. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/quality/snr_edge.py +0 -0
  145. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/quality/snr_linear_fit.py +0 -0
  146. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/__init__.py +0 -0
  147. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/_easyspec_apply.py +0 -0
  148. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/_easyspec_helpers.py +0 -0
  149. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/bias_combine.py +0 -0
  150. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/clip_cosmic_rays.py +0 -0
  151. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/dark_combine.py +0 -0
  152. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/dark_subtract.py +0 -0
  153. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/denoise_2d.py +0 -0
  154. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/easyspec_bias.py +0 -0
  155. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/easyspec_cosmic_ray.py +0 -0
  156. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/easyspec_dark.py +0 -0
  157. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/easyspec_flat.py +0 -0
  158. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/easyspec_flat_normalize.py +0 -0
  159. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/easyspec_subtract_bias.py +0 -0
  160. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/easyspec_subtract_dark.py +0 -0
  161. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/extract_spectrum_sum.py +0 -0
  162. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/flat_combine.py +0 -0
  163. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/flat_normalize.py +0 -0
  164. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/geometry.py +0 -0
  165. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/subtract_sky_2d.py +0 -0
  166. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/wavelength_calibrate.py +0 -0
  167. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/rv/__init__.py +0 -0
  168. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/rv/fit_keplerian_orbit.py +0 -0
  169. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/rv/measure.py +0 -0
  170. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/rv/precision_bouchy.py +0 -0
  171. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/rv/redshift_lines.py +0 -0
  172. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/smoothing/__init__.py +0 -0
  173. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/smoothing/compare_smoothings.py +0 -0
  174. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/smoothing/smooth_gaussian.py +0 -0
  175. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/smoothing/smooth_savgol.py +0 -0
  176. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/stacking/__init__.py +0 -0
  177. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/stacking/merge_echelle_orders.py +0 -0
  178. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/stacking/stack.py +0 -0
  179. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/timeseries/__init__.py +0 -0
  180. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/timeseries/lomb_scargle.py +0 -0
  181. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/timeseries/phase_fold.py +0 -0
  182. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/transforms/__init__.py +0 -0
  183. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/transforms/clip_sigma.py +0 -0
  184. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/transforms/combine_arithmetic.py +0 -0
  185. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/transforms/extract_region.py +0 -0
  186. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/transforms/mask_range.py +0 -0
  187. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/transforms/resample.py +0 -0
  188. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/transforms/resample_flux_conserving.py +0 -0
  189. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/viz/__init__.py +0 -0
  190. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/viz/plot_3d_surface.py +0 -0
  191. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/viz/plot_animation.py +0 -0
  192. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/viz/plot_dynamic_spectrum.py +0 -0
  193. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/viz/plot_plotly.py +0 -0
  194. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/wavelength_calibration/__init__.py +0 -0
  195. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/wavelength_calibration/arc_geometry.py +0 -0
  196. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/wavelength_calibration/easyspec_wavelength.py +0 -0
  197. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/wavelength_calibration/in_situ.py +0 -0
  198. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/wavelength_calibration/lamp_atlas.py +0 -0
  199. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/wavelength_calibration/match_lamp.py +0 -0
  200. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/wavelength_calibration/solar.py +0 -0
  201. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/base.py +0 -0
  202. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/cli.py +0 -0
  203. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/embeddings.py +0 -0
  204. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/errors.py +0 -0
  205. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/io/__init__.py +0 -0
  206. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/io/ascii.py +0 -0
  207. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/io/fits.py +0 -0
  208. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/io/votable.py +0 -0
  209. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/pipeline.py +0 -0
  210. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/__init__.py +0 -0
  211. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/catalog/analysis/balmer_quick.yaml +0 -0
  212. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/catalog/analysis/embed_quick.yaml +0 -0
  213. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/catalog/analysis/quality_report.yaml +0 -0
  214. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/catalog/analysis/rv_quick.yaml +0 -0
  215. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/catalog/analysis/snr_check.yaml +0 -0
  216. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/catalog/analysis/time_series_overview.yaml +0 -0
  217. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/catalog/reduction/full_reduction_easyspec.yaml +0 -0
  218. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/loader.py +0 -0
  219. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/py.typed +0 -0
  220. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/registry.py +0 -0
  221. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/__init__.py +0 -0
  222. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/catalog.py +0 -0
  223. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/context.py +0 -0
  224. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/enums.py +0 -0
  225. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/history.py +0 -0
  226. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/image.py +0 -0
  227. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/line.py +0 -0
  228. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/spectrum.py +0 -0
  229. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/timeseries.py +0 -0
  230. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/__init__.py +0 -0
  231. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/__main__.py +0 -0
  232. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/auth.py +0 -0
  233. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/auto_tools.py +0 -0
  234. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/observability.py +0 -0
  235. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/py.typed +0 -0
  236. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/server.py +0 -0
  237. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/session.py +0 -0
  238. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/url_safety.py +0 -0
  239. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/conftest.py +0 -0
  240. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/reference/conftest.py +0 -0
  241. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/reference/data/.gitkeep +0 -0
  242. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/reference/data/README.md +0 -0
  243. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/reference/data/sun/sun_reference_stis_002.fits +0 -0
  244. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/reference/data/vega/alpha_lyr_stis_011.fits +0 -0
  245. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/reference/test_known_answers.py +0 -0
  246. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/reference/test_sun.py +0 -0
  247. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/reference/test_vega.py +0 -0
  248. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_base.py +0 -0
  249. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_classify_template_chi2.py +0 -0
  250. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_cli.py +0 -0
  251. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_combine.py +0 -0
  252. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_combine_arithmetic.py +0 -0
  253. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_continuum.py +0 -0
  254. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_corrections.py +0 -0
  255. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_dark_flat_combine.py +0 -0
  256. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_denoise_2d.py +0 -0
  257. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_detect_lines_blind.py +0 -0
  258. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_detect_trace.py +0 -0
  259. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_doppler_tomogram.py +0 -0
  260. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_easyspec_apply_staging.py +0 -0
  261. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_easyspec_wrappers.py +0 -0
  262. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_embedding.py +0 -0
  263. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_embedding_pretrained.py +0 -0
  264. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_embedding_remote_lick.py +0 -0
  265. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_embedding_tier1.py +0 -0
  266. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_export_fits_bess.py +0 -0
  267. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_extract_boxcar.py +0 -0
  268. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_extract_optimal.py +0 -0
  269. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_geometry_corrections.py +0 -0
  270. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_io.py +0 -0
  271. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_lamp_atlas_match.py +0 -0
  272. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_line_profiles.py +0 -0
  273. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_lines.py +0 -0
  274. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_mcp.py +0 -0
  275. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_mcp_production.py +0 -0
  276. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_mcp_security.py +0 -0
  277. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_measure_arc_geometry.py +0 -0
  278. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_misc_algorithms.py +0 -0
  279. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_nebular_diagnostics.py +0 -0
  280. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_no_circular_imports.py +0 -0
  281. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_normalize_to_region.py +0 -0
  282. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_pipeline.py +0 -0
  283. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_quality.py +0 -0
  284. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_read_sdss.py +0 -0
  285. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_redshift_lines.py +0 -0
  286. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_registry.py +0 -0
  287. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_response_from_standard.py +0 -0
  288. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_rotation_curve.py +0 -0
  289. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_sky_lateral_bands.py +0 -0
  290. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_smoothing.py +0 -0
  291. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_timeseries.py +0 -0
  292. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_transforms.py +0 -0
  293. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_types.py +0 -0
  294. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_viz.py +0 -0
  295. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_vr_ratio.py +0 -0
  296. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_wavelength_calibration_in_situ.py +0 -0
  297. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_wavelength_calibration_solar.py +0 -0
  298. {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/website/README.md +0 -0
@@ -6,6 +6,42 @@ Until `1.0.0` the public API may change between minor versions.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.1] — 2026-06-26
10
+
11
+ Backwards-compatible extension of `cross_correlate_rv` so downstream
12
+ RV consumers can delegate to the kernel without losing the
13
+ peak-confidence / error / CCF-curve / template-provenance surface
14
+ they currently maintain in-house.
15
+
16
+ ### Changed — `cross_correlate_rv` v1.1.0 → v1.2.0
17
+
18
+ - The CCF is now **Pearson-normalised** so the peak height reads
19
+ directly as a similarity score bounded in ``[-1, 1]``.
20
+ - New ``metrics.peak_strength`` (clamped to ``[0, 1]``) — UI-friendly
21
+ confidence indicator (1 ⇒ template-identical observed spectrum).
22
+ - New ``metrics.radial_velocity_error_kms`` — the canonical
23
+ **Tonry & Davis 1979** §III formula
24
+ ``σ_v = 3 · w / (8 · (1 + r))`` with
25
+ ``r = h / (√2 · σ_a)`` (peak height over noise antisymmetry RMS)
26
+ and ``w`` the CCF peak FWHM. Reports ``NaN`` when the CCF shape
27
+ forbids a clean estimate (no resolved peak, no antisymmetric
28
+ noise sample).
29
+ - New ``extras["ccf"] = {"lags_kms": [...], "ccf": [...]}`` — the CCF
30
+ curve restricted to the velocity search window, for downstream
31
+ plotting.
32
+ - New ``extras["template_id"]`` — best-effort label of the template
33
+ used (file basename for ``template_path``, key name for
34
+ ``template_key``).
35
+ - Pre-existing output ``metrics.radial_velocity_kms`` is preserved
36
+ bit-exact ; the ``v1.2.0`` bump reflects the *additions*. A
37
+ regression test (`tests/reference/test_known_answers.py`) covers
38
+ the existing 50 km/s recovery.
39
+
40
+ ### Numbers
41
+
42
+ 113 algorithms (unchanged). 331 tests pass (6 new), ruff clean,
43
+ mkdocs strict OK, twine strict OK.
44
+
9
45
  ## [0.4.0] — 2026-06-26
10
46
 
11
47
  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.4.1
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,350 @@
1
+ """Algorithm: cross-correlate against a template to measure radial velocity.
2
+
3
+ Standard Tonry & Davis 1979 recipe: resample observed and template
4
+ spectra onto a common log-wavelength grid (so a sample-shift is a
5
+ constant velocity shift), centre both at zero mean, compute the
6
+ normalised cross-correlation function, locate the peak with sub-pixel
7
+ parabolic refinement, and report the radial velocity.
8
+
9
+ In addition to the RV itself, the brick computes :
10
+
11
+ - The **CCF peak strength** in ``[0, 1]`` — Pearson-normalised peak
12
+ height, a 0..1 confidence score that an observer can show in a UI
13
+ (1 ⇒ template-identical match, 0 ⇒ uncorrelated).
14
+ - The **RV uncertainty** in km/s, using the **Tonry & Davis 1979
15
+ r-value** : ``σ_v = 3 · w / (8 · (1 + r))`` with ``w`` the peak FWHM
16
+ and ``r = h / (√2 · σ_a)`` where ``h`` is the peak height and
17
+ ``σ_a`` is the RMS of the antisymmetric component of the CCF away
18
+ from the peak. This is the canonical RV error estimator from §III
19
+ of the 1979 paper.
20
+ - The **CCF curve** itself (lags km/s + ccf values) on
21
+ ``ctx.extras["ccf"]`` for downstream plotting.
22
+ - The **template identifier** on ``ctx.extras["template_id"]`` so a
23
+ downstream pipeline can record provenance of which template fit
24
+ best.
25
+
26
+ References
27
+ ----------
28
+ - **Tonry, J. & Davis, M. 1979**, AJ 84, 1511 — fundamental CCF
29
+ method for radial velocity ; defines the ``r`` parameter and the
30
+ velocity-error formula used here.
31
+ - ``scipy.signal.correlate`` — the cross-correlation engine.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import math
37
+ import os
38
+ from typing import Any
39
+
40
+ import astropy.constants as const
41
+ import astropy.units as u
42
+ import numpy as np
43
+ from scipy.signal import correlate
44
+
45
+ from ...base import AlgorithmOutput, BaseAlgorithm
46
+ from ...io import read_fits
47
+ from ...registry import register_algorithm
48
+ from ...types import AlgorithmCategory, Spectrum1D, WorkContext
49
+
50
+ _C_KMS = const.c.to(u.km / u.s).value
51
+
52
+ # Tonry-Davis 1979 §III error coefficient: σ_v = 3 · w / (8 · (1 + r)).
53
+ _TD_ERROR_COEF: float = 3.0 / 8.0
54
+
55
+
56
+ def _peak_fwhm_kms(velocities: np.ndarray, ccf: np.ndarray, ipeak: int) -> float | None:
57
+ """FWHM (km/s) of the CCF peak above half-maximum, or None when ambiguous."""
58
+ if ipeak <= 0 or ipeak >= ccf.size - 1:
59
+ return None
60
+ peak = float(ccf[ipeak])
61
+ baseline = float(np.min(ccf))
62
+ if peak <= baseline:
63
+ return None
64
+ half = 0.5 * (peak + baseline)
65
+
66
+ # Walk left and right from the peak until the CCF drops below half.
67
+ left = ipeak
68
+ while left > 0 and ccf[left] >= half:
69
+ left -= 1
70
+ right = ipeak
71
+ while right < ccf.size - 1 and ccf[right] >= half:
72
+ right += 1
73
+ if left == ipeak or right == ipeak:
74
+ return None
75
+ # Linear interpolation at the half-max crossings for sub-bin precision.
76
+ if ccf[left + 1] != ccf[left]:
77
+ frac_left = (half - ccf[left]) / (ccf[left + 1] - ccf[left])
78
+ v_left = velocities[left] + frac_left * (velocities[left + 1] - velocities[left])
79
+ else:
80
+ v_left = float(velocities[left])
81
+ if ccf[right - 1] != ccf[right]:
82
+ frac_right = (half - ccf[right]) / (ccf[right - 1] - ccf[right])
83
+ v_right = velocities[right] + frac_right * (velocities[right - 1] - velocities[right])
84
+ else:
85
+ v_right = float(velocities[right])
86
+ return float(abs(v_right - v_left))
87
+
88
+
89
+ def _tonry_davis_r(ccf: np.ndarray, ipeak: int, peak_height: float) -> float | None:
90
+ """Tonry & Davis 1979 r = h / (√2 · σ_a).
91
+
92
+ σ_a is the RMS of the antisymmetric component
93
+ ``a(δ) = [ccf(peak+δ) − ccf(peak−δ)] / 2`` measured **away from
94
+ the peak**, where the signal is dominated by noise. The window
95
+ used here is the outer half of the CCF (the inner half is the
96
+ peak itself).
97
+ """
98
+ if peak_height <= 0.0:
99
+ return None
100
+ n = ccf.size
101
+ if n < 16:
102
+ return None
103
+ # Symmetric δ range around the peak — capped to whichever side of
104
+ # the peak is shorter, so we never index past either end.
105
+ half_span = min(ipeak, n - 1 - ipeak)
106
+ if half_span < 4:
107
+ return None
108
+ deltas = np.arange(1, half_span + 1)
109
+ left = ccf[ipeak - deltas]
110
+ right = ccf[ipeak + deltas]
111
+ antisym = 0.5 * (right - left)
112
+ # Far-from-peak window for the noise estimate (outer 50 %).
113
+ tail_start = half_span // 2
114
+ tail = antisym[tail_start:]
115
+ if tail.size < 4:
116
+ return None
117
+ sigma_a = float(np.sqrt(np.mean(tail ** 2)))
118
+ if sigma_a <= 0.0:
119
+ return None
120
+ return float(peak_height / (math.sqrt(2.0) * sigma_a))
121
+
122
+
123
+ def _template_identifier(params: dict[str, Any]) -> str | None:
124
+ """Best-effort label for the template used (path basename or extras key)."""
125
+ if params.get("template_path"):
126
+ return os.path.basename(str(params["template_path"]))
127
+ if params.get("template_key"):
128
+ return str(params["template_key"])
129
+ return None
130
+
131
+
132
+ @register_algorithm(
133
+ "cross_correlate_rv",
134
+ category=AlgorithmCategory.RADIAL_VELOCITY,
135
+ # 1.2.0: add peak_strength, Tonry-Davis r-value error, extras.ccf, extras.template_id.
136
+ version="1.2.0",
137
+ )
138
+ class CrossCorrelateRv(BaseAlgorithm):
139
+ """Measure radial velocity by cross-correlation against a template spectrum.
140
+
141
+ Both spectra are resampled to a common log-wavelength grid (so a
142
+ shift in samples maps to a constant velocity); their normalised
143
+ cross-correlation function (CCF) is computed; the peak is located
144
+ with sub-pixel parabolic refinement; the shift converts to
145
+ velocity via ``v = c · dln(λ)``.
146
+
147
+ Outputs (v1.2.0) :
148
+
149
+ - ``metrics.radial_velocity_kms`` — RV in km/s (positive = redshifted
150
+ vs. template).
151
+ - ``metrics.radial_velocity_error_kms`` — Tonry & Davis 1979 §III
152
+ error : ``σ_v = 3 · w / (8 · (1 + r))`` with ``w`` = peak FWHM
153
+ and ``r = h / (√2 · σ_a)``. ``NaN`` if the CCF shape forbids the
154
+ estimate (no resolved peak, no antisymmetric noise sample).
155
+ - ``metrics.peak_strength`` — Pearson-normalised peak height in
156
+ ``[0, 1]``. 1 ⇒ template-identical observed spectrum.
157
+ - ``extras.ccf`` — ``{"lags_kms": [...], "ccf": [...]}``, the CCF
158
+ curve restricted to the velocity search window.
159
+ - ``extras.template_id`` — best-effort label of the template used
160
+ (file basename or ``ctx.extras`` key).
161
+ """
162
+
163
+ backend = "scipy"
164
+ references = [
165
+ "Tonry & Davis 1979, AJ 84, 1511 — fundamental CCF method for "
166
+ "RV ; r-value and σ_v = 3·w/(8·(1+r)) error formula (§III).",
167
+ "scipy.signal.correlate — cross-correlation engine.",
168
+ ]
169
+ long_description = (
170
+ "Template comes from `template_path` (FITS), `template_key` (a "
171
+ "Spectrum1D under ctx.extras), `ctx.extras['template_spectrum']` "
172
+ "or `ctx.spectra[0]` — first match wins. Both spectra are "
173
+ "centred (mean = 0) before correlation, so the reported "
174
+ "peak_strength is bounded in [0, 1]. The RV error is the "
175
+ "Tonry-Davis 1979 formula ; it underestimates the true error "
176
+ "for asymmetric CCFs (multi-component blended templates) — use "
177
+ "MCMC / bootstrap if higher fidelity is needed."
178
+ )
179
+ default_params = {
180
+ "template_path": None,
181
+ "template_key": None,
182
+ "n_grid": 4096,
183
+ "vmin_kms": -800.0,
184
+ "vmax_kms": 800.0,
185
+ }
186
+ param_descriptions = {
187
+ "template_path": "Path / URL of a template FITS spectrum to load.",
188
+ "template_key": "Key in ctx.extras where a Spectrum1D template is stored.",
189
+ "n_grid": "Number of log-wavelength samples on which to interpolate.",
190
+ "vmin_kms": "Lower bound of the velocity search range (km/s).",
191
+ "vmax_kms": "Upper bound of the velocity search range (km/s).",
192
+ }
193
+ input_requirements = ["spectrum"]
194
+ output_produces = [
195
+ "metrics.radial_velocity_kms",
196
+ "metrics.radial_velocity_error_kms",
197
+ "metrics.peak_strength",
198
+ "extras.ccf",
199
+ "extras.template_id",
200
+ ]
201
+
202
+ def run(self, ctx: WorkContext, params: dict[str, Any]) -> AlgorithmOutput:
203
+ spec = ctx.spectrum
204
+ template = self._resolve_template(ctx, params)
205
+ if template is None:
206
+ return AlgorithmOutput.fail(
207
+ "No template available — pass template_path, set "
208
+ "ctx.extras['template_spectrum'], or put one in ctx.spectra[0]."
209
+ )
210
+
211
+ wmin = max(spec.wavelength_min, template.wavelength_min)
212
+ wmax = min(spec.wavelength_max, template.wavelength_max)
213
+ if wmin >= wmax:
214
+ return AlgorithmOutput.fail(
215
+ "Spectrum and template do not overlap in wavelength."
216
+ )
217
+
218
+ n = int(params["n_grid"])
219
+ log_grid = np.linspace(np.log(wmin), np.log(wmax), n)
220
+ delta_log = (log_grid[-1] - log_grid[0]) / (n - 1)
221
+ wave_grid = np.exp(log_grid)
222
+
223
+ # np.interp does NOT skip NaN samples — a single bad pixel in
224
+ # the input poisons the whole row. Strip NaNs before interpolating.
225
+ spec_f = _interp_skip_nan(wave_grid, spec.wavelength, spec.flux)
226
+ templ_f = _interp_skip_nan(wave_grid, template.wavelength, template.flux)
227
+ # Centre both signals so the CCF is bounded in [-1, 1] after
228
+ # Pearson normalisation below.
229
+ spec_f = spec_f - float(np.nanmean(spec_f))
230
+ templ_f = templ_f - float(np.nanmean(templ_f))
231
+
232
+ # Pearson normalisation : ccf_norm ∈ [-1, 1] regardless of
233
+ # absolute scale. The CCF peak then directly reads as a
234
+ # similarity score, suitable for a UI confidence indicator.
235
+ spec_norm = float(np.sqrt(np.sum(spec_f * spec_f)))
236
+ templ_norm = float(np.sqrt(np.sum(templ_f * templ_f)))
237
+ if spec_norm <= 0.0 or templ_norm <= 0.0:
238
+ return AlgorithmOutput.fail(
239
+ "Observed or template signal vanishes after centring — "
240
+ "check input is not flat."
241
+ )
242
+ ccf = correlate(spec_f, templ_f, mode="full") / (spec_norm * templ_norm)
243
+ lags = np.arange(-(n - 1), n, dtype=np.float64)
244
+ velocities = lags * delta_log * _C_KMS
245
+
246
+ vmin = float(params["vmin_kms"])
247
+ vmax = float(params["vmax_kms"])
248
+ window = (velocities >= vmin) & (velocities <= vmax)
249
+ if not window.any():
250
+ return AlgorithmOutput.fail(
251
+ "Velocity search range too narrow given n_grid."
252
+ )
253
+ ccf_w = ccf[window]
254
+ v_w = velocities[window]
255
+
256
+ ipeak = int(np.argmax(ccf_w))
257
+ v_peak = float(v_w[ipeak])
258
+ peak_height = float(ccf_w[ipeak])
259
+ # Parabolic vertex refinement (sub-velocity-bin precision).
260
+ if 0 < ipeak < ccf_w.size - 1:
261
+ y0, y1, y2 = ccf_w[ipeak - 1], ccf_w[ipeak], ccf_w[ipeak + 1]
262
+ denom = y0 - 2.0 * y1 + y2
263
+ if denom != 0.0:
264
+ offset = 0.5 * (y0 - y2) / denom
265
+ v_peak += float(offset) * float(v_w[1] - v_w[0])
266
+
267
+ # peak_strength: clamp to [0, 1] for the UI confidence indicator
268
+ # (negative peaks would mean anti-correlation, which the consumer
269
+ # treats as "no match").
270
+ peak_strength = float(max(0.0, min(1.0, peak_height)))
271
+
272
+ # RV error — Tonry & Davis 1979 §III: σ_v = 3·w / (8·(1+r)).
273
+ fwhm_kms = _peak_fwhm_kms(v_w, ccf_w, ipeak)
274
+ r_value = _tonry_davis_r(ccf_w, ipeak, peak_height)
275
+ if fwhm_kms is not None and r_value is not None and (1.0 + r_value) > 0.0:
276
+ rv_error_kms: float = _TD_ERROR_COEF * fwhm_kms / (1.0 + r_value)
277
+ else:
278
+ rv_error_kms = float("nan")
279
+
280
+ template_id = _template_identifier(params)
281
+
282
+ ctx.metrics["radial_velocity_kms"] = v_peak
283
+ ctx.metrics["radial_velocity_error_kms"] = rv_error_kms
284
+ ctx.metrics["peak_strength"] = peak_strength
285
+ ctx.extras["ccf"] = {
286
+ "lags_kms": v_w.tolist(),
287
+ "ccf": ccf_w.tolist(),
288
+ }
289
+ if template_id is not None:
290
+ ctx.extras["template_id"] = template_id
291
+
292
+ metrics_out: dict[str, float] = {
293
+ "radial_velocity_kms": v_peak,
294
+ "radial_velocity_error_kms": rv_error_kms,
295
+ "peak_strength": peak_strength,
296
+ "ccf_peak": peak_height,
297
+ }
298
+ if r_value is not None:
299
+ metrics_out["tonry_davis_r"] = float(r_value)
300
+ if fwhm_kms is not None:
301
+ metrics_out["ccf_fwhm_kms"] = float(fwhm_kms)
302
+
303
+ message_bits = [f"RV = {v_peak:+.2f} km/s"]
304
+ if not math.isnan(rv_error_kms):
305
+ message_bits.append(f"± {rv_error_kms:.2f} km/s")
306
+ message_bits.append(f"peak strength = {peak_strength:.3f}")
307
+ if template_id:
308
+ message_bits.append(f"template = {template_id}")
309
+ message = ", ".join(message_bits) + "."
310
+
311
+ return AlgorithmOutput.ok(
312
+ metrics=metrics_out,
313
+ artifacts={
314
+ "velocity_kms": v_w.tolist(),
315
+ "ccf": ccf_w.tolist(),
316
+ "template_id": template_id,
317
+ },
318
+ message=message,
319
+ )
320
+
321
+ @staticmethod
322
+ def _resolve_template(ctx: WorkContext, params: dict[str, Any]) -> Spectrum1D | None:
323
+ if params["template_path"]:
324
+ return read_fits(params["template_path"])
325
+ if params["template_key"]:
326
+ value = ctx.extras.get(params["template_key"])
327
+ if isinstance(value, Spectrum1D):
328
+ return value
329
+ value = ctx.extras.get("template_spectrum")
330
+ if isinstance(value, Spectrum1D):
331
+ return value
332
+ if ctx.spectra:
333
+ return ctx.spectra[0]
334
+ return None
335
+
336
+
337
+ def _interp_skip_nan(x_new: np.ndarray, xp: np.ndarray, fp: np.ndarray) -> np.ndarray:
338
+ """Linear interp that ignores NaN samples in ``fp``.
339
+
340
+ ``np.interp`` happily extrapolates a NaN through every sample whose
341
+ neighbouring source point is NaN; for cross-correlation that means a
342
+ single bad pixel destroys the CCF. Stripping NaNs first preserves the
343
+ rest of the signal at the cost of a slightly biased local interpolation.
344
+ """
345
+ finite = np.isfinite(fp)
346
+ if finite.all():
347
+ return np.interp(x_new, xp, fp)
348
+ if not finite.any():
349
+ return np.zeros_like(x_new)
350
+ return np.interp(x_new, xp[finite], fp[finite])
@@ -1,3 +1,3 @@
1
1
  """Single source of truth for the package version."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.4.1"
@@ -0,0 +1,118 @@
1
+ """Tests for the v1.2.0 cross_correlate_rv extensions.
2
+
3
+ The v1.1.0 RV-recovery behaviour is exercised in
4
+ ``tests/reference/test_known_answers.py`` ; here we exercise the
5
+ **new outputs** : peak_strength, Tonry-Davis r-value error, ccf
6
+ artefact, and template_id provenance.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import math
12
+
13
+ import numpy as np
14
+
15
+ from spectro_kernel import WorkContext, run_algorithm
16
+ from spectro_kernel.types import Spectrum1D
17
+
18
+
19
+ def _template(seed: int = 0, npix: int = 2000) -> Spectrum1D:
20
+ """A template-like spectrum: smooth continuum + a few absorption dips."""
21
+ rng = np.random.default_rng(seed)
22
+ wave = np.linspace(5000.0, 6000.0, npix)
23
+ flux = 100.0 + rng.normal(0.0, 0.3, npix)
24
+ for centre in (5172.0, 5183.6, 5400.0, 5600.0, 5890.0):
25
+ flux -= 30.0 * np.exp(-0.5 * ((wave - centre) / 1.5) ** 2)
26
+ return Spectrum1D(wavelength=wave, flux=flux, wavelength_unit="Angstrom")
27
+
28
+
29
+ def _doppler_shifted(template: Spectrum1D, v_kms: float) -> Spectrum1D:
30
+ c = 299_792.458
31
+ shifted_wave = template.wavelength * (1.0 + v_kms / c)
32
+ return Spectrum1D(wavelength=shifted_wave, flux=template.flux.copy())
33
+
34
+
35
+ def test_peak_strength_is_near_unity_for_self_correlation():
36
+ """Spec = template (v = 0) ⇒ peak_strength ≈ 1, error small."""
37
+ tmpl = _template()
38
+ ctx = WorkContext(spectrum=Spectrum1D(wavelength=tmpl.wavelength, flux=tmpl.flux))
39
+ ctx.extras["template_spectrum"] = tmpl
40
+ out = run_algorithm("cross_correlate_rv", ctx, {"n_grid": 4096})
41
+ assert out.success
42
+ assert abs(ctx.metrics["radial_velocity_kms"]) < 2.0
43
+ assert ctx.metrics["peak_strength"] > 0.95
44
+ assert ctx.metrics["peak_strength"] <= 1.0
45
+ # Error finite, small for a self-correlation.
46
+ err = ctx.metrics["radial_velocity_error_kms"]
47
+ assert math.isfinite(err)
48
+ assert err < 10.0
49
+
50
+
51
+ def test_peak_strength_and_error_present_for_shifted_input():
52
+ """Shifted by +120 km/s ⇒ RV ≈ 120, peak_strength ∈ ]0, 1], error > 0."""
53
+ tmpl = _template()
54
+ obs = _doppler_shifted(tmpl, +120.0)
55
+ ctx = WorkContext(spectrum=obs)
56
+ ctx.extras["template_spectrum"] = tmpl
57
+ out = run_algorithm("cross_correlate_rv", ctx, {"n_grid": 4096})
58
+ assert out.success
59
+ assert abs(ctx.metrics["radial_velocity_kms"] - 120.0) < 2.0
60
+ ps = ctx.metrics["peak_strength"]
61
+ assert 0.0 < ps <= 1.0
62
+ assert ctx.metrics["radial_velocity_error_kms"] > 0.0
63
+
64
+
65
+ def test_ccf_artefact_is_exposed_in_extras():
66
+ """extras['ccf'] = {lags_kms, ccf} for downstream plotting."""
67
+ tmpl = _template()
68
+ ctx = WorkContext(spectrum=_doppler_shifted(tmpl, 50.0))
69
+ ctx.extras["template_spectrum"] = tmpl
70
+ out = run_algorithm("cross_correlate_rv", ctx, {"n_grid": 2048})
71
+ assert out.success
72
+ ccf = ctx.extras["ccf"]
73
+ assert set(ccf.keys()) == {"lags_kms", "ccf"}
74
+ assert len(ccf["lags_kms"]) == len(ccf["ccf"])
75
+ assert len(ccf["lags_kms"]) > 10
76
+ # The CCF peak in extras should align with the reported RV (within
77
+ # one velocity-bin given the parabolic refinement).
78
+ lags = np.asarray(ccf["lags_kms"])
79
+ values = np.asarray(ccf["ccf"])
80
+ v_peak_bin = float(lags[int(np.argmax(values))])
81
+ assert abs(v_peak_bin - ctx.metrics["radial_velocity_kms"]) < (lags[1] - lags[0]) + 1e-9
82
+
83
+
84
+ def test_template_id_is_extracted_from_template_key():
85
+ """When the user passes template_key, extras['template_id'] echoes it."""
86
+ tmpl = _template()
87
+ ctx = WorkContext(spectrum=_doppler_shifted(tmpl, 0.0))
88
+ ctx.extras["my_template"] = tmpl
89
+ out = run_algorithm(
90
+ "cross_correlate_rv", ctx,
91
+ {"template_key": "my_template", "n_grid": 2048},
92
+ )
93
+ assert out.success
94
+ assert ctx.extras["template_id"] == "my_template"
95
+
96
+
97
+ def test_failure_on_no_template_keeps_pre_v1_2_0_contract():
98
+ spec = _template()
99
+ ctx = WorkContext(spectrum=spec)
100
+ out = run_algorithm("cross_correlate_rv", ctx, raise_on_error=False)
101
+ assert not out.success
102
+ assert "template" in (out.error or "").lower()
103
+
104
+
105
+ def test_failure_on_zero_wavelength_overlap_is_clear():
106
+ tmpl = Spectrum1D(
107
+ wavelength=np.linspace(5000.0, 5500.0, 1000),
108
+ flux=np.ones(1000),
109
+ )
110
+ obs = Spectrum1D(
111
+ wavelength=np.linspace(6000.0, 6500.0, 1000),
112
+ flux=np.ones(1000),
113
+ )
114
+ ctx = WorkContext(spectrum=obs)
115
+ ctx.extras["template_spectrum"] = tmpl
116
+ out = run_algorithm("cross_correlate_rv", ctx, raise_on_error=False)
117
+ assert not out.success
118
+ assert "overlap" in (out.error or "").lower()
@@ -1,151 +0,0 @@
1
- """Algorithm: cross-correlate against a template to measure radial velocity."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import Any
6
-
7
- import astropy.constants as const
8
- import astropy.units as u
9
- import numpy as np
10
- from scipy.signal import correlate
11
-
12
- from ...base import AlgorithmOutput, BaseAlgorithm
13
- from ...io import read_fits
14
- from ...registry import register_algorithm
15
- from ...types import AlgorithmCategory, Spectrum1D, WorkContext
16
-
17
- _C_KMS = const.c.to(u.km / u.s).value
18
-
19
-
20
- @register_algorithm(
21
- "cross_correlate_rv",
22
- category=AlgorithmCategory.RADIAL_VELOCITY,
23
- version="1.1.0", # 1.1.0: strip NaN samples before np.interp (no NaN propagation)
24
- )
25
- class CrossCorrelateRv(BaseAlgorithm):
26
- """Measure radial velocity by cross-correlation against a template spectrum.
27
-
28
- Both spectra are resampled to a common log-wavelength grid (so a shift in samples
29
- maps to a constant velocity); their cross-correlation function (CCF) is computed;
30
- the peak is located with sub-pixel parabolic refinement; the shift converts to
31
- velocity via ``v = c * dln(lambda)``.
32
- """
33
-
34
- backend = "scipy"
35
- references = [
36
- "Tonry & Davis 1979, AJ 84, 1511 — fundamental CCF method for RV",
37
- "scipy.signal.correlate",
38
- ]
39
- long_description = (
40
- "Provide the template either as a file (`template_path`), a key into "
41
- "ctx.extras (`template_key`), or fall back to `ctx.spectra[0]`. The reported "
42
- "velocity is positive when the science spectrum is redshifted vs. the template."
43
- )
44
- default_params = {
45
- "template_path": None,
46
- "template_key": None,
47
- "n_grid": 4096,
48
- "vmin_kms": -800.0,
49
- "vmax_kms": 800.0,
50
- }
51
- param_descriptions = {
52
- "template_path": "Path / URL of a template FITS spectrum to load.",
53
- "template_key": "Key in ctx.extras where a Spectrum1D template is stored.",
54
- "n_grid": "Number of log-wavelength samples on which to interpolate.",
55
- "vmin_kms": "Lower bound of the velocity search range (km/s).",
56
- "vmax_kms": "Upper bound of the velocity search range (km/s).",
57
- }
58
- input_requirements = ["spectrum"]
59
- output_produces = ["metrics.radial_velocity_kms"]
60
-
61
- def run(self, ctx: WorkContext, params: dict[str, Any]) -> AlgorithmOutput:
62
- spec = ctx.spectrum
63
- template = self._resolve_template(ctx, params)
64
- if template is None:
65
- return AlgorithmOutput.fail(
66
- "No template available — pass template_path, set ctx.extras['template_spectrum'], "
67
- "or put one in ctx.spectra[0]."
68
- )
69
-
70
- wmin = max(spec.wavelength_min, template.wavelength_min)
71
- wmax = min(spec.wavelength_max, template.wavelength_max)
72
- if wmin >= wmax:
73
- return AlgorithmOutput.fail("Spectrum and template do not overlap in wavelength.")
74
-
75
- n = int(params["n_grid"])
76
- log_grid = np.linspace(np.log(wmin), np.log(wmax), n)
77
- delta_log = (log_grid[-1] - log_grid[0]) / (n - 1)
78
- wave_grid = np.exp(log_grid)
79
-
80
- # np.interp does NOT skip NaN samples — a single bad pixel in the
81
- # input poisons the whole row. Strip NaNs before interpolating.
82
- spec_f = _interp_skip_nan(wave_grid, spec.wavelength, spec.flux)
83
- templ_f = _interp_skip_nan(wave_grid, template.wavelength, template.flux)
84
- # Centre both signals so the CCF is comparable across spectra.
85
- spec_f = spec_f - float(np.nanmean(spec_f))
86
- templ_f = templ_f - float(np.nanmean(templ_f))
87
-
88
- ccf = correlate(spec_f, templ_f, mode="full")
89
- lags = np.arange(-(n - 1), n, dtype=np.float64)
90
- velocities = lags * delta_log * _C_KMS
91
-
92
- vmin = float(params["vmin_kms"])
93
- vmax = float(params["vmax_kms"])
94
- window = (velocities >= vmin) & (velocities <= vmax)
95
- if not window.any():
96
- return AlgorithmOutput.fail("Velocity search range too narrow given n_grid.")
97
- ccf_w = ccf[window]
98
- v_w = velocities[window]
99
-
100
- ipeak = int(np.argmax(ccf_w))
101
- v_peak = float(v_w[ipeak])
102
- # Parabolic vertex refinement (sub-velocity-bin precision).
103
- if 0 < ipeak < ccf_w.size - 1:
104
- y0, y1, y2 = ccf_w[ipeak - 1], ccf_w[ipeak], ccf_w[ipeak + 1]
105
- denom = y0 - 2.0 * y1 + y2
106
- if denom != 0.0:
107
- offset = 0.5 * (y0 - y2) / denom
108
- v_peak += float(offset) * float(v_w[1] - v_w[0])
109
-
110
- ctx.metrics["radial_velocity_kms"] = v_peak
111
- return AlgorithmOutput.ok(
112
- metrics={"radial_velocity_kms": v_peak, "ccf_peak": float(ccf_w[ipeak])},
113
- artifacts={
114
- "velocity_kms": v_w.tolist(),
115
- "ccf": ccf_w.tolist(),
116
- },
117
- message=f"Cross-correlation RV = {v_peak:+.2f} km/s",
118
- )
119
-
120
- @staticmethod
121
- def _resolve_template(ctx: WorkContext, params: dict[str, Any]) -> Spectrum1D | None:
122
- if params["template_path"]:
123
- return read_fits(params["template_path"])
124
- if params["template_key"]:
125
- value = ctx.extras.get(params["template_key"])
126
- if isinstance(value, Spectrum1D):
127
- return value
128
- value = ctx.extras.get("template_spectrum")
129
- if isinstance(value, Spectrum1D):
130
- return value
131
- if ctx.spectra:
132
- return ctx.spectra[0]
133
- return None
134
-
135
-
136
- def _interp_skip_nan(x_new: np.ndarray, xp: np.ndarray, fp: np.ndarray) -> np.ndarray:
137
- """Linear interp that ignores NaN samples in ``fp``.
138
-
139
- ``np.interp`` happily extrapolates a NaN through every sample whose
140
- neighbouring source point is NaN; for cross-correlation that means a
141
- single bad pixel destroys the CCF. Stripping NaNs first preserves the
142
- rest of the signal at the cost of a slightly biased local interpolation.
143
- """
144
- finite = np.isfinite(fp)
145
- if finite.all():
146
- return np.interp(x_new, xp, fp)
147
- if not finite.any():
148
- return np.zeros_like(x_new)
149
- return np.interp(x_new, xp[finite], fp[finite])
150
-
151
-
File without changes
File without changes