Rotation Periods and Measured Confidence¶
adam_core.photometry can recover an asteroid’s rotation period from sparse,
multi-band photometry and, crucially, return a measured confidence verdict
instead of a bare number. The estimator fits a truncated-harmonic Fourier model to
the distance-reduced, light-time-corrected lightcurve, searches a frequency grid,
clusters harmonic aliases, and classifies the outcome.
The headline is the verdict, not the period: the API is built so that a caller can tell, per object, whether to trust the number.
What You Get Back¶
Every solve returns a one-row RotationPeriodResult
whose fields fall into three groups:
The answer –
period_hours/period_days,frequency_cycles_per_day, an uncertainty interval (period_lower_days/period_upper_days/relative_period_uncertainty), andalternate_period_days(the other candidates considered – this is where harmonic aliases live).How sure it is –
period_verdictandreliability_code(read these first; see below).Why –
confidence_flagsandinsufficiency_reasons(machine-readable rationale), plus diagnostics you can sanity-check yourself:amplitude_snr,phase_coverage_fraction,n_rotations_spanned,n_observations.
Confidence Model and Guarantees¶
period_verdict takes one of
three values, and it is a decision guide:
single_period– one period is believed. In validation against LCDB/DAMIT standard-candle asteroids these calls are correct the large majority of the time. Use the value directly.period_family– there is a real signal, but a harmonic ambiguity (typically 2x / 0.5x) cannot be ruled out. The reported period may be off by an integer factor; consultalternate_period_daysfor the candidate family.insufficient_data– the data cannot responsibly support a period. The period fields areNaN;insufficiency_reasonsgives a first reason as to why a reliable period could not be fit (too few observations, too little phase coverage, spans too few rotations, …).
reliability_code mirrors the LCDB “U” quality scale as a string –
"3" (secure), "2" (some ambiguity), "1" (weak). It is deterministic
from the verdict (single_period -> "3", period_family -> "2",
insufficient_data -> "1"). Do not sort or compare it numerically.
- Note that
single_periodis high-confidence but not a zero-alias guarantee. A confident call can, rarely, be a harmonic alias of the true period. If a period that is wrong by an integer factor would be costly, cross-check a
single_periodresult againstalternate_period_daysandreliability_coderather than treating it as infallible.
One disclosed assumption: when the best fit is a single-peaked fold whose only
surviving ambiguity is the half/full octave, the solver doubles the period on
the two-peaks-per-rotation shape prior and, by default, claims
single_period with the confidence flag doubled_fold_assumed. Measured
on the LCDB/DAMIT standard-candle sets this raises confident claims (76 to 83
pooled) with strict precision improving and no new wrong-family claim; on the
Rubin survey regression it raises claims from 6 to 11 of 76, all strictly
correct. A genuinely single-peaked (spheroidal) body would make such a claim a
2x alias, so pass claim_doubled_fold=False to keep these cases as
period_family (reason single_max_alias). Filter on the flag to treat
them separately in bulk results.
Data Requirements¶
The solver works on RotationPeriodObservations: one
row per photometric measurement, carrying the observing geometry so the intrinsic
rotational variation can be isolated.
Required per row:
time,mag,r_au(heliocentric distance),delta_au(observer distance),phase_angle_deg.Optional:
mag_sigma(per-point uncertainty),filter(multi-band is fit jointly with per-band offsets),session_id(per-night labels used for session-offset handling).Reductions are applied internally: distances are removed via
mag - 5 * log10(r_au * delta_au)and times are light-time corrected.
Estimating From Arrays¶
The most direct path: build the observations table and call the estimator.
import numpy as np
from adam_core.photometry import (
RotationPeriodObservations,
estimate_rotation_period,
)
from adam_core.time import Timestamp
observations = RotationPeriodObservations.from_kwargs(
time=Timestamp.from_mjd(mjd, scale="tdb"),
mag=mag,
mag_sigma=mag_sigma, # optional
filter=filters, # optional; e.g. ["g", "r", "g", ...]
session_id=session_ids, # optional; per-night labels
r_au=r_au,
delta_au=delta_au,
phase_angle_deg=phase_angle_deg,
)
result = estimate_rotation_period(observations)
verdict = result.period_verdict[0].as_py()
if verdict == "single_period":
print(f"P = {result.period_hours[0].as_py():.3f} h "
f"(reliability {result.reliability_code[0].as_py()})")
elif verdict == "period_family":
print("period family; candidates:",
result.alternate_period_days[0].as_py())
else:
print("insufficient:", result.insufficiency_reasons[0].as_py())
Estimating From Detections and Exposures¶
If you are already working with adam_core observation primitives, the geometry
is derived for you from aligned detections, exposures, and heliocentric object
coordinates.
from adam_core.photometry import estimate_rotation_period_from_detections
# object_coords must be heliocentric (origin=SUN), row-aligned with detections.
result = estimate_rotation_period_from_detections(
detections=detections,
exposures=exposures,
object_coords=object_coords,
)
The same geometry pipeline is available directly as
RotationPeriodObservations.from_point_source_observations(detections, exposures,
object_coords) if you want the observations table rather than a solve.
Survey Scale: One Row Per Object¶
For many objects at once, the grouped wrapper solves per object id and returns a
GroupedRotationPeriodResults table.
from adam_core.photometry import estimate_rotation_period_from_detections_grouped
grouped = estimate_rotation_period_from_detections_grouped(
detections=detections,
exposures=exposures,
object_coords=object_coords,
object_ids=object_ids,
)
# grouped.object_id -> the object ids
# grouped.result -> a RotationPeriodResult column, one row per id
The grouped API never silently drops an object: an object whose solve fails on
expected bad/insufficient data comes back as an insufficient_data row carrying a
solve_error flag, so the output always has exactly one row per distinct input
id. Unexpected (programmer/contract) errors are re-raised with the offending object
id attached rather than swallowed.
Multiple Apparitions: Solve Each, Keep the Most Confident¶
If your photometry spans more than one apparition (observing seasons separated
by months), do not pool it into a single solve. Apparitions differ in viewing
aspect, noise, and nightly cadence, so each has its own alias structure – and
one of them often samples the rotation cleanly where the densest one locks onto
a sampling alias or hedges. estimate_rotation_period_best_apparition
partitions the observations at gaps longer than apparition_gap_days
(default 120), solves each apparition independently, and returns the most
confident result (verdict rank, then amplitude SNR; the rule never sees any
reference answer).
from adam_core.photometry import estimate_rotation_period_best_apparition
result = estimate_rotation_period_best_apparition(observations)
# result.confidence_flags[0] includes "apparition_selected_<k>_of_<n>"
Measured on the 118-object LCDB standard-candle calibration set, this policy
raised confident (single_period) results from 35 to 43 objects while the
strict precision of those claims improved (0.80 to 0.84) and no new
wrong-family claim appeared. The safety is measured on that set, not
structural: a clean apparition can still be an alias, so the verdict semantics
are unchanged. Requiring a second apparition to corroborate before claiming was
also tested and is deliberately not applied: on the calibration set it only
removed correct claims, because an object’s systematic harmonic aliases repeat
across apparitions and corroboration therefore does not catch them.
Performance¶
The defaults are validated and correct out of the box. The solver has an optional JAX backend that is numerically identical to the NumPy path but faster on large frequency grids:
result = estimate_rotation_period(
observations,
search_fidelity="validated_staged", # default; coarse-then-refine on big grids
exact_evaluation_backend="jax", # ~2-5x faster than "numpy", same numbers
)
session_mode (default "auto") controls how multi-night magnitude offsets
are handled; the default decides per object whether to fit per-session offsets.
Entry Points¶
estimate_rotation_period: you already have a per-object RotationPeriodObservations table.estimate_rotation_period_from_detections: single object, from detections / exposures / heliocentric coordinates.estimate_rotation_period_from_detections_grouped: many objects; guarantees one result row per id.estimate_rotation_period_best_apparition: multi-apparition photometry of one object; solves each apparition and keeps the most confident result.RotationPeriodObservations.from_point_source_observations: build the observations table (e.g. to inspect or reuse) without solving.