mirror of
https://github.com/NawfalMotii79/PLFM_RADAR.git
synced 2026-06-10 23:41:18 +00:00
chore(repo): cosim_dir replay revival + ruff lint cleanup
cosim_dir revival:
- gen_realdata_hex.py: also emit decimated_range_{i,q}.npy (48x512)
and doppler_map_{i,q}.npy (512x48) at production dimensions; the
same Python pipeline that produces the RTL .hex stimuli now writes
the .npy intermediates v7.replay COSIM_DIR loads. Replaces the
workflow lost when golden_reference.py was deleted in e8b495c
- test_v7.py: update test_get_frame_cosim shape from pre-PR-O.6
(64,32) to (NUM_RANGE_BINS, NUM_DOPPLER_BINS)
- check in 4 .npy reference files (~400 KB, deterministic SCENE_SEED=42)
Ruff lint cleanup (was 66 errors; now 0):
- pyproject.toml: ignore T20 in tb/cosim/**.py (CLI tools)
- compare_independent.py: drop redundant int() casts (RUF046),
swap try/except scipy import for importlib.util.find_spec,
remove dead duplicate np import, ASCII-ize comment unicode,
wrap E501 format strings
- fpga_reference.py: drop unused fs arg from nco_reference,
collapse if/else to ternary, mark _out_im unused
- v7/processing.py: ASCII-ize x in docstring, collapse if-branches
- {dashboard,software_fpga,workers,radar_protocol}.py: wrap E501
- test_v7.py: ASCII-ize comment unicode, _alias renames where unused
Result: test_v7 100/100 (0 skips on radar_venv, was 9 graceful
skips); 5 cosim_dir orphan tests now active and passing.
This commit is contained in:
@@ -38,14 +38,14 @@ from pathlib import Path
|
||||
# Required: numpy + scipy. If either is missing, exit code 2 with a [SKIP]
|
||||
# marker so the regression can distinguish missing-deps from real failures
|
||||
# (see run_regression.sh "Independent Reference Drift (T-6)" block).
|
||||
import importlib.util
|
||||
|
||||
_MISSING = []
|
||||
try:
|
||||
import numpy as np # noqa: F401
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
_MISSING.append("numpy")
|
||||
try:
|
||||
import scipy.signal.windows # noqa: F401
|
||||
except ImportError:
|
||||
if importlib.util.find_spec("scipy.signal") is None:
|
||||
_MISSING.append("scipy")
|
||||
if _MISSING:
|
||||
print(
|
||||
@@ -56,8 +56,6 @@ if _MISSING:
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
import numpy as np # re-import to get module binding now that we know it's there
|
||||
|
||||
# Make local imports work when invoked from anywhere
|
||||
THIS_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(THIS_DIR))
|
||||
@@ -80,9 +78,9 @@ from fpga_model import ( # noqa: E402
|
||||
|
||||
TOL_NCO_LUT_LSB = 1 # NCO_SINE_LUT: tightest possible
|
||||
TOL_TWIDDLE_LSB = 1 # twiddle ROMs: same — quarter-wave Q15 cosine
|
||||
TOL_WINDOW_LSB = 4 # 4 LSB ≈ 1.2e-4 rounding budget on Q15 round
|
||||
TOL_WINDOW_LSB = 4 # 4 LSB ~= 1.2e-4 rounding budget on Q15 round
|
||||
TOL_NCO_MAG_REL = 0.04 # quarter-wave LUT artifact at quadrant edges
|
||||
TOL_FFT_ROUNDTRIP_LSB = 60 # 11 stages × Q15 noise on 2048-pt; empirical
|
||||
TOL_FFT_ROUNDTRIP_LSB = 60 # 11 stages * Q15 noise on 2048-pt; empirical
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -169,7 +167,10 @@ def check_twiddle_rom(result: CheckResult, n: int, mem_filename: str):
|
||||
bad.append((k, cos_rom[k], ideal, dev))
|
||||
result.check(
|
||||
max_dev <= TOL_TWIDDLE_LSB,
|
||||
f"{mem_filename}: all {expected_entries} entries match cos(2pi*k/{n}) Q15 (tol {TOL_TWIDDLE_LSB} LSB)",
|
||||
(
|
||||
f"{mem_filename}: all {expected_entries} entries match "
|
||||
f"cos(2pi*k/{n}) Q15 (tol {TOL_TWIDDLE_LSB} LSB)"
|
||||
),
|
||||
f"max |ROM - ideal| = {max_dev} LSB" + (
|
||||
f"; {len(bad)} bad, e.g. k={bad[0][0]}: ROM={bad[0][1]}, ideal={bad[0][2]}"
|
||||
if bad else ""
|
||||
@@ -186,7 +187,10 @@ def check_doppler_window_lut(result: CheckResult):
|
||||
worst_idx = int(np.argmax(diff))
|
||||
result.check(
|
||||
max_dev <= TOL_WINDOW_LSB,
|
||||
f"DOPPLER_WINDOW_COEFF: all 16 entries match Dolph-Chebyshev 60 dB Q15 (tol {TOL_WINDOW_LSB} LSB)",
|
||||
(
|
||||
f"DOPPLER_WINDOW_COEFF: all 16 entries match "
|
||||
f"Dolph-Chebyshev 60 dB Q15 (tol {TOL_WINDOW_LSB} LSB)"
|
||||
),
|
||||
f"max |LUT - ideal| = {max_dev} LSB at n={worst_idx} "
|
||||
f"(LUT={int(win_lut[worst_idx])}, ideal={int(win_ref[worst_idx])})"
|
||||
)
|
||||
@@ -233,7 +237,7 @@ def check_nco_invariants(result: CheckResult):
|
||||
z = cos_arr + 1j * sin_arr
|
||||
Z = np.fft.fft(z)
|
||||
peak_bin = int(np.argmax(np.abs(Z)))
|
||||
expected_bin = int(round(ftw / (1 << 32) * n_capture))
|
||||
expected_bin = round(ftw / (1 << 32) * n_capture)
|
||||
result.check(
|
||||
abs(peak_bin - expected_bin) <= 1,
|
||||
f"NCO dominant frequency at FTW = {ftw:08X} (expected bin {expected_bin})",
|
||||
@@ -264,8 +268,8 @@ def check_fft_invariants(result: CheckResult):
|
||||
# peak = amp*N stays below Q15 saturation (32767).
|
||||
bin_k = 137
|
||||
amp = 15
|
||||
in_re = [int(round(amp * math.cos(2 * math.pi * bin_k * i / n))) for i in range(n)]
|
||||
in_im = [int(round(amp * math.sin(2 * math.pi * bin_k * i / n))) for i in range(n)]
|
||||
in_re = [round(amp * math.cos(2 * math.pi * bin_k * i / n)) for i in range(n)]
|
||||
in_im = [round(amp * math.sin(2 * math.pi * bin_k * i / n)) for i in range(n)]
|
||||
twin_re, twin_im = fft.compute(in_re, in_im, inverse=False)
|
||||
ref_re, ref_im = ref.fft_reference(in_re, in_im, n=n)
|
||||
twin_mag2 = np.array(twin_re) ** 2 + np.array(twin_im) ** 2
|
||||
@@ -280,7 +284,7 @@ def check_fft_invariants(result: CheckResult):
|
||||
|
||||
# Roundtrip — small amplitude (peak = amp*N/2 ≤ 32767 → amp ≤ 32) so the
|
||||
# forward FFT does not saturate, then IFFT should recover input within
|
||||
# 11×Q15 butterfly noise.
|
||||
# 11*Q15 butterfly noise.
|
||||
rt_amp = 30
|
||||
in_re = [int(rt_amp * math.sin(2 * math.pi * 73 * i / n)) for i in range(n)]
|
||||
in_im = [0] * n
|
||||
@@ -289,7 +293,10 @@ def check_fft_invariants(result: CheckResult):
|
||||
rt_max_err = max(abs(rt_re[i] - in_re[i]) for i in range(n))
|
||||
result.check(
|
||||
rt_max_err <= TOL_FFT_ROUNDTRIP_LSB,
|
||||
f"FFT-2048(roundtrip, amp={rt_amp}): FFT->IFFT recovers input within {TOL_FFT_ROUNDTRIP_LSB} LSB",
|
||||
(
|
||||
f"FFT-2048(roundtrip, amp={rt_amp}): FFT->IFFT recovers input "
|
||||
f"within {TOL_FFT_ROUNDTRIP_LSB} LSB"
|
||||
),
|
||||
f"max |rt - in| = {rt_max_err}"
|
||||
)
|
||||
|
||||
@@ -309,8 +316,8 @@ def check_mf_invariants(result: CheckResult):
|
||||
ref_im_in = [0] * n
|
||||
pulse_len = 256
|
||||
for i in range(pulse_len):
|
||||
ref_re_in[i] = int(round(amp * math.cos(2 * math.pi * bin_k * i / pulse_len)))
|
||||
ref_im_in[i] = int(round(amp * math.sin(2 * math.pi * bin_k * i / pulse_len)))
|
||||
ref_re_in[i] = round(amp * math.cos(2 * math.pi * bin_k * i / pulse_len))
|
||||
ref_im_in[i] = round(amp * math.sin(2 * math.pi * bin_k * i / pulse_len))
|
||||
sig_re[i + delay] = ref_re_in[i]
|
||||
sig_im[i + delay] = ref_im_in[i]
|
||||
|
||||
@@ -328,7 +335,7 @@ def check_mf_invariants(result: CheckResult):
|
||||
f"twin={twin_peak}, ref={ref_peak}"
|
||||
)
|
||||
|
||||
# Sidelobe behaviour: peak should be N×stronger than median.
|
||||
# Sidelobe behaviour: peak should be N*stronger than median.
|
||||
twin_peak_val = float(twin_mag[delay])
|
||||
twin_median = float(np.median(twin_mag))
|
||||
pk_ratio = twin_peak_val / max(twin_median, 1.0)
|
||||
@@ -356,8 +363,8 @@ def check_doppler_invariants(result: CheckResult):
|
||||
for c in range(chirps_per_subframe):
|
||||
chirp_idx = sf * chirps_per_subframe + c
|
||||
phase = 2 * math.pi * dop_bin * c / chirps_per_subframe
|
||||
chirp_i[chirp_idx, target_rbin] = int(round(amp * math.cos(phase)))
|
||||
chirp_q[chirp_idx, target_rbin] = int(round(amp * math.sin(phase)))
|
||||
chirp_i[chirp_idx, target_rbin] = round(amp * math.cos(phase))
|
||||
chirp_q[chirp_idx, target_rbin] = round(amp * math.sin(phase))
|
||||
|
||||
dop = DopplerProcessor(num_subframes=num_subframes,
|
||||
chirps_per_frame=chirps_per_frame)
|
||||
|
||||
@@ -44,7 +44,7 @@ import numpy as np
|
||||
# NCO reference — ideal complex sinusoid
|
||||
# =============================================================================
|
||||
|
||||
def nco_reference(num_samples: int, ftw: int, fs: float = 400e6,
|
||||
def nco_reference(num_samples: int, ftw: int,
|
||||
phase_offset_deg: float = 0.0):
|
||||
"""Ideal floating-point NCO output, scaled to match Q15 fpga_model.
|
||||
|
||||
@@ -101,10 +101,7 @@ def fft_reference(in_re, in_im, n: int = 2048, inverse: bool = False):
|
||||
if len(re) != n or len(im) != n:
|
||||
raise ValueError(f"input length {len(re)} != N={n}")
|
||||
x = re + 1j * im
|
||||
if inverse:
|
||||
y = np.fft.ifft(x)
|
||||
else:
|
||||
y = np.fft.fft(x) / n
|
||||
y = np.fft.ifft(x) if inverse else np.fft.fft(x) / n
|
||||
return y.real.copy(), y.imag.copy()
|
||||
|
||||
|
||||
@@ -221,7 +218,7 @@ def doppler_reference(chirp_data_i, chirp_data_q,
|
||||
def _self_test():
|
||||
"""Quick sanity checks."""
|
||||
# NCO: at FTW = 0x4CCCCCCD, frequency = 0.3 * fs = 120 MHz at 400 MSPS.
|
||||
cos_q15, sin_q15 = nco_reference(8, 0x4CCCCCCD, fs=400e6)
|
||||
cos_q15, sin_q15 = nco_reference(8, 0x4CCCCCCD)
|
||||
# First sample should be cos(0)=1, sin(0)=0 in Q15
|
||||
assert abs(cos_q15[0] - 32767.0) < 1.0, f"NCO[0].cos = {cos_q15[0]}"
|
||||
assert abs(sin_q15[0]) < 1.0, f"NCO[0].sin = {sin_q15[0]}"
|
||||
@@ -229,7 +226,7 @@ def _self_test():
|
||||
# FFT: impulse -> all bins = amplitude/N (scaled-mode schedule)
|
||||
in_re = [1000] + [0] * 15
|
||||
in_im = [0] * 16
|
||||
out_re, out_im = fft_reference(in_re, in_im, n=16)
|
||||
out_re, _out_im = fft_reference(in_re, in_im, n=16)
|
||||
for k in range(16):
|
||||
# AUDIT-C10/C-8: FWD FFT now applies /N (=/16), so each bin = 1000/16
|
||||
assert abs(out_re[k] - 1000.0 / 16.0) < 1e-9, \
|
||||
|
||||
@@ -8,12 +8,18 @@ Replaces the legacy ADI CN0566 hardware captures (32-chirp / 2-subframe /
|
||||
(48-chirp / 3-subframe / 48-bin Doppler) so the regression no longer
|
||||
depends on out-of-tree .npy files.
|
||||
|
||||
Outputs (six files, all under tb/cosim/real_data/hex/):
|
||||
doppler_input_realdata.hex 48 chirps x 512 range bins, packed {Q,I}
|
||||
doppler_ref_i.hex / _q.hex 512 range bins x 48 Doppler bins (signed 16-bit)
|
||||
fullchain_range_input.hex 48 chirps x 2048 range bins, packed {Q,I}
|
||||
fullchain_doppler_ref_i.hex
|
||||
fullchain_doppler_ref_q.hex same shape as doppler_ref_*
|
||||
Outputs (all under tb/cosim/real_data/hex/):
|
||||
|
||||
RTL stimuli + goldens (.hex):
|
||||
doppler_input_realdata.hex 48 chirps x 512 range bins, packed {Q,I}
|
||||
doppler_ref_i.hex / _q.hex 512 range bins x 48 Doppler bins (signed 16-bit)
|
||||
fullchain_range_input.hex 48 chirps x 2048 range bins, packed {Q,I}
|
||||
fullchain_doppler_ref_i.hex
|
||||
fullchain_doppler_ref_q.hex same shape as doppler_ref_*
|
||||
|
||||
GUI replay intermediates (.npy, COSIM_DIR ReplayFormat in v7.replay):
|
||||
decimated_range_i.npy / _q.npy (48, 512) — post range_bin_decimator
|
||||
doppler_map_i.npy / _q.npy (512, 48) — post doppler_processor
|
||||
|
||||
Dimensions match production (radar_params.vh: RP_FFT_SIZE=2048,
|
||||
RP_DECIMATION_FACTOR=4, RP_NUM_RANGE_BINS=512, RP_NUM_DOPPLER_BINS=48).
|
||||
@@ -29,6 +35,8 @@ Usage: python3 gen_realdata_hex.py
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from fpga_model import DopplerProcessor, RangeBinDecimator
|
||||
@@ -106,10 +114,11 @@ def gen_doppler_realdata():
|
||||
seed=SCENE_SEED,
|
||||
)
|
||||
|
||||
stim = []
|
||||
for c in range(CHIRPS_PER_FRAME):
|
||||
for rb in range(DOPPLER_RANGE_BINS):
|
||||
stim.append((frame_i[c][rb], frame_q[c][rb]))
|
||||
stim = [
|
||||
(frame_i[c][rb], frame_q[c][rb])
|
||||
for c in range(CHIRPS_PER_FRAME)
|
||||
for rb in range(DOPPLER_RANGE_BINS)
|
||||
]
|
||||
write_hex_32(os.path.join(OUT_DIR, "doppler_input_realdata.hex"), stim)
|
||||
|
||||
dp = make_doppler_processor()
|
||||
@@ -118,7 +127,8 @@ def gen_doppler_realdata():
|
||||
write_hex_16(os.path.join(OUT_DIR, "doppler_ref_i.hex"), flat_i)
|
||||
write_hex_16(os.path.join(OUT_DIR, "doppler_ref_q.hex"), flat_q)
|
||||
|
||||
print(f" stimulus: {len(stim)} packed lines (expected {CHIRPS_PER_FRAME * DOPPLER_RANGE_BINS})")
|
||||
expected_stim = CHIRPS_PER_FRAME * DOPPLER_RANGE_BINS
|
||||
print(f" stimulus: {len(stim)} packed lines (expected {expected_stim})")
|
||||
print(f" golden: {len(flat_i)} lines i / {len(flat_q)} lines q "
|
||||
f"(expected {DOPPLER_RANGE_BINS * DOPPLER_TOTAL_BINS})")
|
||||
|
||||
@@ -133,10 +143,11 @@ def gen_fullchain_realdata():
|
||||
seed=SCENE_SEED,
|
||||
)
|
||||
|
||||
stim = []
|
||||
for c in range(CHIRPS_PER_FRAME):
|
||||
for rb in range(FULLCHAIN_INPUT_BINS):
|
||||
stim.append((frame_i[c][rb], frame_q[c][rb]))
|
||||
stim = [
|
||||
(frame_i[c][rb], frame_q[c][rb])
|
||||
for c in range(CHIRPS_PER_FRAME)
|
||||
for rb in range(FULLCHAIN_INPUT_BINS)
|
||||
]
|
||||
write_hex_32(os.path.join(OUT_DIR, "fullchain_range_input.hex"), stim)
|
||||
|
||||
# fpga_model.RangeBinDecimator is hard-coded to 2048->512, DECIM=4 — production.
|
||||
@@ -152,6 +163,16 @@ def gen_fullchain_realdata():
|
||||
write_hex_16(os.path.join(OUT_DIR, "fullchain_doppler_ref_i.hex"), flat_i)
|
||||
write_hex_16(os.path.join(OUT_DIR, "fullchain_doppler_ref_q.hex"), flat_q)
|
||||
|
||||
# Same arrays serialized for v7.replay COSIM_DIR format (GUI replay).
|
||||
np.save(os.path.join(OUT_DIR, "decimated_range_i.npy"),
|
||||
np.asarray(decim_i_2d, dtype=np.int32))
|
||||
np.save(os.path.join(OUT_DIR, "decimated_range_q.npy"),
|
||||
np.asarray(decim_q_2d, dtype=np.int32))
|
||||
np.save(os.path.join(OUT_DIR, "doppler_map_i.npy"),
|
||||
np.asarray(doppler_i, dtype=np.int32))
|
||||
np.save(os.path.join(OUT_DIR, "doppler_map_q.npy"),
|
||||
np.asarray(doppler_q, dtype=np.int32))
|
||||
|
||||
print(f" stimulus: {len(stim)} packed lines "
|
||||
f"(expected {CHIRPS_PER_FRAME * FULLCHAIN_INPUT_BINS})")
|
||||
print(f" golden: {len(flat_i)} lines i / {len(flat_q)} lines q "
|
||||
@@ -164,19 +185,31 @@ def main():
|
||||
gen_fullchain_realdata()
|
||||
|
||||
print("\nGenerated files:")
|
||||
for f in (
|
||||
hex_files = (
|
||||
"doppler_input_realdata.hex",
|
||||
"doppler_ref_i.hex",
|
||||
"doppler_ref_q.hex",
|
||||
"fullchain_range_input.hex",
|
||||
"fullchain_doppler_ref_i.hex",
|
||||
"fullchain_doppler_ref_q.hex",
|
||||
):
|
||||
)
|
||||
for f in hex_files:
|
||||
path = os.path.join(OUT_DIR, f)
|
||||
with open(path) as fp:
|
||||
n_lines = sum(1 for _ in fp)
|
||||
print(f" {f:40s} {n_lines:7d} lines ({os.path.getsize(path):7d} bytes)")
|
||||
|
||||
npy_files = (
|
||||
"decimated_range_i.npy",
|
||||
"decimated_range_q.npy",
|
||||
"doppler_map_i.npy",
|
||||
"doppler_map_q.npy",
|
||||
)
|
||||
for f in npy_files:
|
||||
path = os.path.join(OUT_DIR, f)
|
||||
arr = np.load(path)
|
||||
print(f" {f:40s} shape={arr.shape!s:>12s} ({os.path.getsize(path):7d} bytes)")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user