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 =
23type FmcwLinearChirpFields = {
24 direction: 'up' | 'down';
25 chirp_bandwidth: number;
26 chirp_duration: number;
28 start_frequency_offset: number | null;
29 chirp_count: number | null;
32type FmcwTriangleFields = {
33 chirp_bandwidth: number;
34 chirp_duration: number;
35 start_frequency_offset: number | null;
36 triangle_count: number | null;
39type AuthorableWaveform = Omit<Waveform, 'waveformType'> &
40 Partial<FmcwLinearChirpFields> &
41 Partial<FmcwTriangleFields> & {
42 waveformType: WaveformType;
46export const WAVEFORM_TYPE_OPTIONS: ReadonlyArray<{
50 { value: 'pulsed_from_file', label: 'Pulse File' },
51 { value: 'cw', label: 'CW' },
52 { value: 'fmcw_linear_chirp', label: 'FMCW Linear Chirp' },
53 { value: 'fmcw_triangle', label: 'FMCW Triangle' },
56const DEFAULT_FMCW_LINEAR_CHIRP_FIELDS =
57 createWaveformDefaultsForType('fmcw_linear_chirp');
58const DEFAULT_FMCW_TRIANGLE_FIELDS =
59 createWaveformDefaultsForType('fmcw_triangle');
61interface WaveformInspectorProps {
65const asNumberOrDefault = (value: unknown, fallback: number): number =>
66 typeof value === 'number' && Number.isFinite(value) ? value : fallback;
68const asNullableNumber = (value: unknown): number | null =>
69 typeof value === 'number' && Number.isFinite(value) ? value : null;
71const asNullableNumberOrDefault = (
73 fallback: number | null
75 typeof value === 'number' && Number.isFinite(value) ? value : fallback;
77const asNullableInteger = (value: unknown): number | null =>
78 typeof value === 'number' && Number.isFinite(value)
82export function createWaveformForType(
83 waveform: AuthorableWaveform,
84 waveformType: WaveformType
85): AuthorableWaveform {
86 const nextWaveform = {
91 if (waveformType === 'pulsed_from_file') {
95 typeof waveform.filename === 'string' ? waveform.filename : '',
99 if (waveformType === 'fmcw_linear_chirp') {
102 direction: waveform.direction === 'down' ? 'down' : 'up',
103 chirp_bandwidth: asNumberOrDefault(
104 waveform.chirp_bandwidth,
105 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_bandwidth
107 chirp_duration: asNumberOrDefault(
108 waveform.chirp_duration,
109 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_duration
111 chirp_period: asNumberOrDefault(
112 waveform.chirp_period,
113 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_period
115 start_frequency_offset: asNullableNumberOrDefault(
116 waveform.start_frequency_offset,
117 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.start_frequency_offset
119 chirp_count: asNullableInteger(waveform.chirp_count),
123 if (waveformType === 'fmcw_triangle') {
126 chirp_bandwidth: asNumberOrDefault(
127 waveform.chirp_bandwidth,
128 DEFAULT_FMCW_TRIANGLE_FIELDS.chirp_bandwidth
130 chirp_duration: asNumberOrDefault(
131 waveform.chirp_duration,
132 DEFAULT_FMCW_TRIANGLE_FIELDS.chirp_duration
134 start_frequency_offset: asNullableNumberOrDefault(
135 waveform.start_frequency_offset,
136 DEFAULT_FMCW_TRIANGLE_FIELDS.start_frequency_offset
138 triangle_count: asNullableInteger(waveform.triangle_count),
145export function getVisibleWaveformFieldLabels(
146 waveformType: WaveformType
148 if (waveformType === 'pulsed_from_file') {
149 return ['Waveform File (.csv, .h5)'];
152 if (waveformType === 'fmcw_linear_chirp') {
155 'Chirp Bandwidth (Hz)',
156 'Chirp Duration (s)',
158 'Start Frequency Offset (Hz)',
163 if (waveformType === 'fmcw_triangle') {
165 'Chirp Bandwidth (Hz)',
166 'Chirp Duration (s)',
167 'Start Frequency Offset (Hz)',
175export function WaveformInspector({ item }: WaveformInspectorProps) {
176 const { updateItem, globalParameters } = useScenarioStore.getState();
177 const waveform = item as AuthorableWaveform;
178 const fmcwIssues = validateFmcwWaveform(item, globalParameters);
179 const getFieldIssue = (field: string) =>
180 fmcwIssues.find((issue) => issue.field === field);
181 const globalFmcwIssues = fmcwIssues.filter((issue) => !issue.field);
182 const handleChange = (path: string, value: unknown) =>
183 updateItem(item.id, path, value);
184 const handleTypeChange = (waveformType: WaveformType) => {
185 const nextWaveform = createWaveformForType(waveform, waveformType);
186 const currentValues = waveform as Record<string, unknown>;
187 const nextEntries = Object.entries(nextWaveform).filter(
188 ([key]) => key !== 'id' && key !== 'type'
191 for (const [key, value] of nextEntries.filter(
192 ([key]) => key !== 'waveformType'
194 if (!Object.is(currentValues[key], value)) {
195 handleChange(key, value);
199 if (waveform.waveformType !== waveformType) {
200 handleChange('waveformType', waveformType);
205 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
213 onChange={(v) => handleChange('name', v)}
215 <FormControl fullWidth size="small">
216 <InputLabel>Type</InputLabel>
219 value={waveform.waveformType}
221 handleTypeChange(e.target.value as WaveformType)
224 {WAVEFORM_TYPE_OPTIONS.map((option) => (
225 <MenuItem key={option.value} value={option.value}>
234 emptyBehavior="revert"
235 onChange={(v) => handleChange('power', v)}
238 label="Carrier Frequency (Hz)"
239 value={item.carrier_frequency}
240 emptyBehavior="revert"
241 onChange={(v) => handleChange('carrier_frequency', v)}
243 {globalFmcwIssues.map((issue) => (
246 severity={issue.severity}
252 {waveform.waveformType === 'pulsed_from_file' && (
254 label="Waveform File (.csv, .h5)"
255 value={waveform.filename}
256 onChange={(v) => handleChange('filename', v)}
258 { name: 'Waveform', extensions: ['csv', 'h5'] },
259 { name: 'All Files', extensions: ['*'] },
263 {waveform.waveformType === 'fmcw_linear_chirp' && (
265 <FormControl fullWidth size="small">
266 <InputLabel>Direction</InputLabel>
269 value={waveform.direction ?? 'up'}
271 handleChange('direction', e.target.value)
274 <MenuItem value="up">Up</MenuItem>
275 <MenuItem value="down">Down</MenuItem>
279 label="Chirp Bandwidth (Hz)"
280 value={asNumberOrDefault(
281 waveform.chirp_bandwidth,
282 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_bandwidth
284 emptyBehavior="revert"
285 onChange={(v) => handleChange('chirp_bandwidth', v)}
288 label="Chirp Duration (s)"
289 value={asNumberOrDefault(
290 waveform.chirp_duration,
291 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_duration
293 emptyBehavior="revert"
294 onChange={(v) => handleChange('chirp_duration', v)}
297 label="Chirp Period (s)"
298 value={asNumberOrDefault(
299 waveform.chirp_period,
300 DEFAULT_FMCW_LINEAR_CHIRP_FIELDS.chirp_period
302 emptyBehavior="revert"
303 helperText={getFieldIssue('chirp_period')?.message}
305 getFieldIssue('chirp_period')?.severity === 'error'
307 onChange={(v) => handleChange('chirp_period', v)}
310 label="Start Frequency Offset (Hz)"
311 value={asNullableNumber(
312 waveform.start_frequency_offset
316 handleChange('start_frequency_offset', v)
321 value={asNullableInteger(waveform.chirp_count)}
326 v === null ? null : Math.trunc(v)
332 {waveform.waveformType === 'fmcw_triangle' && (
335 label="Chirp Bandwidth (Hz)"
336 value={asNumberOrDefault(
337 waveform.chirp_bandwidth,
338 DEFAULT_FMCW_TRIANGLE_FIELDS.chirp_bandwidth
340 emptyBehavior="revert"
341 onChange={(v) => handleChange('chirp_bandwidth', v)}
344 label="Chirp Duration (s)"
345 value={asNumberOrDefault(
346 waveform.chirp_duration,
347 DEFAULT_FMCW_TRIANGLE_FIELDS.chirp_duration
349 emptyBehavior="revert"
350 onChange={(v) => handleChange('chirp_duration', v)}
353 label="Start Frequency Offset (Hz)"
354 value={asNullableNumber(
355 waveform.start_frequency_offset
359 handleChange('start_frequency_offset', v)
363 label="Triangle Count"
364 value={asNullableInteger(waveform.triangle_count)}
369 v === null ? null : Math.trunc(v)