Files
trx-rs/WIP.md
T
sjg 57e88b3590 [fix](trx-rds): tune RDS parameters for maximum sensitivity
- 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>
2026-03-27 01:46:01 +01:00

5.3 KiB
Raw Blame History

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.