spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1import Link from 'next/link';2import { Callout, Code, DocSection, Prose, SubHeading } from '@/components/meta/prose';3import { routes } from '@/lib/site';45/**6 * Static prose sections of /methodology. Facts come from the backend implementation:7 * services/resolution.py, orbital/propagate.py, orbital/elements.py, registry/reference.py (SATCAT_STATUS), services/classify.py,8 * connectors/orbital/celestrak/satcat.py (_ensure_launches), api/common.py (freshness_status), config.py.9 */1011export function PipelineSection() {12 return (13 <DocSection id="pipeline" eyebrow="01" title="Data pipeline">14 <Prose>15 <p>The site is never coupled to a third-party feed. Every value passes through the same chain, and each stage keeps what the previous one produced:</p>16 <ol>17 <li>18 <strong>Connector</strong> — a scheduled worker per upstream feed (<code>celestrak_gp</code>, <code>celestrak_groups</code>, <code>celestrak_satcat</code>, <code>derived_analytics</code>). Each run is recorded with duration, record counts, a payload hash and any error; repeated failures open a circuit breaker.19 </li>20 <li>21 <strong>Raw snapshot</strong> — the exact upstream payload is stored gzip-compressed on disk and indexed in <code>raw_records</code> before anything is parsed. Only payloads whose hash changed are processed; unchanged responses are logged as such.22 </li>23 <li>24 <strong>Normalize</strong> — rows are parsed into a common shape (identifiers, names, dates, element sets). Suspiciously short responses (for example a catalog with far fewer than 50 000 rows) are rejected as truncated rather than treated as “no data”.25 </li>26 <li>27 <strong>Entity resolution</strong> — each normalized row is matched to an existing canonical object (next section) or creates a new one. Ambiguous matches go to a manual review queue.28 </li>29 <li>30 <strong>Canonical store</strong> — one row per object in <code>satellites</code>, an append-only <code>orbital_elements</code> history with <code>orbital_state</code> pointing at the latest epoch, field-level history and <code>field_provenance</code> (source, observation time, confidence) for every accepted value. Objects missing from an upstream response are <em>never</em> deleted.31 </li>32 <li>33 <strong>Derived</strong> — materialized statistics, constellation/operator/country aggregates, the search index and detected events are refreshed by <code>derived_analytics</code> (hourly) using the versioned metric definitions below.34 </li>35 <li>36 <strong>API → web</strong> — the public JSON API reads only the canonical and derived layers; the website is a client of that API. Positions are computed at request time (see orbit calculation).37 </li>38 </ol>39 </Prose>40 </DocSection>41 );42}4344export function ResolutionSection() {45 return (46 <DocSection id="entity-resolution" eyebrow="03" title="Entity resolution">47 <Prose>48 <p>Matching an incoming row to a canonical object follows a strict priority; the first rule that yields exactly one candidate wins:</p>49 <ol>50 <li>51 <strong>NORAD catalog number</strong> — if the row has a NORAD id already known, it is the same object.52 </li>53 <li>54 <strong>COSPAR designator</strong> — only when the row has no NORAD id, and only if exactly one canonical object carries that designator.55 </li>56 <li>57 <strong>Exact normalized name</strong> — only when the row has no NORAD id, against canonical objects that also lack one, and only if the name is unique.58 </li>59 <li>60 <strong>Create</strong> — otherwise a new canonical object is created with a ULID and a slug (<code>name-norad</code>, de-duplicated with a numeric suffix).61 </li>62 </ol>63 <p>64 Anything ambiguous — a COSPAR id that maps to several objects, a name shared by several objects, or a NORAD id that appears alongside a different existing designator — is <strong>never merged automatically</strong>. It is written to <code>manual_review_queue</code> with both candidates and a confidence; an operator decides <em>merge</em>, <em>keep separate</em> or <em>dismiss</em>. Merges move aliases, identifiers and element history to the kept object and are journaled in <code>entity_merges</code> with a snapshot of the removed row, so they can be audited or reversed.65 </p>66 </Prose>67 </DocSection>68 );69}7071export function OrbitCalcSection() {72 return (73 <DocSection id="orbit-calculation" eyebrow="04" title="Orbit calculation">74 <Prose>75 <p>76 Positions are propagated with <strong>SGP4</strong> (the <code>python-sgp4</code> implementation of the Vallado et al. reference code, vectorised with <code>SatrecArray</code>) using the <strong>WGS-72</strong> gravity constants — the model the element sets were fitted against. Inputs are the OMM element sets from CelesTrak (epoch, mean motion, eccentricity, inclination, RAAN, argument of perigee, mean anomaly, B*, first derivative of mean motion).77 </p>78 <p>79 SGP4 yields a position vector in the <strong>TEME</strong> frame. It is rotated to Earth-fixed coordinates (ECEF) about the Z axis by the <strong>Greenwich Mean Sidereal Time</strong> of the requested instant (UT1 ≈ UTC; polar motion ignored, ≈ 10 m), then converted to geodetic latitude, longitude and altitude on the <strong>WGS84</strong> ellipsoid by iteration. Velocity is the norm of the TEME velocity vector.80 </p>81 <p>82 Derived geometry shown on satellite pages comes from the same element set: semi-major axis <em>a</em> = (μ / n²)^1/3 with μ = 398 600.4418 km³/s², perigee/apogee = a(1 ∓ e) − 6 378.137 km, period = 1440 / n minutes.83 </p>84 </Prose>85 <SubHeading>Accuracy</SubHeading>86 <Prose>87 <p>88 SGP4 is an analytical mean-element model: typical along-track error is of the order of a kilometre at epoch and grows by kilometres per day, faster for low, high-drag orbits and after manoeuvres. Positions shown on this site are therefore <strong>indicative</strong>: good for “where is it over the Earth right now”, not for pointing, conjunction or reentry work. Every position carries the epoch it was propagated from and its age.89 </p>90 <p>91 Positions are <strong>computed on demand and never stored</strong>: the propagator keeps the latest element set of every object in memory and answers batch and single-object requests with a 30-second cache. Only element sets are persisted.92 </p>93 </Prose>94 </DocSection>95 );96}9798const OPS_CODES: { code: string; meaning: string; status: string }[] = [99 { code: '+', meaning: 'Operational', status: 'ACTIVE' },100 { code: 'P', meaning: 'Partially operational', status: 'ACTIVE' },101 { code: 'B', meaning: 'Backup / standby', status: 'INACTIVE' },102 { code: 'S', meaning: 'Spare', status: 'INACTIVE' },103 { code: 'X', meaning: 'Extended mission', status: 'INACTIVE' },104 { code: 'D', meaning: 'Decayed', status: 'DECAYED' },105 { code: '?', meaning: 'Unknown', status: 'UNKNOWN' },106 { code: '(blank)', meaning: 'No code published', status: 'UNKNOWN' },107];108109export function StatusSection({ methodologyText }: { methodologyText: string | null }) {110 return (111 <DocSection id="status" eyebrow="05" title="Status classification">112 {methodologyText && <Callout className="mt-0 mb-4">{methodologyText}</Callout>}113 <div className="overflow-x-auto">114 <table className="data-table sm:max-w-lg">115 <thead>116 <tr>117 <th>SATCAT code</th>118 <th>Meaning</th>119 <th>Canonical status</th>120 </tr>121 </thead>122 <tbody>123 {OPS_CODES.map((c) => (124 <tr key={c.code}>125 <td className="mono text-sm">{c.code}</td>126 <td className="text-sm text-ink-2">{c.meaning}</td>127 <td className="mono text-xs">{c.status}</td>128 </tr>129 ))}130 </tbody>131 </table>132 </div>133 <Prose className="mt-4">134 <p>135 Two overrides apply after the code lookup, in this order: a <strong>decay date</strong> always produces <code>DECAYED</code>; debris and rocket bodies with no code are <code>INACTIVE</code> rather than <code>UNKNOWN</code> (they cannot be “operational”). Payloads present in the CelesTrak <code>active</code> GP group but lacking a SATCAT code are considered <code>ACTIVE</code>. Status changes emit <em>decommission</em> / <em>activation</em> events and are kept in the field history.136 </p>137 </Prose>138 </DocSection>139 );140}141142export function OrbitClassSection({ methodologyText }: { methodologyText: string | null }) {143 return (144 <DocSection id="orbit-class" eyebrow="06" title="Orbit class">145 {methodologyText && <Callout className="mt-0 mb-4">{methodologyText}</Callout>}146 <Prose>147 <p>The rule is evaluated on the latest element set (or, for objects without GP data, on the SATCAT apsides with eccentricity estimated from perigee and apogee). Implementation order, as in <code>orbital/elements.py</code>:</p>148 </Prose>149 <Code label="classify_orbit">{`if apogee or perigee missing → OTHER150if |period − 1436.07 min| ≤ 30 and e < 0.05 and i < 20° → GEO151if e > 0.25 and apogee > 35 000 km → HEO152if apogee < 2 000 km → LEO153if perigee ≥ 2 000 km and apogee < 35 786 + 2 000 km:154 if |period − 1436.07| ≤ 60 → GEO (inclined / drifting geosynchronous)155 else → MEO156if |period − 1436.07| ≤ 60 and e < 0.1 → GEO157else → OTHER`}</Code>158 <Prose className="mt-3">159 <p>160 <code>OTHER</code> therefore collects transfer orbits, highly eccentric non-HEO objects and anything with an apogee below 2 000 km but a perigee above it — impossible by definition, so effectively it is the “does not fit” bucket. Orbit class is a <em>derived</em> label, versioned as metric <code>orbit_class</code>.161 </p>162 </Prose>163 </DocSection>164 );165}166167export function MissionLaunchSection() {168 return (169 <>170 <DocSection id="mission-type" eyebrow="08" title="Mission type">171 <Prose>172 <p>Mission type is derived, in this order of preference (<code>services/classify.py</code>):</p>173 <ol>174 <li>the <strong>service type of the matched constellation</strong> (communications, earth-observation, navigation, iot…);</li>175 <li>176 otherwise the <strong>object type</strong> when it is decisive: rocket bodies → <code>rocket-body</code>, debris → <code>debris</code>, stations → <code>station</code>;177 </li>178 <li>179 otherwise a <strong>documented name pattern</strong> from the curated registry (for example weather, GNSS or science families);180 </li>181 <li>182 otherwise <code>unknown</code>. A later run never downgrades a known mission type to unknown.183 </li>184 </ol>185 </Prose>186 </DocSection>187 <DocSection id="launches" eyebrow="09" title="Launches">188 <Prose>189 <p>190 No launch feed is ingested yet. Launches are <strong>derived from international designators</strong>: the first eight characters of a COSPAR id (<code>YYYY-NNN</code>) identify the launch, so every object sharing that prefix is attached to one launch row. The launch date and site are the earliest date and the site reported for those objects in SATCAT; the “primary” payload is the object whose piece letter is <code>A</code>; payload, object and on-orbit counts are recomputed after every catalog run. Objects with no COSPAR id (analyst objects) belong to no launch and are flagged.191 </p>192 </Prose>193 </DocSection>194 </>195 );196}197198export function FreshnessSection({ methodologyText }: { methodologyText: string | null }) {199 return (200 <DocSection id="freshness" eyebrow="12" title="Freshness thresholds">201 {methodologyText && <Callout className="mt-0 mb-4">{methodologyText}</Callout>}202 <Prose>203 <p>Two scales are in use, and both are shown rather than hidden behind an average:</p>204 <ul>205 <li>206 <strong>Per connector</strong> (<Link href={routes.statusData()} className="link">/status/data</Link>, <Link href={routes.sources()} className="link">/sources</Link>): relative to the connector’s own interval — <em>fresh</em> under 3 × interval, <em>aging</em> under 12 ×, <em>stale</em> beyond; <em>unavailable</em> if it never succeeded, <em>not enabled</em> for planned connectors.207 </li>208 <li>209 <strong>Per object</strong> (satellite pages): the orbit is <em>fresh</em> under 12 h, <em>aging</em> from 12 to 48 h and <em>stale</em> after 48 h since the element epoch or the last successful orbital sync; catalog metadata uses 36 h / 96 h.210 </li>211 </ul>212 <p>The health endpoint reports the worst connector freshness as the platform’s data status.</p>213 </Prose>214 </DocSection>215 );216}217218export function LimitationsSection() {219 return (220 <DocSection id="limitations" eyebrow="13" title="Limitations">221 <Prose>222 <ul>223 <li>224 <strong>Name-pattern classification errors.</strong> Constellation membership and mission type inferred from names can misfile objects with unusual or reused names (test payloads, renamed spacecraft, shared bus names). Group-backed memberships are more reliable than pattern-backed ones; both are labelled.225 </li>226 <li>227 <strong>Catalog latency.</strong> SATCAT status codes, decay dates and new-object entries lag reality by hours to weeks. A satellite can be operational before it is marked <code>+</code>, and decayed objects may stay “on orbit” until the catalog catches up.228 </li>229 <li>230 <strong>GEO drift and graveyard objects.</strong> Inclined, drifting or super-synchronous objects sit near the GEO/MEO/OTHER boundaries and may switch class between element sets.231 </li>232 <li>233 <strong>Analyst objects.</strong> Objects tracked without a COSPAR designator (and sometimes with temporary NORAD numbers) have no launch, owner or country and can later be re-identified — that is exactly the case the manual review queue exists for.234 </li>235 <li>236 <strong>Element-set age.</strong> Positions inherit the age of their elements; a stale feed means every live position on the site is stale, and the status page says so.237 </li>238 <li>239 <strong>Coverage.</strong> Only CelesTrak feeds are ingested today; government registries (Space-Track, UNOOSA), ESA DISCOS and GCAT are registered as planned sources but contribute nothing yet.240 </li>241 </ul>242 </Prose>243 </DocSection>244 );245}246