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: 'cw'; 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,
27 'cw-rendering-interference': 1,
28 'cw-applying-noise': 2,
35const sortDetails = (details: SimulationProgressDetail[]) =>
38 (detailOrder[a.id] ?? Number.MAX_SAFE_INTEGER) -
39 (detailOrder[b.id] ?? Number.MAX_SAFE_INTEGER)
42const buildReceiverMatch = (
43 kind: 'cw' | '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),
85 if (message.startsWith('Rendering Interference for ')) {
86 return buildReceiverMatch(
88 message.slice('Rendering Interference for '.length),
89 'cw-rendering-interference'
93 if (message.startsWith('Applying Noise for ')) {
94 return buildReceiverMatch(
96 message.slice('Applying Noise for '.length),
101 if (message.startsWith('Writing HDF5 for ')) {
102 return buildReceiverMatch(
104 message.slice('Writing HDF5 for '.length),
109 if (message.startsWith('Finalized ')) {
110 return buildReceiverMatch(
112 message.slice('Finalized '.length),
117 const exportChunkMatch = message.match(/^Exporting (.+): Chunk \d+$/);
118 if (exportChunkMatch) {
119 return buildReceiverMatch(
126 if (message.startsWith('Finished Exporting ')) {
127 return buildReceiverMatch(
129 message.slice('Finished Exporting '.length),
134 return { kind: 'unknown', key: message, detailId: message };
137export const normalizeSimulationProgressEvent = (
138 progress: SimulationProgressState
139): NormalizedSimulationProgress => {
140 const match = matchProgressMessage(progress.message);
149 match: ProgressMessageMatch,
150 progress: SimulationProgressState
151): SimulationProgressDetail => ({
153 ...stripDetails(progress),
156const upsertDetail = (
157 details: SimulationProgressDetail[],
158 detail: SimulationProgressDetail
160 const detailIndex = details.findIndex((item) => item.id === detail.id);
162 if (detailIndex === -1) {
163 return sortDetails([...details, detail]);
167 details.map((item, index) => (index === detailIndex ? detail : item))
171const mergeProgress = (
172 existing: SimulationProgressState | undefined,
173 progress: SimulationProgressState
174): SimulationProgressState => {
175 const match = matchProgressMessage(progress.message);
176 const existingDetails = existing?.details ?? [];
179 ...stripDetails(progress),
180 details: upsertDetail(existingDetails, toDetail(match, progress)),
184export const addSimulationProgressEvent = (
185 progress: Record<string, SimulationProgressState>,
186 event: SimulationProgressState
187): Record<string, SimulationProgressState> => {
188 const { key, progress: normalizedProgress } =
189 normalizeSimulationProgressEvent(event);
193 [key]: mergeProgress(progress[key], normalizedProgress),
197const completeProgressForMatch = (
198 match: ProgressMessageMatch,
199 progress: SimulationProgressState
200): SimulationProgressState => {
201 if (match.kind === 'main') {
203 message: 'Simulation complete',
209 if (match.kind === 'cw') {
211 message: `Finalized ${match.receiverName}`,
217 if (match.kind === 'export') {
219 message: `Finished Exporting ${match.receiverName}`,
228const getProgressSortValue = (progress: SimulationProgressState) =>
229 getSimulationProgressPercent(progress) ?? 0;
231const chooseLatestProgress = (
232 existing: SimulationProgressState | undefined,
233 next: SimulationProgressState
239 return getProgressSortValue(next) >= getProgressSortValue(existing)
244export const normalizeCompletedProgressSnapshot = (
245 progress: Record<string, SimulationProgressState>
246): Record<string, SimulationProgressState> => {
247 const completedProgress: Record<string, SimulationProgressState> = {};
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],
259 const originalItem = stripDetails(item);
260 const match = matchProgressMessage(item.message);
261 completedProgress[match.key] = mergeProgress(
262 completedProgress[match.key],
266 const completedItem = completeProgressForMatch(match, originalItem);
267 const mergedItem = mergeProgress(
268 completedProgress[match.key],
271 completedProgress[match.key] = chooseLatestProgress(
272 completedProgress[match.key],
277 completedProgress.main = mergeProgress(completedProgress.main, {
278 message: 'Simulation complete',
283 return completedProgress;
286export const getSimulationProgressPercent = (
287 progress: SimulationProgressState
290 progress.total <= 0 ||
291 !Number.isFinite(progress.current) ||
292 !Number.isFinite(progress.total)
297 const percent = (progress.current / progress.total) * 100;
299 return Math.min(100, Math.max(0, percent));