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 { PlatformComponentSchema, WaveformSchema } from '../scenarioSchema';
6import { createWaveformForType, defaultGlobalParameters } from './defaults';
9 serializeComponentInner,
10 serializeGlobalParameters,
12} from './serializers';
13import type { Antenna, PlatformComponent, Waveform } from './types';
15describe('serializeAntenna', () => {
16 test('uses an isotropic backend placeholder while an H5 antenna has no filename', () => {
17 const antenna: Antenna = {
25 design_frequency: null,
28 expect(serializeAntenna(antenna)).toEqual({
36 test('preserves H5 antenna payload once a filename is present', () => {
37 const antenna: Antenna = {
42 filename: '/tmp/pattern.h5',
45 design_frequency: null,
48 expect(serializeAntenna(antenna)).toEqual({
52 filename: '/tmp/pattern.h5',
58describe('serializeGlobalParameters', () => {
59 test('fills UTM zone and hemisphere defaults before backend sync', () => {
61 serializeGlobalParameters({
62 ...defaultGlobalParameters,
63 coordinateSystem: { frame: 'UTM' },
75describe('FMCW schema', () => {
76 const validWaveform: Extract<
78 { waveformType: 'fmcw_linear_chirp' }
80 ...createWaveformForType('fmcw_linear_chirp'),
85 test('accepts valid FMCW waveform fields', () => {
86 expect(WaveformSchema.safeParse(validWaveform).success).toBe(true);
88 WaveformSchema.safeParse({
89 ...createWaveformForType('fmcw_triangle'),
96 test('keeps generated FMCW defaults within the backend aliasing limit', () => {
97 const sweepStart = validWaveform.start_frequency_offset ?? 0;
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;
107 expect(effectiveRate).toBeGreaterThan(maxBaseband);
110 test('rejects invalid FMCW waveform fields', () => {
111 const invalidWaveforms = [
112 { ...validWaveform, chirp_bandwidth: 0 },
113 { ...validWaveform, chirp_duration: 0 },
116 chirp_duration: 2e-6,
119 { ...validWaveform, chirp_count: 0 },
120 { ...validWaveform, chirp_count: 1.5 },
123 start_frequency_offset: Number.POSITIVE_INFINITY,
127 for (const waveform of invalidWaveforms) {
128 expect(WaveformSchema.safeParse(waveform).success).toBe(false);
131 const invalidTriangles = [
133 ...createWaveformForType('fmcw_triangle'),
135 name: 'Bad Triangle',
139 ...createWaveformForType('fmcw_triangle'),
141 name: 'Bad Triangle Count',
145 for (const waveform of invalidTriangles) {
146 expect(WaveformSchema.safeParse(waveform).success).toBe(false);
150 test('accepts FMCW radar mode on radar components', () => {
151 const component: PlatformComponent = {
163 expect(PlatformComponentSchema.safeParse(component).success).toBe(true);
167describe('file waveform schema', () => {
168 test('requires a non-empty filename for CW and FMCW file variants', () => {
171 ...createWaveformForType('cw_from_file'),
176 ...createWaveformForType('fmcw_from_file'),
181 for (const waveform of waveforms) {
182 expect(WaveformSchema.safeParse(waveform).success).toBe(false);
184 WaveformSchema.safeParse({
186 filename: `${waveform.waveformType}.h5`,
190 WaveformSchema.safeParse({
192 filename: `${waveform.waveformType}.csv`,
199describe('SFCW schema', () => {
200 const validWaveform: Extract<
202 { waveformType: 'stepped_frequency' }
204 ...createWaveformForType('stepped_frequency'),
209 test('accepts valid SFCW waveform fields and radar mode', () => {
210 expect(WaveformSchema.safeParse(validWaveform).success).toBe(true);
211 const component: PlatformComponent = {
223 expect(PlatformComponentSchema.safeParse(component).success).toBe(true);
226 test('rejects invalid SFCW waveform fields', () => {
227 const invalidWaveforms = [
228 { ...validWaveform, step_size: 0 },
229 { ...validWaveform, step_count: 0 },
230 { ...validWaveform, step_count: 1.5 },
231 { ...validWaveform, dwell_time: 0 },
232 { ...validWaveform, step_period: 0 },
238 { ...validWaveform, sweep_count: 0 },
239 { ...validWaveform, sweep_count: 2.5 },
242 for (const waveform of invalidWaveforms) {
243 expect(WaveformSchema.safeParse(waveform).success).toBe(false);
248describe('serializeWaveform', () => {
249 test('serializes CW and FMCW file waveform payloads', () => {
250 const cw: Waveform = {
251 ...createWaveformForType('cw_from_file'),
254 filename: 'waveforms/cw.h5',
256 const fmcw: Waveform = {
257 ...createWaveformForType('fmcw_from_file'),
260 filename: 'waveforms/fmcw.h5',
263 expect(serializeWaveform(cw)).toMatchObject({
264 cw_from_file: { filename: 'waveforms/cw.h5' },
266 expect(serializeWaveform(fmcw)).toMatchObject({
267 fmcw_from_file: { filename: 'waveforms/fmcw.h5' },
271 test('serializes FMCW waveform payload and omits unset optional fields', () => {
272 const waveform: Waveform = {
273 ...createWaveformForType('fmcw_linear_chirp'),
278 expect(serializeWaveform(waveform)).toEqual({
282 carrier_frequency: 1e9,
285 chirp_bandwidth: 4e3,
286 chirp_duration: 1e-3,
292 test('serializes FMCW optional fields when set', () => {
293 const waveform: Waveform = {
294 ...createWaveformForType('fmcw_linear_chirp'),
297 start_frequency_offset: -1000,
301 expect(serializeWaveform(waveform)).toMatchObject({
304 start_frequency_offset: -1000,
310 test('serializes FMCW triangle waveform payload', () => {
311 const waveform: Waveform = {
312 ...createWaveformForType('fmcw_triangle'),
315 start_frequency_offset: -1000,
319 expect(serializeWaveform(waveform)).toEqual({
323 carrier_frequency: 1e9,
325 chirp_bandwidth: 4e3,
326 chirp_duration: 1e-3,
327 start_frequency_offset: -1000,
333 test('serializes SFCW waveform payload', () => {
334 const waveform: Waveform = {
335 ...createWaveformForType('stepped_frequency'),
338 start_frequency_offset: 0,
346 expect(serializeWaveform(waveform)).toEqual({
350 carrier_frequency: 1e9,
352 start_frequency_offset: 0,
363describe('serializeComponentInner', () => {
364 test('serializes empty component references as backend placeholders', () => {
365 const component: PlatformComponent = {
377 expect(serializeComponentInner(component)).toMatchObject({
384 test('serializes FMCW mode without pulsed fields', () => {
385 const component: PlatformComponent = {
398 noiseTemperature: null,
399 noDirectPaths: false,
400 noPropagationLoss: false,
404 expect(serializeComponentInner(component)).toEqual({
414 nopropagationloss: false,
419 test('serializes SFCW mode without pulsed or dechirp fields', () => {
420 const component: PlatformComponent = {
432 expect(serializeComponentInner(component)).toEqual({
443 test('preserves receiver FMCW attached dechirp mode fields', () => {
444 const component: PlatformComponent = {
454 noiseTemperature: null,
455 noDirectPaths: false,
456 noPropagationLoss: false,
458 dechirp_mode: 'physical',
459 dechirp_reference: { source: 'attached' },
461 if_filter_bandwidth: 4e5,
462 if_filter_transition_width: 1e5,
467 expect(serializeComponentInner(component)).toMatchObject({
469 dechirp_mode: 'physical',
470 dechirp_reference: { source: 'attached' },
472 if_filter_bandwidth: 4e5,
473 if_filter_transition_width: 1e5,
478 test('preserves receiver FMCW transmitter dechirp reference name', () => {
479 const component: PlatformComponent = {
489 noiseTemperature: null,
490 noDirectPaths: false,
491 noPropagationLoss: false,
493 dechirp_mode: 'ideal',
495 source: 'transmitter',
496 transmitter_name: 'Reference TX',
502 expect(serializeComponentInner(component)).toMatchObject({
504 dechirp_mode: 'ideal',
506 source: 'transmitter',
507 transmitter_name: 'Reference TX',
513 test('preserves monostatic FMCW custom dechirp reference waveform name', () => {
514 const component: PlatformComponent = {
527 noiseTemperature: null,
528 noDirectPaths: false,
529 noPropagationLoss: false,
531 dechirp_mode: 'physical',
534 waveform_name: 'Reference Waveform',
540 expect(serializeComponentInner(component)).toMatchObject({
542 dechirp_mode: 'physical',
545 waveform_name: 'Reference Waveform',
551 test('uses an isotropic backend placeholder while a file target has no filename', () => {
552 const component: PlatformComponent = {
555 name: 'Draft Target',
558 rcs_model: 'constant',
561 expect(serializeComponentInner(component)).toEqual({
563 name: 'Draft Target',
571 test('serializes file target RCS once a filename is present', () => {
572 const component: PlatformComponent = {
575 name: 'Loaded Target',
577 rcs_filename: '/tmp/target.xml',
578 rcs_model: 'constant',
581 expect(serializeComponentInner(component)).toEqual({
583 name: 'Loaded Target',
586 filename: '/tmp/target.xml',