Doppler Shift
Calculating Doppler shift for satellite communications, tracking frequency changes during a pass, and applying Doppler corrections. Use this when predicting the received frequency of a satellite downlink or planning transceiver tuning for a ground station.
import {
Degrees,
dopplerFactor,
GroundStation,
Kilometers,
Satellite,
TleLine1,
TleLine2,
} from 'ootk';Run it
npm run build
npx tsx ./examples/doppler.tsSetup
A GroundStation acts as the observer and a Satellite (built from a TLE) as the transmitter. The carrier frequency is plain Hz; there is no branded frequency type.
// Create the ground station (the observer) and the ISS (the transmitter).
const groundStation = new GroundStation({
lat: 41.754785 as Degrees,
lon: -70.539151 as Degrees,
alt: 0.060966 as Kilometers,
name: 'Cape Cod Ground Station',
});
const iss = new Satellite({
tle1: '1 25544U 98067A 24028.54545847 .00031576 00000-0 57240-3 0 9991' as TleLine1,
tle2: '2 25544 51.6418 292.2590 0002595 167.5319 252.0460 15.49326324436741' as TleLine2,
});
const transmitFreq = 437.8e6; // 437.8 MHz (UHF amateur radio frequency)
const date = new Date('2024-01-28T12:00:00.000Z');
console.log(`Ground Station: ${groundStation.name}`);
console.log(`Transmit Frequency: ${(transmitFreq / 1e6).toFixed(1)} MHz`);
console.log(`Time: ${date.toISOString()}\n`);Basic Doppler
Satellite.dopplerFactor(observer, date) returns the multiplicative factor (1 minus range-rate over c), and applyDoppler(freq, observer, date) multiplies a carrier by it. Both return null if SGP4 propagation fails at the requested time, so guard the results. A factor below 1 means the satellite is receding (negative shift).
console.log('=== Example 1: Basic Doppler Shift Calculation ===\n');
// Satellite.dopplerFactor() and applyDoppler() return null if propagation
// fails, so guard the results before using them.
const doppler = iss.dopplerFactor(groundStation, date);
const receivedFreq = iss.applyDoppler(transmitFreq, groundStation, date);
if (doppler === null || receivedFreq === null) {
throw new Error('Failed to propagate satellite for Doppler calculation');
}
const freqShift = receivedFreq - transmitFreq;
console.log('Doppler Calculations:');
console.log(` Doppler Factor: ${doppler.toFixed(8)}`);
console.log(` Transmit Frequency: ${(transmitFreq / 1e6).toFixed(4)} MHz`);
console.log(` Received Frequency: ${(receivedFreq / 1e6).toFixed(4)} MHz`);
console.log(` Frequency Shift: ${(freqShift / 1e3).toFixed(2)} kHz`);
// Get look angles for context
const rae = groundStation.rae(iss, date);
if (rae) {
console.log('\nSatellite Position:');
console.log(` Azimuth: ${rae.az.toFixed(1)}°`);
console.log(` Elevation: ${rae.el.toFixed(1)}°`);
console.log(` Range: ${rae.rng.toFixed(1)} km`);
}Doppler during a pass
Sampling the factor over time shows the classic S-curve: the shift sweeps from positive (approaching) through zero (closest approach) to negative (receding). Here the satellite is below the horizon, but the geometry-driven trend is the same.
console.log('\n=== Example 2: Doppler Shift During a Pass ===\n');
// Simulate a pass over 10 minutes
const passStart = new Date('2024-01-28T12:00:00.000Z');
const interval = 60; // seconds
console.log('Time El Range Doppler Freq Shift');
console.log('──────── ─── ──────── ────────── ────────────');
for (let i = 0; i <= 10; i++) {
const time = new Date(passStart.getTime() + i * interval * 1000);
const timeStr = time.toISOString().substring(11, 19);
const passRae = groundStation.rae(iss, time);
const passDoppler = iss.dopplerFactor(groundStation, time);
const passReceivedFreq = iss.applyDoppler(transmitFreq, groundStation, time);
if (!passRae || passDoppler === null || passReceivedFreq === null) {
continue;
}
const passFreqShift = passReceivedFreq - transmitFreq;
const elStr = passRae.el.toFixed(1).padStart(5);
const rngStr = passRae.rng.toFixed(1).padStart(8);
const dopplerStr = passDoppler.toFixed(8);
const shiftStr = (passFreqShift / 1e3).toFixed(2).padStart(9);
console.log(`${timeStr} ${elStr}° ${rngStr} km ${dopplerStr} ${shiftStr} kHz`);
}Frequency bands
The Doppler shift in Hz scales linearly with the carrier frequency, so an X-band link sees roughly 19x the absolute shift of a VHF link for the same geometry. The theoretical worst case uses the full orbital velocity as the radial component.
console.log('\n=== Example 3: Doppler Shift Across Different Bands ===\n');
// Doppler shift scales linearly with carrier frequency, so higher bands
// see proportionally larger absolute shifts.
const frequencies = [
{ band: 'VHF', freq: 145.8e6, name: '145.8 MHz' },
{ band: 'UHF', freq: 437.8e6, name: '437.8 MHz' },
{ band: 'L-band', freq: 1575.42e6, name: '1575.42 MHz (GPS L1)' },
{ band: 'S-band', freq: 2200e6, name: '2.2 GHz' },
{ band: 'X-band', freq: 8400e6, name: '8.4 GHz' },
{ band: 'Ku-band', freq: 12e9, name: '12 GHz' },
];
const bandCheckTime = new Date('2024-01-28T12:05:00.000Z');
console.log(`Time: ${bandCheckTime.toISOString()}\n`);
console.log('Band Frequency Received Freq Shift');
console.log('──────── ────────────── ────────────── ─────────');
frequencies.forEach((f) => {
const bandReceivedFreq = iss.applyDoppler(f.freq, groundStation, bandCheckTime);
if (bandReceivedFreq === null) {
return;
}
const bandFreqShift = bandReceivedFreq - f.freq;
const bandStr = f.band.padEnd(8);
const freqStr = f.name.padEnd(14);
const recvStr = formatFrequency(bandReceivedFreq).padEnd(14);
const shiftStr = formatFrequency(Math.abs(bandFreqShift)).padStart(9);
console.log(`${bandStr} ${freqStr} ${recvStr} ${bandFreqShift >= 0 ? '+' : '-'}${shiftStr}`);
});
// Theoretical maximum: worst case is the full orbital velocity along the
// line of sight (~7.66 km/s for the ISS).
const orbitalVelocity = 7.66; // km/s
const speedOfLight = 299792.458; // km/s
const maxDopplerFactor = orbitalVelocity / speedOfLight;
console.log(`\nISS Orbital Velocity: ~${orbitalVelocity} km/s`);
console.log(`Max Doppler Factor: ±${(maxDopplerFactor * 100).toFixed(6)}%`);
console.log('\nTheoretical Maximum Frequency Shifts:');
frequencies.slice(0, 4).forEach((f) => {
const maxShift = f.freq * maxDopplerFactor;
console.log(` ${f.name}: ±${formatFrequency(maxShift)}`);
});Doppler rate
Differencing the received frequency over a short interval gives the Doppler rate (Hz/s), which drives how fast a receiver must retune during a pass.
console.log('\n=== Example 4: Doppler Rate of Change ===\n');
const rateStart = new Date('2024-01-28T12:00:00.000Z');
const deltaTime = 10; // seconds
console.log('Measuring Doppler rate of change over time:\n');
for (let i = 0; i < 5; i++) {
const t1 = new Date(rateStart.getTime() + i * 60 * 1000);
const t2 = new Date(t1.getTime() + deltaTime * 1000);
const freq1 = iss.applyDoppler(transmitFreq, groundStation, t1);
const freq2 = iss.applyDoppler(transmitFreq, groundStation, t2);
if (freq1 === null || freq2 === null) {
continue;
}
const freqChange = freq2 - freq1;
const rateOfChange = freqChange / deltaTime; // Hz per second
const timeStr = t1.toISOString().substring(11, 19);
console.log(`At ${timeStr}:`);
console.log(` Frequency: ${(freq1 / 1e6).toFixed(4)} MHz`);
console.log(` Rate of change: ${rateOfChange.toFixed(2)} Hz/s`);
console.log('');
}Manual dopplerFactor
The standalone dopplerFactor(location, position, velocity) utility is what the Satellite method uses internally: observer ECI position (from GroundObject.eci(date), which returns a bare {x, y, z} vector), plus the satellite's ECI position and velocity from Satellite.eci(date). The results match the method exactly.
console.log('=== Example 5: Using dopplerFactor Utility Function ===\n');
// The dopplerFactor(location, position, velocity) utility is what the
// Satellite method uses internally: observer ECI position, satellite ECI
// position, and satellite ECI velocity.
const observerEci = groundStation.eci(date);
const satelliteState = iss.eci(date);
if (!satelliteState) {
throw new Error('Failed to propagate satellite state');
}
console.log('Observer ECI Position (km):');
console.log(` [${observerEci.x.toFixed(2)}, ${observerEci.y.toFixed(2)}, ${observerEci.z.toFixed(2)}]`);
console.log('\nSatellite ECI Position (km):');
console.log(
` [${satelliteState.position.x.toFixed(2)}, ${satelliteState.position.y.toFixed(2)}, ` +
`${satelliteState.position.z.toFixed(2)}]`,
);
console.log('\nSatellite ECI Velocity (km/s):');
console.log(
` [${satelliteState.velocity.x.toFixed(4)}, ${satelliteState.velocity.y.toFixed(4)}, ` +
`${satelliteState.velocity.z.toFixed(4)}]`,
);
const manualDoppler = dopplerFactor(observerEci, satelliteState.position, satelliteState.velocity);
console.log(`\nCalculated Doppler Factor: ${manualDoppler.toFixed(8)}`);
console.log(`Satellite method result: ${doppler.toFixed(8)}`);Helpers
// Helper function to format frequencies
function formatFrequency(freq: number): string {
if (freq >= 1e9) {
return `${(freq / 1e9).toFixed(4)} GHz`;
} else if (freq >= 1e6) {
return `${(freq / 1e6).toFixed(4)} MHz`;
} else if (freq >= 1e3) {
return `${(freq / 1e3).toFixed(2)} kHz`;
}
return `${freq.toFixed(2)} Hz`;
}Output
Ground Station: Cape Cod Ground Station
Transmit Frequency: 437.8 MHz
Time: 2024-01-28T12:00:00.000Z
=== Example 1: Basic Doppler Shift Calculation ===
Doppler Calculations:
Doppler Factor: 0.99999592
Transmit Frequency: 437.8000 MHz
Received Frequency: 437.7982 MHz
Frequency Shift: -1.79 kHz
Satellite Position:
Azimuth: 309.5°
Elevation: -54.7°
Range: 10918.1 km
=== Example 2: Doppler Shift During a Pass ===
Time El Range Doppler Freq Shift
──────── ─── ──────── ────────── ────────────
12:00:00 -54.7° 10918.1 km 0.99999592 -1.79 kHz
12:01:00 -55.2° 10987.5 km 0.99999628 -1.63 kHz
12:02:00 -55.8° 11050.3 km 0.99999666 -1.46 kHz
12:03:00 -56.2° 11106.1 km 0.99999705 -1.29 kHz
12:04:00 -56.7° 11154.8 km 0.99999745 -1.12 kHz
12:05:00 -57.0° 11196.1 km 0.99999787 -0.93 kHz
12:06:00 -57.3° 11229.8 km 0.99999829 -0.75 kHz
12:07:00 -57.6° 11255.8 km 0.99999873 -0.56 kHz
12:08:00 -57.7° 11273.9 km 0.99999917 -0.36 kHz
12:09:00 -57.8° 11284.0 km 0.99999962 -0.17 kHz
12:10:00 -57.9° 11285.9 km 1.00000008 0.03 kHz
=== Example 3: Doppler Shift Across Different Bands ===
Time: 2024-01-28T12:05:00.000Z
Band Frequency Received Freq Shift
──────── ────────────── ────────────── ─────────
VHF 145.8 MHz 145.7997 MHz -311.06 Hz
UHF 437.8 MHz 437.7991 MHz -934.04 Hz
L-band 1575.42 MHz (GPS L1) 1.5754 GHz - 3.36 kHz
S-band 2.2 GHz 2.2000 GHz - 4.69 kHz
X-band 8.4 GHz 8.4000 GHz -17.92 kHz
Ku-band 12 GHz 12.0000 GHz -25.60 kHz
ISS Orbital Velocity: ~7.66 km/s
Max Doppler Factor: ±0.002555%
Theoretical Maximum Frequency Shifts:
145.8 MHz: ±3.73 kHz
437.8 MHz: ±11.19 kHz
1575.42 MHz (GPS L1): ±40.25 kHz
2.2 GHz: ±56.21 kHz
... (truncated)
=== Example 5: Using dopplerFactor Utility Function ===
Observer ECI Position (km):
[-2608.11, -3973.30, 4242.78]
Satellite ECI Position (km):
[-1574.82, 6481.63, 1292.52]
Satellite ECI Velocity (km/s):
[-4.9744, -0.0382, -5.8304]
Calculated Doppler Factor: 0.99999592
Satellite method result: 0.99999592