FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
fmcwValidation.test.ts
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
3
4import { describe, expect, test } from 'bun:test';
5import { createWaveformForType, defaultGlobalParameters } from './defaults';
6import { validateFmcwScenario, validateFmcwWaveform } from './fmcwValidation';
7import type { ScenarioData, Waveform } from './types';
8
9const makeLinearWaveform = (
10 overrides: Partial<Extract<Waveform, { waveformType: 'fmcw_linear_chirp' }>>
11): Extract<Waveform, { waveformType: 'fmcw_linear_chirp' }> => ({
12 ...createWaveformForType('fmcw_linear_chirp'),
13 id: 'wave-1',
14 name: 'Linear',
15 ...overrides,
16});
17
18const makeTriangleWaveform = (
19 overrides: Partial<Extract<Waveform, { waveformType: 'fmcw_triangle' }>>
20): Extract<Waveform, { waveformType: 'fmcw_triangle' }> => ({
21 ...createWaveformForType('fmcw_triangle'),
22 id: 'wave-1',
23 name: 'Triangle',
24 ...overrides,
25});
26
27const makeScenario = (waveform: Waveform): ScenarioData => ({
28 globalParameters: defaultGlobalParameters,
29 waveforms: [waveform],
30 timings: [],
31 antennas: [],
32 platforms: [
33 {
34 id: 'platform-1',
35 type: 'Platform',
36 name: 'Platform',
37 motionPath: {
38 interpolation: 'static',
39 waypoints: [
40 {
41 id: 'wp-1',
42 x: 0,
43 y: 0,
44 altitude: 0,
45 time: 0,
46 },
47 ],
48 },
49 rotation: {
50 type: 'fixed',
51 startAzimuth: 0,
52 startElevation: 0,
53 azimuthRate: 0,
54 elevationRate: 0,
55 },
56 components: [
57 {
58 id: 'tx-1',
59 type: 'transmitter',
60 name: 'FMCW Tx',
61 radarType: 'fmcw',
62 prf: null,
63 antennaId: null,
64 waveformId: waveform.id,
65 timingId: null,
66 schedule: [],
67 },
68 ],
69 },
70 ],
71});
72
73describe('FMCW validation', () => {
74 test('matches backend baseband and RF sweep checks', () => {
75 const aliasIssues = validateFmcwWaveform(
76 makeLinearWaveform({ chirp_bandwidth: 20e3 }),
77 defaultGlobalParameters
78 );
79 expect(aliasIssues.some((issue) => issue.severity === 'error')).toBe(
80 true
81 );
82
83 const rfIssues = validateFmcwWaveform(
84 makeLinearWaveform({
85 carrier_frequency: 100,
86 start_frequency_offset: -200,
87 }),
88 defaultGlobalParameters
89 );
90 expect(
91 rfIssues.some((issue) => issue.message.includes('lower sweep edge'))
92 ).toBe(true);
93 });
94
95 test('reports linear schedule errors and warnings', () => {
96 const scenario = makeScenario(
97 makeLinearWaveform({
98 chirp_duration: 1e-3,
99 chirp_period: 2e-3,
100 })
101 );
102 const component = scenario.platforms[0].components[0];
103 if (component.type !== 'transmitter') {
104 throw new Error('Expected transmitter fixture');
105 }
106 component.schedule = [
107 { start: 0, end: 0.5e-3 },
108 { start: 1, end: 1.0015 },
109 ];
110
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);
114 });
115
116 test('reports triangle period errors and silent-tail warnings', () => {
117 const scenario = makeScenario(
118 makeTriangleWaveform({ chirp_duration: 1e-3 })
119 );
120 const component = scenario.platforms[0].components[0];
121 if (component.type !== 'transmitter') {
122 throw new Error('Expected transmitter fixture');
123 }
124 component.schedule = [
125 { start: 0, end: 1e-3 },
126 { start: 1, end: 1.0025 },
127 ];
128
129 const issues = validateFmcwScenario(scenario);
130 expect(
131 issues.some((issue) => issue.message.includes('triangle period'))
132 ).toBe(true);
133 expect(issues.some((issue) => issue.message.includes('silent'))).toBe(
134 true
135 );
136 });
137
138 test('reports receiver dechirp mode/reference mismatches', () => {
139 const scenario = makeScenario(makeLinearWaveform({}));
140 scenario.platforms[0].components.push({
141 id: 'rx-1',
142 type: 'receiver',
143 name: 'FMCW Rx',
144 radarType: 'fmcw',
145 window_skip: null,
146 window_length: null,
147 prf: null,
148 antennaId: null,
149 timingId: null,
150 noiseTemperature: null,
151 noDirectPaths: false,
152 noPropagationLoss: false,
153 fmcwModeConfig: {
154 dechirp_mode: 'none',
155 dechirp_reference: { source: 'attached' },
156 },
157 schedule: [],
158 });
159
160 expect(
161 validateFmcwScenario(scenario).some((issue) =>
162 issue.message.includes('while dechirp mode is none')
163 )
164 ).toBe(true);
165
166 const receiver = scenario.platforms[0].components[1];
167 if (receiver.type !== 'receiver') {
168 throw new Error('Expected receiver fixture');
169 }
170 receiver.fmcwModeConfig = { dechirp_mode: 'physical' };
171 expect(
172 validateFmcwScenario(scenario).some((issue) =>
173 issue.message.includes('does not declare a dechirp reference')
174 )
175 ).toBe(true);
176
177 receiver.fmcwModeConfig = {
178 dechirp_mode: 'ideal',
179 dechirp_reference: { source: 'attached' },
180 };
181 expect(
182 validateFmcwScenario(scenario).some((issue) =>
183 issue.message.includes('only monostatic receivers')
184 )
185 ).toBe(true);
186 });
187
188 test('validates receiver IF-chain settings', () => {
189 const scenario = makeScenario(makeLinearWaveform({}));
190 scenario.platforms[0].components.push({
191 id: 'rx-1',
192 type: 'receiver',
193 name: 'FMCW Rx',
194 radarType: 'fmcw',
195 window_skip: null,
196 window_length: null,
197 prf: null,
198 antennaId: null,
199 timingId: null,
200 noiseTemperature: null,
201 noDirectPaths: false,
202 noPropagationLoss: false,
203 fmcwModeConfig: {
204 dechirp_mode: 'physical',
205 dechirp_reference: {
206 source: 'transmitter',
207 transmitter_name: 'FMCW Tx',
208 },
209 if_sample_rate: 4e3,
210 if_filter_bandwidth: 1e3,
211 if_filter_transition_width: 500,
212 },
213 schedule: [],
214 });
215
216 expect(
217 validateFmcwScenario(scenario).some(
218 (issue) => issue.field === 'fmcwModeConfig'
219 )
220 ).toBe(false);
221
222 const receiver = scenario.platforms[0].components[1];
223 if (receiver.type !== 'receiver') {
224 throw new Error('Expected receiver fixture');
225 }
226
227 receiver.fmcwModeConfig = {
228 dechirp_mode: 'none',
229 if_sample_rate: 4e3,
230 };
231 expect(
232 validateFmcwScenario(scenario).some((issue) =>
233 issue.message.includes('IF-chain settings')
234 )
235 ).toBe(true);
236
237 receiver.fmcwModeConfig = {
238 dechirp_mode: 'physical',
239 dechirp_reference: {
240 source: 'transmitter',
241 transmitter_name: 'FMCW Tx',
242 },
243 if_sample_rate: -1,
244 };
245 expect(
246 validateFmcwScenario(scenario).some((issue) =>
247 issue.message.includes('IF sample rate')
248 )
249 ).toBe(true);
250
251 receiver.fmcwModeConfig = {
252 dechirp_mode: 'physical',
253 dechirp_reference: {
254 source: 'transmitter',
255 transmitter_name: 'FMCW Tx',
256 },
257 if_sample_rate: 4e3,
258 if_filter_bandwidth: 2e3,
259 };
260 expect(
261 validateFmcwScenario(scenario).some((issue) =>
262 issue.message.includes('less than half')
263 )
264 ).toBe(true);
265
266 receiver.fmcwModeConfig = {
267 dechirp_mode: 'physical',
268 dechirp_reference: {
269 source: 'transmitter',
270 transmitter_name: 'FMCW Tx',
271 },
272 if_sample_rate:
273 defaultGlobalParameters.rate *
274 defaultGlobalParameters.oversample_ratio +
275 1,
276 };
277 expect(
278 validateFmcwScenario(scenario).some((issue) =>
279 issue.message.includes('effective simulation sample rate')
280 )
281 ).toBe(true);
282 });
283
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({
288 id: 'wave-2',
289 type: 'Waveform',
290 name: 'CW Tone',
291 waveformType: 'cw',
292 power: 1,
293 carrier_frequency: 1e9,
294 });
295 scenario.platforms[0].components.push({
296 id: 'rx-1',
297 type: 'receiver',
298 name: 'FMCW Rx',
299 radarType: 'fmcw',
300 window_skip: null,
301 window_length: null,
302 prf: null,
303 antennaId: null,
304 timingId: null,
305 noiseTemperature: null,
306 noDirectPaths: false,
307 noPropagationLoss: false,
308 fmcwModeConfig: {
309 dechirp_mode: 'physical',
310 dechirp_reference: {
311 source: 'transmitter',
312 transmitter_name: 'FMCW Tx',
313 },
314 },
315 schedule: [],
316 });
317
318 expect(
319 validateFmcwScenario(scenario).some(
320 (issue) => issue.field === 'fmcwModeConfig'
321 )
322 ).toBe(false);
323
324 const receiver = scenario.platforms[0].components[1];
325 if (receiver.type !== 'receiver') {
326 throw new Error('Expected receiver fixture');
327 }
328
329 receiver.fmcwModeConfig = {
330 dechirp_mode: 'physical',
331 dechirp_reference: {
332 source: 'transmitter',
333 transmitter_name: 'Missing TX',
334 },
335 };
336 expect(
337 validateFmcwScenario(scenario).some((issue) =>
338 issue.message.includes('must be an FMCW transmitter')
339 )
340 ).toBe(true);
341
342 receiver.fmcwModeConfig = {
343 dechirp_mode: 'ideal',
344 dechirp_reference: {
345 source: 'custom',
346 waveform_name: 'FMCW LO',
347 },
348 };
349 expect(
350 validateFmcwScenario(scenario).some(
351 (issue) => issue.field === 'fmcwModeConfig'
352 )
353 ).toBe(false);
354
355 receiver.fmcwModeConfig = {
356 dechirp_mode: 'ideal',
357 dechirp_reference: {
358 source: 'custom',
359 waveform_name: 'CW Tone',
360 },
361 };
362 expect(
363 validateFmcwScenario(scenario).some((issue) =>
364 issue.message.includes('must be a top-level FMCW waveform')
365 )
366 ).toBe(true);
367 });
368});