spectro-kernel 0.1.2__tar.gz → 0.1.3__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.1.2 → spectro_kernel-0.1.3}/CHANGELOG.md +36 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/PKG-INFO +1 -1
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_ew_timeseries.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_halpha_phase_stack.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_halpha_timeseries.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_overlay.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_pc1_vs_phase.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_pca_2d.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_pca_3d.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_periodogram.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_phase_folded.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_rv_timeseries.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_similarity.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_cyg_similarity_date.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_halpha_phase_stack.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_pc1_vs_phase.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_pca_2d.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_pca_3d.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_periodogram.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_phase_folded.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_rv_timeseries.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_similarity_date.png +0 -0
- spectro_kernel-0.1.3/docs/images/notebooks/alpha_dra_similarity_phase.png +0 -0
- spectro_kernel-0.1.3/docs/notebooks/alpha-cyg-time-series.md +155 -0
- spectro_kernel-0.1.3/docs/notebooks/alpha-dra-binary-period.md +210 -0
- spectro_kernel-0.1.3/src/spectro_kernel/algorithms/__init__.py +17 -0
- spectro_kernel-0.1.3/src/spectro_kernel/algorithms/io/read_sdss.py +121 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/registry.py +25 -4
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/version.py +1 -1
- spectro_kernel-0.1.3/tests/unit/test_no_circular_imports.py +89 -0
- spectro_kernel-0.1.3/tests/unit/test_read_sdss.py +76 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_registry.py +1 -0
- spectro_kernel-0.1.2/src/spectro_kernel/algorithms/__init__.py +0 -20
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/.gitignore +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/LICENSE +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/README.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/concepts/algorithms.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/concepts/architecture.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/concepts/data-types.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/concepts/pipelines.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/contributing.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/cookbook/bess-dashboard.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/cookbook/multi-star-viewer.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/cookbook/web-playground.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/gen_catalogue.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/getting-started.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/index.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/notebooks/aurora-line-monitor.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/notebooks/be-star-variability.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/notebooks/claude-mcp-end-to-end.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/notebooks/exoplanet-transit-rv.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/notebooks/full-reduction-walkthrough.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/notebooks/native-vs-easyspec-showdown.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/notebooks/your-first-sb2.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/reference.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/tutorials/add-an-algorithm.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/tutorials/analyse-a-spectrum.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/tutorials/discover-the-catalogue.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/tutorials/first-spectrum.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/usage/cli.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/usage/library.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/usage/mcp.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/docs/why.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/playground/README.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/pyproject.toml +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/adapters/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/adapters/easyspec.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/_common.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/advanced/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/advanced/aperture_photometry.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/advanced/disentangle_sb2.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/catalogs/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/catalogs/gaia.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/catalogs/simbad.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/catalogs/vizier.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/continuum/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/continuum/compare_normalisations.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/continuum/normalize_edges.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/continuum/normalize_max.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/continuum/normalize_percentile.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/continuum/normalize_polynomial.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/continuum/subtract_continuum.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/air_vacuum.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/barycentric.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/doppler_shift.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/extinction_correct_easyspec.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/fit_telluric_scaling.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/flux_calibrate_easyspec.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/remove_telluric.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/corrections/synth_telluric.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/embed_band_power.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/embed_continuum_subtracted.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/embed_lick_indices.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/embed_log_lambda.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/embed_pretrained.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/embed_remote.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/embed_spectrum.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/embedding/embed_wavelets.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/exports/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/exports/export_csv.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/exports/export_fits.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/exports/export_hdf5.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/exports/export_votable.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/extraction/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/extraction/easyspec_extract.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/io/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/io/read_ascii.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/io/read_echelle.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/io/read_fits.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/io/read_votable.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/_profiles.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/catalogs.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/compare_line_fits.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/detect.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/equivalent_width.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/fit_gaussian.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/fit_lorentzian.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/lines/fit_voigt.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/quality/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/quality/compare_snr_methods.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/quality/snr_der.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/quality/snr_edge.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/quality/snr_linear_fit.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/_easyspec_apply.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/_easyspec_helpers.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/bias_combine.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/clip_cosmic_rays.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/dark_subtract.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/easyspec_bias.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/easyspec_cosmic_ray.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/easyspec_dark.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/easyspec_flat.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/easyspec_flat_normalize.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/easyspec_subtract_bias.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/easyspec_subtract_dark.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/extract_spectrum_sum.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/flat_normalize.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/subtract_sky_2d.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/reduction/wavelength_calibrate.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/rv/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/rv/cross_correlate.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/rv/fit_keplerian_orbit.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/rv/measure.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/rv/precision_bouchy.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/smoothing/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/smoothing/compare_smoothings.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/smoothing/smooth_gaussian.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/smoothing/smooth_savgol.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/stacking/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/stacking/merge_echelle_orders.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/stacking/stack.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/timeseries/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/timeseries/lomb_scargle.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/timeseries/phase_fold.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/transforms/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/transforms/clip_sigma.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/transforms/extract_region.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/transforms/mask_range.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/transforms/resample.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/transforms/resample_flux_conserving.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/viz/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/viz/plot_3d_surface.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/viz/plot_animation.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/viz/plot_dynamic_spectrum.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/viz/plot_plotly.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/wavelength_calibration/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/algorithms/wavelength_calibration/easyspec_wavelength.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/base.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/cli.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/embeddings.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/errors.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/io/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/io/ascii.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/io/fits.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/io/votable.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/pipeline.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/catalog/analysis/balmer_quick.yaml +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/catalog/analysis/embed_quick.yaml +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/catalog/analysis/quality_report.yaml +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/catalog/analysis/rv_quick.yaml +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/catalog/analysis/snr_check.yaml +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/catalog/analysis/time_series_overview.yaml +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/catalog/reduction/full_reduction_easyspec.yaml +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/presets/loader.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/py.typed +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/catalog.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/context.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/enums.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/history.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/image.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/line.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/spectrum.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_kernel/types/timeseries.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_mcp/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_mcp/__main__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_mcp/auth.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_mcp/auto_tools.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_mcp/observability.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_mcp/py.typed +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_mcp/server.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/src/spectro_mcp/session.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/conftest.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/reference/conftest.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/reference/data/.gitkeep +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/reference/data/README.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/reference/data/sun/sun_reference_stis_002.fits +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/reference/data/vega/alpha_lyr_stis_011.fits +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/reference/test_known_answers.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/reference/test_sun.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/reference/test_vega.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_base.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_cli.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_combine.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_continuum.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_corrections.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_easyspec_wrappers.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_embedding.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_embedding_pretrained.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_embedding_remote_lick.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_embedding_tier1.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_io.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_line_profiles.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_lines.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_mcp.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_mcp_production.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_misc_algorithms.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_pipeline.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_quality.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_smoothing.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_timeseries.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_transforms.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_types.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/tests/unit/test_viz.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.3}/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.1.3] — 2026-06-09
|
|
10
|
+
|
|
11
|
+
### Fixed — circular import that prevented `from spectro_kernel.embeddings import …`
|
|
12
|
+
|
|
13
|
+
In 0.1.2, `spectro_kernel.algorithms.__init__` ran an eager submodule
|
|
14
|
+
walk at import time. That walk imported every algorithm, including
|
|
15
|
+
`embed_continuum_subtracted`, which itself did a top-level
|
|
16
|
+
`from ...embeddings import VALID_NORM_METHODS, VALID_STRATEGIES,
|
|
17
|
+
embed_flux`. But `embeddings` itself imports
|
|
18
|
+
`spectro_kernel.algorithms._common` before defining `VALID_*` /
|
|
19
|
+
`embed_flux` — so when an external consumer wrote
|
|
20
|
+
`from spectro_kernel.embeddings import embed_flux` as its very first
|
|
21
|
+
contact with the kernel, Python's import machinery saw a partially
|
|
22
|
+
initialised module and raised `ImportError`.
|
|
23
|
+
|
|
24
|
+
Reported by a downstream library whose service worker crashed
|
|
25
|
+
deterministically on the same PyPI 0.1.2 wheel.
|
|
26
|
+
|
|
27
|
+
The fix moves discovery into `registry.ensure_discovered()`, which was
|
|
28
|
+
already the lazy entry point — every registry getter (`list_algorithms`,
|
|
29
|
+
`get_algorithm`, `run_algorithm`, …) calls it before answering.
|
|
30
|
+
Importing `spectro_kernel.algorithms` is now a true no-op, so pulling a
|
|
31
|
+
helper out of `algorithms._common` from inside `embeddings` no longer
|
|
32
|
+
triggers a transitive cycle. Discovery now silently skips any
|
|
33
|
+
optional-extra algorithm whose dependency is missing, instead of
|
|
34
|
+
breaking the entire import chain.
|
|
35
|
+
|
|
36
|
+
Behaviour preservation: existing callers that go through the registry
|
|
37
|
+
see the same catalogue at the same time (the first registry call
|
|
38
|
+
populates it, exactly as before). The only change is **when** discovery
|
|
39
|
+
runs — it's properly lazy now.
|
|
40
|
+
|
|
41
|
+
Added 4 regression tests in `test_no_circular_imports.py` that run the
|
|
42
|
+
exact cold-start imports the bug reporter hit, in fresh subprocesses,
|
|
43
|
+
so a future re-introduction of the cycle is caught immediately by CI.
|
|
44
|
+
|
|
9
45
|
## [0.1.2] — 2026-06-05
|
|
10
46
|
|
|
11
47
|
### Added — embedding category (8 algorithms total)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spectro-kernel
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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/
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# α Cyg — 124 amateur spectra of a pulsating supergiant
|
|
2
|
+
|
|
3
|
+
**A second real-world walkthrough of `spectro-kernel`, complementary to the
|
|
4
|
+
[α Dra binary case](alpha-dra-binary-period.md).** Deneb (α Cyg, A2 Ia) is
|
|
5
|
+
not a binary — it's the **prototype of the α-Cygni variables**: a
|
|
6
|
+
non-radial pulsating supergiant whose H-α profile is also shaped by an
|
|
7
|
+
ionised stellar wind. Where α Dra gave a single clean orbital period, α
|
|
8
|
+
Cyg exhibits low-amplitude, **multi-periodic, sometimes irregular**
|
|
9
|
+
variability on timescales of weeks. This notebook applies the same kernel
|
|
10
|
+
pipeline as the α Dra case and **honestly reports** what amateur data of a
|
|
11
|
+
genuinely harder target can and can't deliver.
|
|
12
|
+
|
|
13
|
+
The complete script is at
|
|
14
|
+
[`playground/analyse_alpha_cyg.py`](https://github.com/matthieulel/spectro-kernel/blob/main/playground/analyse_alpha_cyg.py).
|
|
15
|
+
|
|
16
|
+
## Dataset provenance
|
|
17
|
+
|
|
18
|
+
The 124 FITS files come from an amateur monitoring campaign of Deneb run
|
|
19
|
+
between **2020 and 2024** (~12 contributing observers, slit spectrographs
|
|
20
|
+
typically R ≈ 5 000–20 000 centred on H-α). The data is **not bundled
|
|
21
|
+
with this repository** — drop your own FITS into
|
|
22
|
+
`playground/datas/alphacygni/` and the script picks them up automatically.
|
|
23
|
+
|
|
24
|
+
> If you contributed spectra to or reuse spectra from a public archive,
|
|
25
|
+
> please cite the relevant campaign. Personal observer attributions have
|
|
26
|
+
> been omitted from this notebook — the kernel showcases the analysis
|
|
27
|
+
> pipeline, not the source of any specific measurement.
|
|
28
|
+
|
|
29
|
+
## The pipeline
|
|
30
|
+
|
|
31
|
+
Identical to the α Dra one — same algorithms, same defaults:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from spectro_kernel import WorkContext, run_algorithm
|
|
35
|
+
from spectro_kernel.io import read_fits
|
|
36
|
+
|
|
37
|
+
ctx = WorkContext(spectrum=read_fits(path))
|
|
38
|
+
run_algorithm("snr_der", ctx)
|
|
39
|
+
run_algorithm("normalize_polynomial", ctx, {"order": 3})
|
|
40
|
+
run_algorithm("fit_gaussian_line", ctx,
|
|
41
|
+
{"line_center_angstrom": 6562.81, "window_angstrom": 30.0})
|
|
42
|
+
# Embedding restricted to H-α ± 50 Å for similarity comparison
|
|
43
|
+
region = ctx.copy()
|
|
44
|
+
run_algorithm("extract_region", region,
|
|
45
|
+
{"wavelength_min": 6512.81, "wavelength_max": 6612.81})
|
|
46
|
+
run_algorithm("embed_spectrum", region, {"dim": 128, "strategy": "dct"})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
124/124 spectra pass the basic SNR/RV sanity filter — Deneb is bright
|
|
50
|
+
enough that even modest amateur setups give usable spectra.
|
|
51
|
+
|
|
52
|
+
## Result 1 — RV vs time
|
|
53
|
+
|
|
54
|
+
Centroid of the H-α Gaussian fit, converted to radial velocity (km/s),
|
|
55
|
+
median-subtracted:
|
|
56
|
+
|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
The series shows **two main observing campaigns** (a dense 2023 cluster
|
|
60
|
+
around MJD 60 100–60 250, and a 2024 cluster around MJD 60 400–60 550)
|
|
61
|
+
plus a few isolated 2020 / 2023-summer points. Within each campaign, RV
|
|
62
|
+
scatter is **~±20 km/s**, consistent with α Cyg-variable pulsation
|
|
63
|
+
amplitudes documented in the literature. Between campaigns, there is a
|
|
64
|
+
**systematic offset** (the 2024 mean RV sits ~15 km/s above the 2023 mean)
|
|
65
|
+
that could be a real long-term modulation OR an instrument-mix effect
|
|
66
|
+
between observers — the kernel can't tell those apart.
|
|
67
|
+
|
|
68
|
+
## Result 2 — Periodogram (and an honest caveat)
|
|
69
|
+
|
|
70
|
+
Lomb-Scargle on the (time, RV) pairs:
|
|
71
|
+
|
|
72
|
+

|
|
73
|
+
|
|
74
|
+
The dominant peak sits at **~553 days**, but **this is largely a sampling
|
|
75
|
+
artefact** rather than a stellar period: it corresponds to the spacing
|
|
76
|
+
between the two observing campaigns. The genuine α Cyg-variable
|
|
77
|
+
pulsations have published timescales of ~11–100 days; the periodogram
|
|
78
|
+
shows enhanced power in that range too (the bumpy plateau between 30 and
|
|
79
|
+
100 days) but no single sharp peak — exactly what's expected from a
|
|
80
|
+
multi-periodic, semi-coherent pulsator observed at heterogeneous cadence.
|
|
81
|
+
|
|
82
|
+
> **Take-away.** A periodogram on amateur data is a *measurement*, not a
|
|
83
|
+
> *truth*. The kernel reports what's in the data without lying; turning
|
|
84
|
+
> that into astrophysics requires a critical eye on the sampling. For α
|
|
85
|
+
> Dra (single clean binary period, ~4 cycles densely sampled) the
|
|
86
|
+
> periodogram nails it. For α Cyg (multi-periodic, unevenly sampled), the
|
|
87
|
+
> answer is "yes there's variability on 10–100 day scales, no single
|
|
88
|
+
> period dominates".
|
|
89
|
+
|
|
90
|
+
## Result 3 — H-α profile gallery
|
|
91
|
+
|
|
92
|
+
The H-α region of every spectrum, overlaid:
|
|
93
|
+
|
|
94
|
+

|
|
95
|
+
|
|
96
|
+
Most spectra share the same characteristic A-supergiant H-α absorption
|
|
97
|
+
profile, with the small profile variations expected for α Cyg-variables.
|
|
98
|
+
A handful of low-SNR observations and a few obvious mis-calibrations stand
|
|
99
|
+
out — visible without any per-file labelling, just from the spread of the
|
|
100
|
+
flux trace.
|
|
101
|
+
|
|
102
|
+
## Result 4 — Embedding latent space
|
|
103
|
+
|
|
104
|
+
Each H-α region is embedded to a 128-d vector via `embed_spectrum`
|
|
105
|
+
(`strategy="dct"`); the 124 vectors are then PCA-projected to 2D, with
|
|
106
|
+
each point coloured by its phase at the periodogram's best period:
|
|
107
|
+
|
|
108
|
+

|
|
109
|
+
|
|
110
|
+
There **is** a colour gradient (early phases at right, mid phases at
|
|
111
|
+
centre, late phases scattered at top) but it's far less clean than the α
|
|
112
|
+
Dra version — because the underlying period isn't really 553 days, it's a
|
|
113
|
+
mix of shorter-scale pulsations and a sampling artefact. **The
|
|
114
|
+
embedding's latent axes still capture the most variable directions of the
|
|
115
|
+
dataset** — but with α Cyg those don't align with one single phase like
|
|
116
|
+
they did with α Dra's clean orbit.
|
|
117
|
+
|
|
118
|
+
## What you actually learn from this dataset
|
|
119
|
+
|
|
120
|
+
- **Sub-day RV scatter of ~20 km/s** within each observing campaign,
|
|
121
|
+
consistent with low-amplitude pulsations of an A Ia supergiant.
|
|
122
|
+
- **A long-term offset between 2023 and 2024 campaigns** that requires
|
|
123
|
+
inter-instrument cross-calibration to interpret astrophysically.
|
|
124
|
+
- **No single dominant pulsation period** — α Cyg-variables are known to
|
|
125
|
+
be multi-periodic / quasi-periodic, and a 4-year amateur monitoring is
|
|
126
|
+
consistent with that.
|
|
127
|
+
- **The kernel pipeline reports all of the above without any
|
|
128
|
+
Deneb-specific tuning** — same six algorithm calls as the α Dra binary
|
|
129
|
+
notebook, applied to 124 files instead of 240.
|
|
130
|
+
|
|
131
|
+
## Reproducing this
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
cd playground
|
|
135
|
+
python3.12 -m venv venv && source venv/bin/activate
|
|
136
|
+
pip install -e "..[viz]" plotly kaleido
|
|
137
|
+
# drop your own alphacyg_*.fits files in playground/datas/alphacygni/
|
|
138
|
+
python analyse_alpha_cyg.py
|
|
139
|
+
# → output/alpha_cyg_*.png + alpha_cyg_summary.csv
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Takeaway compared to α Dra
|
|
143
|
+
|
|
144
|
+
| | α Dra (binary) | α Cyg (supergiant pulsator) |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| Underlying physics | single Keplerian orbit | multi-periodic pulsations + wind |
|
|
147
|
+
| Expected RV signal | clean ~120 km/s sinusoid at 51 d | semi-coherent ~±20 km/s on 10–100 d |
|
|
148
|
+
| Periodogram outcome | single sharp peak at 51.6 d | broad plateau + sampling-driven peak |
|
|
149
|
+
| Phase-folded curve | clean asymmetric loop (eccentric orbit) | mostly noise at any candidate period |
|
|
150
|
+
| Embedding latent space | PCA axis = orbital phase | PCA axis = mixed variability + outliers |
|
|
151
|
+
|
|
152
|
+
These are the **right** behaviours given the underlying physics — the
|
|
153
|
+
kernel doesn't try to force a periodicity on a star that doesn't have a
|
|
154
|
+
clean one. The same pipeline tells two different stories from two
|
|
155
|
+
different datasets, faithfully.
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# α Dra — recovering a binary period from 240 amateur spectra
|
|
2
|
+
|
|
3
|
+
**A second real-world walkthrough of `spectro-kernel`, this time on a known
|
|
4
|
+
spectroscopic binary.** Alpha Draconis (Thuban) has a published orbital
|
|
5
|
+
period of **51.42 days** and a non-negligible eccentricity (e ≈ 0.42; see
|
|
6
|
+
Bischoff et al. 2017, A&A). The dataset below — 240 H-α spectra collected
|
|
7
|
+
by amateur observers between May 2022 and August 2023 — is enough to
|
|
8
|
+
**independently recover the period to within 0.3 %** and **reveal the
|
|
9
|
+
eccentric character of the orbit**.
|
|
10
|
+
|
|
11
|
+
The full script is at
|
|
12
|
+
[`playground/analyse_alpha_dra.py`](https://github.com/matthieulel/scal-kernel/blob/main/playground/analyse_alpha_dra.py).
|
|
13
|
+
|
|
14
|
+
## Dataset provenance
|
|
15
|
+
|
|
16
|
+
The 240 FITS files come from an amateur monitoring campaign of α Dra
|
|
17
|
+
between **May 2022 and August 2023** (14-month baseline, ~10 contributing
|
|
18
|
+
observers, slit spectrographs typically R ≈ 5 000 – 20 000 centred on
|
|
19
|
+
H-α). The dense 2023 sub-campaign covers ~4 full binary orbits — enough
|
|
20
|
+
for a clean periodogram.
|
|
21
|
+
|
|
22
|
+
The data is **not bundled with this repository** — drop your own
|
|
23
|
+
`alphadra_*.fits` files into `playground/datas/alphadra/` and the script
|
|
24
|
+
picks them up automatically.
|
|
25
|
+
|
|
26
|
+
> If you contributed spectra to or reuse spectra from a public archive,
|
|
27
|
+
> please cite the relevant campaign. Personal observer attributions have
|
|
28
|
+
> been omitted from this notebook — the kernel showcases the analysis
|
|
29
|
+
> pipeline, not the source of any specific measurement.
|
|
30
|
+
|
|
31
|
+
## The pipeline
|
|
32
|
+
|
|
33
|
+
Identical to the α Cyg one — same algorithms, same defaults — except for
|
|
34
|
+
two adaptations to the binary case:
|
|
35
|
+
|
|
36
|
+
1. The Gaussian-fit window is **40 Å** (vs. 30 Å for α Cyg) because the
|
|
37
|
+
H-α line itself drifts by several Å across the orbit due to the
|
|
38
|
+
primary's Doppler motion.
|
|
39
|
+
2. We record the **line CENTROID** (not just the equivalent width), and
|
|
40
|
+
convert wavelength shift → radial velocity:
|
|
41
|
+
`RV [km/s] = c × (λ_fit − λ_rest) / λ_rest`.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from spectro_kernel import WorkContext, run_algorithm
|
|
45
|
+
from spectro_kernel.io import read_fits
|
|
46
|
+
|
|
47
|
+
ctx = WorkContext(spectrum=read_fits(path))
|
|
48
|
+
run_algorithm("snr_der", ctx)
|
|
49
|
+
run_algorithm("normalize_polynomial", ctx, {"order": 3})
|
|
50
|
+
run_algorithm("fit_gaussian_line", ctx,
|
|
51
|
+
{"line_center_angstrom": 6562.81, "window_angstrom": 40.0})
|
|
52
|
+
fit = next(iter(ctx.line_fits.values()))
|
|
53
|
+
rv_kms = 299792.458 * (fit.line_center_angstrom - 6562.81) / 6562.81
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
That's the per-spectrum extraction. The periodogram and phase fold then
|
|
57
|
+
operate on the **(time, RV)** sample set, agnostic to the kernel — they're
|
|
58
|
+
just `astropy.timeseries.LombScargle` calls.
|
|
59
|
+
|
|
60
|
+
## Result 1 — Radial velocity vs time
|
|
61
|
+
|
|
62
|
+
After fitting all 240 spectra and dropping the bad-SNR / unphysical-RV
|
|
63
|
+
outliers, the median-subtracted RV time series:
|
|
64
|
+
|
|
65
|
+

|
|
66
|
+
|
|
67
|
+
The 2022 epoch is sparse (a handful of points), but the 2023 campaign
|
|
68
|
+
already shows **the binary signal by eye**: ~4 visible peaks corresponding
|
|
69
|
+
to ~4 orbital cycles. Peak-to-peak amplitude ~120 km/s, which sets the
|
|
70
|
+
order of magnitude of the binary motion.
|
|
71
|
+
|
|
72
|
+
## Result 2 — Lomb-Scargle periodogram
|
|
73
|
+
|
|
74
|
+
Run on the unevenly-sampled (time, RV) pairs, no pre-detrending other
|
|
75
|
+
than median subtraction:
|
|
76
|
+
|
|
77
|
+

|
|
78
|
+
|
|
79
|
+
A **single dominant peak at 51.59 days** towers above a noisy background.
|
|
80
|
+
|
|
81
|
+
| Quantity | Our value | Literature (Bischoff+ 2017) | Relative error |
|
|
82
|
+
|---|---|---|---|
|
|
83
|
+
| Orbital period (days) | **51.59** | 51.42 | **0.33 %** |
|
|
84
|
+
|
|
85
|
+
The smaller bump around 25-27 d is a harmonic — expected for an eccentric
|
|
86
|
+
orbit (the asymmetric RV curve has power at the 2nd harmonic of the
|
|
87
|
+
fundamental). The high-frequency noise around 1 day is the sampling
|
|
88
|
+
cadence (1 spectrum per night).
|
|
89
|
+
|
|
90
|
+
## Result 3 — Phase-folded RV curve
|
|
91
|
+
|
|
92
|
+
Folding the 240 RV points at the best period:
|
|
93
|
+
|
|
94
|
+

|
|
95
|
+
|
|
96
|
+
**The folded curve is asymmetric** — not the gentle sinusoid you'd expect
|
|
97
|
+
from a circular orbit. It shows a fast positive peak around phase 0.1
|
|
98
|
+
(reaching ~+50 km/s), then a slow descent through phase 0.3 – 0.8 (around
|
|
99
|
+
−15 km/s), then a slow rise back to zero. This is the canonical signature
|
|
100
|
+
of an **eccentric Keplerian orbit** — the primary spends more time at
|
|
101
|
+
apastron (slow tail) and zips through periastron (sharp peak).
|
|
102
|
+
|
|
103
|
+
The literature value for α Dra is **e ≈ 0.42**, which would produce exactly
|
|
104
|
+
this kind of shape. We didn't fit an explicit Kepler orbit (we could —
|
|
105
|
+
`fit_keplerian_orbit` exists in the catalogue), but the *qualitative*
|
|
106
|
+
asymmetry is unmistakable from the folded raw measurements.
|
|
107
|
+
|
|
108
|
+
## Result 4 — The orbit is visible in the raw spectra
|
|
109
|
+
|
|
110
|
+
Before talking about latent spaces, the most direct evidence the binary
|
|
111
|
+
motion is in the data: bin the 240 spectra by orbital phase, average each
|
|
112
|
+
bin, plot the means side-by-side around H-α.
|
|
113
|
+
|
|
114
|
+

|
|
115
|
+
|
|
116
|
+
**The absorption notch slides left/right of the rest wavelength** (the
|
|
117
|
+
black dotted line at 6562.81 Å) as the orbital phase advances:
|
|
118
|
+
|
|
119
|
+
- Phase 0.5-0.67 (cyan): the line is **blueshifted** — the primary is moving toward us.
|
|
120
|
+
- Phase 0.0-0.17 (red, bottom) and 0.17-0.33 (yellow): the line drifts **back through
|
|
121
|
+
the rest wavelength and into the red** — the primary's RV reverses through
|
|
122
|
+
periastron.
|
|
123
|
+
|
|
124
|
+
You don't need an algorithm to see this; you just need 240 well-calibrated
|
|
125
|
+
amateur spectra and a phase-binning. This is the *raw spectroscopic
|
|
126
|
+
signature* of a spectroscopic binary.
|
|
127
|
+
|
|
128
|
+
## Result 5 — The embedding latent space *is* the orbit
|
|
129
|
+
|
|
130
|
+
Now the algorithmic claim: each of the 240 H-α regions is embedded to a
|
|
131
|
+
128-d vector via `embed_spectrum(strategy="dct")`. **The Doppler shift we
|
|
132
|
+
just saw by eye gets automatically encoded as the principal direction of
|
|
133
|
+
the embedding cloud.** Plot it the simple way — the first principal
|
|
134
|
+
component vs. orbital phase:
|
|
135
|
+
|
|
136
|
+

|
|
137
|
+
|
|
138
|
+
The shape is unmistakable: PC1 follows a clear (asymmetric) cycle in
|
|
139
|
+
phase, with the **same morphology** as the RV phase-fold from Result 3 —
|
|
140
|
+
sharp positive peak at phase 0.1, slow descent to a plateau around
|
|
141
|
+
phase 0.4-0.7, slow rise back. A pure sinusoid here would mean a
|
|
142
|
+
circular orbit; this asymmetric shape echoes the eccentricity again.
|
|
143
|
+
|
|
144
|
+
In other words, **PCA on the embedding cloud rediscovers the radial
|
|
145
|
+
velocity**, with no input other than "flux → DCT → truncate → L2".
|
|
146
|
+
|
|
147
|
+
For completeness, the 2D PCA scatter coloured by phase tells the same
|
|
148
|
+
story in two dimensions:
|
|
149
|
+
|
|
150
|
+

|
|
151
|
+
|
|
152
|
+
A horizontal colour gradient (blue on the left for late phases, red on
|
|
153
|
+
the right for early phases) shows that PC1 carries the phase axis. The
|
|
154
|
+
2D plot is busier than the 1D PC1-vs-phase view above, but it confirms
|
|
155
|
+
the structure isn't a fluke of one direction.
|
|
156
|
+
|
|
157
|
+
We did not tell the embedding what to look for. We just gave it
|
|
158
|
+
``flux → DCT → truncate → L2 normalise`` and let it run. The Doppler shift
|
|
159
|
+
of H-α across the orbit ends up encoded in one direction of the latent
|
|
160
|
+
space — which is exactly the property a similarity layer or a clustering
|
|
161
|
+
algorithm needs to leverage.
|
|
162
|
+
|
|
163
|
+
A small caveat: a handful of low-SNR / mis-fit spectra would otherwise
|
|
164
|
+
dominate the leading PCs and hide the phase structure of the bulk. The
|
|
165
|
+
plot above uses the inner 95 % of the embedding cloud (15 outliers
|
|
166
|
+
dropped on a robust Euclidean-distance criterion). The dropped points
|
|
167
|
+
are real — they correspond to spectra where the H-α profile fit fails or
|
|
168
|
+
where the data quality is too poor — but their structure is independent
|
|
169
|
+
of orbital phase, so removing them lets the phase signal become visible.
|
|
170
|
+
|
|
171
|
+
For completeness, the pairwise cosine-similarity matrix re-ordered by
|
|
172
|
+
phase tells the same story in a different form:
|
|
173
|
+
|
|
174
|
+

|
|
175
|
+
|
|
176
|
+
(At 240 × 240 the per-cell labels are unreadable in this preview — the
|
|
177
|
+
full-resolution version is in the script's output folder.) Spectra near
|
|
178
|
+
the same orbital phase are slightly more similar to each other than to
|
|
179
|
+
spectra at the opposite phase, which gives a faint block-diagonal pattern
|
|
180
|
+
in the third decimal of the cosine similarity.
|
|
181
|
+
|
|
182
|
+
## Reproducing this
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
cd playground
|
|
186
|
+
python3.12 -m venv venv && source venv/bin/activate
|
|
187
|
+
pip install -e "..[viz]" plotly kaleido
|
|
188
|
+
# drop your own alphadra_*.fits files in playground/datas/alphadra/
|
|
189
|
+
python analyse_alpha_dra.py
|
|
190
|
+
# → output/alpha_dra_*.png + alpha_dra_summary.csv
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Takeaways
|
|
194
|
+
|
|
195
|
+
- **A 70-year-old published orbital period drops out of 240 amateur
|
|
196
|
+
spectra**, recovered to better than 1 % from the same kernel pipeline
|
|
197
|
+
that loads, normalises, and fits H-α on every file.
|
|
198
|
+
- **The eccentric character of the orbit is visible** in the shape of the
|
|
199
|
+
phase-folded curve, without ever explicitly fitting a Kepler model.
|
|
200
|
+
- **The principal axis of the embedding latent space *is* the orbital
|
|
201
|
+
phase**. Project the 240 vectors onto their first two PCs, colour by
|
|
202
|
+
phase: you get a smooth gradient. The DCT recipe, given nothing but
|
|
203
|
+
flux, ended up encoding the Doppler shift as a vector dimension — a
|
|
204
|
+
free property to exploit for clustering and similarity search.
|
|
205
|
+
- This is also the **first concrete demonstration** that amateur
|
|
206
|
+
spectroscopy at moderate resolution is more than sufficient to do real
|
|
207
|
+
binary-star science when the data are reduced through a consistent,
|
|
208
|
+
reproducible pipeline. The kernel didn't need any α-Dra-specific
|
|
209
|
+
tuning — it's the same `snr_der → normalize → fit_gaussian_line` recipe
|
|
210
|
+
used on α Cyg, run on 240 files instead of 18.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""The algorithm catalogue.
|
|
2
|
+
|
|
3
|
+
Importing this package is a no-op: it does **not** trigger algorithm
|
|
4
|
+
discovery on its own. Discovery is driven from
|
|
5
|
+
:func:`spectro_kernel.registry.ensure_discovered`, which is called
|
|
6
|
+
lazily by every registry getter (``list_algorithms``, ``get_algorithm``,
|
|
7
|
+
``run_algorithm``, …). This keeps importing helpers from inside the
|
|
8
|
+
``algorithms.*`` tree (for example
|
|
9
|
+
``spectro_kernel.algorithms._common``) free of any circular-import
|
|
10
|
+
hazard.
|
|
11
|
+
|
|
12
|
+
Adding a new algorithm is still as simple as dropping a file under
|
|
13
|
+
``algorithms/<category>/`` with a ``@register_algorithm`` decorator —
|
|
14
|
+
the lazy walk picks it up the first time the registry is queried.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Algorithm: read an SDSS spectrum (loglam-based binary table)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from astropy.io import fits
|
|
10
|
+
|
|
11
|
+
from ...base import AlgorithmOutput, BaseAlgorithm
|
|
12
|
+
from ...registry import register_algorithm
|
|
13
|
+
from ...types import AlgorithmCategory, Spectrum1D, WorkContext
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@register_algorithm("read_sdss_spectrum", category=AlgorithmCategory.IO, version="1.0.0")
|
|
17
|
+
class ReadSdssSpectrum(BaseAlgorithm):
|
|
18
|
+
"""Read an SDSS-format spectrum (``spec-*.fits``) into ``ctx.spectrum``.
|
|
19
|
+
|
|
20
|
+
SDSS spectra store the wavelength axis as ``loglam`` (log10 of the
|
|
21
|
+
wavelength in Å) and the flux as ``flux`` in a binary-table extension.
|
|
22
|
+
This is incompatible with the generic ``read_fits`` algorithm (which
|
|
23
|
+
looks for a column named ``wavelength``/``wave``/``lambda``), so we
|
|
24
|
+
handle it explicitly here.
|
|
25
|
+
|
|
26
|
+
The reader:
|
|
27
|
+
|
|
28
|
+
1. Opens the file (local path or http(s) URL).
|
|
29
|
+
2. Locates the spectrum extension — typically HDU 1 (``COADD``), with
|
|
30
|
+
``flux``, ``loglam``, ``ivar`` columns.
|
|
31
|
+
3. Converts ``loglam`` → linear Å.
|
|
32
|
+
4. Converts inverse variance ``ivar`` → 1σ uncertainty.
|
|
33
|
+
5. Returns a ``Spectrum1D`` in the kernel's canonical form.
|
|
34
|
+
|
|
35
|
+
Flux units in SDSS are 10⁻¹⁷ erg/s/cm²/Å — stored as-is in
|
|
36
|
+
``Spectrum1D.flux_unit`` for downstream sanity.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
backend = "astropy"
|
|
40
|
+
references = [
|
|
41
|
+
"York et al. 2000, AJ, 120, 1579 — Sloan Digital Sky Survey overview.",
|
|
42
|
+
"Smee et al. 2013, AJ, 146, 32 — SDSS BOSS spectrograph and data format.",
|
|
43
|
+
"Bolton et al. 2012, AJ, 144, 144 — DR9 spectroscopic data release "
|
|
44
|
+
"documenting the spec-*.fits layout.",
|
|
45
|
+
]
|
|
46
|
+
long_description = (
|
|
47
|
+
"Reading ``loglam``-based spectra is the one big gap of the generic "
|
|
48
|
+
"FITS reader — this algorithm closes it. Compatible with SDSS DR9 "
|
|
49
|
+
"through the current DR (the ``COADD`` HDU layout has been stable "
|
|
50
|
+
"since DR9)."
|
|
51
|
+
)
|
|
52
|
+
default_params = {"path": None}
|
|
53
|
+
required_params = ["path"]
|
|
54
|
+
param_descriptions = {
|
|
55
|
+
"path": "Local path or http(s) URL of an SDSS spec-*.fits file."
|
|
56
|
+
}
|
|
57
|
+
input_requirements: list[str] = []
|
|
58
|
+
output_produces = ["spectrum"]
|
|
59
|
+
|
|
60
|
+
def run(self, ctx: WorkContext, params: dict[str, Any]) -> AlgorithmOutput:
|
|
61
|
+
path = params["path"]
|
|
62
|
+
with fits.open(path) as hdulist:
|
|
63
|
+
hdu = _find_spectrum_hdu(hdulist)
|
|
64
|
+
if hdu is None:
|
|
65
|
+
return AlgorithmOutput.fail(
|
|
66
|
+
f"No SDSS spectrum extension found in {os.path.basename(str(path))}; "
|
|
67
|
+
"expected an extension with 'flux' + 'loglam' columns."
|
|
68
|
+
)
|
|
69
|
+
data = hdu.data
|
|
70
|
+
loglam = np.asarray(data["loglam"], dtype=np.float64).ravel()
|
|
71
|
+
flux = np.asarray(data["flux"], dtype=np.float64).ravel()
|
|
72
|
+
wavelength = np.power(10.0, loglam)
|
|
73
|
+
uncertainty: np.ndarray | None = None
|
|
74
|
+
if "ivar" in data.names:
|
|
75
|
+
ivar = np.asarray(data["ivar"], dtype=np.float64).ravel()
|
|
76
|
+
with np.errstate(divide="ignore", invalid="ignore"):
|
|
77
|
+
uncertainty = np.where(ivar > 0.0, 1.0 / np.sqrt(ivar), np.nan)
|
|
78
|
+
|
|
79
|
+
mask: np.ndarray | None = None
|
|
80
|
+
if "and_mask" in data.names:
|
|
81
|
+
mask = np.asarray(data["and_mask"], dtype=np.int64).ravel() != 0
|
|
82
|
+
|
|
83
|
+
meta: dict[str, Any] = {"source": str(path), "format": "sdss"}
|
|
84
|
+
# Carry the standard SDSS keywords if present (helpful for downstream tools).
|
|
85
|
+
with fits.open(path) as hdulist:
|
|
86
|
+
primary = hdulist[0].header
|
|
87
|
+
for key in ("PLUG_RA", "PLUG_DEC", "PLATE", "MJD", "FIBERID", "OBJTYPE"):
|
|
88
|
+
if key in primary:
|
|
89
|
+
meta[key.lower()] = primary[key]
|
|
90
|
+
|
|
91
|
+
ctx.spectrum = Spectrum1D(
|
|
92
|
+
wavelength=wavelength,
|
|
93
|
+
flux=flux,
|
|
94
|
+
uncertainty=uncertainty,
|
|
95
|
+
mask=mask,
|
|
96
|
+
wavelength_unit="Angstrom",
|
|
97
|
+
flux_unit="1e-17 erg/s/cm2/Angstrom",
|
|
98
|
+
meta=meta,
|
|
99
|
+
)
|
|
100
|
+
return AlgorithmOutput.ok(
|
|
101
|
+
metrics={
|
|
102
|
+
"npix": float(wavelength.size),
|
|
103
|
+
"wavelength_min": float(wavelength.min()),
|
|
104
|
+
"wavelength_max": float(wavelength.max()),
|
|
105
|
+
},
|
|
106
|
+
message=(
|
|
107
|
+
f"Loaded SDSS spectrum: {wavelength.size} pixels, "
|
|
108
|
+
f"{wavelength.min():.0f}-{wavelength.max():.0f} Å."
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _find_spectrum_hdu(hdulist: fits.HDUList) -> fits.BinTableHDU | None:
|
|
114
|
+
"""Return the first BinTableHDU that has 'flux' + 'loglam' columns."""
|
|
115
|
+
for hdu in hdulist:
|
|
116
|
+
if not isinstance(hdu, fits.BinTableHDU) or hdu.data is None:
|
|
117
|
+
continue
|
|
118
|
+
names = {n.lower() for n in hdu.data.names}
|
|
119
|
+
if "flux" in names and "loglam" in names:
|
|
120
|
+
return hdu
|
|
121
|
+
return None
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import importlib
|
|
12
12
|
import inspect
|
|
13
|
+
import pkgutil
|
|
13
14
|
from collections.abc import Callable
|
|
14
15
|
from dataclasses import dataclass
|
|
15
16
|
from pathlib import Path
|
|
@@ -152,15 +153,35 @@ def register_algorithm(
|
|
|
152
153
|
|
|
153
154
|
|
|
154
155
|
def ensure_discovered() -> None:
|
|
155
|
-
"""Import
|
|
156
|
-
|
|
157
|
-
Called automatically by the
|
|
156
|
+
"""Import every algorithm submodule once so each ``@register_algorithm`` runs.
|
|
157
|
+
|
|
158
|
+
Called automatically by the registry getter functions below; safe to call
|
|
159
|
+
repeatedly. Discovery is **lazy** on purpose: importing
|
|
160
|
+
``spectro_kernel.algorithms`` (the package) used to trigger eager
|
|
161
|
+
submodule discovery via the package's ``__init__``, which created a
|
|
162
|
+
circular-import hazard for any module that pulled even one helper out of
|
|
163
|
+
the ``algorithms`` tree before its own top-level definitions had run
|
|
164
|
+
(notably ``spectro_kernel.embeddings`` which uses
|
|
165
|
+
``algorithms._common.fit_polynomial_continuum_asymmetric``).
|
|
166
|
+
Triggering discovery here, only when something actively asks the
|
|
167
|
+
registry for an algorithm, sidesteps that hazard entirely.
|
|
158
168
|
"""
|
|
159
169
|
global _discovered
|
|
160
170
|
if _discovered:
|
|
161
171
|
return
|
|
162
|
-
|
|
172
|
+
# Mark *before* the walk so any algorithm that re-enters the registry
|
|
173
|
+
# during its own import (rare but possible) doesn't recurse infinitely.
|
|
163
174
|
_discovered = True
|
|
175
|
+
pkg = importlib.import_module("spectro_kernel.algorithms")
|
|
176
|
+
for module_info in pkgutil.walk_packages(pkg.__path__, prefix=pkg.__name__ + "."):
|
|
177
|
+
try:
|
|
178
|
+
importlib.import_module(module_info.name)
|
|
179
|
+
except Exception: # noqa: BLE001 - optional algorithms may need extras
|
|
180
|
+
# An algorithm whose dependencies aren't installed should not break
|
|
181
|
+
# discovery of the others. The algorithm will simply not appear in
|
|
182
|
+
# the registry; users who actually try to call it get a clear
|
|
183
|
+
# ImportError from inside its own module.
|
|
184
|
+
continue
|
|
164
185
|
|
|
165
186
|
|
|
166
187
|
def has_algorithm(name: str) -> bool:
|