diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index db24bea..1a3fd51 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -8,100 +8,77 @@ on: jobs: # =========================================================================== - # Job 0: Ruff Lint (all maintained Python files) - # Covers: active GUI files, v6+ GUIs, v7/ module, FPGA cosim scripts - # Excludes: legacy GUI_V1-V5, schematics, simulation, 8_Utils - # =========================================================================== - lint: - name: Ruff Lint - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install ruff - run: pip install ruff - - - name: Run ruff on maintained files - run: | - ruff check \ - 9_Firmware/9_3_GUI/radar_protocol.py \ - 9_Firmware/9_3_GUI/radar_dashboard.py \ - 9_Firmware/9_3_GUI/smoke_test.py \ - 9_Firmware/9_3_GUI/test_radar_dashboard.py \ - 9_Firmware/9_3_GUI/GUI_V6.py \ - 9_Firmware/9_3_GUI/GUI_V6_Demo.py \ - 9_Firmware/9_3_GUI/GUI_PyQt_Map.py \ - 9_Firmware/9_3_GUI/GUI_V7_PyQt.py \ - 9_Firmware/9_3_GUI/v7/ \ - 9_Firmware/9_2_FPGA/tb/cosim/ \ - 9_Firmware/9_2_FPGA/tb/gen_mf_golden_ref.py - - # =========================================================================== - # Job 1: Python Host Software Tests (58 tests) - # radar_protocol, radar_dashboard, FT2232H connection, replay, opcodes, e2e + # Python: lint (ruff), syntax check (py_compile), unit tests (pytest) + # CI structure proposed by hcm444 — uses uv for dependency management # =========================================================================== python-tests: - name: Python Dashboard Tests (58) + name: Python Lint + Tests runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Set up Python 3.12 - uses: actions/setup-python@v5 + - uses: actions/setup-python@v5 with: python-version: "3.12" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pytest numpy h5py + - uses: astral-sh/setup-uv@v5 - - name: Run test suite - run: python -m pytest 9_Firmware/9_3_GUI/test_radar_dashboard.py -v --tb=short + - name: Install dependencies + run: uv sync --group dev + + - name: Ruff lint (whole repo) + run: uv run ruff check . + + - name: Syntax check (py_compile) + run: | + uv run python - <<'PY' + import py_compile + from pathlib import Path + + skip = {".git", "__pycache__", ".venv", "venv", "docs"} + for p in Path(".").rglob("*.py"): + if skip & set(p.parts): + continue + py_compile.compile(str(p), doraise=True) + PY + + - name: Unit tests + run: > + uv run pytest + 9_Firmware/9_3_GUI/test_radar_dashboard.py -v --tb=short # =========================================================================== - # Job 2: MCU Firmware Unit Tests (20 tests) + # MCU Firmware Unit Tests (20 tests) # Bug regression (15) + Gap-3 safety tests (5) # =========================================================================== mcu-tests: - name: MCU Firmware Tests (20) + name: MCU Firmware Tests runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - name: Install build tools run: sudo apt-get update && sudo apt-get install -y build-essential - name: Build and run MCU tests - working-directory: 9_Firmware/9_1_Microcontroller/tests run: make test + working-directory: 9_Firmware/9_1_Microcontroller/tests # =========================================================================== - # Job 3: FPGA RTL Regression (23 testbenches + lint) - # Phase 0: Vivado-style lint, Phase 1-4: unit + integration + e2e + # FPGA RTL Regression (23 testbenches + lint) # =========================================================================== fpga-regression: - name: FPGA Regression (23 TBs + lint) + name: FPGA Regression runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - name: Install Icarus Verilog run: sudo apt-get update && sudo apt-get install -y iverilog - name: Run full FPGA regression - working-directory: 9_Firmware/9_2_FPGA run: bash run_regression.sh + working-directory: 9_Firmware/9_2_FPGA diff --git a/5_Simulations/Antenna/Quartz_Waveguide.py b/5_Simulations/Antenna/Quartz_Waveguide.py index d38751c..3b37b39 100644 --- a/5_Simulations/Antenna/Quartz_Waveguide.py +++ b/5_Simulations/Antenna/Quartz_Waveguide.py @@ -106,7 +106,8 @@ mesh.SmoothMeshLines('all', mesh_res, ratio=1.4) # Materials # ------------------------- pec = CSX.AddMetal('PEC') -quartz = CSX.AddMaterial('QUARTZ'); quartz.SetMaterialProperty(epsilon=er_quartz) +quartz = CSX.AddMaterial('QUARTZ') +quartz.SetMaterialProperty(epsilon=er_quartz) air = CSX.AddMaterial('AIR') # explicit for slot holes # ------------------------- @@ -191,13 +192,19 @@ Zin = ports[0].uf_tot / ports[0].if_tot plt.figure(figsize=(7.6,4.6)) plt.plot(freq*1e-9, 20*np.log10(np.abs(S11)), lw=2, label='|S11|') plt.plot(freq*1e-9, 20*np.log10(np.abs(S21)), lw=2, ls='--', label='|S21|') -plt.grid(True); plt.legend(); plt.xlabel('Frequency (GHz)'); plt.ylabel('Magnitude (dB)') +plt.grid(True) +plt.legend() +plt.xlabel('Frequency (GHz)') +plt.ylabel('Magnitude (dB)') plt.title('S-Parameters: Slotted Quartz-Filled WG') plt.figure(figsize=(7.6,4.6)) plt.plot(freq*1e-9, np.real(Zin), lw=2, label='Re{Zin}') plt.plot(freq*1e-9, np.imag(Zin), lw=2, ls='--', label='Im{Zin}') -plt.grid(True); plt.legend(); plt.xlabel('Frequency (GHz)'); plt.ylabel('Ohms') +plt.grid(True) +plt.legend() +plt.xlabel('Frequency (GHz)') +plt.ylabel('Ohms') plt.title('Input Impedance (Port 1)') # ------------------------- @@ -237,19 +244,26 @@ ax = fig.add_subplot(111, projection='3d') ax.plot_surface(X, Y, Z, rstride=2, cstride=2, linewidth=0, antialiased=True, alpha=0.92) ax.set_title(f'Normalized 3D Pattern @ {f0/1e9:.2f} GHz\n(peak ≈ {Gmax_dBi:.1f} dBi)') ax.set_box_aspect((1,1,1)) -ax.set_xlabel('x'); ax.set_ylabel('y'); ax.set_zlabel('z') +ax.set_xlabel('x') +ax.set_ylabel('y') +ax.set_zlabel('z') plt.tight_layout() # Quick 2D geometry preview (top view at y=b) plt.figure(figsize=(8.4,2.8)) -plt.fill_between([0,a], [0,0], [L,L], color='#dddddd', alpha=0.5, step='pre', label='WG aperture (top)') +plt.fill_between( + [0, a], [0, 0], [L, L], color='#dddddd', alpha=0.5, step='pre', label='WG aperture (top)' +) for zc, xc in zip(z_centers, x_centers): plt.gca().add_patch(plt.Rectangle((xc - slot_w/2.0, zc - slot_L/2.0), slot_w, slot_L, fc='#3355ff', ec='k')) -plt.xlim(-2, a+2); plt.ylim(-5, L+5) +plt.xlim(-2, a + 2) +plt.ylim(-5, L + 5) plt.gca().invert_yaxis() -plt.xlabel('x (mm)'); plt.ylabel('z (mm)') +plt.xlabel('x (mm)') +plt.ylabel('z (mm)') plt.title('Top-view slot layout (y=b plane)') -plt.grid(True); plt.legend() +plt.grid(True) +plt.legend() plt.show() diff --git a/5_Simulations/Antenna/openems_quartz_slotted_wg_10p5GHz.py b/5_Simulations/Antenna/openems_quartz_slotted_wg_10p5GHz.py index fa7d35d..fbdabae 100644 --- a/5_Simulations/Antenna/openems_quartz_slotted_wg_10p5GHz.py +++ b/5_Simulations/Antenna/openems_quartz_slotted_wg_10p5GHz.py @@ -137,7 +137,9 @@ Ncells = Nx*Ny*Nz print(f"[mesh] cells: {Nx} × {Ny} × {Nz} = {Ncells:,}") mem_fields_bytes = Ncells * 6 * 8 # rough ~ (Ex,Ey,Ez,Hx,Hy,Hz) doubles print(f"[mesh] rough field memory: ~{mem_fields_bytes/1e9:.2f} GB (solver overhead extra)") -dx_min = min(np.diff(x_lines)); dy_min = min(np.diff(y_lines)); dz_min = min(np.diff(z_lines)) +dx_min = min(np.diff(x_lines)) +dy_min = min(np.diff(y_lines)) +dz_min = min(np.diff(z_lines)) print(f"[mesh] min steps (mm): dx={dx_min:.3f}, dy={dy_min:.3f}, dz={dz_min:.3f}") # Optional smoothing to limit max cell size @@ -147,7 +149,8 @@ mesh.SmoothMeshLines('all', mesh_res, ratio=1.4) # MATERIALS & SOLIDS # ================= pec = CSX.AddMetal('PEC') -quartzM = CSX.AddMaterial('QUARTZ'); quartzM.SetMaterialProperty(epsilon=er_quartz) +quartzM = CSX.AddMaterial('QUARTZ') +quartzM.SetMaterialProperty(epsilon=er_quartz) airM = CSX.AddMaterial('AIR') # Quartz full block @@ -157,7 +160,9 @@ quartzM.AddBox([0, 0, 0], [a, b, guide_length_mm]) pec.AddBox([-t_metal, 0, 0], [0, b, guide_length_mm]) # left pec.AddBox([a, 0, 0], [a+t_metal,b, guide_length_mm]) # right pec.AddBox([-t_metal,-t_metal,0],[a+t_metal,0, guide_length_mm]) # bottom -pec.AddBox([-t_metal, b, 0], [a+t_metal,b+t_metal,guide_length_mm]) # top (slots will pierce) +pec.AddBox( + [-t_metal, b, 0], [a + t_metal, b + t_metal, guide_length_mm] +) # top (slots will pierce) # Slots (AIR) overriding top metal for zc, xc in zip(z_centers, x_centers): @@ -215,16 +220,16 @@ print(f"[timing] FDTD solve elapsed: {t1 - t0:.2f} s") # ... right before NF2FF (far-field): t2 = time.time() try: - res = nf2ff.CalcNF2FF(Sim_Path, [f0], theta, phi) -except AttributeError: - res = FDTD.CalcNF2FF(nf2ff, Sim_Path, [f0], theta, phi) + res = nf2ff.CalcNF2FF(Sim_Path, [f0], theta, phi) # noqa: F821 +except AttributeError: + res = FDTD.CalcNF2FF(nf2ff, Sim_Path, [f0], theta, phi) # noqa: F821 t3 = time.time() print(f"[timing] NF2FF (far-field) elapsed: {t3 - t2:.2f} s") # ... S-parameters postproc timing (optional): t4 = time.time() -for p in ports: - p.CalcPort(Sim_Path, freq) +for p in ports: # noqa: F821 + p.CalcPort(Sim_Path, freq) # noqa: F821 t5 = time.time() print(f"[timing] Port/S-params postproc elapsed: {t5 - t4:.2f} s") @@ -250,13 +255,19 @@ Zin = ports[0].uf_tot / ports[0].if_tot plt.figure(figsize=(7.6,4.6)) plt.plot(freq*1e-9, 20*np.log10(np.abs(S11)), lw=2, label='|S11|') plt.plot(freq*1e-9, 20*np.log10(np.abs(S21)), lw=2, ls='--', label='|S21|') -plt.grid(True); plt.legend(); plt.xlabel('Frequency (GHz)'); plt.ylabel('Magnitude (dB)') +plt.grid(True) +plt.legend() +plt.xlabel('Frequency (GHz)') +plt.ylabel('Magnitude (dB)') plt.title(f'S-Parameters (profile: {PROFILE})') plt.figure(figsize=(7.6,4.6)) plt.plot(freq*1e-9, np.real(Zin), lw=2, label='Re{Zin}') plt.plot(freq*1e-9, np.imag(Zin), lw=2, ls='--', label='Im{Zin}') -plt.grid(True); plt.legend(); plt.xlabel('Frequency (GHz)'); plt.ylabel('Ohms') +plt.grid(True) +plt.legend() +plt.xlabel('Frequency (GHz)') +plt.ylabel('Ohms') plt.title('Input Impedance (Port 1)') # ========================== @@ -295,22 +306,35 @@ ax = fig.add_subplot(111, projection='3d') ax.plot_surface(X, Y, Z, rstride=2, cstride=2, linewidth=0, antialiased=True, alpha=0.92) ax.set_title(f'Normalized 3D Pattern @ {f0/1e9:.2f} GHz\n(peak ≈ {Gmax_dBi:.1f} dBi)') ax.set_box_aspect((1,1,1)) -ax.set_xlabel('x'); ax.set_ylabel('y'); ax.set_zlabel('z') +ax.set_xlabel('x') +ax.set_ylabel('y') +ax.set_zlabel('z') plt.tight_layout() # ========================== # QUICK 2D GEOMETRY PREVIEW # ========================== plt.figure(figsize=(8.4,2.8)) -plt.fill_between([0,a], [0,0], [guide_length_mm, guide_length_mm], color='#dddddd', alpha=0.5, step='pre', label='WG top aperture') +plt.fill_between( + [0, a], + [0, 0], + [guide_length_mm, guide_length_mm], + color='#dddddd', + alpha=0.5, + step='pre', + label='WG top aperture', +) for zc, xc in zip(z_centers, x_centers): plt.gca().add_patch(plt.Rectangle((xc - slot_w/2.0, zc - slot_L/2.0), slot_w, slot_L, fc='#3355ff', ec='k')) -plt.xlim(-2, a+2); plt.ylim(-5, guide_length_mm+5) +plt.xlim(-2, a + 2) +plt.ylim(-5, guide_length_mm + 5) plt.gca().invert_yaxis() -plt.xlabel('x (mm)'); plt.ylabel('z (mm)') +plt.xlabel('x (mm)') +plt.ylabel('z (mm)') plt.title(f'Top-view slot layout (N={Nslots}, profile={PROFILE})') -plt.grid(True); plt.legend() +plt.grid(True) +plt.legend() diff --git a/5_Simulations/DAC_ReconstructionFilter/Generate_ChirpcsvFile.py b/5_Simulations/DAC_ReconstructionFilter/Generate_ChirpcsvFile.py index 6907b6f..c264832 100644 --- a/5_Simulations/DAC_ReconstructionFilter/Generate_ChirpcsvFile.py +++ b/5_Simulations/DAC_ReconstructionFilter/Generate_ChirpcsvFile.py @@ -69,7 +69,10 @@ def generate_multi_ramp_csv(Fs=125e6, Tb=1e-6, Tau=2e-6, fmax=30e6, fmin=10e6, df = pd.DataFrame({"time(s)": t_csv, "voltage(V)": y_csv}) df.to_csv(filename, index=False, header=False) print(f"CSV saved: {filename}") - print(f"Total raw samples: {total_samples} | Ramps inserted: {ramps_inserted} | CSV points: {len(y_csv)}") + print( + f"Total raw samples: {total_samples} | Ramps inserted: {ramps_inserted} " + f"| CSV points: {len(y_csv)}" + ) # --- Plot (staircase) if show_plot or save_plot_png: diff --git a/5_Simulations/Fencing/Via_fencing.py b/5_Simulations/Fencing/Via_fencing.py index 8b7cfdc..6ad51ca 100644 --- a/5_Simulations/Fencing/Via_fencing.py +++ b/5_Simulations/Fencing/Via_fencing.py @@ -27,10 +27,20 @@ ax.axhline(polygon_y2, color="blue", linestyle="--") via_positions = [2, 4, 6, 8] # x positions for visualization for x in via_positions: # Case A - ax.add_patch(plt.Circle((x, polygon_y1), via_pad_A/2, facecolor="green", alpha=0.5, label="Via pad A" if x==2 else "")) + ax.add_patch( + plt.Circle( + (x, polygon_y1), via_pad_A / 2, facecolor="green", alpha=0.5, + label="Via pad A" if x == 2 else "" + ) + ) ax.add_patch(plt.Circle((x, polygon_y2), via_pad_A/2, facecolor="green", alpha=0.5)) # Case B - ax.add_patch(plt.Circle((-x, polygon_y1), via_pad_B/2, facecolor="red", alpha=0.3, label="Via pad B" if x==2 else "")) + ax.add_patch( + plt.Circle( + (-x, polygon_y1), via_pad_B / 2, facecolor="red", alpha=0.3, + label="Via pad B" if x == 2 else "" + ) + ) ax.add_patch(plt.Circle((-x, polygon_y2), via_pad_B/2, facecolor="red", alpha=0.3)) # Add dimensions text diff --git a/5_Simulations/Fencing/Via_fencing2.py b/5_Simulations/Fencing/Via_fencing2.py index 0435bce..0fe75ae 100644 --- a/5_Simulations/Fencing/Via_fencing2.py +++ b/5_Simulations/Fencing/Via_fencing2.py @@ -26,10 +26,20 @@ ax.axhline(polygon_y2, color="blue", linestyle="--") via_positions = [2, 2 + via_pitch] # two vias for showing spacing for x in via_positions: # Case A - ax.add_patch(plt.Circle((x, polygon_y1), via_pad_A/2, facecolor="green", alpha=0.5, label="Via pad A" if x==2 else "")) + ax.add_patch( + plt.Circle( + (x, polygon_y1), via_pad_A / 2, facecolor="green", alpha=0.5, + label="Via pad A" if x == 2 else "" + ) + ) ax.add_patch(plt.Circle((x, polygon_y2), via_pad_A/2, facecolor="green", alpha=0.5)) # Case B - ax.add_patch(plt.Circle((-x, polygon_y1), via_pad_B/2, facecolor="red", alpha=0.3, label="Via pad B" if x==2 else "")) + ax.add_patch( + plt.Circle( + (-x, polygon_y1), via_pad_B / 2, facecolor="red", alpha=0.3, + label="Via pad B" if x == 2 else "" + ) + ) ax.add_patch(plt.Circle((-x, polygon_y2), via_pad_B/2, facecolor="red", alpha=0.3)) # Add text annotations @@ -48,7 +58,9 @@ line_edge_y = rf_line_y + line_width/2 via_center_y = polygon_y1 ax.annotate("", xy=(2.4, line_edge_y), xytext=(2.4, via_center_y), arrowprops=dict(arrowstyle="<->", color="brown")) -ax.text(2.5, (line_edge_y + via_center_y)/2, f"{via_center_offset:.2f} mm", color="brown", va="center") +ax.text( + 2.5, (line_edge_y + via_center_y) / 2, f"{via_center_offset:.2f} mm", color="brown", va="center" +) # Formatting ax.set_xlim(-5, 5) diff --git a/5_Simulations/array_pattern_Kaiser25dB_like.py b/5_Simulations/array_pattern_Kaiser25dB_like.py index 84e3a6c..df06583 100644 --- a/5_Simulations/array_pattern_Kaiser25dB_like.py +++ b/5_Simulations/array_pattern_Kaiser25dB_like.py @@ -106,4 +106,7 @@ plt.tight_layout() plt.savefig('Heatmap_Kaiser25dB_like.png', bbox_inches='tight') plt.show() -print('Saved: E_plane_Kaiser25dB_like.png, H_plane_Kaiser25dB_like.png, Heatmap_Kaiser25dB_like.png') +print( + 'Saved: E_plane_Kaiser25dB_like.png, H_plane_Kaiser25dB_like.png, ' + 'Heatmap_Kaiser25dB_like.png' +) diff --git a/8_Utils/Python/CSV_radar.py b/8_Utils/Python/CSV_radar.py index aa3a1fc..7e85ffb 100644 --- a/8_Utils/Python/CSV_radar.py +++ b/8_Utils/Python/CSV_radar.py @@ -15,12 +15,20 @@ def generate_radar_csv(filename="pulse_compression_output.csv"): timestamp_ns = 0 # Target parameters - targets = [ - {'range': 3000, 'velocity': 25, 'snr': 30, 'azimuth': 10, 'elevation': 5}, # Fast moving target - {'range': 5000, 'velocity': -15, 'snr': 25, 'azimuth': 20, 'elevation': 2}, # Approaching target - {'range': 8000, 'velocity': 5, 'snr': 20, 'azimuth': 30, 'elevation': 8}, # Slow moving target - {'range': 12000, 'velocity': -8, 'snr': 18, 'azimuth': 45, 'elevation': 3}, # Distant target - ] + targets = [ + { + 'range': 3000, 'velocity': 25, 'snr': 30, 'azimuth': 10, 'elevation': 5 + }, # Fast moving target + { + 'range': 5000, 'velocity': -15, 'snr': 25, 'azimuth': 20, 'elevation': 2 + }, # Approaching target + { + 'range': 8000, 'velocity': 5, 'snr': 20, 'azimuth': 30, 'elevation': 8 + }, # Slow moving target + { + 'range': 12000, 'velocity': -8, 'snr': 18, 'azimuth': 45, 'elevation': 3 + }, # Distant target + ] # Noise parameters noise_std = 5 @@ -38,7 +46,7 @@ def generate_radar_csv(filename="pulse_compression_output.csv"): q_val = np.random.normal(0, noise_std) # Add clutter (stationary targets) - clutter_range = 2000 # Fixed clutter at 2km + _clutter_range = 2000 # Fixed clutter at 2km if sample < 100: # Simulate clutter in first 100 samples i_val += np.random.normal(0, clutter_std) q_val += np.random.normal(0, clutter_std) @@ -47,7 +55,9 @@ def generate_radar_csv(filename="pulse_compression_output.csv"): for target in targets: # Calculate range bin (simplified) range_bin = int(target['range'] / 20) # ~20m per bin - doppler_phase = 2 * math.pi * target['velocity'] * chirp / 100 # Doppler phase shift + doppler_phase = ( + 2 * math.pi * target['velocity'] * chirp / 100 + ) # Doppler phase shift # Target appears around its range bin with some spread if abs(sample - range_bin) < 10: @@ -96,7 +106,9 @@ def generate_radar_csv(filename="pulse_compression_output.csv"): for target in targets: # Range bin calculation (different for short chirps) range_bin = int(target['range'] / 40) # Different range resolution - doppler_phase = 2 * math.pi * target['velocity'] * (chirp + 5) / 80 # Different Doppler + doppler_phase = ( + 2 * math.pi * target['velocity'] * (chirp + 5) / 80 + ) # Different Doppler # Target appears around its range bin if abs(sample - range_bin) < 8: diff --git a/8_Utils/Python/Gen_Triangular.py b/8_Utils/Python/Gen_Triangular.py index 5842c21..64d5550 100644 --- a/8_Utils/Python/Gen_Triangular.py +++ b/8_Utils/Python/Gen_Triangular.py @@ -1,5 +1,5 @@ import numpy as np -from numpy.fft import fft, ifft +from numpy.fft import fft import matplotlib.pyplot as plt @@ -15,7 +15,10 @@ theta_n= 2*np.pi*(pow(N,2)*pow(Ts,2)*(fmax-fmin)/(2*Tb)+fmin*N*Ts) # instantaneo y = 1 + np.sin(theta_n) # ramp signal in time domain M = np.arange(n, 2*n, 1) -theta_m= 2*np.pi*(pow(M,2)*pow(Ts,2)*(-fmax+fmin)/(2*Tb)+(-fmin+2*fmax)*M*Ts)-2*np.pi*((fmin-fmax)*Tb/2+(2*fmax-fmin)*Tb) # instantaneous phase +theta_m= ( + 2*np.pi*(pow(M,2)*pow(Ts,2)*(-fmax+fmin)/(2*Tb)+(-fmin+2*fmax)*M*Ts) + - 2*np.pi*((fmin-fmax)*Tb/2+(2*fmax-fmin)*Tb) +) # instantaneous phase z = 1 + np.sin(theta_m) # ramp signal in time domain x = np.concatenate((y, z)) @@ -23,9 +26,9 @@ x = np.concatenate((y, z)) t = Ts*np.arange(0,2*n,1) X = fft(x) L =len(X) -l = np.arange(L) +freq_indices = np.arange(L) T = L*Ts -freq = l/T +freq = freq_indices/T plt.figure(figsize = (12, 6)) diff --git a/8_Utils/Python/Generic_Triangular_Frequency.py b/8_Utils/Python/Generic_Triangular_Frequency.py index d559538..f027403 100644 --- a/8_Utils/Python/Generic_Triangular_Frequency.py +++ b/8_Utils/Python/Generic_Triangular_Frequency.py @@ -15,7 +15,10 @@ theta_n= 2*np.pi*(pow(N,2)*pow(Ts,2)*(fmax-fmin)/(2*Tb)+fmin*N*Ts) # instantaneo y = 1 + np.sin(theta_n) # ramp signal in time domain M = np.arange(n, 2*n, 1) -theta_m= 2*np.pi*(pow(M,2)*pow(Ts,2)*(-fmax+fmin)/(2*Tb)+(-fmin+2*fmax)*M*Ts)-2*np.pi*((fmin-fmax)*Tb/2+(2*fmax-fmin)*Tb) # instantaneous phase +theta_m= ( + 2*np.pi*(pow(M,2)*pow(Ts,2)*(-fmax+fmin)/(2*Tb)+(-fmin+2*fmax)*M*Ts) + - 2*np.pi*((fmin-fmax)*Tb/2+(2*fmax-fmin)*Tb) +) # instantaneous phase z = 1 + np.sin(theta_m) # ramp signal in time domain x = np.concatenate((y, z)) @@ -24,9 +27,9 @@ t = Ts*np.arange(0,2*n,1) plt.plot(t, x) X = fft(x) L =len(X) -l = np.arange(L) +freq_indices = np.arange(L) T = L*Ts -freq = l/T +freq = freq_indices/T print("The Array is: ", x) #printing the array diff --git a/8_Utils/Python/RADAR_eq.py b/8_Utils/Python/RADAR_eq.py index 1020b31..60ce3a7 100644 --- a/8_Utils/Python/RADAR_eq.py +++ b/8_Utils/Python/RADAR_eq.py @@ -221,7 +221,10 @@ class RadarCalculatorGUI: temp = self.get_float_value(self.entries["Temperature (K):"]) # Validate inputs - if None in [f_ghz, pulse_duration_us, prf, p_dbm, g_dbi, sens_dbm, rcs, losses_db, nf_db, temp]: + if None in [ + f_ghz, pulse_duration_us, prf, p_dbm, g_dbi, + sens_dbm, rcs, losses_db, nf_db, temp, + ]: messagebox.showerror("Error", "Please enter valid numeric values for all fields") return @@ -235,7 +238,7 @@ class RadarCalculatorGUI: g_linear = 10 ** (g_dbi / 10) sens_linear = 10 ** ((sens_dbm - 30) / 10) losses_linear = 10 ** (losses_db / 10) - nf_linear = 10 ** (nf_db / 10) + _nf_linear = 10 ** (nf_db / 10) # Calculate receiver noise power if k is None: @@ -298,11 +301,14 @@ class RadarCalculatorGUI: messagebox.showinfo("Success", "Calculation completed successfully!") except Exception as e: - messagebox.showerror("Calculation Error", f"An error occurred during calculation:\n{str(e)}") + messagebox.showerror( + "Calculation Error", + f"An error occurred during calculation:\n{str(e)}", + ) def main(): root = tk.Tk() - app = RadarCalculatorGUI(root) + _app = RadarCalculatorGUI(root) root.mainloop() if __name__ == "__main__": diff --git a/8_Utils/Python/patch_antenna.py b/8_Utils/Python/patch_antenna.py index bc0f094..85dd886 100644 --- a/8_Utils/Python/patch_antenna.py +++ b/8_Utils/Python/patch_antenna.py @@ -12,13 +12,22 @@ def calculate_patch_antenna_parameters(frequency, epsilon_r, h_sub, h_cu, array) lamb = c /(frequency * 1e9) # Calculate the effective dielectric constant - epsilon_eff = (epsilon_r + 1) / 2 + (epsilon_r - 1) / 2 * (1 + 12 * h_sub_m / (array[1] * h_cu_m)) ** (-0.5) + epsilon_eff = ( + (epsilon_r + 1) / 2 + + (epsilon_r - 1) / 2 * (1 + 12 * h_sub_m / (array[1] * h_cu_m)) ** (-0.5) + ) # Calculate the width of the patch W = c / (2 * frequency * 1e9) * np.sqrt(2 / (epsilon_r + 1)) # Calculate the effective length - delta_L = 0.412 * h_sub_m * (epsilon_eff + 0.3) * (W / h_sub_m + 0.264) / ((epsilon_eff - 0.258) * (W / h_sub_m + 0.8)) + delta_L = ( + 0.412 + * h_sub_m + * (epsilon_eff + 0.3) + * (W / h_sub_m + 0.264) + / ((epsilon_eff - 0.258) * (W / h_sub_m + 0.8)) + ) # Calculate the length of the patch L = c / (2 * frequency * 1e9 * np.sqrt(epsilon_eff)) - 2 * delta_L @@ -31,7 +40,10 @@ def calculate_patch_antenna_parameters(frequency, epsilon_r, h_sub, h_cu, array) # Calculate the feeding line width (W_feed) Z0 = 50 # Characteristic impedance of the feeding line (typically 50 ohms) - A = Z0 / 60 * np.sqrt((epsilon_r + 1) / 2) + (epsilon_r - 1) / (epsilon_r + 1) * (0.23 + 0.11 / epsilon_r) + A = ( + Z0 / 60 * np.sqrt((epsilon_r + 1) / 2) + + (epsilon_r - 1) / (epsilon_r + 1) * (0.23 + 0.11 / epsilon_r) + ) W_feed = 8 * h_sub_m / np.exp(A) - 2 * h_cu_m # Convert results back to mm @@ -50,7 +62,9 @@ h_sub = 0.102 # Height of substrate in mm h_cu = 0.07 # Height of copper in mm array = [2, 2] # 2x2 array -W_mm, L_mm, dx_mm, dy_mm, W_feed_mm = calculate_patch_antenna_parameters(frequency, epsilon_r, h_sub, h_cu, array) +W_mm, L_mm, dx_mm, dy_mm, W_feed_mm = calculate_patch_antenna_parameters( + frequency, epsilon_r, h_sub, h_cu, array +) print(f"Width of the patch: {W_mm:.4f} mm") print(f"Length of the patch: {L_mm:.4f} mm") diff --git a/9_Firmware/9_2_FPGA/tb/cosim/compare.py b/9_Firmware/9_2_FPGA/tb/cosim/compare.py index 9290954..7913670 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/compare.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/compare.py @@ -358,7 +358,10 @@ def compare_scenario(scenario_name): # ---- First/last sample comparison ---- print("\nFirst 10 samples (after alignment):") - print(f" {'idx':>4s} {'RTL_I':>8s} {'Py_I':>8s} {'Err_I':>6s} {'RTL_Q':>8s} {'Py_Q':>8s} {'Err_Q':>6s}") + print( + f" {'idx':>4s} {'RTL_I':>8s} {'Py_I':>8s} {'Err_I':>6s} " + f"{'RTL_Q':>8s} {'Py_Q':>8s} {'Err_Q':>6s}" + ) for k in range(min(10, aligned_len)): ei = aligned_rtl_i[k] - aligned_py_i[k] eq = aligned_rtl_q[k] - aligned_py_q[k] diff --git a/9_Firmware/9_2_FPGA/tb/cosim/fpga_model.py b/9_Firmware/9_2_FPGA/tb/cosim/fpga_model.py index 876d1f0..ad98042 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/fpga_model.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/fpga_model.py @@ -199,14 +199,17 @@ class NCO: # Wait - let me re-derive. The Verilog is: # phase_accumulator <= phase_accumulator + frequency_tuning_word; # phase_accum_reg <= phase_accumulator; // OLD value (NBA) - # phase_with_offset <= phase_accum_reg + {phase_offset, 16'b0}; // OLD phase_accum_reg + # phase_with_offset <= phase_accum_reg + {phase_offset, 16'b0}; + # // OLD phase_accum_reg # Since all are NBA (<=), they all read the values from BEFORE this edge. # So: new_phase_accumulator = old_phase_accumulator + ftw # new_phase_accum_reg = old_phase_accumulator # new_phase_with_offset = old_phase_accum_reg + offset old_phase_accumulator = (self.phase_accumulator - ftw) & 0xFFFFFFFF # reconstruct self.phase_accum_reg = old_phase_accumulator - self.phase_with_offset = (old_phase_accum_reg + ((phase_offset << 16) & 0xFFFFFFFF)) & 0xFFFFFFFF + self.phase_with_offset = ( + old_phase_accum_reg + ((phase_offset << 16) & 0xFFFFFFFF) + ) & 0xFFFFFFFF # phase_accumulator was already updated above # ---- Stage 3a: Register LUT address + quadrant ---- @@ -607,8 +610,14 @@ class FIRFilter: if (old_valid_pipe >> 0) & 1: for i in range(16): # Sign-extend products to ACCUM_WIDTH - a = sign_extend(mult_results[2*i] & ((1 << self.PRODUCT_WIDTH) - 1), self.PRODUCT_WIDTH) - b = sign_extend(mult_results[2*i+1] & ((1 << self.PRODUCT_WIDTH) - 1), self.PRODUCT_WIDTH) + a = sign_extend( + mult_results[2 * i] & ((1 << self.PRODUCT_WIDTH) - 1), + self.PRODUCT_WIDTH, + ) + b = sign_extend( + mult_results[2 * i + 1] & ((1 << self.PRODUCT_WIDTH) - 1), + self.PRODUCT_WIDTH, + ) self.add_l0[i] = a + b # ---- Stage 2 (Level 1): 8 pairwise sums ---- @@ -1365,7 +1374,10 @@ def _self_test(): mag_sq = s * s + c * c expected = 32767 * 32767 error_pct = abs(mag_sq - expected) / expected * 100 - print(f" Quadrature check: sin^2+cos^2={mag_sq}, expected~{expected}, error={error_pct:.2f}%") + print( + f" Quadrature check: sin^2+cos^2={mag_sq}, " + f"expected~{expected}, error={error_pct:.2f}%" + ) print(" NCO: OK") # --- Mixer test --- diff --git a/9_Firmware/9_2_FPGA/tb/cosim/gen_multiseg_golden.py b/9_Firmware/9_2_FPGA/tb/cosim/gen_multiseg_golden.py index 3f44726..dc7d732 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/gen_multiseg_golden.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/gen_multiseg_golden.py @@ -218,7 +218,8 @@ def generate_long_chirp_test(): if seg == 0: buffer_write_ptr = 0 else: - # Overlap-save: copy buffer[SEGMENT_ADVANCE:SEGMENT_ADVANCE+OVERLAP] -> buffer[0:OVERLAP] + # Overlap-save: copy + # buffer[SEGMENT_ADVANCE:SEGMENT_ADVANCE+OVERLAP] -> buffer[0:OVERLAP] for i in range(OVERLAP_SAMPLES): input_buffer_i[i] = input_buffer_i[i + SEGMENT_ADVANCE] input_buffer_q[i] = input_buffer_q[i + SEGMENT_ADVANCE] diff --git a/9_Firmware/9_2_FPGA/tb/cosim/real_data/golden_reference.py b/9_Firmware/9_2_FPGA/tb/cosim/real_data/golden_reference.py index 008d325..b8b8347 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/real_data/golden_reference.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/real_data/golden_reference.py @@ -335,7 +335,9 @@ def run_ddc(adc_samples): for n in range(n_samples): integrators[0][n + 1] = (integrators[0][n] + mixed_i[n]) & ((1 << CIC_ACC_WIDTH) - 1) for s in range(1, CIC_STAGES): - integrators[s][n + 1] = (integrators[s][n] + integrators[s - 1][n + 1]) & ((1 << CIC_ACC_WIDTH) - 1) + integrators[s][n + 1] = ( + integrators[s][n] + integrators[s - 1][n + 1] + ) & ((1 << CIC_ACC_WIDTH) - 1) # Downsample by 4 n_decimated = n_samples // CIC_DECIMATION @@ -580,8 +582,11 @@ def run_range_bin_decimator(range_fft_i, range_fft_q, decimated_i = np.zeros((n_chirps, output_bins), dtype=np.int64) decimated_q = np.zeros((n_chirps, output_bins), dtype=np.int64) - print(f"[DECIM] Decimating {n_in}→{output_bins} bins, mode={'peak' if mode==1 else 'avg' if mode==2 else 'simple'}, " - f"start_bin={start_bin}, {n_chirps} chirps") + mode_str = 'peak' if mode == 1 else 'avg' if mode == 2 else 'simple' + print( + f"[DECIM] Decimating {n_in}→{output_bins} bins, mode={mode_str}, " + f"start_bin={start_bin}, {n_chirps} chirps" + ) for c in range(n_chirps): # Index into input, skip start_bin @@ -678,7 +683,9 @@ def run_doppler_fft(range_data_i, range_data_q, twiddle_file_16=None): if twiddle_file_16 and os.path.exists(twiddle_file_16): cos_rom_16 = load_twiddle_rom(twiddle_file_16) else: - cos_rom_16 = np.round(32767 * np.cos(2 * np.pi * np.arange(n_fft // 4) / n_fft)).astype(np.int64) + cos_rom_16 = np.round( + 32767 * np.cos(2 * np.pi * np.arange(n_fft // 4) / n_fft) + ).astype(np.int64) LOG2N_16 = 4 doppler_map_i = np.zeros((n_range, n_total), dtype=np.int64) @@ -835,7 +842,10 @@ def run_dc_notch(doppler_i, doppler_q, width=2): notched_i = doppler_i.copy() notched_q = doppler_q.copy() - print(f"[DC NOTCH] width={width}, {n_range} range bins x {n_doppler} Doppler bins (dual sub-frame)") + print( + f"[DC NOTCH] width={width}, {n_range} range bins x " + f"{n_doppler} Doppler bins (dual sub-frame)" + ) if width == 0: print(" Pass-through (width=0)") @@ -1167,7 +1177,12 @@ def main(): parser = argparse.ArgumentParser(description="AERIS-10 FPGA golden reference model") parser.add_argument('--frame', type=int, default=0, help='Frame index to process') parser.add_argument('--plot', action='store_true', help='Show plots') - parser.add_argument('--threshold', type=int, default=10000, help='Detection threshold (L1 magnitude)') + parser.add_argument( + '--threshold', + type=int, + default=10000, + help='Detection threshold (L1 magnitude)' + ) args = parser.parse_args() # Paths @@ -1175,7 +1190,11 @@ def main(): fpga_dir = os.path.abspath(os.path.join(script_dir, '..', '..', '..')) data_base = os.path.expanduser("~/Downloads/adi_radar_data") amp_data = os.path.join(data_base, "amp_radar", "phaser_amp_4MSPS_500M_300u_256_m3dB.npy") - amp_config = os.path.join(data_base, "amp_radar", "phaser_amp_4MSPS_500M_300u_256_m3dB_config.npy") + amp_config = os.path.join( + data_base, + "amp_radar", + "phaser_amp_4MSPS_500M_300u_256_m3dB_config.npy" + ) twiddle_1024 = os.path.join(fpga_dir, "fft_twiddle_1024.mem") output_dir = os.path.join(script_dir, "hex") @@ -1290,7 +1309,10 @@ def main(): q_val = int(fc_doppler_q[rbin, dbin]) & 0xFFFF packed = (q_val << 16) | i_val f.write(f"{packed:08X}\n") - print(f" Wrote {fc_doppler_packed_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)") + print( + f" Wrote {fc_doppler_packed_file} (" + f"{DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)" + ) # Save numpy arrays for the full-chain path np.save(os.path.join(output_dir, "decimated_range_i.npy"), decim_i) @@ -1336,7 +1358,10 @@ def main(): q_val = int(notched_q[rbin, dbin]) & 0xFFFF packed = (q_val << 16) | i_val f.write(f"{packed:08X}\n") - print(f" Wrote {fc_notched_packed_file} ({DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)") + print( + f" Wrote {fc_notched_packed_file} (" + f"{DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS} packed IQ words)" + ) # CFAR on DC-notched data CFAR_GUARD = 2 @@ -1385,7 +1410,10 @@ def main(): with open(cfar_det_list_file, 'w') as f: f.write("# AERIS-10 Full-Chain CFAR Detection List\n") f.write(f"# Chain: decim -> MTI -> Doppler -> DC notch(w={DC_NOTCH_WIDTH}) -> CA-CFAR\n") - f.write(f"# CFAR: guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X}, mode={CFAR_MODE}\n") + f.write( + f"# CFAR: guard={CFAR_GUARD}, train={CFAR_TRAIN}, " + f"alpha=0x{CFAR_ALPHA:02X}, mode={CFAR_MODE}\n" + ) f.write("# Format: range_bin doppler_bin magnitude threshold\n") for det in cfar_detections: r, d = det @@ -1481,12 +1509,18 @@ def main(): print(f" Chirps processed: {DOPPLER_CHIRPS}") print(f" Samples/chirp: {FFT_SIZE}") print(f" Range FFT: {FFT_SIZE}-point → {snr_range:.1f} dB vs float") - print(f" Doppler FFT (direct): {DOPPLER_FFT_SIZE}-point Hamming → {snr_doppler:.1f} dB vs float") + print( + f" Doppler FFT (direct): {DOPPLER_FFT_SIZE}-point Hamming " + f"→ {snr_doppler:.1f} dB vs float" + ) print(f" Detections (direct): {len(detections)} (threshold={args.threshold})") print(" Full-chain decimator: 1024→64 peak detection") print(f" Full-chain detections: {len(fc_detections)} (threshold={args.threshold})") print(f" MTI+CFAR chain: decim → MTI → Doppler → DC notch(w={DC_NOTCH_WIDTH}) → CA-CFAR") - print(f" CFAR detections: {len(cfar_detections)} (guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X})") + print( + f" CFAR detections: {len(cfar_detections)} " + f"(guard={CFAR_GUARD}, train={CFAR_TRAIN}, alpha=0x{CFAR_ALPHA:02X})" + ) print(f" Hex stimulus files: {output_dir}/") print(" Ready for RTL co-simulation with Icarus Verilog") diff --git a/9_Firmware/9_2_FPGA/tb/cosim/validate_mem_files.py b/9_Firmware/9_2_FPGA/tb/cosim/validate_mem_files.py index d9e71dc..4ccd6d0 100644 --- a/9_Firmware/9_2_FPGA/tb/cosim/validate_mem_files.py +++ b/9_Firmware/9_2_FPGA/tb/cosim/validate_mem_files.py @@ -199,7 +199,10 @@ def test_long_chirp(): avg_mag = sum(magnitudes) / len(magnitudes) print(f" Magnitude: min={min_mag:.1f}, max={max_mag:.1f}, avg={avg_mag:.1f}") - print(f" Max magnitude as fraction of Q15 range: {max_mag/32767:.4f} ({max_mag/32767*100:.2f}%)") + print( + f" Max magnitude as fraction of Q15 range: " + f"{max_mag/32767:.4f} ({max_mag/32767*100:.2f}%)" + ) # Check if this looks like it came from generate_reference_chirp_q15 # That function uses 32767 * 0.9 scaling => max magnitude ~29490 @@ -262,7 +265,10 @@ def test_long_chirp(): # Check if bandwidth roughly matches expected bw_match = abs(f_range - CHIRP_BW) / CHIRP_BW < 0.5 # within 50% if bw_match: - print(f" Bandwidth {f_range/1e6:.2f} MHz roughly matches expected {CHIRP_BW/1e6:.2f} MHz") + print( + f" Bandwidth {f_range/1e6:.2f} MHz roughly matches expected " + f"{CHIRP_BW/1e6:.2f} MHz" + ) else: warn(f"Bandwidth {f_range/1e6:.2f} MHz does NOT match expected {CHIRP_BW/1e6:.2f} MHz") @@ -415,8 +421,11 @@ def test_chirp_vs_model(): print(f" Max phase diff: {max_phase_diff:.4f} rad ({math.degrees(max_phase_diff):.2f} deg)") phase_match = max_phase_diff < 0.5 # within 0.5 rad - check(phase_match, - f"Phase shape match: max diff = {math.degrees(max_phase_diff):.1f} deg (tolerance: 28.6 deg)") + check( + phase_match, + f"Phase shape match: max diff = {math.degrees(max_phase_diff):.1f} deg " + f"(tolerance: 28.6 deg)", + ) # ============================================================================ @@ -521,8 +530,11 @@ def test_memory_addressing(): addr_from_concat = (seg << 10) | 0 # {seg[1:0], 10'b0} addr_end = (seg << 10) | 1023 - check(addr_from_concat == base, - f"Seg {seg} base address: {{{seg}[1:0], 10'b0}} = {addr_from_concat} (expected {base})") + check( + addr_from_concat == base, + f"Seg {seg} base address: {{{seg}[1:0], 10'b0}} = {addr_from_concat} " + f"(expected {base})", + ) check(addr_end == end, f"Seg {seg} end address: {{{seg}[1:0], 10'h3FF}} = {addr_end} (expected {end})") diff --git a/9_Firmware/9_3_GUI/GUI_PyQt_Map.py b/9_Firmware/9_3_GUI/GUI_PyQt_Map.py index 703e58c..798fb33 100644 --- a/9_Firmware/9_3_GUI/GUI_PyQt_Map.py +++ b/9_Firmware/9_3_GUI/GUI_PyQt_Map.py @@ -33,7 +33,8 @@ from enum import Enum from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QLabel, QPushButton, QComboBox, QSpinBox, QDoubleSpinBox, - QGroupBox, QGridLayout, QSplitter, QFrame, QStatusBar, QCheckBox, QTableWidget, QTableWidgetItem, + QGroupBox, QGridLayout, QSplitter, QFrame, QStatusBar, QCheckBox, + QTableWidget, QTableWidgetItem, QHeaderView ) from PyQt6.QtCore import ( @@ -554,11 +555,20 @@ class RadarMapWidget(QWidget): if (!radarMarker) return; var content = '' + - '