spectro-kernel 0.1.2__tar.gz → 0.1.4__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.4}/CHANGELOG.md +145 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/PKG-INFO +1 -1
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_ew_timeseries.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_halpha_phase_stack.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_halpha_timeseries.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_overlay.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_pc1_vs_phase.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_pca_2d.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_pca_3d.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_periodogram.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_phase_folded.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_rv_timeseries.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_similarity.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_cyg_similarity_date.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_halpha_phase_stack.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_pc1_vs_phase.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_pca_2d.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_pca_3d.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_periodogram.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_phase_folded.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_rv_timeseries.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_similarity_date.png +0 -0
- spectro_kernel-0.1.4/docs/images/notebooks/alpha_dra_similarity_phase.png +0 -0
- spectro_kernel-0.1.4/docs/notebooks/alpha-cyg-time-series.md +155 -0
- spectro_kernel-0.1.4/docs/notebooks/alpha-dra-binary-period.md +210 -0
- spectro_kernel-0.1.4/src/spectro_kernel/algorithms/__init__.py +17 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/embed_lick_indices.py +42 -5
- spectro_kernel-0.1.4/src/spectro_kernel/algorithms/io/read_sdss.py +121 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/registry.py +25 -4
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/version.py +1 -1
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_mcp/__main__.py +63 -16
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_mcp/auto_tools.py +49 -14
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_mcp/observability.py +9 -2
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_mcp/session.py +80 -5
- spectro_kernel-0.1.4/src/spectro_mcp/url_safety.py +155 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_embedding_remote_lick.py +63 -0
- spectro_kernel-0.1.4/tests/unit/test_mcp_security.py +169 -0
- spectro_kernel-0.1.4/tests/unit/test_no_circular_imports.py +89 -0
- spectro_kernel-0.1.4/tests/unit/test_read_sdss.py +76 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/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.4}/.gitignore +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/LICENSE +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/README.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/concepts/algorithms.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/concepts/architecture.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/concepts/data-types.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/concepts/pipelines.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/contributing.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/cookbook/bess-dashboard.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/cookbook/multi-star-viewer.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/cookbook/web-playground.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/gen_catalogue.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/getting-started.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/index.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/notebooks/aurora-line-monitor.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/notebooks/be-star-variability.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/notebooks/claude-mcp-end-to-end.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/notebooks/exoplanet-transit-rv.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/notebooks/full-reduction-walkthrough.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/notebooks/native-vs-easyspec-showdown.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/notebooks/your-first-sb2.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/reference.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/tutorials/add-an-algorithm.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/tutorials/analyse-a-spectrum.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/tutorials/discover-the-catalogue.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/tutorials/first-spectrum.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/usage/cli.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/usage/library.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/usage/mcp.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/docs/why.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/playground/README.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/pyproject.toml +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/adapters/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/adapters/easyspec.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/_common.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/advanced/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/advanced/aperture_photometry.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/advanced/disentangle_sb2.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/catalogs/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/catalogs/gaia.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/catalogs/simbad.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/catalogs/vizier.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/continuum/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/continuum/compare_normalisations.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/continuum/normalize_edges.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/continuum/normalize_max.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/continuum/normalize_percentile.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/continuum/normalize_polynomial.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/continuum/subtract_continuum.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/air_vacuum.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/barycentric.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/doppler_shift.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/extinction_correct_easyspec.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/fit_telluric_scaling.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/flux_calibrate_easyspec.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/remove_telluric.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/corrections/synth_telluric.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/embed_band_power.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/embed_continuum_subtracted.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/embed_log_lambda.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/embed_pretrained.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/embed_remote.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/embed_spectrum.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/embedding/embed_wavelets.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/exports/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/exports/export_csv.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/exports/export_fits.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/exports/export_hdf5.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/exports/export_votable.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/extraction/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/extraction/easyspec_extract.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/io/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/io/read_ascii.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/io/read_echelle.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/io/read_fits.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/io/read_votable.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/_profiles.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/catalogs.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/compare_line_fits.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/detect.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/equivalent_width.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/fit_gaussian.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/fit_lorentzian.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/lines/fit_voigt.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/quality/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/quality/compare_snr_methods.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/quality/snr_der.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/quality/snr_edge.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/quality/snr_linear_fit.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/_easyspec_apply.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/_easyspec_helpers.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/bias_combine.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/clip_cosmic_rays.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/dark_subtract.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/easyspec_bias.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/easyspec_cosmic_ray.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/easyspec_dark.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/easyspec_flat.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/easyspec_flat_normalize.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/easyspec_subtract_bias.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/easyspec_subtract_dark.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/extract_spectrum_sum.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/flat_normalize.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/subtract_sky_2d.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/reduction/wavelength_calibrate.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/rv/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/rv/cross_correlate.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/rv/fit_keplerian_orbit.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/rv/measure.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/rv/precision_bouchy.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/smoothing/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/smoothing/compare_smoothings.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/smoothing/smooth_gaussian.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/smoothing/smooth_savgol.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/stacking/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/stacking/merge_echelle_orders.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/stacking/stack.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/timeseries/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/timeseries/lomb_scargle.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/timeseries/phase_fold.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/transforms/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/transforms/clip_sigma.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/transforms/extract_region.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/transforms/mask_range.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/transforms/resample.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/transforms/resample_flux_conserving.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/viz/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/viz/plot_3d_surface.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/viz/plot_animation.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/viz/plot_dynamic_spectrum.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/viz/plot_plotly.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/wavelength_calibration/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/algorithms/wavelength_calibration/easyspec_wavelength.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/base.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/cli.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/embeddings.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/errors.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/io/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/io/ascii.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/io/fits.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/io/votable.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/pipeline.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/catalog/analysis/balmer_quick.yaml +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/catalog/analysis/embed_quick.yaml +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/catalog/analysis/quality_report.yaml +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/catalog/analysis/rv_quick.yaml +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/catalog/analysis/snr_check.yaml +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/catalog/analysis/time_series_overview.yaml +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/catalog/reduction/full_reduction_easyspec.yaml +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/presets/loader.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/py.typed +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/catalog.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/context.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/enums.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/history.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/image.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/line.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/spectrum.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_kernel/types/timeseries.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_mcp/__init__.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_mcp/auth.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_mcp/py.typed +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/src/spectro_mcp/server.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/conftest.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/reference/conftest.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/reference/data/.gitkeep +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/reference/data/README.md +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/reference/data/sun/sun_reference_stis_002.fits +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/reference/data/vega/alpha_lyr_stis_011.fits +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/reference/test_known_answers.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/reference/test_sun.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/reference/test_vega.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_base.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_cli.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_combine.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_continuum.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_corrections.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_easyspec_wrappers.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_embedding.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_embedding_pretrained.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_embedding_tier1.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_io.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_line_profiles.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_lines.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_mcp.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_mcp_production.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_misc_algorithms.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_pipeline.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_quality.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_smoothing.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_timeseries.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_transforms.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_types.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/tests/unit/test_viz.py +0 -0
- {spectro_kernel-0.1.2 → spectro_kernel-0.1.4}/website/README.md +0 -0
|
@@ -6,6 +6,151 @@ Until `1.0.0` the public API may change between minor versions.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.4] — 2026-06-10
|
|
10
|
+
|
|
11
|
+
### Fixed — Lick atomic equivalent-width formula (CRITICAL)
|
|
12
|
+
|
|
13
|
+
`embed_lick_indices` was computing
|
|
14
|
+
``EW = (λ_hi − λ_lo) × (1 − mean(F/Fc))`` for atomic indices instead of
|
|
15
|
+
the canonical Worthey-1994 form ``EW = ∫(1 − F/Fc) dλ``. The two only
|
|
16
|
+
agree when pixel edges fall exactly on the band limits — which never
|
|
17
|
+
happens on real data — so every atomic Lick value produced by ≤ 0.1.3
|
|
18
|
+
was systematically **inflated by ~1–2 Å**. The molecular-magnitude form
|
|
19
|
+
was correct.
|
|
20
|
+
|
|
21
|
+
Algorithm version bumped 1.0.0 → 2.0.0; any indexed Lick embedding from
|
|
22
|
+
≤ 0.1.3 should be **re-computed**. Companion fix in ``_band_mean_flux``:
|
|
23
|
+
the band mean now uses the actual covered pixel span (not the nominal
|
|
24
|
+
``hi − lo``) and refuses bands less than half-covered by the spectrum.
|
|
25
|
+
|
|
26
|
+
### Fixed — SSRF on `load_spectrum` / `load_spectrum_from_url` (CRITICAL)
|
|
27
|
+
|
|
28
|
+
The MCP file-loading tools forwarded the user-supplied URL directly to
|
|
29
|
+
``urllib.request.urlretrieve`` with no host validation, no redirect
|
|
30
|
+
control, no timeout and no size cap. An attacker could fetch instance
|
|
31
|
+
metadata (``http://169.254.169.254/latest/meta-data/iam/...``), scan the
|
|
32
|
+
VPC interior, or stream a 10 GB file to exhaust the worker.
|
|
33
|
+
|
|
34
|
+
New module ``spectro_mcp/url_safety.py`` gates every URL coming from
|
|
35
|
+
MCP through:
|
|
36
|
+
- scheme allowlist (``http``/``https`` only);
|
|
37
|
+
- ``localhost`` and DNS-resolved IP validation (refuses loopback,
|
|
38
|
+
RFC1918, link-local, multicast, reserved, unspecified);
|
|
39
|
+
- a no-redirect HTTP handler (a friendly upstream can't 302 to
|
|
40
|
+
``169.254.169.254`` after the hostname guard passes);
|
|
41
|
+
- 60-second timeout plus 256 MiB byte cap on downloads.
|
|
42
|
+
|
|
43
|
+
### Fixed — Path traversal on the HTTP MCP server (HIGH)
|
|
44
|
+
|
|
45
|
+
``load_spectrum(session_id, path)`` accepted any local filesystem path,
|
|
46
|
+
including ``/etc/passwd`` and ``/proc/self/environ`` (which leaks
|
|
47
|
+
``SPECTRO_MCP_API_KEY``, ``AWS_*``, ``SPECTRO_MCP_REDIS_URL``, …).
|
|
48
|
+
|
|
49
|
+
Local paths are now refused when the new env var
|
|
50
|
+
``SPECTRO_MCP_DENY_LOCAL_PATHS=1`` is set, and the ``--http`` entry
|
|
51
|
+
point sets it automatically. ``--stdio`` mode (the local default) is
|
|
52
|
+
unaffected — the user's own machine is trusted by definition.
|
|
53
|
+
|
|
54
|
+
### Fixed — Pickle deserialisation from Redis (HIGH)
|
|
55
|
+
|
|
56
|
+
``RedisSessionManager.get()`` called ``pickle.loads`` on whatever
|
|
57
|
+
arbitrary bytes Redis returned. Any path that lets an attacker write to
|
|
58
|
+
the Redis backing store (compromised Redis, MITM with no TLS, leaked
|
|
59
|
+
``SPECTRO_MCP_REDIS_URL``) → arbitrary code execution on the next
|
|
60
|
+
``get``.
|
|
61
|
+
|
|
62
|
+
Payloads are now wrapped with an HMAC-SHA256 signature derived from
|
|
63
|
+
``SPECTRO_MCP_SESSION_SECRET`` (env). Without that env var the server
|
|
64
|
+
generates a one-shot random secret at boot (sessions don't survive a
|
|
65
|
+
restart in that case — a clear stderr warning is emitted). Unsigned or
|
|
66
|
+
mis-signed blobs raise ``ValueError`` before ``pickle.loads`` runs.
|
|
67
|
+
|
|
68
|
+
### Fixed — MCP stdio corruption from boot-time stdout prints (HIGH)
|
|
69
|
+
|
|
70
|
+
In stdio mode, stdout is the JSON-RPC wire to the agent. Two paths
|
|
71
|
+
emitted bytes on stdout that corrupted the very first handshake:
|
|
72
|
+
|
|
73
|
+
1. Algorithm discovery imported ``astroquery`` (Gaia DR4 banner, ~390 B)
|
|
74
|
+
and ``easyspec`` (version banner, ~30 B). FD-level writes that
|
|
75
|
+
``contextlib.redirect_stdout`` doesn't catch.
|
|
76
|
+
2. ``configure_logger`` attached the audit handler to ``sys.stdout``;
|
|
77
|
+
every tool call therefore emitted a JSON record on the wire.
|
|
78
|
+
|
|
79
|
+
Fix: a new ``_silence_fd_stdout`` context manager in ``__main__``
|
|
80
|
+
``dup2``'s FD 1 to FD 2 for the boot phase (catches even C-level
|
|
81
|
+
writes), and the audit logger now targets ``sys.stderr``. HTTP mode is
|
|
82
|
+
unchanged.
|
|
83
|
+
|
|
84
|
+
### Hardened — Dockerfile non-root user (MED)
|
|
85
|
+
|
|
86
|
+
Default container user was root. The Dockerfile now creates an
|
|
87
|
+
unprivileged ``app`` (UID 10001) and drops to it before ``CMD``. Any
|
|
88
|
+
future vulnerability that reaches the filesystem is confined to the
|
|
89
|
+
app user's home.
|
|
90
|
+
|
|
91
|
+
### Hardened — Discovery silent error swallow (LOW)
|
|
92
|
+
|
|
93
|
+
``registry.ensure_discovered`` silently dropped any submodule that
|
|
94
|
+
failed to import. The intent (gracefully skip algorithms whose optional
|
|
95
|
+
extra is missing) is preserved, but a bug-induced import failure used
|
|
96
|
+
to vanish without trace. Discovery now logs every dropped submodule
|
|
97
|
+
via the ``spectro_kernel.registry`` logger.
|
|
98
|
+
|
|
99
|
+
### Added
|
|
100
|
+
|
|
101
|
+
- ``tests/unit/test_mcp_security.py`` — 15 new tests covering every
|
|
102
|
+
v0.1.4 finding (SSRF target families, path-traversal env-var gate,
|
|
103
|
+
HMAC roundtrip + tamper + wrong-key, RedisSessionManager round-trip
|
|
104
|
+
via fakeredis, ``_resolve_redis_secret`` env-vs-random behaviour).
|
|
105
|
+
- 2 new Lick regression tests (``test_lick_atomic_ew_matches_worthey_integral``,
|
|
106
|
+
``test_lick_band_mean_uses_actual_pixel_span``).
|
|
107
|
+
|
|
108
|
+
### Migration notes
|
|
109
|
+
|
|
110
|
+
- Re-index any vectors produced by ``embed_lick_indices`` ≤ 0.1.3
|
|
111
|
+
(values changed by 1–2 Å on atomic indices).
|
|
112
|
+
- For Redis-backed deployments: set ``SPECTRO_MCP_SESSION_SECRET`` to
|
|
113
|
+
a stable 32-byte secret in the environment. Otherwise sessions don't
|
|
114
|
+
survive a restart.
|
|
115
|
+
- For HTTP deployments where you want users to send local paths anyway
|
|
116
|
+
(uncommon), unset ``SPECTRO_MCP_DENY_LOCAL_PATHS`` explicitly.
|
|
117
|
+
|
|
118
|
+
## [0.1.3] — 2026-06-09
|
|
119
|
+
|
|
120
|
+
### Fixed — circular import that prevented `from spectro_kernel.embeddings import …`
|
|
121
|
+
|
|
122
|
+
In 0.1.2, `spectro_kernel.algorithms.__init__` ran an eager submodule
|
|
123
|
+
walk at import time. That walk imported every algorithm, including
|
|
124
|
+
`embed_continuum_subtracted`, which itself did a top-level
|
|
125
|
+
`from ...embeddings import VALID_NORM_METHODS, VALID_STRATEGIES,
|
|
126
|
+
embed_flux`. But `embeddings` itself imports
|
|
127
|
+
`spectro_kernel.algorithms._common` before defining `VALID_*` /
|
|
128
|
+
`embed_flux` — so when an external consumer wrote
|
|
129
|
+
`from spectro_kernel.embeddings import embed_flux` as its very first
|
|
130
|
+
contact with the kernel, Python's import machinery saw a partially
|
|
131
|
+
initialised module and raised `ImportError`.
|
|
132
|
+
|
|
133
|
+
Reported by a downstream library whose service worker crashed
|
|
134
|
+
deterministically on the same PyPI 0.1.2 wheel.
|
|
135
|
+
|
|
136
|
+
The fix moves discovery into `registry.ensure_discovered()`, which was
|
|
137
|
+
already the lazy entry point — every registry getter (`list_algorithms`,
|
|
138
|
+
`get_algorithm`, `run_algorithm`, …) calls it before answering.
|
|
139
|
+
Importing `spectro_kernel.algorithms` is now a true no-op, so pulling a
|
|
140
|
+
helper out of `algorithms._common` from inside `embeddings` no longer
|
|
141
|
+
triggers a transitive cycle. Discovery now silently skips any
|
|
142
|
+
optional-extra algorithm whose dependency is missing, instead of
|
|
143
|
+
breaking the entire import chain.
|
|
144
|
+
|
|
145
|
+
Behaviour preservation: existing callers that go through the registry
|
|
146
|
+
see the same catalogue at the same time (the first registry call
|
|
147
|
+
populates it, exactly as before). The only change is **when** discovery
|
|
148
|
+
runs — it's properly lazy now.
|
|
149
|
+
|
|
150
|
+
Added 4 regression tests in `test_no_circular_imports.py` that run the
|
|
151
|
+
exact cold-start imports the bug reporter hit, in fresh subprocesses,
|
|
152
|
+
so a future re-introduction of the cycle is caught immediately by CI.
|
|
153
|
+
|
|
9
154
|
## [0.1.2] — 2026-06-05
|
|
10
155
|
|
|
11
156
|
### 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.4
|
|
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
|