FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
WaveformInspector.tsx
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
3
4import {
5 Alert,
6 Box,
7 FormControl,
8 InputLabel,
9 MenuItem,
10 Select,
11} from '@mui/material';
12import { useScenarioStore, Waveform } from '@/stores/scenarioStore';
13import { createWaveformForType as createWaveformDefaultsForType } from '@/stores/scenarioStore/defaults';
14import { validateFmcwWaveform } from '@/stores/scenarioStore/fmcwValidation';
15import { BufferedTextField, FileInput, NumberField } from './InspectorControls';
16
17export type WaveformType =
18 | 'pulsed_from_file'
19 | 'cw_from_file'
20 | 'fmcw_from_file'
21 | 'cw'
22 | 'fmcw_linear_chirp'
23 | 'fmcw_triangle'
24 | 'stepped_frequency';
25
26type FmcwLinearChirpFields = {
27 direction: 'up' | 'down';
28 chirp_bandwidth: number;
29 chirp_duration: number;
30 chirp_period: number;
31 start_frequency_offset: number | null;
32 chirp_count: number | null;
33};
34
35type FmcwTriangleFields = {
36 chirp_bandwidth: number;
37 chirp_duration: number;
38 start_frequency_offset: number | null;
39 triangle_count: number | null;
40};
41
42type SfcwFields = {
43 start_frequency_offset: number | null;
44 step_size: number;
45 step_count: number;
46 dwell_time: number;
47 step_period: number;
48 sweep_count: number | null;
49};
50
51type AuthorableWaveform = Omit<Waveform, 'waveformType'> &
52 Partial<FmcwLinearChirpFields> &
53 Partial<FmcwTriangleFields> &
54 Partial<SfcwFields> & {
55 waveformType: WaveformType;
56 filename?: string;
57 };
58
59export const WAVEFORM_TYPE_OPTIONS: ReadonlyArray<{
60 value: WaveformType;
61 label: string;
62}> = [
63 { value: 'pulsed_from_file', label: 'Pulse File' },
64 { value: 'cw_from_file', label: 'CW File' },
65 { value: 'fmcw_from_file', label: 'FMCW File' },
66 { value: 'cw', label: 'CW Tone (Generated)' },
67 { value: 'fmcw_linear_chirp', label: 'FMCW Linear Chirp (Generated)' },
68 { value: 'fmcw_triangle', label: 'FMCW Triangle (Generated)' },
69 { value: 'stepped_frequency', label: 'SFCW' },
70];
71
72const DEFAULT_FMCW_LINEAR_CHIRP_FIELDS =
73 createWaveformDefaultsForType('fmcw_linear_chirp');
74const DEFAULT_FMCW_TRIANGLE_FIELDS =
75 createWaveformDefaultsForType('fmcw_triangle');
76const DEFAULT_SFCW_FIELDS = createWaveformDefaultsForType('stepped_frequency');
77
78interface WaveformInspectorProps {
79 item: Waveform;
80}
81
82const asNumberOrDefault = (value: unknown, fallback: number): number =>
83 typeof value === 'number' && Number.isFinite(value) ? value : fallback;
84
85const asNullableNumber = (value: unknown): number | null =>
86 typeof value === 'number' && Number.isFinite(value) ? value : null;
87
88const asNullableNumberOrDefault = (
89 value: unknown,
90 fallback: number | null
91): number | null =>
92 typeof value === 'number' && Number.isFinite(value) ? value : fallback;
93
94const asNullableInteger = (value: unknown): number | null =>
95 typeof value === 'number' && Number.isFinite(value)
96 ? Math.trunc(value)
97 : null;
98
99export function createWaveformForType(
100 waveform: AuthorableWaveform,
101 waveformType: WaveformType
102): AuthorableWaveform {
103 const nextWaveform = {
104 ...waveform,
105 waveformType,
106 };
107
108 if (
109 waveformType === 'pulsed_from_file' ||
110 waveformType === 'cw_from_file' ||
111 waveformType === 'fmcw_from_file'
112 ) {
113 return {
114 ...nextWaveform,
115 filename:
116 typeof waveform.filename === 'string' ? waveform.filename : '',
117 };
118 }
119
120 if (waveformType === 'fmcw_linear_chirp') {
121 return {
122 ...nextWaveform,
123 direction: waveform.direction === 'down' ? 'down' : 'up',
124 chirp_bandwidth: asNumberOrDefault(
125 waveform.chirp_bandwidth,
126 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_bandwidth
127 ),
128 chirp_duration: asNumberOrDefault(
129 waveform.chirp_duration,
130 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_duration
131 ),
132 chirp_period: asNumberOrDefault(
133 waveform.chirp_period,
134 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_period
135 ),
136 start_frequency_offset: asNullableNumberOrDefault(
137 waveform.start_frequency_offset,
138 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.start_frequency_offset
139 ),
140 chirp_count: asNullableInteger(waveform.chirp_count),
141 };
142 }
143
144 if (waveformType === 'fmcw_triangle') {
145 return {
146 ...nextWaveform,
147 chirp_bandwidth: asNumberOrDefault(
148 waveform.chirp_bandwidth,
149 DEFAULT_FMCW_TRIANGLE_FIELDS.chirp_bandwidth
150 ),
151 chirp_duration: asNumberOrDefault(
152 waveform.chirp_duration,
153 DEFAULT_FMCW_TRIANGLE_FIELDS.chirp_duration
154 ),
155 start_frequency_offset: asNullableNumberOrDefault(
156 waveform.start_frequency_offset,
157 DEFAULT_FMCW_TRIANGLE_FIELDS.start_frequency_offset
158 ),
159 triangle_count: asNullableInteger(waveform.triangle_count),
160 };
161 }
162
163 if (waveformType === 'stepped_frequency') {
164 return {
165 ...nextWaveform,
166 start_frequency_offset: asNumberOrDefault(
167 waveform.start_frequency_offset,
168 DEFAULT_SFCW_FIELDS.start_frequency_offset
169 ),
170 step_size: asNumberOrDefault(
171 waveform.step_size,
172 DEFAULT_SFCW_FIELDS.step_size
173 ),
174 step_count:
175 asNullableInteger(waveform.step_count) ??
176 DEFAULT_SFCW_FIELDS.step_count,
177 dwell_time: asNumberOrDefault(
178 waveform.dwell_time,
179 DEFAULT_SFCW_FIELDS.dwell_time
180 ),
181 step_period: asNumberOrDefault(
182 waveform.step_period,
183 DEFAULT_SFCW_FIELDS.step_period
184 ),
185 sweep_count: asNullableInteger(waveform.sweep_count),
186 };
187 }
188
189 return nextWaveform;
190}
191
192export function getVisibleWaveformFieldLabels(
193 waveformType: WaveformType
194): string[] {
195 if (waveformType === 'pulsed_from_file') {
196 return ['Waveform File (.csv, .h5)'];
197 }
198 if (waveformType === 'cw_from_file' || waveformType === 'fmcw_from_file') {
199 return ['Waveform File (.h5)'];
200 }
201
202 if (waveformType === 'fmcw_linear_chirp') {
203 return [
204 'Direction',
205 'Chirp Bandwidth (Hz)',
206 'Chirp Duration (s)',
207 'Chirp Period (s)',
208 'Start Frequency Offset (Hz)',
209 'Chirp Count',
210 ];
211 }
212
213 if (waveformType === 'fmcw_triangle') {
214 return [
215 'Chirp Bandwidth (Hz)',
216 'Chirp Duration (s)',
217 'Start Frequency Offset (Hz)',
218 'Triangle Count',
219 ];
220 }
221
222 if (waveformType === 'stepped_frequency') {
223 return [
224 'Start Frequency Offset (Hz)',
225 'Step Size (Hz)',
226 'Step Count',
227 'Dwell Time (s)',
228 'Step Period (s)',
229 'Sweep Count',
230 ];
231 }
232
233 return [];
234}
235
236export function WaveformInspector({ item }: WaveformInspectorProps) {
237 const { updateItem, globalParameters } = useScenarioStore.getState();
238 const waveform = item as AuthorableWaveform;
239 const fmcwIssues = validateFmcwWaveform(item, globalParameters);
240 const getFieldIssue = (field: string) =>
241 fmcwIssues.find((issue) => issue.field === field);
242 const globalFmcwIssues = fmcwIssues.filter((issue) => !issue.field);
243 const sfcwStepPeriodIssue =
244 waveform.waveformType === 'stepped_frequency' &&
245 asNumberOrDefault(
246 waveform.step_period,
247 DEFAULT_SFCW_FIELDS.step_period
248 ) <
249 asNumberOrDefault(
250 waveform.dwell_time,
251 DEFAULT_SFCW_FIELDS.dwell_time
252 )
253 ? 'Step period must be greater than or equal to dwell time.'
254 : undefined;
255 const handleChange = (path: string, value: unknown) =>
256 updateItem(item.id, path, value);
257 const handleTypeChange = (waveformType: WaveformType) => {
258 const nextWaveform = createWaveformForType(waveform, waveformType);
259 const currentValues = waveform as Record<string, unknown>;
260 const nextEntries = Object.entries(nextWaveform).filter(
261 ([key]) => key !== 'id' && key !== 'type'
262 );
263
264 for (const [key, value] of nextEntries.filter(
265 ([key]) => key !== 'waveformType'
266 )) {
267 if (!Object.is(currentValues[key], value)) {
268 handleChange(key, value);
269 }
270 }
271
272 if (waveform.waveformType !== waveformType) {
273 handleChange('waveformType', waveformType);
274 }
275 };
276
277 return (
278 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
279 <BufferedTextField
280 label="Name"
281 variant="outlined"
282 size="small"
283 fullWidth
284 value={item.name}
285 allowEmpty={false}
286 onChange={(v) => handleChange('name', v)}
287 />
288 <FormControl fullWidth size="small">
289 <InputLabel>Type</InputLabel>
290 <Select
291 label="Type"
292 value={waveform.waveformType}
293 onChange={(e) =>
294 handleTypeChange(e.target.value as WaveformType)
295 }
296 >
297 {WAVEFORM_TYPE_OPTIONS.map((option) => (
298 <MenuItem key={option.value} value={option.value}>
299 {option.label}
300 </MenuItem>
301 ))}
302 </Select>
303 </FormControl>
304 <NumberField
305 label="Power (W)"
306 value={item.power}
307 emptyBehavior="revert"
308 onChange={(v) => handleChange('power', v)}
309 />
310 <NumberField
311 label="Carrier Frequency (Hz)"
312 value={item.carrier_frequency}
313 emptyBehavior="revert"
314 onChange={(v) => handleChange('carrier_frequency', v)}
315 />
316 {globalFmcwIssues.map((issue) => (
317 <Alert
318 key={issue.message}
319 severity={issue.severity}
320 variant="outlined"
321 >
322 {issue.message}
323 </Alert>
324 ))}
325 {(waveform.waveformType === 'pulsed_from_file' ||
326 waveform.waveformType === 'cw_from_file' ||
327 waveform.waveformType === 'fmcw_from_file') && (
328 <FileInput
329 label={
330 waveform.waveformType === 'pulsed_from_file'
331 ? 'Waveform File (.csv, .h5)'
332 : 'Waveform File (.h5)'
333 }
334 value={waveform.filename}
335 onChange={(v) => handleChange('filename', v)}
336 filters={[
337 {
338 name: 'Waveform',
339 extensions:
340 waveform.waveformType === 'pulsed_from_file'
341 ? ['csv', 'h5']
342 : ['h5'],
343 },
344 { name: 'All Files', extensions: ['*'] },
345 ]}
346 />
347 )}
348 {waveform.waveformType === 'fmcw_linear_chirp' && (
349 <>
350 <FormControl fullWidth size="small">
351 <InputLabel>Direction</InputLabel>
352 <Select
353 label="Direction"
354 value={waveform.direction ?? 'up'}
355 onChange={(e) =>
356 handleChange('direction', e.target.value)
357 }
358 >
359 <MenuItem value="up">Up</MenuItem>
360 <MenuItem value="down">Down</MenuItem>
361 </Select>
362 </FormControl>
363 <NumberField
364 label="Chirp Bandwidth (Hz)"
365 value={asNumberOrDefault(
366 waveform.chirp_bandwidth,
367 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_bandwidth
368 )}
369 emptyBehavior="revert"
370 onChange={(v) => handleChange('chirp_bandwidth', v)}
371 />
372 <NumberField
373 label="Chirp Duration (s)"
374 value={asNumberOrDefault(
375 waveform.chirp_duration,
376 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_duration
377 )}
378 emptyBehavior="revert"
379 onChange={(v) => handleChange('chirp_duration', v)}
380 />
381 <NumberField
382 label="Chirp Period (s)"
383 value={asNumberOrDefault(
384 waveform.chirp_period,
385 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_period
386 )}
387 emptyBehavior="revert"
388 helperText={getFieldIssue('chirp_period')?.message}
389 externalError={
390 getFieldIssue('chirp_period')?.severity === 'error'
391 }
392 onChange={(v) => handleChange('chirp_period', v)}
393 />
394 <NumberField
395 label="Start Frequency Offset (Hz)"
396 value={asNullableNumber(
397 waveform.start_frequency_offset
398 )}
399 emptyBehavior="null"
400 onChange={(v) =>
401 handleChange('start_frequency_offset', v)
402 }
403 />
404 <NumberField
405 label="Chirp Count"
406 value={asNullableInteger(waveform.chirp_count)}
407 emptyBehavior="null"
408 onChange={(v) =>
409 handleChange(
410 'chirp_count',
411 v === null ? null : Math.trunc(v)
412 )
413 }
414 />
415 </>
416 )}
417 {waveform.waveformType === 'fmcw_triangle' && (
418 <>
419 <NumberField
420 label="Chirp Bandwidth (Hz)"
421 value={asNumberOrDefault(
422 waveform.chirp_bandwidth,
423 DEFAULT_FMCW_TRIANGLE_FIELDS.chirp_bandwidth
424 )}
425 emptyBehavior="revert"
426 onChange={(v) => handleChange('chirp_bandwidth', v)}
427 />
428 <NumberField
429 label="Chirp Duration (s)"
430 value={asNumberOrDefault(
431 waveform.chirp_duration,
432 DEFAULT_FMCW_TRIANGLE_FIELDS.chirp_duration
433 )}
434 emptyBehavior="revert"
435 onChange={(v) => handleChange('chirp_duration', v)}
436 />
437 <NumberField
438 label="Start Frequency Offset (Hz)"
439 value={asNullableNumber(
440 waveform.start_frequency_offset
441 )}
442 emptyBehavior="null"
443 onChange={(v) =>
444 handleChange('start_frequency_offset', v)
445 }
446 />
447 <NumberField
448 label="Triangle Count"
449 value={asNullableInteger(waveform.triangle_count)}
450 emptyBehavior="null"
451 onChange={(v) =>
452 handleChange(
453 'triangle_count',
454 v === null ? null : Math.trunc(v)
455 )
456 }
457 />
458 </>
459 )}
460 {waveform.waveformType === 'stepped_frequency' && (
461 <>
462 <NumberField
463 label="Start Frequency Offset (Hz)"
464 value={asNumberOrDefault(
465 waveform.start_frequency_offset,
466 DEFAULT_SFCW_FIELDS.start_frequency_offset
467 )}
468 emptyBehavior="revert"
469 onChange={(v) =>
470 handleChange('start_frequency_offset', v)
471 }
472 />
473 <NumberField
474 label="Step Size (Hz)"
475 value={asNumberOrDefault(
476 waveform.step_size,
477 DEFAULT_SFCW_FIELDS.step_size
478 )}
479 emptyBehavior="revert"
480 onChange={(v) => handleChange('step_size', v)}
481 />
482 <NumberField
483 label="Step Count"
484 value={
485 asNullableInteger(waveform.step_count) ??
486 DEFAULT_SFCW_FIELDS.step_count
487 }
488 emptyBehavior="revert"
489 onChange={(v) =>
490 handleChange('step_count', Math.trunc(v ?? 1))
491 }
492 />
493 <NumberField
494 label="Dwell Time (s)"
495 value={asNumberOrDefault(
496 waveform.dwell_time,
497 DEFAULT_SFCW_FIELDS.dwell_time
498 )}
499 emptyBehavior="revert"
500 onChange={(v) => handleChange('dwell_time', v)}
501 />
502 <NumberField
503 label="Step Period (s)"
504 value={asNumberOrDefault(
505 waveform.step_period,
506 DEFAULT_SFCW_FIELDS.step_period
507 )}
508 emptyBehavior="revert"
509 helperText={sfcwStepPeriodIssue}
510 externalError={Boolean(sfcwStepPeriodIssue)}
511 onChange={(v) => handleChange('step_period', v)}
512 />
513 <NumberField
514 label="Sweep Count"
515 value={asNullableInteger(waveform.sweep_count)}
516 emptyBehavior="null"
517 onChange={(v) =>
518 handleChange(
519 'sweep_count',
520 v === null ? null : Math.trunc(v)
521 )
522 }
523 />
524 </>
525 )}
526 </Box>
527 );
528}