Skip to content

Commit 4d7d489

Browse files
authored
Merge pull request #698 from light-curve/atcat
Add ATCAT embedding model
2 parents 19435ef + de4003e commit 4d7d489

16 files changed

Lines changed: 1467 additions & 493 deletions

CHANGELOG.md

Lines changed: 169 additions & 86 deletions
Large diffs are not rendered by default.

light-curve/README.md

Lines changed: 142 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,14 @@ amplitudes = lc.Amplitude().many(light_curves)
4545
print(f"Amplitude: mean = {np.mean(amplitudes):.3f} mag, std = {np.std(amplitudes):.3f} mag")
4646
```
4747

48-
For conda, alternative package names, and platform-specific installation notes, see the [Installation](#installation)
48+
For conda, alternative package names, and platform-specific installation notes, see
49+
the [Installation](#installation)
4950
section.
5051

5152
## Feature extractors
5253

53-
Most classes implement feature extractors useful for astrophysical source classification and characterization based
54+
Most classes implement feature extractors useful for astrophysical source classification and characterization
55+
based
5456
on light curves.
5557

5658
To list all available feature extractors:
@@ -86,7 +88,8 @@ The Flux/magnitude column indicates whether a feature should be used with flux l
8688
magnitude light curves only, or either.
8789

8890
The `transform=True` column shows the default transformation applied to the output when `transform=True`
89-
is passed to the extractor constructor (default is `transform=None`, which does not apply any transformations).
91+
is passed to the extractor constructor (default is `transform=None`, which does not apply any
92+
transformations).
9093
Transformations are monotonic functions intended to reduce dynamic range and improve the suitability of
9194
raw feature values for machine learning.
9295
You can also select a transformation by name string (e.g. `transform="lg"`) for supported features;
@@ -457,20 +460,24 @@ Meta-features accept other feature extractors and apply them to pre-processed da
457460
#### Periodogram
458461

459462
This feature transforms time-series data into the Lomb-Scargle periodogram, providing an estimate of the power
460-
spectrum. The `peaks` argument specifies how many of the most significant spectral density peaks to return. For
463+
spectrum. The `peaks` argument specifies how many of the most significant spectral density peaks to return.
464+
For
461465
each peak, its period and signal-to-noise ratio are returned:
462466

463467
$$
464468
\text{signal to noise of peak} \equiv \frac{P(\omega_\mathrm{peak}) - \langle P(\omega) \rangle}{\sigma\_{P(\omega)}}
465469
$$
466470

467-
The optional `features` argument accepts a list of additional feature extractors, which are applied to the power
468-
spectrum: frequency is passed as "time," power spectrum is passed as "magnitude," and no uncertainties are set.
471+
The optional `features` argument accepts a list of additional feature extractors, which are applied to the
472+
power
473+
spectrum: frequency is passed as "time," power spectrum is passed as "magnitude," and no uncertainties are
474+
set.
469475

470476
#### Bins
471477

472478
Bins the time series into windows of width $\mathrm{window}$ with respect to some $\mathrm{offset}$.
473-
The $j$-th bin spans $[j \cdot \mathrm{window} + \mathrm{offset},\ (j + 1) \cdot \mathrm{window} + \mathrm{offset}]$.
479+
The $j$-th bin
480+
spans $[j \cdot \mathrm{window} + \mathrm{offset},\ (j + 1) \cdot \mathrm{window} + \mathrm{offset}]$.
474481

475482
The binned time series is defined by
476483
$$t_j^* = (j + \frac12) \cdot \mathrm{window} + \mathrm{offset},$$
@@ -501,7 +508,8 @@ gives the best balance of robustness and final accuracy.
501508

502509
By default, initial parameter values and bounds are estimated from the data. Override them with the
503510
`init` and `bounds` arguments (supported by MCMC-based algorithms only), each a list of values or
504-
`None`s to keep data-derived defaults for individual parameters. `BazinFit` has 5 parameters, `LinexpFit` has 4,
511+
`None`s to keep data-derived defaults for individual parameters. `BazinFit` has 5 parameters, `LinexpFit` has
512+
4,
505513
`VillarFit` has 7; run `help()` on the respective class for the parameter order.
506514

507515
The `ln_prior` argument sets the MCMC prior. It accepts `None` / `"no"` (flat prior, the default),
@@ -511,7 +519,8 @@ distributions (see `help(light_curve.ln_prior)` for the available distribution t
511519
encodes supernova physics constraints; it assumes time values are in days.
512520

513521
Pass `transform=True` to convert the raw fit parameters to a magnitude-like representation: the
514-
amplitude becomes a magnitude (assuming input flux in janskys), the baseline is normalized by the amplitude, the
522+
amplitude becomes a magnitude (assuming input flux in janskys), the baseline is normalized by the amplitude,
523+
the
515524
reference time is
516525
dropped, and the reduced chi^2 is log-scaled. Run `help(lc.BazinFit)` for the exact definition.
517526

@@ -539,9 +548,12 @@ print(values)
539548
#### Rainbow Fit
540549

541550
Rainbow ([Russeil+23](https://arxiv.org/abs/2310.02916)) is a black-body parametric model for transient light
542-
curves. By default, it uses the Bazin function as a model for bolometric flux evolution and a logistic function
543-
for temperature evolution. The user may customize the model by providing their own functions for bolometric flux
544-
and temperature evolution. This example demonstrates the reconstruction of a synthetic light curve with this model.
551+
curves. By default, it uses the Bazin function as a model for bolometric flux evolution and a logistic
552+
function
553+
for temperature evolution. The user may customize the model by providing their own functions for bolometric
554+
flux
555+
and temperature evolution. This example demonstrates the reconstruction of a synthetic light curve with this
556+
model.
545557
`RainbowFit` requires the `iminuit` package.
546558

547559
<!-- name: test_rainbow_fit_example -->
@@ -594,13 +606,15 @@ print(dict(zip(feature.names, values)))
594606
print(f"Goodness of fit: {values[-1]}")
595607
```
596608

