Fold, embed, probe.
Compare foundation-model embeddings, hand-crafted features and catalogue columns
of astronomical light curves, under identical conditions.
origami answers one kind of question: which representation of a light curve carries
the information you need, and how robust is it? It puts every candidate on the same
footing:
- the same objects;
- the same (sub)sampled epochs;
- the same folds;
- the same simple probe.
Candidates can be embeddings from frozen time-series foundation models, classical features, values from a catalogue, or your own model. Typical questions:
- Do embeddings from a pretrained model add anything to Fourier parameters for predicting a physical quantity?
- Which model separates my classes best, and does that survive when curves have 20 epochs instead of 200?
- Does a representation learned on bright objects transfer to faint ones, or from one survey to another?
origami is a benchmarking and selection tool. It does not train foundation models, and
it is not a classifier for producing catalogues.
pip install "origami-astro[features,chronos]"
Highlights
- Light curves: irregular, single-band, any length, each with a provenance tag (survey, band).
- Models: 31 checkpoints from 10 model families behind one interface (Chronos, Chronos-Bolt, Chronos-2, Moirai, MOMENT, Mantis, Toto 2.0, Timestral, Astromer, StarCLR). Weights are downloaded on first use.
- Tasks: regression, classification, clustering and anomaly detection. Evaluation can use cross-validation, an explicit train/test split, or metrics per group.
- Robustness tests: controlled epoch subsampling, identical for every representation.
- Clusters: a YAML + command-line workflow runs one SLURM array task per model, works on compute nodes without internet, and collects everything into tables at the end.
Contents
- Quick start
- Concepts
- Command-line workflow
- Models
- Installation and environments
- Good practice
- Adding a model
- Licence
Quick start
import origami as ori
import pandas as pd
cat = pd.read_csv("catalog.csv") # one row per object: id, period, target, ...
lcs = ori.LightCurves.from_csv_dir("light_curves/", # one CSV per object: time, mag, err
ids=cat["id"], err_col="mag_err", survey="ztf", band="r")
lcs = lcs.with_periods(cat["period"]) # needed by folded inputs and Fourier features
reps = {
"chronos": ori.Embedder("chronos-bolt-small", input="folded-grid"),
"mantis": ori.Embedder("mantis-v2", input="folded-grid"),
"fourier": ori.HandCrafted("fourier"),
"physical": ori.HandCrafted("physical"),
"period": ori.Catalog(cat, ["period"]),
}
res = ori.compare(lcs, cat["target"], reps, task="regression",
combine=[("chronos", "physical"), ("mantis", "physical"), ("fourier", "period")],
subsample_epochs=["all", 50, 20], seeds=[0, 1, 2])
print(res.table()) # primary metric (RMSE): rows = representations, columns = epoch levels
res.records # every run with all metrics, as a DataFrame
Concepts
Light curves
LightCurves holds a set of single-band curves. Each curve has:
- its own time, magnitude and error arrays (any length);
- an identifier;
- a provenance tag (
survey,band); - optionally a period, a reference epoch and the period's origin (
"catalog"or"estimated").
| constructor | input |
|---|---|
LightCurves.from_csv_dir(dir, ids=..., ...) |
one CSV per object; magnitudes with errors, or fluxes (errors are then converted) |
LightCurves.from_frame(df, id_col=..., ...) |
a long table, one row per observation |
LightCurves.from_arrays(times, mags, errs, ...) |
lists of arrays |
Other operations:
lcs.with_periods(periods, epochs=None)attaches periods.lcs.subsample(k, seed)keeps k random epochs per curve. The selection is reproducible and does not depend on the other curves in the set.
Input transforms
How a curve becomes the sequence a model sees. Set it per embedder with input=.
input |
sequence | needs a period |
|---|---|---|
raw |
magnitudes in time order, z-normalised, with real timestamps | no |
folded |
magnitudes sorted by phase, z-normalised | yes |
folded-grid |
phase-folded curve resampled on a regular grid (default 200 points), z-normalised, maximum light at phase 0 | yes |
padded-starembed |
last 200 points, zero left-padding: the protocol of StarEmbed, for comparison | no |
How folded-grid resamples the curve:
- It uses a smoothing spline, with automatic smoothing chosen by GCV.
- If that fit collapses to a near-linear curve, it falls back to a Fourier series whose number of harmonics is chosen by BIC.
- The curve is aligned on maximum light, so the sequence does not depend on the reference epoch.
Every transform also passes, for each curve, the times and the magnitude scale removed by z-normalisation. Models that use timestamps receive the times; models trained on unscaled magnitudes get the scale back.
Representations
Anything with a name and a transform(lcs) -> array (n_curves, dim) method.
| class | what it gives |
|---|---|
Embedder(model, input=...) |
frozen foundation-model embedding: encoder states mean-pooled over the positions that hold real data; cached on disk |
HandCrafted("light-curve") |
about 30 period-free statistics from the light-curve package |
HandCrafted("fourier", n_harmonics=3) |
Fourier amplitudes and cos/sin of φ21, φ31 on the folded curve |
HandCrafted("physical") |
log period and folded peak-to-peak amplitude |
Catalog(table, columns) |
columns of an external table, row-aligned with the curves |
| your own object | e.g. an autoencoder: anything implementing transform(lcs) |
combine=[("a", "b"), ...] evaluates concatenations of representations. In distance-based
tasks each concatenated block is weighted by 1/√width, so a narrow feature block is not
outweighed by a wide embedding.
Tasks and metrics
task |
probe | metrics |
|---|---|---|
regression |
standardised ridge regression (RidgeCV) | RMSE, MAE |
classification |
standardised logistic regression with balanced class weights | balanced accuracy, macro F1 |
clustering |
k-means (k = number of classes), HDBSCAN, or CLiMB | ARI, NMI |
anomaly |
kNN distance or isolation forest, fitted on the normal objects of the training folds (y = 1 for anomalies) |
AUROC, average precision |
The probes are deliberately simple and fixed, so that results measure the representation and not the model on top of it.
Evaluation options of compare:
folds: defaults to 5-fold cross-validation, stratified for classification and anomaly. Pass your own fold index to reuse an existing protocol.split=(train_mask, test_mask): train on one subset and score another, e.g. bright versus faint, or one survey versus another.groups=labels: also report metrics per group, e.g. magnitude bins or classes. Read them withres.group_table().subsample_epochsandseeds: thin every curve to the given numbers of epochs, identically for all representations.- Task options are passed through, e.g.
method="hdbscan",detector="isolation-forest"orpca=20.
Command-line workflow
For large runs, especially on clusters whose compute nodes have no internet access, an experiment is described by one YAML file:
name: my_experiment
output: /path/to/runs/my_experiment
data:
light_curves: /path/to/light_curves # one CSV per object, "{id}.csv"
catalog: /path/to/catalog.csv # one row per object
id_col: id
time_col: time
mag_col: mag
err_col: mag_err # or flux_col + flux_err_col
survey: ztf
band: r
period_col: period # optional
epoch_col: epoch # optional
fold_col: fold # optional, otherwise k-fold
representations:
bolt: {model: chronos-bolt-small, input: folded-grid}
mantis: {model: mantis-v2, input: folded-grid}
moirai: {model: moirai-2.0-small, input: folded-grid, setup: "source ~/venvs/origami-moirai/bin/activate"}
astromer: {model: astromer2-macho, input: raw}
fourier: {features: fourier}
physical: {features: physical}
period: {catalog: [period]}
my_model: {python: "my_package.encoders:MyEncoder", path: /path/to/my_package, kwargs: {checkpoint: model.pt}, version: v1}
combine: [[bolt, physical], [mantis, physical], [fourier, period]]
tasks:
- {task: regression, target: my_target}
- {task: classification, target: class}
- task: classification
target: class
split: {train: "mag < 19", test: "mag >= 19"} # pandas queries, or {column, train, test}
groups: {column: mag, bins: [16, 18, 19, 20, 21]}
- {task: clustering, target: class, method: hdbscan}
subsample_epochs: [all, 50, 20]
seeds: [0, 1, 2]
origami models # available models (licence, size, install, installed?)
origami prefetch exp.yaml # on a machine with internet: download all weights
origami slurm exp.yaml --account ACC --partition PART \
--setup "module load python" --setup "source ~/venvs/origami/bin/activate"
sbatch origami_my_experiment.sbatch # one array task per model, each runs `origami embed`
origami status exp.yaml # which embeddings are stored / missing
origami collect exp.yaml # run all tasks, write results/*.csv and summary.md
What each step does:
origami embed exp.yaml --representation NAMEcomputes one representation for every epoch level and seed. It also works without SLURM.- Embeddings are stored under
output/embeddings. Their key covers the model and its settings, the input transform, the code version and the data, including subsampling and seed. collectnever runs a model. If an embedding is missing, it says which representation to rerun.- User models (
python:) are computed and cached like the foundation models. Changeversionwhenever your weights change. setup:gives a representation a shell line run by its own array task, for models that need a separate environment.- Offline nodes: the generated job sets
HF_HUB_OFFLINE=1.
Models
origami models prints the live list and whether each package is installed.
| family | checkpoints | params | licence of the weights | uses timestamps | extra |
|---|---|---|---|---|---|
| Chronos (T5) | chronos-t5-tiny/mini/small/base |
8M–200M | Apache-2.0 | no | chronos |
| Chronos-Bolt | chronos-bolt-tiny/small |
9M–48M | Apache-2.0 | no | chronos |
| Chronos-2 | chronos-2-small, chronos-2 |
28M, 120M | Apache-2.0 | no | chronos |
| Moirai | moirai-1.1-small/base, moirai-2.0-small |
11M–91M | CC-BY-NC-4.0 (non-commercial) | no | moirai |
| MOMENT | moment-1-small/base/large |
38M–346M | MIT | no | moment |
| Mantis | mantis-8m, mantis-plus, mantis-v2, mantis-v2-cls |
4M–8M | Apache-2.0 | no | mantis |
| Toto 2.0 | toto-2.0-4m/22m/313m/1b/2.5b |
4M–2.5B | Apache-2.0 | no | toto |
| Timestral | timestral-5m/28m/123m/209m |
5M–209M | Modified MIT: excludes companies above $20M monthly revenue | no | timestral |
| Astromer | astromer1-macho, astromer2-macho, astromer1-ztfdr20 |
0.7M–4M | CC-BY-4.0; GPL-3.0 for astromer1-ztfdr20 |
yes | astromer |
| StarCLR | starclr |
19M | MIT | yes | starclr |
Notes on specific families:
- Astromer runs through the ONNX exports of the light-curve project, so it needs neither TensorFlow nor the original package.
- StarCLR uses a PyTorch re-implementation of the official inference code. Its weights
come from Zenodo, are verified by checksum,
and are stored in
$ORIGAMI_WEIGHTS. - Toto, Timestral and Moirai have no public embedding API. Their states are read from internal modules, stated in each plugin's docstring, so their package versions are pinned.
- Not included yet:
- models that require
trust_remote_code; - models without public pretrained weights.
- models that require
The weights are not part of origami. They are downloaded from their original
sources, and their licences govern your use of them. Check the non-commercial,
copyleft and revenue clauses marked above before using those models outside academic
research.
Installation and environments
pip install "origami-astro[features,chronos,mantis,astromer,starclr]"
| extra | adds |
|---|---|
features |
hand-crafted features (light-curve) |
chronos, mantis, astromer, starclr, timestral |
those model families (they can share one environment) |
moirai, moment, toto |
those model families, each in its own environment (see below) |
dev |
tests (pytest) |
Some model packages pin incompatible versions:
| environment | extras | reason |
|---|---|---|
| main | features, chronos, mantis, astromer, starclr, timestral |
— |
| moirai | features, moirai |
uni2ts 2.0 pins torch<2.5 and numpy~=1.26 |
| moment | features, moment |
momentfm pins transformers==4.33.3 and numpy==1.25.2 |
| toto | features, toto |
toto2 needs Python ≥ 3.12 |
In the YAML workflow, point each representation to its environment with setup:.
Timestral is not on PyPI; install it from GitHub:
pip install "timestral @ git+https://github.com/mistralai/timestral.git@a534bdbd244d64f3d9d743f46c7bf6fc330ed4ff"
On clusters, build environments on top of the site's Python/PyTorch module
(python -m venv --system-site-packages ...), so that only the model packages are added.
Environment variables:
ORIGAMI_CACHE: embedding cache in interactive use (the CLI stores embeddings underoutput/embeddings).ORIGAMI_WEIGHTS: where weights not hosted on Hugging Face are stored.HF_HUB_OFFLINE=1: use only weights that are already downloaded.
Requires Python ≥ 3.10.
Good practice
- Compare input transforms, not only models. For periodic sources, folded and raw inputs can give very different results with the same model. Each model also expects data that looks like its pre-training data: scale, flux versus magnitude, regular versus irregular sampling.
- Put back what normalisation removes. Z-normalised, folded inputs carry no period and
no amplitude. Combine embeddings with
HandCrafted("physical")or catalogue values before comparing them with features that contain this information. - Mind collapsed embeddings. Some models map all objects to nearly parallel vectors,
with the information in small residual directions. Standardised probes, which
origamialways uses, recover it; raw distances do not. - Do not assume larger checkpoints are better. Compare sizes within a family.
- Report variability. Use several
seedsand fixedfolds, and check that differences between representations exceed the fold-to-fold scatter before ranking them. - Check where the labels come from. If the target was derived from light-curve features (e.g. a quantity computed from Fourier parameters), a representation can score well by re-learning that relation. Validate on independently measured labels when possible.
Adding a model
Add one file to origami/models/; it is discovered automatically.
MODELS = {"my-model": dict(family="my", hf_id="org/my-model", license="MIT", extra="my",
context=512, params="10M", uses_time=False)}
class Encoder:
def __init__(self, spec, device=None):
... # heavy imports here, not at module level
def encode(self, values, masks, times, batch_size=256, scales=None): # `scales` is optional
... # return (n_curves, dim): states mean-pooled over positions where mask is True
Add a test in tests/test_model_<name>.py that checks:
- shape, finite values and determinism;
- that a curve's embedding does not change when it is batched with longer curves.
Licence
origami is released under the MIT licence. Model weights are downloaded from
their original sources and keep their own licences (see Models).
If you use origami in your work, please cite this repository and the papers of the
models you use.
Metadata
Release files for origami-astro 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| origami_astro-0.1.0.tar.gz | 58.7 kB | With provenance | Details for origami_astro-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| origami_astro-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for origami_astro-0.1.0-py3-none-any.whl |
Total release size: 110.3 kB
Release files / origami_astro-0.1.0.tar.gz
| Download URL | origami_astro-0.1.0.tar.gz |
|---|---|
| Size | 58.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4ebd9041837d94e0ec80f265a054754261e510e347dcbf7e0064fe09a77c4b71
|
|
BLAKE2b-256 checksum How to use checksums |
12fb8ada81719cf308837ee4e237cbf876a0a32b42c7307537e42881a54835e0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logRelease files / origami_astro-0.1.0-py3-none-any.whl
| Download URL | origami_astro-0.1.0-py3-none-any.whl |
|---|---|
| Size | 51.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8aaedebb28980f82446842c951df39e99a3321a7d0a588c83efc56d3eb8dfb2a
|
|
BLAKE2b-256 checksum How to use checksums |
0a3cd631ec060e581f5741b46cf1166b99e3a06a9db1b443677aa01e3ec7ac20
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logSecurity for origami-astro 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for origami_astro-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 23, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.