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';
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' },
69const WAVEFORM_TYPE_BY_RADAR_TYPE: Record<RadarType, string[]> = {
70 pulsed: ['pulsed_from_file'],
72 fmcw: ['fmcw_linear_chirp', 'fmcw_triangle'],
75export function isWaveformCompatibleWithRadarType(
76 waveform: CompatibleWaveform | undefined,
80 ? WAVEFORM_TYPE_BY_RADAR_TYPE[radarType].includes(waveform.waveformType)
84export function getCompatibleWaveforms(
85 waveforms: CompatibleWaveform[],
87): CompatibleWaveform[] {
88 return waveforms.filter((waveform) =>
89 isWaveformCompatibleWithRadarType(waveform, radarType)
93export function shouldClearWaveformForRadarType(
94 waveformId: string | null | undefined,
95 waveforms: CompatibleWaveform[],
102 return !isWaveformCompatibleWithRadarType(
103 waveforms.find((waveform) => waveform.id === waveformId),
108export function resolveWaveformSelectValue(
109 waveformId: string | null | undefined,
110 waveforms: CompatibleWaveform[],
113 return shouldClearWaveformForRadarType(waveformId, waveforms, radarType)
115 : (waveformId ?? '');
118export function getPulsedRadarFieldLabels(radarType: RadarType): string[] {
119 return radarType === 'pulsed'
120 ? ['PRF (Hz)', 'Window Skip (s)', 'Window Length (s)']
125 createDechirpReference,
126 createFmcwModeConfig,
127 DECHIRP_MODE_OPTIONS,
128 DECHIRP_REFERENCE_SOURCE_OPTIONS,
131const uniqueNames = (names: string[]): string[] =>
132 Array.from(new Set(names.filter((name) => name.trim().length > 0)));
134const includeCurrentName = (names: string[], currentName?: string): string[] =>
135 currentName && !names.includes(currentName)
136 ? [currentName, ...names]
139export function getFmcwWaveformNames(waveforms: Waveform[]): string[] {
142 .filter((waveform) => isFmcwWaveformType(waveform.waveformType))
143 .map((waveform) => waveform.name)
147export function getFmcwEmitterNames(
148 platforms: Platform[],
149 waveforms: Waveform[]
151 const waveformsById = new Map(
152 waveforms.map((waveform) => [waveform.id, waveform])
155 platforms.flatMap((platform) =>
156 platform.components.flatMap((component) => {
158 component.type !== 'transmitter' &&
159 component.type !== 'monostatic'
164 component.radarType !== 'fmcw' ||
165 !component.waveformId ||
167 waveformsById.get(component.waveformId)?.waveformType
172 return [component.name];
178export function getAvailableDechirpReferenceSourceOptions(
179 componentType: MonostaticComponent['type'] | ReceiverComponent['type'],
180 currentSource?: DechirpReferenceSource
182 return DECHIRP_REFERENCE_SOURCE_OPTIONS.filter(
184 option.value !== 'attached' ||
185 componentType === 'monostatic' ||
186 currentSource === 'attached'
190interface PlatformComponentInspectorProps {
191 component: PlatformComponent;
196export function PlatformComponentInspector({
200}: PlatformComponentInspectorProps) {
209 } = useScenarioStore.getState();
210 const fmcwIssues = validateFmcwScenario({
215 const fmcwEmitterNames = getFmcwEmitterNames(platforms, waveforms);
216 const fmcwWaveformNames = getFmcwWaveformNames(waveforms);
218 // Updates are targeted using the array index in the path string
219 const handleChange = (path: string, value: unknown) =>
220 updateItem(platformId, `components.${index}.${path}`, value);
221 const handleComponentChange = (value: PlatformComponent) =>
222 updateItem(platformId, `components.${index}`, value);
224 const renderSchedule = (
225 c: MonostaticComponent | TransmitterComponent | ReceiverComponent
227 const schedule = c.schedule || [];
228 const scheduleIssues = fmcwIssues.filter(
229 (issue) => issue.componentId === c.id && issue.field === 'schedule'
232 const handleAddPeriod = () => {
233 handleChange('schedule', [...schedule, { start: 0, end: 0 }]);
236 const handleRemovePeriod = (idx: number) => {
237 const newSchedule = [...schedule];
238 newSchedule.splice(idx, 1);
239 handleChange('schedule', newSchedule);
242 const handlePeriodChange = (
244 field: keyof SchedulePeriod,
247 const newSchedule = [...schedule];
248 newSchedule[idx] = { ...newSchedule[idx], [field]: val ?? 0 };
249 handleChange('schedule', newSchedule);
253 <Section title="Operating Schedule">
254 {scheduleIssues.map((issue) => (
257 severity={issue.severity}
263 {schedule.length === 0 && (
264 <Typography variant="body2" color="text.secondary">
265 No specific schedule defined (always active).
268 {schedule.map((period, i) => (
273 alignItems: 'center',
277 borderColor: 'divider',
284 emptyBehavior="revert"
285 onChange={(v) => handlePeriodChange(i, 'start', v)}
290 emptyBehavior="revert"
291 onChange={(v) => handlePeriodChange(i, 'end', v)}
295 onClick={() => handleRemovePeriod(i)}
298 <DeleteIcon fontSize="small" />
303 onClick={handleAddPeriod}
314 const renderCommonRadarFields = (
315 c: MonostaticComponent | TransmitterComponent | ReceiverComponent
317 const radarType = c.radarType as RadarType;
318 const compatibleWaveforms = getCompatibleWaveforms(
322 const handleRadarTypeChange = (nextRadarType: RadarType) => {
323 const prepareModeChange = <
325 | MonostaticComponent
326 | TransmitterComponent
330 waveformId?: string | null
332 const nextComponent = {
334 radarType: nextRadarType,
335 ...(waveformId !== undefined ? { waveformId } : {}),
339 nextComponent.type === 'receiver' ||
340 nextComponent.type === 'monostatic'
342 if (nextRadarType === 'fmcw') {
345 fmcwModeConfig: nextComponent.fmcwModeConfig ?? {},
350 fmcwModeConfig: _fmcwModeConfig,
353 return withoutFmcwMode as T;
356 return nextComponent;
359 if ('waveformId' in c) {
360 const waveformId = shouldClearWaveformForRadarType(
368 handleComponentChange(prepareModeChange(c, waveformId));
372 handleComponentChange(prepareModeChange(c));
378 label="Component Name"
383 onChange={(v) => handleChange('name', v)}
386 <FormControl fullWidth size="small" sx={{ mb: 2 }}>
387 <InputLabel>Radar Mode</InputLabel>
392 handleRadarTypeChange(e.target.value as RadarType)
395 {RADAR_MODE_OPTIONS.map((option) => (
396 <MenuItem key={option.value} value={option.value}>
403 {'waveformId' in c && (
404 <FormControl fullWidth size="small" sx={{ mb: 2 }}>
405 <InputLabel>Waveform</InputLabel>
408 value={resolveWaveformSelectValue(
416 e.target.value === ''
425 {compatibleWaveforms.map((w) => (
426 <MenuItem key={w.id} value={w.id}>
434 <FormControl fullWidth size="small" sx={{ mb: 2 }}>
435 <InputLabel>Antenna</InputLabel>
438 value={c.antennaId ?? ''}
442 e.target.value === '' ? null : e.target.value
449 {antennas.map((a) => (
450 <MenuItem key={a.id} value={a.id}>
456 <FormControl fullWidth size="small" sx={{ mb: 2 }}>
457 <InputLabel>Timing Source</InputLabel>
459 label="Timing Source"
460 value={c.timingId ?? ''}
464 e.target.value === '' ? null : e.target.value
471 {timings.map((t) => (
472 <MenuItem key={t.id} value={t.id}>
482 const renderFmcwReceiverFields = (
483 c: MonostaticComponent | ReceiverComponent
485 const config = c.fmcwModeConfig ?? {};
486 const dechirpMode = getDechirpMode(config);
487 const reference = config.dechirp_reference;
488 const referenceSource = getDechirpReferenceSource(reference);
489 const transmitterName =
490 reference?.source === 'transmitter'
491 ? reference.transmitter_name
494 reference?.source === 'custom'
495 ? reference.waveform_name
497 const referenceIssues = fmcwIssues.filter(
499 issue.componentId === c.id && issue.field === 'fmcwModeConfig'
501 const referenceSourceOptions =
502 getAvailableDechirpReferenceSourceOptions(c.type, referenceSource);
504 const createDefaultReference = () => {
505 if (c.type === 'monostatic') {
506 return createDechirpReference('attached');
508 const transmitterName = fmcwEmitterNames[0];
509 if (transmitterName) {
511 source: 'transmitter' as const,
512 transmitter_name: transmitterName,
515 const waveformName = fmcwWaveformNames[0];
518 source: 'custom' as const,
519 waveform_name: waveformName,
522 return createDechirpReference('transmitter');
525 const commitConfig = (nextConfig: ReceiverFmcwModeConfig) => {
526 handleChange('fmcwModeConfig', nextConfig);
529 const handleModeChange = (nextMode: DechirpMode) => {
530 const nextConfig = createFmcwModeConfig(nextMode, config);
532 nextMode !== 'none' &&
533 (!nextConfig.dechirp_reference ||
534 (c.type === 'receiver' &&
535 nextConfig.dechirp_reference.source === 'attached'))
537 nextConfig.dechirp_reference = createDefaultReference();
539 commitConfig(nextConfig);
542 const handleReferenceSourceChange = (
543 nextSource: DechirpReferenceSource
545 const nextReference = createDechirpReference(nextSource, reference);
547 nextReference.source === 'transmitter' &&
548 !nextReference.transmitter_name &&
551 nextReference.transmitter_name = fmcwEmitterNames[0];
554 nextReference.source === 'custom' &&
555 !nextReference.waveform_name &&
558 nextReference.waveform_name = fmcwWaveformNames[0];
562 dechirp_mode: dechirpMode,
563 dechirp_reference: nextReference,
567 const handleTransmitterReferenceChange = (name: string) => {
570 dechirp_mode: dechirpMode,
572 source: 'transmitter',
573 ...(name ? { transmitter_name: name } : {}),
578 const handleCustomWaveformReferenceChange = (name: string) => {
581 dechirp_mode: dechirpMode,
584 ...(name ? { waveform_name: name } : {}),
589 const handleIfChainNumberChange = (
593 const nextConfig: ReceiverFmcwModeConfig = {
595 dechirp_mode: dechirpMode,
597 if (value === null) {
598 delete nextConfig[key];
600 nextConfig[key] = value;
602 commitConfig(nextConfig);
606 <Section title="FMCW Receiver">
607 {referenceIssues.map((issue) => (
610 severity={issue.severity}
616 <FormControl fullWidth size="small">
617 <InputLabel>Dechirp Mode</InputLabel>
622 handleModeChange(e.target.value as DechirpMode)
625 {DECHIRP_MODE_OPTIONS.map((option) => (
626 <MenuItem key={option.value} value={option.value}>
633 {dechirpMode !== 'none' && (
635 <FormControl fullWidth size="small">
636 <InputLabel>Dechirp Reference</InputLabel>
638 label="Dechirp Reference"
639 value={referenceSource}
641 handleReferenceSourceChange(
642 e.target.value as DechirpReferenceSource
646 {referenceSourceOptions.map((option) => (
657 {referenceSource === 'transmitter' && (
658 <FormControl fullWidth size="small">
659 <InputLabel>Reference Transmitter</InputLabel>
661 label="Reference Transmitter"
662 value={transmitterName ?? ''}
664 handleTransmitterReferenceChange(
676 <MenuItem key={name} value={name}>
684 {referenceSource === 'custom' && (
685 <FormControl fullWidth size="small">
686 <InputLabel>Reference Waveform</InputLabel>
688 label="Reference Waveform"
689 value={waveformName ?? ''}
691 handleCustomWaveformReferenceChange(
703 <MenuItem key={name} value={name}>
712 label="IF Sample Rate (Hz)"
713 value={config.if_sample_rate ?? null}
716 handleIfChainNumberChange(
723 label="IF Filter Bandwidth (Hz)"
724 value={config.if_filter_bandwidth ?? null}
727 handleIfChainNumberChange(
728 'if_filter_bandwidth',
734 label="IF Transition Width (Hz)"
735 value={config.if_filter_transition_width ?? null}
738 handleIfChainNumberChange(
739 'if_filter_transition_width',
750 const renderReceiverFields = (
751 c: MonostaticComponent | ReceiverComponent
754 {(c.radarType as RadarType) === 'pulsed' && (
757 label="Window Skip (s)"
758 value={c.window_skip}
759 emptyBehavior="revert"
760 onChange={(v) => handleChange('window_skip', v)}
763 label="Window Length (s)"
764 value={c.window_length}
765 emptyBehavior="revert"
766 onChange={(v) => handleChange('window_length', v)}
771 label="Noise Temperature (K)"
772 value={c.noiseTemperature}
773 emptyBehavior="revert"
774 onChange={(v) => handleChange('noiseTemperature', v)}
779 checked={c.noDirectPaths}
781 handleChange('noDirectPaths', e.target.checked)
785 label="Ignore Direct Paths"
790 checked={c.noPropagationLoss}
792 handleChange('noPropagationLoss', e.target.checked)
796 label="Ignore Propagation Loss"
798 {(c.radarType as RadarType) === 'fmcw' &&
799 renderFmcwReceiverFields(c)}
803 switch (component.type) {
806 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
807 {renderCommonRadarFields(component)}
808 {(component.radarType as RadarType) === 'pulsed' && (
811 value={component.prf}
812 emptyBehavior="revert"
813 onChange={(v) => handleChange('prf', v)}
816 {renderReceiverFields(component)}
817 {renderSchedule(component)}
822 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
823 {renderCommonRadarFields(component)}
824 {(component.radarType as RadarType) === 'pulsed' && (
827 value={component.prf}
828 emptyBehavior="revert"
829 onChange={(v) => handleChange('prf', v)}
832 {renderSchedule(component)}
837 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
838 {renderCommonRadarFields(component)}
839 {(component.radarType as RadarType) === 'pulsed' && (
842 value={component.prf}
843 emptyBehavior="revert"
844 onChange={(v) => handleChange('prf', v)}
847 {renderReceiverFields(component)}
848 {renderSchedule(component)}
853 <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
855 label="Component Name"
858 value={component.name}
860 onChange={(v) => handleChange('name', v)}
862 <FormControl fullWidth size="small">
863 <InputLabel>RCS Type</InputLabel>
866 value={component.rcs_type}
868 handleChange('rcs_type', e.target.value)
871 <MenuItem value="isotropic">Isotropic</MenuItem>
872 <MenuItem value="file">File</MenuItem>
875 {component.rcs_type === 'isotropic' && (
877 label="RCS Value (m^2)"
878 value={component.rcs_value ?? 0}
879 emptyBehavior="revert"
880 onChange={(v) => handleChange('rcs_value', v)}
883 {component.rcs_type === 'file' && (
886 value={component.rcs_filename}
887 onChange={(v) => handleChange('rcs_filename', v)}
890 name: 'Target RCS XML',
897 <FormControl fullWidth size="small">
898 <InputLabel>RCS Model</InputLabel>
901 value={component.rcs_model}
907 .value as TargetComponent['rcs_model']
911 <MenuItem value="constant">Constant</MenuItem>
912 <MenuItem value="chisquare">Chi-Square</MenuItem>
913 <MenuItem value="gamma">Gamma</MenuItem>
916 {(component.rcs_model === 'chisquare' ||
917 component.rcs_model === 'gamma') && (
920 value={component.rcs_k ?? 0}
921 emptyBehavior="revert"
922 onChange={(v) => handleChange('rcs_k', v)}
929 <Typography color="text.secondary">
930 Unknown component type.