Scope freeze — satellite_decay CelestialObjectDex kind
Frozen 2026-07-15. The 3rd kind of the CelestialObjectDex family
(docs/celestialdex-framework.md), after the neo pilot and satcat. One slot
= one catalogued space object; the slot's array is the object's orbital-decay
track — the time-ordered mean-motion element snapshots the live CelesTrak GP
feed logs, and the altitude/decay-rate those measured elements imply.
Mike's rulings (2026-07-15)
Two calls, made while flipping the starlink-thermosphere-storm-pilot paper
(SW-2) onto dex data:
- Family = CelestialObjectDex, not MovingPlatformDex. A satellite accreting
a series along its orbit is the literal CelestialObjectDex definition ("a
celestial object accretes a series of observations along its orbit"), and
satcatalready establishes that satellites live in this family. MovingPlatformDex is explicitly its terrestrial sibling (ships, floats, aircraft). So the decay track is a celestial object's array. The paper's "thermosphere sensor made of falling satellites" framing is what the paper does with the data (analysis), not how the data organises. - Scope = every catalogued object, not just Starlink. "Orbital decay is still orbital decay" — every object's element track is real measured decay, so the kind carries the whole GP catalog, not only the paper's Starlink subject.
This kind realises exactly the piece satcat deferred (its docstring: "the
orbital-element history array from the live celestrak GP feed [is] deferred-v2").
satcat holds each object's static identity + launch→decay lifecycle; this
kind holds its moving element track. Different source (the live celestrak
GP feed fbed4afa, not satcat's static satcat.csv), different slot shape.
The slot
- One slot = one catalogued object. Slot id = the
norad_id(globally unique, stable). Id shape^\d+$. - Roster: 16,377 objects — every distinct object the
celestrakGP feed carries (the CelesTrak "active" GP group). 10,950 are Starlink. 14 objects have a single snapshot (no fittable slope;provisional=true). - No ground point: an orbiting object has no fixed terrestrial position, so
geographic lat/lon are NULL (consistent with
neo/satcat); the "position" is the orbital elements and the derived altitude.
The array — the decay track
The heart of the slot: track, the time-ordered element snapshots, each
{utc, mean_motion, altitude_km, eccentricity, inclination_deg}. Coverage is the
GP feed's own ingestion window, 2026-03-17 .. 2026-07-17 (~4 months, up to
115 days per object, near-daily snapshots). This is a shorter-but-richer series
(the live element curve), a sibling to satcat's two-point lifecycle — data is
data (feedback_data_is_data_partial_coverage); the track grows as the live feed
runs.
Object-level decay summary
- Identity/intrinsic:
object_name,group(GP query bucket), medianeccentricityandinclination_degover the track. - Track summary:
n_snaps,span_days,mean_motion_first/_last,delta_mean_motion,altitude_first_km/_last_km. - Decay rate:
decay_rate_rev_day2(least-squares slope of mean motion vs time; positive = falling) andaltitude_decay_km_day(least-squares slope of altitude vs time; negative = falling).
Of the 16,363 objects with ≥2 snapshots, 11,000 show a positive decay slope (falling) and 5,358 a slightly negative one (station-keeping raises / measurement noise at high orbits). Final altitudes span 135 km (209 objects below 200 km, near reentry) to ~91,600 km (deep/eccentric orbits). Sanity anchors: STARLINK-3004 falls 563→140 km over 72 days (−5.4 km/day, a genuine reentry track); a GEO satellite sits flat at 35,787 km (~−0.002 km/day).
Measured reality
Inside the bright line via the celestial-mechanics refinement
(feedback_measured_reality_only, Mike 2026-06-26): TLE/GP mean elements are the
deterministic product of real radar/optical tracking (SGP4 orbit determination).
The altitude and decay rate are computed from those measured elements (Kepler's
third law + a least-squares slope on the observed series), not a drag model's
forecast. Nothing here is projected forward; every point is a fitted measurement.
Storage
File-per-slot under data/celestial_storehouse/satellite_decay/<norad_id>.json,
like neo at 18.7k — below satcat's spine-parquet crossover. Each slot embeds
its own track. Reuses the shared event_storehouse write + disk-rebuilt-index
machinery via base_dir.
Live edge
Unlike neo/satcat (which re-pull a catalog csv), this kind's spine is the live
celestrak GP fetcher already ingesting element snapshots into PG on the
ingestion schedule. So refresh_and_store() is a single step — regroup the
freshest snapshots into decay tracks and rebuild the index (no catalog pull).
Scheduler: satellite_decay_refresh, weekly Mon 08:30 UTC (staggered 15 min
after satcat_refresh).
Provenance
KIND_SOURCES["satellite_decay"] = ["celestrak"], resolved via
dex_reader.resolve_source_ids like every other kind. Distinct from
satcat → celestrak_satcat.
Build
src/terrapulse/monitor/satellite_decay_celestialdex.py
(backfill_and_store / sweep_and_store / refresh_and_store). Self-validates
via dex_validator.validate_kind_or_raise at the end of __main__
(required fields event_id, norad_id, n_snaps, track; id pattern ^\d+$).
Tests: tests/test_satellite_decay_celestialdex.py.