Listening for events…

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:

  1. 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 satcat already 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.
  2. 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 celestrak GP 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), median eccentricity and inclination_deg over 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) and altitude_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 satcatcelestrak_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.

Live Feed