Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ztf-variable-finder

A rigorous, zero-AI pipeline for mining public ZTF light curves — and an honest field report on why solo variable-star discovery from public survey data is walled in 2026. The one path that isn't walled: O-C timing of known eclipsing binaries.

license python tests no-ML

Only photometry, statistics, periodograms, and catalog cross-matching — no machine learning, no black boxes. Every layer is offline-testable; network I/O is isolated at the edges. This repository is as much a reproducible negative result as it is a toolkit: if you were about to spend a month hunting new variable stars in ZTF by yourself, read the decision log first — it will save you the month.


TL;DR

  • Discovering / characterizing NEW variable stars, solo, from public ZTF is walled in every accessible regime. Not a code bug — the walls are physical (see the terrain map below). Every statistically significant candidate we produced was killed by an eyeball / magnitude / independent-instrument check. Nothing false was ever submitted.
  • One path hits no wall and works today: O-C timing of known eclipsing binaries. Fold a catalogued EB on 7 years of ZTF, refine its period to ~ppm, and build an Observed−Calculated (O-C) diagram to look for a changing period (mass transfer / a third body). This produces a real, submittable contribution to O-C databases (BAV, JAAVSO).
  • Built, adversarially reviewed, 59 tests green, defenses proven on real data.

It works: known eclipsing binaries on ZTF

A bright, isolated EA binary (CSS_J114242) folded on 577 clean ZTF epochs — a textbook double eclipse, catalog period recovered to ~4 ppm, O-C flat/stable over 5 years:

Phased light curve of CSS_J114242 showing a textbook double eclipse

The screening tool then builds a primary-locked O-C diagram for each candidate and ranks by non-flatness. Below, the O-C ranking metric is (deliberately shown being) fooled by a single poorly-sampled segment — an honest illustration of a known limitation, not a real period change (see next steps):

O-C screening output showing a single-segment outlier

Honest reward note: refining a known EB's ephemeris credits you as a contributor on a catalogued object (and a possible author line in an O-C database) — a real, citable record with your name. It is not "discovering" or "naming" a star (which is not a real thing for any path — see the decision log).


Why solo discovery is walled (the field report)

The clean, bright, periodic sky is ~85–95% already catalogued — Gaia DR3 (~9.5M variables), Chen et al. 2020 (~781k from ZTF), and SCoPe (scored ~210M ZTF light curves) swept it. What's left is walled from the other side:

Regime Wall
bright, uncrowded, any class ~100% already in VSX
faint, short-period diurnal-alias-dominated + no reachable independent confirmer
plane LPVs (dense, uncharacterized) bright Mira saturates ZTF + blended in ASAS-SN
off-plane LPVs too sparse to be worth scanning

The full chronology — blind scans → cross-band confirmation → adversarial review → the alias trap → the LPV pivot → the physical wall — is in docs/DECISION-LOG.md. The red-team reports that shaped it are in docs/adversarial/.


Install

Python 3.14. Clone, create a virtualenv, install requirements.txt:

git clone <your-fork-url> ztf-variable-finder && cd ztf-variable-finder
python3.14 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt

Gotcha: always invoke tools in module form — .venv/bin/python -m pytest, .venv/bin/python -m pip …. The venv's console-script shebangs can go stale after a directory rename. Set PYTHONPATH=src so import ztfvar resolves.

Run the tests:

PYTHONPATH=src .venv/bin/python -m pytest -q     # 59 passing

Quickstart

Screen known eclipsing binaries for a changing period (the working path):

PYTHONPATH=src .venv/bin/python scripts/screen_eb.py \
  --ra 173.0 --dec 25.0 --radius 8 --vmin 13 --vmax 15.5 --type EA --out eb_out

It fetches ZTF per EB, keeps clean-eclipse ones (via PDM), refines the period, builds a primary-locked O-C, ranks by non-flatness, and writes dossiers (phased + O-C plots) for the top candidates. Eyeball each top O-C plot — a genuine smooth slope or parabola is the candidate; a single jumping point is not.

Characterize / discovery scan (walled, kept for reproducibility):

PYTHONPATH=src .venv/bin/python scripts/characterize.py --source gaia-lpv \
  --ra 288.0 --dec 10.0 --radius 0.25 --gmin 14 --gmax 17 \
  --min-period 20 --max-period 1000 --archive archive_lpv --out lpv_out

Runs are resumable (a local archive skips already-fetched data and survives network timeouts). Add --vsx-mode require-unperiodized to target VSX objects that lack a period. See docs/DECISION-LOG.md for why these paths are walled.


