FERS 1.0.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 simulation_name: z.string().min(1, 'Simulation name cannot be empty.'),
15 start: z.number(),
16 end: z.number(),
17 rate: z.number().positive('Rate must be positive.'),
18 simSamplingRate: nullableNumber.refine((val) => val === null || val > 0, {
19 message: 'Sim Sampling Rate must be positive if specified.',
20 }),
21 c: z.number().positive('Speed of light must be positive.'),
22 random_seed: nullableNumber.pipe(z.number().int().nullable()),
23 adc_bits: z.number().int().min(0, 'ADC bits cannot be negative.'),
24 oversample_ratio: z
25 .number()
26 .int()
27 .min(1, 'Oversample ratio must be at least 1.'),
28 origin: z.object({
29 latitude: z.number().min(-90).max(90),
30 longitude: z.number().min(-180).max(180),
31 altitude: z.number(),
32 }),
33 coordinateSystem: z
34 .object({
35 frame: z.enum(['ENU', 'UTM', 'ECEF']),
36 zone: z.number().int().optional(),
37 hemisphere: z.enum(['N', 'S']).optional(),
38 })
39 .refine(
40 (data) => {
41 if (data.frame === 'UTM') {
42 return (
43 data.zone !== undefined && data.hemisphere !== undefined
44 );
45 }
46 return true;
47 },
48 { message: 'UTM frame requires a zone and hemisphere.' }
49 ),
50});
51
52export const WaveformSchema = z
53 .object({
54 id: z.string().uuid(),
55 type: z.literal('Waveform'),
56 name: z.string().min(1, 'Waveform name cannot be empty.'),
57 waveformType: z.enum(['pulsed_from_file', 'cw']),
58 power: z.number().min(0, 'Power cannot be negative.'),
59 carrier_frequency: z
60 .number()
61 .positive('Carrier frequency must be positive.'),
62 filename: z.string().optional(),
63 })
64 .refine(
65 (data) => {
66 if (data.waveformType === 'pulsed_from_file') {
67 return data.filename !== undefined && data.filename.length > 0;
68 }
69 return true;
70 },
71 {
72 message: 'A filename is required for this waveform type.',
73 path: ['filename'],
74 }
75 );
76
77export const NoiseEntrySchema = z.object({
78 id: z.string().uuid(),
79 alpha: z.number(),
80 weight: z.number(),
81});
82
83export const TimingSchema = z.object({
84 id: z.string().uuid(),
85 type: z.literal('Timing'),
86 name: z.string().min(1, 'Timing name cannot be empty.'),
87 frequency: z.number().positive('Frequency must be positive.'),
88 freqOffset: nullableNumber,
89 randomFreqOffsetStdev: nullableNumber.pipe(z.number().min(0).nullable()),
90 phaseOffset: nullableNumber,
91 randomPhaseOffsetStdev: nullableNumber.pipe(z.number().min(0).nullable()),
92 noiseEntries: z.array(NoiseEntrySchema),
93});
94
95const BaseAntennaSchema = z.object({
96 id: z.string().uuid(),
97 type: z.literal('Antenna'),
98 name: z.string().min(1, 'Antenna name cannot be empty.'),
99 efficiency: nullableNumber.pipe(z.number().min(0).max(1).nullable()),
100 meshScale: nullableNumber.pipe(z.number().positive().nullable()).optional(),
101 design_frequency: nullableNumber
102 .pipe(z.number().positive().nullable())
103 .optional(),
104});
105
106export const AntennaSchema = z.discriminatedUnion('pattern', [
107 BaseAntennaSchema.extend({ pattern: z.literal('isotropic') }),
108 BaseAntennaSchema.extend({
109 pattern: z.literal('sinc'),
110 alpha: nullableNumber.pipe(z.number().nullable()),
111 beta: nullableNumber.pipe(z.number().nullable()),
112 gamma: nullableNumber.pipe(z.number().nullable()),
113 }),
114 BaseAntennaSchema.extend({
115 pattern: z.literal('gaussian'),
116 azscale: nullableNumber.pipe(z.number().nullable()),
117 elscale: nullableNumber.pipe(z.number().nullable()),
118 }),
119 BaseAntennaSchema.extend({
120 pattern: z.literal('squarehorn'),
121 diameter: nullableNumber.pipe(z.number().positive().nullable()),
122 }),
123 BaseAntennaSchema.extend({
124 pattern: z.literal('parabolic'),
125 diameter: nullableNumber.pipe(z.number().positive().nullable()),
126 }),
127 BaseAntennaSchema.extend({
128 pattern: z.literal('xml'),
129 filename: z
130 .string()
131 .min(1, 'Filename is required for XML pattern.')
132 .optional(),
133 }),
134 BaseAntennaSchema.extend({
135 pattern: z.literal('file'),
136 filename: z
137 .string()
138 .min(1, 'Filename is required for file pattern.')
139 .optional(),
140 }),
141]);
142
143export const PositionWaypointSchema = z.object({
144 id: z.string().uuid(),
145 x: z.number(),
146 y: z.number(),
147 altitude: z.number(),
148 time: z.number().min(0, 'Time cannot be negative.'),
149});
150
151export const MotionPathSchema = z.object({
152 interpolation: z.enum(['static', 'linear', 'cubic']),
153 waypoints: z
154 .array(PositionWaypointSchema)
155 .min(1, 'At least one waypoint is required.'),
156});
157
158export const FixedRotationSchema = z.object({
159 type: z.literal('fixed'),
160 startAzimuth: z.number(),
161 startElevation: z.number(),
162 azimuthRate: z.number(),
163 elevationRate: z.number(),
164});
165
166export const RotationWaypointSchema = z.object({
167 id: z.string().uuid(),
168 azimuth: z.number(),
169 elevation: z.number(),
170 time: z.number().min(0, 'Time cannot be negative.'),
171});
172
173export const RotationPathSchema = z.object({
174 type: z.literal('path'),
175 interpolation: z.enum(['static', 'linear', 'cubic']),
176 waypoints: z
177 .array(RotationWaypointSchema)
178 .min(1, 'At least one waypoint is required.'),
179});
180
181export const SchedulePeriodSchema = z.object({
182 start: z.number().min(0, 'Start time cannot be negative.'),
183 end: z.number().min(0, 'End time cannot be negative.'),
184});
185
186const MonostaticComponentSchema = z.object({
187 id: z.string().uuid(),
188 type: z.literal('monostatic'),
189 name: z.string().min(1),
190 radarType: z.enum(['pulsed', 'cw']),
191 window_skip: nullableNumber,
192 window_length: nullableNumber,
193 prf: nullableNumber,
194 antennaId: z.string().uuid().nullable(),
195 waveformId: z.string().uuid().nullable(),
196 timingId: z.string().uuid().nullable(),
197 noiseTemperature: nullableNumber.pipe(z.number().min(0).nullable()),
198 noDirectPaths: z.boolean(),
199 noPropagationLoss: z.boolean(),
200 schedule: z.array(SchedulePeriodSchema).default([]),
201});
202
203const TransmitterComponentSchema = z.object({
204 id: z.string().uuid(),
205 type: z.literal('transmitter'),
206 name: z.string().min(1),
207 radarType: z.enum(['pulsed', 'cw']),
208 prf: nullableNumber,
209 antennaId: z.string().uuid().nullable(),
210 waveformId: z.string().uuid().nullable(),
211 timingId: z.string().uuid().nullable(),
212 schedule: z.array(SchedulePeriodSchema).default([]),
213});
214
215const ReceiverComponentSchema = z.object({
216 id: z.string().uuid(),
217 type: z.literal('receiver'),
218 name: z.string().min(1),
219 radarType: z.enum(['pulsed', 'cw']),
220 window_skip: nullableNumber,
221 window_length: nullableNumber,
222 prf: nullableNumber,
223 antennaId: z.string().uuid().nullable(),
224 timingId: z.string().uuid().nullable(),
225 noiseTemperature: nullableNumber.pipe(z.number().min(0).nullable()),
226 noDirectPaths: z.boolean(),
227 noPropagationLoss: z.boolean(),
228 schedule: z.array(SchedulePeriodSchema).default([]),
229});
230
231const TargetComponentSchema = z.object({
232 id: z.string().uuid(),
233 type: z.literal('target'),
234 name: z.string().min(1),
235 rcs_type: z.enum(['isotropic', 'file']),
236 rcs_value: z.number().optional(),
237 rcs_filename: z.string().optional(),
238 rcs_model: z.enum(['constant', 'chisquare', 'gamma']),
239 rcs_k: z.number().optional(),
240});
241
242export const PlatformComponentSchema = z.discriminatedUnion('type', [
243 MonostaticComponentSchema,
244 TransmitterComponentSchema,
245 ReceiverComponentSchema,
246 TargetComponentSchema,
247]);
248
249export const PlatformSchema = z.object({
250 id: z.string().uuid(),
251 type: z.literal('Platform'),
252 name: z.string().min(1, 'Platform name cannot be empty.'),
253 motionPath: MotionPathSchema,
254 rotation: z.union([FixedRotationSchema, RotationPathSchema]),
255 components: z.array(PlatformComponentSchema),
256});
257
258export const ScenarioDataSchema = z.object({
259 globalParameters: GlobalParametersSchema,
260 waveforms: z.array(WaveformSchema),
261 timings: z.array(TimingSchema),
262 antennas: z.array(AntennaSchema),
263 platforms: z.array(PlatformSchema),
264});