FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
hydration.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 { defaultGlobalParameters } from './defaults';
6import { buildHydratedScenarioState, parseScenarioData } from './hydration';
7import { ScenarioData, ScenarioState } from './types';
8
9function createScenarioData(): ScenarioData {
10 return {
11 globalParameters: {
12 ...defaultGlobalParameters,
13 start: 5,
14 end: 20,
15 },
16 waveforms: [],
17 timings: [],
18 antennas: [],
19 platforms: [
20 {
21 id: 'platform-1',
22 type: 'Platform',
23 name: 'Platform 1',
24 motionPath: {
25 interpolation: 'static',
26 waypoints: [
27 {
28 id: 'waypoint-1',
29 x: 0,
30 y: 0,
31 altitude: 0,
32 time: 0,
33 },
34 ],
35 },
36 rotation: {
37 type: 'fixed',
38 startAzimuth: 0,
39 startElevation: 0,
40 azimuthRate: 0,
41 elevationRate: 0,
42 },
43 components: [
44 {
45 id: 'component-1',
46 type: 'transmitter',
47 name: 'TX 1',
48 radarType: 'pulsed',
49 prf: 1000,
50 antennaId: null,
51 waveformId: null,
52 timingId: null,
53 schedule: [],
54 },
55 ],
56 },
57 ],
58 };
59}
60
61function createState(overrides: Partial<ScenarioState> = {}): ScenarioState {
62 return {
63 globalParameters: defaultGlobalParameters,
64 waveforms: [],
65 timings: [],
66 antennas: [],
67 platforms: [],
68 selectedItemId: null,
69 selectedComponentId: null,
70 isDirty: false,
71 isPlaying: false,
72 currentTime: 0,
73 targetPlaybackDuration: null,
74 isBackendSyncing: false,
75 backendVersion: 0,
76 scenarioFilePath: null,
77 outputDirectory: null,
78 antennaPreviewErrors: {},
79 notificationSnackbar: {
80 open: false,
81 message: '',
82 severity: 'error',
83 },
84 notificationQueue: [],
85 viewControlAction: { type: null, timestamp: 0 },
86 visibility: {
87 showAxes: true,
88 showPatterns: true,
89 showBoresights: true,
90 showLinks: true,
91 showLinkLabels: true,
92 showLinkMonostatic: true,
93 showLinkIlluminator: true,
94 showLinkScattered: true,
95 showLinkDirect: true,
96 showVelocities: true,
97 showPlatforms: true,
98 showPlatformLabels: true,
99 showMotionPaths: true,
100 },
101 ...overrides,
102 };
103}
104
105describe('buildHydratedScenarioState', () => {
106 test('preserves selection and clamps current time during backend recovery', () => {
107 const hydrated = buildHydratedScenarioState(
108 createState({
109 selectedItemId: 'platform-1',
110 selectedComponentId: 'component-1',
111 currentTime: 99,
112 }),
113 createScenarioData(),
114 {
115 isDirty: false,
116 preserveSelection: true,
117 preserveCurrentTime: true,
118 }
119 );
120
121 expect(hydrated.selectedItemId).toBe('platform-1');
122 expect(hydrated.selectedComponentId).toBe('component-1');
123 expect(hydrated.currentTime).toBe(20);
124 expect(hydrated.isDirty).toBe(false);
125 });
126
127 test('clears stale selection and resets time for import-style loads', () => {
128 const hydrated = buildHydratedScenarioState(
129 createState({
130 selectedItemId: 'missing-platform',
131 selectedComponentId: 'missing-component',
132 currentTime: 12,
133 }),
134 createScenarioData(),
135 {
136 isDirty: true,
137 }
138 );
139
140 expect(hydrated.selectedItemId).toBeNull();
141 expect(hydrated.selectedComponentId).toBeNull();
142 expect(hydrated.currentTime).toBe(5);
143 expect(hydrated.isDirty).toBe(true);
144 });
145});
146
147describe('parseScenarioData FMCW hydration', () => {
148 test('hydrates CW and FMCW file waveform variants', () => {
149 const scenario = parseScenarioData({
150 name: 'File waveforms',
151 parameters: {},
152 waveforms: [
153 {
154 id: 11,
155 name: 'CW File',
156 power: 1,
157 carrier_frequency: 1e9,
158 cw_from_file: { filename: 'cw.h5' },
159 },
160 {
161 id: 12,
162 name: 'FMCW File',
163 power: 2,
164 carrier_frequency: 2e9,
165 fmcw_from_file: { filename: 'fmcw.h5' },
166 },
167 ],
168 timings: [],
169 antennas: [],
170 platforms: [],
171 });
172
173 expect(scenario?.waveforms).toEqual([
174 expect.objectContaining({
175 id: '11',
176 waveformType: 'cw_from_file',
177 filename: 'cw.h5',
178 }),
179 expect.objectContaining({
180 id: '12',
181 waveformType: 'fmcw_from_file',
182 filename: 'fmcw.h5',
183 }),
184 ]);
185 });
186
187 test('hydrates FMCW waveform and component mode from backend data', () => {
188 const scenario = parseScenarioData({
189 name: 'FMCW Scenario',
190 parameters: {},
191 waveforms: [
192 {
193 id: 1,
194 name: 'FMCW Down Chirp',
195 power: 50,
196 carrier_frequency: 10e9,
197 fmcw_linear_chirp: {
198 direction: 'down',
199 chirp_bandwidth: 20e6,
200 chirp_duration: 250e-6,
201 chirp_period: 300e-6,
202 start_frequency_offset: -10e6,
203 chirp_count: 8,
204 },
205 },
206 {
207 id: 4,
208 name: 'FMCW Triangle',
209 power: 50,
210 carrier_frequency: 10e9,
211 fmcw_triangle: {
212 chirp_bandwidth: 20e6,
213 chirp_duration: 250e-6,
214 start_frequency_offset: -10e6,
215 triangle_count: 8,
216 },
217 },
218 ],
219 timings: [],
220 antennas: [],
221 platforms: [
222 {
223 id: 2,
224 name: 'Radar Platform',
225 motionpath: {
226 interpolation: 'static',
227 positionwaypoints: [
228 { x: 0, y: 0, altitude: 0, time: 0 },
229 ],
230 },
231 fixedrotation: {
232 startazimuth: 0,
233 startelevation: 0,
234 azimuthrate: 0,
235 elevationrate: 0,
236 },
237 components: [
238 {
239 receiver: {
240 id: 3,
241 name: 'FMCW RX',
242 fmcw_mode: {
243 dechirp_mode: 'ideal',
244 dechirp_reference: {
245 source: 'custom',
246 waveform_name: 'FMCW Down Chirp',
247 },
248 if_sample_rate: 1e6,
249 if_filter_bandwidth: 4e5,
250 if_filter_transition_width: 1e5,
251 },
252 },
253 },
254 ],
255 },
256 ],
257 });
258
259 expect(scenario).not.toBeNull();
260 expect(scenario?.waveforms[0]).toMatchObject({
261 id: '1',
262 waveformType: 'fmcw_linear_chirp',
263 direction: 'down',
264 chirp_bandwidth: 20e6,
265 chirp_duration: 250e-6,
266 chirp_period: 300e-6,
267 start_frequency_offset: -10e6,
268 chirp_count: 8,
269 });
270 expect(scenario?.waveforms[1]).toMatchObject({
271 id: '4',
272 waveformType: 'fmcw_triangle',
273 chirp_bandwidth: 20e6,
274 chirp_duration: 250e-6,
275 start_frequency_offset: -10e6,
276 triangle_count: 8,
277 });
278 expect(scenario?.platforms[0].components[0]).toMatchObject({
279 id: '3',
280 type: 'receiver',
281 radarType: 'fmcw',
282 fmcwModeConfig: {
283 dechirp_mode: 'ideal',
284 dechirp_reference: {
285 source: 'custom',
286 waveform_name: 'FMCW Down Chirp',
287 },
288 if_sample_rate: 1e6,
289 if_filter_bandwidth: 4e5,
290 if_filter_transition_width: 1e5,
291 },
292 });
293 });
294
295 test('hydrates receiver and monostatic dechirp references by backend names', () => {
296 const scenario = parseScenarioData({
297 name: 'FMCW Dechirp Scenario',
298 parameters: {},
299 waveforms: [
300 {
301 id: 10,
302 name: 'FMCW LO',
303 power: 50,
304 carrier_frequency: 10e9,
305 fmcw_linear_chirp: {
306 direction: 'up',
307 chirp_bandwidth: 20e6,
308 chirp_duration: 250e-6,
309 chirp_period: 300e-6,
310 },
311 },
312 ],
313 timings: [],
314 antennas: [],
315 platforms: [
316 {
317 id: 20,
318 name: 'Radar Platform',
319 motionpath: {
320 interpolation: 'static',
321 positionwaypoints: [
322 { x: 0, y: 0, altitude: 0, time: 0 },
323 ],
324 },
325 fixedrotation: {
326 startazimuth: 0,
327 startelevation: 0,
328 azimuthrate: 0,
329 elevationrate: 0,
330 },
331 components: [
332 {
333 transmitter: {
334 id: 21,
335 name: 'Reference TX',
336 waveform: 10,
337 fmcw_mode: {},
338 },
339 },
340 {
341 receiver: {
342 id: 22,
343 name: 'FMCW RX',
344 fmcw_mode: {
345 dechirp_mode: 'physical',
346 dechirp_reference: {
347 source: 'transmitter',
348 transmitter_name: 'Reference TX',
349 },
350 },
351 },
352 },
353 {
354 monostatic: {
355 id: 23,
356 tx_id: 24,
357 rx_id: 25,
358 name: 'FMCW Mono',
359 waveform: 10,
360 fmcw_mode: {
361 dechirp_mode: 'ideal',
362 dechirp_reference: {
363 source: 'attached',
364 },
365 },
366 },
367 },
368 ],
369 },
370 ],
371 });
372
373 expect(scenario).not.toBeNull();
374 expect(scenario?.platforms[0].components[1]).toMatchObject({
375 id: '22',
376 type: 'receiver',
377 radarType: 'fmcw',
378 fmcwModeConfig: {
379 dechirp_mode: 'physical',
380 dechirp_reference: {
381 source: 'transmitter',
382 transmitter_name: 'Reference TX',
383 },
384 },
385 });
386 expect(scenario?.platforms[0].components[2]).toMatchObject({
387 id: '23',
388 type: 'monostatic',
389 txId: '24',
390 rxId: '25',
391 radarType: 'fmcw',
392 fmcwModeConfig: {
393 dechirp_mode: 'ideal',
394 dechirp_reference: {
395 source: 'attached',
396 },
397 },
398 });
399 });
400});