From 3d2ffc3f2c98ae48770120bbdea622996d8f5446 Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Sat, 2 May 2026 15:45:56 +0545 Subject: [PATCH] chore(repo): cosim_dir replay revival + ruff lint cleanup cosim_dir revival: - gen_realdata_hex.py: also emit decimated_range_{i,q}.npy (48x512) and doppler_map_{i,q}.npy (512x48) at production dimensions; the same Python pipeline that produces the RTL .hex stimuli now writes the .npy intermediates v7.replay COSIM_DIR loads. Replaces the workflow lost when golden_reference.py was deleted in e8b495c - test_v7.py: update test_get_frame_cosim shape from pre-PR-O.6 (64,32) to (NUM_RANGE_BINS, NUM_DOPPLER_BINS) - check in 4 .npy reference files (~400 KB, deterministic SCENE_SEED=42) Ruff lint cleanup (was 66 errors; now 0): - pyproject.toml: ignore T20 in tb/cosim/**.py (CLI tools) - compare_independent.py: drop redundant int() casts (RUF046), swap try/except scipy import for importlib.util.find_spec, remove dead duplicate np import, ASCII-ize comment unicode, wrap E501 format strings - fpga_reference.py: drop unused fs arg from nco_reference, collapse if/else to ternary, mark _out_im unused - v7/processing.py: ASCII-ize x in docstring, collapse if-branches - {dashboard,software_fpga,workers,radar_protocol}.py: wrap E501 - test_v7.py: ASCII-ize comment unicode, _alias renames where unused Result: test_v7 100/100 (0 skips on radar_venv, was 9 graceful skips); 5 cosim_dir orphan tests now active and passing. --- .../9_2_FPGA/tb/cosim/compare_independent.py | 47 ++++++------ .../9_2_FPGA/tb/cosim/fpga_reference.py | 11 ++- .../tb/cosim/real_data/gen_realdata_hex.py | 67 +++++++++++++----- .../cosim/real_data/hex/decimated_range_i.npy | Bin 0 -> 98432 bytes .../cosim/real_data/hex/decimated_range_q.npy | Bin 0 -> 98432 bytes .../tb/cosim/real_data/hex/doppler_map_i.npy | Bin 0 -> 98432 bytes .../tb/cosim/real_data/hex/doppler_map_q.npy | Bin 0 -> 98432 bytes 9_Firmware/9_3_GUI/radar_protocol.py | 2 +- 9_Firmware/9_3_GUI/test_v7.py | 20 +++--- 9_Firmware/9_3_GUI/v7/dashboard.py | 2 +- 9_Firmware/9_3_GUI/v7/processing.py | 6 +- 9_Firmware/9_3_GUI/v7/software_fpga.py | 6 +- 9_Firmware/9_3_GUI/v7/workers.py | 6 +- pyproject.toml | 2 + 14 files changed, 107 insertions(+), 62 deletions(-) create mode 100644 9_Firmware/9_2_FPGA/tb/cosim/real_data/hex/decimated_range_i.npy create mode 100644 9_Firmware/9_2_FPGA/tb/cosim/real_data/hex/decimated_range_q.npy create mode 100644 9_Firmware/9_2_FPGA/tb/cosim/real_data/hex/doppler_map_i.npy create mode 100644 9_Firmware/9_2_FPGA/tb/cosim/real_data/hex/doppler_map_q.npy diff --git a/9_Firmware/9_2_FPGA/tb/cosim/compare_independent.py b/9_Firmware/9_2_FPGA/tb/cosim/compare_independent.py index d148bcb..1158560 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/compare_independent.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/compare_independent.py @@ -38,14 +38,14 @@ from pathlib import Path # Required: numpy + scipy. If either is missing, exit code 2 with a [SKIP] # marker so the regression can distinguish missing-deps from real failures # (see run_regression.sh "Independent Reference Drift (T-6)" block). +import importlib.util + _MISSING = [] try: - import numpy as np # noqa: F401 + import numpy as np except ImportError: _MISSING.append("numpy") -try: - import scipy.signal.windows # noqa: F401 -except ImportError: +if importlib.util.find_spec("scipy.signal") is None: _MISSING.append("scipy") if _MISSING: print( @@ -56,8 +56,6 @@ if _MISSING: ) sys.exit(2) -import numpy as np # re-import to get module binding now that we know it's there - # Make local imports work when invoked from anywhere THIS_DIR = Path(__file__).resolve().parent sys.path.insert(0, str(THIS_DIR)) @@ -80,9 +78,9 @@ from fpga_model import ( # noqa: E402 TOL_NCO_LUT_LSB = 1 # NCO_SINE_LUT: tightest possible TOL_TWIDDLE_LSB = 1 # twiddle ROMs: same — quarter-wave Q15 cosine -TOL_WINDOW_LSB = 4 # 4 LSB ≈ 1.2e-4 rounding budget on Q15 round +TOL_WINDOW_LSB = 4 # 4 LSB ~= 1.2e-4 rounding budget on Q15 round TOL_NCO_MAG_REL = 0.04 # quarter-wave LUT artifact at quadrant edges -TOL_FFT_ROUNDTRIP_LSB = 60 # 11 stages × Q15 noise on 2048-pt; empirical +TOL_FFT_ROUNDTRIP_LSB = 60 # 11 stages * Q15 noise on 2048-pt; empirical # ============================================================================= @@ -169,7 +167,10 @@ def check_twiddle_rom(result: CheckResult, n: int, mem_filename: str): bad.append((k, cos_rom[k], ideal, dev)) result.check( max_dev <= TOL_TWIDDLE_LSB, - f"{mem_filename}: all {expected_entries} entries match cos(2pi*k/{n}) Q15 (tol {TOL_TWIDDLE_LSB} LSB)", + ( + f"{mem_filename}: all {expected_entries} entries match " + f"cos(2pi*k/{n}) Q15 (tol {TOL_TWIDDLE_LSB} LSB)" + ), f"max |ROM - ideal| = {max_dev} LSB" + ( f"; {len(bad)} bad, e.g. k={bad[0][0]}: ROM={bad[0][1]}, ideal={bad[0][2]}" if bad else "" @@ -186,7 +187,10 @@ def check_doppler_window_lut(result: CheckResult): worst_idx = int(np.argmax(diff)) result.check( max_dev <= TOL_WINDOW_LSB, - f"DOPPLER_WINDOW_COEFF: all 16 entries match Dolph-Chebyshev 60 dB Q15 (tol {TOL_WINDOW_LSB} LSB)", + ( + f"DOPPLER_WINDOW_COEFF: all 16 entries match " + f"Dolph-Chebyshev 60 dB Q15 (tol {TOL_WINDOW_LSB} LSB)" + ), f"max |LUT - ideal| = {max_dev} LSB at n={worst_idx} " f"(LUT={int(win_lut[worst_idx])}, ideal={int(win_ref[worst_idx])})" ) @@ -233,7 +237,7 @@ def check_nco_invariants(result: CheckResult): z = cos_arr + 1j * sin_arr Z = np.fft.fft(z) peak_bin = int(np.argmax(np.abs(Z))) - expected_bin = int(round(ftw / (1 << 32) * n_capture)) + expected_bin = round(ftw / (1 << 32) * n_capture) result.check( abs(peak_bin - expected_bin) <= 1, f"NCO dominant frequency at FTW = {ftw:08X} (expected bin {expected_bin})", @@ -264,8 +268,8 @@ def check_fft_invariants(result: CheckResult): # peak = amp*N stays below Q15 saturation (32767). bin_k = 137 amp = 15 - in_re = [int(round(amp * math.cos(2 * math.pi * bin_k * i / n))) for i in range(n)] - in_im = [int(round(amp * math.sin(2 * math.pi * bin_k * i / n))) for i in range(n)] + in_re = [round(amp * math.cos(2 * math.pi * bin_k * i / n)) for i in range(n)] + in_im = [round(amp * math.sin(2 * math.pi * bin_k * i / n)) for i in range(n)] twin_re, twin_im = fft.compute(in_re, in_im, inverse=False) ref_re, ref_im = ref.fft_reference(in_re, in_im, n=n) twin_mag2 = np.array(twin_re) ** 2 + np.array(twin_im) ** 2 @@ -280,7 +284,7 @@ def check_fft_invariants(result: CheckResult): # Roundtrip — small amplitude (peak = amp*N/2 ≤ 32767 → amp ≤ 32) so the # forward FFT does not saturate, then IFFT should recover input within - # 11×Q15 butterfly noise. + # 11*Q15 butterfly noise. rt_amp = 30 in_re = [int(rt_amp * math.sin(2 * math.pi * 73 * i / n)) for i in range(n)] in_im = [0] * n @@ -289,7 +293,10 @@ def check_fft_invariants(result: CheckResult): rt_max_err = max(abs(rt_re[i] - in_re[i]) for i in range(n)) result.check( rt_max_err <= TOL_FFT_ROUNDTRIP_LSB, - f"FFT-2048(roundtrip, amp={rt_amp}): FFT->IFFT recovers input within {TOL_FFT_ROUNDTRIP_LSB} LSB", + ( + f"FFT-2048(roundtrip, amp={rt_amp}): FFT->IFFT recovers input " + f"within {TOL_FFT_ROUNDTRIP_LSB} LSB" + ), f"max |rt - in| = {rt_max_err}" ) @@ -309,8 +316,8 @@ def check_mf_invariants(result: CheckResult): ref_im_in = [0] * n pulse_len = 256 for i in range(pulse_len): - ref_re_in[i] = int(round(amp * math.cos(2 * math.pi * bin_k * i / pulse_len))) - ref_im_in[i] = int(round(amp * math.sin(2 * math.pi * bin_k * i / pulse_len))) + ref_re_in[i] = round(amp * math.cos(2 * math.pi * bin_k * i / pulse_len)) + ref_im_in[i] = round(amp * math.sin(2 * math.pi * bin_k * i / pulse_len)) sig_re[i + delay] = ref_re_in[i] sig_im[i + delay] = ref_im_in[i] @@ -328,7 +335,7 @@ def check_mf_invariants(result: CheckResult): f"twin={twin_peak}, ref={ref_peak}" ) - # Sidelobe behaviour: peak should be N×stronger than median. + # Sidelobe behaviour: peak should be N*stronger than median. twin_peak_val = float(twin_mag[delay]) twin_median = float(np.median(twin_mag)) pk_ratio = twin_peak_val / max(twin_median, 1.0) @@ -356,8 +363,8 @@ def check_doppler_invariants(result: CheckResult): for c in range(chirps_per_subframe): chirp_idx = sf * chirps_per_subframe + c phase = 2 * math.pi * dop_bin * c / chirps_per_subframe - chirp_i[chirp_idx, target_rbin] = int(round(amp * math.cos(phase))) - chirp_q[chirp_idx, target_rbin] = int(round(amp * math.sin(phase))) + chirp_i[chirp_idx, target_rbin] = round(amp * math.cos(phase)) + chirp_q[chirp_idx, target_rbin] = round(amp * math.sin(phase)) dop = DopplerProcessor(num_subframes=num_subframes, chirps_per_frame=chirps_per_frame) diff --git a/9_Firmware/9_2_FPGA/tb/cosim/fpga_reference.py b/9_Firmware/9_2_FPGA/tb/cosim/fpga_reference.py index 5e04f23..132a15d 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/fpga_reference.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/fpga_reference.py @@ -44,7 +44,7 @@ import numpy as np # NCO reference — ideal complex sinusoid # ============================================================================= -def nco_reference(num_samples: int, ftw: int, fs: float = 400e6, +def nco_reference(num_samples: int, ftw: int, phase_offset_deg: float = 0.0): """Ideal floating-point NCO output, scaled to match Q15 fpga_model. @@ -101,10 +101,7 @@ def fft_reference(in_re, in_im, n: int = 2048, inverse: bool = False): if len(re) != n or len(im) != n: raise ValueError(f"input length {len(re)} != N={n}") x = re + 1j * im - if inverse: - y = np.fft.ifft(x) - else: - y = np.fft.fft(x) / n + y = np.fft.ifft(x) if inverse else np.fft.fft(x) / n return y.real.copy(), y.imag.copy() @@ -221,7 +218,7 @@ def doppler_reference(chirp_data_i, chirp_data_q, def _self_test(): """Quick sanity checks.""" # NCO: at FTW = 0x4CCCCCCD, frequency = 0.3 * fs = 120 MHz at 400 MSPS. - cos_q15, sin_q15 = nco_reference(8, 0x4CCCCCCD, fs=400e6) + cos_q15, sin_q15 = nco_reference(8, 0x4CCCCCCD) # First sample should be cos(0)=1, sin(0)=0 in Q15 assert abs(cos_q15[0] - 32767.0) < 1.0, f"NCO[0].cos = {cos_q15[0]}" assert abs(sin_q15[0]) < 1.0, f"NCO[0].sin = {sin_q15[0]}" @@ -229,7 +226,7 @@ def _self_test(): # FFT: impulse -> all bins = amplitude/N (scaled-mode schedule) in_re = [1000] + [0] * 15 in_im = [0] * 16 - out_re, out_im = fft_reference(in_re, in_im, n=16) + out_re, _out_im = fft_reference(in_re, in_im, n=16) for k in range(16): # AUDIT-C10/C-8: FWD FFT now applies /N (=/16), so each bin = 1000/16 assert abs(out_re[k] - 1000.0 / 16.0) < 1e-9, \ diff --git a/9_Firmware/9_2_FPGA/tb/cosim/real_data/gen_realdata_hex.py b/9_Firmware/9_2_FPGA/tb/cosim/real_data/gen_realdata_hex.py index 351b8e7..82e1d34 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/real_data/gen_realdata_hex.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/real_data/gen_realdata_hex.py @@ -8,12 +8,18 @@ Replaces the legacy ADI CN0566 hardware captures (32-chirp / 2-subframe / (48-chirp / 3-subframe / 48-bin Doppler) so the regression no longer depends on out-of-tree .npy files. -Outputs (six files, all under tb/cosim/real_data/hex/): - doppler_input_realdata.hex 48 chirps x 512 range bins, packed {Q,I} - doppler_ref_i.hex / _q.hex 512 range bins x 48 Doppler bins (signed 16-bit) - fullchain_range_input.hex 48 chirps x 2048 range bins, packed {Q,I} - fullchain_doppler_ref_i.hex - fullchain_doppler_ref_q.hex same shape as doppler_ref_* +Outputs (all under tb/cosim/real_data/hex/): + + RTL stimuli + goldens (.hex): + doppler_input_realdata.hex 48 chirps x 512 range bins, packed {Q,I} + doppler_ref_i.hex / _q.hex 512 range bins x 48 Doppler bins (signed 16-bit) + fullchain_range_input.hex 48 chirps x 2048 range bins, packed {Q,I} + fullchain_doppler_ref_i.hex + fullchain_doppler_ref_q.hex same shape as doppler_ref_* + + GUI replay intermediates (.npy, COSIM_DIR ReplayFormat in v7.replay): + decimated_range_i.npy / _q.npy (48, 512) — post range_bin_decimator + doppler_map_i.npy / _q.npy (512, 48) — post doppler_processor Dimensions match production (radar_params.vh: RP_FFT_SIZE=2048, RP_DECIMATION_FACTOR=4, RP_NUM_RANGE_BINS=512, RP_NUM_DOPPLER_BINS=48). @@ -29,6 +35,8 @@ Usage: python3 gen_realdata_hex.py import os import sys +import numpy as np + sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from fpga_model import DopplerProcessor, RangeBinDecimator @@ -106,10 +114,11 @@ def gen_doppler_realdata(): seed=SCENE_SEED, ) - stim = [] - for c in range(CHIRPS_PER_FRAME): - for rb in range(DOPPLER_RANGE_BINS): - stim.append((frame_i[c][rb], frame_q[c][rb])) + stim = [ + (frame_i[c][rb], frame_q[c][rb]) + for c in range(CHIRPS_PER_FRAME) + for rb in range(DOPPLER_RANGE_BINS) + ] write_hex_32(os.path.join(OUT_DIR, "doppler_input_realdata.hex"), stim) dp = make_doppler_processor() @@ -118,7 +127,8 @@ def gen_doppler_realdata(): write_hex_16(os.path.join(OUT_DIR, "doppler_ref_i.hex"), flat_i) write_hex_16(os.path.join(OUT_DIR, "doppler_ref_q.hex"), flat_q) - print(f" stimulus: {len(stim)} packed lines (expected {CHIRPS_PER_FRAME * DOPPLER_RANGE_BINS})") + expected_stim = CHIRPS_PER_FRAME * DOPPLER_RANGE_BINS + print(f" stimulus: {len(stim)} packed lines (expected {expected_stim})") print(f" golden: {len(flat_i)} lines i / {len(flat_q)} lines q " f"(expected {DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS})") @@ -133,10 +143,11 @@ def gen_fullchain_realdata(): seed=SCENE_SEED, ) - stim = [] - for c in range(CHIRPS_PER_FRAME): - for rb in range(FULLCHAIN_INPUT_BINS): - stim.append((frame_i[c][rb], frame_q[c][rb])) + stim = [ + (frame_i[c][rb], frame_q[c][rb]) + for c in range(CHIRPS_PER_FRAME) + for rb in range(FULLCHAIN_INPUT_BINS) + ] write_hex_32(os.path.join(OUT_DIR, "fullchain_range_input.hex"), stim) # fpga_model.RangeBinDecimator is hard-coded to 2048->512, DECIM=4 — production. @@ -152,6 +163,16 @@ def gen_fullchain_realdata(): write_hex_16(os.path.join(OUT_DIR, "fullchain_doppler_ref_i.hex"), flat_i) write_hex_16(os.path.join(OUT_DIR, "fullchain_doppler_ref_q.hex"), flat_q) + # Same arrays serialized for v7.replay COSIM_DIR format (GUI replay). + np.save(os.path.join(OUT_DIR, "decimated_range_i.npy"), + np.asarray(decim_i_2d, dtype=np.int32)) + np.save(os.path.join(OUT_DIR, "decimated_range_q.npy"), + np.asarray(decim_q_2d, dtype=np.int32)) + np.save(os.path.join(OUT_DIR, "doppler_map_i.npy"), + np.asarray(doppler_i, dtype=np.int32)) + np.save(os.path.join(OUT_DIR, "doppler_map_q.npy"), + np.asarray(doppler_q, dtype=np.int32)) + print(f" stimulus: {len(stim)} packed lines " f"(expected {CHIRPS_PER_FRAME * FULLCHAIN_INPUT_BINS})") print(f" golden: {len(flat_i)} lines i / {len(flat_q)} lines q " @@ -164,19 +185,31 @@ def main(): gen_fullchain_realdata() print("\nGenerated files:") - for f in ( + hex_files = ( "doppler_input_realdata.hex", "doppler_ref_i.hex", "doppler_ref_q.hex", "fullchain_range_input.hex", "fullchain_doppler_ref_i.hex", "fullchain_doppler_ref_q.hex", - ): + ) + for f in hex_files: path = os.path.join(OUT_DIR, f) with open(path) as fp: n_lines = sum(1 for _ in fp) print(f" {f:40s} {n_lines:7d} lines ({os.path.getsize(path):7d} bytes)") + npy_files = ( + "decimated_range_i.npy", + "decimated_range_q.npy", + "doppler_map_i.npy", + "doppler_map_q.npy", + ) + for f in npy_files: + path = os.path.join(OUT_DIR, f) + arr = np.load(path) + print(f" {f:40s} shape={arr.shape!s:>12s} ({os.path.getsize(path):7d} bytes)") + if __name__ == '__main__': main() diff --git a/9_Firmware/9_2_FPGA/tb/cosim/real_data/hex/decimated_range_i.npy b/9_Firmware/9_2_FPGA/tb/cosim/real_data/hex/decimated_range_i.npy new file mode 100644 index 0000000000000000000000000000000000000000..7cae61a47596cca11cc34069ed908ee9ec23f93b GIT binary patch literal 98432 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 literal 0 HcmV?d00001 diff --git a/9_Firmware/9_2_FPGA/tb/cosim/real_data/hex/decimated_range_q.npy b/9_Firmware/9_2_FPGA/tb/cosim/real_data/hex/decimated_range_q.npy new file mode 100644 index 0000000000000000000000000000000000000000..aa2b0fd6df909f2c2dd00cb5c7681068ae5d81c8 GIT binary patch literal 98432 zcmbW=%c`yGde!m7B&abe<_dOgE2N@C5V34cv0}lJ1QSsZBZ*iMufqE@o*;t9@$c{W z?cT<_zA<;OLGRD=wA0t;n5VH8*(?9?AO7Qi@Q?n&V75+<96EXpvCFE`LsXR+Xwi0nqhg`a_u3nOw+!ay&3q- z!@j?M>uK*9xb|{x9VUPG6J+`F;AG6Sd;w?g?)9U4^X&_{W>~E~&NFFyrw5;7KTH0- z?Rw13x8rrjg>Imk@p)Fq#Rr;+)1kka=H_31_2qe+`ggVmdiy@d9v`}KGR?3$PaLfo zA9rJU{y4Av_a2uw(Cld;KD3|h>*rrhhsnQ_)~qwo?dE#(bcgh?J8#@}=X3AeUC;j6 zeD`;U&E5O`XWvQu^y}X9ppIT4f7AWM@i*7+Sx z?RYajqy_tn{Cv~98C;omIX9iwwDa<~%i5p(tMfItw;OI=9^W1EFV`M$&E;O=ywU1G z+jZb~<_S37xVv+;{N3xJ<8nR8EuzQ)Xx$fJW9v^sjr?2)hf4tRYZ{FSA<-)CZ zZ|D7-&G7NazKTEJ;zB2%8Q%`wfHV1;UcBA*=6bv9*i1L@^O@;SyKefO_Z&VQJ2$g_ zeBJW<9388rhqORz$G;5o#zS|P39P&4>2B_8xpM1sf5+js!(O(%8S+AWkjHoP*?E>f z>GNK;O-DQLce@PVPJ3Ct?Y7hS&3$HEcd&0>Ca&F`KduSx?b%Fv;BR(6`S&?|yv^m> z`E}vCX})jTJS`5kLr+?JSpCkgPQ!KMcK2K}zr88{mp{qXX-PV9e4csVc-i#D=i^=@ zp0CNbdt99~yXWkrJ>}zg+e7ze+%NItt!K8I_s+jFuTD97`Q6h)d}vP(=DE&0Zxiyh z=fK%_@@+3ajo;0y-+j+=;m_hLyZ;VFPUm*`d_3;Fe9gV{ZWld1Y;JcMT{IcInI5`( z1zP)2fBNft{rSS}?LNwR@AizCeU5weI33Nfdb@kIm+{k^-d&fyTds-6oZc5D}aT(8^T9cb;k+86QNynDBUXLYxSa%cPcu5U9p=XpHab#rM!rl;|E@^Ksw zEsl35zL_`Jn~S5K_vP)fX1q>W+)lgC+Qi*C9{PIin`zH`bIU*F;&|xG>7? z-Rpny-aYp-b!^tY-R+yrL$4#?On$!jz`Osh1yAdiyE(lw&A^xb^h(#6e?2r@dWaAB zX4pyV$Fqo+-FbW5{Ok1`o@TmRrrX}M^XZHabn~$9AMbb1;M<JVD0(B0qVWuk&jB_1xWZ z_-63A&UM~#_1SOlt#$v%lOFat?Kq#=jBgK{>Bir?zSWn*(|tO|9H8Zbp%|##dgN??rhKJl~2y@Hsd$f4m{oV{1D&XT+laj?|I@v z+4jA+ul;5Ge52|7-=Df$Pj)-Cgz-`poIV@2zV&*(Toi8q1}HI6d^EocGn2Z&)*$-_SxP)=W#r%<4id{>um@6lRr;=A-!F{zmM?Em+{kUH&@5$cIOG6bL;s$ ze`owjzJDF*Negt{hdq6slPBK2&yZVgJLPct?@8ae&&&U$#jWq@bKE)iQ_kl~+wZTs z_obuxqn`TG^RyFq7n zJLC`D_oTfHA8)#M>S->&nY6v_zVCn1bviM$^CO>px*Wb4cw}*R=lWj3kGDKQ2EX&poy0rS?b+?R@b}HDHy?cGP4}7Z zydHYq{rYvwv@bRrza3ARI3819w~m|hoSpKUv&-|xw+DMO9QOTx3%!}N&7H(~Puu*D za=g0y{T7eYt*3c&`S0&X^T?#{Eq_nGrfi<>5EoXnH+h@9ufS;Cx6ZpAxVy7E)Wfqm z+K=>C!N*%U9k_Bja8Djw{&0Jry7lt!%%8r!yYuqsmx~Yi{66ibZ~jMoTF_rE#GUQD z)Ajf3@4tNJZeLkgEv~zcpfi5C?nT~ieltFF-~RZxW@z7>I}INf?!Ion-)QB6?76-^ z^Y-RBPdM-0^1SXGtJ`MM7zB`kiH#~WD^T~HZS-#bE=39@O-V9~E*0WlBxP9HepL-cT-s*@C{TU$BofhbE z;jr(|i@SX<^X1p4V{iGmvi@$nKhyTg+#dWsN3O|F?{1ef*V`U=^LIDHX1db?J}w;g zo#Q#``<;*O-siXPi^r^Ob=h=JqT{;lpX)Dv%zXO27o_EjdYM1HG-wf;P{;o@Yb-j0=-tFF;=j_}reKJkY-_AR~cYEO1 zp%Z^xZ{N+Q-+zwg2~B&q?%jFz`RRbqC%;qPbDr+)&{O|ry7M)`z4^4wt%ncC*B!R^ zFz205_j=RlJMDRQmt8*HYd?d%sl&wSG2=a>J#2<=xjZuQz4m(nTvPsI{YA#U9dRKp z=&+kQyP;fs*vWfy`DB~vJbN!E6L)*$`Q7<|wl}UB-!pEmdtZD%@}`03eK+&%Gwt!7 z<@s@t7SFRczb=!vj@!Sz^rmNQhwPKjGxBf7yOmb?0f%XE)csJ?;DFME<~kuJ@0}vplElU;23I z#LLImu{nEuaPMsIReq;EK6HoO-S^DPZ{`Vm+jVxg=QG=vZ}&>Ov+G?gFD<@b6W!tV z#jQ7eee`CKZ-%nEUxrT)4aehIX8LN~WuGQr+P%xf_wsx@9p4OfLATy^NN?A@ewvwY zcR3kaQ{O%l--J2Mf-)>)? zIKO7;@p`s5-p#}3UJvc=_2(wOxhUsu58bBy&c!e1#}yyso1VS*pop4d*)V{6{(O z^se+L&vCxHc+0l$^f_eQo^wBgZsuzbyf~htzV*H3o-*b4_qV>h!L4sO-v{9KIp1-= z_F?*KA1`|Sy@KCA?l`{o&>j4owjbsFt}`vT-;VdsPh6hW?03%n?a7lKXiYg>+2eZq;?{fXI=5qc@jLImj-CVb?)F}ugKOfNAuh=1IqKJ+ z)AeuWRq*k)8P89X!7xuH(PtYT49`*gTcW=h7!`<{uI}PtSd*^}p+mjX-cF((a zIxgNbUZ&5Rr^&ONeS3KIbcZ}=JMVP8{aR0PH@oj{+CP+RFz1xbU4nEk92)$ft$n(&+KWao+jpCZBoprQ@19+QaR2r|~ue zKW%S0Kc8s}`f2*Ja2O~9y8su z?P-B0-QDD09#=-rUS{!>OV?q_^XJjc-@K7uHmyD2OdS6{$KR)~;Agg-N5^KDlS^OV zoB7ksCm%fz@Hg>%JWam#yIc0V+*3wJn!IP|aj!?t%mX-iJkQ;iFAu(XzjWN)`T8CJ zZXKU5tnN1Rq?`7=am{i+(s=coGN!I{SZ#OSI4_*%t>3=A-|hNj^qF+|=FN2nnWz0_ z_;~Xi9qzKh-3;bY&pW=Wy_s*baRGO(FL+HWmSh^My)ym{F7`}*^A`};~>c4xb+y?Miv;`4_%yNNgZ4DH}q{7sx^Jl*Bj zd-u`npl#oB`f+zQJ;=9z_w2Ml-x9^nA%$rXJ*W7%1h&$WY&$)RX-}iflT(h6MKh5}o{d1^0-E6<%S$TXe9?!aGx?Fpo ztvtj9edb}$``x+sovjy_Z(nKM!Hf&;w8Ol=9^EpuW|=0x$LHxe(O_|ehI^rO{ zJ?{Sbli_LJ&(U88-HZ#}XFKoR5VIZa!93TqXFKw_%j(pF%eP#ao1@1EH}7(3_}%k_ zw2*fHT8;DK?OxovuJyDp-wtVYberw*=Kak0uzb5$+10uOuIypnT$)Zj-NCKnzT^2! zecf{HAq}@(xIFE={5Za5SpD90;k$3pOuD;g#C6{BHZ5zPP~CzWjFa+?l=koA9K2j;Y(-40W9C{keF} z`#Jn{GhcgXKif~=`6+n=zUj`R^ShlcmrvfWk@d9yuMfk2%JczRxp3Gw-+h$#XSq2Y zW_tVjbjS1A`SHB*!M#&94vzc%+2iBx-uSq*_TbjLxz%{%fLkh4Zo6=NtHsgl-RHVJ&n?f}z2L9=&cp?L zd)b@ohShgp9gp+by3<0t&vuyiubuVt`HY*{y$&~T+0dQdZsP7+M_%A<_UhLeO}=}5 z^f<^1akQpvy4%bvcXM)iXipt!!OU}*ukW%ZmH+7(G+q<$nPz-&^Ecz0p7YB%{(PZ( zQBQZ8y<0!7>F1R$v$GwxLk~^IcYJzYKD)aKX`U6=9!wm6Gu`CdnbzH_{f0abA3p#7 z^Fe%wH_I&20^ZD{f4R-l2cfL9m)h*{wA)uyuBRXJ!SLk-W_z_IXBPKj0?PVh39ei2 z`#w+C)A#T)&3rzCT~B&@u+Y0uwh3OC=K9_Y<**ux*@o!4{c%PiMj7T3$#gpWS>F zeY|yt&C>MH;*b0NetT2aM~|;(5f^Zt*S*iA=f&%a3wfG!`||dk?c3EI^t9KtoNk_- z`EI0_S;Q@9y1LU%-fn%owDrrGeGiZ>W6E|1{&Su8U#96=Up+xjd-`(xcYZVNcMd17 z(@o!J@-zeQxxRkRJHNNAe6yDxy19Bi3mKfA?#=1K^PB6nr|FkZ1ABi?rN`s=rh7fB>q&pgr}MY#X%A^ld7KQMkFE#Lmmc=sef{ylv)XxSW*Xpl@y$58 zXW^S+J#I6tzuOjR-P^0GJEvmkD|NxbKc`ue^)x z#BaVm&uYu1_xX^Pqf7tULzka?{TK)F;z1u;LJ8!tN z^!4-5cCIh}w7%b;xH3VO+if1#*LPZ(?T8P$>`huzK0f5fuV!zCC(rWs=6;5o?*`BA zwtL=s>^t+j+ZQ^_eD-GEJKL>ez40>mIMBrtfA5>4?W8ZK*K>CIoXgpDq;hdhqYj1S#7Q?^NOp2YcVvOL}GJnp#Q_6)u8%N_UE zcOJgf-PgUncwAFgTKmqsQ|``mKmA!NpB{AI`;*VJny>KnR+k*_TGD?R~-5^W1s%=awG7p8GSV-QKuxd%K_bxSkW*120Y&?Pb3Fym|Z_;+nX+wm%Iv z+a5f7b4Pi9PCg#}=IT95zS+&!49(s7(`1@Fxb#4y$9=c+Hv?a@o<*D&@7~LmOIzUl zT$u6s=zH7g&3c!2o3!p;VQ0E~z3q_R&S%Eyqc<0_`CxTilXv^`cJFJsa_(j~@AB;- zJ)G-ncbsSEu|M^t2cC1C`>E&ldA?k?{hqva*!kW1n|$@`-o3fs__+J;|2$>lLVo-0 zyY+6K&ERkUewOwizxxwcS4hkIIzI3AxL|LF^{3z6GV#IP40sdQzB=zv)#r z&|k)R%je{4hV9D}w|~xf#`>0%p_dOlPtTRvxjxzE^4q&w9+^Dd-+AvG@6O%p33BH; zU!#0C-@S0>4QaThoo2?{n_;u1a^77D@^BkJV)8xVB!L^6mm%gu; z>GMstxt+-}?K=7U=aE~N-^s6IwSGTr2TvSdT)^?Cs$Ta`QwA0bDj75GrCjnXXtCvciQQ7+0%lKH0ama(=w+ zfuHxV@2?NtGwJ5`*wf_f-RV5tfbU*^ds@7S)3fvTb?1lm#_ipnKhVzg^-jaDS6AG2 z;hT6IZ=E;zIrnDrhI;*Z%ZKOhUadQn4SeT%|2VpD zAMf_94_{Gr|V#@+QbLt4Q3|5<<7<4fDmAlHvHqP+gEGf z*X_|~%I>u133Y5|JMLxpc%!@Ny1LW&bnvC|Gy`w@>N<6#*=6E_9v!PM$Ge=J*4*dN zO}_QC!tB^-+gH}$0twMEAzysm6J8&K~I_vzXsgR{b#%7w|BjM9bX=n=S}0K-yA-lS6pzLhyD6> z)tASf@7{6vX59YV@oZPVkk;S*|Lt?f!?#^v? zwQ}8FdX}%Z8@hGa<(v7Q^t?LOXQ$-@UAN|z<@en5IP*zQlQ;RBq1<-b(3EQwi)s^)AtpZ z7U!Qe;~<_cM>E$G*B#1*cG<&xeV32jtPGB?KZC;Z?mKhy-S8uC9-rs-xq0;YQFZ5$ zZ{C~^T62*f@`m)Ldpl@!8gD${WZdaD*PY&6EGL(K^J)6i_qM0+E1tI*R`=^vZ!=H1 z-Iq&qw});WhxzGyIuHNTy<9E62fg!YxbHrXEyMMj>JZU%QbLnhu zM?0<==sS<{*4EeVxiYKM__~)XgEODJ`SdnJ{u{Wvt9+o_Z=d^R{(S2h{Ovr=C*OM7 z17DdZtnOLOymyAz+|RMzba&GpU#}T&Z-#XIVcx&K?mI8eZhEGh&)jF&^M*W|KkWPK zpU!8>&^FUvpU(ZWUXH&T_?x&UZxhdB@}$T03VYj|M}7Bo%gCF&?xqfVH{9Iat#5hR zxOUItiF?}P0=+5Q9**Pveslcy=k5;qcR%d==a{}dcWyIwxn|tvRA-1m0tHOtW3 z%kJcBhMl`_u04%j4rkKcdH7AaCal)J`SyIbr+Z&{ei`Zte10$BnmTd!b9tJJ&Be=^ zxSj1ji+qzuHV`fk=o6p>z7hE&w*%_}3 z-+hCJzZvrFlwI!0lO8!9Jfbu1Sj zaCzF@_VwVJLGL~3JZ{h6zxh|`$6Nf97FRapb@T5$uCMQ#(Xm{6ydHNm;QiYE+Ws|n z66d{K;6K;bA0v*Zi8JH23*YqF*7M|DpFH1AJgu@=2cFZNzP!%3_3<=iwmYw1qwBFh^_KSx zUES^bHIt3k*W`(p+rB(OuAL93dvp5ohi9{=Y&G_%Ym%iS+Cmq)w@`GoY>FpuU z+1~Gb&E0dzEsvwG76+ci|MdCN=)32=?|S3(blY{fY5nj2`@jE>j|)6aIsD7;`Ig?w z(+yAj`snM&<%{obb}xrd3x5CbnfZe3ale19@zI-rbC+%BSsjer#C8F*#Q<@4d~-MzYl zdwucsEaJe|qg#$wUpGCj9k)A8?#`@NzL|f)clY48x7_C2(}CvZ*WJBZyN>m)=jPp; zZI5sEXH{_9_cPc%dv9F%&>isg*^hGG;{&Zd#k6-XmY0odhy3=Yn@3-_4!gVD?%km* zetYBKiI>Z>|K5(*)U6M%JDw*lJ=mLcUK*aiJM4e|C$7(zN8X-Srd`kOGI38R6ViAO zbDk$dYo@!~d7AF#f@kxqxAWZl?W-g3`Cg-2Kh5NS`rLA_($BXi-#*vw=cbNDHF@H6 z;hGD+xb4^+EpCzDeQWV|UpJqA+)kaiz1z2AwfN1ngG~OLlfyUHU#6SS%$Ei{Cf z^!uhgP27IA@@}2^;uf-=$K%c`-|Xhu*$%SnPjhc){N8uJu6!Xqv_ENaz8}Z~W%l=D zd|t2a?bRK(i0`J$$JG(qZ}7(-*Lh#xo%O8scplBnL(}u5zmhO>S@aJoAE*Kxz62wcevNT&y%6=bNI~dirZ)R zHD2gx^4aem+4b?(*&g<@cKiE3zB}BW!=Cq^Lx0M6rkMxgd=5JwzNgqX7avyJJuN@3 zDc?*F_;a0izOMG$b9cIRtpDEK&0fLYv-alQ&#*hMTr-;X8&*ypaq}{yx zDDSW7K9fhkiQlQGsl&dx)$Sjc4nLN>!ROc4#Oc@!o#(W?zsBi6d&=j*>%#5lS}i`0 z$)5+Ovm11nx0dfn-`o%CjW$h*An z9hT$4mAm(De7id?tiQfE^Shm{uN$^MPRFggdHC+>^O^bEzx#T~>%DXGX1bRQZKvM6 z?J_(0(*tjOcQfGEgERS?VRLzUg`=ML!+d?;J~{Kwc5m9vrz=mKo@OxRR@WiNfAe>i zp1;4(|H8-1bK-8lXRdbd@@3b*zWCK;@Vq?T@SQiFRxW=tJZb6itG~>jZ!$Xh-Mmje z-ll&3xUhFSuBmf9x85Cmo_2nHxZ9(DXWpl~E_|NQ9q`TjncwO8Li*X>?|Rc9HNIb; zbkCz{DAV4|!`F>(>ao-C=IYy3AJ2BThcvpJneKV*y!`u2dh_OKdd=<56XL+V&wH6Z z-j>tX^s2)y6KCHke`nSdtYw*@o`?VW**PGJ!ye=eQ`&9{yL`Lewm%S-~PSZy~-@^&yf~)`}vwY z?fIHv_rtusWzs`h^S*KEy5d6j{dx(*Z%%ifc7NXG3Gq#x-2vAOdia|0d3VPx|35$T zw0r7~5BrRI!cpE|55A+G9~Yl_XME0ho@U>d2AsVc^0fPGdBW3Y&qw3GpDoAR_A-9F?JX1MYo4z=$gOX8o|pOKZL@LR z!ESEH?s?rib;!HT<;%r~cz1iix2FZqO5=axR=4--p*=ix`Sr5BdG{_CmTM19uNN_Q zJt3Zd_tm!J75dEa)0_02`QYZez?bGcLyC`zCHMSJf4Zq z!@tv>zL2NaV{h(r?B(q*^Ss6PTJUcsE)VVYua+n9w0n-u`{rpb^ln$Xf3L9KI={ne zW%A*g%XQ=8_ihjLop^uW^2XN_c-qr>x&cp<@6Kb|>CIcqgEx7afu|V``~LHq&!0o| zcv?AoKR5q%deX`?^E3f(Zf9Ir?^FJ`e%c@ZqdrTBZj1ow7q=D6OzwAJOBd7p02xVyS( z-{o%Ivpug4S=!F@5Qp#He0NyScE$&DHG9*aY58`S-F$nma;vW=o_>4c!`%_LIeODe zS2Jv;J@}raJx@QLW*v~W{oNtmT)!OdW&ZT*k?mgY&hj<$_4#hdo4n1i{yaC*@l7v0 zZSVGaOgm3^xo%i)JLygQdfUTlGV!~wFW%jLf1YmKkGj{x_h;$jmG85ZYlisMaJswg zawg4f#)s~3KO-(3@A>9t_~Jr$;QemqrO9>o3b%IH^G@s9EKeNpcKbWOJuj^p;zKvh zfBy&fGJbm7`Axa*Aon!O$A{I+-5lS`Ja2h}Jn68fr#&8L^6EDEH$!Wt-+v}&OGTm^_uxt zcQ?!K^!G*BHBlP~_>w_8u1 z;CW`;cHj5r^MyR2oxXkT={R@(AbYN#c0BR=bnf>$X+Ym+?ykEj)6DafO_Nzqd{~ap z)YsjfpN9L6fAZp*LD#v?jcdWnCSiZfNj?K5rHQhVg_36nQ@Vn#ItB1NHE?$9D&Jb3r$c`|Ic6*Zt(7>1p~q3#X@9hnpU^|NY5xcRpTwd58-% zcOC70eQCRw$rIY;nz(p~)6*W(`jT~;dq+?UpCx4Pnx|M z_+<`z-f_)7gF98OJ-*k??QL(Cam&Sp?aBl3VRhM^$Mv-zC*Qn#aZT{7el3M~bN76H zZ>M($pKH1J?r{6kAZ_*CdFp64^X$&YlNQ`M<2EZ74>+0bb}xGV<@n8X+4kV(dCDK> zd4GCVO1b9t?S20~%9jqF9}l)D;B8PD(cNcVS0D-+g(#~sIcr|oQqT(mfMdpvJ^-0IzQ``wj4 z+`hw}&NRAx@4ET7tNniNo$FqoKd*4|%{czud1RV_m+m&>e}vO=-q#m%-`knq?bRJN z!+(_b*C`K8K5l*OX#sD_y6-7h*1pqDckgVk*UamVyS?e~q|2H-?q+$tx>l#f1^#or zZ`mfF-E-Y^yj$nq>-n?j`;7eZJ9*+f%kGxV8}RLUL9e@+H>{@r&gSPU(;e35-VVBq z$!o^*Hofp>{;=M|9BzF)al!pG)9cy!wC9nx=Zo`-U!;e)d-9c~Ew`Pwp6}-L;P;Mm zmu(Nc?YQxW^eNPUaC7@Ztpno&qeavwR!&Ar?+{(bTxylsWYuzet8^EGw5iS zl}p3Zy2E;OZ&#k~-}>y3;n{q9de~hrK21LE)_)nNyIkH77j(79?{nPWap~nkduaFj zadYuE^5#Q-y?K_?Dcfyt`hC$()7!mW>rV@D!Op*TJAS=9 zuY&1tZ{{hdmrq}}*LqgRH$hL&`fksg9(eXycwDp0&h_YPhP04B=sD_JPs8!xJZp7Y z-0rf?AdAnBJC65f>%RMzH6t-U3fk5aY2^WTy6KXAVWLs;c0w5V|UNo$)l$m zZcf+oyO&#!&(rjIZ;$*=x#h|&bmocYz4y(m2QM%8^lZ<%XPNcW`*SJK?9HARAMUxD z&!tn>o!OiBw4e9wiRp&DWt;Sy>kjwiKk7U0*5l@nZ^!#P%1yg{`qF8q6T`R>vbR~%3K0$(Pi#f7I`r=0sF-tWKuvHq2h*Hf45+x_0| zzWofkc+yIm?YiY?JKGmB<>7t?zTA_hFD-26az}l?)8r}g^rn5i?Yw1gO<(-#y3BNY zv)5*}hnK!NTobnM$&2$0{$?Hc=4x`DvtHTt(`ow5be(bCVg2!euSw6FCbOP6$g|)} zkJo9B$MN9wEWf#Uh;KKSj|&|bE*1VAqbT{MNo`sij z^ETHj|0Dg@wzID8pchvz;QjM#+)11t!+P37x+%YzxZwMDp2NQL-MnT!?ag$z4!k?h z6Y_;L{|>Q?ALl;9Zk9Re_t#BVbA9arzdLRH>3f$8abE71@bPwc;+y*J&ij_(-)y^Q zuO9E;0k;SG-H&g=?JpB{+T&jj_=`P#ku9v}|lL$}>rUcR|rJ$dDxI9+MH zzIJyfo=+d&cH#2u&eIIrahUh}t<|%yoZFwapv7$$pP9z9c^tjl^J%zdUmv&? zPcIYd2zZkwmj}ns+e|atgZuXRapUvW8RG8OOPII*9#~J34t$)wnI7=x`sru9d6{yc zue&|$v-WQ9$E(MT_v2^}I*$8%w)k$(=?>e|O~-e)hxF!p+IiC5JUj1f{+rwTDVrWV z$1|JZ&K&pgw;$KncU`i2me-?$*OW=eHNVU0k-IbTp`UX--TIc>U9Xu&U#_nBMLO=z z@X>kA^fa&d&EWY=y!q6(J$61heer+#@#1U7x$VtR-sCZLr^lJ?X%HXy&0BZNmWea% z?q-PB>3#_xZ@Th?Zk{`nF4rE`W8ZoE%i#7o%iHVWX>RZC{Jd#69k|Wec|GI4Wz)lb z<4n4qZdx0xiVqn!7+us%6lb6>B(=i7SiN&Io{)j#9E`1*Mke>23N?d-SD z?HS$6J@Gtz_2&!iboWcR-tMehrn@d!p0-@R?MUnP+WX$S%QtUquYNPHy_q+}o9%(# zo+iiFy`Fry_423d*@?$(=Vo>D-MRbzu1B7}y>)bl+t(fBj_bTH$D?PvXw8r&d*AUo zcGt&m`u98i@Bjbgi|#s{AP&+A^D-{+z= zZ{&^Rk&UCZFL>j4ngPdSZvHsm&y_LDc=75n_gT8*(!nl+Z{qaR@NUn+?`#h{Z{3~m z{(Lx-t}kAm=AY@hL!L!`88csd*!?hvTYhsg=|K;EJJMFaJ?{0?spE-H2l?*y_3zEU zdb?b6@7?wAb_e&J-CGatB)@*mX?eYH`<$meUr5_$bwj$|xb@>Vn;!f-1lsns$A`RU zJMZrM&(3{T&b@W%sRQ`Bqw_bhqP|YY%B>d;4To z&)5I{|381c?$5)YM+Vn@?{Q&!4)gxZo2fT!#@>9=mQUY4yXUVLx1Dr7-T4Bp8SrVl zyK%hkba)kgytz%kf6{T~z2urW{j|-+b<=p8aXZ`hRW9T+)9vv$(sZV+e{(!HxBm6; z;rQZlo@Z``zsbMZcHpfC((YVbx+!PMK24c4{y5*ac-Crq;`XJh37fMw>)>hPnqKjq z#ot^#Pq?|;-wpg`eEUw`X5F-Wu%6X@mN;3HuRECSX<_-pygy&JjL!Ta?QB1Nm!aHQ zd{a-8ueqN-7J+AM2^5+evOq`t-cY`Os-IQs@n>?%g_0CrZzqz^gu$sHEX?oE-yQ#Cu zw|m@zXS40=Yp0iO5B8>>opfDg?CaCDJ?%H-b)>h)gF7v*KN|v`uZg3-jGx~2Jk5N) zf=n9F(s9l8?#|b|c{k5@cf42L&>dv@4Bb8peYvziYkK7i-FezgTz5Nk2V8p^t{LRh z+TFDH(Cz2Y9oH^{-=4VrGr2oFW%M1#d1hQ*u-`LZ$al8$zBzvu$H~*5vQM5kURk~- zJDUw99tmPU`)&S&zM$KX1E? zU7xu=&!Nfi(Bs>6<8RJQFBi(KM!%ow`_JmJo--tLyy?@r&}NBp|?#+hlrxBY4EcAlmk-yQgQ;&^Gz zKx>A>zTa_8{d&35Ag!-OnrF(zFewEJaawk-Q8#6b?0f{muIBy_pIGC z{y1HpV|7?|$8;$86ty|JsM=uV(x}+Rk?U_&j0x zw6M>yALacyx;L+f|Gr-j`+U>29ddE9GBmt-KR;e(r%pX-anMb>JMzSZe8K)AFP@ff zJL7dX`?+0j{%+{r=kn8Od*hnDblUNzXEcHL?oE$-($ki=H-mYuub(SUKA%^-T>5wS z`tyanXZ!kd@Exb4nP;c2+p~PT%)Rejw_9H~=-8RIe{T4EGCJLQ`MMYQIM2ED?tH=C z)aCn0`rWgk1xhL39xc@~HL+KDr7zt6e4XY4G;W2Vdh zvi9;>E@HTIh^Ga(ECQdG`{r0Rc zEi89uJd^Jgn74iYX7Tb(dhsXT=SXi4eC_cIT$8UEX!qWohu#EP8Tb0{e0R`c%5>j7 zab=db$3vV~+~)8*)7^K+k8Zu5QHQQLcY8>K!<_f^wFmQDC%fN2)x#$rC)XbCJ8nDO zJMG`~%J1_%2iNqye|;hD39D^SeD`wY`SG{UbMx}tJv!F2``vkK^0&`(nzyHpaPw~2 z^~AZ?xBKc(Jl$vFX~M0QdE&N!uV07s{GmPUj*DL}-z)g}#*@!W7vB@F*IaLVy&)ZVnmWz(=rr@wo10lJKG5ae zeLZn6!T#ripL%3hSrG`}2%4p}@;9Gw=k?^J$<_SZ=58)$ zm+9VJ?&jTQzHvP0Gp0Llfum8Bde$DHU)mLV{t1q9=-=+JUdhSeI9$^u3o$3oqm6o1X`~toaIjX4>Ny)$q2bnY=sG!g6JF*xkqdeqWs@ z?g{%0-ko&W`+h(5{dK*)dE#VcaJQb1HxDe|zBzkd**Lqb$(JYKo9>}N=mYjy{I^RUOedYL@S@ix=JbKT7+?*9QV CgwrGd literal 0 HcmV?d00001 diff --git a/9_Firmware/9_2_FPGA/tb/cosim/real_data/hex/doppler_map_i.npy b/9_Firmware/9_2_FPGA/tb/cosim/real_data/hex/doppler_map_i.npy new file mode 100644 index 0000000000000000000000000000000000000000..315587d5d5eb78980a076f8e047b8fcf48c9f5d0 GIT binary patch literal 98432 zcmeIuu}T9$5C-5&KnQt??bg6Gh6F^c(IlL009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009DL7U*^C@9KBNJKOtIZ9{GQw<$YF;h%s0Xv;P`cPV#qIGg*i)lY%uk7IM@2@oJa zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk Y1PBlyK!5-N0t5&UAV7csfxiWQ0gUxCp8x;= literal 0 HcmV?d00001 diff --git a/9_Firmware/9_2_FPGA/tb/cosim/real_data/hex/doppler_map_q.npy b/9_Firmware/9_2_FPGA/tb/cosim/real_data/hex/doppler_map_q.npy new file mode 100644 index 0000000000000000000000000000000000000000..b73c2f8c11d0b13f17192170680bb71f5a1ff6a5 GIT binary patch literal 98432 zcmeIuu}T9$5CG7NXyaFGw+h!pA_TFvGo-Pwk|Q}W5RBYOn&4ODXC;NYSJ*7c1?+@3 z%`!7Pv+UbW9`2`)gJNB5;=FEWO}q*5b}@=&h|jC$y{TTGR?WQb>UY($ty8^ysov_8 zUtABb$}k#Vmf<7(EzS!;fB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB=DiF7TbA?+b&zX6k(%neQ7vNzMHIJkQ_%&AWSNX?~Z^ZciTho_+n(RQx2LnHk literal 0 HcmV?d00001 diff --git a/9_Firmware/9_3_GUI/radar_protocol.py b/9_Firmware/9_3_GUI/radar_protocol.py index eb68986..9976f13 100644 --- a/9_Firmware/9_3_GUI/radar_protocol.py +++ b/9_Firmware/9_3_GUI/radar_protocol.py @@ -68,7 +68,7 @@ DATA_PACKET_SIZE = 11 # 1 + 4 + 2 + 2 + 1 + 1 STATUS_PACKET_SIZE = 26 # 1 + 24 + 1 NUM_RANGE_BINS = 512 -NUM_DOPPLER_BINS = 48 # PR-F/PR-Q: 3 sub-frames * 16 (matches FPGA RP_NUM_DOPPLER_BINS) +NUM_DOPPLER_BINS = 48 # PR-F/PR-Q: 3 sub-frames * 16 (= FPGA RP_NUM_DOPPLER_BINS) NUM_CELLS = NUM_RANGE_BINS * NUM_DOPPLER_BINS # 24576 WATERFALL_DEPTH = 64 diff --git a/9_Firmware/9_3_GUI/test_v7.py b/9_Firmware/9_3_GUI/test_v7.py index 191acc3..9d36f8d 100644 --- a/9_Firmware/9_3_GUI/test_v7.py +++ b/9_Firmware/9_3_GUI/test_v7.py @@ -483,7 +483,7 @@ class TestWaveformConfig(unittest.TestCase): from v7.models import WaveformConfig wc = WaveformConfig() # MEDIUM has the largest per-subframe v_unamb (smallest PRI). - # K=6 default → ~266 m/s; well above UAS speeds 50–80 m/s. + # K=6 default -> ~266 m/s; well above UAS speeds 50-80 m/s. v6 = wc.extended_max_velocity_mps_crt() self.assertAlmostEqual(v6, wc.max_velocity_medium_mps * 6, places=2) # K=3 should give half of K=6. @@ -669,7 +669,7 @@ class TestSoftwareFPGASignalChain(unittest.TestCase): from v7.software_fpga import SoftwareFPGA from radar_protocol import NUM_RANGE_BINS, NUM_DOPPLER_BINS - # Production dimensions: 48 chirps × 2048 samples. + # Production dimensions: 48 chirps x 2048 samples. iq_i = np.zeros((NUM_DOPPLER_BINS, 2048), dtype=np.int64) iq_q = np.zeros((NUM_DOPPLER_BINS, 2048), dtype=np.int64) # Inject a single strong tone in bin 10 of every chirp. @@ -803,12 +803,12 @@ class TestReplayEngineCosim(unittest.TestCase): if not self._available(): self.skipTest("co-sim data not found") from v7.replay import ReplayEngine - from radar_protocol import RadarFrame + from radar_protocol import RadarFrame, NUM_RANGE_BINS, NUM_DOPPLER_BINS engine = ReplayEngine(self.COSIM_DIR) frame = engine.get_frame(0) self.assertIsInstance(frame, RadarFrame) - self.assertEqual(frame.range_doppler_i.shape, (64, 32)) - self.assertEqual(frame.magnitude.shape, (64, 32)) + self.assertEqual(frame.range_doppler_i.shape, (NUM_RANGE_BINS, NUM_DOPPLER_BINS)) + self.assertEqual(frame.magnitude.shape, (NUM_RANGE_BINS, NUM_DOPPLER_BINS)) def test_get_frame_out_of_range(self): if not self._available(): @@ -849,7 +849,7 @@ class TestReplayEngineRawIQ(unittest.TestCase): from v7.software_fpga import SoftwareFPGA from radar_protocol import RadarFrame, NUM_RANGE_BINS, NUM_DOPPLER_BINS - # Production dimensions: 48 chirps × 2048 samples per frame. + # Production dimensions: 48 chirps x 2048 samples per frame. raw = (np.random.randn(2, NUM_DOPPLER_BINS, 2048) + 1j * np.random.randn(2, NUM_DOPPLER_BINS, 2048)) with tempfile.NamedTemporaryFile(suffix=".npy", delete=False) as f: @@ -1082,7 +1082,7 @@ class TestUnfoldVelocityCRT(unittest.TestCase): v_unamb, v_res = self._vu_vr() v_true = -75.0 v_meas = [_fold_v(v_true, vu) for vu in v_unamb] - v_est, conf, alias = unfold_velocity_crt(v_meas, v_unamb, v_res) + v_est, conf, _alias = unfold_velocity_crt(v_meas, v_unamb, v_res) self.assertAlmostEqual(v_est, v_true, places=1) self.assertEqual(conf, "CONFIRMED") @@ -1105,7 +1105,7 @@ class TestUnfoldVelocityCRT(unittest.TestCase): v_true = 25.0 # SHORT + MEDIUM only (LONG dropped out, e.g. clutter). v_meas = [_fold_v(v_true, v_unamb[0]), _fold_v(v_true, v_unamb[1])] - v_est, conf, alias = unfold_velocity_crt( + v_est, conf, _alias = unfold_velocity_crt( v_meas, [v_unamb[0], v_unamb[1]], [v_res[0], v_res[1]], ) self.assertAlmostEqual(v_est, v_true, places=1) @@ -1117,7 +1117,7 @@ class TestUnfoldVelocityCRT(unittest.TestCase): v_unamb, v_res = self._vu_vr() # Random per-PRI values that do not correspond to any v_true. v_meas = [10.0, -30.0, 35.0] - v_est, conf, alias = unfold_velocity_crt(v_meas, v_unamb, v_res) + v_est, conf, _alias = unfold_velocity_crt(v_meas, v_unamb, v_res) self.assertEqual(conf, "AMBIGUOUS") self.assertAlmostEqual(v_est, 10.0, places=2) # PRI-0 fallback @@ -1130,7 +1130,7 @@ class TestUnfoldVelocityCRT(unittest.TestCase): # Pick v_true near the advertised CRT ceiling. v_true = wc.extended_max_velocity_mps_crt(max_alias_k=6) - 5.0 # ~261 m/s v_meas = [_fold_v(v_true, vu) for vu in v_unamb] - v_est, conf, alias = unfold_velocity_crt(v_meas, v_unamb, v_res, max_alias_k=6) + v_est, conf, _alias = unfold_velocity_crt(v_meas, v_unamb, v_res, max_alias_k=6) self.assertAlmostEqual(v_est, v_true, places=0) # within 1 m/s # Should still be CONFIRMED for a real velocity at this scale. self.assertIn(conf, ("CONFIRMED", "LIKELY")) diff --git a/9_Firmware/9_3_GUI/v7/dashboard.py b/9_Firmware/9_3_GUI/v7/dashboard.py index 52c9280..31e5cb9 100644 --- a/9_Firmware/9_3_GUI/v7/dashboard.py +++ b/9_Firmware/9_3_GUI/v7/dashboard.py @@ -94,7 +94,7 @@ def _make_dspin() -> QDoubleSpinBox: # ============================================================================= class RangeDopplerCanvas(FigureCanvasQTAgg): - """Matplotlib canvas showing the Range-Doppler map (NUM_RANGE_BINS x NUM_DOPPLER_BINS) with dark theme.""" + """Matplotlib canvas showing the Range-Doppler map (NUM_RANGE_BINS x NUM_DOPPLER_BINS) with dark theme.""" # noqa: E501 def __init__(self, _parent=None): fig = Figure(figsize=(10, 6), facecolor=DARK_BG) diff --git a/9_Firmware/9_3_GUI/v7/processing.py b/9_Firmware/9_3_GUI/v7/processing.py index acc8bf4..f519f7b 100644 --- a/9_Firmware/9_3_GUI/v7/processing.py +++ b/9_Firmware/9_3_GUI/v7/processing.py @@ -573,7 +573,7 @@ def unfold_velocity_crt( alias depth k_0 ∈ [-K, K] generates candidates ``v_true = v_meas_0 + k_0 · 2 · v_unamb_0``. A candidate is *valid* when it folds back into all other active PRIs to within - ``tol_factor × max(v_res)``. + ``tol_factor * max(v_res)``. Args: v_meas_per_sf: signed velocity measurement per active sub-frame @@ -652,9 +652,7 @@ def unfold_velocity_crt( confidence = "AMBIGUOUS" elif n_sf == 3 and n_cands == 1: confidence = "CONFIRMED" - elif n_sf == 3 and n_cands == 2: - confidence = "LIKELY" - elif n_sf == 2 and n_cands == 1: + elif (n_sf == 3 and n_cands == 2) or (n_sf == 2 and n_cands == 1): confidence = "LIKELY" else: # n_sf == 2 and n_cands == 2 confidence = "AMBIGUOUS" diff --git a/9_Firmware/9_3_GUI/v7/software_fpga.py b/9_Firmware/9_3_GUI/v7/software_fpga.py index d2963c1..ca44972 100644 --- a/9_Firmware/9_3_GUI/v7/software_fpga.py +++ b/9_Firmware/9_3_GUI/v7/software_fpga.py @@ -192,7 +192,11 @@ class SoftwareFPGA: # to math-generated twiddles otherwise). range_i = np.zeros((n_chirps, n_samples), dtype=np.int64) range_q = np.zeros((n_chirps, n_samples), dtype=np.int64) - twiddle_path = TWIDDLE_2048 if (n_samples == 2048 and os.path.exists(TWIDDLE_2048)) else None + twiddle_path = ( + TWIDDLE_2048 + if (n_samples == 2048 and os.path.exists(TWIDDLE_2048)) + else None + ) for c in range(n_chirps): range_i[c], range_q[c] = run_range_fft( iq_i[c].astype(np.int64), diff --git a/9_Firmware/9_3_GUI/v7/workers.py b/9_Firmware/9_3_GUI/v7/workers.py index 406456a..8b82eac 100644 --- a/9_Firmware/9_3_GUI/v7/workers.py +++ b/9_Firmware/9_3_GUI/v7/workers.py @@ -196,7 +196,11 @@ class RadarDataWorker(QThread): # for SHORT/MEDIUM sub-frame bins until PR-Q.5 replaces this path # with extract_targets_from_frame_crt. v_res = self._waveform.velocity_resolution_long_mps - n_doppler = frame.detections.shape[1] if frame.detections.ndim == 2 else self._waveform.n_doppler_bins + n_doppler = ( + frame.detections.shape[1] + if frame.detections.ndim == 2 + else self._waveform.n_doppler_bins + ) doppler_center = n_doppler // 2 for idx in det_indices: diff --git a/pyproject.toml b/pyproject.toml index 7dd0037..096b43a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,3 +55,5 @@ select = [ "test_*.py" = ["ARG", "T20", "ERA"] # Re-export modules: unused imports are intentional "v7/hardware.py" = ["F401"] +# FPGA cosim scripts: CLI tools — print() is the intended output channel +"9_Firmware/9_2_FPGA/tb/cosim/**.py" = ["T20"]