Skip to content

Commit d0f2f35

Browse files
committed
Add ATCAT model
1 parent 19435ef commit d0f2f35

12 files changed

Lines changed: 830 additions & 375 deletions

File tree

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
]

light-curve/light_curve/embed/astromer.py

Lines changed: 76 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,41 @@
11
from __future__ import annotations
22

3-
from typing import Sequence
3+
from dataclasses import dataclass, field
4+
from typing import TYPE_CHECKING, Sequence
45

56
import numpy as np
67
from numpy.typing import ArrayLike
78

8-
from .model import AstromerInputs, SingleBandModel
9-
from .reduction import Reduction
9+
from light_curve.embed.input_tensors import InputTensors
10+
from light_curve.embed.model import SingleBandModel
11+
from light_curve.embed.reduction import Reduction
1012

11-
_ONNX_INSTALL_HINT = (
12-
"An ONNX runtime is required to run embedding models. "
13-
"Install the variant that matches your hardware:\n"
14-
" CPU / Apple Silicon: pip install onnxruntime\n"
15-
" NVIDIA GPU (CUDA): pip install onnxruntime-gpu\n"
16-
" Windows DirectML: pip install onnxruntime-directml\n"
17-
"See https://onnxruntime.ai for the full list of packages."
18-
)
13+
if TYPE_CHECKING:
14+
from typing import Self
1915

2016

21-
def create_onnx_session(model_path: str, **kwargs):
22-
"""Create an ``onnxruntime.InferenceSession``, with a helpful error if the package is missing.
17+
@dataclass
18+
class AstromerInputs(InputTensors):
19+
"""Input tensors for Astromer-family models.
2320
24-
Parameters
25-
----------
26-
model_path : str
27-
Path to the ONNX model file.
28-
**kwargs
29-
Forwarded verbatim to ``onnxruntime.InferenceSession``.
30-
31-
Returns
32-
-------
33-
onnxruntime.InferenceSession
34-
A ready-to-use inference session.
35-
36-
Raises
37-
------
38-
ImportError
39-
If no onnxruntime variant is installed, with installation instructions.
21+
``times`` and ``input`` are float32 arrays of shape
22+
``(n_windows, seq_size, 1)`` ready for ONNX inference. ``mask`` is the
23+
same validity information cast to float32 for the model (1 = valid,
24+
0 = padded), shape ``(n_windows, seq_size, 1)``. ``bool_mask`` (inherited)
25+
is the boolean equivalent, shape ``(n_windows, seq_size)``.
4026
"""
41-
try:
42-
import onnxruntime as ort
43-
except ImportError as exc:
44-
raise ImportError(_ONNX_INSTALL_HINT) from exc
45-
return ort.InferenceSession(model_path, **kwargs)
27+
28+
times: np.ndarray = field(kw_only=True)
29+
input: np.ndarray = field(kw_only=True)
30+
mask_in: np.ndarray = field(kw_only=True)
4631

4732

4833
class _AstromerModel(SingleBandModel):
4934
"""Internal base class for Astromer-family embedding models.
5035
5136
Provides shared preprocessing (per-window zero-mean normalisation) and
5237
ONNX inference logic for all Astromer variants. Concrete subclasses set
53-
:attr:`_HF_REPO` and, if needed, override the default ``reduction``.
38+
:attr:`hf_repo` and, if needed, override the default ``reduction``.
5439
5540
Output shape
5641
------------
@@ -71,8 +56,8 @@ class _AstromerModel(SingleBandModel):
7156

7257
seq_size: int = 200
7358
dtype: type = np.float32
74-
_HF_REPO: str
75-
_OUTPUTS: frozenset[str] = frozenset({"mean", "max", "sequence"})
59+
hf_filename: str
60+
model_outputs: frozenset[str] = frozenset({"mean", "max", "sequence"})
7661

