Add optional full-band Galileo E1 CBOC support
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
|
||||
This repository is a modular Python reference model for a Galileo receiver
|
||||
baseband and a starting point for bit-true/RTL verification. It currently
|
||||
implements E1B data and E1C pilot processing with a BOC(1,1) local replica,
|
||||
implements E1B data and E1C pilot processing with selectable BOC(1,1) or
|
||||
full-band CBOC(6,1,1/11) local replicas,
|
||||
streaming real-IF or complex-IQ front ends, FFT PCPS acquisition, independent
|
||||
tracking channels, fixed-point boundary models, and hardware sizing models.
|
||||
The floating reference uses `numpy.float64`/`numpy.complex128` throughout;
|
||||
@@ -23,7 +24,9 @@ complex IQ; both acquisition and tracking pass on its IQ8 output. See the
|
||||
## Status and limitations
|
||||
|
||||
- E1 profile: 1.023 Mcps, 4092 chips, 4 ms, 1575.42 MHz.
|
||||
- BOC(1,1) is implemented; CBOC and unambiguous BOC are API extension points.
|
||||
- BOC(1,1) remains the default regression mode. Full-band CBOC is enabled
|
||||
explicitly per acquisition/tracking configuration. Unambiguous BOC remains
|
||||
an API extension point.
|
||||
- E1B bit-transition hypotheses and E1C secondary wipeoff/search are available
|
||||
for acquisition. E1C tracking jointly searches the 25-symbol phase and
|
||||
residual carrier frequency before continuous secondary-code wipeoff.
|
||||
@@ -74,6 +77,7 @@ python -m galileo_rx.cli validate-signalsim --signalsim-path signalsim/SignalSim
|
||||
python -m galileo_rx.cli collect-navigation --config configs/navigation_collection.yaml --output-dir reports/navigation_run
|
||||
python -m galileo_rx.cli collect-navigation --config configs/navigation_collection.yaml --output-dir reports/navigation_e1b --tracking-mode e1b_only
|
||||
python tools/compare_e1_tracking_modes.py --config configs/navigation_collection.yaml --output-dir reports/e1_tracking_mode_comparison
|
||||
python tools/run_cboc_validation.py --output-dir reports --duration-s 0.30
|
||||
python -m galileo_rx.cli prompts-to-symbols --input verification/navigation/signalsim_navigation_multisat_e1c_pilot_aided_e1b/prn_01/prompt_epochs.npz --prn 1 --output-dir reports/navigation_replay/prn_01
|
||||
python -m galileo_rx.cli decode-inav --input verification/navigation/signalsim_navigation_multisat_e1c_pilot_aided_e1b/prn_01/prompt_epochs.npz --prn 1 --output-dir reports/navigation_replay/prn_01
|
||||
python -m galileo_rx.cli inspect-navigation --input verification/navigation/signalsim_navigation_multisat_e1c_pilot_aided_e1b/prn_01
|
||||
@@ -136,10 +140,9 @@ the official 30×8 deinterleaver and soft Viterbi decoder, validates CRC-24Q, an
|
||||
parses typed word 1–6 data. Ephemerides are committed atomically only when words
|
||||
1–4 have matching IODnav; the previous valid set survives incomplete updates.
|
||||
|
||||
The measured 32.2 s SignalSim regression decoded 70/70 CRC-valid pages. Four of
|
||||
five tracked satellites produced complete ephemerides, clocks, healthy status,
|
||||
and GST; PRN 19 correctly remained incomplete because word 1 was outside the
|
||||
observation. Post-decode comparison against SignalSim's RINEX source passed at
|
||||
The post-CBOC 32.2 s raw-IQ SignalSim regression decoded 75/75 CRC-valid pages.
|
||||
All five tracked satellites produced complete ephemerides, clocks, healthy
|
||||
status, and GST. Post-decode comparison against SignalSim's RINEX source passed at
|
||||
the broadcast quantization resolution. No simulator truth is accepted by the
|
||||
normal decoder API.
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ rf:
|
||||
sample_bit_width: 8
|
||||
sample_endianness: little
|
||||
acquisition:
|
||||
modulation: BOC11
|
||||
doppler_min_hz: -5000.0
|
||||
doppler_max_hz: 5000.0
|
||||
doppler_step_hz: 250.0
|
||||
@@ -18,6 +19,7 @@ acquisition:
|
||||
peak_exclusion_chips: 1.0
|
||||
zero_padding_mode: none
|
||||
tracking:
|
||||
modulation: BOC11
|
||||
integration_ms: 4.0
|
||||
early_late_spacing_chips: 0.25
|
||||
very_early_late_spacing_chips: 0.75
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
signal_profile: GALILEO_E1
|
||||
component: E1C
|
||||
prns: [1]
|
||||
rf:
|
||||
sample_format: complex128
|
||||
input_is_real_if: false
|
||||
sampling_frequency_hz: 32736000.0
|
||||
intermediate_frequency_hz: 2250000.0
|
||||
sample_bit_width: 8
|
||||
sample_endianness: little
|
||||
acquisition:
|
||||
modulation: CBOC
|
||||
doppler_min_hz: -10000.0
|
||||
doppler_max_hz: 10000.0
|
||||
doppler_step_hz: 250.0
|
||||
coherent_integration_ms: 4.0
|
||||
noncoherent_accumulations: 2
|
||||
detection_threshold: 2.5
|
||||
peak_exclusion_chips: 1.0
|
||||
zero_padding_mode: none
|
||||
tracking:
|
||||
modulation: CBOC
|
||||
integration_ms: 4.0
|
||||
early_late_spacing_chips: 0.25
|
||||
very_early_late_spacing_chips: 0.75
|
||||
dll_noise_bandwidth_hz: 1.0
|
||||
pll_noise_bandwidth_hz: 15.0
|
||||
fll_noise_bandwidth_hz: 20.0
|
||||
pll_order: 3
|
||||
dll_order: 2
|
||||
use_fll_assist: true
|
||||
pull_in_duration_ms: 20.0
|
||||
secondary_code_mode: wipeoff
|
||||
@@ -9,6 +9,7 @@ rf:
|
||||
sample_bit_width: 8
|
||||
sample_endianness: little
|
||||
acquisition:
|
||||
modulation: BOC11
|
||||
doppler_min_hz: -10000.0
|
||||
doppler_max_hz: 10000.0
|
||||
doppler_step_hz: 250.0
|
||||
@@ -19,6 +20,7 @@ acquisition:
|
||||
fft_length: null
|
||||
zero_padding_mode: next_power_of_two
|
||||
tracking:
|
||||
modulation: BOC11
|
||||
integration_ms: 4.0
|
||||
early_late_spacing_chips: 0.25
|
||||
very_early_late_spacing_chips: 0.75
|
||||
|
||||
@@ -9,6 +9,7 @@ rf:
|
||||
sample_bit_width: 8
|
||||
sample_endianness: little
|
||||
tracking:
|
||||
modulation: BOC11
|
||||
integration_ms: 4.0
|
||||
early_late_spacing_chips: 0.25
|
||||
very_early_late_spacing_chips: 0.75
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# Galileo E1 CBOC support
|
||||
|
||||
## Definition and convention
|
||||
|
||||
The implementation follows *Galileo Open Service Signal-in-Space Interface
|
||||
Control Document*, Issue 2.2 (November 2025), section 2.3.3, Table 10 and
|
||||
equation (12). With sine-phased square subcarriers
|
||||
|
||||
```text
|
||||
sc_n(t) = sgn(sin(2 pi n R_c t)), R_c = 1.023 MHz
|
||||
alpha = sqrt(10/11)
|
||||
beta = sqrt(1/11)
|
||||
```
|
||||
|
||||
the complete transmitted multiplex and its component replicas are
|
||||
|
||||
```text
|
||||
s_E1(t) = (1/sqrt(2)) {
|
||||
e_E1B(t) [alpha sc_1(t) + beta sc_6(t)]
|
||||
- e_E1C(t) [alpha sc_1(t) - beta sc_6(t)]
|
||||
}
|
||||
|
||||
CBOC_E1B(t) = alpha sc_1(t) + beta sc_6(t)
|
||||
CBOC_E1C(t) = alpha sc_1(t) - beta sc_6(t)
|
||||
```
|
||||
|
||||
The complete transmitted E1 equation contains a common `1/sqrt(2)` power split
|
||||
and an additional outer minus sign on the E1C component. The outer sign is a
|
||||
constant carrier-phase ambiguity for an individual component receiver; it is
|
||||
not folded into the internal CBOC(-) weighting above. E1B and E1C occupy the
|
||||
same RF carrier and are not modeled as independent RF I/Q branches.
|
||||
|
||||
At an exact subcarrier transition, the discrete convention selects the new
|
||||
half-cycle (`+1` at integer phase, `-1` at half-integer phase). A `1e-12`-cycle
|
||||
boundary tolerance makes vector and arbitrarily chunked float64 phase
|
||||
accumulators deterministic.
|
||||
|
||||
## Architecture boundaries
|
||||
|
||||
- Acquisition PRN/subcarrier replica: `signals/galileo_e1.py`, called and FFT
|
||||
cached by `acquisition/fft_pcps.py`.
|
||||
- Acquisition carrier wipeoff: `fft_pcps.py`; it remains
|
||||
`input * exp(-j 2 pi (IF + Doppler) n/Fs)` and is independent of modulation.
|
||||
- Tracking replicas: `tracking/correlator.py`; one reusable bank creates
|
||||
VE/E/P/L/VL replicas for either modulation.
|
||||
- Tracking carrier wipeoff and coherent sums: `CorrelatorBank.correlate_with_phases`.
|
||||
- Physical parameters: `RfConfig` owns sampling rate and IF, `SignalProfile`
|
||||
owns carrier/code rates, and `AcquisitionConfig`/`TrackingConfig` own the
|
||||
explicit modulation choice.
|
||||
|
||||
The tracking correlator carries independent float64 state for carrier phase,
|
||||
code phase, BOC(1,1) phase, and BOC(6,1) phase. The subcarrier frequency words
|
||||
follow the code-NCO frequency. The E1C pilot-aided E1B class shares those phase
|
||||
vectors between its pilot and data correlator branches; only E1B Prompt I/Q is
|
||||
sent to I/NAV recovery.
|
||||
|
||||
## Full-band generator
|
||||
|
||||
`FullBandE1SignalGenerator` defaults to 65.472 Msps internal generation, a
|
||||
129-tap Kaiser-window FIR with 12 MHz one-sided bandwidth, and integer
|
||||
decimation to 32.736 Msps. Filter coefficients, bandwidth, tap count, and
|
||||
oversampling are configurable. Its complex FIR state and all carrier/code
|
||||
states persist between calls. The causal FIR group delay is included in
|
||||
`GenerationTruth`; simulator truth is used only after receiver processing.
|
||||
|
||||
This is intentionally distinct from SignalSim commit
|
||||
`80b262113e77c4482ca9f5cbfef9bcada402ed28`, whose Galileo E1 vector is a
|
||||
BOC(1,1) approximation and remains labeled only as BOC(1,1) validation.
|
||||
|
||||
## Measured 32.736 Msps matrix
|
||||
|
||||
The checked-in [JSON report](../reports/cboc_validation_report.json) and
|
||||
[Markdown report](../reports/cboc_validation_report.md) are produced by:
|
||||
|
||||
```bash
|
||||
python tools/run_cboc_validation.py --output-dir reports --duration-s 0.30
|
||||
```
|
||||
|
||||
All three cases acquired the correct 1250 Hz Doppler and 81.25-chip filtered
|
||||
delay, synchronized the E1C secondary code with correlation above 0.9997 and
|
||||
zero sign errors, and ended with carrier and code lock. No cycle slips,
|
||||
discontinuities, or side-peak indications occurred.
|
||||
|
||||
Using a BOC11 replica on the CBOC vector reduced acquisition peak power by
|
||||
0.317 dB relative to the matched CBOC replica. It produced no measurable delay
|
||||
bias at the 1/32-chip acquisition grid. Tail code biases were -0.00085 chips
|
||||
(compatibility) and -0.00087 chips (matched). The existing lock thresholds were
|
||||
not changed: all modes had the same 184 ms confirmed-lock interval after the
|
||||
configured 25-epoch smoother and three-epoch confirmation delay.
|
||||
|
||||
The matched CBOC DLL discriminator RMS was 0.0527 chips versus 0.0351 chips in
|
||||
compatibility mode for this short run. The NCO state and code bias remained
|
||||
bounded, so this is recorded as a correlator-shape/discriminator calibration
|
||||
observation, not silently corrected by threshold or loop retuning.
|
||||
|
||||
For the configured 55 dB-Hz source, the existing moment estimator reported
|
||||
47.88 dB-Hz for filtered BOC11 and approximately 42.31 dB-Hz for both CBOC
|
||||
receiver modes. Since matched and compatibility CBOC estimates agree while
|
||||
their lock/NCO states remain stable, this is retained as a front-end/CBOC C/N0
|
||||
calibration limitation rather than used to retune the loops or lock thresholds.
|
||||
The matched CBOC tail main-peak fraction remained above 0.915, its side-peak
|
||||
metric below 0.045, and it produced no side-peak flags.
|
||||
|
||||
## Limitations
|
||||
|
||||
- The focused CBOC matrix uses deterministic synthetic codes, not fabricated
|
||||
Galileo code tables, and E1C pilot only. E1B page count is therefore reported
|
||||
as not enabled. Official-code BOC11 E1B I/NAV/ephemeris coverage remains the
|
||||
independent SignalSim regression.
|
||||
- The generator models a configurable linear front-end FIR, AWGN, Doppler,
|
||||
code Doppler, clipping, and I/Q quantization. It does not yet model analog
|
||||
group-delay ripple, oscillator phase noise, multipath, or interference.
|
||||
- No unambiguous CBOC acquisition discriminator is claimed. Side-peak
|
||||
monitoring remains explicit.
|
||||
@@ -25,10 +25,17 @@ Default YAML files:
|
||||
- `e1_default.yaml`: requested 16.368 MHz nominal defaults.
|
||||
- `acquisition_example.yaml`: faster 4.092 MHz executable search.
|
||||
- `tracking_example.yaml`: 4.092 MHz tracking defaults.
|
||||
- `e1_cboc.yaml`: explicit full-band CBOC selection at 32.736 Msps and a
|
||||
2.25 MHz analytic low IF.
|
||||
- `navigation_collection.yaml`: SignalSim IQ-to-I/NAV regression. Its default
|
||||
`tracking.architecture` is `e1c_pilot_aided_e1b`; the CLI can select
|
||||
`e1b_only` for an A/B replay.
|
||||
|
||||
`AcquisitionConfig.modulation` and `TrackingConfig.modulation` independently
|
||||
select `BOC11` or `CBOC`. Both default to `BOC11`, so existing SignalSim and
|
||||
low-bandwidth configurations retain their measured behavior. A CBOC vector
|
||||
must set both fields to `CBOC` for a matched acquisition/tracking replica.
|
||||
|
||||
The BOC lock detector parameters are part of `TrackingConfig`. The defaults use
|
||||
25 noncoherent power epochs, three confirmations, and the configured
|
||||
`loss_of_lock_epochs` for sustained failure. Carrier and BOC central/outer
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"generator": "independent band-limited Python Galileo E1 generator",
|
||||
"truth_usage": "post-receiver error measurement only",
|
||||
"cases": [
|
||||
{
|
||||
"case": "generated_BOC11__receiver_BOC11",
|
||||
"transmit_modulation": "BOC11",
|
||||
"receiver_modulation": "BOC11",
|
||||
"sample_format": "complex128",
|
||||
"sampling_frequency_hz": 32736000.0,
|
||||
"internal_sampling_frequency_hz": 65472000.0,
|
||||
"intermediate_frequency_hz": 2250000.0,
|
||||
"frontend_one_sided_bandwidth_hz": 12000000.0,
|
||||
"frontend_fir_taps": 129,
|
||||
"filter_group_delay_samples": 32.0,
|
||||
"acquisition_detected": true,
|
||||
"acquisition_peak_metric": 111.59981066286072,
|
||||
"acquisition_peak_power": 34317234269.486805,
|
||||
"estimated_code_delay_chips": 81.25,
|
||||
"truth_code_delay_chips_including_filter": 81.25,
|
||||
"code_delay_error_chips": 0.0,
|
||||
"estimated_carrier_frequency_hz": 2251250.0,
|
||||
"estimated_doppler_hz": 1250.0,
|
||||
"doppler_error_hz": 0.0,
|
||||
"secondary_code_phase": 2,
|
||||
"secondary_code_correlation": 0.999801055475596,
|
||||
"secondary_code_sign_errors": 0,
|
||||
"tracking_epochs": 72,
|
||||
"tracking_duration_s": 0.28800000000000003,
|
||||
"longest_joint_lock_duration_s": 0.184,
|
||||
"final_carrier_lock": true,
|
||||
"final_code_lock": true,
|
||||
"pll_error_rms_rad": 0.029726592473925416,
|
||||
"dll_error_rms_chips": 0.02297667464497775,
|
||||
"code_tracking_bias_chips": 1.5854226921874216e-05,
|
||||
"code_tracking_error_rms_chips": 0.00025151975426697875,
|
||||
"median_cn0_estimate_db_hz": 47.880388780252005,
|
||||
"cycle_slip_count": 0,
|
||||
"sample_discontinuity_count": 0,
|
||||
"side_peak_suspected_epochs": 0,
|
||||
"tail_smoothed_code_lock_metric_median": 15.4703736731197,
|
||||
"tail_boc_main_peak_fraction_minimum": 0.8977679303792508,
|
||||
"tail_boc_side_peak_metric_maximum": 0.0575388903784898,
|
||||
"e1b_page_decode_count": null,
|
||||
"e1b_navigation_data_enabled": false,
|
||||
"processing_time_s": 4.839926584994828
|
||||
},
|
||||
{
|
||||
"case": "generated_CBOC__receiver_BOC11",
|
||||
"transmit_modulation": "CBOC",
|
||||
"receiver_modulation": "BOC11",
|
||||
"sample_format": "complex128",
|
||||
"sampling_frequency_hz": 32736000.0,
|
||||
"internal_sampling_frequency_hz": 65472000.0,
|
||||
"intermediate_frequency_hz": 2250000.0,
|
||||
"frontend_one_sided_bandwidth_hz": 12000000.0,
|
||||
"frontend_fir_taps": 129,
|
||||
"filter_group_delay_samples": 32.0,
|
||||
"acquisition_detected": true,
|
||||
"acquisition_peak_metric": 115.61214182481689,
|
||||
"acquisition_peak_power": 28044260424.17594,
|
||||
"estimated_code_delay_chips": 81.25,
|
||||
"truth_code_delay_chips_including_filter": 81.25,
|
||||
"code_delay_error_chips": 0.0,
|
||||
"estimated_carrier_frequency_hz": 2251250.0,
|
||||
"estimated_doppler_hz": 1250.0,
|
||||
"doppler_error_hz": 0.0,
|
||||
"secondary_code_phase": 2,
|
||||
"secondary_code_correlation": 0.9997971190771551,
|
||||
"secondary_code_sign_errors": 0,
|
||||
"tracking_epochs": 72,
|
||||
"tracking_duration_s": 0.28800000000000003,
|
||||
"longest_joint_lock_duration_s": 0.184,
|
||||
"final_carrier_lock": true,
|
||||
"final_code_lock": true,
|
||||
"pll_error_rms_rad": 0.030845816038584276,
|
||||
"dll_error_rms_chips": 0.03505125628223577,
|
||||
"code_tracking_bias_chips": -0.0008470738236610487,
|
||||
"code_tracking_error_rms_chips": 0.0009952893933792282,
|
||||
"median_cn0_estimate_db_hz": 42.31146354235335,
|
||||
"cycle_slip_count": 0,
|
||||
"sample_discontinuity_count": 0,
|
||||
"side_peak_suspected_epochs": 0,
|
||||
"tail_smoothed_code_lock_metric_median": 17.865460658014058,
|
||||
"tail_boc_main_peak_fraction_minimum": 0.9061270492837439,
|
||||
"tail_boc_side_peak_metric_maximum": 0.05289988968883203,
|
||||
"e1b_page_decode_count": null,
|
||||
"e1b_navigation_data_enabled": false,
|
||||
"processing_time_s": 5.107619269001589
|
||||
},
|
||||
{
|
||||
"case": "generated_CBOC__receiver_CBOC",
|
||||
"transmit_modulation": "CBOC",
|
||||
"receiver_modulation": "CBOC",
|
||||
"sample_format": "complex128",
|
||||
"sampling_frequency_hz": 32736000.0,
|
||||
"internal_sampling_frequency_hz": 65472000.0,
|
||||
"intermediate_frequency_hz": 2250000.0,
|
||||
"frontend_one_sided_bandwidth_hz": 12000000.0,
|
||||
"frontend_fir_taps": 129,
|
||||
"filter_group_delay_samples": 32.0,
|
||||
"acquisition_detected": true,
|
||||
"acquisition_peak_metric": 118.51359102364712,
|
||||
"acquisition_peak_power": 30169065694.99317,
|
||||
"estimated_code_delay_chips": 81.25,
|
||||
"truth_code_delay_chips_including_filter": 81.25,
|
||||
"code_delay_error_chips": 0.0,
|
||||
"estimated_carrier_frequency_hz": 2251250.0,
|
||||
"estimated_doppler_hz": 1250.0,
|
||||
"doppler_error_hz": 0.0,
|
||||
"secondary_code_phase": 2,
|
||||
"secondary_code_correlation": 0.9997767185931239,
|
||||
"secondary_code_sign_errors": 0,
|
||||
"tracking_epochs": 72,
|
||||
"tracking_duration_s": 0.28800000000000003,
|
||||
"longest_joint_lock_duration_s": 0.184,
|
||||
"final_carrier_lock": true,
|
||||
"final_code_lock": true,
|
||||
"pll_error_rms_rad": 0.0272969018394947,
|
||||
"dll_error_rms_chips": 0.052729413588935264,
|
||||
"code_tracking_bias_chips": -0.0008745638097479968,
|
||||
"code_tracking_error_rms_chips": 0.0011967645110863632,
|
||||
"median_cn0_estimate_db_hz": 42.30148722049758,
|
||||
"cycle_slip_count": 0,
|
||||
"sample_discontinuity_count": 0,
|
||||
"side_peak_suspected_epochs": 0,
|
||||
"tail_smoothed_code_lock_metric_median": 50.858434903966014,
|
||||
"tail_boc_main_peak_fraction_minimum": 0.9155160124924913,
|
||||
"tail_boc_side_peak_metric_maximum": 0.04623326118514734,
|
||||
"e1b_page_decode_count": null,
|
||||
"e1b_navigation_data_enabled": false,
|
||||
"processing_time_s": 5.774479466002958
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Galileo E1 full-band CBOC validation
|
||||
|
||||
Generated at 65.472 Msps, passed through a 129-tap Kaiser FIR, and decimated to 32.736 Msps. All receiver arrays use float64/complex128.
|
||||
|
||||
| Case | Acquisition metric | Code delay (chips) | Doppler (Hz) | Longest lock (s) | PLL RMS (rad) | DLL RMS (chips) | Code bias (chips) | C/N0 estimate | E1C secondary corr. |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| generated_BOC11__receiver_BOC11 | 111.600 | 81.250000 | 1250.0 | 0.184 | 0.029727 | 0.022977 | 0.000016 | 47.880 | 0.999801 |
|
||||
| generated_CBOC__receiver_BOC11 | 115.612 | 81.250000 | 1250.0 | 0.184 | 0.030846 | 0.035051 | -0.000847 | 42.311 | 0.999797 |
|
||||
| generated_CBOC__receiver_CBOC | 118.514 | 81.250000 | 1250.0 | 0.184 | 0.027297 | 0.052729 | -0.000875 | 42.301 | 0.999777 |
|
||||
|
||||
E1B navigation data was not enabled in this focused E1C-pilot matrix; page counts are therefore null, not zero-result decoder failures. The independent SignalSim regression remains the E1B BOC(1,1) I/NAV proof.
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"decoder_truth_inputs": [],
|
||||
"tracking_architecture": "e1c_pilot_aided_e1b",
|
||||
"icd": "Galileo OS SIS ICD Issue 2.2, November 2025",
|
||||
"signalsim_commit": "80b262113e77c4482ca9f5cbfef9bcada402ed28",
|
||||
"sample_file": "/home/onur/Desktop/galileo/verification/vectors/signalsim/navigation_multisat/navigation_multisat_iq8.bin",
|
||||
@@ -10,17 +11,24 @@
|
||||
"satellites": {
|
||||
"1": {
|
||||
"prn": 1,
|
||||
"tracking_architecture": "e1c_pilot_aided_e1b",
|
||||
"acquisition_component": "E1C",
|
||||
"acquisition_detected": true,
|
||||
"acquisition_time_s": 0.1004119270000956,
|
||||
"acquisition_time_s": 0.10627389200089965,
|
||||
"raw_doppler_hz": 1750.0,
|
||||
"expected_doppler_hz": 1828.0,
|
||||
"start_sample_timestamp": 90995,
|
||||
"tracking_epoch_count": 16088,
|
||||
"tracking_time_s": 13.650105177999649,
|
||||
"tracking_time_s": 30.29040445300052,
|
||||
"tracking_final_state": "PLL_TRACK",
|
||||
"tracking_reacquisition_requests": 10,
|
||||
"tracking_lock_timestamp": 107363,
|
||||
"tracking_reacquisition_requests": 0,
|
||||
"tracking_lock_timestamp": 311963,
|
||||
"prompt_epoch_count": 8044,
|
||||
"valid_prompt_epoch_count": 8044,
|
||||
"invalid_prompt_epoch_count": 0,
|
||||
"cycle_slips": 0,
|
||||
"sample_discontinuities": 0,
|
||||
"raw_navigation_symbol_count": 8044,
|
||||
"first_synchronized_symbol_timestamp": 4428515,
|
||||
"first_page_sync_timestamp": 4428515,
|
||||
"first_crc_valid_page_timestamp": 4428515,
|
||||
@@ -44,27 +52,151 @@
|
||||
"clock_parameters": true,
|
||||
"health_valid": true,
|
||||
"data_valid": true,
|
||||
"decoding_time_s": 1.260811995001859
|
||||
"decoding_time_s": 1.2302890369974193,
|
||||
"secondary_code_sync": {
|
||||
"phase": 10,
|
||||
"residual_frequency_hz": 59.5,
|
||||
"normalized_correlation": 0.9941599620460754,
|
||||
"compared_periods": 100,
|
||||
"sign_errors": 0
|
||||
},
|
||||
"lock_analysis": {
|
||||
"prn": 1,
|
||||
"epochs": 16088,
|
||||
"integration_ms": 2.0,
|
||||
"start_sample_timestamp": 90995,
|
||||
"end_sample_timestamp": 131755187,
|
||||
"raw_carrier_lock_pass_epochs": 16078,
|
||||
"raw_code_lock_pass_epochs": 15617,
|
||||
"raw_combined_lock_pass_epochs": 15617,
|
||||
"raw_combined_lock_fail_epochs": 471,
|
||||
"false_transition_count": 102,
|
||||
"legacy_code_lock_falling_edges": 102,
|
||||
"boc_aware_code_lock_falling_edges": 0,
|
||||
"boc_aware_metric_valid_epochs": 16064,
|
||||
"boc_aware_code_lock_pass_epochs": 16062,
|
||||
"boc_aware_combined_lock_pass_epochs": 16062,
|
||||
"boc_aware_longest_code_lock_s": 32.124,
|
||||
"boc_side_peak_suspected_epochs": 0,
|
||||
"carrier_metric_caused_transitions": 0,
|
||||
"code_metric_caused_transitions": 102,
|
||||
"symbol_transition_coincident_false_transitions": 0,
|
||||
"cycle_slips": 0,
|
||||
"sample_discontinuities": 0,
|
||||
"cn0_unavailable_false_transitions": 0,
|
||||
"false_run_count": 102,
|
||||
"single_epoch_false_runs": 45,
|
||||
"maximum_false_run_epochs": 49,
|
||||
"maximum_false_run_ms": 98.0,
|
||||
"maximum_continuous_raw_lock_epochs": 632,
|
||||
"maximum_continuous_raw_lock_s": 1.264,
|
||||
"carrier_metric_percentiles": {
|
||||
"1": 0.926123934471329,
|
||||
"5": 0.9887965609469924,
|
||||
"50": 0.9988996165475693,
|
||||
"95": 0.9999914964295384,
|
||||
"99": 0.9999996897371967
|
||||
},
|
||||
"code_metric_percentiles": {
|
||||
"1": 0.09265723002677347,
|
||||
"5": 9.240924942018399,
|
||||
"50": 14.358082125149325,
|
||||
"95": 20.787106913901518,
|
||||
"99": 24.857166929725683
|
||||
},
|
||||
"boc_main_peak_fraction_percentiles": {
|
||||
"1": 0.8475615556941004,
|
||||
"5": 0.8752438026290285,
|
||||
"50": 0.8897649686117325,
|
||||
"95": 0.899335220269324,
|
||||
"99": 0.9027108176195193
|
||||
},
|
||||
"boc_side_peak_metric_percentiles": {
|
||||
"1": 0.055600833755340986,
|
||||
"5": 0.05813312240679952,
|
||||
"50": 0.06516955958934093,
|
||||
"95": 0.08724568096229031,
|
||||
"99": 0.15809803712879886
|
||||
},
|
||||
"boc_main_peak_coherence_percentiles": {
|
||||
"1": 0.5909426356047754,
|
||||
"5": 0.8795912735269322,
|
||||
"50": 0.9687487070428722,
|
||||
"95": 0.9810030499701589,
|
||||
"99": 0.9846774791776529
|
||||
},
|
||||
"legacy_code_metric_failure_behavior": {
|
||||
"epochs": 383,
|
||||
"dll_error_rms_chips": 0.08574237706359694,
|
||||
"dll_error_p99_abs_chips": 0.1203286532395151,
|
||||
"code_phase_residual_rms_chips": 14.049922537033622,
|
||||
"code_phase_residual_p99_abs_chips": 18.527759786491757,
|
||||
"prompt_power_median": 110703392.74175157,
|
||||
"early_power_median": 1367287690.10518,
|
||||
"late_power_median": 486504907.02446777,
|
||||
"very_early_power_median": 294439962.72082204,
|
||||
"very_late_power_median": 28908749.984774385,
|
||||
"boc_main_peak_fraction_min": 0.8290411435832495,
|
||||
"boc_side_peak_metric_max": 0.1881158513724548,
|
||||
"boc_main_peak_coherence_min": 0.3224312067233396
|
||||
},
|
||||
"legacy_code_metric_pass_behavior": {
|
||||
"epochs": 15456,
|
||||
"dll_error_rms_chips": 0.05564808250796124,
|
||||
"dll_error_p99_abs_chips": 0.1566524337921172,
|
||||
"code_phase_residual_rms_chips": 10.738451874002433,
|
||||
"code_phase_residual_p99_abs_chips": 18.667906550937186,
|
||||
"prompt_power_median": 1558915633.5385687,
|
||||
"early_power_median": 103260909.49179389,
|
||||
"late_power_median": 102910301.02848622,
|
||||
"very_early_power_median": 103411517.73663911,
|
||||
"very_late_power_median": 103558607.31775787,
|
||||
"boc_main_peak_fraction_min": 0.8293685997191675,
|
||||
"boc_side_peak_metric_max": 0.19289641323661258,
|
||||
"boc_main_peak_coherence_min": 0.34335711247300654
|
||||
},
|
||||
"post_pull_in": {
|
||||
"pll_error_rms_rad": 0.07859984972937414,
|
||||
"pll_error_p99_abs_rad": 0.240824657200552,
|
||||
"dll_error_rms_chips": 0.056565003221219566,
|
||||
"dll_error_p99_abs_chips": 0.1551231594917256,
|
||||
"carrier_frequency_min_hz": 1024814.8951608158,
|
||||
"carrier_frequency_max_hz": 1024835.3125753923,
|
||||
"carrier_frequency_last_minus_first_hz": -10.013514776015654,
|
||||
"code_frequency_min_hz": 1023000.5447241135,
|
||||
"code_frequency_max_hz": 1023001.8192012197,
|
||||
"code_frequency_last_minus_first_hz": -0.18815856706351042,
|
||||
"cn0_median_db_hz": 46.94012968974618,
|
||||
"cn0_min_db_hz": 30.850429544319983
|
||||
}
|
||||
}
|
||||
},
|
||||
"4": {
|
||||
"prn": 4,
|
||||
"tracking_architecture": "e1c_pilot_aided_e1b",
|
||||
"acquisition_component": "E1C",
|
||||
"acquisition_detected": true,
|
||||
"acquisition_time_s": 0.08580062900000485,
|
||||
"acquisition_time_s": 0.09288738900067983,
|
||||
"raw_doppler_hz": 2250.0,
|
||||
"expected_doppler_hz": 2171.0,
|
||||
"start_sample_timestamp": 86819,
|
||||
"tracking_epoch_count": 16088,
|
||||
"tracking_time_s": 13.877967087995785,
|
||||
"tracking_time_s": 30.40403210999648,
|
||||
"tracking_final_state": "PLL_TRACK",
|
||||
"tracking_reacquisition_requests": 61,
|
||||
"tracking_lock_timestamp": 103187,
|
||||
"tracking_reacquisition_requests": 0,
|
||||
"tracking_lock_timestamp": 324155,
|
||||
"prompt_epoch_count": 8044,
|
||||
"first_synchronized_symbol_timestamp": 12624707,
|
||||
"first_page_sync_timestamp": 12624707,
|
||||
"first_crc_valid_page_timestamp": 12624707,
|
||||
"page_parts": 29,
|
||||
"pages": 14,
|
||||
"crc_passes": 14,
|
||||
"valid_prompt_epoch_count": 8044,
|
||||
"invalid_prompt_epoch_count": 0,
|
||||
"cycle_slips": 0,
|
||||
"sample_discontinuities": 0,
|
||||
"raw_navigation_symbol_count": 8044,
|
||||
"first_synchronized_symbol_timestamp": 4440707,
|
||||
"first_page_sync_timestamp": 4440707,
|
||||
"first_crc_valid_page_timestamp": 4440707,
|
||||
"page_parts": 31,
|
||||
"pages": 15,
|
||||
"crc_passes": 15,
|
||||
"crc_failures": 0,
|
||||
"fec_failures": 0,
|
||||
"received_word_types": [
|
||||
@@ -82,66 +214,313 @@
|
||||
"clock_parameters": true,
|
||||
"health_valid": true,
|
||||
"data_valid": true,
|
||||
"decoding_time_s": 1.3411525530027575
|
||||
"decoding_time_s": 1.2405268940055976,
|
||||
"secondary_code_sync": {
|
||||
"phase": 9,
|
||||
"residual_frequency_hz": -62.5,
|
||||
"normalized_correlation": 0.9860075818874949,
|
||||
"compared_periods": 100,
|
||||
"sign_errors": 0
|
||||
},
|
||||
"lock_analysis": {
|
||||
"prn": 4,
|
||||
"epochs": 16088,
|
||||
"integration_ms": 2.0,
|
||||
"start_sample_timestamp": 86819,
|
||||
"end_sample_timestamp": 131751011,
|
||||
"raw_carrier_lock_pass_epochs": 15999,
|
||||
"raw_code_lock_pass_epochs": 15416,
|
||||
"raw_combined_lock_pass_epochs": 15400,
|
||||
"raw_combined_lock_fail_epochs": 688,
|
||||
"false_transition_count": 110,
|
||||
"legacy_code_lock_falling_edges": 110,
|
||||
"boc_aware_code_lock_falling_edges": 0,
|
||||
"boc_aware_metric_valid_epochs": 16064,
|
||||
"boc_aware_code_lock_pass_epochs": 16062,
|
||||
"boc_aware_combined_lock_pass_epochs": 16060,
|
||||
"boc_aware_longest_code_lock_s": 32.124,
|
||||
"boc_side_peak_suspected_epochs": 0,
|
||||
"carrier_metric_caused_transitions": 2,
|
||||
"code_metric_caused_transitions": 108,
|
||||
"symbol_transition_coincident_false_transitions": 0,
|
||||
"cycle_slips": 0,
|
||||
"sample_discontinuities": 0,
|
||||
"cn0_unavailable_false_transitions": 1,
|
||||
"false_run_count": 111,
|
||||
"single_epoch_false_runs": 44,
|
||||
"maximum_false_run_epochs": 83,
|
||||
"maximum_false_run_ms": 166.0,
|
||||
"maximum_continuous_raw_lock_epochs": 1332,
|
||||
"maximum_continuous_raw_lock_s": 2.664,
|
||||
"carrier_metric_percentiles": {
|
||||
"1": 0.7792907543694391,
|
||||
"5": 0.986886910203348,
|
||||
"50": 0.9988692376063891,
|
||||
"95": 0.9999904419181908,
|
||||
"99": 0.9999995892743159
|
||||
},
|
||||
"code_metric_percentiles": {
|
||||
"1": 0.07231780273965693,
|
||||
"5": 4.680507563993592,
|
||||
"50": 14.135902184076237,
|
||||
"95": 20.836601051980484,
|
||||
"99": 24.70518847538168
|
||||
},
|
||||
"boc_main_peak_fraction_percentiles": {
|
||||
"1": 0.8383947757378236,
|
||||
"5": 0.8717822698761091,
|
||||
"50": 0.888551897894365,
|
||||
"95": 0.8981109549819073,
|
||||
"99": 0.9024539160758924
|
||||
},
|
||||
"boc_side_peak_metric_percentiles": {
|
||||
"1": 0.05606138409551904,
|
||||
"5": 0.058899453610220434,
|
||||
"50": 0.06631402357636437,
|
||||
"95": 0.09431335702543445,
|
||||
"99": 0.182588937217342
|
||||
},
|
||||
"boc_main_peak_coherence_percentiles": {
|
||||
"1": 0.3941670522754639,
|
||||
"5": 0.8633412765595349,
|
||||
"50": 0.9665765778788711,
|
||||
"95": 0.9798465018068564,
|
||||
"99": 0.9838025458145414
|
||||
},
|
||||
"legacy_code_metric_failure_behavior": {
|
||||
"epochs": 495,
|
||||
"dll_error_rms_chips": 0.08511890392660415,
|
||||
"dll_error_p99_abs_chips": 0.11897485396810367,
|
||||
"code_phase_residual_rms_chips": 17.39502421893431,
|
||||
"code_phase_residual_p99_abs_chips": 22.010231444418938,
|
||||
"prompt_power_median": 110387217.19998863,
|
||||
"early_power_median": 1414197765.1115377,
|
||||
"late_power_median": 455470385.12999207,
|
||||
"very_early_power_median": 327808041.9437474,
|
||||
"very_late_power_median": 17217167.445349354,
|
||||
"boc_main_peak_fraction_min": 0.8208748889416547,
|
||||
"boc_side_peak_metric_max": 0.2108131397387269,
|
||||
"boc_main_peak_coherence_min": 0.3186205201893039
|
||||
},
|
||||
"legacy_code_metric_pass_behavior": {
|
||||
"epochs": 15344,
|
||||
"dll_error_rms_chips": 0.05701516121119478,
|
||||
"dll_error_p99_abs_chips": 0.15711356864497972,
|
||||
"code_phase_residual_rms_chips": 12.712359749555613,
|
||||
"code_phase_residual_p99_abs_chips": 22.22504688444559,
|
||||
"prompt_power_median": 1557607909.2091415,
|
||||
"early_power_median": 104464066.41081777,
|
||||
"late_power_median": 103832797.49488433,
|
||||
"very_early_power_median": 104021624.65023576,
|
||||
"very_late_power_median": 104184118.93725646,
|
||||
"boc_main_peak_fraction_min": 0.825903881328457,
|
||||
"boc_side_peak_metric_max": 0.20186498523859175,
|
||||
"boc_main_peak_coherence_min": 0.3410282015915683
|
||||
},
|
||||
"post_pull_in": {
|
||||
"pll_error_rms_rad": 0.08218993802219834,
|
||||
"pll_error_p99_abs_rad": 0.2578335220481495,
|
||||
"dll_error_rms_chips": 0.05809960919771263,
|
||||
"dll_error_p99_abs_chips": 0.1559104269486663,
|
||||
"carrier_frequency_min_hz": 1025162.5739244855,
|
||||
"carrier_frequency_max_hz": 1025179.2658243881,
|
||||
"carrier_frequency_last_minus_first_hz": -4.755713645135984,
|
||||
"code_frequency_min_hz": 1023000.7767271643,
|
||||
"code_frequency_max_hz": 1023002.1806047091,
|
||||
"code_frequency_last_minus_first_hz": -0.02835774887353182,
|
||||
"cn0_median_db_hz": 46.40741068588617,
|
||||
"cn0_min_db_hz": 29.42711443027489
|
||||
}
|
||||
}
|
||||
},
|
||||
"19": {
|
||||
"prn": 19,
|
||||
"tracking_architecture": "e1c_pilot_aided_e1b",
|
||||
"acquisition_component": "E1C",
|
||||
"acquisition_detected": true,
|
||||
"acquisition_time_s": 0.0836479790013982,
|
||||
"acquisition_time_s": 0.09294268899975577,
|
||||
"raw_doppler_hz": -250.0,
|
||||
"expected_doppler_hz": -367.0,
|
||||
"start_sample_timestamp": 87566,
|
||||
"tracking_epoch_count": 16088,
|
||||
"tracking_time_s": 13.9836710640011,
|
||||
"tracking_time_s": 30.35874111299927,
|
||||
"tracking_final_state": "PLL_TRACK",
|
||||
"tracking_reacquisition_requests": 406,
|
||||
"tracking_lock_timestamp": 103934,
|
||||
"tracking_reacquisition_requests": 0,
|
||||
"tracking_lock_timestamp": 734102,
|
||||
"prompt_epoch_count": 8044,
|
||||
"first_synchronized_symbol_timestamp": 16684718,
|
||||
"first_page_sync_timestamp": 16684718,
|
||||
"first_crc_valid_page_timestamp": 20776718,
|
||||
"page_parts": 28,
|
||||
"pages": 13,
|
||||
"crc_passes": 13,
|
||||
"valid_prompt_epoch_count": 8044,
|
||||
"invalid_prompt_epoch_count": 0,
|
||||
"cycle_slips": 0,
|
||||
"sample_discontinuities": 0,
|
||||
"raw_navigation_symbol_count": 8044,
|
||||
"first_synchronized_symbol_timestamp": 4408718,
|
||||
"first_page_sync_timestamp": 4408718,
|
||||
"first_crc_valid_page_timestamp": 4408718,
|
||||
"page_parts": 31,
|
||||
"pages": 15,
|
||||
"crc_passes": 15,
|
||||
"crc_failures": 0,
|
||||
"fec_failures": 0,
|
||||
"received_word_types": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6
|
||||
],
|
||||
"missing_ephemeris_word_types": [
|
||||
1
|
||||
],
|
||||
"complete_ephemeris": false,
|
||||
"ephemeris_iod_nav": null,
|
||||
"first_complete_ephemeris_timestamp": null,
|
||||
"missing_ephemeris_word_types": [],
|
||||
"complete_ephemeris": true,
|
||||
"ephemeris_iod_nav": 29,
|
||||
"first_complete_ephemeris_timestamp": 61696718,
|
||||
"clock_parameters": true,
|
||||
"health_valid": false,
|
||||
"data_valid": false,
|
||||
"decoding_time_s": 1.2081766550036264
|
||||
"health_valid": true,
|
||||
"data_valid": true,
|
||||
"decoding_time_s": 1.3045363380006165,
|
||||
"secondary_code_sync": {
|
||||
"phase": 11,
|
||||
"residual_frequency_hz": -38.5,
|
||||
"normalized_correlation": 0.9953855496694146,
|
||||
"compared_periods": 100,
|
||||
"sign_errors": 0
|
||||
},
|
||||
"lock_analysis": {
|
||||
"prn": 19,
|
||||
"epochs": 16088,
|
||||
"integration_ms": 2.0,
|
||||
"start_sample_timestamp": 87566,
|
||||
"end_sample_timestamp": 131751758,
|
||||
"raw_carrier_lock_pass_epochs": 15949,
|
||||
"raw_code_lock_pass_epochs": 15183,
|
||||
"raw_combined_lock_pass_epochs": 15111,
|
||||
"raw_combined_lock_fail_epochs": 977,
|
||||
"false_transition_count": 30,
|
||||
"legacy_code_lock_falling_edges": 31,
|
||||
"boc_aware_code_lock_falling_edges": 0,
|
||||
"boc_aware_metric_valid_epochs": 16064,
|
||||
"boc_aware_code_lock_pass_epochs": 16062,
|
||||
"boc_aware_combined_lock_pass_epochs": 16010,
|
||||
"boc_aware_longest_code_lock_s": 32.124,
|
||||
"boc_side_peak_suspected_epochs": 0,
|
||||
"carrier_metric_caused_transitions": 0,
|
||||
"code_metric_caused_transitions": 30,
|
||||
"symbol_transition_coincident_false_transitions": 0,
|
||||
"cycle_slips": 0,
|
||||
"sample_discontinuities": 0,
|
||||
"cn0_unavailable_false_transitions": 0,
|
||||
"false_run_count": 31,
|
||||
"single_epoch_false_runs": 2,
|
||||
"maximum_false_run_epochs": 279,
|
||||
"maximum_false_run_ms": 558.0,
|
||||
"maximum_continuous_raw_lock_epochs": 523,
|
||||
"maximum_continuous_raw_lock_s": 1.046,
|
||||
"carrier_metric_percentiles": {
|
||||
"1": 0.7348107538430972,
|
||||
"5": 0.9860941032539999,
|
||||
"50": 0.9988355432993672,
|
||||
"95": 0.9999895126224544,
|
||||
"99": 0.9999995737755365
|
||||
},
|
||||
"code_metric_percentiles": {
|
||||
"1": 0.06685317434009187,
|
||||
"5": 0.18474101219165578,
|
||||
"50": 14.188567533729127,
|
||||
"95": 20.853641899942982,
|
||||
"99": 24.900783119459
|
||||
},
|
||||
"boc_main_peak_fraction_percentiles": {
|
||||
"1": 0.8331077551592363,
|
||||
"5": 0.8556563253779765,
|
||||
"50": 0.8898638926439826,
|
||||
"95": 0.8995272735597457,
|
||||
"99": 0.9034500829578935
|
||||
},
|
||||
"boc_side_peak_metric_percentiles": {
|
||||
"1": 0.05517877177836974,
|
||||
"5": 0.057838743800940465,
|
||||
"50": 0.06512106911574578,
|
||||
"95": 0.13637269202092908,
|
||||
"99": 0.19432472241219176
|
||||
},
|
||||
"boc_main_peak_coherence_percentiles": {
|
||||
"1": 0.3285514186082982,
|
||||
"5": 0.6780597999663746,
|
||||
"50": 0.9697975202111472,
|
||||
"95": 0.9804065041471643,
|
||||
"99": 0.98403198606922
|
||||
},
|
||||
"legacy_code_metric_failure_behavior": {
|
||||
"epochs": 728,
|
||||
"dll_error_rms_chips": 0.08437694864253784,
|
||||
"dll_error_p99_abs_chips": 0.11420464213225176,
|
||||
"code_phase_residual_rms_chips": 2.68046518243341,
|
||||
"code_phase_residual_p99_abs_chips": 3.8578061850383074,
|
||||
"prompt_power_median": 107058906.74306475,
|
||||
"early_power_median": 589909306.6672449,
|
||||
"late_power_median": 1246213886.4347653,
|
||||
"very_early_power_median": 42916283.52768795,
|
||||
"very_late_power_median": 242361314.9778272,
|
||||
"boc_main_peak_fraction_min": 0.8262777121115853,
|
||||
"boc_side_peak_metric_max": 0.2062479029943051,
|
||||
"boc_main_peak_coherence_min": 0.3152967643499338
|
||||
},
|
||||
"legacy_code_metric_pass_behavior": {
|
||||
"epochs": 15111,
|
||||
"dll_error_rms_chips": 0.05496916685224953,
|
||||
"dll_error_p99_abs_chips": 0.14811405306726963,
|
||||
"code_phase_residual_rms_chips": 2.1905835998108185,
|
||||
"code_phase_residual_p99_abs_chips": 3.853546458312098,
|
||||
"prompt_power_median": 1572867668.3084347,
|
||||
"early_power_median": 104616403.78707226,
|
||||
"late_power_median": 105273180.84896234,
|
||||
"very_early_power_median": 104208400.61576208,
|
||||
"very_late_power_median": 104196595.71394174,
|
||||
"boc_main_peak_fraction_min": 0.8244860197459938,
|
||||
"boc_side_peak_metric_max": 0.20227746225808624,
|
||||
"boc_main_peak_coherence_min": 0.34505028117893527
|
||||
},
|
||||
"post_pull_in": {
|
||||
"pll_error_rms_rad": 0.08798351512883444,
|
||||
"pll_error_p99_abs_rad": 0.33384337105425854,
|
||||
"dll_error_rms_chips": 0.05665648411462819,
|
||||
"dll_error_p99_abs_chips": 0.14671966000911835,
|
||||
"carrier_frequency_min_hz": 1022617.3013250588,
|
||||
"carrier_frequency_max_hz": 1022640.4852699582,
|
||||
"carrier_frequency_last_minus_first_hz": -12.029085914371535,
|
||||
"code_frequency_min_hz": 1022999.1296675402,
|
||||
"code_frequency_max_hz": 1023000.3486243897,
|
||||
"code_frequency_last_minus_first_hz": 0.4431725781178102,
|
||||
"cn0_median_db_hz": 47.306377947072626,
|
||||
"cn0_min_db_hz": 28.833883219972133
|
||||
}
|
||||
}
|
||||
},
|
||||
"21": {
|
||||
"prn": 21,
|
||||
"tracking_architecture": "e1c_pilot_aided_e1b",
|
||||
"acquisition_component": "E1C",
|
||||
"acquisition_detected": true,
|
||||
"acquisition_time_s": 0.08741291100159287,
|
||||
"acquisition_time_s": 0.08547867099696305,
|
||||
"raw_doppler_hz": -750.0,
|
||||
"expected_doppler_hz": -712.0,
|
||||
"start_sample_timestamp": 93628,
|
||||
"tracking_epoch_count": 16088,
|
||||
"tracking_time_s": 13.982213923998643,
|
||||
"tracking_time_s": 30.185307627005386,
|
||||
"tracking_final_state": "PLL_TRACK",
|
||||
"tracking_reacquisition_requests": 259,
|
||||
"tracking_lock_timestamp": 101812,
|
||||
"tracking_reacquisition_requests": 0,
|
||||
"tracking_lock_timestamp": 339148,
|
||||
"prompt_epoch_count": 8044,
|
||||
"first_synchronized_symbol_timestamp": 12598780,
|
||||
"first_page_sync_timestamp": 12598780,
|
||||
"first_crc_valid_page_timestamp": 12598780,
|
||||
"page_parts": 29,
|
||||
"pages": 14,
|
||||
"crc_passes": 14,
|
||||
"valid_prompt_epoch_count": 8044,
|
||||
"invalid_prompt_epoch_count": 0,
|
||||
"cycle_slips": 0,
|
||||
"sample_discontinuities": 0,
|
||||
"raw_navigation_symbol_count": 8044,
|
||||
"first_synchronized_symbol_timestamp": 4414780,
|
||||
"first_page_sync_timestamp": 4414780,
|
||||
"first_crc_valid_page_timestamp": 4414780,
|
||||
"page_parts": 31,
|
||||
"pages": 15,
|
||||
"crc_passes": 15,
|
||||
"crc_failures": 0,
|
||||
"fec_failures": 0,
|
||||
"received_word_types": [
|
||||
@@ -159,27 +538,151 @@
|
||||
"clock_parameters": true,
|
||||
"health_valid": true,
|
||||
"data_valid": true,
|
||||
"decoding_time_s": 1.231179278001946
|
||||
"decoding_time_s": 1.3240309810062172,
|
||||
"secondary_code_sync": {
|
||||
"phase": 11,
|
||||
"residual_frequency_hz": 37.5,
|
||||
"normalized_correlation": 0.9856821042522145,
|
||||
"compared_periods": 100,
|
||||
"sign_errors": 0
|
||||
},
|
||||
"lock_analysis": {
|
||||
"prn": 21,
|
||||
"epochs": 16088,
|
||||
"integration_ms": 2.0,
|
||||
"start_sample_timestamp": 93628,
|
||||
"end_sample_timestamp": 131757820,
|
||||
"raw_carrier_lock_pass_epochs": 16031,
|
||||
"raw_code_lock_pass_epochs": 15249,
|
||||
"raw_combined_lock_pass_epochs": 15249,
|
||||
"raw_combined_lock_fail_epochs": 839,
|
||||
"false_transition_count": 51,
|
||||
"legacy_code_lock_falling_edges": 51,
|
||||
"boc_aware_code_lock_falling_edges": 0,
|
||||
"boc_aware_metric_valid_epochs": 16064,
|
||||
"boc_aware_code_lock_pass_epochs": 16062,
|
||||
"boc_aware_combined_lock_pass_epochs": 16059,
|
||||
"boc_aware_longest_code_lock_s": 32.124,
|
||||
"boc_side_peak_suspected_epochs": 0,
|
||||
"carrier_metric_caused_transitions": 0,
|
||||
"code_metric_caused_transitions": 51,
|
||||
"symbol_transition_coincident_false_transitions": 0,
|
||||
"cycle_slips": 0,
|
||||
"sample_discontinuities": 0,
|
||||
"cn0_unavailable_false_transitions": 0,
|
||||
"false_run_count": 51,
|
||||
"single_epoch_false_runs": 4,
|
||||
"maximum_false_run_epochs": 157,
|
||||
"maximum_false_run_ms": 314.0,
|
||||
"maximum_continuous_raw_lock_epochs": 793,
|
||||
"maximum_continuous_raw_lock_s": 1.586,
|
||||
"carrier_metric_percentiles": {
|
||||
"1": 0.7939514446493192,
|
||||
"5": 0.985632857889365,
|
||||
"50": 0.9988523785765591,
|
||||
"95": 0.9999895032338855,
|
||||
"99": 0.9999995002250147
|
||||
},
|
||||
"code_metric_percentiles": {
|
||||
"1": 0.06616391188826821,
|
||||
"5": 0.2581154135198231,
|
||||
"50": 14.22150508819822,
|
||||
"95": 21.041903291304745,
|
||||
"99": 25.29011298405983
|
||||
},
|
||||
"boc_main_peak_fraction_percentiles": {
|
||||
"1": 0.8328410713646706,
|
||||
"5": 0.862839862016292,
|
||||
"50": 0.8894454265519192,
|
||||
"95": 0.899005820753033,
|
||||
"99": 0.902725891411603
|
||||
},
|
||||
"boc_side_peak_metric_percentiles": {
|
||||
"1": 0.05581216956145967,
|
||||
"5": 0.0581020070281789,
|
||||
"50": 0.06537705791281424,
|
||||
"95": 0.12414647994867094,
|
||||
"99": 0.19464888907499955
|
||||
},
|
||||
"boc_main_peak_coherence_percentiles": {
|
||||
"1": 0.32651431878020987,
|
||||
"5": 0.7456684567711471,
|
||||
"50": 0.9698504431158739,
|
||||
"95": 0.9804936771751901,
|
||||
"99": 0.9837504008164109
|
||||
},
|
||||
"legacy_code_metric_failure_behavior": {
|
||||
"epochs": 682,
|
||||
"dll_error_rms_chips": 0.08442597625870917,
|
||||
"dll_error_p99_abs_chips": 0.11360544478812587,
|
||||
"code_phase_residual_rms_chips": 5.679331578203824,
|
||||
"code_phase_residual_p99_abs_chips": 7.326207964266705,
|
||||
"prompt_power_median": 104189541.93969142,
|
||||
"early_power_median": 503192371.15070486,
|
||||
"late_power_median": 1403405103.7404156,
|
||||
"very_early_power_median": 22039174.93790304,
|
||||
"very_late_power_median": 317213067.5344795,
|
||||
"boc_main_peak_fraction_min": 0.8192520869211634,
|
||||
"boc_side_peak_metric_max": 0.21126892991099358,
|
||||
"boc_main_peak_coherence_min": 0.3116800253668606
|
||||
},
|
||||
"legacy_code_metric_pass_behavior": {
|
||||
"epochs": 15157,
|
||||
"dll_error_rms_chips": 0.05453659411635098,
|
||||
"dll_error_p99_abs_chips": 0.1449088577056262,
|
||||
"code_phase_residual_rms_chips": 4.188063318602212,
|
||||
"code_phase_residual_p99_abs_chips": 7.374789512632724,
|
||||
"prompt_power_median": 1569583185.8754468,
|
||||
"early_power_median": 103618886.08421741,
|
||||
"late_power_median": 103783265.51861823,
|
||||
"very_early_power_median": 104017003.65515691,
|
||||
"very_late_power_median": 104035110.46635103,
|
||||
"boc_main_peak_fraction_min": 0.8344860559063105,
|
||||
"boc_side_peak_metric_max": 0.19273063690182018,
|
||||
"boc_main_peak_coherence_min": 0.33714214192677894
|
||||
},
|
||||
"post_pull_in": {
|
||||
"pll_error_rms_rad": 0.08950722912725978,
|
||||
"pll_error_p99_abs_rad": 0.34532589523816604,
|
||||
"dll_error_rms_chips": 0.056152318470159265,
|
||||
"dll_error_p99_abs_chips": 0.14402921672670457,
|
||||
"carrier_frequency_min_hz": 1022268.5496190831,
|
||||
"carrier_frequency_max_hz": 1022294.5921728422,
|
||||
"carrier_frequency_last_minus_first_hz": -11.265239437692799,
|
||||
"code_frequency_min_hz": 1022998.8592261485,
|
||||
"code_frequency_max_hz": 1023000.1806048183,
|
||||
"code_frequency_last_minus_first_hz": -0.1938835997134447,
|
||||
"cn0_median_db_hz": 47.64003907257697,
|
||||
"cn0_min_db_hz": 28.276106571211788
|
||||
}
|
||||
}
|
||||
},
|
||||
"27": {
|
||||
"prn": 27,
|
||||
"tracking_architecture": "e1c_pilot_aided_e1b",
|
||||
"acquisition_component": "E1C",
|
||||
"acquisition_detected": true,
|
||||
"acquisition_time_s": 0.08157938700605882,
|
||||
"acquisition_time_s": 0.09328085999732139,
|
||||
"raw_doppler_hz": -2750.0,
|
||||
"expected_doppler_hz": -2637.0,
|
||||
"start_sample_timestamp": 88014,
|
||||
"tracking_epoch_count": 16088,
|
||||
"tracking_time_s": 13.978430488001322,
|
||||
"tracking_time_s": 30.066800435000914,
|
||||
"tracking_final_state": "PLL_TRACK",
|
||||
"tracking_reacquisition_requests": 0,
|
||||
"tracking_lock_timestamp": 96198,
|
||||
"tracking_lock_timestamp": 1381086,
|
||||
"prompt_epoch_count": 8044,
|
||||
"first_synchronized_symbol_timestamp": 12642270,
|
||||
"first_page_sync_timestamp": 12642270,
|
||||
"first_crc_valid_page_timestamp": 12642270,
|
||||
"page_parts": 29,
|
||||
"pages": 14,
|
||||
"crc_passes": 14,
|
||||
"valid_prompt_epoch_count": 8044,
|
||||
"invalid_prompt_epoch_count": 0,
|
||||
"cycle_slips": 0,
|
||||
"sample_discontinuities": 0,
|
||||
"raw_navigation_symbol_count": 8044,
|
||||
"first_synchronized_symbol_timestamp": 4458270,
|
||||
"first_page_sync_timestamp": 4458270,
|
||||
"first_crc_valid_page_timestamp": 4458270,
|
||||
"page_parts": 31,
|
||||
"pages": 15,
|
||||
"crc_passes": 15,
|
||||
"crc_failures": 0,
|
||||
"fec_failures": 0,
|
||||
"received_word_types": [
|
||||
@@ -197,7 +700,124 @@
|
||||
"clock_parameters": true,
|
||||
"health_valid": true,
|
||||
"data_valid": true,
|
||||
"decoding_time_s": 1.2002744679994066
|
||||
"decoding_time_s": 1.2845996300020488,
|
||||
"secondary_code_sync": {
|
||||
"phase": 8,
|
||||
"residual_frequency_hz": 36.5,
|
||||
"normalized_correlation": 0.996052910867296,
|
||||
"compared_periods": 100,
|
||||
"sign_errors": 0
|
||||
},
|
||||
"lock_analysis": {
|
||||
"prn": 27,
|
||||
"epochs": 16088,
|
||||
"integration_ms": 2.0,
|
||||
"start_sample_timestamp": 88014,
|
||||
"end_sample_timestamp": 131752206,
|
||||
"raw_carrier_lock_pass_epochs": 15936,
|
||||
"raw_code_lock_pass_epochs": 15647,
|
||||
"raw_combined_lock_pass_epochs": 15556,
|
||||
"raw_combined_lock_fail_epochs": 532,
|
||||
"false_transition_count": 120,
|
||||
"legacy_code_lock_falling_edges": 122,
|
||||
"boc_aware_code_lock_falling_edges": 0,
|
||||
"boc_aware_metric_valid_epochs": 16064,
|
||||
"boc_aware_code_lock_pass_epochs": 16062,
|
||||
"boc_aware_combined_lock_pass_epochs": 15931,
|
||||
"boc_aware_longest_code_lock_s": 32.124,
|
||||
"boc_side_peak_suspected_epochs": 0,
|
||||
"carrier_metric_caused_transitions": 0,
|
||||
"code_metric_caused_transitions": 120,
|
||||
"symbol_transition_coincident_false_transitions": 0,
|
||||
"cycle_slips": 0,
|
||||
"sample_discontinuities": 0,
|
||||
"cn0_unavailable_false_transitions": 0,
|
||||
"false_run_count": 121,
|
||||
"single_epoch_false_runs": 58,
|
||||
"maximum_false_run_epochs": 155,
|
||||
"maximum_false_run_ms": 310.0,
|
||||
"maximum_continuous_raw_lock_epochs": 1095,
|
||||
"maximum_continuous_raw_lock_s": 2.19,
|
||||
"carrier_metric_percentiles": {
|
||||
"1": 0.7206432555049623,
|
||||
"5": 0.988611380400891,
|
||||
"50": 0.9988895300080884,
|
||||
"95": 0.9999910174511765,
|
||||
"99": 0.9999996463442009
|
||||
},
|
||||
"code_metric_percentiles": {
|
||||
"1": 0.0963945544624243,
|
||||
"5": 9.022094975682883,
|
||||
"50": 14.336371788159624,
|
||||
"95": 21.01430502486875,
|
||||
"99": 25.223778227213373
|
||||
},
|
||||
"boc_main_peak_fraction_percentiles": {
|
||||
"1": 0.8563053481254727,
|
||||
"5": 0.8761216856241046,
|
||||
"50": 0.8897298346486104,
|
||||
"95": 0.8989983348293147,
|
||||
"99": 0.9027659012322223
|
||||
},
|
||||
"boc_side_peak_metric_percentiles": {
|
||||
"1": 0.055865232111185666,
|
||||
"5": 0.05827544455675619,
|
||||
"50": 0.06545776761892909,
|
||||
"95": 0.08721718306155764,
|
||||
"99": 0.13554248142317915
|
||||
},
|
||||
"boc_main_peak_coherence_percentiles": {
|
||||
"1": 0.6963850681372143,
|
||||
"5": 0.8864149571238227,
|
||||
"50": 0.9668775731513674,
|
||||
"95": 0.9797144500640755,
|
||||
"99": 0.9833546124056554
|
||||
},
|
||||
"legacy_code_metric_failure_behavior": {
|
||||
"epochs": 353,
|
||||
"dll_error_rms_chips": 0.08692287486188564,
|
||||
"dll_error_p99_abs_chips": 0.12211690739906192,
|
||||
"code_phase_residual_rms_chips": 20.83535121208654,
|
||||
"code_phase_residual_p99_abs_chips": 26.7740789856638,
|
||||
"prompt_power_median": 113099284.09916593,
|
||||
"early_power_median": 496253334.48126817,
|
||||
"late_power_median": 1219569538.2556756,
|
||||
"very_early_power_median": 22055861.48928148,
|
||||
"very_late_power_median": 272969579.3795633,
|
||||
"boc_main_peak_fraction_min": 0.8211892376838884,
|
||||
"boc_side_peak_metric_max": 0.20314364512635572,
|
||||
"boc_main_peak_coherence_min": 0.47009464074954066
|
||||
},
|
||||
"legacy_code_metric_pass_behavior": {
|
||||
"epochs": 15486,
|
||||
"dll_error_rms_chips": 0.05696868079676946,
|
||||
"dll_error_p99_abs_chips": 0.15879673488599466,
|
||||
"code_phase_residual_rms_chips": 15.537475523073843,
|
||||
"code_phase_residual_p99_abs_chips": 27.042686964558467,
|
||||
"prompt_power_median": 1549780078.7121413,
|
||||
"early_power_median": 102583529.59728253,
|
||||
"late_power_median": 102880717.96247455,
|
||||
"very_early_power_median": 101579724.68762413,
|
||||
"very_late_power_median": 103913161.92134063,
|
||||
"boc_main_peak_fraction_min": 0.8199308789902463,
|
||||
"boc_side_peak_metric_max": 0.20370582760979403,
|
||||
"boc_main_peak_coherence_min": 0.46610449527987624
|
||||
},
|
||||
"post_pull_in": {
|
||||
"pll_error_rms_rad": 0.0768043956210642,
|
||||
"pll_error_p99_abs_rad": 0.21602664107569355,
|
||||
"dll_error_rms_chips": 0.057805622538207146,
|
||||
"dll_error_p99_abs_chips": 0.15780488364179804,
|
||||
"carrier_frequency_min_hz": 1020353.622410044,
|
||||
"carrier_frequency_max_hz": 1020369.6683440519,
|
||||
"carrier_frequency_last_minus_first_hz": -2.8634769301861525,
|
||||
"code_frequency_min_hz": 1022997.605345076,
|
||||
"code_frequency_max_hz": 1022998.939424797,
|
||||
"code_frequency_last_minus_first_hz": -0.08650950586888939,
|
||||
"cn0_median_db_hz": 45.94570799038448,
|
||||
"cn0_min_db_hz": 32.27042575677653
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"galileo_time": {
|
||||
|
||||
@@ -5,7 +5,7 @@ The standalone decoder consumed tracked E1B prompts only; simulator ephemeris an
|
||||
| PRN | Tracking | Symbol sync | Page sync | CRC pages | Complete ephemeris | Clock | Time | Ready |
|
||||
| --: | -------: | ----------: | --------: | --------: | -----------------: | ----: | ---: | ----: |
|
||||
| 1 | PLL_TRACK | True | 31 | 15 | True | True | True | True |
|
||||
| 4 | PLL_TRACK | True | 29 | 14 | True | True | True | True |
|
||||
| 19 | PLL_TRACK | True | 28 | 13 | False | True | True | False |
|
||||
| 21 | PLL_TRACK | True | 29 | 14 | True | True | True | True |
|
||||
| 27 | PLL_TRACK | True | 29 | 14 | True | True | True | True |
|
||||
| 4 | PLL_TRACK | True | 31 | 15 | True | True | True | True |
|
||||
| 19 | PLL_TRACK | True | 31 | 15 | True | True | True | True |
|
||||
| 21 | PLL_TRACK | True | 31 | 15 | True | True | True | True |
|
||||
| 27 | PLL_TRACK | True | 31 | 15 | True | True | True | True |
|
||||
|
||||
@@ -11,6 +11,7 @@ from galileo_rx.config import AcquisitionConfig
|
||||
from galileo_rx.signal_profiles import SignalProfile
|
||||
from galileo_rx.signals.code_provider import CodeProvider
|
||||
from galileo_rx.signals.galileo_e1 import e1_local_replica
|
||||
from galileo_rx.signals.e1_modulation import E1Modulation, normalize_e1_modulation
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -29,7 +30,8 @@ class PcpsAcquisition:
|
||||
self.sampling_frequency_hz = sampling_frequency_hz
|
||||
self.config = config
|
||||
config.validate()
|
||||
self._code_fft_cache: dict[tuple[str, int, int, int, tuple[int, ...], float], npt.NDArray[np.complex128]] = {}
|
||||
self.modulation = normalize_e1_modulation(config.modulation)
|
||||
self._code_fft_cache: dict[tuple[str, E1Modulation, int, int, int, tuple[int, ...], float], npt.NDArray[np.complex128]] = {}
|
||||
|
||||
@property
|
||||
def coherent_samples(self) -> int:
|
||||
@@ -52,11 +54,12 @@ class PcpsAcquisition:
|
||||
effective_code_rate_hz = self.profile.code_rate_hz
|
||||
if self.config.code_doppler_aiding:
|
||||
effective_code_rate_hz *= 1.0 + doppler_hz / self.profile.carrier_frequency_hz
|
||||
key = (component, prn, sample_count, fft_length, primary_signs, effective_code_rate_hz)
|
||||
key = (component, self.modulation, prn, sample_count, fft_length, primary_signs, effective_code_rate_hz)
|
||||
if key not in self._code_fft_cache:
|
||||
replica = e1_local_replica(
|
||||
self.profile, self.provider, component, prn, sample_count,
|
||||
self.sampling_frequency_hz, code_rate_hz=effective_code_rate_hz,
|
||||
modulation=self.modulation,
|
||||
)
|
||||
if primary_signs != (1,):
|
||||
primary_samples = int(round(self.profile.component(component).primary_code_period_s * self.sampling_frequency_hz))
|
||||
|
||||
@@ -8,6 +8,8 @@ from typing import Any, Literal, TypeVar
|
||||
|
||||
import yaml
|
||||
|
||||
from galileo_rx.signals.e1_modulation import E1Component, E1Modulation, normalize_e1_component, normalize_e1_modulation
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
@@ -31,6 +33,7 @@ class RfConfig:
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AcquisitionConfig:
|
||||
modulation: E1Modulation | str = E1Modulation.BOC11
|
||||
doppler_min_hz: float = -10_000.0
|
||||
doppler_max_hz: float = 10_000.0
|
||||
doppler_step_hz: float = 250.0
|
||||
@@ -51,6 +54,7 @@ class AcquisitionConfig:
|
||||
code_doppler_aiding: bool = True
|
||||
|
||||
def validate(self) -> None:
|
||||
normalize_e1_modulation(self.modulation)
|
||||
if self.doppler_max_hz < self.doppler_min_hz or self.doppler_step_hz <= 0:
|
||||
raise ValueError("invalid Doppler search range or step")
|
||||
if self.coherent_integration_ms <= 0 or self.noncoherent_accumulations < 1:
|
||||
@@ -63,6 +67,7 @@ class AcquisitionConfig:
|
||||
|
||||
@dataclass(slots=True)
|
||||
class TrackingConfig:
|
||||
modulation: E1Modulation | str = E1Modulation.BOC11
|
||||
integration_ms: float = 4.0
|
||||
early_late_spacing_chips: float = 0.25
|
||||
very_early_late_spacing_chips: float = 0.75
|
||||
@@ -92,6 +97,7 @@ class TrackingConfig:
|
||||
lock_signal_loss_power_ratio: float = 0.02
|
||||
|
||||
def validate(self) -> None:
|
||||
normalize_e1_modulation(self.modulation)
|
||||
if self.integration_ms <= 0:
|
||||
raise ValueError("tracking integration_ms must be positive")
|
||||
if not 0 < self.early_late_spacing_chips < self.very_early_late_spacing_chips:
|
||||
@@ -132,7 +138,7 @@ class FixedPointConfig:
|
||||
@dataclass(slots=True)
|
||||
class ReceiverConfig:
|
||||
signal_profile: str = "GALILEO_E1"
|
||||
component: Literal["E1B", "E1C"] = "E1C"
|
||||
component: E1Component | str = E1Component.E1C
|
||||
prns: list[int] = field(default_factory=lambda: [1])
|
||||
rf: RfConfig = field(default_factory=RfConfig)
|
||||
acquisition: AcquisitionConfig = field(default_factory=AcquisitionConfig)
|
||||
@@ -141,18 +147,19 @@ class ReceiverConfig:
|
||||
fixed_point: FixedPointConfig = field(default_factory=FixedPointConfig)
|
||||
|
||||
def validate(self) -> None:
|
||||
component = normalize_e1_component(self.component)
|
||||
self.rf.validate()
|
||||
self.acquisition.validate()
|
||||
self.tracking.validate()
|
||||
if not self.prns or any(prn <= 0 for prn in self.prns):
|
||||
raise ValueError("prns must contain positive identifiers")
|
||||
if self.acquisition.coherent_integration_ms > 4.0:
|
||||
if self.component == "E1B" and self.acquisition.bit_transition_mode == "none":
|
||||
if component is E1Component.E1B and self.acquisition.bit_transition_mode == "none":
|
||||
raise ValueError("E1B coherent integration beyond 4 ms requires a bit-transition mode")
|
||||
if self.component == "E1C" and self.acquisition.secondary_code_mode == "none":
|
||||
if component is E1Component.E1C and self.acquisition.secondary_code_mode == "none":
|
||||
raise ValueError("E1C coherent integration beyond 4 ms requires secondary-code handling")
|
||||
if self.tracking.integration_ms > 4.0:
|
||||
if self.component == "E1B":
|
||||
if component is E1Component.E1B:
|
||||
raise ValueError("E1B tracking integration beyond 4 ms is unsafe without symbol synchronization")
|
||||
if self.tracking.secondary_code_mode == "none":
|
||||
raise ValueError("E1C tracking beyond 4 ms requires secondary-code handling")
|
||||
|
||||
@@ -24,6 +24,7 @@ class SignalProfile:
|
||||
modulation: str
|
||||
boc_subcarrier_rate_hz: float
|
||||
components: dict[str, ComponentProfile]
|
||||
supported_modulations: tuple[str, ...] = ("BOC11",)
|
||||
|
||||
def component(self, name: str) -> ComponentProfile:
|
||||
try:
|
||||
@@ -44,6 +45,7 @@ def galileo_e1_profile() -> SignalProfile:
|
||||
"E1B": ComponentProfile("E1B", "data", 4092, 0.004, None, 0.004),
|
||||
"E1C": ComponentProfile("E1C", "pilot", 4092, 0.004, 25, None),
|
||||
},
|
||||
supported_modulations=("BOC11", "CBOC"),
|
||||
)
|
||||
|
||||
|
||||
@@ -55,4 +57,3 @@ def get_signal_profile(name: str) -> SignalProfile:
|
||||
return PROFILES[name]()
|
||||
except KeyError as exc:
|
||||
raise ValueError(f"unknown signal profile {name!r}; available: {sorted(PROFILES)}") from exc
|
||||
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
"""Signal code, modulation, and generation."""
|
||||
|
||||
from .code_provider import CodeProvider, FileCodeProvider, SyntheticCodeProvider
|
||||
from .e1_modulation import E1Component, E1Modulation
|
||||
from .fullband_e1_generator import FullBandE1SignalGenerator, FullBandFrontendConfig
|
||||
from .signal_generator import SatelliteSignalConfig, SyntheticSignalGenerator
|
||||
|
||||
__all__ = ["CodeProvider", "FileCodeProvider", "SyntheticCodeProvider", "SatelliteSignalConfig", "SyntheticSignalGenerator"]
|
||||
|
||||
__all__ = [
|
||||
"CodeProvider",
|
||||
"E1Component",
|
||||
"E1Modulation",
|
||||
"FileCodeProvider",
|
||||
"FullBandE1SignalGenerator",
|
||||
"FullBandFrontendConfig",
|
||||
"SatelliteSignalConfig",
|
||||
"SyntheticCodeProvider",
|
||||
"SyntheticSignalGenerator",
|
||||
]
|
||||
|
||||
@@ -2,10 +2,55 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
|
||||
def square_subcarrier_from_phase(
|
||||
phase_cycles: npt.ArrayLike,
|
||||
) -> npt.NDArray[np.float64]:
|
||||
"""Return a sine-phased square subcarrier from phase in cycles.
|
||||
|
||||
The value at an exact integer-cycle boundary is defined as +1. This is
|
||||
the existing receiver convention and removes the otherwise undefined
|
||||
``sgn(sin(0))`` sample without affecting continuous-time signal power.
|
||||
"""
|
||||
phase = np.mod(np.asarray(phase_cycles, dtype=np.float64), 1.0)
|
||||
# A tiny boundary tolerance makes vector and arbitrarily chunked NCO runs
|
||||
# choose the same half-cycle when roundoff lands immediately below an
|
||||
# analytically exact transition.
|
||||
half_cycle = np.floor(2.0 * phase + 1e-12).astype(np.int64) & 1
|
||||
return np.where(half_cycle == 0, 1.0, -1.0).astype(np.float64)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SquareSubcarrierNco:
|
||||
"""Phase-continuous square-wave NCO suitable for BOC subcarriers."""
|
||||
|
||||
sampling_frequency_hz: float
|
||||
frequency_hz: float
|
||||
phase_cycles: float = 0.0
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.sampling_frequency_hz <= 0.0 or self.frequency_hz <= 0.0:
|
||||
raise ValueError("subcarrier sampling and NCO frequencies must be positive")
|
||||
self.phase_cycles %= 1.0
|
||||
|
||||
def phases(self, sample_count: int) -> npt.NDArray[np.float64]:
|
||||
"""Return current-sample phases and advance the accumulator."""
|
||||
if sample_count < 0:
|
||||
raise ValueError("sample_count must be nonnegative")
|
||||
increment = self.frequency_hz / self.sampling_frequency_hz
|
||||
phase = self.phase_cycles + np.arange(sample_count, dtype=np.float64) * increment
|
||||
self.phase_cycles = float((self.phase_cycles + sample_count * increment) % 1.0)
|
||||
return np.mod(phase, 1.0).astype(np.float64)
|
||||
|
||||
def samples(self, sample_count: int) -> npt.NDArray[np.float64]:
|
||||
return square_subcarrier_from_phase(self.phases(sample_count))
|
||||
|
||||
|
||||
def boc_subcarrier(
|
||||
sample_count: int,
|
||||
sampling_frequency_hz: float,
|
||||
@@ -16,7 +61,7 @@ def boc_subcarrier(
|
||||
if sample_count < 0 or sampling_frequency_hz <= 0 or subcarrier_frequency_hz <= 0:
|
||||
raise ValueError("invalid BOC generator parameters")
|
||||
phase = initial_phase_cycles + np.arange(sample_count, dtype=np.float64) * subcarrier_frequency_hz / sampling_frequency_hz
|
||||
wave = np.where(np.mod(phase, 1.0) < 0.5, 1.0, -1.0)
|
||||
wave = square_subcarrier_from_phase(phase)
|
||||
ending_phase = float((initial_phase_cycles + sample_count * subcarrier_frequency_hz / sampling_frequency_hz) % 1.0)
|
||||
return wave, ending_phase
|
||||
|
||||
@@ -32,7 +77,6 @@ def sample_primary_code(
|
||||
if code.ndim != 1 or code.size == 0 or sampling_frequency_hz <= 0 or code_rate_hz <= 0:
|
||||
raise ValueError("invalid code sampling parameters")
|
||||
phases = initial_code_phase_chips + np.arange(sample_count, dtype=np.float64) * code_rate_hz / sampling_frequency_hz
|
||||
values = code[np.floor(phases).astype(np.int64) % code.size].astype(np.float64)
|
||||
values = code[np.floor(phases + 1e-12).astype(np.int64) % code.size].astype(np.float64)
|
||||
ending_phase = float((initial_code_phase_chips + sample_count * code_rate_hz / sampling_frequency_hz) % code.size)
|
||||
return values, ending_phase
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Galileo E1 BOC/CBOC component-replica definitions.
|
||||
|
||||
The full-band convention follows Galileo OS SIS ICD Issue 2.2, equation (12):
|
||||
|
||||
* E1B component bracket: ``alpha * sc_1 + beta * sc_6``
|
||||
* E1C component bracket: ``alpha * sc_1 - beta * sc_6``
|
||||
|
||||
where ``alpha=sqrt(10/11)``, ``beta=sqrt(1/11)``, and each ``sc_n`` is a
|
||||
sine-phased square wave ``sgn(sin(2*pi*n*R_c*t))``. Equation (12) also has an
|
||||
outer minus sign on the transmitted E1C term. That constant sign is a carrier
|
||||
phase ambiguity for a component receiver and is deliberately not folded into
|
||||
the CBOC(-) replica definition here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from .boc import square_subcarrier_from_phase
|
||||
|
||||
|
||||
class E1Modulation(str, Enum):
|
||||
BOC11 = "BOC11"
|
||||
CBOC = "CBOC"
|
||||
|
||||
|
||||
class E1Component(str, Enum):
|
||||
E1B = "E1B"
|
||||
E1C = "E1C"
|
||||
|
||||
|
||||
CBOC_ALPHA = float(np.sqrt(10.0 / 11.0))
|
||||
CBOC_BETA = float(np.sqrt(1.0 / 11.0))
|
||||
|
||||
|
||||
def normalize_e1_modulation(value: E1Modulation | str) -> E1Modulation:
|
||||
if isinstance(value, E1Modulation):
|
||||
return value
|
||||
normalized = str(value).strip().upper().replace("(", "").replace(")", "").replace(",", "")
|
||||
aliases = {
|
||||
"BOC11": E1Modulation.BOC11,
|
||||
"BOC1 1": E1Modulation.BOC11,
|
||||
"CBOC": E1Modulation.CBOC,
|
||||
"CBOC611/11": E1Modulation.CBOC,
|
||||
}
|
||||
try:
|
||||
return aliases[normalized]
|
||||
except KeyError as exc:
|
||||
raise ValueError(f"unsupported Galileo E1 modulation {value!r}") from exc
|
||||
|
||||
|
||||
def normalize_e1_component(value: E1Component | str) -> E1Component:
|
||||
try:
|
||||
return value if isinstance(value, E1Component) else E1Component(str(value).upper())
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"unsupported Galileo E1 component {value!r}") from exc
|
||||
|
||||
|
||||
def component_cboc_boc61_sign(component: E1Component | str) -> int:
|
||||
"""Return the internal high-rate CBOC weight sign (+ for B, - for C)."""
|
||||
return 1 if normalize_e1_component(component) is E1Component.E1B else -1
|
||||
|
||||
|
||||
def e1_transmitted_component_sign(component: E1Component | str) -> int:
|
||||
"""Return equation (12)'s outer E1 component sign (+B, -C)."""
|
||||
return 1 if normalize_e1_component(component) is E1Component.E1B else -1
|
||||
|
||||
|
||||
def e1_subcarrier_replica(
|
||||
component: E1Component | str,
|
||||
modulation: E1Modulation | str,
|
||||
boc11_phase_cycles: npt.ArrayLike,
|
||||
boc61_phase_cycles: npt.ArrayLike | None = None,
|
||||
) -> npt.NDArray[np.float64]:
|
||||
"""Generate the normalized real E1 component subcarrier replica."""
|
||||
selected = normalize_e1_modulation(modulation)
|
||||
sc11 = square_subcarrier_from_phase(boc11_phase_cycles)
|
||||
if selected is E1Modulation.BOC11:
|
||||
return sc11
|
||||
if boc61_phase_cycles is None:
|
||||
raise ValueError("CBOC generation requires BOC(6,1) phase")
|
||||
sc61 = square_subcarrier_from_phase(boc61_phase_cycles)
|
||||
return (
|
||||
CBOC_ALPHA * sc11
|
||||
+ component_cboc_boc61_sign(component) * CBOC_BETA * sc61
|
||||
).astype(np.float64)
|
||||
|
||||
|
||||
def e1_transmitted_subcarrier_replica(
|
||||
component: E1Component | str,
|
||||
modulation: E1Modulation | str,
|
||||
boc11_phase_cycles: npt.ArrayLike,
|
||||
boc61_phase_cycles: npt.ArrayLike | None = None,
|
||||
) -> npt.NDArray[np.float64]:
|
||||
"""Generate a transmitted component, including the CBOC E1C outer sign.
|
||||
|
||||
BOC11 remains the historical compatibility approximation and therefore
|
||||
preserves its previous component sign. Full CBOC applies equation (12).
|
||||
"""
|
||||
selected = normalize_e1_modulation(modulation)
|
||||
replica = e1_subcarrier_replica(
|
||||
component, selected, boc11_phase_cycles, boc61_phase_cycles
|
||||
)
|
||||
if selected is E1Modulation.CBOC:
|
||||
replica = replica * e1_transmitted_component_sign(component)
|
||||
return np.asarray(replica, dtype=np.float64)
|
||||
@@ -0,0 +1,184 @@
|
||||
"""Band-limited full-band Galileo E1 BOC/CBOC test-vector generator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from scipy.signal import firwin, lfilter
|
||||
|
||||
from galileo_rx.signal_profiles import SignalProfile
|
||||
from galileo_rx.types import GenerationTruth, SatelliteTruth
|
||||
from .code_provider import CodeProvider
|
||||
from .signal_generator import (
|
||||
ChannelModelConfig,
|
||||
SatelliteSignalConfig,
|
||||
SyntheticSignalGenerator,
|
||||
quantize_normalized,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class FullBandFrontendConfig:
|
||||
"""Oversampling and realizable anti-alias/front-end filter settings."""
|
||||
|
||||
internal_oversampling: int = 2
|
||||
frontend_one_sided_bandwidth_hz: float = 12.0e6
|
||||
fir_tap_count: int = 129
|
||||
fir_taps: tuple[float, ...] | None = None
|
||||
|
||||
def validate(self, output_sampling_frequency_hz: float) -> None:
|
||||
if self.internal_oversampling < 2:
|
||||
raise ValueError("full-band generation requires internal_oversampling >= 2")
|
||||
if not 0.0 < self.frontend_one_sided_bandwidth_hz < output_sampling_frequency_hz / 2.0:
|
||||
raise ValueError("front-end one-sided bandwidth must lie below output Nyquist")
|
||||
if self.fir_taps is None and (self.fir_tap_count < 3 or self.fir_tap_count % 2 == 0):
|
||||
raise ValueError("designed full-band FIR must have an odd tap count >= 3")
|
||||
if self.fir_taps is not None and len(self.fir_taps) < 2:
|
||||
raise ValueError("explicit full-band FIR requires at least two taps")
|
||||
|
||||
|
||||
class FullBandE1SignalGenerator:
|
||||
"""Generate CBOC at a higher rate, FIR filter, and integer-decimate.
|
||||
|
||||
The FIR and decimation states persist across calls. No assumption is made
|
||||
about integral samples per BOC(6,1) sign interval. A causal FIR is used,
|
||||
so ``GenerationTruth`` includes its deterministic group delay in the
|
||||
reported code delay.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
profile: SignalProfile,
|
||||
code_provider: CodeProvider,
|
||||
sampling_frequency_hz: float = 32.736e6,
|
||||
intermediate_frequency_hz: float = 0.0,
|
||||
output_is_real_if: bool = False,
|
||||
satellites: list[SatelliteSignalConfig] | None = None,
|
||||
channel: ChannelModelConfig | None = None,
|
||||
frontend: FullBandFrontendConfig | None = None,
|
||||
) -> None:
|
||||
if sampling_frequency_hz <= 0.0:
|
||||
raise ValueError("sampling_frequency_hz must be positive")
|
||||
if abs(intermediate_frequency_hz) >= sampling_frequency_hz / 2.0:
|
||||
raise ValueError("output IF must lie strictly inside Nyquist")
|
||||
self.profile = profile
|
||||
self.code_provider = code_provider
|
||||
self.sampling_frequency_hz = float(sampling_frequency_hz)
|
||||
self.intermediate_frequency_hz = float(intermediate_frequency_hz)
|
||||
self.output_is_real_if = bool(output_is_real_if)
|
||||
self.satellites = satellites or []
|
||||
self.channel = channel or ChannelModelConfig()
|
||||
self.frontend = frontend or FullBandFrontendConfig()
|
||||
self.frontend.validate(self.sampling_frequency_hz)
|
||||
self.internal_sampling_frequency_hz = (
|
||||
self.sampling_frequency_hz * self.frontend.internal_oversampling
|
||||
)
|
||||
for satellite in self.satellites:
|
||||
satellite.validate(profile)
|
||||
if self.frontend.fir_taps is None:
|
||||
taps = firwin(
|
||||
self.frontend.fir_tap_count,
|
||||
self.frontend.frontend_one_sided_bandwidth_hz,
|
||||
fs=self.internal_sampling_frequency_hz,
|
||||
window=("kaiser", 8.0),
|
||||
)
|
||||
else:
|
||||
taps = np.asarray(self.frontend.fir_taps, dtype=np.float64)
|
||||
self.fir_taps = np.asarray(taps, dtype=np.float64)
|
||||
self._filter_state = np.zeros(self.fir_taps.size - 1, dtype=np.complex128)
|
||||
inner_channel = replace(
|
||||
self.channel,
|
||||
clip_amplitude=None,
|
||||
quantization_bits=None,
|
||||
)
|
||||
self._inner = SyntheticSignalGenerator(
|
||||
profile,
|
||||
code_provider,
|
||||
self.internal_sampling_frequency_hz,
|
||||
intermediate_frequency_hz,
|
||||
False,
|
||||
self.satellites,
|
||||
inner_channel,
|
||||
)
|
||||
self._output_sample_index = 0
|
||||
|
||||
@property
|
||||
def group_delay_output_samples(self) -> float:
|
||||
return (self.fir_taps.size - 1) / (2.0 * self.frontend.internal_oversampling)
|
||||
|
||||
@property
|
||||
def sample_index(self) -> int:
|
||||
return self._output_sample_index
|
||||
|
||||
def reset(self) -> None:
|
||||
self._inner.reset()
|
||||
self._filter_state.fill(0.0)
|
||||
self._output_sample_index = 0
|
||||
|
||||
def generate(
|
||||
self,
|
||||
sample_count: int,
|
||||
) -> tuple[npt.NDArray[np.complex128] | npt.NDArray[np.float64], GenerationTruth]:
|
||||
if sample_count < 0:
|
||||
raise ValueError("sample_count must be nonnegative")
|
||||
decimation = self.frontend.internal_oversampling
|
||||
high_rate, inner_truth = self._inner.generate(sample_count * decimation)
|
||||
filtered, self._filter_state = lfilter(
|
||||
self.fir_taps,
|
||||
np.asarray([1.0], dtype=np.float64),
|
||||
np.asarray(high_rate, dtype=np.complex128),
|
||||
zi=self._filter_state,
|
||||
)
|
||||
baseband = np.asarray(filtered[::decimation], dtype=np.complex128)
|
||||
if baseband.size != sample_count:
|
||||
raise RuntimeError("internal decimator produced an inconsistent sample count")
|
||||
result: npt.NDArray[np.complex128] | npt.NDArray[np.float64]
|
||||
if self.output_is_real_if:
|
||||
result = (np.sqrt(2.0) * baseband.real).astype(np.float64)
|
||||
else:
|
||||
result = baseband.astype(np.complex128)
|
||||
if self.channel.clip_amplitude is not None:
|
||||
limit = self.channel.clip_amplitude
|
||||
if np.iscomplexobj(result):
|
||||
result = (
|
||||
np.clip(result.real, -limit, limit)
|
||||
+ 1j * np.clip(result.imag, -limit, limit)
|
||||
).astype(np.complex128)
|
||||
else:
|
||||
result = np.clip(result, -limit, limit).astype(np.float64)
|
||||
if self.channel.quantization_bits is not None:
|
||||
result = quantize_normalized(result, self.channel.quantization_bits)
|
||||
delay_samples = self.group_delay_output_samples
|
||||
delay_chips = delay_samples * self.profile.code_rate_hz / self.sampling_frequency_hz
|
||||
satellites = [
|
||||
SatelliteTruth(
|
||||
prn=truth.prn,
|
||||
component=truth.component,
|
||||
initial_code_delay_chips=truth.initial_code_delay_chips + delay_chips,
|
||||
initial_code_delay_samples=(
|
||||
truth.initial_code_delay_chips
|
||||
* self.sampling_frequency_hz
|
||||
/ self.profile.code_rate_hz
|
||||
+ delay_samples
|
||||
),
|
||||
initial_doppler_hz=truth.initial_doppler_hz,
|
||||
doppler_rate_hz_s=truth.doppler_rate_hz_s,
|
||||
initial_carrier_phase_rad=truth.initial_carrier_phase_rad,
|
||||
amplitude=truth.amplitude,
|
||||
cn0_db_hz=truth.cn0_db_hz,
|
||||
initial_code_doppler_hz=truth.initial_code_doppler_hz,
|
||||
)
|
||||
for truth in inner_truth.satellites
|
||||
]
|
||||
truth = GenerationTruth(
|
||||
self.sampling_frequency_hz,
|
||||
self.intermediate_frequency_hz,
|
||||
self.output_is_real_if,
|
||||
self._output_sample_index,
|
||||
sample_count,
|
||||
satellites,
|
||||
)
|
||||
self._output_sample_index += sample_count
|
||||
return result, truth
|
||||
@@ -6,8 +6,9 @@ import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from galileo_rx.signal_profiles import SignalProfile
|
||||
from .boc import boc_subcarrier, sample_primary_code
|
||||
from .boc import sample_primary_code
|
||||
from .code_provider import CodeProvider
|
||||
from .e1_modulation import E1Modulation, e1_subcarrier_replica
|
||||
|
||||
|
||||
def e1_local_replica(
|
||||
@@ -20,12 +21,19 @@ def e1_local_replica(
|
||||
initial_code_phase_chips: float = 0.0,
|
||||
boc_enabled: bool = True,
|
||||
code_rate_hz: float | None = None,
|
||||
modulation: E1Modulation | str = E1Modulation.BOC11,
|
||||
) -> npt.NDArray[np.float64]:
|
||||
primary = provider.primary_code(profile, component, prn)
|
||||
effective_code_rate_hz = profile.code_rate_hz if code_rate_hz is None else code_rate_hz
|
||||
code, _ = sample_primary_code(primary, sample_count, sampling_frequency_hz, effective_code_rate_hz, initial_code_phase_chips)
|
||||
if not boc_enabled:
|
||||
return code
|
||||
rate_scale = effective_code_rate_hz / profile.code_rate_hz
|
||||
subcarrier, _ = boc_subcarrier(sample_count, sampling_frequency_hz, profile.boc_subcarrier_rate_hz * rate_scale, initial_code_phase_chips % 1.0)
|
||||
sample_indices = np.arange(sample_count, dtype=np.float64)
|
||||
code_phase_chips = initial_code_phase_chips + sample_indices * effective_code_rate_hz / sampling_frequency_hz
|
||||
subcarrier = e1_subcarrier_replica(
|
||||
component,
|
||||
modulation,
|
||||
np.mod(code_phase_chips, 1.0),
|
||||
np.mod(6.0 * code_phase_chips, 1.0),
|
||||
)
|
||||
return code * subcarrier
|
||||
|
||||
@@ -11,6 +11,11 @@ import numpy.typing as npt
|
||||
from galileo_rx.signal_profiles import SignalProfile
|
||||
from galileo_rx.types import GenerationTruth, SatelliteTruth
|
||||
from .code_provider import CodeProvider
|
||||
from .e1_modulation import (
|
||||
E1Modulation,
|
||||
e1_transmitted_subcarrier_replica,
|
||||
normalize_e1_modulation,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -26,6 +31,8 @@ class SatelliteSignalConfig:
|
||||
navigation_bits: list[int] = field(default_factory=lambda: [1])
|
||||
navigation_bit_period_s: float = 0.004
|
||||
secondary_code_phase: int = 0
|
||||
modulation: E1Modulation | str = E1Modulation.BOC11
|
||||
code_doppler_hz: float | None = None
|
||||
|
||||
def validate(self, profile: SignalProfile) -> None:
|
||||
profile.component(self.component)
|
||||
@@ -33,6 +40,7 @@ class SatelliteSignalConfig:
|
||||
raise ValueError("PRN must be positive and amplitude nonnegative")
|
||||
if not self.navigation_bits or not set(self.navigation_bits) <= {-1, 1}:
|
||||
raise ValueError("navigation_bits must be a nonempty +/-1 list")
|
||||
normalize_e1_modulation(self.modulation)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -99,19 +107,28 @@ class SyntheticSignalGenerator:
|
||||
|
||||
for satellite in self.satellites:
|
||||
primary = self.code_provider.primary_code(self.profile, satellite.component, satellite.prn)
|
||||
# Common line-of-sight dynamics scale carrier and spreading-code
|
||||
# frequencies by the same fractional Doppler. Keeping this coupling
|
||||
# makes carrier-aided code tracking physically self-consistent.
|
||||
# Common line-of-sight dynamics normally scale carrier and code by
|
||||
# the same fraction. An explicit code_doppler_hz overrides that
|
||||
# coupling for clock-error and stress-test vectors.
|
||||
code_doppler_hz = (
|
||||
satellite.doppler_hz * self.profile.code_rate_hz / self.profile.carrier_frequency_hz
|
||||
if satellite.code_doppler_hz is None
|
||||
else satellite.code_doppler_hz
|
||||
)
|
||||
code_phase_chips = (
|
||||
signal_t_s * self.profile.code_rate_hz
|
||||
+ signal_t_s * satellite.doppler_hz * self.profile.code_rate_hz / self.profile.carrier_frequency_hz
|
||||
+ signal_t_s * code_doppler_hz
|
||||
+ 0.5 * signal_t_s**2 * satellite.doppler_rate_hz_s * self.profile.code_rate_hz / self.profile.carrier_frequency_hz
|
||||
- satellite.code_delay_chips
|
||||
)
|
||||
chip_indices = np.floor(code_phase_chips).astype(np.int64)
|
||||
chip_indices = np.floor(code_phase_chips + 1e-12).astype(np.int64)
|
||||
spreading = self._sign_sequence(primary, chip_indices)
|
||||
subcarrier_phase = code_phase_chips
|
||||
subcarrier = np.where(np.mod(subcarrier_phase, 1.0) < 0.5, 1.0, -1.0)
|
||||
subcarrier = e1_transmitted_subcarrier_replica(
|
||||
satellite.component,
|
||||
satellite.modulation,
|
||||
np.mod(code_phase_chips, 1.0),
|
||||
np.mod(6.0 * code_phase_chips, 1.0),
|
||||
)
|
||||
|
||||
component_spec = self.profile.component(satellite.component)
|
||||
if component_spec.channel_type == "data":
|
||||
@@ -142,6 +159,7 @@ class SyntheticSignalGenerator:
|
||||
initial_carrier_phase_rad=satellite.carrier_phase_rad,
|
||||
amplitude=satellite.amplitude,
|
||||
cn0_db_hz=satellite.cn0_db_hz,
|
||||
initial_code_doppler_hz=code_doppler_hz,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ import numpy as np
|
||||
import numpy.typing as npt
|
||||
|
||||
from galileo_rx.signal_profiles import SignalProfile
|
||||
from galileo_rx.signals.boc import SquareSubcarrierNco
|
||||
from galileo_rx.signals.code_provider import CodeProvider
|
||||
from galileo_rx.signals.e1_modulation import E1Modulation, e1_subcarrier_replica, normalize_e1_modulation
|
||||
from galileo_rx.types import CorrelatorOutputs
|
||||
from .nco import CarrierNco, CodeNco
|
||||
|
||||
@@ -23,22 +25,74 @@ class CorrelatorBank:
|
||||
code_nco: CodeNco
|
||||
early_late_spacing_chips: float
|
||||
very_early_late_spacing_chips: float
|
||||
modulation: E1Modulation | str = E1Modulation.BOC11
|
||||
_code: npt.NDArray[np.int8] = field(init=False, repr=False)
|
||||
_boc11_nco: SquareSubcarrierNco = field(init=False, repr=False)
|
||||
_boc61_nco: SquareSubcarrierNco = field(init=False, repr=False)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self._code = self.provider.primary_code(self.profile, self.component, self.prn)
|
||||
self.modulation = normalize_e1_modulation(self.modulation)
|
||||
self._boc11_nco = SquareSubcarrierNco(
|
||||
self.code_nco.sampling_frequency_hz,
|
||||
self.code_nco.frequency_hz,
|
||||
self.code_nco.phase_chips % 1.0,
|
||||
)
|
||||
self._boc61_nco = SquareSubcarrierNco(
|
||||
self.code_nco.sampling_frequency_hz,
|
||||
6.0 * self.code_nco.frequency_hz,
|
||||
(6.0 * self.code_nco.phase_chips) % 1.0,
|
||||
)
|
||||
|
||||
def _replica(self, phases_chips: np.ndarray, offset_chips: float) -> np.ndarray:
|
||||
def _replica(
|
||||
self,
|
||||
phases_chips: npt.NDArray[np.float64],
|
||||
boc11_phase_cycles: npt.NDArray[np.float64],
|
||||
boc61_phase_cycles: npt.NDArray[np.float64],
|
||||
offset_chips: float,
|
||||
) -> npt.NDArray[np.float64]:
|
||||
tap_phase = phases_chips + offset_chips
|
||||
code = self._code[np.floor(tap_phase).astype(np.int64) % self._code.size]
|
||||
boc = np.where(np.mod(tap_phase, 1.0) < 0.5, 1.0, -1.0)
|
||||
return code * boc
|
||||
code = self._code[np.floor(tap_phase + 1e-12).astype(np.int64) % self._code.size]
|
||||
subcarrier = e1_subcarrier_replica(
|
||||
self.component,
|
||||
self.modulation,
|
||||
boc11_phase_cycles + offset_chips,
|
||||
boc61_phase_cycles + 6.0 * offset_chips,
|
||||
)
|
||||
return code.astype(np.float64) * subcarrier
|
||||
|
||||
def phase_vectors(
|
||||
self,
|
||||
sample_count: int,
|
||||
) -> tuple[
|
||||
npt.NDArray[np.float64],
|
||||
npt.NDArray[np.float64],
|
||||
npt.NDArray[np.float64],
|
||||
npt.NDArray[np.float64],
|
||||
]:
|
||||
"""Advance and return independent carrier/code/BOC11/BOC61 phases."""
|
||||
carrier_phase = self.carrier_nco.phases(sample_count)
|
||||
code_phase = self.code_nco.phases(sample_count)
|
||||
# The two subcarrier accumulators are distinct state elements, but
|
||||
# their frequencies are code-aided so loop updates preserve the CBOC
|
||||
# phase relationship without assuming integral samples/half-cycle.
|
||||
self._boc11_nco.frequency_hz = self.code_nco.frequency_hz
|
||||
self._boc61_nco.frequency_hz = 6.0 * self.code_nco.frequency_hz
|
||||
boc11_phase = self._boc11_nco.phases(sample_count)
|
||||
boc61_phase = self._boc61_nco.phases(sample_count)
|
||||
return carrier_phase, code_phase, boc11_phase, boc61_phase
|
||||
|
||||
def correlate(self, samples: npt.NDArray[np.generic], secondary_sign: int = 1) -> CorrelatorOutputs:
|
||||
array = np.asarray(samples, dtype=np.complex128)
|
||||
carrier_phase = self.carrier_nco.phases(array.size)
|
||||
code_phase = self.code_nco.phases(array.size)
|
||||
return self.correlate_with_phases(array, carrier_phase, code_phase, secondary_sign)
|
||||
carrier_phase, code_phase, boc11_phase, boc61_phase = self.phase_vectors(array.size)
|
||||
return self.correlate_with_phases(
|
||||
array,
|
||||
carrier_phase,
|
||||
code_phase,
|
||||
secondary_sign,
|
||||
boc11_phase,
|
||||
boc61_phase,
|
||||
)
|
||||
|
||||
def correlate_with_phases(
|
||||
self,
|
||||
@@ -46,6 +100,8 @@ class CorrelatorBank:
|
||||
carrier_phase_rad: npt.NDArray[np.float64],
|
||||
code_phase_chips: npt.NDArray[np.float64],
|
||||
secondary_sign: int = 1,
|
||||
boc11_phase_cycles: npt.NDArray[np.float64] | None = None,
|
||||
boc61_phase_cycles: npt.NDArray[np.float64] | None = None,
|
||||
) -> CorrelatorOutputs:
|
||||
"""Correlate using shared phases without advancing either NCO.
|
||||
|
||||
@@ -58,6 +114,16 @@ class CorrelatorBank:
|
||||
code_phase = np.asarray(code_phase_chips, dtype=np.float64)
|
||||
if carrier_phase.shape != array.shape or code_phase.shape != array.shape:
|
||||
raise ValueError("sample, carrier-phase, and code-phase vectors must have equal shapes")
|
||||
if boc11_phase_cycles is None:
|
||||
boc11_phase = np.mod(code_phase, 1.0)
|
||||
else:
|
||||
boc11_phase = np.asarray(boc11_phase_cycles, dtype=np.float64)
|
||||
if boc61_phase_cycles is None:
|
||||
boc61_phase = np.mod(6.0 * code_phase, 1.0)
|
||||
else:
|
||||
boc61_phase = np.asarray(boc61_phase_cycles, dtype=np.float64)
|
||||
if boc11_phase.shape != array.shape or boc61_phase.shape != array.shape:
|
||||
raise ValueError("sample and subcarrier-phase vectors must have equal shapes")
|
||||
wiped = array * np.exp(-1j * carrier_phase) * secondary_sign
|
||||
taps = {
|
||||
"very_early": -self.very_early_late_spacing_chips,
|
||||
@@ -66,7 +132,10 @@ class CorrelatorBank:
|
||||
"late": self.early_late_spacing_chips,
|
||||
"very_late": self.very_early_late_spacing_chips,
|
||||
}
|
||||
values = {name: complex(np.sum(wiped * self._replica(code_phase, offset))) for name, offset in taps.items()}
|
||||
values = {
|
||||
name: complex(np.sum(wiped * self._replica(code_phase, boc11_phase, boc61_phase, offset)))
|
||||
for name, offset in taps.items()
|
||||
}
|
||||
return CorrelatorOutputs(**values)
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import numpy.typing as npt
|
||||
from galileo_rx.config import TrackingConfig
|
||||
from galileo_rx.signal_profiles import SignalProfile
|
||||
from galileo_rx.signals.code_provider import CodeProvider
|
||||
from galileo_rx.signals.e1_modulation import E1Modulation
|
||||
from galileo_rx.types import AcquisitionResult, CorrelatorOutputs, TrackingMeasurement
|
||||
from .correlator import CorrelatorBank
|
||||
from .nco import CarrierNco, CodeNco
|
||||
@@ -41,6 +42,7 @@ def synchronize_e1c_for_tracking(
|
||||
*,
|
||||
search_primary_epochs: int = 100,
|
||||
residual_frequency_step_hz: float = 0.5,
|
||||
modulation: E1Modulation | str = E1Modulation.BOC11,
|
||||
) -> E1CSecondarySynchronization:
|
||||
"""Synchronize E1C secondary phase before closing the pilot loops."""
|
||||
if acquisition.component != "E1C":
|
||||
@@ -69,7 +71,10 @@ def synchronize_e1c_for_tracking(
|
||||
(-acquisition.code_phase_chips + start * profile.code_rate_hz / sampling_frequency_hz)
|
||||
% component.primary_code_length_chips,
|
||||
)
|
||||
bank = CorrelatorBank(profile, provider, "E1C", acquisition.prn, carrier, code, 0.25, 0.75)
|
||||
bank = CorrelatorBank(
|
||||
profile, provider, "E1C", acquisition.prn, carrier, code, 0.25, 0.75,
|
||||
modulation,
|
||||
)
|
||||
prompts = np.asarray([
|
||||
bank.correlate(
|
||||
np.asarray(samples)[start + index * primary_samples : start + (index + 1) * primary_samples]
|
||||
@@ -130,6 +135,7 @@ class E1CPilotAidedE1TrackingChannel(TrackingChannel):
|
||||
self.code_nco,
|
||||
config.early_late_spacing_chips,
|
||||
config.very_early_late_spacing_chips,
|
||||
config.modulation,
|
||||
)
|
||||
self.last_e1b_correlators = CorrelatorOutputs()
|
||||
|
||||
@@ -139,13 +145,12 @@ class E1CPilotAidedE1TrackingChannel(TrackingChannel):
|
||||
secondary_sign: int,
|
||||
) -> CorrelatorOutputs:
|
||||
array = np.asarray(samples, dtype=np.complex128)
|
||||
carrier_phase = self.carrier_nco.phases(array.size)
|
||||
code_phase = self.code_nco.phases(array.size)
|
||||
carrier_phase, code_phase, boc11_phase, boc61_phase = self.correlator.phase_vectors(array.size)
|
||||
pilot = self.correlator.correlate_with_phases(
|
||||
array, carrier_phase, code_phase, secondary_sign
|
||||
array, carrier_phase, code_phase, secondary_sign, boc11_phase, boc61_phase
|
||||
)
|
||||
self.last_e1b_correlators = self.e1b_correlator.correlate_with_phases(
|
||||
array, carrier_phase, code_phase, 1
|
||||
array, carrier_phase, code_phase, 1, boc11_phase, boc61_phase
|
||||
)
|
||||
return pilot
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ class TrackingChannel:
|
||||
self.correlator = CorrelatorBank(
|
||||
profile, provider, acquisition.component, acquisition.prn, self.carrier_nco, self.code_nco,
|
||||
config.early_late_spacing_chips, config.very_early_late_spacing_chips,
|
||||
config.modulation,
|
||||
)
|
||||
self.pll_filter = ThirdOrderLoopFilter(config.pll_noise_bandwidth_hz, config.damping_ratio) if config.pll_order == 3 else SecondOrderLoopFilter(config.pll_noise_bandwidth_hz, config.damping_ratio)
|
||||
self.dll_filter = SecondOrderLoopFilter(config.dll_noise_bandwidth_hz, config.damping_ratio)
|
||||
|
||||
@@ -104,6 +104,7 @@ class SatelliteTruth:
|
||||
initial_carrier_phase_rad: float
|
||||
amplitude: float
|
||||
cn0_db_hz: float | None
|
||||
initial_code_doppler_hz: float = 0.0
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
"""Full-band CBOC acquisition/tracking evidence at 32.736 Msps."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from galileo_rx.acquisition.acquisition_manager import AcquisitionManager
|
||||
from galileo_rx.config import AcquisitionConfig, TrackingConfig
|
||||
from galileo_rx.signal_profiles import galileo_e1_profile
|
||||
from galileo_rx.signals.code_provider import SyntheticCodeProvider
|
||||
from galileo_rx.signals.e1_modulation import E1Modulation
|
||||
from galileo_rx.signals.fullband_e1_generator import FullBandE1SignalGenerator
|
||||
from galileo_rx.signals.signal_generator import ChannelModelConfig, SatelliteSignalConfig
|
||||
from galileo_rx.tracking.pilot_aided_e1 import synchronize_e1c_for_tracking
|
||||
from galileo_rx.tracking.tracking_channel import TrackingChannel
|
||||
|
||||
|
||||
FS_HZ = 32.736e6
|
||||
IF_HZ = 2.25e6
|
||||
DOPPLER_HZ = 1250.0
|
||||
DELAY_CHIPS = 80.25
|
||||
|
||||
|
||||
def _run_case(transmit_modulation: E1Modulation, receiver_modulation: E1Modulation):
|
||||
profile = galileo_e1_profile()
|
||||
provider = SyntheticCodeProvider()
|
||||
generator = FullBandE1SignalGenerator(
|
||||
profile,
|
||||
provider,
|
||||
FS_HZ,
|
||||
IF_HZ,
|
||||
satellites=[
|
||||
SatelliteSignalConfig(
|
||||
1,
|
||||
"E1C",
|
||||
code_delay_chips=DELAY_CHIPS,
|
||||
doppler_hz=DOPPLER_HZ,
|
||||
carrier_phase_rad=0.3,
|
||||
cn0_db_hz=55.0,
|
||||
modulation=transmit_modulation,
|
||||
)
|
||||
],
|
||||
channel=ChannelModelConfig(seed=2026),
|
||||
)
|
||||
samples, truth = generator.generate(round(0.16 * FS_HZ))
|
||||
acquisition_config = AcquisitionConfig(
|
||||
modulation=receiver_modulation,
|
||||
doppler_min_hz=1000.0,
|
||||
doppler_max_hz=1500.0,
|
||||
doppler_step_hz=250.0,
|
||||
coherent_integration_ms=4.0,
|
||||
noncoherent_accumulations=2,
|
||||
detection_threshold=2.0,
|
||||
zero_padding_mode="none",
|
||||
parabolic_interpolation=False,
|
||||
)
|
||||
acquisition, _ = AcquisitionManager(
|
||||
profile, provider, FS_HZ, acquisition_config
|
||||
).acquire_one(samples, "E1C", 1, IF_HZ)
|
||||
synchronization = synchronize_e1c_for_tracking(
|
||||
samples,
|
||||
profile,
|
||||
provider,
|
||||
FS_HZ,
|
||||
acquisition,
|
||||
round(0.008 * FS_HZ),
|
||||
search_primary_epochs=30,
|
||||
residual_frequency_step_hz=1.0,
|
||||
modulation=receiver_modulation,
|
||||
)
|
||||
tracking = TrackingChannel(
|
||||
profile,
|
||||
provider,
|
||||
FS_HZ,
|
||||
TrackingConfig(
|
||||
modulation=receiver_modulation,
|
||||
pull_in_duration_ms=20.0,
|
||||
loss_of_lock_epochs=20,
|
||||
),
|
||||
synchronization.acquisition,
|
||||
synchronization.tracking_start_sample,
|
||||
synchronization.sync.phase,
|
||||
)
|
||||
measurements = tracking.process(samples[synchronization.tracking_start_sample :])
|
||||
return acquisition, synchronization.sync, measurements, truth
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("transmit_modulation", "receiver_modulation"),
|
||||
[
|
||||
(E1Modulation.BOC11, E1Modulation.BOC11),
|
||||
(E1Modulation.CBOC, E1Modulation.BOC11),
|
||||
(E1Modulation.CBOC, E1Modulation.CBOC),
|
||||
],
|
||||
)
|
||||
def test_fullband_modulation_matrix_acquires_and_tracks(
|
||||
transmit_modulation: E1Modulation,
|
||||
receiver_modulation: E1Modulation,
|
||||
) -> None:
|
||||
acquisition, secondary, measurements, truth = _run_case(
|
||||
transmit_modulation, receiver_modulation
|
||||
)
|
||||
expected = truth.satellites[0]
|
||||
assert acquisition.detected
|
||||
assert acquisition.raw_doppler_hz == DOPPLER_HZ
|
||||
assert abs(acquisition.code_phase_chips - expected.initial_code_delay_chips) <= 1.0 / 32.0
|
||||
assert secondary.normalized_correlation > 0.98
|
||||
assert secondary.sign_errors == 0
|
||||
assert len(measurements) >= 35
|
||||
assert measurements[-1].carrier_lock
|
||||
assert measurements[-1].code_lock
|
||||
assert not any(item.cycle_slip for item in measurements)
|
||||
assert not any(item.sample_discontinuity for item in measurements)
|
||||
tail = measurements[-10:]
|
||||
assert np.sqrt(np.mean([item.pll_error_rad**2 for item in tail])) < 0.12
|
||||
assert np.sqrt(np.mean([item.dll_error_chips**2 for item in tail])) < 0.12
|
||||
|
||||
|
||||
@pytest.mark.parametrize("intermediate_frequency_hz", [0.0, 2.25e6])
|
||||
def test_cboc_pcps_carrier_wipeoff_at_zero_and_nonzero_if(
|
||||
intermediate_frequency_hz: float,
|
||||
) -> None:
|
||||
profile = galileo_e1_profile()
|
||||
provider = SyntheticCodeProvider()
|
||||
samples, _ = FullBandE1SignalGenerator(
|
||||
profile,
|
||||
provider,
|
||||
FS_HZ,
|
||||
intermediate_frequency_hz,
|
||||
satellites=[
|
||||
SatelliteSignalConfig(
|
||||
5,
|
||||
"E1B",
|
||||
code_delay_chips=19.5,
|
||||
doppler_hz=-750.0,
|
||||
modulation=E1Modulation.CBOC,
|
||||
)
|
||||
],
|
||||
channel=ChannelModelConfig(add_awgn=False),
|
||||
).generate(round(0.008 * FS_HZ))
|
||||
result, _ = AcquisitionManager(
|
||||
profile,
|
||||
provider,
|
||||
FS_HZ,
|
||||
AcquisitionConfig(
|
||||
modulation=E1Modulation.CBOC,
|
||||
doppler_min_hz=-1000.0,
|
||||
doppler_max_hz=-500.0,
|
||||
doppler_step_hz=250.0,
|
||||
coherent_integration_ms=4.0,
|
||||
noncoherent_accumulations=2,
|
||||
detection_threshold=2.0,
|
||||
zero_padding_mode="none",
|
||||
parabolic_interpolation=False,
|
||||
),
|
||||
).acquire_one(samples, "E1B", 5, intermediate_frequency_hz)
|
||||
assert result.detected
|
||||
assert result.raw_doppler_hz == -750.0
|
||||
assert abs(result.code_phase_chips - 20.5) <= 1.0 / 32.0
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Unit evidence for phase-continuous Galileo E1 CBOC replicas."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from galileo_rx.signals.boc import SquareSubcarrierNco, boc_subcarrier
|
||||
from galileo_rx.signals.e1_modulation import (
|
||||
CBOC_ALPHA,
|
||||
CBOC_BETA,
|
||||
E1Modulation,
|
||||
e1_subcarrier_replica,
|
||||
e1_transmitted_subcarrier_replica,
|
||||
)
|
||||
|
||||
|
||||
def _phases(sample_count: int, sampling_frequency_hz: float) -> np.ndarray:
|
||||
return np.arange(sample_count, dtype=np.float64) * 1.023e6 / sampling_frequency_hz
|
||||
|
||||
|
||||
def test_boc11_subcarrier_sequence() -> None:
|
||||
wave, ending = boc_subcarrier(8, 8.0, 1.0)
|
||||
np.testing.assert_array_equal(wave, [1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0])
|
||||
assert ending == 0.0
|
||||
|
||||
|
||||
def test_boc61_subcarrier_sequence() -> None:
|
||||
wave, ending = boc_subcarrier(24, 24.0, 6.0)
|
||||
np.testing.assert_array_equal(wave, np.tile([1.0, 1.0, -1.0, -1.0], 6))
|
||||
assert ending == 0.0
|
||||
|
||||
|
||||
def test_subcarrier_phase_continuity_across_arbitrary_chunks() -> None:
|
||||
fs = 31.25e6 # deliberately nonintegral samples per BOC(6,1) sign interval
|
||||
total = 10_003
|
||||
reference = SquareSubcarrierNco(fs, 6.138e6, 0.137).samples(total)
|
||||
chunked_nco = SquareSubcarrierNco(fs, 6.138e6, 0.137)
|
||||
chunks = [1, 7, 31, 2, 997, 4093, 3, 4869]
|
||||
chunked = np.concatenate([chunked_nco.samples(count) for count in chunks])
|
||||
np.testing.assert_array_equal(chunked, reference)
|
||||
|
||||
|
||||
def test_cboc_power_normalization_and_component_sign() -> None:
|
||||
phase = _phases(4092 * 32, 32.736e6)
|
||||
sc11 = e1_subcarrier_replica("E1B", E1Modulation.BOC11, phase)
|
||||
sc61 = np.where((np.floor(2.0 * np.mod(6.0 * phase, 1.0) + 1e-12).astype(int) & 1) == 0, 1.0, -1.0)
|
||||
e1b = e1_subcarrier_replica("E1B", E1Modulation.CBOC, phase, 6.0 * phase)
|
||||
e1c = e1_subcarrier_replica("E1C", E1Modulation.CBOC, phase, 6.0 * phase)
|
||||
assert abs(float(np.mean(e1b**2)) - 1.0) < 1e-12
|
||||
assert abs(float(np.mean(e1c**2)) - 1.0) < 1e-12
|
||||
np.testing.assert_allclose((e1b + e1c) / 2.0, CBOC_ALPHA * sc11, atol=1e-15)
|
||||
np.testing.assert_allclose((e1b - e1c) / 2.0, CBOC_BETA * sc61, atol=1e-15)
|
||||
transmitted_e1b = e1_transmitted_subcarrier_replica(
|
||||
"E1B", E1Modulation.CBOC, phase, 6.0 * phase
|
||||
)
|
||||
transmitted_e1c = e1_transmitted_subcarrier_replica(
|
||||
"E1C", E1Modulation.CBOC, phase, 6.0 * phase
|
||||
)
|
||||
# Equation (12): low-rate terms oppose, high-rate terms have equal sign.
|
||||
np.testing.assert_allclose((transmitted_e1b - transmitted_e1c) / 2.0, CBOC_ALPHA * sc11, atol=1e-15)
|
||||
np.testing.assert_allclose((transmitted_e1b + transmitted_e1c) / 2.0, CBOC_BETA * sc61, atol=1e-15)
|
||||
|
||||
|
||||
def test_cboc_spectrum_contains_the_boc61_lobe() -> None:
|
||||
fs = 32.736e6
|
||||
phase = _phases(4092 * 32, fs)
|
||||
boc11 = e1_subcarrier_replica("E1B", E1Modulation.BOC11, phase)
|
||||
cboc = e1_subcarrier_replica("E1B", E1Modulation.CBOC, phase, 6.0 * phase)
|
||||
frequencies = np.fft.rfftfreq(phase.size, 1.0 / fs)
|
||||
boc_psd = np.abs(np.fft.rfft(boc11)) ** 2
|
||||
cboc_psd = np.abs(np.fft.rfft(cboc)) ** 2
|
||||
high_lobe = (frequencies > 5.8e6) & (frequencies < 6.5e6)
|
||||
fundamental = (frequencies > 0.8e6) & (frequencies < 1.25e6)
|
||||
assert cboc_psd[high_lobe].sum() > 1_000.0 * boc_psd[high_lobe].sum()
|
||||
assert cboc_psd[fundamental].sum() > cboc_psd[high_lobe].sum()
|
||||
|
||||
|
||||
def test_cboc_autocorrelation_side_structure_and_epl_symmetry() -> None:
|
||||
fs = 32.736e6
|
||||
samples_per_chip = 32
|
||||
phase = _phases(4092 * samples_per_chip, fs)
|
||||
cboc = e1_subcarrier_replica("E1B", E1Modulation.CBOC, phase, 6.0 * phase)
|
||||
|
||||
def circular_correlation(offset_samples: int) -> float:
|
||||
return float(np.mean(cboc * np.roll(cboc, offset_samples)))
|
||||
|
||||
assert circular_correlation(0) > 0.999999
|
||||
assert circular_correlation(samples_per_chip // 2) < -0.80
|
||||
for offset in (2, 4, 6, 8, 10, 12):
|
||||
assert abs(circular_correlation(offset) - circular_correlation(-offset)) < 2e-4
|
||||
# E and L at +/-0.25 chip see equal magnitude at the correct prompt.
|
||||
assert abs(abs(circular_correlation(8)) - abs(circular_correlation(-8))) < 2e-4
|
||||
|
||||
|
||||
def test_cboc_sample_rate_independence() -> None:
|
||||
powers = []
|
||||
quarter_chip_correlations = []
|
||||
for fs in (32.736e6, 30.0e6, 27.5e6):
|
||||
count = int(round(4092 * fs / 1.023e6))
|
||||
phase = _phases(count, fs)
|
||||
cboc = e1_subcarrier_replica("E1C", E1Modulation.CBOC, phase, 6.0 * phase)
|
||||
powers.append(float(np.mean(cboc**2)))
|
||||
quarter_late = e1_subcarrier_replica(
|
||||
"E1C", E1Modulation.CBOC, phase + 0.25, 6.0 * (phase + 0.25)
|
||||
)
|
||||
quarter_chip_correlations.append(float(np.mean(cboc * quarter_late)))
|
||||
assert max(abs(power - 1.0) for power in powers) < 2e-3
|
||||
assert np.ptp(quarter_chip_correlations) < 0.04
|
||||
@@ -0,0 +1,72 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from galileo_rx.signal_profiles import galileo_e1_profile
|
||||
from galileo_rx.signals.code_provider import SyntheticCodeProvider
|
||||
from galileo_rx.signals.e1_modulation import E1Modulation
|
||||
from galileo_rx.signals.fullband_e1_generator import FullBandE1SignalGenerator
|
||||
from galileo_rx.signals.signal_generator import ChannelModelConfig, SatelliteSignalConfig
|
||||
|
||||
|
||||
def _generator() -> FullBandE1SignalGenerator:
|
||||
return FullBandE1SignalGenerator(
|
||||
galileo_e1_profile(),
|
||||
SyntheticCodeProvider(),
|
||||
32.736e6,
|
||||
2.25e6,
|
||||
satellites=[
|
||||
SatelliteSignalConfig(
|
||||
3,
|
||||
"E1C",
|
||||
code_delay_chips=7.25,
|
||||
doppler_hz=1375.0,
|
||||
modulation=E1Modulation.CBOC,
|
||||
)
|
||||
],
|
||||
channel=ChannelModelConfig(add_awgn=False),
|
||||
)
|
||||
|
||||
|
||||
def test_fullband_generator_is_streaming_and_double_precision() -> None:
|
||||
one_shot, truth = _generator().generate(4099)
|
||||
chunked_generator = _generator()
|
||||
chunked = np.concatenate([chunked_generator.generate(count)[0] for count in (1, 17, 1000, 3, 3078)])
|
||||
assert one_shot.dtype == np.complex128
|
||||
np.testing.assert_allclose(chunked, one_shot, atol=2e-13, rtol=0.0)
|
||||
assert truth.satellites[0].initial_code_delay_samples == 7.25 * 32.0 + 32.0
|
||||
|
||||
|
||||
def test_fullband_generator_supports_real_if_quantization() -> None:
|
||||
generator = FullBandE1SignalGenerator(
|
||||
galileo_e1_profile(),
|
||||
SyntheticCodeProvider(),
|
||||
32.736e6,
|
||||
4.092e6,
|
||||
True,
|
||||
[SatelliteSignalConfig(1, "E1B", modulation=E1Modulation.CBOC)],
|
||||
ChannelModelConfig(add_awgn=False, quantization_bits=8),
|
||||
)
|
||||
samples, _ = generator.generate(2048)
|
||||
assert samples.dtype == np.float64
|
||||
assert np.max(samples) <= 1.0 - 1.0 / 128.0
|
||||
np.testing.assert_allclose(samples * 128.0, np.round(samples * 128.0), atol=1e-12)
|
||||
|
||||
|
||||
def test_fullband_generator_exposes_independent_code_doppler_truth() -> None:
|
||||
generator = FullBandE1SignalGenerator(
|
||||
galileo_e1_profile(),
|
||||
SyntheticCodeProvider(),
|
||||
satellites=[
|
||||
SatelliteSignalConfig(
|
||||
1,
|
||||
"E1C",
|
||||
doppler_hz=1000.0,
|
||||
code_doppler_hz=0.75,
|
||||
modulation=E1Modulation.CBOC,
|
||||
)
|
||||
],
|
||||
channel=ChannelModelConfig(add_awgn=False),
|
||||
)
|
||||
_, truth = generator.generate(128)
|
||||
assert truth.satellites[0].initial_code_doppler_hz == 0.75
|
||||
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run and report the 32.736 Msps BOC11/CBOC receiver matrix."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
from galileo_rx.acquisition.acquisition_manager import AcquisitionManager
|
||||
from galileo_rx.config import AcquisitionConfig, TrackingConfig
|
||||
from galileo_rx.signal_profiles import galileo_e1_profile
|
||||
from galileo_rx.signals.code_provider import SyntheticCodeProvider
|
||||
from galileo_rx.signals.e1_modulation import E1Modulation
|
||||
from galileo_rx.signals.fullband_e1_generator import FullBandE1SignalGenerator
|
||||
from galileo_rx.signals.signal_generator import ChannelModelConfig, SatelliteSignalConfig
|
||||
from galileo_rx.tracking.pilot_aided_e1 import synchronize_e1c_for_tracking
|
||||
from galileo_rx.tracking.tracking_channel import TrackingChannel
|
||||
|
||||
|
||||
FS_HZ = 32.736e6
|
||||
IF_HZ = 2.25e6
|
||||
DOPPLER_HZ = 1250.0
|
||||
DELAY_CHIPS = 80.25
|
||||
PRN = 1
|
||||
|
||||
|
||||
def _rms(values: list[float]) -> float:
|
||||
return float(np.sqrt(np.mean(np.square(np.asarray(values, dtype=np.float64)))))
|
||||
|
||||
|
||||
def _longest_true(values: list[bool]) -> int:
|
||||
longest = current = 0
|
||||
for value in values:
|
||||
current = current + 1 if value else 0
|
||||
longest = max(longest, current)
|
||||
return longest
|
||||
|
||||
|
||||
def run_case(transmit: E1Modulation, receive: E1Modulation, duration_s: float) -> dict[str, object]:
|
||||
profile = galileo_e1_profile()
|
||||
provider = SyntheticCodeProvider()
|
||||
started = time.perf_counter()
|
||||
generator = FullBandE1SignalGenerator(
|
||||
profile,
|
||||
provider,
|
||||
FS_HZ,
|
||||
IF_HZ,
|
||||
satellites=[
|
||||
SatelliteSignalConfig(
|
||||
PRN,
|
||||
"E1C",
|
||||
code_delay_chips=DELAY_CHIPS,
|
||||
doppler_hz=DOPPLER_HZ,
|
||||
carrier_phase_rad=0.3,
|
||||
cn0_db_hz=55.0,
|
||||
modulation=transmit,
|
||||
)
|
||||
],
|
||||
channel=ChannelModelConfig(seed=2026),
|
||||
)
|
||||
samples, truth = generator.generate(round(duration_s * FS_HZ))
|
||||
acquisition_config = AcquisitionConfig(
|
||||
modulation=receive,
|
||||
doppler_min_hz=1000.0,
|
||||
doppler_max_hz=1500.0,
|
||||
doppler_step_hz=250.0,
|
||||
coherent_integration_ms=4.0,
|
||||
noncoherent_accumulations=2,
|
||||
detection_threshold=2.0,
|
||||
zero_padding_mode="none",
|
||||
parabolic_interpolation=False,
|
||||
)
|
||||
acquisition, _ = AcquisitionManager(
|
||||
profile, provider, FS_HZ, acquisition_config
|
||||
).acquire_one(samples, "E1C", PRN, IF_HZ)
|
||||
synchronization = synchronize_e1c_for_tracking(
|
||||
samples,
|
||||
profile,
|
||||
provider,
|
||||
FS_HZ,
|
||||
acquisition,
|
||||
round(0.008 * FS_HZ),
|
||||
search_primary_epochs=min(30, int(duration_s / 0.004) - 3),
|
||||
residual_frequency_step_hz=1.0,
|
||||
modulation=receive,
|
||||
)
|
||||
channel = TrackingChannel(
|
||||
profile,
|
||||
provider,
|
||||
FS_HZ,
|
||||
TrackingConfig(
|
||||
modulation=receive,
|
||||
pull_in_duration_ms=20.0,
|
||||
loss_of_lock_epochs=20,
|
||||
),
|
||||
synchronization.acquisition,
|
||||
synchronization.tracking_start_sample,
|
||||
synchronization.sync.phase,
|
||||
)
|
||||
measurements = channel.process(samples[synchronization.tracking_start_sample :])
|
||||
tail = measurements[-min(10, len(measurements)) :]
|
||||
effective_code_rate_hz = profile.code_rate_hz * (
|
||||
1.0 + DOPPLER_HZ / profile.carrier_frequency_hz
|
||||
)
|
||||
truth_delay_chips = truth.satellites[0].initial_code_delay_chips
|
||||
phase_errors_chips = []
|
||||
for measurement in tail:
|
||||
expected_phase = (
|
||||
measurement.end_sample_timestamp / FS_HZ * effective_code_rate_hz
|
||||
- truth_delay_chips
|
||||
) % profile.component("E1C").primary_code_length_chips
|
||||
phase_errors_chips.append(
|
||||
float(
|
||||
(
|
||||
measurement.code_phase_chips
|
||||
- expected_phase
|
||||
+ profile.component("E1C").primary_code_length_chips / 2.0
|
||||
)
|
||||
% profile.component("E1C").primary_code_length_chips
|
||||
- profile.component("E1C").primary_code_length_chips / 2.0
|
||||
)
|
||||
)
|
||||
joint_lock = [item.carrier_lock and item.code_lock for item in measurements]
|
||||
cn0_values = [item.cn0_db_hz for item in tail if item.cn0_db_hz is not None]
|
||||
return {
|
||||
"case": f"generated_{transmit.value}__receiver_{receive.value}",
|
||||
"transmit_modulation": transmit.value,
|
||||
"receiver_modulation": receive.value,
|
||||
"sample_format": "complex128",
|
||||
"sampling_frequency_hz": FS_HZ,
|
||||
"internal_sampling_frequency_hz": generator.internal_sampling_frequency_hz,
|
||||
"intermediate_frequency_hz": IF_HZ,
|
||||
"frontend_one_sided_bandwidth_hz": generator.frontend.frontend_one_sided_bandwidth_hz,
|
||||
"frontend_fir_taps": int(generator.fir_taps.size),
|
||||
"filter_group_delay_samples": generator.group_delay_output_samples,
|
||||
"acquisition_detected": acquisition.detected,
|
||||
"acquisition_peak_metric": acquisition.detection_metric,
|
||||
"acquisition_peak_power": acquisition.peak_power,
|
||||
"estimated_code_delay_chips": acquisition.code_phase_chips,
|
||||
"truth_code_delay_chips_including_filter": truth_delay_chips,
|
||||
"code_delay_error_chips": acquisition.code_phase_chips - truth_delay_chips,
|
||||
"estimated_carrier_frequency_hz": acquisition.carrier_frequency_hz,
|
||||
"estimated_doppler_hz": acquisition.doppler_hz,
|
||||
"doppler_error_hz": acquisition.doppler_hz - DOPPLER_HZ,
|
||||
"secondary_code_phase": synchronization.sync.phase,
|
||||
"secondary_code_correlation": synchronization.sync.normalized_correlation,
|
||||
"secondary_code_sign_errors": synchronization.sync.sign_errors,
|
||||
"tracking_epochs": len(measurements),
|
||||
"tracking_duration_s": len(measurements) * 0.004,
|
||||
"longest_joint_lock_duration_s": _longest_true(joint_lock) * 0.004,
|
||||
"final_carrier_lock": measurements[-1].carrier_lock,
|
||||
"final_code_lock": measurements[-1].code_lock,
|
||||
"pll_error_rms_rad": _rms([item.pll_error_rad for item in tail]),
|
||||
"dll_error_rms_chips": _rms([item.dll_error_chips for item in tail]),
|
||||
"code_tracking_bias_chips": float(np.mean(phase_errors_chips)),
|
||||
"code_tracking_error_rms_chips": _rms(phase_errors_chips),
|
||||
"median_cn0_estimate_db_hz": float(np.median(cn0_values)) if cn0_values else None,
|
||||
"cycle_slip_count": sum(item.cycle_slip for item in measurements),
|
||||
"sample_discontinuity_count": sum(item.sample_discontinuity for item in measurements),
|
||||
"side_peak_suspected_epochs": sum(item.side_peak_suspected for item in measurements),
|
||||
"tail_smoothed_code_lock_metric_median": float(
|
||||
np.median([item.smoothed_code_lock_metric for item in tail])
|
||||
),
|
||||
"tail_boc_main_peak_fraction_minimum": float(
|
||||
min(item.boc_main_peak_fraction for item in tail)
|
||||
),
|
||||
"tail_boc_side_peak_metric_maximum": float(
|
||||
max(item.boc_side_peak_metric for item in tail)
|
||||
),
|
||||
"e1b_page_decode_count": None,
|
||||
"e1b_navigation_data_enabled": False,
|
||||
"processing_time_s": time.perf_counter() - started,
|
||||
}
|
||||
|
||||
|
||||
def markdown_report(report: dict[str, object]) -> str:
|
||||
rows = []
|
||||
for case in report["cases"]: # type: ignore[index]
|
||||
rows.append(
|
||||
"| {case} | {acquisition_peak_metric:.3f} | {estimated_code_delay_chips:.6f} | "
|
||||
"{estimated_doppler_hz:.1f} | {longest_joint_lock_duration_s:.3f} | "
|
||||
"{pll_error_rms_rad:.6f} | {dll_error_rms_chips:.6f} | "
|
||||
"{code_tracking_bias_chips:.6f} | {median_cn0_estimate_db_hz:.3f} | "
|
||||
"{secondary_code_correlation:.6f} |".format(**case)
|
||||
)
|
||||
return "\n".join(
|
||||
[
|
||||
"# Galileo E1 full-band CBOC validation",
|
||||
"",
|
||||
"Generated at 65.472 Msps, passed through a 129-tap Kaiser FIR, and decimated to "
|
||||
"32.736 Msps. All receiver arrays use float64/complex128.",
|
||||
"",
|
||||
"| Case | Acquisition metric | Code delay (chips) | Doppler (Hz) | Longest lock (s) | PLL RMS (rad) | DLL RMS (chips) | Code bias (chips) | C/N0 estimate | E1C secondary corr. |",
|
||||
"| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |",
|
||||
*rows,
|
||||
"",
|
||||
"E1B navigation data was not enabled in this focused E1C-pilot matrix; page counts are therefore null, not zero-result decoder failures. The independent SignalSim regression remains the E1B BOC(1,1) I/NAV proof.",
|
||||
]
|
||||
) + "\n"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--output-dir", type=Path, default=Path("reports"))
|
||||
parser.add_argument("--duration-s", type=float, default=0.30)
|
||||
arguments = parser.parse_args()
|
||||
if arguments.duration_s < 0.24:
|
||||
raise SystemExit("duration must be at least 0.24 s for lock and C/N0 settling")
|
||||
cases = [
|
||||
run_case(E1Modulation.BOC11, E1Modulation.BOC11, arguments.duration_s),
|
||||
run_case(E1Modulation.CBOC, E1Modulation.BOC11, arguments.duration_s),
|
||||
run_case(E1Modulation.CBOC, E1Modulation.CBOC, arguments.duration_s),
|
||||
]
|
||||
report: dict[str, object] = {
|
||||
"generator": "independent band-limited Python Galileo E1 generator",
|
||||
"truth_usage": "post-receiver error measurement only",
|
||||
"cases": cases,
|
||||
}
|
||||
arguments.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
(arguments.output_dir / "cboc_validation_report.json").write_text(
|
||||
json.dumps(report, indent=2) + "\n", encoding="utf-8"
|
||||
)
|
||||
(arguments.output_dir / "cboc_validation_report.md").write_text(
|
||||
markdown_report(report), encoding="utf-8"
|
||||
)
|
||||
print(json.dumps(report, indent=2))
|
||||
return 0 if all(case["acquisition_detected"] and case["final_code_lock"] for case in cases) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user