57e88b3590
- RRC_ALPHA 0.75→0.50: narrower noise BW, ~0.6 dB SNR gain - COSTAS_KI 3.5e-7: maintain ζ≈0.68 (1e-6 caused loop instability) - Soft confidence: use biphase_i.abs() instead of full vector magnitude so OSD confidence is aligned with bit-decision sign; suppresses false groups under noise with residual Costas phase error - OSD(2) in locked mode: corrects ≤2-bit errors after block sync - Search mode: hard decode only for Block A; OSD(1) in search yielded ~13% false Block A rate per bit, letting wrong clock candidates accumulate false groups as fast as the correct candidate - Incumbent candidate tracking (best_candidate_idx): the winning candidate updates best_state at equal score; challengers need strictly higher score; best_score tracks incumbent even on no-state-change groups so challengers can't leapfrog on a single false group - blocks_to_chips: add NRZI (NRZ-Mark) pre-encoding so the differential biphase decoder recovers actual data bits rather than XOR-of-pairs - Add blocks_to_chips_round_trips_all_groups test: verifies all 16 blocks across 4 PS segments round-trip correctly without BPSK modulation [fix](trx-backend-soapysdr): lower pilot lock threshold for weak-signal RDS - PILOT_LOCK_THRESHOLD 0.25→0.20, add PILOT_LOCK_ONSET=0.30 constant - Pilot reference engages at coherence ≥0.36 (was ≥0.45) WIP: end_to_end_clean_signal_decodes_ps still failing (13/15 pass). Decoder skips segment 2 due to ISI from rectangular test chips through RRC receive filter. chips_to_rds_signal needs RRC pulse shaping. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
115 lines
5.3 KiB
Markdown
115 lines
5.3 KiB
Markdown
# RDS Parameter Tuning — Work in Progress
|
||
|
||
## Goal
|
||
Maximum sensitivity (weak-signal decode) with zero false positive PI decodes.
|
||
|
||
## Changes Made
|
||
|
||
### `src/decoders/trx-rds/src/lib.rs`
|
||
|
||
#### Constants tuned
|
||
- `RRC_ALPHA = 0.50` (was 0.75) — narrower noise bandwidth, ~0.6 dB SNR gain
|
||
- `COSTAS_KI = 3.5e-7` — loop damping ζ≈0.68, well-damped (1e-6 caused instability)
|
||
- `PI_ACC_THRESHOLD = 2` — accumulate 2 Block A observations before committing PI
|
||
|
||
#### Soft confidence fix
|
||
In `Candidate::process_sample`, the soft confidence passed to `push_bit_soft` is now
|
||
`biphase_i.abs()` (was full vector magnitude). This aligns confidence with the bit
|
||
decision sign and prevents OSD(2) from false-decoding noise when the Costas loop
|
||
has residual phase error.
|
||
|
||
#### OSD(2) in locked mode (kept)
|
||
`decode_block_soft` performs OSD(2): hard decode → all 26 single-bit flips → all
|
||
325 two-bit flip pairs. Only active in locked mode; sequential B→C→D block-type
|
||
gating limits false positives.
|
||
|
||
#### Search mode: hard decode only
|
||
Removed OSD(1) from Block A acquisition (search mode). With OSD(1), ~13% of
|
||
random 26-bit words would falsely pass the Block A test per bit, allowing wrong
|
||
clock-phase candidates to accumulate false groups as fast as the correct candidate
|
||
accumulates real ones. Hard decode reduces the false Block A rate to ~0.5%.
|
||
|
||
#### Candidate selection: incumbent tracking
|
||
Added `best_candidate_idx: Option<usize>` to `RdsDecoder`. The incumbent (winning)
|
||
candidate can always update `best_state` at equal score (its `ps_seen`/`rt_seen`
|
||
arrays accumulate coherently). A challenger must achieve a strictly higher score to
|
||
take over. The incumbent's `best_score` is also updated when it returns `None`
|
||
(no state change) so challengers cannot leapfrog with a single false group.
|
||
|
||
#### Test fixes
|
||
- `blocks_to_chips`: added NRZI (NRZ-Mark) pre-encoding. The differential biphase
|
||
decoder computes `bit = input_bit XOR prev_input_bit`; without NRZI the recovered
|
||
bits were XOR-of-consecutive-bits, not the original data.
|
||
- `decode_block_soft_rejects_three_bit_error`: removed (OSD(2) legitimately finds
|
||
distance-2 codewords; `pure_noise_produces_zero_pi_decodes` is the real guard).
|
||
- New test: `blocks_to_chips_round_trips_all_groups` — verifies round-trip decode
|
||
of all 16 blocks across all 4 PS segments without BPSK modulation.
|
||
|
||
### `src/trx-server/trx-backend/trx-backend-soapysdr/src/demod/wfm.rs`
|
||
|
||
- `PILOT_LOCK_THRESHOLD = 0.20` (was 0.25) — pilot reference enabled at lower coherence
|
||
- Added `PILOT_LOCK_ONSET = 0.30` constant (was hardcoded 0.4)
|
||
- `pilot_lock` ramp: `((pilot_coherence - PILOT_LOCK_ONSET) / 0.2).clamp(0.0, 1.0)`
|
||
— pilot reference engages at coherence ≥ 0.36 instead of ≥ 0.45
|
||
|
||
## Test Status
|
||
|
||
```
|
||
cargo test -p trx-rds
|
||
```
|
||
|
||
13/15 passing:
|
||
- ✅ decode_block_recognizes_valid_offsets
|
||
- ✅ decode_block_soft_corrects_single_bit_error
|
||
- ✅ decode_block_soft_corrects_two_bit_error_osd2
|
||
- ✅ block_decode_rate_osd1_vs_osd2
|
||
- ✅ decode_block_soft_prefers_least_costly_flip
|
||
- ✅ full_group_with_two_bit_errors_in_each_locked_block
|
||
- ✅ pi_accumulation_corrects_weak_pi_after_threshold
|
||
- ✅ decoder_emits_ps_and_pty_from_group_0a
|
||
- ✅ rrc_tap_dc_gain
|
||
- ✅ pure_noise_produces_zero_pi_decodes
|
||
- ✅ end_to_end_with_pilot_reference_decodes_pi
|
||
- ✅ end_to_end_noisy_signal_snr_10db_decodes_pi
|
||
- ✅ costas_tracks_without_diverging_on_clean_signal
|
||
- ✅ blocks_to_chips_round_trips_all_groups ← new, proves chip encoding correct
|
||
- ❌ end_to_end_clean_signal_decodes_ps ← remaining failure
|
||
|
||
## Remaining Bug: `end_to_end_clean_signal_decodes_ps`
|
||
|
||
### Symptom
|
||
The decoder sees segments 0 (×8 candidates) and 1 (×1), then jumps to segment 3,
|
||
skipping segment 2. `ps_seen` never has all four flags set in the winning candidate,
|
||
so `program_service` is never assembled.
|
||
|
||
### Diagnosis (from temporary `eprintln!` in `process_group`)
|
||
```
|
||
[DBG] process_group pi=0x9801 seg=0 (×8 — all 8 clock candidates decode seg 0)
|
||
[DBG] process_group pi=0x9801 seg=1 (×1)
|
||
[DBG] process_group pi=0x9801 seg=3 (×1 — seg 2 skipped!)
|
||
[DBG] process_group pi=0x9BB2 seg=3 (false positive)
|
||
```
|
||
|
||
Segment 2 is consistently skipped. The `blocks_to_chips_round_trips_all_groups`
|
||
test confirms the chip stream is correct for all 16 blocks. The issue is therefore
|
||
in the RRC filter / symbol clock / biphase chain between seg 1 and seg 2.
|
||
|
||
### Key observation
|
||
- `blocks_to_chips_round_trips_all_groups` passes — chip encoding is correct
|
||
- The FIR block size is 256 samples, introducing a 255-sample startup delay where
|
||
the filter returns `(0.0, 0.0)` before the first batch is flushed
|
||
- The test signal uses rectangular chip pulses; the receiver RRC filter expects
|
||
RRC-shaped transmit pulses for zero ISI. Rectangular × RRC ≠ raised cosine.
|
||
|
||
### Hypothesis
|
||
A rectangular chip pulse convolved with an RRC matched filter produces ISI. Over
|
||
time this may cause the effective chip sampling point to drift, eventually missing
|
||
the correct window for Block A of segment 2. `chips_to_rds_signal` should probably
|
||
pre-shape each chip with an RRC pulse to make it a proper matched-filter test.
|
||
|
||
### Next steps
|
||
1. Fix `chips_to_rds_signal` to apply RRC pulse shaping per chip so that
|
||
RRC × RRC = raised cosine → zero ISI at the decoder's sampling instants.
|
||
2. Alternatively verify that the FIR startup zeros are not permanently skewing
|
||
the clock candidate phases.
|