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 simulation_name: z.string().min(1, 'Simulation name cannot be empty.'),
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.',
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.'),
27 .min(1, 'Oversample ratio must be at least 1.'),
29 latitude: z.number().min(-90).max(90),
30 longitude: z.number().min(-180).max(180),
35 frame: z.enum(['ENU', 'UTM', 'ECEF']),
36 zone: z.number().int().optional(),
37 hemisphere: z.enum(['N', 'S']).optional(),
41 if (data.frame === 'UTM') {
43 data.zone !== undefined && data.hemisphere !== undefined
48 { message: 'UTM frame requires a zone and hemisphere.' }
52export const WaveformSchema = z
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.'),
61 .positive('Carrier frequency must be positive.'),
62 filename: z.string().optional(),
66 if (data.waveformType === 'pulsed_from_file') {
67 return data.filename !== undefined && data.filename.length > 0;
72 message: 'A filename is required for this waveform type.',
77export const NoiseEntrySchema = z.object({
78 id: z.string().uuid(),
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),
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())
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()),
114 BaseAntennaSchema.extend({
115 pattern: z.literal('gaussian'),
116 azscale: nullableNumber.pipe(z.number().nullable()),
117 elscale: nullableNumber.pipe(z.number().nullable()),
119 BaseAntennaSchema.extend({
120 pattern: z.literal('squarehorn'),
121 diameter: nullableNumber.pipe(z.number().positive().nullable()),
123 BaseAntennaSchema.extend({
124 pattern: z.literal('parabolic'),
125 diameter: nullableNumber.pipe(z.number().positive().nullable()),
127 BaseAntennaSchema.extend({
128 pattern: z.literal('xml'),
131 .min(1, 'Filename is required for XML pattern.')
134 BaseAntennaSchema.extend({
135 pattern: z.literal('file'),
138 .min(1, 'Filename is required for file pattern.')
143export const PositionWaypointSchema = z.object({
144 id: z.string().uuid(),
147 altitude: z.number(),
148 time: z.number().min(0, 'Time cannot be negative.'),
151export const MotionPathSchema = z.object({
152 interpolation: z.enum(['static', 'linear', 'cubic']),
154 .array(PositionWaypointSchema)
155 .min(1, 'At least one waypoint is required.'),
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(),
166export const RotationWaypointSchema = z.object({
167 id: z.string().uuid(),
169 elevation: z.number(),
170 time: z.number().min(0, 'Time cannot be negative.'),
173export const RotationPathSchema = z.object({
174 type: z.literal('path'),
175 interpolation: z.enum(['static', 'linear', 'cubic']),
177 .array(RotationWaypointSchema)
178 .min(1, 'At least one waypoint is required.'),
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.'),
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,
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([]),
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']),
209 antennaId: z.string().uuid().nullable(),
210 waveformId: z.string().uuid().nullable(),
211 timingId: z.string().uuid().nullable(),
212 schedule: z.array(SchedulePeriodSchema).default([]),
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,
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([]),
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(),
242export const PlatformComponentSchema = z.discriminatedUnion('type', [
243 MonostaticComponentSchema,
244 TransmitterComponentSchema,
245 ReceiverComponentSchema,
246 TargetComponentSchema,
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),
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),