710 lines
25 KiB
Python
710 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Synthetic Radar Scene Generator for AERIS-10 FPGA Co-simulation.
|
|
|
|
Generates test vectors (ADC samples + reference chirps) for multi-target
|
|
radar scenes with configurable:
|
|
- Target range, velocity, RCS
|
|
- Noise floor and clutter
|
|
- ADC quantization (8-bit, 400 MSPS)
|
|
|
|
Output formats:
|
|
- Hex files for Verilog $readmemh
|
|
- CSV for analysis
|
|
- Python arrays for direct use with fpga_model.py
|
|
|
|
The scene generator models the complete RF path:
|
|
TX chirp -> propagation delay -> Doppler shift -> RX IF signal -> ADC
|
|
|
|
Author: Phase 0.5 co-simulation suite for PLFM_RADAR
|
|
"""
|
|
|
|
import math
|
|
import os
|
|
import struct
|
|
|
|
|
|
# =============================================================================
|
|
# AERIS-10 System Parameters
|
|
# =============================================================================
|
|
|
|
# RF parameters
|
|
F_CARRIER = 10.5e9 # 10.5 GHz carrier
|
|
C_LIGHT = 3.0e8 # Speed of light (m/s)
|
|
WAVELENGTH = C_LIGHT / F_CARRIER # ~0.02857 m
|
|
|
|
# Chirp parameters
|
|
F_IF = 120e6 # IF frequency (120 MHz)
|
|
CHIRP_BW = 20e6 # Chirp bandwidth (30 MHz -> 10 MHz = 20 MHz sweep)
|
|
F_CHIRP_START = 30e6 # Chirp start frequency (relative to IF)
|
|
F_CHIRP_END = 10e6 # Chirp end frequency (relative to IF)
|
|
|
|
# Sampling
|
|
FS_ADC = 400e6 # ADC sample rate (400 MSPS)
|
|
FS_SYS = 100e6 # System clock (100 MHz)
|
|
ADC_BITS = 8 # ADC resolution
|
|
|
|
# Chirp timing
|
|
T_LONG_CHIRP = 30e-6 # 30 us long chirp duration
|
|
T_SHORT_CHIRP = 0.5e-6 # 0.5 us short chirp
|
|
T_LISTEN_LONG = 137e-6 # 137 us listening window
|
|
T_PRI_LONG = 167e-6 # 30 us chirp + 137 us listen
|
|
T_PRI_SHORT = 175e-6 # staggered short-PRI sub-frame
|
|
N_SAMPLES_LISTEN = int(T_LISTEN_LONG * FS_ADC) # 54800 samples
|
|
|
|
# Processing chain
|
|
CIC_DECIMATION = 4
|
|
FFT_SIZE = 1024
|
|
RANGE_BINS = 64
|
|
DOPPLER_FFT_SIZE = 16 # Per sub-frame
|
|
DOPPLER_TOTAL_BINS = 32 # Total output bins (2 sub-frames x 16)
|
|
CHIRPS_PER_SUBFRAME = 16
|
|
CHIRPS_PER_FRAME = 32
|
|
|
|
# Derived
|
|
RANGE_RESOLUTION = C_LIGHT / (2 * CHIRP_BW) # 7.5 m
|
|
MAX_UNAMBIGUOUS_RANGE = C_LIGHT * T_LISTEN_LONG / 2 # ~20.55 km
|
|
VELOCITY_RESOLUTION_LONG = WAVELENGTH / (2 * CHIRPS_PER_SUBFRAME * T_PRI_LONG)
|
|
VELOCITY_RESOLUTION_SHORT = WAVELENGTH / (2 * CHIRPS_PER_SUBFRAME * T_PRI_SHORT)
|
|
|
|
# Short chirp LUT (60 entries, 8-bit unsigned)
|
|
SHORT_CHIRP_LUT = [
|
|
255, 237, 187, 118, 49, 6, 7, 54, 132, 210, 253, 237, 167, 75, 10, 10,
|
|
80, 180, 248, 237, 150, 45, 1, 54, 167, 249, 228, 118, 15, 18, 127, 238,
|
|
235, 118, 10, 34, 167, 254, 187, 45, 8, 129, 248, 201, 49, 10, 145, 254,
|
|
167, 17, 46, 210, 235, 75, 7, 155, 253, 118, 1, 129,
|
|
]
|
|
|
|
|
|
# =============================================================================
|
|
# Target definition
|
|
# =============================================================================
|
|
|
|
class Target:
|
|
"""Represents a radar target."""
|
|
|
|
def __init__(self, range_m, velocity_mps=0.0, rcs_dbsm=0.0, phase_deg=0.0):
|
|
"""
|
|
Args:
|
|
range_m: Target range in meters
|
|
velocity_mps: Target radial velocity in m/s (positive = approaching)
|
|
rcs_dbsm: Radar cross-section in dBsm
|
|
phase_deg: Initial phase in degrees
|
|
"""
|
|
self.range_m = range_m
|
|
self.velocity_mps = velocity_mps
|
|
self.rcs_dbsm = rcs_dbsm
|
|
self.phase_deg = phase_deg
|
|
|
|
@property
|
|
def delay_s(self):
|
|
"""Round-trip delay in seconds."""
|
|
return 2 * self.range_m / C_LIGHT
|
|
|
|
@property
|
|
def delay_samples(self):
|
|
"""Round-trip delay in ADC samples at 400 MSPS."""
|
|
return self.delay_s * FS_ADC
|
|
|
|
@property
|
|
def doppler_hz(self):
|
|
"""Doppler frequency shift in Hz."""
|
|
return 2 * self.velocity_mps * F_CARRIER / C_LIGHT
|
|
|
|
@property
|
|
def amplitude(self):
|
|
"""Linear amplitude from RCS (arbitrary scaling for ADC range)."""
|
|
# Simple model: amplitude proportional to sqrt(RCS) / R^2
|
|
# Normalized so 0 dBsm at 100m gives roughly 50% ADC scale
|
|
rcs_linear = 10 ** (self.rcs_dbsm / 10.0)
|
|
if self.range_m <= 0:
|
|
return 0.0
|
|
amp = math.sqrt(rcs_linear) / (self.range_m ** 2)
|
|
# Scale to ADC range: 100m/0dBsm -> ~64 counts (half of 128 peak-to-peak)
|
|
return amp * (100.0 ** 2) * 64.0
|
|
|
|
def __repr__(self):
|
|
return (f"Target(range={self.range_m:.1f}m, vel={self.velocity_mps:.1f}m/s, "
|
|
f"RCS={self.rcs_dbsm:.1f}dBsm, delay={self.delay_samples:.1f}samp)")
|
|
|
|
|
|
# =============================================================================
|
|
# IF chirp signal generation
|
|
# =============================================================================
|
|
|
|
def generate_if_chirp(n_samples, chirp_bw=CHIRP_BW, f_if=F_IF, fs=FS_ADC):
|
|
"""
|
|
Generate an IF chirp signal (the transmitted waveform as seen at IF).
|
|
|
|
This models the PLFM chirp as a linear frequency sweep around the IF.
|
|
The ADC sees this chirp after mixing with the LO.
|
|
|
|
Args:
|
|
n_samples: number of samples to generate
|
|
chirp_bw: chirp bandwidth in Hz
|
|
f_if: IF center frequency in Hz
|
|
fs: sample rate in Hz
|
|
|
|
Returns:
|
|
(chirp_i, chirp_q): lists of float I/Q samples (normalized to [-1, 1])
|
|
"""
|
|
chirp_i = []
|
|
chirp_q = []
|
|
chirp_rate = chirp_bw / (n_samples / fs) # Hz/s
|
|
|
|
for n in range(n_samples):
|
|
t = n / fs
|
|
# Instantaneous frequency: f_if - chirp_bw/2 + chirp_rate * t
|
|
# Phase: integral of 2*pi*f(t)*dt
|
|
f_inst = f_if - chirp_bw / 2 + chirp_rate * t
|
|
phase = 2 * math.pi * (f_if - chirp_bw / 2) * t + math.pi * chirp_rate * t * t
|
|
chirp_i.append(math.cos(phase))
|
|
chirp_q.append(math.sin(phase))
|
|
|
|
return chirp_i, chirp_q
|
|
|
|
|
|
def generate_reference_chirp_q15(n_fft=FFT_SIZE, chirp_bw=CHIRP_BW, f_if=F_IF, fs=FS_ADC):
|
|
"""
|
|
Generate a reference chirp in Q15 format for the matched filter.
|
|
|
|
The reference chirp is the expected received signal (zero-delay, zero-Doppler).
|
|
Padded with zeros to FFT_SIZE.
|
|
|
|
Returns:
|
|
(ref_re, ref_im): lists of N_FFT signed 16-bit integers
|
|
"""
|
|
# Generate chirp for a reasonable number of samples
|
|
# The chirp duration determines how many samples of the reference are non-zero
|
|
# For 30 us chirp at 100 MHz (after decimation): 3000 samples
|
|
# But FFT is 1024, so we use 1024 samples of the chirp
|
|
chirp_samples = min(n_fft, int(T_LONG_CHIRP * FS_SYS))
|
|
|
|
ref_re = [0] * n_fft
|
|
ref_im = [0] * n_fft
|
|
|
|
chirp_rate = chirp_bw / T_LONG_CHIRP
|
|
|
|
for n in range(chirp_samples):
|
|
t = n / FS_SYS
|
|
# After DDC, the chirp is at baseband
|
|
# The beat frequency from a target at delay tau is: f_beat = chirp_rate * tau
|
|
# Reference chirp is the TX chirp at baseband (zero delay)
|
|
phase = math.pi * chirp_rate * t * t
|
|
re_val = int(round(32767 * 0.9 * math.cos(phase)))
|
|
im_val = int(round(32767 * 0.9 * math.sin(phase)))
|
|
ref_re[n] = max(-32768, min(32767, re_val))
|
|
ref_im[n] = max(-32768, min(32767, im_val))
|
|
|
|
return ref_re, ref_im
|
|
|
|
|
|
# =============================================================================
|
|
# ADC sample generation with targets
|
|
# =============================================================================
|
|
|
|
def generate_adc_samples(targets, n_samples, noise_stddev=3.0,
|
|
clutter_amplitude=0.0, seed=42):
|
|
"""
|
|
Generate synthetic ADC samples for a radar scene.
|
|
|
|
Models:
|
|
- Multiple targets at different ranges (delays)
|
|
- Each target produces a delayed, attenuated copy of the TX chirp at IF
|
|
- Doppler shift applied as phase rotation
|
|
- Additive white Gaussian noise
|
|
- Optional clutter
|
|
|
|
Args:
|
|
targets: list of Target objects
|
|
n_samples: number of ADC samples at 400 MSPS
|
|
noise_stddev: noise standard deviation in ADC LSBs
|
|
clutter_amplitude: clutter amplitude in ADC LSBs
|
|
seed: random seed for reproducibility
|
|
|
|
Returns:
|
|
list of n_samples 8-bit unsigned integers (0-255)
|
|
"""
|
|
# Simple LCG random number generator (no numpy dependency)
|
|
rng_state = seed
|
|
def next_rand():
|
|
nonlocal rng_state
|
|
rng_state = (rng_state * 1103515245 + 12345) & 0x7FFFFFFF
|
|
return rng_state
|
|
|
|
def rand_gaussian():
|
|
"""Box-Muller transform using LCG."""
|
|
while True:
|
|
u1 = (next_rand() / 0x7FFFFFFF)
|
|
u2 = (next_rand() / 0x7FFFFFFF)
|
|
if u1 > 1e-10:
|
|
break
|
|
return math.sqrt(-2.0 * math.log(u1)) * math.cos(2.0 * math.pi * u2)
|
|
|
|
# Generate TX chirp (at IF) - this is what the ADC would see from a target
|
|
chirp_rate = CHIRP_BW / T_LONG_CHIRP
|
|
chirp_samples = int(T_LONG_CHIRP * FS_ADC) # 12000 samples at 400 MSPS
|
|
|
|
adc_float = [0.0] * n_samples
|
|
|
|
for target in targets:
|
|
delay_samp = target.delay_samples
|
|
amp = target.amplitude
|
|
doppler_hz = target.doppler_hz
|
|
phase0 = target.phase_deg * math.pi / 180.0
|
|
|
|
for n in range(n_samples):
|
|
# Check if this sample falls within the delayed chirp
|
|
n_delayed = n - delay_samp
|
|
if n_delayed < 0 or n_delayed >= chirp_samples:
|
|
continue
|
|
|
|
t = n / FS_ADC
|
|
t_delayed = n_delayed / FS_ADC
|
|
|
|
# Signal at IF: cos(2*pi*f_if*t + pi*chirp_rate*t_delayed^2 + doppler + phase)
|
|
phase = (2 * math.pi * F_IF * t
|
|
+ math.pi * chirp_rate * t_delayed * t_delayed
|
|
+ 2 * math.pi * doppler_hz * t
|
|
+ phase0)
|
|
|
|
adc_float[n] += amp * math.cos(phase)
|
|
|
|
# Add noise
|
|
for n in range(n_samples):
|
|
adc_float[n] += noise_stddev * rand_gaussian()
|
|
|
|
# Add clutter (slow-varying, correlated noise)
|
|
if clutter_amplitude > 0:
|
|
clutter_phase = 0.0
|
|
clutter_freq = 0.001 # Very slow variation
|
|
for n in range(n_samples):
|
|
clutter_phase += 2 * math.pi * clutter_freq
|
|
adc_float[n] += clutter_amplitude * math.sin(clutter_phase + rand_gaussian() * 0.1)
|
|
|
|
# Quantize to 8-bit unsigned (0-255), centered at 128
|
|
adc_samples = []
|
|
for val in adc_float:
|
|
quantized = int(round(val + 128))
|
|
quantized = max(0, min(255, quantized))
|
|
adc_samples.append(quantized)
|
|
|
|
return adc_samples
|
|
|
|
|
|
def generate_baseband_samples(targets, n_samples_baseband, noise_stddev=0.5,
|
|
seed=42):
|
|
"""
|
|
Generate synthetic baseband I/Q samples AFTER DDC.
|
|
|
|
This bypasses the DDC entirely, generating what the DDC output should look
|
|
like for given targets. Useful for testing matched filter and downstream
|
|
processing without running through NCO/mixer/CIC/FIR.
|
|
|
|
Each target produces a beat frequency: f_beat = chirp_rate * delay
|
|
After DDC, the signal is at baseband with this beat frequency.
|
|
|
|
Args:
|
|
targets: list of Target objects
|
|
n_samples_baseband: number of baseband samples (at 100 MHz)
|
|
noise_stddev: noise in Q15 LSBs
|
|
seed: random seed
|
|
|
|
Returns:
|
|
(bb_i, bb_q): lists of signed 16-bit integers (Q15)
|
|
"""
|
|
rng_state = seed
|
|
def next_rand():
|
|
nonlocal rng_state
|
|
rng_state = (rng_state * 1103515245 + 12345) & 0x7FFFFFFF
|
|
return rng_state
|
|
|
|
def rand_gaussian():
|
|
while True:
|
|
u1 = (next_rand() / 0x7FFFFFFF)
|
|
u2 = (next_rand() / 0x7FFFFFFF)
|
|
if u1 > 1e-10:
|
|
break
|
|
return math.sqrt(-2.0 * math.log(u1)) * math.cos(2.0 * math.pi * u2)
|
|
|
|
chirp_rate = CHIRP_BW / T_LONG_CHIRP
|
|
bb_i_float = [0.0] * n_samples_baseband
|
|
bb_q_float = [0.0] * n_samples_baseband
|
|
|
|
for target in targets:
|
|
f_beat = chirp_rate * target.delay_s # Beat frequency
|
|
amp = target.amplitude / 4.0 # Scale down for baseband (DDC gain ~ 1/4)
|
|
doppler_hz = target.doppler_hz
|
|
phase0 = target.phase_deg * math.pi / 180.0
|
|
|
|
for n in range(n_samples_baseband):
|
|
t = n / FS_SYS
|
|
phase = 2 * math.pi * (f_beat + doppler_hz) * t + phase0
|
|
bb_i_float[n] += amp * math.cos(phase)
|
|
bb_q_float[n] += amp * math.sin(phase)
|
|
|
|
# Add noise and quantize to Q15
|
|
bb_i = []
|
|
bb_q = []
|
|
for n in range(n_samples_baseband):
|
|
i_val = int(round(bb_i_float[n] + noise_stddev * rand_gaussian()))
|
|
q_val = int(round(bb_q_float[n] + noise_stddev * rand_gaussian()))
|
|
bb_i.append(max(-32768, min(32767, i_val)))
|
|
bb_q.append(max(-32768, min(32767, q_val)))
|
|
|
|
return bb_i, bb_q
|
|
|
|
|
|
# =============================================================================
|
|
# Multi-chirp frame generation (for Doppler processing)
|
|
# =============================================================================
|
|
|
|
def generate_doppler_frame(targets, n_chirps=CHIRPS_PER_FRAME,
|
|
n_range_bins=RANGE_BINS, noise_stddev=0.5, seed=42):
|
|
"""
|
|
Generate a complete Doppler frame (32 chirps x 64 range bins).
|
|
|
|
Each chirp sees a phase rotation due to target velocity:
|
|
phase_shift_per_chirp = 2*pi * doppler_hz * T_chirp_repeat
|
|
|
|
Args:
|
|
targets: list of Target objects
|
|
n_chirps: chirps per frame (32)
|
|
n_range_bins: range bins per chirp (64)
|
|
|
|
Returns:
|
|
(frame_i, frame_q): [n_chirps][n_range_bins] arrays of signed 16-bit
|
|
"""
|
|
rng_state = seed
|
|
def next_rand():
|
|
nonlocal rng_state
|
|
rng_state = (rng_state * 1103515245 + 12345) & 0x7FFFFFFF
|
|
return rng_state
|
|
|
|
def rand_gaussian():
|
|
while True:
|
|
u1 = (next_rand() / 0x7FFFFFFF)
|
|
u2 = (next_rand() / 0x7FFFFFFF)
|
|
if u1 > 1e-10:
|
|
break
|
|
return math.sqrt(-2.0 * math.log(u1)) * math.cos(2.0 * math.pi * u2)
|
|
|
|
frame_i = []
|
|
frame_q = []
|
|
|
|
for chirp_idx in range(n_chirps):
|
|
chirp_i = [0.0] * n_range_bins
|
|
chirp_q = [0.0] * n_range_bins
|
|
|
|
for target in targets:
|
|
# Which range bin does this target fall in?
|
|
# After matched filter + range decimation:
|
|
# range_bin = target_delay_in_baseband_samples / decimation_factor
|
|
delay_baseband_samples = target.delay_s * FS_SYS
|
|
range_bin_float = delay_baseband_samples * n_range_bins / FFT_SIZE
|
|
range_bin = int(round(range_bin_float))
|
|
|
|
if range_bin < 0 or range_bin >= n_range_bins:
|
|
continue
|
|
|
|
# Amplitude (simplified)
|
|
amp = target.amplitude / 4.0
|
|
|
|
# Doppler phase for this chirp.
|
|
# The frame uses staggered PRF: chirps 0-15 use the long PRI,
|
|
# chirps 16-31 use the short PRI.
|
|
if chirp_idx < CHIRPS_PER_SUBFRAME:
|
|
slow_time_s = chirp_idx * T_PRI_LONG
|
|
else:
|
|
slow_time_s = (CHIRPS_PER_SUBFRAME * T_PRI_LONG) + \
|
|
((chirp_idx - CHIRPS_PER_SUBFRAME) * T_PRI_SHORT)
|
|
|
|
doppler_phase = 2 * math.pi * target.doppler_hz * slow_time_s
|
|
total_phase = doppler_phase + target.phase_deg * math.pi / 180.0
|
|
|
|
# Spread across a few bins (sinc-like response from matched filter)
|
|
for delta in range(-2, 3):
|
|
rb = range_bin + delta
|
|
if 0 <= rb < n_range_bins:
|
|
# sinc-like weighting
|
|
if delta == 0:
|
|
weight = 1.0
|
|
else:
|
|
weight = 0.2 / abs(delta)
|
|
chirp_i[rb] += amp * weight * math.cos(total_phase)
|
|
chirp_q[rb] += amp * weight * math.sin(total_phase)
|
|
|
|
# Add noise and quantize
|
|
row_i = []
|
|
row_q = []
|
|
for rb in range(n_range_bins):
|
|
i_val = int(round(chirp_i[rb] + noise_stddev * rand_gaussian()))
|
|
q_val = int(round(chirp_q[rb] + noise_stddev * rand_gaussian()))
|
|
row_i.append(max(-32768, min(32767, i_val)))
|
|
row_q.append(max(-32768, min(32767, q_val)))
|
|
|
|
frame_i.append(row_i)
|
|
frame_q.append(row_q)
|
|
|
|
return frame_i, frame_q
|
|
|
|
|
|
# =============================================================================
|
|
# Output file generators
|
|
# =============================================================================
|
|
|
|
def write_hex_file(filepath, samples, bits=8):
|
|
"""
|
|
Write samples to hex file for Verilog $readmemh.
|
|
|
|
Args:
|
|
filepath: output file path
|
|
samples: list of integer samples
|
|
bits: bit width per sample (8 for ADC, 16 for baseband)
|
|
"""
|
|
hex_digits = (bits + 3) // 4
|
|
fmt = f"{{:0{hex_digits}X}}"
|
|
|
|
with open(filepath, 'w') as f:
|
|
f.write(f"// {len(samples)} samples, {bits}-bit, hex format for $readmemh\n")
|
|
for i, s in enumerate(samples):
|
|
if bits <= 8:
|
|
val = s & 0xFF
|
|
elif bits <= 16:
|
|
val = s & 0xFFFF
|
|
elif bits <= 32:
|
|
val = s & 0xFFFFFFFF
|
|
else:
|
|
val = s & ((1 << bits) - 1)
|
|
f.write(fmt.format(val) + "\n")
|
|
|
|
print(f" Wrote {len(samples)} samples to {filepath}")
|
|
|
|
|
|
def write_csv_file(filepath, columns, headers=None):
|
|
"""
|
|
Write multi-column data to CSV.
|
|
|
|
Args:
|
|
filepath: output file path
|
|
columns: list of lists (each list is a column)
|
|
headers: list of column header strings
|
|
"""
|
|
n_rows = len(columns[0])
|
|
with open(filepath, 'w') as f:
|
|
if headers:
|
|
f.write(",".join(headers) + "\n")
|
|
for i in range(n_rows):
|
|
row = [str(col[i]) for col in columns]
|
|
f.write(",".join(row) + "\n")
|
|
|
|
print(f" Wrote {n_rows} rows to {filepath}")
|
|
|
|
|
|
# =============================================================================
|
|
# Pre-built test scenarios
|
|
# =============================================================================
|
|
|
|
def scenario_single_target(range_m=500, velocity=0, rcs=0, n_adc_samples=16384):
|
|
"""
|
|
Single stationary target at specified range.
|
|
Good for validating matched filter range response.
|
|
"""
|
|
target = Target(range_m=range_m, velocity_mps=velocity, rcs_dbsm=rcs)
|
|
print(f"Scenario: Single target at {range_m}m")
|
|
print(f" {target}")
|
|
print(f" Beat freq: {CHIRP_BW / T_LONG_CHIRP * target.delay_s:.0f} Hz")
|
|
print(f" Delay: {target.delay_samples:.1f} ADC samples")
|
|
|
|
adc = generate_adc_samples([target], n_adc_samples, noise_stddev=2.0)
|
|
return adc, [target]
|
|
|
|
|
|
def scenario_two_targets(n_adc_samples=16384):
|
|
"""
|
|
Two targets at different ranges — tests range resolution.
|
|
Separation: ~2x range resolution (15m).
|
|
"""
|
|
targets = [
|
|
Target(range_m=300, velocity_mps=0, rcs_dbsm=10, phase_deg=0),
|
|
Target(range_m=315, velocity_mps=0, rcs_dbsm=10, phase_deg=45),
|
|
]
|
|
print("Scenario: Two targets (range resolution test)")
|
|
for t in targets:
|
|
print(f" {t}")
|
|
|
|
adc = generate_adc_samples(targets, n_adc_samples, noise_stddev=2.0)
|
|
return adc, targets
|
|
|
|
|
|
def scenario_multi_target(n_adc_samples=16384):
|
|
"""
|
|
Five targets at various ranges and velocities — comprehensive test.
|
|
"""
|
|
targets = [
|
|
Target(range_m=100, velocity_mps=0, rcs_dbsm=20, phase_deg=0),
|
|
Target(range_m=500, velocity_mps=30, rcs_dbsm=10, phase_deg=90),
|
|
Target(range_m=1000, velocity_mps=-15, rcs_dbsm=5, phase_deg=180),
|
|
Target(range_m=2000, velocity_mps=50, rcs_dbsm=0, phase_deg=45),
|
|
Target(range_m=5000, velocity_mps=-5, rcs_dbsm=-5, phase_deg=270),
|
|
]
|
|
print("Scenario: Multi-target (5 targets)")
|
|
for t in targets:
|
|
print(f" {t}")
|
|
|
|
adc = generate_adc_samples(targets, n_adc_samples, noise_stddev=3.0)
|
|
return adc, targets
|
|
|
|
|
|
def scenario_noise_only(n_adc_samples=16384, noise_stddev=5.0):
|
|
"""
|
|
Noise-only scene — baseline for false alarm characterization.
|
|
"""
|
|
print(f"Scenario: Noise only (stddev={noise_stddev})")
|
|
adc = generate_adc_samples([], n_adc_samples, noise_stddev=noise_stddev)
|
|
return adc, []
|
|
|
|
|
|
def scenario_dc_tone(n_adc_samples=16384, adc_value=128):
|
|
"""
|
|
DC input — validates CIC decimation and DC response.
|
|
"""
|
|
print(f"Scenario: DC tone (ADC value={adc_value})")
|
|
return [adc_value] * n_adc_samples, []
|
|
|
|
|
|
def scenario_sine_wave(n_adc_samples=16384, freq_hz=1e6, amplitude=50):
|
|
"""
|
|
Pure sine wave at ADC input — validates NCO/mixer frequency response.
|
|
"""
|
|
print(f"Scenario: Sine wave at {freq_hz/1e6:.1f} MHz, amplitude={amplitude}")
|
|
adc = []
|
|
for n in range(n_adc_samples):
|
|
t = n / FS_ADC
|
|
val = int(round(128 + amplitude * math.sin(2 * math.pi * freq_hz * t)))
|
|
adc.append(max(0, min(255, val)))
|
|
return adc, []
|
|
|
|
|
|
# =============================================================================
|
|
# Main: Generate all test vectors
|
|
# =============================================================================
|
|
|
|
def generate_all_test_vectors(output_dir=None):
|
|
"""
|
|
Generate a complete set of test vectors for co-simulation.
|
|
|
|
Creates:
|
|
- adc_single_target.hex: ADC samples for single target
|
|
- adc_multi_target.hex: ADC samples for 5 targets
|
|
- adc_noise_only.hex: Noise-only ADC samples
|
|
- adc_dc.hex: DC input
|
|
- adc_sine_1mhz.hex: 1 MHz sine wave
|
|
- ref_chirp_i.hex / ref_chirp_q.hex: Reference chirp for matched filter
|
|
- bb_single_target_i.hex / _q.hex: Baseband I/Q for matched filter test
|
|
- scenario_info.csv: Target parameters for each scenario
|
|
"""
|
|
if output_dir is None:
|
|
output_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
print("=" * 60)
|
|
print("Generating AERIS-10 Test Vectors")
|
|
print(f"Output directory: {output_dir}")
|
|
print("=" * 60)
|
|
|
|
n_adc = 16384 # ~41 us of ADC data
|
|
|
|
# --- Scenario 1: Single target ---
|
|
print("\n--- Scenario 1: Single Target ---")
|
|
adc1, targets1 = scenario_single_target(range_m=500, n_adc_samples=n_adc)
|
|
write_hex_file(os.path.join(output_dir, "adc_single_target.hex"), adc1, bits=8)
|
|
|
|
# --- Scenario 2: Multi-target ---
|
|
print("\n--- Scenario 2: Multi-Target ---")
|
|
adc2, targets2 = scenario_multi_target(n_adc_samples=n_adc)
|
|
write_hex_file(os.path.join(output_dir, "adc_multi_target.hex"), adc2, bits=8)
|
|
|
|
# --- Scenario 3: Noise only ---
|
|
print("\n--- Scenario 3: Noise Only ---")
|
|
adc3, _ = scenario_noise_only(n_adc_samples=n_adc)
|
|
write_hex_file(os.path.join(output_dir, "adc_noise_only.hex"), adc3, bits=8)
|
|
|
|
# --- Scenario 4: DC ---
|
|
print("\n--- Scenario 4: DC Input ---")
|
|
adc4, _ = scenario_dc_tone(n_adc_samples=n_adc)
|
|
write_hex_file(os.path.join(output_dir, "adc_dc.hex"), adc4, bits=8)
|
|
|
|
# --- Scenario 5: Sine wave ---
|
|
print("\n--- Scenario 5: 1 MHz Sine ---")
|
|
adc5, _ = scenario_sine_wave(n_adc_samples=n_adc, freq_hz=1e6, amplitude=50)
|
|
write_hex_file(os.path.join(output_dir, "adc_sine_1mhz.hex"), adc5, bits=8)
|
|
|
|
# --- Reference chirp for matched filter ---
|
|
print("\n--- Reference Chirp ---")
|
|
ref_re, ref_im = generate_reference_chirp_q15()
|
|
write_hex_file(os.path.join(output_dir, "ref_chirp_i.hex"), ref_re, bits=16)
|
|
write_hex_file(os.path.join(output_dir, "ref_chirp_q.hex"), ref_im, bits=16)
|
|
|
|
# --- Baseband samples for matched filter test (bypass DDC) ---
|
|
print("\n--- Baseband Samples (bypass DDC) ---")
|
|
bb_targets = [
|
|
Target(range_m=500, velocity_mps=0, rcs_dbsm=10),
|
|
Target(range_m=1500, velocity_mps=20, rcs_dbsm=5),
|
|
]
|
|
bb_i, bb_q = generate_baseband_samples(bb_targets, FFT_SIZE, noise_stddev=1.0)
|
|
write_hex_file(os.path.join(output_dir, "bb_mf_test_i.hex"), bb_i, bits=16)
|
|
write_hex_file(os.path.join(output_dir, "bb_mf_test_q.hex"), bb_q, bits=16)
|
|
|
|
# --- Scenario info CSV ---
|
|
print("\n--- Scenario Info ---")
|
|
with open(os.path.join(output_dir, "scenario_info.txt"), 'w') as f:
|
|
f.write("AERIS-10 Test Vector Scenarios\n")
|
|
f.write("=" * 60 + "\n\n")
|
|
|
|
f.write("System Parameters:\n")
|
|
f.write(f" Carrier: {F_CARRIER/1e9:.1f} GHz\n")
|
|
f.write(f" IF: {F_IF/1e6:.0f} MHz\n")
|
|
f.write(f" Chirp BW: {CHIRP_BW/1e6:.0f} MHz\n")
|
|
f.write(f" ADC: {FS_ADC/1e6:.0f} MSPS, {ADC_BITS}-bit\n")
|
|
f.write(f" Range resolution: {RANGE_RESOLUTION:.1f} m\n")
|
|
f.write(f" Wavelength: {WAVELENGTH*1000:.2f} mm\n")
|
|
f.write(f"\n")
|
|
|
|
f.write("Scenario 1: Single target\n")
|
|
for t in targets1:
|
|
f.write(f" {t}\n")
|
|
|
|
f.write("\nScenario 2: Multi-target (5 targets)\n")
|
|
for t in targets2:
|
|
f.write(f" {t}\n")
|
|
|
|
f.write("\nScenario 3: Noise only (stddev=5.0 LSB)\n")
|
|
f.write("\nScenario 4: DC input (value=128)\n")
|
|
f.write("\nScenario 5: 1 MHz sine wave (amplitude=50 LSB)\n")
|
|
|
|
f.write("\nBaseband MF test targets:\n")
|
|
for t in bb_targets:
|
|
f.write(f" {t}\n")
|
|
|
|
print(f"\n Wrote scenario info to {os.path.join(output_dir, 'scenario_info.txt')}")
|
|
|
|
print("\n" + "=" * 60)
|
|
print("ALL TEST VECTORS GENERATED")
|
|
print("=" * 60)
|
|
|
|
return {
|
|
'adc_single': adc1,
|
|
'adc_multi': adc2,
|
|
'adc_noise': adc3,
|
|
'adc_dc': adc4,
|
|
'adc_sine': adc5,
|
|
'ref_chirp_re': ref_re,
|
|
'ref_chirp_im': ref_im,
|
|
'bb_i': bb_i,
|
|
'bb_q': bb_q,
|
|
}
|
|
|
|
|
|
if __name__ == '__main__':
|
|
generate_all_test_vectors()
|