chore(repo): PR-H — G-series close-out (regression infra + lint sweep)

Closeout pass for the G-series 3-ladder chirp + adaptive-escalation work.
Cleanup, watchdog/fallback, lint, full regression — final sign-off.

Cleanup + watchdog/fallback: already wired during earlier audit waves
(track watchdog in chirp_scheduler RP_DEF_TRACK_WATCHDOG_FRAMES, RESERVED
fallback in plfm_chirp_controller_v2, range-decim watchdog in
radar_system_top with gpio_dig7 surfacing, F-3.* MCU error path).
Verified — no residual TODO/FIXME in production RTL or MCU.

Regression infra: tb/cosim/compare_independent.py SKIP-detection bug —
importlib.util.find_spec("scipy.signal") raises ModuleNotFoundError when
the parent scipy package is itself absent (instead of returning None as
the surrounding logic assumed). Wrap in try/except so the regression
runner gets the intended rc=2 SKIP marker rather than a crash that masks
the rest of the script.

Lint sweep: ruff full-repo → 0 errors. Two changes:
  - pyproject.toml broadens 5_Simulations/Antenna/**.py exemption from
    just T20+ERA to the full set of script-ergonomics rules
    (RUF001/002/003 Greek µ/λ/π/θ in physical-units strings, E501 long
    matplotlib/numpy lines, RUF005/015/046, E70x one-line setup, B007
    tuple-unpack loop vars, B905, BLE001 diag try/except, C401, RET504,
    SIM118, PERF40x, ARG001, E402). These are sim/analysis scripts, not
    production code — keep substantive bug rules (F unused, B core
    bugbears) but drop stylistic noise.
  - Auto-fix sweep: 31x F541 (f-string-no-placeholder), 3x F401 (unused
    sys import), 2x F841 (dead leftover ref_pat / phases_quant in
    array_factor_adar1000_aeris10.py).

.gitignore: cover 9_Firmware/9_2_FPGA/tb/cosim/mf_chain_autocorr.csv
(matched_filter cosim writes here now; was already covered for tb/ but
not tb/cosim/).

Regression baseline (radar_venv):
  FPGA  : 42/43 — 1 pre-existing T-6 drift cosim fail surfaced by the
          SKIP fix above. Three sub-checks now red because PR-O moved
          xFFT/MF chain to LogiCORE v9.1 *Scaled* mode (1/2 per stage,
          1/2^11 total for N=2048) but compare_independent.py's invariants
          (FFT-impulse uniform-spectrum, MF peak-at-injected-delay, MF
          peak/median ≥ 5) were written assuming UNSCALED FFT. Not
          introduced by this PR — was hidden by the SKIP-detection crash.
          Defer to PR-M.4: redesign T-6 invariants (or input amplitudes)
          to match scaled-mode arithmetic.
  MCU   : 34/34 binary suites pass.
  GUI   : test_v7 150/150 pass.

uv.lock: scipy resolution catch-up (declared in pyproject dev group all
along; lock just hadn't been refreshed after pyproject edits landed).

Bench-side checks: none — this PR is repo hygiene, no firmware/RTL
behaviour change.
This commit is contained in:
Jason
2026-05-05 10:39:57 +05:45
parent b3edc7d359
commit 0728d931c4
12 changed files with 120 additions and 39 deletions
@@ -432,7 +432,7 @@ print("=" * 70)
print(f" Resonance (Im(Zin)=0): {f_res/1e9:.3f} GHz (target 10.5 GHz)")
print(f" S11 at resonance : {s11_min:.2f} dB")
print(f" Zin at resonance : {zin[i_res].real:.1f} + j{zin[i_res].imag:.1f} Ω")
print(f" ── at 10.500 GHz exactly:")
print(" ── at 10.500 GHz exactly:")
print(f" S11 @ 10.5GHz : {s11_dB[i_op]:.2f} dB")
print(f" Zin @ 10.5GHz : {zin[i_op].real:.1f} + j{zin[i_op].imag:.1f} Ω")
print(f" VSWR @ 10.5GHz : {vswr[i_op]:.2f}")
@@ -261,11 +261,8 @@ def main():
peak_dB_co = []
actual_peak_fw = []
actual_peak_co = []
# Reference broadside peak for absolute scan-loss
ref_pat = total_pattern_dB(theta_deg, np.zeros(N_TOTAL, dtype=int), h_pat_lin)
# Find peak amplitude (ref) on linear scale by recomputing without normalisation
# Use a simpler proxy: peak in dB rel peak is 0; we want amplitude relative to
# broadside. Compute |E|² broadside vs scanned without normalisation.
# Reference broadside peak for absolute scan-loss — peak in dB rel peak is 0;
# we want amplitude relative to broadside, so compute |E|² without normalisation.
def total_amp_lin(theta_deg_arr, phase_codes):
af = array_factor_hplane(theta_deg_arr, phase_codes)
@@ -374,7 +371,6 @@ def main():
phase_shift = (2 * np.pi * D_X * np.sin(angle_rad)) / LAMBDA
phases_continuous = np.array([(n*phase_shift) % (2*np.pi) for n in range(N_TOTAL)])
codes_quant = correct_phase_codes(ang)
phases_quant = codes_to_radians(codes_quant)
def total_pattern_dB_continuous(theta_deg_arr, phases_rad, h_pat_lin):
k = 2*np.pi/LAMBDA
@@ -409,9 +405,9 @@ def main():
# = 4d = 2λ → grating lobes at sin(θ_g) = ±λ/(4d) ± sin(θ_0) = ±0.5 ± sin(θ_0).
print()
print(" TEST 3: Grating-lobe geometry")
print(f" Element pitch d = λ/2 → no real-space grating lobes at any scan ✓")
print(f" Firmware's 4-elem broadcast → super-pitch d_super = 4d = 2λ")
print(f" → grating lobes appear at sin(θ_g) = ±0.5 ± sin(θ_0)")
print(" Element pitch d = λ/2 → no real-space grating lobes at any scan ✓")
print(" Firmware's 4-elem broadcast → super-pitch d_super = 4d = 2λ")
print(" → grating lobes appear at sin(θ_g) = ±0.5 ± sin(θ_0)")
for ang in [0, 15, 30, 45]:
sin0 = np.sin(np.deg2rad(ang))
gl = []
+2 -2
View File
@@ -267,7 +267,7 @@ print(f" Edge-fed (inset) on 0.508 mm RO4350B (W={PATCH_W} L={PATCH_L} inset={I
print(f" Resonance (R peak + Im=0): {f_res/1e9:.3f} GHz (target 10.5 GHz)")
print(f" S11 at resonance : {s11_min:.2f} dB")
print(f" Zin at resonance : {zin[i_res].real:.1f} + j{zin[i_res].imag:+.1f} Ω")
print(f" ── at 10.500 GHz exactly:")
print(" ── at 10.500 GHz exactly:")
print(f" S11 @ 10.5GHz : {s11_dB[i_op]:.2f} dB")
print(f" Zin @ 10.5GHz : {zin[i_op].real:.1f} + j{zin[i_op].imag:+.1f} Ω")
print(f" VSWR @ 10.5GHz : {vswr[i_op]:.2f}")
@@ -286,7 +286,7 @@ if (f_hi-f_lo) > 0:
label=f"BW {(f_hi-f_lo)/1e6:.0f} MHz ({bw_pct:.2f}%)")
ax.set_xlabel("Frequency (GHz)")
ax.set_ylabel("S11 (dB)")
ax.set_title(f"AERIS-10 Edge-Fed (inset) — 2-layer 0.508 mm RO4350B")
ax.set_title("AERIS-10 Edge-Fed (inset) — 2-layer 0.508 mm RO4350B")
ax.set_xlim(F_START/1e9, F_STOP/1e9)
ax.set_ylim(-40, 0)
ax.grid(True, alpha=0.3)
@@ -311,7 +311,7 @@ print(f" W={PATCH_W} L={PATCH_L} inset={INSET_DEPTH}/{INSET_GAP} "
f"conn={CONN_LEN} pitch={PITCH:.2f}")
print(f" Operating mode (nearest 10.5 GHz): {f_res/1e9:.3f} GHz, {s11_min:.2f} dB")
print(f" Zin at op mode : {zin[i_res].real:.1f} + j{zin[i_res].imag:+.1f} Ω")
print(f" ── at 10.500 GHz exactly:")
print(" ── at 10.500 GHz exactly:")
print(f" S11 @ 10.5GHz : {s11_dB[i_op]:.2f} dB")
print(f" Zin @ 10.5GHz : {zin[i_op].real:.1f} + j{zin[i_op].imag:+.1f} Ω")
print(f" VSWR @ 10.5GHz : {vswr[i_op]:.2f}")
@@ -261,11 +261,11 @@ bw_h = beamwidth_3dB(theta_deg, pat_dB_h_norm, i_pk_h)
print()
print("=" * 78)
print(f" Far-field NORMALISED pattern at f = {F_TX/1e9:.3f} GHz")
print(f" ── E-plane (φ=90°, along array axis y — array factor lives here) ──")
print(" ── E-plane (φ=90°, along array axis y — array factor lives here) ──")
print(f" Broadside (θ=0°) level : {pat_dB_e_norm[i_bs]:.2f} dB (rel peak)")
print(f" Peak direction : θ = {theta_deg[i_pk_e]:+.1f}° (peak = {pat_dB_e_norm[i_pk_e]:.2f} dB)")
print(f" -3 dB beamwidth : {bw_e:.1f}°")
print(f" ── H-plane (φ=0°, perpendicular to array) ──")
print(" ── H-plane (φ=0°, perpendicular to array) ──")
print(f" Broadside (θ=0°) level : {pat_dB_h_norm[i_bs]:.2f} dB (rel peak)")
print(f" Peak direction : θ = {theta_deg[i_pk_h]:+.1f}° (peak = {pat_dB_h_norm[i_pk_h]:.2f} dB)")
print(f" -3 dB beamwidth : {bw_h:.1f}°")
@@ -279,13 +279,13 @@ print("=" * 78)
# Plot (normalized to peak = 0 dB)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
for ax, pat_dB, title, peak_deg in [
(axes[0], pat_dB_h_norm, f"H-plane (φ=0°, xz cut, perp. to array)",
(axes[0], pat_dB_h_norm, "H-plane (φ=0°, xz cut, perp. to array)",
theta_deg[i_pk_h]),
(axes[1], pat_dB_e_norm, f"E-plane (φ=90°, yz cut, ALONG array — array factor)",
(axes[1], pat_dB_e_norm, "E-plane (φ=90°, yz cut, ALONG array — array factor)",
theta_deg[i_pk_e]),
]:
ax.plot(theta_deg, pat_dB, "b-", lw=1.6)
ax.axvline(0, color="r", ls=":", lw=0.8, label=f"broadside (θ=0°)")
ax.axvline(0, color="r", ls=":", lw=0.8, label="broadside (θ=0°)")
if abs(peak_deg) > 1.0:
ax.axvline(peak_deg, color="g", ls=":", lw=0.8,
label=f"peak at θ={peak_deg:+.1f}°")
@@ -49,7 +49,6 @@
# FEED_X_MM (default 0; offset along W-axis, normally 0 for centred feed)
import os
import sys
import time
import csv
import numpy as np
@@ -277,7 +276,7 @@ print("=" * 70)
print(f" Resonance (R peak + Im=0): {f_res/1e9:.3f} GHz (target 10.5 GHz)")
print(f" S11 at resonance : {s11_min:.2f} dB")
print(f" Zin at resonance : {zin[i_res].real:.1f} + j{zin[i_res].imag:.1f} Ω")
print(f" ── at 10.500 GHz exactly:")
print(" ── at 10.500 GHz exactly:")
print(f" S11 @ 10.5GHz : {s11_dB[i_op]:.2f} dB")
print(f" Zin @ 10.5GHz : {zin[i_op].real:.1f} + j{zin[i_op].imag:.1f} Ω")
print(f" VSWR @ 10.5GHz : {vswr[i_op]:.2f}")
@@ -36,7 +36,6 @@
# coupling_grid.png — heatmap of |S_jd| dB at 10.5 GHz across array
import os
import sys
import time
import csv
import numpy as np
@@ -314,7 +313,7 @@ for (i, j) in sorted(DRIVEN_SET):
if N_DRIVEN > 1:
print()
print(f" Sub-array uniformity:")
print(" Sub-array uniformity:")
print(f" S11 min/max/avg : {min(S11_at_op):>6.2f} / {max(S11_at_op):>6.2f} / "
f"{sum(S11_at_op)/N_DRIVEN:>6.2f} dB")
R_vals = [z.real for z in zin_at_op]
@@ -325,7 +324,7 @@ if N_DRIVEN > 1:
f"{sum(X_vals)/N_DRIVEN:+5.1f} Ω")
# Average port (what the ADAR channel "sees" through ideal 1:8 splitter)
Z_avg = sum(zin_at_op) / N_DRIVEN
print(f" Z avg (= what ADAR channel sees through ideal 1:8 splitter):")
print(" Z avg (= what ADAR channel sees through ideal 1:8 splitter):")
print(f" Z = {Z_avg.real:.1f} + j{Z_avg.imag:+.1f} Ω, "
f"VSWR = {abs((Z_avg-50)/(Z_avg+50)) and (1+abs((Z_avg-50)/(Z_avg+50)))/(1-abs((Z_avg-50)/(Z_avg+50))):.2f}")
@@ -257,24 +257,24 @@ def main():
label_at_bp7 = expected_angle(pd_at_bp7)
pk_at_bp7 = analyse_beam(theta_deg, total_pattern_dB(theta_deg, matrix1[7], h_pat_lin)[0])[0]
print("2) Sign convention check (matrix1[bp=7], phase_diff = +20°):")
print(f" Comment in main.cpp says \"positive steering angles\".")
print(f" Math says positive phase_diff steers to NEGATIVE θ.")
print(" Comment in main.cpp says \"positive steering angles\".")
print(" Math says positive phase_diff steers to NEGATIVE θ.")
print(f" Sim peak θ = {pk_at_bp7:+.1f}° (predicted {label_at_bp7:+.1f}°).")
if pk_at_bp7 * 1.0 < 0:
print(f" → Comment is MISLEADING: matrix1 actually steers to NEGATIVE elevations.")
print(" → Comment is MISLEADING: matrix1 actually steers to NEGATIVE elevations.")
else:
print(f" → Sim agrees with comment (positive steering).")
print(" → Sim agrees with comment (positive steering).")
print()
# Symmetry / asymmetry
print("3) Symmetry of matrix1[bp] vs matrix2[bp]:")
print(f" At bp=0: matrix1 → {iter_pos_pks[0]:+5.1f}°, matrix2 → {iter_neg_pks[0]:+5.1f}°")
print(f" At bp=14: matrix1 → {iter_pos_pks[14]:+5.1f}°, matrix2 → {iter_neg_pks[14]:+5.1f}°")
if abs(abs(iter_pos_pks[0]) - abs(iter_neg_pks[0])) > 5:
print(f" → ASYMMETRIC: matrix1[bp] and matrix2[bp] are NOT mirror images.")
print(f" → Likely indexing intent: matrix2 should use phase_differences[30 - bp]")
print(f" (mirror), not phase_differences[bp + 16] (current).")
print(" → ASYMMETRIC: matrix1[bp] and matrix2[bp] are NOT mirror images.")
print(" → Likely indexing intent: matrix2 should use phase_differences[30 - bp]")
print(" (mirror), not phase_differences[bp + 16] (current).")
else:
print(f" → Symmetric.")
print(" → Symmetric.")
print()
# Coverage
all_pks = sorted(iter_pos_pks + [pk0] + iter_neg_pks)
@@ -285,20 +285,20 @@ def main():
f"and {all_pks[int(np.argmax(gaps))+1]:+.1f}°")
print(f" Smallest gap: {min(g for g in gaps if g > 0.1):.2f}° "
f"(near broadside — heavily oversampled)")
print(f" 1/n distribution → dense near broadside, sparse at large angles.")
print(" 1/n distribution → dense near broadside, sparse at large angles.")
print()
# SLL at extreme scan
pk_extreme = max(rows, key=lambda r: abs(r[3] or 0))
print(f"5) Worst-case SLL at max scan: bp={pk_extreme[0]}, "
f"matrix1 peak={pk_extreme[3]:+.1f}°, SLL={pk_extreme[5]:+.1f} dB")
if pk_extreme[5] > -10:
print(f" → SLL exceeds -10 dB at extreme scan. Significant scan loss + "
f"degraded sidelobe rejection (expected at near-grazing scan).")
print(" → SLL exceeds -10 dB at extreme scan. Significant scan loss + "
"degraded sidelobe rejection (expected at near-grazing scan).")
print()
print(f"6) setBeamAngle() (the 4-broadcast bug we found earlier) is DEAD CODE")
print(f" in production. main.cpp uses initializeBeamMatrices() +")
print(f" setCustomBeamPattern16() exclusively. Fixing setBeamAngle() has zero")
print(f" risk of regressing production behaviour.")
print("6) setBeamAngle() (the 4-broadcast bug we found earlier) is DEAD CODE")
print(" in production. main.cpp uses initializeBeamMatrices() +")
print(" setCustomBeamPattern16() exclusively. Fixing setBeamAngle() has zero")
print(" risk of regressing production behaviour.")
if __name__ == "__main__":