Skip to content

Commit 4dc009c

Browse files
drowe67claude
andcommitted
V2 Tx: apply SSB BPF to data and EOO uniformly, default on
radae_v2.py's RADEv2Transmitter (used by tx2.py, the streaming reference rade_c ports) previously used a different, narrower carrier-bandwidth-derived filter than inference.py's actual SSB radio emulation (300-2700Hz, matching training), and never filtered the EOO frame at all. Both now use the same 300-2700Hz filter, applied continuously across data and EOO through one persistent filter state -- a real SSB radio's front end doesn't distinguish data samples from EOO ones. tx2.py's --ssb_bpf flipped to default-on (--no_bpf added as opt-out, matching the existing --no_auxdata/--no_eoo pattern), since this is the filter the model was actually trained under. inference.py's own default is left untouched; --ssb_bpf added explicitly to the v2_tx2 ctest to keep it matched now that tx2.sh defaults to filtered. Also replaces rade_c_v2_rx_eoo (which used inference.py's --end_of_over_v2 splice -- AWGN-only on the EOO tail regardless of channel flags, and never SSB-filtered) with a tx2.py -> ch -> C rx stack that genuinely fades/filters the whole continuous data+EOO stream, plus MPP variants for both tx2.py and the C Tx. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PzNQqmYXwUStAMrN4a5GFy
1 parent 758e825 commit 4dc009c

3 files changed

Lines changed: 50 additions & 18 deletions

File tree

CMakeLists.txt

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -737,7 +737,11 @@ add_test(NAME rade_c_v2_dec
737737
set_tests_properties(rade_c_v2_dec PROPERTIES PASS_REGULAR_EXPRESSION "PASS")
738738

739739
# Test V2 C transmitter: compare IQ output to tx2.py reference using RMS difference
740-
# EOO max diff threshold: 1e-4 (essentially float32 precision)
740+
# EOO max diff threshold: 0.01 -- the SSB BPF is one continuous filter spanning the
741+
# data->EOO boundary, so its memory carries the pre-existing ~1.2% data-side encoder
742+
# float difference into the start of the EOO window as a decaying transient (observed
743+
# peak ~0.006, decaying to ~1e-5 by the end of the 960-sample EOO window); a tight
744+
# near-float32-precision threshold is no longer the right expectation here.
741745
# Data diff_rms/sig_rms threshold: 3% (encoder float differences are ~1.2%)
742746
add_test(NAME rade_c_v2_tx
743747
COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \
@@ -756,7 +760,7 @@ diff_rms=np.sqrt(np.mean(np.abs(data_py-data_c)**2)); \
756760
sig_rms=np.sqrt(np.mean(np.abs(data_py)**2)); \
757761
eoo_max=np.max(np.abs(eoo_py-eoo_c)); \
758762
print(f'diff_rms/sig_rms: {diff_rms/sig_rms:.4f} eoo_max_diff: {eoo_max:.2e}'); \
759-
ok = diff_rms/sig_rms < 0.03 and eoo_max < 1e-4; \
763+
ok = diff_rms/sig_rms < 0.03 and eoo_max < 0.01; \
760764
print('PASS' if ok else 'FAIL')\"")
761765
set_tests_properties(rade_c_v2_tx PROPERTIES PASS_REGULAR_EXPRESSION "PASS")
762766

@@ -820,17 +824,38 @@ add_test(NAME rade_c_v2_rx_mpp_low
820824
python3 loss.py features_in.f32 features_rx_v2_c.f32 --loss_test 0.40 --clip_start 100 --clip_end 300")
821825
set_tests_properties(rade_c_v2_rx_mpp_low PROPERTIES PASS_REGULAR_EXPRESSION "PASS")
822826

823-
# V2 C receiver EOO detection: MPP channel at EbNodB=8 (mirrors v2_eoo_mpp_low)
827+
# V2 C receiver EOO detection: tx2.py -> ch -> C rx, AWGN channel.
824828
add_test(NAME rade_c_v2_rx_eoo
825829
COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \
826-
test/make_g.sh; \
827-
./inference.sh 250725/checkpoints/checkpoint_epoch_200.pth wav/brian_g8sez.wav /dev/null \
828-
--rate_Fs --latent-dim 56 --peak --cp 0.004 --time_offset -16 --correct_time_offset -16 \
829-
--auxdata --w1_dec 128 --end_of_over_v2 --write_rx rx_v2_eoo_test.f32 \
830-
--append_noise 1 --g_file g_mpp.f32 --EbNodB 8; \
831-
cat rx_v2_eoo_test.f32 | ${RADE_C_BUILD_DIR}/src/radae_rx --v2 -v 0 2>&1 >/dev/null")
830+
./tx2.sh 250725/checkpoints/checkpoint_epoch_200.pth wav/brian_g8sez.wav v2_eoo_tx2.f32; \
831+
cat v2_eoo_tx2.f32 | python3 f32toint16.py --real --scale 16383 | \
832+
${CMAKE_BINARY_DIR}/src/ch - - --No -24 | python3 int16tof32.py --zeropad > v2_eoo_rx2.f32; \
833+
cat v2_eoo_rx2.f32 | ${RADE_C_BUILD_DIR}/src/radae_rx --v2 -v 0 2>&1 >/dev/null")
832834
set_tests_properties(rade_c_v2_rx_eoo PROPERTIES PASS_REGULAR_EXPRESSION "End-of-over detected")
833835

836+
# V2 C receiver EOO detection: tx2.py -> ch -> C rx, MPP fading channel.
837+
# scale 8192 (rather than 16383) + No -30 gives the same ~10dB SNR3k as the AWGN
838+
# test above with zero clipping from multipath anti-fade peaks (16383 clips ~0.09%).
839+
add_test(NAME rade_c_v2_rx_eoo_mpp
840+
COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \
841+
test/make_g.sh; \
842+
./tx2.sh 250725/checkpoints/checkpoint_epoch_200.pth wav/brian_g8sez.wav v2_eoo_mpp_tx2.f32; \
843+
cat v2_eoo_mpp_tx2.f32 | python3 f32toint16.py --real --scale 8192 | \
844+
${CMAKE_BINARY_DIR}/src/ch - - --No -30 --mpp --fading_dir . | python3 int16tof32.py --zeropad > v2_eoo_mpp_rx2.f32; \
845+
cat v2_eoo_mpp_rx2.f32 | ${RADE_C_BUILD_DIR}/src/radae_rx --v2 -v 0 2>&1 >/dev/null")
846+
set_tests_properties(rade_c_v2_rx_eoo_mpp PROPERTIES PASS_REGULAR_EXPRESSION "End-of-over detected")
847+
848+
# V2 C receiver EOO detection: C tx (radae_tx) -> ch -> C rx, MPP fading channel.
849+
add_test(NAME rade_c_v2_tx_eoo_mpp
850+
COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \
851+
test/make_g.sh; \
852+
${CMAKE_CURRENT_BINARY_DIR}/src/lpcnet_demo -features wav/brian_g8sez.wav features_in.f32; \
853+
cat features_in.f32 | ${RADE_C_BUILD_DIR}/src/radae_tx --v2 > v2_eoo_ctx.f32; \
854+
cat v2_eoo_ctx.f32 | python3 f32toint16.py --real --scale 8192 | \
855+
${CMAKE_BINARY_DIR}/src/ch - - --No -30 --mpp --fading_dir . | python3 int16tof32.py --zeropad > v2_eoo_ctx_rx.f32; \
856+
cat v2_eoo_ctx_rx.f32 | ${RADE_C_BUILD_DIR}/src/radae_rx --v2 -v 0 2>&1 >/dev/null")
857+
set_tests_properties(rade_c_v2_tx_eoo_mpp PROPERTIES PASS_REGULAR_EXPRESSION "End-of-over detected")
858+
834859
# V2 C receiver SNR estimator: high SNR (EbNodB=12, mirrors v2_snr_est_high)
835860
add_test(NAME rade_c_v2_rx_snr_high
836861
COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \
@@ -1274,7 +1299,7 @@ add_test(NAME v2_tx2
12741299
./tx2.sh 250725/checkpoints/checkpoint_epoch_200.pth wav/brian_g8sez.wav v2_tx2_rx.f32; \
12751300
python3 inference.py 250725/checkpoints/checkpoint_epoch_200.pth features_in.f32 features_out.f32 \
12761301
--latent-dim 56 --w1_dec 128 --auxdata --rate_Fs --cp 0.004 \
1277-
--time_offset -16 --correct_time_offset -8 --peak; \
1302+
--time_offset -16 --correct_time_offset -8 --peak --ssb_bpf; \
12781303
./rx2.sh 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync v2_tx2_rx.f32 /dev/null --quiet; \
12791304
python3 loss.py features_in.f32 features_out.f32 --features_hat2 features_out_rx2.f32 \
12801305
--clip_start 100 --clip_end 300 --compare --delta 0.05")
@@ -1291,7 +1316,7 @@ add_test(NAME v2_tx2_ch
12911316
./rx2.sh 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync v2_tx2ch_inf_rx.f32 /dev/null \
12921317
--agc --gain 1.22E-4 --quiet; \
12931318
cp features_out_rx2.f32 features_out_rx2_inf.f32; \
1294-
./tx2.sh 250725/checkpoints/checkpoint_epoch_200.pth wav/brian_g8sez.wav v2_tx2ch_tx.f32 --ssb_bpf; \
1319+
./tx2.sh 250725/checkpoints/checkpoint_epoch_200.pth wav/brian_g8sez.wav v2_tx2ch_tx.f32; \
12951320
cat v2_tx2ch_tx.f32 | python3 f32toint16.py --real --scale 16383 | ${CMAKE_BINARY_DIR}/src/ch - - --No -20 | python3 int16tof32.py --zeropad > v2_tx2ch_rx.f32; \
12961321
./rx2.sh 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync v2_tx2ch_rx.f32 /dev/null \
12971322
--agc --gain 1.22E-4 --quiet; \

radae_v2.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -367,14 +367,17 @@ def __init__(self, model):
367367

368368
self.ssb_bpf_en = model.ssb_bpf
369369
if self.ssb_bpf_en:
370-
# Streaming complex BPF with same parameters as model
370+
# Streaming complex BPF, same 300-2700Hz SSB radio front end the model
371+
# was trained under (radae.py's ssb_bpf) -- applied to both regular
372+
# frames and the EOO sequence via the same continuous filter state,
373+
# since a real SSB radio's passband doesn't distinguish data from EOO.
371374
from radae import complex_bpf as ComplexBPF
372375
Ntap = 101
373-
w = model.w.cpu().numpy()
374376
Fs = float(model.Fs)
375-
bandwidth = 1.2 * (w[model.Nc - 1] - w[0]) * Fs / (2 * np.pi)
376-
centre = (w[model.Nc - 1] + w[0]) * Fs / (2 * np.pi) / 2
377-
frame_len = self.Ns * self.sym_len
377+
bandwidth = 2700 - 300
378+
centre = (2700 + 300) / 2
379+
eoo_len = model.eoo_v2.numel()
380+
frame_len = max(self.Ns * self.sym_len, eoo_len)
378381
self._ssb_bpf = ComplexBPF(Ntap, Fs, bandwidth, centre, frame_len)
379382

380383
def transmit_frame(self, features):
@@ -412,4 +415,7 @@ def transmit_frame(self, features):
412415

413416
def eoo(self):
414417
"""Return the V2 end-of-over sequence as complex64 IQ samples."""
415-
return self.model.eoo_v2.numpy().flatten().astype(np.csingle)
418+
eoo = self.model.eoo_v2.numpy().flatten().astype(np.csingle)
419+
if self.ssb_bpf_en:
420+
eoo = self._ssb_bpf.bpf(eoo)
421+
return eoo

tx2.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@
5454
parser.add_argument('--w2_enc', type=int, default=96, help='encoder conv output dimension (default 96)')
5555
parser.add_argument('--w1_dec', type=int, default=128, help='decoder GRU output dimension, needed for checkpoint loading (default 128)')
5656
parser.add_argument('--w2_dec', type=int, default=32, help='decoder conv output dimension, needed for checkpoint loading (default 32)')
57-
parser.add_argument('--ssb_bpf', action='store_true', help='apply SSB BPF (default off)')
5857
parser.add_argument('--write_latent', type=str, default="", help='path to output latent vectors z[latent_dim] in .f32 format')
58+
parser.set_defaults(ssb_bpf=True)
59+
parser.add_argument('--no_bpf', action='store_false', dest='ssb_bpf', help='disable SSB BPF (default enabled)')
5960
parser.set_defaults(auxdata=True)
6061
parser.add_argument('--no_auxdata', action='store_false', dest='auxdata', help='disable auxiliary data symbol (default enabled)')
6162
parser.set_defaults(end_of_over_v2=True)

0 commit comments

Comments
 (0)