Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ First public release.
`cuperiod.batch_periodograms` (CPU process pool or GPU, resumable Parquet/CSV output).
- Multi-band joint modelling for GLS, BLS, and MHAOV.
- Frictionless inputs: arrays, dicts, pandas/astropy/pyarrow tables, and
CSV/ECSV/FITS/Parquet files, with case-insensitive column auto-detection (`ColumnMap`)
and automatic magnitude/flux handling.
CSV/ECSV/FITS/Parquet files, with case-insensitive, **survey-aware** column
auto-detection (`ColumnMap` — ASAS-SN, ASAS-3, ATLAS, CRTS/CSS, ZTF, Pan-STARRS, LSST,
TESS, Kepler, Gaia, MACHO) and domain-aware error pairing, plus automatic
magnitude/flux handling.
- N-best-period peak finding with alias- and harmonic-aware selection.
- `cuperiod` command-line interface: `run`, `batch`, `methods`, `gpu-info`, `grid-info`.
- Per-method settings models with `CUPERIOD_<METHOD>_<FIELD>` environment overrides, and
Expand Down
126 changes: 117 additions & 9 deletions docs/guide/light-curves.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,25 @@ The originating path is recorded in `lc.meta["source"]`.

## Column auto-detection with `ColumnMap`

Astronomers' tables carry the same three quantities under wildly different names. cuPeriod
detects the common spellings case-insensitively, most-specific first, so a table with both
`BJD_TDB` and `JD` picks the corrected time. The detection lists are:

- **Time** — `bjd_tdb`, `bjd`, `hjd`, `btjd`, `mjd`, `jd`, `time`, `date`, `t`
- **Magnitude** — `mag`, `magnitude`, `vmag`, `gmag`, `rmag`, `phot_mag`, `m`
- **Flux** — `pdcsap_flux`, `sap_flux`, `norm_flux`, `rel_flux`, `flux`, `fnu`, `f`
- **Error** — `mag_err`, `e_mag`, `dmag`, `flux_err`, `err`, `sigma`, … (mag & flux spellings)
- **Band** — `band`, `filter`, `phot_filter`, `passband`, `fid`
Light curves will always carry temporal and magnitude/flux measurements, with optional
magnitude/flux errors. cuPeriod detects the common spellings case-insensitively,
most-specific first, so a table with both `BJD_TDB` and `JD` picks the corrected time.
The detection lists are:

- **Time** — `bjd_tdb`, `bjd`, `hjd`, `btjd`, `bkjd`, `mjd`, `hmjd`, `midpointMjdTai`,
`obsTime`, `jd`, `time`, `date`, `t`
- **Magnitude** — `mag`, `magnitude`, `vmag`/`gmag`/`rmag`/`bmag`/`imag`, `psfMag`,
`MAG_0`, `m`
- **Flux** — `PDCSAP_FLUX`, `KSPSAP_FLUX`, `SAP_FLUX`, `psfFlux`, `flux`, `uJy`, `mJy`, …
(corrected/detrended fluxes are preferred over raw)
- **Error** — resolved to match the value's measurement and domain: a flux value pairs
with `flux_err`/`flux_error`/`psfFluxErr`/`duJy`, a magnitude value with
`mag_err`/`magerr`/`dm`/`MER_0`, then the neutral `err`/`error`/`sigma`
- **Band** — `band`, `filter`, `filtercode`, `filterID`, `phot_filter`, `passband`, `fid`

