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 }
102 })();
103
104 switch (component.type) {
105 case 'monostatic':
106 return {
107 tx_id: component.txId,
108 rx_id: component.rxId,
109 name: component.name,
110 ...mode,
111 antenna: serializeReferenceId(component.antennaId),
112 waveform: serializeReferenceId(component.waveformId),
113 timing: serializeReferenceId(component.timingId),
114 noise_temp: component.noiseTemperature,
115 nodirect: component.noDirectPaths,
116 nopropagationloss: component.noPropagationLoss,
117 schedule: component.schedule,
118 };
119 case 'transmitter':
120 return {
121 id: component.id,
122 name: component.name,
123 ...mode,
124 antenna: serializeReferenceId(component.antennaId),
125 waveform: serializeReferenceId(component.waveformId),
126 timing: serializeReferenceId(component.timingId),
127 schedule: component.schedule,
128 };
129 case 'receiver':
130 return {
131 id: component.id,
132 name: component.name,
133 ...mode,
134 antenna: serializeReferenceId(component.antennaId),
135 timing: serializeReferenceId(component.timingId),
136 noise_temp: component.noiseTemperature,
137 nodirect: component.noDirectPaths,
138 nopropagationloss: component.noPropagationLoss,
139 schedule: component.schedule,
140 };
141 case 'target': {
142 const targetObj: Record<string, unknown> = {
143 id: component.id,
144 name: component.name,
145 rcs: serializeTargetRcs(component),
146 };
147 if (component.rcs_model !== 'constant') {
148 targetObj.model = {
149 type: component.rcs_model,
150 k: component.rcs_k,
151 };
152 }
153 return targetObj;
154 }
155 }
156};
157
158/**
159 * Serializes a component wrapped in its type key (e.g., { transmitter: { ... } }).
160 * This is the format expected by the full scenario JSON array.
161 */
162export const serializeComponent = (component: PlatformComponent) => {
163 return cleanObject({
164 [component.type]: serializeComponentInner(component),
165 });
166};
167
168export const serializePlatform = (p: Platform) => {
169 const { components, motionPath, rotation, ...rest } = p;
170
171 const backendComponents = components.map(serializeComponent);
172
173 const backendRotation: Record<string, unknown> = {};
174 if (rotation.type === 'fixed') {
175 const r = omit(rotation, 'type');
176 backendRotation.fixedrotation = {
177 interpolation: 'constant',
178 startazimuth: r.startAzimuth,
179 startelevation: r.startElevation,
180 azimuthrate: r.azimuthRate,
181 elevationrate: r.elevationRate,
182 };
183 } else {
184 const r = omit(rotation, 'type');
185 backendRotation.rotationpath = {
186 interpolation: r.interpolation,
187 rotationwaypoints: r.waypoints.map((wp) => omit(wp, 'id')),
188 };
189 }
190
191 return cleanObject({
192 ...rest,
193 id: p.id,
194 motionpath: {
195 interpolation: motionPath.interpolation,
196 positionwaypoints: motionPath.waypoints.map((wp) => omit(wp, 'id')),
197 },
198 ...backendRotation,
199 components: backendComponents,
200 });
201};
202
203export const serializeWaveform = (w: Waveform) => {
204 const waveformContent = (() => {
205 switch (w.waveformType) {
206 case 'cw':
207 return { cw: {} };
208 case 'pulsed_from_file':
209 return { pulsed_from_file: { filename: w.filename } };
210 case 'fmcw_linear_chirp':
211 return {
212 fmcw_linear_chirp: {
213 direction: w.direction,
214 chirp_bandwidth: w.chirp_bandwidth,
215 chirp_duration: w.chirp_duration,
216 chirp_period: w.chirp_period,
217 ...(w.start_frequency_offset
218 ? {
219 start_frequency_offset:
220 w.start_frequency_offset,
221 }
222 : {}),
223 ...(w.chirp_count !== null
224 ? { chirp_count: w.chirp_count }
225 : {}),
226 },
227 };
228 case 'fmcw_triangle':
229 return {
230 fmcw_triangle: {
231 chirp_bandwidth: w.chirp_bandwidth,
232 chirp_duration: w.chirp_duration,
233 ...(w.start_frequency_offset
234 ? {
235 start_frequency_offset:
236 w.start_frequency_offset,
237 }
238 : {}),
239 ...(w.triangle_count !== null
240 ? { triangle_count: w.triangle_count }
241 : {}),
242 },
243 };
244 }
245 })();
246
247 return cleanObject({
248 id: w.id,
249 name: w.name,
250 power: w.power,
251 carrier_frequency: w.carrier_frequency,
252 ...waveformContent,
253 });
254};
255
256export const serializeTiming = (t: Timing) => {
257 const rest = omit(t, 'type');
258 const timingObj = {
259 ...rest,
260 synconpulse: false,
261 freq_offset: t.freqOffset,
262 random_freq_offset_stdev: t.randomFreqOffsetStdev,
263 phase_offset: t.phaseOffset,
264 random_phase_offset_stdev: t.randomPhaseOffsetStdev,
265 noise_entries: t.noiseEntries.map((entry) => omit(entry, 'id')),
266 };
267 if (timingObj.noise_entries?.length === 0) {
268 delete (timingObj as Partial<typeof timingObj>).noise_entries;
269 }
270 return cleanObject(timingObj);
271};
272
273export const isFileBackedAntennaPendingFile = (a: Antenna): boolean =>
274 (a.pattern === 'xml' || a.pattern === 'file') &&
275 (a.filename ?? '').trim().length === 0;
276
277export const serializeAntenna = (a: Antenna): SerializedAntenna => {
278 if (isFileBackedAntennaPendingFile(a)) {
279 return cleanObject({
280 id: a.id,
281 name: a.name,
282 pattern: 'isotropic',
283 efficiency: a.efficiency,
284 });
285 }
286
287 const rest = omit(a, 'type', 'meshScale');
288 return cleanObject(rest) as SerializedAntenna;
289};
290
291export function deriveUtmZone(longitude: number): number {
292 if (!Number.isFinite(longitude)) {
293 return 1;
294 }
295 const clampedLongitude = Math.max(-180, Math.min(180, longitude));
296 return Math.max(
297 1,
298 Math.min(60, Math.floor((clampedLongitude + 180) / 6) + 1)
299 );
300}
301
302const serializeCoordinateSystem = (
303 gp: GlobalParameters
304): GlobalParameters['coordinateSystem'] => {
305 if (gp.coordinateSystem.frame !== 'UTM') {
306 return {
307 frame: gp.coordinateSystem.frame,
308 };
309 }
310
311 return {
312 frame: 'UTM',
313 zone: gp.coordinateSystem.zone ?? deriveUtmZone(gp.origin.longitude),
314 hemisphere:
315 gp.coordinateSystem.hemisphere ??
316 (gp.origin.latitude < 0 ? 'S' : 'N'),
317 };
318};
319
320export const serializeGlobalParameters = (gp: GlobalParameters) => {
321 const { start, end, random_seed, oversample_ratio } = gp;
322 const gpRest = omit(
323 gp,
324 'start',
325 'end',
326 'random_seed',
327 'oversample_ratio',
328 'coordinateSystem'
329 );
330
331 return cleanObject({
332 ...gpRest,
333 starttime: start,
334 endtime: end,
335 randomseed: random_seed,
336 oversample: oversample_ratio,
337 rotationangleunit: gp.rotationAngleUnit,
338 coordinatesystem: serializeCoordinateSystem(gp),
339 });
340};