From 1461c247f00e8030457241b4171568bb1e7d311c Mon Sep 17 00:00:00 2001 From: Leo-Marbehan <90802811+Leo-Marbehan@users.noreply.github.com> Date: Sat, 4 Oct 2025 10:34:37 -0400 Subject: [PATCH 01/10] task allocation mode --- multimodal-ui/src/app/services/animation.service.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/multimodal-ui/src/app/services/animation.service.ts b/multimodal-ui/src/app/services/animation.service.ts index d21dbb2a..fffdc62d 100644 --- a/multimodal-ui/src/app/services/animation.service.ts +++ b/multimodal-ui/src/app/services/animation.service.ts @@ -70,9 +70,12 @@ export class AnimationService { private readonly SAFETY_RATIO = 0.95; // Safety ratio to ensure we don't exceed the frame time private readonly MIN_FRAME_RATE = 60; // This can be increased at the cost of a longer load time. private readonly MIN_TASK_ALLOCATION = 5; + private readonly MIN_TASK_ALLOCATION_PAUSED = 25; private readonly MIN_TIME_PER_FRAME = (1000 / this.MIN_FRAME_RATE) * this.SAFETY_RATIO; + private taskAllocationMode: 'normal' | 'paused' = 'normal'; + // MARK: Properties // Use two variables to avoid changing the ones used for the animation during the animation @@ -338,7 +341,10 @@ export class AnimationService { this.taskService.processTasks( Math.max( lastRedrawTime + this.MIN_TIME_PER_FRAME, - now + this.MIN_TASK_ALLOCATION, + now + + (this.taskAllocationMode === 'normal' + ? this.MIN_TASK_ALLOCATION + : this.MIN_TASK_ALLOCATION_PAUSED), ), ); @@ -367,8 +373,13 @@ export class AnimationService { return; // No polylines available } + const previousTime = this.animationVisualizationTime; + this.updateAnimationTime(); + this.taskAllocationMode = + this.animationVisualizationTime !== previousTime ? 'normal' : 'paused'; + const environment = this.updateEnvironment(); if (environment === null) { From 56f4feeca5cb6015b8696b8fc54bea6cf2eab722 Mon Sep 17 00:00:00 2001 From: Leo-Marbehan <90802811+Leo-Marbehan@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:02:54 -0400 Subject: [PATCH 02/10] handle multiple tasks in parallel --- .env | 1 + .gitignore | 3 + multimodal-ui/public/environment.json | 1 + .../src/app/interfaces/continuous.model.ts | 41 +- .../src/app/interfaces/state.model.ts | 177 ++------ .../src/app/interfaces/task.model.ts | 2 +- .../src/app/services/simulation.service.ts | 426 ++++++++++++++---- .../src/app/services/task.service.ts | 34 +- .../src/app/services/timer.service.ts | 67 +-- .../src/app/services/visualization.service.ts | 76 +--- multimodal-ui/src/environments/environment.ts | 2 + .../common/environments/.env | 1 + .../server/data_manager.py | 16 +- .../server/simulation_manager.py | 4 +- .../ui/static/environment.json | 1 + 15 files changed, 452 insertions(+), 400 deletions(-) diff --git a/.env b/.env index 78f4c7b3..c35c5b49 100644 --- a/.env +++ b/.env @@ -5,3 +5,4 @@ INPUT_DATA_DIRECTORY_PATH=data OUTPUT_DATA_DIRECTORY_PATH=output NUMBER_OF_UPDATES_BETWEEN_STATES=1000 NUMBER_OF_STATES_TO_SEND_AT_ONCE=1 +DEBUG_TASKS=true diff --git a/.gitignore b/.gitignore index 84cd01ff..2d60a2b9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ venv # Python cache __pycache__ +# Input data +data/ + # Data files (saved_logs, saved_simulations) output/saved_logs output/saved_simulations diff --git a/multimodal-ui/public/environment.json b/multimodal-ui/public/environment.json index c3731a7d..868eee7d 100644 --- a/multimodal-ui/public/environment.json +++ b/multimodal-ui/public/environment.json @@ -6,5 +6,6 @@ "OUTPUT_DATA_DIRECTORY_PATH": "output", "NUMBER_OF_UPDATES_BETWEEN_STATES": "1000", "NUMBER_OF_STATES_TO_SEND_AT_ONCE": "1", + "DEBUG_TASKS": "true", "HOST": "127.0.0.1" } diff --git a/multimodal-ui/src/app/interfaces/continuous.model.ts b/multimodal-ui/src/app/interfaces/continuous.model.ts index 0a01359b..8701f8de 100644 --- a/multimodal-ui/src/app/interfaces/continuous.model.ts +++ b/multimodal-ui/src/app/interfaces/continuous.model.ts @@ -6,12 +6,7 @@ import { SimulationState } from './state.model'; import { Statistics } from './statistics.model'; import { Stop } from './stop.model'; import { Tagged } from './tags.model'; -import { - AtomicTask, - BUILD_CONTINUOUS_ENVIRONMENT_TASK_PRIORITY as BUILD_CONTINUOUS_ENVIRONMENTS_TASK_PRIORITY, - CompositeTask, - Task, -} from './task.model'; +import { AtomicTask, CompositeTask, Task } from './task.model'; import { PassengerUpdate, StatisticsUpdate, @@ -112,42 +107,14 @@ export function createContinuousEnvironmentReferences(): ContinuousEnvironmentRe } // MARK: Tasks -export class BuildContinuousEnvironmentsTask extends CompositeTask { - private readonly continuousEnvironments: ContinuousEnvironment[] = []; - - constructor( - queue: SortedList, - private readonly states: SimulationState[], - private readonly references: ContinuousEnvironmentReferences, - private readonly callback: (environments: ContinuousEnvironment[]) => void, - ) { - super(BUILD_CONTINUOUS_ENVIRONMENTS_TASK_PRIORITY, queue); - } - - protected override beforeAll(): void { - for (const state of this.states) { - new BuildContinuousEnvironmentTask( - this.subtasks, - state, - this.references, - this.continuousEnvironments, - ).addToQueue(); - } - } - - protected override afterAll(): void { - this.callback(this.continuousEnvironments); - } -} - -class BuildContinuousEnvironmentTask extends CompositeTask { +export class BuildContinuousEnvironmentTask extends CompositeTask { private readonly continuousEnvironment: ContinuousEnvironment; constructor( queue: SortedList, private readonly state: SimulationState, private readonly references: ContinuousEnvironmentReferences, - private readonly continuousEnvironments: ContinuousEnvironment[], + private readonly callback: (environment: ContinuousEnvironment) => void, ) { super(0, queue); @@ -207,7 +174,7 @@ class BuildContinuousEnvironmentTask extends CompositeTask { } protected override afterAll(): void { - this.continuousEnvironments.push(this.continuousEnvironment); + this.callback(this.continuousEnvironment); } private buildEmptyContinuousEnvironment(): ContinuousEnvironment { diff --git a/multimodal-ui/src/app/interfaces/state.model.ts b/multimodal-ui/src/app/interfaces/state.model.ts index 25a78be4..d6df0c50 100644 --- a/multimodal-ui/src/app/interfaces/state.model.ts +++ b/multimodal-ui/src/app/interfaces/state.model.ts @@ -18,100 +18,49 @@ export interface SimulationState extends SimulationEnvironment { } // MARK: Extract State + +interface ExtractStateTaskState { + environment: + | (SimulationEnvironment & Pick) + | null; + updates: Update[]; +} + export class ExtractStateTask extends CompositeTask { - private environments: (SimulationEnvironment & - Pick)[] = []; - private updatesByFirstUpdateIndex: Record = {}; + private state: ExtractStateTaskState = { + environment: null, + updates: [], + }; constructor( queue: SortedList, - private readonly serializedEnvironments: unknown, + private readonly serializedEnvironment: unknown, private readonly serializedUpdates: unknown, - private readonly callback: (states: SimulationState[] | null) => void, + private readonly callback: (state: SimulationState | null) => void, ) { super(EXTRACT_STATE_TASK_PRIORITY, queue); } public override beforeAll(): void { - new ExtractEnvironmentsTask( + new ExtractEnvironmentTask( this.subtasks, - this.serializedEnvironments, - this.environments, + this.serializedEnvironment, + this.state, ).addToQueue(); - new ExtractAllUpdatesTask( + new ExtractUpdatesTask( this.subtasks, this.serializedUpdates, - this.updatesByFirstUpdateIndex, + this.state, ).addToQueue(); } public override afterAll(): void { - const states: SimulationState[] = []; - - for (const environment of this.environments) { - const updates = this.updatesByFirstUpdateIndex[environment.updateIndex]; - - if (updates === undefined) { - console.error( - 'No updates found for environment', - environment, - 'in updates', - this.updatesByFirstUpdateIndex, - 'from serialized updates', - this.serializedUpdates, - 'and serialized environments', - this.serializedEnvironments, - ); - - // TODO #42 Failed - this.callback(null); - continue; - } - - states.push({ - ...environment, - updates, - }); - } - - this.callback(states); - } -} - -// MARK: Extract Environments -class ExtractEnvironmentsTask extends CompositeTask { - constructor( - queue: SortedList, - private readonly serializedEnvironments: unknown, - private readonly environments: (SimulationEnvironment & - Pick)[], - ) { - super(EXTRACT_STATE_TASK_PRIORITY, queue); - } - - protected override beforeAll(): void { - if (!Array.isArray(this.serializedEnvironments)) { - console.error( - 'Invalid data type for serialized environments', - this.serializedEnvironments, - ); - - // TODO #42 Failed - return; - } - - for (const serializedEnvironment of this.serializedEnvironments) { - new ExtractEnvironmentTask( - this.subtasks, - serializedEnvironment, - this.environments, - ).addToQueue(); - } - } - - protected override afterAll(): void { - // Nothing to do + this.callback( + this.state.environment + ? { ...this.state.environment, updates: this.state.updates } + : null, + ); } } @@ -130,8 +79,7 @@ class ExtractEnvironmentTask extends CompositeTask { constructor( queue: SortedList, private readonly serializedEnvironment: unknown, - private readonly environments: (SimulationEnvironment & - Pick)[], + private readonly state: ExtractStateTaskState, ) { super(EXTRACT_STATE_TASK_PRIORITY, queue); } @@ -263,7 +211,7 @@ class ExtractEnvironmentTask extends CompositeTask { } protected override afterAll(): void { - this.environments.push(this.environment); + this.state.environment = this.environment; } } @@ -339,45 +287,6 @@ class ExtractVehicleTask extends Task { } } -// MARK: Extract All Updates -class ExtractAllUpdatesTask extends CompositeTask { - constructor( - queue: SortedList, - private readonly serializedUpdates: unknown, - private readonly updatesByFirstUpdateIndex: Record, - ) { - super(EXTRACT_STATE_TASK_PRIORITY, queue); - } - - protected override beforeAll(): void { - if ( - typeof this.serializedUpdates !== 'object' || - this.serializedUpdates === null - ) { - console.error( - 'Invalid data type for serialized updates', - this.serializedUpdates, - ); - - // TODO #42 Failed - return; - } - - for (const [key, value] of Object.entries(this.serializedUpdates)) { - new ExtractUpdatesTask( - this.subtasks, - key, - value, - this.updatesByFirstUpdateIndex, - ).addToQueue(); - } - } - - protected override afterAll(): void { - // Nothing to do - } -} - // MARK: Extract Updates class ExtractUpdatesTask extends CompositeTask { private updates: SortedList = new SortedList( @@ -386,54 +295,23 @@ class ExtractUpdatesTask extends CompositeTask { constructor( queue: SortedList, - private readonly serializedUpdatesKey: unknown, private readonly serializedUpdates: unknown, - private readonly updatesByFirstUpdateIndex: Record, + private readonly state: ExtractStateTaskState, ) { super(EXTRACT_STATE_TASK_PRIORITY, queue); } beforeAll(): void { - if (typeof this.serializedUpdatesKey !== 'string') { - console.error( - 'Invalid data type for key', - this.serializedUpdatesKey, - 'with serialized updates', - this.serializedUpdates, - ); - - // TODO #42 Failed - return; - } - - const key = parseInt(this.serializedUpdatesKey); - - if (isNaN(key)) { - console.error( - 'Key is not a valid number', - this.serializedUpdatesKey, - 'with serialized updates', - this.serializedUpdates, - ); - - // TODO #42 Failed - return; - } - if (!Array.isArray(this.serializedUpdates)) { console.error( 'Invalid data type for serialized updates', this.serializedUpdates, - 'with key', - this.serializedUpdatesKey, ); // TODO #42 Failed return; } - this.updatesByFirstUpdateIndex[key] = this.updates.editableItems; - for (const serializedUpdate of this.serializedUpdates) { new ExtractUpdateTask( this.subtasks, @@ -445,6 +323,7 @@ class ExtractUpdatesTask extends CompositeTask { protected override afterAll(): void { // Nothing to do + this.state.updates = this.updates.editableItems; } } diff --git a/multimodal-ui/src/app/interfaces/task.model.ts b/multimodal-ui/src/app/interfaces/task.model.ts index 55bb2eca..7c2923f9 100644 --- a/multimodal-ui/src/app/interfaces/task.model.ts +++ b/multimodal-ui/src/app/interfaces/task.model.ts @@ -14,7 +14,7 @@ export function emptyTaskQueue(): SortedList { */ export abstract class Task { constructor( - public readonly priority: number, + public priority: number, protected readonly queue: SortedList, ) {} diff --git a/multimodal-ui/src/app/services/simulation.service.ts b/multimodal-ui/src/app/services/simulation.service.ts index b0ba31dd..d0d3868f 100644 --- a/multimodal-ui/src/app/services/simulation.service.ts +++ b/multimodal-ui/src/app/services/simulation.service.ts @@ -1,10 +1,12 @@ import { computed, + effect, Injectable, signal, Signal, WritableSignal, } from '@angular/core'; +import { DEBUG_TASKS } from '../../environments/environment'; import { ContinuousEnvironment, ContinuousEnvironmentReferences, @@ -17,9 +19,11 @@ import { } from '../interfaces/polylines.model'; import { Simulation } from '../interfaces/simulation.model'; import { SimulationState } from '../interfaces/state.model'; +import { EXTRACT_STATE_TASK_PRIORITY, Task } from '../interfaces/task.model'; import { CommunicationService } from './communication.service'; import { DataService } from './data.service'; import { TaskService } from './task.service'; +import { TimerService } from './timer.service'; interface DebounceSettings { readonly debounceTime: number; @@ -32,16 +36,23 @@ interface DebounceSettings { }) export class SimulationService { // MARK: Properties + private readonly MAX_STATES_EXTRACTION_CONCURRENT_TASKS = 10; + + private readonly stateExtractionTasks = new Map< + number, + { task: Task; startTimestamp: number } + >(); + private readonly _activeSimulationIdSignal: WritableSignal = signal(null); private readonly _simulationPolylinesSignal: WritableSignal = signal(null); - private readonly _isFetchingStatesSignal: WritableSignal = + private readonly isFetchingStatesSignal: WritableSignal = signal(false); - private readonly _isFetchingPolylinesSignal: WritableSignal = + private readonly isFetchingPolylinesSignal: WritableSignal = signal(false); private readonly _continuousEnvironmentsSignal: WritableSignal< @@ -53,7 +64,7 @@ export class SimulationService { private references: ContinuousEnvironmentReferences = createContinuousEnvironmentReferences(); - private readonly _hasAllStatesSignal: WritableSignal = signal(false); + private readonly hasAllStatesSignal: WritableSignal = signal(false); private readonly getPolylinesDebounceSettings: DebounceSettings = { debounceTime: 500, @@ -63,17 +74,35 @@ export class SimulationService { private readonly getMissingSimulationStatesDebounceSettings: DebounceSettings = { - debounceTime: 500, + debounceTime: 100, lastExecutionTime: null, timeoutId: null, }; + private wantedVisualizationTimeSignal: WritableSignal = + signal(null); + // MARK: Constructor constructor( private readonly dataService: DataService, private readonly communicationService: CommunicationService, private readonly taskService: TaskService, - ) {} + private readonly timerService: TimerService, + ) { + effect(() => this.getPolylinesIfNeeded()); + + effect(() => this.getMissingSimulationStatesIfNeeded()); + + effect( + () => (this.timerService.simulation = this.activeSimulationSignal()), + ); + + effect( + () => + (this.timerService.continuousEnvironments = + this.continuousEnvironmentsSignal()), + ); + } // MARK: Active simulation setActiveSimulationId(simulationId: string) { @@ -83,14 +112,9 @@ export class SimulationService { this.communicationService.on( 'missing-simulation-states', - ( - serializedMissingStatesEnvironments, - serializedMissingStatesUpdates, - hasAllStates, - ) => { + (serializedMissingStatesEnvironments, hasAllStates) => { this.onMissingSimulationStates( serializedMissingStatesEnvironments, - serializedMissingStatesUpdates, hasAllStates, ); }, @@ -99,7 +123,7 @@ export class SimulationService { this.communicationService.on( `polylines-${simulationId}`, (polylinesByCoordinates, version) => { - this._isFetchingPolylinesSignal.set(false); + this.isFetchingPolylinesSignal.set(false); this._simulationPolylinesSignal.set( extractAllPolylines(polylinesByCoordinates, version), @@ -115,8 +139,8 @@ export class SimulationService { this._simulationPolylinesSignal.set(null); - this._isFetchingStatesSignal.set(false); - this._isFetchingPolylinesSignal.set(false); + this.isFetchingStatesSignal.set(false); + this.isFetchingPolylinesSignal.set(false); this.communicationService.removeAllListeners('missing-simulation-states'); @@ -132,7 +156,7 @@ export class SimulationService { this.references = createContinuousEnvironmentReferences(); - this._hasAllStatesSignal.set(false); + this.hasAllStatesSignal.set(false); } get activeSimulationSignal(): Signal { @@ -176,26 +200,121 @@ export class SimulationService { ); } - getMissingSimulationStates( - simulationId: string, - visualizationTime: number, - completeStateUpdateIndexes: number[], - ) { - return this.runWithDebounce(() => { - this.getMissingSimulationStatesWithDebounce( - simulationId, - visualizationTime, - completeStateUpdateIndexes, + // MARK: States + private getMissingSimulationStatesIfNeeded() { + const hasAllStates = this.hasAllStatesSignal(); + + if (hasAllStates) { + return; + } + + const simulation = this.activeSimulationSignal(); + + if (simulation === null) { + this.timerService.isLoading = true; + return; + } + + const wantedVisualizationTime = this.wantedVisualizationTimeSignal(); + + if (wantedVisualizationTime === null) { + this.timerService.isLoading = true; + return; + } + + this.updateTasksPriority(wantedVisualizationTime); + + const isFetching = this.isFetchingStatesSignal(); + + if (isFetching) { + return; + } + + const continuousEnvironments = this.continuousEnvironmentsSignal(); + + if ( + this.stateExtractionTasks.size >= + this.MAX_STATES_EXTRACTION_CONCURRENT_TASKS + ) { + return; + } + + const completeStateUpdateIndexes = continuousEnvironments + .filter((continuousEnvironment) => continuousEnvironment.isComplete) + .map((continuousEnvironment) => continuousEnvironment.startUpdateIndex); + + const currentlyProcessingStateUpdateIndexes = Array.from( + this.stateExtractionTasks.keys(), + ); + + this.runWithDebounce(() => { + this.getMissingSimulationStates( + simulation.id, + wantedVisualizationTime, + Array.from( + new Set( + completeStateUpdateIndexes.concat( + currentlyProcessingStateUpdateIndexes, + ), + ), + ), ); }, this.getMissingSimulationStatesDebounceSettings); } - private getMissingSimulationStatesWithDebounce( + private updateTasksPriority(wantedVisualizationTime: number) { + let mostUrgentTask: { task: Task; startTimestamp: number } | null = null; + + for (const task of this.stateExtractionTasks.values()) { + task.task.priority = EXTRACT_STATE_TASK_PRIORITY; + + const proximity = wantedVisualizationTime - task.startTimestamp; + + const previousProximity = mostUrgentTask === null ? Infinity : proximity; + + /** + * The most urgent task is the one with a start time before the wanted + * visualization time that is the closest to it. + * + * If all tasks are after the wanted visualization time, then the most + * urgent task is the one with the closest start time. + **/ + if ( + (proximity < 0 && + (previousProximity > 0 || proximity > previousProximity)) || + proximity < previousProximity + ) { + mostUrgentTask = task; + } + } + + if (mostUrgentTask) { + mostUrgentTask.task.priority = EXTRACT_STATE_TASK_PRIORITY + 1; + } + + if (DEBUG_TASKS) { + console.debug('updateTasksPriority', { + wantedVisualizationTime, + mostUrgentTask, + stateExtractionTasks: this.stateExtractionTasks, + }); + } + } + + private getMissingSimulationStates( simulationId: string, visualizationTime: number, completeStateUpdateIndexes: number[], ) { - this._isFetchingStatesSignal.set(true); + if (DEBUG_TASKS) { + console.debug('getMissingSimulationStates', { + simulationId, + visualizationTime, + completeStateUpdateIndexes, + }); + } + + this.isFetchingStatesSignal.set(true); this.communicationService.emit( 'get-missing-simulation-states', @@ -205,95 +324,226 @@ export class SimulationService { ); } - getPolylines(simulationId: string) { - this.runWithDebounce(() => { - this.getPolylinesWithoutDebounce(simulationId); - }, this.getPolylinesDebounceSettings); + set wantedVisualizationTime(visualizationTime: number | null) { + this.wantedVisualizationTimeSignal.set(visualizationTime); } - private getPolylinesWithoutDebounce(simulationId: string) { - this._isFetchingPolylinesSignal.set(true); - - this.communicationService.emit('get-polylines', simulationId); + get continuousEnvironmentsSignal(): Signal { + return this._continuousEnvironmentsSignal; } - get simulationPolylinesSignal(): Signal { - return this._simulationPolylinesSignal; - } + // MARK: Polylines + private getPolylinesIfNeeded() { + const simulation = this.activeSimulationSignal(); - get isFetchingStatesSignal(): Signal { - return this._isFetchingStatesSignal; - } + if (simulation === null) { + return; + } - get isFetchingPolylinesSignal(): Signal { - return this._isFetchingPolylinesSignal; + const polylines = this.simulationPolylinesSignal(); + const isFetching = this.isFetchingPolylinesSignal(); + + const needPolylineUpdate = + polylines === null || polylines.version !== simulation.polylinesVersion; + + if (needPolylineUpdate && !isFetching) { + this.runWithDebounce(() => { + this.getPolylinesWithoutDebounce(simulation.id); + }, this.getPolylinesDebounceSettings); + } } - get continuousEnvironmentsSignal(): Signal { - return this._continuousEnvironmentsSignal; + private getPolylinesWithoutDebounce(simulationId: string) { + this.isFetchingPolylinesSignal.set(true); + + this.communicationService.emit('get-polylines', simulationId); } - get hasAllStatesSignal(): Signal { - return this._hasAllStatesSignal; + get simulationPolylinesSignal(): Signal { + return this._simulationPolylinesSignal; } // MARK: Event handlers private onMissingSimulationStates( - serializedMissingStatesEnvironments: unknown, - serializedMissingStatesUpdates: unknown, + serializedMissingStates: unknown, hasAllStates: unknown, ): void { - this.taskService.extractStateTask( - serializedMissingStatesEnvironments, - serializedMissingStatesUpdates, - (extractedStates) => - this.afterExtractStateTask(extractedStates, hasAllStates), - ); + if (DEBUG_TASKS) { + console.debug('onMissingSimulationStates', { + serializedMissingStates, + hasAllStates, + }); + } + + this.isFetchingStatesSignal.set(false); + + if (!Array.isArray(serializedMissingStates)) { + console.error( + 'Received invalid serializedMissingStates value from the server.', + ); + return; + } + + for (const serializedMissingState of serializedMissingStates as unknown[]) { + if ( + typeof serializedMissingState !== 'object' || + serializedMissingState === null + ) { + console.error( + 'Received invalid serializedMissingState value from the server.', + ); + return; + } + + if (!('environment' in serializedMissingState)) { + console.error( + 'Received invalid serializedMissingState value from the server. Missing environment.', + ); + return; + } + + if (!('updates' in serializedMissingState)) { + console.error( + 'Received invalid serializedMissingState value from the server. Missing updates.', + ); + return; + } + + if (!('startTimestamp' in serializedMissingState)) { + console.error( + 'Received invalid serializedMissingState value from the server. Missing startTimestamp.', + ); + return; + } + + if (typeof serializedMissingState.startTimestamp !== 'number') { + console.error( + 'Received invalid serializedMissingState value from the server. Invalid startTimestamp.', + ); + return; + } + + if (!('startUpdateIndex' in serializedMissingState)) { + console.error( + 'Received invalid serializedMissingState value from the server. Missing startUpdateIndex.', + ); + return; + } + + if (typeof serializedMissingState.startUpdateIndex !== 'number') { + console.error( + 'Received invalid serializedMissingState value from the server. Invalid startUpdateIndex.', + ); + return; + } + + const startTimestamp = serializedMissingState.startTimestamp; + const startUpdateIndex = serializedMissingState.startUpdateIndex; + + const task = this.taskService.extractStateTask( + serializedMissingState.environment, + serializedMissingState.updates, + (extractedState) => + this.afterExtractStateTask( + extractedState, + startUpdateIndex, + startTimestamp, + ), + ); + + this.stateExtractionTasks.set(startUpdateIndex, { + task, + startTimestamp, + }); + + if (DEBUG_TASKS) { + console.debug('stateExtractionTasks', this.stateExtractionTasks); + } + } + + if (typeof hasAllStates !== 'boolean') { + console.error('Received invalid hasAllStates value from the server.'); + return; + } + + this.hasAllStatesSignal.set(hasAllStates); } private afterExtractStateTask( - extractedStates: SimulationState[] | null, - hasAllStates: unknown, + extractedState: SimulationState | null, + startUpdateIndex: number, + startTimestamp: number, ): void { - if (extractedStates === null) { + if (DEBUG_TASKS) { + console.debug('afterExtractStateTask', { + extractedState, + startUpdateIndex, + startTimestamp, + }); + } + + if (extractedState === null) { console.error( - 'Failed to extract missing simulation states from the server response.', + 'Failed to extract missing simulation state from the server response.', ); - this._isFetchingStatesSignal.set(false); + this.stateExtractionTasks.delete(startUpdateIndex); + + if (DEBUG_TASKS) { + console.debug('stateExtractionTasks', this.stateExtractionTasks); + } + return; } - this.taskService.buildContinuousEnvironmentsTask( - extractedStates, - this.references, - (continuousEnvironments) => - this.afterBuildContinuousEnvironmentsTask( - continuousEnvironments, - hasAllStates, - ), - ); + this.stateExtractionTasks.set(startUpdateIndex, { + task: this.taskService.buildContinuousEnvironmentTask( + extractedState, + this.references, + (continuousEnvironment) => + this.afterBuildContinuousEnvironmentTask( + continuousEnvironment, + startUpdateIndex, + ), + ), + startTimestamp, + }); + + if (DEBUG_TASKS) { + console.debug('stateExtractionTasks', this.stateExtractionTasks); + } } - private afterBuildContinuousEnvironmentsTask( - continuousEnvironments: ContinuousEnvironment[], - hasAllStates: unknown, + private afterBuildContinuousEnvironmentTask( + continuousEnvironment: ContinuousEnvironment, + startUpdateIndex: number, ) { + if (DEBUG_TASKS) { + console.debug('afterBuildContinuousEnvironmentTask', { + continuousEnvironment, + startUpdateIndex, + }); + } + + this.stateExtractionTasks.delete(startUpdateIndex); + + if (DEBUG_TASKS) { + console.debug('stateExtractionTasks', this.stateExtractionTasks); + } + this._continuousEnvironmentsSignal.update((environments) => { const newEnvironments = [...environments]; - for (const environment of continuousEnvironments) { - const existingEnvironmentIndex = newEnvironments.findIndex( - (existingEnvironment) => - existingEnvironment.startUpdateIndex === - environment.startUpdateIndex, - ); + const existingEnvironmentIndex = newEnvironments.findIndex( + (existingEnvironment) => + existingEnvironment.startUpdateIndex === + continuousEnvironment.startUpdateIndex, + ); - if (existingEnvironmentIndex === -1) { - newEnvironments.push(environment); - } else { - newEnvironments[existingEnvironmentIndex] = environment; - } + if (existingEnvironmentIndex === -1) { + newEnvironments.push(continuousEnvironment); + } else { + newEnvironments[existingEnvironmentIndex] = continuousEnvironment; } newEnvironments.sort((a, b) => a.startUpdateIndex - b.startUpdateIndex); @@ -305,16 +555,6 @@ export class SimulationService { return newEnvironments; }); - - if (typeof hasAllStates !== 'boolean') { - console.error('Received invalid hasAllStates value from the server.'); - - hasAllStates = false; - } else { - this._hasAllStatesSignal.set(hasAllStates); - } - - this._isFetchingStatesSignal.set(false); } private runWithDebounce( diff --git a/multimodal-ui/src/app/services/task.service.ts b/multimodal-ui/src/app/services/task.service.ts index f7e4e0c9..7a8a1a44 100644 --- a/multimodal-ui/src/app/services/task.service.ts +++ b/multimodal-ui/src/app/services/task.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { - BuildContinuousEnvironmentsTask, + BuildContinuousEnvironmentTask, ContinuousEnvironment, ContinuousEnvironmentReferences, } from '../interfaces/continuous.model'; @@ -40,29 +40,37 @@ export class TaskService { // MARK: Specific Tasks extractStateTask( - serializedEnvironments: unknown, + serializedEnvironment: unknown, serializedUpdates: unknown, - callback: (states: SimulationState[] | null) => void, - ): void { - new ExtractStateTask( + callback: (states: SimulationState | null) => void, + ): ExtractStateTask { + const task = new ExtractStateTask( this.queue, - serializedEnvironments, + serializedEnvironment, serializedUpdates, callback, - ).addToQueue(); + ); + + task.addToQueue(); + + return task; } - buildContinuousEnvironmentsTask( - states: SimulationState[], + buildContinuousEnvironmentTask( + states: SimulationState, references: ContinuousEnvironmentReferences, - callback: (environments: ContinuousEnvironment[]) => void, - ) { - new BuildContinuousEnvironmentsTask( + callback: (environment: ContinuousEnvironment) => void, + ): BuildContinuousEnvironmentTask { + const task = new BuildContinuousEnvironmentTask( this.queue, states, references, callback, - ).addToQueue(); + ); + + task.addToQueue(); + + return task; } // For debugging purposes diff --git a/multimodal-ui/src/app/services/timer.service.ts b/multimodal-ui/src/app/services/timer.service.ts index 45413a7b..37b2dd25 100644 --- a/multimodal-ui/src/app/services/timer.service.ts +++ b/multimodal-ui/src/app/services/timer.service.ts @@ -20,8 +20,10 @@ import { setVisualizationSpeedPowerLocalStorage, setVisualizationTimeLocalStorage, } from '../interfaces/local-storage'; -import { RUNNING_SIMULATION_STATUSES } from '../interfaces/simulation.model'; -import { SimulationService } from './simulation.service'; +import { + RUNNING_SIMULATION_STATUSES, + Simulation, +} from '../interfaces/simulation.model'; @Injectable({ providedIn: 'root', @@ -32,7 +34,8 @@ export class TimerService { private readonly MAX_TIME_STEP = 1 / this.MIN_FRAME_RATE; // seconds private _visualizationTime: number | null = null; private isEnvironmentLoaded = false; - private _continuousEnvironments: ContinuousEnvironment[] = []; + + continuousEnvironments: ContinuousEnvironment[] = []; private lastUpdateTime: number | null = null; @@ -50,6 +53,9 @@ export class TimerService { private readonly _directionSignal: WritableSignal = signal(null); + private readonly simulationSignal: WritableSignal = + signal(null); + readonly isPausedSignal: Signal = computed( () => this._isPausedSignal() === true, ); @@ -75,38 +81,38 @@ export class TimerService { }); // MARK: Constructor - constructor(private readonly simulationService: SimulationService) { + constructor() { effect(() => { - const activeSimulation = this.simulationService.activeSimulationSignal(); + const simulation = this.simulationSignal(); - if (activeSimulation !== null) { - this.load(activeSimulation.id); + if (simulation !== null) { + this.load(simulation.id); } else { this.reset(); } }); effect(() => { - const activeSimulation = this.simulationService.activeSimulationSignal(); + const simulation = this.simulationSignal(); - if (activeSimulation !== null) { - this.saveIsPaused(activeSimulation.id); + if (simulation !== null) { + this.saveIsPaused(simulation.id); } }); effect(() => { - const activeSimulation = this.simulationService.activeSimulationSignal(); + const simulation = this.simulationSignal(); - if (activeSimulation !== null) { - this.saveSpeedPower(activeSimulation.id); + if (simulation !== null) { + this.saveSpeedPower(simulation.id); } }); effect(() => { - const activeSimulation = this.simulationService.activeSimulationSignal(); + const simulation = this.simulationSignal(); - if (activeSimulation !== null) { - this.saveDirection(activeSimulation.id); + if (simulation !== null) { + this.saveDirection(simulation.id); } }); } @@ -128,15 +134,15 @@ export class TimerService { this.nextDirection = value; } + set simulation(value: Simulation | null) { + this.simulationSignal.set(value); + } + // MARK: Getters get visualizationTime(): number | null { return this._visualizationTime; } - get continuousEnvironments(): ContinuousEnvironment[] { - return this._continuousEnvironments; - } - // MARK: Time Update /** * @@ -167,30 +173,25 @@ export class TimerService { visualizationTimeOverride, ); - const activeSimulation = this.simulationService.activeSimulationSignal(); + const simulation = this.simulationSignal(); if ( visualizationTime === null || elapsedTime === null || - activeSimulation === null + simulation === null ) { this.isEnvironmentLoaded = false; this._visualizationTime = null; - if (activeSimulation !== null) { - this.saveTime(activeSimulation.id); + if (simulation !== null) { + this.saveTime(simulation.id); } return null; } - const continuousEnvironments = - this.simulationService.continuousEnvironmentsSignal(); - - this._continuousEnvironments = continuousEnvironments; - const closestContinuousEnvironment = findClosestContinuousEnvironment( - continuousEnvironments, + this.continuousEnvironments, visualizationTime, ); @@ -198,7 +199,7 @@ export class TimerService { this._visualizationTime = visualizationTime; - this.saveTime(activeSimulation.id); + this.saveTime(simulation.id); return elapsedTime; } @@ -207,7 +208,7 @@ export class TimerService { visualizationTime: number | null; elapsedTime: number | null; } { - const simulation = this.simulationService.activeSimulationSignal(); + const simulation = this.simulationSignal(); if (simulation === null) { this.lastUpdateTime = null; @@ -276,7 +277,7 @@ export class TimerService { this._directionSignal.set(null); this._visualizationTime = null; this.isEnvironmentLoaded = false; - this._continuousEnvironments = []; + this.continuousEnvironments = []; this.lastUpdateTime = null; } diff --git a/multimodal-ui/src/app/services/visualization.service.ts b/multimodal-ui/src/app/services/visualization.service.ts index 804d0e75..9c292d60 100644 --- a/multimodal-ui/src/app/services/visualization.service.ts +++ b/multimodal-ui/src/app/services/visualization.service.ts @@ -76,73 +76,15 @@ export class VisualizationService { private readonly animationService: AnimationService, private readonly timerService: TimerService, ) { - effect(() => { - const environmentSlice = this.environmentSignal(); - this.environmentSlice = environmentSlice; - }); - - effect(() => { - const hasAllStates = this.simulationService.hasAllStatesSignal(); - - if (hasAllStates) { - this.timerService.isLoading = false; - return; - } - - const simulation = this.simulationService.activeSimulationSignal(); - - if (simulation === null) { - this.timerService.isLoading = true; - return; - } - - const wantedVisualizationTime = this._wantedVisualizationTimeSignal(); - - if (wantedVisualizationTime === null) { - this.timerService.isLoading = true; - return; - } - - const isFetching = this.simulationService.isFetchingStatesSignal(); - - if (!isFetching) { - const continuousEnvironments = - this.simulationService.continuousEnvironmentsSignal(); - const completeStateUpdateIndexes = continuousEnvironments - .filter((continuousEnvironment) => continuousEnvironment.isComplete) - .map( - (continuousEnvironment) => continuousEnvironment.startUpdateIndex, - ); - - this.simulationService.getMissingSimulationStates( - simulation.id, - wantedVisualizationTime, - completeStateUpdateIndexes, - ); - } - - const environmentSlice = this.environmentSignal(); - - this.timerService.isLoading = environmentSlice === null; - }); - - effect(() => { - const simulation = this.simulationService.activeSimulationSignal(); - - if (simulation === null) { - return; - } - - const polylines = this.simulationService.simulationPolylinesSignal(); - const isFetching = this.simulationService.isFetchingPolylinesSignal(); - - const needPolylineUpdate = - polylines === null || polylines.version !== simulation.polylinesVersion; + effect( + () => + (this.simulationService.wantedVisualizationTime = + this.wantedVisualizationTimeSignal()), + ); - if (needPolylineUpdate && !isFetching) { - this.simulationService.getPolylines(simulation.id); - } - }); + effect( + () => (this.timerService.isLoading = this.sliceEnvironment() === null), + ); // MARK: Animation effect(() => { @@ -299,6 +241,8 @@ export class VisualizationService { this.hasEnvironmentChanged = false; + this.environmentSlice = environment; + return environment; } } diff --git a/multimodal-ui/src/environments/environment.ts b/multimodal-ui/src/environments/environment.ts index 11db0c4c..5b45cb5b 100644 --- a/multimodal-ui/src/environments/environment.ts +++ b/multimodal-ui/src/environments/environment.ts @@ -5,6 +5,7 @@ const jsonEnvironment = ( SERVER_PORT: string; HOST: string; SIMULATION_SAVE_FILE_SEPARATOR: string; + DEBUG_TASKS: string; }; } ).environment; @@ -13,6 +14,7 @@ const SERVER_PORT = jsonEnvironment.SERVER_PORT; const HOST = jsonEnvironment.HOST; export const SIMULATION_SAVE_FILE_SEPARATOR = jsonEnvironment.SIMULATION_SAVE_FILE_SEPARATOR; +export const DEBUG_TASKS = jsonEnvironment.DEBUG_TASKS === 'true'; export const environment = { socketUrl: `:${SERVER_PORT}/`, diff --git a/python/multimodalsim_viewer/common/environments/.env b/python/multimodalsim_viewer/common/environments/.env index 78f4c7b3..c35c5b49 100644 --- a/python/multimodalsim_viewer/common/environments/.env +++ b/python/multimodalsim_viewer/common/environments/.env @@ -5,3 +5,4 @@ INPUT_DATA_DIRECTORY_PATH=data OUTPUT_DATA_DIRECTORY_PATH=output NUMBER_OF_UPDATES_BETWEEN_STATES=1000 NUMBER_OF_STATES_TO_SEND_AT_ONCE=1 +DEBUG_TASKS=true diff --git a/python/multimodalsim_viewer/server/data_manager.py b/python/multimodalsim_viewer/server/data_manager.py index f02f18e7..df2c3c06 100644 --- a/python/multimodalsim_viewer/server/data_manager.py +++ b/python/multimodalsim_viewer/server/data_manager.py @@ -292,7 +292,7 @@ def get_missing_states( # pylint: disable=too-many-locals, too-many-branches, t visualization_time: float, complete_state_update_indexes: list[int], is_simulation_complete: bool, - ) -> tuple[list[dict], dict[list[str]], bool]: + ) -> tuple[list[dict], bool]: sorted_states = SimulationVisualizationDataManager.get_sorted_states(simulation_id) if len(sorted_states) == 0: @@ -321,7 +321,6 @@ def get_missing_states( # pylint: disable=too-many-locals, too-many-branches, t necessary_state_index = max(0, necessary_state_index) missing_states = [] - missing_updates = {} has_incomplete_states = False # We want to load the necessary state first, followed by @@ -361,20 +360,25 @@ def get_missing_states( # pylint: disable=too-many-locals, too-many-branches, t has_incomplete_states = True state["isComplete"] = is_complete - missing_states.append(state) - updates_data = file.readlines() current_state_updates = [] for update_data in updates_data: current_state_updates.append(update_data) - missing_updates[update_index] = current_state_updates + missing_states.append( + { + "startTimestamp": state_timestamp, + "startUpdateIndex": update_index, + "environment": state, + "updates": current_state_updates, + } + ) has_all_states = ( len(missing_states) + len(complete_state_update_indexes) == len(sorted_states) and not has_incomplete_states ) - return (missing_states, missing_updates, has_all_states) + return (missing_states, has_all_states) # MARK: +- Polylines diff --git a/python/multimodalsim_viewer/server/simulation_manager.py b/python/multimodalsim_viewer/server/simulation_manager.py index f9a0992e..ae570dcb 100644 --- a/python/multimodalsim_viewer/server/simulation_manager.py +++ b/python/multimodalsim_viewer/server/simulation_manager.py @@ -556,7 +556,7 @@ def emit_missing_simulation_states( ) -> None: try: - (missing_states, missing_updates, has_all_states) = SimulationVisualizationDataManager.get_missing_states( + (missing_states, has_all_states) = SimulationVisualizationDataManager.get_missing_states( simulation_id, visualization_time, complete_state_update_indexes, @@ -565,7 +565,7 @@ def emit_missing_simulation_states( self.socketio.emit( "missing-simulation-states", - (missing_states, missing_updates, has_all_states), + (missing_states, has_all_states), to=get_session_id(), ) diff --git a/python/multimodalsim_viewer/ui/static/environment.json b/python/multimodalsim_viewer/ui/static/environment.json index acc41469..9631949a 100644 --- a/python/multimodalsim_viewer/ui/static/environment.json +++ b/python/multimodalsim_viewer/ui/static/environment.json @@ -1,5 +1,6 @@ { "CLIENT_PORT": "8085", + "DEBUG_TASKS": "true", "HOST": "127.0.0.1", "INPUT_DATA_DIRECTORY_PATH": "data", "NUMBER_OF_STATES_TO_SEND_AT_ONCE": "1", From 70cdc40f06b3279b1e3849ba32e0ab078b417c47 Mon Sep 17 00:00:00 2001 From: Leo-Marbehan <90802811+Leo-Marbehan@users.noreply.github.com> Date: Tue, 7 Oct 2025 19:07:29 -0400 Subject: [PATCH 03/10] remove debug environment variable --- .env | 1 - multimodal-ui/public/environment.json | 1 - python/multimodalsim_viewer/common/environments/.env | 1 - python/multimodalsim_viewer/ui/static/environment.json | 1 - 4 files changed, 4 deletions(-) diff --git a/.env b/.env index c35c5b49..78f4c7b3 100644 --- a/.env +++ b/.env @@ -5,4 +5,3 @@ INPUT_DATA_DIRECTORY_PATH=data OUTPUT_DATA_DIRECTORY_PATH=output NUMBER_OF_UPDATES_BETWEEN_STATES=1000 NUMBER_OF_STATES_TO_SEND_AT_ONCE=1 -DEBUG_TASKS=true diff --git a/multimodal-ui/public/environment.json b/multimodal-ui/public/environment.json index 868eee7d..c3731a7d 100644 --- a/multimodal-ui/public/environment.json +++ b/multimodal-ui/public/environment.json @@ -6,6 +6,5 @@ "OUTPUT_DATA_DIRECTORY_PATH": "output", "NUMBER_OF_UPDATES_BETWEEN_STATES": "1000", "NUMBER_OF_STATES_TO_SEND_AT_ONCE": "1", - "DEBUG_TASKS": "true", "HOST": "127.0.0.1" } diff --git a/python/multimodalsim_viewer/common/environments/.env b/python/multimodalsim_viewer/common/environments/.env index c35c5b49..78f4c7b3 100644 --- a/python/multimodalsim_viewer/common/environments/.env +++ b/python/multimodalsim_viewer/common/environments/.env @@ -5,4 +5,3 @@ INPUT_DATA_DIRECTORY_PATH=data OUTPUT_DATA_DIRECTORY_PATH=output NUMBER_OF_UPDATES_BETWEEN_STATES=1000 NUMBER_OF_STATES_TO_SEND_AT_ONCE=1 -DEBUG_TASKS=true diff --git a/python/multimodalsim_viewer/ui/static/environment.json b/python/multimodalsim_viewer/ui/static/environment.json index 9631949a..acc41469 100644 --- a/python/multimodalsim_viewer/ui/static/environment.json +++ b/python/multimodalsim_viewer/ui/static/environment.json @@ -1,6 +1,5 @@ { "CLIENT_PORT": "8085", - "DEBUG_TASKS": "true", "HOST": "127.0.0.1", "INPUT_DATA_DIRECTORY_PATH": "data", "NUMBER_OF_STATES_TO_SEND_AT_ONCE": "1", From 30032c9269adf5c19474eb7ce6709bdae5eb308a Mon Sep 17 00:00:00 2001 From: Leo-Marbehan <90802811+Leo-Marbehan@users.noreply.github.com> Date: Tue, 7 Oct 2025 20:44:36 -0400 Subject: [PATCH 04/10] add environment variable and update doc --- .env | 2 ++ README.md | 2 ++ multimodal-ui/public/environment.json | 2 ++ multimodal-ui/src/app/services/simulation.service.ts | 10 +++++----- multimodal-ui/src/environments/environment.ts | 4 ++++ python/multimodalsim_viewer/common/environments/.env | 2 ++ python/multimodalsim_viewer/ui/static/environment.json | 2 ++ 7 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 78f4c7b3..da9bb1fb 100644 --- a/.env +++ b/.env @@ -5,3 +5,5 @@ INPUT_DATA_DIRECTORY_PATH=data OUTPUT_DATA_DIRECTORY_PATH=output NUMBER_OF_UPDATES_BETWEEN_STATES=1000 NUMBER_OF_STATES_TO_SEND_AT_ONCE=1 +MAX_STATES_EXTRACTION_CONCURRENT_TASKS=10 +DEBUG_TASKS=false \ No newline at end of file diff --git a/README.md b/README.md index 4ab07be7..65425a75 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,8 @@ The most useful environment variables are `CLIENT_PORT`, `SERVER_PORT`, `INPUT_D - `OUTPUT_DATA_DIRECTORY_PATH` (default `output`): The path to the output data directory. - `NUMBER_OF_UPDATES_BETWEEN_STATES` (default `1000`): The number of updates between simulation states. The lower the number, the larger the save file will be. However, increasing this number may cause the animation to freeze when receiving new states from the server. - `NUMBER_OF_STATES_TO_SEND_AT_ONCE` (default `1`): The number of states to send at once. Increasing this number may cause the animation to freeze when receiving new states from the server. +- `MAX_STATES_EXTRACTION_CONCURRENT_TASKS` (default `10`): The maximum number of states being extracted at the same time. Increasing this number might impact the memory usage during the loading only for a slight increase in performance. +- `DEBUG_TASKS` (default `false`): If set to `true`, debug logs regarding the tasks will be printed in the client console. ## Frontend diff --git a/multimodal-ui/public/environment.json b/multimodal-ui/public/environment.json index c3731a7d..2e0cf521 100644 --- a/multimodal-ui/public/environment.json +++ b/multimodal-ui/public/environment.json @@ -6,5 +6,7 @@ "OUTPUT_DATA_DIRECTORY_PATH": "output", "NUMBER_OF_UPDATES_BETWEEN_STATES": "1000", "NUMBER_OF_STATES_TO_SEND_AT_ONCE": "1", + "MAX_STATES_EXTRACTION_CONCURRENT_TASKS": "10", + "DEBUG_TASKS": "false", "HOST": "127.0.0.1" } diff --git a/multimodal-ui/src/app/services/simulation.service.ts b/multimodal-ui/src/app/services/simulation.service.ts index d0d3868f..983bda5f 100644 --- a/multimodal-ui/src/app/services/simulation.service.ts +++ b/multimodal-ui/src/app/services/simulation.service.ts @@ -6,7 +6,10 @@ import { Signal, WritableSignal, } from '@angular/core'; -import { DEBUG_TASKS } from '../../environments/environment'; +import { + DEBUG_TASKS, + MAX_STATES_EXTRACTION_CONCURRENT_TASKS, +} from '../../environments/environment'; import { ContinuousEnvironment, ContinuousEnvironmentReferences, @@ -36,8 +39,6 @@ interface DebounceSettings { }) export class SimulationService { // MARK: Properties - private readonly MAX_STATES_EXTRACTION_CONCURRENT_TASKS = 10; - private readonly stateExtractionTasks = new Map< number, { task: Task; startTimestamp: number } @@ -233,8 +234,7 @@ export class SimulationService { const continuousEnvironments = this.continuousEnvironmentsSignal(); if ( - this.stateExtractionTasks.size >= - this.MAX_STATES_EXTRACTION_CONCURRENT_TASKS + this.stateExtractionTasks.size >= MAX_STATES_EXTRACTION_CONCURRENT_TASKS ) { return; } diff --git a/multimodal-ui/src/environments/environment.ts b/multimodal-ui/src/environments/environment.ts index 5b45cb5b..85cf9459 100644 --- a/multimodal-ui/src/environments/environment.ts +++ b/multimodal-ui/src/environments/environment.ts @@ -6,6 +6,7 @@ const jsonEnvironment = ( HOST: string; SIMULATION_SAVE_FILE_SEPARATOR: string; DEBUG_TASKS: string; + MAX_STATES_EXTRACTION_CONCURRENT_TASKS: string; }; } ).environment; @@ -15,6 +16,9 @@ const HOST = jsonEnvironment.HOST; export const SIMULATION_SAVE_FILE_SEPARATOR = jsonEnvironment.SIMULATION_SAVE_FILE_SEPARATOR; export const DEBUG_TASKS = jsonEnvironment.DEBUG_TASKS === 'true'; +export const MAX_STATES_EXTRACTION_CONCURRENT_TASKS = Number( + jsonEnvironment.MAX_STATES_EXTRACTION_CONCURRENT_TASKS, +); export const environment = { socketUrl: `:${SERVER_PORT}/`, diff --git a/python/multimodalsim_viewer/common/environments/.env b/python/multimodalsim_viewer/common/environments/.env index 78f4c7b3..da9bb1fb 100644 --- a/python/multimodalsim_viewer/common/environments/.env +++ b/python/multimodalsim_viewer/common/environments/.env @@ -5,3 +5,5 @@ INPUT_DATA_DIRECTORY_PATH=data OUTPUT_DATA_DIRECTORY_PATH=output NUMBER_OF_UPDATES_BETWEEN_STATES=1000 NUMBER_OF_STATES_TO_SEND_AT_ONCE=1 +MAX_STATES_EXTRACTION_CONCURRENT_TASKS=10 +DEBUG_TASKS=false \ No newline at end of file diff --git a/python/multimodalsim_viewer/ui/static/environment.json b/python/multimodalsim_viewer/ui/static/environment.json index acc41469..b3d75a2b 100644 --- a/python/multimodalsim_viewer/ui/static/environment.json +++ b/python/multimodalsim_viewer/ui/static/environment.json @@ -1,7 +1,9 @@ { "CLIENT_PORT": "8085", + "DEBUG_TASKS": "false", "HOST": "127.0.0.1", "INPUT_DATA_DIRECTORY_PATH": "data", + "MAX_STATES_EXTRACTION_CONCURRENT_TASKS": "10", "NUMBER_OF_STATES_TO_SEND_AT_ONCE": "1", "NUMBER_OF_UPDATES_BETWEEN_STATES": "1000", "OUTPUT_DATA_DIRECTORY_PATH": "output", From c68c83ea443421d5bf6409de2f49d348ac9c4043 Mon Sep 17 00:00:00 2001 From: Leo-Marbehan <90802811+Leo-Marbehan@users.noreply.github.com> Date: Tue, 7 Oct 2025 20:45:25 -0400 Subject: [PATCH 05/10] fix lint --- python/multimodalsim_viewer/server/data_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/multimodalsim_viewer/server/data_manager.py b/python/multimodalsim_viewer/server/data_manager.py index df2c3c06..70708b38 100644 --- a/python/multimodalsim_viewer/server/data_manager.py +++ b/python/multimodalsim_viewer/server/data_manager.py @@ -296,11 +296,11 @@ def get_missing_states( # pylint: disable=too-many-locals, too-many-branches, t sorted_states = SimulationVisualizationDataManager.get_sorted_states(simulation_id) if len(sorted_states) == 0: - return [], {}, False + return [], False if len(complete_state_update_indexes) == len(sorted_states): # If the client has all states, no need to request more - return [], {}, True + return [], True necessary_state_index = None From 9fe839909f478683a467ea95a677da537337cfe2 Mon Sep 17 00:00:00 2001 From: Leo-Marbehan <90802811+Leo-Marbehan@users.noreply.github.com> Date: Sat, 11 Oct 2025 17:44:28 -0400 Subject: [PATCH 06/10] fix update priority --- .../src/app/interfaces/performances.model.ts | 8 +++++ .../src/app/interfaces/task.model.ts | 21 ++++++++++-- .../src/app/services/simulation.service.ts | 34 +++++++++++++------ 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/multimodal-ui/src/app/interfaces/performances.model.ts b/multimodal-ui/src/app/interfaces/performances.model.ts index df87b0a5..78e1f4f1 100644 --- a/multimodal-ui/src/app/interfaces/performances.model.ts +++ b/multimodal-ui/src/app/interfaces/performances.model.ts @@ -13,6 +13,14 @@ export class SortedList { return this._items.shift(); } + public remove(item: T): void { + const index = this._items.indexOf(item); + + if (index !== -1) { + this._items.splice(index, 1); + } + } + public get length(): number { return this._items.length; } diff --git a/multimodal-ui/src/app/interfaces/task.model.ts b/multimodal-ui/src/app/interfaces/task.model.ts index 7c2923f9..d611685b 100644 --- a/multimodal-ui/src/app/interfaces/task.model.ts +++ b/multimodal-ui/src/app/interfaces/task.model.ts @@ -13,10 +13,14 @@ export function emptyTaskQueue(): SortedList { * Tasks should be quick to process. */ export abstract class Task { + protected _priority: number; + constructor( - public priority: number, + priority: number, protected readonly queue: SortedList, - ) {} + ) { + this._priority = priority; + } public addToQueue(): void { this.queue.add(this); @@ -33,6 +37,19 @@ export abstract class Task { public get numberOfTasks(): number { return 1; } + + public get priority(): number { + return this._priority; + } + + public updatePriority(newPriority: number): void { + if (this._priority !== newPriority) { + this.queue.remove(this); + + this._priority = newPriority; + this.addToQueue(); + } + } } /** diff --git a/multimodal-ui/src/app/services/simulation.service.ts b/multimodal-ui/src/app/services/simulation.service.ts index 983bda5f..c4ba8c76 100644 --- a/multimodal-ui/src/app/services/simulation.service.ts +++ b/multimodal-ui/src/app/services/simulation.service.ts @@ -265,12 +265,12 @@ export class SimulationService { private updateTasksPriority(wantedVisualizationTime: number) { let mostUrgentTask: { task: Task; startTimestamp: number } | null = null; - for (const task of this.stateExtractionTasks.values()) { - task.task.priority = EXTRACT_STATE_TASK_PRIORITY; + let previousProximity = Infinity; - const proximity = wantedVisualizationTime - task.startTimestamp; + for (const task of this.stateExtractionTasks.values()) { + task.task.updatePriority(EXTRACT_STATE_TASK_PRIORITY); - const previousProximity = mostUrgentTask === null ? Infinity : proximity; + const proximity = task.startTimestamp - wantedVisualizationTime; /** * The most urgent task is the one with a start time before the wanted @@ -285,18 +285,20 @@ export class SimulationService { proximity < previousProximity ) { mostUrgentTask = task; + + previousProximity = proximity; } } if (mostUrgentTask) { - mostUrgentTask.task.priority = EXTRACT_STATE_TASK_PRIORITY + 1; + mostUrgentTask.task.updatePriority(EXTRACT_STATE_TASK_PRIORITY + 1); } if (DEBUG_TASKS) { console.debug('updateTasksPriority', { wantedVisualizationTime, mostUrgentTask, - stateExtractionTasks: this.stateExtractionTasks, + stateExtractionTasks: Array.from(this.stateExtractionTasks.entries()), }); } } @@ -457,7 +459,10 @@ export class SimulationService { }); if (DEBUG_TASKS) { - console.debug('stateExtractionTasks', this.stateExtractionTasks); + console.debug( + 'stateExtractionTasks', + Array.from(this.stateExtractionTasks.entries()), + ); } } @@ -490,7 +495,10 @@ export class SimulationService { this.stateExtractionTasks.delete(startUpdateIndex); if (DEBUG_TASKS) { - console.debug('stateExtractionTasks', this.stateExtractionTasks); + console.debug( + 'stateExtractionTasks', + Array.from(this.stateExtractionTasks.entries()), + ); } return; @@ -510,7 +518,10 @@ export class SimulationService { }); if (DEBUG_TASKS) { - console.debug('stateExtractionTasks', this.stateExtractionTasks); + console.debug( + 'stateExtractionTasks', + Array.from(this.stateExtractionTasks.entries()), + ); } } @@ -528,7 +539,10 @@ export class SimulationService { this.stateExtractionTasks.delete(startUpdateIndex); if (DEBUG_TASKS) { - console.debug('stateExtractionTasks', this.stateExtractionTasks); + console.debug( + 'stateExtractionTasks', + Array.from(this.stateExtractionTasks.entries()), + ); } this._continuousEnvironmentsSignal.update((environments) => { From c2ca85b950de26d698495542f4ecdb996a4cd5f7 Mon Sep 17 00:00:00 2001 From: Leo-Marbehan <90802811+Leo-Marbehan@users.noreply.github.com> Date: Sat, 11 Oct 2025 17:56:00 -0400 Subject: [PATCH 07/10] use binary search when removing --- .../src/app/interfaces/performances.model.ts | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/multimodal-ui/src/app/interfaces/performances.model.ts b/multimodal-ui/src/app/interfaces/performances.model.ts index 78e1f4f1..6b7ed13a 100644 --- a/multimodal-ui/src/app/interfaces/performances.model.ts +++ b/multimodal-ui/src/app/interfaces/performances.model.ts @@ -14,7 +14,14 @@ export class SortedList { } public remove(item: T): void { - const index = this._items.indexOf(item); + const startIndex = this.findFirstEqualIndex(item); + + if (startIndex === null) { + // Item not in list + return; + } + + const index = this._items.indexOf(item, startIndex); if (index !== -1) { this._items.splice(index, 1); @@ -52,4 +59,36 @@ export class SortedList { return low; // Insertion point } + + private findFirstEqualIndex(item: T): number | null { + let low = 0; + let high = this._items.length - 1; + + while (low < high) { + const mid = Math.floor((low + high) / 2); + const comparison = this.compare(this._items[mid], item); + + if (comparison < 0) { + low = mid + 1; + } else if (comparison > 0) { + high = mid - 1; + } else { + high = mid; + } + } + + const lowComparison = this.compare(this._items[low], item); + + if (lowComparison === 0) { + return low; + } + + const highComparison = this.compare(this._items[high], item); + + if (highComparison === 0) { + return high; + } + + return null; + } } From 1bde7109a5f1fe10adc400f09321a3b0bd274831 Mon Sep 17 00:00:00 2001 From: Leo-Marbehan <90802811+Leo-Marbehan@users.noreply.github.com> Date: Sat, 18 Oct 2025 18:18:15 -0400 Subject: [PATCH 08/10] add tests and fix priority update --- .github/workflows/angular-tests.yml | 34 ++++ multimodal-ui/angular.json | 9 +- .../src/app/interfaces/task.model.ts | 5 +- .../app/services/simulation.service.spec.ts | 185 ++++++++++++++++++ .../src/app/services/simulation.service.ts | 18 +- .../src/app/services/task.service.ts | 16 +- .../src/environments/environment.test.ts | 8 + multimodal-ui/tsconfig.app.json | 4 +- multimodal-ui/tsconfig.json | 10 +- multimodal-ui/tsconfig.spec.json | 2 +- 10 files changed, 267 insertions(+), 24 deletions(-) create mode 100644 .github/workflows/angular-tests.yml create mode 100644 multimodal-ui/src/app/services/simulation.service.spec.ts create mode 100644 multimodal-ui/src/environments/environment.test.ts diff --git a/.github/workflows/angular-tests.yml b/.github/workflows/angular-tests.yml new file mode 100644 index 00000000..2db9071c --- /dev/null +++ b/.github/workflows/angular-tests.yml @@ -0,0 +1,34 @@ +name: Angular Tests + +on: + push: + branches: + - main + paths: + - 'python/**' + pull_request: + branches: + - main + paths: + - 'python/**' + +jobs: + verify-python-format-and-lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: '22' + + - name: Install dependencies + run: npm install + working-directory: ./multimodal-ui + + - name: Run tests + run: npm run test + working-directory: ./multimodal-ui diff --git a/multimodal-ui/angular.json b/multimodal-ui/angular.json index f583dd8d..212363c1 100644 --- a/multimodal-ui/angular.json +++ b/multimodal-ui/angular.json @@ -26,9 +26,6 @@ }, "@schematics/angular:resolver": { "skipTests": true - }, - "@schematics/angular:service": { - "skipTests": true } }, "root": "", @@ -114,6 +111,12 @@ "@angular/material/prebuilt-themes/azure-blue.css", "src/styles.scss" ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.test.ts" + } + ], "scripts": [] } }, diff --git a/multimodal-ui/src/app/interfaces/task.model.ts b/multimodal-ui/src/app/interfaces/task.model.ts index d611685b..f61e8792 100644 --- a/multimodal-ui/src/app/interfaces/task.model.ts +++ b/multimodal-ui/src/app/interfaces/task.model.ts @@ -1,7 +1,7 @@ import { SortedList } from './performances.model'; export const EXTRACT_STATE_TASK_PRIORITY = 1; -export const BUILD_CONTINUOUS_ENVIRONMENT_TASK_PRIORITY = 2; +export const BUILD_CONTINUOUS_ENVIRONMENT_TASK_PRIORITY = 1; export function emptyTaskQueue(): SortedList { return new SortedList((a, b) => b.priority - a.priority); @@ -22,8 +22,9 @@ export abstract class Task { this._priority = priority; } - public addToQueue(): void { + public addToQueue(this: T): T { this.queue.add(this); + return this; } /** diff --git a/multimodal-ui/src/app/services/simulation.service.spec.ts b/multimodal-ui/src/app/services/simulation.service.spec.ts new file mode 100644 index 00000000..396754ed --- /dev/null +++ b/multimodal-ui/src/app/services/simulation.service.spec.ts @@ -0,0 +1,185 @@ +import { TestBed } from '@angular/core/testing'; +import { SortedList } from '../interfaces/performances.model'; +import { + AtomicTask, + emptyTaskQueue, + EXTRACT_STATE_TASK_PRIORITY, + Task, +} from '../interfaces/task.model'; +import { CommunicationService } from './communication.service'; +import { DataService } from './data.service'; +import { SimulationService, StateExtractionTask } from './simulation.service'; +import { TaskService } from './task.service'; +import { TimerService } from './timer.service'; + +function createTestTask(queue: SortedList, priority = 0): Task { + // eslint-disable-next-line @typescript-eslint/no-empty-function + return new AtomicTask(priority, queue, () => {}).addToQueue(); +} + +describe('SimulationService', () => { + let service: SimulationService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: DataService, useValue: {} }, + { provide: CommunicationService, useValue: {} }, + { provide: TaskService, useValue: {} }, + { provide: TimerService, useValue: {} }, + ], + }); + + service = TestBed.inject(SimulationService); + }); + + describe('updateTasksPriority', () => { + let wantedVisualizationTime: number; + + let taskAfter1: StateExtractionTask; + let taskAfter2: StateExtractionTask; + + let taskBefore1: StateExtractionTask; + let taskBefore2: StateExtractionTask; + + let taskEqual1: StateExtractionTask; + let taskEqual2: StateExtractionTask; + + let queue: SortedList; + + beforeEach(() => { + wantedVisualizationTime = 50; + + queue = emptyTaskQueue(); + + taskBefore1 = { + task: createTestTask(queue, EXTRACT_STATE_TASK_PRIORITY), + startTimestamp: wantedVisualizationTime - 2, + startUpdateIndex: 0, + }; + taskBefore2 = { + task: createTestTask(queue, EXTRACT_STATE_TASK_PRIORITY), + startTimestamp: wantedVisualizationTime - 1, + startUpdateIndex: 1, + }; + + taskEqual1 = { + task: createTestTask(queue, EXTRACT_STATE_TASK_PRIORITY), + startTimestamp: wantedVisualizationTime, + startUpdateIndex: 2, + }; + taskEqual2 = { + task: createTestTask(queue, EXTRACT_STATE_TASK_PRIORITY), + startTimestamp: wantedVisualizationTime, + startUpdateIndex: 3, + }; + + taskAfter1 = { + task: createTestTask(queue, EXTRACT_STATE_TASK_PRIORITY), + startTimestamp: wantedVisualizationTime + 1, + startUpdateIndex: 4, + }; + taskAfter2 = { + task: createTestTask(queue, EXTRACT_STATE_TASK_PRIORITY), + startTimestamp: wantedVisualizationTime + 2, + startUpdateIndex: 5, + }; + }); + + describe('when all tasks are after the wanted visualization time', () => { + beforeEach(() => { + service['stateExtractionTasks'].set(1, taskAfter1); + service['stateExtractionTasks'].set(2, taskAfter2); + }); + + it('should make the closest task the most urgent', () => { + service['updateTasksPriority'](wantedVisualizationTime); + + expect(taskAfter1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY + 1); + expect(taskAfter2.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + }); + }); + + describe('when all tasks are before the wanted visualization time', () => { + beforeEach(() => { + service['stateExtractionTasks'].set(2, taskBefore1); + service['stateExtractionTasks'].set(1, taskBefore2); + }); + + it('should make the closest task the most urgent', () => { + service['updateTasksPriority'](wantedVisualizationTime); + + expect(taskBefore1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + expect(taskBefore2.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY + 1); + }); + }); + + describe('when there are tasks before and after the wanted visualization time and the closest task is the one after', () => { + beforeEach(() => { + service['stateExtractionTasks'].set(1, taskBefore1); + service['stateExtractionTasks'].set(2, taskAfter1); + }); + + it('should make the task before the most urgent', () => { + service['updateTasksPriority'](wantedVisualizationTime); + + expect(taskBefore1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY + 1); + expect(taskAfter1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + }); + }); + + describe('when there are tasks before and after the wanted visualization time', () => { + beforeEach(() => { + service['stateExtractionTasks'].set(2, taskBefore1); + service['stateExtractionTasks'].set(1, taskBefore2); + service['stateExtractionTasks'].set(3, taskAfter1); + service['stateExtractionTasks'].set(4, taskAfter2); + }); + + it('should make the closest task before the most urgent', () => { + service['updateTasksPriority'](wantedVisualizationTime); + + expect(taskBefore1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + expect(taskBefore2.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY + 1); + expect(taskAfter1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + expect(taskAfter2.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + }); + }); + + describe('when there are tasks with the same start time as the wanted visualization time', () => { + beforeEach(() => { + service['stateExtractionTasks'].set(1, taskEqual1); + service['stateExtractionTasks'].set(2, taskEqual2); + }); + + it('should make the task with the greatest update index the most urgent', () => { + service['updateTasksPriority'](wantedVisualizationTime); + + expect(taskEqual1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + expect(taskEqual2.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY + 1); + }); + }); + + describe('when there are tasks before, after and at the wanted visualization time', () => { + beforeEach(() => { + service['stateExtractionTasks'].set(2, taskBefore1); + service['stateExtractionTasks'].set(1, taskBefore2); + service['stateExtractionTasks'].set(1, taskEqual1); + service['stateExtractionTasks'].set(2, taskEqual2); + service['stateExtractionTasks'].set(3, taskAfter1); + service['stateExtractionTasks'].set(4, taskAfter2); + }); + + it('should make the task at the wanted visualization time with the greatest update index the most urgent', () => { + service['updateTasksPriority'](wantedVisualizationTime); + + expect(taskBefore1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + expect(taskBefore2.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + expect(taskEqual1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + expect(taskEqual2.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY + 1); + expect(taskAfter1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + expect(taskAfter2.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + }); + }); + }); +}); diff --git a/multimodal-ui/src/app/services/simulation.service.ts b/multimodal-ui/src/app/services/simulation.service.ts index c4ba8c76..417dbac3 100644 --- a/multimodal-ui/src/app/services/simulation.service.ts +++ b/multimodal-ui/src/app/services/simulation.service.ts @@ -34,6 +34,12 @@ interface DebounceSettings { timeoutId: ReturnType | null; } +export interface StateExtractionTask { + task: Task; + startTimestamp: number; + startUpdateIndex: number; +} + @Injectable({ providedIn: 'root', }) @@ -41,7 +47,7 @@ export class SimulationService { // MARK: Properties private readonly stateExtractionTasks = new Map< number, - { task: Task; startTimestamp: number } + StateExtractionTask >(); private readonly _activeSimulationIdSignal: WritableSignal = @@ -263,7 +269,7 @@ export class SimulationService { } private updateTasksPriority(wantedVisualizationTime: number) { - let mostUrgentTask: { task: Task; startTimestamp: number } | null = null; + let mostUrgentTask: StateExtractionTask | null = null; let previousProximity = Infinity; @@ -282,7 +288,11 @@ export class SimulationService { if ( (proximity < 0 && (previousProximity > 0 || proximity > previousProximity)) || - proximity < previousProximity + (proximity > 0 && proximity < previousProximity) || + (proximity === 0 && + (previousProximity !== 0 || + (mostUrgentTask?.startUpdateIndex ?? -Infinity) < + task.startUpdateIndex)) ) { mostUrgentTask = task; @@ -456,6 +466,7 @@ export class SimulationService { this.stateExtractionTasks.set(startUpdateIndex, { task, startTimestamp, + startUpdateIndex, }); if (DEBUG_TASKS) { @@ -515,6 +526,7 @@ export class SimulationService { ), ), startTimestamp, + startUpdateIndex, }); if (DEBUG_TASKS) { diff --git a/multimodal-ui/src/app/services/task.service.ts b/multimodal-ui/src/app/services/task.service.ts index 7a8a1a44..a1654e95 100644 --- a/multimodal-ui/src/app/services/task.service.ts +++ b/multimodal-ui/src/app/services/task.service.ts @@ -44,16 +44,12 @@ export class TaskService { serializedUpdates: unknown, callback: (states: SimulationState | null) => void, ): ExtractStateTask { - const task = new ExtractStateTask( + return new ExtractStateTask( this.queue, serializedEnvironment, serializedUpdates, callback, - ); - - task.addToQueue(); - - return task; + ).addToQueue(); } buildContinuousEnvironmentTask( @@ -61,16 +57,12 @@ export class TaskService { references: ContinuousEnvironmentReferences, callback: (environment: ContinuousEnvironment) => void, ): BuildContinuousEnvironmentTask { - const task = new BuildContinuousEnvironmentTask( + return new BuildContinuousEnvironmentTask( this.queue, states, references, callback, - ); - - task.addToQueue(); - - return task; + ).addToQueue(); } // For debugging purposes diff --git a/multimodal-ui/src/environments/environment.test.ts b/multimodal-ui/src/environments/environment.test.ts new file mode 100644 index 00000000..d1c4be4e --- /dev/null +++ b/multimodal-ui/src/environments/environment.test.ts @@ -0,0 +1,8 @@ +export const SIMULATION_SAVE_FILE_SEPARATOR = '---'; +export const DEBUG_TASKS = false; +export const MAX_STATES_EXTRACTION_CONCURRENT_TASKS = 10; + +export const environment = { + socketUrl: '', + apiUrl: '', +}; diff --git a/multimodal-ui/tsconfig.app.json b/multimodal-ui/tsconfig.app.json index 8886e903..a0dcc37c 100644 --- a/multimodal-ui/tsconfig.app.json +++ b/multimodal-ui/tsconfig.app.json @@ -6,6 +6,6 @@ "outDir": "./out-tsc/app", "types": [] }, - "files": ["src/main.ts"], - "include": ["src/**/*.d.ts"] + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] } diff --git a/multimodal-ui/tsconfig.json b/multimodal-ui/tsconfig.json index ab3965ce..e45be3ec 100644 --- a/multimodal-ui/tsconfig.json +++ b/multimodal-ui/tsconfig.json @@ -28,5 +28,13 @@ }, "ts-node": { "compilerOptions": { "module": "commonjs" } - } + }, + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] } diff --git a/multimodal-ui/tsconfig.spec.json b/multimodal-ui/tsconfig.spec.json index e00e30e6..e9773211 100644 --- a/multimodal-ui/tsconfig.spec.json +++ b/multimodal-ui/tsconfig.spec.json @@ -6,5 +6,5 @@ "outDir": "./out-tsc/spec", "types": ["jasmine"] }, - "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] + "include": ["src/**/*.ts"] } From fae04cb3cdd1e956ae85d74d300b1579ed70376e Mon Sep 17 00:00:00 2001 From: Leo-Marbehan <90802811+Leo-Marbehan@users.noreply.github.com> Date: Sat, 18 Oct 2025 18:20:35 -0400 Subject: [PATCH 09/10] fix test workflow --- .github/workflows/angular-tests.yml | 2 +- multimodal-ui/package.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/angular-tests.yml b/.github/workflows/angular-tests.yml index 2db9071c..8a9d43f3 100644 --- a/.github/workflows/angular-tests.yml +++ b/.github/workflows/angular-tests.yml @@ -30,5 +30,5 @@ jobs: working-directory: ./multimodal-ui - name: Run tests - run: npm run test + run: npm run test:ci working-directory: ./multimodal-ui diff --git a/multimodal-ui/package.json b/multimodal-ui/package.json index a041a7bf..75be5138 100644 --- a/multimodal-ui/package.json +++ b/multimodal-ui/package.json @@ -7,7 +7,8 @@ "build": "npm run set-env && ng build", "set-env": "ts-node scripts/set-environment.ts && prettier --write ./angular.json ./public/environment.json", "watch": "ng build --watch --configuration development", - "test": "ng test", + "test": "ng test --poll 1000", + "test:ci": "ng test --no-watch --no-progress --browsers=ChromeHeadless", "lint": "ng lint", "format": "prettier --write .", "format:check": "prettier --check ." From eb88fa91a1b241385fb3c78f16e73dd9814fc3ef Mon Sep 17 00:00:00 2001 From: Leo-Marbehan <90802811+Leo-Marbehan@users.noreply.github.com> Date: Sun, 19 Oct 2025 11:58:16 -0400 Subject: [PATCH 10/10] rename test workflow --- .github/workflows/angular-tests.yml | 2 +- multimodal-ui/tsconfig.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/angular-tests.yml b/.github/workflows/angular-tests.yml index 8a9d43f3..043e566a 100644 --- a/.github/workflows/angular-tests.yml +++ b/.github/workflows/angular-tests.yml @@ -13,7 +13,7 @@ on: - 'python/**' jobs: - verify-python-format-and-lint: + angular-tests: runs-on: ubuntu-latest steps: diff --git a/multimodal-ui/tsconfig.json b/multimodal-ui/tsconfig.json index e45be3ec..081ec4b6 100644 --- a/multimodal-ui/tsconfig.json +++ b/multimodal-ui/tsconfig.json @@ -29,6 +29,7 @@ "ts-node": { "compilerOptions": { "module": "commonjs" } }, + "files": [], "references": [ { "path": "./tsconfig.app.json"