FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
scenarioSchema.ts
Go to the documentation of this file.
1import { z } from 'zod';
2
3// Base numeric type for zod schemas - handles empty strings from forms
4const nullableNumber = z.preprocess(
5 (val) => (val === '' ? null : val),
6 z.number().nullable()
7);
8
9// --- SCHEMA DEFINITIONS ---
10
11export const GlobalParametersSchema = z.object({
12 id: z.literal('global-parameters'),
13 type: z.literal('GlobalParameters'),
14 rotationAngleUnit: z.enum(['deg', 'rad']),
15 simulation_name: z.string().min(1, 'Simulation name cannot be empty.'),
16 start: z.number(),
17 end: z.number(),
18 rate: z.number().positive('Rate must be positive.'),
19 simSamplingRate: nullableNumber.refine((val) => val === null || val > 0, {
20 message: 'Sim Sampling Rate must be positive if specified.',
21 }),
22 c: z.number().positive('Speed of light must be positive.'),
23 random_seed: nullableNumber.pipe(z.number().int().nullable()),
24 adc_bits: z.number().int().min(0, 'ADC bits cannot be negative.'),
25 oversample_ratio: z
26 .number()
27 .int()
28 .min(1, 'Oversample ratio must be at least 1.'),
29 origin: z.object({
30 latitude: z.number().min(-90).max(90),
31 longitude: z.number().min(-180).max(180),
32 altitude: z.number(),
33 }),
34 coordinateSystem: z
35 .object({
36 frame: z.enum(['ENU', 'UTM', 'ECEF']),
37 zone: z.number().int().optional(),
38 hemisphere: z.enum(['N', 'S']).optional(),
39 })
40 .refine(
41 (data) => {
42 if (data.frame === 'UTM') {
43 return (
44 data.zone !== undefined && data.hemisphere !== undefined
45 );
46 }
47 return true;
48 },
49 { message: 'UTM frame requires a zone and hemisphere.' }
50 ),
51});
52
53const SimIdSchema = z.string().regex(/^\d+$/, 'ID must be a numeric string.');
54
55const BaseWaveformSchema = z.object({
56 id: SimIdSchema,
57 type: z.literal('Waveform'),
58 name: z.string().min(1, 'Waveform name cannot be empty.'),
59 power: z.number().min(0, 'Power cannot be negative.'),
60 carrier_frequency: z
61 .number()
62 .positive('Carrier frequency must be positive.'),
63});
64
65export const WaveformSchema = z
66 .discriminatedUnion('waveformType', [
67 BaseWaveformSchema.extend({
68 waveformType: z.literal('pulsed_from_file'),
69 filename: z
70 .string()
71 .min(1, 'A filename is required for this waveform type.'),
72 }),
73 BaseWaveformSchema.extend({
74 waveformType: z.literal('cw'),
75 }),
76 BaseWaveformSchema.extend({
77 waveformType: z.literal('fmcw_linear_chirp'),
78 direction: z.enum(['up', 'down']),
79 chirp_bandwidth: z
80 .number()
81 .positive('Chirp bandwidth must be positive.'),
82 chirp_duration: z
83 .number()
84 .positive('Chirp duration must be positive.'),
85 chirp_period: z.number().positive('Chirp period must be positive.'),
86 start_frequency_offset: nullableNumber.pipe(
87 z.number().finite().nullable()
88 ),
89 chirp_count: nullableNumber.pipe(
90 z.number().int().positive().nullable()
91 ),
92 }),
93 BaseWaveformSchema.extend({
94 waveformType: z.literal('fmcw_triangle'),
95 chirp_bandwidth: z
96 .number()
97 .positive('Chirp bandwidth must be positive.'),
98 chirp_duration: z
99 .number()
100 .positive('Chirp duration must be positive.'),
101 start_frequency_offset: nullableNumber.pipe(
102 z.number().finite().nullable()
103 ),
104 triangle_count: nullableNumber.pipe(
105 z.number().int().positive().nullable()
106 ),
107 }),
108 ])
109 .superRefine((data, ctx) => {
110 if (
111 data.waveformType === 'fmcw_linear_chirp' &&
112 data.chirp_period < data.chirp_duration
113 ) {
114 ctx.addIssue({
115 code: 'custom',
116 message:
117 'Chirp period must be greater than or equal to chirp duration.',
118 path: ['chirp_period'],
119 });
120 }
121 });
122
123export const NoiseEntrySchema = z.object({
124 id: SimIdSchema,
125 alpha: z.number(),
126 weight: z.number(),
127});
128
129export const TimingSchema = z.object({
130 id: SimIdSchema,
131 type: z.literal('Timing'),
132 name: z.string().min(1, 'Timing name cannot be empty.'),
133 frequency: z.number().positive('Frequency must be positive.'),
134 freqOffset: nullableNumber,
135 randomFreqOffsetStdev: nullableNumber.pipe(z.number().min(0).nullable()),
136 phaseOffset: nullableNumber,
137 randomPhaseOffsetStdev: nullableNumber.pipe(z.number().min(0).nullable()),
138 noiseEntries: z.array(NoiseEntrySchema),
139});
140
141const BaseAntennaSchema = z.object({
142 id: SimIdSchema,
143 type: z.literal('Antenna'),
144 name: z.string().min(1, 'Antenna name cannot be empty.'),
145 efficiency: nullableNumber.pipe(z.number().min(0).max(1).nullable()),
146 meshScale: nullableNumber.pipe(z.number().positive().nullable()).optional(),
147 design_frequency: nullableNumber
148 .pipe(z.number().positive().nullable())
149 .optional(),
150});
151
152export const AntennaSchema = z.discriminatedUnion('pattern', [
153 BaseAntennaSchema.extend({ pattern: z.literal('isotropic') }),
154 BaseAntennaSchema.extend({
155 pattern: z.literal('sinc'),
156 alpha: nullableNumber.pipe(z.number().nullable()),
157 beta: nullableNumber.pipe(z.number().nullable()),
158 gamma: nullableNumber.pipe(z.number().nullable()),
159 }),
160 BaseAntennaSchema.extend({
161 pattern: z.literal('gaussian'),
162 azscale: nullableNumber.pipe(z.number().nullable()),
163 elscale: nullableNumber.pipe(z.number().nullable()),
164 }),
165 BaseAntennaSchema.extend({
166 pattern: z.literal('squarehorn'),
167 diameter: nullableNumber.pipe(z.number().positive().nullable()),
168 }),
169 BaseAntennaSchema.extend({
170 pattern: z.literal('parabolic'),
171 diameter: nullableNumber.pipe(z.number().positive().nullable()),
172 }),
173 BaseAntennaSchema.extend({
174 pattern: z.literal('xml'),
175 filename: z
176 .string()
177 .min(1, 'Filename is required for XML pattern.')
178 .optional(),
179 }),
180 BaseAntennaSchema.extend({
181 pattern: z.literal('file'),
182 filename: z
183 .string()
184 .min(1, 'Filename is required for file pattern.')
185 .optional(),
186 }),
187]);
188
189export const PositionWaypointSchema = z.object({
190 id: SimIdSchema,
191 x: z.number(),
192 y: z.number(),
193 altitude: z.number(),
194 time: z.number().min(0, 'Time cannot be negative.'),
195});
196
197export const MotionPathSchema = z.object({
198 interpolation: z.enum(['static', 'linear', 'cubic']),
199 waypoints: z
200 .array(PositionWaypointSchema)
201 .min(1, 'At least one waypoint is required.'),
202});
203
204export const FixedRotationSchema = z.object({
205 type: z.literal('fixed'),
206 startAzimuth: z.number(),
207 startElevation: z.number(),
208 azimuthRate: z.number(),
209 elevationRate: z.number(),
210});
211
212export const RotationWaypointSchema = z.object({
213 id: SimIdSchema,
214 azimuth: z.number(),
215 elevation: z.number(),
216 time: z.number().min(0, 'Time cannot be negative.'),
217});
218
219export const RotationPathSchema = z.object({
220 type: z.literal('path'),
221 interpolation: z.enum(['static', 'linear', 'cubic']),
222 waypoints: z
223 .array(RotationWaypointSchema)
224 .min(1, 'At least one waypoint is required.'),
225});
226
227export const SchedulePeriodSchema = z.object({
228 start: z.number().min(0, 'Start time cannot be negative.'),
229 end: z.number().min(0, 'End time cannot be negative.'),
230});
231
232const DechirpReferenceSchema = z.object({
233 source: z.enum(['attached', 'transmitter', 'custom']),
234 transmitter_name: z.string().optional(),
235 waveform_name: z.string().optional(),
236});
237
238const FmcwModeConfigSchema = z
239 .object({
240 dechirp_mode: z.enum(['none', 'physical', 'ideal']).optional(),
241 dechirp_reference: DechirpReferenceSchema.optional(),
242 if_sample_rate: z.number().optional(),
243 if_filter_bandwidth: z.number().optional(),
244 if_filter_transition_width: z.number().optional(),
245 })
246 .optional();
247
248const MonostaticComponentSchema = z.object({
249 id: SimIdSchema,
250 type: z.literal('monostatic'),
251 name: z.string().min(1),
252 txId: SimIdSchema,
253 rxId: SimIdSchema,
254 radarType: z.enum(['pulsed', 'cw', 'fmcw']),
255 window_skip: nullableNumber,
256 window_length: nullableNumber,
257 prf: nullableNumber,
258 antennaId: SimIdSchema.nullable(),
259 waveformId: SimIdSchema.nullable(),
260 timingId: SimIdSchema.nullable(),
261 noiseTemperature: nullableNumber.pipe(z.number().min(0).nullable()),
262 noDirectPaths: z.boolean(),
263 noPropagationLoss: z.boolean(),
264 fmcwModeConfig: FmcwModeConfigSchema,
265 schedule: z.array(SchedulePeriodSchema).default([]),
266});
267
268const TransmitterComponentSchema = z.object({
269 id: SimIdSchema,
270 type: z.literal('transmitter'),
271 name: z.string().min(1),
272 radarType: z.enum(['pulsed', 'cw', 'fmcw']),
273 prf: nullableNumber,
274 antennaId: SimIdSchema.nullable(),
275 waveformId: SimIdSchema.nullable(),
276 timingId: SimIdSchema.nullable(),
277 schedule: z.array(SchedulePeriodSchema).default([]),
278});
279
280const ReceiverComponentSchema = z.object({
281 id: SimIdSchema,
282 type: z.literal('receiver'),
283 name: z.string().min(1),
284 radarType: z.enum(['pulsed', 'cw', 'fmcw']),
285 window_skip: nullableNumber,
286 window_length: nullableNumber,
287 prf: nullableNumber,
288 antennaId: SimIdSchema.nullable(),
289 timingId: SimIdSchema.nullable(),
290 noiseTemperature: nullableNumber.pipe(z.number().min(0).nullable()),
291 noDirectPaths: z.boolean(),
292 noPropagationLoss: z.boolean(),
293 fmcwModeConfig: FmcwModeConfigSchema,
294 schedule: z.array(SchedulePeriodSchema).default([]),
295});
296
297const TargetComponentSchema = z.object({
298 id: SimIdSchema,
299 type: z.literal('target'),
300 name: z.string().min(1),
301 rcs_type: z.enum(['isotropic', 'file']),
302 rcs_value: z.number().optional(),
303 rcs_filename: z.string().optional(),
304 rcs_model: z.enum(['constant', 'chisquare', 'gamma']),
305 rcs_k: z.number().optional(),
306});
307
308export const PlatformComponentSchema = z.discriminatedUnion('type', [
309 MonostaticComponentSchema,
310 TransmitterComponentSchema,
311 ReceiverComponentSchema,
312 TargetComponentSchema,
313]);
314
315export const PlatformSchema = z.object({
316 id: SimIdSchema,
317 type: z.literal('Platform'),
318 name: z.string().min(1, 'Platform name cannot be empty.'),
319 motionPath: MotionPathSchema,
320 rotation: z.union([FixedRotationSchema, RotationPathSchema]),
321 components: z.array(PlatformComponentSchema),
322});
323
324export const ScenarioDataSchema = z.object({
325 globalParameters: GlobalParametersSchema,
326 waveforms: z.array(WaveformSchema),
327 timings: z.array(TimingSchema),
328 antennas: z.array(AntennaSchema),
329 platforms: z.array(PlatformSchema),
330});