FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
simulationProgress.ts
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
3
4import type {
5 SimulationProgressDetail,
6 SimulationProgressState,
7} from '@/stores/simulationProgressStore';
8
9type ProgressMessageMatch =
10 | { kind: 'main'; key: 'main'; detailId: string }
11 | { kind: 'streaming'; key: string; receiverName: string; detailId: string }
12 | { kind: 'export'; key: string; receiverName: string; detailId: string }
13 | { kind: 'unknown'; key: string; detailId: string };
14
15export type NormalizedSimulationProgress = {
16 key: string;
17 progress: SimulationProgressState;
18};
19
20const receiverKey = (receiverName: string) => `receiver:${receiverName}`;
21
22const detailOrder: Record<string, number> = {
23 'main-progress': 0,
24 'main-export-wait': 1,
25 'main-complete': 2,
26 'streaming-finalizing': 0,
27 'streaming-rendering-interference': 1,
28 'streaming-applying-noise': 2,
29 'streaming-writing-hdf5': 3,
30 'streaming-finalized': 4,
31 'export-chunk': 0,
32 'export-finished': 1,
33};
34
35const sortDetails = (details: SimulationProgressDetail[]) =>
36 [...details].sort(
37 (a, b) =>
38 (detailOrder[a.id] ?? Number.MAX_SAFE_INTEGER) -
39 (detailOrder[b.id] ?? Number.MAX_SAFE_INTEGER)
40 );
41
42const buildReceiverMatch = (
43 kind: 'streaming' | 'export',
44 receiverName: string,
45 detailId: string
46): ProgressMessageMatch => ({
47 kind,
48 key: receiverKey(receiverName),
49 receiverName,
50 detailId,
51});
52
53const stripDetails = (
54 progress: SimulationProgressState | SimulationProgressDetail
55): SimulationProgressState => ({
56 message: progress.message,
57 current: progress.current,
58 total: progress.total,
59});
60
61const matchProgressMessage = (message: string): ProgressMessageMatch => {
62 if (
63 message.startsWith('Simulating') ||
64 message.startsWith('Initializing')
65 ) {
66 return { kind: 'main', key: 'main', detailId: 'main-progress' };
67 }
68
69 if (message.startsWith('Main simulation finished')) {
70 return { kind: 'main', key: 'main', detailId: 'main-export-wait' };
71 }
72
73 if (message === 'Simulation complete') {
74 return { kind: 'main', key: 'main', detailId: 'main-complete' };
75 }
76
77 if (message.startsWith('Finalizing CW Receiver ')) {
78 return buildReceiverMatch(
79 'streaming',
80 message.slice('Finalizing CW Receiver '.length),
81 'streaming-finalizing'
82 );
83 }
84
85 if (message.startsWith('Finalizing FMCW Receiver ')) {
86 return buildReceiverMatch(
87 'streaming',
88 message.slice('Finalizing FMCW Receiver '.length),
89 'streaming-finalizing'
90 );
91 }
92
93 if (message.startsWith('Rendering Interference for ')) {
94 return buildReceiverMatch(
95 'streaming',
96 message.slice('Rendering Interference for '.length),
97 'streaming-rendering-interference'
98 );
99 }
100
101 if (message.startsWith('Applying Noise for ')) {
102 return buildReceiverMatch(
103 'streaming',
104 message.slice('Applying Noise for '.length),
105 'streaming-applying-noise'
106 );
107 }
108
109 if (message.startsWith('Writing HDF5 for ')) {
110 return buildReceiverMatch(
111 'streaming',
112 message.slice('Writing HDF5 for '.length),
113 'streaming-writing-hdf5'
114 );
115 }
116
117 if (message.startsWith('Finalized ')) {
118 return buildReceiverMatch(
119 'streaming',
120 message.slice('Finalized '.length),
121 'streaming-finalized'
122 );
123 }
124
125 const exportChunkMatch = message.match(/^Exporting (.+): Chunk \d+$/);
126 if (exportChunkMatch) {
127 return buildReceiverMatch(
128 'export',
129 exportChunkMatch[1],
130 'export-chunk'
131 );
132 }
133
134 if (message.startsWith('Finished Exporting ')) {
135 return buildReceiverMatch(
136 'export',
137 message.slice('Finished Exporting '.length),
138 'export-finished'
139 );
140 }
141
142 return { kind: 'unknown', key: message, detailId: message };
143};
144
145export const normalizeSimulationProgressEvent = (
146 progress: SimulationProgressState
147): NormalizedSimulationProgress => {
148 const match = matchProgressMessage(progress.message);
149
150 return {
151 key: match.key,
152 progress,
153 };
154};
155
156const toDetail = (
157 match: ProgressMessageMatch,
158 progress: SimulationProgressState
159): SimulationProgressDetail => ({
160 id: match.detailId,
161 ...stripDetails(progress),
162});
163
164const upsertDetail = (
165 details: SimulationProgressDetail[],
166 detail: SimulationProgressDetail
167) => {
168 const detailIndex = details.findIndex((item) => item.id === detail.id);
169
170 if (detailIndex === -1) {
171 return sortDetails([...details, detail]);
172 }
173
174 return sortDetails(
175 details.map((item, index) => (index === detailIndex ? detail : item))
176 );
177};
178
179const mergeProgress = (
180 existing: SimulationProgressState | undefined,
181 progress: SimulationProgressState
182): SimulationProgressState => {
183 const match = matchProgressMessage(progress.message);
184 const existingDetails = existing?.details ?? [];
185
186 if (match.kind === 'main') {
187 return stripDetails(progress);
188 }
189
190 return {
191 ...stripDetails(progress),
192 details: upsertDetail(existingDetails, toDetail(match, progress)),
193 };
194};
195
196export const addSimulationProgressEvent = (
197 progress: Record<string, SimulationProgressState>,
198 event: SimulationProgressState
199): Record<string, SimulationProgressState> => {
200 const { key, progress: normalizedProgress } =
201 normalizeSimulationProgressEvent(event);
202
203 return {
204 ...progress,
205 [key]: mergeProgress(progress[key], normalizedProgress),
206 };
207};
208
209const completeProgressForMatch = (
210 match: ProgressMessageMatch,
211 progress: SimulationProgressState
212): SimulationProgressState => {
213 if (match.kind === 'main') {
214 return {
215 message: 'Simulation complete',
216 current: 100,
217 total: 100,
218 };
219 }
220
221 if (match.kind === 'streaming') {
222 return {
223 message: `Finalized ${match.receiverName}`,
224 current: 100,
225 total: 100,
226 };
227 }
228
229 if (match.kind === 'export') {
230 return {
231 message: `Finished Exporting ${match.receiverName}`,
232 current: 100,
233 total: 100,
234 };
235 }
236
237 return progress;
238};
239
240const getProgressSortValue = (progress: SimulationProgressState) =>
241 getSimulationProgressPercent(progress) ?? 0;
242
243const chooseLatestProgress = (
244 existing: SimulationProgressState | undefined,
245 next: SimulationProgressState
246) => {
247 if (!existing) {
248 return next;
249 }
250
251 return getProgressSortValue(next) >= getProgressSortValue(existing)
252 ? next
253 : existing;
254};
255
256export const normalizeCompletedProgressSnapshot = (
257 progress: Record<string, SimulationProgressState>
258): Record<string, SimulationProgressState> => {
259 const completedProgress: Record<string, SimulationProgressState> = {};
260
261 for (const item of Object.values(progress)) {
262 for (const detail of item.details ?? []) {
263 const detailProgress = stripDetails(detail);
264 const { key } = normalizeSimulationProgressEvent(detailProgress);
265 completedProgress[key] = mergeProgress(
266 completedProgress[key],
267 detailProgress
268 );
269 }
270
271 const originalItem = stripDetails(item);
272 const match = matchProgressMessage(item.message);
273 completedProgress[match.key] = mergeProgress(
274 completedProgress[match.key],
275 originalItem
276 );
277
278 const completedItem = completeProgressForMatch(match, originalItem);
279 const mergedItem = mergeProgress(
280 completedProgress[match.key],
281 completedItem
282 );
283 completedProgress[match.key] = chooseLatestProgress(
284 completedProgress[match.key],
285 mergedItem
286 );
287 }
288
289 completedProgress.main = mergeProgress(completedProgress.main, {
290 message: 'Simulation complete',
291 current: 100,
292 total: 100,
293 });
294
295 return completedProgress;
296};
297
298export const getSimulationProgressPercent = (
299 progress: SimulationProgressState
300): number | null => {
301 if (
302 progress.total <= 0 ||
303 !Number.isFinite(progress.current) ||
304 !Number.isFinite(progress.total)
305 ) {
306 return null;
307 }
308
309 const percent = (progress.current / progress.total) * 100;
310
311 return Math.min(100, Math.max(0, percent));
312};