How the rigor works (the reusable part)

These defenses are the transferable value of the repo — they apply to any ground-based time-domain survey:

  • Cross-band ≠ independent confirmation. ZTF g/r/i share the same nights, so the diurnal sampling alias is coherent across bands. Only a different instrument (ASAS-SN / ATLAS) or masking the alias comb can rule it out. find_period(avoid_aliases=True) masks the solar/sidereal harmonic comb; vetting.is_alias rejects survivors.
  • Independent-instrument confirmation (asassn.py) is 3-state: True / False / None — a failed query never masquerades as agreement.
  • Calibrated significance — reduced χ² with an error floor, plus a bootstrap FAP, so red noise and underestimated errors don't manufacture detections.
  • Injection–recovery (injection.py) measures the real false-positive rate and completeness on the actual cadence, not an idealized one.
  • Fail closeddecide_novelty treats a failed catalog query as unknown, never NOVEL; masked IPAC nulls are dropped, not read as variability.

Architecture (src/ztfvar/)

Module Responsibility
lightcurve.py LightCurve dataclass + clean() (drops bad catflags / nan / zero-error epochs)
variability.py reduced χ² (with error floor), MAD, Stetson J, is_variable
period.py Lomb–Scargle find_period (analytic / bootstrap FAP, avoid_aliases comb mask), phase fold, classify
oc.py the working path — PDM refine_period, oc_points, oc_trend (O-C diagram)
source.py group_by_position (per-band oids → physical source), analyze_source (cross-band confirmation)
novelty.py VizieR / SIMBAD cross-match; decide_novelty (fails closed); query_vsx (returns each match's period)
vetting.py alias / band-consistency / FAP rejection
asassn.py independent ASAS-SN Sky Patrol period confirmation (3-state)
ephemeris.py T0 (epoch of extremum) + amplitude / type → VSX-grade entry
sample.py ZTF fetch — fetch_ztf_lightcurves_fast (resolve oid via TAP → fetch by ID, not the slow POS cone)
prefilter.py ZTF DR24 object-stats via IRSA TAP → download only suspects (~15–30× less data)
archive.py persistent local light-curve cache (never re-download; reprocess offline)
gaia_targets.py Gaia DR3 parent samples (variable / LPV / eclipsing-binary)
injection.py injection–recovery (false-positive rate + completeness on real cadence)
dossier.py per-target result dossier (period + T0 + type + confirmation + plot)

Scripts (scripts/): screen_eb.py (the working O-C path), characterize.py (characterization orchestrator), scan_grid.py (blind / prefilter scan), reprocess.py (re-run detection over the archive, no network), plus validate_known.py, injection_recovery.py, hunt_field.py, scrutinize.py.


What's next

The immediate, high-value fix and the path to a real result:

  1. Harden the O-C ranking metric. oc.oc_trend's raw swing (max − min) is fooled by a single outlier segment — exactly the plot shown above. Rank by the quadratic curvature significance and/or a linear-slope significance instead, and sigma-clip single-point segments. A real period change is a smooth slope or parabola, never one jumping point.
  2. Scale the EB screening. Period changes are rare — a small fraction of EBs — so you must screen hundreds to expect one. Add caching to screen_eb.py (reuse archive.LightCurveArchive) so large runs are resumable, then sweep bright, isolated EA binaries across several fields.
  3. Package refined ephemerides. Even without a period change, the 7-year ZTF baseline gives ppm-level periods for known EBs — a genuine, submittable O-C-database contribution.

Detailed developer state and gotchas live in HANDOFF.md.


Constraints (design rules, do not break)

  • Deterministic, zero AI/ML. No fetching light curves from pixels — consume calibrated ZTF light curves only.
  • Lose no data useful for a discovery. The archive persists everything; ML brokers may be used for triage but never as a filter that discards.
  • Any VSX / O-C-database submission is outward-facing and human-only. Eyeball every phased and O-C plot first, and let a human press submit.

Acknowledgments

Built on public data and open tools: the Zwicky Transient Facility (ZTF) and IRSA, ASAS-SN Sky Patrol, Gaia DR3, VizieR / SIMBAD, the AAVSO VSX, and astropy / astroquery / numpy / scipy.

License

MIT © 2026 psd (fraa.psd@gmail.com)

About

Zero-AI pipeline for mining public ZTF light curves — and an honest field report on why solo variable-star discovery is walled in 2026. Working niche: O-C timing of known eclipsing binaries.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages