1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
4import { describe, expect, test } from 'bun:test';
5import { createWaveformForType, defaultGlobalParameters } from './defaults';
6import { validateFmcwScenario, validateFmcwWaveform } from './fmcwValidation';
7import type { ScenarioData, Waveform } from './types';
9const makeLinearWaveform = (
10 overrides: Partial<Extract<Waveform, { waveformType: 'fmcw_linear_chirp' }>>
11): Extract<Waveform, { waveformType: 'fmcw_linear_chirp' }> => ({
12 ...createWaveformForType('fmcw_linear_chirp'),
18const makeTriangleWaveform = (
19 overrides: Partial<Extract<Waveform, { waveformType: 'fmcw_triangle' }>>
20): Extract<Waveform, { waveformType: 'fmcw_triangle' }> => ({
21 ...createWaveformForType('fmcw_triangle'),
27const makeScenario = (waveform: Waveform): ScenarioData => ({
28 globalParameters: defaultGlobalParameters,
29 waveforms: [waveform],
38 interpolation: 'static',
64 waveformId: waveform.id,
73describe('FMCW validation', () => {
74 test('matches backend baseband and RF sweep checks', () => {
75 const aliasIssues = validateFmcwWaveform(
76 makeLinearWaveform({ chirp_bandwidth: 20e3 }),
77 defaultGlobalParameters
79 expect(aliasIssues.some((issue) => issue.severity === 'error')).toBe(
83 const rfIssues = validateFmcwWaveform(
85 carrier_frequency: 100,
86 start_frequency_offset: -200,
88 defaultGlobalParameters
91 rfIssues.some((issue) => issue.message.includes('lower sweep edge'))
95 test('reports linear schedule errors and warnings', () => {
96 const scenario = makeScenario(
102 const component = scenario.platforms[0].components[0];
103 if (component.type !== 'transmitter') {
104 throw new Error('Expected transmitter fixture');
106 component.schedule = [
107 { start: 0, end: 0.5e-3 },
108 { start: 1, end: 1.0015 },
111 const issues = validateFmcwScenario(scenario);
112 expect(issues.some((issue) => issue.severity === 'error')).toBe(true);
113 expect(issues.some((issue) => issue.severity === 'warning')).toBe(true);
116 test('reports triangle period errors and silent-tail warnings', () => {
117 const scenario = makeScenario(
118 makeTriangleWaveform({ chirp_duration: 1e-3 })
120 const component = scenario.platforms[0].components[0];
121 if (component.type !== 'transmitter') {
122 throw new Error('Expected transmitter fixture');
124 component.schedule = [
125 { start: 0, end: 1e-3 },
126 { start: 1, end: 1.0025 },
129 const issues = validateFmcwScenario(scenario);
131 issues.some((issue) => issue.message.includes('triangle period'))
133 expect(issues.some((issue) => issue.message.includes('silent'))).toBe(
138 test('reports receiver dechirp mode/reference mismatches', () => {
139 const scenario = makeScenario(makeLinearWaveform({}));
140 scenario.platforms[0].components.push({
150 noiseTemperature: null,
151 noDirectPaths: false,
152 noPropagationLoss: false,
154 dechirp_mode: 'none',
155 dechirp_reference: { source: 'attached' },
161 validateFmcwScenario(scenario).some((issue) =>
162 issue.message.includes('while dechirp mode is none')
166 const receiver = scenario.platforms[0].components[1];
167 if (receiver.type !== 'receiver') {
168 throw new Error('Expected receiver fixture');
170 receiver.fmcwModeConfig = { dechirp_mode: 'physical' };
172 validateFmcwScenario(scenario).some((issue) =>
173 issue.message.includes('does not declare a dechirp reference')
177 receiver.fmcwModeConfig = {
178 dechirp_mode: 'ideal',
179 dechirp_reference: { source: 'attached' },
182 validateFmcwScenario(scenario).some((issue) =>
183 issue.message.includes('only monostatic receivers')
188 test('validates receiver IF-chain settings', () => {
189 const scenario = makeScenario(makeLinearWaveform({}));
190 scenario.platforms[0].components.push({
200 noiseTemperature: null,
201 noDirectPaths: false,
202 noPropagationLoss: false,
204 dechirp_mode: 'physical',
206 source: 'transmitter',
207 transmitter_name: 'FMCW Tx',
210 if_filter_bandwidth: 1e3,
211 if_filter_transition_width: 500,
217 validateFmcwScenario(scenario).some(
218 (issue) => issue.field === 'fmcwModeConfig'
222 const receiver = scenario.platforms[0].components[1];
223 if (receiver.type !== 'receiver') {
224 throw new Error('Expected receiver fixture');
227 receiver.fmcwModeConfig = {
228 dechirp_mode: 'none',
232 validateFmcwScenario(scenario).some((issue) =>
233 issue.message.includes('IF-chain settings')
237 receiver.fmcwModeConfig = {
238 dechirp_mode: 'physical',
240 source: 'transmitter',
241 transmitter_name: 'FMCW Tx',
246 validateFmcwScenario(scenario).some((issue) =>
247 issue.message.includes('IF sample rate')
251 receiver.fmcwModeConfig = {
252 dechirp_mode: 'physical',
254 source: 'transmitter',
255 transmitter_name: 'FMCW Tx',
258 if_filter_bandwidth: 2e3,
261 validateFmcwScenario(scenario).some((issue) =>
262 issue.message.includes('less than half')
266 receiver.fmcwModeConfig = {
267 dechirp_mode: 'physical',
269 source: 'transmitter',
270 transmitter_name: 'FMCW Tx',
273 defaultGlobalParameters.rate *
274 defaultGlobalParameters.oversample_ratio +
278 validateFmcwScenario(scenario).some((issue) =>
279 issue.message.includes('effective simulation sample rate')
284 test('validates named transmitter and custom waveform dechirp references', () => {
285 const waveform = makeLinearWaveform({ id: 'wave-1', name: 'FMCW LO' });
286 const scenario = makeScenario(waveform);
287 scenario.waveforms.push({
293 carrier_frequency: 1e9,
295 scenario.platforms[0].components.push({
305 noiseTemperature: null,
306 noDirectPaths: false,
307 noPropagationLoss: false,
309 dechirp_mode: 'physical',
311 source: 'transmitter',
312 transmitter_name: 'FMCW Tx',
319 validateFmcwScenario(scenario).some(
320 (issue) => issue.field === 'fmcwModeConfig'
324 const receiver = scenario.platforms[0].components[1];
325 if (receiver.type !== 'receiver') {
326 throw new Error('Expected receiver fixture');
329 receiver.fmcwModeConfig = {
330 dechirp_mode: 'physical',
332 source: 'transmitter',
333 transmitter_name: 'Missing TX',
337 validateFmcwScenario(scenario).some((issue) =>
338 issue.message.includes('must be an FMCW transmitter')
342 receiver.fmcwModeConfig = {
343 dechirp_mode: 'ideal',
346 waveform_name: 'FMCW LO',
350 validateFmcwScenario(scenario).some(
351 (issue) => issue.field === 'fmcwModeConfig'
355 receiver.fmcwModeConfig = {
356 dechirp_mode: 'ideal',
359 waveform_name: 'CW Tone',
363 validateFmcwScenario(scenario).some((issue) =>
364 issue.message.includes('must be a top-level FMCW waveform')