Compare commits
merge into: rakadu1:main
rakadu1:1843_integration
rakadu1:Shenron
rakadu1:main
rakadu1:origin/1843_integration
rakadu1:origin/HEAD
rakadu1:origin/Shenron
rakadu1:origin/main
rakadu1:origin/perf/Shenron-optimization
rakadu1:remotes/origin/perf/shenron-optimization
pull from: rakadu1:Shenron
rakadu1:1843_integration
rakadu1:Shenron
rakadu1:main
rakadu1:origin/1843_integration
rakadu1:origin/HEAD
rakadu1:origin/Shenron
rakadu1:origin/main
rakadu1:origin/perf/Shenron-optimization
rakadu1:remotes/origin/perf/shenron-optimization
7 Commits
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
9c028d606e |
docs: update context.md manifest and fix radar SNR/telemetry logic
- Expanded intel/internal/context.md with modern stage-based architecture and internal documentation structure. - Corrected SNR calculation in model_wrapper.py by dividing out the CFAR threshold. - Tuned CFAR threshold to 19dB for increased point cloud density. - Integrated stop flag detection into PipelineManager and ShenronOrchestrator for graceful halting. - Synchronized metrology gain offsets (-78.0dB) across production and testbench scripts. - Added 'ti_cascade' support to default radar list and testbench visualization. |
2 weeks ago |
|
|
11d6583230 |
Intel update
|
2 weeks ago |
|
|
bd466a3568 |
refactor: unify Shenron radar processing pipeline
Established a common orchestration framework to eliminate logic duplication between production data generation and the iterative testbench. Key changes: - Created 'ShenronOrchestrator' in scripts/ISOLATE/shenron_orchestrator.py to serve as the single source of truth for the processing loop. - Refactored 'generate_shenron.py' to use the orchestrator, ensuring production data benefit from research-level DSP improvements. - Refactored 'test_shenron.py' to use the orchestrator, guaranteeing that debug iterations are bit-identical to production outputs. - Centralized LiDAR padding, model execution, and metrology/metric serialization logic. - Preserved Dashboard SSE telemetry patterns ([SHENRON_INIT/STEP]) to maintain full UI compatibility. This restructuring ensures that any iterative changes made in the 'test_shenron' lab are automatically and safely inherited by the Dashboard's automated simulation pipeline. |
2 weeks ago |
|
|
05f9d181e1 |
feat(radar): recalibrate visualization engine for high-fidelity diagnostics
Restores sharp target peaks and improves signal clarity in Shenron radar outputs by optimizing windowing functions and disabling smoothing-heavy interpolation. - radar_processor.py: Switched windowing from Hann to Blackman-Harris for range and velocity processing to achieve deeper sidelobe rejection (-92 dB) and suppress horizontal artifacts. - test_shenron.py: Replaced 'bicubic' interpolation with 'nearest' for Range-Doppler (RD) and CFAR maps to restore diagnostic clarity and prevent target blurring. - config.yaml: Removed hardcoded RD plot limits (xRange/yRange) to ensure the renderer utilizes the true physical axis (e.g., ±26.8 m/s) derived from hardware specs. - Added technical comments explaining the trade-off between main-lobe width and dynamic range in the new DSP windowing. |
2 weeks ago |
|
|
70aa058e4d |
feat(shenron): gain recalibration and visualization stability
- [Radar Cfg] Increase gain by +10dB (110dB -> 120dB) for all radar models in ConfigureRadar.py - [Root Cause] Compensate for the SNR drop caused by restoring the 1/R^4 physics law (Iteration 37), which removed the artificial R^2 area boost and made RD maps 'hazy' or 'out of focus'. - [Viz] Update SYSTEM_GAIN_OFFSET from 68.0 to 78.0 in plots.py and test_shenron.py - [Viz] Increase dynamic range clipping ceiling from 45dB to 55dB - [Root Cause] Prevents RA heatmap saturation. The +10dB gain increase caused signals to hit the previous 45dB hard-clip ceiling, making the dynamic plots look identical to the static ones (blown out). |
2 weeks ago |
|
|
0bbfe68ca3 |
feat(shenron): Physics audit, azimuth metrology & pipeline stability
- [Physics] Disable R^2 area expansion in Sceneset.get_loss_3 to restore physically correct 1/R^4 two-way radar power law (point scatterer model) - [Metrology] Normalize RA heatmap [0,1] before variance calculation in model_wrapper.py to prevent 1e20 scalar overflow values - [Metrology] Add peak_azimuth_spread_deg (Half-Power Beamwidth proxy) to signal metrics for per-frame angular resolution monitoring - [DSP] Switch spatial Angle-FFT window from Hanning to Hamming in radar_processor.py for sharper beamforming on small antenna arrays (6-8 Rx) - [Telemetry] Expose az_std and spread in [SHENRON_STEP] dashboard stream for both generate_shenron.py and test_shenron.py pipelines - [BugFix] Add missing sys/os imports in src/main.py and video_stage.py that caused NameError crashes in the stage-based pipeline orchestrator |
2 weeks ago |
|
|
274401b6e1 |
ADC_Data.md added.
|
3 weeks ago |
18 changed files with 885 additions and 267 deletions
-
2README.md
-
8gemini.md
-
8intel/CHRONICLES.md
-
180intel/internal/context.md
-
473intel/radar/ADC_Data.md
-
6scripts/ISOLATE/e2e_agent_sem_lidar2shenron_package/ConfigureRadar.py
-
29scripts/ISOLATE/e2e_agent_sem_lidar2shenron_package/shenron/Sceneset.py
-
60scripts/ISOLATE/model_wrapper.py
-
140scripts/ISOLATE/shenron_orchestrator.py
-
9scripts/ISOLATE/sim_radar_utils/config.yaml
-
11scripts/ISOLATE/sim_radar_utils/plots.py
-
12scripts/ISOLATE/sim_radar_utils/radar_processor.py
-
7scripts/data_to_mcap.py
-
110scripts/generate_shenron.py
-
79scripts/test_shenron.py
-
3src/main.py
-
12src/pipeline/manager.py
-
1src/pipeline/stages/video_stage.py
@ -0,0 +1,473 @@ |
|||||
|
# 📡 Radar Deep Dive: The Comprehensive Guide to ADC Data (Raw Samples) |
||||
|
|
||||
|
This document serves as the master reference for Raw ADC (Analog-to-Digital Converter) data within the Fox CARLA Shenron simulation engine. It is designed to bridge the gap between Mechanical Engineering, Computer Science, and Electrical Engineering, providing intuitive explanations and deep technical dives into how radar physics are simulated in code. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 1. Foundation: The FMCW Radar Principle (For Non-EE Engineers) |
||||
|
Before analyzing data structures, we must understand the physical mechanism of the sensor. The sensors we simulate (TI AWRL1432, Radarbook) are **FMCW (Frequency Modulated Continuous Wave)** radars. |
||||
|
|
||||
|
### 1.1 What is a Chirp? (The Sweeping Pitch Analogy) |
||||
|
Imagine you are standing in a large empty factory hall and you have a special whistle that changes pitch continuously. You press the whistle and it starts at a very low bass note, then steadily ramps up to a high-pitched squeal over 40 microseconds (0.00004 seconds), then instantly resets to the low note and repeats. Each of these pitch-sweeps is called a **"Chirp."** |
||||
|
|
||||
|
In radar, we do the same with electromagnetic waves (radio waves) instead of sound: |
||||
|
* **Start Frequency ($f_c$):** The starting pitch. For automotive radars this is 77 GHz (77,000,000,000 vibrations per second). This frequency is invisible and harmless — think of it like a colour of light you cannot see. |
||||
|
* **Bandwidth ($B$):** The total pitch range swept. The AWRL1432 sweeps 137.2 MHz; the Radarbook sweeps 250 MHz. A wider sweep = finer ability to separate nearby objects. |
||||
|
* **Chirp Duration ($T_c$):** How long one sweep takes. For the Radarbook: `N_sample / samp_rate = 256 / 1e6 = 256 microseconds`. |
||||
|
* **Frequency Slope ($k = B / T_c$):** How fast the frequency increases. Think of this as "how steep the ramp is." |
||||
|
* **Range Resolution Formula:** `range_res = c / (2 * B)`. For the Radarbook (B=250 MHz): `3e8 / (2 * 250e6) = 0.6 metres`. This means two objects 0.6 m apart are the minimum resolvable. |
||||
|
|
||||
|
### 1.2 What is a Mixer? (The Pitch-Comparator Analogy) |
||||
|
Back to the factory hall. You blow your sweeping whistle at a metal wall. The sound travels to the wall and echoes back. Here is the key insight: |
||||
|
|
||||
|
**Because sound takes time to travel,** the echo reaching your ear right now was emitted slightly in the past. Since your whistle pitch is always increasing, the echo has a *lower pitch* than what you are currently blowing. |
||||
|
|
||||
|
Now imagine you hold two instruments simultaneously: |
||||
|
- Your left hand plays the current whistle note (the transmitted signal, TX). |
||||
|
- Your right hand plays the echo from the wall (the received signal, RX). |
||||
|
|
||||
|
If you play both at the same time, your ear (or a microphone) hears an acoustic phenomenon called a **"beat"** — a slow wobble in loudness caused by two slightly different frequencies competing. The rate of this wobble is exactly the *difference* between the two frequencies. This is the **Beat Frequency**. |
||||
|
|
||||
|
In hardware, the **Mixer** is the electronic component that performs this comparison. It multiplies TX by RX mathematically. The result contains the Beat Frequency (the difference), which is then isolated by a low-pass filter. This Beat Frequency signal is what the ADC samples. |
||||
|
|
||||
|
**The golden rule: `Beat Frequency = (2 * k * Range) / c`** |
||||
|
* Close target (5 m) → small beat frequency → low-pitched wobble. |
||||
|
* Far target (80 m) → large beat frequency → high-pitched wobble. |
||||
|
|
||||
|
The ADC simply records this "wobble" at discrete time steps. When you later apply an FFT, each distinct wobble frequency appears as a peak — and you convert it back to a distance. |
||||
|
|
||||
|
### 1.3 Hardware Parameters in Our Codebase |
||||
|
These parameters are defined in `scripts/ISOLATE/e2e_agent_sem_lidar2shenron_package/ConfigureRadar.py`: |
||||
|
|
||||
|
| Parameter | AWRL1432 | Radarbook | Meaning | |
||||
|
|---|---|---|---| |
||||
|
| `f` | 77 GHz | 24 GHz | Start frequency | |
||||
|
| `B` | 137.2 MHz | 250 MHz | Bandwidth (range resolution) | |
||||
|
| `N_sample` | 256 | 256 | ADC samples per chirp | |
||||
|
| `chirps` | 128 | 128 | Chirps per frame (velocity resolution) | |
||||
|
| `nRx` | 6 | 8 | Receiver antenna channels | |
||||
|
| `chirp_rep` | 36.4 µs | 750 µs | Repetition interval between chirps | |
||||
|
| `noise_amp` | 0.005 | 0.005 | Noise floor amplitude | |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 2. I/Q Sampling: The Reason ADC Data is Complex |
||||
|
|
||||
|
A common point of confusion is why ADC data is stored as **Complex Numbers (Real + Imaginary pairs)** rather than a simple list of voltage values. This section explains the physics and hardware design that necessitates this. |
||||
|
|
||||
|
### 2.1 The Ambiguity Problem (The Blindfold Analogy) |
||||
|
Imagine you are blindfolded and someone hands you a microphone. A person walks past you dragging a stick along a picket fence: "clack-clack-clack." You can determine how fast they are walking from the rhythm. But **which direction** are they walking? Left-to-right, or right-to-left? With only one microphone (one measurement), you cannot tell. The signal is intrinsically ambiguous about direction. |
||||
|
|
||||
|
In radar signal processing, this problem appears as **Doppler ambiguity**. The Doppler effect shifts the beat frequency slightly depending on whether the target is approaching or receding. However, if we only record a single real voltage stream, the mathematical result is a cosine wave `A·cos(2π·f·t + φ)`. By fundamental trigonometry, `cos(x) = cos(-x)`, meaning the positive and negative frequency components look **identical**. A car moving toward you at 10 m/s produces the exact same signal as a car moving away at 10 m/s. You know the speed magnitude, but the direction is lost. |
||||
|
|
||||
|
### 2.2 The Hardware Solution: Two Mixers at 90° Offset |
||||
|
The hardware solution is elegant. Instead of one mixer, the radar uses **two mixers in parallel**: |
||||
|
|
||||
|
**Path 1 — I (In-phase):** |
||||
|
The received echo is multiplied by the transmitted chirp at exactly 0° phase. This produces the **In-phase (I)** voltage. |
||||
|
|
||||
|
**Path 2 — Q (Quadrature):** |
||||
|
A copy of the transmitted chirp is shifted by exactly 90° (a quarter of a full wave cycle) using a phase-shifter circuit. The received echo is multiplied by this 90°-shifted version. This produces the **Quadrature (Q)** voltage. |
||||
|
|
||||
|
The word *Quadrature* simply means "at 90 degrees" — it comes from the Latin for "square," referring to the quarter-turn rotation. Two separate ADC chips sample I and Q simultaneously. |
||||
|
|
||||
|
### 2.3 What I and Q Values Tell You |
||||
|
Let us walk through a concrete example. Suppose a car is at 20 metres, moving toward the sensor at 8 m/s. |
||||
|
|
||||
|
* The beat frequency encodes the range (20 m). |
||||
|
* The Doppler shift creates a tiny phase advancement in the I and Q signals from chirp to chirp. |
||||
|
* If the car is approaching: I leads Q in phase (I peaks before Q). |
||||
|
* If the car is receding: Q leads I in phase (Q peaks before I). |
||||
|
|
||||
|
By looking at the *relationship* between I and Q, the DSP can unambiguously determine direction. |
||||
|
|
||||
|
### 2.4 The Complex Representation (The Clock-Hand Analogy) |
||||
|
Mathematically, we merge the two real streams into one complex number: |
||||
|
`S(t) = I(t) + j·Q(t)` |
||||
|
|
||||
|
Where `j = √(-1)` is the imaginary unit. This is not just a notation trick — it allows us to use the powerful mathematics of complex exponentials. |
||||
|
|
||||
|
Instead of thinking of the signal as a wave going up and down, picture it as a clock hand spinning on a dial: |
||||
|
* The **length** of the clock hand is the signal amplitude (how strong the reflection is). |
||||
|
* The **angle** of the clock hand is the signal phase (encoding range and velocity information). |
||||
|
* **Approaching target:** the clock hand spins counter-clockwise over successive chirps. |
||||
|
* **Receding target:** the clock hand spins clockwise over successive chirps. |
||||
|
|
||||
|
A complex FFT naturally sees the direction of this spin and separates positive from negative velocities into different bins on the Doppler axis. A real-only FFT cannot. |
||||
|
|
||||
|
### 2.5 Practical Consequence in Our Data |
||||
|
Because we use I/Q sampling: |
||||
|
* Our ADC tensors use `dtype=complex128` (or `complex64`), storing a real and imaginary float per sample. |
||||
|
* The `reformat_adc_shenron()` function in `sim_radar_utils/utils_radar.py` rearranges the axes but preserves the complex dtype throughout. |
||||
|
* The `RadarProcessor.cal_range_fft()` and `cal_doppler_fft()` functions use `scipy.fft.fft()` which accepts complex input and correctly resolves positive/negative velocities. |
||||
|
* If ADC data were real-valued only, the Range-Doppler plot would be **symmetric** (mirrored), and we could not distinguish which side was real. |
||||
|
|
||||
|
### 2.6 SNR Benefit of Complex Sampling |
||||
|
There is an additional engineering benefit beyond direction disambiguation. A complex FFT effectively doubles the number of usable frequency bins compared to a real FFT of the same length. This results in approximately **3 dB better Signal-to-Noise Ratio (SNR)** — meaning we can detect targets that are half as reflective. For ADAS this is significant: a pedestrian at 50 m reflects far less energy than a truck. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 3. Shenron's Signal Synthesis: Physics to Code |
||||
|
|
||||
|
In `heatmap_gen_fast.py`, since we have no physical antenna, we mathematically synthesize what the ADC voltages would be, using GPU-accelerated tensor operations (PyTorch). Here is the exact step-by-step physics model with the corresponding code. |
||||
|
|
||||
|
### Step 3.1: Input Geometry — The Point Cloud as Scatterers |
||||
|
The pipeline takes the semantic LiDAR point cloud where every $(x, y, z)$ coordinate is treated as an independent **Point Scatterer** — a tiny reflective surface. |
||||
|
|
||||
|
For each scatterer, three physical quantities are extracted: |
||||
|
* **Range ($\rho$):** Euclidean distance `np.linalg.norm([x, y, z])`. This determines the time delay. |
||||
|
* **Azimuth ($\theta$):** Angle relative to the radar boresight. Used for Angle of Arrival (AoA) computation. |
||||
|
* **Radar Cross Section (loss):** The LiDAR intensity (optical reflectivity) is used as a proxy for electronic reflectivity. It is scaled by `1/rho^2` to model the **radar range equation**: the power returned from a target falls off as the 4th power of distance (the signal travels twice — once to the target, once back). The code does this with: `loss = torch.tensor(loss * (1 / rho ** 2))`. |
||||
|
|
||||
|
### Step 3.2: The Round-Trip Time Delay ($\tau$) |
||||
|
For each scatterer and each chirp, the code computes the exact round-trip time delay: |
||||
|
```python |
||||
|
tau = 2 * (rho / radar.c + chirp * radar.chirp_rep * speed / radar.c) |
||||
|
``` |
||||
|
Breaking this down: |
||||
|
* `2 * rho / c` — the static delay for sound (or light) to travel to the target and back. |
||||
|
* `chirp * chirp_rep * speed / c` — the additional delay caused by the target moving during the frame. `chirp_rep` is the time between two consecutive chirps. So for chirp number 3, the target has moved `3 * chirp_rep * speed` further. Dividing by `c` converts that distance to additional time. |
||||
|
|
||||
|
This second term is what physically generates the **Doppler Effect** across chirps. It is typically nanometer-scale but critically important because the Doppler FFT is extremely sensitive to phase. |
||||
|
|
||||
|
### Step 3.3: Antenna Array Spatial Delays ($\delta$) |
||||
|
For a multi-antenna radar (e.g., AWRL1432 has 6 Rx antennas; Radarbook has 8), the signal hits each antenna at a slightly different time depending on the target's azimuth angle. The code computes a per-antenna spatial delay: |
||||
|
```python |
||||
|
delta[i, :] = i * sRx * torch.sin(np.pi / 2 - theta) / radar.c |
||||
|
``` |
||||
|
Where `sRx = lambda / 2` is the half-wavelength antenna spacing (a standard MIMO design). `i` is the antenna index (0 through nRx-1). A target directly in front (theta=0) has zero additional delay across antennas. A target at 45° off-boresight has a significant inter-antenna delay — this is what allows us to compute the Angle of Arrival later. |
||||
|
|
||||
|
### Step 3.4: The Beamforming Vector (Spatial Phase) |
||||
|
The beamforming vector encodes the complete spatial and temporal phase for every point, antenna, and time sample: |
||||
|
```python |
||||
|
beamforming_vector = torch.exp( |
||||
|
1j*2*np.pi*( |
||||
|
radar.f * delta[:,:,None] # Carrier phase across antennas |
||||
|
- 0.5 * k * delta[:,:,None]**2 # Second-order FMCW correction |
||||
|
- k * tau[None,:,None] * delta[:,:,None] # Cross-coupling (range x angle) |
||||
|
+ k * delta[:,:,None] @ t[None,None,:]) # Per-sample phase |
||||
|
) |
||||
|
``` |
||||
|
This is the most mathematically complex line. It computes `exp(j * phase)` which produces a complex unit vector rotating at the beat frequency. The `@` (matrix multiply) operation broadcasts this across all 256 sample times simultaneously on the GPU. |
||||
|
|
||||
|
### Step 3.5: The Dechirped Beat Signal |
||||
|
The actual beat signal (what the mixer outputs) is: |
||||
|
```python |
||||
|
dechirped = torch.exp((1j * 2 * np.pi) * |
||||
|
(radar.f * tau[:,None] + k * tau[:,None] @ t[None,:] - 0.5 * k * tau[:,None]**2)) |
||||
|
``` |
||||
|
This implements the FMCW beat frequency equation directly. For a target at 20 m (`tau = 2*20/3e8 ≈ 133 ns`), this produces a complex sinusoid oscillating at the beat frequency corresponding to that range. |
||||
|
|
||||
|
### Step 3.6: Signal Superposition (The Summation) |
||||
|
All scatterer contributions are combined: |
||||
|
```python |
||||
|
signal_single_antenna = loss_factor * dechirped # Per-scatterer amplitude |
||||
|
signal = beamforming_vector * signal_single_antenna[None,:,:] # Spread across antennas |
||||
|
signal = torch.squeeze(torch.sum(signal, 1)) # Sum all scatterers -> final voltage |
||||
|
``` |
||||
|
At every single ADC time sample, this produces the total complex voltage that a physical antenna would measure if all those LiDAR points were real physical objects. |
||||
|
|
||||
|
### Step 3.7: Hardware Gain Scaling |
||||
|
After superposition, the signal is scaled by the radar's system gain constant: |
||||
|
```python |
||||
|
adc_sampled = torch.sqrt(torch.tensor(radar.gain * _lambda**2 / (4*np.pi)**3)) * signal |
||||
|
``` |
||||
|
This implements the **Friis Transmission Equation** accounting for antenna efficiency and wavelength. `radar.gain = 10**(110/10)` for the current calibration in `ConfigureRadar.py`. |
||||
|
|
||||
|
### Step 3.8: Noise Floor Addition and Tuning |
||||
|
The clean synthetic signal is then corrupted with thermal noise: |
||||
|
```python |
||||
|
adc_sampled = adc_sampled + signal_Noisy |
||||
|
``` |
||||
|
Where `signal_Noisy = radar.get_noise()` generates: |
||||
|
```python |
||||
|
noise_real = np.random.normal(0, 1, size=(nRx, N_sample)) |
||||
|
noise_complex = np.random.normal(0, 1, size=(nRx, N_sample)) |
||||
|
signal_Noisy = (noise_real + 1j * noise_complex) * self.noise_amp |
||||
|
``` |
||||
|
**Why this matters for real-world matching:** |
||||
|
Real radar hardware has a **Noise Figure (NF)** (typically 10-15 dB for automotive radar) which determines the minimum detectable signal. Our `noise_amp = 0.005` parameter controls this floor. If `noise_amp` is too low, every distant weak scatterer (road surface dust, tree leaves) registers as a target. If too high, real targets get buried. |
||||
|
|
||||
|
The current calibration (`noise_amp = 0.005`) was determined empirically through iteration testing to match the CFAR false-alarm rate to realistic values. In a real system, you would characterize the hardware Noise Figure via bench measurements and set `noise_amp` accordingly. |
||||
|
|
||||
|
### Step 3.9: Writing Into the ADC Measurement Cube |
||||
|
The final complex voltages are placed into the output 3D array: |
||||
|
```python |
||||
|
measurement[chirp, :, :] = adc_sampled # Shape: (N_chirps, nRx, N_sample) |
||||
|
``` |
||||
|
After all chirps are processed, `measurement` is the complete raw ADC tensor — a 3D complex array representing what the physical radar hardware would have recorded. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 4. Pipeline Integration: The Complete Data Flow |
||||
|
|
||||
|
The Fox pipeline orchestrates radar signal synthesis and storage through a layered architecture. Understanding this flow is essential for debugging, modifying, or extending the ADC capture capability. |
||||
|
|
||||
|
### 4.1 High-Level Data Flow Diagram |
||||
|
``` |
||||
|
CARA Simulator |
||||
|
└─> Semantic LiDAR (.npy per frame) |
||||
|
└─> generate_shenron.py [Orchestrator] |
||||
|
└─> ShenronRadarModel.process() [model_wrapper.py] |
||||
|
└─> run_lidar() [lidar.py] |
||||
|
└─> heatmap_gen() [heatmap_gen_fast.py] |
||||
|
└─> RAW ADC TENSOR ← saved HERE |
||||
|
└─> reformat_adc_shenron() [utils_radar.py] |
||||
|
└─> REFORMATTED ADC |
||||
|
└─> RadarProcessor [radar_processor.py] |
||||
|
├─> cal_range_fft() → Range Profile |
||||
|
├─> cal_doppler_fft() → Doppler Profile |
||||
|
└─> convert_to_pcd() → Rich Point Cloud + Heatmaps |
||||
|
├─> Saves: <session>/<radar_type>/<frame>.npy (Point Cloud) |
||||
|
├─> Saves: <session>/<radar_type>/adc_raw/<frame>.npy (Raw ADC) |
||||
|
└─> Saves: <session>/<radar_type>/metrology/{rd,ra,cfar}/<frame>.npy |
||||
|
``` |
||||
|
|
||||
|
### 4.2 The Orchestrator: `generate_shenron.py` |
||||
|
This script is the master batch processor. It processes every simulation session in the `data/` directory. |
||||
|
|
||||
|
**Initialization Phase:** |
||||
|
1. For each radar type (`awrl1432`, `radarbook`), a `ShenronRadarModel` is instantiated. |
||||
|
2. The model internally creates a `radar` hardware object (from `ConfigureRadar.py`) and a `RadarProcessor`. |
||||
|
3. Three metrology sub-folders are created: `rd/`, `ra/`, `cfar/`. |
||||
|
4. The physical axes (`range_axis.npy`, `angle_axis.npy`) are saved once per session — these do not change between frames as they are determined by the hardware config. |
||||
|
5. **The `adc_raw/` folder** is also created here, one per radar type per session. |
||||
|
|
||||
|
**Per-Frame Processing Loop:** |
||||
|
```python |
||||
|
# From generate_shenron.py (simplified) |
||||
|
for lidar_file in lidar_files: |
||||
|
data = np.load(lidar_file) # Load LiDAR cloud |
||||
|
rich_pcd = model.process(data) # Run physics synthesis |
||||
|
|
||||
|
# Capture raw ADC from buffer |
||||
|
if hasattr(model, 'last_adc') and model.last_adc is not None: |
||||
|
adc_path = session_path / r_type / 'adc_raw' / lidar_file.name |
||||
|
np.save(adc_path, model.last_adc) # Save raw ADC |
||||
|
|
||||
|
np.save(output_file, rich_pcd) # Save point cloud |
||||
|
``` |
||||
|
|
||||
|
### 4.3 The Physics Bridge: `model_wrapper.py` |
||||
|
The `ShenronRadarModel.process()` method is the critical integration point. |
||||
|
|
||||
|
**Inside `process(data)`:** |
||||
|
```python |
||||
|
# Step 1: Re-sync hardware config (critical for multi-radar sessions) |
||||
|
self._sync_configs() |
||||
|
|
||||
|
# Step 2: Synthesize raw ADC (this calls heatmap_gen_fast.py) |
||||
|
raw_adc = run_lidar(self.sim_config, semantic_lidar_data, radarobj=self.radar_obj) |
||||
|
# raw_adc shape at this point: (N_chirps, nRx, N_sample) — as output by heatmap_gen |
||||
|
|
||||
|
# Step 3: Buffer raw ADC for external access |
||||
|
self.last_adc = raw_adc |
||||
|
adc_data = raw_adc |
||||
|
|
||||
|
# Step 4: Reformat axes to match RadarProcessor expectations |
||||
|
adc_data = reformat_adc_shenron(adc_data) |
||||
|
# After reformat: (N_sample, N_chirps, nRx) — transposed |
||||
|
|
||||
|
# Step 5: Run DSP chain |
||||
|
range_profile = self.processor.cal_range_fft(adc_data) |
||||
|
doppler_profile = self.processor.cal_doppler_fft(range_profile) |
||||
|
_, rich_pcd, metrology = self.processor.convert_to_pcd(doppler_profile) |
||||
|
``` |
||||
|
|
||||
|
### 4.4 The `reformat_adc_shenron()` Step — Why It Matters |
||||
|
The raw ADC from `heatmap_gen_fast.py` has shape `(N_chirps, nRx, N_sample)` because the synthesis loops over chirps and builds the antenna dimension first. |
||||
|
|
||||
|
The `RadarProcessor` in `radar_processor.py` expects data as `(N_sample, N_chirps, nRx)` because its windowing arrays (`rangeWin`, `velWin`) are pre-computed in that axis order. The `reformat_adc_shenron()` function performs this axis reordering: |
||||
|
```python |
||||
|
def reformat_adc_shenron(data): |
||||
|
data = np.swapaxes(data, 1, 2) # Swap axes 1 and 2: (Np, N, nRx) |
||||
|
chirpLevelData = data |
||||
|
return np.transpose(chirpLevelData[:, :, :], (1, 0, 2)) # -> (N, Np, nRx) |
||||
|
``` |
||||
|
This is the step where we **save `self.last_adc = raw_adc` BEFORE this reformat** — so the stored ADC is in the original synthesis shape `(N_chirps, nRx, N_sample)`, closest to the hardware physical format. |
||||
|
|
||||
|
### 4.5 Memory Footprint and Disk Impact |
||||
|
| Radar | Chirps | Samples | Antennas | Per-Frame Size (complex64) | |
||||
|
|---|---|---|---|---| |
||||
|
| AWRL1432 | 128 | 256 | 6 | 128×256×6×8 bytes ≈ **1.5 MB** | |
||||
|
| Radarbook | 128 | 256 | 8 | 128×256×8×8 bytes ≈ **2.0 MB** | |
||||
|
|
||||
|
* A 500-frame braking scenario generates ~750 MB of ADC data (both radars combined). |
||||
|
* Compared to the point cloud (~4 KB per frame), ADC data is ~375x larger. |
||||
|
* If disk space is constrained, set a frame sub-sampling factor in `generate_shenron.py`. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 5. DSP Implementation: From ADC to Heatmaps (Codebase Walkthrough) |
||||
|
|
||||
|
This section traces exactly how the stored ADC data is processed into the RD and RA heatmaps visible in the Foxglove dashboard. All code references are from `scripts/ISOLATE/sim_radar_utils/radar_processor.py`. |
||||
|
|
||||
|
### 5.1 The RadarProcessor Initialization |
||||
|
When `ShenronRadarModel` is created, it instantiates a `RadarProcessor`. Inside `__init__`, the processor pre-computes all the windowing arrays and physical axes that will be reused for every frame: |
||||
|
|
||||
|
```python |
||||
|
# Hanning windows for Range and Doppler dimensions |
||||
|
self.rangeWin = np.tile(signal.windows.hann(radarCfg['N']), (radarCfg['Np'], radarCfg['NrChn'], 1)) |
||||
|
self.rangeWin = np.transpose(self.rangeWin, (2, 0, 1)) # -> (N_sample, N_chirps, nRx) |
||||
|
|
||||
|
self.velWin = np.tile(signal.windows.hann(radarCfg['Np']), (radarCfg['N'], radarCfg['NrChn'], 1)) |
||||
|
self.velWin = np.transpose(self.velWin, (0, 2, 1)) # -> (N_sample, N_chirps, nRx) |
||||
|
``` |
||||
|
**Why Hanning Windows?** When you take an FFT of a finite signal, the sharp edges at the start and end of the data cause the frequency content to "smear" into adjacent bins (Spectral Leakage). A Hanning window gently tapers the data to zero at both edges, dramatically reducing this leakage and creating cleaner peaks. It is the equivalent of slowly fading in and fading out a recording instead of cutting it sharply. |
||||
|
|
||||
|
Physical range and velocity axes are pre-calculated: |
||||
|
```python |
||||
|
rangeRes = c0 / (2 * (fStop - fStrt)) # metres per bin |
||||
|
self.rangeAxis = np.arange(0, NFFT) * N/NFFT * rangeRes # physical range values |
||||
|
self.velAxis = np.arange(-NFFTVel//2, NFFTVel//2)/NFFTVel * (1/Tp) * c0/(2*fc) # m/s |
||||
|
``` |
||||
|
|
||||
|
### 5.2 Step 1: The Range FFT (`cal_range_fft`) |
||||
|
```python |
||||
|
def cal_range_fft(self, data): |
||||
|
return fft(data * self.rangeWin * self.velWin, fftCfg['NFFT'], 0) |
||||
|
``` |
||||
|
What this does: |
||||
|
1. **Windowing:** `data * self.rangeWin * self.velWin` applies the Hanning window to both the samples and chirps dimensions simultaneously. |
||||
|
2. **FFT along axis 0:** The FFT is applied across the N_sample (range) dimension. For each chirp and each antenna, the time-domain beat frequency signal is converted into a frequency-domain spectrum. Each frequency bin maps to a specific range. |
||||
|
3. **Output shape:** `(NFFT, N_chirps, nRx)` where `NFFT >= N_sample` (zero-padded for interpolation). |
||||
|
|
||||
|
After this step, you have a **Range Profile** — for each antenna and each chirp, you can see which distances have strong reflections. |
||||
|
|
||||
|
### 5.3 Step 2: The Doppler FFT (`cal_doppler_fft`) |
||||
|
```python |
||||
|
def cal_doppler_fft(self, rangeProfile): |
||||
|
return fftshift(fft(rangeProfile[self.RMinIdx:self.RMaxIdx+1, :], fftCfg['NFFTVel'], 1), 1) |
||||
|
``` |
||||
|
What this does: |
||||
|
1. **Range Clipping:** `rangeProfile[RMinIdx:RMaxIdx+1, :]` discards range bins outside the configured `RMin`/`RMax` window (set in `config.yaml`). |
||||
|
2. **FFT along axis 1:** The FFT is applied across the N_chirps (Doppler) dimension. For each range bin and each antenna, the slow phase rotation from chirp to chirp is converted into a velocity spectrum. |
||||
|
3. **`fftshift`:** By default, FFT places zero frequency at index 0, with the positive half first and negative half at the end. `fftshift` reorders this so zero velocity is in the centre, negative velocities are on the left, and positive are on the right — exactly as seen in the RD heatmap. |
||||
|
4. **Output shape:** `(N_range_bins, NFFTVel, nRx)`. |
||||
|
|
||||
|
### 5.4 Step 3: Range-Doppler Heatmap (Inside `convert_to_pcd`) |
||||
|
```python |
||||
|
rd_heatmap = np.sum(np.abs(dopplerProfile)**2, axis=2) |
||||
|
``` |
||||
|
This collapses the antenna dimension by: |
||||
|
1. `np.abs(dopplerProfile)` — computing the magnitude of each complex number (clock hand length). |
||||
|
2. `**2` — squaring it to get power (energy) rather than amplitude. |
||||
|
3. `np.sum(..., axis=2)` — incoherent summation across all `nRx` antennas. |
||||
|
|
||||
|
"Incoherent" means we add the *powers* (not the complex voltages). This avoids antenna-phase cancellation from interfering signals. The result is a 2D array `(N_range_bins, NFFTVel)` — the brightly coloured Range-Doppler map. |
||||
|
|
||||
|
### 5.5 Step 4: CFAR Detection |
||||
|
```python |
||||
|
hit_matrix, threshold_matrix = self.cfar(rd_heatmap) |
||||
|
``` |
||||
|
The `CA_CFAR` (Cell Averaging CFAR) detector scans every cell of the RD heatmap. For each cell: |
||||
|
1. It looks at a window of surrounding cells (guard cells excluded) and averages their power — this is the estimated noise floor. |
||||
|
2. It multiplies by a threshold factor: `threshold = noise_floor * cfar_threshold`. |
||||
|
3. If the current cell exceeds this threshold, it is marked as a target in `hit_matrix`. |
||||
|
|
||||
|
The `threshold_matrix` (the CFAR noise floor) is also saved as the `cfar` metrology file. |
||||
|
|
||||
|
### 5.6 Step 5: Angle of Arrival via Antenna FFT |
||||
|
For each detected target `(row, col)` in the RD map: |
||||
|
```python |
||||
|
pointSel[i] = dopplerProfile[row, col, :] # Complex voltages at the 8 antennas |
||||
|
pointSel_windowed = pointSel * self.spatialWin # Apply Hanning spatial window |
||||
|
aoaProfile = fftshift(fft(pointSel_windowed, fftCfg['NFFTAnt'], 1), 1) |
||||
|
angleIdx = np.argmax(np.abs(aoaProfile), axis=1) # Peak = angle bin |
||||
|
angleVals = self.angleAxis[angleIdx] # Convert bin to radians |
||||
|
``` |
||||
|
The complex voltages across the 8 antennas form a spatial array. An FFT across this array converts inter-antenna phase differences into an angle spectrum. The peak of this spectrum is the Angle of Arrival. Combined with range (`rangeVals = self.rangeAxis[rowSel]`), we get $(x, y)$ positions. |
||||
|
|
||||
|
### 5.7 Step 6: Range-Azimuth Heatmap (Doppler-Slice Accumulation) |
||||
|
```python |
||||
|
ra_heatmap = np.zeros((N_range, fftCfg['NFFTAnt']), dtype=np.float64) |
||||
|
for d in range(N_doppler): |
||||
|
doppler_slice = dopplerProfile[:, d, :] # One velocity slice |
||||
|
doppler_slice_windowed = doppler_slice * self.spatialWin |
||||
|
angle_fft = fftshift(fft(doppler_slice_windowed, fftCfg['NFFTAnt'], axis=1), axes=1) |
||||
|
ra_heatmap += np.abs(angle_fft) ** 2 # Accumulate power |
||||
|
``` |
||||
|
This is the most important difference from a simple RA plot. By iterating through every Doppler bin and doing the Angle-FFT independently, we preserve the angular sharpness of moving targets. If we had collapsed the Doppler dimension first (averaged all chirps), a moving target's phase would smear across the chirps and produce a blurred angle estimate. |
||||
|
|
||||
|
The resulting `ra_heatmap` is saved as the `ra` metrology file and rendered as the BEV (Bird's Eye View) polar sector image in Foxglove. |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 6. The "Reverse DSP" Fallacy: A Deep Dive |
||||
|
|
||||
|
A persistent engineering hypothesis is: *If we have the final Radar Point Cloud, can we read the $(x, y, z)$ coordinates and run the math backward to re-create the raw ADC data?* |
||||
|
|
||||
|
This question is technically answerable with "yes, you can generate *a* signal," but the critical answer is **no, you cannot recover the *original* signal.** The two are fundamentally different things, and conflating them leads to systematically over-optimistic simulations. |
||||
|
|
||||
|
### 6.1 The One-Way Information Bottleneck |
||||
|
The entire DSP chain from ADC to Point Cloud is a **lossy compression pipeline**: |
||||
|
|
||||
|
``` |
||||
|
RAW ADC (131,072 complex numbers per frame) |
||||
|
| Range FFT |
||||
|
v |
||||
|
Range Profile (still 131,072 numbers, but transformed) |
||||
|
| Doppler FFT |
||||
|
v |
||||
|
RD Heatmap (reduced to range_bins x doppler_bins = ~5,000 numbers) |
||||
|
| CFAR threshold (discards ~99% of cells) |
||||
|
v |
||||
|
Point Cloud (perhaps 20-50 complex detections) |
||||
|
``` |
||||
|
At each step, information is permanently discarded. Going backwards from 20 points to 131,072 complex numbers requires *inventing* the missing 131,052 numbers. There is no mathematical inverse of a lossy compression. |
||||
|
|
||||
|
### 6.2 The "Silent Background" Problem — CFAR Entropy Loss |
||||
|
In `radar_processor.py`, the `CA_CFAR` detector examines every cell of the RD heatmap. It estimates the local noise floor by averaging surrounding cells and only retains cells exceeding a threshold. Everything below the threshold — the noise floor itself, ground clutter, and marginal reflections — is discarded. |
||||
|
|
||||
|
**The concrete consequence:** A reconstructed ADC signal has absolute silence between targets. When you re-run CFAR on this reconstructed ADC, the adaptive threshold drops to near-zero because there is no noise floor for CFAR to measure. Every tiny numerical artefact exceeds the threshold. The CFAR algorithm, calibrated against a real noise floor, now produces hundreds of false detections in a signal that was supposed to be clean. |
||||
|
|
||||
|
This is the "clean-room" paradox: the cleaner you make the reconstructed data, the more wrong CFAR behaves on it. |
||||
|
|
||||
|
### 6.3 The Illusion of Perfect Sidelobes |
||||
|
When an FFT processes a finite-duration signal, energy unavoidably leaks into neighbouring frequency bins. These ripples are called **Sidelobes**. Every real target produces a main peak surrounded by a pattern of sidelobes. |
||||
|
|
||||
|
* **In real hardware:** The sidelobes are distorted by Local Oscillator (LO) phase noise (random jitter in the clock), amplifier non-linearities (signal clipping), and temperature-dependent gain drift. The sidelobe pattern is messy and irregular. |
||||
|
* **In reconstructed ADC:** Each target is represented by a pure, perfect complex sinusoid. Its FFT produces textbook-perfect sidelobes defined only by the Hanning window function. There is no irregularity. |
||||
|
|
||||
|
Consequence: A CFAR algorithm tuned on real hardware sidelobes will behave completely differently when tested on perfect-sidelobe reconstructed data. A tracker that correctly ignores real hardware sidelobes may falsely detect the perfect sidelobes as real targets, or vice versa. |
||||
|
|
||||
|
### 6.4 Multipath Erasure |
||||
|
In the real physical world (and in our Shenron simulation from LiDAR), electromagnetic waves bounce multiple times. A signal transmitted from the radar may: |
||||
|
1. Travel directly to a car and reflect back — the primary detection. |
||||
|
2. Bounce off the ground first, hit the car at an angle, and return — a "Ghost" at a slightly different apparent range. |
||||
|
3. Reflect off the car, bounce off a nearby wall, and return after an extra delay — another ghost. |
||||
|
|
||||
|
These multipath reflections create **Ghost Targets** in the RD map. The CFAR algorithm may or may not detect them as targets depending on their strength. If they survive into the point cloud, they appear as extra spurious points. |
||||
|
|
||||
|
**The fallacy:** If the point cloud has already filtered out ghost targets (or included them), reconstructing ADC from just the surviving real targets produces a signal with zero multipath. Testing a tracker on this tells you nothing about how it handles real-world multipath environments. |
||||
|
|
||||
|
### 6.5 Irreversible Sub-Resolution Merging |
||||
|
The Radarbook (B=250 MHz) has a range resolution of 0.6 m. This means any two objects closer than 0.6 m to each other are indistinguishable in range — their beats overlap and appear as one stronger peak. |
||||
|
|
||||
|
If two pedestrians are walking 0.4 m apart, the CFAR detects them as a single, stronger point at the midpoint between them. The point cloud stores one point. When you reconstruct ADC from that one point, you generate one single sine wave at the midpoint. There is no mathematical transformation that recovers two separate sine waves from that one point — the information is permanently merged. |
||||
|
|
||||
|
### 6.6 Phase Matrix Loss — The AoA Bottleneck |
||||
|
Angle of Arrival estimation extracts the target angle from the inter-antenna phase pattern. After extracting the angle (`angleVals` in `convert_to_pcd`), the original complex phase across the 8 antennas is immediately discarded — only the angle in degrees is stored in the point cloud. |
||||
|
|
||||
|
To reconstruct ADC for that target, you need to synthesize the complex voltages at all 8 antennas. You can compute what those phases *should* be given the stored angle, but there are two problems: |
||||
|
1. **Quantization error:** The stored angle is a single bin from an FFT. The true angle might be between two bins. The reconstruction picks exactly one bin, creating a slightly wrong phase pattern. |
||||
|
2. **Amplitude ambiguity:** The relative magnitudes across antennas were shaped by the real hardware antenna pattern. Unless you model that pattern precisely, the per-antenna amplitudes will be wrong, producing an AoA estimate that is biased after re-processing. |
||||
|
|
||||
|
### 6.7 When Reverse DSP Is Acceptable |
||||
|
Despite all the above limitations, there are narrow use cases where reconstructing ADC from a point cloud is acceptable: |
||||
|
* **Unit testing DSP code:** If you want to verify that your `cal_range_fft` and `cal_doppler_fft` functions correctly detect a target at 30 m, constructing a synthetic single-target ADC (a pure sine wave at the 30 m beat frequency) is a valid unit test. You are testing the code logic, not the realism. |
||||
|
* **HIL injection with known ground truth:** If you inject a synthetic perfect ADC into a hardware ECU and measure where the ECU places the detection, you are characterizing the hardware's detection bias, not testing its performance against real noise. |
||||
|
|
||||
|
For any scenario where you need the result to be comparable to real-world driving data, you must use the forward path: **CARLA → LiDAR → ADC Synthesis → DSP → Detection.** |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
## 7. Advanced Use Cases: The Value of Raw ADC |
||||
|
Given the memory footprint, why did we modify the Fox pipeline to capture ADC data? |
||||
|
|
||||
|
1. **Custom DSP Architecture Design:** Engineers can bypass `sim_radar_utils`. With raw ADC data, they can write and test custom FFT windowing, dynamic 2D-CFAR algorithms, or Super-Resolution Angle estimation algorithms (like MUSIC or ESPRIT) directly in Python, validating changes without re-running the heavy CARLA simulation. |
||||
|
2. **Machine Learning & Neural Perception:** The cutting edge of AI research abandons traditional FFT/CFAR chains. Instead, massive 3D Complex ADC cubes are fed directly into 3D Convolutional Neural Networks (3D-CNNs) or Spatiotemporal Transformers. The AI learns the physics directly from the raw voltage, bypassing human-engineered thresholds. |
||||
|
3. **Hardware-in-the-Loop (HIL) Stimulation:** Raw ADC matrices can be formatted and injected directly into the digital baseband ports of physical radar Electronic Control Units (ECUs) on a testbench. This physically tricks the silicon processor into "seeing" the virtual CARLA simulation, allowing for hardware validation before the sensor is ever mounted on a car. |
||||
|
|
||||
|
--- |
||||
|
*Created: 2026-04-30 | Part of the Fox Radar Metrology Suite | Generated via Antigravity Agentic Memory.* |
||||
@ -0,0 +1,140 @@ |
|||||
|
import os |
||||
|
import sys |
||||
|
import time |
||||
|
import numpy as np |
||||
|
import json |
||||
|
from pathlib import Path |
||||
|
|
||||
|
# Ensure we can import the model wrapper |
||||
|
sys.path.append(str(Path(__file__).parent)) |
||||
|
try: |
||||
|
from model_wrapper import ShenronRadarModel |
||||
|
except ImportError: |
||||
|
# Fallback if called from a different context |
||||
|
from scripts.ISOLATE.model_wrapper import ShenronRadarModel |
||||
|
|
||||
|
class ShenronOrchestrator: |
||||
|
""" |
||||
|
Unified orchestration engine for physics-based radar synthesis. |
||||
|
Ensures parity between production generation (dashboard) and iterative testbench (test_shenron). |
||||
|
""" |
||||
|
|
||||
|
def __init__(self, radar_types=['awrl1432', 'radarbook']): |
||||
|
self.radar_types = radar_types |
||||
|
self.models = {} |
||||
|
|
||||
|
def init_models(self, output_root: Path): |
||||
|
"""Initializes models and prepares directory structure.""" |
||||
|
specs = {} |
||||
|
# Resolve project root for stop flag check (root/scripts/ISOLATE/shenron_orchestrator.py) |
||||
|
self.project_root = Path(__file__).resolve().parents[2] |
||||
|
self.flag_path = self.project_root / "tmp" / "stop.flag" |
||||
|
|
||||
|
for r_type in self.radar_types: |
||||
|
try: |
||||
|
print(f" - Initializing Shenron {r_type} engine...") |
||||
|
model = ShenronRadarModel(radar_type=r_type) |
||||
|
self.models[r_type] = model |
||||
|
|
||||
|
# Setup Folders |
||||
|
radar_dir = output_root / r_type |
||||
|
radar_dir.mkdir(exist_ok=True, parents=True) |
||||
|
|
||||
|
met_base = radar_dir / "metrology" |
||||
|
for sub in ["rd", "ra", "cfar"]: |
||||
|
(met_base / sub).mkdir(parents=True, exist_ok=True) |
||||
|
|
||||
|
# Save physical axes (static per session) |
||||
|
np.save(met_base / "range_axis.npy", model.processor.rangeAxis) |
||||
|
np.save(met_base / "angle_axis.npy", model.processor.angleAxis) |
||||
|
|
||||
|
# Get hardware specs |
||||
|
specs[r_type] = model.get_radar_specs() |
||||
|
|
||||
|
# Save specs for MCAP converter downstream |
||||
|
hw_specs = { |
||||
|
'f': float(model.radar_obj.f), |
||||
|
'chirp_rep': float(model.radar_obj.chirp_rep), |
||||
|
'max_velocity': float((3e8 / model.radar_obj.f) / (4 * model.radar_obj.chirp_rep)), |
||||
|
} |
||||
|
with open(met_base / "radar_specs.json", "w") as sf: |
||||
|
json.dump(hw_specs, sf) |
||||
|
|
||||
|
except Exception as e: |
||||
|
print(f" [WARNING] Failed to init {r_type}: {e}") |
||||
|
continue |
||||
|
return specs |
||||
|
|
||||
|
def process_frame(self, lidar_file: Path, output_root: Path, save_adc=False): |
||||
|
"""Processes a single LiDAR frame through all active radar models.""" |
||||
|
try: |
||||
|
data = np.load(lidar_file) |
||||
|
except Exception as e: |
||||
|
print(f" [ERROR] Failed to load {lidar_file.name}: {e}") |
||||
|
return None |
||||
|
|
||||
|
# Standard Padding: Ensure [x, y, z, intensity, cos_inc_angle, obj, tag] |
||||
|
if data.shape[1] == 6: |
||||
|
padded_data = np.zeros((data.shape[0], 7), dtype=np.float32) |
||||
|
padded_data[:, 0:3] = data[:, 0:3] |
||||
|
padded_data[:, 4:7] = data[:, 3:6] |
||||
|
data = padded_data |
||||
|
|
||||
|
frame_results = {} |
||||
|
|
||||
|
for r_type, model in self.models.items(): |
||||
|
try: |
||||
|
# 1. Physics Processing |
||||
|
rich_pcd = model.process(data) |
||||
|
|
||||
|
# 2. Save Pointcloud |
||||
|
output_file = output_root / r_type / lidar_file.name |
||||
|
np.save(output_file, rich_pcd) |
||||
|
|
||||
|
# 3. Optional ADC Saving |
||||
|
if save_adc and hasattr(model, "last_adc") and model.last_adc is not None: |
||||
|
adc_folder = output_root / r_type / "adc_raw" |
||||
|
adc_folder.mkdir(parents=True, exist_ok=True) |
||||
|
np.save(adc_folder / lidar_file.name, model.last_adc) |
||||
|
|
||||
|
# 4. Save Metrology (.npy) |
||||
|
met = model.get_last_metrology() |
||||
|
met_base = output_root / r_type / "metrology" |
||||
|
if met: |
||||
|
frame_name = lidar_file.stem |
||||
|
np.save(met_base / "rd" / f"{frame_name}.npy", met['rd_heatmap']) |
||||
|
np.save(met_base / "ra" / f"{frame_name}.npy", met['ra_heatmap']) |
||||
|
np.save(met_base / "cfar" / f"{frame_name}.npy", met['threshold_matrix']) |
||||
|
|
||||
|
# 5. Extract and Save Metrics |
||||
|
metrics = model.get_signal_metrics() |
||||
|
if metrics: |
||||
|
# Clean for JSON (handle NaN/Inf) |
||||
|
clean_metrics = {} |
||||
|
for k, v in metrics.items(): |
||||
|
if isinstance(v, float) and (np.isnan(v) or np.isinf(v)): |
||||
|
clean_metrics[k] = 0.0 |
||||
|
else: |
||||
|
clean_metrics[k] = v |
||||
|
|
||||
|
# Append to metrics log |
||||
|
metrics_file = met_base / "metrics.jsonl" |
||||
|
with open(metrics_file, "a") as f: |
||||
|
f.write(json.dumps({"frame": lidar_file.stem, **clean_metrics}) + "\n") |
||||
|
|
||||
|
# Add point count for telemetry |
||||
|
clean_metrics["pts"] = int(rich_pcd.shape[0]) if hasattr(rich_pcd, 'shape') else 0 |
||||
|
frame_results[r_type] = clean_metrics |
||||
|
|
||||
|
except Exception as e: |
||||
|
print(f" [ERROR] {r_type} processing failed for {lidar_file.name}: {e}") |
||||
|
continue |
||||
|
|
||||
|
return frame_results |
||||
|
|
||||
|
def check_stop_flag(self) -> bool: |
||||
|
"""Check if a stop request has been issued by the user/dashboard.""" |
||||
|
if hasattr(self, 'flag_path') and self.flag_path.exists(): |
||||
|
print(f"\n[SHENRON] Stop flag detected at {self.flag_path}! Halting processing...") |
||||
|
return True |
||||
|
return False |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue