FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
serializers.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 { PlatformComponentSchema, WaveformSchema } from '../scenarioSchema';
6import { createWaveformForType, defaultGlobalParameters } from './defaults';
7import {
8 serializeAntenna,
9 serializeComponentInner,
10 serializeGlobalParameters,
11 serializeWaveform,
12} from './serializers';
13import type { Antenna, PlatformComponent, Waveform } from './types';
14
15describe('serializeAntenna', () => {
16 test('uses an isotropic backend placeholder while an H5 antenna has no filename', () => {
17 const antenna: Antenna = {
18 id: '1',
19 type: 'Antenna',
20 name: 'Draft H5',
21 pattern: 'file',
22 filename: ' ',
23 efficiency: 0.8,
24 meshScale: 1,
25 design_frequency: null,
26 };
27
28 expect(serializeAntenna(antenna)).toEqual({
29 id: '1',
30 name: 'Draft H5',
31 pattern: 'isotropic',
32 efficiency: 0.8,
33 });
34 });
35
36 test('preserves H5 antenna payload once a filename is present', () => {
37 const antenna: Antenna = {
38 id: '2',
39 type: 'Antenna',
40 name: 'Loaded H5',
41 pattern: 'file',
42 filename: '/tmp/pattern.h5',
43 efficiency: 1,
44 meshScale: 1,
45 design_frequency: null,
46 };
47
48 expect(serializeAntenna(antenna)).toEqual({
49 id: '2',
50 name: 'Loaded H5',
51 pattern: 'file',
52 filename: '/tmp/pattern.h5',
53 efficiency: 1,
54 });
55 });
56});
57
58describe('serializeGlobalParameters', () => {
59 test('fills UTM zone and hemisphere defaults before backend sync', () => {
60 expect(
61 serializeGlobalParameters({
62 ...defaultGlobalParameters,
63 coordinateSystem: { frame: 'UTM' },
64 })
65 ).toMatchObject({
66 coordinatesystem: {
67 frame: 'UTM',
68 zone: 34,
69 hemisphere: 'S',
70 },
71 });
72 });
73});
74
75describe('FMCW schema', () => {
76 const validWaveform: Extract<
77 Waveform,
78 { waveformType: 'fmcw_linear_chirp' }
79 > = {
80 ...createWaveformForType('fmcw_linear_chirp'),
81 id: '10',
82 name: 'FMCW',
83 };
84
85 test('accepts valid FMCW waveform fields', () => {
86 expect(WaveformSchema.safeParse(validWaveform).success).toBe(true);
87 expect(
88 WaveformSchema.safeParse({
89 ...createWaveformForType('fmcw_triangle'),
90 id: '11',
91 name: 'Triangle',
92 }).success
93 ).toBe(true);
94 });
95
96 test('keeps generated FMCW defaults within the backend aliasing limit', () => {
97 const sweepStart = validWaveform.start_frequency_offset ?? 0;
98 const sweepEnd =
99 sweepStart +
100 (validWaveform.direction === 'down' ? -1 : 1) *
101 validWaveform.chirp_bandwidth;
102 const maxBaseband = Math.max(Math.abs(sweepStart), Math.abs(sweepEnd));
103 const effectiveRate =
104 defaultGlobalParameters.rate *
105 defaultGlobalParameters.oversample_ratio;
106
107 expect(effectiveRate).toBeGreaterThan(maxBaseband);
108 });
109
110 test('rejects invalid FMCW waveform fields', () => {
111 const invalidWaveforms = [
112 { ...validWaveform, chirp_bandwidth: 0 },
113 { ...validWaveform, chirp_duration: 0 },
114 {
115 ...validWaveform,
116 chirp_duration: 2e-6,
117 chirp_period: 1e-6,
118 },
119 { ...validWaveform, chirp_count: 0 },
120 { ...validWaveform, chirp_count: 1.5 },
121 {
122 ...validWaveform,
123 start_frequency_offset: Number.POSITIVE_INFINITY,
124 },
125 ];
126
127 for (const waveform of invalidWaveforms) {
128 expect(WaveformSchema.safeParse(waveform).success).toBe(false);
129 }
130
131 const invalidTriangles = [
132 {
133 ...createWaveformForType('fmcw_triangle'),
134 id: '12',
135 name: 'Bad Triangle',
136 chirp_bandwidth: 0,
137 },
138 {
139 ...createWaveformForType('fmcw_triangle'),
140 id: '13',
141 name: 'Bad Triangle Count',
142 triangle_count: 1.5,
143 },
144 ];
145 for (const waveform of invalidTriangles) {
146 expect(WaveformSchema.safeParse(waveform).success).toBe(false);
147 }
148 });
149
150 test('accepts FMCW radar mode on radar components', () => {
151 const component: PlatformComponent = {
152 id: '20',
153 type: 'transmitter',
154 name: 'FMCW TX',
155 radarType: 'fmcw',
156 prf: null,
157 antennaId: null,
158 waveformId: null,
159 timingId: null,
160 schedule: [],
161 };
162
163 expect(PlatformComponentSchema.safeParse(component).success).toBe(true);
164 });
165});
166
167describe('serializeWaveform', () => {
168 test('serializes FMCW waveform payload and omits unset optional fields', () => {
169 const waveform: Waveform = {
170 ...createWaveformForType('fmcw_linear_chirp'),
171 id: '30',
172 name: 'FMCW',
173 };
174
175 expect(serializeWaveform(waveform)).toEqual({
176 id: '30',
177 name: 'FMCW',
178 power: 1000,
179 carrier_frequency: 1e9,
180 fmcw_linear_chirp: {
181 direction: 'up',
182 chirp_bandwidth: 4e3,
183 chirp_duration: 1e-3,
184 chirp_period: 1e-3,
185 },
186 });
187 });
188
189 test('serializes FMCW optional fields when set', () => {
190 const waveform: Waveform = {
191 ...createWaveformForType('fmcw_linear_chirp'),
192 id: '31',
193 name: 'FMCW Offset',
194 start_frequency_offset: -1000,
195 chirp_count: 4,
196 };
197
198 expect(serializeWaveform(waveform)).toMatchObject({
199 fmcw_linear_chirp: {
200 direction: 'up',
201 start_frequency_offset: -1000,
202 chirp_count: 4,
203 },
204 });
205 });
206
207 test('serializes FMCW triangle waveform payload', () => {
208 const waveform: Waveform = {
209 ...createWaveformForType('fmcw_triangle'),
210 id: '32',
211 name: 'Triangle',
212 start_frequency_offset: -1000,
213 triangle_count: 4,
214 };
215
216 expect(serializeWaveform(waveform)).toEqual({
217 id: '32',
218 name: 'Triangle',
219 power: 1000,
220 carrier_frequency: 1e9,
221 fmcw_triangle: {
222 chirp_bandwidth: 4e3,
223 chirp_duration: 1e-3,
224 start_frequency_offset: -1000,
225 triangle_count: 4,
226 },
227 });
228 });
229});
230
231describe('serializeComponentInner', () => {
232 test('serializes empty component references as backend placeholders', () => {
233 const component: PlatformComponent = {
234 id: '41',
235 type: 'transmitter',
236 name: 'Draft Tx',
237 radarType: 'pulsed',
238 prf: 1000,
239 antennaId: '',
240 waveformId: '',
241 timingId: '',
242 schedule: [],
243 };
244
245 expect(serializeComponentInner(component)).toMatchObject({
246 antenna: 0,
247 waveform: 0,
248 timing: 0,
249 });
250 });
251
252 test('serializes FMCW mode without pulsed fields', () => {
253 const component: PlatformComponent = {
254 id: '40',
255 type: 'monostatic',
256 name: 'FMCW Mono',
257 txId: '41',
258 rxId: '42',
259 radarType: 'fmcw',
260 window_skip: 1,
261 window_length: 2,
262 prf: 3,
263 antennaId: null,
264 waveformId: null,
265 timingId: null,
266 noiseTemperature: null,
267 noDirectPaths: false,
268 noPropagationLoss: false,
269 schedule: [],
270 };
271
272 expect(serializeComponentInner(component)).toEqual({
273 tx_id: '41',
274 rx_id: '42',
275 name: 'FMCW Mono',
276 fmcw_mode: {},
277 antenna: 0,
278 waveform: 0,
279 timing: 0,
280 noise_temp: null,
281 nodirect: false,
282 nopropagationloss: false,
283 schedule: [],
284 });
285 });
286
287 test('preserves receiver FMCW attached dechirp mode fields', () => {
288 const component: PlatformComponent = {
289 id: '42',
290 type: 'receiver',
291 name: 'FMCW Rx',
292 radarType: 'fmcw',
293 window_skip: null,
294 window_length: null,
295 prf: null,
296 antennaId: null,
297 timingId: null,
298 noiseTemperature: null,
299 noDirectPaths: false,
300 noPropagationLoss: false,
301 fmcwModeConfig: {
302 dechirp_mode: 'physical',
303 dechirp_reference: { source: 'attached' },
304 if_sample_rate: 1e6,
305 if_filter_bandwidth: 4e5,
306 if_filter_transition_width: 1e5,
307 },
308 schedule: [],
309 };
310
311 expect(serializeComponentInner(component)).toMatchObject({
312 fmcw_mode: {
313 dechirp_mode: 'physical',
314 dechirp_reference: { source: 'attached' },
315 if_sample_rate: 1e6,
316 if_filter_bandwidth: 4e5,
317 if_filter_transition_width: 1e5,
318 },
319 });
320 });
321
322 test('preserves receiver FMCW transmitter dechirp reference name', () => {
323 const component: PlatformComponent = {
324 id: '43',
325 type: 'receiver',
326 name: 'FMCW Rx',
327 radarType: 'fmcw',
328 window_skip: null,
329 window_length: null,
330 prf: null,
331 antennaId: null,
332 timingId: null,
333 noiseTemperature: null,
334 noDirectPaths: false,
335 noPropagationLoss: false,
336 fmcwModeConfig: {
337 dechirp_mode: 'ideal',
338 dechirp_reference: {
339 source: 'transmitter',
340 transmitter_name: 'Reference TX',
341 },
342 },
343 schedule: [],
344 };
345
346 expect(serializeComponentInner(component)).toMatchObject({
347 fmcw_mode: {
348 dechirp_mode: 'ideal',
349 dechirp_reference: {
350 source: 'transmitter',
351 transmitter_name: 'Reference TX',
352 },
353 },
354 });
355 });
356
357 test('preserves monostatic FMCW custom dechirp reference waveform name', () => {
358 const component: PlatformComponent = {
359 id: '44',
360 type: 'monostatic',
361 name: 'FMCW Mono',
362 txId: '45',
363 rxId: '46',
364 radarType: 'fmcw',
365 window_skip: null,
366 window_length: null,
367 prf: null,
368 antennaId: null,
369 waveformId: null,
370 timingId: null,
371 noiseTemperature: null,
372 noDirectPaths: false,
373 noPropagationLoss: false,
374 fmcwModeConfig: {
375 dechirp_mode: 'physical',
376 dechirp_reference: {
377 source: 'custom',
378 waveform_name: 'Reference Waveform',
379 },
380 },
381 schedule: [],
382 };
383
384 expect(serializeComponentInner(component)).toMatchObject({
385 fmcw_mode: {
386 dechirp_mode: 'physical',
387 dechirp_reference: {
388 source: 'custom',
389 waveform_name: 'Reference Waveform',
390 },
391 },
392 });
393 });
394
395 test('uses an isotropic backend placeholder while a file target has no filename', () => {
396 const component: PlatformComponent = {
397 id: '50',
398 type: 'target',
399 name: 'Draft Target',
400 rcs_type: 'file',
401 rcs_value: 3,
402 rcs_model: 'constant',
403 };
404
405 expect(serializeComponentInner(component)).toEqual({
406 id: '50',
407 name: 'Draft Target',
408 rcs: {
409 type: 'isotropic',
410 value: 3,
411 },
412 });
413 });
414
415 test('serializes file target RCS once a filename is present', () => {
416 const component: PlatformComponent = {
417 id: '51',
418 type: 'target',
419 name: 'Loaded Target',
420 rcs_type: 'file',
421 rcs_filename: '/tmp/target.xml',
422 rcs_model: 'constant',
423 };
424
425 expect(serializeComponentInner(component)).toEqual({
426 id: '51',
427 name: 'Loaded Target',
428 rcs: {
429 type: 'file',
430 filename: '/tmp/target.xml',
431 },
432 });
433 });
434});