FERS 1.0.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: 'cw'; 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 'cw-finalizing': 0,
27 'cw-rendering-interference': 1,
28 'cw-applying-noise': 2,
29 'cw-writing-hdf5': 3,
30 'cw-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: 'cw' | '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 'cw',
80 message.slice('Finalizing CW Receiver '.length),
81 'cw-finalizing'
82 );
83 }
84
85 if (message.startsWith('Rendering Interference for ')) {
86 return buildReceiverMatch(
87 'cw',
88 message.slice('Rendering Interference for '.length),
89 'cw-rendering-interference'
90 );
91 }
92
93 if (message.startsWith('Applying Noise for ')) {
94 return buildReceiverMatch(
95 'cw',
96 message.slice('Applying Noise for '.length),
97 'cw-applying-noise'
98 );
99 }
100
101 if (message.startsWith('Writing HDF5 for ')) {
102 return buildReceiverMatch(
103 'cw',
104 message.slice('Writing HDF5 for '.length),
105 'cw-writing-hdf5'
106 );
107 }
108
109 if (message.startsWith('Finalized ')) {
110 return buildReceiverMatch(
111 'cw',
112 message.slice('Finalized '.length),
113 'cw-finalized'
114 );
115 }
116
117 const exportChunkMatch = message.match(/^Exporting (.+): Chunk \d+$/);
118 if (exportChunkMatch) {
119 return buildReceiverMatch(
120 'export',
121 exportChunkMatch[1],
122 'export-chunk'
123 );
124 }
125
126 if (message.startsWith('Finished Exporting ')) {
127 return buildReceiverMatch(
128 'export',
129 message.slice('Finished Exporting '.length),
130 'export-finished'
131 );
132 }
133
134 return { kind: 'unknown', key: message, detailId: message };
135};
136
137export const normalizeSimulationProgressEvent = (
138 progress: SimulationProgressState
139): NormalizedSimulationProgress => {
140 const match = matchProgressMessage(progress.message);
141
142 return {
143 key: match.key,
144 progress,
145 };
146};
147
148const toDetail = (
149 match: ProgressMessageMatch,
150 progress: SimulationProgressState
151): SimulationProgressDetail => ({
152 id: match.detailId,
153 ...stripDetails(progress),
154});
155
156const upsertDetail = (
157 details: SimulationProgressDetail[],
158 detail: SimulationProgressDetail
159) => {
160 const detailIndex = details.findIndex((item) => item.id === detail.id);
161
162 if (detailIndex === -1) {
163 return sortDetails([...details, detail]);
164 }
165
166 return sortDetails(
167 details.map((item, index) => (index === detailIndex ? detail : item))
168 );
169};
170
171const mergeProgress = (
172 existing: SimulationProgressState | undefined,
173 progress: SimulationProgressState
174): SimulationProgressState => {
175 const match = matchProgressMessage(progress.message);
176 const existingDetails = existing?.details ?? [];
177
178 return {
179 ...stripDetails(progress),
180 details: upsertDetail(existingDetails, toDetail(match, progress)),
181 };
182};
183
184export const addSimulationProgressEvent = (
185 progress: Record<string, SimulationProgressState>,
186 event: SimulationProgressState
187): Record<string, SimulationProgressState> => {
188 const { key, progress: normalizedProgress } =
189 normalizeSimulationProgressEvent(event);
190
191 return {
192 ...progress,
193 [key]: mergeProgress(progress[key], normalizedProgress),
194 };
195};
196
197const completeProgressForMatch = (
198 match: ProgressMessageMatch,
199 progress: SimulationProgressState
200): SimulationProgressState => {
201 if (match.kind === 'main') {
202 return {
203 message: 'Simulation complete',
204 current: 100,
205 total: 100,
206 };
207 }
208
209 if (match.kind === 'cw') {
210 return {
211 message: `Finalized ${match.receiverName}`,
212 current: 100,
213 total: 100,
214 };
215 }
216
217 if (match.kind === 'export') {
218 return {
219 message: `Finished Exporting ${match.receiverName}`,
220 current: 100,
221 total: 100,
222 };
223 }
224
225 return progress;
226};
227
228const getProgressSortValue = (progress: SimulationProgressState) =>
229 getSimulationProgressPercent(progress) ?? 0;
230
231const chooseLatestProgress = (
232 existing: SimulationProgressState | undefined,
233 next: SimulationProgressState
234) => {
235 if (!existing) {
236 return next;
237 }
238
239 return getProgressSortValue(next) >= getProgressSortValue(existing)
240 ? next
241 : existing;
242};
243
244export const normalizeCompletedProgressSnapshot = (
245 progress: Record<string, SimulationProgressState>
246): Record<string, SimulationProgressState> => {
247 const completedProgress: Record<string, SimulationProgressState> = {};
248
249 for (const item of Object.values(progress)) {
250 for (const detail of item.details ?? []) {
251 const detailProgress = stripDetails(detail);
252 const { key } = normalizeSimulationProgressEvent(detailProgress);
253 completedProgress[key] = mergeProgress(
254 completedProgress[key],
255 detailProgress
256 );
257 }
258
259 const originalItem = stripDetails(item);
260 const match = matchProgressMessage(item.message);
261 completedProgress[match.key] = mergeProgress(
262 completedProgress[match.key],
263 originalItem
264 );
265
266 const completedItem = completeProgressForMatch(match, originalItem);
267 const mergedItem = mergeProgress(
268 completedProgress[match.key],
269 completedItem
270 );
271 completedProgress[match.key] = chooseLatestProgress(
272 completedProgress[match.key],
273 mergedItem
274 );
275 }
276
277 completedProgress.main = mergeProgress(completedProgress.main, {
278 message: 'Simulation complete',
279 current: 100,
280 total: 100,
281 });
282
283 return completedProgress;
284};
285
286export const getSimulationProgressPercent = (
287 progress: SimulationProgressState
288): number | null => {
289 if (
290 progress.total <= 0 ||
291 !Number.isFinite(progress.current) ||
292 !Number.isFinite(progress.total)
293 ) {
294 return null;
295 }
296
297 const percent = (progress.current / progress.total) * 100;
298
299 return Math.min(100, Math.max(0, percent));
300};