From cbb4e5f6f0e15507909e22aad9b1acaa7f59a77e Mon Sep 17 00:00:00 2001 From: michealroberts Date: Mon, 12 Oct 2026 06:40:19 +0000 Subject: [PATCH] feat: add getStateVectorAtMinutesSinceEpoch to SatelliteRecord in sgp4 module in @observerly/astrometry feat: add getStateVectorAtMinutesSinceEpoch to SatelliteRecord in sgp4 module in @observerly/astrometry --- src/sgp4.ts | 803 ++++++++++++++++++++++++++++++++- tests/conformance/sgp4.spec.ts | 93 ++++ tests/conformance/sgp4.ts | 397 ++++++++++++++++ tests/sgp4.spec.ts | 103 ++++- 4 files changed, 1393 insertions(+), 3 deletions(-) create mode 100644 tests/conformance/sgp4.spec.ts create mode 100644 tests/conformance/sgp4.ts diff --git a/src/sgp4.ts b/src/sgp4.ts index 4c4b3a2..243dd54 100644 --- a/src/sgp4.ts +++ b/src/sgp4.ts @@ -6,10 +6,16 @@ /*****************************************************************************************************************/ +import type { StateVector } from './common' + +import { MINUTES_IN_DAY, TWO_PI } from './constants' + import { type GravityModel, WGS72 } from './gravity' import type { TwoLineElement } from './tle' +import { convertDegreesToRadians as radians } from './utilities' + /*****************************************************************************************************************/ /** @@ -263,6 +269,746 @@ export type SatelliteRecordOptions = { /*****************************************************************************************************************/ +// Two thirds, e.g., the exponent of Kepler's third law that relates the mean motion to the semi-major +// axis of an orbit: +const TWO_THIRDS = 2 / 3 + +// The number of minutes in a day per radian, e.g., which converts a mean motion of revolutions per day +// to one of radians per minute: +const MINUTES_PER_DAY_PER_RADIAN = MINUTES_IN_DAY / TWO_PI + +// The minimum period (in minutes) of an orbit that the deep-space terms of the theory resolve, e.g., of +// a semi-synchronous orbit and beyond: +const SGP4_DEEP_SPACE_PERIOD = 225 + +/*****************************************************************************************************************/ + +/** + * + * The propagation constants of the SGP4 theory, e.g., the mean elements, secular rates and + * coefficients that are resolved once, when an element set is initialised, and that every + * propagation of it is resolved from. + * + * The constants are in the canonical units of the theory, e.g., distances in Earth radii and times + * in minutes, and are named by the symbols of Hoots & Roehrich (1980) where the theory gives them + * one, e.g., C1, D2 or η, and descriptively otherwise. + * + * N.B. The name of each constant in the reference implementation of Vallado et al. (2006), from + * which the theory is ported, is given alongside it, e.g., (cc1), so that each may be traced to it. + * + */ +type SGP4PropagationConstants = { + // The equatorial radius of the Earth of the gravity model (in kilometres) (radiusearthkm): + radius: number + // The square root of the gravitational parameter of the gravity model, in canonical units (xke): + xke: number + // The second zonal harmonic of the gravity model (j2): + J2: number + // The ratio of the third to the second zonal harmonic of the gravity model (j3oj2): + J3OverJ2: number + // The canonical unit of velocity of the gravity model (in kilometres per second) (vkmpersec): + velocityUnit: number + // The drag term of the element set, e.g., B* (in inverse Earth radii) (bstar): + bstar: number + // The mean eccentricity at the epoch (ecco): + e0: number + // The mean argument of perigee at the epoch (in radians) (argpo): + ω0: number + // The mean inclination at the epoch (in radians) (inclo): + i0: number + // The mean anomaly at the epoch (in radians) (mo): + M0: number + // The mean longitude of the ascending node at the epoch (in radians) (nodeo): + Ω0: number + // The Brouwer mean motion at the epoch, e.g., recovered from the Kozai mean motion of the element + // set (in radians per minute) (no_unkozai): + n0: number + // Whether the orbit is propagated by the simplified drag terms, e.g., a perigee below 220 km (isimp): + isSimplified: boolean + // The secular rate of the mean anomaly (in radians per minute) (mdot): + MDot: number + // The secular rate of the argument of perigee (in radians per minute) (argpdot): + ωDot: number + // The secular rate of the longitude of the ascending node (in radians per minute) (nodedot): + ΩDot: number + // The coefficients of the secular effects of the atmospheric drag (cc1, cc4, cc5, d2, d3, d4): + C1: number + C4: number + C5: number + D2: number + D3: number + D4: number + // The coefficients of the secular effects of the atmospheric drag on the mean longitude, e.g., + // of the second to the fifth powers of the time since the epoch (t2cof, t3cof, t4cof, t5cof): + t2Coefficient: number + t3Coefficient: number + t4Coefficient: number + t5Coefficient: number + // The coefficients of the secular effects of the atmospheric drag on the argument of perigee, the + // mean anomaly and the longitude of the ascending node (omgcof, xmcof, nodecf): + ωDragCoefficient: number + MDragCoefficient: number + ΩDragCoefficient: number + // The parameter η of the theory, e.g., a0 e0 ξ (eta): + η: number + // The cube of 1 + η cos(M0) (delmo): + δM0: number + // The sine of the mean anomaly at the epoch (sinmao): + sinM0: number + // The coefficients of the long-period periodic effects of the third zonal harmonic (aycof, xlcof): + ayNCoefficient: number + LCoefficient: number + // The functions of θ = cos(i0) of the short-period periodic effects (con41, x1mth2, x7thm1): + threeθ2MinusOne: number + oneMinusθ2: number + sevenθ2MinusOne: number +} + +/*****************************************************************************************************************/ + +/** + * + * Initialises the SGP4 theory for an element set, e.g., resolves the propagation constants of the + * theory from the mean elements, as the initl() and sgp4init() routines of the reference + * implementation do. + * + * @param tle - The two-line element set to initialise. + * @param gravity - The gravity model to initialise it with. + * @throws An SGP4Error if the orbit is of the deep-space theory, which is not yet resolved. + * @returns The propagation constants of the theory for the element set. + * + */ +const initialiseSGP4 = (tle: TwoLineElement, gravity: GravityModel): SGP4PropagationConstants => { + const { radius, xke, J2, J4 } = gravity + + const J3OverJ2 = gravity.J3 / gravity.J2 + + // The mean elements of the element set, in the units of the theory, e.g., radians and radians per + // minute: + const bstar = tle.bstar + + const e0 = tle.e + + const ω0 = radians(tle.ω) + + const i0 = radians(tle.i) + + const M0 = radians(tle.M) + + const Ω0 = radians(tle.Ω) + + // The Kozai mean motion of the element set (in radians per minute) (no_kozai): + const nKozai = tle.n / MINUTES_PER_DAY_PER_RADIAN + + // The parameter s of the atmospheric density function, e.g., 78 km above the surface, in Earth + // radii from the centre (ss): + const s = 78.0 / radius + 1.0 + + // The parameter (q0 - s)⁴ of the atmospheric density function, e.g., of q0 at 120 km (qzms2t): + const q0MinusSDifference = (120.0 - 78.0) / radius + + const q0MinusS4 = + q0MinusSDifference * q0MinusSDifference * q0MinusSDifference * q0MinusSDifference + + // initl(): recover the Brouwer mean motion from the Kozai mean motion of the element set: + // e0² (eccsq): + const e02 = e0 * e0 + + // β0² = 1 - e0² (omeosq): + const β02 = 1.0 - e02 + + // β0 = √(1 - e0²) (rteosq): + const β0 = Math.sqrt(β02) + + // θ = cos(i0) (cosio), and θ² (cosio2): + const θ = Math.cos(i0) + + const θ2 = θ * θ + + // The semi-major axis of the Kozai mean motion (in Earth radii) (ak): + const a1 = (xke / nKozai) ** TWO_THIRDS + + // The numerator of the corrections δ1 and δ0 of the semi-major axis (d1): + const δNumerator = (0.75 * J2 * (3.0 * θ2 - 1.0)) / (β0 * β02) + + // δ1 (del): + let δ = δNumerator / (a1 * a1) + + // The semi-major axis, corrected by δ1 (in Earth radii) (adel): + const aδ = a1 * (1.0 - δ * δ - δ * (1.0 / 3.0 + (134.0 * δ * δ) / 81.0)) + + // δ0 (del): + δ = δNumerator / (aδ * aδ) + + // The Brouwer mean motion (in radians per minute) (no_unkozai): + const n0 = nKozai / (1.0 + δ) + + // The Brouwer semi-major axis (in Earth radii) (ao): + const a0 = (xke / n0) ** TWO_THIRDS + + // sin(i0) (sinio): + const sinI0 = Math.sin(i0) + + // The semi-latus rectum (in Earth radii) (po), and its square (posq): + const p0 = a0 * β02 + + const p02 = p0 * p0 + + // 1 - 5θ² (con42), and 3θ² - 1 (con41): + const oneMinusFiveθ2 = 1.0 - 5.0 * θ2 + + const threeθ2MinusOne = -oneMinusFiveθ2 - θ2 - θ2 + + // The radius of perigee (in Earth radii) (rp): + const rPerigee = a0 * (1.0 - e0) + + // An orbit of a period of 225 minutes or more is resolved by the deep-space terms of the theory, + // e.g., the lunar and solar perturbations and the resonances of a geosynchronous or a 12-hour orbit, + // which are not yet resolved, and so it is not propagated rather than propagated without them: + if (TWO_PI / n0 >= SGP4_DEEP_SPACE_PERIOD) { + throw new SGP4Error( + `The orbit of a period of ${TWO_PI / n0} minutes is of the deep-space theory, which is not yet resolved` + ) + } + + // sgp4init(): resolve the coefficients of the theory: + let isSimplified = false + + let C1 = 0 + + let C4 = 0 + + let C5 = 0 + + let D2 = 0 + + let D3 = 0 + + let D4 = 0 + + let t2Coefficient = 0 + + let t3Coefficient = 0 + + let t4Coefficient = 0 + + let t5Coefficient = 0 + + let ωDragCoefficient = 0 + + let MDragCoefficient = 0 + + let ΩDragCoefficient = 0 + + let MDot = 0 + + let ωDot = 0 + + let ΩDot = 0 + + let η = 0 + + let δM0 = 0 + + let sinM0 = 0 + + let ayNCoefficient = 0 + + let LCoefficient = 0 + + let oneMinusθ2 = 0 + + let sevenθ2MinusOne = 0 + + if (β02 >= 0.0 || n0 >= 0.0) { + // An orbit of a perigee below 220 km is propagated by the simplified drag terms of the theory: + isSimplified = rPerigee < 220.0 / radius + 1.0 + + // The parameter s* of the atmospheric density function, e.g., s adjusted for a low perigee (sfour): + let sStar = s + + // The parameter (q0 - s*)⁴ of the atmospheric density function (qzms24): + let q0MinusSStar4 = q0MinusS4 + + // The altitude of perigee (in kilometres) (perige): + const perigeeAltitude = (rPerigee - 1.0) * radius + + // The atmospheric density parameter is adjusted for an orbit of a perigee below 156 km: + if (perigeeAltitude < 156.0) { + sStar = perigeeAltitude - 78.0 + + if (perigeeAltitude < 98.0) { + sStar = 20.0 + } + + const q0MinusSStarDifference = (120.0 - sStar) / radius + + q0MinusSStar4 = + q0MinusSStarDifference * + q0MinusSStarDifference * + q0MinusSStarDifference * + q0MinusSStarDifference + + sStar = sStar / radius + 1.0 + } + + // 1 / p0² (pinvsq): + const inverseP02 = 1.0 / p02 + + // ξ = 1 / (a0 - s*) (tsi): + const ξ = 1.0 / (a0 - sStar) + + η = a0 * e0 * ξ + + // η² (etasq), and e0 η (eeta): + const η2 = η * η + + const e0η = e0 * η + + // ψ² = |1 - η²| (psisq): + const ψ2 = Math.abs(1.0 - η2) + + // (q0 - s*)⁴ ξ⁴ (coef), and (q0 - s*)⁴ ξ⁴ / ψ⁷ (coef1): + const q0MinusSStar4ξ4 = q0MinusSStar4 * ξ ** 4.0 + + const q0MinusSStar4ξ4OverΨ7 = q0MinusSStar4ξ4 / ψ2 ** 3.5 + + // C2 (cc2): + const C2 = + q0MinusSStar4ξ4OverΨ7 * + n0 * + (a0 * (1.0 + 1.5 * η2 + e0η * (4.0 + η2)) + + ((0.375 * J2 * ξ) / ψ2) * threeθ2MinusOne * (8.0 + 3.0 * η2 * (8.0 + η2))) + + C1 = bstar * C2 + + // C3 (cc3): + let C3 = 0.0 + + if (e0 > 1.0e-4) { + C3 = (-2.0 * q0MinusSStar4ξ4 * ξ * J3OverJ2 * n0 * sinI0) / e0 + } + + oneMinusθ2 = 1.0 - θ2 + + C4 = + 2.0 * + n0 * + q0MinusSStar4ξ4OverΨ7 * + a0 * + β02 * + (η * (2.0 + 0.5 * η2) + + e0 * (0.5 + 2.0 * η2) - + ((J2 * ξ) / (a0 * ψ2)) * + (-3.0 * threeθ2MinusOne * (1.0 - 2.0 * e0η + η2 * (1.5 - 0.5 * e0η)) + + 0.75 * oneMinusθ2 * (2.0 * η2 - e0η * (1.0 + η2)) * Math.cos(2.0 * ω0))) + + C5 = 2.0 * q0MinusSStar4ξ4OverΨ7 * a0 * β02 * (1.0 + 2.75 * (η2 + e0η) + e0η * η2) + + // θ⁴ (cosio4): + const θ4 = θ2 * θ2 + + // The terms of the secular rates of the second and fourth zonal harmonics (temp1, temp2, temp3): + const J2Term = 1.5 * J2 * inverseP02 * n0 + + const J2SquaredTerm = 0.5 * J2Term * J2 * inverseP02 + + const J4Term = -0.46875 * J4 * inverseP02 * inverseP02 * n0 + + MDot = + n0 + + 0.5 * J2Term * β0 * threeθ2MinusOne + + 0.0625 * J2SquaredTerm * β0 * (13.0 - 78.0 * θ2 + 137.0 * θ4) + + ωDot = + -0.5 * J2Term * oneMinusFiveθ2 + + 0.0625 * J2SquaredTerm * (7.0 - 114.0 * θ2 + 395.0 * θ4) + + J4Term * (3.0 - 36.0 * θ2 + 49.0 * θ4) + + // The secular rate of the longitude of the ascending node of the second zonal harmonic (xhdot1): + const ΩDotJ2 = -J2Term * θ + + ΩDot = ΩDotJ2 + (0.5 * J2SquaredTerm * (4.0 - 19.0 * θ2) + 2.0 * J4Term * (3.0 - 7.0 * θ2)) * θ + + ωDragCoefficient = bstar * C3 * Math.cos(ω0) + + if (e0 > 1.0e-4) { + MDragCoefficient = (-TWO_THIRDS * q0MinusSStar4ξ4 * bstar) / e0η + } + + ΩDragCoefficient = 3.5 * β02 * ΩDotJ2 * C1 + + t2Coefficient = 1.5 * C1 + + // The long-period coefficient is singular for an orbit of an inclination of 180°, and so it is + // resolved against a small divisor there, as the reference implementation resolves it: + LCoefficient = + Math.abs(θ + 1.0) > 1.5e-12 + ? (-0.25 * J3OverJ2 * sinI0 * (3.0 + 5.0 * θ)) / (1.0 + θ) + : (-0.25 * J3OverJ2 * sinI0 * (3.0 + 5.0 * θ)) / 1.5e-12 + + ayNCoefficient = -0.5 * J3OverJ2 * sinI0 + + // 1 + η cos(M0) (delmotemp): + const onePlusηCosM0 = 1.0 + η * Math.cos(M0) + + δM0 = onePlusηCosM0 * onePlusηCosM0 * onePlusηCosM0 + + sinM0 = Math.sin(M0) + + sevenθ2MinusOne = 7.0 * θ2 - 1.0 + + if (!isSimplified) { + // C1² (cc1sq): + const C12 = C1 * C1 + + D2 = 4.0 * a0 * ξ * C12 + + // D2 ξ C1 / 3 (temp): + const D2ξC1OverThree = (D2 * ξ * C1) / 3.0 + + D3 = (17.0 * a0 + sStar) * D2ξC1OverThree + + D4 = 0.5 * D2ξC1OverThree * a0 * ξ * (221.0 * a0 + 31.0 * sStar) * C1 + + t3Coefficient = D2 + 2.0 * C12 + + t4Coefficient = 0.25 * (3.0 * D3 + C1 * (12.0 * D2 + 10.0 * C12)) + + t5Coefficient = + 0.2 * (3.0 * D4 + 12.0 * C1 * D3 + 6.0 * D2 * D2 + 15.0 * C12 * (2.0 * D2 + C12)) + } + } + + return { + radius, + xke, + J2, + J3OverJ2, + velocityUnit: (radius * xke) / 60.0, + bstar, + e0, + ω0, + i0, + M0, + Ω0, + n0, + isSimplified, + MDot, + ωDot, + ΩDot, + C1, + C4, + C5, + D2, + D3, + D4, + t2Coefficient, + t3Coefficient, + t4Coefficient, + t5Coefficient, + ωDragCoefficient, + MDragCoefficient, + ΩDragCoefficient, + η, + δM0, + sinM0, + ayNCoefficient, + LCoefficient, + threeθ2MinusOne, + oneMinusθ2, + sevenθ2MinusOne + } +} + +/*****************************************************************************************************************/ + +/** + * + * Propagates an element set by the SGP4 theory to a time since its epoch, as the sgp4() routine of + * the reference implementation does, e.g., resolves the secular effects of the gravity of the + * Earth and of the atmospheric drag on the mean elements, the long-period and the short-period + * periodic effects on the osculating elements, and the state vector of the object from them. + * + * @param propagationConstants - The propagation constants of the theory for the element set. + * @param t - The time since the epoch of the element set (in minutes). + * @throws An SGP4MeanMotionError, an SGP4MeanEccentricityError, an SGP4SemiLatusRectumError or an SGP4DecayedError where the theory fails. + * @returns The state vector of the object, in the True Equator, Mean Equinox (TEME) frame of the epoch (in kilometres and kilometres per second). + * + */ +const propagateSGP4 = (propagationConstants: SGP4PropagationConstants, t: number): StateVector => { + const { xke, J2, isSimplified } = propagationConstants + + // The secular effects of the gravity of the Earth on the mean anomaly, the argument of perigee and + // the longitude of the ascending node (xmdf, argpdf, nodedf): + const MSecular = propagationConstants.M0 + propagationConstants.MDot * t + + const ωSecular = propagationConstants.ω0 + propagationConstants.ωDot * t + + const ΩSecular = propagationConstants.Ω0 + propagationConstants.ΩDot * t + + // The mean argument of perigee (argpm), mean anomaly (mm) and longitude of the ascending node (nodem): + let ω = ωSecular + + let M = MSecular + + const t2 = t * t + + let Ω = ΩSecular + propagationConstants.ΩDragCoefficient * t2 + + // The secular effects of the atmospheric drag on the semi-major axis (tempa), the eccentricity + // (tempe) and the mean longitude (templ): + let aDragFactor = 1.0 - propagationConstants.C1 * t + + let eDragDecrement = propagationConstants.bstar * propagationConstants.C4 * t + + let LDragIncrement = propagationConstants.t2Coefficient * t2 + + if (!isSimplified) { + // The secular effects of the atmospheric drag on the argument of perigee (delomg) and on the mean + // anomaly (delm): + const δω = propagationConstants.ωDragCoefficient * t + + const onePlusηCosM = 1.0 + propagationConstants.η * Math.cos(MSecular) + + const δM = + propagationConstants.MDragCoefficient * + (onePlusηCosM * onePlusηCosM * onePlusηCosM - propagationConstants.δM0) + + const δωPlusδM = δω + δM + + M = MSecular + δωPlusδM + + ω = ωSecular - δωPlusδM + + const t3 = t2 * t + + const t4 = t3 * t + + aDragFactor = + aDragFactor - + propagationConstants.D2 * t2 - + propagationConstants.D3 * t3 - + propagationConstants.D4 * t4 + + eDragDecrement = + eDragDecrement + + propagationConstants.bstar * + propagationConstants.C5 * + (Math.sin(M) - propagationConstants.sinM0) + + LDragIncrement = + LDragIncrement + + propagationConstants.t3Coefficient * t3 + + t4 * (propagationConstants.t4Coefficient + t * propagationConstants.t5Coefficient) + } + + // The mean motion (nm), eccentricity (em) and inclination (inclm): + let n = propagationConstants.n0 + + let e = propagationConstants.e0 + + const i = propagationConstants.i0 + + if (n <= 0.0) { + throw new SGP4MeanMotionError(n) + } + + // The mean semi-major axis (in Earth radii) (am): + const a = (xke / n) ** TWO_THIRDS * aDragFactor * aDragFactor + + n = xke / a ** 1.5 + + e = e - eDragDecrement + + if (e >= 1.0 || e < -0.001) { + throw new SGP4MeanEccentricityError(e) + } + + // The eccentricity is held at a floor, below which the theory is singular: + if (e < 1.0e-6) { + e = 1.0e-6 + } + + M = M + propagationConstants.n0 * LDragIncrement + + // The mean longitude (xlm): + let L = M + ω + Ω + + // N.B. The angles are reduced by the remainder of a whole turn that takes the sign of the angle, + // e.g., as the fmod() of the reference implementation does, and not into [0, 2π): + Ω = Ω % TWO_PI + + ω = ω % TWO_PI + + L = L % TWO_PI + + M = (L - ω - Ω) % TWO_PI + + const sinI = Math.sin(i) + + const cosI = Math.cos(i) + + // The long-period periodic effects of the third zonal harmonic, e.g., on the components of the + // eccentricity vector (axnl, aynl) and on the mean longitude (xl): + const axN = e * Math.cos(ω) + + // 1 / a(1 - e²) (temp): + const inverseAβ2 = 1.0 / (a * (1.0 - e * e)) + + const ayN = e * Math.sin(ω) + inverseAβ2 * propagationConstants.ayNCoefficient + + const LT = M + ω + Ω + inverseAβ2 * propagationConstants.LCoefficient * axN + + // Solve Kepler's equation for the eccentric longitude E + ω, e.g., by Newton's method, with each + // step limited to 0.95 radians, as the reference implementation solves it (u, eo1, tem5): + const U = (LT - Ω) % TWO_PI + + let eccentricLongitude = U + + let Δ = 9999.9 + + let sinEccentricLongitude = 0 + + let cosEccentricLongitude = 0 + + for (let iteration = 1; Math.abs(Δ) >= 1.0e-12 && iteration <= 10; iteration++) { + sinEccentricLongitude = Math.sin(eccentricLongitude) + + cosEccentricLongitude = Math.cos(eccentricLongitude) + + Δ = 1.0 - cosEccentricLongitude * axN - sinEccentricLongitude * ayN + + Δ = (U - ayN * cosEccentricLongitude + axN * sinEccentricLongitude - eccentricLongitude) / Δ + + if (Math.abs(Δ) >= 0.95) { + Δ = Δ > 0.0 ? 0.95 : -0.95 + } + + eccentricLongitude = eccentricLongitude + Δ + } + + // The short-period preliminary quantities, e.g., e cos(E) (ecose), e sin(E) (esine), eL² (el2) and + // the semi-latus rectum (pl): + const eCosE = axN * cosEccentricLongitude + ayN * sinEccentricLongitude + + const eSinE = axN * sinEccentricLongitude - ayN * cosEccentricLongitude + + const eL2 = axN * axN + ayN * ayN + + const pL = a * (1.0 - eL2) + + if (pL < 0.0) { + throw new SGP4SemiLatusRectumError(pL) + } + + // The radius (rl), the radial rate (rdotl) and the transverse rate (rvdotl): + const r = a * (1.0 - eCosE) + + const rDot = (Math.sqrt(a) * eSinE) / r + + const rfDot = Math.sqrt(pL) / r + + // βL = √(1 - eL²) (betal): + const βL = Math.sqrt(1.0 - eL2) + + const eSinEOverOnePlusβL = eSinE / (1.0 + βL) + + // The argument of latitude (su), and its sine and cosine: + const sinu = (a / r) * (sinEccentricLongitude - ayN - axN * eSinEOverOnePlusβL) + + const cosu = (a / r) * (cosEccentricLongitude - axN + ayN * eSinEOverOnePlusβL) + + let u = Math.atan2(sinu, cosu) + + const sin2u = (cosu + cosu) * sinu + + const cos2u = 1.0 - 2.0 * sinu * sinu + + // 1 / pL (temp): + const inversePL = 1.0 / pL + + // J2 / 2pL (temp1), and J2 / 2pL² (temp2): + const J2OverTwoPL = 0.5 * J2 * inversePL + + const J2OverTwoPL2 = J2OverTwoPL * inversePL + + // The short-period periodic effects, e.g., the osculating radius (mrt), argument of latitude (su), + // longitude of the ascending node (xnode) and inclination (xinc), and the radial (mvt) and + // transverse (rvdot) rates: + const rk = + r * (1.0 - 1.5 * J2OverTwoPL2 * βL * propagationConstants.threeθ2MinusOne) + + 0.5 * J2OverTwoPL * propagationConstants.oneMinusθ2 * cos2u + + u = u - 0.25 * J2OverTwoPL2 * propagationConstants.sevenθ2MinusOne * sin2u + + const Ωk = Ω + 1.5 * J2OverTwoPL2 * cosI * sin2u + + const ik = i + 1.5 * J2OverTwoPL2 * cosI * sinI * cos2u + + const rDotk = rDot - (n * J2OverTwoPL * propagationConstants.oneMinusθ2 * sin2u) / xke + + const rfDotk = + rfDot + + (n * + J2OverTwoPL * + (propagationConstants.oneMinusθ2 * cos2u + 1.5 * propagationConstants.threeθ2MinusOne)) / + xke + + // The orientation vectors of the orbit, e.g., the unit vectors along the radius (U) and along the + // direction of motion (V), in the TEME frame: + const sinuk = Math.sin(u) + + const cosuk = Math.cos(u) + + const sinΩk = Math.sin(Ωk) + + const cosΩk = Math.cos(Ωk) + + const sinik = Math.sin(ik) + + const cosik = Math.cos(ik) + + const Mx = -sinΩk * cosik + + const My = cosΩk * cosik + + const Ux = Mx * sinuk + cosΩk * cosuk + + const Uy = My * sinuk + sinΩk * cosuk + + const Uz = sinik * sinuk + + const Vx = Mx * cosuk - cosΩk * sinuk + + const Vy = My * cosuk - sinΩk * sinuk + + const Vz = sinik * cosuk + + // An osculating radius less than that of the Earth is an orbit that has decayed: + if (rk < 1.0) { + throw new SGP4DecayedError(rk) + } + + // The osculating radius (in kilometres) (mr): + const radius = rk * propagationConstants.radius + + return { + position: { + x: radius * Ux, + y: radius * Uy, + z: radius * Uz + }, + velocity: { + x: (rDotk * Ux + rfDotk * Vx) * propagationConstants.velocityUnit, + y: (rDotk * Uy + rfDotk * Vy) * propagationConstants.velocityUnit, + z: (rDotk * Uz + rfDotk * Vz) * propagationConstants.velocityUnit + } + } +} + +/*****************************************************************************************************************/ + /** * * SatelliteRecord @@ -271,13 +1017,22 @@ export type SatelliteRecordOptions = { * simplified general perturbations theory of the United States Space Force that two-line element * sets are fitted to, as revised by Vallado et al. (2006). * - * The record carries the two-line element set of the object, together with the gravity model and - * the mode of operation of the theory that the element set is to be propagated with. + * The record is initialised once from a two-line element set, e.g., the constants of the theory + * are resolved from its mean elements, together with the gravity model and the mode of operation + * of the theory, and each propagation of it is then resolved from those constants, e.g., to the + * state vector of the object at a time since the epoch of the element set. + * + * The state vector is given in the True Equator, Mean Equinox (TEME) frame of the epoch, e.g., the + * frame the SGP4 theory resolves it in, in SI metres and metres per second. * * N.B. An element set of ephemeris type 4 is fitted to the extended theory of the United States * Space Force, SGP4-XP, and not to SGP4, and so it is rejected, rather than recorded for a * propagation to a position that is not that of the object. * + * N.B. An orbit of a period of 225 minutes or more is of the deep-space theory, e.g., the lunar and + * solar perturbations and the resonances of a geosynchronous or a 12-hour orbit, which is not yet + * resolved, and so it is rejected, rather than propagated without those terms. + * * @see Hoots, F. R., & Roehrich, R. L. (1980), "Spacetrack Report No. 3: Models for Propagation of NORAD Element Sets". * @see Vallado, D. A., Crawford, P., Hujsak, R., & Kelso, T. S. (2006), "Revisiting Spacetrack Report #3", AIAA 2006-6753. * @@ -292,14 +1047,24 @@ export class SatelliteRecord { // The mode of operation of the theory the record is propagated with: public readonly mode: 'improved' | 'afspc' + // The propagation constants of the SGP4 theory, resolved once from the element set as the record is + // initialised, and from which every propagation of it is resolved: + readonly #propagationConstants: SGP4PropagationConstants + /** * * Initialises the record of an element set for its propagation by the SGP4 theory. * + * N.B. The record is propagated to the epoch of the element set as it is initialised, as the + * reference implementation does, and so an element set the theory fails for at its epoch is + * rejected as it is initialised. + * * @param tle - The two-line element set of the record. * @param options - The gravity model and the mode of operation of the theory. * @throws An SGP4EphemerisTypeError if the element set is of ephemeris type 4, e.g., of SGP4-XP. * @throws An error if the mode of operation is not one of "improved" or "afspc". + * @throws An SGP4Error if the orbit is of the deep-space theory, which is not yet resolved. + * @throws An SGP4Error of the failure, where the theory fails for the element set at its epoch. * */ constructor( @@ -321,6 +1086,40 @@ export class SatelliteRecord { this.gravity = gravity this.mode = mode + + this.#propagationConstants = initialiseSGP4(this.tle, gravity) + + // Propagate to the epoch, as the reference implementation does as it initialises, so that an + // element set the theory fails for at its epoch is rejected as it is initialised: + propagateSGP4(this.#propagationConstants, 0) + } + + /** + * + * getStateVectorAtMinutesSinceEpoch() + * + * Propagates the record to a time since the epoch of its element set, e.g., the state vector of + * the object, in the True Equator, Mean Equinox (TEME) frame of the epoch. + * + * @param minutes - The time since the epoch of the element set (in minutes), which may be negative. + * @throws An error if the time is not finite. + * @throws An SGP4Error of the failure, e.g., an SGP4DecayedError, where the theory fails for the record at the time. + * @returns The state vector of the object (in SI metres and metres per second). + * + */ + public getStateVectorAtMinutesSinceEpoch(minutes: number): StateVector { + if (!Number.isFinite(minutes)) { + throw new Error('Invalid time: the minutes since the epoch must be finite') + } + + const { position, velocity } = propagateSGP4(this.#propagationConstants, minutes) + + // The theory resolves the state vector in kilometres and kilometres per second, and so it is + // converted to SI metres and metres per second: + return { + position: { x: position.x * 1000, y: position.y * 1000, z: position.z * 1000 }, + velocity: { x: velocity.x * 1000, y: velocity.y * 1000, z: velocity.z * 1000 } + } } } diff --git a/tests/conformance/sgp4.spec.ts b/tests/conformance/sgp4.spec.ts new file mode 100644 index 0000000..d3a8fb7 --- /dev/null +++ b/tests/conformance/sgp4.spec.ts @@ -0,0 +1,93 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/astrometry/tests/conformance/sgp4 +// @license Copyright © 2021-2026 observerly + +/*****************************************************************************************************************/ + +import { describe, expect, it } from 'vitest' + +import { WGS72, WGS72OLD, WGS84 } from '../../src/gravity' + +import { SGP4Error, SatelliteRecord } from '../../src/sgp4' + +import { parseTwoLineElement } from '../../src/tle' + +import { sgp4NearEarthReferences } from './sgp4' + +/*****************************************************************************************************************/ + +// The tolerances below are the error envelope of the propagation as it stands, measured against the references of +// the reference implementation, and pinned with a modest headroom so that a regression beyond the current envelope +// fails. The envelope is that of the rounding of the arithmetic of the two implementations, e.g., ~0.2 micrometres +// in position and ~2 × 10⁻¹⁰ metres per second in velocity, far beneath the accuracy of the theory itself. + +/*****************************************************************************************************************/ + +// The position of the object (in SI metres): +const POSITION_TOLERANCE = 1e-6 + +/*****************************************************************************************************************/ + +// The velocity of the object (in SI metres per second): +const VELOCITY_TOLERANCE = 1e-9 + +/*****************************************************************************************************************/ + +// The gravity models of the references, by name: +const GRAVITY_MODELS = { WGS72OLD, WGS72, WGS84 } + +/*****************************************************************************************************************/ + +describe('SatelliteRecord conformance against the reference implementation of SGP4', () => { + for (const reference of sgp4NearEarthReferences) { + describe(`satellite ${reference.satellite} with the gravity model of ${reference.gravity}`, () => { + const record = new SatelliteRecord(parseTwoLineElement(reference.line1, reference.line2), { + gravity: GRAVITY_MODELS[reference.gravity] + }) + + for (const state of reference.states) { + it(`should resolve the state vector at ${state.minutes} minutes within the tolerance`, () => { + const { position, velocity } = record.getStateVectorAtMinutesSinceEpoch(state.minutes) + + // The references are in kilometres and kilometres per second: + expect( + Math.hypot( + position.x - state.position[0] * 1000, + position.y - state.position[1] * 1000, + position.z - state.position[2] * 1000 + ) + ).toBeLessThan(POSITION_TOLERANCE) + + expect( + Math.hypot( + velocity.x - state.velocity[0] * 1000, + velocity.y - state.velocity[1] * 1000, + velocity.z - state.velocity[2] * 1000 + ) + ).toBeLessThan(VELOCITY_TOLERANCE) + }) + } + + const failure = reference.error + + if (failure) { + it(`should fail at ${failure.minutes} minutes with the error code ${failure.code}, as the reference does`, () => { + let error: unknown + + try { + record.getStateVectorAtMinutesSinceEpoch(failure.minutes) + } catch (e) { + error = e + } + + expect(error).toBeInstanceOf(SGP4Error) + expect((error as SGP4Error).code).toBe(failure.code) + }) + } + }) + } +}) + +/*****************************************************************************************************************/ diff --git a/tests/conformance/sgp4.ts b/tests/conformance/sgp4.ts new file mode 100644 index 0000000..aa4817a --- /dev/null +++ b/tests/conformance/sgp4.ts @@ -0,0 +1,397 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/astrometry/tests/conformance/sgp4 +// @license Copyright © 2021-2026 observerly + +/*****************************************************************************************************************/ + +// The reference values in this module are generated with the reference implementation of SGP4 of Vallado, D. A., +// Crawford, P., Hujsak, R., & Kelso, T. S. (2006), "Revisiting Spacetrack Report #3", AIAA 2006-6753, e.g., its C++ +// implementation, as bound by the sgp4 package (version 2.24), in its improved mode of operation. +// +// The element sets are those of the verification set of that paper, e.g., SGP4-VER.TLE, of the near-Earth theory, +// e.g., of a period of less than 225 minutes, each propagated over the times of its verification run, e.g., the +// start, stop and step carried beyond the 69th column of its second line, with the gravity model of WGS 72, and +// two of them with the gravity models of WGS 72 (old) and WGS 84 also. +// +// The states are those of the reference implementation, e.g., in the True Equator, Mean Equinox (TEME) frame of the +// epoch, in kilometres and kilometres per second. Where the reference implementation fails, the propagation of the +// element set stops at the time of the failure, and its error code is recorded. + +/*****************************************************************************************************************/ + +export interface SGP4StateReference { + // The time since the epoch of the element set (in minutes): + minutes: number + // The position of the object, in the TEME frame (in kilometres): + position: [number, number, number] + // The velocity of the object, in the TEME frame (in kilometres per second): + velocity: [number, number, number] +} + +/*****************************************************************************************************************/ + +export interface SGP4Reference { + // The satellite catalogue number of the element set: + satellite: number + // The first line of the element set: + line1: string + // The second line of the element set: + line2: string + // The gravity model the element set is propagated with: + gravity: 'WGS72OLD' | 'WGS72' | 'WGS84' + // The states of the object over the times of its verification run: + states: SGP4StateReference[] + // The failure of the reference implementation, e.g., the time and the error code of it, where it fails: + error?: { minutes: number; code: number } +} + +/*****************************************************************************************************************/ + +// The near-Earth element sets of the verification set, as the reference implementation propagates them: +export const sgp4NearEarthReferences: SGP4Reference[] = [ + { + satellite: 5, + line1: '1 00005U 58002B 00179.78495062 .00000023 00000-0 28098-4 0 4753', + line2: '2 00005 34.2682 348.7242 1859667 331.7664 19.3264 10.82419157413667', + gravity: 'WGS72', + states: [ + { minutes: 0, position: [7022.465292664064, -1400.0829675535551, 0.03995155416521326], velocity: [1.8938410145129514, 6.405893759209842, 4.534807250354738] }, + { minutes: 360, position: [-7154.031202015707, -3783.176825036568, -3536.1941229422155], velocity: [4.741887408996156, -4.151817765373694, -2.0939354249073663] }, + { minutes: 720, position: [-7134.593401193215, 6531.686413336448, 3260.271864825572], velocity: [-4.113793027161286, -2.9119220386229627, -2.5573278509305486] }, + { minutes: 1080, position: [5568.539011812027, 4492.069925905509, 3863.876419829078], velocity: [-4.20910647559332, 5.159719888479543, 2.7448529795547034] }, + { minutes: 1440, position: [-938.559239429339, -6268.187488313942, -4294.029247511629], velocity: [7.536105209256085, -0.4271277071235073, 0.98987807955916] }, + { minutes: 1800, position: [-9680.561217281369, 2802.477713538924, 124.10688038238322], velocity: [-0.9058741021590407, -4.659467969919601, -3.2273475167127206] }, + { minutes: 2160, position: [190.19796987850174, 7746.966536135269, 5110.006754118663], velocity: [-6.112325142013842, 1.5270081835204619, -0.13915235788255884] }, + { minutes: 2520, position: [5579.556401157338, -3995.613967894298, -1518.821089660347], velocity: [4.767927482843895, 5.12318530095394, 4.276837354501653] }, + { minutes: 2880, position: [-8650.730822189496, -1914.9381152518936, -3007.036034428082], velocity: [3.0671651265427506, -4.828384068443628, -2.5153228357217925] }, + { minutes: 3240, position: [-5429.792041644596, 7574.364937924292, 3747.393052358827], velocity: [-4.999442109603997, -1.8005614223050064, -2.2293928302408985] }, + { minutes: 3600, position: [6759.0458372178555, 2001.581982196808, 2783.55192532931], velocity: [-2.180993947176853, 6.402085603047054, 3.6447239516048207] }, + { minutes: 3960, position: [-3791.445315589389, -5712.956178939109, -4533.486307143729], velocity: [6.66881749254763, -2.5163823265273395, -0.08238435374723926] }, + { minutes: 4320, position: [-9060.473735694444, 4658.709525022637, 813.6867315341166], velocity: [-2.232832782742506, -4.110453489937012, -3.1573454334573743] } + ] + }, + { + satellite: 6251, + line1: '1 06251U 62025E 06176.82412014 .00008885 00000-0 12808-3 0 3985', + line2: '2 06251 58.0579 54.0425 0030035 139.1568 221.1854 15.56387291 6774', + gravity: 'WGS72', + states: [ + { minutes: 0, position: [3988.3102269938663, 5498.966572352187, 0.9005587865923731], velocity: [-3.290032737938881, 2.3576528196347417, 6.496623474956849] }, + { minutes: 120, position: [-3935.698000833816, 409.10980836548407, 5471.335773274221], velocity: [-3.3747841826433285, -6.6352110434903455, -1.9420562212599477] }, + { minutes: 240, position: [-1675.1276691494627, -5683.304323518119, -3286.2151093673824], velocity: [5.282496924661049, 1.5086742585735726, -5.35487297767785] }, + { minutes: 360, position: [4993.626428356406, 2890.549699000398, -3600.401456268915], velocity: [0.3473334286679832, 5.707031557192846, 5.0706996376402795] }, + { minutes: 480, position: [-1115.0795951392222, 4015.1169149099514, 5326.997277177558], velocity: [-5.52427944319586, -4.765738774071813, 2.402255961200541] }, + { minutes: 600, position: [-4329.10008197525, -5176.70287935161, 409.6531385744557], velocity: [2.8584083032412333, -2.9330917920543618, -6.509690396536205] }, + { minutes: 720, position: [3692.600300280244, -976.2426525529509, -5623.364474928707], velocity: [3.897257243214073, 6.415554948136158, 1.4291121897703176] }, + { minutes: 840, position: [2301.8351003726193, 5723.923945531924, 2814.6151458027775], velocity: [-5.11092496623846, -0.7645105585248811, 5.662120144737242] }, + { minutes: 960, position: [-4990.916379502503, -2303.4254788002513, 3920.86335598479], velocity: [-0.9934393720412668, -5.967458359672714, -4.759110855938096] }, + { minutes: 1080, position: [642.2776997684242, -4332.898219008863, -5183.315239095724], velocity: [5.72054257871053, 4.216573837772707, -2.846576139177479] }, + { minutes: 1200, position: [4719.783357520406, 4798.0693899591815, -943.5885106241908], velocity: [-2.2948606621071943, 3.4924993885279476, 6.408334723213857] }, + { minutes: 1320, position: [-3299.1699360229763, 1576.831683195467, 5678.678406384836], velocity: [-4.460347074024495, -6.202025195789859, -0.8858745862875084] }, + { minutes: 1440, position: [-2777.146823354639, -5663.16031707687, -2462.5488912323144], velocity: [4.915493146038544, 0.12332899209052428, -5.8964950907015] }, + { minutes: 1560, position: [4992.315738931069, 1716.6235676954177, -4287.8606558115125], velocity: [1.640717188721202, 6.071570433629135, 4.338797930854189] }, + { minutes: 1680, position: [-8.223847547332083, 4662.215216679678, 4905.664118572891], velocity: [-5.891011273528884, -3.5931738719212447, 3.3651004604317665] }, + { minutes: 1800, position: [-4966.201379626488, -4379.591550374151, 1349.3334750222982], velocity: [1.7631725805719454, -3.981456386823269, -6.343279442946705] }, + { minutes: 1920, position: [2954.493903313682, -2080.659846503557, -5754.750380565072], velocity: [4.89589330576897, 5.858184321868794, 0.37547482522996245] }, + { minutes: 2040, position: [3363.2879432082896, 5559.558411795425, 1956.0554226626314], velocity: [-4.587378862766357, 0.5919434032977842, 6.107838604574175] }, + { minutes: 2160, position: [-4856.667800699508, -1107.0345019186243, 4557.212582405701], velocity: [-2.304158557386036, -6.186437069610591, -3.9565495420138097] }, + { minutes: 2280, position: [-497.8448007120538, -4863.460053117073, -4700.812112173796], velocity: [5.960065406702171, 2.996683369168428, -3.767123328696954] }, + { minutes: 2400, position: [5241.619360958935, 3910.759606833708, -1857.9347395222298], velocity: [-1.1248348055814752, 4.406213160311382, 6.148161298549305] }, + { minutes: 2520, position: [-2451.380459529749, 2610.6046326102382, 5729.790220687837], velocity: [-5.366560525017705, -5.500855666468153, 0.18795871634615322] }, + { minutes: 2640, position: [-3791.8752063802217, -5378.82851381942, -1575.8273793010655], velocity: [4.266273591570908, -1.1991625509676176, -6.276154079875782] }, + { minutes: 2760, position: [4730.539583564619, 524.0500643312546, -4857.29369725264], velocity: [2.9180562880121466, 6.135412849159785, 3.495115635633302] }, + { minutes: 2880, position: [1159.2780289715447, 5056.601754953892, 4353.494185788894], velocity: [-5.968060340911224, -2.3147904058676363, 4.230722669089957] } + ] + }, + { + satellite: 22312, + line1: '1 22312U 93002D 06094.46235912 .99999999 81888-5 49949-3 0 3953', + line2: '2 22312 62.1486 77.4698 0308723 267.9229 88.7392 15.95744531 98783', + gravity: 'WGS72', + states: [ + { minutes: 54.2028672, position: [306.10478453210754, -5816.456555245726, -2979.5584606814045], velocity: [3.9506638545949513, 3.415332542536633, -5.879974328912515] }, + { minutes: 74.2028672, position: [3282.8208546395886, 2077.469729049188, -5189.179887704459], velocity: [0.09734270093809777, 7.375135691836999, 2.9001967017804158] }, + { minutes: 94.2028672, position: [530.8272917598147, 6426.207900027287, 1712.3707679335032], velocity: [-3.8371203945186756, -1.2524306368713625, 6.561602576910266] }, + { minutes: 114.2028672, position: [-3191.691702119963, 170.2721991215675, 5956.298077746852], velocity: [-1.394956871506951, -7.438073470735338, -0.5575531150954345] }, + { minutes: 134.20286720000001, position: [-1818.992224649069, -6322.451466164009, 681.9524715393419], velocity: [3.3497951728049853, -1.530140265237883, -6.8315227645351015] }, + { minutes: 154.20286720000001, position: [2515.664486338567, -2158.8309122400465, -5552.133205438366], velocity: [2.5719796600720706, 7.31193050859491, -1.6398656196775476] }, + { minutes: 174.20286720000001, position: [2414.5283321046118, 5749.101509221235, -1998.5969316488213], velocity: [-2.6810329601045364, 3.5275893006301335, 6.452951428507466] }, + { minutes: 194.20286720000001, position: [-1877.9894433108047, 3862.2784830176074, 5112.4843586271445], velocity: [-3.261489804107922, -6.026859137475694, 3.4332547675522447] }, + { minutes: 214.20286720000001, position: [-3117.365843952769, -4419.747738641016, 3840.859609122991], velocity: [1.5454791818096052, -5.475416581198432, -5.207913748469597] }, + { minutes: 234.20286720000001, position: [815.3203467829541, -5231.676922490735, -3760.0469035354126], velocity: [3.8708642004319676, 4.4555885519316565, -5.211082190603326] }, + { minutes: 254.20286720000001, position: [3269.5434181010614, 3029.00081082518, -4704.679697130904], velocity: [-0.5267113448094572, 6.812157949700457, 3.9298250873048755] }, + { minutes: 274.2028672, position: [-10.180997555321385, 6026.233414533572, 2643.5051840711208], velocity: [-3.953623254352354, -2.6160700119518125, 6.145637499655247] }, + { minutes: 294.2028672, position: [-3320.588195836959, -1248.4267994459137, 5563.060179268364], velocity: [-0.6370469743004795, -7.417786043640504, -2.076120187110498] }, + { minutes: 314.2028672, position: [-1025.4897461644732, -6366.989457817915, -911.2355915262766], velocity: [3.8117719089085664, 0.43807148974326404, -6.829260617051935] }, + { minutes: 334.2028672, position: [3003.7599612763292, -413.8570800320805, -5706.155914352839], velocity: [1.6743500833804354, 7.694169068130377, 0.3169152040347238] }, + { minutes: 354.2028672, position: [1731.4281698008595, 6258.276769247225, -409.32527982209496], velocity: [-3.400497805702365, 1.4479454240568772, 6.904010051791686] }, + { minutes: 374.2028672, position: [-2582.5211146016445, 2024.1902067990013, 5647.556502679327], velocity: [-2.530348120668741, -7.2217193926247285, 1.4381415525853605] }, + { minutes: 394.2028672, position: [-2440.5684857785373, -5702.773118773599, 1934.8109468891694], velocity: [2.731792946600513, -3.3505760753816274, -6.5277733386525405] }, + { minutes: 414.2028672, position: [1951.2293439136704, -3423.5944304465243, -5121.678082005962], velocity: [3.2490391326431536, 6.465974362320956, -3.069806658892126] }, + { minutes: 434.2028672, position: [2886.509393555781, 4888.686262156714, -3096.2988598897014], velocity: [-1.9731621385077411, 4.877039020188417, 5.832414910422037] }, + { minutes: 454.2028672, position: [-1276.55532181761, 4553.2689846325575, 4406.197873753053], velocity: [-3.7151464214034684, -5.320176914386, 4.418210777086693] }, + { minutes: 474.2028672, position: [-3181.546980418462, -3831.2997650636603, 4096.802427871688], velocity: [1.1141599698329185, -6.10477357781032, -4.829967400235838] } + ], + error: { minutes: 494.2028672, code: 1 } + }, + { + satellite: 28057, + line1: '1 28057U 03049A 06177.78615833 .00000060 00000-0 35940-4 0 1836', + line2: '2 28057 98.4283 247.6961 0000884 88.1964 271.9322 14.35478080140550', + gravity: 'WGS72', + states: [ + { minutes: 0, position: [-2715.282374856451, -6619.264368890808, -0.013414430179686425], velocity: [-1.008587273274863, 0.4227820027829844, 7.385272941602004] }, + { minutes: 120, position: [-1816.8792094194175, -1835.7876213217032, 6661.079264647466], velocity: [2.325140070633625, 6.655669328663988, 2.4633945115760296] }, + { minutes: 240, position: [1483.173642905295, 5395.212487860186, 4448.659071715157], velocity: [2.5605403872237997, 4.039025765877992, -5.736648561316744] }, + { minutes: 360, position: [2801.256071573132, 5455.039313331243, -3692.1286569449726], velocity: [-0.5950958644237622, -3.9519231170767064, -6.2987991250776165] }, + { minutes: 480, position: [411.0933281183771, -1728.9976915202808, -6935.45548810143], velocity: [-2.935970964045366, -6.684085057511209, 1.4928008855159618] }, + { minutes: 600, position: [-2506.5255845379006, -6628.986550941632, -988.0778449704275], velocity: [-1.3905771887216563, -0.5561641428183158, 7.31273646778384] }, + { minutes: 720, position: [-2090.798842661567, -2723.228321928111, 6266.133565761319], velocity: [1.9926406650610011, 6.337529519478544, 3.411803080481305] }, + { minutes: 840, position: [1091.805602222754, 4809.882295025346, 5172.428978938384], velocity: [2.717483545731576, 4.805518976627159, -5.030019896090944] }, + { minutes: 960, position: [2811.140622995842, 5950.657071710156, -2813.237053893786], velocity: [-0.15966274182351436, -3.121215491213181, -6.775341949331627] }, + { minutes: 1080, position: [805.726983043184, -812.1662790683223, -7067.584839683047], velocity: [-2.7989360197593824, -6.889265977001623, 0.4727708725617341] }, + { minutes: 1200, position: [-2249.59837531573, -6505.8489071390695, -1956.7236506195916], velocity: [-1.7312347287786134, -1.5287502303345437, 7.0966608847654795] }, + { minutes: 1320, position: [-2311.5737579737047, -3560.9911289118127, 5748.167495995566], velocity: [1.6265697511936423, 5.890482233105226, 4.293545047747781] }, + { minutes: 1440, position: [688.1605659366169, 4124.876189635687, 5794.55994449049], velocity: [2.8109736647277113, 5.479585562882076, -4.224866315922067] }, + { minutes: 1560, position: [2759.9408822957216, 6329.872717979826, -1879.1951833085852], velocity: [0.26693067240426094, -2.222670877520611, -7.119390566961012] }, + { minutes: 1680, position: [1171.5067713732456, 125.82053747638038, -7061.966262019728], velocity: [-2.6056878522500613, -6.958489748881182, -0.5563332247578189] }, + { minutes: 1800, position: [-1951.437084724517, -6251.719458202084, -2886.9547235504297], velocity: [-2.0241314825418057, -2.4752142721536483, 6.741537478137292] }, + { minutes: 1920, position: [-2475.7072228788143, -4331.905699581607, 5117.312349243564], velocity: [1.2358235388291188, 5.322743370812032, 5.091281211250665] }, + { minutes: 2040, position: [281.4609784740558, 3353.5105710225935, 6302.8790065024305], velocity: [2.840647273466004, 6.047222485033793, -3.3370859924204423] }, + { minutes: 2160, position: [2650.3311885972207, 6584.334348512519, -908.290271343069], velocity: [0.6754572353940503, -1.2740449716160245, -7.323921566628449] }, + { minutes: 2280, position: [1501.172265974828, 1066.3113275587864, -6918.714729523985], velocity: [-2.3618919043585818, -6.889669973793281, -1.574718619162615] }, + { minutes: 2400, position: [-1619.7346833442643, -5871.140519913147, -3760.56587071421], velocity: [-2.2640939746242545, -3.376316601072307, 6.254622256152414] }, + { minutes: 2520, position: [-2581.0420250494676, -5020.055725305871, 4385.923290466615], velocity: [0.8296684584055073, 4.645048038434418, 5.789262667122109] }, + { minutes: 2640, position: [-119.22080627531297, 2510.906204877387, 6687.456154588034], velocity: [2.807575711632768, 6.496549689314342, -2.384136660660112] }, + { minutes: 2760, position: [2486.2380672577765, 6708.182100279123, 80.43349580651841], velocity: [1.0572749046984022, -0.29429402736528576, -7.384689123310352] }, + { minutes: 2880, position: [1788.4233458037966, 1990.5053095696403, -6640.593377252101], velocity: [-2.07416909063938, -6.683381288034106, -2.562777775601973] } + ] + }, + { + satellite: 28350, + line1: '1 28350U 04020A 06167.21788666 .16154492 76267-5 18678-3 0 8894', + line2: '2 28350 64.9977 345.6130 0024870 260.7578 99.9590 16.47856722116490', + gravity: 'WGS72', + states: [ + { minutes: 0, position: [6333.081231282308, -1580.8285232593505, 90.69355720387874], velocity: [0.7146344234421929, 3.224246549563264, 7.08312813228868] }, + { minutes: 120, position: [-3990.9384585453995, 3052.983419071396, 4155.327006289535], velocity: [-5.909006188027874, -0.8763079661833709, -5.039131403570853] }, + { minutes: 240, position: [-603.552320097287, -2685.1347456901417, -5891.702742823141], velocity: [7.572519906820314, -1.9756567258385833, 0.12172260518519995] }, + { minutes: 360, position: [4788.223456270026, 782.5616921357, 4335.142846214145], velocity: [-4.954509025954119, 3.6833464637795026, 4.804645838578953] }, + { minutes: 480, position: [-6291.846016440598, 1547.8279077185045, -453.67116497694553], velocity: [-0.30862558824839115, -3.3415385735653365, -7.082659115217719] }, + { minutes: 600, position: [4480.745734281553, -3028.5520037409033, -3586.9434364097556], velocity: [5.320920856922729, 1.1997362749268707, 5.626350480559622] }, + { minutes: 720, position: [-446.4246091556006, 2932.2887258780456, 5759.193897566413], velocity: [-7.5610002446957125, 1.5509754933325983, -1.3749708845520852] }, + { minutes: 840, position: [-3713.7958183057613, -1382.6612513012233, -5122.4513113587445], velocity: [6.0909316255420975, -3.5126297328266407, -3.467571745776089] }, + { minutes: 960, position: [6058.320175220019, -827.474067224923, 2104.0467865060023], velocity: [-1.7984030242727496, 3.787067271798423, 6.641439743741389] }, + { minutes: 1080, position: [-5631.736590063632, 2623.7095364359593, 1766.4912508420023], velocity: [-3.216401578199582, -2.309140958514573, -6.788609120174682] }, + { minutes: 1200, position: [2776.849915600965, -3255.3694195261487, -4837.19667789996], velocity: [6.748135564436667, -0.1930448249989965, 4.005718697631363] }, + { minutes: 1320, position: [1148.0443083716918, 2486.0734338613074, 5826.340759132984], velocity: [-7.420162295259302, 2.5894563818792937, 0.3563500059242328] }, + { minutes: 1440, position: [-4527.908718278014, -723.2919904113805, -4527.446083186964], velocity: [5.1216742172243075, -3.909895426835778, -4.500218555578428] } + ], + error: { minutes: 1560, code: 1 } + }, + { + satellite: 28872, + line1: '1 28872U 05037B 05333.02012661 .25992681 00000-0 24476-3 0 1534', + line2: '2 28872 96.4736 157.9986 0303955 244.0492 110.6523 16.46015938 10708', + gravity: 'WGS72', + states: [ + { minutes: 0, position: [-6131.827304557578, 2446.5281552811302, -253.64211033478088], velocity: [-0.14492022756089892, 0.9951009627968789, 7.658645066821606] }, + { minutes: 5, position: [-5799.242561335917, 2589.1481111876355, 2011.5451509955988], velocity: [2.3252073643678517, -0.04712567240988647, 7.296234070991355] }, + { minutes: 10, position: [-4769.050619669655, 2420.4658056178746, 4035.308558368168], velocity: [4.464585796417195, -1.060923208852902, 6.070907874485179] }, + { minutes: 15, position: [-3175.4515733990224, 1965.9873808587436, 5582.125696073361], velocity: [6.0496393764778835, -1.935777557718341, 4.1486070193768025] }, + { minutes: 20, position: [-1210.1902480238302, 1281.545412936926, 6474.681727721375], velocity: [6.920746272878132, -2.5805173371720125, 1.7487838675086753] }, + { minutes: 25, position: [896.7379953279715, 447.1235730478032, 6607.224005065362], velocity: [6.983396281749225, -2.925846167662451, -0.8726552065310886] }, + { minutes: 30, position: [2896.996635343566, -440.04738594359463, 5954.926754863545], velocity: [6.211488246364875, -2.9269498148660444, -3.433959805617249] }, + { minutes: 35, position: [4545.789701674152, -1273.5595287205315, 4580.165129838184], velocity: [4.656984232728908, -2.568711512558554, -5.6385109537518785] }, + { minutes: 40, position: [5627.432993705983, -1947.942824694088, 2634.167149295457], velocity: [2.4641410467823537, -1.873985160836486, -7.195743032304255] }, + { minutes: 45, position: [5984.723185336559, -2371.376916089509, 349.87996209105285], velocity: [-0.12127694956891878, -0.9119815461884553, -7.859613893997606] }, + { minutes: 50, position: [5548.433259217731, -2480.1646924483034, -1979.2431452695466], velocity: [-2.763269533888766, 0.19969191531464883, -7.482796996303026] } + ], + error: { minutes: 55, code: 6 } + }, + { + satellite: 29141, + line1: '1 29141U 85108AA 06170.26783845 .99999999 00000-0 13519-0 0 718', + line2: '2 29141 82.4288 273.4882 0015848 277.2124 83.9133 15.93343074 6828', + gravity: 'WGS72', + states: [ + { minutes: 0, position: [423.99295523853493, -6658.122561487883, 136.13040356370743], velocity: [1.0063736128804568, 0.21730998326153186, 7.6625878922395625] }, + { minutes: 20, position: [931.8088358699861, -1017.178522392653, 6529.192445267757], velocity: [-0.2988479182834455, 7.613891977074904, 1.2263994803688032] }, + { minutes: 40, position: [-83.44906141046083, 6286.202084526288, 2223.4983716064403], velocity: [-1.113515974335641, 2.5309702834276626, -7.219445567640433] }, + { minutes: 60, position: [-958.5768122082874, 3259.2600534781322, -5722.637324667127], velocity: [-0.10122581272574771, -6.735338321376095, -3.804851872132342] }, + { minutes: 80, position: [-255.25619984630657, -5132.5976297356465, -4221.272331177083], velocity: [1.0777093031738276, -4.9059388244232895, 5.892521263779435] }, + { minutes: 100, position: [867.4429509677432, -5038.404029332889, 4256.738105326341], velocity: [0.4794475348774422, 5.032326445668294, 5.857126248379358] }, + { minutes: 120, position: [559.1688201275209, 3376.305879365118, 5699.220173906217], velocity: [-0.9067493283051573, 6.646149866798387, -3.8523318321889435] }, + { minutes: 140, position: [-669.8518420486322, 6196.002294840498, -2281.957417704236], velocity: [-0.7958040917752033, -2.7521148271039855, -7.202478519577238] }, + { minutes: 160, position: [-784.2070801929785, -1278.5312555326452, -6449.19892596474], velocity: [0.6367023798192911, -7.595425203450997, 1.4310908017587354] }, + { minutes: 180, position: [406.1581165884289, -6607.031157990043, 148.3302147702311], velocity: [1.0098185745113821, 0.23184376547108268, 7.692047844379437] }, + { minutes: 200, position: [916.3491181316775, -884.0864924779974, 6491.0981036166495], velocity: [-0.3021630486618081, 7.669887108902456, 1.08433690889141] }, + { minutes: 220, position: [-104.02490969991038, 6304.318214051429, 1960.0873988151543], velocity: [-1.1088738230882464, 2.259522809253055, -7.351147709904713] }, + { minutes: 240, position: [-944.6164284893264, 2872.172483790068, -5846.941033615948], velocity: [-0.05111768583039081, -6.989747075746093, -3.4131026003346205] }, + { minutes: 260, position: [-187.16569888181903, -5404.86163467312, -3731.97057617958], velocity: [1.094696706448572, -4.412110995417177, 6.326060952339141] }, + { minutes: 280, position: [884.5972046732949, -4465.745161630328, 4725.836326961098], velocity: [0.38065602780935603, 5.691554045612658, 5.30391098338551] }, + { minutes: 300, position: [446.407672361325, 4086.6683962040274, 5093.055966500996], velocity: [-0.9824244465966789, 6.072965199474507, -4.791630681765543] }, + { minutes: 320, position: [-752.2446749458933, 5588.354733010528, -3275.040925732244], velocity: [-0.6611613700329351, -4.016290740302341, -6.676898026329769] }, + { minutes: 340, position: [-643.7287252547507, -2585.025285602386, -5923.013066083448], velocity: [0.8079221418095234, -7.171597814175038, 3.041115058342119] }, + { minutes: 360, position: [584.4029581850625, -6202.356058170829, 1781.005360193892], velocity: [0.8692504496186341, 2.2269275139567317, 7.4716767647099] }, + { minutes: 380, position: [779.592117649139, 1100.737283014804, 6311.59529480455], velocity: [-0.5995523048655652, 7.721032522310464, -1.2751530265277595] }, + { minutes: 400, position: [-403.031555876765, 6399.180008372443, -364.1273587498667], velocity: [-1.0088619238843504, -0.5166366152128109, -7.799812287320059] }, + { minutes: 420, position: [-852.9391007119066, 192.65232022727355, -6322.470547838404], velocity: [0.3960061942603383, -7.882964919438255, -0.2893315173451259] } + ], + error: { minutes: 440, code: 6 } + }, + { + satellite: 29238, + line1: '1 29238U 06022G 06177.28732010 .00766286 10823-4 13334-2 0 101', + line2: '2 29238 51.5595 213.7903 0202579 95.2503 267.9010 15.73823839 1061', + gravity: 'WGS72', + states: [ + { minutes: 0, position: [-5566.595128191503, -3789.759911585479, 67.6038224526737], velocity: [2.8737593669482417, -3.8253405226616213, 6.023253925536158] }, + { minutes: 120, position: [4474.279154953501, -1447.7228614161045, 4619.839272345221], velocity: [4.712595821979766, 5.668306152772172, -2.701606741038794] }, + { minutes: 240, position: [1922.1771247430481, 5113.011383417407, -4087.0847020334027], velocity: [-6.490769650540698, -0.5223501575278644, -3.896001153600789] }, + { minutes: 360, position: [-6157.935468820675, -2094.7079879027106, -1941.6373095978577], velocity: [0.14990066085939194, -5.175192523207431, 5.604262034415371] }, + { minutes: 480, position: [2482.6405241086377, -3268.459445550347, 5146.380061901752], velocity: [6.501814698229904, 4.402848753801318, -0.35094351075615166] }, + { minutes: 600, position: [4036.2645528664953, 4827.433472009508, -2507.990639547933], velocity: [-5.184409515423431, 1.7722806947173175, -5.3313901679897855] }, + { minutes: 720, position: [-5776.813716215107, -118.64155319302131, -3641.2205241818747], velocity: [-2.539917206750309, -5.622701582125303, 4.403125405189846] }, + { minutes: 840, position: [67.98699486520641, -4456.492134728365, 4863.717942833174], velocity: [7.183809419588049, 2.4189177910521233, 2.0156424952176573] }, + { minutes: 960, position: [5520.622070378933, 3782.3820355419807, -596.7319316080601], velocity: [-3.0279660693466015, 3.7541525247682404, -6.013506362859319] }, + { minutes: 1080, position: [-4528.051044554213, 1808.4627332919815, -4816.997277621287], velocity: [-4.808419762696622, -5.185789344827758, 2.6421044937772957] }, + { minutes: 1200, position: [-2356.6146807790774, -4852.512022719205, 3856.53816183684], velocity: [6.688446735148577, 0.1185209579824994, 4.021854210077395] }, + { minutes: 1320, position: [6149.65800134396, 2173.5942326070954, 1369.2948873245466], velocity: [-0.3458327771842257, 5.10985786107633, -5.842951828174663] }, + { minutes: 1440, position: [-2629.5501144882205, 3400.9804015773816, -5344.382171288449], velocity: [-6.368548448364396, -3.998963508931854, 0.5772530637682715] } + ] + }, + { + satellite: 88888, + line1: '1 88888U 80275.98708465 .00073094 13844-3 66816-4 0 87', + line2: '2 88888 72.8435 115.9689 0086731 52.6988 110.5714 16.05824518 1058', + gravity: 'WGS72', + states: [ + { minutes: 0, position: [2328.969752620943, -5995.220513378918, 1719.9729719163176], velocity: [2.912073281253137, -0.9834179557957405, -7.0908162100620356] }, + { minutes: 120, position: [1020.6923455765659, 2286.5626063423774, -6191.555659269564], velocity: [-3.7465439022343627, 6.467532720712933, 1.8279856781429002] }, + { minutes: 240, position: [-3226.5434915541346, 3503.709775249396, 4532.809793430064], velocity: [1.000992116066745, -5.788042888411696, 5.162585825661675] }, + { minutes: 360, position: [2456.1070653344295, -6071.938555030082, 1222.8976855384808], velocity: [2.6793900402341913, -0.4482908110760663, -7.228792154938372] }, + { minutes: 480, position: [787.1645734898926, 2719.918009459884, -6043.866620243129], velocity: [-3.759883838594647, 6.277439314036993, 2.3978978638579056] }, + { minutes: 600, position: [-3110.97648029332, 3121.7302623530013, 4878.152170354566], velocity: [1.2449160559939776, -6.124880424954971, 4.7005763534279765] }, + { minutes: 720, position: [2567.562296951302, -6112.50383922254, 713.9637443537242], velocity: [2.440245751323666, 0.0981090021393261, -7.319959258254312] }, + { minutes: 840, position: [556.0566178033511, 3144.522882008354, -5855.346361781805], velocity: [-3.7546601432527513, 6.044752775391769, 2.957941671728186] }, + { minutes: 960, position: [-2982.479405392962, 2712.616637110355, 5192.323304718569], velocity: [1.4755667732325752, -6.427737013518157, 4.202420226902073] }, + { minutes: 1080, position: [2663.0896435219693, -6115.482908846454, 196.400728664597], velocity: [2.1961215638782967, 0.6524150925789076, -7.362824152460273] }, + { minutes: 1200, position: [328.54999673840695, 3557.09490552017, -5626.214272111322], velocity: [-3.731193288240498, 5.769341171972462, 3.5040587308184157] }, + { minutes: 1320, position: [-2842.0687675733257, 2278.4234349205335, 5472.334371495679], velocity: [1.6918526353822458, -6.693216334929985, 3.6710227118769274] }, + { minutes: 1440, position: [2742.553988316696, -6079.6700912285605, -326.3901264920671], velocity: [1.948497651477911, 1.211072678443041, -7.356193131277518] } + ] + }, + { + satellite: 5, + line1: '1 00005U 58002B 00179.78495062 .00000023 00000-0 28098-4 0 4753', + line2: '2 00005 34.2682 348.7242 1859667 331.7664 19.3264 10.82419157413667', + gravity: 'WGS72OLD', + states: [ + { minutes: 0, position: [7022.465290574917, -1400.0829671389206, 0.039951552899142956], velocity: [1.8938410139508999, 6.405893757305204, 4.534807249007841] }, + { minutes: 360, position: [-7154.031199858261, -3783.1768239300586, -3536.1941219313903], velocity: [4.7418874076075, -4.15181776413348, -2.0939354242488966] }, + { minutes: 720, position: [-7134.593399135211, 6531.686411378113, 3260.2718637436788], velocity: [-4.11379302589669, -2.911922037778983, -2.557327850215744] }, + { minutes: 1080, position: [5568.539010059382, 4492.069924613793, 3863.8764187636693], velocity: [-4.209106474421074, 5.159719886937953, 2.7448529786349654] }, + { minutes: 1440, position: [-938.5592390002769, -6268.187486484606, -4294.029246217086], velocity: [7.536105206992379, -0.4271277069405316, 0.9898780794587957] }, + { minutes: 1800, position: [-9680.561214429745, 2802.4777126241797, 124.10688001370676], velocity: [-0.9058741017484131, -4.659467968570097, -3.227347515743341] }, + { minutes: 2160, position: [190.19796955072036, 7746.9665338872355, 5110.006752520551], velocity: [-6.112325140207838, 1.5270081830043607, -0.1391523580896084] }, + { minutes: 2520, position: [5579.556399609561, -3995.6139666627473, -1518.8210889116715], velocity: [4.767927481180304, 5.123185299541292, 4.276837353373376] }, + { minutes: 2880, position: [-8650.730819440645, -1914.9381148459934, -3007.0360339368694], velocity: [3.067165125821611, -4.828384067006513, -2.5153228347449956] }, + { minutes: 3240, position: [-5429.792040354122, 7574.364935658298, 3747.3930507983564], velocity: [-4.9994421079665905, -1.800561421885857, -2.229392829826686] }, + { minutes: 3600, position: [6759.045835000906, 2001.581981771888, 2783.5519248763935], velocity: [-2.1809939468613453, 6.402085601185975, 3.6447239502506212] }, + { minutes: 3960, position: [-3791.4453140503224, -5712.95617742003, -4533.486305913112], velocity: [6.668817490615769, -2.516382325659447, -0.08238435320879575] }, + { minutes: 4320, position: [-9060.47373315342, 4658.709523477374, 813.6867304944666], velocity: [-2.232832781763688, -4.110453488847057, -3.1573454325714447] } + ] + }, + { + satellite: 5, + line1: '1 00005U 58002B 00179.78495062 .00000023 00000-0 28098-4 0 4753', + line2: '2 00005 34.2682 348.7242 1859667 331.7664 19.3264 10.82419157413667', + gravity: 'WGS84', + states: [ + { minutes: 0, position: [7022.46647249137, -1400.0665618178339, 0.05106558274635007], velocity: [1.8938310806788694, 6.405894872518269, 4.534806700953066] }, + { minutes: 360, position: [-7154.031829698212, -3783.162221437533, -3536.1837282572305], velocity: [4.741886113777932, -4.151823664356791, -2.0939402147881627] }, + { minutes: 720, position: [-7134.585278038725, 6531.696403883128, 3260.281688470028], velocity: [-4.113791542441389, -2.911916882771503, -2.557323420821262] }, + { minutes: 1080, position: [5568.531485688488, 4492.085586027695, 3863.8840128249576], velocity: [-4.20910817972236, 5.159708462020683, 2.7448470236039015] }, + { minutes: 1440, position: [-938.5492314464462, -6268.178320422504, -4294.021784719687], velocity: [7.536113288532667, -0.42712129595803333, 0.9898801742737523] }, + { minutes: 1800, position: [-9680.557045549427, 2802.484097100532, 124.11776697232013], velocity: [-0.9058744558806456, -4.659466343823729, -3.2273467318776103] }, + { minutes: 2160, position: [190.18894747891187, 7746.973895976892, 5110.011231260967], velocity: [-6.112317703355869, 1.5269988580145808, -0.13915262726632643] }, + { minutes: 2520, position: [5579.570191319096, -3995.5890377891155, -1518.8074881532496], velocity: [4.767910019364167, 5.123201411873871, 4.276841933039963] }, + { minutes: 2880, position: [-8650.724261366455, -1914.9389859602882, -3007.0279856661928], velocity: [3.0671712542532696, -4.8283828531197015, -2.5153249521719316] }, + { minutes: 3240, position: [-5429.799700744388, 7574.36327385986, 3747.3983173814086], velocity: [-4.999433929131717, -1.8005667617423156, -2.229390109354348] }, + { minutes: 3600, position: [6759.036900849698, 2001.6145171818812, 2783.564105201304], velocity: [-2.18101694489972, 6.4020727103992465, 3.6447154603464846] }, + { minutes: 3960, position: [-3791.4240655706617, -5712.957045303169, -4533.47981732043], velocity: [6.668830369014187, -2.5163683422573055, -0.08238169738616816] }, + { minutes: 4320, position: [-9060.478175076545, 4658.697917019259, 813.6926008473886], velocity: [-2.232823140968453, -4.110456334189285, -3.157344043920063] } + ] + }, + { + satellite: 28057, + line1: '1 28057U 03049A 06177.78615833 .00000060 00000-0 35940-4 0 1836', + line2: '2 28057 98.4283 247.6961 0000884 88.1964 271.9322 14.35478080140550', + gravity: 'WGS72OLD', + states: [ + { minutes: 0, position: [-2715.2823740490353, -6619.264366924208, -0.013414434536107979], velocity: [-1.0085872729754288, 0.4227820026549733, 7.385272939405877] }, + { minutes: 120, position: [-1816.8792088835103, -1835.7876207941226, 6661.07926265826], velocity: [2.3251400699316487, 6.655669326680901, 2.4633945108603617] }, + { minutes: 240, position: [1483.1736424432002, 5395.21248623976, 4448.659070417949], velocity: [2.5605403864633094, 4.0390257647107095, -5.736648559586539] }, + { minutes: 360, position: [2801.2560707314683, 5455.039311744864, -3692.1286558051497], velocity: [-0.59509586421602, -3.9519231158609758, -6.298799123230336] }, + { minutes: 480, position: [411.0933280318233, -1728.9976909385066, -6935.455486055927], velocity: [-2.9359709631428768, -6.684085055547003, 1.4928008849954124] }, + { minutes: 600, position: [-2506.5255837472037, -6628.986548975553, -988.0778447713388], velocity: [-1.3905771883399942, -0.5561641427505757, 7.3127364655952105] }, + { minutes: 720, position: [-2090.7988420557617, -2723.2283212232355, 6266.133563845184], velocity: [1.9926406644028665, 6.337529517559671, 3.4118030795667305] }, + { minutes: 840, position: [1091.8056018233267, 4809.882293519749, 5172.428977484562], velocity: [2.717483544915323, 4.805518975304961, -5.03001989449698] }, + { minutes: 960, position: [2811.1406221237753, 5950.65707002101, -2813.2370529286763], velocity: [-0.15966274169659972, -3.1212154901611173, -6.775341947374366] }, + { minutes: 1080, position: [805.7269828724557, -812.1662786682064, -7067.584837593817], velocity: [-2.7989360188530967, -6.889265974988122, 0.4727708722507829] }, + { minutes: 1200, position: [-2249.5983745487483, -6505.84890518682, -1956.7236502171857], velocity: [-1.7312347283116465, -1.528750230071915, 7.096660882600826] }, + { minutes: 1320, position: [-2311.5737572975895, -3560.9911280346587, 5748.167494167678], velocity: [1.6265697505874788, 5.89048223126357, 4.293545046639861] }, + { minutes: 1440, position: [688.1605656075718, 4124.876188255164, 5794.5599428897385], velocity: [2.8109736638580367, 5.479585561413818, -4.224866314477867] }, + { minutes: 1560, position: [2759.9408813980594, 6329.872716199384, -1879.195182526892], velocity: [0.2669306724436282, -2.2226708766389955, -7.119390564907783] }, + { minutes: 1680, position: [1171.506771113695, 125.82053769199769, -7061.966259902544], velocity: [-2.6056878513473665, -6.958489746832401, -0.5563332248549326] }, + { minutes: 1800, position: [-1951.4370839896678, -6251.719456276975, -2886.954722945383], velocity: [-2.024131481987081, -2.4752142716969243, 6.741537476013573] }, + { minutes: 1920, position: [-2475.707222133046, -4331.905698537273, 5117.312347519255], velocity: [1.2358235382841631, 5.322743369060512, 5.091281209955704] }, + { minutes: 2040, position: [281.4609782237898, 3353.510569777417, 6302.879004765229], velocity: [2.8406472725467866, 6.047222483428505, -3.3370859911395314] }, + { minutes: 2160, position: [2650.3311876803477, 6584.334346652094, -908.2902707539073], velocity: [0.6754572353386523, -1.2740449709123636, -7.323921564493666] }, + { minutes: 2280, position: [1501.1722656220004, 1066.311327586479, -6918.714727395258], velocity: [-2.3618919034685155, -6.889669971723363, -1.5747186190416547] }, + { minutes: 2400, position: [-1619.7346826513972, -5871.140518028921, -3760.565869907575], velocity: [-2.264093973980243, -3.3763166004219998, 6.254622254087449] }, + { minutes: 2520, position: [-2581.0420242361165, -5020.0557240997205, 4385.9232888613105], velocity: [0.8296684579318948, 4.645048036785977, 5.789262665647375] }, + { minutes: 2640, position: [-119.2208064379567, 2510.906203778179, 6687.456152725767], velocity: [2.8075757106696115, 6.496549687581391, -2.38413665955614] }, + { minutes: 2760, position: [2486.238066329707, 6708.182098350562, 80.43349619506009], velocity: [1.0572749045414667, -0.2942940268465959, -7.384689121109456] }, + { minutes: 2880, position: [1788.423345354112, 1990.5053094062157, -6640.593375129071], velocity: [-2.074169089772498, -6.683381285957658, -2.562777775259701] } + ] + }, + { + satellite: 28057, + line1: '1 28057U 03049A 06177.78615833 .00000060 00000-0 35940-4 0 1836', + line2: '2 28057 98.4283 247.6961 0000884 88.1964 271.9322 14.35478080140550', + gravity: 'WGS84', + states: [ + { minutes: 0, position: [-2715.287003707291, -6619.260574810896, 0.025181077284709197], velocity: [-1.0085792691107278, 0.42280075852894716, 7.385271069499136] }, + { minutes: 120, position: [-1816.8798319359091, -1835.7822929448066, 6661.098355909296], velocity: [2.3251339812304037, 6.655651908235241, 2.463383158274037] }, + { minutes: 240, position: [1483.16289096613, 5395.202124256539, 4448.692066064492], velocity: [2.560538665957922, 4.0390335437722555, -5.7366246449458655] }, + { minutes: 360, position: [2801.252858692904, 5455.052015583947, -3692.0892112011197], velocity: [-0.5950860533200637, -3.951909843587366, -6.298818750169017] }, + { minutes: 480, position: [411.09297751177473, -1728.9922025358508, -6935.435312154954], velocity: [-2.9359750031719902, -6.684103466506731, 1.4928001621083618] }, + { minutes: 600, position: [-2506.525873665195, -6628.985857896601, -988.0494450228834], velocity: [-1.3905737595858132, -0.5561561902480492, 7.312738711167631] }, + { minutes: 720, position: [-2090.8001675825526, -2723.2306881532077, 6266.149777910668], velocity: [1.9926303780997459, 6.337512476611499, 3.41179692371137] }, + { minutes: 840, position: [1091.7890942947954, 4809.865151271798, 5172.465627750378], velocity: [2.7174783272029783, 4.805529578086341, -5.029987561544257] }, + { minutes: 960, position: [2811.1330676355137, 5950.67338549166, -2813.1859069030766], velocity: [-0.15964623730707372, -3.1211884748559955, -6.775361672514328] }, + { minutes: 1080, position: [805.7313528952357, -812.1483651138979, -7067.564866463458], velocity: [-2.7989344469817365, -6.88928757891339, 0.47275434581369546] }, + { minutes: 1200, position: [-2249.5933128766997, -6505.848258223141, -1956.7055801994566], velocity: [-1.7312345909426958, -1.5287545158373768, 7.096663925987036] }, + { minutes: 1320, position: [-2311.5742300221136, -3561.0007143842095, 5748.178885028406], velocity: [1.6265552091286353, 5.890463670356852, 4.293543814180238] }, + { minutes: 1440, position: [688.1390909531544, 4124.8507822764695, 5794.598057492924], velocity: [2.810963399552649, 5.479596967785742, -4.2248253660471775] }, + { minutes: 1560, position: [2759.927362565463, 6329.889511052364, -1879.132023574364], velocity: [0.26695210266068214, -2.222629570076655, -7.11940641788587] }, + { minutes: 1680, position: [1171.5141891837775, 125.85142412703125, -7061.9429455304535], velocity: [-2.6056790022417586, -6.958510101583218, -0.5563655693042824] }, + { minutes: 1800, position: [-1951.426004376037, -6251.715649822133, -2886.9464596342827], velocity: [-2.024133055834403, -2.4752314254731913, 6.741537779634024] }, + { minutes: 1920, position: [-2475.705291178601, -4331.9215555746805, 5117.317131875997], velocity: [1.2358049335726924, 5.322721295124203, 5.091284325596196] }, + { minutes: 2040, position: [281.43565857969213, 3353.475831899817, 6302.916271052201], velocity: [2.840630717830594, 6.047232620442443, -3.337036653529736] }, + { minutes: 2160, position: [2650.310450273117, 6584.348352607726, -908.2154980653728], velocity: [0.6754814560865361, -1.2739897184291116, -7.3239297572088775] }, + { minutes: 2280, position: [1501.180785983508, 1066.354910713358, -6918.684592279528], velocity: [-2.3618745856644545, -6.889684537776863, -1.5747657670969082] }, + { minutes: 2400, position: [-1619.7173068069908, -5871.13034943093, -3760.5662236025682], velocity: [-2.264095447093818, -3.3763463625281016, 6.254616238760084] }, + { minutes: 2520, position: [-2581.036201351331, -5020.076433018415, 4385.919910393559], velocity: [0.8296462342005452, 4.645020426757836, 5.789269207462996] }, + { minutes: 2640, position: [-119.24857017254386, 2510.8615086478912, 6687.490211366355], velocity: [2.807551961829033, 6.496556492715384, -2.384079521580549] }, + { minutes: 2760, position: [2486.209281835464, 6708.190058825787, 80.51880210665097], velocity: [1.0572995308119966, -0.2942260267235499, -7.384686231198953] }, + { minutes: 2880, position: [1788.430841796557, 1990.5604855482973, -6640.553173606418], velocity: [-2.074142657362771, -6.683385660497638, -2.562837717983687] } + ] + } +] + +/*****************************************************************************************************************/ diff --git a/tests/sgp4.spec.ts b/tests/sgp4.spec.ts index 732b359..2458944 100644 --- a/tests/sgp4.spec.ts +++ b/tests/sgp4.spec.ts @@ -370,12 +370,18 @@ describe('SatelliteRecordOptions', () => { /*****************************************************************************************************************/ -// The element set of the International Space Station (ZARYA) of 2008-09-20: +// The element set of the International Space Station (ZARYA) of 2008-09-20, e.g., a near-Earth orbit: const iss = parseTwoLineElement( '1 25544U 98067A 08264.51782528 -.00002182 00000-0 -11606-4 0 2927', '2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537' ) +// The element set of a geostationary satellite (24208) of the verification set, e.g., a deep-space orbit: +const geostationary = parseTwoLineElement( + '1 24208U 96044A 06177.04061740 -.00000094 00000-0 10000-3 0 1600', + '2 24208 3.8536 80.0121 0026640 311.0977 48.3000 1.00778054 36119' +) + /*****************************************************************************************************************/ describe('SatelliteRecord', () => { @@ -441,6 +447,101 @@ describe('SatelliteRecord', () => { () => new SatelliteRecord(iss, { mode: 'i' as SatelliteRecordOptions['mode'] }) ).toThrowError(/Invalid mode: "i" is not one of "improved" or "afspc"/) }) + + it('should throw an SGP4Error for an orbit of the deep-space theory, which is not yet resolved', () => { + expect(() => new SatelliteRecord(geostationary)).toThrowError(SGP4Error) + expect(() => new SatelliteRecord(geostationary)).toThrowError(/deep-space theory/) + }) + + it('should throw an SGP4DecayedError for an element set whose orbit has decayed at its epoch', () => { + // A mean motion of 17.5 revolutions per day is of an orbit whose semi-major axis is less than + // the radius of the Earth: + expect(() => new SatelliteRecord({ ...iss, n: 17.5 })).toThrowError(SGP4DecayedError) + }) +}) + +/*****************************************************************************************************************/ + +describe('getStateVectorAtMinutesSinceEpoch', () => { + it('should be defined', () => { + expect(new SatelliteRecord(iss).getStateVectorAtMinutesSinceEpoch).toBeDefined() + }) + + it('should resolve the state vector of a near-Earth orbit in SI metres and metres per second', () => { + const { position, velocity } = new SatelliteRecord(iss).getStateVectorAtMinutesSinceEpoch(0) + + // The International Space Station orbits at ~350 km above the surface, at ~7.7 km/s: + const radius = Math.hypot(position.x, position.y, position.z) + + const speed = Math.hypot(velocity.x, velocity.y, velocity.z) + + expect(radius).toBeGreaterThan(6.6e6) + expect(radius).toBeLessThan(6.8e6) + expect(speed).toBeGreaterThan(7.6e3) + expect(speed).toBeLessThan(7.8e3) + }) + + it('should return to its position after one orbital period, to within the perturbations of the theory', () => { + const record = new SatelliteRecord(iss) + + // The period of the orbit, from its mean motion of revolutions per day (in minutes): + const period = 1440 / iss.n + + const { position: start } = record.getStateVectorAtMinutesSinceEpoch(0) + + const { position: end } = record.getStateVectorAtMinutesSinceEpoch(period) + + // The secular perturbations of the theory, e.g., the regression of the node and the advance of + // the perigee, carry the object ~5.6 km from where it began over one period: + const separation = Math.hypot(end.x - start.x, end.y - start.y, end.z - start.z) + + expect(separation).toBeGreaterThan(1e3) + expect(separation).toBeLessThan(10e3) + }) + + it('should propagate to a time before the epoch, e.g., a negative time since it', () => { + const { position } = new SatelliteRecord(iss).getStateVectorAtMinutesSinceEpoch(-1440) + + expect(Math.hypot(position.x, position.y, position.z)).toBeGreaterThan(6.6e6) + }) + + it('should propagate a near-Earth orbit alike in either mode of operation', () => { + // The mode of operation displaces the sidereal angle at the epoch alone, which the near-Earth + // theory does not resolve the state vector from: + const improved = new SatelliteRecord(iss, { mode: 'improved' }) + + const afspc = new SatelliteRecord(iss, { mode: 'afspc' }) + + expect(afspc.getStateVectorAtMinutesSinceEpoch(720)).toEqual( + improved.getStateVectorAtMinutesSinceEpoch(720) + ) + }) + + it('should not be displaced by a mutation of the element set it was initialised from', () => { + const tle = { ...iss } + + const record = new SatelliteRecord(tle) + + const before = record.getStateVectorAtMinutesSinceEpoch(90) + + tle.n = 1 + + tle.i = 0 + + expect(record.getStateVectorAtMinutesSinceEpoch(90)).toEqual(before) + }) + + it('should throw for a time since the epoch that is not finite', () => { + const record = new SatelliteRecord(iss) + + expect(() => record.getStateVectorAtMinutesSinceEpoch(Number.NaN)).toThrowError( + /the minutes since the epoch must be finite/ + ) + + expect(() => record.getStateVectorAtMinutesSinceEpoch(Number.POSITIVE_INFINITY)).toThrowError( + /the minutes since the epoch must be finite/ + ) + }) }) /*****************************************************************************************************************/