Browse Source

docs(intel): identify isotropic illumination root cause and add metrology suite

main
RUSHIL AMBARISH KADU 1 month ago
parent
commit
726688ec9c
  1. 207
      intel/radar/isotropic_illumination_problem.md
  2. 78
      scripts/compare_metrology.py
  3. 51
      scripts/track_full_state.py
  4. 39
      scripts/track_peaks.py

207
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*

78
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.")

51
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")

39
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.")
Loading…
Cancel
Save