Summary
Allow a single SignalGroup to contain signals with different code chipping rates (different get_code_frequency). Today the SignalGroup constructor rejects this: _validate_signal_group (src/sat_state.jl) requires every signal in a group to share one chip rate (added in #129).
Motivation
There is no fundamental reason mixed chip rates can't share a satellite: all signals from one SV share an arrival time, so each signal's code phase and code Doppler are recoverable from a shared time reference by scaling with that signal's chip rate. The restriction is an implementation simplification, not a physical limit. Relaxing it would let a group co-track signals that share a band but not a chip rate.
Why it's currently blocked (load-bearing assumptions)
The shared per-satellite code state is stored as a single scalar in the driver signal's (signals[1]) chip units and consumed by every signal without rate scaling:
- Phase advance —
update (src/downconvert_and_correlate.jl) advances the shared code_phase at get_code_frequency(signals[1]) and wraps it with current_code_wrap.
- Per-signal replica generation (
src/downconvert_and_correlate_cpu.jl):
per_signal_phase = mod(code_phase, <signal's own wrap>) — uses the driver-chip phase directly as each signal's chip phase.
code_frequency = code_doppler + get_code_frequency(s) — adds the shared code_doppler straight onto each signal's base frequency, but code Doppler scales with chip rate, so the driver's value is wrong for a different-rate signal.
- Wrap computation —
current_code_wrap / max_code_length (src/sat_state.jl) take a max over each signal's length in its own chips, which is only coherent when all signals share the driver's chip units.
Consequently, simply removing the #129 guard would make any non-driver signal with a different rate build its replica at the wrong phase and the wrong code Doppler → silent mistracking (the exact failure #129 prevents).
What a correct implementation requires
- Convert the shared
code_phase to each signal's chips by rate_i / rate_driver at replica-generation sites (and _snap_code_phase_from_synced_signal).
- Scale the shared
code_doppler by rate_i / rate_driver per signal when forming code_frequency.
- Make
current_code_wrap / max_code_length express every signal's wrap in the driver's chip units (or move the shared phase to a rate-independent unit, e.g. time/seconds).
- Decide the reference convention (keep
signals[1] as the chip-rate reference, or store the shared phase as time).
- Add a synthetic mixed-chip-rate test signal to validate, since no real same-band GNSS pair has different chip rates that are co-tracked on a shared code phase (L1 C/A + L1C are both 1.023 Mcps; L5 I/Q both 10.23 Mcps; Galileo E1B/E1C both 1.023 Mcps).
Acceptance criteria
References
Found while reviewing acquisition routing in #146 (issue #134). Related: #113 (multi-signal/multi-band design), #129 (band + chip-rate invariant), #132 (API naming).
Summary
Allow a single
SignalGroupto contain signals with different code chipping rates (differentget_code_frequency). Today theSignalGroupconstructor rejects this:_validate_signal_group(src/sat_state.jl) requires every signal in a group to share one chip rate (added in #129).Motivation
There is no fundamental reason mixed chip rates can't share a satellite: all signals from one SV share an arrival time, so each signal's code phase and code Doppler are recoverable from a shared time reference by scaling with that signal's chip rate. The restriction is an implementation simplification, not a physical limit. Relaxing it would let a group co-track signals that share a band but not a chip rate.
Why it's currently blocked (load-bearing assumptions)
The shared per-satellite code state is stored as a single scalar in the driver signal's (
signals[1]) chip units and consumed by every signal without rate scaling:update(src/downconvert_and_correlate.jl) advances the sharedcode_phaseatget_code_frequency(signals[1])and wraps it withcurrent_code_wrap.src/downconvert_and_correlate_cpu.jl):per_signal_phase = mod(code_phase, <signal's own wrap>)— uses the driver-chip phase directly as each signal's chip phase.code_frequency = code_doppler + get_code_frequency(s)— adds the sharedcode_dopplerstraight onto each signal's base frequency, but code Doppler scales with chip rate, so the driver's value is wrong for a different-rate signal.current_code_wrap/max_code_length(src/sat_state.jl) take amaxover each signal's length in its own chips, which is only coherent when all signals share the driver's chip units.Consequently, simply removing the
#129guard would make any non-driver signal with a different rate build its replica at the wrong phase and the wrong code Doppler → silent mistracking (the exact failure #129 prevents).What a correct implementation requires
code_phaseto each signal's chips byrate_i / rate_driverat replica-generation sites (and_snap_code_phase_from_synced_signal).code_dopplerbyrate_i / rate_driverper signal when formingcode_frequency.current_code_wrap/max_code_lengthexpress every signal's wrap in the driver's chip units (or move the shared phase to a rate-independent unit, e.g. time/seconds).signals[1]as the chip-rate reference, or store the shared phase as time).Acceptance criteria
SignalGroupaccepts mixed-chip-rate signals (relax/remove the#129chip-rate check) only after the per-signal rate scaling above is in place._longest_code_signal(inTrackingAcquisitionExt) switches from comparingget_code_length(chips) to code period (get_code_length / get_code_frequency); the equal-chip-rate dependency is noted in a comment there today.References
Found while reviewing acquisition routing in #146 (issue #134). Related: #113 (multi-signal/multi-band design), #129 (band + chip-rate invariant), #132 (API naming).