7762
def __init__(
7863
self,
@@ -81,29 +66,54 @@ def __init__(
8166
output: str = "mean",
8267
bands: Sequence[str | int] | None = None,
8368
reduction: str | list[str] | Reduction = "non-overlapping-windows",
84-
time_red_kwargs: dict[str, object] | None = None,
69+
reduction_kwargs: dict[str, object] | None = None,
8570
) -> None:
8671
super().__init__(
8772
session,
8873
bands=bands,
8974
reduction=reduction,
90-
time_red_kwargs=time_red_kwargs,
75+
reduction_kwargs=reduction_kwargs,
9176
)
92-
if output not in self._OUTPUTS:
93-
raise ValueError(f"Unknown output '{output}'. Must be one of: {', '.join(sorted(self._OUTPUTS))}")
77+
if output not in self.model_outputs:
78+
raise ValueError(f"Unknown output '{output}'. Must be one of: {', '.join(sorted(self.model_outputs))}")
9479
self.output = output
9580

81+
def __call__(self, time: ArrayLike, mag: ArrayLike, band: ArrayLike | None = None) -> np.ndarray:
82+
"""Embed a light curve.
83+
84+
Parameters
85+
----------
86+
Parameters
87+
----------
88+
time : array-like, shape ``(n,)``
89+
Observation times in days (e.g. MJD).
90+
mag : array-like, shape ``(n,)``
91+
Magnitudes.
92+
band : array-like, shape ``(n,)``, optional
93+
Band labels, required when ``self.bands`` is not ``None``.
94+
95+
Returns
96+
-------
97+
np.ndarray, shape ``(n_bands, n_subsamples, seq_size, embed_dim)``
98+
Embedding tensor. ``n_bands`` is 1 when ``self.bands`` is ``None``.
99+
100+
Raises
101+
------
102+
ValueError
103+
If ``band`` is provided but ``self.bands`` is ``None``, or vice versa.
104+
"""
105+
return super().__call__(time, mag, band=band)
106+
96107
@classmethod
97108
def from_hf(
98109
cls,
99110
output: str = "mean",
100111
*,
101112
bands: Sequence[str | int] | None = None,
102113
reduction: str | list[str] | Reduction = "non-overlapping-windows",
103-
time_red_kwargs: dict[str, object] | None = None,
104-
providers=None,
105-
sess_options=None,
106-
) -> "_AstromerModel":
114+
reduction_kwargs: dict[str, object] | None = None,
115+
ort_session_kwargs: dict[str, object] | None = None,
116+
) -> Self:
107117
"""Load a model from the HuggingFace Hub.
108118
109119
Downloads (and caches) the ONNX model file, creates an
@@ -117,27 +127,24 @@ def from_hf(
117127
Named ONNX output to return. One of:
118128
119129
* ``"mean"`` (default) — masked mean pooling over valid timesteps,
120-
output shape ``(batch, 256)``
130+
output shape ``(bands, reductions, 1, 256)``
121131
* ``"max"`` — masked max pooling over valid timesteps,
122-
output shape ``(batch, 256)``
132+
output shape ``(bands, reductions, 1, 256)``
123133
* ``"sequence"`` — per-timestep embeddings (no aggregation),
124-
output shape ``(batch, seq_size, 256)``
134+
output shape ``(bands, reductions, 200, 256)``
125135
126-
bands : sequence of str or int, optional
136+
bands : sequence of str or int or None, optional
127137
Ordered band labels to embed. ``None`` (default) treats the whole
128138
light curve as one band.
129139
reduction : str, list of str, or Reduction, optional
130140
Windowing / subsampling strategy. Defaults to
131141
``"non-overlapping-windows"``.
132-
time_red_kwargs : dict, optional
142+
reduction_kwargs : dict or None, optional
133143
Extra keyword arguments forwarded to :func:`reduction_from_str`
134144
when ``reduction`` is given as a string.
135-
providers : list of str, optional
136-
ONNX Runtime execution providers, e.g.
137-
``["CUDAExecutionProvider", "CPUExecutionProvider"]``.
138-
sess_options : onnxruntime.SessionOptions, optional
139-
Advanced session configuration passed directly to
140-
``onnxruntime.InferenceSession``.
145+
ort_session_kwargs : dict or None, optional
146+
Additional keyword arguments forwarded to ``onnxruntime.InferenceSession``:
147+
"sess_options", "providers", "provider_options".
141148
142149
Returns
143150
-------
@@ -149,48 +156,17 @@ def from_hf(
149156
ValueError
150157
If ``output`` is not one of the recognised output names.
151158
ImportError
152-
If ``huggingface_hub`` is not installed, with instructions to
153-
install it or to download the model file manually.
159+
If ``huggingface_hub`` is not installed.
154160
ImportError
155-
If no ``onnxruntime`` variant is installed, with hardware-specific
156-
installation instructions.
161+
If no ``onnxruntime`` variant is installed.
157162
"""
158-
if output not in cls._OUTPUTS:
159-
raise ValueError(f"Unknown output '{output}'. Must be one of: {', '.join(sorted(cls._OUTPUTS))}")
160-
161-
model_prefix = cls._HF_REPO.split("/")[-1]
162-
filename = f"{model_prefix}.onnx"
163-
164-
try:
165-
from huggingface_hub import hf_hub_download
166-
except ImportError as exc:
167-
hf_url = f"https://huggingface.co/{cls._HF_REPO}/resolve/main/{filename}"
168-
raise ImportError(
169-
"huggingface_hub is required to download models from HuggingFace.\n"
170-
"Install it with:\n"
171-
" pip install huggingface-hub\n"
172-
"Or download the model file directly:\n"
173-
f" {hf_url}\n"
174-
"then load it with:\n"
175-
" import onnxruntime as ort\n"
176-
f' {cls.__name__}(session=ort.InferenceSession("/path/to/{filename}"), output="{output}")'
177-
) from exc
178-
179-
model_path = hf_hub_download(repo_id=cls._HF_REPO, filename=filename)
180-
181-
session_kwargs = {}
182-
if providers is not None:
183-
session_kwargs["providers"] = providers
184-
if sess_options is not None:
185-
session_kwargs["sess_options"] = sess_options
186-
187-
session = create_onnx_session(model_path, **session_kwargs)
188-
return cls(
189-
session=session,
163+
return super().from_hf(
164+
cls.hf_filename,
190165
output=output,
191166
bands=bands,
192167
reduction=reduction,
193-
time_red_kwargs=time_red_kwargs,
168+
reduction_kwargs=reduction_kwargs,
169+
ort_session_kwargs=ort_session_kwargs,
194170
)
195171

196172
def preprocess_lc(
@@ -230,7 +206,7 @@ def preprocess_lc(
230206
mask = mask.astype(self.dtype)
231207

232208
idx = (..., np.newaxis)
233-
return AstromerInputs(times=time[idx], input=mag[idx], mask=mask[idx], bool_mask=bool_mask)
209+
return AstromerInputs(times=time[idx], input=mag[idx], mask_in=mask[idx], bool_mask=bool_mask)
234210

235211
def predict_tensors(self, tensors: AstromerInputs) -> np.ndarray:
236212
"""Run the ONNX model on pre-processed tensors and return reduced embeddings.
@@ -248,7 +224,8 @@ def predict_tensors(self, tensors: AstromerInputs) -> np.ndarray:
248224
"""
249225
(raw_embedding,) = self.session.run(
250226
[self.output],
251-
{"input": tensors.input, "times": tensors.times, "mask_in": tensors.mask},
227+
# {"input": tensors.input, "times": tensors.times, "mask_in": tensors.mask},
228+
tensors.asdict(),
252229
)
253230

254231
# Aggregated outputs (mean / max) have shape (n_windows, embed_dim); add SEQUENCE=1 axis
@@ -289,7 +266,8 @@ class Astromer1(_AstromerModel):
289266
Windowing strategy. Defaults to :class:`NonOverlappingWindows`.
290267
"""
291268

292-
_HF_REPO = "light-curve/astromer1"
269+
hf_repo: str = "light-curve/astromer1"
270+
hf_filename: str = "astromer1.onnx"
293271

294272

295273
class Astromer2(_AstromerModel):
@@ -324,4 +302,5 @@ class Astromer2(_AstromerModel):
324302
embeddings on HuggingFace.
325303
"""
326304

327-
_HF_REPO = "light-curve/astromer2"
305+
hf_repo: str = "light-curve/astromer2"
306+
hf_filename: str = "astromer2.onnx"

0 commit comments

Comments
 (0)