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.
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.
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.RotationPeriodObservations.from_point_source_observations: build the observations table (e.g. to inspect or reuse) without solving.