Doppler and orbital elements
What you will accomplish
A Doppler profile you can hand to a receiver designer, and an osculating-element series you have checked against theory rather than assumed.
Both come from the API rather than the CLI.
Prerequisites
- The API running, and a constellation registered with it.
- Access windows and pointing, whose range rate is the same quantity seen from the other side.
Doppler over a pass
curl -s -X POST localhost:8080/analysis/doppler \
-H 'content-type: application/json' \
-d '{
"constellation_id": "ph1",
"duration_hours": 2,
"step_seconds": 30,
"satellite_id": "ph1-s0-p00-sat00",
"frequency_hz": 12e9,
"ground_station": {
"id": "nyc", "name": "New York",
"latitude_deg": 40.7, "longitude_deg": -74.0, "altitude_km": 0.01
}
}'{
"satellite_id": "ph1-s0-p00-sat00",
"ground_station_id": "nyc",
"frequency_hz": 12000000000.0,
"start": "2026-01-01T00:00:00Z",
"samples": [
{
"time_s": 0.0,
"range_km": 5347.98,
"range_rate_km_s": -6.3357,
"doppler_hz": 253601.72
}
]
}The sign convention is worth fixing in your head immediately: range rate negative means closing, and closing gives positive Doppler. A satellite approaching the station arrives high in frequency.
Every sample is returned, including the ones through the Earth
The response has no elevation field and applies no visibility gate. Over this 2-hour window at 30 seconds, all 241 samples are returned, and 191 of them are for a satellite below the horizon.
Range runs from 811 km to 13297 km. For a 550 km circular orbit the slant range at zero elevation is about 2705 km, so anything beyond that is geometry computed straight through the planet.
This matters less for magnitudes than you might expect, and much more for interpretation.
| Population | Doppler extremes | Span |
|---|---|---|
| All 241 samples | -263.6 to +263.4 kHz | 526.9 kHz |
| Above the horizon (50) | -261.9 to +261.5 kHz | 523.4 kHz |
| Above 10 degrees (30) | -244.4 to +242.9 kHz | 487.3 kHz |
Sizing a receiver’s search range from the raw series overshoots by 8 percent against a 10-degree mask. That is a small error and would not by itself break a design.
The interpretation error is the large one: 79 percent of that series is not an observation. Over these 2 hours the satellite rises above 10 degrees exactly twice, for 15 minutes in total. Plot the series unfiltered and you get a smooth continuous Doppler curve for a satellite that was in view for one eighth of the window. Anything time-dependent, such as scheduling an acquisition or estimating how long the receiver must hold lock, is wrong by far more than 8 percent.
Filter by elevation from a
platform-access run over the same window
and step before using the series.
What the receiver actually needs
Two numbers, from the same geometry:
- Frequency uncertainty, roughly plus or minus 245 kHz at 12 GHz above a 10-degree mask. This sets the acquisition search range.
- Doppler rate, which sets how fast the loop must track. From a 1-second access run over the highest pass, the peak is 3464 Hz/s at 12 GHz.
Doppler scales linearly with carrier frequency, so the same orbit at 2 GHz gives about one sixth of both figures. Scale rather than re-running, but state the frequency you scaled from.
A 30-second step cannot resolve a Doppler rate. Range rate swings through its full excursion in a couple of minutes near closest approach, so use 1-second steps for any rate figure, exactly as for antenna slew rates.
Osculating elements
curl -s -X POST localhost:8080/analysis/elements \
-H 'content-type: application/json' \
-d '{
"constellation_id": "ph1",
"duration_hours": 2,
"step_seconds": 30,
"propagation_model": "j2",
"sample_index": 240
}'{
"satellite_id": "ph1-s0-p00-sat00",
"semi_major_axis_km": 6928.137,
"eccentricity": 0.0,
"inclination_deg": 53.0,
"raan_deg": 359.6259,
"argument_of_perigee_deg": 0.0,
"true_anomaly_deg": 91.9261,
"perigee_altitude_km": 550.0,
"apogee_altitude_km": 550.0,
"period_minutes": 95.6499
}sample_index selects one sample from the propagated series, so the window and
step determine which instant you get. Index 240 at 30-second steps is 7200
seconds in.
The API spells the model two_body with an underscore. The CLI spells it
two-body with a hyphen. Passing the CLI spelling to the API returns a clear
error naming the valid values:
unknown propagation_model `two-body` (expected `two_body`, `j2`, `sgp4`,
`numerical`, or `ephemeris`)Verify the propagator instead of trusting it
RAAN moved from 0 to 359.6259 degrees in 7200 seconds, which is -4.489 degrees per day. That is a claim the tool is making, and it has a closed form to check against.
For a near-circular orbit the J2 secular nodal regression is
With , km, km, and degrees:
| Source | Nodal regression |
|---|---|
| Closed form | -4.489 deg/day |
Measured from the j2 model | -4.489 deg/day |
They agree to four decimal places. Running two_body over the identical window
leaves RAAN at exactly 0.0000 degrees, which is the control: without J2 there is
no regression to find.
This check takes two requests and closes off a whole category of doubt. Do it once for any model you are about to quote numbers from.
The same comparison also exposes a smaller effect. At the same sample, true
anomaly is 91.6472 degrees under two_body and 91.9261 degrees under j2. J2
perturbs the along-track rate as well as the node, and over weeks that difference
is what moves a satellite out of its slot.
Why nodal regression is the number to check
It sets whether a Sun-synchronous orbit holds its local time, how quickly planes drift apart, and how a constellation’s geometry ages. At -4.489 deg/day, a plane walks 31 degrees in a week. A design verified over 6 hours has seen none of that, which is the argument made in Synthesize from requirements arriving from a different direction.
Checklist
- Did you gate the Doppler series by elevation before using it?
- Are you quoting a Doppler rate from a step fine enough to resolve it?
- Is the carrier frequency stated alongside every Doppler figure?
- Did you check the model’s secular rates against the closed form?
- Are you using the API spelling of the model name, not the CLI one?
Next steps
- Analysis API reference for the full request and response schemas.
- Closing a link for the power side of the same pass.
main (pre-release)