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.
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/CHANGELOG.md +36 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/PKG-INFO +1 -1
- spectro_kernel-0.4.1/src/spectro_kernel/algorithms/rv/cross_correlate.py +350 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/version.py +1 -1
- spectro_kernel-0.4.1/tests/unit/test_cross_correlate_extension.py +118 -0
- spectro_kernel-0.4.0/src/spectro_kernel/algorithms/rv/cross_correlate.py +0 -151
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/.gitignore +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/LICENSE +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/README.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/concepts/algorithms.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/concepts/architecture.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/concepts/data-types.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/concepts/pipelines.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/contributing.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/cookbook/bess-dashboard.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/cookbook/multi-star-viewer.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/cookbook/web-playground.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/gen_catalogue.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/getting-started.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_ew_timeseries.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_halpha_phase_stack.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_halpha_timeseries.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_overlay.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_pc1_vs_phase.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_pca_2d.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_pca_3d.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_periodogram.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_phase_folded.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_rv_timeseries.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_similarity.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_cyg_similarity_date.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_halpha_phase_stack.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_pc1_vs_phase.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_pca_2d.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_pca_3d.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_periodogram.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_phase_folded.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_rv_timeseries.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_similarity_date.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/images/notebooks/alpha_dra_similarity_phase.png +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/index.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/alpha-cyg-time-series.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/alpha-dra-binary-period.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/aurora-line-monitor.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/be-star-variability.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/claude-mcp-end-to-end.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/exoplanet-transit-rv.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/full-reduction-walkthrough.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/native-vs-easyspec-showdown.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/notebooks/your-first-sb2.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/reference.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/tutorials/add-an-algorithm.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/tutorials/analyse-a-spectrum.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/tutorials/discover-the-catalogue.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/tutorials/first-spectrum.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/tutorials/long-slit-reduction.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/usage/cli.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/usage/library.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/usage/mcp.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/docs/why.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/playground/README.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/pyproject.toml +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/adapters/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/adapters/easyspec.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/_common.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/advanced/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/advanced/aperture_photometry.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/advanced/disentangle_sb2.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/advanced/doppler_tomogram.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/catalogs/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/catalogs/gaia.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/catalogs/simbad.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/catalogs/vizier.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/classification/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/classification/classify_template_chi2.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/classification/pickles_atlas.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/continuum/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/continuum/compare_normalisations.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/continuum/normalize_edges.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/continuum/normalize_max.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/continuum/normalize_percentile.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/continuum/normalize_polynomial.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/continuum/normalize_region.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/continuum/subtract_continuum.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/air_vacuum.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/barycentric.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/doppler_shift.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/extinction_correct_easyspec.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/fit_telluric_scaling.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/flux_calibrate_easyspec.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/remove_telluric.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/response_from_standard.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/corrections/synth_telluric.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/embed_band_power.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/embed_continuum_subtracted.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/embed_lick_indices.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/embed_log_lambda.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/embed_pretrained.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/embed_remote.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/embed_spectrum.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/embedding/embed_wavelets.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/exports/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/exports/export_csv.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/exports/export_fits.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/exports/export_fits_bess.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/exports/export_hdf5.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/exports/export_votable.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/extraction/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/extraction/boxcar.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/extraction/detect_trace.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/extraction/easyspec_extract.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/extraction/optimal.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/extraction/sky_lateral_bands.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/io/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/io/read_ascii.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/io/read_echelle.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/io/read_fits.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/io/read_sdss.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/io/read_votable.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/kinematics/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/kinematics/rotation_curve.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/_line_flux.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/_profiles.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/catalogs.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/compare_line_fits.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/detect.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/equivalent_width.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/fit_gaussian.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/fit_lorentzian.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/fit_voigt.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/lines/vr_ratio.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/nebular/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/nebular/bpt_line_ratios.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/nebular/oiii_electron_temperature.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/nebular/sii_electron_density.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/quality/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/quality/compare_snr_methods.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/quality/snr_der.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/quality/snr_edge.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/quality/snr_linear_fit.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/_easyspec_apply.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/_easyspec_helpers.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/bias_combine.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/clip_cosmic_rays.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/dark_combine.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/dark_subtract.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/denoise_2d.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/easyspec_bias.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/easyspec_cosmic_ray.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/easyspec_dark.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/easyspec_flat.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/easyspec_flat_normalize.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/easyspec_subtract_bias.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/easyspec_subtract_dark.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/extract_spectrum_sum.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/flat_combine.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/flat_normalize.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/geometry.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/subtract_sky_2d.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/reduction/wavelength_calibrate.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/rv/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/rv/fit_keplerian_orbit.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/rv/measure.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/rv/precision_bouchy.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/rv/redshift_lines.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/smoothing/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/smoothing/compare_smoothings.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/smoothing/smooth_gaussian.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/smoothing/smooth_savgol.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/stacking/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/stacking/merge_echelle_orders.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/stacking/stack.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/timeseries/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/timeseries/lomb_scargle.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/timeseries/phase_fold.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/transforms/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/transforms/clip_sigma.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/transforms/combine_arithmetic.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/transforms/extract_region.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/transforms/mask_range.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/transforms/resample.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/transforms/resample_flux_conserving.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/viz/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/viz/plot_3d_surface.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/viz/plot_animation.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/viz/plot_dynamic_spectrum.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/viz/plot_plotly.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/wavelength_calibration/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/wavelength_calibration/arc_geometry.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/wavelength_calibration/easyspec_wavelength.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/wavelength_calibration/in_situ.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/wavelength_calibration/lamp_atlas.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/wavelength_calibration/match_lamp.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/algorithms/wavelength_calibration/solar.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/base.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/cli.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/embeddings.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/errors.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/io/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/io/ascii.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/io/fits.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/io/votable.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/pipeline.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/catalog/analysis/balmer_quick.yaml +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/catalog/analysis/embed_quick.yaml +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/catalog/analysis/quality_report.yaml +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/catalog/analysis/rv_quick.yaml +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/catalog/analysis/snr_check.yaml +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/catalog/analysis/time_series_overview.yaml +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/catalog/reduction/full_reduction_easyspec.yaml +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/presets/loader.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/py.typed +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/registry.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/catalog.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/context.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/enums.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/history.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/image.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/line.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/spectrum.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_kernel/types/timeseries.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/__init__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/__main__.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/auth.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/auto_tools.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/observability.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/py.typed +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/server.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/session.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/src/spectro_mcp/url_safety.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/conftest.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/reference/conftest.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/reference/data/.gitkeep +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/reference/data/README.md +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/reference/data/sun/sun_reference_stis_002.fits +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/reference/data/vega/alpha_lyr_stis_011.fits +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/reference/test_known_answers.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/reference/test_sun.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/reference/test_vega.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_base.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_classify_template_chi2.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_cli.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_combine.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_combine_arithmetic.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_continuum.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_corrections.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_dark_flat_combine.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_denoise_2d.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_detect_lines_blind.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_detect_trace.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_doppler_tomogram.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_easyspec_apply_staging.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_easyspec_wrappers.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_embedding.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_embedding_pretrained.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_embedding_remote_lick.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_embedding_tier1.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_export_fits_bess.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_extract_boxcar.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_extract_optimal.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_geometry_corrections.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_io.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_lamp_atlas_match.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_line_profiles.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_lines.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_mcp.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_mcp_production.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_mcp_security.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_measure_arc_geometry.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_misc_algorithms.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_nebular_diagnostics.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_no_circular_imports.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_normalize_to_region.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_pipeline.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_quality.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_read_sdss.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_redshift_lines.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_registry.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_response_from_standard.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_rotation_curve.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_sky_lateral_bands.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_smoothing.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_timeseries.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_transforms.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_types.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_viz.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_vr_ratio.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_wavelength_calibration_in_situ.py +0 -0
- {spectro_kernel-0.4.0 → spectro_kernel-0.4.1}/tests/unit/test_wavelength_calibration_solar.py +0 -0
- {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.
|
|
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])
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|