1// SPDX-License-Identifier: GPL-2.0-only
2// Copyright (c) 2026-present FERS Contributors (see AUTHORS.md).
5 SimulationProgressDetail,
6 SimulationProgressState,
7} from '@/stores/simulationProgressStore';
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 };
15export type NormalizedSimulationProgress = {
17 progress: SimulationProgressState;
20const receiverKey = (receiverName: string) => `receiver:${receiverName}`;
22const detailOrder: Record<string, number> = {
24 'main-export-wait': 1,
26 'streaming-finalizing': 0,
27 'streaming-rendering-interference': 1,
28 'streaming-applying-noise': 2,
29 'streaming-writing-hdf5': 3,
30 'streaming-finalized': 4,
35const sortDetails = (details: SimulationProgressDetail[]) =>
38 (detailOrder[a.id] ?? Number.MAX_SAFE_INTEGER) -
39 (detailOrder[b.id] ?? Number.MAX_SAFE_INTEGER)
42const buildReceiverMatch = (
43 kind: 'streaming' | 'export',
46): ProgressMessageMatch => ({
48 key: receiverKey(receiverName),
54 progress: SimulationProgressState | SimulationProgressDetail
55): SimulationProgressState => ({
56 message: progress.message,
57 current: progress.current,
58 total: progress.total,
61const matchProgressMessage = (message: string): ProgressMessageMatch => {
63 message.startsWith('Simulating') ||
64 message.startsWith('Initializing')
66 return { kind: 'main', key: 'main', detailId: 'main-progress' };
69 if (message.startsWith('Main simulation finished')) {
70 return { kind: 'main', key: 'main', detailId: 'main-export-wait' };
73 if (message === 'Simulation complete') {
74 return { kind: 'main', key: 'main', detailId: 'main-complete' };
77 if (message.startsWith('Finalizing CW Receiver ')) {
78 return buildReceiverMatch(
80 message.slice('Finalizing CW Receiver '.length),
81 'streaming-finalizing'
85 if (message.startsWith('Finalizing FMCW Receiver ')) {
86 return buildReceiverMatch(
88 message.slice('Finalizing FMCW Receiver '.length),
89 'streaming-finalizing'
93 if (message.startsWith('Rendering Interference for ')) {
94 return buildReceiverMatch(
96 message.slice('Rendering Interference for '.length),
97 'streaming-rendering-interference'
101 if (message.startsWith('Applying Noise for ')) {
102 return buildReceiverMatch(
104 message.slice('Applying Noise for '.length),
105 'streaming-applying-noise'
109 if (message.startsWith('Writing HDF5 for ')) {
110 return buildReceiverMatch(
112 message.slice('Writing HDF5 for '.length),
113 'streaming-writing-hdf5'
117 if (message.startsWith('Finalized ')) {
118 return buildReceiverMatch(
120 message.slice('Finalized '.length),
121 'streaming-finalized'
125 const exportChunkMatch = message.match(/^Exporting (.+): Chunk \d+$/);
126 if (exportChunkMatch) {
127 return buildReceiverMatch(
134 if (message.startsWith('Finished Exporting ')) {
135 return buildReceiverMatch(
137 message.slice('Finished Exporting '.length),
142 return { kind: 'unknown', key: message, detailId: message };
145export const normalizeSimulationProgressEvent = (
146 progress: SimulationProgressState
147): NormalizedSimulationProgress => {
148 const match = matchProgressMessage(progress.message);
157 match: ProgressMessageMatch,
158 progress: SimulationProgressState
159): SimulationProgressDetail => ({
161 ...stripDetails(progress),
164const upsertDetail = (
165 details: SimulationProgressDetail[],
166 detail: SimulationProgressDetail
168 const detailIndex = details.findIndex((item) => item.id === detail.id);
170 if (detailIndex === -1) {
171 return sortDetails([...details, detail]);
175 details.map((item, index) => (index === detailIndex ? detail : item))
179const mergeProgress = (
180 existing: SimulationProgressState | undefined,
181 progress: SimulationProgressState
182): SimulationProgressState => {
183 const match = matchProgressMessage(progress.message);
184 const existingDetails = existing?.details ?? [];
186 if (match.kind === 'main') {
187 return stripDetails(progress);
191 ...stripDetails(progress),
192 details: upsertDetail(existingDetails, toDetail(match, progress)),
196export const addSimulationProgressEvent = (
197 progress: Record<string, SimulationProgressState>,
198 event: SimulationProgressState
199): Record<string, SimulationProgressState> => {
200 const { key, progress: normalizedProgress } =
201 normalizeSimulationProgressEvent(event);
205 [key]: mergeProgress(progress[key], normalizedProgress),
209const completeProgressForMatch = (
210 match: ProgressMessageMatch,
211 progress: SimulationProgressState
212): SimulationProgressState => {
213 if (match.kind === 'main') {
215 message: 'Simulation complete',
221 if (match.kind === 'streaming') {
223 message: `Finalized ${match.receiverName}`,
229 if (match.kind === 'export') {
231 message: `Finished Exporting ${match.receiverName}`,
240const getProgressSortValue = (progress: SimulationProgressState) =>
241 getSimulationProgressPercent(progress) ?? 0;
243const chooseLatestProgress = (
244 existing: SimulationProgressState | undefined,
245 next: SimulationProgressState
251 return getProgressSortValue(next) >= getProgressSortValue(existing)
256export const normalizeCompletedProgressSnapshot = (
257 progress: Record<string, SimulationProgressState>
258): Record<string, SimulationProgressState> => {
259 const completedProgress: Record<string, SimulationProgressState> = {};
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],
271 const originalItem = stripDetails(item);
272 const match = matchProgressMessage(item.message);
273 completedProgress[match.key] = mergeProgress(
274 completedProgress[match.key],
278 const completedItem = completeProgressForMatch(match, originalItem);
279 const mergedItem = mergeProgress(
280 completedProgress[match.key],
283 completedProgress[match.key] = chooseLatestProgress(
284 completedProgress[match.key],
289 completedProgress.main = mergeProgress(completedProgress.main, {
290 message: 'Simulation complete',
295 return completedProgress;
298export const getSimulationProgressPercent = (
299 progress: SimulationProgressState
302 progress.total <= 0 ||
303 !Number.isFinite(progress.current) ||
304 !Number.isFinite(progress.total)
309 const percent = (progress.current / progress.total) * 100;
311 return Math.min(100, Math.max(0, percent));