597-
Note that while the data generation above uses approximate physical constant values, `RainbowFit` uses CODATA 2018
609+
Note that while the data generation above uses approximate physical constant values, `RainbowFit` uses CODATA
610+
2018
598611
values internally.
599612

600613
### Experimental extractors
601614

602615
The package consists of two parts: a wrapper for the
603-
[`light-curve-feature` Rust crate](https://crates.io/crates/light-curve-feature) (`light_curve_ext` sub-package)
616+
[`light-curve-feature` Rust crate](https://crates.io/crates/light-curve-feature) (`light_curve_ext`
617+
sub-package)
604618
and a pure-Python sub-package `light_curve_py`.
605619
We use the Python implementation to test the Rust implementation and to develop new experimental extractors.
606620
The Python implementation is significantly slower for most extractors and does not provide the same full
@@ -631,12 +645,15 @@ This should print a warning about the experimental status of the Python class.
631645

632646
## Performance
633647

634-
The package is designed for high throughput. The following techniques help extract features with minimal overhead.
648+
The package is designed for high throughput. The following techniques help extract features with minimal
649+
overhead.
635650

636651
### `sorted` and `check` parameters
637652

638-
The `sorted=True` argument tells the extractor that `t` is already sorted in ascending order, and `check=False`
639-
disables validation of NaN/inf values. The defaults are `sorted=None` (the array will be validated and an error raised
653+
The `sorted=True` argument tells the extractor that `t` is already sorted in ascending order, and
654+
`check=False`
655+
disables validation of NaN/inf values. The defaults are `sorted=None` (the array will be validated and an
656+
error raised
640657
if unsorted) and
641658
`check=True`. Passing invalid inputs without validation can cause incorrect results or crashes.
642659

@@ -780,7 +797,8 @@ Run all benchmarks from the Python project folder with
780797
`python3 -mpytest --benchmark-enable tests/test_w_bench.py`, or with slow benchmarks disabled:
781798
`python3 -mpytest -m "not (nobs or multi)" --benchmark-enable tests/test_w_bench.py`.
782799

783-
Below we benchmark the Rust implementation (`rust`) against the [`feets`](https://feets.readthedocs.io/en/latest/)
800+
Below we benchmark the Rust implementation (`rust`) against the [
801+
`feets`](https://feets.readthedocs.io/en/latest/)
784802
package and our own Python implementation (`lc_py`) for a light curve with n=1000 observations.
785803

786804
![Benchmarks, Rust is much faster](https://github.com/light-curve/light-curve-python/raw/main/light-curve/.readme/benchplot_v2.png)
@@ -792,17 +810,112 @@ but the typical extraction time including these features is 20–50 ms for a few
792810

793811
![Benchmark for different number of observations](https://github.com/light-curve/light-curve-python/raw/main/light-curve/.readme/nobs_bench_v2.png)
794812

795-
Benchmark results for both the pure-Python and Rust implementations as a function of the number of observations.
813+
Benchmark results for both the pure-Python and Rust implementations as a function of the number of
814+
observations.
796815
Both axes are on a logarithmic scale.
797816

798817
![Benchmark for multithreading and multiprocessing](https://github.com/light-curve/light-curve-python/raw/main/light-curve/.readme/multi_bench_v2.png)
799818

800-
Processing time per light curve for the feature subset from the first benchmark, as a function of the number of
819+
Processing time per light curve for the feature subset from the first benchmark, as a function of the number
820+
of
801821
CPU cores used. The dataset consists of 10,000 light curves with 1,000 observations each.
802822

803823
See the benchmarks described in more detail in
804824
["Performant feature extraction for photometric time series"](https://arxiv.org/abs/2302.10837).
805825

826+
## Light curve embeddings (`light_curve.embed`)
827+
828+
The `light_curve.embed` submodule provides pretrained neural network models that map raw photometric
829+
time series to dense vector embeddings suitable for downstream machine learning tasks such as
830+
classification, anomaly detection, and similarity search.
831+
Models are loaded directly from HuggingFace (weights are cached locally after the first download)
832+
and require `onnxruntime` and `huggingface-hub`:
833+
834+
```
835+
pip install onnxruntime huggingface_hub
836+
```
837+
838+
See the [onnxruntime install guide](https://onnxruntime.ai/docs/install/) for GPU and platform-specific
839+
packages (`onnxruntime-gpu`, `onnxruntime-directml`, etc.).
840+
Hardware-specific options such as execution providers can be passed via `ort_session_kwargs`
841+
(see [onnxruntime Python API](https://onnxruntime.ai/docs/api/python/api_summary.html)).
842+
843+
### Single-band models: Astromer
844+
845+
`Astromer1` ([Donoso-Oliva et al. 2023](https://ui.adsabs.harvard.edu/abs/2023A%26A...670A..54D/abstract))
846+
and `Astromer2` ([Donoso-Oliva et al. 2026](https://ui.adsabs.harvard.edu/abs/2026A%26A...707A.170D/abstract))
847+
are models pretrained on MACHO light curves.
848+
They accept irregularly-sampled `(time, mag)` pairs and return 256-dimensional embeddings.
849+
850+
<!-- name: test_embed_astromer -->
851+
852+
```python
853+
import numpy as np
854+
from light_curve.embed import Astromer2
855+
856+
model = Astromer2.from_hf(output="mean")
857+
# The default reduction splits long light curves into non-overlapping windows;
858+
# pass e.g. reduction="beginning" to always take the first 200 observations.
859+
860+
rng = np.random.default_rng(0)
861+
time = np.sort(rng.uniform(0, 500, 120)).astype(np.float64)
862+
mag = rng.normal(15, 0.5, 120).astype(np.float64)
863+
864+
# Returns shape (n_bands, n_subsamples, seq_size, embed_dim)
865+
embedding = model(time, mag)
866+
print(embedding.shape) # (1, 1, 1, 256)
867+
```
868+
869+
Multi-band light curves can be embedded per-band by passing a list of band labels to the `bands`
870+
constructor argument:
871+
872+
<!-- name: test_embed_astromer_multiband -->
873+
874+
```python
875+
import numpy as np
876+
from light_curve.embed import Astromer2
877+
878+
model = Astromer2.from_hf(output="mean", bands=["g", "r"])
879+
880+
rng = np.random.default_rng(1)
881+
n = 80
882+
time = np.sort(rng.uniform(0, 300, n)).astype(np.float64)
883+
mag = rng.normal(15, 0.5, n).astype(np.float64)
884+
band = np.array(["g", "r"] * (n // 2))
885+
886+
# n_bands=2: one embedding per band
887+
embedding = model(time, mag, band=band)
888+
print(embedding.shape) # (2, 1, 1, 256)
889+
```
890+
891+
### Multi-band model: ATCAT
892+
893+
`ATCAT` ([Tung 2025](https://ui.adsabs.harvard.edu/abs/2025arXiv251100614T/abstract)) is a model
894+
trained on ELAsTiCC light curves. It processes all six LSST bands jointly and returns
895+
384-dimensional embeddings. Inputs are flux, flux-error, time, and integer band indices (ugrizY → 012345):
896+
897+
<!-- name: test_embed_atcat -->
898+
899+
```python
900+
import numpy as np
901+
from light_curve.embed import ATCAT
902+
903+
model = ATCAT.from_hf(output="last")
904+
905+
rng = np.random.default_rng(2)
906+
n = 120
907+
time = np.sort(rng.uniform(0, 500, n)).astype(np.float32)
908+
flux = rng.normal(100, 10, n).astype(np.float32)
909+
flux_err = np.full(n, 5.0, dtype=np.float32)
910+
band = np.array([i % 6 for i in range(n)]) # ugrizY → 012345
911+
912+
embedding = model(time, flux, flux_err, band)
913+
print(embedding.shape) # (1, 1, 1, 384)
914+
```
915+
916+
Input fluxes should be in AB units. The default zero-point is 31.4 (LSST nJy); set `mag_zp=27.5`
917+
for ELAsTiCC / SNANA FITS data or `mag_zp=8.9` for Jy.
918+
806919
## dm-dt map
807920

808921
In addition to the feature extractors above, the package provides a separate dm–dt mapping tool.
@@ -856,12 +969,12 @@ On PyPI, we publish wheels for the stable CPython ABI, ensuring compatibility wi
856969

857970
### Support matrix
858971

859-
| Arch \ OS | Linux glibc 2.17+ | Linux musl 1.2+ | macOS | Windows |
860-
|-------------|-------------------|--------------------------------|-----------------------|---------------------------|
972+
| Arch \ OS | Linux glibc 2.17+ | Linux musl 1.2+ | macOS | Windows |
973+
|-------------|-------------------|--------------------------------|-----------------------|-----------------------------|
861974
| **x86-64** | PyPI (MKL), conda | PyPI (MKL) | PyPI macOS 15+, conda | PyPI, conda (conda: no GSL) |
862-
| **i686** | src | src || not tested |
863-
| **aarch64** | PyPI | PyPI | PyPI macOS 14+, conda | not tested |
864-
| **ppc64le** | src | not tested (no Rust toolchain) || |
975+
| **i686** | src | src || not tested |
976+
| **aarch64** | PyPI | PyPI | PyPI macOS 14+, conda | not tested |
977+
| **ppc64le** | src | not tested (no Rust toolchain) |||
865978

866979
- **PyPI / conda**: A binary wheel or package is available on pypi.org or anaconda.org.
867980
Local building is not required; the only prerequisite is a recent version of `pip` or `conda`.
@@ -921,7 +1034,8 @@ python -mpip install maturin
9211034
maturin develop --group dev
9221035
```
9231036

924-
Run this command during initial setup. On subsequent runs, activate the environment with `source venv/bin/activate`
1037+
Run this command during initial setup. On subsequent runs, activate the environment with
1038+
`source venv/bin/activate`
9251039
and rebuild Rust code with `maturin develop`. Python-only changes require no rebuild. The `--group dev` flag
9261040
(run from the `light-curve/` directory) is only needed once to install development dependencies.
9271041

@@ -953,7 +1067,8 @@ See the [Benchmarks](#benchmarks) section in [Performance](#performance) for mor
9531067
#### Dependencies and Cargo features
9541068

9551069
The package has a number of compile-time Cargo features, mostly controlling which C/C++ dependencies are used.
956-
Pass the desired features to `maturin` with `--features`; it is also recommended to use `--no-default-features`
1070+
Pass the desired features to `maturin` with `--features`; it is also recommended to use
1071+
`--no-default-features`
9571072
to avoid building unnecessary dependencies.
9581073

9591074
Available features:
Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,25 @@
1-
from .astromer import Astromer1, Astromer2, create_onnx_session
2-
from .model import AstromerInputs, Dim, EmbeddingSession, SingleBandModel
1+
from .astromer import Astromer1, Astromer2
2+
from .atcat import ATCAT
3+
from .model import EmbeddingSession, SingleBandModel
34
from .reduction import (
45
Beginning,
56
End,
6-
InputTensors,
77
MultipleReductions,
88
NonOverlappingWindows,
99
RandomSubsample,
10-
Reduction,
1110
SingleSubsampleReduction,
12-
reduction_from_str,
1311
)
1412

1513
__all__ = [
1614
"Astromer1",
1715
"Astromer2",
18-
"AstromerInputs",
16+
"ATCAT",
1917
"Beginning",
20-
"Dim",
2118
"EmbeddingSession",
2219
"End",
23-
"InputTensors",
2420
"MultipleReductions",
2521
"NonOverlappingWindows",
2622
"RandomSubsample",
27-
"Reduction",
2823
"SingleBandModel",
2924
"SingleSubsampleReduction",
30-
"create_onnx_session",
31-
"reduction_from_str",
3225
]

0 commit comments

Comments
 (0)