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('accepts file-backed FMCW without applying analytic chirp constraints', () => {
75 const waveform: Waveform = {
76 ...createWaveformForType('fmcw_from_file'),
77 id: 'file-fmcw',
78 name: 'File FMCW',
79 filename: 'fmcw.h5',
80 };
81
82 expect(validateFmcwWaveform(waveform, defaultGlobalParameters)).toEqual(
83 []
84 );
85 expect(validateFmcwScenario(makeScenario(waveform))).toEqual([]);
86 });
87
88 test('matches backend baseband and RF sweep checks', () => {
89 const aliasIssues = validateFmcwWaveform(
90 makeLinearWaveform({ chirp_bandwidth: 20e3 }),
91 defaultGlobalParameters
92 );
93 expect(aliasIssues.some((issue) => issue.severity === 'error')).toBe(
94 true
95 );
96
97 const rfIssues = validateFmcwWaveform(
98 makeLinearWaveform({
99 carrier_frequency: 100,
100 start_frequency_offset: -200,
101 }),
102 defaultGlobalParameters
103 );
104 expect(
105 rfIssues.some((issue) => issue.message.includes('lower sweep edge'))
106 ).toBe(true);
107 });
108
109 test('reports linear schedule errors and warnings', () => {
110 const scenario = makeScenario(
111 makeLinearWaveform({
112 chirp_duration: 1e-3,
113 chirp_period: 2e-3,
114 })
115 );
116 const component = scenario.platforms[0].components[0];
117 if (component.type !== 'transmitter') {
118 throw new Error('Expected transmitter fixture');
119 }
120 component.schedule = [
121 { start: 0, end: 0.5e-3 },
122 { start: 1, end: 1.0015 },
123 ];
124
125 const issues = validateFmcwScenario(scenario);
126 expect(issues.some((issue) => issue.severity === 'error')).toBe(true);
127 expect(issues.some((issue) => issue.severity === 'warning')).toBe(true);
128 });
129
130 test('reports triangle period errors and silent-tail warnings', () => {
131 const scenario = makeScenario(
132 makeTriangleWaveform({ chirp_duration: 1e-3 })
133 );
134 const component = scenario.platforms[0].components[0];
135 if (component.type !== 'transmitter') {
136 throw new Error('Expected transmitter fixture');
137 }
138 component.schedule = [
139 { start: 0, end: 1e-3 },
140 { start: 1, end: 1.0025 },
141 ];
142
143 const issues = validateFmcwScenario(scenario);
144 expect(
145 issues.some((issue) => issue.message.includes('triangle period'))
146 ).toBe(true);
147 expect(issues.some((issue) => issue.message.includes('silent'))).toBe(
148 true
149 );
150 });
151
152 test('reports receiver dechirp mode/reference mismatches', () => {
153 const scenario = makeScenario(makeLinearWaveform({}));
154 scenario.platforms[0].components.push({
155 id: 'rx-1',
156 type: 'receiver',
157 name: 'FMCW Rx',
158 radarType: 'fmcw',
159 window_skip: null,
160 window_length: null,
161 prf: null,
162 antennaId: null,
163 timingId: null,
164 noiseTemperature: null,
165 noDirectPaths: false,
166 noPropagationLoss: false,
167 fmcwModeConfig: {
168 dechirp_mode: 'none',
169 dechirp_reference: { source: 'attached' },
170 },
171 schedule: [],
172 });
173
174 expect(
175 validateFmcwScenario(scenario).some((issue) =>
176 issue.message.includes('while dechirp mode is none')
177 )
178 ).toBe(true);
179
180 const receiver = scenario.platforms[0].components[1];
181 if (receiver.type !== 'receiver') {
182 throw new Error('Expected receiver fixture');
183 }
184 receiver.fmcwModeConfig = { dechirp_mode: 'physical' };
185 expect(
186 validateFmcwScenario(scenario).some((issue) =>
187 issue.message.includes('does not declare a dechirp reference')
188 )
189 ).toBe(true);
190
191 receiver.fmcwModeConfig = {
192 dechirp_mode: 'ideal',
193 dechirp_reference: { source: 'attached' },
194 };
195 expect(
196 validateFmcwScenario(scenario).some((issue) =>
197 issue.message.includes('only monostatic receivers')
198 )
199 ).toBe(true);
200 });
201
202 test('validates receiver IF-chain settings', () => {
203 const scenario = makeScenario(makeLinearWaveform({}));
204 scenario.platforms[0].components.push({
205 id: 'rx-1',
206 type: 'receiver',
207 name: 'FMCW Rx',
208 radarType: 'fmcw',
209 window_skip: null,
210 window_length: null,
211 prf: null,
212 antennaId: null,
213 timingId: null,
214 noiseTemperature: null,
215 noDirectPaths: false,
216 noPropagationLoss: false,
217 fmcwModeConfig: {
218 dechirp_mode: 'physical',
219 dechirp_reference: {
220 source: 'transmitter',
221 transmitter_name: 'FMCW Tx',
222 },
223 if_sample_rate: 4e3,
224 if_filter_bandwidth: 1e3,
225 if_filter_transition_width: 500,
226 },
227 schedule: [],
228 });
229
230 expect(
231 validateFmcwScenario(scenario).some(
232 (issue) => issue.field === 'fmcwModeConfig'
233 )
234 ).toBe(false);
235
236 const receiver = scenario.platforms[0].components[1];
237 if (receiver.type !== 'receiver') {
238 throw new Error('Expected receiver fixture');
239 }
240
241 receiver.fmcwModeConfig = {
242 dechirp_mode: 'none',
243 if_sample_rate: 4e3,
244 };
245 expect(
246 validateFmcwScenario(scenario).some((issue) =>
247 issue.message.includes('IF-chain settings')
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: -1,
258 };
259 expect(
260 validateFmcwScenario(scenario).some((issue) =>
261 issue.message.includes('IF sample rate')
262 )
263 ).toBe(true);
264
265 receiver.fmcwModeConfig = {
266 dechirp_mode: 'physical',
267 dechirp_reference: {
268 source: 'transmitter',
269 transmitter_name: 'FMCW Tx',
270 },
271 if_sample_rate: 4e3,
272 if_filter_bandwidth: 2e3,
273 };
274 expect(
275 validateFmcwScenario(scenario).some((issue) =>
276 issue.message.includes('less than half')
277 )
278 ).toBe(true);
279
280 receiver.fmcwModeConfig = {
281 dechirp_mode: 'physical',
282 dechirp_reference: {
283 source: 'transmitter',
284 transmitter_name: 'FMCW Tx',
285 },
286 if_sample_rate:
287 defaultGlobalParameters.rate *
288 defaultGlobalParameters.oversample_ratio +
289 1,
290 };
291 expect(
292 validateFmcwScenario(scenario).some((issue) =>
293 issue.message.includes('effective simulation sample rate')
294 )
295 ).toBe(true);
296 });
297
298 test('validates named transmitter and custom waveform dechirp references', () => {
299 const waveform = makeLinearWaveform({ id: 'wave-1', name: 'FMCW LO' });
300 const scenario = makeScenario(waveform);
301 scenario.waveforms.push({
302 id: 'wave-2',
303 type: 'Waveform',
304 name: 'CW Tone',
305 waveformType: 'cw',
306 power: 1,
307 carrier_frequency: 1e9,
308 });
309 scenario.platforms[0].components.push({
310 id: 'rx-1',
311 type: 'receiver',
312 name: 'FMCW Rx',
313 radarType: 'fmcw',
314 window_skip: null,
315 window_length: null,
316 prf: null,
317 antennaId: null,
318 timingId: null,
319 noiseTemperature: null,
320 noDirectPaths: false,
321 noPropagationLoss: false,
322 fmcwModeConfig: {
323 dechirp_mode: 'physical',
324 dechirp_reference: {
325 source: 'transmitter',
326 transmitter_name: 'FMCW Tx',
327 },
328 },
329 schedule: [],
330 });
331
332 expect(
333 validateFmcwScenario(scenario).some(
334 (issue) => issue.field === 'fmcwModeConfig'
335 )
336 ).toBe(false);
337
338 const receiver = scenario.platforms[0].components[1];
339 if (receiver.type !== 'receiver') {
340 throw new Error('Expected receiver fixture');
341 }
342
343 receiver.fmcwModeConfig = {
344 dechirp_mode: 'physical',
345 dechirp_reference: {
346 source: 'transmitter',
347 transmitter_name: 'Missing TX',
348 },
349 };
350 expect(
351 validateFmcwScenario(scenario).some((issue) =>
352 issue.message.includes('must be an FMCW transmitter')
353 )
354 ).toBe(true);
355
356 receiver.fmcwModeConfig = {
357 dechirp_mode: 'ideal',
358 dechirp_reference: {
359 source: 'custom',
360 waveform_name: 'FMCW LO',
361 },
362 };
363 expect(
364 validateFmcwScenario(scenario).some(
365 (issue) => issue.field === 'fmcwModeConfig'
366 )
367 ).toBe(false);
368
369 receiver.fmcwModeConfig = {
370 dechirp_mode: 'ideal',
371 dechirp_reference: {
372 source: 'custom',
373 waveform_name: 'CW Tone',
374 },
375 };
376 expect(
377 validateFmcwScenario(scenario).some((issue) =>
378 issue.message.includes('must be a top-level FMCW waveform')
379 )
380 ).toBe(true);
381 });
382});