1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
4import DeleteIcon from '@mui/icons-material/Delete';
17} from '@mui/material';
28} from '@/stores/scenarioStore';
30 createDechirpReference,
33 DECHIRP_REFERENCE_SOURCE_OPTIONS,
35 DechirpReferenceSource,
37 getDechirpReferenceSource,
39 ReceiverFmcwModeConfig,
40} from '@/stores/scenarioStore/fmcwModeConfig';
41import { validateFmcwScenario } from '@/stores/scenarioStore/fmcwValidation';
47} from './InspectorControls';
49export type RadarType = 'pulsed' | 'cw' | 'fmcw' | 'sfcw';
50type CompatibleWaveform = {
57 | 'if_filter_bandwidth'
58 | 'if_filter_transition_width';
60export const RADAR_MODE_OPTIONS: ReadonlyArray<{
64 { value: 'pulsed', label: 'Pulsed' },
65 { value: 'cw', label: 'CW' },
66 { value: 'fmcw', label: 'FMCW' },
67 { value: 'sfcw', label: 'SFCW' },
70const WAVEFORM_TYPE_BY_RADAR_TYPE: Record<RadarType, string[]> = {
71 pulsed: ['pulsed_from_file'],
72 cw: ['cw_from_file', 'cw'],
73 fmcw: ['fmcw_from_file', 'fmcw_linear_chirp', 'fmcw_triangle'],
74 sfcw: ['stepped_frequency'],
77export function isWaveformCompatibleWithRadarType(
78 waveform: CompatibleWaveform | undefined,
82 ? WAVEFORM_TYPE_BY_RADAR_TYPE[radarType].includes(waveform.waveformType)
86export function getCompatibleWaveforms(
87 waveforms: CompatibleWaveform[],
89): CompatibleWaveform[] {
90 return waveforms.filter((waveform) =>
91 isWaveformCompatibleWithRadarType(waveform, radarType)
95export function shouldClearWaveformForRadarType(
96 waveformId: string | null | undefined,
97 waveforms: CompatibleWaveform[],
104 return !isWaveformCompatibleWithRadarType(
105 waveforms.find((waveform) => waveform.id === waveformId),
110export function resolveWaveformSelectValue(
111 waveformId: string | null | undefined,
112 waveforms: CompatibleWaveform[],
115 return shouldClearWaveformForRadarType(waveformId, waveforms, radarType)
117 : (waveformId ?? '');
120export function getPulsedRadarFieldLabels(radarType: RadarType): string[] {
121 return radarType === 'pulsed'
122 ? ['PRF (Hz)', 'Window Skip (s)', 'Window Length (s)']
127 createDechirpReference,
128 createFmcwModeConfig,
129 DECHIRP_MODE_OPTIONS,
130 DECHIRP_REFERENCE_SOURCE_OPTIONS,
133const uniqueNames = (names: string[]): string[] =>
134 Array.from(new Set(names.filter((name) => name.trim().length > 0)));
136const includeCurrentName = (names: string[], currentName?: string): string[] =>
137 currentName && !names.includes(currentName)
138 ? [currentName, ...names]
141export function getFmcwWaveformNames(waveforms: Waveform[]): string[] {
144 .filter((waveform) => isFmcwWaveformType(waveform.waveformType))
145 .map((waveform) => waveform.name)
149export function getFmcwEmitterNames(
150 platforms: Platform[],
151 waveforms: Waveform[]
153 const waveformsById = new Map(
154 waveforms.map((waveform) => [waveform.id, waveform])
157 platforms.flatMap((platform) =>
158 platform.components.flatMap((component) => {
160 component.type !== 'transmitter' &&
161 component.type !== 'monostatic'
166 component.radarType !== 'fmcw' ||
167 !component.waveformId ||
169 waveformsById.get(component.waveformId)?.waveformType
174 return [component.name];
180export function getAvailableDechirpReferenceSourceOptions(
181 componentType: MonostaticComponent['type'] | ReceiverComponent['type'],
182 currentSource?: DechirpReferenceSource
184 return DECHIRP_REFERENCE_SOURCE_OPTIONS.filter(
186 option.value !== 'attached' ||
187 componentType === 'monostatic' ||
188 currentSource === 'attached'
192interface PlatformComponentInspectorProps {
193 component: PlatformComponent;
198export function PlatformComponentInspector({
202}: PlatformComponentInspectorProps) {
211 } = useScenarioStore.getState();
212 const fmcwIssues = validateFmcwScenario({
217 const fmcwEmitterNames = getFmcwEmitterNames(platforms, waveforms);
218 const fmcwWaveformNames = getFmcwWaveformNames(waveforms);
220 // Updates are targeted using the array index in the path string
221 const handleChange = (path: string, value: unknown) =>
222 updateItem(platformId, `components.${index}.${path}`, value);
223 const handleComponentChange = (value: PlatformComponent) =>
224 updateItem(platformId, `components.${index}`, value);
226 const renderSchedule = (
227 c: MonostaticComponent | TransmitterComponent | ReceiverComponent
229 const schedule = c.schedule || [];
230 const scheduleIssues = fmcwIssues.filter(
231 (issue) => issue.componentId === c.id && issue.field === 'schedule'
234 const handleAddPeriod = () => {
235 handleChange('schedule', [...schedule, { start: 0, end: 0 }]);
238 const handleRemovePeriod = (idx: number) => {
239 const newSchedule = [...schedule];
240 newSchedule.splice(idx, 1);
241 handleChange('schedule', newSchedule);
244 const handlePeriodChange = (
246 field: keyof SchedulePeriod,
249 const newSchedule = [...schedule];
250 newSchedule[idx] = { ...newSchedule[idx], [field]: val ?? 0 };
251 handleChange('schedule', newSchedule);
255 <Section title="Operating Schedule">
256 {scheduleIssues.map((issue) => (
259 severity={issue.severity}
265 {schedule.length === 0 && (
266 <Typography variant="body2" color="text.secondary">
267 No specific schedule defined (always active).
270 {schedule.map((period, i) => (
275 alignItems: 'center',
279 borderColor: 'divider',
286 emptyBehavior="revert"
287 onChange={(v) => handlePeriodChange(i, 'start', v)}
292 emptyBehavior="revert"
293 onChange={(v) => handlePeriodChange(i, 'end', v)}
297 onClick={() => handleRemovePeriod(i)}
300 <DeleteIcon fontSize="small" />
305 onClick={handleAddPeriod}
316 const renderCommonRadarFields = (
317 c: MonostaticComponent | TransmitterComponent | ReceiverComponent
319 const radarType = c.radarType as RadarType;
320 const compatibleWaveforms = getCompatibleWaveforms(
324 const handleRadarTypeChange = (nextRadarType: RadarType) => {
325 const prepareModeChange = <
327 | MonostaticComponent
328 | TransmitterComponent
332 waveformId?: string | null
334 const nextComponent = {
336 radarType: nextRadarType,
337 ...(waveformId !== undefined ? { waveformId } : {}),
341 nextComponent.type === 'receiver' ||
342 nextComponent.type === 'monostatic'
344 if (nextRadarType === 'fmcw') {
347 fmcwModeConfig: nextComponent.fmcwModeConfig ?? {},
352 fmcwModeConfig: _fmcwModeConfig,
355 return withoutFmcwMode as T;
358 return nextComponent;
361 if ('waveformId' in c) {
362 const waveformId = shouldClearWaveformForRadarType(
370 handleComponentChange(prepareModeChange(c, waveformId));
374 handleComponentChange(prepareModeChange(c));
380 label="Component Name"
385 onChange={(v) => handleChange('name', v)}
388 <FormControl fullWidth size="small" sx={{ mb: 2 }}>
389 <InputLabel>Radar Mode</InputLabel>
394 handleRadarTypeChange(e.target.value as RadarType)
397 {RADAR_MODE_OPTIONS.map((option) => (
398 <MenuItem key={option.value} value={option.value}>
405 {'waveformId' in c && (
406 <FormControl fullWidth size="small" sx={{ mb: 2 }}>
407 <InputLabel>Waveform</InputLabel>
410 value={resolveWaveformSelectValue(
418 e.target.value === ''
427 {compatibleWaveforms.map((w) => (
428 <MenuItem key={w.id} value={w.id}>
436 <FormControl fullWidth size="small" sx={{ mb: 2 }}>
437 <InputLabel>Antenna</InputLabel>
440 value={c.antennaId ?? ''}
444 e.target.value === '' ? null : e.target.value
451 {antennas.map((a) => (
452 <MenuItem key={a.id} value={a.id}>
458 <FormControl fullWidth size="small" sx={{ mb: 2 }}>
459 <InputLabel>Timing Source</InputLabel>
461 label="Timing Source"
462 value={c.timingId ?? ''}
466 e.target.value === '' ? null : e.target.value
473 {timings.map((t) => (
474 <MenuItem key={t.id} value={t.id}>
484 const renderFmcwReceiverFields = (
485 c: MonostaticComponent | ReceiverComponent
487 const config = c.fmcwModeConfig ?? {};
488 const dechirpMode = getDechirpMode(config);
489 const reference = config.dechirp_reference;
490 const referenceSource = getDechirpReferenceSource(reference);
491 const transmitterName =
492 reference?.source === 'transmitter'
493 ? reference.transmitter_name
496 reference?.source === 'custom'
497 ? reference.waveform_name
499 const referenceIssues = fmcwIssues.filter(
501 issue.componentId === c.id && issue.field === 'fmcwModeConfig'
503 const referenceSourceOptions =
504 getAvailableDechirpReferenceSourceOptions(c.type, referenceSource);
506 const createDefaultReference = () => {
507 if (c.type === 'monostatic') {
508 return createDechirpReference('attached');
510 const transmitterName = fmcwEmitterNames[0];
511 if (transmitterName) {
513 source: 'transmitter' as const,
514 transmitter_name: transmitterName,
517 const waveformName = fmcwWaveformNames[0];
520 source: 'custom' as const,
521 waveform_name: waveformName,
524 return createDechirpReference('transmitter');
527 const commitConfig = (nextConfig: ReceiverFmcwModeConfig) => {
528 handleChange('fmcwModeConfig', nextConfig);
531 const handleModeChange = (nextMode: DechirpMode) => {
532 const nextConfig = createFmcwModeConfig(nextMode, config);
534 nextMode !== 'none' &&
535 (!nextConfig.dechirp_reference ||
536 (c.type === 'receiver' &&
537 nextConfig.dechirp_reference.source === 'attached'))
539 nextConfig.dechirp_reference = createDefaultReference();
541 commitConfig(nextConfig);
544 const handleReferenceSourceChange = (
545 nextSource: DechirpReferenceSource
547 const nextReference = createDechirpReference(nextSource, reference);
549 nextReference.source === 'transmitter' &&
550 !nextReference.transmitter_name &&
553 nextReference.transmitter_name = fmcwEmitterNames[0];
556 nextReference.source === 'custom' &&
557 !nextReference.waveform_name &&
560 nextReference.waveform_name = fmcwWaveformNames[0];
564 dechirp_mode: dechirpMode,
565 dechirp_reference: nextReference,
569 const handleTransmitterReferenceChange = (name: string) => {
572 dechirp_mode: dechirpMode,
574 source: 'transmitter',
575 ...(name ? { transmitter_name: name } : {}),
580 const handleCustomWaveformReferenceChange = (name: string) => {
583 dechirp_mode: dechirpMode,
586 ...(name ? { waveform_name: name } : {}),
591 const handleIfChainNumberChange = (
595 const nextConfig: ReceiverFmcwModeConfig = {
597 dechirp_mode: dechirpMode,
599 if (value === null) {
600 delete nextConfig[key];
602 nextConfig[key] = value;
604 commitConfig(nextConfig);
608 <Section title="FMCW Receiver">
609 {referenceIssues.map((issue) => (
612 severity={issue.severity}
618 <FormControl fullWidth size="small">
619 <InputLabel>Dechirp Mode</InputLabel>
624 handleModeChange(e.target.value as DechirpMode)
627 {DECHIRP_MODE_OPTIONS.map((option) => (
628 <MenuItem key={option.value} value={option.value}>
635 {dechirpMode !== 'none' && (
637 <FormControl fullWidth size="small">
638 <InputLabel>Dechirp Reference</InputLabel>
640 label="Dechirp Reference"
641 value={referenceSource}
643 handleReferenceSourceChange(
644 e.target.value as DechirpReferenceSource
648 {referenceSourceOptions.map((option) => (
659 {referenceSource === 'transmitter' && (
660 <FormControl fullWidth size="small">
661 <InputLabel>Reference Transmitter</InputLabel>
663 label="Reference Transmitter"
664 value={transmitterName ?? ''}
666 handleTransmitterReferenceChange(
678 <MenuItem key={name} value={name}>
686 {referenceSource === 'custom' && (
687 <FormControl fullWidth size="small">
688 <InputLabel>Reference Waveform</InputLabel>
690 label="Reference Waveform"
691 value={waveformName ?? ''}
693 handleCustomWaveformReferenceChange(
705 <MenuItem key={name} value={name}>
714 label="IF Sample Rate (Hz)"
715 value={config.if_sample_rate ?? null}
718 handleIfChainNumberChange(
725 label="IF Filter Bandwidth (Hz)"
726 value={config.if_filter_bandwidth ?? null}
729 handleIfChainNumberChange(
730 'if_filter_bandwidth',
736 label="IF Transition Width (Hz)"
737 value={config.if_filter_transition_width ?? null}
740 handleIfChainNumberChange(
741 'if_filter_transition_width',
752 const renderReceiverFields = (
753 c: MonostaticComponent | ReceiverComponent
756 {(c.radarType as RadarType) === 'pulsed' && (
759 label="Window Skip (s)"
760 value={c.window_skip}
761 emptyBehavior="revert"
762 onChange={(v) => handleChange('window_skip', v)}
765 label="Window Length (s)"
766 value={c.window_length}
767 emptyBehavior="revert"
768 onChange={(v) => handleChange('window_length', v)}
773 label="Noise Temperature (K)"
774 value={c.noiseTemperature}
775 emptyBehavior="revert"
776 onChange={(v) => handleChange('noiseTemperature', v)}
781 checked={c.noDirectPaths}
783 handleChange('noDirectPaths', e.target.checked)
787 label="Ignore Direct Paths"
792 checked={c.noPropagationLoss}
794 handleChange('noPropagationLoss', e.target.checked)
798 label="Ignore Propagation Loss"
800 {(c.radarType as RadarType) === 'fmcw' &&
801 renderFmcwReceiverFields(c)}
805 switch (component.type) {
808 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
809 {renderCommonRadarFields(component)}
810 {(component.radarType as RadarType) === 'pulsed' && (
813 value={component.prf}
814 emptyBehavior="revert"
815 onChange={(v) => handleChange('prf', v)}
818 {renderReceiverFields(component)}
819 {renderSchedule(component)}
824 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
825 {renderCommonRadarFields(component)}
826 {(component.radarType as RadarType) === 'pulsed' && (
829 value={component.prf}
830 emptyBehavior="revert"
831 onChange={(v) => handleChange('prf', v)}
834 {renderSchedule(component)}
839 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
840 {renderCommonRadarFields(component)}
841 {(component.radarType as RadarType) === 'pulsed' && (
844 value={component.prf}
845 emptyBehavior="revert"
846 onChange={(v) => handleChange('prf', v)}
849 {renderReceiverFields(component)}
850 {renderSchedule(component)}
855 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
857 label="Component Name"
860 value={component.name}
862 onChange={(v) => handleChange('name', v)}
864 <FormControl fullWidth size="small">
865 <InputLabel>RCS Type</InputLabel>
868 value={component.rcs_type}
870 handleChange('rcs_type', e.target.value)
873 <MenuItem value="isotropic">Isotropic</MenuItem>
874 <MenuItem value="file">File</MenuItem>
877 {component.rcs_type === 'isotropic' && (
879 label="RCS Value (m^2)"
880 value={component.rcs_value ?? 0}
881 emptyBehavior="revert"
882 onChange={(v) => handleChange('rcs_value', v)}
885 {component.rcs_type === 'file' && (
888 value={component.rcs_filename}
889 onChange={(v) => handleChange('rcs_filename', v)}
892 name: 'Target RCS XML',
899 <FormControl fullWidth size="small">
900 <InputLabel>RCS Model</InputLabel>
903 value={component.rcs_model}
909 .value as TargetComponent['rcs_model']
913 <MenuItem value="constant">Constant</MenuItem>
914 <MenuItem value="chisquare">Chi-Square</MenuItem>
915 <MenuItem value="gamma">Gamma</MenuItem>
918 {(component.rcs_model === 'chisquare' ||
919 component.rcs_model === 'gamma') && (
922 value={component.rcs_k ?? 0}
923 emptyBehavior="revert"
924 onChange={(v) => handleChange('rcs_k', v)}
931 <Typography color="text.secondary">
932 Unknown component type.