FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
serializers.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 { omit } from '@/utils/typeUtils';
5import {
6 Antenna,
7 GlobalParameters,
8 Platform,
9 PlatformComponent,
10 Timing,
11 Waveform,
12} from './types';
13
14type DistributiveOmit<T, K extends PropertyKey> = T extends unknown
15 ? Omit<T, Extract<keyof T, K>>
16 : never;
17
18type SerializedAntenna =
19 | DistributiveOmit<Antenna, 'type' | 'meshScale'>
20 | {
21 id: Antenna['id'];
22 name: Antenna['name'];
23 pattern: 'isotropic';
24 efficiency: Antenna['efficiency'];
25 };
26
27type TargetComponent = Extract<PlatformComponent, { type: 'target' }>;
28
29/**
30 * Recursively removes null or undefined values from an object or array.
31 */
32export const cleanObject = <T>(obj: T): T => {
33 if (Array.isArray(obj)) {
34 return obj.map(cleanObject) as unknown as T;
35 }
36 if (obj !== null && typeof obj === 'object') {
37 const newObj = {} as Record<string, unknown>;
38 for (const [key, value] of Object.entries(obj)) {
39 if (value !== null && value !== undefined) {
40 newObj[key] = cleanObject(value);
41 }
42 }
43 return newObj as T;
44 }
45 return obj;
46};
47
48const serializeReferenceId = (id: string | null | undefined): string | 0 =>
49 typeof id === 'string' && id.trim().length > 0 ? id : 0;
50
51const serializeTargetRcs = (component: TargetComponent) => {
52 const filename = component.rcs_filename;
53 if (
54 component.rcs_type === 'file' &&
55 typeof filename === 'string' &&
56 filename.trim().length > 0
57 ) {
58 return {
59 type: 'file',
60 filename,
61 };
62 }
63
64 return {
65 type: 'isotropic',
66 value: component.rcs_value ?? 1,
67 };
68};
69
70/**
71 * Serializes the inner properties of a component (unwrapped).
72 * This is the exact format the C++ backend expects for granular updates.
73 */
74export const serializeComponentInner = (component: PlatformComponent) => {
75 const mode = (() => {
76 if (!('radarType' in component)) {
77 return {};
78 }
79
80 switch (component.radarType) {
81 case 'pulsed':
82 return {
83 pulsed_mode: {
84 prf: component.prf,
85 ...(component.type !== 'transmitter' && {
86 window_skip: component.window_skip,
87 window_length: component.window_length,
88 }),
89 },
90 };
91 case 'cw':
92 return { cw_mode: {} };
93 case 'fmcw':
94 if (
95 component.type === 'receiver' ||
96 component.type === 'monostatic'
97 ) {
98 return { fmcw_mode: component.fmcwModeConfig ?? {} };
99 }
100 return { fmcw_mode: {} };
101 case 'sfcw':
102 return { sfcw_mode: {} };
103 }
104 })();
105
106 switch (component.type) {
107 case 'monostatic':
108 return {
109 tx_id: component.txId,
110 rx_id: component.rxId,
111 name: component.name,
112 ...mode,
113 antenna: serializeReferenceId(component.antennaId),
114 waveform: serializeReferenceId(component.waveformId),
115 timing: serializeReferenceId(component.timingId),
116 noise_temp: component.noiseTemperature,
117 nodirect: component.noDirectPaths,
118 nopropagationloss: component.noPropagationLoss,
119 schedule: component.schedule,
120 };
121 case 'transmitter':
122 return {
123 id: component.id,
124 name: component.name,
125 ...mode,
126 antenna: serializeReferenceId(component.antennaId),
127 waveform: serializeReferenceId(component.waveformId),
128 timing: serializeReferenceId(component.timingId),
129 schedule: component.schedule,
130 };
131 case 'receiver':
132 return {
133 id: component.id,
134 name: component.name,
135 ...mode,
136 antenna: serializeReferenceId(component.antennaId),
137 timing: serializeReferenceId(component.timingId),
138 noise_temp: component.noiseTemperature,
139 nodirect: component.noDirectPaths,
140 nopropagationloss: component.noPropagationLoss,
141 schedule: component.schedule,
142 };
143 case 'target': {
144 const targetObj: Record<string, unknown> = {
145 id: component.id,
146 name: component.name,
147 rcs: serializeTargetRcs(component),
148 };
149 if (component.rcs_model !== 'constant') {
150 targetObj.model = {
151 type: component.rcs_model,
152 k: component.rcs_k,
153 };
154 }
155 return targetObj;
156 }
157 }
158};
159
160/**
161 * Serializes a component wrapped in its type key (e.g., { transmitter: { ... } }).
162 * This is the format expected by the full scenario JSON array.
163 */
164export const serializeComponent = (component: PlatformComponent) => {
165 return cleanObject({
166 [component.type]: serializeComponentInner(component),
167 });
168};
169
170export const serializePlatform = (p: Platform) => {
171 const { components, motionPath, rotation, ...rest } = p;
172
173 const backendComponents = components.map(serializeComponent);
174
175 const backendRotation: Record<string, unknown> = {};
176 if (rotation.type === 'fixed') {
177 const r = omit(rotation, 'type');
178 backendRotation.fixedrotation = {
179 interpolation: 'constant',
180 startazimuth: r.startAzimuth,
181 startelevation: r.startElevation,
182 azimuthrate: r.azimuthRate,
183 elevationrate: r.elevationRate,
184 };
185 } else {
186 const r = omit(rotation, 'type');
187 backendRotation.rotationpath = {
188 interpolation: r.interpolation,
189 rotationwaypoints: r.waypoints.map((wp) => omit(wp, 'id')),
190 };
191 }
192
193 return cleanObject({
194 ...rest,
195 id: p.id,
196 motionpath: {
197 interpolation: motionPath.interpolation,
198 positionwaypoints: motionPath.waypoints.map((wp) => omit(wp, 'id')),
199 },
200 ...backendRotation,
201 components: backendComponents,
202 });
203};
204
205export const serializeWaveform = (w: Waveform) => {
206 const waveformContent = (() => {
207 switch (w.waveformType) {
208 case 'cw':
209 return { cw: {} };
210 case 'pulsed_from_file':
211 return { pulsed_from_file: { filename: w.filename } };
212 case 'cw_from_file':
213 return { cw_from_file: { filename: w.filename } };
214 case 'fmcw_from_file':
215 return { fmcw_from_file: { filename: w.filename } };
216 case 'fmcw_linear_chirp':
217 return {
218 fmcw_linear_chirp: {
219 direction: w.direction,
220 chirp_bandwidth: w.chirp_bandwidth,
221 chirp_duration: w.chirp_duration,
222 chirp_period: w.chirp_period,
223 ...(w.start_frequency_offset
224 ? {
225 start_frequency_offset:
226 w.start_frequency_offset,
227 }
228 : {}),
229 ...(w.chirp_count !== null
230 ? { chirp_count: w.chirp_count }
231 : {}),
232 },
233 };
234 case 'fmcw_triangle':
235 return {
236 fmcw_triangle: {
237 chirp_bandwidth: w.chirp_bandwidth,
238 chirp_duration: w.chirp_duration,
239 ...(w.start_frequency_offset
240 ? {
241 start_frequency_offset:
242 w.start_frequency_offset,
243 }
244 : {}),
245 ...(w.triangle_count !== null
246 ? { triangle_count: w.triangle_count }
247 : {}),
248 },
249 };
250 case 'stepped_frequency':
251 return {
252 stepped_frequency: {
253 start_frequency_offset: w.start_frequency_offset,
254 step_size: w.step_size,
255 step_count: w.step_count,
256 dwell_time: w.dwell_time,
257 step_period: w.step_period,
258 ...(w.sweep_count !== null
259 ? { sweep_count: w.sweep_count }
260 : {}),
261 },
262 };
263 }
264 })();
265
266 return cleanObject({
267 id: w.id,
268 name: w.name,
269 power: w.power,
270 carrier_frequency: w.carrier_frequency,
271 ...waveformContent,
272 });
273};
274
275export const serializeTiming = (t: Timing) => {
276 const rest = omit(t, 'type');
277 const timingObj = {
278 ...rest,
279 synconpulse: false,
280 freq_offset: t.freqOffset,
281 random_freq_offset_stdev: t.randomFreqOffsetStdev,
282 phase_offset: t.phaseOffset,
283 random_phase_offset_stdev: t.randomPhaseOffsetStdev,
284 noise_entries: t.noiseEntries.map((entry) => omit(entry, 'id')),
285 };
286 if (timingObj.noise_entries?.length === 0) {
287 delete (timingObj as Partial<typeof timingObj>).noise_entries;
288 }
289 return cleanObject(timingObj);
290};
291
292export const isFileBackedAntennaPendingFile = (a: Antenna): boolean =>
293 (a.pattern === 'xml' || a.pattern === 'file') &&
294 (a.filename ?? '').trim().length === 0;
295
296export const serializeAntenna = (a: Antenna): SerializedAntenna => {
297 if (isFileBackedAntennaPendingFile(a)) {
298 return cleanObject({
299 id: a.id,
300 name: a.name,
301 pattern: 'isotropic',
302 efficiency: a.efficiency,
303 });
304 }
305
306 const rest = omit(a, 'type', 'meshScale');
307 return cleanObject(rest) as SerializedAntenna;
308};
309
310export function deriveUtmZone(longitude: number): number {
311 if (!Number.isFinite(longitude)) {
312 return 1;
313 }
314 const clampedLongitude = Math.max(-180, Math.min(180, longitude));
315 return Math.max(
316 1,
317 Math.min(60, Math.floor((clampedLongitude + 180) / 6) + 1)
318 );
319}
320
321const serializeCoordinateSystem = (
322 gp: GlobalParameters
323): GlobalParameters['coordinateSystem'] => {
324 if (gp.coordinateSystem.frame !== 'UTM') {
325 return {
326 frame: gp.coordinateSystem.frame,
327 };
328 }
329
330 return {
331 frame: 'UTM',
332 zone: gp.coordinateSystem.zone ?? deriveUtmZone(gp.origin.longitude),
333 hemisphere:
334 gp.coordinateSystem.hemisphere ??
335 (gp.origin.latitude < 0 ? 'S' : 'N'),
336 };
337};
338
339export const serializeGlobalParameters = (gp: GlobalParameters) => {
340 const { start, end, random_seed, oversample_ratio } = gp;
341 const gpRest = omit(
342 gp,
343 'start',
344 'end',
345 'random_seed',
346 'oversample_ratio',
347 'coordinateSystem'
348 );
349
350 return cleanObject({
351 ...gpRest,
352 starttime: start,
353 endtime: end,
354 randomseed: random_seed,
355 oversample: oversample_ratio,
356 rotationangleunit: gp.rotationAngleUnit,
357 coordinatesystem: serializeCoordinateSystem(gp),
358 });
359};