1import { z } from 'zod';
3// Base numeric type for zod schemas - handles empty strings from forms
4const nullableNumber = z.preprocess(
5 (val) => (val === '' ? null : val),
9// --- SCHEMA DEFINITIONS ---
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.'),
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.',
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.'),
28 .min(1, 'Oversample ratio must be at least 1.'),
30 latitude: z.number().min(-90).max(90),
31 longitude: z.number().min(-180).max(180),
36 frame: z.enum(['ENU', 'UTM', 'ECEF']),
37 zone: z.number().int().optional(),
38 hemisphere: z.enum(['N', 'S']).optional(),
42 if (data.frame === 'UTM') {
44 data.zone !== undefined && data.hemisphere !== undefined
49 { message: 'UTM frame requires a zone and hemisphere.' }
53const SimIdSchema = z.string().regex(/^\d+$/, 'ID must be a numeric string.');
55const BaseWaveformSchema = z.object({
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.'),
62 .positive('Carrier frequency must be positive.'),
65export const WaveformSchema = z
66 .discriminatedUnion('waveformType', [
67 BaseWaveformSchema.extend({
68 waveformType: z.literal('pulsed_from_file'),
71 .min(1, 'A filename is required for this waveform type.'),
73 BaseWaveformSchema.extend({
74 waveformType: z.literal('cw'),
76 BaseWaveformSchema.extend({
77 waveformType: z.literal('fmcw_linear_chirp'),
78 direction: z.enum(['up', 'down']),
81 .positive('Chirp bandwidth must be positive.'),
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()
89 chirp_count: nullableNumber.pipe(
90 z.number().int().positive().nullable()
93 BaseWaveformSchema.extend({
94 waveformType: z.literal('fmcw_triangle'),
97 .positive('Chirp bandwidth must be positive.'),
100 .positive('Chirp duration must be positive.'),
101 start_frequency_offset: nullableNumber.pipe(
102 z.number().finite().nullable()
104 triangle_count: nullableNumber.pipe(
105 z.number().int().positive().nullable()
109 .superRefine((data, ctx) => {
111 data.waveformType === 'fmcw_linear_chirp' &&
112 data.chirp_period < data.chirp_duration
117 'Chirp period must be greater than or equal to chirp duration.',
118 path: ['chirp_period'],
123export const NoiseEntrySchema = z.object({
129export const TimingSchema = z.object({
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),
141const BaseAntennaSchema = z.object({
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())
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()),
160 BaseAntennaSchema.extend({
161 pattern: z.literal('gaussian'),
162 azscale: nullableNumber.pipe(z.number().nullable()),
163 elscale: nullableNumber.pipe(z.number().nullable()),
165 BaseAntennaSchema.extend({
166 pattern: z.literal('squarehorn'),
167 diameter: nullableNumber.pipe(z.number().positive().nullable()),
169 BaseAntennaSchema.extend({
170 pattern: z.literal('parabolic'),
171 diameter: nullableNumber.pipe(z.number().positive().nullable()),
173 BaseAntennaSchema.extend({
174 pattern: z.literal('xml'),
177 .min(1, 'Filename is required for XML pattern.')
180 BaseAntennaSchema.extend({
181 pattern: z.literal('file'),
184 .min(1, 'Filename is required for file pattern.')
189export const PositionWaypointSchema = z.object({
193 altitude: z.number(),
194 time: z.number().min(0, 'Time cannot be negative.'),
197export const MotionPathSchema = z.object({
198 interpolation: z.enum(['static', 'linear', 'cubic']),
200 .array(PositionWaypointSchema)
201 .min(1, 'At least one waypoint is required.'),
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(),
212export const RotationWaypointSchema = z.object({
215 elevation: z.number(),
216 time: z.number().min(0, 'Time cannot be negative.'),
219export const RotationPathSchema = z.object({
220 type: z.literal('path'),
221 interpolation: z.enum(['static', 'linear', 'cubic']),
223 .array(RotationWaypointSchema)
224 .min(1, 'At least one waypoint is required.'),
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.'),
232const DechirpReferenceSchema = z.object({
233 source: z.enum(['attached', 'transmitter', 'custom']),
234 transmitter_name: z.string().optional(),
235 waveform_name: z.string().optional(),
238const FmcwModeConfigSchema = z
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(),
248const MonostaticComponentSchema = z.object({
250 type: z.literal('monostatic'),
251 name: z.string().min(1),
254 radarType: z.enum(['pulsed', 'cw', 'fmcw']),
255 window_skip: nullableNumber,
256 window_length: 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([]),
268const TransmitterComponentSchema = z.object({
270 type: z.literal('transmitter'),
271 name: z.string().min(1),
272 radarType: z.enum(['pulsed', 'cw', 'fmcw']),
274 antennaId: SimIdSchema.nullable(),
275 waveformId: SimIdSchema.nullable(),
276 timingId: SimIdSchema.nullable(),
277 schedule: z.array(SchedulePeriodSchema).default([]),
280const ReceiverComponentSchema = z.object({
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,
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([]),
297const TargetComponentSchema = z.object({
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(),
308export const PlatformComponentSchema = z.discriminatedUnion('type', [
309 MonostaticComponentSchema,
310 TransmitterComponentSchema,
311 ReceiverComponentSchema,
312 TargetComponentSchema,
315export const PlatformSchema = z.object({
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),
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),