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