1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
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';
17export type WaveformType =
24 | 'stepped_frequency';
26type FmcwLinearChirpFields = {
27 direction: 'up' | 'down';
28 chirp_bandwidth: number;
29 chirp_duration: number;
31 start_frequency_offset: number | null;
32 chirp_count: number | null;
35type FmcwTriangleFields = {
36 chirp_bandwidth: number;
37 chirp_duration: number;
38 start_frequency_offset: number | null;
39 triangle_count: number | null;
43 start_frequency_offset: number | null;
48 sweep_count: number | null;
51type AuthorableWaveform = Omit<Waveform, 'waveformType'> &
52 Partial<FmcwLinearChirpFields> &
53 Partial<FmcwTriangleFields> &
54 Partial<SfcwFields> & {
55 waveformType: WaveformType;
59export const WAVEFORM_TYPE_OPTIONS: ReadonlyArray<{
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' },
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');
78interface WaveformInspectorProps {
82const asNumberOrDefault = (value: unknown, fallback: number): number =>
83 typeof value === 'number' && Number.isFinite(value) ? value : fallback;
85const asNullableNumber = (value: unknown): number | null =>
86 typeof value === 'number' && Number.isFinite(value) ? value : null;
88const asNullableNumberOrDefault = (
90 fallback: number | null
92 typeof value === 'number' && Number.isFinite(value) ? value : fallback;
94const asNullableInteger = (value: unknown): number | null =>
95 typeof value === 'number' && Number.isFinite(value)
99export function createWaveformForType(
100 waveform: AuthorableWaveform,
101 waveformType: WaveformType
102): AuthorableWaveform {
103 const nextWaveform = {
109 waveformType === 'pulsed_from_file' ||
110 waveformType === 'cw_from_file' ||
111 waveformType === 'fmcw_from_file'
116 typeof waveform.filename === 'string' ? waveform.filename : '',
120 if (waveformType === 'fmcw_linear_chirp') {
123 direction: waveform.direction === 'down' ? 'down' : 'up',
124 chirp_bandwidth: asNumberOrDefault(
125 waveform.chirp_bandwidth,
126 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_bandwidth
128 chirp_duration: asNumberOrDefault(
129 waveform.chirp_duration,
130 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_duration
132 chirp_period: asNumberOrDefault(
133 waveform.chirp_period,
134 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_period
136 start_frequency_offset: asNullableNumberOrDefault(
137 waveform.start_frequency_offset,
138 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.start_frequency_offset
140 chirp_count: asNullableInteger(waveform.chirp_count),
144 if (waveformType === 'fmcw_triangle') {
147 chirp_bandwidth: asNumberOrDefault(
148 waveform.chirp_bandwidth,
149 DEFAULT_FMCW_TRIANGLE_FIELDS.chirp_bandwidth
151 chirp_duration: asNumberOrDefault(
152 waveform.chirp_duration,
153 DEFAULT_FMCW_TRIANGLE_FIELDS.chirp_duration
155 start_frequency_offset: asNullableNumberOrDefault(
156 waveform.start_frequency_offset,
157 DEFAULT_FMCW_TRIANGLE_FIELDS.start_frequency_offset
159 triangle_count: asNullableInteger(waveform.triangle_count),
163 if (waveformType === 'stepped_frequency') {
166 start_frequency_offset: asNumberOrDefault(
167 waveform.start_frequency_offset,
168 DEFAULT_SFCW_FIELDS.start_frequency_offset
170 step_size: asNumberOrDefault(
172 DEFAULT_SFCW_FIELDS.step_size
175 asNullableInteger(waveform.step_count) ??
176 DEFAULT_SFCW_FIELDS.step_count,
177 dwell_time: asNumberOrDefault(
179 DEFAULT_SFCW_FIELDS.dwell_time
181 step_period: asNumberOrDefault(
182 waveform.step_period,
183 DEFAULT_SFCW_FIELDS.step_period
185 sweep_count: asNullableInteger(waveform.sweep_count),
192export function getVisibleWaveformFieldLabels(
193 waveformType: WaveformType
195 if (waveformType === 'pulsed_from_file') {
196 return ['Waveform File (.csv, .h5)'];
198 if (waveformType === 'cw_from_file' || waveformType === 'fmcw_from_file') {
199 return ['Waveform File (.h5)'];
202 if (waveformType === 'fmcw_linear_chirp') {
205 'Chirp Bandwidth (Hz)',
206 'Chirp Duration (s)',
208 'Start Frequency Offset (Hz)',
213 if (waveformType === 'fmcw_triangle') {
215 'Chirp Bandwidth (Hz)',
216 'Chirp Duration (s)',
217 'Start Frequency Offset (Hz)',
222 if (waveformType === 'stepped_frequency') {
224 'Start Frequency Offset (Hz)',
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' &&
246 waveform.step_period,
247 DEFAULT_SFCW_FIELDS.step_period
251 DEFAULT_SFCW_FIELDS.dwell_time
253 ? 'Step period must be greater than or equal to dwell time.'
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'
264 for (const [key, value] of nextEntries.filter(
265 ([key]) => key !== 'waveformType'
267 if (!Object.is(currentValues[key], value)) {
268 handleChange(key, value);
272 if (waveform.waveformType !== waveformType) {
273 handleChange('waveformType', waveformType);
278 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
286 onChange={(v) => handleChange('name', v)}
288 <FormControl fullWidth size="small">
289 <InputLabel>Type</InputLabel>
292 value={waveform.waveformType}
294 handleTypeChange(e.target.value as WaveformType)
297 {WAVEFORM_TYPE_OPTIONS.map((option) => (
298 <MenuItem key={option.value} value={option.value}>
307 emptyBehavior="revert"
308 onChange={(v) => handleChange('power', v)}
311 label="Carrier Frequency (Hz)"
312 value={item.carrier_frequency}
313 emptyBehavior="revert"
314 onChange={(v) => handleChange('carrier_frequency', v)}
316 {globalFmcwIssues.map((issue) => (
319 severity={issue.severity}
325 {(waveform.waveformType === 'pulsed_from_file' ||
326 waveform.waveformType === 'cw_from_file' ||
327 waveform.waveformType === 'fmcw_from_file') && (
330 waveform.waveformType === 'pulsed_from_file'
331 ? 'Waveform File (.csv, .h5)'
332 : 'Waveform File (.h5)'
334 value={waveform.filename}
335 onChange={(v) => handleChange('filename', v)}
340 waveform.waveformType === 'pulsed_from_file'
344 { name: 'All Files', extensions: ['*'] },
348 {waveform.waveformType === 'fmcw_linear_chirp' && (
350 <FormControl fullWidth size="small">
351 <InputLabel>Direction</InputLabel>
354 value={waveform.direction ?? 'up'}
356 handleChange('direction', e.target.value)
359 <MenuItem value="up">Up</MenuItem>
360 <MenuItem value="down">Down</MenuItem>
364 label="Chirp Bandwidth (Hz)"
365 value={asNumberOrDefault(
366 waveform.chirp_bandwidth,
367 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_bandwidth
369 emptyBehavior="revert"
370 onChange={(v) => handleChange('chirp_bandwidth', v)}
373 label="Chirp Duration (s)"
374 value={asNumberOrDefault(
375 waveform.chirp_duration,
376 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_duration
378 emptyBehavior="revert"
379 onChange={(v) => handleChange('chirp_duration', v)}
382 label="Chirp Period (s)"
383 value={asNumberOrDefault(
384 waveform.chirp_period,
385 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_period
387 emptyBehavior="revert"
388 helperText={getFieldIssue('chirp_period')?.message}
390 getFieldIssue('chirp_period')?.severity === 'error'
392 onChange={(v) => handleChange('chirp_period', v)}
395 label="Start Frequency Offset (Hz)"
396 value={asNullableNumber(
397 waveform.start_frequency_offset
401 handleChange('start_frequency_offset', v)
406 value={asNullableInteger(waveform.chirp_count)}
411 v === null ? null : Math.trunc(v)
417 {waveform.waveformType === 'fmcw_triangle' && (
420 label="Chirp Bandwidth (Hz)"
421 value={asNumberOrDefault(
422 waveform.chirp_bandwidth,
423 DEFAULT_FMCW_TRIANGLE_FIELDS.chirp_bandwidth
425 emptyBehavior="revert"
426 onChange={(v) => handleChange('chirp_bandwidth', v)}
429 label="Chirp Duration (s)"
430 value={asNumberOrDefault(
431 waveform.chirp_duration,
432 DEFAULT_FMCW_TRIANGLE_FIELDS.chirp_duration
434 emptyBehavior="revert"
435 onChange={(v) => handleChange('chirp_duration', v)}
438 label="Start Frequency Offset (Hz)"
439 value={asNullableNumber(
440 waveform.start_frequency_offset
444 handleChange('start_frequency_offset', v)
448 label="Triangle Count"
449 value={asNullableInteger(waveform.triangle_count)}
454 v === null ? null : Math.trunc(v)
460 {waveform.waveformType === 'stepped_frequency' && (
463 label="Start Frequency Offset (Hz)"
464 value={asNumberOrDefault(
465 waveform.start_frequency_offset,
466 DEFAULT_SFCW_FIELDS.start_frequency_offset
468 emptyBehavior="revert"
470 handleChange('start_frequency_offset', v)
474 label="Step Size (Hz)"
475 value={asNumberOrDefault(
477 DEFAULT_SFCW_FIELDS.step_size
479 emptyBehavior="revert"
480 onChange={(v) => handleChange('step_size', v)}
485 asNullableInteger(waveform.step_count) ??
486 DEFAULT_SFCW_FIELDS.step_count
488 emptyBehavior="revert"
490 handleChange('step_count', Math.trunc(v ?? 1))
494 label="Dwell Time (s)"
495 value={asNumberOrDefault(
497 DEFAULT_SFCW_FIELDS.dwell_time
499 emptyBehavior="revert"
500 onChange={(v) => handleChange('dwell_time', v)}
503 label="Step Period (s)"
504 value={asNumberOrDefault(
505 waveform.step_period,
506 DEFAULT_SFCW_FIELDS.step_period
508 emptyBehavior="revert"
509 helperText={sfcwStepPeriodIssue}
510 externalError={Boolean(sfcwStepPeriodIssue)}
511 onChange={(v) => handleChange('step_period', v)}
515 value={asNullableInteger(waveform.sweep_count)}
520 v === null ? null : Math.trunc(v)