From 4dc009c7bb36f031c06c7ba001c349107ae20984 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 5 Sep 2026 08:08:13 +0930 Subject: [PATCH 1/7] 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 Claude-Session: https://claude.ai/code/session_01PzNQqmYXwUStAMrN4a5GFy --- CMakeLists.txt | 47 ++++++++++++++++++++++++++++++++++++----------- radae_v2.py | 18 ++++++++++++------ tx2.py | 3 ++- 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 04bafa1..3117e27 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -737,7 +737,11 @@ add_test(NAME rade_c_v2_dec set_tests_properties(rade_c_v2_dec PROPERTIES PASS_REGULAR_EXPRESSION "PASS") # Test V2 C transmitter: compare IQ output to tx2.py reference using RMS difference -# EOO max diff threshold: 1e-4 (essentially float32 precision) +# EOO max diff threshold: 0.01 -- the SSB BPF is one continuous filter spanning the +# data->EOO boundary, so its memory carries the pre-existing ~1.2% data-side encoder +# float difference into the start of the EOO window as a decaying transient (observed +# peak ~0.006, decaying to ~1e-5 by the end of the 960-sample EOO window); a tight +# near-float32-precision threshold is no longer the right expectation here. # Data diff_rms/sig_rms threshold: 3% (encoder float differences are ~1.2%) add_test(NAME rade_c_v2_tx COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ @@ -756,7 +760,7 @@ diff_rms=np.sqrt(np.mean(np.abs(data_py-data_c)**2)); \ sig_rms=np.sqrt(np.mean(np.abs(data_py)**2)); \ eoo_max=np.max(np.abs(eoo_py-eoo_c)); \ print(f'diff_rms/sig_rms: {diff_rms/sig_rms:.4f} eoo_max_diff: {eoo_max:.2e}'); \ -ok = diff_rms/sig_rms < 0.03 and eoo_max < 1e-4; \ +ok = diff_rms/sig_rms < 0.03 and eoo_max < 0.01; \ print('PASS' if ok else 'FAIL')\"") set_tests_properties(rade_c_v2_tx PROPERTIES PASS_REGULAR_EXPRESSION "PASS") @@ -820,17 +824,38 @@ add_test(NAME rade_c_v2_rx_mpp_low python3 loss.py features_in.f32 features_rx_v2_c.f32 --loss_test 0.40 --clip_start 100 --clip_end 300") set_tests_properties(rade_c_v2_rx_mpp_low PROPERTIES PASS_REGULAR_EXPRESSION "PASS") -# V2 C receiver EOO detection: MPP channel at EbNodB=8 (mirrors v2_eoo_mpp_low) +# V2 C receiver EOO detection: tx2.py -> ch -> C rx, AWGN channel. add_test(NAME rade_c_v2_rx_eoo COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ - test/make_g.sh; \ - ./inference.sh 250725/checkpoints/checkpoint_epoch_200.pth wav/brian_g8sez.wav /dev/null \ - --rate_Fs --latent-dim 56 --peak --cp 0.004 --time_offset -16 --correct_time_offset -16 \ - --auxdata --w1_dec 128 --end_of_over_v2 --write_rx rx_v2_eoo_test.f32 \ - --append_noise 1 --g_file g_mpp.f32 --EbNodB 8; \ - cat rx_v2_eoo_test.f32 | ${RADE_C_BUILD_DIR}/src/radae_rx --v2 -v 0 2>&1 >/dev/null") + ./tx2.sh 250725/checkpoints/checkpoint_epoch_200.pth wav/brian_g8sez.wav v2_eoo_tx2.f32; \ + cat v2_eoo_tx2.f32 | python3 f32toint16.py --real --scale 16383 | \ + ${CMAKE_BINARY_DIR}/src/ch - - --No -24 | python3 int16tof32.py --zeropad > v2_eoo_rx2.f32; \ + cat v2_eoo_rx2.f32 | ${RADE_C_BUILD_DIR}/src/radae_rx --v2 -v 0 2>&1 >/dev/null") set_tests_properties(rade_c_v2_rx_eoo PROPERTIES PASS_REGULAR_EXPRESSION "End-of-over detected") +# V2 C receiver EOO detection: tx2.py -> ch -> C rx, MPP fading channel. +# scale 8192 (rather than 16383) + No -30 gives the same ~10dB SNR3k as the AWGN +# test above with zero clipping from multipath anti-fade peaks (16383 clips ~0.09%). +add_test(NAME rade_c_v2_rx_eoo_mpp + COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ + test/make_g.sh; \ + ./tx2.sh 250725/checkpoints/checkpoint_epoch_200.pth wav/brian_g8sez.wav v2_eoo_mpp_tx2.f32; \ + cat v2_eoo_mpp_tx2.f32 | python3 f32toint16.py --real --scale 8192 | \ + ${CMAKE_BINARY_DIR}/src/ch - - --No -30 --mpp --fading_dir . | python3 int16tof32.py --zeropad > v2_eoo_mpp_rx2.f32; \ + cat v2_eoo_mpp_rx2.f32 | ${RADE_C_BUILD_DIR}/src/radae_rx --v2 -v 0 2>&1 >/dev/null") + set_tests_properties(rade_c_v2_rx_eoo_mpp PROPERTIES PASS_REGULAR_EXPRESSION "End-of-over detected") + +# V2 C receiver EOO detection: C tx (radae_tx) -> ch -> C rx, MPP fading channel. +add_test(NAME rade_c_v2_tx_eoo_mpp + COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ + test/make_g.sh; \ + ${CMAKE_CURRENT_BINARY_DIR}/src/lpcnet_demo -features wav/brian_g8sez.wav features_in.f32; \ + cat features_in.f32 | ${RADE_C_BUILD_DIR}/src/radae_tx --v2 > v2_eoo_ctx.f32; \ + cat v2_eoo_ctx.f32 | python3 f32toint16.py --real --scale 8192 | \ + ${CMAKE_BINARY_DIR}/src/ch - - --No -30 --mpp --fading_dir . | python3 int16tof32.py --zeropad > v2_eoo_ctx_rx.f32; \ + cat v2_eoo_ctx_rx.f32 | ${RADE_C_BUILD_DIR}/src/radae_rx --v2 -v 0 2>&1 >/dev/null") + set_tests_properties(rade_c_v2_tx_eoo_mpp PROPERTIES PASS_REGULAR_EXPRESSION "End-of-over detected") + # V2 C receiver SNR estimator: high SNR (EbNodB=12, mirrors v2_snr_est_high) add_test(NAME rade_c_v2_rx_snr_high COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ @@ -1274,7 +1299,7 @@ add_test(NAME v2_tx2 ./tx2.sh 250725/checkpoints/checkpoint_epoch_200.pth wav/brian_g8sez.wav v2_tx2_rx.f32; \ python3 inference.py 250725/checkpoints/checkpoint_epoch_200.pth features_in.f32 features_out.f32 \ --latent-dim 56 --w1_dec 128 --auxdata --rate_Fs --cp 0.004 \ - --time_offset -16 --correct_time_offset -8 --peak; \ + --time_offset -16 --correct_time_offset -8 --peak --ssb_bpf; \ ./rx2.sh 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync v2_tx2_rx.f32 /dev/null --quiet; \ python3 loss.py features_in.f32 features_out.f32 --features_hat2 features_out_rx2.f32 \ --clip_start 100 --clip_end 300 --compare --delta 0.05") @@ -1291,7 +1316,7 @@ add_test(NAME v2_tx2_ch ./rx2.sh 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync v2_tx2ch_inf_rx.f32 /dev/null \ --agc --gain 1.22E-4 --quiet; \ cp features_out_rx2.f32 features_out_rx2_inf.f32; \ - ./tx2.sh 250725/checkpoints/checkpoint_epoch_200.pth wav/brian_g8sez.wav v2_tx2ch_tx.f32 --ssb_bpf; \ + ./tx2.sh 250725/checkpoints/checkpoint_epoch_200.pth wav/brian_g8sez.wav v2_tx2ch_tx.f32; \ 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; \ ./rx2.sh 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync v2_tx2ch_rx.f32 /dev/null \ --agc --gain 1.22E-4 --quiet; \ diff --git a/radae_v2.py b/radae_v2.py index 66a3711..b23bb01 100644 --- a/radae_v2.py +++ b/radae_v2.py @@ -367,14 +367,17 @@ def __init__(self, model): self.ssb_bpf_en = model.ssb_bpf if self.ssb_bpf_en: - # Streaming complex BPF with same parameters as model + # Streaming complex BPF, same 300-2700Hz SSB radio front end the model + # was trained under (radae.py's ssb_bpf) -- applied to both regular + # frames and the EOO sequence via the same continuous filter state, + # since a real SSB radio's passband doesn't distinguish data from EOO. from radae import complex_bpf as ComplexBPF Ntap = 101 - w = model.w.cpu().numpy() Fs = float(model.Fs) - bandwidth = 1.2 * (w[model.Nc - 1] - w[0]) * Fs / (2 * np.pi) - centre = (w[model.Nc - 1] + w[0]) * Fs / (2 * np.pi) / 2 - frame_len = self.Ns * self.sym_len + bandwidth = 2700 - 300 + centre = (2700 + 300) / 2 + eoo_len = model.eoo_v2.numel() + frame_len = max(self.Ns * self.sym_len, eoo_len) self._ssb_bpf = ComplexBPF(Ntap, Fs, bandwidth, centre, frame_len) def transmit_frame(self, features): @@ -412,4 +415,7 @@ def transmit_frame(self, features): def eoo(self): """Return the V2 end-of-over sequence as complex64 IQ samples.""" - return self.model.eoo_v2.numpy().flatten().astype(np.csingle) + eoo = self.model.eoo_v2.numpy().flatten().astype(np.csingle) + if self.ssb_bpf_en: + eoo = self._ssb_bpf.bpf(eoo) + return eoo diff --git a/tx2.py b/tx2.py index 68cb6b7..a1d21c4 100644 --- a/tx2.py +++ b/tx2.py @@ -54,8 +54,9 @@ parser.add_argument('--w2_enc', type=int, default=96, help='encoder conv output dimension (default 96)') parser.add_argument('--w1_dec', type=int, default=128, help='decoder GRU output dimension, needed for checkpoint loading (default 128)') parser.add_argument('--w2_dec', type=int, default=32, help='decoder conv output dimension, needed for checkpoint loading (default 32)') -parser.add_argument('--ssb_bpf', action='store_true', help='apply SSB BPF (default off)') parser.add_argument('--write_latent', type=str, default="", help='path to output latent vectors z[latent_dim] in .f32 format') +parser.set_defaults(ssb_bpf=True) +parser.add_argument('--no_bpf', action='store_false', dest='ssb_bpf', help='disable SSB BPF (default enabled)') parser.set_defaults(auxdata=True) parser.add_argument('--no_auxdata', action='store_false', dest='auxdata', help='disable auxiliary data symbol (default enabled)') parser.set_defaults(end_of_over_v2=True) From 59aa01584a6999ca90849c71b8bfac6f38b7db40 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sat, 5 Sep 2026 08:47:35 +0930 Subject: [PATCH 2/7] ota_test.sh: use tx2.py instead of inference.py for V2 Tx tx2.py is the streaming reference (what rade_c ports, what the new default-on SSB BPF applies to for both data and EOO); inference.py's V2 path never filtered EOO and used a different, non-canonical filter definition. V1 stays on inference.sh -- untouched. tx2.py is Tx-only, so runs rx2.sh separately on the clean, untransmitted output to get the genie decode reference, same pattern already used for the --v2_c (C) branch. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PzNQqmYXwUStAMrN4a5GFy --- ota_test.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ota_test.sh b/ota_test.sh index a2e455f..9a49ddc 100755 --- a/ota_test.sh +++ b/ota_test.sh @@ -424,11 +424,11 @@ if [ $v2_c -eq 1 ]; then sox ${tx_radae2}.wav -t .s16 ${tx_radae2}.raw python3 loss.py ${speechfile_no_path_no_ext}_features_in_tx2.f32 ${speechfile_no_path_no_ext}_features_out_tx2.f32 --clip_start 25 else - ./inference.sh 250725/checkpoints/checkpoint_epoch_200.pth $speechfile_pad /dev/null --rate_Fs --latent-dim 56 --peak --ssb_bpf --end_of_over_v2 \ - --cp 0.004 --time_offset -16 --correct_time_offset -16 --auxdata --w1_dec 128 --write_rx ${tx_radae2}.f32 - # save features in/out for later "loss.py" measurments + ./tx2.sh 250725/checkpoints/checkpoint_epoch_200.pth $speechfile_pad ${tx_radae2}.f32 cp features_in.f32 ${speechfile_no_path_no_ext}_features_in_tx2.f32 - cp features_out.f32 ${speechfile_no_path_no_ext}_features_out_tx2.f32 + # Run decoder to get the genie decoded features (tx2.py is Tx-only). + ./rx2.sh 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync ${tx_radae2}.f32 /dev/null --quiet + cp features_out_rx2.f32 ${speechfile_no_path_no_ext}_features_out_tx2.f32 # extract real (I) channel cat ${tx_radae2}.f32 | python3 f32toint16.py --real --scale 16383 > ${tx_radae2}.raw fi From 027f7bf51b3af4ff56e6cf9fcfc4506f10184b84 Mon Sep 17 00:00:00 2001 From: David Date: Sat, 5 Sep 2026 09:26:33 +0930 Subject: [PATCH 3/7] extended len-rade2 to make sure EOO detetced --- ota_test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ota_test.sh b/ota_test.sh index 9a49ddc..b91284d 100755 --- a/ota_test.sh +++ b/ota_test.sh @@ -160,7 +160,7 @@ function process_rx { start_rade1=$(python3 -c "start_rade1=6+${x}+0.5; print(\"%f\" % start_rade1)") len_rade=$(python3 -c "len_rade=${x}+1; print(\"%f\" % len_rade)") start_rade2=$(python3 -c "start_rade2=$start_rade1+2+${x}; print(\"%f\" % start_rade2)") - len_rade2=$(python3 -c "len_rade2=${x}+1; print(\"%f\" % len_rade2)") + len_rade2=$(python3 -c "len_rade2=${x}+1.5; print(\"%f\" % len_rade2)") rx_rade1=$(mktemp) rx_rade2=$(mktemp) sox $rx ${filename}_ssb.wav trim 5 $x From b91cb45e0295c4a788f277e9ceb97b5d2e17eae4 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 6 Sep 2026 10:04:09 +0930 Subject: [PATCH 4/7] ota_test.sh: usage 1A example for --v2_c, aligned V1/V2 loss table Adds a usage example demonstrating the C tools (--v2_c) path and the requirement to match --v2_c on both Tx and Rx to get a valid loss comparison. Also formats the V1/V2 loss output into an aligned Target/Measured table instead of raw loss.py passthrough lines. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PzNQqmYXwUStAMrN4a5GFy --- ota_test.sh | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ota_test.sh b/ota_test.sh index b91284d..b95f46a 100755 --- a/ota_test.sh +++ b/ota_test.sh @@ -28,8 +28,15 @@ # ./ota_test.sh wav/peter.wav -x # build/src/ch tx.wav - --No -20 | sox -t .s16 -r 8000 -c 1 - rx.wav # ./ota_test.sh -r rx.wav +# cat rx_report.txt # aplay rx_ssb.wav rx_radae1.wav rx_radae2.wav # +# 1A. File based I/O example and measure "loss" metric (C version of RADE V2): +# ./ota_test.sh wav/all.wav --v2_c -x +# ./ota_test.sh --v2_c -r tx.wav -l wav/all.wav +# cat tx_report.txt +# Note to correctly calculate "loss" from the C tools, you must use --v2_c at both the Tx and Rx side +# # 2. Use IC-7200 SSB radio to Tx (first generate tx.raw, then tx it on 7160 kHz) # ./ota_test.sh wav/david_vk5dgr.wav -x -d # ./ota_test.sh tx.raw -t -d -f 7160 @@ -92,7 +99,7 @@ function print_help { echo " --rms Equalise RMS power of RADAE and SSB (default is equal peak power)" echo " --tx_path optional path to tx.raw/tx.wav" echo " -t SSBRadioFile.raw Tx SSBRadioFile.raw over SSB radio (e.g. tx.wav or RADAE encoded file), no pre-processing" - echo " --v2_c Use rade_c's rade_tx_wav/rade_rx_wav (production API) for RADE V2 instead of Python" + echo " --v2_c Use rade_c's rade_tx_wav/rade_rx_wav (production C port) for RADE V2 instead of reference Python" echo exit } @@ -202,8 +209,11 @@ function process_rx { speechfile_no_path_no_ext="${loss_input_wav_file##*/}" # Removes path speechfile_no_path_no_ext="${speechfile_no_path_no_ext%.*}" # Removes extension # optional loss measurements - python3 loss.py ${speechfile_no_path_no_ext}_features_in_tx1.f32 ${speechfile_no_path_no_ext}_features_out_tx1.f32 --features_hat2 features_out_rx1.f32 --compare --clip_start 25 | sed -n '5p' | tee -a ${filename}_report.txt - python3 loss.py ${speechfile_no_path_no_ext}_features_in_tx2.f32 ${speechfile_no_path_no_ext}_features_out_tx2.f32 --features_hat2 features_out_rx2.f32 --compare --clip_start 25 | sed -n '5p' | tee -a ${filename}_report.txt + printf "%-6s%-10s%-10s\n" "" "Target" "Measured" | tee -a ${filename}_report.txt + v1_loss=$(python3 loss.py ${speechfile_no_path_no_ext}_features_in_tx1.f32 ${speechfile_no_path_no_ext}_features_out_tx1.f32 --features_hat2 features_out_rx1.f32 --compare --clip_start 25 | sed -n '5p') + printf "%-6s%-10s%-10s%s\n" "V1:" "$(echo $v1_loss | awk '{print $2}')" "$(echo $v1_loss | awk '{print $4}')" "$(echo $v1_loss | awk '{print $5, $6}')" | tee -a ${filename}_report.txt + v2_loss=$(python3 loss.py ${speechfile_no_path_no_ext}_features_in_tx2.f32 ${speechfile_no_path_no_ext}_features_out_tx2.f32 --features_hat2 features_out_rx2.f32 --compare --clip_start 25 | sed -n '5p') + printf "%-6s%-10s%-10s%s\n" "V2:" "$(echo $v2_loss | awk '{print $2}')" "$(echo $v2_loss | awk '{print $4}')" "$(echo $v2_loss | awk '{print $5, $6}')" | tee -a ${filename}_report.txt fi # filter NNPACK awarning for old machine without AVX From 0fa57aa68da0df87e6c88e21668e24bf16021400 Mon Sep 17 00:00:00 2001 From: David Rowe Date: Sun, 6 Sep 2026 10:04:34 +0930 Subject: [PATCH 5/7] Verification procedure: make rade_c the primary reference path In practice every real integration uses rade_c, not pure Python, so the verification procedure now assumes and demonstrates the rade_c library throughout rather than treating it as a secondary case: - Scope: states the rade_c integration assumption up front. - Requirements: RX-only reference transmitter is rade_tx_wav, not tx2.py. - Step 1 (baseline): uses rade_tx_wav/rade_rx_wav instead of tx2.py/rx2.py. - RX-only worked example: simplified, no longer needs the f32toint16/sox conversion dance since rade_tx_wav already outputs a WAV directly. - Cross-referenced Step 1 to the existing Worked Example section (already rade_c-based) for clip-value guidance and the --compare pass/fail pattern, rather than duplicating that content. - Refreshed all baseline/example loss numbers and the two plot PNGs against the current build (0.113/0.082 -> 0.111/0.079) -- the old values had gone stale relative to the doc's own "don't use a cached value" guidance. README.md's verification section trimmed to a short pointer at verification_procedure.md, removing a duplicated baseline command block that would otherwise need updating in two places every time the model or baseline shifts. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PzNQqmYXwUStAMrN4a5GFy --- README.md | 16 ----- doc/verification/loss_clipped.png | Bin 26912 -> 26539 bytes doc/verification/loss_unclipped.png | Bin 16667 -> 16551 bytes doc/verification/verification_procedure.md | 78 ++++++++++----------- 4 files changed, 38 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index e5e67f4..0aaa965 100644 --- a/README.md +++ b/README.md @@ -267,22 +267,6 @@ issues. The full procedure, including a checklist template for submitting results, is in [doc/verification/verification_procedure.md](doc/verification/verification_procedure.md). -A software-only loss baseline must be established using the current version -of the code under test — loss values shift slightly between model versions. -We use the reference Python implementation and `wav/all.wav` to establish -the baseline; re-run with the latest version to obtain the current baseline: -``` -lpcnet_demo -features wav/all.wav features_in.f32 -python3 tx2.py 250725/checkpoints/checkpoint_epoch_200.pth features_in.f32 tx.f32 -python3 rx2.py 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync tx.f32 features_rx.f32 --quiet -python3 loss.py features_in.f32 features_rx.f32 --clip_start 100 --clip_end 300 -``` -Example output (Python reference, `wav/all.wav`, model `250725`, commit `b549586`): -``` -loss: 0.081 start: 224 acq_time: 1.24 s -``` -Record the current baseline loss value. A pass is within ±10% of the baseline. - ## Stored File Tests The `ota_test.sh` script supports stored-file over-the-air and over-the-cable testing. It assembles a transmit file containing a chirp reference, compressed SSB, RADE V1, and RADE V2 signals in sequence, which can be sent over a real HF channel or processed through a channel simulator. The script performs a *controlled* test of SSB, RADE V1, and RADE V2 over real-world channels. diff --git a/doc/verification/loss_clipped.png b/doc/verification/loss_clipped.png index 9036ed8c6e20bd416532b9bc64c7b205093acda1..c1691e343130e67166827a1cae0bc59fe65a2d66 100644 GIT binary patch literal 26539 zcmeFZbyStz+b+5m9nvW+C0#1rf~1rp-2x)r4GR!Z=~hVvrMnyH5J~B7SadgMF5hqO z?>pc4?cdpFj6Kd@hcVuvvYz!k^SS38*LB_Zd_SnE$l+j7U_l@d90hq94G07Y27y2q zFwnqno(#>bfIoy>WVKyh*ju=`n>d+6o}0Kh*x0++SeeqfnL9aK+1owhdcwuaLHEYR z#lcyao7?t(UchDVWXU~Ya@YuNg6SZy;|zh|nIQgy=162)K_Hgo3NlhJJyLdN-93m` zr_lDjUp$b^iB+RFkoe|#y8V@iXRpp8be>w(=29akXWs=7=P8K zXJo_wNUtfW7Eh{ZdXFXG9b@vgkN>Yp@p_SZ5s^pr&UZ!ZT5j15N52|gr1@rlMv=mo zlmR~>1m9gDh`(fUF~bmlqP3w!zkmO}+xQcNkbr>T{yic|dhiouf+P<(qY5Tz|4$hZf>s6FZDh$@|m)7_}AitqhN%4<>r0+hIDap!NSS;N0@k)00r99 z(mLp}4^`>?PPVPXSQgmdPV0>d~ip?u5C9IA0NYwzgb9da}-)pocz zG#@R{87aYI!A+#Y$jQlRq4mEGAbO~h@jRJdKamjO=`nBF*x10uz+!>#-k}t>4N-@6 z1b+O8^)mlegn|-&o>p<>#DsQ=uxoFXXwUs|E_!+m>NrZQCq-s+j+^-Z87cQsq*`*+ zGu4U>#Xc!BZ_hUy!J}J;Pj{w=a$h`b@9Z?)82qXmYNq`+j$Ticm>?%|>ViVyq5HhN zm$gx4d3ji*VX~E*I_b;g@6Ea*7f+|{5hJIzFj`xaJ-?;p;vX! z$p7Za@bIwB^7ce2a;k`XXLh>ZM`Uzd7dP9q z?anvTGRI*)R82#HAXe+THTsjx$S1(MxRLbW;GjyxFx`7s=&{u>3c^AW0(0;ClLe-G zg%UrDY)ZOZg!gV+3xI6fzG4jooTYo z&CQ8A*L>TVp16n030j6y18HJId7A9u_aC9T&v`x^D>7iuzLXF2^J~HqL%RfGJ5?cN zXU9e3wedjT^^fMpNH!h^DfSnB+mBJ=ml!w{k6R+CMR?whvLL+WY}TEYS@{!;K`t_* zriNT8i3=4nn)?DB>~&?qCq*Ttn9rX-bcB&*`rTZ0BI-$)|Ef@Wxig~F zZAW*;b3IkiW1nuS!YXJeQ!W$4Y;A2V2mv%)TJdmTr(Q#g0kJcxIiI8F6*iNEU}FO4 zFyvKL<%$g(@hK_89;SI96*u1s3%hQu@Y^XWVjUhG4OQ9dDW{85w}s-#?Mzk9hKPWS zH&&n%B<_3Z5wW_|5%y-jsd0Ov=RTr54J%u*F38<}&FM=Dpm?%^qgU@{JDR77u#CKl z$}a$6QZcV%2n4YT;8|_oWWxrtRcR4H2^L?)r(5wRfLg?DAu~$6)%R>R z6s-N5*U1(OH@BRV6CdQ zC@^6M*w5&sq)_j@#_`0AN}EZI(_@J7w#JL`AmooM?xlHcvA(vnT#gjkZD~B4gTD;no~?IxhgZGs zA=G#7fAaq0$JVb;Ly`Q-`mzO+qs7OQKE+!bKZMm zE*panGOP&xZjzdG-~^%jHo3#Ww~6QdFIoc1sz!Y;R+HWgXMK3le17f$X_2G#eRr_j zWd?S7V<;0Hl(=C3+oSY}TIcNXvO>&wI*ckii}Fd^nyo+iB20RuVW?n>C(14HK+aDM zsZ3k*y*}s~YWDZ{0r&0x{rj1P1+zu}mte~@&u3#kSrzQo>hOURPL;^6_vUCql@t`Dbag2}j^(i$W^6c{^DZNj)FR{3 zZ()cM4P<@sBO@aN0`fZD>F#V8umJkYw1g)YoZQ^iuG0;kk9M1`?0%&P5oJD$z{>Mk zGH3yH-!C5&dPIODta@>{il_si7KM5qt(ky-LSSQsdS)O)P1HCtMAC?T1R0Z{-fhQ; zEgM`R=&%6syI3V7CL#H0!=DL~H2?xXL3(fm6#-%4Pfx592R;?mu2G1dF9acK03t*| zVKp8vGWaCq{9{LD-f_9p1Y`qxl5Ggi)ZEkL|Is9GFWdmO&P4q)S0f*?x=!=a%`DBRdA*p-|{Fw`pHi412v9Pez z)YM?W>rtrchj(-D^3xAP&sCC!<6))lU%zCoPIvw~#_h%Wo1t&05MCh)ejD-- zJlaS~`v!vbfpqt`I4CegF0!4jk_9Gl{G+L50>1S=BBBGG)(4jEe|z7c&gJK?&yNna z@{4;vT7d;3-Z{Z^vYHbndI92nLXH>SpLxzOCBriiG;#vic>~47BioV(~F7 zEauav_c<(8Lnv({b!goZesX+82Ed>GgC2A*fDKeDw|L$o(WO>O9}Ec!Gg01f+tm%$en!~cT3NW6>;wPoS8zZYvn zobW#S{{1_JoOI0Zy2u0G7sLOcMrL7Q3Q#$uOiUg?=pHIx8|CF75ghi0LM4JPE zI!W+S>B!`CQuu1Rx*uCxrGcY%?KE~X{sRsbRaDR*dW0Ad|7)l=C8}!5AU&HeJ_=N; zNMGt?>yOD&aEw&$i|j~S0|s{lW|G$gkBFdnBHb2(W5S;dg*dN4C5vGE3hc!8yrA*@** z0nmTPoamXMzxw+-GNghA0f+?A5i`QPznW6vprdkUOnqZiGVo3m4XE|5M<6)_LrG4+C6d@ z6dA3X#_xBEkjlK}uTKFW|1BR4NOXb3RP5k_xc4OiFDLA0nKlQ4U4WF6?aG4q^#c;b zuR(nzRa8`TnD;&Nr25g&K()HMTD1rG0;K(6wO+afcgNJgG*1@pKLp_^p6dgQp^_>E za?jMkrv~KEByJ)}NDHW;ysld>3yhj+a47kY1@%G7{_#9{ZA?r=gc6Vp@$9&`IHH#+ zSycaVo4&g4fj!&|Ob8u2yHdH}ap1JE8zd~@(eoWkrc=aD#8BRiX? zjZl>!(SxwFg-;7-hSu=}E-jfpelrLm<2F3@x{iDuVh07)mFZa|)rSOjtxM%w2%We% z4M2)^vu0*y=C(Obp_zmKal?v&FqTmE;aYKrdWCon4h;!mRpvZc3?qF4+TuyF&Y5itJ(aK43ZoKg22ll%nN%&5u_vcf$1*N zT%)%@QS+_WG=n~ZGu)hvFMm%mM1riWuQPCS5Aa_DprfXv^8ta*{^pHRv#z&<5(qEU zrT>At4-j^6b>ICYPQA5b-=nquuQ-6=zn9MjpCP1x_bdO7`}coV)|Hcd&GDCK{GZeX z`VTZ6WVZeW@BazeLEhPi2qUHr5u^VLRYTr6FvAr^rnB|G2p}bR>f0C^6Dfb#XF z<15(=mvnwb#Y;H;W6KE?e26kvR2mtIR9XF>EQPq}m>82QLw)`5_;}*1A3y$9K9^q> zkDQ#G28)d(dJ<(d@Zi!C+GLksL+i9_`KC@i7g_?ov}d4$MKhG7{d~mN#tZ|x0t*X^ z!`3Tp?f0NCj}{vlIi&zZ+TPVw?-~^mkx==ympW29>K|cbu#D^5&IRo_`hNs-9rlC~ zNXQP>rUL+S0_ad{3Sv`T25jE`;c6cuxKH=y@g#kI_1)fF#Uv%6ON#o6fWV&Kx3aPl z1Pl}rwkzEP96Dtvd(C&^0I+4qo+BI}@584TEr>0%t)8{GvTJ;`w-`#>S!UiBot|#5 zga*x!4I}OD?QK0jSmw2#6Gbo)1j3?)K9$J|xHAlVF2nW#tO7)Mo@US6j!42XGKp<)o74->tFa7S{JMcf;-sArm_Atv@8 zFyqs^{zY;BCw0ZgU0Od0{$+e-2}unKBhj1v*DNRR+<^H`-r#>DsXgvWmV7_j@n3Sr ze@QPNa!?!qCqe+uJk9(NyV*Ju_*kJ~=pDd)gWr|N00Km)SRThia*nsxo(bPT2(vd~ zMamlx#6b|H3n1sVc-9vs_VeP$yR($YX!j%m&sf<<(AW0_4EqyiQHwx^Y3-u*4>zcH zTLkPzOUMbaudcGnNP^y{Hln8!W*!Ju1h6Q8uIQ|+tlA~U0W16S{&!*kOh@vXbt1f| z=3*`RU?on~6!9oAF6WnGFpyr;R{UfEe>90{{go?asO_xdxCF|05bpw7;3y z#_=2qgUIAT;+n@$fP{VgsHUkIoR!6xy$f;&h{6B53bl@TJ4 zHju)W9fd!C{``sG_KV)x_O=Z$=6N{CC0*FtSq`GPe?v|_&~GFm)zgo(}RF!00dIDq{L`|RP(cL5KnBh<6*kYv^XA> zpjsUHu_T_W3Ch?bk7uL=n8tt!;!{)aj7@22Yj2FPy-5Hfi(#Xe$x{0V2n>Ss zcR4LJ=PeS+xhN7nz2@I2la)cjfZV@-ALt9bi5xnCKn60tK0oO0>w5~`9CC2H1DwpJ z4Ugh61^}Qx15vRZuFoB9=9{Pyw!#sZfptbuT0fygq^bvG1lcgC@%jFup#QDt57qR@ z>uVpE?Fj;q#ovq-+=tLfNX&13h9RT@E`z#vK%QQjv`m*x6?S#F+Nm~M`}JA39|-|U z*g%;?fgK;aWMyY7or2={S+5!}c(ei0Bjxv*z_*P&R&iqL=+}sOz~uxze>)ccHV*&a zg?5(E-w^z-!EY7m4kh}R+2emuqW|ca0Ko8Hl4JN9<^mu@2$5*|jMncQiiqd>y{3GV zmk5)^C>^2f2cXlm0Kf}@prWDzfe2i+h_0L_dLIb({4 zsCYo7Y6mLbJ}3sJAg52E$$`5O2Rv4V^jkbyT3&`i5MnJF9?d<-oADxQAgEQ|ivUQD zh2AO+;$ z7Rz*B3y~+v;9>8Bu>)`@D+t+8?GY=>ikL7PFpd)bueerAO5Gb5AP9Ck~1hs>r zP%6IH7_SPnp%8?|>#!UlUu1|8v96hn2WZTHj&5nQIWTt64-l-WWoLO;C6MbZ$*W9dqX~OAvEcGmD?A zbkx;yeY5A^)h{p~4nk)75d@lHKi^cdp62~HJREEX3m@1uBuSt_#>T}Vd+xsh7X1zOQNU#mx3JUHOH=ceOdq7B_PQK(CERzNv%VpF=1r&wOg`YvT zvk09YBtkkdvDANHLhEA*RG-CGR3M)HF7yV%kpuV#4m~{^%#^?Tyiz>pqyH)lh2uGR<@ zudIx3@+188g+UflsF^GmXF@{EMhVc5+U0BH#{N5sz; zkb%ri3G<-1*z>;=0q-3O$Zvf_94LdJMOo{LfYH*@fdl6N#jpHA%cXnl%8LR4B%()T z;-+OsS_07Nk?dmR!Fv$mX}{B5Ta*Pf*}j+9)nS8dh+ZK|zf~ecH;&8xMlPKEL4E;H zKj?&orwS{JOG*X{H3#!RcKWM2Q|(MwhtgpPIfWBRiBf`a^ByP!*xQ(9me|Z9h$VJh}zI;gx+BR%LDxlp5 zlKO02S7&GMl-erd@h%1_B9?a?7|dhkv^54BfHV=TI>^>(@U{z5EwQ@ms z5n(_O!u-Kfhj!U(%t$JsK%fKzRR9k#4@mo%NEc}bI*kVeC_i1G?eP&b6Bq;p$N=pf6;Cwjn@((&ZLQPndwV4Uj`Q zju8+BS5{X3w)_B^RB!h4opGP{^#CcU@(U`K*}Q)-;Mt22dpk@TTJ4N5NOW3SBq;Yi zetn6Y&T!BL6ES?F1S(nx5pOmOF>WAFgJ>mZ)v5thZcp~$5GaQ+tsa9Q)EfVGLB1?M zr~5}MfD)lB?3-l7+BY%KW3hu0&SH#B3ZWK;++8*6$}LTIgte^p>B>pHHO;4wu_QPl zC#TmK|Mhw4PF3O$Kdw=4FHG3AeLz$5w|#0xcL&_@gXpqdw7iMv#>T+r$mxJZxtrtp zVX;4S+yI@J#EKe|Kvo#BG~t*S==C`Rk6FlA48D36SQNp|FjK}iX*df#S#c>=#Gz9m zgBtBP6N1@fy>~sJkN$-5*uKb>QRa5h97Tp0Aq|@P-Zh>}NEl5Ef>%BU6BaS86JAeO zB>pEFak2P`RLDU0O}+~ynx2%K4Nr{AOatqmoBrn*1~CD@EfOi`urSuQ{T4ua028gb zaXawuij*uioa&U$a<>6X`2OdKY{OBsyxv3Ax75&>TR^zJ!46t|H9p3|3H{#@Ty!ER z4Lu=|6#W2Ted;X}NaPp4EHcV8;4If|?NQwV;Jt}?Xx=dBnKKR6@D7%lZ@|C631jMj zVj{}{suFR^NOr(C`U7X3`)ZAKRR{PnP7|PI1zlz}US7bX{MP+fXkpC#yb$W8t8Dxs z$v?eTICHims?TGhb6kB?FZl&1G@d9P zcfRI*s|GEzs+_jYa%HMTVvAm;6Ds_DK z0*OSWl+X$cIH;3P*O9#WgEdj|^Q=U9tlLQ6@;$v4kZ)$Z_T2q;(2*_5uHJ4N-%AqfOUc=CB}G5 z8g}DsmFwn+4<(5IHym{QfXlD}u!Cm^timR%m=&Q$WjyIcIXUWsUqK)vMCC{wCq>UG z^KV(`_(7V_p@Qgs3o_`Igeu0wl6vfabEq~&Ein%pQ-2rgFfsQT5<5n}MA7muNNC%# z2cp{w)Xe?#yKD5~rpvs6e?vVP^7B3-;p0e7n!nUE4|F9FNbcwPEUiOUTBRCD*J4xj zvc_kAB-8z zNlNCs3YfS{)Wj^;ku?AAORJ`C;`TORBrs*QXE|87#)p9s%E87a4F(%x^^0G3Hs4;dr}^J{+RWCG@bVs#KK<{qch^8qPs|hQ%!|!zQi$+h{u;<;eRKH=^jqsZ zPAA0#ow8_Ppf6$M_j$GANUMn}WI`M>5dYW*h0nlfgoAYxJL5-z$*RUT zHOLSEZ=b!)|1n-~nFT+(+M|Tl{4A^=)`u^(2ET<|PG9~hxL+4!VQ6Rx;e?<(ids4p zaaf=$D?2Rl_cw$1BSR1kZb!!{{`$}T>JT(2gx|Ke!@k+zRFETz?5k#P7!Lyjw1Tfz zKbjFEP)`7olb6?uj3){MMSby$by1AL?Z2+C%O3x&zFNM_R+)TM9)OIFra3nFb?MM| z?K9(yOI+hsO8_zf$#)flPs%?s)*0}de(03{@O9Q=Yod69#0~aqY20Wh{pxHl3UqFS zg=7V}RLdtBCkRG=vf3||<`r#qDApdq}dwbpUhc2!r!&zy* z&qofbqcskMs`Aiue^5*Zn;k42|ML<=p4*S$tUA3gH7H9W37XKZ*+H%-a_{Qm zmzDF53hfFTTq=u*PL^C+S{f|W6R2iBN=czQ-kuz>J#TN90dp$&z$(=@pttrFG&m8O zq+P?ovr6e~{3YrPUh+9 zDX*l2OqbRKWCadD%mP3g9Z>ustLqRPN<@RpZuZ0b@Q+aenACHq%DO-Rb@=ys!1^O7 zt5dG$K2|mIYLo<$TolsXk%X?CbQVWrBi3(TG-g;<<$DDP6i^OoFg+7{1KOW!t}}qH zm~M^c_M*#X5v8dx=o-PwUD-juG%q;wz8E9Y?eG@y{-c2jiBPZF&Be#nUs@f-vbqTg z#vvLWFP3pg-tOA8ok*UJsNO~q6&)!a`dsFY5uh+r#j-bbC0BsLM;}Y~CMo4z+fO;g znoe63FZl}JLRv5XBCc_JM1kqSuQ@v7j5f~-ZEwdoZb~`ZLtk2gvA#g!Zo`<)Ay|uP zMJJLRPNB$#{mB|n^F_FvUpEoNseELm>-Ate|f(zCpet&*C~;C0Bs+o8$p8PjDj`k4fYV zh$kLstJ%FS*2lC}_9C_#*G1cE!wdL`RhUVaiJkrK3q28$B$l`3z@@--m2HH2y&K1z z7yILbclnaI@{1n$zybIkRVFvlLF^l#Hd9#!_X04w>c$>5at{DWfXe{pyj~IcP8t+A zE0u^>j9sZa+mo|U$Z}_}pKewJnIS~C66tq(((YXc*;eQBn;Wl%yIVW}P%f6=nJMMZ zZJ5-LKE3B8c0vrM4|qJw#$wcRc%WG&>zfPRK^7aEuJ@ZyDIBf^c`PHxVroPjTkjmH zl8y$ywtOV{7|ft2$i$sR#bC%~MvT}dBry*ij+UuPNyD_VgEv35%1A$;dx(igGO%?w z?p~g>wBfN6VGnulL5rJrYyBB_$>iJXVXWK1WvC9TiVI&u+XrR{VhNFSxK9R$DR$w_ zG)v=8?-f0ZAiTS~*^`8i0W`H`iVaYPs)(MyTkE`EzA{IQBHjmu8q6%z=)Oq_FXamT zP0miGq^x@9*)V-juU%zjmryUc8?)vfBhOA!SgmJQBgYaR3#$H1LmK-yWBxe_z2Wd< zP3nvD{fgp|Wqz4nj1dEZ;BtX^V(vlzRgwem5{1O(Y*fl94UFeDu@F7ch-~7lT<0*Q^ zi-&~_n4De0noGnECJ%V*Y4*pBq=IGH5Ra`r)y;6b9}K!b<_ zUB!qx(KxH^c{!eTqwY0rudgN}^QikArs&P%{f$fz^G=9WP>bccqV$Hn3%Z;r1!|KO zb#R5`{A_+#kWCG+&vMx@$Z zK*HCqpd<~Hqrd&R4IJIQED0=~Gg zS$ycUxtqj&MKhj$Qj&vlex!QQ*0*Z=IrOP7iWipP%T|-mR;&z9uO+M#s~ly3BxT zn}#Iru5v-v8OIV!C@B{Sf2kf*W-iyuhlSc@?tEI{=GdJOXC@s%(d18sj|zbN{lFt` zg9|wYd(4H;kCtS6BPdV_$URqf)Wy;9df6E?2$s%_7W89dM%~nrJd#_j#^OdL9dQL`ng{#UW#fa zr)USKow=zKVz4+vh(xLhHKf?T$U>EvNURKkLUC5JJg!0h4JGo_% zkSyI5H)+@AdWr1qOOP%=%#P@yAz-*asLbO};IqX15W` z=Mwsx4?bg`ndbmZ2Pqe-;gdF4XJTrp-wP?a7MQ%D=0^9d9&JC#NLs7vi-MG&_+TabV4dr}d9c!~Cqw0SrQ+~$o#>6NY?X$opHF4e z)kmh$zT;GP7$%XlKx3=4c^|_nAe2Gy5xML6YHpCrzJw6+GN4JPBAt)0o;6fJ=$ZIZ@4B$>jQFNY7{YG09r#hK)^l` z+y>)2h!#0wNEk@hGEp>Qh-qh%OeRSm+Vwb9G_8mya;O$}E)tv^4` zQY*4upW1T2z)$@ zTXH;EA#SFAX9iO_ZToiN4>}D5?Vj41@%>+{3wIZ^#w+s1?HB`XDdL55*YB2K^a7{z z{^%CJkA0`uqsyL3;lpmO0?PPS`x5g)GX5~B0S`wC-@gz^QW1;CC8q{@>1~-v2g%oB zzk-+>fv!XMFBFSkCGPooNUw@e_-!D5H-E%B)i9Z%*K3pqcb4f2XGW#cK7uDX>H!^V z5ox%OAr1p#Ek^^$PeV`)Lt@cUa{KTNeMu%N6t0_0QNb*bgG+-=0EMCXh%- zLT^DKz2wpo;`KNj(;J?>nZ!d&9)&YOHQuK7_LJQCpCs_Ds_)a&Lkqx33DD(@-YTnB z`A2zvonQjaQ)~`1jpl1{6Ft+wf{nDCYz5H({@sqz(160|gJ!8GG&fD-hE)fdV)i>ECe{QW9 z6y;n}$z0DDhsR%@+{Y!U)Pp2UFkjd1YVBcWyu6sMM$VLDcb@!6_rB1Vy8+)vhnRxk zfcz7fFL|k{`GA$RC)Zm;gAfdbzE0q^9^;6Qk6#ey-iu=cW89vewL9Ji{^=#wpeKEL zII4=!wpmt(nRpMb7J{@P;N*nRS#b_ey=cbcu~P-MroE;xBz#-Akv2CP$!}-98oKcu(`13a)*Rjh+2LrB~RhY`yZpBPoY8e>u`H z1t*H4qeUxWb#-3Avs%V+HmwA?ihFw%KlLM_{2Vz>NlLZmu>1`vZXk>}K4x>-ToQF1 zFu#l(8F4Fh`i|L`B)1qPq3ocqit-c5s99&n{KH7OcAhc6-hyycps`UkWFg5&A1*Y- z&uLzz=PjC$fTTZ8DTQy$uhsg(_tmSqVrvo-(?;PhkGg28B}$e41j8?Pn)V#R=RNE0 zb}r{VV_)fRHS7u|F7EN&!MTkGXuKOVHBB2uxCF{4!}HkAR$0UfwLjSXhG!HX^!Cmq zC7nN`-@8Kf7P#``d)$S4b%+k@ReWUEG)a-{D`8bCXwN=3xo$hoh3&RSn)IiS+k2YU zYpbaJcIeTQ$SyB30?dqrp4EpDh^9bRfqpIFi)<i=0tDw)L7E!02!c3*$!0VGw#p9wewQCH0`Jteu&`u}kc{`6xQftZRFOi$xwX zIFOqH4h0cN3REA?d-dI;`)%%(MlCT1m9*|LIoq|x$Os*{*ngbLuC)tMd-{qhO+mHI z(Gww|`ClK9eGI#5shdg7UiUE^dy>fd!VAn~P#A9xvw+#nx!m-1zSR!Q8hbK42@c;h zL5Vvm*bl=Wi`rG%ojq~c(Fv~jMMoY7ro3fkRTE8jm)3fa9UJrOi}ilUmQp7rx#K0E z-4oS&cn|@>lfpd0hN4GuQDdkMVIzXm)&5aoWCJ3uBCdIqR7o+-r6OY;X=1N$~{STdAp4jdmblIlX(Sqp*9i1-6 zg_b{%B6Nrk=*J6slziQ(U#^%{C=Ln^Zbp9dnzFb?5ck6elGx?dHM^&VX-9{b@M1v- zvX8Q!%Y_rKkFB0uVISTOV3R&5wvn)_I$+LbT8R^x6dd1}9KRxF2m}5M(qo z=!5fNq@e%1JnH-F6K^YL^~{p%jAs5TOwg($YdT-TP9I@qXD0%(gqp*df&$@i<_M0aQEtq6`efKFXQsU z2E?7y)`|?$x|)}9&+0xzRMy~V8SZ+fk_!$RKC`k4-d~RT-KKOCL2JZXt8dXy2=^VL zzYMJZ93cperIypsw7T(p zvg_NSX(M&@7>HyI@MyIQIl-{^q%Hrd>($<;xegAuAi7MYPOFND(OmItB3jxgFzHgg zTUuKBJ|aQf9KYM)D=`rfw(5hL&ct*)pSwnvyNwOU>-+P7x2-*eFU%hwR!uE33bcs7zX~sRKua zgbm=|SNPdGm*-|pd264qFZ|S=W}-@awf-~6?ABX%NVmxuh^6i`avdECCvh~YZ{zrr zca9h_PA6`!M{a(O7xi5?U73PW{uUrRL`m!#p3?e53{}SHgmnwt$9Dv#d^Pv2@5{28LIf zLUG>BtkRvukbkAeXu*>};Wx~wDVqPO+;?E{#eM!n?3&Ez{heih?Czca9hnEm&wa~e zS_zu-;}-|&YOjj=2B-N>EU90biaBsReLFRh9R_3=`^|#{zxbhxqo&ZW3y5w57!yOx zxqnB*&H{X<}2x+2>WiA%QvBOxJyT+Hh+I1>5#ic-X_;8pnoBe$xL z;%z)@Z({x^bUFR*p3h9CoY5Z)vx=WyxoiE^7s9F4_rQv(fW5& z&O7QVZa+oRX_}ie*%5=f>|PuV>_)Ar#}#|xCDEq7Modx_4v&n)q@{69+JgZfm3aaC zIjT!oAdlm>n$m$!(1z15POH7M@(F%a-q0D}q}mN|!Qq>Rihu zNM#gxiUS{gZrD>}XAkh+Bat}Uf#0}j2@>r+CP@`Wy*@kB?cSMOzBYi9O0qhcV*EkQ zO5%36tHnxOZa6QuK`soHjEe!imFSw@hdCZ>jWfw6rQOp}*ZV=|$X`vKZieQv&mysq zfQ3i3hkll;dv65|e+>HAV0F7JXn1aWReoSN{dw2gqk)!Oe!co;^)-X#uvsK zEPe$Cs=IBSb-zfF5lNL`mJ4hBU8}r#f3jlt!;=DqF3L^Sv;y0!ZFugz01kVjzQtjt zg&Cm}LdoxX3O)(zJ3seGNF>w6Tbu`K&%8I8?y^ZZa?NRM#Lh>Mv|lnB&abU17#qe_ zHhO=rPwqEGIy)ZOoer6?wVo9|XwJT@n0vMu*7o98gNB!$89YOr7do12teez;Y4Maw z_+|2@VuZc#!cOjsmKcVg2|J3rzg2rkTMQdT@z~w7cjR-acTaat!y0^Ruk4Ix1c6xC zfG6OZ=i;7=ef+f0&-Vbycf2b&&lgL(HJ<^9P4V}-sF7&Djqo<>Lt zoi_@QC>oMwO;hUZ7O-emv6Mipm$ImX&CNI8O#HiN9gUjqH5^6qsTQ0uf71GS!8Xnt zAnMv%+q~@L(9rjSOSO>TR?zKU&4tB9+urS7W~;u@3FDb(eiPxAS_QqfnX~WH4>W|5 zU6dtAsqXd?G}i{jfYpWIk;PHxM@gVjJccygPBoXU7(TU0-7B{Y*tWTHuPv@Pwyf>R zB~xt^-G{9w+IP7l-5tj{H}jgouJ-UmY2>=1!mecO#jgjQn~mc`KfIEi@dZ$ z!OE#vOXHBZL}0Gy!C`x;=4jpab;Q}e$z3-5&Ahnr;p-~~o-CIw#-q1>@79P*65!M| zi`U4?m>PXMzePG$?9Uy%tHZ-vdXt2Awx5suSc>Z9ZYFLH{idm}lIndKV_FI+ZswXD zI3UE$CFi$07Bi^L^Jyq~x_gS>4V0SRG{K>Zt0IGdB-vgQq?{LBk&_bMN)QlZ^}B?$ z?DV0oZI@f@V63)f8Ci6`U+gADkb`s`UQOk$PwPW>dv?l#(ujE+DJwTdsUYg;!&8)ftC9X^ z#cHltAF5kPNc(oWru3L6jO^AT_!}+8HuB?DM0JfdR2~&opFKJ+8{}07)(p z4Ak*1WSyj-R}!S)0OvyI*~3ytebx9cwDu$JDIsvyn@qU?AQBi>NsL1-DH1thTgKQK zxI$I8&^f&DTN>p85tyXPSNZ$G#Bb(iE<6o1czv%58xBQn&zYERpOJdaxoMPJdefd2 zH&=MWDmA5`;IzZuqJFB#!#Up1>3dh-oQ+U^(&RKtSV;FANZ#K*`Q%kOeFQf!KdQu? zcaAzYd!fR|V40eb451N5Sy`!h_(W06Lr5*43c~iMm3%`y+g5MBt@}AwQ56Sa3XPANIO>AE)Eyt;k< za4y6-Tz0cQ?#7_G>29t$*-_uKuKv$>6fskQ-b2HDxO`rkl2V872mPk8;O|NSqc7Lt z+O*!v4HuG1VC}uDR<-(d#Fwe<_Nr~SH+xG@;&jL3)Onzz55{rkE6rDo?g(i^ zfPQ*wKiK3Z3C>gdr345x88Q&d3q@Z0+PU)-e*~;tkXWFqz>ssUW$_$a@Z~r}QA}+t z>1ciNc&f^n(6;5~BGM`JB+K5&c$m?1`By5uHhtM`$s^A&?8OT)FBE~MY7vP-{olkcj7fmRtfoQC-sdLF1e{0a=9rfqil_l=ncd>Bje;R7qaDLn}8ATkj$PFwoyz$dF~5JabIIm_=dd?4;MW z@vvijGZUR=zxZChPWei6NFSwjU*<3;40g`*R_Ju^Y=-+KCv4t#{m#4MrqnjsTR;fz z(3e=+i@qE%T3Kv8=0Rh)<)u*z50pqEc~d)IrrqvazHC+)Sc|rwsMlT}YvkE+)vp_H zcos`li)oyb=C3qdQ{&pLg`J`4S$iiE(i7M5B3$Fz)~aTc`D$IQ8uHc^JxAc{8)&>@ zp!5(@jgz@C$g;XBMril&<7V^NF2*5jzLpu)K@r)cEyhM>9{U+r47~A9n}iOR72fW( zCse&6+|)@XHb@hlDQ#>28>U%+TZKiq0-DR^nVtYph=`|DGoWLsvJ+ zG+PESIF^E;trWvVD9f1_{YDA0I>$s| zgT3Ir=jnGZd}?}nJ-v^1Ow07-WPIt-z4EfaFjoA?JpwIy=)DfspWIDqO}~r%X% zrV|+^y4V$V9@2Qp`xHC8&1>NEyVaRaH+~4yLqfCZF1&6`<)-4ZT zpN+^w9*CsH=4vVxgjw_>B`2ng&?%-Oxi!4%A31Mw%hM1sW71nR^nKFHT7^Ri3&BZX zpC^M`cj!k@FpX6vEIS>pOEO|CNez!YsdhR)VrbLCHg5J?x~tzkY*4vArQF}lZpvoD zFs)UbMde6x^-|n@2{TR7GeMv^t!n1#U zv?N;%{yzeK=!@3HRzz|64vp9ztBPlKkq7lnW2tjy{MCdB+%Pt^j)h}{kaR0 zoOUV?chQRSkW7(8y*TvACp;qK_3`#<%JgH~_hGG|q>6{^+mP+CS_z2R?Ph$*?9g=MO71D9VoUNr0b= zim`t$Zo9g@qMOfO3nGNanWg$)m!1i{p%Q-W@B5iBs_y{A8ASZb#$lwa2t9zq$f%h3 zXl+I)Ly2s&K9wPYqS$|gk{Je0E|Obh8Fm+;FHRxle3w*(l97BE&m|%2Oa7f~kK{hT zbBjzLjT}CVX)Fj#c4O}!M^jXSzcJe5_W`Cm{S&lBzR9K$N#=Uto^sMDiWLtVS&whQ z*95-H7qZV)4K#X-6^9m$-aa6_mxlC;OcfgXnSh}pQc{<(5|YO>NZKM5)Ly?-R=%15 z-~4tbsjE)EX;7o+Ec#vl73uXOvu5mpiovv&r`oDAEeMO)Yu;{(JT(4#dh~ z`BS2m5Q@#33}(=Ri>^l5ic=j8Yok?Uf}!J~(JIwoJcd??6aE~jqjR^s-<|22#r1Ts z+?XpZNeLeoh;$I?lwZcT5F;loVJjDktKg0&TlPu^b9<6Vd|)z+k%{Rr4ovg2g3yQ} z2M0W{K%7zWF`jF;2EQy@-_*JTlOQ`&7k4OrSZT%YF6X=ho<+b++O}tpU3%GGVj7?A z_9j`s)WFk@3X7w0JKrDd{KoXx;5knhJ#bPr#v!Dn!V-ZDu|17w4o4OT7w?*{>6t0B zxQpCBBDbXb=h22f{3;KrtV83`NEKS1D^wubG@?ls2<0c`^b~g6j}00$Mw0(HQ9E)g zG=Vb4f|)^_o5yPy)&ZaYA>G>d*P0{LCdJn=U$XV zkByx%dZ0*oiFzk`(5M{(Jah*|Y-PK)B>yP5Lqv8IAnP{+~)_-}%5uL~R zN13C|Y@UthN~FZfwg(m>*T*i=pWs%ii8zOF!oDt?3pY!8TuL3yd4mocpfJS@ax2ZR z`iAaq(hp1Vb30Br6|;czjD2;GfJt2DfDyv%aYO9g;A8jsexqIuVHb?&wQv8g((XK* zsa@R%}1B^1d#m5R(VkD-u45sJuAhBD8#DPt%pbLJ`I-kUOI z+nnFs`@GM2-}hYCIe(n~=<2F2d#|n(;ku>!RAP4ZNyk6M`BK zp5>%zr6^FN&E=yG@Y&cawG(enZtlmg5EB{8#QixVqUSn^5z6?AxoD2(S^mi!xo}?2#vadq_b7f=cMjH8sY1H3LYex`y*~2 z|53qE=NPH^$To@+n{?kw*pjR?cPaYO%_o~oP6rL(&x{@weR=!S6Wa-KA2oCN!c2Bb zxLR7N6gXSUTlOEmyMHXeyv@6J0AhPq9}{>SRr47nV_Z(}po32N3g4ttLLqonioAPxkWMQOgVqsVYcKfZ{y=Pb~J}{7# zkNJPg9h+{eFa9hn!6~z~O198Xp9iGatDBc*9UdPh%hdYCbxpXkxX3KgZ!*NE4mR9! zBg@Ccb}$d8*}aO2?-gF3`9ox*jp|b3UM_mC7K)SPxyFKA7LFTEoP}*iYg-neK5xXR zY}Nl~TY~r`yFw&4yJzC4;H6%%xfz6Xy|33ZZSmyDPi+M>;CVX4ea_(=33kH_iH-Fy! z6}2JVU|8qhIYwMvk$aav1%2^Z2Qlr!P#UJQFR`9q>LzwK_pqH(m8;+STJ)OasvNY@ zEs$#*QTH*}Yy79KcKC7m=g;fk2jfX~zgmc!y*V-g`DUD1mm7)Fkiz@v_4Cug{hV7O zMkUdm?z`(!gqdHww<@tY#!?kFd-=K+FOlrSDFcc4nEoHmyCnPwl$Lp~ZKvFncB)>7 zCw3z-!Gt<0s6Of6^%!-=mpi52eeM#|7|P|c<4;<{H&zJDq^o1g3)*I0a8z74Pxk#Y zm4FsA(^T=T<_uh-W7GVF%3n8EI_HQ4)|6l`B5ty1Yl@zDvd{kAwG;EFYDotTN@*$< z#Xc9x;JFhe-L%Mu%BjA0vMAPV7kSDwUpz5LRNOwfjK{7YExdDW-tX1ZT9RbR%*Dd8 zS1&mI)$K-?yf)809>y95m?->wv^kaT;rKd@Pl}v!378Xd%mn;y7Wy_Tivf~3f_i#- zAB3;jMKJ9`3Nw=11OFZOIVPOxb*R_c)xx2^+#m)Pep9fUf}DO8`S`jmct&2Y$0!+; zUSQan4Okkf-E8>jYcyq4-dRJebPZZwDh^s+%wzms!BBX_b$15D$mk zoB|XVn!0ci2E+-C#-Z3nF`x3e+q0)^dy6(ignw`l=ezMUgm3)Rp>%_F@v$1(yAz=` zt%4hb21>8SKe)xHi5nL;BBDHY{98GL3M_+XtDLtJPri4pbH^>YRXC;o+?upW!VQgJ zNyM?qqB4W(e*1!Ht~$O$zVyn;Sy|s) zx(ro!89f&A6i9jud9_9i?n?|1wt6kKbSZJEky}DRg-Kg$BbjyZ*K(winri2%q$zh(u1HzA{wi1udu`69Yy@O3ZI<5%-<>LP;2b_3 z{ELZV1!I2s2z83@q!*hKL#T}Fx$SLuOx@U-Lhv(+BtMVg_BW90ij8|X)}PnwFeqe5 zmHfT#=B4?aJfjjPF$q~??>{5QTwPx;Dho?yJJ=6>!y0ZJNnZG@yT7mQki_(-%uEB{ zKVPXP66*W1bw@i5@CB|1PHV zPBS+xas52>{5iAU#+6sLEzjJ1#IuT9lp7uKh)|T4BW)7Dv-`+k-DO%upBZQ;@KCd! z_|wB@?J6pUXN>e<%Oh=zGH$-a8xJnUt1z6E9iS2sMU-#}61g%B|dL zUC_ws?N}K<5Y~RdSv@X=;4=WXcqF#MVEgKByU3(RYlX>TgB7EPN#73EgSKIH{;O<- zJIx~k|~;p$;d#Z*atTs^1v&QRI7)rz{> z{mnb4WR0upN@q@sb-J4RZWj*lT3Ek~0`-M{y02;Q8|ig?(yWxgIlHS)94%{(M0cfQ zB1|Ddx)c*XKjTJ>OMldM*UfTmI7G?u?xaXxmOI0BJ8iP<-k#3fqAPTq2jY!8;)0gf z`q%M^lHunKmG1GFc)XN;`6d1J!lyc%GF^5Ydv+b!o~EPY+g?hWb9}Qmnsog{%|u7! zWrt)|hAX}#TMYXauS!RTXU0~!HT&vN~@i)!OZw zeBi8-lXi4xGD}F0o{~YGnVDSBxLlpNujQ`zEy;-U8s{GH$Zfvb&D@GR!D^h{nY<=% zcl8+$^TDlawY^XHCZja6XZWvOc`BWk$$ceqUu9;UkZw~5E_^7< z_^G?G@LbumY`RCtr_yIbShvh50dvlPTpI9+R3O8YeSNFDO7C1d(yuPj1Hr!gd8Swz zAaRtT`X~gdV>Zv!=kf!j0UGB2ff|}$Y2Ty9|+mqRd7E#}AfA#dTKg>LO;{XCUW3d?Oz zqElpMvxsohjhRj7mAX=5IMd{Yer^-4%h}ze{INf&DMMPS--mO1>#gTF{b?^oib8%l z8AChH61+rv2d>*qj>P(~s=(}rIbXVL zKO(#ikrQ?Av z5&p%7s^GlKjg`c$wOXdLN(@Xgiwidk2ja1Ao~?b{i}SO_j?W@zO8bX1Z`u2GpCVfo z&>L!gab~jU(?{lf(n4>CU#*CUd=$acFP$eWJho@q+;?L5f}^A?ED7l4cxtL=KNj`} zWNDacKT9EuzF>J+1tp(__q98-&+&;sBfA;!ORf6*ptC&gi^}30FYFcod*ldF=p)CE zf64}<5f?I>5@6=C2~kL`+p_~rywvchSwU#{Zuh04%$k#5GsPc_PVUXsK9da;^^U0X3f$ z`7vt7m6rK`29B_DNur=V$ht2@bId|^UnQsGj?fk)Ou%U5hTIA~t9F<|;KlF+3pK*8 z{xWhTtDlvolv`?WUg&GiEhi^#752l086L;X zb-gzOAOzA|7qw{_QrtX$WNf}`NnnG-oi7_MRb}1M#r2W_wl9Q5B<-Z@O%^Ucy=WHg ztgK2?Iil$Dbj&2Xrfq+m*#mt6CRiJuIcNRG(J$_~nFrbL!=qd15l_#d$;$aE{ffHT zjWaBaq97{^W-eDXE?jU~NREvSA*VkfeH@7!wLomBLAL4WN=W;EVm&R-V`8RU*{*~d zxfR~chLC)(^7-?k>jXu|i)=H|KNo}p))sFCun}4wx5sl+JN(1K=mViEF3y%S=sQ%E zm7J2Pji3rdwLiXpoah~~{hD$9gOg{u0r+)*+D5;4@el$lQQhKy%Nj?G51yC*8zcrj zWC6K7MPV8k_S3fmL02p9s|D|^TwmcWK#pw_*R8B{#u|q{OifKOo>+O>3U03-OCv;cAPH~dVu=se`jA;k-@G;-oFJpBA>QyOr%AI#&$OqBs}Eu~Xo5_tb%-Z2_7 z;2@lYLRRSgjZ$R*>Gix){xC2H+kN_5cxR1i1j*0WA`x zQau2K2%^@5#a}r@X#BV41?__#TYy>fyEGI|K`IE2h_gV-W~L=HY@`GH>4CTNsds{U z+~wYD=7U?sTqTf<-xt#s0+!}rRiVPMo&?N;0f@|tI}A%}K0ieXN~H?8rQ#O#nCY1r z8%`rt5g{QZIQ1tz@Fs0Lj@2uB5r7NgiWR_|h{Of2#*W}2U~M;8M47H0)dHG%(~5(ad{PP>>d=->O_=L&N7fp!8Z!G~7R|dYc3I404Pm z#J_`h3dBnEfe^8T$oe~~$8hz}L-Fk6=$IHj@HW)hpjd;4dRR<@6L#2A|LUW&v$M5E zbaizh2}<`kEe*-uuU^$>n?Y#)3xE-Q7sE`eJsB~`g^1|= zu;hbq{rLAp$8(@nkQIpaxG#?@A?X7O60;181t7{E0RVMhoL&J}PcaPfOCwlTME4)eeVNpEE4`;wYx(?{0VH)r%WRK3nFlK zBt!ZIbkBes9045_XcwQRoZivX@WZxK>c4RZxOmQNK_I^|1}B8QzdygV4Cam{Am4Cp zvvXw^l}*7QzCBWPrpkj537tJcBpqwk4lXKL`>Zb^ZWA2q2VkN#fp!4b#}i5e8JU=_ z8-GWiCJ=ap`5*n~5pb8XeYCXbyw{hqFYEOH!2uUrw)di%8YL(f4c?bwN#waTpIHb@ zHL5QJ6YIWiK;Qi`3Z=6k5X0wzPucuFn5r;f*MG7PB!KS@KI?}Tc~=FrlOK`o z*$bT-CEQXC0D1k!#|H}Tt-UIOn|LrUxD-T*y?`h?E}|Qu8^8C)RDzUaau>MYU@mLT zeJg#7V`O9g6F&R^rh+8Dz1$eY6BcFY35f1}7_*KjD412u#2JTxY-P)Sm*Q#89!$g6 z*B9*@p-qXqOlu7J&*i+$JFvBm6M?GOA=D?#Y_hQ_S@`M9p?5i)ZkPY4B!k+M>>)pNh<&xy?Q+WPMbLNF=+Y& zf~K&V^#+kpmkz8xYQ><7;axXjWG=DorJ$=m`GF#9T1kiDV?Ifs6|&|Um$yJ=mkQe<#wQZs$fgxMNlLOK zzJ-gWDG*~U)vsHRY^euyoDUC4+~fi91R3xpHr+@c3}+HdgVun0G#Hg9Pv=4fzZGOt z5ylJ`E(*G}@#(f0vnk_JTNZ$mmU?h77gYiTNT;f82)U=8j)frcN6;GcAH;7V+0r|7W+-<%B2^o)y%wO|quW#G zt^V;$m-hB{r=l%1BdNo9YTt-P(ZGQP4b=>U zLoly^!}78=QB$DJ0Tgfyytx65$G*$&sT{0LA<^=*`*znWGR%rzfm;I15P>7HfG!&4 zvcbCw1`Nnzw)15f)*l3+@lq4sG@WrF3tnDUF!Y*XfO?4H%s@P81tjB7Ie@hfa}(#y zWy?%FpujcWYYot{z>5?H zs@y~nxqRHX%mP+%AkI9RhTY)H6+o>Qz&@dIIgF!LsLeyY9CT?&^9Qw$8!`9x3DEQr z5niVIURBi=o=vGv^MZj4%>M7*&2Hc%l05-$Yl3zeoscl+Tn)kv8so}z+kmMg0Sx>)ucoD%r)&}QUvIwiGXMYp literal 26912 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 diff --git a/doc/verification/loss_unclipped.png b/doc/verification/loss_unclipped.png index 4f1d450dc72536724cf0d5fb70781bb6a9c1cf2d..59aebefa40573fc4fdde2c92fde6f2b1b71eb3b2 100644 GIT binary patch literal 16551 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 literal 16667 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 diff --git a/doc/verification/verification_procedure.md b/doc/verification/verification_procedure.md index 3daf9a1..b948d0a 100644 --- a/doc/verification/verification_procedure.md +++ b/doc/verification/verification_procedure.md @@ -7,10 +7,12 @@ or hardware radio. The goal is to confirm the signal path is clean — no droppe sample buffers, no unintended DSP, no scaling errors — so that any on-air results reflect RADE performance, not integration issues. -**Scope:** This procedure tests integration correctness only. It does not -evaluate speech quality, compare V1 vs V2, or directly assess radio hardware -performance. Any additional tests beyond this procedure should be agreed with -the RADE team before being submitted as results. +**Scope:** This procedure assumes the application under test integrates the +`rade_c` library (C port of reference Python). It tests integration correctness +only — it does not evaluate speech quality, compare V1 vs V2, or directly +assess radio hardware performance. Any additional tests beyond this +procedure should be agreed with the RADE team before being submitted as +results. ## Requirements @@ -18,8 +20,8 @@ the RADE team before being submitted as results. input (Tx) and RADE decoder output (Rx) to disk files, for use with `loss.py`. See `rade_tx_wav -f` and `rade_rx_wav -f` in the rade_c repo, and the `rade_c_v2_wav` ctest, for worked examples. -- **RX-only applications** (no transmit capability) should use `tx2.py` from - this repository as the reference transmitter, generating the TX WAV at +- **RX-only applications** (no transmit capability) should use `rade_c`'s + `rade_tx_wav` as the reference transmitter, generating the TX WAV at test time so it tracks any model changes. For OTAC and OTC tests, the TX WAV must be played using a simple command-line tool with no signal processing (e.g. `aplay`, `afplay`, or `ffplay`). See the RX-only worked @@ -69,26 +71,29 @@ Tests the complete hardware chain. ### Step 1 — Establish the current baseline -Re-run the following with the latest version of this repository to obtain the -current software-only baseline. Do not use a cached value — the baseline shifts -slightly between model versions. +Re-run the following with the latest version of `rade_c` to obtain the +current software-only baseline. Do not use a cached value — the baseline +shifts slightly between model versions. ``` -cd ~/radae -lpcnet_demo -features wav/all.wav features_in.f32 -python3 tx2.py 250725/checkpoints/checkpoint_epoch_200.pth features_in.f32 tx.f32 -python3 rx2.py 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync tx.f32 features_rx.f32 --quiet -python3 loss.py features_in.f32 features_rx.f32 --clip_start 100 --clip_end 300 +cd ~/rade_c/build +./src/rade_tx_wav --v2 -f features_tx.f32 ../wav/all.wav tx.wav +./src/rade_rx_wav --v2 -f features_rx.f32 tx.wav decoded.wav +python3 ~/radae/loss.py features_tx.f32 features_rx.f32 --clip_start 100 --clip_end 300 ``` -Example output (Python reference, `wav/all.wav`, model `250725`, commit `b549586`): +Example output (`rade_c` reference, `wav/all.wav`, model `250725`): ``` -loss: 0.081 start: 224 acq_time: 1.24 s +loss: 0.079 start: 224 acq_time: 1.24 s ``` -Record the current baseline loss value and the git commit hash used +Record the current baseline loss value and the `rade_c` git commit hash used (e.g. `git log --oneline -1` → `cafebabe`). **A pass is within ±10% of the baseline.** +See [Worked Example of Loss Tests](#worked-example-of-loss-tests) below for +guidance on choosing `--clip_start`/`--clip_end` and an automated pass/fail +pattern using `--compare`. + ### Step 2 — Level 1: Software loopback Run `wav/all.wav` through your application's full encode/decode pipeline with no @@ -138,8 +143,8 @@ python3 ~/radae/loss.py features_tx.f32 features_rx.f32 \ ![Loss before clipping](loss_unclipped.png) -The spike at the start (~22) is the RADE acquisition transient; the smaller -spike at the end (~3) is the end-of-over frame. Both are expected behaviour +The spike at the start (~28) is the RADE acquisition transient; the smaller +spike at the end (~2.5) is the end-of-over frame. Both are expected behaviour from the state machine. Clip them out and re-run: ``` @@ -150,7 +155,7 @@ python3 ~/radae/loss.py features_tx.f32 features_rx.f32 \ ![Loss after clipping](loss_clipped.png) -With transients removed, loss drops from 0.113 to 0.082 — consistent with +With transients removed, loss drops from 0.111 to 0.079 — consistent with the reference baseline. `--clip_start 100` (1 s) and `--clip_end 300` (3 s) are conservative defaults; your integration may need different values depending on acquisition time. Use `--plot` to check. @@ -177,47 +182,40 @@ python3 ~/radae/loss.py features_tx.f32 features_rx_ref.f32 \ Output: ``` -loss1: 0.082 loss2: 0.082 delta: 0.000 +loss1: 0.079 loss2: 0.079 delta: 0.000 PASS ``` `loss.py` prints `PASS` or `FAIL` and exits with code 0 or 1 respectively, making it suitable for use in CI scripts. A `--delta` of 0.008 corresponds -to approximately ±10% of the V2 software loopback baseline (0.082). +to approximately ±10% of the V2 software loopback baseline (0.079). ### RX-only application -For applications with no transmit capability, use `tx2.py` to generate the -reference TX signal, then convert to a real-valued WAV for playback or -loopback testing. The `tx.f32` IQ file is already generated in Step 1. - -Convert to a real-valued 8 kHz mono WAV: +For applications with no transmit capability, use `rade_tx_wav` to generate +the reference TX WAV — `tx.wav` and `features_tx.f32` are already generated +in Step 1, no format conversion needed. -``` -python3 f32toint16.py --real --scale 16384 < tx.f32 | \ - sox -t s16 -r 8000 -c 1 - tx_real.wav -``` - -For a software loopback test, feed `tx_real.wav` directly to your -application's RX input and export `features_rx.f32`. For OTAC or OTC tests, -play `tx_real.wav` via a command-line audio player: +For a software loopback test, feed `tx.wav` directly to your application's +RX input and export `features_rx.f32`. For OTAC or OTC tests, play `tx.wav` +via a command-line audio player: ``` -aplay tx_real.wav # Linux -afplay tx_real.wav # macOS -ffplay tx_real.wav # Windows / cross-platform +aplay tx.wav # Linux +afplay tx.wav # macOS +ffplay tx.wav # Windows / cross-platform ``` Then measure loss against the TX features from Step 1: ``` -python3 loss.py features_in.f32 features_rx.f32 \ +python3 ~/radae/loss.py features_tx.f32 features_rx.f32 \ --clip_start 100 --clip_end 300 ``` Expected output (V2, software loopback, `wav/all.wav`): ``` -loss: 0.083 start: 224 acq_time: 1.24 s +loss: 0.079 start: 224 acq_time: 1.24 s ``` ## Submitting Results From 054a260434d87062f22cc843094dd04e9beb8c4a Mon Sep 17 00:00:00 2001 From: David Rowe Date: Tue, 8 Sep 2026 12:03:34 +0930 Subject: [PATCH 6/7] ota_test.sh: enable V2 C receiver diagnostics/plot for --v2_c Wires up rade_c's new rade_get_stats()-based diagnostics (see matching rade_c commit) into the --v2_c path: writes state/delta_hat/delta_hat_g/ freq_offset/gain/snr_est per OFDM symbol and calls the same plot_v2_logs() Octave plot the Python path already produced, closing the "no plot for the C path yet" gap. Also adds -v 2 to the rade_rx_wav call, giving V2 the same one-line-per-symbol log in *_report.txt that V1 already has by default (V1/V2 receivers differ in their default verbosity: rade_rx.c defaults verbose=2, rade_rx_v2 defaults to 0). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PzNQqmYXwUStAMrN4a5GFy --- ota_test.sh | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ota_test.sh b/ota_test.sh index b95f46a..e5949c1 100755 --- a/ota_test.sh +++ b/ota_test.sh @@ -190,8 +190,13 @@ function process_rx { # RADE2 Rx if [ $v2_c -eq 1 ]; then sox -t .s16 -r 8000 -c 1 ${rx_rade2}.raw ${rx_rade2}.wav - ${RADE_C}/rade_rx_wav --v2 -f features_out_rx2.f32 ${rx_rade2}.wav ${filename}_rade2.wav 2>>${filename}_report.txt - # note: rade_rx_wav doesn't yet expose delta_hat/gain/freq_offset/snr_est diagnostics -- no plot for the C path yet + ${RADE_C}/rade_rx_wav --v2 -v 2 -f features_out_rx2.f32 ${rx_rade2}.wav ${filename}_rade2.wav \ + --write_state state.int16 --write_delta_hat delta_hat.f32 --write_delta_hat_g delta_hat_g.f32 \ + --write_freq_offset freq_offset.f32 --write_gain gain.f32 --write_snr_est snr_est.f32 2>>${filename}_report.txt + DISPLAY=""; echo "warning('off', 'all'); \ + radae_plots; \ + plot_v2_logs('${filename}_plots.png', 'state.int16', 'delta_hat.f32','delta_hat_g.f32','freq_offset.f32','gain.f32','snr_est.f32'); \ + quit;" | octave-cli -qf > /dev/null else cat ${rx_rade2}.raw | python3 int16tof32.py --zeropad > ${rx_rade2}.f32 ./rx2.sh 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync ${rx_rade2}.f32 ${filename}_rade2.wav \ From e0980d138ea709dec396874adce7eff57df8fbec Mon Sep 17 00:00:00 2001 From: David Rowe Date: Wed, 9 Sep 2026 13:13:04 +0930 Subject: [PATCH 7/7] V2 EOO: fix MPP test methodology (Issue #1), update test report tables test/eoo_detect_prob.sh, test/eoo_false_prob.sh, and the v2_eoo_mpp_high/ low ctests (CMakeLists.txt) converted from inference.py's --end_of_over_v2 --g_file splice to tx2.py -> ch -> rx2.py. The old splice applied a fixed AWGN-only tail onto the already-channel-processed signal, so multipath fading and the TX SSB BPF never actually reached the EOO frame regardless of --channel/filter flags -- only AWGN-on-EOO was ever genuinely tested. tx2.py's continuous data+EOO stream through ch fades/filters both for real. CLI changed --EbNodB -> --No (ch's native parameter; no fixed conversion between the two exists). eoo_false_prob.sh needs --no_eoo explicitly now that tx2.py defaults EOO on, and a fading-offset bound fix (never accounted for needing wav_dur of remaining fading-file data after the offset). Both scripts now surface SNR3k per-trial and as a run mean. test/eoo_report_data.sh: runs the corrected detection-probability sweep (AWGN + MPP, N=20, --No values from quick single-trial calibration) plus the false-trigger sweep, writing doc/v2_test_report_eoo.tex. Fixed a row- ordering bug found while reviewing: rows were written in sweep order, not sorted, so SNR ran descending only for the leading clean-reference point then ascending for the three real test points -- now sorted (channel, -SNR) for a consistent descending order matching report convention. doc/v2_test_report.tex: EOO section now \input{}s the real N=20 table instead of the old hand-written --EbNodB-based one. Updated prose to describe the corrected methodology and replace a stale "sharp knee at ~1dB" claim (no longer accurate -- AWGN now declines gradually from 65% at 4.3dB to 5% at 0.3dB) with the actual curve. False-trigger paragraph now cites the new N=20 zero-false-trigger results alongside the existing librispeech.wav finding. Sample command line fixed (--EbNodB -> --No). Also: removed a stale "SSB BPF bandwidth: ..." debug print from radae.py's constructor (radae/radae.py) -- useful during the BPF investigation, just noise now that it's settled. Two follow-up questions identified reviewing the new numbers, to become GitHub Issues: (1) AWGN EOO detection is noticeably worse than the old table even though AWGN was supposedly always genuinely tested -- possible cause is the TX BPF added for Issue #2, which postdates the old characterization and could be affecting the EOO frame's time-domain sparsity signature that detect_eoo() relies on. (2) The false-trigger sweep only tests two fixed, moderate-SNR points and can't distinguish a genuinely near-zero false-alarm rate from a threshold that's simply not being stressed -- proper characterization needs a P(detect) vs. P(false-alarm) tradeoff curve, and should test whether false triggers are data-content-driven (as recalled from development) rather than purely SNR-driven before assuming SNR is even the right variable to sweep. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PzNQqmYXwUStAMrN4a5GFy --- CMakeLists.txt | 28 +++++++--- doc/v2_test_report.tex | 26 ++------- doc/v2_test_report_eoo.tex | 28 ++++++++++ radae/radae.py | 1 - test/eoo_detect_prob.sh | 47 +++++++++------- test/eoo_false_prob.sh | 50 ++++++++++------- test/eoo_report_data.sh | 106 +++++++++++++++++++++++++++++++++++++ 7 files changed, 216 insertions(+), 70 deletions(-) create mode 100644 doc/v2_test_report_eoo.tex create mode 100755 test/eoo_report_data.sh diff --git a/CMakeLists.txt b/CMakeLists.txt index 3117e27..2d559ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1225,21 +1225,33 @@ add_test(NAME v2_eoo_awgn_low 2>&1 | grep 'EOO detected' && echo PASS || echo FAIL") set_tests_properties(v2_eoo_awgn_low PROPERTIES PASS_REGULAR_EXPRESSION "PASS") +# V2 EOO detection under real MPP fading: tx2.py -> ch -> rx2.py. +# inference.py's --end_of_over_v2 splice only ever applies flat AWGN to the +# EOO tail regardless of channel flags (never real H/G fading, never the SSB +# BPF) -- tx2.py's continuous data+EOO stream through ch fades/filters both +# genuinely. Scale 8192 (not 16383) avoids clipping from multipath anti-fade +# peaks. --No -25 matches inference.py's --EbNodB 8 target C/No (39.46dB-Hz) +# for this file; "high" uses ch's default (no --No) -- both this and +# inference.py's default --EbNodB are so far into the noiseless regime +# (SNR3k 80-96dB) that AWGN is negligible either way and MPP fading is the +# only real impairment in both. add_test(NAME v2_eoo_mpp_high COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ - ./inference.sh 250725/checkpoints/checkpoint_epoch_200.pth wav/brian_g8sez.wav /dev/null --rate_Fs \ - --latent-dim 56 --peak --cp 0.004 --time_offset -16 --correct_time_offset -16 --auxdata --w1_dec 128 \ - --end_of_over_v2 --write_rx v2_eoo_tx.f32 --append_noise 1 --g_file g_mpp.f32; \ - ./rx2.sh 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync v2_eoo_tx.f32 /dev/null \ + test/make_g.sh; \ + ./tx2.sh 250725/checkpoints/checkpoint_epoch_200.pth wav/brian_g8sez.wav v2_eoo_mpp_high_tx2.f32; \ + cat v2_eoo_mpp_high_tx2.f32 | python3 f32toint16.py --real --scale 8192 | \ + ${CMAKE_BINARY_DIR}/src/ch - - --mpp --fading_dir . | python3 int16tof32.py --zeropad > v2_eoo_mpp_high_rx2.f32; \ + ./rx2.sh 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync v2_eoo_mpp_high_rx2.f32 /dev/null \ 2>&1 | grep 'EOO detected' && echo PASS || echo FAIL") set_tests_properties(v2_eoo_mpp_high PROPERTIES PASS_REGULAR_EXPRESSION "PASS") add_test(NAME v2_eoo_mpp_low COMMAND sh -c "cd ${CMAKE_SOURCE_DIR}; \ - ./inference.sh 250725/checkpoints/checkpoint_epoch_200.pth wav/brian_g8sez.wav /dev/null --rate_Fs \ - --latent-dim 56 --peak --cp 0.004 --time_offset -16 --correct_time_offset -16 --auxdata --w1_dec 128 \ - --end_of_over_v2 --write_rx v2_eoo_tx.f32 --append_noise 1 --g_file g_mpp.f32 --EbNodB 8; \ - ./rx2.sh 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync v2_eoo_tx.f32 /dev/null \ + test/make_g.sh; \ + ./tx2.sh 250725/checkpoints/checkpoint_epoch_200.pth wav/brian_g8sez.wav v2_eoo_mpp_low_tx2.f32; \ + cat v2_eoo_mpp_low_tx2.f32 | python3 f32toint16.py --real --scale 8192 | \ + ${CMAKE_BINARY_DIR}/src/ch - - --No -25 --mpp --fading_dir . | python3 int16tof32.py --zeropad > v2_eoo_mpp_low_rx2.f32; \ + ./rx2.sh 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync v2_eoo_mpp_low_rx2.f32 /dev/null \ 2>&1 | grep 'EOO detected' && echo PASS || echo FAIL") set_tests_properties(v2_eoo_mpp_low PROPERTIES PASS_REGULAR_EXPRESSION "PASS") diff --git a/doc/v2_test_report.tex b/doc/v2_test_report.tex index b1b09bd..5e5e3f3 100644 --- a/doc/v2_test_report.tex +++ b/doc/v2_test_report.tex @@ -369,33 +369,15 @@ \subsection{End of Over Detection} This section describes the End of Over (EOO) system performance. When the transmission is about to cease, a special EOO frame is sent to instruct the receiver to immediately stop decoding. This prevents decoding of spurious received signal and unwanted ``R2D2" tones emerging from the decoder at the end of a transmission. The design goal is reliable operation in high SNR benign channels. In poor channels (e.g. low SNR MPP), it is acceptable to have a lower probability of EOO detection, as clean end of over behaviour is a secondary requirement in the low SNR use case. In all channels, should the EOO frame not be detected, the hangover counter in the receiver state machine will eventually stop decoding after a few seconds. -The script \emph{eoo\_detect\_prob.sh} generates $N=20$ by 9 \si{s} duration samples (extracted from \emph{all.wav}) each terminated in an EOO frame. The number of successful EOO detections is counted to determine the probability of EOO detection for each condition. The 9 \si{s} duration is chosen to ensure the alignment shifts between \emph{all.wav} and \emph{g\_mpp.f32} as the trials modulo rotate around each file. Note the sharp knee in the AWGN performance at around 1 \si{dB} SNR. +The script \emph{eoo\_detect\_prob.sh} generates $N=20$ by 9 \si{s} duration samples (extracted from \emph{all.wav}) each terminated in an EOO frame, transmitted through \emph{tx2.py} $\to$ \emph{ch} $\to$ \emph{rx2.py} so that both the payload and the EOO frame are genuinely faded and filtered by the channel (an earlier version of this test spliced an AWGN-only EOO tail onto the channel output, so multipath fading never actually reached the EOO frame -- see Issue \#1). The number of successful EOO detections is counted to determine the probability of EOO detection for each condition. The 9 \si{s} duration is chosen to ensure the alignment shifts between \emph{all.wav} and \emph{g\_mpp.f32} as the trials modulo rotate around each file. AWGN detection probability declines steadily from 65\% at 4.3 \si{dB} SNR to 5\% at 0.3 \si{dB} SNR; MPP is weaker at comparable SNR, declining from 55\% at 8.3 \si{dB} SNR to 10\% at 2.3 \si{dB} SNR, since a real fade can occasionally land on the EOO frame itself. -\begin{table} [h] -\caption{End of Over Probability of Detection over $N=20$ trials} -\label{tab:eoo_p_detetc} -\centering -\begin{tabular}{ c | c | r | r } - \hline - Channel & EbNodB & SNR (\si{dB}) & P(detect) \\ - \hline - AWGN & 100 & 100 & 1.00 \\ - AWGN & 10 & 3.5 & 1.0 \\ - AWGN & 8 & 1.5 & 1.0 \\ - AWGN & 6 & -0.5 & 0.0 \\ - MPP & 100 & 100 & 1.0 \\ - MPP & 10 & 4.5 & 1.0 \\ - MPP & 6 & -0.5 & 0.3 \\ - MPP & 3 & -3.5 & 0.0 \\ - \hline -\end{tabular} -\end{table} +\input{v2_test_report_eoo.tex} -The script \emph{eoo\_false\_prob.sh} counts how many times the EOO detection system detects a EOO frame when none is present (false detection), e.g. due to the regular payload V2 OFDM symbols resembling an EOO frame. This is undesirable as the receiver would stop decoding mid-over and have to re-acquire. The longest source file tested was \emph{librispeech.wav} (used for ASR testing) at 540 seconds with no false detections. The source file \emph{all.wav} was also tested with various channel conditions (e.g. 10 trials over a MPP channel at 3.5 dB SNR) with no false detections. +The script \emph{eoo\_false\_prob.sh} counts how many times the EOO detection system detects a EOO frame when none is present (false detection), e.g. due to the regular payload V2 OFDM symbols resembling an EOO frame. This is undesirable as the receiver would stop decoding mid-over and have to re-acquire. The longest source file tested was \emph{librispeech.wav} (used for ASR testing) at 540 seconds with no false detections. Over $N=20$ trials each, no false detections were observed on \emph{all.wav} at 4.4 \si{dB} SNR over AWGN or 5.4 \si{dB} SNR over MPP. Sample command lines: \begin{verbatim} - ./test/eoo_detect_prob.sh --channel mpp --EbNodB 10 + ./test/eoo_detect_prob.sh --channel mpp --No -25 ./test/eoo_false_prob.sh \end{verbatim} diff --git a/doc/v2_test_report_eoo.tex b/doc/v2_test_report_eoo.tex new file mode 100644 index 0000000..c7c3f39 --- /dev/null +++ b/doc/v2_test_report_eoo.tex @@ -0,0 +1,28 @@ +%% Auto-generated by test/eoo_report_data.sh -- N=20 trials per point +%% tx2.py -> ch -> rx2.py, both data and EOO genuinely faded/filtered (see Issue #1) +\begin{table} [h] +\caption{End of Over Probability of Detection over $N=20$ trials} +\label{tab:eoo_p_detect} +\centering +\begin{tabular}{ c | r | r } + \hline + Channel & SNR3k (\si{dB}) & P(detect) \\ + \hline + AWGN & 80.3 & 0.95 \\ + AWGN & 4.3 & 0.65 \\ + AWGN & 2.3 & 0.40 \\ + AWGN & 0.3 & 0.05 \\ + MPP & 80.3 & 0.85 \\ + MPP & 8.3 & 0.55 \\ + MPP & 5.3 & 0.25 \\ + MPP & 2.3 & 0.10 \\ + \hline +\end{tabular} +\end{table} + +%% False-trigger results +%% AWGN -24: Mean SNR3k = 4.36dB +%% AWGN -24: Result: 0 false triggers over 20 trials +%% MPP -25: Mean SNR3k = 5.36dB +%% MPP -25: Result: 0 false triggers over 20 trials +%% \ No newline at end of file diff --git a/radae/radae.py b/radae/radae.py index 485d995..6e4f350 100644 --- a/radae/radae.py +++ b/radae/radae.py @@ -293,7 +293,6 @@ def __init__(self, Ntap=101 bandwidth = 2700-300 centre = (2700+300)/2 - print(f"SSB BPF bandwidth: {bandwidth:f} centre: {centre:f}") ssb_bpf = dsp.complex_bpf(Ntap, self.Fs, bandwidth, centre, 0) self.ssb_bpf_conv = nn.Conv1d(1, 1, kernel_size=len(ssb_bpf.h), dtype=torch.complex64) self.ssb_bpf_delay = int(Ntap // 2) diff --git a/test/eoo_detect_prob.sh b/test/eoo_detect_prob.sh index 9050b55..3c1e2ca 100755 --- a/test/eoo_detect_prob.sh +++ b/test/eoo_detect_prob.sh @@ -2,16 +2,18 @@ # # Measure probability of correct EOO detection for RADE V2. # Steps through all.wav in 2-second segments, running one EOO trial per segment. -# For MPP, g_offset is stepped across trials to sample different fade positions. +# For MPP, fading_adv is stepped across trials to sample different fade positions. +# Uses tx2.py -> ch -> rx2.py, so data and EOO both genuinely see the channel +# (fading, noise, SSB filtering), unlike inference.py's --end_of_over_v2 splice. # # Usage: -# ./test/eoo_detect_prob.sh [--EbNodB ] [--channel ] [--N ] +# ./test/eoo_detect_prob.sh [--No ] [--channel ] [--N ] -EbNodB=100 +No=-100 channel=awgn N=20 seg_len=9 # seconds per trial, make it long compared to fading duration - # and such that wav_dur/seg_len and g_mpp_dur/seg_len are not integers - + # and such that wav_dur/seg_len and g_mpp_dur/seg_len are not integers - # so sucessive trial are not aligned with either file verbose=0 @@ -19,15 +21,15 @@ function print_help { echo echo "Measure RADE V2 EOO detection probability" echo - echo " usage: ./test/eoo_detect_prob.sh [--EbNodB dB] [--channel awgn|mpp] [--N trials] [--verbose]" - echo " example: ./test/eoo_detect_prob.sh --EbNodB 6 --channel mpp --N 20" + echo " usage: ./test/eoo_detect_prob.sh [--No dBHz] [--channel awgn|mpp] [--N trials] [--verbose]" + echo " example: ./test/eoo_detect_prob.sh --No -25 --channel mpp --N 20" echo exit } while [[ $# -gt 0 ]]; do case $1 in - --EbNodB) EbNodB=$2; shift 2 ;; + --No) No=$2; shift 2 ;; --channel) channel=$2; shift 2 ;; --N) N=$2; shift 2 ;; --verbose) verbose=1; shift ;; @@ -37,8 +39,9 @@ while [[ $# -gt 0 ]]; do done if [ "$channel" = "mpp" ]; then - chan_args="--g_file g_mpp.f32" + chan_args="--mpp --fading_dir ." g_step=$seg_len + test/make_g.sh elif [ "$channel" = "awgn" ]; then chan_args="" else @@ -51,27 +54,32 @@ wav_dur=$(soxi -D wav/all.wav) g_mpp_dur=$(python3 -c "import os; print(os.path.getsize('g_mpp.f32')//(2*2*4*8000))") detected=0 +snr3k_sum=0 +tx_tmp=$(mktemp /tmp/eoo_detect_tx_XXXXXX.f32) rx_tmp=$(mktemp /tmp/eoo_detect_XXXXXX.f32) wav_tmp=$(mktemp /tmp/eoo_seg_XXXXXX.wav) -trap "rm -f $rx_tmp $wav_tmp" EXIT +ch_log=$(mktemp /tmp/eoo_detect_ch_XXXXXX.log) +trap "rm -f $tx_tmp $rx_tmp $wav_tmp $ch_log" EXIT -echo "EOO detection probability: EbNodB=$EbNodB channel=$channel N=$N seg_len=${seg_len}s" +echo "EOO detection probability: No=$No channel=$channel N=$N seg_len=${seg_len}s" for i in $(seq 1 $N); do offset=$(python3 -c "print((($i-1)*$seg_len) % (int($wav_dur)-$seg_len))") sox wav/all.wav $wav_tmp trim $offset $seg_len - g_offset_args="" + fading_adv_args="" g_off=0 if [ "$channel" = "mpp" ]; then g_off=$(python3 -c "print((($i-1)*$g_step) % ($g_mpp_dur-$seg_len))") - g_offset_args="--g_offset $g_off" + fading_adv_args="--fading_adv $g_off" fi - ./inference.sh 250725/checkpoints/checkpoint_epoch_200.pth $wav_tmp /dev/null --rate_Fs \ - --latent-dim 56 --peak --cp 0.004 --time_offset -16 --correct_time_offset -16 --auxdata --w1_dec 128 \ - --end_of_over_v2 --write_rx $rx_tmp --append_noise 1 --EbNodB $EbNodB \ - $chan_args $g_offset_args 2>/dev/null + ./tx2.sh 250725/checkpoints/checkpoint_epoch_200.pth $wav_tmp ${tx_tmp} 2>/dev/null + cat ${tx_tmp} | python3 f32toint16.py --real --scale 8192 | \ + build/src/ch - - --No $No $chan_args $fading_adv_args 2>$ch_log | \ + python3 int16tof32.py --zeropad > $rx_tmp + snr3k=$(grep -oP 'SNR3k\(dB\):\s*\K[-0-9.]+' $ch_log) + snr3k_sum=$(python3 -c "print($snr3k_sum + $snr3k)") if [ "$verbose" -eq 1 ]; then result=$(./rx2.sh 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync $rx_tmp /dev/null \ @@ -83,11 +91,12 @@ for i in $(seq 1 $N); do if [ "$result" -gt 0 ]; then detected=$((detected + 1)) - echo " trial $i: DETECTED (wav_dur=$wav_dur offset=${offset}s g_mpp_dur=$g_mpp_dur g_offset=$g_off)" + echo " trial $i: DETECTED (SNR3k=${snr3k}dB offset=${offset}s fading_adv=$g_off)" else - echo " trial $i: missed (wav_dur=$wav_dur offset=${offset}s g_mpp_dur=$g_mpp_dur g_offset=$g_off)" + echo " trial $i: missed (SNR3k=${snr3k}dB offset=${offset}s fading_adv=$g_off)" fi done prob=$(python3 -c "print(f'{$detected/$N:.2f}')") -echo "Result: $detected/$N detected, P(detect) = $prob" +snr3k_mean=$(python3 -c "print(f'{$snr3k_sum/$N:.2f}')") +echo "Result: $detected/$N detected, P(detect) = $prob, mean SNR3k = ${snr3k_mean}dB" diff --git a/test/eoo_false_prob.sh b/test/eoo_false_prob.sh index cb6fb9b..cb9e5fe 100755 --- a/test/eoo_false_prob.sh +++ b/test/eoo_false_prob.sh @@ -1,15 +1,17 @@ #!/bin/bash -e # # Measure false EOO detection rate for RADE V2. -# Runs N independent trials using the full all.wav WITHOUT --end_of_over_v2, -# counts how many times rx2.py falsely triggers "EOO detected" per trial. -# Each trial has a fresh noise realisation; for MPP, g_offset steps through -# the channel file to sample different fade positions. +# Runs N independent trials using the full wav with tx2.py --no_eoo (no real +# EOO frame present), counts how many times rx2.py falsely triggers +# "EOO detected" per trial. Each trial has a fresh noise realisation; for +# MPP, fading_adv steps through the channel file to sample different fade +# positions. Uses tx2.py -> ch -> rx2.py, same channel path as +# eoo_detect_prob.sh, for consistency. # # Usage: -# ./test/eoo_false_prob.sh [--EbNodB ] [--channel ] [--N ] +# ./test/eoo_false_prob.sh [--No ] [--channel ] [--N ] -EbNodB=100 +No=-100 channel=awgn N=20 wav=wav/all.wav @@ -18,15 +20,15 @@ function print_help { echo echo "Measure RADE V2 EOO false detection rate" echo - echo " usage: ./test/eoo_false_prob.sh [--EbNodB dB] [--channel awgn|mpp] [--N trials] [--wav wavefile]" - echo " example: ./test/eoo_false_prob.sh --EbNodB 10 --channel mpp --N 20" + echo " usage: ./test/eoo_false_prob.sh [--No dBHz] [--channel awgn|mpp] [--N trials] [--wav wavefile]" + echo " example: ./test/eoo_false_prob.sh --No -25 --channel mpp --N 20" echo exit } while [[ $# -gt 0 ]]; do case $1 in - --EbNodB) EbNodB=$2; shift 2 ;; + --No) No=$2; shift 2 ;; --channel) channel=$2; shift 2 ;; --N) N=$2; shift 2 ;; --wav) wav=$2; shift 2 ;; @@ -38,8 +40,9 @@ done wav_dur=$(soxi -D $wav) if [ "$channel" = "mpp" ]; then - chan_args="--g_file g_mpp.f32" + chan_args="--mpp --fading_dir ." g_step=$(python3 -c "print(int($wav_dur))") # step by one wav length per trial + test/make_g.sh g_mpp_dur=$(python3 -c "import os; print(os.path.getsize('g_mpp.f32')//(2*2*4*8000))") elif [ "$channel" = "awgn" ]; then chan_args="" @@ -49,31 +52,38 @@ else fi total_false=0 +snr3k_sum=0 +tx_tmp=$(mktemp /tmp/eoo_false_tx_XXXXXX.f32) rx_tmp=$(mktemp /tmp/eoo_false_XXXXXX.f32) -trap "rm -f $rx_tmp" EXIT +ch_log=$(mktemp /tmp/eoo_false_ch_XXXXXX.log) +trap "rm -f $tx_tmp $rx_tmp $ch_log" EXIT -echo "EOO false detection rate: EbNodB=$EbNodB channel=$channel N=$N" +echo "EOO false detection rate: No=$No channel=$channel N=$N" for i in $(seq 1 $N); do - g_offset_args="" + fading_adv_args="" g_off=0 if [ "$channel" = "mpp" ]; then - g_off=$(python3 -c "print((($i-1)*$g_step) % $g_mpp_dur)") - g_offset_args="--g_offset $g_off" + g_off=$(python3 -c "print((($i-1)*$g_step) % ($g_mpp_dur - $wav_dur))") + fading_adv_args="--fading_adv $g_off" fi - ./inference.sh 250725/checkpoints/checkpoint_epoch_200.pth $wav /dev/null --rate_Fs \ - --latent-dim 56 --peak --cp 0.004 --time_offset -16 --correct_time_offset -16 --auxdata --w1_dec 128 \ - --write_rx $rx_tmp --EbNodB $EbNodB \ - $chan_args $g_offset_args 2>/dev/null + ./tx2.sh 250725/checkpoints/checkpoint_epoch_200.pth $wav ${tx_tmp} --no_eoo 2>/dev/null + cat ${tx_tmp} | python3 f32toint16.py --real --scale 8192 | \ + build/src/ch - - --No $No $chan_args $fading_adv_args 2>$ch_log | \ + python3 int16tof32.py --zeropad > $rx_tmp + snr3k=$(grep -oP 'SNR3k\(dB\):\s*\K[-0-9.]+' $ch_log) + snr3k_sum=$(python3 -c "print($snr3k_sum + $snr3k)") count=$(./rx2.sh 250725/checkpoints/checkpoint_epoch_200.pth 250725a_ml_sync $rx_tmp /dev/null \ --latent-dim 56 --w1_dec 128 --correct_time_offset -8 2>&1 | grep -c "EOO detected" || true) total_false=$((total_false + count)) - echo " trial $i: $count false triggers (g_offset=$g_off)" + echo " trial $i: $count false triggers (SNR3k=${snr3k}dB fading_adv=$g_off)" done +snr3k_mean=$(python3 -c "print(f'{$snr3k_sum/$N:.2f}')") +echo "Mean SNR3k = ${snr3k_mean}dB" if [ "$total_false" -gt 0 ]; then avg_time=$(python3 -c "print(f'{$N*$wav_dur/$total_false:.1f}')") echo "Result: $total_false false triggers over $N trials, avg time between false triggers = ${avg_time} s" diff --git a/test/eoo_report_data.sh b/test/eoo_report_data.sh new file mode 100755 index 0000000..99d8e22 --- /dev/null +++ b/test/eoo_report_data.sh @@ -0,0 +1,106 @@ +#!/bin/bash -e +# +# Generates EOO detection/false-trigger data for the V2 test report, using +# tx2.py -> ch -> rx2.py (see eoo_detect_prob.sh/eoo_false_prob.sh). Writes +# a LaTeX table (tab:eoo_p_detect equivalent) plus a false-trigger summary +# to a .tex file for direct inclusion in the report. +# +# This is a rough re-calibration against the old (--EbNodB-based) table -- +# the --No values below aren't chosen to hit the old table's SNR points +# precisely, just to give a similar spread from clean to poor. The real +# numbers may differ noticeably from the old table now that the channel +# genuinely fades/filters the EOO segment (see Issue#1) -- that's expected +# and is the point of this re-run, not a bug. +# +# Usage: +# ./test/eoo_report_data.sh [--N ] [--out ] + +N=20 +out=doc/v2_test_report_eoo.tex + +function print_help { + echo + echo "Generate EOO detection/false-trigger data for the V2 test report" + echo + echo " usage: ./test/eoo_report_data.sh [--N trials] [--out file.tex]" + echo + exit +} + +while [[ $# -gt 0 ]]; do + case $1 in + --N) N=$2; shift 2 ;; + --out) out=$2; shift 2 ;; + -h|--help) print_help ;; + *) echo "Unknown argument: $1"; print_help ;; + esac +done + +# AWGN and MPP No sweeps: clean, then three points spanning the knee region +# (picked from quick single-trial calibration checks, not precisely matched +# to the old table's EbNodB-based SNR points). +awgn_Nos="-100 -20 -22 -24" +mpp_Nos="-100 -22 -25 -28" + +detect_log=$(mktemp) +false_log=$(mktemp) +trap "rm -f $detect_log $false_log" EXIT + +echo "Running EOO detection probability sweep (N=$N per point)..." +> $detect_log +for No in $awgn_Nos; do + echo " AWGN No=$No" + ./test/eoo_detect_prob.sh --No $No --channel awgn --N $N 2>/dev/null | grep "Result:" | \ + sed "s/^/AWGN $No: /" >> $detect_log +done +for No in $mpp_Nos; do + echo " MPP No=$No" + ./test/eoo_detect_prob.sh --No $No --channel mpp --N $N 2>/dev/null | grep "Result:" | \ + sed "s/^/MPP $No: /" >> $detect_log +done + +echo "Running EOO false-trigger sweep (N=$N per point)..." +> $false_log +echo " AWGN No=-24" && ./test/eoo_false_prob.sh --No -24 --channel awgn --N $N 2>/dev/null | \ + grep -E "Mean SNR3k|Result:" | sed "s/^/AWGN -24: /" >> $false_log +echo " MPP No=-25" && ./test/eoo_false_prob.sh --No -25 --channel mpp --N $N 2>/dev/null | \ + grep -E "Mean SNR3k|Result:" | sed "s/^/MPP -25: /" >> $false_log + +# Parse "CHANNEL No: Result: D/N detected, P(detect) = P, mean SNR3k = SdB" +python3 -c " +import re + +rows = [] +with open('$detect_log') as f: + for line in f: + m = re.match(r'(\w+) (-?\d+): Result: (\d+)/(\d+) detected, P\(detect\) = ([\d.]+), mean SNR3k = (-?[\d.]+)dB', line) + if m: + chan, No, d, n, p, snr = m.groups() + rows.append((chan, No, float(snr), float(p))) +rows.sort(key=lambda r: (r[0], -r[2])) # group by channel, descending SNR within each + +with open('$out', 'w') as f: + f.write('%% Auto-generated by test/eoo_report_data.sh -- N=$N trials per point\n') + f.write('%% tx2.py -> ch -> rx2.py, both data and EOO genuinely faded/filtered (see Issue #1)\n') + f.write('\\\\begin{table} [h]\n') + f.write(f'\\\\caption{{End of Over Probability of Detection over \$N={$N}\$ trials}}\n') + f.write('\\\\label{tab:eoo_p_detect}\n') + f.write('\\\\centering\n') + f.write('\\\\begin{tabular}{ c | r | r }\n') + f.write(' \\\\hline\n') + f.write(' Channel & SNR3k (\\\\si{dB}) & P(detect) \\\\\\\\\n') + f.write(' \\\\hline\n') + for chan, No, snr, p in rows: + f.write(f' {chan} & {snr:.1f} & {p:.2f} \\\\\\\\\n') + f.write(' \\\\hline\n') + f.write('\\\\end{tabular}\n') + f.write('\\\\end{table}\n\n') + + f.write('%% False-trigger results\n') + with open('$false_log') as flog: + f.write('%% ' + flog.read().replace(chr(10), chr(10)+'%% ')) +print(f'Wrote {\"$out\"}')" + +cat $detect_log +echo +cat $false_log