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