The error is matched to the **same measurement** as the value, so a table that carries
both (e.g. ATLAS's `m`/`dm` and `uJy`/`duJy`) pairs `uJy` with `duJy`, never with the
magnitude error.

When detection isn't enough — ambiguous names, or a column you want to force — pin it with
{class}`~cuperiod.ColumnMap`:
Expand All @@ -82,6 +92,104 @@ Any field left `None` is auto-detected; a non-`None` field is honored verbatim (
raises {exc}`~cuperiod.ColumnResolutionError` if that column is absent). Only `time` and
`value` are required; `error` and `band` are optional.

## Supported surveys

The detection lists cover the standard light-curve products of the major time-domain
surveys, so a downloaded table usually needs **no column hints** — `cup.periodogram(df,
"GLS")` just works:

```{list-table}
:header-rows: 1
:widths: 18 16 22 22 16

* - Survey
- Time
- Value
- Error
- Band
* - ASAS-SN (Sky Patrol)
- `jd` / `hjd`
- `mag` / `flux`
- `mag_err` / `flux_err`
- `phot_filter`
* - ASAS-3
- `HJD`
- `MAG_0`..`MAG_4`
- `MER_0`..`MER_4`
- —
* - ATLAS
- `MJD`
- `uJy` (or `m`)
- `duJy` (or `dm`)
- `F` ¹
* - CRTS / CSS
- `MJD`
- `Mag`
- `Magerr`
- —
* - ZTF
- `mjd` / `HMJD`
- `mag`
- `magerr`
- `filtercode` / `filterID`
* - Pan-STARRS (PS1)
- `obsTime`
- `psfFlux`
- `psfFluxErr`
- `filterID`
* - LSST / Rubin
- `midpointMjdTai` ²
- `psfFlux` ²
- `psfFluxErr` ²
- `band`
* - TESS (SPOC / QLP)
- `TIME`
- `PDCSAP_FLUX` / `KSPSAP_FLUX`
- `*_FLUX_ERR`
- —
* - Kepler (SPOC)
- `TIME`
- `PDCSAP_FLUX`
- `PDCSAP_FLUX_ERR`
- —
* - Gaia (DR3 epoch)
- `time`
- `flux` (or `mag`)
- `flux_error`
- `band`
* - MACHO
- `time` / `mjd`
- `mag` (red/blue)
- `error`
- red / blue ³
* - OGLE
- `HJD`
- magnitude
- error
- — (per file)
```

¹ ATLAS's single-character `F` filter isn't auto-detected (to avoid clashing with a flux
`f`); pass `ColumnMap(band="F")` if you need it for multi-band. The value defaults to the
flux (`uJy`); pin `ColumnMap(value="m")` for magnitudes. ² LSST DP0.2 uses the older
`midPointTai` / `psFlux` / `psFluxErr` / `filterName`, which are also detected. ³ MACHO's
raw per-column header spellings are not consistently documented; if a file uses
non-standard names, pass an explicit {class}`~cuperiod.ColumnMap`.

:::{note}
**Headerless files** — OGLE `.dat` photometry (and some raw ASAS-3 dumps) have no column
names to detect. Read them positionally instead, e.g. with
[`numpy.loadtxt`](https://numpy.org/doc/stable/reference/generated/numpy.loadtxt.html):

```python
import numpy as np
from cuperiod import LightCurve

hjd, mag, err = np.loadtxt("OGLE-LMC-CEP-0001.dat", usecols=(0, 1, 2), unpack=True)
pg = cup.periodogram(LightCurve.from_arrays(hjd, mag, err), "GLS")
```
:::

## Magnitude vs flux: the `Domain`

A value column is either a **magnitude** or a **flux**. cuPeriod infers which from the
Expand Down
87 changes: 79 additions & 8 deletions src/cuperiod/core/columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@
The brightness *domain* (magnitude vs flux) is inferred from the matched value
column when possible (``flux`` → flux, ``mag`` → magnitude) and can always be set
explicitly. Methods that are naturally flux-based (BLS, TLS) convert internally.

The built-in detection lists cover the standard light-curve products of the major
time-domain surveys — ASAS-SN, ASAS-3, ATLAS, CRTS/CSS, ZTF, Pan-STARRS, TESS, Kepler,
Gaia, LSST, and MACHO — so a downloaded table usually needs no column hints. Error
resolution is *domain-aware*: a flux value is paired with a flux error and a magnitude
value with a magnitude error, which disambiguates surveys (e.g. ATLAS) that expose both
``m``/``dm`` and ``uJy``/``duJy`` in one table. Headerless products (OGLE ``.dat``) have
no column names to detect — pass the data as a ``(time, value, error)`` tuple or with an
explicit :class:`ColumnMap`.
"""

from __future__ import annotations
Expand All @@ -30,61 +39,110 @@ class Domain(StrEnum):

#: Time-column name candidates, most-specific first. Barycentric/heliocentric
#: corrected times precede plain JD so a table carrying both picks the better one.
#: Covers JD/HJD/BJD (ASAS, ASAS-SN, OGLE, Gaia), MJD (ATLAS, CRTS, ZTF, Pan-STARRS),
#: BTJD (TESS), BKJD (Kepler), ``midpointMjdTai`` (LSST), and ``obsTime`` (Pan-STARRS).
TIME_NAMES: Final[tuple[str, ...]] = (
"bjd_tdb",
"bjd",
"hjd",
"btjd",
"bkjd",
"mjd",
"hmjd",
"midpointmjdtai",
"midpointtai",
"obstime",
"jd",
"time",
"date",
"t",
)

#: Magnitude value-column candidates.
#: Magnitude value-column candidates. Includes ASAS-3 ``MAG_0`` (first aperture),
#: ``psfMag`` (Pan-STARRS/LSST), and ``Rmag``/``Bmag`` (MACHO red/blue).
MAG_NAMES: Final[tuple[str, ...]] = (
"mag",
"magnitude",
"vmag",
"gmag",
"rmag",
"bmag",
"imag",
"psfmag",
"phot_mag",
"mag_0",
"m",
)

#: Flux value-column candidates.
#: Flux value-column candidates. Includes ``PDCSAP_FLUX``/``SAP_FLUX``/``KSPSAP_FLUX``
#: (TESS/Kepler), ``psfFlux`` (Pan-STARRS/LSST), and ``uJy``/``mJy`` (ATLAS, ASAS-SN).
#: ``uJy`` precedes the bare ``f`` so ATLAS's flux column wins over its ``F`` filter.
FLUX_NAMES: Final[tuple[str, ...]] = (
"pdcsap_flux",
"kspsap_flux",
"sap_flux",
"psfflux",
"psflux",
"psfdiffflux",
"norm_flux",
"rel_flux",
"flux",
"ujy",
"mjy",
"fnu",
"f",
)

#: Error-column candidates (both magnitude and flux error spellings).
ERR_NAMES: Final[tuple[str, ...]] = (
#: Magnitude-error candidates (used when the value column is a magnitude): ``magerr``
#: (ZTF, CRTS, ASAS-SN), ``dm`` (ATLAS), ``MER_0`` (ASAS-3), ``e_mag``, ``dmag``, ...
MAG_ERR_NAMES: Final[tuple[str, ...]] = (
"mag_err",
"magnitude_err",
"mag_error",
"magerr",
"e_mag",
"merr",
"dmag",
"dm",
"mer_0",
)

#: Flux-error candidates (used when the value column is a flux): ``flux_error`` (Gaia),
#: ``*SAP_FLUX_ERR`` (TESS/Kepler), ``psfFluxErr`` (Pan-STARRS/LSST), ``duJy`` (ATLAS).
FLUX_ERR_NAMES: Final[tuple[str, ...]] = (
"flux_err",
"flux_error",
"sap_flux_err",
"pdcsap_flux_err",
"kspsap_flux_err",
"psfflux_err",
"psffluxerr",
"psfluxerr",
"psfdiffflux_err",
"psfdifffluxerr",
"e_flux",
"dujy",
)

#: Domain-neutral error spellings, tried after the domain-specific lists.
GENERIC_ERR_NAMES: Final[tuple[str, ...]] = (
"err",
"error",
"sigma",
)

#: Band/filter-column candidates (used only by multi-band ingestion).
#: All error spellings. Resolution is domain-aware (see :meth:`ColumnMap.resolve`): a
#: flux value pairs with a flux error and a magnitude value with a magnitude error.
ERR_NAMES: Final[tuple[str, ...]] = MAG_ERR_NAMES + FLUX_ERR_NAMES + GENERIC_ERR_NAMES

#: Band/filter-column candidates (used only by multi-band ingestion): ``filtercode``
#: (ZTF), ``filterID`` (Pan-STARRS), ``band`` (Gaia/LSST), ``fid`` (ZTF alerts).
BAND_NAMES: Final[tuple[str, ...]] = (
"band",
"filter",
"filtercode",
"filterid",
"filtername",
"phot_filter",
"passband",
"fid",
Expand Down Expand Up @@ -139,9 +197,10 @@ def infer_domain(value_column: str) -> Domain | None:
explicit domain).
"""
low = value_column.lower()
if any(tok in low for tok in ("flux", "fnu")):
flux_tokens = ("flux", "fnu", "ujy", "mjy", "njy", "nmgy", "jansky")
if any(tok in low for tok in flux_tokens):
return Domain.FLUX
if "mag" in low or low in {"m", "vmag", "gmag", "rmag"}:
if "mag" in low or low in {"m", "vmag", "gmag", "rmag", "bmag", "imag"}:
return Domain.MAGNITUDE
return None

Expand Down Expand Up @@ -204,7 +263,6 @@ def resolve(
value = _match(
"value", self.value, value_candidates, available, lower_to_actual
)
error = _match("error", self.error, ERR_NAMES, available, lower_to_actual)
band = _match("band", self.band, BAND_NAMES, available, lower_to_actual)

if time is None:
Expand All @@ -220,6 +278,16 @@ def resolve(
f"{available}"
)
resolved_domain = domain or infer_domain(value) or Domain.MAGNITUDE
# Resolve the error for the *same* measurement as the value: first try names
# derived from the value column (so ``PDCSAP_FLUX`` pairs with
# ``PDCSAP_FLUX_ERR`` and ``uJy`` with ``duJy``), then the spellings for the
# value's domain, then domain-neutral spellings. This keeps a flux value with a
# flux error and a magnitude value with a magnitude error.
v = value.lower()
derived = (f"{v}_err", f"{v}err", f"{v}_error", f"{v}error", f"e_{v}", f"d{v}")
domain_err = FLUX_ERR_NAMES if resolved_domain is Domain.FLUX else MAG_ERR_NAMES
err_candidates = derived + domain_err + GENERIC_ERR_NAMES
error = _match("error", self.error, err_candidates, available, lower_to_actual)
return ResolvedColumns(
time=time, value=value, error=error, band=band, domain=resolved_domain
)
Expand All @@ -228,7 +296,10 @@ def resolve(
__all__ = [
"BAND_NAMES",
"ERR_NAMES",
"FLUX_ERR_NAMES",
"FLUX_NAMES",
"GENERIC_ERR_NAMES",
"MAG_ERR_NAMES",
"MAG_NAMES",
"TIME_NAMES",
"ColumnMap",
Expand Down
Loading
Loading