4 changed files with 375 additions and 0 deletions
-
207intel/radar/isotropic_illumination_problem.md
-
78scripts/compare_metrology.py
-
51scripts/track_full_state.py
-
39scripts/track_peaks.py
@ -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* |
|||
@ -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.") |
|||
@ -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") |
|||
@ -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.") |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue