From 726688ec9cf39fc7f5ff9e7ebc8e6d97c9b7643a Mon Sep 17 00:00:00 2001 From: rakadu1 Date: Tue, 14 Apr 2026 15:37:07 +0530 Subject: [PATCH] docs(intel): identify isotropic illumination root cause and add metrology suite --- intel/radar/isotropic_illumination_problem.md | 207 ++++++++++++++++++ scripts/compare_metrology.py | 78 +++++++ scripts/track_full_state.py | 51 +++++ scripts/track_peaks.py | 39 ++++ 4 files changed, 375 insertions(+) create mode 100644 intel/radar/isotropic_illumination_problem.md create mode 100644 scripts/compare_metrology.py create mode 100644 scripts/track_full_state.py create mode 100644 scripts/track_peaks.py diff --git a/intel/radar/isotropic_illumination_problem.md b/intel/radar/isotropic_illumination_problem.md new file mode 100644 index 0000000..efce50b --- /dev/null +++ b/intel/radar/isotropic_illumination_problem.md @@ -0,0 +1,207 @@ +# 🚨 The Isotropic Illumination Problem — Why Shenron Cannot See the Car + +**Date:** 2026-04-14 +**Engineer:** Fox ADAS Pipeline | Antigravity AI +**Status:** 🔴 CRITICAL — Root Cause Identified, Implementation Deferred +**Prerequisite Reading:** [Shenron_debug.md](./Shenron_debug.md) (Iterations 01–26) + +--- + +## Executive Summary + +After 30+ iterations of calibrating RCS physics, fixing coordinate bugs, tuning CFAR thresholds, adjusting gain, and increasing ray density, the **AWRL1432 radar still cannot reliably detect a vehicle at 25m in a straight-line braking scenario.** + +The root cause is **not** in the signal processing, the CFAR threshold, or the gain. It is a fundamental architectural omission in the Shenron physics engine: + +> **Shenron does not simulate the radar's antenna radiation pattern.** +> Every scatterer in the scene — whether it's at 0° (directly ahead) or at 80° (a distant wall) — receives and returns energy as if it were perfectly on boresight. + +This single missing physics element explains **why 30 iterations of downstream tuning have failed to reliably produce the car as a detection**, and why the Radarbook (with 8 vRx) sometimes succeeds where the 1432 (6 vRx) consistently fails. + +--- + +## The Evidence + +### Diagnostic Script Output (`track_full_state.py`) + +The following table shows where the **strongest peak** in each radar's Range-Doppler map is located: + +| Frame | Radar | Range (m) | **Angle (deg)** | Magnitude (dB) | +| :--- | :--- | :--- | :--- | :--- | +| 6 | **1432** | 20.76 | **76.57°** | 138.5 | +| 6 | R-Book | 20.39 | **-84.93°** | 142.4 | +| 8 | **1432** | 20.76 | **76.57°** | 142.0 | +| 8 | R-Book | 20.39 | **73.14°** | 142.0 | +| 14 | **1432** | 7.65 | **69.00°** | 135.5 | +| 14 | R-Book | 20.39 | **-84.93°** | 141.7 | + +**The car is at 0° azimuth (straight ahead).** Yet the strongest detection is consistently at **70–85°** — far off to the side. This is because the side-clutter (barriers, walls, trees at those wide angles) is returning the **same power** as the car, since Shenron treats all angles equally. + +### Visual Confirmation (Range-Azimuth Heatmap) + +The RA heatmap from the dashboard shows **complete, uniform energy rings** extending from -90° to +90°. In a real radar, you would see a focused **120° sector** with significant energy roll-off beyond ±60°. + +--- + +## Why Previous Iterations Could Not Fix This + +Looking back at the iteration history, here is what each major tuning attempt was actually fighting against: + +| Iteration | What We Tried | Why It Felt Like It Worked | Why It Didn't Actually Fix It | +| :--- | :--- | :--- | :--- | +| **10** | Metal Roughness tuning | Reduced some specular reflections | Clutter at 80° was still as loud as car at 0° | +| **14a** | **Vertical** Gaussian Damping | Killed ceiling/floor clutter by 90% | Only addressed **elevation**. Azimuthal clutter was untouched. | +| **16** | Area-Density Integration (+234%) | Boosted car signal significantly | Also boosted wall/barrier signal by the same 234% | +| **26** | Pure 1/R⁴ alignment | Correct physics — distance now matters | But **angle** still doesn't matter. A wall at 80° at 20m is just as "loud" as a car at 0° at 20m | +| **This Session** | Gain 110→115dB, CFAR 20→15, voxel_rho 0.05→0.02 | Peak SNR rose from 15→21 dB | The peak is at 76°, not 0°. We boosted the wrong target. | + +> [!CAUTION] +> **The fundamental issue:** Every iteration that "lifts all boats" (gain, density, normalization) lifts the **clutter equally** with the target. Without angular selectivity, the car can never be louder than the surrounding environment unless it is physically closer or has a dramatically higher RCS. + +--- + +## The Missing Physics: Antenna Radiation Pattern + +### What a Real Radar Does + +A real FMCW radar's patch antenna array has a **directional gain pattern**. The transmitted energy is concentrated in a forward-facing beam. A typical 77 GHz ADAS radar has: + +- **3dB Beamwidth (Azimuth):** ±60° (120° total) +- **Gain at Boresight (0°):** Maximum (0 dB relative) +- **Gain at ±60°:** -3 dB (half power) +- **Gain at ±80°:** -10 to -15 dB (almost invisible) +- **Gain at ±90°:** -20 dB or below (effectively blind) + +This means a wall at 80° would return **10–15 dB less power** than a car at 0°, even if they were at the same range with the same RCS. The car would dominate the detection. + +### What Shenron Currently Does + +``` +Sceneset.py → get_loss_3(): + + # --- Iteration 14a: Vertical Antenna Gain (Gaussian Damping) --- + phi_deg = np.rad2deg(np.abs(np.pi/2 - elev_angle)) + G_vertical = np.exp(-2.77 * np.power(phi_deg / radar.vertical_beamwidth, 2)) + + P_incident = (1 / np.power(rho, tx_dist_loss_exponent)) * K_sq * G_vertical + # ^^^^^^^^^^^ + # Only VERTICAL gain is applied. + # There is NO horizontal/azimuthal gain. +``` + +The `P_incident` calculation includes: +- ✅ **Distance decay** (`1/R²`) — Correct +- ✅ **Vertical beam pattern** (`G_vertical`) — Added in Iteration 14a +- ❌ **Horizontal beam pattern** (`G_horizontal`) — **MISSING** + +Every LiDAR point, regardless of its azimuth angle relative to the radar boresight, receives the same transmit power. The ADC synthesis in `heatmap_gen_fast.py` then faithfully encodes these equal-power returns into the beamforming vectors, creating the uniform rings we see in the RA heatmap. + +--- + +## The Proposed Fix + +### Implementation (Single Line Addition) + +In `Sceneset.py`, inside `get_loss_3()`, after the vertical gain calculation: + +```python +# --- Iteration 14a: Vertical Antenna Gain (Gaussian Damping) --- +phi_deg = np.rad2deg(np.abs(np.pi/2 - elev_angle)) +G_vertical = np.exp(-2.77 * np.power(phi_deg / radar.vertical_beamwidth, 2)) + +# --- NEW: Horizontal Antenna Gain (Azimuthal Beam Pattern) --- +# theta is computed in specularpoints() as: +# theta = pi/2 - arctan(x / y) where y=forward, x=side +# This means theta=pi/2 (90°) is boresight (straight ahead). +# The azimuth offset from boresight is: az_offset = |pi/2 - theta| +az_offset_deg = np.rad2deg(np.abs(np.pi/2 - theta_for_gain)) # degrees off boresight +G_horizontal = np.exp(-2.77 * np.power(az_offset_deg / radar.horizontal_beamwidth, 2)) + +P_incident = (1 / np.power(rho, tx_dist_loss_exponent)) * K_sq * G_vertical * G_horizontal +``` + +### ConfigureRadar.py Addition + +Add `self.horizontal_beamwidth` to each radar profile: + +| Radar | `horizontal_beamwidth` | Physical Basis | +| :--- | :--- | :--- | +| **awrl1432** | `60.0` (±60° = 120° total) | 6 vRx, narrow-beam ADAS profile | +| **radarbook** | `90.0` (±90° = 180° total) | 8 vRx, wider research-grade beam | +| **ti_cascade** | `60.0` | MIMO cascade, comparable to 1432 | + +### Coordinate Note + +> [!WARNING] +> The `theta` variable passed to `heatmap_gen` is the **angular position** of each scatterer in radar coordinates (`pi/2 - arctan(x/y)`). This is the correct variable to use for the azimuthal gain. However, it is computed in `specularpoints()` and returned alongside `rho`, `loss`, and `speed`. It is **not currently passed** to `get_loss_3()`. The fix requires either: +> 1. Passing `theta` into `get_loss_3()` as a new argument, or +> 2. Re-computing the azimuth angle inside `get_loss_3()` from the point coordinates. + +--- + +## Impact Assessment: What Happens to Previous Iterations? + +> [!IMPORTANT] +> **This is a "tide change" fix, not an additive tweak.** Once implemented, the energy landscape of the entire simulation changes fundamentally. Here is what to expect: + +### Parameters That Will Likely Need Re-Tuning + +| Parameter | Current Value | Expected Direction | Reason | +| :--- | :--- | :--- | :--- | +| **Gain (dB)** | 115 | **↓ Decrease** back toward 105–110 | With clutter suppressed, the car will be the dominant target again. The current 115dB was compensating for clutter competition. | +| **CFAR Threshold** | 15 | **↑ Increase** back toward 18–20 | Lower threshold was needed to "dig out" the car from equal-power clutter. With clutter gone, sensitivity can be reduced to avoid ground noise. | +| **voxel_rho** | 0.02 | **↑ Increase** back toward~0.05 | Higher ray density was needed to "outpower" the clutter. With beam shaping, fewer rays on the car will still dominate. | +| **Metal Roughness** (Iter 10) | 0.00005 | Likely unchanged | Material physics is orthogonal to beam pattern. | +| **Z-Filter** (Iter 13) | -2.2m | Likely unchanged | Ground clutter filtering is still needed regardless of azimuth. | +| **Vertical Beamwidth** (Iter 14a) | 20.0° | Likely unchanged | Elevation damping is still correct and complementary. | + +### Parameters That Should Remain Stable + +- **Bandwidth (137.2 MHz):** Hardware-derived, not a compensation knob. +- **Chirps (128):** Hardware-derived. +- **Coordinate System:** Locked since Iteration 05. +- **Semantic Tag Mapping:** Bug fix, not tuning. +- **1/R⁴ Power Law (Iter 26):** Correct physics, complementary to beam pattern. + +### The Core Prediction + +Once the horizontal beam pattern is implemented: + +1. The car at 0° will be the **dominant detection** in Range-Doppler. The peak tracker should lock onto Range Bin 19 consistently. +2. Side-clutter at 80° will be attenuated by **~15-20 dB**, effectively removing it from CFAR contention. +3. The RA heatmap will transform from "full rings" to a **focused 120° sector** — matching what you see on real hardware. +4. **Many of the compensatory tuning parameters (gain, CFAR, voxel density) can likely be relaxed** back to more physically accurate values, because they were fighting the wrong battle. + +--- + +## Why the Radarbook Sometimes Worked + +The Radarbook succeeded in some frames not because it has a beam pattern (it doesn't either), but because: + +1. **8 vRx vs 6 vRx:** More antennas = narrower main lobe in the Angle-FFT output. The DSP-level beamforming partially compensates for the missing physics-level beam shaping. +2. **Higher Doppler Resolution:** With 128 chirps at a much longer `chirp_rep` (0.75ms vs 36.4µs), the Radarbook has ~3.5x better velocity resolution. This allows it to separate the (slightly) moving car from the static side-clutter in the Doppler domain, even if both are equally loud in the Range domain. + +The 1432, with fewer antennas and coarser Doppler bins, has no such "escape hatch." + +--- + +## Recommended Implementation Order + +1. **Implement `G_horizontal` in `get_loss_3()`** — The single most impactful change. +2. **Run Braking scenario with current settings** (115dB gain, CFAR 15, voxel 0.02) — This will likely produce a very dense, accurate point cloud because the car now has a 15-20dB advantage. +3. **Gradually relax compensatory parameters** — Bring gain back toward 110, CFAR toward 18, and voxel toward 0.05, verifying detection stability at each step. +4. **Final Calibration** — Once stable, this becomes the new "Iteration 31" baseline. + +--- + +## Key Files for Implementation + +| File | Change Required | +| :--- | :--- | +| `scripts/ISOLATE/e2e_agent_sem_lidar2shenron_package/shenron/Sceneset.py` | Add `G_horizontal` to `get_loss_3()` | +| `scripts/ISOLATE/e2e_agent_sem_lidar2shenron_package/ConfigureRadar.py` | Add `horizontal_beamwidth` to all profiles | +| `scripts/ISOLATE/e2e_agent_sem_lidar2shenron_package/shenron/Sceneset.py` | Pass `theta` (or recompute azimuth) into `get_loss_3()` | + +--- + +*Generated by Antigravity AI | Fox CARLA ADAS Pipeline | 2026-04-14* diff --git a/scripts/compare_metrology.py b/scripts/compare_metrology.py new file mode 100644 index 0000000..500121e --- /dev/null +++ b/scripts/compare_metrology.py @@ -0,0 +1,78 @@ +import numpy as np +import matplotlib.pyplot as plt +import json +from pathlib import Path + +def analyze_session(session_path): + session_path = Path(session_path) + radar_types = ['awrl1432', 'radarbook'] + + # 1. Load Metrics + stats = {} + for r in radar_types: + metrics_file = session_path / r / "metrology" / "metrics.jsonl" + if metrics_file.exists(): + with open(metrics_file, 'r') as f: + stats[r] = [json.loads(line) for line in f] + + # 2. Pick a frame to analyze (e.g., middle of braking) + # Let's target frame 85 (where relative velocity is near zero) + frame_idx = 85 + frame_name = f"frame_{frame_idx:06d}" + + plt.figure(figsize=(15, 10)) + plt.suptitle(f"Metrology Diagnostic: {session_path.name} (Frame {frame_idx})", fontsize=16) + + for i, r in enumerate(radar_types): + rd_path = session_path / r / "metrology" / "rd" / f"{frame_name}.npy" + cfar_path = session_path / r / "metrology" / "cfar" / f"{frame_name}.npy" + + if not rd_path.exists(): + print(f"Warning: {rd_path} not found.") + continue + + rd = np.load(rd_path) + cfar = np.load(cfar_path) + + # Log scaling for visualization + rd_log = 10 * np.log10(rd + 1e-9) + cfar_log = 10 * np.log10(cfar + 1e-9) + + # Plot Range-Doppler Heatmap + plt.subplot(2, 2, i+1) + im = plt.imshow(np.flipud(rd_log), aspect='auto', cmap='viridis') + plt.title(f"{r.upper()} - Range-Doppler Energy") + plt.colorbar(im, label='dB') + plt.ylabel("Range Bins") + plt.xlabel("Doppler Bins (Center=0)") + + # Plot SNR (Signal / Threshold) + # This shows what the CFAR "sees" as a potential target + plt.subplot(2, 2, i+3) + snr_map = rd_log - cfar_log + im = plt.imshow(np.flipud(snr_map), aspect='auto', cmap='RdYlGn', vmin=-10, vmax=20) + plt.title(f"{r.upper()} - SNR over CFAR Threshold") + plt.colorbar(im, label='dB') + plt.ylabel("Range Bins") + plt.xlabel("Doppler Bins") + + plt.tight_layout(rect=[0, 0.03, 1, 0.95]) + + # Save the analysis plot + output_img = session_path / "metrology_comparison.png" + plt.savefig(output_img) + print(f"\n[SUCCESS] Analysis image saved to: {output_img}") + + # Print SNR stats comparison + print("\n--- Signal Statistics (Frame 85) ---") + for r in radar_types: + if r in stats and len(stats[r]) > frame_idx: + m = stats[r][frame_idx] + print(f"[{r.upper()}] Peak SNR: {m.get('peak_snr_db', 0):.2f} dB | Active Bins: {m.get('active_bins', 0)}") + +if __name__ == "__main__": + target_session = Path("data/braking_20260414_142359") + if target_session.exists(): + analyze_session(target_session) + else: + print(f"Session {target_session} not found.") diff --git a/scripts/track_full_state.py b/scripts/track_full_state.py new file mode 100644 index 0000000..845347d --- /dev/null +++ b/scripts/track_full_state.py @@ -0,0 +1,51 @@ +import numpy as np +import json +from pathlib import Path + +def track_full_state(session_path): + session_path = Path(session_path) + radar_types = ['awrl1432', 'radarbook'] + + # Load axes + axes = {} + for r in radar_types: + met_base = session_path / r / "metrology" + axes[r] = { + "range": np.load(met_base / "range_axis.npy"), + "angle": np.load(met_base / "angle_axis.npy") + } + + print(f"\n--- FULL STATE PEAK TRACKING: {session_path.name} ---") + header = f"{'Frame':<15} | {'Radar':<10} | {'Range(m)':<10} | {'Angle(deg)':<10} | {'Mag(dB)':<10}" + print(header) + print("-" * len(header)) + + for frame_idx in range(6, 16): # Core samples + frame_name = f"frame_{frame_idx:06d}" + + for r in radar_types: + rd_path = session_path / r / "metrology" / "rd" / f"{frame_name}.npy" + ra_path = session_path / r / "metrology" / "ra" / f"{frame_name}.npy" + + if not rd_path.exists() or not ra_path.exists(): continue + + rd = np.load(rd_path) + ra = np.load(ra_path) + + # Peak in RD (Range/Doppler) + rd_idx = np.unravel_index(np.argmax(rd), rd.shape) + r_bin = rd_idx[0] + r_m = axes[r]["range"][r_bin] + peak_db = 10 * np.log10(np.max(rd) + 1e-9) + + # Peak in RA (Range/Azimuth) + ra_idx = np.unravel_index(np.argmax(ra), ra.shape) + a_bin = ra_idx[1] + a_deg = np.degrees(axes[r]["angle"][a_bin]) + + name = "1432" if r == 'awrl1432' else "R-Book" + print(f"{frame_name:<15} | {name:<10} | {r_m:<10.2f} | {a_deg:<10.2f} | {peak_db:<10.1f}") + print("-" * 15) + +if __name__ == "__main__": + track_full_state("data/braking_20260414_142359") diff --git a/scripts/track_peaks.py b/scripts/track_peaks.py new file mode 100644 index 0000000..7462108 --- /dev/null +++ b/scripts/track_peaks.py @@ -0,0 +1,39 @@ +import numpy as np +import json +from pathlib import Path + +def get_peak_tracking(session_path): + session_path = Path(session_path) + radar_types = ['awrl1432', 'radarbook'] + + results = {} + for r in radar_types: + rd_dir = session_path / r / "metrology" / "rd" + if not rd_dir.exists(): continue + + frames = sorted(list(rd_dir.glob("*.npy"))) + track = [] + for f_path in frames: + rd = np.load(f_path) + # Find coordinates of the max energy + idx = np.unravel_index(np.argmax(rd), rd.shape) + val = 10 * np.log10(np.max(rd) + 1e-9) + track.append({"frame": f_path.stem, "range_bin": int(idx[0]), "doppler_bin": int(idx[1]), "db": float(val)}) + results[r] = track + + print("\n--- Peak Tracking Comparison (First 20 frames) ---") + header = f"{'Frame':<15} | {'1432 R-Bin':<12} | {'RB R-Bin':<12} | {'1432 dB':<10} | {'RB dB':<10}" + print(header) + print("-" * len(header)) + + for i in range(min(20, len(results.get('awrl1432', [])))): + f14 = results['awrl1432'][i] + frb = results['radarbook'][i] + print(f"{f14['frame']:<15} | {f14['range_bin']:<12} | {frb['range_bin']:<12} | {f14['db']:<10.1f} | {frb['db']:<10.1f}") + +if __name__ == "__main__": + target_session = Path("data/braking_20260414_142359") + if target_session.exists(): + get_peak_tracking(target_session) + else: + print(f"Session {target_session} not found.")