orbit-studio / src /modules /util /temeToFixed.ts
moncefem's picture
Deploy Orbit Studio propagator
9f21d0a
Raw
History Blame Contribute Delete
4.8 kB
// The TEME to pseudo-fixed rotation, as arithmetic on epoch milliseconds.
//
// Cesium's `Transforms.computeTemeToPseudoFixedMatrix` is a rotation about Z by a
// single angle, and everything around it is overhead on six useful flops: a
// `JulianDate` per sample, a leap-second lookup, a `Matrix3` allocated because the
// call site passes no result, and a full 3x3 multiply. Measured over 5,000
// satellites of 241 samples, the transform half of a window fill:
//
// Cesium, as #applyChunk called it 220 ms
// Cesium, with scratch objects 105 ms
// this, rotation written inline 31 ms
//
// It is the same formula rather than an approximation of it — the constants below
// are Cesium's — so the two agree to 0.001 mm over 42 anchors spanning three years,
// including the midnight and noon crossings where the parameterisation changes
// branch. The point is not a cheaper transform but a Cesium-free one: it can move
// into the propagation worker, where the main thread stops paying for it at all.
//
// Deliberately *not* the GMST that satellite.js's `gstime` computes. That evaluates
// the polynomial at the instant where this evaluates it at 0h and adds Earth's
// rotation rate since; they differ by 1.5e-9 rad, which is 1 cm at LEO and 6 cm at
// GEO. Immaterial on its own — the interpolation error in GridPositionProperty is
// metres — but this way the stored positions stay bit-identical to what Cesium's own
// transform produced, so the change is invisible to everything downstream.
/** Cesium's GMST polynomial, in seconds, evaluated in Julian centuries from J2000. */
const GMST_C0 = 6 * 3600 + 41 * 60 + 50.54841;
const GMST_C1 = 8640184.812866;
const GMST_C2 = 0.093104;
const GMST_C3 = -6.2e-6;
/** Precession of the rotation rate, per day from J2000. */
const RATE_COEF = 1.1772758384668e-19;
const WGS84_WR_PRECESSING = 7.2921158553e-5;
const TWO_PI = Math.PI * 2;
const SECONDS_PER_DAY = 86400;
const TWO_PI_PER_SECONDS_PER_DAY = TWO_PI / SECONDS_PER_DAY;
const MS_PER_DAY = 86_400_000;
const J2000_DAY_NUMBER = 2451545;
/** J2000 proper is noon, so the rate term is referred to the midnight before it. */
const J2000_MIDNIGHT = J2000_DAY_NUMBER - 0.5;
/**
* Cesium splits a Julian date this way, and the Unix epoch lands mid-day in it —
* a Julian day starts at noon, so 00:00 UTC is half a day in.
*/
const UNIX_EPOCH_DAY_NUMBER = 2440587;
const HALF_DAY_SECONDS = 43200;
/**
* Greenwich hour angle for a UTC instant, in radians.
*
* Unix time is already UTC, which is the frame Cesium reduces to before doing this
* arithmetic — it converts UTC to TAI on the way in and subtracts `taiMinusUtc` back
* off here — so taking epoch milliseconds skips a round trip rather than skipping a
* correction. The one case where that is not merely equivalent is an interval
* spanning a leap second, where TAI and Unix time disagree about how many seconds
* elapsed; see the test, which pins the bound rather than assuming it away.
*/
export function greenwichHourAngle(epochMs: number): number {
// The day is split off as an integer first. Carrying the whole Julian date in one
// double puts the value near 2.44e6, whose ulp is about 47 us of Earth rotation —
// measured as a 9.5 mm error before this was split. Keeping `secondsOfDay` small
// keeps its resolution.
const days = Math.floor(epochMs / MS_PER_DAY);
const msIntoDay = epochMs - days * MS_PER_DAY;
let dayNumber = UNIX_EPOCH_DAY_NUMBER + days;
let secondsOfDay = HALF_DAY_SECONDS + msIntoDay / 1000;
if (secondsOfDay >= SECONDS_PER_DAY) {
secondsOfDay -= SECONDS_PER_DAY;
dayNumber += 1;
}
// GMST is tabulated at 0h, so the half-day offset picks the tabulation this
// instant belongs to.
const centuries = (dayNumber - J2000_DAY_NUMBER + (secondsOfDay >= HALF_DAY_SECONDS ? 0.5 : -0.5)) / 36525;
const gmstSeconds = GMST_C0 + centuries * (GMST_C1 + centuries * (GMST_C2 + centuries * GMST_C3));
const angleAt0h = (gmstSeconds * TWO_PI_PER_SECONDS_PER_DAY) % TWO_PI;
const rotationRate = WGS84_WR_PRECESSING + RATE_COEF * (dayNumber - J2000_MIDNIGHT);
const secondsSinceMidnight = (secondsOfDay + HALF_DAY_SECONDS) % SECONDS_PER_DAY;
return angleAt0h + rotationRate * secondsSinceMidnight;
}
/**
* The rotation for one instant, as the cosine and sine of the hour angle.
*
* Handed back as a pair rather than a matrix because seven of the nine entries are
* constant: the fixed-frame position is
* `(c*x + s*y, -s*x + c*y, z)`.
*/
export interface FixedRotation {
cos: number;
sin: number;
}
export function fixedRotationAt(epochMs: number, result: FixedRotation): FixedRotation {
const gha = greenwichHourAngle(epochMs);
result.cos = Math.cos(gha);
result.sin = Math.sin(gha);
return result;
}