orbitforge waveform doppler-apply
Synopsis
orbitforge waveform doppler-apply \
--iq <PATH> --sample-rate-hz <HZ> \
--constellation <PATH> --satellite <ID> --site <SPEC> \
--output <PATH> [OPTIONS]Description
Takes a clean IQ capture and imposes the Doppler shift and propagation delay that a real satellite pass would produce, by simulating the pass in process and sampling its geometry.
This is what turns a transmit-side reference from
waveform generate into something worth
testing a receiver against. A demodulator that works on a static signal and fails
on a real pass has failed at carrier tracking, and this is how you find that out
before flight.
Options
| Parameter | Type | Unit | Default | Required | Description |
|---|---|---|---|---|---|
--iq | path | n/a | — | Yes | Input IQ capture, cf32 format. |
--sample-rate-hz | float | Hz | — | Yes | Capture sample rate. Used to convert the time-varying shift into a per-sample rotation. |
--constellation | path | n/a | — | Yes | Constellation JSON. The pass is simulated in process. |
--satellite | string | n/a | — | Yes | Satellite identifier within the constellation. |
--site | string | n/a | — | Yes | Ground site as `name,lat_deg,lon_deg,alt_km`. |
--output | path | n/a | — | Yes | Output IQ path. |
--carrier-hz | float | Hz | 2200000000 | No | Carrier frequency the Doppler is scaled to. Shift is proportional to carrier, so this matters. |
--start | string | RFC 3339 UTC | 2026-01-01T00:00:00Z | No | Scenario start epoch. |
--duration-hours | float | h | 6 | No | Span searched for contacts. |
--step-seconds | float | s | 10 | No | Simulation step, setting profile sampling fidelity. |
--min-elevation-deg | float | deg | 5 | No | Minimum elevation defining a contact. |
--contact | integer | index | — | No | Contact index in time order. Defaults to the highest-elevation contact. |
--offset-s | float | s | 0 | No | Seconds into the contact at which the capture starts. |
--doppler-only | flag | n/a | — | No | Apply Doppler without the propagation delay. |
Worked example
orbitforge waveform doppler-apply \
--iq iq.bin --sample-rate-hz 32000 \
--constellation demo.json \
--satellite demo-s0-p00-sat00 \
--site "Madrid,40.43,-3.7,0.6" \
--output iq_dop.binContacts for demo-s0-p00-sat00 over Madrid:
[0] t = 1360..1850 s, peak elevation 16.5 deg, Doppler +37.3..-37.3 kHz
[1] t = 7340..7930 s, peak elevation 41.9 deg, Doppler +46.8..-46.9 kHz
[2] t = 13320..13880 s, peak elevation 31.4 deg, Doppler +45.6..-45.3 kHz
Applied Doppler + delay to 65680 samples (Doppler +46.8 kHz at capture start) -> iq_dop.binReading the contact list
Three passes were found, and contact 1 was selected automatically because it has the highest peak elevation at 41.9 degrees.
| Contact | Duration | Peak elevation | Doppler range |
|---|---|---|---|
| 0 | 490 s | 16.5 deg | +37.3 to -37.3 kHz |
| 1 | 590 s | 41.9 deg | +46.8 to -46.9 kHz |
| 2 | 560 s | 31.4 deg | +45.6 to -45.3 kHz |
The sign reversal is the whole point
Every contact starts positive and ends negative. The satellite approaches, so the received frequency is high; it recedes, so the frequency is low; and it passes through zero at closest approach.
where is range rate. At 2.2 GHz a low Earth orbit pass sweeps roughly 90 kHz peak to peak, and the steepest part of that sweep happens exactly at closest approach, when the signal is strongest and a receiver is most likely to be tracking.
A higher pass has a larger Doppler swing, not a smaller one. Contact 1 at 41.9 degrees sweeps +46.8 to -46.9 kHz; contact 0 at 16.5 degrees only sweeps +37.3 to -37.3 kHz.
A high pass brings the satellite closer and more directly overhead, so the line-of-sight velocity changes faster and through a wider range. The best pass for link margin is therefore the hardest pass for carrier tracking, which is a trap worth knowing about when you choose a test case.
Delay as well as Doppler
By default both Doppler and propagation delay are applied. Delay changes with range over the pass, which stretches and compresses the sample timeline, so a receiver must track symbol timing as well as carrier frequency.
--doppler-only omits the delay, which is useful for isolating a carrier-tracking
problem from a timing-recovery problem. Both together is the realistic case.
Choosing the contact and the offset
| Flag | Use |
|---|---|
--contact | Select a specific pass by time order rather than taking the highest-elevation one |
--offset-s | Start the capture partway into the pass |
--offset-s matters because the hardest moment is not the start. Setting it to
place the capture across closest approach exercises the receiver where Doppler
rate is highest, which is where carrier tracking usually breaks.
Profile fidelity
--step-seconds defaults to 10 seconds, and the profile is interpolated between
samples. That is adequate for a pass lasting several minutes, because range rate
varies smoothly. Reduce it if you need the Doppler rate through closest approach
represented precisely, since that is where the curvature is greatest.
A complete receiver test
# 1. Generate a clean reference.
orbitforge waveform generate --standard dvbs2 --modcod qpsk-1/2 --iq iq.bin
# 2. Impose a real pass.
orbitforge waveform doppler-apply \
--iq iq.bin --sample-rate-hz 32000 \
--constellation demo.json --satellite demo-s0-p00-sat00 \
--site "Madrid,40.43,-3.7,0.6" --output iq_dop.bin
# 3. Feed iq_dop.bin to the receiver under test.Keeping the steps separate means a failure is attributable: if the receiver
handles iq.bin and fails on iq_dop.bin, the problem is Doppler or timing
rather than demodulation.
See also
waveform generateto produce the input.- Units and conventions for the Doppler sign convention.
main (pre-release)