From eef5cd1cc35ff37f13a09a3cd13e3f57300fe6c3 Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 1 Apr 2024 21:26:12 -0700 Subject: [PATCH 01/43] Add Task combinators to simplify makeTaskFactory --- .../aerie/merlin/driver/SimulationDriver.java | 111 +++++++------- .../jpl/aerie/merlin/protocol/model/Task.java | 144 +++++++++++++++++- .../merlin/protocol/model/TaskFactory.java | 22 ++- 3 files changed, 214 insertions(+), 63 deletions(-) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDriver.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDriver.java index bf0a8cbe3c..900e7da930 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDriver.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDriver.java @@ -5,16 +5,16 @@ import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; -import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; import org.apache.commons.lang3.tuple.Pair; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -209,30 +209,23 @@ void simulateTask(final MissionModel missionModel, final TaskFactory void scheduleActivities( final Map schedule, final HashMap>> resolved, final MissionModel missionModel, final SimulationEngine engine, final Topic activityTopic - ) - { - if(resolved.get(null) == null) { return; } // Nothing to simulate - + ) { + if (resolved.get(null) == null) { + // Nothing to simulate + return; + } for (final Pair directivePair : resolved.get(null)) { final var directiveId = directivePair.getLeft(); final var startOffset = directivePair.getRight(); final var serializedDirective = schedule.get(directiveId).serializedActivity(); - final TaskFactory task; - try { - task = missionModel.getTaskFactory(serializedDirective); - } catch (final InstantiationException ex) { - // All activity instantiations are assumed to be validated by this point - throw new Error("Unexpected state: activity instantiation %s failed with: %s" - .formatted(serializedDirective.getTypeName(), ex.toString())); - } + final TaskFactory task = deserializeActivity(missionModel, serializedDirective); engine.scheduleTask(startOffset, makeTaskFactory( directiveId, @@ -247,52 +240,54 @@ private static void scheduleActivities( private static TaskFactory makeTaskFactory( final ActivityDirectiveId directiveId, - final TaskFactory task, + final TaskFactory taskFactory, final Map schedule, final HashMap>> resolved, final MissionModel missionModel, final Topic activityTopic - ) - { - // Emit the current activity (defined by directiveId) - return executor -> scheduler0 -> TaskStatus.calling(InSpan.Fresh, (TaskFactory) (executor1 -> scheduler1 -> { - scheduler1.emit(directiveId, activityTopic); - return task.create(executor1).step(scheduler1); - }), scheduler2 -> { - // When the current activity finishes, get the list of the activities that needed this activity to finish to know their start time - final List> dependents = resolved.get(directiveId) == null ? List.of() : resolved.get(directiveId); - // Iterate over the dependents - for (final var dependent : dependents) { - scheduler2.spawn(InSpan.Parent, executor2 -> scheduler3 -> - // Delay until the dependent starts - TaskStatus.delayed(dependent.getRight(), scheduler4 -> { - final var dependentDirectiveId = dependent.getLeft(); - final var serializedDependentDirective = schedule.get(dependentDirectiveId).serializedActivity(); - - // Initialize the Task for the dependent - final TaskFactory dependantTask; - try { - dependantTask = missionModel.getTaskFactory(serializedDependentDirective); - } catch (final InstantiationException ex) { - // All activity instantiations are assumed to be validated by this point - throw new Error("Unexpected state: activity instantiation %s failed with: %s" - .formatted(serializedDependentDirective.getTypeName(), ex.toString())); - } - - // Schedule the dependent - // When it finishes, it will schedule the activities depending on it to know their start time - scheduler4.spawn(InSpan.Parent, makeTaskFactory( - dependentDirectiveId, - dependantTask, - schedule, - resolved, - missionModel, - activityTopic - )); - return TaskStatus.completed(Unit.UNIT); - })); - } - return TaskStatus.completed(Unit.UNIT); - }); + ) { + record Dependent(Duration offset, TaskFactory task) {} + + final List dependents = new ArrayList<>(); + for (final var pair : resolved.getOrDefault(directiveId, List.of())) { + dependents.add(new Dependent( + pair.getRight(), + makeTaskFactory( + pair.getLeft(), + deserializeActivity(missionModel, schedule.get(pair.getLeft()).serializedActivity()), + schedule, + resolved, + missionModel, + activityTopic))); + } + + return executor -> { + final var task = taskFactory.create(executor); + return Task + .callingWithSpan( + Task.emitting(directiveId, activityTopic) + .andThen(task)) + .andThen( + Task.spawning( + dependents + .stream() + .map( + dependent -> + TaskFactory.delaying(dependent.offset()) + .andThen(dependent.task())) + .toList())); + }; + } + + private static TaskFactory deserializeActivity(MissionModel missionModel, SerializedActivity serializedDirective) { + final TaskFactory task; + try { + task = missionModel.getTaskFactory(serializedDirective); + } catch (final InstantiationException ex) { + // All activity instantiations are assumed to be validated by this point + throw new Error("Unexpected state: activity instantiation %s failed with: %s" + .formatted(serializedDirective.getTypeName(), ex.toString())); + } + return task; } } diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java index 577cc3aeea..ccb2be62d2 100644 --- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java +++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java @@ -1,16 +1,24 @@ package gov.nasa.jpl.aerie.merlin.protocol.model; import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; +import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; -public interface Task { +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; + +public interface Task { /** * Perform one step of the task, returning the next step of the task and the conditions under which to perform it. * *

Clients must only call {@code step()} at most once, and must not invoke {@code step()} after {@link #release()} * has been invoked.

*/ - TaskStatus step(Scheduler scheduler); + TaskStatus step(Scheduler scheduler); /** * Release any transient system resources allocated to this task. @@ -23,4 +31,136 @@ public interface Task { * nor shall {@link #step(Scheduler)} be called after this method.

*/ default void release() {} + + default Task andThen(Task task2) { + return new Task<>() { + @Override + public TaskStatus step(final Scheduler scheduler) { + switch (Task.this.step(scheduler)) { + case TaskStatus.Completed s -> { + return task2.step(scheduler); + } + case TaskStatus.AwaitingCondition s -> { + return new TaskStatus.AwaitingCondition<>(s.condition(), s.continuation().andThen(task2)); + } + case TaskStatus.CallingTask s -> { + return new TaskStatus.CallingTask<>(s.childSpan(), s.child(), s.continuation().andThen(task2)); + } + case TaskStatus.Delayed s -> { + return new TaskStatus.Delayed<>(s.delay(), s.continuation().andThen(task2)); + } + } + } + }; + } + + default Task dropOutput() { + return new Task<>() { + @Override + public TaskStatus step(final Scheduler scheduler) { + switch (this.step(scheduler)) { + case TaskStatus.Completed s -> { + return TaskStatus.completed(Unit.UNIT); + } + case TaskStatus.AwaitingCondition s -> { + return new TaskStatus.AwaitingCondition<>(s.condition(), s.continuation().dropOutput()); + } + case TaskStatus.CallingTask s -> { + return new TaskStatus.CallingTask<>(s.childSpan(), s.child(), s.continuation().dropOutput()); + } + case TaskStatus.Delayed s -> { + return new TaskStatus.Delayed<>(s.delay(), s.continuation().dropOutput()); + } + } + } + }; + } + + static Task calling(Task task) { + return new Task() { + @Override + public TaskStatus step(final Scheduler scheduler) { + return TaskStatus.calling(InSpan.Parent, (TaskFactory < Output >)executor -> task, Task.empty()); + } + }; + } + + static Task callingWithSpan(Task task) { + return new Task() { + @Override + public TaskStatus step(final Scheduler scheduler) { + return TaskStatus.calling(InSpan.Fresh, (TaskFactory) executor -> task, Task.empty()); + } + }; + } + + static Task delaying(Duration duration) { + return Task.of($ -> TaskStatus.delayed(duration, Task.empty())); + } + + static Task emitting(EventType eventType, Topic topic) { + return Task.run($ -> $.emit(eventType, topic)); + } + + static Task spawning(TaskFactory taskFactory) { + return Task.run($ -> $.spawn(InSpan.Parent, taskFactory)); + } + + static Task spawning(Consumer f) { + return Task.run($ -> $.spawn(InSpan.Parent, (TaskFactory) executor -> Task.run(f))); + } + + static Task spawningWithSpan(TaskFactory taskFactory) { + return Task.run($ -> $.spawn(InSpan.Fresh, taskFactory)); + } + + static Task spawningWithSpan(Consumer f) { + return Task.run($ -> $.spawn(InSpan.Fresh, (TaskFactory) executor -> Task.run(f))); + } + + static Task spawning(List> tasks) { + return Task.run($ -> { + for (final var task : tasks) { + $.spawn(InSpan.Fresh, task); + } + }); + } + + /** + * @param f Must not yield + * @return + */ + static Task run(Consumer f) { + return Task.evaluate(scheduler -> { + f.accept(scheduler); + return Unit.UNIT; + }); + } + + static Task evaluate(Function f) { + return new Task<>() { + @Override + public TaskStatus step(final Scheduler scheduler) { + return TaskStatus.completed(f.apply(scheduler)); + } + }; + } + + static Task empty() { + return new Task<>() { + @Override + public TaskStatus step(final Scheduler scheduler) { + return TaskStatus.completed(Unit.UNIT); + } + }; + } + + static Task of(Function> f) { + return new Task() { + @Override + public TaskStatus step(final Scheduler scheduler) { + return f.apply(scheduler); + } + }; + } } diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/TaskFactory.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/TaskFactory.java index 4ec05c866c..1de8ab53bc 100644 --- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/TaskFactory.java +++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/TaskFactory.java @@ -1,13 +1,29 @@ package gov.nasa.jpl.aerie.merlin.protocol.model; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; + import java.util.concurrent.Executor; /** * A factory for creating fresh copies of a task. All tasks created by a factory must be observationally equivalent. * - * @param + * @param * The type of data returned by a task created by this factory. */ -public interface TaskFactory { - Task create(Executor executor); +public interface TaskFactory { + Task create(Executor executor); + + static TaskFactory delaying(Duration duration) { + return executor -> Task.delaying(duration); + } + + default TaskFactory andThen(TaskFactory task) { + return executor -> { + final var task1 = this.create(executor); + final var task2 = task.create(executor); + + return task1.andThen(task2); + }; + } } From fb8b4576235ca3b1dbafefdf81030fd0714c6a91 Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 23 Oct 2023 08:09:58 -0700 Subject: [PATCH 02/43] Track breadcrumbs in threaded tasks --- .../merlin/framework/ThreadedReactionContext.java | 10 ++++++++-- .../nasa/jpl/aerie/merlin/framework/ThreadedTask.java | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java index d4bc94e559..9ddd9cde45 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java @@ -8,6 +8,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; +import java.util.List; import java.util.Objects; import java.util.function.Function; @@ -16,15 +17,18 @@ final class ThreadedReactionContext implements Context { private final Scoped rootContext; private final TaskHandle handle; private Scheduler scheduler; + private List readLog; public ThreadedReactionContext( final Scoped rootContext, final Scheduler scheduler, - final TaskHandle handle) + final TaskHandle handle, + final List readLog) { this.rootContext = Objects.requireNonNull(rootContext); this.scheduler = scheduler; this.handle = handle; + this.readLog = readLog; } @Override @@ -34,7 +38,9 @@ public ContextType getContextType() { @Override public State ask(final CellId cellId) { - return this.scheduler.get(cellId); + final State state = this.scheduler.get(cellId); + this.readLog.add(state); + return state; } @Override diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java index fec837e0c2..5ce31f0106 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java @@ -7,6 +7,8 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Executor; @@ -22,6 +24,8 @@ public final class ThreadedTask implements Task { private Lifecycle lifecycle = Lifecycle.Inactive; private Return returnValue; + private final List readLog = new ArrayList<>(); + private int stepCount = 0; public ThreadedTask(final Executor executor, final Scoped rootContext, final Supplier task) { this.rootContext = Objects.requireNonNull(rootContext); @@ -31,6 +35,7 @@ public ThreadedTask(final Executor executor, final Scoped rootContext, @Override public TaskStatus step(final Scheduler scheduler) { + this.stepCount++; try { if (this.lifecycle == Lifecycle.Terminated) { return TaskStatus.completed(this.returnValue); @@ -136,7 +141,7 @@ public TaskResponse run(final TaskRequest request) { if (request instanceof TaskRequest.Resume resume) { final var scheduler = resume.scheduler; - final var context = new ThreadedReactionContext(ThreadedTask.this.rootContext, scheduler, this); + final var context = new ThreadedReactionContext(ThreadedTask.this.rootContext, scheduler, this, ThreadedTask.this.readLog); try (final var restore = ThreadedTask.this.rootContext.set(context)) { return new TaskResponse.Success<>(TaskStatus.completed(ThreadedTask.this.task.get())); From a0bccb9361527ffb38ec16ded9bc92694eb5ae73 Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 23 Oct 2023 08:10:34 -0700 Subject: [PATCH 03/43] Implement duplicate for SlabList --- .../gov/nasa/jpl/aerie/merlin/driver/engine/SlabList.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SlabList.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SlabList.java index 82fda9b742..d87e6faf86 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SlabList.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SlabList.java @@ -99,4 +99,12 @@ public Slab() { this(new ArrayList<>(SLAB_SIZE), new MutableObject<>(null)); } } + + public SlabList duplicate() { + final SlabList slabList = new SlabList<>(); + for (T t : this) { + slabList.append(t); + } + return slabList; + } } From 1f3f6e749e3af984b7c19b5ae82a8fdbf7b588b7 Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 23 Oct 2023 08:11:11 -0700 Subject: [PATCH 04/43] Implement duplicate for Subscriptions --- .../jpl/aerie/merlin/driver/engine/Subscriptions.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/Subscriptions.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/Subscriptions.java index e533a37802..6f7866355e 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/Subscriptions.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/Subscriptions.java @@ -50,4 +50,14 @@ public void clear() { this.topicsByQuery.clear(); this.queriesByTopic.clear(); } + + public Subscriptions duplicate() { + final Subscriptions subscriptions = new Subscriptions<>(); + for (final var entry : this.topicsByQuery.entrySet()) { + final var query = entry.getKey(); + final var topics = entry.getValue(); + subscriptions.subscribeQuery(query, new HashSet<>(topics)); + } + return subscriptions; + } } From 5fdb95f93a3c9eccda0c85d435d3040eef7bc746 Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 23 Oct 2023 08:11:32 -0700 Subject: [PATCH 05/43] Implement duplicate for JobSchedule --- .../nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java index 4f7f7bda02..3268b9c60f 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java @@ -58,4 +58,13 @@ public void clear() { } public record Batch(Duration offsetFromStart, Set jobs) {} + + public JobSchedule duplicate() { + final JobSchedule jobSchedule = new JobSchedule<>(); + for (final var entry : this.queue.entrySet()) { + jobSchedule.queue.put(entry.getKey(), new HashSet<>(entry.getValue())); + } + jobSchedule.scheduledJobs.putAll(this.scheduledJobs); + return jobSchedule; + } } From 2aa64b2590e5c8697eeefd348e691901376937d5 Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 23 Oct 2023 08:11:56 -0700 Subject: [PATCH 06/43] Implement duplicate for Profile and ProfilingState --- .../java/gov/nasa/jpl/aerie/merlin/driver/engine/Profile.java | 4 ++++ .../nasa/jpl/aerie/merlin/driver/engine/ProfilingState.java | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/Profile.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/Profile.java index f35a8c87e1..df7ea415b3 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/Profile.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/Profile.java @@ -20,4 +20,8 @@ public void append(final Duration currentTime, final Dynamics dynamics) { public Iterator> iterator() { return this.segments.iterator(); } + + public Profile duplicate() { + return new Profile<>(segments.duplicate()); + } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/ProfilingState.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/ProfilingState.java index 74709fef5e..d34294e329 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/ProfilingState.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/ProfilingState.java @@ -14,4 +14,8 @@ ProfilingState create(final Resource resource) { public void append(final Duration currentTime, final Querier querier) { this.profile.append(currentTime, this.resource.getDynamics(querier)); } + + public ProfilingState duplicate() { + return new ProfilingState<>(resource, profile.duplicate()); + } } From c544949361ac4ebe36f5db654669a91398ae563e Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 23 Oct 2023 08:34:22 -0700 Subject: [PATCH 07/43] Implement duplicate for Tasks --- .../jpl/aerie/merlin/driver/MissionModel.java | 17 +++++-- .../merlin/driver/AnchorSimulationTest.java | 38 +++++++++++---- .../generator/MissionModelGenerator.java | 18 ++++++-- .../aerie/merlin/framework/ReplayingTask.java | 9 ++++ .../aerie/merlin/framework/ThreadedTask.java | 28 +++++++++++ .../jpl/aerie/merlin/protocol/model/Task.java | 46 +++++++++++++++++++ .../simulation/ResumableSimulationDriver.java | 8 ++-- .../simulation/AnchorSchedulerTest.java | 38 +++++++++++---- .../simulation/InstantiateArgumentsTest.java | 26 +++++++++-- 9 files changed, 193 insertions(+), 35 deletions(-) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModel.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModel.java index 03ae26c4ee..fc9b477e34 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModel.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModel.java @@ -1,9 +1,11 @@ package gov.nasa.jpl.aerie.merlin.driver; import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; import gov.nasa.jpl.aerie.merlin.protocol.model.Resource; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; @@ -14,6 +16,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.Executor; public final class MissionModel { private final Model model; @@ -55,9 +58,17 @@ public TaskFactory getTaskFactory(final SerializedActivity specification) thr } public TaskFactory getDaemon() { - return executor -> scheduler -> { - MissionModel.this.daemons.forEach($ -> scheduler.spawn(InSpan.Fresh, $)); - return TaskStatus.completed(Unit.UNIT); + return executor -> new Task<>() { + @Override + public TaskStatus step(final Scheduler scheduler) { + MissionModel.this.daemons.forEach($ -> scheduler.spawn(InSpan.Fresh, $)); + return TaskStatus.completed(Unit.UNIT); + } + + @Override + public Task duplicate(final Executor executor) { + return this; + } }; } diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/AnchorSimulationTest.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/AnchorSimulationTest.java index d7e8984d81..c52749321e 100644 --- a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/AnchorSimulationTest.java +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/AnchorSimulationTest.java @@ -2,11 +2,13 @@ import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType; import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; import gov.nasa.jpl.aerie.merlin.protocol.model.ModelType; import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; @@ -30,6 +32,8 @@ import java.util.Map; import java.util.Optional; import java.util.TreeMap; +import java.util.concurrent.Executor; +import java.util.function.Function; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -1083,13 +1087,13 @@ public OutputType getOutputType() { @Override public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> $ -> { + return executor -> oneShotTask($ -> { $.emit(this, delayedActivityDirectiveInputTopic); - return TaskStatus.delayed(oneMinute, $$ -> { + return TaskStatus.delayed(oneMinute, oneShotTask($$ -> { $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); return TaskStatus.completed(Unit.UNIT); - }); - }; + })); + }); } }; @@ -1108,18 +1112,18 @@ public OutputType getOutputType() { @Override public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> scheduler -> { + return executor -> oneShotTask(scheduler -> { scheduler.emit(this, decomposingActivityDirectiveInputTopic); return TaskStatus.delayed( Duration.ZERO, - $ -> { + oneShotTask($ -> { try { $.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); } catch (final InstantiationException ex) { throw new Error("Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( ex.toString())); } - return TaskStatus.delayed(Duration.of(120, Duration.SECOND), $$ -> { + return TaskStatus.delayed(Duration.of(120, Duration.SECOND), oneShotTask($$ -> { try { $$.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); } catch (final InstantiationException ex) { @@ -1129,12 +1133,26 @@ public TaskFactory getTaskFactory(final Object o, final Object o2) { } $$.emit(Unit.UNIT, decomposingActivityDirectiveOutputTopic); return TaskStatus.completed(Unit.UNIT); - }); - }); - }; + })); + })); + }); } }; + private static Task oneShotTask(Function> f) { + return new Task<>() { + @Override + public TaskStatus step(final Scheduler scheduler) { + return f.apply(scheduler); + } + + @Override + public Task duplicate(Executor executor) { + return this; + } + }; + } + private static final InputType testModelInputType = new InputType<>() { @Override public List getParameters() { diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java index e8760293db..9d71dca7f5 100644 --- a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java @@ -24,6 +24,7 @@ import gov.nasa.jpl.aerie.merlin.processor.metamodel.InputTypeRecord; import gov.nasa.jpl.aerie.merlin.processor.metamodel.MissionModelRecord; import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; import gov.nasa.jpl.aerie.merlin.protocol.model.MerlinPlugin; @@ -31,6 +32,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerPlugin; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; @@ -50,6 +52,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.Executor; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -859,12 +862,19 @@ public Optional generateActivityMapper(final MissionModelRecord missio .orElseGet(() -> CodeBlock .builder() .add( - "return executor -> scheduler -> {$>\n$L$<};\n", + "return executor -> new $T<>() { public $T<$T> step($T scheduler) {$>\n$L$<} public $T<$T> duplicate($T executor) { return this; }};\n", + Task.class, + TaskStatus.class, + Unit.class, + Scheduler.class, CodeBlock.builder() - .addStatement("scheduler.emit($L, this.$L)", "activity", "inputTopic") - .addStatement("scheduler.emit($T.UNIT, this.$L)", Unit.class, "outputTopic") + .addStatement("scheduler.emit($L, $L.this.$L)", "activity", activityType.inputType().mapper().name, "inputTopic") + .addStatement("scheduler.emit($T.UNIT, $L.this.$L)", Unit.class, activityType.inputType().mapper().name, "outputTopic") .addStatement("return $T.completed($T.UNIT)", TaskStatus.class, Unit.class) - .build()) + .build(), + Task.class, + Unit.class, + Executor.class) .build())) .build()) .build(); diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingTask.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingTask.java index a89e4048b3..daae4a274e 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingTask.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingTask.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.Objects; +import java.util.concurrent.Executor; import java.util.function.Supplier; public final class ReplayingTask implements Task { @@ -69,4 +70,12 @@ public Scheduler await(final gov.nasa.jpl.aerie.merlin.protocol.model.Condition // (most notably the call stack snapshotting). private static final class Yield extends RuntimeException {} private static final Yield Yield = new Yield(); + + @Override + public Task duplicate(Executor executor) { + final ReplayingTask replayingTask = new ReplayingTask<>(rootContext, task); + replayingTask.memory.reads().addAll(this.memory.reads()); + replayingTask.memory.writes().setValue(this.memory.writes()); + return replayingTask; + } } diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java index 5ce31f0106..88ad3bd2d8 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java @@ -1,6 +1,8 @@ package gov.nasa.jpl.aerie.merlin.framework; +import gov.nasa.jpl.aerie.merlin.protocol.driver.CellId; import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; @@ -251,4 +253,30 @@ public TaskAbort() { super(null, null, /* capture suppressed exceptions? */ true, /* capture stack trace? */ false); } } + + @Override + public Task duplicate(Executor executor) { + final ThreadedTask threadedTask = new ThreadedTask<>(executor, rootContext, task); + final var readIterator = readLog.iterator(); + final Scheduler scheduler = new Scheduler() { + @Override + public State get(final CellId cellId) { + return (State) readIterator.next(); + } + + @Override + public void emit(final Event event, final Topic topic) { + + } + + @Override + public void spawn(final InSpan childSpan, final TaskFactory task) { + + } + }; + for (int i = 0; i < stepCount; i++) { + threadedTask.step(scheduler); + } + return threadedTask; + } } diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java index ccb2be62d2..8696126e24 100644 --- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java +++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java @@ -8,6 +8,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; import java.util.List; +import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.Function; @@ -32,6 +33,16 @@ public interface Task { */ default void release() {} + /** + * Produce a copy of this Task that can be stepped independently from this Task + * + *

Clients must not invoke {@code duplicate()} after {@link #step(Scheduler)} or {@link #release()} + * has been invoked.

+ * @param executor the executor to use for the new Task + * @return a copy of this Task that can be stepped independently from this Task + */ + Task duplicate(Executor executor); + default Task andThen(Task task2) { return new Task<>() { @Override @@ -51,6 +62,11 @@ public TaskStatus step(final Scheduler scheduler) { } } } + + @Override + public Task duplicate(final Executor executor) { + return Task.this.duplicate(executor).andThen(task2.duplicate(executor)); + } }; } @@ -73,6 +89,11 @@ public TaskStatus step(final Scheduler scheduler) { } } } + + @Override + public Task duplicate(final Executor executor) { + return Task.this.duplicate(executor).dropOutput(); + } }; } @@ -82,6 +103,11 @@ static Task calling(Task task) { public TaskStatus step(final Scheduler scheduler) { return TaskStatus.calling(InSpan.Parent, (TaskFactory < Output >)executor -> task, Task.empty()); } + + @Override + public Task duplicate(final Executor executor) { + return calling(task.duplicate(executor)); + } }; } @@ -91,6 +117,11 @@ static Task callingWithSpan(Task task) { public TaskStatus step(final Scheduler scheduler) { return TaskStatus.calling(InSpan.Fresh, (TaskFactory) executor -> task, Task.empty()); } + + @Override + public Task duplicate(final Executor executor) { + return callingWithSpan(task.duplicate(executor)); + } }; } @@ -143,6 +174,11 @@ static Task evaluate(Function f) { public TaskStatus step(final Scheduler scheduler) { return TaskStatus.completed(f.apply(scheduler)); } + + @Override + public Task duplicate(final Executor executor) { + return this; + } }; } @@ -152,6 +188,11 @@ static Task empty() { public TaskStatus step(final Scheduler scheduler) { return TaskStatus.completed(Unit.UNIT); } + + @Override + public Task duplicate(final Executor executor) { + return this; + } }; } @@ -161,6 +202,11 @@ static Task of(Function> f) { public TaskStatus step(final Scheduler scheduler) { return f.apply(scheduler); } + + @Override + public Task duplicate(final Executor executor) { + return this; + } }; } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java index 4e3c47f9e9..872d87e4d6 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java @@ -12,6 +12,7 @@ import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; @@ -398,9 +399,8 @@ private static TaskFactory makeTaskFactory( final ActivityDirectiveId directiveId, final TaskFactory task, final Topic activityTopic) { - return executor -> scheduler -> { - scheduler.emit(directiveId, activityTopic); - return task.create(executor).step(scheduler); - }; + return executor -> + Task.run($ -> $.emit(directiveId, activityTopic)) + .andThen(task.create(executor)); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java index c6c47c1a72..d0889c721a 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java @@ -10,11 +10,13 @@ import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType; import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; import gov.nasa.jpl.aerie.merlin.protocol.model.ModelType; import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; @@ -38,6 +40,8 @@ import java.util.Map; import java.util.Optional; import java.util.TreeMap; +import java.util.concurrent.Executor; +import java.util.function.Function; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -666,13 +670,13 @@ public OutputType getOutputType() { @Override public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> $ -> { + return executor -> oneShotTask($ -> { $.emit(this, delayedActivityDirectiveInputTopic); - return TaskStatus.delayed(oneMinute, $$ -> { + return TaskStatus.delayed(oneMinute, oneShotTask($$ -> { $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); return TaskStatus.completed(Unit.UNIT); - }); - }; + })); + }); } }; @@ -691,18 +695,18 @@ public OutputType getOutputType() { @Override public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> scheduler -> { + return executor -> oneShotTask(scheduler -> { scheduler.emit(this, decomposingActivityDirectiveInputTopic); return TaskStatus.delayed( Duration.ZERO, - $ -> { + oneShotTask($ -> { try { $.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); } catch (final InstantiationException ex) { throw new Error("Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( ex.toString())); } - return TaskStatus.delayed(Duration.of(120, Duration.SECOND), $$ -> { + return TaskStatus.delayed(Duration.of(120, Duration.SECOND), oneShotTask($$ -> { try { $$.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); } catch (final InstantiationException ex) { @@ -712,9 +716,9 @@ public TaskFactory getTaskFactory(final Object o, final Object o2) { } $$.emit(Unit.UNIT, decomposingActivityDirectiveOutputTopic); return TaskStatus.completed(Unit.UNIT); - }); - }); - }; + })); + })); + }); } }; @@ -808,4 +812,18 @@ public Object instantiate( ) ); //endregion + + private static Task oneShotTask(Function> f) { + return new Task<>() { + @Override + public TaskStatus step(final Scheduler scheduler) { + return f.apply(scheduler); + } + + @Override + public Task duplicate(Executor executor) { + return this; + } + }; + } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InstantiateArgumentsTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InstantiateArgumentsTest.java index b9ab4b11bc..ae87e50e8c 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InstantiateArgumentsTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InstantiateArgumentsTest.java @@ -11,10 +11,12 @@ import gov.nasa.jpl.aerie.constraints.tree.RealValue; import gov.nasa.jpl.aerie.constraints.tree.StructExpressionAt; import gov.nasa.jpl.aerie.constraints.tree.ValueAt; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType; import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; @@ -29,6 +31,8 @@ import java.time.Instant; import java.util.List; import java.util.Map; +import java.util.concurrent.Executor; +import java.util.function.Function; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOUR; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECONDS; @@ -141,13 +145,13 @@ public OutputType getOutputType() { @Override public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> $ -> { + return executor -> oneShotTask($ -> { $.emit(this, delayedActivityDirectiveInputTopic); - return TaskStatus.delayed(oneMinute, $$ -> { + return TaskStatus.delayed(oneMinute, oneShotTask($$ -> { $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); return TaskStatus.completed(Unit.UNIT); - }); - }; + })); + }); } }; @@ -195,4 +199,18 @@ public SerializedValue serialize(final Object value) { return SerializedValue.of(Map.of()); } }; + + private static Task oneShotTask(Function> f) { + return new Task<>() { + @Override + public TaskStatus step(final Scheduler scheduler) { + return f.apply(scheduler); + } + + @Override + public Task duplicate(Executor executor) { + return this; + } + }; + } } From 1e1dac1a7aee423bfab2442f5d5cb5068ea93824 Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 23 Oct 2023 09:28:46 -0700 Subject: [PATCH 08/43] Implement duplicate for ExecutionState --- .../jpl/aerie/merlin/driver/engine/SimulationEngine.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java index 8bd15a193e..da1b996f3e 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java @@ -46,6 +46,7 @@ import java.util.Optional; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; @@ -803,6 +804,10 @@ private record ExecutionState(SpanId span, Optional caller, Task public ExecutionState continueWith(final Task newState) { return new ExecutionState<>(this.span, this.caller, newState); } + + public ExecutionState duplicate(Executor executor) { + return new ExecutionState<>(span, caller, state.duplicate(executor)); + } } /** The span of time over which a subtree of tasks has acted. */ From 53939d87d10abed33258521f13004d437b5f3650 Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 23 Oct 2023 09:00:14 -0700 Subject: [PATCH 09/43] Implement duplicate for SimulationEngine --- .../driver/engine/SimulationEngine.java | 68 ++++++++++++++++--- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java index da1b996f3e..f8f28f1202 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java @@ -39,6 +39,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -56,30 +57,71 @@ */ public final class SimulationEngine implements AutoCloseable { /** The set of all jobs waiting for time to pass. */ - private final JobSchedule scheduledJobs = new JobSchedule<>(); + private final JobSchedule scheduledJobs; /** The set of all jobs waiting on a condition. */ - private final Map waitingTasks = new HashMap<>(); + private final Map waitingTasks; /** The set of all tasks blocked on some number of subtasks. */ - private final Map blockedTasks = new HashMap<>(); + private final Map blockedTasks; /** The set of conditions depending on a given set of topics. */ - private final Subscriptions, ConditionId> waitingConditions = new Subscriptions<>(); + private final Subscriptions, ConditionId> waitingConditions; /** The set of queries depending on a given set of topics. */ - private final Subscriptions, ResourceId> waitingResources = new Subscriptions<>(); + private final Subscriptions, ResourceId> waitingResources; /** The execution state for every task. */ - private final Map> tasks = new HashMap<>(); + private final Map> tasks; /** The getter for each tracked condition. */ - private final Map conditions = new HashMap<>(); + private final Map conditions; /** The profiling state for each tracked resource. */ - private final Map> resources = new HashMap<>(); + private final Map> resources; /** The set of all spans of work contributed to by modeled tasks. */ - private final Map spans = new HashMap<>(); + private final Map spans; /** A count of the direct contributors to each span, including child spans and tasks. */ - private final Map spanContributorCount = new HashMap<>(); + private final Map spanContributorCount; /** A thread pool that modeled tasks can use to keep track of their state between steps. */ - private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + private final ExecutorService executor; + + public SimulationEngine() { + scheduledJobs = new JobSchedule<>(); + waitingTasks = new LinkedHashMap<>(); + blockedTasks = new LinkedHashMap<>(); + waitingConditions = new Subscriptions<>(); + waitingResources = new Subscriptions<>(); + tasks = new LinkedHashMap<>(); + conditions = new LinkedHashMap<>(); + resources = new LinkedHashMap<>(); + spans = new LinkedHashMap<>(); + spanContributorCount = new LinkedHashMap<>(); + executor = Executors.newVirtualThreadPerTaskExecutor(); + } + + private SimulationEngine(SimulationEngine other) { + // New Executor allows other SimulationEngine to be closed + executor = Executors.newVirtualThreadPerTaskExecutor(); + scheduledJobs = other.scheduledJobs.duplicate(); + waitingTasks = new LinkedHashMap<>(other.waitingTasks); + blockedTasks = new LinkedHashMap<>(); + for (final var entry : other.blockedTasks.entrySet()) { + blockedTasks.put(entry.getKey(), new MutableInt(entry.getValue())); + } + waitingConditions = other.waitingConditions.duplicate(); + waitingResources = other.waitingResources.duplicate(); + tasks = new LinkedHashMap<>(); + for (final var entry : other.tasks.entrySet()) { + tasks.put(entry.getKey(), entry.getValue().duplicate(executor)); + } + conditions = new LinkedHashMap<>(other.conditions); + resources = new LinkedHashMap<>(); + for (final var entry : other.resources.entrySet()) { + resources.put(entry.getKey(), entry.getValue().duplicate()); + } + spans = new LinkedHashMap<>(other.spans); + spanContributorCount = new LinkedHashMap<>(); + for (final var entry : other.spanContributorCount.entrySet()) { + spanContributorCount.put(entry.getKey(), new MutableInt(entry.getValue().getValue())); + } + } /** Schedule a new task to be performed at the given time. */ public SpanId scheduleTask(final Duration startTime, final TaskFactory state) { @@ -826,4 +868,8 @@ public boolean isComplete() { return this.endOffset.isPresent(); } } + + public SimulationEngine duplicate() { + return new SimulationEngine(this); + } } From 4bf0db05dadbc4131ada93bbabde598e3f734fda Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 23 Oct 2023 09:01:31 -0700 Subject: [PATCH 10/43] Track unstarted tasks --- .../jpl/aerie/merlin/driver/engine/SimulationEngine.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java index f8f28f1202..8bdb212b11 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java @@ -40,6 +40,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -74,6 +75,9 @@ public final class SimulationEngine implements AutoCloseable { /** The profiling state for each tracked resource. */ private final Map> resources; + /** Tasks that have been scheduled, but not started */ + private final Set unstartedTasks; + /** The set of all spans of work contributed to by modeled tasks. */ private final Map spans; /** A count of the direct contributors to each span, including child spans and tasks. */ @@ -91,6 +95,7 @@ public SimulationEngine() { tasks = new LinkedHashMap<>(); conditions = new LinkedHashMap<>(); resources = new LinkedHashMap<>(); + unstartedTasks = new LinkedHashSet<>(); spans = new LinkedHashMap<>(); spanContributorCount = new LinkedHashMap<>(); executor = Executors.newVirtualThreadPerTaskExecutor(); @@ -116,6 +121,7 @@ private SimulationEngine(SimulationEngine other) { for (final var entry : other.resources.entrySet()) { resources.put(entry.getKey(), entry.getValue().duplicate()); } + unstartedTasks = new LinkedHashSet<>(other.unstartedTasks); spans = new LinkedHashMap<>(other.spans); spanContributorCount = new LinkedHashMap<>(); for (final var entry : other.spanContributorCount.entrySet()) { @@ -135,6 +141,8 @@ public SpanId scheduleTask(final Duration startTime, final TaskFactory< this.tasks.put(task, new ExecutionState<>(span, Optional.empty(), state.create(this.executor))); this.scheduledJobs.schedule(JobId.forTask(task), SubInstant.Tasks.at(startTime)); + this.unstartedTasks.add(task); + return span; } @@ -228,6 +236,7 @@ public void performJob( /** Perform the next step of a modeled task. */ public void stepTask(final TaskId task, final TaskFrame frame, final Duration currentTime) throws SpanException { + this.unstartedTasks.remove(task); // The handler for the next status of the task is responsible // for putting an updated state back into the task set. var state = this.tasks.remove(task); From c46327bf6fc66c4f654a9f3f13e54966260af35f Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 23 Oct 2023 09:03:00 -0700 Subject: [PATCH 11/43] Implement unscheduleAfter in SimulationEngine --- .../merlin/driver/engine/SimulationEngine.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java index 8bdb212b11..e4ea888f48 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java @@ -76,7 +76,7 @@ public final class SimulationEngine implements AutoCloseable { private final Map> resources; /** Tasks that have been scheduled, but not started */ - private final Set unstartedTasks; + private final Map unstartedTasks; /** The set of all spans of work contributed to by modeled tasks. */ private final Map spans; @@ -95,7 +95,7 @@ public SimulationEngine() { tasks = new LinkedHashMap<>(); conditions = new LinkedHashMap<>(); resources = new LinkedHashMap<>(); - unstartedTasks = new LinkedHashSet<>(); + unstartedTasks = new LinkedHashMap<>(); spans = new LinkedHashMap<>(); spanContributorCount = new LinkedHashMap<>(); executor = Executors.newVirtualThreadPerTaskExecutor(); @@ -121,7 +121,7 @@ private SimulationEngine(SimulationEngine other) { for (final var entry : other.resources.entrySet()) { resources.put(entry.getKey(), entry.getValue().duplicate()); } - unstartedTasks = new LinkedHashSet<>(other.unstartedTasks); + unstartedTasks = new LinkedHashMap<>(other.unstartedTasks); spans = new LinkedHashMap<>(other.spans); spanContributorCount = new LinkedHashMap<>(); for (final var entry : other.spanContributorCount.entrySet()) { @@ -141,7 +141,7 @@ public SpanId scheduleTask(final Duration startTime, final TaskFactory< this.tasks.put(task, new ExecutionState<>(span, Optional.empty(), state.create(this.executor))); this.scheduledJobs.schedule(JobId.forTask(task), SubInstant.Tasks.at(startTime)); - this.unstartedTasks.add(task); + this.unstartedTasks.put(task, startTime); return span; } @@ -384,6 +384,15 @@ public void close() { this.executor.shutdownNow(); } + public void unscheduleAfter(final Duration duration) { + for (final var taskId : new ArrayList<>(this.tasks.keySet())) { + if (this.unstartedTasks.containsKey(taskId) && this.unstartedTasks.get(taskId).longerThan(duration)) { + this.tasks.remove(taskId); + this.scheduledJobs.unschedule(JobId.forTask(taskId)); + } + } + } + private record SpanInfo( Map spanToPlannedDirective, Map input, From 0c11d54a6108fad685cd632c2ba1234f9badc65e Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 23 Oct 2023 09:23:09 -0700 Subject: [PATCH 12/43] Add THREADED_TASK_CACHE_READS environment variable --- .../merlin/framework/ThreadedReactionContext.java | 10 +++++----- .../jpl/aerie/merlin/framework/ThreadedTask.java | 14 +++++++++++++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java index 9ddd9cde45..1fa4efb3f5 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java @@ -8,8 +8,8 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; -import java.util.List; import java.util.Objects; +import java.util.function.Consumer; import java.util.function.Function; /* package-local */ @@ -17,18 +17,18 @@ final class ThreadedReactionContext implements Context { private final Scoped rootContext; private final TaskHandle handle; private Scheduler scheduler; - private List readLog; + private final Consumer readLogger; public ThreadedReactionContext( final Scoped rootContext, final Scheduler scheduler, final TaskHandle handle, - final List readLog) + final Consumer readLog) { this.rootContext = Objects.requireNonNull(rootContext); this.scheduler = scheduler; this.handle = handle; - this.readLog = readLog; + this.readLogger = readLog; } @Override @@ -39,7 +39,7 @@ public ContextType getContextType() { @Override public State ask(final CellId cellId) { final State state = this.scheduler.get(cellId); - this.readLog.add(state); + this.readLogger.accept(state); return state; } diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java index 88ad3bd2d8..b918ef3af7 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java @@ -14,9 +14,12 @@ import java.util.Objects; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Executor; +import java.util.function.Consumer; import java.util.function.Supplier; public final class ThreadedTask implements Task { + private final boolean CACHE_READS = Boolean.parseBoolean(getEnv("THREADED_TASK_CACHE_READS", "false")); + private final Scoped rootContext; private final Supplier task; private final Executor executor; @@ -143,7 +146,8 @@ public TaskResponse run(final TaskRequest request) { if (request instanceof TaskRequest.Resume resume) { final var scheduler = resume.scheduler; - final var context = new ThreadedReactionContext(ThreadedTask.this.rootContext, scheduler, this, ThreadedTask.this.readLog); + final Consumer readLogger = CACHE_READS ? ThreadedTask.this.readLog::add : $ -> {}; + final var context = new ThreadedReactionContext(ThreadedTask.this.rootContext, scheduler, this, readLogger); try (final var restore = ThreadedTask.this.rootContext.set(context)) { return new TaskResponse.Success<>(TaskStatus.completed(ThreadedTask.this.task.get())); @@ -256,6 +260,9 @@ public TaskAbort() { @Override public Task duplicate(Executor executor) { + if (!CACHE_READS) { + throw new RuntimeException("Cannot duplicate threaded task without cached reads"); + } final ThreadedTask threadedTask = new ThreadedTask<>(executor, rootContext, task); final var readIterator = readLog.iterator(); final Scheduler scheduler = new Scheduler() { @@ -279,4 +286,9 @@ public void spawn(final InSpan childSpan, final TaskFactory task) { } return threadedTask; } + + private static String getEnv(final String key, final String fallback) { + final var env = System.getenv(key); + return env == null ? fallback : env; + } } From c7e8afbd542246e3d2b35272a1c2039ef43f1102 Mon Sep 17 00:00:00 2001 From: maillard Date: Thu, 11 Apr 2024 16:00:49 -0700 Subject: [PATCH 13/43] Driver updates and tests --- .../SimulationDuplicationTest.java | 479 ++++++++++++++++++ .../merlin/driver/SimulationResults.java | 30 ++ .../merlin/driver/engine/JobSchedule.java | 6 + .../driver/engine/SimulationEngine.java | 5 +- .../aerie/merlin/driver/engine/SlabList.java | 8 + .../driver/timeline/CausalEventSource.java | 9 + .../merlin/driver/timeline/EventSource.java | 2 + .../merlin/driver/timeline/LiveCells.java | 5 + .../driver/timeline/TemporalEventSource.java | 4 + .../driver/SimulationDuplicationTest.java | 343 +++++++++++++ .../aerie/merlin/framework/ThreadedTask.java | 12 +- 11 files changed, 899 insertions(+), 4 deletions(-) create mode 100644 examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java create mode 100644 merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java diff --git a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java new file mode 100644 index 0000000000..46efd53dd9 --- /dev/null +++ b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java @@ -0,0 +1,479 @@ +package gov.nasa.jpl.aerie.foomissionmodel; + +import gov.nasa.jpl.aerie.foomissionmodel.generated.GeneratedModelType; +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; +import gov.nasa.jpl.aerie.merlin.driver.DirectiveTypeRegistry; +import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelBuilder; +import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; +import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; +import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; +import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; +import gov.nasa.jpl.aerie.merlin.framework.ThreadedTask; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; +import gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType; +import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; +import gov.nasa.jpl.aerie.merlin.protocol.model.ModelType; +import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; +import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; +import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; +import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.lang3.tuple.Triple; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.Executor; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTE; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTES; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SimulationDuplicationTest { + @BeforeAll + static void beforeAll() { + ThreadedTask.CACHE_READS = true; + } + + private static MissionModel makeMissionModel(final MissionModelBuilder builder, final Instant planStart, final Configuration config) { + final var factory = new GeneratedModelType(); + final var registry = DirectiveTypeRegistry.extract(factory); + final var model = factory.instantiate(planStart, config, builder); + return builder.build(model, registry); + } + + @Test + void emptyPlanTest() { + final SimulationResults results = SimulationDriver.simulate( + missionModel, + Map.of(), + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false); + final List> standardTopics = List.of( + Triple.of( + 0, + "ActivityType.Input.DelayActivityDirective", + ValueSchema.ofStruct(Map.of())), + Triple.of( + 1, + "ActivityType.Output.DelayActivityDirective", + ValueSchema.ofStruct(Map.of())), + Triple.of( + 2, + "ActivityType.Input.DecomposingActivityDirective", + ValueSchema.ofStruct(Map.of())), + Triple.of( + 3, + "ActivityType.Output.DecomposingActivityDirective", + ValueSchema.ofStruct(Map.of()))); + final SimulationResults expected = new SimulationResults( + Map.of(), + Map.of(), + Map.of(), + Map.of(), + Instant.EPOCH, + Duration.HOUR, + standardTopics, + new TreeMap<>()); + assertResultsEqual(expected, results); + } + + @Test + void testTrivialDuplicate() { + final Topic activityTopic = new Topic<>(); + final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints( + missionModel, + SimulationDriver.CachedSimulationEngine.empty(), List.of(Duration.of(5, MINUTES)), Map.of() + ); + final SimulationResults expected = SimulationDriver.simulate( + missionModel, + Map.of(), + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false); + assertResultsEqual(expected, results.results()); + final SimulationDriver.SimulationResultsWithCheckpoints newResults = simulateWithCheckpoints( + missionModel, + results.checkpoints().get(0), List.of(), Map.of() + ); + assertResultsEqual(expected, newResults.results()); + } + + @Test + void testFooDuplicateEmptyPlan() { + final MissionModel missionModel = makeMissionModel( + new MissionModelBuilder(), + Instant.EPOCH, + new Configuration()); + final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints( + missionModel, + List.of(Duration.of(5, MINUTES)), + Map.of() + ); + final SimulationResults expected = SimulationDriver.simulate( + missionModel, + Map.of(), + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false); + assertResultsEqual(expected, results.results()); + } + + @Test + void testFooNonEmptyPlan() { + final MissionModel missionModel = makeMissionModel( + new MissionModelBuilder(), + Instant.EPOCH, + new Configuration()); + final Map schedule = Map.ofEntries( + activity(1, MINUTE, "foo", Map.of("z", SerializedValue.of(123))), + activity(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) + ); + final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints( + missionModel, + List.of(Duration.of(5, MINUTES)), + schedule + ); + final SimulationResults expected = SimulationDriver.simulate( + missionModel, + schedule, + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false); + assertResultsEqual(expected, results.results()); + + assertEquals(Duration.of(5, MINUTES), results.checkpoints().get(0).startOffset()); + + final SimulationDriver.SimulationResultsWithCheckpoints results2 = simulateWithCheckpoints( + missionModel, + results.checkpoints().get(0), + List.of(Duration.of(5, MINUTES)), + schedule + ); + + assertResultsEqual(expected, results2.results()); + } + + private static long nextActivityDirectiveId = 0L; + + private static Pair activity(final long quantity, final Duration unit, final String type) { + return activity(quantity, unit, type, Map.of()); + } + private static Pair activity(final Duration startOffset, final String type) { + return activity(startOffset, type, Map.of()); + } + + private static Pair activity(final long quantity, final Duration unit, final String type, final Map args) { + return activity(Duration.of(quantity, unit), type, args); + } + + private static Pair activity(final Duration startOffset, final String type, final Map args) { + if (nextActivityDirectiveId > 1) { + System.out.println(); + } + return Pair.of(new ActivityDirectiveId(nextActivityDirectiveId++), new ActivityDirective(startOffset, type, args, null, true)); + } + + + static void assertResultsEqual(SimulationResults expected, SimulationResults actual) { + if (expected.equals(actual)) return; + final var differences = new ArrayList(); + if (!expected.duration.isEqualTo(actual.duration)) { + differences.add("duration"); + } + if (!expected.realProfiles.equals(actual.realProfiles)) { + differences.add("realProfiles"); + } + if (!expected.discreteProfiles.equals(actual.discreteProfiles)) { + differences.add("discreteProfiles"); + } + if (!expected.simulatedActivities.equals(actual.simulatedActivities)) { + differences.add("simulatedActivities"); + } + if (!expected.unfinishedActivities.equals(actual.unfinishedActivities)) { + differences.add("unfinishedActivities"); + } + if (!expected.startTime.equals(actual.startTime)) { + differences.add("startTime"); + } + if (!expected.duration.isEqualTo(actual.duration)) { + differences.add("duration"); + } + if (!expected.topics.equals(actual.topics)) { + differences.add("topics"); + } + if (!expected.events.equals(actual.events)) { + differences.add("events"); + } + if (!differences.isEmpty()) { + System.out.println(); + } + System.out.println(differences); + assertEquals(expected, actual); + } + + static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints( + final MissionModel missionModel, + final SimulationDriver.CachedSimulationEngine cachedSimulationEngine, + final List desiredCheckpoints, + final Map schedule + ) { + return SimulationDriver.simulateWithCheckpoints( + missionModel, + schedule, + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + $ -> {}, + () -> false, + desiredCheckpoints, + cachedSimulationEngine); + } + + static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints( + final MissionModel missionModel, + final List desiredCheckpoints, + final Map schedule + ) { + final SimulationEngine engine = new SimulationEngine(); + final TemporalEventSource timeline = new TemporalEventSource(); + final LiveCells cells = new LiveCells(timeline, missionModel.getInitialCells()); + + // Begin tracking all resources. + for (final var entry : missionModel.getResources().entrySet()) { + final var name = entry.getKey(); + final var resource = entry.getValue(); + + engine.trackResource(name, resource, Duration.ZERO); + } + + { + // Start daemon task(s) immediately, before anything else happens. + engine.scheduleTask(Duration.ZERO, missionModel.getDaemon()); + { + final var batch = engine.extractNextJobs(Duration.MAX_VALUE); + final var commit = engine.performJobs(batch.jobs(), cells, Duration.ZERO, Duration.MAX_VALUE); + timeline.add(commit); + } + } + + final var cachedSimulationEngine = new SimulationDriver.CachedSimulationEngine( + Duration.ZERO, + List.of(), + engine, + cells, + timeline.points(), + new Topic<>() + ); + + return SimulationDriver.simulateWithCheckpoints( + missionModel, + schedule, + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + $ -> {}, + () -> false, + desiredCheckpoints, + cachedSimulationEngine); + } + + private static final Topic delayedActivityDirectiveInputTopic = new Topic<>(); + private static final Topic delayedActivityDirectiveOutputTopic = new Topic<>(); + + private static final InputType testModelInputType = new InputType<>() { + @Override + public List getParameters() { + return List.of(); + } + + @Override + public List getRequiredParameters() { + return List.of(); + } + + @Override + public Object instantiate(final Map arguments) { + return new Object(); + } + + @Override + public Map getArguments(final Object value) { + return Map.of(); + } + + @Override + public List getValidationFailures(final Object value) { + return List.of(); + } + }; + + private static final OutputType testModelOutputType = new OutputType<>() { + @Override + public ValueSchema getSchema() { + return ValueSchema.ofStruct(Map.of()); + } + + @Override + public SerializedValue serialize(final Object value) { + return SerializedValue.of(Map.of()); + } + }; + + /* package-private*/ static final DirectiveType delayedActivityDirective = new DirectiveType<>() { + @Override + public InputType getInputType() { + return testModelInputType; + } + + @Override + public OutputType getOutputType() { + return testModelOutputType; + } + + @Override + public TaskFactory getTaskFactory(final Object o, final Object o2) { + return executor -> oneShotTask($ -> { + $.emit(this, delayedActivityDirectiveInputTopic); + return TaskStatus.delayed(Duration.MINUTE, oneShotTask($$ -> { + $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); + return TaskStatus.completed(Unit.UNIT); + })); + }); + } + }; + + public static Task oneShotTask(Function> f) { + return new Task<>() { + @Override + public TaskStatus step(final Scheduler scheduler) { + return f.apply(scheduler); + } + + @Override + public Task duplicate(Executor executor) { + return this; + } + }; + } + + private static final Topic decomposingActivityDirectiveInputTopic = new Topic<>(); + private static final Topic decomposingActivityDirectiveOutputTopic = new Topic<>(); + /* package-private */ static final DirectiveType decomposingActivityDirective = new DirectiveType<>() { + @Override + public InputType getInputType() { + return testModelInputType; + } + + @Override + public OutputType getOutputType() { + return testModelOutputType; + } + + @Override + public TaskFactory getTaskFactory(final Object o, final Object o2) { + return executor -> oneShotTask(scheduler -> { + scheduler.emit(this, decomposingActivityDirectiveInputTopic); + return TaskStatus.delayed( + Duration.ZERO, + oneShotTask($ -> { + try { + $.spawn(InSpan.Parent, delayedActivityDirective.getTaskFactory(null, null)); + } catch (final InstantiationException ex) { + throw new Error("Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( + ex.toString())); + } + return TaskStatus.delayed(Duration.of(120, Duration.SECOND), oneShotTask($$ -> { + try { + $$.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); + } catch (final InstantiationException ex) { + throw new Error( + "Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( + ex.toString())); + } + $$.emit(Unit.UNIT, decomposingActivityDirectiveOutputTopic); + return TaskStatus.completed(Unit.UNIT); + })); + })); + }); + } + }; + + static MissionModel missionModel = new MissionModel<>( + new Object(), + new LiveCells(null), + Map.of(), + List.of( + new MissionModel.SerializableTopic<>( + "ActivityType.Input.DelayActivityDirective", + delayedActivityDirectiveInputTopic, + testModelOutputType), + new MissionModel.SerializableTopic<>( + "ActivityType.Output.DelayActivityDirective", + delayedActivityDirectiveOutputTopic, + testModelOutputType), + new MissionModel.SerializableTopic<>( + "ActivityType.Input.DecomposingActivityDirective", + decomposingActivityDirectiveInputTopic, + testModelOutputType), + new MissionModel.SerializableTopic<>( + "ActivityType.Output.DecomposingActivityDirective", + decomposingActivityDirectiveOutputTopic, + testModelOutputType)), + List.of(), + DirectiveTypeRegistry.extract( + new ModelType<>() { + + @Override + public Map> getDirectiveTypes() { + return Map.of( + "DelayActivityDirective", + delayedActivityDirective, + "DecomposingActivityDirective", + decomposingActivityDirective); + } + + @Override + public InputType getConfigurationType() { + return testModelInputType; + } + + @Override + public Object instantiate( + final Instant planStart, + final Object configuration, + final Initializer builder) + { + return new Object(); + } + } + ) + ); +} diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResults.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResults.java index bbe2bcdd0e..4309434865 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResults.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResults.java @@ -55,4 +55,34 @@ public String toString() { + ", unfinishedActivities=" + this.unfinishedActivities + " }"; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SimulationResults that = (SimulationResults) o; + + if (!startTime.equals(that.startTime)) return false; + if (!duration.isEqualTo(that.duration)) return false; + if (!realProfiles.equals(that.realProfiles)) return false; + if (!discreteProfiles.equals(that.discreteProfiles)) return false; + if (!simulatedActivities.equals(that.simulatedActivities)) return false; + if (!unfinishedActivities.equals(that.unfinishedActivities)) return false; + if (!topics.equals(that.topics)) return false; + return events.equals(that.events); + } + + @Override + public int hashCode() { + int result = startTime.hashCode(); + result = 31 * result + duration.hashCode(); + result = 31 * result + realProfiles.hashCode(); + result = 31 * result + discreteProfiles.hashCode(); + result = 31 * result + simulatedActivities.hashCode(); + result = 31 * result + unfinishedActivities.hashCode(); + result = 31 * result + topics.hashCode(); + result = 31 * result + events.hashCode(); + return result; + } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java index 3268b9c60f..a1f2dd061b 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java @@ -6,6 +6,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentSkipListMap; @@ -57,6 +58,11 @@ public void clear() { this.queue.clear(); } + public Optional peekNextTime() { + if(this.queue.isEmpty()) return Optional.empty(); + return Optional.ofNullable(this.queue.firstKey()).map(SchedulingInstant::offsetFromStart); + } + public record Batch(Duration offsetFromStart, Set jobs) {} public JobSchedule duplicate() { diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java index e4ea888f48..9d8a19f0bd 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java @@ -40,7 +40,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -890,4 +889,8 @@ public boolean isComplete() { public SimulationEngine duplicate() { return new SimulationEngine(this); } + + public Optional peekNextTime() { + return this.scheduledJobs.peekNextTime(); + } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SlabList.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SlabList.java index d87e6faf86..1ed9b8b4ed 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SlabList.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SlabList.java @@ -24,8 +24,12 @@ public final class SlabList implements Iterable { private Slab tail = this.head; /*derived*/ private int size = 0; + private boolean frozen = false; public void append(final T element) { + if (this.frozen) { + throw new IllegalStateException("Cannot append to frozen SlabList"); + } this.tail.elements().add(element); this.size += 1; @@ -107,4 +111,8 @@ public SlabList duplicate() { } return slabList; } + + public void freeze() { + this.frozen = true; + } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/CausalEventSource.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/CausalEventSource.java index 21c2670080..f436f226f0 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/CausalEventSource.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/CausalEventSource.java @@ -5,8 +5,12 @@ public final class CausalEventSource implements EventSource { private Event[] points = new Event[2]; private int size = 0; + private boolean frozen = false; public void add(final Event point) { + if (this.frozen) { + throw new IllegalStateException("Cannot add to frozen CausalEventSource"); + } if (this.size == this.points.length) { this.points = Arrays.copyOf(this.points, 3 * this.size / 2); } @@ -41,4 +45,9 @@ public void stepUp(final Cell cell) { this.index = size; } } + + @Override + public void freeze() { + this.frozen = true; + } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/EventSource.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/EventSource.java index 7357695d54..cb2b5f0ed8 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/EventSource.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/EventSource.java @@ -3,6 +3,8 @@ public interface EventSource { Cursor cursor(); + void freeze(); + interface Cursor { void stepUp(Cell cell); } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/LiveCells.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/LiveCells.java index 41661543d1..781c3dc547 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/LiveCells.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/LiveCells.java @@ -57,4 +57,9 @@ private Optional> getCell(final Query query) { return Optional.of(cell.get()); } + + public void freeze() { + if (this.parent != null) this.parent.freeze(); + this.source.freeze(); + } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/TemporalEventSource.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/TemporalEventSource.java index 9f4ec53af5..6964c9217d 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/TemporalEventSource.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/TemporalEventSource.java @@ -86,4 +86,8 @@ public sealed interface TimePoint { record Delta(Duration delta) implements TimePoint {} record Commit(EventGraph events, Set> topics) implements TimePoint {} } + + public void freeze() { + this.points.freeze(); + } } diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java new file mode 100644 index 0000000000..d3b9b6629a --- /dev/null +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java @@ -0,0 +1,343 @@ +package gov.nasa.jpl.aerie.merlin.driver; + +import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; +import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; +import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; +import gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType; +import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; +import gov.nasa.jpl.aerie.merlin.protocol.model.ModelType; +import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; +import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; +import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; +import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; +import org.apache.commons.lang3.tuple.Triple; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.Executor; +import java.util.function.Function; + +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTES; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SimulationDuplicationTest { + @Test + void emptyPlanTest() { + final SimulationResults results = SimulationDriver.simulate( + missionModel, + Map.of(), + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false); + final List> standardTopics = List.of( + Triple.of( + 0, + "ActivityType.Input.DelayActivityDirective", + ValueSchema.ofStruct(Map.of())), + Triple.of( + 1, + "ActivityType.Output.DelayActivityDirective", + ValueSchema.ofStruct(Map.of())), + Triple.of( + 2, + "ActivityType.Input.DecomposingActivityDirective", + ValueSchema.ofStruct(Map.of())), + Triple.of( + 3, + "ActivityType.Output.DecomposingActivityDirective", + ValueSchema.ofStruct(Map.of()))); + final SimulationResults expected = new SimulationResults( + Map.of(), + Map.of(), + Map.of(), + Map.of(), + Instant.EPOCH, + Duration.HOUR, + standardTopics, + new TreeMap<>()); + assertEquals(expected, results); + } + + @Test + void testDuplicate() { + final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints(SimulationDriver.CachedSimulationEngine.empty(), List.of(Duration.of(5, MINUTES))); + final SimulationResults expected = SimulationDriver.simulate( + missionModel, + Map.of(), + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false); + assertEquals(expected, results.results()); + final SimulationDriver.SimulationResultsWithCheckpoints newResults = simulateWithCheckpoints(results.checkpoints().get(0), List.of()); + assertEquals(expected, newResults.results()); + } + + static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints( + final List desiredCheckpoints + ) { + + final var engine = new SimulationEngine(); +// Begin tracking all resources. + for (final var entry : missionModel.getResources().entrySet()) { + final var name = entry.getKey(); + final var resource = entry.getValue(); + + engine.trackResource(name, resource, Duration.ZERO); + } + + final var timeline = new TemporalEventSource(); + final var cells = new LiveCells(timeline, missionModel.getInitialCells()); + + // Start daemon task(s) immediately, before anything else happens. + engine.scheduleTask(Duration.ZERO, missionModel.getDaemon()); + { + final var batch = engine.extractNextJobs(Duration.MAX_VALUE); + final var commit = engine.performJobs(batch.jobs(), cells, Duration.ZERO, Duration.MAX_VALUE); + timeline.add(commit); + } + + + return SimulationDriver.simulateWithCheckpoints( + missionModel, + Map.of(), + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + $ -> {}, + () -> false, + desiredCheckpoints, + new SimulationDriver.CachedSimulationEngine( + Duration.ZERO, + List.of(), + engine, + cells, + timeline.points(), + new Topic<>() + )); + } + + static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints( + final SimulationDriver.CachedSimulationEngine cachedEngine, + final List desiredCheckpoints + ) { + + // Begin tracking all resources. +// for (final var entry : missionModel.getResources().entrySet()) { +// final var name = entry.getKey(); +// final var resource = entry.getValue(); +// +// engine.trackResource(name, resource, elapsedTime); +// } + +// if (true) { +// // Start daemon task(s) immediately, before anything else happens. +// engine.scheduleTask(Duration.ZERO, missionModel.getDaemon()); +// { +// final var batch = engine.extractNextJobs(Duration.MAX_VALUE); +// final var commit = engine.performJobs(batch.jobs(), cells, elapsedTime, Duration.MAX_VALUE); +// timeline.add(commit); +// } +// } + + return SimulationDriver.simulateWithCheckpoints( + missionModel, + Map.of(), + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + $ -> {}, + () -> false, + desiredCheckpoints, + cachedEngine); + } + + private static final Topic delayedActivityDirectiveInputTopic = new Topic<>(); + private static final Topic delayedActivityDirectiveOutputTopic = new Topic<>(); + + private static final InputType testModelInputType = new InputType<>() { + @Override + public List getParameters() { + return List.of(); + } + + @Override + public List getRequiredParameters() { + return List.of(); + } + + @Override + public Object instantiate(final Map arguments) { + return new Object(); + } + + @Override + public Map getArguments(final Object value) { + return Map.of(); + } + + @Override + public List getValidationFailures(final Object value) { + return List.of(); + } + }; + + private static final OutputType testModelOutputType = new OutputType<>() { + @Override + public ValueSchema getSchema() { + return ValueSchema.ofStruct(Map.of()); + } + + @Override + public SerializedValue serialize(final Object value) { + return SerializedValue.of(Map.of()); + } + }; + + /* package-private*/ static final DirectiveType delayedActivityDirective = new DirectiveType<>() { + @Override + public InputType getInputType() { + return testModelInputType; + } + + @Override + public OutputType getOutputType() { + return testModelOutputType; + } + + @Override + public TaskFactory getTaskFactory(final Object o, final Object o2) { + return executor -> oneShotTask($ -> { + $.emit(this, delayedActivityDirectiveInputTopic); + return TaskStatus.delayed(Duration.MINUTE, oneShotTask($$ -> { + $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); + return TaskStatus.completed(Unit.UNIT); + })); + }); + } + }; + + private static final Topic decomposingActivityDirectiveInputTopic = new Topic<>(); + private static final Topic decomposingActivityDirectiveOutputTopic = new Topic<>(); + /* package-private */ static final DirectiveType decomposingActivityDirective = new DirectiveType<>() { + @Override + public InputType getInputType() { + return testModelInputType; + } + + @Override + public OutputType getOutputType() { + return testModelOutputType; + } + + @Override + public TaskFactory getTaskFactory(final Object o, final Object o2) { + return executor -> oneShotTask(scheduler -> { + scheduler.emit(this, decomposingActivityDirectiveInputTopic); + return TaskStatus.delayed( + Duration.ZERO, + oneShotTask($ -> { + try { + $.spawn(InSpan.Parent, delayedActivityDirective.getTaskFactory(null, null)); + } catch (final InstantiationException ex) { + throw new Error("Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( + ex.toString())); + } + return TaskStatus.delayed(Duration.of(120, Duration.SECOND), oneShotTask($$ -> { + try { + $$.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); + } catch (final InstantiationException ex) { + throw new Error( + "Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( + ex.toString())); + } + $$.emit(Unit.UNIT, decomposingActivityDirectiveOutputTopic); + return TaskStatus.completed(Unit.UNIT); + })); + })); + }); + } + }; + + static MissionModel missionModel = new MissionModel<>( + new Object(), + new LiveCells(null), + Map.of(), + List.of( + new MissionModel.SerializableTopic<>( + "ActivityType.Input.DelayActivityDirective", + delayedActivityDirectiveInputTopic, + testModelOutputType), + new MissionModel.SerializableTopic<>( + "ActivityType.Output.DelayActivityDirective", + delayedActivityDirectiveOutputTopic, + testModelOutputType), + new MissionModel.SerializableTopic<>( + "ActivityType.Input.DecomposingActivityDirective", + decomposingActivityDirectiveInputTopic, + testModelOutputType), + new MissionModel.SerializableTopic<>( + "ActivityType.Output.DecomposingActivityDirective", + decomposingActivityDirectiveOutputTopic, + testModelOutputType)), + List.of(), + DirectiveTypeRegistry.extract( + new ModelType<>() { + + @Override + public Map> getDirectiveTypes() { + return Map.of( + "DelayActivityDirective", + delayedActivityDirective, + "DecomposingActivityDirective", + decomposingActivityDirective); + } + + @Override + public InputType getConfigurationType() { + return testModelInputType; + } + + @Override + public Object instantiate( + final Instant planStart, + final Object configuration, + final Initializer builder) + { + return new Object(); + } + } + ) + ); + + private static Task oneShotTask(Function> f) { + return new Task<>() { + @Override + public TaskStatus step(final Scheduler scheduler) { + return f.apply(scheduler); + } + + @Override + public Task duplicate(Executor executor) { + return this; + } + }; + } +} diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java index b918ef3af7..3a280607c8 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java @@ -14,11 +14,13 @@ import java.util.Objects; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; import java.util.function.Consumer; import java.util.function.Supplier; public final class ThreadedTask implements Task { - private final boolean CACHE_READS = Boolean.parseBoolean(getEnv("THREADED_TASK_CACHE_READS", "false")); + public static boolean CACHE_READS = Boolean.parseBoolean(getEnv("THREADED_TASK_CACHE_READS", "false")); + private final boolean cacheReads = CACHE_READS; private final Scoped rootContext; private final Supplier task; @@ -98,6 +100,10 @@ public TaskStatus step(final Scheduler scheduler) { private void beginAsync() { final var handle = new ThreadedTaskHandle(); + if (((ExecutorService) this.executor).isShutdown()) { + throw new RuntimeException("Executor is shut down!"); + } + this.executor.execute(() -> { final TaskRequest request; try { @@ -146,7 +152,7 @@ public TaskResponse run(final TaskRequest request) { if (request instanceof TaskRequest.Resume resume) { final var scheduler = resume.scheduler; - final Consumer readLogger = CACHE_READS ? ThreadedTask.this.readLog::add : $ -> {}; + final Consumer readLogger = cacheReads ? ThreadedTask.this.readLog::add : $ -> {}; final var context = new ThreadedReactionContext(ThreadedTask.this.rootContext, scheduler, this, readLogger); try (final var restore = ThreadedTask.this.rootContext.set(context)) { @@ -260,7 +266,7 @@ public TaskAbort() { @Override public Task duplicate(Executor executor) { - if (!CACHE_READS) { + if (!cacheReads) { throw new RuntimeException("Cannot duplicate threaded task without cached reads"); } final ThreadedTask threadedTask = new ThreadedTask<>(executor, rootContext, task); From 20fd5839580d58f4c3c239033242a8bf00ae96c1 Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 6 Nov 2023 21:07:30 -0800 Subject: [PATCH 14/43] Forbid all operations except duplicate on closed sim engine --- .../driver/engine/SimulationEngine.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java index 9d8a19f0bd..08f70dd47a 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java @@ -56,6 +56,13 @@ * A representation of the work remaining to do during a simulation, and its accumulated results. */ public final class SimulationEngine implements AutoCloseable { + private static int numActiveSimulationEngines = 0; + private boolean closed = false; + + public static int getNumActiveSimulationEngines() { + return numActiveSimulationEngines; + } + /** The set of all jobs waiting for time to pass. */ private final JobSchedule scheduledJobs; /** The set of all jobs waiting on a condition. */ @@ -86,6 +93,7 @@ public final class SimulationEngine implements AutoCloseable { private final ExecutorService executor; public SimulationEngine() { + numActiveSimulationEngines++; scheduledJobs = new JobSchedule<>(); waitingTasks = new LinkedHashMap<>(); blockedTasks = new LinkedHashMap<>(); @@ -101,6 +109,7 @@ public SimulationEngine() { } private SimulationEngine(SimulationEngine other) { + numActiveSimulationEngines++; // New Executor allows other SimulationEngine to be closed executor = Executors.newVirtualThreadPerTaskExecutor(); scheduledJobs = other.scheduledJobs.duplicate(); @@ -130,6 +139,7 @@ private SimulationEngine(SimulationEngine other) { /** Schedule a new task to be performed at the given time. */ public SpanId scheduleTask(final Duration startTime, final TaskFactory state) { + if (this.closed) throw new IllegalStateException("Cannot schedule task on closed simulation engine"); if (startTime.isNegative()) throw new IllegalArgumentException("Cannot schedule a task before the start time of the simulation"); final var span = SpanId.generate(); @@ -148,6 +158,7 @@ public SpanId scheduleTask(final Duration startTime, final TaskFactory< /** Register a resource whose profile should be accumulated over time. */ public void trackResource(final String name, final Resource resource, final Duration nextQueryTime) { + if (this.closed) throw new IllegalStateException("Cannot track resource on closed simulation engine"); final var id = new ResourceId(name); this.resources.put(id, ProfilingState.create(resource)); @@ -156,6 +167,7 @@ void trackResource(final String name, final Resource resource, final D /** Schedules any conditions or resources dependent on the given topic to be re-checked at the given time. */ public void invalidateTopic(final Topic topic, final Duration invalidationTime) { + if (this.closed) throw new IllegalStateException("Cannot invalidate topic on closed simulation engine"); final var resources = this.waitingResources.invalidateTopic(topic); for (final var resource : resources) { this.scheduledJobs.schedule(JobId.forResource(resource), SubInstant.Resources.at(invalidationTime)); @@ -172,6 +184,7 @@ public void invalidateTopic(final Topic topic, final Duration invalidationTim /** Removes and returns the next set of jobs to be performed concurrently. */ public JobSchedule.Batch extractNextJobs(final Duration maximumTime) { + if (this.closed) throw new IllegalStateException("Cannot extract next jobs on closed simulation engine"); final var batch = this.scheduledJobs.extractNextJobs(maximumTime); // If we're signaling based on a condition, we need to untrack the condition before any tasks run. @@ -195,6 +208,7 @@ public Pair, Optional> performJobs( final Duration currentTime, final Duration maximumTime ) throws SpanException { + if (this.closed) throw new IllegalStateException("Cannot perform jobs on closed simulation engine"); var tip = EventGraph.empty(); Mutable> exception = new MutableObject<>(Optional.empty()); for (final var job$ : jobs) { @@ -234,7 +248,8 @@ public void performJob( } /** Perform the next step of a modeled task. */ - public void stepTask(final TaskId task, final TaskFrame frame, final Duration currentTime) throws SpanException { + public void stepTask(final TaskId task, final TaskFrame frame, final Duration currentTime) throws SpanException { + if (this.closed) throw new IllegalStateException("Cannot step task on closed simulation engine"); this.unstartedTasks.remove(task); // The handler for the next status of the task is responsible // for putting an updated state back into the task set. @@ -338,6 +353,7 @@ public void updateCondition( final Duration currentTime, final Duration horizonTime ) { + if (this.closed) throw new IllegalStateException("Cannot update condition on closed simulation engine"); final var querier = new EngineQuerier(frame); final var prediction = this.conditions .get(condition) @@ -362,6 +378,7 @@ public void updateResource( final TaskFrame frame, final Duration currentTime ) { + if (this.closed) throw new IllegalStateException("Cannot update resource on closed simulation engine"); final var querier = new EngineQuerier(frame); this.resources.get(resource).append(currentTime, querier); @@ -376,14 +393,17 @@ public void updateResource( /** Resets all tasks (freeing any held resources). The engine should not be used after being closed. */ @Override public void close() { + numActiveSimulationEngines--; for (final var task : this.tasks.values()) { task.state().release(); } this.executor.shutdownNow(); + this.closed = true; } public void unscheduleAfter(final Duration duration) { + if (this.closed) throw new IllegalStateException("Cannot unschedule jobs on closed simulation engine"); for (final var taskId : new ArrayList<>(this.tasks.keySet())) { if (this.unstartedTasks.containsKey(taskId) && this.unstartedTasks.get(taskId).longerThan(duration)) { this.tasks.remove(taskId); From 1e32b65937fd8185cb0fcf77d0ddea4ae46e11a9 Mon Sep 17 00:00:00 2001 From: maillard Date: Thu, 11 Apr 2024 16:05:45 -0700 Subject: [PATCH 15/43] Testing improvements --- .../jpl/aerie/foomissionmodel/Mission.java | 17 +- .../SimulationDuplicationTest.java | 207 ++++++++++++++---- .../driver/SimulationDuplicationTest.java | 20 +- .../aerie/merlin/framework/ThreadedTask.java | 2 +- .../services/LocalMissionModelService.java | 7 + .../simulation/ResumableSimulationDriver.java | 1 + 6 files changed, 200 insertions(+), 54 deletions(-) diff --git a/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/Mission.java b/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/Mission.java index 3cd7cdaae8..b56922bc22 100644 --- a/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/Mission.java +++ b/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/Mission.java @@ -41,6 +41,8 @@ public final class Mission { public final TimeTrackerDaemon timeTrackerDaemon = new TimeTrackerDaemon(); + public final Counter counter = Counter.ofInteger(); + public Mission(final Registrar registrar, final Instant planStart, final Configuration config) { this.cachedRegistrar = registrar; @@ -74,13 +76,20 @@ public Mission(final Registrar registrar, final Instant planStart, final Configu registrar.real("/simple_data/b/rate", this.simpleData.b.rate); registrar.real("/simple_data/total_volume", this.simpleData.totalVolume); + registrar.discrete("/counter", this.counter, new IntegerValueMapper()); + spawn(timeTrackerDaemon::run); - spawn(() -> { // Register a never-ending daemon task - while (true) { - ModelActions.delay(Duration.SECOND); + spawn(replaying(new Runnable() { + @Override + public void run() { // Register a never-ending daemon task + for (int i = 0; i < 1000; i++) { + ModelActions.delay(Duration.SECOND); + } + counter.add(1); + spawn(replaying(this)); } - }); + })); if(config.raiseException) { spawn(() -> { diff --git a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java index 46efd53dd9..4c9448c050 100644 --- a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java +++ b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java @@ -8,9 +8,8 @@ import gov.nasa.jpl.aerie.merlin.driver.MissionModelBuilder; import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; -import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; +import gov.nasa.jpl.aerie.merlin.driver.timeline.CausalEventSource; import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; -import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; import gov.nasa.jpl.aerie.merlin.framework.ThreadedTask; import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; @@ -43,6 +42,7 @@ import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTE; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTES; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; public class SimulationDuplicationTest { @@ -99,10 +99,9 @@ void emptyPlanTest() { @Test void testTrivialDuplicate() { - final Topic activityTopic = new Topic<>(); final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints( missionModel, - SimulationDriver.CachedSimulationEngine.empty(), List.of(Duration.of(5, MINUTES)), Map.of() + SimulationDriver.CachedSimulationEngine.empty(missionModel), List.of(Duration.of(5, MINUTES)), Map.of() ); final SimulationResults expected = SimulationDriver.simulate( missionModel, @@ -179,6 +178,162 @@ void testFooNonEmptyPlan() { assertResultsEqual(expected, results2.results()); } + @Test + void testFooNonEmptyPlanMultipleResumes() { + final MissionModel missionModel = makeMissionModel( + new MissionModelBuilder(), + Instant.EPOCH, + new Configuration()); + final Map schedule = Map.ofEntries( + activity(1, MINUTE, "foo", Map.of("z", SerializedValue.of(123))), + activity(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) + ); + final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints( + missionModel, + List.of(Duration.of(5, MINUTES)), + schedule + ); + final SimulationResults expected = SimulationDriver.simulate( + missionModel, + schedule, + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false); + assertResultsEqual(expected, results.results()); + + assertEquals(Duration.of(5, MINUTES), results.checkpoints().get(0).startOffset()); + + final SimulationDriver.SimulationResultsWithCheckpoints results2 = simulateWithCheckpoints( + missionModel, + results.checkpoints().get(0), + List.of(Duration.of(5, MINUTES)), + schedule + ); + + assertResultsEqual(expected, results2.results()); + + final SimulationDriver.SimulationResultsWithCheckpoints results3 = simulateWithCheckpoints( + missionModel, + results.checkpoints().get(0), + List.of(Duration.of(5, MINUTES)), + schedule + ); + + assertResultsEqual(expected, results3.results()); + } + + @Test + void testFooNonEmptyPlanMultipleCheckpointsMultipleResumes() { + final MissionModel missionModel = makeMissionModel( + new MissionModelBuilder(), + Instant.EPOCH, + new Configuration()); + final Map schedule = Map.ofEntries( + activity(1, MINUTE, "foo", Map.of("z", SerializedValue.of(123))), + activity(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) + ); + final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints( + missionModel, + List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), + schedule + ); + final SimulationResults expected = SimulationDriver.simulate( + missionModel, + schedule, + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false); + assertResultsEqual(expected, results.results()); + + assertEquals(Duration.of(5, MINUTES), results.checkpoints().get(0).startOffset()); + + final SimulationDriver.SimulationResultsWithCheckpoints results2 = simulateWithCheckpoints( + missionModel, + results.checkpoints().get(0), + List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), + schedule + ); + + assertResultsEqual(expected, results2.results()); + + final SimulationDriver.SimulationResultsWithCheckpoints results3 = simulateWithCheckpoints( + missionModel, + results.checkpoints().get(1), + List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), + schedule + ); + + assertResultsEqual(expected, results3.results()); + } + + @Test + void testFooNonEmptyPlanMultipleCheckpointsMultipleResumesWithEdits() { + final MissionModel missionModel = makeMissionModel( + new MissionModelBuilder(), + Instant.EPOCH, + new Configuration()); + final Pair activity1 = activity( + 1, + MINUTE, + "foo", + Map.of("z", SerializedValue.of(123))); + final Map schedule1 = Map.ofEntries( + activity1, + activity(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) + ); + final Map schedule2 = Map.ofEntries( + activity1, + activity(390, SECONDS, "foo", Map.of("z", SerializedValue.of(999))) + ); + final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints( + missionModel, + List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), + schedule1 + ); + final SimulationResults expected1 = SimulationDriver.simulate( + missionModel, + schedule1, + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false); + + final SimulationResults expected2 = SimulationDriver.simulate( + missionModel, + schedule2, + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false); + + assertResultsEqual(expected1, results.results()); + + assertEquals(Duration.of(5, MINUTES), results.checkpoints().get(0).startOffset()); + + final SimulationDriver.SimulationResultsWithCheckpoints results2 = simulateWithCheckpoints( + missionModel, + results.checkpoints().get(0), + List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), + schedule2 + ); + assertResultsEqual(expected2, results2.results()); + + final SimulationDriver.SimulationResultsWithCheckpoints results3 = simulateWithCheckpoints( + missionModel, + results.checkpoints().get(1), + List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), + schedule2 + ); + + assertResultsEqual(expected2, results3.results()); + } + private static long nextActivityDirectiveId = 0L; private static Pair activity(final long quantity, final Duration unit, final String type) { @@ -193,9 +348,6 @@ private static Pair activity(final long } private static Pair activity(final Duration startOffset, final String type, final Map args) { - if (nextActivityDirectiveId > 1) { - System.out.println(); - } return Pair.of(new ActivityDirectiveId(nextActivityDirectiveId++), new ActivityDirective(startOffset, type, args, null, true)); } @@ -252,8 +404,8 @@ static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints Duration.HOUR, $ -> {}, () -> false, - desiredCheckpoints, - cachedSimulationEngine); + cachedSimulationEngine, + SimulationDriver.desiredCheckpoints(desiredCheckpoints)); } static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints( @@ -261,37 +413,6 @@ static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints final List desiredCheckpoints, final Map schedule ) { - final SimulationEngine engine = new SimulationEngine(); - final TemporalEventSource timeline = new TemporalEventSource(); - final LiveCells cells = new LiveCells(timeline, missionModel.getInitialCells()); - - // Begin tracking all resources. - for (final var entry : missionModel.getResources().entrySet()) { - final var name = entry.getKey(); - final var resource = entry.getValue(); - - engine.trackResource(name, resource, Duration.ZERO); - } - - { - // Start daemon task(s) immediately, before anything else happens. - engine.scheduleTask(Duration.ZERO, missionModel.getDaemon()); - { - final var batch = engine.extractNextJobs(Duration.MAX_VALUE); - final var commit = engine.performJobs(batch.jobs(), cells, Duration.ZERO, Duration.MAX_VALUE); - timeline.add(commit); - } - } - - final var cachedSimulationEngine = new SimulationDriver.CachedSimulationEngine( - Duration.ZERO, - List.of(), - engine, - cells, - timeline.points(), - new Topic<>() - ); - return SimulationDriver.simulateWithCheckpoints( missionModel, schedule, @@ -301,8 +422,8 @@ static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints Duration.HOUR, $ -> {}, () -> false, - desiredCheckpoints, - cachedSimulationEngine); + SimulationDriver.CachedSimulationEngine.empty(missionModel), + SimulationDriver.desiredCheckpoints(desiredCheckpoints)); } private static final Topic delayedActivityDirectiveInputTopic = new Topic<>(); @@ -428,7 +549,7 @@ public TaskFactory getTaskFactory(final Object o, final Object o2) { static MissionModel missionModel = new MissionModel<>( new Object(), - new LiveCells(null), + new LiveCells(new CausalEventSource()), Map.of(), List.of( new MissionModel.SerializableTopic<>( diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java index d3b9b6629a..704ef1644b 100644 --- a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java @@ -1,6 +1,7 @@ package gov.nasa.jpl.aerie.merlin.driver; import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; +import gov.nasa.jpl.aerie.merlin.driver.timeline.CausalEventSource; import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; @@ -20,6 +21,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import org.apache.commons.lang3.tuple.Triple; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import java.time.Instant; @@ -74,7 +76,8 @@ void emptyPlanTest() { @Test void testDuplicate() { - final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints(SimulationDriver.CachedSimulationEngine.empty(), List.of(Duration.of(5, MINUTES))); + final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints(SimulationDriver.CachedSimulationEngine.empty( + missionModel), List.of(Duration.of(5, MINUTES))); final SimulationResults expected = SimulationDriver.simulate( missionModel, Map.of(), @@ -122,7 +125,6 @@ static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints Duration.HOUR, $ -> {}, () -> false, - desiredCheckpoints, new SimulationDriver.CachedSimulationEngine( Duration.ZERO, List.of(), @@ -130,7 +132,8 @@ static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints cells, timeline.points(), new Topic<>() - )); + ), + SimulationDriver.desiredCheckpoints(desiredCheckpoints)); } static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints( @@ -165,8 +168,8 @@ static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints Duration.HOUR, $ -> {}, () -> false, - desiredCheckpoints, - cachedEngine); + cachedEngine, + SimulationDriver.desiredCheckpoints(desiredCheckpoints)); } private static final Topic delayedActivityDirectiveInputTopic = new Topic<>(); @@ -278,7 +281,7 @@ public TaskFactory getTaskFactory(final Object o, final Object o2) { static MissionModel missionModel = new MissionModel<>( new Object(), - new LiveCells(null), + new LiveCells(new CausalEventSource()), Map.of(), List.of( new MissionModel.SerializableTopic<>( @@ -340,4 +343,9 @@ public Task duplicate(Executor executor) { } }; } + + @AfterEach + void afterEach() { + assertEquals(SimulationDriver.cachedEngines.size(), SimulationEngine.getNumActiveSimulationEngines()); + } } diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java index 3a280607c8..bab6058c51 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java @@ -19,7 +19,7 @@ import java.util.function.Supplier; public final class ThreadedTask implements Task { - public static boolean CACHE_READS = Boolean.parseBoolean(getEnv("THREADED_TASK_CACHE_READS", "false")); + public static boolean CACHE_READS = Boolean.parseBoolean(getEnv("THREADED_TASK_CACHE_READS", "true")); private final boolean cacheReads = CACHE_READS; private final Scoped rootContext; diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java index 6c1c2b4c65..5d606a24ea 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java @@ -7,6 +7,10 @@ import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; +import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; +import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; +import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.InputType.Parameter; import gov.nasa.jpl.aerie.merlin.protocol.model.InputType.ValidationNotice; import gov.nasa.jpl.aerie.merlin.protocol.model.ModelType; @@ -278,6 +282,9 @@ public Map getModelEffectiveArguments(final String miss .getEffectiveArguments(arguments); } + static Map> missionModelCache = new HashMap<>(); + static Map> cachedEngines = new HashMap<>(); + /** * Validate that a set of activity parameters conforms to the expectations of a named mission model. * diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java index 872d87e4d6..71a31394e1 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.Executor; import java.util.Set; import java.util.function.Supplier; From 78d289afd8fd29361b8fa1e59deed724d6efa566 Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Mon, 1 Apr 2024 22:36:29 -0700 Subject: [PATCH 16/43] Implement extractResource in goals, time exprs, and sched conds --- .../activities/ActivityExpression.java | 8 +++- .../scheduling/ConstraintState.java | 33 -------------- .../scheduling/GlobalConstraint.java | 19 -------- .../GlobalConstraintWithIntrospection.java | 8 ++-- .../TimeExpressionRelative.java | 2 + .../TimeExpressionRelativeBefore.java | 6 +++ .../TimeExpressionRelativeBinary.java | 7 +++ .../TimeExpressionRelativeSimple.java | 4 ++ .../scheduler/goals/ActivityTemplateGoal.java | 24 +++------- .../scheduler/goals/CardinalityGoal.java | 8 ---- .../scheduler/goals/CoexistenceGoal.java | 45 ++++--------------- .../scheduler/goals/CompositeAndGoal.java | 9 +++- .../nasa/jpl/aerie/scheduler/goals/Goal.java | 5 +++ .../jpl/aerie/scheduler/goals/OptionGoal.java | 9 ++++ .../aerie/scheduler/goals/RecurrenceGoal.java | 8 ---- .../jpl/aerie/scheduler/model/Problem.java | 8 ++-- .../scheduler/model/SchedulingCondition.java | 15 ++----- .../scheduler/solver/PrioritySolver.java | 5 +-- 18 files changed, 78 insertions(+), 145 deletions(-) delete mode 100644 scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/ConstraintState.java delete mode 100644 scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/GlobalConstraint.java diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityExpression.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityExpression.java index 75b32eb809..64e5eebbbf 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityExpression.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityExpression.java @@ -445,7 +445,13 @@ public String prettyPrint(final String prefix) { ); } @Override - public void extractResources(final Set names) { } + public void extractResources(final Set names) { + if(this.durationRange != null) { + this.durationRange.getLeft().extractResources(names); + this.durationRange.getRight().extractResources(names); + } + this.arguments.forEach((name, pe)-> pe.extractResources(names)); + } public Interval instantiateDurationInterval( final PlanningHorizon planningHorizon, diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/ConstraintState.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/ConstraintState.java deleted file mode 100644 index 85453e64a1..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/ConstraintState.java +++ /dev/null @@ -1,33 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.scheduling; - -import gov.nasa.jpl.aerie.constraints.time.Windows; - -/** - * Class similar to Conflict but for GlobalConstraints - */ -public class ConstraintState { - - /** - * constraint concerned by this state - */ - final public GlobalConstraint constraint; - - /** - * boolean stating whether the constraint is violated or not - */ - public boolean isViolation = true; - - /** - * intervals during which the constraint is violated - */ - final public Windows violationWindows; - - //readable explanation when possible - public String cause; - - public ConstraintState(GlobalConstraint constraint, boolean isViolation, Windows violationWindows) { - this.isViolation = isViolation; - this.violationWindows = violationWindows; - this.constraint = constraint; - } -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/GlobalConstraint.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/GlobalConstraint.java deleted file mode 100644 index d12c19a32a..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/GlobalConstraint.java +++ /dev/null @@ -1,19 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.scheduling; - -import gov.nasa.jpl.aerie.constraints.model.SimulationResults; -import gov.nasa.jpl.aerie.constraints.time.Windows; -import gov.nasa.jpl.aerie.scheduler.model.Plan; - -/** - * Interface defining methods that must be implemented by global constraints such as mutex or cardinality - * Also provides a directory for creating these constraints - */ -public interface GlobalConstraint { - - //todo: probably needs a domain - - //is the constraint enforced on its domain - ConstraintState isEnforced(Plan plan, Windows windows, SimulationResults simulationResults); - - -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/GlobalConstraintWithIntrospection.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/GlobalConstraintWithIntrospection.java index aaac4a1bed..e8d0eb55cd 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/GlobalConstraintWithIntrospection.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/GlobalConstraintWithIntrospection.java @@ -6,15 +6,15 @@ import gov.nasa.jpl.aerie.scheduler.model.Plan; import gov.nasa.jpl.aerie.scheduler.conflicts.Conflict; +import java.util.Set; + /** * Interface defining methods that must be implemented by global constraints such as mutex or cardinality * Also provides a directory for creating these constraints */ -public interface GlobalConstraintWithIntrospection extends GlobalConstraint { - +public interface GlobalConstraintWithIntrospection { //specific to introspectable constraint : find the windows in which we can insert activities without violating //the constraint Windows findWindows(Plan plan, Windows windows, Conflict conflict, SimulationResults simulationResults, EvaluationEnvironment evaluationEnvironment); - - + void extractResources(Set names); } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelative.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelative.java index 0f5ac45567..844e07a96a 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelative.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelative.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Set; /** * class allowing to define dynamic expressions of timepoints, relative to time anchors @@ -25,6 +26,7 @@ public abstract class TimeExpressionRelative { */ public abstract Interval computeTime(final SimulationResults simulationResults, final Plan plan, final Interval interval); public abstract Optional getAnchor(); + public abstract void extractResources(final Set names); protected final List> operations = new ArrayList<>(); diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeBefore.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeBefore.java index c64fc8bcc5..344f726836 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeBefore.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeBefore.java @@ -7,6 +7,7 @@ import gov.nasa.jpl.aerie.scheduler.model.Plan; import java.util.Optional; +import java.util.Set; public class TimeExpressionRelativeBefore extends TimeExpressionRelative { @@ -38,4 +39,9 @@ public Interval computeTime(final SimulationResults simulationResults, final Pla public Optional getAnchor() { return Optional.empty(); } + + @Override + public void extractResources(final Set names) { + expr.extractResources(names); + } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeBinary.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeBinary.java index d444f4eb84..f15adb4a84 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeBinary.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeBinary.java @@ -4,6 +4,7 @@ import gov.nasa.jpl.aerie.constraints.time.Interval; import gov.nasa.jpl.aerie.scheduler.model.Plan; +import java.util.Set; import java.util.Optional; public class TimeExpressionRelativeBinary extends TimeExpressionRelative { @@ -27,4 +28,10 @@ public Optional getAnchor(){ return Optional.empty(); } + + @Override + public void extractResources(final Set names) { + lowerBound.extractResources(names); + upperBound.extractResources(names); + } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeSimple.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeSimple.java index 2d010e42d6..b0bf30a98f 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeSimple.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeSimple.java @@ -6,6 +6,7 @@ import gov.nasa.jpl.aerie.scheduler.TimeUtility; import gov.nasa.jpl.aerie.scheduler.model.Plan; +import java.util.Set; import java.util.Optional; public class TimeExpressionRelativeSimple extends TimeExpressionRelative { @@ -57,4 +58,7 @@ public Interval computeTimeRelativeAbsolute(final Interval interval) { return retRange; } + + @Override + public void extractResources(final Set names) {} } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/ActivityTemplateGoal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/ActivityTemplateGoal.java index 6c84075509..55b80f8c73 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/ActivityTemplateGoal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/ActivityTemplateGoal.java @@ -1,15 +1,10 @@ package gov.nasa.jpl.aerie.scheduler.goals; -import gov.nasa.jpl.aerie.constraints.model.EvaluationEnvironment; import gov.nasa.jpl.aerie.constraints.time.Windows; import gov.nasa.jpl.aerie.constraints.tree.Expression; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; -import gov.nasa.jpl.aerie.scheduler.model.Plan; -import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; -import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; -import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; -import java.util.Optional; +import java.util.Set; /** * describes the desired existence of an activity matching a given template/preset @@ -74,9 +69,6 @@ protected ActivityTemplateGoal fill(ActivityTemplateGoal goal) { } else { goal.matchActTemplate = matchingActTemplate; } - - goal.initiallyEvaluatedTemporalContext = null; - return goal; } @@ -116,12 +108,10 @@ protected ActivityTemplateGoal() { } */ protected ActivityExpression matchActTemplate; - - /** - * checked by getConflicts every time it is invoked to see if the Window(s) - * corresponding to when this goal has changed, which is unexpected behavior - * that needs to be caught - */ - protected Windows initiallyEvaluatedTemporalContext; - + @Override + public void extractResources(final Set names) { + super.extractResources(names); + matchActTemplate.extractResources(names); + desiredActTemplate.extractResources(names); + } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CardinalityGoal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CardinalityGoal.java index 5a16b0edc1..9b0b7055e1 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CardinalityGoal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CardinalityGoal.java @@ -136,14 +136,6 @@ public Collection getConflicts( //unwrap temporalContext final var windows = getTemporalContext().evaluate(simulationResults, evaluationEnvironment); - //make sure it hasn't changed - if (this.initiallyEvaluatedTemporalContext != null && !windows.equals(this.initiallyEvaluatedTemporalContext)) { - throw new UnexpectedTemporalContextChangeException("The temporalContext Windows has changed from: " + this.initiallyEvaluatedTemporalContext.toString() + " to " + windows.toString()); - } - else if (this.initiallyEvaluatedTemporalContext == null) { - this.initiallyEvaluatedTemporalContext = windows; - } - //iterate through it and then within each iteration do exactly what you did before final var conflicts = new LinkedList(); diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CoexistenceGoal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CoexistenceGoal.java index 15f5752ae9..e1aa0a90ab 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CoexistenceGoal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CoexistenceGoal.java @@ -29,6 +29,7 @@ import java.util.Objects; import java.util.List; import java.util.Optional; +import java.util.Set; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; @@ -39,7 +40,6 @@ public class CoexistenceGoal extends ActivityTemplateGoal { private TimeExpressionRelative startExpr; private TimeExpressionRelative endExpr; - private DurationExpression durExpr; private String alias; private PersistentTimeAnchor persistentAnchor; /** @@ -47,10 +47,6 @@ public class CoexistenceGoal extends ActivityTemplateGoal { */ protected Expression expr; - /** - * used to check this hasn't changed, as if it did, that's probably unanticipated behavior - */ - protected Spans evaluatedExpr; /** * the builder can construct goals piecemeal via a series of method calls */ @@ -171,8 +167,6 @@ protected CoexistenceGoal fill(CoexistenceGoal goal) { goal.endExpr = endExpr; - goal.durExpr = durExpression; - goal.alias = alias; goal.persistentAnchor = Objects.requireNonNullElse(persistentAnchor, PersistentTimeAnchor.DISABLED); @@ -202,34 +196,11 @@ public java.util.Collection getConflicts( final EvaluationEnvironment evaluationEnvironment, final SchedulerModel schedulerModel) { //TODO: check if interval gets split and if so, notify user? - //NOTE: temporalContext IS A WINDOWS OVER WHICH THE GOAL APPLIES, USUALLY SOMETHING BROAD LIKE A MISSION PHASE - //NOTE: expr IS A WINDOWS OVER WHICH A COEXISTENCEGOAL APPLIES, FOR EXAMPLE THE WINDOWS CORRESPONDING TO 5 SECONDS AFTER EVERY BASICACTIVITY IS SCHEDULED - //NOTE: IF temporalContext IS SMALLER THAN expr OR SOMEHOW BISECTS IT, ODDS ARE THIS ISN'T ANTICIPATED USER BEHAVIOR. GENERALLY, ANALYZEWHEN SHOULDN'T BE PROVIDING - // A SMALLER WINDOW, AND HONESTLY DOESN'T MAKE SENSE TO USE ON TOP BUT IS SUPPORTED TO MAKE CODE MORE CONSISTENT. IF ONE NEEDS TO USE ANALYZEWHEN ON TOP - // OF COEXISTENCEGOAL THEY SHOULD PROBABLY REFACTOR THEIR COEXISTENCE GOAL. ONE SUCH USE WOULD BE IF THE COEXISTENCEGOAL WAS SPECIFIED IN TERMS OF - // AN ACTIVITYEXPRESSION AND THEN ANALYZEWHEN WAS A MISSION PHASE, ALTHOUGH IT IS POSSIBLE TO JUST SPECIFY AN EXPRESSION THAT COMBINES THOSE. - //unwrap temporalContext final var windows = getTemporalContext().evaluate(simulationResults, evaluationEnvironment); - //make sure it hasn't changed - if (this.initiallyEvaluatedTemporalContext != null && !windows.includes(this.initiallyEvaluatedTemporalContext)) { - throw new UnexpectedTemporalContextChangeException("The temporalContext Windows has changed from: " + this.initiallyEvaluatedTemporalContext.toString() + " to " + windows); - } - else if (this.initiallyEvaluatedTemporalContext == null) { - this.initiallyEvaluatedTemporalContext = windows; - } - final var anchors = expr.evaluate(simulationResults, evaluationEnvironment).intersectWith(windows); - //make sure expr hasn't changed either as that could yield unexpected behavior - if (this.evaluatedExpr != null && !anchors.isCollectionSubsetOf(this.evaluatedExpr)) { - throw new UnexpectedTemporalContextChangeException("The expr Windows has changed from: " + this.expr.toString() + " to " + anchors); - } - else if (this.initiallyEvaluatedTemporalContext == null) { - this.evaluatedExpr = anchors; - } - // can only check if bisection has happened if you can extract the interval from expr like you do in computeRange but without the final windows parameter, // then use that and compare it to local variable windows to check for bisection; // I can add that, but it doesn't seem necessary for now. @@ -266,12 +237,6 @@ else if (this.initiallyEvaluatedTemporalContext == null) { activityFinder.endsIn(endTimeRange); activityCreationTemplate.endsIn(endTimeRange); } - /* this will override whatever might be already present in the template */ - if (durExpr != null) { - var durRange = this.durExpr.compute(window.interval(), simulationResults); - activityFinder.durationIn(durRange); - activityCreationTemplate.durationIn(durRange); - } final var activitiesFound = plan.find( activityFinder.build(), @@ -412,6 +377,14 @@ private EvaluationEnvironment createEvaluationEnvironmentFromAnchor(EvaluationEn } } + @Override + public void extractResources(final Set names) { + super.extractResources(names); + this.expr.extractResources(names); + if(this.startExpr != null) this.startExpr.extractResources(names); + if(this.endExpr != null) this.endExpr.extractResources(names); + } + /** * ctor creates an empty goal without details * diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CompositeAndGoal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CompositeAndGoal.java index cb23dc9e3e..600d52ce31 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CompositeAndGoal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CompositeAndGoal.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; /** * Class representing a conjunction of goal as a goal @@ -38,5 +39,11 @@ public List getSubgoals() { return goals; } - + @Override + public void extractResources(final Set names) { + super.extractResources(names); + for(final var goal: goals){ + goal.extractResources(names); + } + } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Goal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Goal.java index ac375ec1a0..8d10f333a0 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Goal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Goal.java @@ -18,6 +18,7 @@ import java.util.LinkedList; import java.util.List; +import java.util.Set; import java.util.Optional; /** @@ -253,6 +254,10 @@ protected Goal fill(Goal goal) { }//Builder + public void extractResources(Set names) { + temporalContext.extractResources(names); + if(resourceConstraints != null) resourceConstraints.extractResources(names); + } /** * fetches the human-legible identifier of the goal diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/OptionGoal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/OptionGoal.java index 4cac7cca10..50f4d57f49 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/OptionGoal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/OptionGoal.java @@ -13,6 +13,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.Optional; public class OptionGoal extends Goal { @@ -43,6 +44,14 @@ public java.util.Collection getConflicts( throw new NotImplementedException("Conflict detection is performed at solver level"); } + @Override + public void extractResources(final Set names) { + super.extractResources(names); + for(final var goal: goals){ + goal.extractResources(names); + } + } + public static class Builder extends Goal.Builder { final List goals = new ArrayList<>(); diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/RecurrenceGoal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/RecurrenceGoal.java index c5168ad05f..36995145e4 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/RecurrenceGoal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/RecurrenceGoal.java @@ -133,14 +133,6 @@ public java.util.Collection getConflicts( } } - //make sure it hasn't changed - if (this.initiallyEvaluatedTemporalContext != null && !windows.includes(this.initiallyEvaluatedTemporalContext)) { - throw new UnexpectedTemporalContextChangeException("The temporalContext Windows has changed from: " + this.initiallyEvaluatedTemporalContext.toString() + " to " + windows); - } - else if (this.initiallyEvaluatedTemporalContext == null) { - this.initiallyEvaluatedTemporalContext = windows; - } - //iterate through it and then within each iteration do exactly what you did before for (Interval subInterval : windows.iterateEqualTo(true)) { //collect all matching target acts ordered by start time diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Problem.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Problem.java index 8b9f39bf61..ac8435bf13 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Problem.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Problem.java @@ -6,7 +6,7 @@ import gov.nasa.jpl.aerie.merlin.driver.MissionModel; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; -import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.GlobalConstraint; +import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.GlobalConstraintWithIntrospection; import gov.nasa.jpl.aerie.scheduler.goals.Goal; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationData; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; @@ -44,7 +44,7 @@ public class Problem { /** * global constraints in the mission model, indexed by name */ - private final List globalConstraints + private final List globalConstraints = new java.util.LinkedList<>(); private final Map realExternalProfiles = new HashMap<>(); @@ -109,11 +109,11 @@ public PlanningHorizon getPlanningHorizon(){ * * @param globalConstraint IN the global constraint */ - public void add(GlobalConstraint globalConstraint) { + public void add(GlobalConstraintWithIntrospection globalConstraint) { this.globalConstraints.add(globalConstraint); } - public List getGlobalConstraints() { + public List getGlobalConstraints() { return this.globalConstraints; } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingCondition.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingCondition.java index 29903aeb01..65a5567759 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingCondition.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingCondition.java @@ -7,10 +7,10 @@ import gov.nasa.jpl.aerie.scheduler.conflicts.Conflict; import gov.nasa.jpl.aerie.scheduler.conflicts.MissingActivityInstanceConflict; import gov.nasa.jpl.aerie.scheduler.conflicts.MissingActivityTemplateConflict; -import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.ConstraintState; import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.GlobalConstraintWithIntrospection; import java.util.List; +import java.util.Set; public record SchedulingCondition( Expression expression, @@ -41,18 +41,11 @@ public Windows findWindows( } @Override - public ConstraintState isEnforced( - final Plan plan, - final Windows windows, - final SimulationResults simulationResults) - { - // A SchedulingCondition is never "violated" per se - if there are no windows in which - // activities can be placed, that does not mean that it has been violated. - // TODO: As of writing isEnforced is unused. Either remove or come up with a more coherent plan for GlobalConstraints. - return new ConstraintState(this, false, null); + public void extractResources(final Set names) { + this.expression.extractResources(names); } - static boolean anyMatch(final List activityTypes, final ActivityType type) { + private static boolean anyMatch(final List activityTypes, final ActivityType type) { // TODO we may want to handle more complex activity expressions, not just type. for (final var activityType : activityTypes) { if (type.equals(activityType)) { diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java index f4a9cfe3b5..a7327d3dee 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java @@ -19,7 +19,6 @@ import gov.nasa.jpl.aerie.scheduler.conflicts.MissingActivityTemplateConflict; import gov.nasa.jpl.aerie.scheduler.conflicts.MissingAssociationConflict; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; -import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.GlobalConstraint; import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.GlobalConstraintWithIntrospection; import gov.nasa.jpl.aerie.scheduler.goals.ActivityTemplateGoal; import gov.nasa.jpl.aerie.scheduler.goals.CompositeAndGoal; @@ -955,7 +954,7 @@ private Windows narrowGlobalConstraints( Plan plan, MissingActivityConflict mac, Windows windows, - Collection constraints, + Collection constraints, EvaluationEnvironment evaluationEnvironment ) throws SchedulingInterruptedException { Windows tmp = windows; @@ -966,7 +965,7 @@ private Windows narrowGlobalConstraints( logger.debug("Computing simulation results until "+ tmp.maxTrueTimePoint().get().getKey() + " in order to compute global scheduling conditions"); final var latestSimulationResults = this.getLatestSimResultsUpTo(tmp.maxTrueTimePoint().get().getKey()); synchronizeSimulationWithSchedulerPlan(); - for (GlobalConstraint gc : constraints) { + for (GlobalConstraintWithIntrospection gc : constraints) { if (gc instanceof GlobalConstraintWithIntrospection c) { tmp = c.findWindows( plan, From 96c9b67ace0424a977d8c39f1054aede004281f4 Mon Sep 17 00:00:00 2001 From: maillard Date: Mon, 5 Feb 2024 17:43:33 -0800 Subject: [PATCH 17/43] Remove duration expressions --- .../DurationExpression.java | 19 ------------- .../DurationExpressionDur.java | 21 -------------- .../DurationExpressionMax.java | 28 ------------------- .../DurationExpressionMinus.java | 22 --------------- .../DurationExpressionRelative.java | 24 ---------------- .../DurationExpressionState.java | 20 ------------- .../DurationExpressions.java | 19 ------------- .../scheduler/goals/CoexistenceGoal.java | 10 ------- 8 files changed, 163 deletions(-) delete mode 100644 scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpression.java delete mode 100644 scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionDur.java delete mode 100644 scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionMax.java delete mode 100644 scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionMinus.java delete mode 100644 scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionRelative.java delete mode 100644 scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionState.java delete mode 100644 scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressions.java diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpression.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpression.java deleted file mode 100644 index 0589e7a4c5..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpression.java +++ /dev/null @@ -1,19 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.durationexpressions; - -import gov.nasa.jpl.aerie.constraints.model.SimulationResults; -import gov.nasa.jpl.aerie.constraints.time.Interval; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; - -public interface DurationExpression { - - enum DurationAnchorEnum { - WindowDuration - } - - Duration compute(final Interval interval, final SimulationResults simulationResults); - - default DurationExpression minus(DurationExpression other){ - return new DurationExpressionMinus(this, other); - } - -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionDur.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionDur.java deleted file mode 100644 index bb2c5d7025..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionDur.java +++ /dev/null @@ -1,21 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.durationexpressions; - -import gov.nasa.jpl.aerie.constraints.model.SimulationResults; -import gov.nasa.jpl.aerie.constraints.time.Interval; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; - -public class DurationExpressionDur implements DurationExpression { - - - final Duration dur; - - public DurationExpressionDur(Duration dur){ - this.dur = dur; - } - - - @Override - public Duration compute(final Interval interval, final SimulationResults simulationResults) { - return dur; - } -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionMax.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionMax.java deleted file mode 100644 index 78a903e013..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionMax.java +++ /dev/null @@ -1,28 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.durationexpressions; - -import gov.nasa.jpl.aerie.constraints.model.SimulationResults; -import gov.nasa.jpl.aerie.constraints.time.Interval; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; - -import java.util.List; - -public class DurationExpressionMax implements DurationExpression{ - - final List exprs; - - public DurationExpressionMax(DurationExpression... exprs){ - this.exprs = List.of(exprs); - } - - @Override - public Duration compute(final Interval interval, final SimulationResults simulationResults) { - var computed = new Duration[exprs.size()]; - int i = 0; - for(var expr: exprs){ - computed[i++]=expr.compute(interval, simulationResults); - } - - return Duration.max(computed); - - } -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionMinus.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionMinus.java deleted file mode 100644 index bd706bf986..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionMinus.java +++ /dev/null @@ -1,22 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.durationexpressions; - -import gov.nasa.jpl.aerie.constraints.model.SimulationResults; -import gov.nasa.jpl.aerie.constraints.time.Interval; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; - -public class DurationExpressionMinus implements DurationExpression{ - - final DurationExpression expr1; - final DurationExpression expr2; - - public DurationExpressionMinus(DurationExpression expr1, DurationExpression expr2){ - this.expr1 = expr1; - this.expr2 = expr2; - - } - - @Override - public Duration compute(final Interval interval, final SimulationResults simulationResults) { - return expr1.compute(interval, simulationResults).minus(expr2.compute(interval, simulationResults)); - } -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionRelative.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionRelative.java deleted file mode 100644 index c624aa1450..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionRelative.java +++ /dev/null @@ -1,24 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.durationexpressions; - -import gov.nasa.jpl.aerie.constraints.model.SimulationResults; -import gov.nasa.jpl.aerie.constraints.time.Interval; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; - -public class DurationExpressionRelative implements DurationExpression { - - - final DurationAnchorEnum anchor; - - public DurationExpressionRelative(DurationAnchorEnum anchor){ - this.anchor = anchor; - } - - - @Override - public Duration compute(final Interval interval, final SimulationResults simulationResults) { - if(anchor==DurationAnchorEnum.WindowDuration){ - return interval.duration(); - } - throw new IllegalArgumentException("Not implemented: Duration anchor different than WindowDuration"); - } -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionState.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionState.java deleted file mode 100644 index e52b8fa271..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionState.java +++ /dev/null @@ -1,20 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.durationexpressions; - -import gov.nasa.jpl.aerie.constraints.model.SimulationResults; -import gov.nasa.jpl.aerie.constraints.time.Interval; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.scheduler.constraints.resources.StateQueryParam; - -public class DurationExpressionState implements DurationExpression { - - final StateQueryParam state; - - public DurationExpressionState(StateQueryParam state){ - this.state = state; - } - - @Override - public Duration compute(final Interval interval, final SimulationResults simulationResults) { - return Duration.of(state.getValue(simulationResults, null, interval).asInt().orElseThrow(), Duration.MICROSECONDS); - } -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressions.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressions.java deleted file mode 100644 index e49f2eb6f4..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressions.java +++ /dev/null @@ -1,19 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.durationexpressions; - -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; - -public class DurationExpressions { - - public static DurationExpression constant(Duration dur){ - return new DurationExpressionDur(dur); - } - - public static DurationExpression windowDuration(){ - return new DurationExpressionRelative(DurationExpression.DurationAnchorEnum.WindowDuration); - } - - public static DurationExpression max(DurationExpression... expr){ - return new DurationExpressionMax(expr); - } - -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CoexistenceGoal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CoexistenceGoal.java index e1aa0a90ab..4ba2f1ec2c 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CoexistenceGoal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CoexistenceGoal.java @@ -14,7 +14,6 @@ import gov.nasa.jpl.aerie.scheduler.conflicts.MissingActivityTemplateConflict; import gov.nasa.jpl.aerie.scheduler.conflicts.MissingAssociationConflict; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; -import gov.nasa.jpl.aerie.scheduler.constraints.durationexpressions.DurationExpression; import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeAnchor; import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeExpressionRelative; import gov.nasa.jpl.aerie.scheduler.model.PersistentTimeAnchor; @@ -22,7 +21,6 @@ import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; import org.apache.commons.collections4.BidiMap; -import gov.nasa.jpl.aerie.scheduler.solver.stn.TaskNetworkAdapter; import java.util.ArrayList; import java.util.HashMap; @@ -31,8 +29,6 @@ import java.util.Optional; import java.util.Set; -import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; - /** * describes the desired coexistence of an activity with another */ @@ -64,12 +60,6 @@ public Builder startsAt(TimeExpressionRelative TimeExpressionRelative) { return getThis(); } - protected DurationExpression durExpression; - public Builder durationIn(DurationExpression durExpr){ - this.durExpression = durExpr; - return getThis(); - } - protected TimeExpressionRelative startExpr; public Builder endsAt(TimeExpressionRelative TimeExpressionRelative) { From 83251a2b0971c52b3423f6e95ee274ac6f563b70 Mon Sep 17 00:00:00 2001 From: maillard Date: Mon, 5 Feb 2024 17:44:41 -0800 Subject: [PATCH 18/43] Allow plan duplication and replacement operations --- .../nasa/jpl/aerie/scheduler/model/Plan.java | 13 +++ .../aerie/scheduler/model/PlanInMemory.java | 93 ++++++++++--------- .../aerie/scheduler/solver/Evaluation.java | 50 ++++++++++ 3 files changed, 114 insertions(+), 42 deletions(-) diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Plan.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Plan.java index 2d6963aec0..7d5a99942c 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Plan.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Plan.java @@ -19,6 +19,19 @@ */ public interface Plan { + /** + * Replace a directive by another in the plan and its attached Evaluation + * @param toBeReplaced the activity to be replaced + * @param replacement the replacement activity + */ + void replace(SchedulingActivityDirective toBeReplaced, SchedulingActivityDirective replacement); + + /** + * Duplicates a plan + * @return the duplicate plan + */ + Plan duplicate(); + /** * adds the given activity instances to the scheduled plan solution * diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/PlanInMemory.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/PlanInMemory.java index f15d9e9503..53a862ac54 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/PlanInMemory.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/PlanInMemory.java @@ -6,6 +6,7 @@ import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; import gov.nasa.jpl.aerie.scheduler.solver.Evaluation; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -32,35 +33,37 @@ public class PlanInMemory implements Plan { */ protected Evaluation evaluation; - /** - * container of all activity instances in plan, indexed by name - */ - private final HashMap actsById - = new HashMap<>(); - - /** - * container of all activity instances in plan, indexed by type - */ - private final HashMap> actsByType - = new HashMap<>(); - /** * container of all activity instances in plan, indexed by start time */ - private final TreeMap> actsByTime - = new TreeMap<>(); - - /** - * container of all activity instances in plan - */ - private final HashSet actsSet - = new HashSet<>(); + private final TreeMap> actsByTime; /** * ctor creates a new empty solution plan * */ public PlanInMemory() { + this.actsByTime = new TreeMap<>(); + } + + @Override + public void replace(final SchedulingActivityDirective toBeReplaced, final SchedulingActivityDirective replacement) { + if(evaluation != null) evaluation.replace(toBeReplaced, replacement); + remove(toBeReplaced); + add(replacement); + } + + public PlanInMemory(final PlanInMemory other){ + if(other.evaluation != null) this.evaluation = other.evaluation.duplicate(); + this.actsByTime = new TreeMap<>(); + for(final var entry: other.actsByTime.entrySet()){ + this.actsByTime.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } + } + + @Override + public Plan duplicate() { + return new PlanInMemory(this); } /** @@ -73,12 +76,19 @@ public void add(Collection acts) { } } + public int size(){ + int size = 0; + for(final var entry: this.actsByTime.entrySet()){ + size += entry.getValue().size(); + } + return size; + } /** * {@inheritDoc} */ @Override - public void add(SchedulingActivityDirective act) { + public void add(final SchedulingActivityDirective act) { if (act == null) { throw new IllegalArgumentException( "adding null activity to plan"); @@ -90,20 +100,8 @@ public void add(SchedulingActivityDirective act) { } final var id = act.getId(); assert id != null; - if (actsById.containsKey(id)) { - throw new IllegalArgumentException( - "adding activity with duplicate name=" + id + " to plan"); - } - final var type = act.getType(); - assert type != null; - - actsById.put(id, act); - //REVIEW: use a cleaner multimap? maybe guava actsByTime.computeIfAbsent(startT, k -> new LinkedList<>()) .add(act); - actsByType.computeIfAbsent(type, k -> new LinkedList<>()) - .add(act); - actsSet.add(act); } @Override @@ -115,13 +113,8 @@ public void remove(Collection acts) { @Override public void remove(SchedulingActivityDirective act) { - //TODO: handle ownership. Constraint propagation ? - actsById.remove(act.getId()); var acts = actsByTime.get(act.startOffset()); if (acts != null) acts.remove(act); - acts = actsByType.get(act.getType()); - if (acts != null) acts.remove(act); - actsSet.remove(act); } /** @@ -152,17 +145,29 @@ public void replaceActivity(SchedulingActivityDirective oldAct, SchedulingActivi */ @Override public Map> getActivitiesByType() { - return Collections.unmodifiableMap(actsByType); + final var map = new HashMap>(); + for(final var entry: this.actsByTime.entrySet()){ + for(final var activity : entry.getValue()){ + map.computeIfAbsent(activity.type(), t -> new ArrayList<>()).add(activity); + } + } + return Collections.unmodifiableMap(map); } @Override public Map getActivitiesById() { - return Collections.unmodifiableMap(actsById); + final var map = new HashMap(); + for(final var entry: this.actsByTime.entrySet()){ + for(final var activity : entry.getValue()){ + map.put(activity.id(), activity); + } + } + return Collections.unmodifiableMap(map); } @Override public Set getAnchorIds() { - return actsSet.stream() + return getActivities().stream() .map(SchedulingActivityDirective::anchorId) .collect(Collectors.toSet()); } @@ -172,7 +177,11 @@ public Set getAnchorIds() { */ @Override public Set getActivities() { - return Collections.unmodifiableSet(actsSet); + final var set = new HashSet(); + for(final var entry: this.actsByTime.entrySet()){ + set.addAll(entry.getValue()); + } + return Collections.unmodifiableSet(set); } /** diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/Evaluation.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/Evaluation.java index a7b096f47c..7f90e24e50 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/Evaluation.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/Evaluation.java @@ -83,6 +83,31 @@ public Optional getNbConflictsDetected() { */ public void associate(SchedulingActivityDirective act, boolean createdByThisGoal) { acts.put(act, createdByThisGoal);} + /** + * Replaces an activity in the goal evaluation by another activity + * @param toBeReplaced the activity to be replaced + * @param replacement the replacement activity + */ + public void replace(final SchedulingActivityDirective toBeReplaced, final SchedulingActivityDirective replacement){ + final var found = acts.get(toBeReplaced); + if(found != null){ + acts.remove(toBeReplaced); + acts.put(replacement, found); + } + } + + /** + * Duplicates the GoalEvaluation + * @return the duplicate + */ + public GoalEvaluation duplicate(){ + final var duplicate = new GoalEvaluation(); + duplicate.acts.putAll(this.acts); + duplicate.nbConflictsDetected = this.nbConflictsDetected; + duplicate.score = this.score; + return duplicate; + } + /** * flags all given activities as contributing to the goal's (dis)satisfaction * @@ -141,6 +166,31 @@ public java.util.Collection getGoals() { return goalEvals.keySet(); } + /** + * Replaces an activity in the Evaluation by another activity + * @param toBeReplaced the activity to be replaced + * @param replacement the replacement activity + */ + public void replace( + final SchedulingActivityDirective toBeReplaced, + final SchedulingActivityDirective replacement){ + for(final var goalEval: goalEvals.entrySet()){ + goalEval.getValue().replace(toBeReplaced, replacement); + } + } + + /** + * Duplicates the Evaluation + * @return the duplicate evaluation + */ + public Evaluation duplicate(){ + final var duplicate = new Evaluation(); + for(final var goalEvaluation : goalEvals.entrySet()){ + duplicate.goalEvals.put(goalEvaluation.getKey(), goalEvaluation.getValue().duplicate()); + } + return duplicate; + } + /** * fetch all goals and their current individual evaluation * From e09bc9ed11895c6ad67811b18e688d0ef0afe4d0 Mon Sep 17 00:00:00 2001 From: maillard Date: Mon, 5 Feb 2024 17:45:18 -0800 Subject: [PATCH 19/43] Add parent to string representation of sched act directive --- .../jpl/aerie/scheduler/model/SchedulingActivityDirective.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingActivityDirective.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingActivityDirective.java index aaeb28c314..ee8e2b275c 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingActivityDirective.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingActivityDirective.java @@ -264,7 +264,7 @@ public ActivityType getType() { } public String toString() { - return "[" + this.type.getName() + ","+ this.id + "," + startOffset + "," + ((duration != null) ? getEndTime() : "no duration") + ", "+anchorId+", "+anchoredToStart+"]"; + return "[" + this.type.getName() + ","+ this.id + "," + startOffset + "," + ((duration != null) ? getEndTime() : "no duration") + ", "+ topParent + ", " + anchorId+", "+anchoredToStart+"]"; } /** From ab444b2ec0c8d11369c62f27b655af849aebf833 Mon Sep 17 00:00:00 2001 From: maillard Date: Thu, 11 Apr 2024 16:06:45 -0700 Subject: [PATCH 20/43] Add cached engine store --- .../jpl/aerie/merlin/driver/CachedEngineStore.java | 10 ++++++++++ .../nasa/jpl/aerie/merlin/driver/MissionModelId.java | 3 +++ .../merlin/driver/SimulationEngineConfiguration.java | 12 ++++++++++++ .../merlin/driver/SimulationDuplicationTest.java | 5 +++-- 4 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CachedEngineStore.java create mode 100644 merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModelId.java create mode 100644 merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationEngineConfiguration.java diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CachedEngineStore.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CachedEngineStore.java new file mode 100644 index 0000000000..f912d44e8e --- /dev/null +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CachedEngineStore.java @@ -0,0 +1,10 @@ +package gov.nasa.jpl.aerie.merlin.driver; + +import java.util.List; + +public interface CachedEngineStore { + void save(final SimulationDriver.CachedSimulationEngine cachedSimulationEngine, + final SimulationEngineConfiguration configuration); + List getCachedEngines( + final SimulationEngineConfiguration configuration); +} diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModelId.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModelId.java new file mode 100644 index 0000000000..5f3d95bd8c --- /dev/null +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModelId.java @@ -0,0 +1,3 @@ +package gov.nasa.jpl.aerie.merlin.driver; + +public record MissionModelId(long id) {} diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationEngineConfiguration.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationEngineConfiguration.java new file mode 100644 index 0000000000..3f1f4e4294 --- /dev/null +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationEngineConfiguration.java @@ -0,0 +1,12 @@ +package gov.nasa.jpl.aerie.merlin.driver; + +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; + +import java.time.Instant; +import java.util.Map; + +public record SimulationEngineConfiguration( + Map simulationConfiguration, + Instant simStartTime, + MissionModelId missionModelId +) {} diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java index 704ef1644b..8d895498bf 100644 --- a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java @@ -127,11 +127,12 @@ static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints () -> false, new SimulationDriver.CachedSimulationEngine( Duration.ZERO, - List.of(), + Map.of(), engine, cells, timeline.points(), - new Topic<>() + new Topic<>(), + missionModel ), SimulationDriver.desiredCheckpoints(desiredCheckpoints)); } From 41aa0e83474bc621eae65d62f240e0def0be51e8 Mon Sep 17 00:00:00 2001 From: maillard Date: Thu, 11 Apr 2024 16:07:16 -0700 Subject: [PATCH 21/43] Compute simulation for subset of resources + separate activity results --- .../driver/engine/SimulationEngine.java | 158 +++++++++++++----- 1 file changed, 115 insertions(+), 43 deletions(-) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java index 08f70dd47a..67488e7782 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java @@ -51,6 +51,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; +import java.util.stream.Collectors; /** * A representation of the work remaining to do during a simulation, and its accumulated results. @@ -524,19 +525,15 @@ public static Optional getDirectiveIdFromSpan( return directiveSpanId.map(spanInfo::getDirective); } - /** Compute a set of results from the current state of simulation. */ - // TODO: Move result extraction out of the SimulationEngine. - // The Engine should only need to stream events of interest to a downstream consumer. - // The Engine cannot be cognizant of all downstream needs. - // TODO: Whatever mechanism replaces `computeResults` also ought to replace `isTaskComplete`. - // TODO: Produce results for all tasks, not just those that have completed. - // Planners need to be aware of failed or unfinished tasks. - public static SimulationResults computeResults( - final SimulationEngine engine, - final Instant startTime, - final Duration elapsedTime, - final Topic activityTopic, + public record SimulationActivityExtract( + Instant startTime, + Duration duration, + Map simulatedActivities, + Map unfinishedActivities){} + + private static SpanInfo computeTaskInfo( final TemporalEventSource timeline, + final Topic activityTopic, final Iterable> serializableTopics ) { // Collect per-span information from the event graph. @@ -548,37 +545,34 @@ public static SimulationResults computeResults( final var trait = new SpanInfo.Trait(serializableTopics, activityTopic); p.events().evaluate(trait, trait::atom).accept(spanInfo); } + return spanInfo; + } - // Extract profiles for every resource. - final var realProfiles = new HashMap>>>(); - final var discreteProfiles = new HashMap>>>(); - - for (final var entry : engine.resources.entrySet()) { - final var id = entry.getKey(); - final var state = entry.getValue(); - - final var name = id.id(); - final var resource = state.resource(); - - switch (resource.getType()) { - case "real" -> realProfiles.put( - name, - Pair.of( - resource.getOutputType().getSchema(), - serializeProfile(elapsedTime, state, SimulationEngine::extractRealDynamics))); - - case "discrete" -> discreteProfiles.put( - name, - Pair.of( - resource.getOutputType().getSchema(), - serializeProfile(elapsedTime, state, SimulationEngine::extractDiscreteDynamics))); - - default -> - throw new IllegalArgumentException( - "Resource `%s` has unknown type `%s`".formatted(name, resource.getType())); - } - } + public static SimulationActivityExtract computeActivitySimulationResults( + final SimulationEngine engine, + final Instant startTime, + final Duration elapsedTime, + final Topic activityTopic, + final TemporalEventSource timeline, + final Iterable> serializableTopics + ){ + return computeActivitySimulationResults( + engine, + startTime, + elapsedTime, + computeTaskInfo(timeline, activityTopic, serializableTopics) + ); + } + /** + * Computes only activity-related results when resources are not needed + */ + public static SimulationActivityExtract computeActivitySimulationResults( + final SimulationEngine engine, + final Instant startTime, + final Duration elapsedTime, + final SpanInfo spanInfo + ){ // Identify the nearest ancestor *activity* (excluding intermediate anonymous tasks). final var activityParents = new HashMap(); final var activityDirectiveIds = new HashMap(); @@ -652,6 +646,80 @@ public static SimulationResults computeResults( )); } }); + return new SimulationActivityExtract(startTime, elapsedTime, simulatedActivities, unfinishedActivities); + } + + public static SimulationResults computeResults( + final SimulationEngine engine, + final Instant startTime, + final Duration elapsedTime, + final Topic activityTopic, + final TemporalEventSource timeline, + final Iterable> serializableTopics + ) { + return computeResults( + engine, + startTime, + elapsedTime, + activityTopic, + timeline, + serializableTopics, + engine.resources.keySet() + .stream() + .map(ResourceId::id) + .collect(Collectors.toSet())); + } + + /** Compute a set of results from the current state of simulation. */ + // TODO: Move result extraction out of the SimulationEngine. + // The Engine should only need to stream events of interest to a downstream consumer. + // The Engine cannot be cognizant of all downstream needs. + // TODO: Whatever mechanism replaces `computeResults` also ought to replace `isTaskComplete`. + // TODO: Produce results for all tasks, not just those that have completed. + // Planners need to be aware of failed or unfinished tasks. + public static SimulationResults computeResults( + final SimulationEngine engine, + final Instant startTime, + final Duration elapsedTime, + final Topic activityTopic, + final TemporalEventSource timeline, + final Iterable> serializableTopics, + final Set resourceNames + ) { + // Collect per-task information from the event graph. + final var taskInfo = computeTaskInfo(timeline, activityTopic, serializableTopics); + + // Extract profiles for every resource. + final var realProfiles = new HashMap>>>(); + final var discreteProfiles = new HashMap>>>(); + + for (final var entry : engine.resources.entrySet()) { + final var id = entry.getKey(); + final var state = entry.getValue(); + + final var name = id.id(); + final var resource = state.resource(); + if(!resourceNames.contains(name)) continue; + switch (resource.getType()) { + case "real" -> realProfiles.put( + name, + Pair.of( + resource.getOutputType().getSchema(), + serializeProfile(elapsedTime, state, SimulationEngine::extractRealDynamics))); + + case "discrete" -> discreteProfiles.put( + name, + Pair.of( + resource.getOutputType().getSchema(), + serializeProfile(elapsedTime, state, SimulationEngine::extractDiscreteDynamics))); + + default -> + throw new IllegalArgumentException( + "Resource `%s` has unknown type `%s`".formatted(name, resource.getType())); + } + } + + final var activityResults = computeActivitySimulationResults(engine, startTime, elapsedTime, taskInfo); final List> topics = new ArrayList<>(); final var serializableTopicToId = new HashMap, Integer>(); @@ -688,8 +756,8 @@ public static SimulationResults computeResults( return new SimulationResults(realProfiles, discreteProfiles, - simulatedActivities, - unfinishedActivities, + activityResults.simulatedActivities, + activityResults.unfinishedActivities, startTime, elapsedTime, topics, @@ -906,6 +974,10 @@ public boolean isComplete() { } } + public boolean spanIsComplete(SpanId spanId) { + return this.spans.get(spanId).isComplete(); + } + public SimulationEngine duplicate() { return new SimulationEngine(this); } From 8ad147d63de695061b03d90d2b025e863b65c59c Mon Sep 17 00:00:00 2001 From: maillard Date: Tue, 6 Feb 2024 11:29:03 -0800 Subject: [PATCH 22/43] Create checkpoint policy --- merlin-driver/build.gradle | 1 + .../ResourceAwareSpreadCheckpointPolicy.java | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResourceAwareSpreadCheckpointPolicy.java diff --git a/merlin-driver/build.gradle b/merlin-driver/build.gradle index 0703239dd3..c6ad4e9c7d 100644 --- a/merlin-driver/build.gradle +++ b/merlin-driver/build.gradle @@ -40,6 +40,7 @@ dependencies { api project(':merlin-sdk') api 'org.glassfish:javax.json:1.1.4' implementation 'it.unimi.dsi:fastutil:8.5.12' + implementation 'org.slf4j:slf4j-simple:2.0.7' testImplementation project(':merlin-framework') testImplementation project(':merlin-framework-junit') diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResourceAwareSpreadCheckpointPolicy.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResourceAwareSpreadCheckpointPolicy.java new file mode 100644 index 0000000000..546f7033bd --- /dev/null +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResourceAwareSpreadCheckpointPolicy.java @@ -0,0 +1,45 @@ +package gov.nasa.jpl.aerie.scheduler.simulation; + +import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; + +/** + * Policy for saving simulation checkpoints in a cache. + * The number of checkpoint saved is equal to the capacity of the cache multiplied by a discount factor. + * The resulting number of cache events are then spread over the whole planning horizon. + */ +public class ResourceAwareSpreadCheckpointPolicy implements Function{ + final Function function; + + public ResourceAwareSpreadCheckpointPolicy( + final int resourceCapacity, + final Duration planningHorizonStart, + final Duration planningHorizonEnd, + final Duration subHorizonStart, + final Duration subHorizonEnd, + final double discount, + final boolean endForSure){ + final List desiredCheckpoints = new ArrayList<>(); + if(resourceCapacity > 0){ + final var period = planningHorizonEnd.minus(planningHorizonStart).dividedBy((int) (resourceCapacity * discount)); + //for a given planning horizon, we try always hitting the same checkpoint times to increase the probability of + //already having it saved in the cache + for (Duration cur = planningHorizonStart.plus(period); + cur.longerThan(subHorizonStart) && cur.shorterThan(subHorizonEnd); + cur = cur.plus(period)) { + desiredCheckpoints.add(cur); + } + if (endForSure && !desiredCheckpoints.contains(subHorizonEnd)) desiredCheckpoints.add(subHorizonEnd); + } + this.function = SimulationDriver.desiredCheckpoints(desiredCheckpoints); + } + + @Override + public Boolean apply(final Duration duration, final Duration duration2) { + return function.apply(duration, duration2); + } +} From 7a7d9ca9ece8ee74677420113786dba72bfcb199 Mon Sep 17 00:00:00 2001 From: maillard Date: Thu, 11 Apr 2024 16:08:32 -0700 Subject: [PATCH 23/43] add cached engine store --- .../simulation/InMemoryCachedEngineStore.java | 122 ++++++++++++++++++ .../InMemoryCachedEngineStoreTest.java | 110 ++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStore.java create mode 100644 scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStoreTest.java diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStore.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStore.java new file mode 100644 index 0000000000..ecfe10b803 --- /dev/null +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStore.java @@ -0,0 +1,122 @@ +package gov.nasa.jpl.aerie.scheduler.simulation; + +import gov.nasa.jpl.aerie.merlin.driver.CachedEngineStore; +import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import org.apache.commons.collections4.map.ListOrderedMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class InMemoryCachedEngineStore implements AutoCloseable, CachedEngineStore { + private record CachedEngineMetadata( + SimulationEngineConfiguration configuration, + Instant creationDate){} + + private static final Logger LOGGER = LoggerFactory.getLogger(InMemoryCachedEngineStore.class); + private final ListOrderedMap cachedEngines; + private final int capacity; + private Duration savedSimulationTime; + + /** + * + * @param capacity the maximum number of engines that can be stored in memory + */ + public InMemoryCachedEngineStore(final int capacity) { + this.cachedEngines = new ListOrderedMap<>(); + this.capacity = capacity; + this.savedSimulationTime = Duration.ZERO; + } + + public Duration getTotalSavedSimulationTime(){ + return savedSimulationTime; + } + + @Override + public void close() throws Exception { + cachedEngines.forEach((cachedEngine, metadata) -> cachedEngine.simulationEngine().close()); + cachedEngines.clear(); + } + + /** + * Register a re-use for a saved cached simulation engine. Will decrease likelihood of this engine being deleted. + * @param cachedSimulationEngine the simulation engine + */ + public void registerUsed(final SimulationDriver.CachedSimulationEngine cachedSimulationEngine){ + final var engineMetadata = this.cachedEngines.remove(cachedSimulationEngine); + if(engineMetadata != null){ + this.cachedEngines.put(0, cachedSimulationEngine, engineMetadata); + this.savedSimulationTime = this.savedSimulationTime.plus(cachedSimulationEngine.startOffset()); + } + } + + public void save( + final SimulationDriver.CachedSimulationEngine engine, + final SimulationEngineConfiguration configuration) { + if (shouldWeSave(engine, configuration)) { + if (cachedEngines.size() + 1 > capacity) { + removeLast(); + } + final var metadata = new CachedEngineMetadata(configuration, Instant.now()); + cachedEngines.put(cachedEngines.size(), engine, metadata); + LOGGER.info("Added a cached simulation engine to the store. Current occupation ratio: " + cachedEngines.size() + "/" + this.capacity); + } + } + + public int capacity(){ + return capacity; + } + + public List getCachedEngines( + final SimulationEngineConfiguration configuration){ + return cachedEngines + .entrySet() + .stream() + .filter(ce -> configuration.equals(ce.getValue().configuration)) + .map(Map.Entry::getKey) + .toList(); + } + + public Optional> getMissionModel( + final Map configuration, + final Instant simulationStartTime){ + for(final var entry: cachedEngines.entrySet()){ + if(entry.getValue().configuration.simulationConfiguration().equals(configuration) && + entry.getValue().configuration.simStartTime().equals(simulationStartTime)){ + return Optional.of(entry.getKey().missionModel()); + } + } + return Optional.empty(); + } + + private boolean shouldWeSave(final SimulationDriver.CachedSimulationEngine engine, + final SimulationEngineConfiguration configuration){ + //avoid duplicates + for(final var cached: cachedEngines.entrySet()){ + final var savedEngine = cached.getKey(); + final var metadata = cached.getValue(); + if(engine.startOffset().isEqualTo(savedEngine.startOffset()) && + engine.activityDirectives().equals(savedEngine.activityDirectives()) && + metadata.configuration.equals(configuration)){ + return false; + } + } + return true; + } + + /** + * Least-recently-used removal policy + */ + private void removeLast(){ + LOGGER.info("Cleaning cached simulation engine from the store"); + this.cachedEngines.remove(this.cachedEngines.size() - 1); + } +} diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStoreTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStoreTest.java new file mode 100644 index 0000000000..da67578c5a --- /dev/null +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStoreTest.java @@ -0,0 +1,110 @@ +package gov.nasa.jpl.aerie.scheduler.simulation; + +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; +import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; +import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; +import gov.nasa.jpl.aerie.merlin.driver.engine.SlabList; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; +import gov.nasa.jpl.aerie.merlin.driver.timeline.CausalEventSource; +import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.scheduler.SimulationUtility; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class InMemoryCachedEngineStoreTest { + SimulationEngineConfiguration simulationEngineConfiguration; + MissionModelId missionModelId; + + @BeforeEach + void beforeEach(){ + this.missionModelId = new MissionModelId(1); + this.simulationEngineConfiguration = new SimulationEngineConfiguration(Map.of(), Instant.EPOCH, this.missionModelId); + } + + public static SimulationDriver.CachedSimulationEngine getCachedEngine1(){ + return new SimulationDriver.CachedSimulationEngine( + Duration.SECOND, + Map.of( + new ActivityDirectiveId(1), new ActivityDirective(Duration.HOUR, "ActivityType1", Map.of(), null, true), + new ActivityDirectiveId(2), new ActivityDirective(Duration.HOUR, "ActivityType2", Map.of(), null, true) + ), + new SimulationEngine(), + new LiveCells(new CausalEventSource()), + new SlabList<>(), + null, + SimulationUtility.getFooMissionModel() + ); + } + + public static SimulationDriver.CachedSimulationEngine getCachedEngine2(){ + return new SimulationDriver.CachedSimulationEngine( + Duration.SECOND, + Map.of( + new ActivityDirectiveId(3), new ActivityDirective(Duration.HOUR, "ActivityType3", Map.of(), null, true), + new ActivityDirectiveId(4), new ActivityDirective(Duration.HOUR, "ActivityType4", Map.of(), null, true) + ), + new SimulationEngine(), + new LiveCells(new CausalEventSource()), + new SlabList<>(), + null, + SimulationUtility.getFooMissionModel() + ); + } + + public static SimulationDriver.CachedSimulationEngine getCachedEngine3(){ + return new SimulationDriver.CachedSimulationEngine( + Duration.SECOND, + Map.of( + new ActivityDirectiveId(5), new ActivityDirective(Duration.HOUR, "ActivityType5", Map.of(), null, true), + new ActivityDirectiveId(6), new ActivityDirective(Duration.HOUR, "ActivityType6", Map.of(), null, true) + ), + new SimulationEngine(), + new LiveCells(new CausalEventSource()), + new SlabList<>(), + null, + SimulationUtility.getFooMissionModel() + ); + } + + @Test + public void duplicateTest(){ + final var store = new InMemoryCachedEngineStore(2); + store.save(SimulationDriver.CachedSimulationEngine.empty(SimulationUtility.getFooMissionModel()), this.simulationEngineConfiguration); + store.save(SimulationDriver.CachedSimulationEngine.empty(SimulationUtility.getFooMissionModel()), this.simulationEngineConfiguration); + store.save(SimulationDriver.CachedSimulationEngine.empty(SimulationUtility.getFooMissionModel()), this.simulationEngineConfiguration); + assertEquals(1, store.getCachedEngines(this.simulationEngineConfiguration).size()); + } + + @Test + public void order(){ + final var store = new InMemoryCachedEngineStore(2); + final var cachedEngine1 = getCachedEngine1(); + final var cachedEngine2 = getCachedEngine2(); + final var cachedEngine3 = getCachedEngine3(); + store.save(cachedEngine1, this.simulationEngineConfiguration); + store.save(cachedEngine2, this.simulationEngineConfiguration); + final var cachedBeforeRegister = store.getCachedEngines(this.simulationEngineConfiguration); + //engines have 0 used, so they are ordered in descending creation date + assertEquals(cachedBeforeRegister.get(0).activityDirectives(), cachedEngine1.activityDirectives()); + assertEquals(cachedBeforeRegister.get(1).activityDirectives(), cachedEngine2.activityDirectives()); + //engine1 has been used so it goes first in the list + store.registerUsed(cachedEngine2); + final var cachedAfterRegister = store.getCachedEngines(this.simulationEngineConfiguration); + assertEquals(cachedAfterRegister.get(0).activityDirectives(), cachedEngine2.activityDirectives()); + assertEquals(cachedAfterRegister.get(1).activityDirectives(), cachedEngine1.activityDirectives()); + store.save(cachedEngine3, this.simulationEngineConfiguration); + //to store cachedEngine3, we had to remove the last element of the list, engine 1 and the order is still most recently used + final var cachedAfterRemoveLast = store.getCachedEngines(this.simulationEngineConfiguration); + assertEquals(cachedAfterRemoveLast.get(0).activityDirectives(), cachedEngine2.activityDirectives()); + assertEquals(cachedAfterRemoveLast.get(1).activityDirectives(), cachedEngine3.activityDirectives()); + System.out.println(); + } +} From 6e3360f653718dcd0d310d3aef938c44c1f03685 Mon Sep 17 00:00:00 2001 From: maillard Date: Thu, 11 Apr 2024 16:09:18 -0700 Subject: [PATCH 24/43] Add stop functions and separate results computation from simulation --- .../SimulationDuplicationTest.java | 225 +++++++---- .../merlin/driver/CachedEngineStore.java | 6 +- .../driver/CheckpointSimulationDriver.java | 376 ++++++++++++++++++ .../SimulationResultsComputerInputs.java | 54 +++ .../driver/SimulationDuplicationTest.java | 133 +++---- .../services/LocalMissionModelService.java | 3 +- .../simulation/InMemoryCachedEngineStore.java | 19 +- .../ResourceAwareSpreadCheckpointPolicy.java | 4 +- .../InMemoryCachedEngineStoreTest.java | 34 +- 9 files changed, 676 insertions(+), 178 deletions(-) create mode 100644 merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java create mode 100644 merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResultsComputerInputs.java diff --git a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java index 4c9448c050..65ecfbe3c5 100644 --- a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java +++ b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java @@ -3,10 +3,14 @@ import gov.nasa.jpl.aerie.foomissionmodel.generated.GeneratedModelType; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; +import gov.nasa.jpl.aerie.merlin.driver.CachedEngineStore; import gov.nasa.jpl.aerie.merlin.driver.DirectiveTypeRegistry; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; import gov.nasa.jpl.aerie.merlin.driver.MissionModelBuilder; +import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; import gov.nasa.jpl.aerie.merlin.driver.timeline.CausalEventSource; import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; @@ -30,10 +34,12 @@ import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Triple; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.time.Instant; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -46,6 +52,36 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class SimulationDuplicationTest { + CachedEngineStore store; + final private class InfiniteCapacityEngineStore implements CachedEngineStore{ + private final Map> store = new HashMap<>(); + @Override + public void save( + final CheckpointSimulationDriver.CachedSimulationEngine cachedSimulationEngine, + final SimulationEngineConfiguration configuration) { + store.computeIfAbsent(configuration, conf -> new ArrayList<>()); + store.get(configuration).add(cachedSimulationEngine); + } + + @Override + public List getCachedEngines(final SimulationEngineConfiguration configuration) { + return store.get(configuration); + } + } + + public static SimulationEngineConfiguration mockConfiguration(){ + return new SimulationEngineConfiguration( + Map.of(), + Instant.EPOCH, + new MissionModelId(0) + ); + } + + @BeforeEach + void beforeEach(){ + this.store = new InfiniteCapacityEngineStore(); + } + @BeforeAll static void beforeAll() { ThreadedTask.CACHE_READS = true; @@ -99,9 +135,13 @@ void emptyPlanTest() { @Test void testTrivialDuplicate() { - final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints( + final SimulationResults results = simulateWithCheckpoints( missionModel, - SimulationDriver.CachedSimulationEngine.empty(missionModel), List.of(Duration.of(5, MINUTES)), Map.of() + CheckpointSimulationDriver.CachedSimulationEngine.empty(missionModel), + List.of(Duration.of(5, MINUTES)), + Map.of(), + store, + mockConfiguration() ); final SimulationResults expected = SimulationDriver.simulate( missionModel, @@ -110,13 +150,18 @@ void testTrivialDuplicate() { Duration.HOUR, Instant.EPOCH, Duration.HOUR, - () -> false); - assertResultsEqual(expected, results.results()); - final SimulationDriver.SimulationResultsWithCheckpoints newResults = simulateWithCheckpoints( + () -> false, + $ -> {}); + assertResultsEqual(expected, results); + final var newResults = simulateWithCheckpoints( missionModel, - results.checkpoints().get(0), List.of(), Map.of() + store.getCachedEngines(mockConfiguration()).get(0), + List.of(), + Map.of(), + store, + mockConfiguration() ); - assertResultsEqual(expected, newResults.results()); + assertResultsEqual(expected, newResults); } @Test @@ -125,10 +170,12 @@ void testFooDuplicateEmptyPlan() { new MissionModelBuilder(), Instant.EPOCH, new Configuration()); - final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints( + final var results = simulateWithCheckpoints( missionModel, List.of(Duration.of(5, MINUTES)), - Map.of() + Map.of(), + store, + mockConfiguration() ); final SimulationResults expected = SimulationDriver.simulate( missionModel, @@ -137,8 +184,9 @@ void testFooDuplicateEmptyPlan() { Duration.HOUR, Instant.EPOCH, Duration.HOUR, - () -> false); - assertResultsEqual(expected, results.results()); + () -> false, + $ -> {}); + assertResultsEqual(expected, results); } @Test @@ -151,10 +199,12 @@ void testFooNonEmptyPlan() { activity(1, MINUTE, "foo", Map.of("z", SerializedValue.of(123))), activity(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) ); - final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints( + final var results = simulateWithCheckpoints( missionModel, List.of(Duration.of(5, MINUTES)), - schedule + schedule, + store, + mockConfiguration() ); final SimulationResults expected = SimulationDriver.simulate( missionModel, @@ -163,19 +213,22 @@ void testFooNonEmptyPlan() { Duration.HOUR, Instant.EPOCH, Duration.HOUR, - () -> false); - assertResultsEqual(expected, results.results()); + () -> false, + $ -> {}); + assertResultsEqual(expected, results); - assertEquals(Duration.of(5, MINUTES), results.checkpoints().get(0).startOffset()); + assertEquals(Duration.of(5, MINUTES), store.getCachedEngines(mockConfiguration()).getFirst().endsAt()); - final SimulationDriver.SimulationResultsWithCheckpoints results2 = simulateWithCheckpoints( + final var results2 = simulateWithCheckpoints( missionModel, - results.checkpoints().get(0), + store.getCachedEngines(mockConfiguration()).get(0), List.of(Duration.of(5, MINUTES)), - schedule + schedule, + store, + mockConfiguration() ); - assertResultsEqual(expected, results2.results()); + assertResultsEqual(expected, results2); } @Test @@ -188,10 +241,12 @@ void testFooNonEmptyPlanMultipleResumes() { activity(1, MINUTE, "foo", Map.of("z", SerializedValue.of(123))), activity(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) ); - final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints( + final var results = simulateWithCheckpoints( missionModel, List.of(Duration.of(5, MINUTES)), - schedule + schedule, + store, + mockConfiguration() ); final SimulationResults expected = SimulationDriver.simulate( missionModel, @@ -200,28 +255,33 @@ void testFooNonEmptyPlanMultipleResumes() { Duration.HOUR, Instant.EPOCH, Duration.HOUR, - () -> false); - assertResultsEqual(expected, results.results()); + () -> false, + $ -> {}); + assertResultsEqual(expected, results); - assertEquals(Duration.of(5, MINUTES), results.checkpoints().get(0).startOffset()); + assertEquals(Duration.of(5, MINUTES), store.getCachedEngines(mockConfiguration()).getFirst().endsAt()); - final SimulationDriver.SimulationResultsWithCheckpoints results2 = simulateWithCheckpoints( + final var results2 = simulateWithCheckpoints( missionModel, - results.checkpoints().get(0), + store.getCachedEngines(mockConfiguration()).getFirst(), List.of(Duration.of(5, MINUTES)), - schedule + schedule, + store, + mockConfiguration() ); - assertResultsEqual(expected, results2.results()); + assertResultsEqual(expected, results2); - final SimulationDriver.SimulationResultsWithCheckpoints results3 = simulateWithCheckpoints( + final var results3 = simulateWithCheckpoints( missionModel, - results.checkpoints().get(0), + store.getCachedEngines(mockConfiguration()).getFirst(), List.of(Duration.of(5, MINUTES)), - schedule + schedule, + store, + mockConfiguration() ); - assertResultsEqual(expected, results3.results()); + assertResultsEqual(expected, results3); } @Test @@ -234,10 +294,12 @@ void testFooNonEmptyPlanMultipleCheckpointsMultipleResumes() { activity(1, MINUTE, "foo", Map.of("z", SerializedValue.of(123))), activity(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) ); - final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints( + final var results = simulateWithCheckpoints( missionModel, List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), - schedule + schedule, + store, + mockConfiguration() ); final SimulationResults expected = SimulationDriver.simulate( missionModel, @@ -246,28 +308,33 @@ void testFooNonEmptyPlanMultipleCheckpointsMultipleResumes() { Duration.HOUR, Instant.EPOCH, Duration.HOUR, - () -> false); - assertResultsEqual(expected, results.results()); + () -> false, + $ -> {}); + assertResultsEqual(expected, results); - assertEquals(Duration.of(5, MINUTES), results.checkpoints().get(0).startOffset()); + assertEquals(Duration.of(5, MINUTES), store.getCachedEngines(mockConfiguration()).getFirst().endsAt()); - final SimulationDriver.SimulationResultsWithCheckpoints results2 = simulateWithCheckpoints( + final var results2 = simulateWithCheckpoints( missionModel, - results.checkpoints().get(0), + store.getCachedEngines(mockConfiguration()).getFirst(), List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), - schedule + schedule, + store, + mockConfiguration() ); - assertResultsEqual(expected, results2.results()); + assertResultsEqual(expected, results2); - final SimulationDriver.SimulationResultsWithCheckpoints results3 = simulateWithCheckpoints( + final var results3 = simulateWithCheckpoints( missionModel, - results.checkpoints().get(1), + store.getCachedEngines(mockConfiguration()).get(1), List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), - schedule + schedule, + store, + mockConfiguration() ); - assertResultsEqual(expected, results3.results()); + assertResultsEqual(expected, results3); } @Test @@ -289,10 +356,12 @@ void testFooNonEmptyPlanMultipleCheckpointsMultipleResumesWithEdits() { activity1, activity(390, SECONDS, "foo", Map.of("z", SerializedValue.of(999))) ); - final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints( + final var results = simulateWithCheckpoints( missionModel, List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), - schedule1 + schedule1, + store, + mockConfiguration() ); final SimulationResults expected1 = SimulationDriver.simulate( missionModel, @@ -301,7 +370,8 @@ void testFooNonEmptyPlanMultipleCheckpointsMultipleResumesWithEdits() { Duration.HOUR, Instant.EPOCH, Duration.HOUR, - () -> false); + () -> false, + $ -> {}); final SimulationResults expected2 = SimulationDriver.simulate( missionModel, @@ -310,28 +380,33 @@ void testFooNonEmptyPlanMultipleCheckpointsMultipleResumesWithEdits() { Duration.HOUR, Instant.EPOCH, Duration.HOUR, - () -> false); + () -> false, + $ -> {}); - assertResultsEqual(expected1, results.results()); + assertResultsEqual(expected1, results); - assertEquals(Duration.of(5, MINUTES), results.checkpoints().get(0).startOffset()); + assertEquals(Duration.of(5, MINUTES), store.getCachedEngines(mockConfiguration()).getFirst().endsAt()); - final SimulationDriver.SimulationResultsWithCheckpoints results2 = simulateWithCheckpoints( + final var results2 = simulateWithCheckpoints( missionModel, - results.checkpoints().get(0), + store.getCachedEngines(mockConfiguration()).getFirst(), List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), - schedule2 + schedule2, + store, + mockConfiguration() ); - assertResultsEqual(expected2, results2.results()); + assertResultsEqual(expected2, results2); - final SimulationDriver.SimulationResultsWithCheckpoints results3 = simulateWithCheckpoints( + final SimulationResults results3 = simulateWithCheckpoints( missionModel, - results.checkpoints().get(1), + store.getCachedEngines(mockConfiguration()).get(1), List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), - schedule2 + schedule2, + store, + mockConfiguration() ); - assertResultsEqual(expected2, results3.results()); + assertResultsEqual(expected2, results3); } private static long nextActivityDirectiveId = 0L; @@ -389,13 +464,15 @@ static void assertResultsEqual(SimulationResults expected, SimulationResults act assertEquals(expected, actual); } - static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints( + static SimulationResults simulateWithCheckpoints( final MissionModel missionModel, - final SimulationDriver.CachedSimulationEngine cachedSimulationEngine, + final CheckpointSimulationDriver.CachedSimulationEngine cachedSimulationEngine, final List desiredCheckpoints, - final Map schedule + final Map schedule, + final CachedEngineStore cachedEngineStore, + final SimulationEngineConfiguration simulationEngineConfiguration ) { - return SimulationDriver.simulateWithCheckpoints( + return CheckpointSimulationDriver.computeResults(CheckpointSimulationDriver.simulateWithCheckpoints( missionModel, schedule, Instant.EPOCH, @@ -405,15 +482,20 @@ static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints $ -> {}, () -> false, cachedSimulationEngine, - SimulationDriver.desiredCheckpoints(desiredCheckpoints)); + CheckpointSimulationDriver.desiredCheckpoints(desiredCheckpoints), + CheckpointSimulationDriver.noCondition(), + cachedEngineStore, + simulationEngineConfiguration)); } - static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints( + static SimulationResults simulateWithCheckpoints( final MissionModel missionModel, final List desiredCheckpoints, - final Map schedule + final Map schedule, + final CachedEngineStore cachedEngineStore, + final SimulationEngineConfiguration simulationEngineConfiguration ) { - return SimulationDriver.simulateWithCheckpoints( + return CheckpointSimulationDriver.computeResults(CheckpointSimulationDriver.simulateWithCheckpoints( missionModel, schedule, Instant.EPOCH, @@ -422,8 +504,11 @@ static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints Duration.HOUR, $ -> {}, () -> false, - SimulationDriver.CachedSimulationEngine.empty(missionModel), - SimulationDriver.desiredCheckpoints(desiredCheckpoints)); + CheckpointSimulationDriver.CachedSimulationEngine.empty(missionModel), + CheckpointSimulationDriver.desiredCheckpoints(desiredCheckpoints), + CheckpointSimulationDriver.noCondition(), + cachedEngineStore, + simulationEngineConfiguration)); } private static final Topic delayedActivityDirectiveInputTopic = new Topic<>(); diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CachedEngineStore.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CachedEngineStore.java index f912d44e8e..c1a069d412 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CachedEngineStore.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CachedEngineStore.java @@ -3,8 +3,10 @@ import java.util.List; public interface CachedEngineStore { - void save(final SimulationDriver.CachedSimulationEngine cachedSimulationEngine, + void save(final CheckpointSimulationDriver.CachedSimulationEngine cachedSimulationEngine, final SimulationEngineConfiguration configuration); - List getCachedEngines( + List getCachedEngines( final SimulationEngineConfiguration configuration); + + int capacity(); } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java new file mode 100644 index 0000000000..99650dc94a --- /dev/null +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java @@ -0,0 +1,376 @@ +package gov.nasa.jpl.aerie.merlin.driver; + +import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; +import gov.nasa.jpl.aerie.merlin.driver.engine.SlabList; +import gov.nasa.jpl.aerie.merlin.driver.engine.SpanId; +import gov.nasa.jpl.aerie.merlin.driver.engine.TaskId; +import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; +import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import org.apache.commons.lang3.function.TriFunction; +import org.apache.commons.lang3.mutable.MutableLong; +import org.apache.commons.lang3.mutable.MutableObject; +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static gov.nasa.jpl.aerie.merlin.driver.SimulationDriver.scheduleActivities; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.min; + +public class CheckpointSimulationDriver { + private static final Logger LOGGER = LoggerFactory.getLogger(CheckpointSimulationDriver.class); + + public record SimulationResultsComputerInputs( + SimulationEngine engine, + Instant simulationStartTime, + Duration elapsedTime, + Topic activityTopic, + TemporalEventSource timeline, + Iterable> serializableTopics, + Map activityDirectiveIdTaskIdMap){} + + public record CachedSimulationEngine( + Duration endsAt, + Map activityDirectives, + SimulationEngine simulationEngine, + LiveCells cells, + SlabList timePoints, + Topic activityTopic, + MissionModel missionModel + ) { + public CachedSimulationEngine { + cells.freeze(); + timePoints.freeze(); + simulationEngine.close(); + } + + public static CachedSimulationEngine empty(final MissionModel missionModel) { + final SimulationEngine engine = new SimulationEngine(); + final TemporalEventSource timeline = new TemporalEventSource(); + final LiveCells cells = new LiveCells(timeline, missionModel.getInitialCells()); + + // Begin tracking all resources. + for (final var entry : missionModel.getResources().entrySet()) { + final var name = entry.getKey(); + final var resource = entry.getValue(); + + engine.trackResource(name, resource, Duration.ZERO); + } + + { + // Start daemon task(s) immediately, before anything else happens. + engine.scheduleTask(Duration.ZERO, missionModel.getDaemon()); + { + final var batch = engine.extractNextJobs(Duration.MAX_VALUE); + final var commit = engine.performJobs(batch.jobs(), cells, Duration.ZERO, Duration.MAX_VALUE); + timeline.add(commit); + } + } + + return new CachedSimulationEngine( + Duration.MIN_VALUE, + Map.of(), + engine, + cells, + timeline.points(), + new Topic<>(), + missionModel + ); + } + } + + /** + * Selects the best cached engine for simulating a given plan. + * @param schedule the schedule/plan + * @param cachedEngines a list of cached engines + * @return the best cached engine as well as the map of corresponding activity ids for this engine + */ + public static Optional>> bestCachedEngine( + final Map schedule, + final List cachedEngines) { + Optional bestCandidate = Optional.empty(); + final Map correspondenceMap = new HashMap<>(); + for (final var cachedEngine : cachedEngines) { + if (bestCandidate.isPresent() && cachedEngine.endsAt().noLongerThan(bestCandidate.get().endsAt())) + continue; + + final var activityDirectivesInCache = new HashMap<>(cachedEngine.activityDirectives()); + // Find the invalidation time + var invalidationTime = Duration.MAX_VALUE; + final var scheduledActivities = new HashMap<>(schedule); + for (final var activity : scheduledActivities.entrySet()) { + if (activityDirectivesInCache.values().contains(activity.getValue())) { + final var removedEntry = + removeValueInMap(activityDirectivesInCache, activity.getValue()); + correspondenceMap.put(activity.getKey(), removedEntry.get().getKey()); + } else { + invalidationTime = min(invalidationTime, activity.getValue().startOffset()); + } + } + for (final var activity : activityDirectivesInCache.values()) { + invalidationTime = min(invalidationTime, activity.startOffset()); + } + if (cachedEngine.endsAt().shorterThan(invalidationTime)) { + bestCandidate = Optional.of(cachedEngine); + } + } + + bestCandidate.ifPresent(cachedSimulationEngine -> System.out.println("Re-using simulation engine at " + + cachedSimulationEngine.endsAt())); + return bestCandidate.map(cachedSimulationEngine -> Pair.of(cachedSimulationEngine, correspondenceMap)); + } + + private static Optional> removeValueInMap(final Map map, V value){ + final var it = map.entrySet().iterator(); + while(it.hasNext()){ + final var entry = it.next(); + if(entry.getValue().equals(value)){ + it.remove(); + return Optional.of(entry); + } + } + return Optional.empty(); + } + + private static TemporalEventSource makeCombinedTimeline(List timelines, TemporalEventSource timeline) { + final TemporalEventSource combinedTimeline = new TemporalEventSource(); + for (final var entry : timelines) { + for (final var timePoint : entry.points()) { + if (timePoint instanceof TemporalEventSource.TimePoint.Delta t) { + combinedTimeline.add(t.delta()); + } else if (timePoint instanceof TemporalEventSource.TimePoint.Commit t) { + combinedTimeline.add(t.events()); + } + } + } + + for (final var timePoint : timeline) { + if (timePoint instanceof TemporalEventSource.TimePoint.Delta t) { + combinedTimeline.add(t.delta()); + } else if (timePoint instanceof TemporalEventSource.TimePoint.Commit t) { + combinedTimeline.add(t.events()); + } + } + return combinedTimeline; + } + + public static BiFunction desiredCheckpoints(final List desiredCheckpoints) { + return (elapsedTime, nextTime) -> { + for (final var desiredCheckpoint : desiredCheckpoints) { + if (elapsedTime.noLongerThan(desiredCheckpoint) && nextTime.longerThan(desiredCheckpoint)) { + return true; + } + } + return false; + }; + } + + public static BiFunction wallClockCheckpoints(final long thresholdSeconds) { + MutableLong lastCheckpointRealTime = new MutableLong(System.nanoTime()); + MutableObject lastCheckpointSimTime = new MutableObject<>(Duration.ZERO); + return (elapsedTime, nextTime) -> { + if (nextTime.longerThan(elapsedTime) && System.nanoTime() - lastCheckpointRealTime.getValue() > (thresholdSeconds * 1000 * 1000 * 1000)) { + lastCheckpointRealTime.setValue(System.nanoTime()); + lastCheckpointSimTime.setValue(elapsedTime); + return true; + } else { + return false; + } + }; + } + + public static SimulationResultsComputerInputs simulateWithCheckpoints( + final MissionModel missionModel, + final Map schedule, + final Instant simulationStartTime, + final Duration simulationDuration, + final Instant planStartTime, + final Duration planDuration, + final Consumer simulationExtentConsumer, + final Supplier simulationCanceled, + final CachedSimulationEngine cachedEngine, + final BiFunction shouldTakeCheckpoint, + final TriFunction, Map, Boolean> stopConditionOnPlan, + final CachedEngineStore cachedEngineStore, + final SimulationEngineConfiguration configuration) { + final var activityToSpan = new HashMap(); + final var activityTopic = cachedEngine.activityTopic(); + final var timelines = new ArrayList(); + timelines.add(new TemporalEventSource(cachedEngine.timePoints)); + var engine = cachedEngine.simulationEngine.duplicate(); + engine.unscheduleAfter(cachedEngine.endsAt); + try (var ignored = cachedEngine.simulationEngine) { + var timeline = new TemporalEventSource(); + var cells = new LiveCells(timeline, cachedEngine.cells()); + /* The current real time. */ + var elapsedTime = Duration.max(ZERO, cachedEngine.endsAt()); + + simulationExtentConsumer.accept(elapsedTime); + + // Specify a topic on which tasks can log the activity they're associated with. + + try { + final var filteredSchedule = new HashMap(); + for (final var entry : schedule.entrySet()) { + if (entry.getValue().startOffset().longerThan(cachedEngine.endsAt())) { + filteredSchedule.put(entry.getKey(), entry.getValue()); + } + } + + // Get all activities as close as possible to absolute time + // Schedule all activities. + // Using HashMap explicitly because it allows `null` as a key. + // `null` key means that an activity is not waiting on another activity to finish to know its start time + HashMap>> resolved = new StartOffsetReducer(planDuration, filteredSchedule).compute(); + if(resolved.size() != 0) { + resolved.put( + null, + StartOffsetReducer.adjustStartOffset( + resolved.get(null), + Duration.of( + planStartTime.until(simulationStartTime, ChronoUnit.MICROS), + Duration.MICROSECONDS))); + } + // Filter out activities that are before simulationStartTime + resolved = StartOffsetReducer.filterOutNegativeStartOffset(resolved); + + activityToSpan.putAll(scheduleActivities( + filteredSchedule, + resolved, + missionModel, + engine, + activityTopic + ) + ); + + // Drive the engine until we're out of time. + // TERMINATION: Actually, we might never break if real time never progresses forward. + while (elapsedTime.noLongerThan(simulationDuration) && !simulationCanceled.get()) { + final var nextTime = engine.peekNextTime().orElse(Duration.MAX_VALUE); + if (shouldTakeCheckpoint.apply(elapsedTime, nextTime)) { + cells.freeze(); + LOGGER.info("Saving a simulation engine in memory"); + cachedEngineStore.save( + new CachedSimulationEngine( + elapsedTime, + schedule, + engine, + cells, + makeCombinedTimeline(timelines, timeline).points(), + activityTopic, + missionModel), + configuration); + timelines.add(timeline); + engine = engine.duplicate(); + timeline = new TemporalEventSource(); + cells = new LiveCells(timeline, cells); + } + //break before changing the state of the engine + if (simulationCanceled.get() || stopConditionOnPlan.apply(engine, schedule, activityToSpan)) { + break; + } + + final var batch = engine.extractNextJobs(simulationDuration); + // Increment real time, if necessary. + final var delta = batch.offsetFromStart().minus(elapsedTime); + elapsedTime = batch.offsetFromStart(); + timeline.add(delta); + // TODO: Advance a dense time counter so that future tasks are strictly ordered relative to these, + // even if they occur at the same real time. + + simulationExtentConsumer.accept(elapsedTime); + + //this break depends on the state of the batch: this is the soonest we can exist for that reason + if (batch.jobs().isEmpty() && (batch.offsetFromStart().isEqualTo(simulationDuration))) { + break; + } + + // Run the jobs in this batch. + final var commit = engine.performJobs(batch.jobs(), cells, elapsedTime, simulationDuration); + timeline.add(commit); + } + } catch (Throwable ex) { + throw new SimulationException(elapsedTime, simulationStartTime, ex); + } + + return new SimulationResultsComputerInputs( + engine, + simulationStartTime, + elapsedTime, + activityTopic, + makeCombinedTimeline(timelines, timeline), + missionModel.getTopics(), + activityToSpan); + } + } + + public static TriFunction,Map, Boolean> + stopOnceAllActivitiessAreFinished(){ + return (engine, schedule, actIdToTaskId) -> actIdToTaskId + .values() + .stream() + .allMatch(engine::spanIsComplete); + } + + public static TriFunction,Map, Boolean> + noCondition(){ + return (engine, schedule, actIdToSpanId) -> false; + } + + public static TriFunction,Map, Boolean> + stopOnceActivityHasFinished(final ActivityDirectiveId activityDirectiveId){ + return (engine, schedule, actIdToSpanId) -> (actIdToSpanId.containsKey(activityDirectiveId) + && engine.spanIsComplete(actIdToSpanId.get(activityDirectiveId))); + } + + public static SimulationResults computeResults( + final SimulationResultsComputerInputs simulationResultsInputs, + final Set resourceNames){ + return SimulationEngine.computeResults( + simulationResultsInputs.engine(), + simulationResultsInputs.simulationStartTime(), + simulationResultsInputs.elapsedTime(), + simulationResultsInputs.activityTopic(), + simulationResultsInputs.timeline(), + simulationResultsInputs.serializableTopics(), + resourceNames + ); + } + + public static SimulationResults computeResults( + final SimulationResultsComputerInputs simulationResultsInputs){ + return SimulationEngine.computeResults( + simulationResultsInputs.engine(), + simulationResultsInputs.simulationStartTime(), + simulationResultsInputs.elapsedTime(), + simulationResultsInputs.activityTopic(), + simulationResultsInputs.timeline(), + simulationResultsInputs.serializableTopics() + ); + } + + public static SimulationEngine.SimulationActivityExtract computeActivitySimulationResults( + final SimulationResultsComputerInputs simulationResultsInputs){ + return SimulationEngine.computeActivitySimulationResults( + simulationResultsInputs.engine(), + simulationResultsInputs.simulationStartTime(), + simulationResultsInputs.elapsedTime(), + simulationResultsInputs.activityTopic(), + simulationResultsInputs.timeline(), + simulationResultsInputs.serializableTopics()); + } +} diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResultsComputerInputs.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResultsComputerInputs.java new file mode 100644 index 0000000000..8a152a2cea --- /dev/null +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResultsComputerInputs.java @@ -0,0 +1,54 @@ +package gov.nasa.jpl.aerie.merlin.driver; + +import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; +import gov.nasa.jpl.aerie.merlin.driver.engine.SpanId; +import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.time.Instant; +import java.util.Map; +import java.util.Set; + +public record SimulationResultsComputerInputs( + SimulationEngine engine, + Instant simulationStartTime, + Duration elapsedTime, + Topic activityTopic, + TemporalEventSource timeline, + Iterable> serializableTopics, + Map activityDirectiveIdTaskIdMap){ + + public SimulationResults computeResults(final Set resourceNames){ + return SimulationEngine.computeResults( + this.engine(), + this.simulationStartTime(), + this.elapsedTime(), + this.activityTopic(), + this.timeline(), + this.serializableTopics(), + resourceNames + ); + } + + public SimulationResults computeResults(){ + return SimulationEngine.computeResults( + this.engine(), + this.simulationStartTime(), + this.elapsedTime(), + this.activityTopic(), + this.timeline(), + this.serializableTopics() + ); + } + + public SimulationEngine.SimulationActivityExtract computeActivitySimulationResults(){ + return SimulationEngine.computeActivitySimulationResults( + this.engine(), + this.simulationStartTime(), + this.elapsedTime(), + this.activityTopic(), + this.timeline(), + this.serializableTopics()); + } +} diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java index 8d895498bf..2130cae61b 100644 --- a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java @@ -21,10 +21,12 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import org.apache.commons.lang3.tuple.Triple; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -35,6 +37,37 @@ import static org.junit.jupiter.api.Assertions.assertEquals; public class SimulationDuplicationTest { + CachedEngineStore store; + final private class InfiniteCapacityEngineStore implements CachedEngineStore{ + private Map> store = new HashMap<>(); + @Override + public void save( + final CheckpointSimulationDriver.CachedSimulationEngine cachedSimulationEngine, + final SimulationEngineConfiguration configuration) + { + store.computeIfAbsent(configuration, conf -> new ArrayList<>()); + store.get(configuration).add(cachedSimulationEngine); + } + + @Override + public List getCachedEngines(final SimulationEngineConfiguration configuration) { + return store.get(configuration); + } + } + + public static SimulationEngineConfiguration mockConfiguration(){ + return new SimulationEngineConfiguration( + Map.of(), + Instant.EPOCH, + new MissionModelId(0) + ); + } + + @BeforeEach + void beforeEach(){ + this.store = new InfiniteCapacityEngineStore(); + } + @Test void emptyPlanTest() { final SimulationResults results = SimulationDriver.simulate( @@ -76,8 +109,10 @@ void emptyPlanTest() { @Test void testDuplicate() { - final SimulationDriver.SimulationResultsWithCheckpoints results = simulateWithCheckpoints(SimulationDriver.CachedSimulationEngine.empty( - missionModel), List.of(Duration.of(5, MINUTES))); + final var results = simulateWithCheckpoints( + CheckpointSimulationDriver.CachedSimulationEngine.empty(missionModel), + List.of(Duration.of(5, MINUTES)), + store); final SimulationResults expected = SimulationDriver.simulate( missionModel, Map.of(), @@ -85,82 +120,19 @@ void testDuplicate() { Duration.HOUR, Instant.EPOCH, Duration.HOUR, - () -> false); - assertEquals(expected, results.results()); - final SimulationDriver.SimulationResultsWithCheckpoints newResults = simulateWithCheckpoints(results.checkpoints().get(0), List.of()); - assertEquals(expected, newResults.results()); - } - - static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints( - final List desiredCheckpoints - ) { - - final var engine = new SimulationEngine(); -// Begin tracking all resources. - for (final var entry : missionModel.getResources().entrySet()) { - final var name = entry.getKey(); - final var resource = entry.getValue(); - - engine.trackResource(name, resource, Duration.ZERO); - } - - final var timeline = new TemporalEventSource(); - final var cells = new LiveCells(timeline, missionModel.getInitialCells()); - - // Start daemon task(s) immediately, before anything else happens. - engine.scheduleTask(Duration.ZERO, missionModel.getDaemon()); - { - final var batch = engine.extractNextJobs(Duration.MAX_VALUE); - final var commit = engine.performJobs(batch.jobs(), cells, Duration.ZERO, Duration.MAX_VALUE); - timeline.add(commit); - } - - - return SimulationDriver.simulateWithCheckpoints( - missionModel, - Map.of(), - Instant.EPOCH, - Duration.HOUR, - Instant.EPOCH, - Duration.HOUR, - $ -> {}, () -> false, - new SimulationDriver.CachedSimulationEngine( - Duration.ZERO, - Map.of(), - engine, - cells, - timeline.points(), - new Topic<>(), - missionModel - ), - SimulationDriver.desiredCheckpoints(desiredCheckpoints)); + $ -> {}); + assertEquals(expected, results); + final var newResults = simulateWithCheckpoints(store.getCachedEngines(mockConfiguration()).get(0), List.of(), store); + assertEquals(expected, newResults); } - static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints( - final SimulationDriver.CachedSimulationEngine cachedEngine, - final List desiredCheckpoints + static SimulationResults simulateWithCheckpoints( + final CheckpointSimulationDriver.CachedSimulationEngine cachedEngine, + final List desiredCheckpoints, + final CachedEngineStore engineStore ) { - - // Begin tracking all resources. -// for (final var entry : missionModel.getResources().entrySet()) { -// final var name = entry.getKey(); -// final var resource = entry.getValue(); -// -// engine.trackResource(name, resource, elapsedTime); -// } - -// if (true) { -// // Start daemon task(s) immediately, before anything else happens. -// engine.scheduleTask(Duration.ZERO, missionModel.getDaemon()); -// { -// final var batch = engine.extractNextJobs(Duration.MAX_VALUE); -// final var commit = engine.performJobs(batch.jobs(), cells, elapsedTime, Duration.MAX_VALUE); -// timeline.add(commit); -// } -// } - - return SimulationDriver.simulateWithCheckpoints( + return CheckpointSimulationDriver.computeResults(CheckpointSimulationDriver.simulateWithCheckpoints( missionModel, Map.of(), Instant.EPOCH, @@ -170,7 +142,11 @@ static SimulationDriver.SimulationResultsWithCheckpoints simulateWithCheckpoints $ -> {}, () -> false, cachedEngine, - SimulationDriver.desiredCheckpoints(desiredCheckpoints)); + CheckpointSimulationDriver.desiredCheckpoints(desiredCheckpoints), + CheckpointSimulationDriver.noCondition(), + engineStore, + mockConfiguration() + )); } private static final Topic delayedActivityDirectiveInputTopic = new Topic<>(); @@ -344,9 +320,4 @@ public Task duplicate(Executor executor) { } }; } - - @AfterEach - void afterEach() { - assertEquals(SimulationDriver.cachedEngines.size(), SimulationEngine.getNumActiveSimulationEngines()); - } } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java index 5d606a24ea..2e8deca06e 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java @@ -1,6 +1,7 @@ package gov.nasa.jpl.aerie.merlin.server.services; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; +import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.DirectiveTypeRegistry; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; import gov.nasa.jpl.aerie.merlin.driver.MissionModelLoader; @@ -283,7 +284,7 @@ public Map getModelEffectiveArguments(final String miss } static Map> missionModelCache = new HashMap<>(); - static Map> cachedEngines = new HashMap<>(); + static Map> cachedEngines = new HashMap<>(); /** * Validate that a set of activity parameters conforms to the expectations of a named mission model. diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStore.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStore.java index ecfe10b803..9c565bb38f 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStore.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStore.java @@ -1,6 +1,7 @@ package gov.nasa.jpl.aerie.scheduler.simulation; import gov.nasa.jpl.aerie.merlin.driver.CachedEngineStore; +import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; @@ -22,7 +23,7 @@ private record CachedEngineMetadata( Instant creationDate){} private static final Logger LOGGER = LoggerFactory.getLogger(InMemoryCachedEngineStore.class); - private final ListOrderedMap cachedEngines; + private final ListOrderedMap cachedEngines; private final int capacity; private Duration savedSimulationTime; @@ -31,6 +32,7 @@ private record CachedEngineMetadata( * @param capacity the maximum number of engines that can be stored in memory */ public InMemoryCachedEngineStore(final int capacity) { + if(capacity <= 0) throw new IllegalArgumentException("Capacity of the cached engine store must be greater than 0"); this.cachedEngines = new ListOrderedMap<>(); this.capacity = capacity; this.savedSimulationTime = Duration.ZERO; @@ -41,7 +43,7 @@ public Duration getTotalSavedSimulationTime(){ } @Override - public void close() throws Exception { + public void close() { cachedEngines.forEach((cachedEngine, metadata) -> cachedEngine.simulationEngine().close()); cachedEngines.clear(); } @@ -50,16 +52,16 @@ public void close() throws Exception { * Register a re-use for a saved cached simulation engine. Will decrease likelihood of this engine being deleted. * @param cachedSimulationEngine the simulation engine */ - public void registerUsed(final SimulationDriver.CachedSimulationEngine cachedSimulationEngine){ + public void registerUsed(final CheckpointSimulationDriver.CachedSimulationEngine cachedSimulationEngine){ final var engineMetadata = this.cachedEngines.remove(cachedSimulationEngine); if(engineMetadata != null){ this.cachedEngines.put(0, cachedSimulationEngine, engineMetadata); - this.savedSimulationTime = this.savedSimulationTime.plus(cachedSimulationEngine.startOffset()); + this.savedSimulationTime = this.savedSimulationTime.plus(cachedSimulationEngine.endsAt()); } } public void save( - final SimulationDriver.CachedSimulationEngine engine, + final CheckpointSimulationDriver.CachedSimulationEngine engine, final SimulationEngineConfiguration configuration) { if (shouldWeSave(engine, configuration)) { if (cachedEngines.size() + 1 > capacity) { @@ -71,11 +73,12 @@ public void save( } } + @Override public int capacity(){ return capacity; } - public List getCachedEngines( + public List getCachedEngines( final SimulationEngineConfiguration configuration){ return cachedEngines .entrySet() @@ -97,13 +100,13 @@ public Optional> getMissionModel( return Optional.empty(); } - private boolean shouldWeSave(final SimulationDriver.CachedSimulationEngine engine, + private boolean shouldWeSave(final CheckpointSimulationDriver.CachedSimulationEngine engine, final SimulationEngineConfiguration configuration){ //avoid duplicates for(final var cached: cachedEngines.entrySet()){ final var savedEngine = cached.getKey(); final var metadata = cached.getValue(); - if(engine.startOffset().isEqualTo(savedEngine.startOffset()) && + if(engine.endsAt().isEqualTo(savedEngine.endsAt()) && engine.activityDirectives().equals(savedEngine.activityDirectives()) && metadata.configuration.equals(configuration)){ return false; diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResourceAwareSpreadCheckpointPolicy.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResourceAwareSpreadCheckpointPolicy.java index 546f7033bd..709c4e03c2 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResourceAwareSpreadCheckpointPolicy.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResourceAwareSpreadCheckpointPolicy.java @@ -1,6 +1,6 @@ package gov.nasa.jpl.aerie.scheduler.simulation; -import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import java.util.ArrayList; @@ -35,7 +35,7 @@ public ResourceAwareSpreadCheckpointPolicy( } if (endForSure && !desiredCheckpoints.contains(subHorizonEnd)) desiredCheckpoints.add(subHorizonEnd); } - this.function = SimulationDriver.desiredCheckpoints(desiredCheckpoints); + this.function = CheckpointSimulationDriver.desiredCheckpoints(desiredCheckpoints); } @Override diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStoreTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStoreTest.java index da67578c5a..52d6afa6d4 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStoreTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStoreTest.java @@ -2,7 +2,7 @@ import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; -import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; import gov.nasa.jpl.aerie.merlin.driver.engine.SlabList; @@ -11,6 +11,7 @@ import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.scheduler.SimulationUtility; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -22,15 +23,22 @@ public class InMemoryCachedEngineStoreTest { SimulationEngineConfiguration simulationEngineConfiguration; MissionModelId missionModelId; + InMemoryCachedEngineStore store; @BeforeEach void beforeEach(){ this.missionModelId = new MissionModelId(1); this.simulationEngineConfiguration = new SimulationEngineConfiguration(Map.of(), Instant.EPOCH, this.missionModelId); + this.store = new InMemoryCachedEngineStore(2); } - public static SimulationDriver.CachedSimulationEngine getCachedEngine1(){ - return new SimulationDriver.CachedSimulationEngine( + @AfterEach + void afterEach() { + this.store.close(); + } + + public static CheckpointSimulationDriver.CachedSimulationEngine getCachedEngine1(){ + return new CheckpointSimulationDriver.CachedSimulationEngine( Duration.SECOND, Map.of( new ActivityDirectiveId(1), new ActivityDirective(Duration.HOUR, "ActivityType1", Map.of(), null, true), @@ -44,8 +52,8 @@ public static SimulationDriver.CachedSimulationEngine getCachedEngine1(){ ); } - public static SimulationDriver.CachedSimulationEngine getCachedEngine2(){ - return new SimulationDriver.CachedSimulationEngine( + public static CheckpointSimulationDriver.CachedSimulationEngine getCachedEngine2(){ + return new CheckpointSimulationDriver.CachedSimulationEngine( Duration.SECOND, Map.of( new ActivityDirectiveId(3), new ActivityDirective(Duration.HOUR, "ActivityType3", Map.of(), null, true), @@ -59,8 +67,8 @@ public static SimulationDriver.CachedSimulationEngine getCachedEngine2(){ ); } - public static SimulationDriver.CachedSimulationEngine getCachedEngine3(){ - return new SimulationDriver.CachedSimulationEngine( + public static CheckpointSimulationDriver.CachedSimulationEngine getCachedEngine3(){ + return new CheckpointSimulationDriver.CachedSimulationEngine( Duration.SECOND, Map.of( new ActivityDirectiveId(5), new ActivityDirective(Duration.HOUR, "ActivityType5", Map.of(), null, true), @@ -77,25 +85,24 @@ public static SimulationDriver.CachedSimulationEngine getCachedEngine3(){ @Test public void duplicateTest(){ final var store = new InMemoryCachedEngineStore(2); - store.save(SimulationDriver.CachedSimulationEngine.empty(SimulationUtility.getFooMissionModel()), this.simulationEngineConfiguration); - store.save(SimulationDriver.CachedSimulationEngine.empty(SimulationUtility.getFooMissionModel()), this.simulationEngineConfiguration); - store.save(SimulationDriver.CachedSimulationEngine.empty(SimulationUtility.getFooMissionModel()), this.simulationEngineConfiguration); + store.save(CheckpointSimulationDriver.CachedSimulationEngine.empty(SimulationUtility.getFooMissionModel()), this.simulationEngineConfiguration); + store.save(CheckpointSimulationDriver.CachedSimulationEngine.empty(SimulationUtility.getFooMissionModel()), this.simulationEngineConfiguration); + store.save(CheckpointSimulationDriver.CachedSimulationEngine.empty(SimulationUtility.getFooMissionModel()), this.simulationEngineConfiguration); assertEquals(1, store.getCachedEngines(this.simulationEngineConfiguration).size()); } @Test public void order(){ - final var store = new InMemoryCachedEngineStore(2); final var cachedEngine1 = getCachedEngine1(); final var cachedEngine2 = getCachedEngine2(); final var cachedEngine3 = getCachedEngine3(); store.save(cachedEngine1, this.simulationEngineConfiguration); store.save(cachedEngine2, this.simulationEngineConfiguration); final var cachedBeforeRegister = store.getCachedEngines(this.simulationEngineConfiguration); - //engines have 0 used, so they are ordered in descending creation date + // no engines have been used, so the cache is ordered in descending creation date assertEquals(cachedBeforeRegister.get(0).activityDirectives(), cachedEngine1.activityDirectives()); assertEquals(cachedBeforeRegister.get(1).activityDirectives(), cachedEngine2.activityDirectives()); - //engine1 has been used so it goes first in the list + //engine2 has been used so it goes first in the list store.registerUsed(cachedEngine2); final var cachedAfterRegister = store.getCachedEngines(this.simulationEngineConfiguration); assertEquals(cachedAfterRegister.get(0).activityDirectives(), cachedEngine2.activityDirectives()); @@ -105,6 +112,5 @@ public void order(){ final var cachedAfterRemoveLast = store.getCachedEngines(this.simulationEngineConfiguration); assertEquals(cachedAfterRemoveLast.get(0).activityDirectives(), cachedEngine2.activityDirectives()); assertEquals(cachedAfterRemoveLast.get(1).activityDirectives(), cachedEngine3.activityDirectives()); - System.out.println(); } } From 167dfaa834bf0e3144fb2812a71b220abb96f1e6 Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Tue, 2 Apr 2024 14:34:18 -0700 Subject: [PATCH 25/43] Use checkpoint sim in scheduling --- .../jpl/aerie/scheduler/model/Problem.java | 20 +- .../scheduler/simulation/SimulationData.java | 3 +- .../simulation/SimulationFacade.java | 585 ++++++++++-------- .../scheduler/solver/PrioritySolver.java | 333 +++------- .../aerie/scheduler/FixedDurationTest.java | 23 +- .../aerie/scheduler/LongDurationPlanTest.java | 3 - .../scheduler/ParametricDurationTest.java | 16 +- .../aerie/scheduler/PrioritySolverTest.java | 60 +- .../aerie/scheduler/SimulationFacadeTest.java | 32 +- .../aerie/scheduler/SimulationUtility.java | 34 +- .../jpl/aerie/scheduler/TestApplyWhen.java | 92 +-- .../aerie/scheduler/TestCardinalityGoal.java | 5 +- .../aerie/scheduler/TestPersistentAnchor.java | 12 +- .../aerie/scheduler/TestRecurrenceGoal.java | 6 +- .../scheduler/TestRecurrenceGoalExtended.java | 8 - .../TestUnsatisfiableCompositeGoals.java | 23 +- .../scheduler/UncontrollableDurationTest.java | 5 - .../CheckpointSimulationFacadeTest.java | 119 ++++ .../SimulationResultsComparisonUtils.java | 189 ++++++ .../server/services/SchedulerAgent.java | 3 +- .../services/ThreadedSchedulerAgent.java | 5 +- .../worker/SchedulerWorkerAppDriver.java | 9 +- .../worker/WorkerAppConfiguration.java | 3 +- .../services/SynchronousSchedulerAgent.java | 95 +-- .../services/SchedulingIntegrationTests.java | 157 +++-- 25 files changed, 1023 insertions(+), 817 deletions(-) create mode 100644 scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacadeTest.java create mode 100644 scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationResultsComparisonUtils.java diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Problem.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Problem.java index ac8435bf13..c0a6774aa7 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Problem.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Problem.java @@ -8,8 +8,8 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.GlobalConstraintWithIntrospection; import gov.nasa.jpl.aerie.scheduler.goals.Goal; -import gov.nasa.jpl.aerie.scheduler.simulation.SimulationData; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; +import gov.nasa.jpl.aerie.scheduler.simulation.SimulationData; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationResultsConverter; import org.apache.commons.collections4.BidiMap; @@ -76,7 +76,11 @@ public class Problem { * * @param mission IN the mission model that this problem is based on */ - public Problem(MissionModel mission, PlanningHorizon planningHorizon, SimulationFacade simulationFacade, SchedulerModel schedulerModel) { + public Problem( + MissionModel mission, + PlanningHorizon planningHorizon, + SimulationFacade simulationFacade, + SchedulerModel schedulerModel) { this.missionModel = mission; this.schedulerModel = schedulerModel; this.initialPlan = new PlanInMemory(); @@ -89,7 +93,7 @@ public Problem(MissionModel mission, PlanningHorizon planningHorizon, Simulat } this.simulationFacade = simulationFacade; if(this.simulationFacade != null) { - this.simulationFacade.setActivityTypes(this.getActivityTypes()); + this.simulationFacade.addActivityTypes(this.getActivityTypes()); } this.initialSimulationResults = Optional.empty(); } @@ -140,12 +144,16 @@ public Plan getInitialPlan() { * @param initialSimulationResults optional initial simulation results associated to the initial plan * @param plan the initial seed plan that schedulers may start from */ - public void setInitialPlan(final Plan plan, final Optional initialSimulationResults, final BidiMap mapSchedulingIdsToActivityIds) { + public void setInitialPlan( + final Plan plan, + final Optional initialSimulationResults, + final BidiMap mapSchedulingIdsToActivityIds) { initialPlan = plan; this.initialSimulationResults = initialSimulationResults.map(simulationResults -> new SimulationData( + plan, simulationResults, - SimulationResultsConverter.convertToConstraintModelResults( - simulationResults), Optional.ofNullable(mapSchedulingIdsToActivityIds))); + SimulationResultsConverter.convertToConstraintModelResults(simulationResults), + Optional.ofNullable(mapSchedulingIdsToActivityIds))); } /** diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationData.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationData.java index 09684c8ff8..5f1517f6ab 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationData.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationData.java @@ -2,13 +2,14 @@ import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; +import gov.nasa.jpl.aerie.scheduler.model.Plan; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; import org.apache.commons.collections4.BidiMap; -import java.util.Collection; import java.util.Optional; public record SimulationData( + Plan plan, SimulationResults driverResults, gov.nasa.jpl.aerie.constraints.model.SimulationResults constraintsResults, Optional> mapSchedulingIdsToActivityIds diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java index 9870fd59e7..64e5a6199a 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java @@ -2,358 +2,428 @@ import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; +import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; +import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivity; import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivityId; -import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; +import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; +import gov.nasa.jpl.aerie.merlin.driver.engine.SpanId; +import gov.nasa.jpl.aerie.merlin.driver.engine.TaskId; import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; import gov.nasa.jpl.aerie.scheduler.SchedulingInterruptedException; -import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; import gov.nasa.jpl.aerie.scheduler.model.ActivityType; +import gov.nasa.jpl.aerie.scheduler.model.Plan; import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; +import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; +import org.apache.commons.collections4.BidiMap; +import org.apache.commons.collections4.bidimap.DualHashBidiMap; +import org.apache.commons.lang3.function.TriFunction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; +import java.time.Instant; import java.util.Collection; -import java.util.Comparator; import java.util.HashMap; -import java.util.List; +import java.util.HashSet; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Supplier; -import org.apache.commons.collections4.BidiMap; -import org.apache.commons.collections4.bidimap.DualHashBidiMap; - -/** - * A facade for simulating plans and processing simulation results. - */ -public class SimulationFacade implements AutoCloseable{ - - private static final Logger logger = LoggerFactory.getLogger(SimulationFacade.class); - private final Supplier canceledListener; - +public class SimulationFacade { + private static final Logger LOGGER = LoggerFactory.getLogger(SimulationFacade.class); private final MissionModel missionModel; - private final SchedulerModel schedulerModel; - - // planning horizon + private final InMemoryCachedEngineStore cachedEngines; private final PlanningHorizon planningHorizon; - private Map activityTypes; - private ResumableSimulationDriver driver; + private final Map activityTypes; private int itSimActivityId; - private final BidiMap mapSchedulingIdsToActivityIds; - - private final Map insertedActivities; - //counts the total number of simulation restarts, used as performance metric in the scheduler - private int pastSimulationRestarts; - - public SimulationData lastSimulationData; - - /** - * state boolean stating whether the initial plan has been modified to allow initial simulation results to be used - */ - private boolean initialPlanHasBeenModified = false; - - /* External initial simulation results that will be served only if initialPlanHasBeenModified is equal to false*/ - private Optional initialSimulationResults; - - /** - * The set of activities to be added to the first simulation. - * Used to potentially delay the first simulation until the loaded results are stale. - * The only way to add activities to the facade is to simulate them. But sometimes, we have initial sim results and we - * do not need to simulate before the first activity insertion. This initial plan allows the facade to "load" the activities in simulation - * and wait until the first needed simulation to simulate them. - */ - private List initialPlan; + private final SimulationEngineConfiguration configuration; + private SimulationData initialSimulationResults; + private final Supplier canceledListener; + private final SchedulerModel schedulerModel; + private Duration totalSimulationTime = Duration.ZERO; /** * Loads initial simulation results into the simulation. They will be served until initialSimulationResultsAreStale() * is called. * @param simulationData the initial simulation results */ - public void loadInitialSimResults(SimulationData simulationData){ - initialPlanHasBeenModified = false; - this.initialSimulationResults = Optional.of(simulationData); + public void setInitialSimResults(final SimulationData simulationData){ + this.initialSimulationResults = simulationData; } - /** - * Signals to the facade that the initial simulation results are stale and should not be used anymore - */ - public void initialSimulationResultsAreStale(){ - this.initialPlanHasBeenModified = true; + + public SimulationFacade( + final MissionModel missionModel, + final SchedulerModel schedulerModel, + final InMemoryCachedEngineStore cachedEngines, + final PlanningHorizon planningHorizon, + final SimulationEngineConfiguration simulationEngineConfiguration, + final Supplier canceledListener){ + this.itSimActivityId = 0; + this.missionModel = missionModel; + this.schedulerModel = schedulerModel; + this.cachedEngines = cachedEngines; + this.planningHorizon = planningHorizon; + this.activityTypes = new HashMap<>(); + this.configuration = simulationEngineConfiguration; + this.canceledListener = canceledListener; } /** - * @return true if initial simulation results are stale, false otherwise + * Returns the total simulated time + * @return */ - public boolean areInitialSimulationResultsStale(){ - return this.initialPlanHasBeenModified; - } - - public Optional getLatestConstraintSimulationResults(){ - if(!initialPlanHasBeenModified && initialSimulationResults.isPresent()) return Optional.of(this.initialSimulationResults.get().constraintsResults()); - if(lastSimulationData == null) return Optional.empty(); - return Optional.of(lastSimulationData.constraintsResults()); - } - - public Optional getLatestDriverSimulationResults(){ - if(!initialPlanHasBeenModified && initialSimulationResults.isPresent()) return Optional.of(this.initialSimulationResults.get().driverResults()); - if(lastSimulationData == null) return Optional.empty(); - return Optional.of(lastSimulationData.driverResults()); + public Duration totalSimulationTime(){ + return totalSimulationTime; } - public Supplier getCanceledListener() {return canceledListener;} - public SimulationFacade( final PlanningHorizon planningHorizon, final MissionModel missionModel, - final SchedulerModel schedulerModel, - Supplier canceledListener - ) { + final SchedulerModel schedulerModel + ){ + this.itSimActivityId = 0; this.missionModel = missionModel; + this.cachedEngines = new InMemoryCachedEngineStore(0); this.planningHorizon = planningHorizon; - this.driver = new ResumableSimulationDriver<>(missionModel, planningHorizon.getAerieHorizonDuration(), canceledListener); - this.itSimActivityId = 0; - this.insertedActivities = new HashMap<>(); - this.mapSchedulingIdsToActivityIds = new DualHashBidiMap<>(); this.activityTypes = new HashMap<>(); - this.pastSimulationRestarts = 0; - this.initialPlan = new ArrayList<>(); - this.initialSimulationResults = Optional.empty(); + this.configuration = new SimulationEngineConfiguration(Map.of(), Instant.now(), new MissionModelId(1)); + this.canceledListener = () -> false; this.schedulerModel = schedulerModel; - this.canceledListener = canceledListener; } - @Override - public void close(){ - driver.close(); + public Supplier getCanceledListener(){ + return this.canceledListener; } - /** - * Adds a set of activities that will not be simulated yet. They will be simulated at the latest possible time, when it cannot be avoided. - * This is to allow the use of initial simulation results in PrioritySolver. - * @param initialPlan the initial set of activities in the plan - */ - public void addInitialPlan(Collection initialPlan){ - this.initialPlan.clear(); - this.initialPlan.addAll(initialPlan); + public void addActivityTypes(final Collection activityTypes){ + activityTypes.forEach(at -> this.activityTypes.put(at.getName(), at)); } - public void setActivityTypes(final Collection activityTypes){ - this.activityTypes = new HashMap<>(); - activityTypes.forEach(at -> this.activityTypes.put(at.getName(), at)); + private void replaceValue(final Map map, final V value, final V replacement){ + for (final Map.Entry entry : map.entrySet()) { + if (entry.getValue().equals(value)) { + entry.setValue(replacement); + break; + } + } } - public Optional> getBidiActivityIdCorrespondence(){ - if(initialSimulationResults.isEmpty() || initialPlanHasBeenModified) - return Optional.ofNullable(mapSchedulingIdsToActivityIds); - else return initialSimulationResults.get().mapSchedulingIdsToActivityIds(); + private void replaceIds( + final PlanSimCorrespondence planSimCorrespondence, + final Map updates){ + for(final var replacements : updates.entrySet()){ + replaceValue(planSimCorrespondence.planActDirectiveIdToSimulationActivityDirectiveId,replacements.getKey(), replacements.getValue()); + if(planSimCorrespondence.directiveIdActivityDirectiveMap.containsKey(replacements.getKey())){ + final var value = planSimCorrespondence.directiveIdActivityDirectiveMap.remove(replacements.getKey()); + planSimCorrespondence.directiveIdActivityDirectiveMap.put(replacements.getValue(), value); + } + } } + /** + * Simulates until the end of the last activity of a plan. Updates the input plan with child activities and activity durations. + * @param plan the plan to simulate + * @return the inputs needed to compute simulation results + * @throws SimulationException if an exception happens during simulation + */ + public CheckpointSimulationDriver.SimulationResultsComputerInputs simulateNoResultsUntilEndPlan(final Plan plan) + throws SimulationException, SchedulingInterruptedException + { + return simulateNoResults(plan, planningHorizon.getEndAerie(), null).simulationResultsComputerInputs(); + } + public static class SimulationException extends Exception { + SimulationException(final String message, final Throwable cause) { + super(message, cause); + } + } /** - * Fetches activity instance durations from last simulation - * - * @param schedulingActivityDirective the activity instance we want the duration for - * @return the duration if found in the last simulation, null otherwise + * Simulates a plan until the end of one of its activities + * Do not use to update the plan as decomposing activities may not finish + * @param plan + * @param activity + * @return + * @throws SimulationException */ - public Optional getActivityDuration(final SchedulingActivityDirective schedulingActivityDirective) { - if(!mapSchedulingIdsToActivityIds.containsKey(schedulingActivityDirective.getId())){ - return Optional.empty(); - } - final var duration = driver.getActivityDuration(mapSchedulingIdsToActivityIds.get( - schedulingActivityDirective.getId())); - return duration; + + public CheckpointSimulationDriver.SimulationResultsComputerInputs simulateNoResultsUntilEndAct( + final Plan plan, + final SchedulingActivityDirective activity) throws SimulationException, SchedulingInterruptedException + { + return simulateNoResults(plan, null, activity).simulationResultsComputerInputs(); } - private ActivityDirectiveId getIdOfRootParent(SimulationResults results, SimulatedActivityId instanceId){ - final var act = results.simulatedActivities.get(instanceId); - if(act.parentId() == null){ - // SAFETY: any activity that has no parent must have a directive id. - return act.directiveId().get(); - } else { - return getIdOfRootParent(results, act.parentId()); - } + public record AugmentedSimulationResultsComputerInputs( + CheckpointSimulationDriver.SimulationResultsComputerInputs simulationResultsComputerInputs, + PlanSimCorrespondence planSimCorrespondence){} + + public AugmentedSimulationResultsComputerInputs simulateNoResults( + final Plan plan, + final Duration until) throws SimulationException, SchedulingInterruptedException + { + return simulateNoResults(plan, until, null); } - public Map getAllChildActivities(final Duration endTime) - throws SimulationException, SchedulingInterruptedException + + /** + * Simulates and updates plan + * @param plan + * @param until can be null + * @param activity can be null + */ + private AugmentedSimulationResultsComputerInputs simulateNoResults( + final Plan plan, + final Duration until, + final SchedulingActivityDirective activity) throws SimulationException, SchedulingInterruptedException { - logger.info("Need to compute simulation results until "+ endTime + " for getting child activities"); - var latestSimulationData = this.getLatestDriverSimulationResults(); - //if no initial sim results and no sim has been performed, perform a sim and get the sim results - if(latestSimulationData.isEmpty()){ - //useful only if there are activities to simulate for this case of getting child activities - if(insertedActivities.size() == 0) return Map.of(); - computeSimulationResultsUntil(endTime); - latestSimulationData = this.getLatestDriverSimulationResults(); + final var planSimCorrespondence = scheduleFromPlan(plan, planningHorizon.getEndAerie(), planningHorizon.getStartInstant(), planningHorizon.getStartInstant()); + + final var best = CheckpointSimulationDriver.bestCachedEngine( + planSimCorrespondence.directiveIdActivityDirectiveMap(), + cachedEngines.getCachedEngines(configuration)); + CheckpointSimulationDriver.CachedSimulationEngine engine = null; + Duration from = Duration.ZERO; + if(best.isPresent()){ + engine = best.get().getKey(); + replaceIds(planSimCorrespondence, best.get().getRight()); + from = engine.endsAt(); } - final Map childActivities = new HashMap<>(); - latestSimulationData.get().simulatedActivities.forEach( (activityInstanceId, activity) -> { - if (activity.parentId() == null) return; - final var rootParent = getIdOfRootParent(this.lastSimulationData.driverResults(), activityInstanceId); - final var schedulingActId = mapSchedulingIdsToActivityIds.inverseBidiMap().get(rootParent); - final var activityInstance = SchedulingActivityDirective.of( - activityTypes.get(activity.type()), - this.planningHorizon.toDur(activity.start()), - activity.duration(), - activity.arguments(), - schedulingActId, - null, - true); - childActivities.put(activityInstance, schedulingActId); - }); - return childActivities; - } - public void removeAndInsertActivitiesFromSimulation( - final Collection activitiesToRemove, - final Collection activitiesToAdd - ) throws SimulationException, SchedulingInterruptedException { - if (canceledListener.get()) throw new SchedulingInterruptedException("removing/adding activities"); - logger.debug("Removing("+activitiesToRemove.size()+")/Adding("+activitiesToAdd.size()+") activities from simulation"); - activitiesToRemove.stream().forEach(remove -> logger.debug("Removing act starting at " + remove.startOffset())); - activitiesToAdd.stream().forEach(adding -> logger.debug("Adding act starting at " + adding.startOffset())); - var atLeastOneActualRemoval = false; - for(final var act: activitiesToRemove){ - if(insertedActivities.containsKey(act)){ - atLeastOneActualRemoval = true; - insertedActivities.remove(act); - } + //Configuration + //Three modes : (1) until a specific end time (2) until end of one specific activity (3) until end of last activity in plan + Duration simulationDuration; + TriFunction, Map, Boolean> + stoppingCondition; + //(1) + if(until != null && activity == null){ + simulationDuration = until; + stoppingCondition = CheckpointSimulationDriver.noCondition(); } - var allActivitiesToSimulate = new ArrayList<>(activitiesToAdd); - if(!initialPlan.isEmpty()) allActivitiesToSimulate.addAll(this.initialPlan); - this.initialPlan.clear(); - allActivitiesToSimulate = new ArrayList<>(allActivitiesToSimulate.stream().filter(a -> !insertedActivities.containsKey(a)).toList()); - Duration earliestActStartTime = Duration.MAX_VALUE; - for(final var act: activitiesToAdd){ - earliestActStartTime = Duration.min(earliestActStartTime, act.startOffset()); + //(2) + else if(activity != null && until == null){ + simulationDuration = planningHorizon.getEndAerie(); + stoppingCondition = CheckpointSimulationDriver.stopOnceActivityHasFinished(planSimCorrespondence.planActDirectiveIdToSimulationActivityDirectiveId.get(activity)); + //(3) + } else if(activity == null && until == null){ + simulationDuration = planningHorizon.getEndAerie(); + stoppingCondition = CheckpointSimulationDriver.stopOnceAllActivitiessAreFinished(); + } else { + throw new SimulationException("Bad configuration", null); } - if(allActivitiesToSimulate.isEmpty() && !atLeastOneActualRemoval) return; - //reset resumable simulation - if(atLeastOneActualRemoval || earliestActStartTime.noLongerThan(this.driver.getCurrentSimulationEndTime())){ - allActivitiesToSimulate.addAll(insertedActivities.keySet()); - insertedActivities.clear(); - mapSchedulingIdsToActivityIds.clear(); - logger.info("(Re)creating simulation driver because at least one removal("+atLeastOneActualRemoval+") or insertion in the past ("+earliestActStartTime+")"); - if (driver != null) { - this.pastSimulationRestarts += driver.getCountSimulationRestarts(); - driver.close(); + + if(engine == null) engine = CheckpointSimulationDriver.CachedSimulationEngine.empty(missionModel); + + if(best.isPresent()) cachedEngines.registerUsed(engine); + try { + final var simulation = CheckpointSimulationDriver.simulateWithCheckpoints( + missionModel, + planSimCorrespondence.directiveIdActivityDirectiveMap(), + planningHorizon.getStartInstant(), + simulationDuration, + planningHorizon.getStartInstant(), + planningHorizon.getEndAerie(), + $ -> {}, + canceledListener, + engine, + new ResourceAwareSpreadCheckpointPolicy( + cachedEngines.capacity(), + Duration.ZERO, + planningHorizon.getEndAerie(), + Duration.max(engine.endsAt(), Duration.ZERO), + simulationDuration, + 1, + true), + stoppingCondition, + cachedEngines, + configuration); + if(canceledListener.get()) throw new SchedulingInterruptedException("simulating"); + this.totalSimulationTime = this.totalSimulationTime.plus(simulation.elapsedTime().minus(from)); + final var activityResults = + CheckpointSimulationDriver.computeActivitySimulationResults(simulation); + + updatePlanWithChildActivities( + activityResults, + activityTypes, + plan, + planSimCorrespondence); + pullActivityDurationsIfNecessary( + plan, + planSimCorrespondence, + activityResults + ); + //plan has been updated + return new AugmentedSimulationResultsComputerInputs(simulation, planSimCorrespondence); + } catch(Exception e){ + if(e instanceof SchedulingInterruptedException sie){ + throw sie; } - logger.info("Number of simulation restarts so far: " + this.pastSimulationRestarts); - driver = new ResumableSimulationDriver<>(missionModel, planningHorizon.getAerieHorizonDuration(), canceledListener); + throw new SimulationException("An exception happened during simulation", e); } - simulateActivities(allActivitiesToSimulate); } - public void removeActivitiesFromSimulation(final Collection activities) - throws SimulationException, SchedulingInterruptedException + public SimulationData simulateWithResults( + final Plan plan, + final Duration until) throws SimulationException, SchedulingInterruptedException { - removeAndInsertActivitiesFromSimulation(activities, List.of()); + return simulateWithResults(plan, until, missionModel.getResources().keySet()); } - /** - * Returns the total number of simulation restarts - * @return the number of simulation restarts - */ - public int countSimulationRestarts(){ - return this.driver.getCountSimulationRestarts() + this.pastSimulationRestarts; - } - - public void insertActivitiesIntoSimulation(final Collection activities) - throws SimulationException, SchedulingInterruptedException + public SimulationData simulateWithResults( + final Plan plan, + final Duration until, + final Set resourceNames) throws SimulationException, SchedulingInterruptedException { - removeAndInsertActivitiesFromSimulation(List.of(), activities); + if(this.initialSimulationResults != null) { + final var inputPlan = scheduleFromPlan(plan, planningHorizon.getEndAerie(), planningHorizon.getStartInstant(), planningHorizon.getStartInstant()); + final var initialPlanA = scheduleFromPlan(this.initialSimulationResults.plan(), planningHorizon.getEndAerie(), planningHorizon.getStartInstant(), planningHorizon.getStartInstant()); + if (initialPlanA.equals(inputPlan)) { + return new SimulationData( + plan, + initialSimulationResults.driverResults(), + SimulationResultsConverter.convertToConstraintModelResults(initialSimulationResults.driverResults()), + this.initialSimulationResults.mapping()); + } + } + final var resultsInput = simulateNoResults(plan, until); + final var driverResults = CheckpointSimulationDriver.computeResults(resultsInput.simulationResultsComputerInputs, resourceNames); + return new SimulationData(plan, driverResults, SimulationResultsConverter.convertToConstraintModelResults(driverResults), resultsInput.planSimCorrespondence.planActDirectiveIdToSimulationActivityDirectiveId); } - /** - * Replaces an activity instance with another, strictly when they have the same id - * @param toBeReplaced the activity to be replaced - * @param replacement the replacement activity - */ - public void replaceActivityFromSimulation(final SchedulingActivityDirective toBeReplaced, final SchedulingActivityDirective replacement){ - if(toBeReplaced.type() != replacement.type() || - ((toBeReplaced.anchorId() == replacement.anchorId()) && toBeReplaced.startOffset() != replacement.startOffset()) || - !(toBeReplaced.arguments().equals(replacement.arguments()))) { - throw new IllegalArgumentException("When replacing an activity, you can only update the duration"); - } - if(!insertedActivities.containsKey(toBeReplaced)){ - throw new IllegalArgumentException("Trying to replace an activity that has not been previously simulated"); + private record PlanSimCorrespondence( + BidiMap planActDirectiveIdToSimulationActivityDirectiveId, + Map directiveIdActivityDirectiveMap){ + @Override + public boolean equals(Object other){ + if(other instanceof PlanSimCorrespondence planSimCorrespondenceAs){ + return directiveIdActivityDirectiveMap.size() == planSimCorrespondenceAs.directiveIdActivityDirectiveMap.size() && + new HashSet<>(directiveIdActivityDirectiveMap.values()).containsAll(new HashSet<>(((PlanSimCorrespondence) other).directiveIdActivityDirectiveMap.values())); + } + return false; } - final var associated = insertedActivities.get(toBeReplaced); - insertedActivities.remove(toBeReplaced); - insertedActivities.put(replacement, associated); - final var simulationId = this.mapSchedulingIdsToActivityIds.get(toBeReplaced.id()); - mapSchedulingIdsToActivityIds.remove(toBeReplaced.id()); - mapSchedulingIdsToActivityIds.put(replacement.id(), simulationId); } - private void simulateActivities(final Collection activities) - throws SimulationException, SchedulingInterruptedException { - final var activitiesSortedByStartTime = - activities.stream().filter(activity -> !(insertedActivities.containsKey(activity))) - .sorted(Comparator.comparing(SchedulingActivityDirective::startOffset)).toList(); - if(activitiesSortedByStartTime.isEmpty()) return; + private PlanSimCorrespondence scheduleFromPlan( + final Plan plan, + final Duration planDuration, + final Instant planStartTime, + final Instant simulationStartTime){ + final var activities = plan.getActivities(); + final var planActDirectiveIdToSimulationActivityDirectiveId = new DualHashBidiMap(); + if(activities.isEmpty()) return new PlanSimCorrespondence(new DualHashBidiMap<>(), Map.of()); + //filter out child activities + final var activitiesWithoutParent = activities.stream().filter(a -> a.topParent() == null).toList(); final Map directivesToSimulate = new HashMap<>(); - for(final var activity : activitiesSortedByStartTime){ + for(final var activity : activitiesWithoutParent){ final var activityIdSim = new ActivityDirectiveId(itSimActivityId++); - mapSchedulingIdsToActivityIds.put(activity.getId(), activityIdSim); + planActDirectiveIdToSimulationActivityDirectiveId.put(activity.getId(), activityIdSim); } - for(final var activity : activitiesSortedByStartTime) { - final var activityDirective = schedulingActToActivityDir(activity); - directivesToSimulate.put(mapSchedulingIdsToActivityIds.get(activity.getId()), + for(final var activity : activitiesWithoutParent) { + final var activityDirective = schedulingActToActivityDir(activity, planActDirectiveIdToSimulationActivityDirectiveId); + directivesToSimulate.put( + planActDirectiveIdToSimulationActivityDirectiveId.get(activity.getId()), activityDirective); - insertedActivities.put(activity, activityDirective); - } - try { - driver.simulateActivities(directivesToSimulate); - } catch (SchedulingInterruptedException e) { - throw e; //pass interruption up - } catch (Exception e){ - throw new SimulationException("An exception happened during simulation", e); } - this.lastSimulationData = null; + return new PlanSimCorrespondence(planActDirectiveIdToSimulationActivityDirectiveId, directivesToSimulate); } - public static class SimulationException extends Exception { - SimulationException(final String message, final Throwable cause) { - super(message, cause); + /** + * For activities that have a null duration (in an initial plan for example) and that have been simulated, we pull the duration and + * replace the original instance with a new instance that includes the duration, both in the plan and the simulation facade + */ + private void pullActivityDurationsIfNecessary( + final Plan plan, + final PlanSimCorrespondence correspondence, + final SimulationEngine.SimulationActivityExtract activityExtract + ) { + final var toReplace = new HashMap(); + for (final var activity : plan.getActivities()) { + if (activity.duration() == null) { + final var activityDirective = findSimulatedActivityById( + activityExtract.simulatedActivities().values(), + correspondence.planActDirectiveIdToSimulationActivityDirectiveId.get(activity.getId())); + if (activityDirective.isPresent()) { + final var replacementAct = SchedulingActivityDirective.copyOf( + activity, + activityDirective.get().duration() + ); + toReplace.put(activity, replacementAct); + } + //if not, maybe the activity is not finished + } } + toReplace.forEach(plan::replace); } - public void computeSimulationResultsUntil(final Duration endTime) - throws SimulationException, SchedulingInterruptedException { - if(!initialPlan.isEmpty()){ - final var toSimulate = new ArrayList<>(this.initialPlan); - this.initialPlan.clear(); - this.insertActivitiesIntoSimulation(toSimulate); - } - try { - final var results = driver.getSimulationResultsUpTo(this.planningHorizon.getStartInstant(), endTime); - //compare references - if(lastSimulationData == null || results != lastSimulationData.driverResults()) { - //simulation results from the last simulation, as converted for use by the constraint evaluation engine - this.lastSimulationData = new SimulationData(results, SimulationResultsConverter.convertToConstraintModelResults(results), Optional.ofNullable(mapSchedulingIdsToActivityIds)); + private final Optional findSimulatedActivityById(Collection simulatedActivities, final ActivityDirectiveId activityDirectiveId){ + return simulatedActivities.stream().filter(a -> a.directiveId().isPresent() && a.directiveId().get().equals(activityDirectiveId)).findFirst(); + } + + private void updatePlanWithChildActivities( + final SimulationEngine.SimulationActivityExtract activityExtract, + final Map activityTypes, + final Plan plan, + final PlanSimCorrespondence planSimCorrespondence) + { + //remove all activities with parents + final var toRemove = plan.getActivities().stream().filter(a -> a.topParent() != null).toList(); + toRemove.forEach(plan::remove); + //pull child activities + activityExtract.simulatedActivities().forEach( (activityInstanceId, activity) -> { + if (activity.parentId() == null) return; + final var rootParent = getIdOfRootParent(activityExtract, activityInstanceId); + if(rootParent.isPresent()) { + final var activityInstance = SchedulingActivityDirective.of( + activityTypes.get(activity.type()), + planningHorizon.toDur(activity.start()), + activity.duration(), + activity.arguments(), + planSimCorrespondence.planActDirectiveIdToSimulationActivityDirectiveId.getKey(rootParent.get()), + null, + true); + plan.add(activityInstance); } - } catch (SchedulingInterruptedException e){ - throw e; //pass interruption up - } catch (Exception e){ - throw new SimulationException("An exception happened during simulation", e); + }); + //no need to replace in Evaluation because child activities are not referenced in it + } + + private static Optional getIdOfRootParent( + final SimulationEngine.SimulationActivityExtract results, + final SimulatedActivityId instanceId){ + if(!results.simulatedActivities().containsKey(instanceId)){ + if(!results.unfinishedActivities().containsKey(instanceId)){ + LOGGER.debug("The simulation of the parent of activity with id "+ instanceId.id() + " has been finished"); + } + return Optional.empty(); + } + final var act = results.simulatedActivities().get(instanceId); + if(act.parentId() == null){ + // SAFETY: any activity that has no parent must have a directive id. + return Optional.of(act.directiveId().get()); + } else { + return getIdOfRootParent(results, act.parentId()); } } - public Duration getCurrentSimulationEndTime(){ - return driver.getCurrentSimulationEndTime(); + private static Optional getActivityDuration( + final ActivityDirectiveId activityDirectiveId, + final CheckpointSimulationDriver.SimulationResultsComputerInputs simulationResultsInputs){ + return simulationResultsInputs.engine().getSpan(simulationResultsInputs.activityDirectiveIdTaskIdMap().get(activityDirectiveId)).duration(); } - private ActivityDirective schedulingActToActivityDir(SchedulingActivityDirective activity) { + private ActivityDirective schedulingActToActivityDir( + final SchedulingActivityDirective activity, + final Map planActDirectiveIdToSimulationActivityDirectiveId) { if(activity.getParentActivity().isPresent()) { throw new Error("This method should not be called with a generated activity but with its top-level parent."); } @@ -373,13 +443,10 @@ private ActivityDirective schedulingActToActivityDir(SchedulingActivityDirective } } final var serializedActivity = new SerializedActivity(activity.getType().getName(), arguments); - if(activity.anchorId()!= null && !mapSchedulingIdsToActivityIds.containsKey(activity.anchorId())){ - throw new RuntimeException("Activity with id "+ activity.anchorId() + " referenced as an anchor by activity " + activity.toString() + " is not present in the plan"); - } return new ActivityDirective( activity.startOffset(), serializedActivity, - mapSchedulingIdsToActivityIds.get(activity.anchorId()), + planActDirectiveIdToSimulationActivityDirectiveId.get(activity.anchorId()), activity.anchoredToStart()); } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java index a7327d3dee..0115ea68cb 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java @@ -38,11 +38,11 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; @@ -58,40 +58,35 @@ public class PrioritySolver implements Solver { private static final Logger logger = LoggerFactory.getLogger(PrioritySolver.class); - boolean checkSimBeforeInsertingActivities; - boolean checkSimBeforeEvaluatingGoal; + private boolean checkSimBeforeInsertingActivities; + + private boolean checkSimBeforeEvaluatingGoal; + + private SimulationResults lastSimulationResults; /** * boolean stating whether only conflict analysis should be performed or not */ - final boolean analysisOnly; + private final boolean analysisOnly; /** * description of the planning problem to solve * * remains constant throughout solver lifetime */ - final Problem problem; + private final Problem problem; /** * the single-shot priority-ordered greedy solution devised by the solver * * this object is null until first call to getNextSolution() */ - Plan plan; - - List> generatedActivityInstances = new ArrayList<>(); - - /** - * tracks how well this solver thinks it has satisfied goals - * - * including which activities were created to satisfy each goal - */ - Evaluation evaluation; + private Plan plan; private final SimulationFacade simulationFacade; public record ActivityMetadata(SchedulingActivityDirective activityDirective){} + public static class HistoryWithActivity implements EquationSolvingAlgorithms.History { List, Optional>> events; @@ -166,7 +161,7 @@ public Optional getNextSolution() throws SchedulingInterruptedException { initializePlan(); if(problem.getInitialSimulationResults().isPresent()) { logger.debug("Loading initial simulation results from the DB"); - simulationFacade.loadInitialSimResults(problem.getInitialSimulationResults().get()); + simulationFacade.setInitialSimResults(problem.getInitialSimulationResults().get()); } } catch (SimulationFacade.SimulationException e) { logger.error("Tried to initializePlan but at least one activity could not be instantiated", e); @@ -202,81 +197,24 @@ private InsertActivityResult checkAndInsertActs(Collection(); - - if(allGood) { - logger.info("New activities have been inserted in the plan successfully"); - if(!acts.isEmpty() && !actsFromInitialPlan) simulationFacade.initialSimulationResultsAreStale(); - //update plan with regard to simulation - for(var act: acts) { - plan.add(act); - finalSetOfActsInserted.add(act); - } - final var replaced = synchronizeSimulationWithSchedulerPlan(); - for(final var actReplaced : replaced.entrySet()){ - if(finalSetOfActsInserted.contains(actReplaced.getKey())){ - finalSetOfActsInserted.remove(actReplaced.getKey()); - finalSetOfActsInserted.add(actReplaced.getValue()); - } - } - } else{ - logger.info("New activities could not be inserted in the plan, see error just above"); - //update simulation with regard to plan - try { - simulationFacade.removeActivitiesFromSimulation(acts); - } catch(SimulationFacade.SimulationException e){ - throw new RuntimeException("Removing activities from the simulation should not result in exception being thrown but one was thrown", e); - } - } - return new InsertActivityResult(allGood, finalSetOfActsInserted); - } - - /** - * Pulls all the child activities from the simulation + fills in activity durations - * This method should be called only when the state of the plan is considered safe, i.e. not during rootfinding - * @return a map of scheduling activity directives (old -> new) that have been replaced in the plan due to updated durations - */ - private Map synchronizeSimulationWithSchedulerPlan() - throws SchedulingInterruptedException { - final Map replacedInPlan; + final var planWithAddedActivities = plan.duplicate(); + planWithAddedActivities.add(acts); try { - final var allGeneratedActivities = - simulationFacade.getAllChildActivities(simulationFacade.getCurrentSimulationEndTime()); - processNewGeneratedActivities(allGeneratedActivities); - replacedInPlan = pullActivityDurationsIfNecessary(); + if(checkSimBeforeInsertingActivities) simulationFacade.simulateNoResultsUntilEndPlan(planWithAddedActivities); + plan = planWithAddedActivities; } catch (SimulationFacade.SimulationException e) { - throw new RuntimeException("Exception while simulating to get child activities", e); + allGood = false; + logger.error("Tried to simulate the plan {} but a simulation exception happened", planWithAddedActivities, e); } - return replacedInPlan; + return new InsertActivityResult(allGood, acts.stream().map(act -> plan.getActivitiesById().get(act.getId())).toList()); } + /** * creates internal storage space to build up partial solutions in **/ @@ -289,86 +227,9 @@ public void initializePlan() throws SimulationFacade.SimulationException, Schedu checkAndInsertActs(problem.getInitialPlan().getActivitiesByTime(), true); this.checkSimBeforeInsertingActivities = prevCheckFlag; - evaluation = new Evaluation(); - plan.addEvaluation(evaluation); - if(simulationFacade != null) simulationFacade.addInitialPlan(this.plan.getActivitiesByTime()); + plan.addEvaluation(new Evaluation()); } - /** - * For activities that have a null duration (in an initial plan for example) and that have been simulated, we pull the duration and - * replace the original instance with a new instance that includes the duration, both in the plan and the simulation facade - */ - public Map pullActivityDurationsIfNecessary() { - final var toRemoveFromPlan = new ArrayList(); - final var toAddToPlan = new ArrayList(); - final var replaced = new HashMap(); - for (final var activity : plan.getActivities()) { - if (activity.duration() == null) { - final var duration = simulationFacade.getActivityDuration(activity); - if (duration.isPresent()) { - final var replacementAct = SchedulingActivityDirective.copyOf( - activity, - duration.get() - ); - simulationFacade.replaceActivityFromSimulation(activity, replacementAct); - toAddToPlan.add(replacementAct); - toRemoveFromPlan.add(activity); - generatedActivityInstances = generatedActivityInstances.stream().map(pair -> pair.getLeft().equals(activity) ? Pair.of(replacementAct, pair.getRight()): pair).collect(Collectors.toList()); - generatedActivityInstances = generatedActivityInstances.stream().map(pair -> pair.getRight().equals(activity) ? Pair.of(pair.getLeft(), replacementAct): pair).collect(Collectors.toList()); - replaced.put(activity, replacementAct); - } - } - } - plan.remove(toRemoveFromPlan); - plan.add(toAddToPlan); - return replaced; - } - - /** - * For activities that have a null duration (in an initial plan for example) and that have been simulated, we pull the duration and - * replace the original instance with a new instance that includes the duration, both in the plan and the simulation facade - */ - public void replaceActivity(SchedulingActivityDirective actOld, SchedulingActivityDirective actNew) { - simulationFacade.replaceActivityFromSimulation(actOld, actNew); - generatedActivityInstances = generatedActivityInstances.stream().map(pair -> pair.getLeft().equals(actOld) ? Pair.of(actNew, pair.getRight()): pair).collect(Collectors.toList()); - generatedActivityInstances = generatedActivityInstances.stream().map(pair -> pair.getRight().equals(actOld) ? Pair.of(pair.getLeft(), actNew): pair).collect(Collectors.toList()); - plan.remove(actOld); - plan.add(actNew); - } - - /** - * Filters generated activities and makes sure that simulations are only adding activities and not removing them - * @param allNewGeneratedActivities all the generated activities from the last simulation results. - */ - private void processNewGeneratedActivities(Map allNewGeneratedActivities) { - final var activitiesById = plan.getActivitiesById(); - final var formattedNewGeneratedActivities = new ArrayList>(); - allNewGeneratedActivities.entrySet().forEach(entry -> formattedNewGeneratedActivities.add(Pair.of(entry.getKey(), activitiesById.get(entry.getValue())))); - - final var copyOld = new ArrayList<>(this.generatedActivityInstances); - final var copyNew = new ArrayList<>(formattedNewGeneratedActivities); - - for(final var pairOld: this.generatedActivityInstances){ - for (final var pairNew : formattedNewGeneratedActivities){ - if(pairOld.getLeft().equalsInProperties(pairNew.getLeft()) && - pairNew.getRight().equals(pairOld.getRight())){ - copyNew.remove(pairNew); - copyOld.remove(pairOld); - //break at first occurrence. there may be several activities equal in properties. - break; - } - } - } - - //TODO: continuous goal satisfaction - //copyNew contains only things that are new - //copyOld contains only present in old but absent in new - //if(copyOld.size() != 0){ - //throw new Error("Activities have disappeared from simulation, failing"); - //} - this.generatedActivityInstances.addAll(copyNew); - this.plan.add(copyNew.stream().map(Pair::getLeft).toList()); - } /** * iteratively fills in output plan to satisfy input problem description @@ -457,9 +318,9 @@ private void satisfyOptionGoal(OptionGoal goal) throws SchedulingInterruptedExce Collection actsToAssociateWith = null; for (var subgoal : goal.getSubgoals()) { satisfyGoal(subgoal); - if(evaluation.forGoal(subgoal).getScore() == 0 || !subgoal.shouldRollbackIfUnsatisfied()) { - var associatedActivities = evaluation.forGoal(subgoal).getAssociatedActivities(); - var insertedActivities = evaluation.forGoal(subgoal).getInsertedActivities(); + if(plan.getEvaluation().forGoal(subgoal).getScore() == 0 || !subgoal.shouldRollbackIfUnsatisfied()) { + var associatedActivities = plan.getEvaluation().forGoal(subgoal).getAssociatedActivities(); + var insertedActivities = plan.getEvaluation().forGoal(subgoal).getInsertedActivities(); var aggregatedActivities = new ArrayList(); aggregatedActivities.addAll(associatedActivities); aggregatedActivities.addAll(insertedActivities); @@ -477,27 +338,28 @@ private void satisfyOptionGoal(OptionGoal goal) throws SchedulingInterruptedExce if (currentSatisfiedGoal != null) { for(var act: actsToAssociateWith){ //we do not care about ownership here as it is not really a piggyback but just the validation of the supergoal - evaluation.forGoal(goal).associate(act, false); + plan.getEvaluation().forGoal(goal).associate(act, false); } final var insertionResult = checkAndInsertActs(actsToInsert, false); if(insertionResult.success()) { for(var act: insertionResult.activitiesInserted()){ - evaluation.forGoal(goal).associate(act, false); + plan.getEvaluation().forGoal(goal).associate(act, false); } - evaluation.forGoal(goal).setScore(0); + plan.getEvaluation().forGoal(goal).setScore(0); } else{ //this should not happen because we have already tried to insert the same set of activities in the plan and it //did not fail throw new IllegalStateException("Had satisfied subgoal but (1) simulation or (2) association with supergoal failed"); } } else { - evaluation.forGoal(goal).setScore(-1); + plan.getEvaluation().forGoal(goal).setScore(-1); } } else { var atLeastOneSatisfied = false; //just satisfy any goal for (var subgoal : goal.getSubgoals()) { satisfyGoal(subgoal); + final var evaluation = plan.getEvaluation(); final var subgoalIsSatisfied = (evaluation.forGoal(subgoal).getScore() == 0); evaluation.forGoal(goal).associate(evaluation.forGoal(subgoal).getAssociatedActivities(), false); evaluation.forGoal(goal).associate(evaluation.forGoal(subgoal).getInsertedActivities(), true); @@ -509,9 +371,9 @@ private void satisfyOptionGoal(OptionGoal goal) throws SchedulingInterruptedExce logger.info("OR goal " + goal.getName() + ": subgoal " + subgoal.getName() + " could not be satisfied, moving on to next subgoal"); } if(atLeastOneSatisfied){ - evaluation.forGoal(goal).setScore(0); + plan.getEvaluation().forGoal(goal).setScore(0); } else { - evaluation.forGoal(goal).setScore(-1); + plan.getEvaluation().forGoal(goal).setScore(-1); if(goal.shouldRollbackIfUnsatisfied()) { for (var subgoal : goal.getSubgoals()) { rollback(subgoal); @@ -522,7 +384,7 @@ private void satisfyOptionGoal(OptionGoal goal) throws SchedulingInterruptedExce } private void rollback(Goal goal){ - var evalForGoal = evaluation.forGoal(goal); + var evalForGoal = plan.getEvaluation().forGoal(goal); var associatedActivities = evalForGoal.getAssociatedActivities(); var insertedActivities = evalForGoal.getInsertedActivities(); plan.remove(insertedActivities); @@ -538,7 +400,7 @@ private void satisfyCompositeGoal(CompositeAndGoal goal) throws SchedulingInterr var nbGoalSatisfied = 0; for (var subgoal : goal.getSubgoals()) { satisfyGoal(subgoal); - if (evaluation.forGoal(subgoal).getScore() == 0) { + if (plan.getEvaluation().forGoal(subgoal).getScore() == 0) { logger.info("AND goal " + goal.getName() + ": subgoal " + subgoal.getName() + " has been satisfied, moving on to next subgoal"); nbGoalSatisfied++; } else { @@ -552,9 +414,9 @@ private void satisfyCompositeGoal(CompositeAndGoal goal) throws SchedulingInterr } final var goalIsSatisfied = (nbGoalSatisfied == goal.getSubgoals().size()); if (goalIsSatisfied) { - evaluation.forGoal(goal).setScore(0); + plan.getEvaluation().forGoal(goal).setScore(0); } else { - evaluation.forGoal(goal).setScore(-1); + plan.getEvaluation().forGoal(goal).setScore(-1); } if(!goalIsSatisfied && goal.shouldRollbackIfUnsatisfied()){ @@ -564,6 +426,7 @@ private void satisfyCompositeGoal(CompositeAndGoal goal) throws SchedulingInterr } if(goalIsSatisfied) { for (var subgoal : goal.getSubgoals()) { + final var evaluation = plan.getEvaluation(); evaluation.forGoal(goal).associate(evaluation.forGoal(subgoal).getAssociatedActivities(), false); evaluation.forGoal(goal).associate(evaluation.forGoal(subgoal).getInsertedActivities(), true); } @@ -602,7 +465,7 @@ private void satisfyGoalGeneral(Goal goal) throws SchedulingInterruptedException var missingConflicts = getConflicts(goal); logger.info("Found "+ missingConflicts.size() +" conflicts in conflict detection"); //setting the number of conflicts detected at first evaluation, will be used at backtracking - evaluation.forGoal(goal).setNbConflictsDetected(missingConflicts.size()); + plan.getEvaluation().forGoal(goal).setNbConflictsDetected(missingConflicts.size()); assert missingConflicts != null; final var itConflicts = missingConflicts.iterator(); @@ -621,7 +484,7 @@ private void satisfyGoalGeneral(Goal goal) throws SchedulingInterruptedException logger.info("Found activity to satisfy missing activity instance conflict"); final var insertionResult = checkAndInsertActs(acts, false); if(insertionResult.success){ - evaluation.forGoal(goal).associate(insertionResult.activitiesInserted(), true); + plan.getEvaluation().forGoal(goal).associate(insertionResult.activitiesInserted(), true); itConflicts.remove(); //REVIEW: really association should be via the goal's own query... } else{ @@ -647,7 +510,7 @@ else if(!analysisOnly && (missing instanceof MissingActivityTemplateConflict mi final var insertionResult = checkAndInsertActs(acts, false); if(insertionResult.success()){ - evaluation.forGoal(goal).associate(insertionResult.activitiesInserted(), true); + plan.getEvaluation().forGoal(goal).associate(insertionResult.activitiesInserted(), true); //REVIEW: really association should be via the goal's own query... cardinalityLeft--; durationLeft = durationLeft.minus(insertionResult @@ -704,7 +567,7 @@ else if(!analysisOnly && (missing instanceof MissingActivityTemplateConflict mi } else { //decision-making here, we choose the first satisfying activity - evaluation.forGoal(goal).associate(act, false); + plan.getEvaluation().forGoal(goal).associate(act, false); itConflicts.remove(); logger.info("Activity " + act @@ -727,7 +590,7 @@ else if(!analysisOnly && (missing instanceof MissingActivityTemplateConflict mi rollback(goal); } logger.info("Finishing goal satisfaction for goal " + goal.getName() +":"+ (missingConflicts.size() == 0 ? "SUCCESS" : "FAILURE. Number of conflicts that could not be addressed: " + missingConflicts.size())); - evaluation.forGoal(goal).setScore(-missingConflicts.size()); + plan.getEvaluation().forGoal(goal).setScore(-missingConflicts.size()); } /** @@ -745,8 +608,9 @@ private Collection getConflicts(Goal goal) throws SchedulingInterrupte assert plan != null; //REVIEW: maybe should have way to request only certain kinds of conflicts logger.debug("Computing simulation results until "+ this.problem.getPlanningHorizon().getEndAerie() + " (planning horizon end) in order to compute conflicts"); - final var lastSimulationResults = this.getLatestSimResultsUpTo(this.problem.getPlanningHorizon().getEndAerie()); - synchronizeSimulationWithSchedulerPlan(); + final var resources = new HashSet(); + goal.extractResources(resources); + this.getLatestSimResultsUpTo(this.problem.getPlanningHorizon().getEndAerie(), resources); final var evaluationEnvironment = new EvaluationEnvironment(this.problem.getRealExternalProfiles(), this.problem.getDiscreteExternalProfiles()); final Optional> mapSchedulingIdsToActivityIds = simulationFacade.getBidiActivityIdCorrespondence(); final var rawConflicts = goal.getConflicts(plan, lastSimulationResults, mapSchedulingIdsToActivityIds, evaluationEnvironment, this.problem.getSchedulerModel()); @@ -920,8 +784,9 @@ private Windows narrowByResourceConstraints( final var totalDomain = Interval.between(windows.minTrueTimePoint().get().getKey(), windows.maxTrueTimePoint().get().getKey()); //make sure the simulation results cover the domain logger.debug("Computing simulation results until "+ totalDomain.end + " in order to compute resource constraints"); - final var latestSimulationResults = this.getLatestSimResultsUpTo(totalDomain.end); - synchronizeSimulationWithSchedulerPlan(); + final var resourceNames = new HashSet(); + constraints.forEach(c -> c.extractResources(resourceNames)); + final var latestSimulationResults = this.getLatestSimResultsUpTo(totalDomain.end, resourceNames); //iteratively narrow the windows from each constraint //REVIEW: could be some optimization in constraint ordering (smallest domain first to fail fast) final var evaluationEnvironment = new EvaluationEnvironment(this.problem.getRealExternalProfiles(), this.problem.getDiscreteExternalProfiles()); @@ -938,46 +803,41 @@ private Windows narrowByResourceConstraints( } - private SimulationResults getLatestSimResultsUpTo(Duration time) throws SchedulingInterruptedException { - var lastSimResultsFromFacade = this.simulationFacade.getLatestConstraintSimulationResults(); - if (lastSimResultsFromFacade.isEmpty() || lastSimResultsFromFacade.get().bounds.end.shorterThan(time)) { - try { - this.simulationFacade.computeSimulationResultsUntil(time); - } catch (SimulationFacade.SimulationException e) { - throw new RuntimeException("Exception while running simulation before evaluating conflicts", e); - } + private SimulationResults getLatestSimResultsUpTo(final Duration time, final Set resourceNames) throws SchedulingInterruptedException { + try { + if(checkSimBeforeEvaluatingGoal || lastSimulationResults == null) + lastSimulationResults = simulationFacade + .simulateWithResults(plan, time.plus(Duration.of(1, Duration.MICROSECONDS)), resourceNames) + .constraintsResults(); + return lastSimulationResults; + } catch (SimulationFacade.SimulationException e) { + throw new RuntimeException("Exception while running simulation before evaluating conflicts", e); } - return this.simulationFacade.getLatestConstraintSimulationResults().get(); } private Windows narrowGlobalConstraints( - Plan plan, - MissingActivityConflict mac, - Windows windows, - Collection constraints, - EvaluationEnvironment evaluationEnvironment - ) throws SchedulingInterruptedException { + final Plan plan, + final MissingActivityConflict mac, + final Windows windows, + final Collection constraints, + final EvaluationEnvironment evaluationEnvironment) throws SchedulingInterruptedException { Windows tmp = windows; if(tmp.stream().noneMatch(Segment::value)){ return tmp; } //make sure the simulation results cover the domain logger.debug("Computing simulation results until "+ tmp.maxTrueTimePoint().get().getKey() + " in order to compute global scheduling conditions"); - final var latestSimulationResults = this.getLatestSimResultsUpTo(tmp.maxTrueTimePoint().get().getKey()); - synchronizeSimulationWithSchedulerPlan(); - for (GlobalConstraintWithIntrospection gc : constraints) { - if (gc instanceof GlobalConstraintWithIntrospection c) { - tmp = c.findWindows( - plan, - tmp, - mac, - latestSimulationResults, - evaluationEnvironment); - } else { - throw new Error("Unhandled variant of GlobalConstraint: %s".formatted(gc)); - } + final var resourceNames = new HashSet(); + constraints.forEach(c -> c.extractResources(resourceNames)); + final var latestSimulationResults = this.getLatestSimResultsUpTo(tmp.maxTrueTimePoint().get().getKey(), resourceNames); + for (final var gc : constraints) { + tmp = gc.findWindows( + plan, + tmp, + mac, + latestSimulationResults, + evaluationEnvironment); } - return tmp; } @@ -1024,6 +884,10 @@ private Optional instantiateActivity( if(reduced.isEmpty()) return Optional.empty(); final var solved = reduced.get(); + //Extract resource names to lighten the computation of simulation results + final var resourceNames = new HashSet(); + activityExpression.extractResources(resourceNames); + //the domain of user/scheduling temporal constraints have been reduced with the STN, //now it is time to find an assignment compatible //CASE 1: activity has an uncontrollable duration @@ -1034,7 +898,7 @@ private Optional instantiateActivity( public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History history) throws EquationSolvingAlgorithms.DiscontinuityException, SchedulingInterruptedException { - final var latestConstraintsSimulationResults = getLatestSimResultsUpTo(start); + final var latestConstraintsSimulationResults = getLatestSimResultsUpTo(start, resourceNames); final var actToSim = SchedulingActivityDirective.of( activityExpression.type(), start, @@ -1049,14 +913,14 @@ public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History< null, true); final var lastInsertion = history.getLastEvent(); - Optional computedDuration = Optional.empty(); - final var toRemove = new ArrayList(); - lastInsertion.ifPresent(eventWithActivity -> toRemove.add(eventWithActivity.getValue().get().activityDirective())); + Duration computedDuration = null; try { - simulationFacade.removeAndInsertActivitiesFromSimulation(toRemove, List.of(actToSim)); - computedDuration = simulationFacade.getActivityDuration(actToSim); - if(computedDuration.isPresent()) { - history.add(new EquationSolvingAlgorithms.FunctionCoordinate<>(start, start.plus(computedDuration.get())), new ActivityMetadata(actToSim)); + final var duplicatePlan = plan.duplicate(); + duplicatePlan.add(actToSim); + simulationFacade.simulateNoResultsUntilEndAct(duplicatePlan, actToSim); + computedDuration = duplicatePlan.getActivitiesById().get(actToSim.getId()).duration(); + if(computedDuration != null) { + history.add(new EquationSolvingAlgorithms.FunctionCoordinate<>(start, start.plus(computedDuration)), new ActivityMetadata(SchedulingActivityDirective.copyOf(actToSim, computedDuration))); } else{ logger.debug("No simulation error but activity duration could not be found in simulation, likely caused by unfinished activity or activity outside plan bounds."); history.add(new EquationSolvingAlgorithms.FunctionCoordinate<>(start, null), new ActivityMetadata(actToSim)); @@ -1065,7 +929,8 @@ public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History< logger.debug("Simulation error while trying to simulate activities: " + e); history.add(new EquationSolvingAlgorithms.FunctionCoordinate<>(start, null), new ActivityMetadata(actToSim)); } - return computedDuration.map(start::plus).orElseThrow(EquationSolvingAlgorithms.DiscontinuityException::new); + if(computedDuration == null) throw new EquationSolvingAlgorithms.DiscontinuityException(); + return start.plus(computedDuration); } }; @@ -1077,7 +942,7 @@ public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History< final var instantiatedArguments = SchedulingActivityDirective.instantiateArguments( activityExpression.arguments(), earliestStart, - getLatestSimResultsUpTo(earliestStart), + getLatestSimResultsUpTo(earliestStart, resourceNames), evaluationEnvironment, activityExpression.type()); @@ -1102,12 +967,7 @@ public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History< activityExpression.type(), earliestStart, setActivityDuration, - SchedulingActivityDirective.instantiateArguments( - activityExpression.arguments(), - earliestStart, - getLatestSimResultsUpTo(earliestStart), - evaluationEnvironment, - activityExpression.type()), + instantiatedArguments, null, null, true)); @@ -1118,7 +978,6 @@ public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History< } final var earliestStart = solved.start().start; - // TODO: When scheduling is allowed to create activities with anchors, this constructor should pull from an expanded creation template return Optional.of(SchedulingActivityDirective.of( activityExpression.type(), @@ -1127,7 +986,7 @@ public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History< SchedulingActivityDirective.instantiateArguments( activityExpression.arguments(), earliestStart, - getLatestSimResultsUpTo(earliestStart), + getLatestSimResultsUpTo(earliestStart, resourceNames), evaluationEnvironment, activityExpression.type()), null, @@ -1142,7 +1001,7 @@ public Duration valueAt(final Duration start, final EquationSolvingAlgorithms.Hi final var instantiatedArgs = SchedulingActivityDirective.instantiateArguments( activityExpression.arguments(), start, - getLatestSimResultsUpTo(start), + getLatestSimResultsUpTo(start, resourceNames), evaluationEnvironment, activityExpression.type() ); @@ -1210,19 +1069,13 @@ private Optional rootFindingHelper( } catch (EquationSolvingAlgorithms.NoSolutionException e) { logger.info("Rootfinding found no solution"); } - if(!history.events.isEmpty()) { - try { - simulationFacade.removeActivitiesFromSimulation(List.of(history.getLastEvent().get().getRight().get().activityDirective())); - } catch (SimulationFacade.SimulationException e) { - throw new RuntimeException("Exception while simulating original plan after activity insertion failure" ,e); - } - } logger.info("Finished rootfinding: FAILURE"); history.logHistory(); return Optional.empty(); } public void printEvaluation() { + final var evaluation = plan.getEvaluation(); logger.warn("Remaining conflicts for goals "); for (var goalEval : evaluation.getGoals()) { logger.warn(goalEval.getName() + " -> " + evaluation.forGoal(goalEval).score); diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/FixedDurationTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/FixedDurationTest.java index 483a2a39ac..28c859d731 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/FixedDurationTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/FixedDurationTest.java @@ -5,18 +5,25 @@ import gov.nasa.jpl.aerie.constraints.tree.SpansFromWindows; import gov.nasa.jpl.aerie.constraints.tree.WindowsWrapperExpression; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeExpressionRelative; import gov.nasa.jpl.aerie.scheduler.goals.CoexistenceGoal; -import gov.nasa.jpl.aerie.scheduler.model.*; +import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; +import gov.nasa.jpl.aerie.scheduler.model.Problem; +import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.time.Instant; import java.util.List; +import java.util.Map; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOUR; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -29,7 +36,17 @@ public class FixedDurationTest { void setUp(){ planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochDays(3)); MissionModel bananaMissionModel = SimulationUtility.getBananaMissionModel(); - problem = new Problem(bananaMissionModel, planningHorizon, new SimulationFacade(planningHorizon, bananaMissionModel, SimulationUtility.getBananaSchedulerModel(), ()-> false), SimulationUtility.getBananaSchedulerModel()); + problem = new Problem( + bananaMissionModel, + planningHorizon, + new SimulationFacade( + bananaMissionModel, + SimulationUtility.getBananaSchedulerModel(), + new InMemoryCachedEngineStore(10), + planningHorizon, + new SimulationEngineConfiguration(Map.of(), Instant.EPOCH, new MissionModelId(1)), + ()-> false), + SimulationUtility.getBananaSchedulerModel()); } @Test @@ -58,7 +75,6 @@ public void testFieldAnnotation() throws SchedulingInterruptedException { final var plan = solver.getNextSolution().get(); solver.printEvaluation(); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT1M"), planningHorizon.fromStart("PT1H1M"), problem.getActivityType("BananaNap"))); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @@ -88,7 +104,6 @@ public void testMethodAnnotation() throws SchedulingInterruptedException { final var plan = solver.getNextSolution().get(); solver.printEvaluation(); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT1M"), planningHorizon.fromStart("P2DT1M"), problem.getActivityType("RipenBanana"))); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/LongDurationPlanTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/LongDurationPlanTest.java index a86d2c79e3..89d8305338 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/LongDurationPlanTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/LongDurationPlanTest.java @@ -14,7 +14,6 @@ import java.util.List; import static gov.nasa.jpl.aerie.scheduler.TestUtility.assertSetEquality; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class LongDurationPlanTest { @@ -59,7 +58,6 @@ public void getNextSolution_initialPlanInOutput() throws SchedulingInterruptedEx assertTrue(plan.isPresent()); assertSetEquality(plan.get().getActivitiesByTime(), expectedPlan.getActivitiesByTime()); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -78,6 +76,5 @@ public void getNextSolution_proceduralGoalCreatesActivities() throws SchedulingI final var plan = solver.getNextSolution().orElseThrow(); assertSetEquality(plan.getActivitiesByTime(), expectedPlan.getActivitiesByTime()); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/ParametricDurationTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/ParametricDurationTest.java index f058ec4bd9..796beae3de 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/ParametricDurationTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/ParametricDurationTest.java @@ -4,7 +4,10 @@ import gov.nasa.jpl.aerie.constraints.time.Windows; import gov.nasa.jpl.aerie.constraints.tree.SpansFromWindows; import gov.nasa.jpl.aerie.constraints.tree.WindowsWrapperExpression; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; +import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; @@ -17,8 +20,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.time.Instant; import java.util.List; +import java.util.Map; +import static gov.nasa.jpl.aerie.scheduler.TestApplyWhen.dur; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -31,7 +37,13 @@ public class ParametricDurationTest { void setUp(){ planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochDays(3)); MissionModel bananaMissionModel = SimulationUtility.getBananaMissionModel(); - problem = new Problem(bananaMissionModel, planningHorizon, new SimulationFacade(planningHorizon, bananaMissionModel, SimulationUtility.getBananaSchedulerModel(), ()-> false), SimulationUtility.getBananaSchedulerModel()); + problem = new Problem(bananaMissionModel, planningHorizon, new SimulationFacade( + bananaMissionModel, + SimulationUtility.getBananaSchedulerModel(), + new InMemoryCachedEngineStore(15), + planningHorizon, + new SimulationEngineConfiguration(Map.of(), Instant.EPOCH, new MissionModelId(1)), + ()-> false), SimulationUtility.getBananaSchedulerModel()); } @Test @@ -61,7 +73,6 @@ public void testStartConstraint() throws SchedulingInterruptedException { final var plan = solver.getNextSolution().get(); solver.printEvaluation(); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT1M"), planningHorizon.fromStart("PT2M"), problem.getActivityType("DownloadBanana"))); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -91,6 +102,5 @@ public void testEndConstraint() throws SchedulingInterruptedException { final var plan = solver.getNextSolution().get(); solver.printEvaluation(); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT2M"), planningHorizon.fromStart("PT12M"), problem.getActivityType("DownloadBanana"))); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java index f365af3730..9d54160f06 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java @@ -5,6 +5,7 @@ import gov.nasa.jpl.aerie.constraints.time.Windows; import gov.nasa.jpl.aerie.constraints.tree.WindowsWrapperExpression; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeAnchor; @@ -17,14 +18,19 @@ import gov.nasa.jpl.aerie.scheduler.model.PlanInMemory; import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; import gov.nasa.jpl.aerie.scheduler.model.Problem; +import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.Evaluation; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import org.junit.jupiter.api.Test; +import java.time.Instant; import java.util.List; +import java.util.Map; import java.util.Optional; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOURS; import static gov.nasa.jpl.aerie.scheduler.TestUtility.assertSetEquality; import static org.junit.jupiter.api.Assertions.*; @@ -36,7 +42,13 @@ private static PrioritySolver makeEmptyProblemSolver() { new Problem( bananaMissionModel, h, - new SimulationFacade(h, bananaMissionModel, schedulerModel, () -> false), + new SimulationFacade( + bananaMissionModel, + schedulerModel, + new InMemoryCachedEngineStore(15), + h, + new SimulationEngineConfiguration(Map.of(),Instant.EPOCH, new MissionModelId(1)), + () -> false), schedulerModel)); } @@ -77,9 +89,7 @@ public void getNextSolution_givesNoSolutionOnSubsequentCall() throws SchedulingI //test mission with two primitive activity types private static Problem makeTestMissionAB() { - final var fooMissionModel = SimulationUtility.getFooMissionModel(); - final var fooSchedulerModel = SimulationUtility.getFooSchedulerModel(); - return new Problem(fooMissionModel, h, new SimulationFacade(h, fooMissionModel, fooSchedulerModel, ()-> false), fooSchedulerModel); + return SimulationUtility.buildProblemFromFoo(h, 15); } private final static PlanningHorizon h = new PlanningHorizon(TimeUtility.fromDOY("2025-001T01:01:01.001"), TimeUtility.fromDOY("2025-005T01:01:01.001")); @@ -130,7 +140,6 @@ public void getNextSolution_initialPlanInOutput() throws SchedulingInterruptedEx assertTrue(plan.isPresent()); assertSetEquality(plan.get().getActivitiesByTime(), expectedPlan.getActivitiesByTime()); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -149,7 +158,7 @@ public void getNextSolution_proceduralGoalCreatesActivities() throws SchedulingI final var plan = solver.getNextSolution().orElseThrow(); assertSetEquality(plan.getActivitiesByTime(), expectedPlan.getActivitiesByTime()); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); + //1 full sim at the beginning + 3 sims for each act insertion } @Test @@ -171,7 +180,6 @@ public void getNextSolution_proceduralGoalAttachesActivitiesToEvaluation() throw final var eval = plan.getEvaluation().forGoal(goal); assertNotNull(eval); assertSetEquality(eval.getAssociatedActivities().stream().toList(), expectedPlan.getActivitiesByTime()); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -199,7 +207,6 @@ public void getNextSolution_recurrenceGoalWorks() throws SchedulingInterruptedEx //TODO: may want looser expectation (eg allow flexibility as long as right repeat pattern met) assertTrue(plan.getActivitiesByTime().get(0).equalsInProperties(expectedPlan.getActivitiesByTime().get(0))); assertSetEquality(plan.getActivitiesByTime(), expectedPlan.getActivitiesByTime()); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -231,7 +238,6 @@ public void getNextSolution_coexistenceGoalOnActivityWorks() throws SchedulingIn //TODO: evaluation should have association of instances to goal //TODO: should ensure no other spurious acts yet need to ignore special interval activities assertSetEquality(plan.getActivitiesByTime(), expectedPlan.getActivitiesByTime()); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); } /** @@ -243,20 +249,15 @@ public void getNextSolution_coexistenceGoalOnActivityWorks_withInitialSimResults throws SimulationFacade.SimulationException, SchedulingInterruptedException { final var problem = makeTestMissionAB(); - final var adHocFacade = new SimulationFacade( - problem.getPlanningHorizon(), problem.getMissionModel(), problem.getSchedulerModel(), - ()-> false); - adHocFacade.insertActivitiesIntoSimulation(makePlanA012(problem).getActivities()); - adHocFacade.computeSimulationResultsUntil(problem.getPlanningHorizon().getEndAerie()); - final var simResults = adHocFacade.getLatestDriverSimulationResults().get(); - if(adHocFacade.getBidiActivityIdCorrespondence().isPresent()) - problem.setInitialPlan(makePlanA012(problem), Optional.of(simResults), adHocFacade.getBidiActivityIdCorrespondence().get()); - else - problem.setInitialPlan(makePlanA012(problem), Optional.of(simResults), null); - + new InMemoryCachedEngineStore(10), + problem.getPlanningHorizon(), + new SimulationEngineConfiguration(Map.of(),Instant.EPOCH, new MissionModelId(1)), + () -> false); + final var simResults = adHocFacade.simulateWithResults(makePlanA012(problem), h.getEndAerie()); + problem.setInitialPlan(makePlanA012(problem), Optional.of(simResults.driverResults()), simResults.mapSchedulingIdsToActivityIds().get()); final var actTypeA = problem.getActivityType("ControllableDurationActivity"); final var actTypeB = problem.getActivityType("OtherControllableDurationActivity"); final var goal = new CoexistenceGoal.Builder() @@ -278,24 +279,11 @@ public void getNextSolution_coexistenceGoalOnActivityWorks_withInitialSimResults final var plan = solver.getNextSolution().orElseThrow(); final var expectedPlan = makePlanAB012(problem); assertSetEquality(plan.getActivitiesByTime(), expectedPlan.getActivitiesByTime()); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test public void testCardGoalWithApplyWhen() throws SchedulingInterruptedException { - var planningHorizon = h; - - final var fooMissionModel = SimulationUtility.getFooMissionModel(); - final var fooSchedulerModel = SimulationUtility.getFooSchedulerModel(); - Problem problem = new Problem( - fooMissionModel, - planningHorizon, - new SimulationFacade( - planningHorizon, - fooMissionModel, - fooSchedulerModel, - ()-> false), - SimulationUtility.getFooSchedulerModel()); + final var problem = SimulationUtility.buildProblemFromFoo(h); final var activityType = problem.getActivityType("ControllableDurationActivity"); //act at t=1hr and at t=2hrs @@ -326,9 +314,5 @@ public void testCardGoalWithApplyWhen() throws SchedulingInterruptedException { var plan = solver.getNextSolution().orElseThrow(); //will insert an activity at the beginning of the plan in addition of the two already-present activities assertEquals(3, plan.getActivities().size()); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } - - - } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationFacadeTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationFacadeTest.java index 6a6fbc9507..5d23080cc0 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationFacadeTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationFacadeTest.java @@ -31,6 +31,7 @@ import static gov.nasa.jpl.aerie.constraints.time.Interval.Inclusivity.Exclusive; import static gov.nasa.jpl.aerie.constraints.time.Interval.Inclusivity.Inclusive; import static gov.nasa.jpl.aerie.constraints.time.Interval.interval; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECONDS; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -81,7 +82,7 @@ private DiscreteResource getPlantRes() { public void setUp() { missionModel = SimulationUtility.getBananaMissionModel(); final var schedulerModel = SimulationUtility.getBananaSchedulerModel(); - facade = new SimulationFacade(horizon, missionModel, schedulerModel, ()-> false); + facade = new SimulationFacade(horizon, missionModel, schedulerModel); problem = new Problem(missionModel, horizon, facade, schedulerModel); } @@ -160,16 +161,14 @@ public void associationToExistingSatisfyingActivity() throws SchedulingInterrupt final var actAssociatedInSecondRun = plan2.get().getEvaluation().forGoal(goal).getAssociatedActivities(); assertEquals(1, actAssociatedInSecondRun.size()); assertTrue(actAssociatedInFirstRun.iterator().next().equalsInProperties(actAssociatedInSecondRun.iterator().next())); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test public void whenValueAboveDoubleOnSimplePlan() throws SimulationFacade.SimulationException, SchedulingInterruptedException { - facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); - facade.computeSimulationResultsUntil(tEnd); - var actual = new GreaterThan(getFruitRes(), new RealValue(2.9)).evaluate(facade.getLatestConstraintSimulationResults().get()); + final var results = facade.simulateWithResults(makeTestPlanP0B1(), tEnd); + var actual = new GreaterThan(getFruitRes(), new RealValue(2.9)).evaluate(results.constraintsResults()); var expected = new Windows( Segment.of(interval(0, Inclusive, 2, Exclusive, SECONDS), true), Segment.of(interval(2, Inclusive,5, Exclusive, SECONDS), false) @@ -181,9 +180,8 @@ public void whenValueAboveDoubleOnSimplePlan() public void whenValueBelowDoubleOnSimplePlan() throws SimulationFacade.SimulationException, SchedulingInterruptedException { - facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); - facade.computeSimulationResultsUntil(tEnd); - var actual = new LessThan(getFruitRes(), new RealValue(3.0)).evaluate(facade.getLatestConstraintSimulationResults().get()); + final var results = facade.simulateWithResults(makeTestPlanP0B1(), tEnd); + var actual = new LessThan(getFruitRes(), new RealValue(3.0)).evaluate(results.constraintsResults()); var expected = new Windows( Segment.of(interval(0, Inclusive, 2, Exclusive, SECONDS), false), Segment.of(interval(2, Inclusive, 5, Exclusive, SECONDS), true) @@ -195,9 +193,8 @@ public void whenValueBelowDoubleOnSimplePlan() public void whenValueBetweenDoubleOnSimplePlan() throws SimulationFacade.SimulationException, SchedulingInterruptedException { - facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); - facade.computeSimulationResultsUntil(tEnd); - var actual = new And(new GreaterThanOrEqual(getFruitRes(), new RealValue(3.0)), new LessThanOrEqual(getFruitRes(), new RealValue(3.99))).evaluate(facade.getLatestConstraintSimulationResults().get()); + final var results = facade.simulateWithResults(makeTestPlanP0B1(), tEnd); + var actual = new And(new GreaterThanOrEqual(getFruitRes(), new RealValue(3.0)), new LessThanOrEqual(getFruitRes(), new RealValue(3.99))).evaluate(results.constraintsResults()); var expected = new Windows( Segment.of(interval(0, Inclusive, 1, Exclusive, SECONDS), false), Segment.of(interval(1, Inclusive, 2, Exclusive, SECONDS), true), @@ -210,9 +207,8 @@ public void whenValueBetweenDoubleOnSimplePlan() public void whenValueEqualDoubleOnSimplePlan() throws SimulationFacade.SimulationException, SchedulingInterruptedException { - facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); - facade.computeSimulationResultsUntil(tEnd); - var actual = new Equal<>(getFruitRes(), new RealValue(3.0)).evaluate(facade.getLatestConstraintSimulationResults().get()); + final var results = facade.simulateWithResults(makeTestPlanP0B1(), tEnd); + var actual = new Equal<>(getFruitRes(), new RealValue(3.0)).evaluate(results.constraintsResults()); var expected = new Windows( Segment.of(interval(0, Inclusive, 1, Exclusive, SECONDS), false), Segment.of(interval(1, Inclusive, 2, Exclusive, SECONDS), true), @@ -225,9 +221,8 @@ public void whenValueEqualDoubleOnSimplePlan() public void whenValueNotEqualDoubleOnSimplePlan() throws SimulationFacade.SimulationException, SchedulingInterruptedException { - facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); - facade.computeSimulationResultsUntil(tEnd); - var actual = new NotEqual<>(getFruitRes(), new RealValue(3.0)).evaluate(facade.getLatestConstraintSimulationResults().get()); + final var results = facade.simulateWithResults(makeTestPlanP0B1(), tEnd); + var actual = new NotEqual<>(getFruitRes(), new RealValue(3.0)).evaluate(results.constraintsResults()); var expected = new Windows( Segment.of(interval(0, Inclusive, 1, Exclusive, SECONDS), true), Segment.of(interval(1, Inclusive, 2, Exclusive, SECONDS), false), @@ -273,7 +268,6 @@ public void testCoexistenceGoalWithResourceConstraint() throws SchedulingInterru final var solver = new PrioritySolver(this.problem); final var plan = solver.getNextSolution().orElseThrow(); assertTrue(TestUtility.containsActivity(plan, t2, t2, actTypePeel)); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -315,7 +309,6 @@ public void testProceduralGoalWithResourceConstraint() throws SchedulingInterrup assertTrue(TestUtility.containsExactlyActivity(plan, act2)); assertTrue(TestUtility.doesNotContainActivity(plan, act1)); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -357,6 +350,5 @@ public void testActivityTypeWithResourceConstraint() throws SchedulingInterrupte final var plan = solver.getNextSolution().orElseThrow(); assertTrue(TestUtility.containsExactlyActivity(plan, act2)); assertTrue(TestUtility.doesNotContainActivity(plan, act1)); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationUtility.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationUtility.java index 468fb7bcde..2e4cf44fa0 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationUtility.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationUtility.java @@ -5,13 +5,17 @@ import gov.nasa.jpl.aerie.merlin.driver.DirectiveTypeRegistry; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; import gov.nasa.jpl.aerie.merlin.driver.MissionModelBuilder; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; import gov.nasa.jpl.aerie.scheduler.model.Problem; +import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; import java.nio.file.Path; import java.time.Instant; +import java.util.Map; public final class SimulationUtility { @@ -32,32 +36,46 @@ private static MissionModel makeMissionModel(final MissionModelBuilder builde return builder.build(model, registry); } - public static Problem buildProblemFromFoo(final PlanningHorizon planningHorizon){ + public static Problem buildProblemFromFoo(final PlanningHorizon planningHorizon) { + return buildProblemFromFoo(planningHorizon, 0); + } + + public static Problem buildProblemFromFoo(final PlanningHorizon planningHorizon, final int simulationCacheSize){ final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var fooSchedulerModel = SimulationUtility.getFooSchedulerModel(); return new Problem( fooMissionModel, planningHorizon, new SimulationFacade( - planningHorizon, fooMissionModel, fooSchedulerModel, + new InMemoryCachedEngineStore(simulationCacheSize), + planningHorizon, + new SimulationEngineConfiguration( + Map.of(), + Instant.EPOCH, + new MissionModelId(1)), ()->false), fooSchedulerModel); } public static Problem buildProblemFromBanana(final PlanningHorizon planningHorizon){ - final var fooMissionModel = SimulationUtility.getBananaMissionModel(); - final var fooSchedulerModel = SimulationUtility.getBananaSchedulerModel(); + final var bananaMissionModel = SimulationUtility.getBananaMissionModel(); + final var bananaSchedulerModel = SimulationUtility.getBananaSchedulerModel(); return new Problem( - fooMissionModel, + bananaMissionModel, planningHorizon, new SimulationFacade( + bananaMissionModel, + bananaSchedulerModel, + new InMemoryCachedEngineStore(15), planningHorizon, - fooMissionModel, - fooSchedulerModel, + new SimulationEngineConfiguration( + Map.of(), + Instant.EPOCH, + new MissionModelId(1)), ()->false), - fooSchedulerModel); + bananaSchedulerModel); } public static SchedulerModel getFooSchedulerModel(){ diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestApplyWhen.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestApplyWhen.java index 504df48b49..11657ff701 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestApplyWhen.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestApplyWhen.java @@ -23,37 +23,42 @@ import gov.nasa.jpl.aerie.constraints.tree.SpansWrapperExpression; import gov.nasa.jpl.aerie.constraints.tree.ValueAt; import gov.nasa.jpl.aerie.constraints.tree.WindowsWrapperExpression; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; -import gov.nasa.jpl.aerie.scheduler.constraints.TimeRangeExpression; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeAnchor; import gov.nasa.jpl.aerie.scheduler.goals.CardinalityGoal; import gov.nasa.jpl.aerie.scheduler.goals.ChildCustody; import gov.nasa.jpl.aerie.scheduler.goals.CoexistenceGoal; import gov.nasa.jpl.aerie.scheduler.goals.RecurrenceGoal; +import gov.nasa.jpl.aerie.scheduler.model.Problem; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; import gov.nasa.jpl.aerie.scheduler.model.PlanInMemory; import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; -import gov.nasa.jpl.aerie.scheduler.model.Problem; -import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; +import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; +import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.time.Instant; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Map; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOURS; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTE; import static gov.nasa.jpl.aerie.scheduler.SimulationUtility.buildProblemFromFoo; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertFalse; public class TestApplyWhen { - private static final Logger logger = LoggerFactory.getLogger(TestApplyWhen.class); ////////////////////////////////////////////RECURRENCE//////////////////////////////////////////// @@ -87,7 +92,6 @@ public void testRecurrenceCutoff1() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -120,7 +124,6 @@ public void testRecurrenceCutoff2() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -153,7 +156,6 @@ public void testRecurrenceShorterWindow() throws SchedulingInterruptedException assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -186,7 +188,6 @@ public void testRecurrenceLongerWindow() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -232,7 +233,6 @@ public void testRecurrenceBabyWindow() throws SchedulingInterruptedException { assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -275,7 +275,6 @@ public void testRecurrenceWindows() throws SchedulingInterruptedException { assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -318,7 +317,6 @@ public void testRecurrenceWindowsCutoffMidInterval() throws SchedulingInterrupte assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -363,7 +361,6 @@ public void testRecurrenceWindowsGlobalCheck() throws SchedulingInterruptedExcep assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(8, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(17, Duration.SECONDS), activityType)); //interval (len 4) needs to be 2 longer than the recurrence repeatingEvery (len 3) - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -408,7 +405,6 @@ public void testRecurrenceWindowsCutoffMidActivity() throws SchedulingInterrupte assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); //assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); //should fail, won't work if cutoff mid-activity - interval expected to be longer than activity duration!!! assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -440,7 +436,6 @@ public void testRecurrenceCutoffUncontrollable() throws SchedulingInterruptedExc assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } @@ -450,7 +445,6 @@ public void testRecurrenceCutoffUncontrollable() throws SchedulingInterruptedExc public void testCardinality() throws SchedulingInterruptedException { Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(5, Duration.SECONDS)); - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); final var problem = buildProblemFromFoo(planningHorizon); @@ -482,7 +476,6 @@ public void testCardinality() throws SchedulingInterruptedException { assertEquals(plan.get().getActivitiesByTime().stream() .map(SchedulingActivityDirective::duration) .reduce(Duration.ZERO, Duration::plus), Duration.of(4, Duration.SECOND)); //1 gets added, then throws 4 warnings meaning it tried to schedule 5 in total, not the expected 8... - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -529,7 +522,6 @@ public void testCardinalityWindows() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(3, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(13, Duration.SECONDS), activityType)); - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -577,7 +569,6 @@ public void testCardinalityWindowsCutoffMidActivity() throws SchedulingInterrupt //assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(7, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(9, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -587,7 +578,6 @@ public void testCardinalityUncontrollable() throws SchedulingInterruptedExceptio */ Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(20, Duration.SECONDS)); - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); final var problem = buildProblemFromFoo(planningHorizon); @@ -621,7 +611,6 @@ public void testCardinalityUncontrollable() throws SchedulingInterruptedExceptio .reduce(Duration.ZERO, Duration::plus); assertTrue(size >= 3 && size <= 10); assertTrue(totalDuration.dividedBy(Duration.SECOND) >= 16 && totalDuration.dividedBy(Duration.SECOND) <= 19); - assertEquals(9, problem.getSimulationFacade().countSimulationRestarts()); } @@ -632,7 +621,6 @@ public void testCoexistenceWindowCutoff() throws SchedulingInterruptedException Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(12, Duration.SECONDS)); - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); final var problem = buildProblemFromFoo(planningHorizon); @@ -673,19 +661,11 @@ public void testCoexistenceWindowCutoff() throws SchedulingInterruptedException logger.debug(a.startOffset().toString() + ", " + a.duration().toString()); } assertEquals(4, plan.get().getActivitiesByTime().size()); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test public void testCoexistenceJustFits() throws SchedulingInterruptedException { - Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(13, Duration.SECONDS));//13, so it just fits in - - var periodTre = new TimeRangeExpression.Builder() - .from(new Windows(false).set(period, true)) - .build(); - - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); final var problem = buildProblemFromFoo(planningHorizon); @@ -726,7 +706,6 @@ public void testCoexistenceJustFits() throws SchedulingInterruptedException { logger.debug(a.startOffset().toString() + ", " + a.duration().toString()); } assertEquals(5, plan.get().getActivitiesByTime().size()); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -741,11 +720,6 @@ public void testCoexistenceUncontrollableCutoff() throws SchedulingInterruptedEx Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(13, Duration.SECONDS)); - var periodTre = new TimeRangeExpression.Builder() - .from(new Windows(false).set(period, true)) - .build(); - - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); final var problem = buildProblemFromFoo(planningHorizon); @@ -765,7 +739,7 @@ public void testCoexistenceUncontrollableCutoff() throws SchedulingInterruptedEx .ofType(actTypeA) .build(); - //and cut off in the middle of one of the already present activities (period ends at 18) + //and cut off in the middle of one of the already present activities (period ends at 13) final var actTypeB = problem.getActivityType("BasicActivity"); CoexistenceGoal goal = new CoexistenceGoal.Builder() .forAllTimeIn(new WindowsWrapperExpression(new Windows(false).set(period, true))) @@ -785,10 +759,7 @@ public void testCoexistenceUncontrollableCutoff() throws SchedulingInterruptedEx for(SchedulingActivityDirective a : plan.get().getActivitiesByTime()){ logger.debug(a.startOffset().toString() + ", " + a.duration().toString()); } - assertEquals(2, plan.get().getActivitiesByTime() - .stream().filter($ -> $.duration().dividedBy(Duration.SECOND) == 2).toList() - .size()); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(2, plan.get().getActivitiesByType().get(actTypeB).size()); } @Test @@ -800,7 +771,6 @@ public void testCoexistenceWindows() throws SchedulingInterruptedException { // GOAL WINDOW: [++++-------+++++++----] // RESULT: [++-----------++-------] - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); final var problem = buildProblemFromFoo(planningHorizon); @@ -853,7 +823,6 @@ public void testCoexistenceWindows() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(1, Duration.SECONDS), actTypeA)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(14, Duration.SECONDS), actTypeA)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(12, Duration.SECONDS), actTypeA)); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -865,7 +834,6 @@ public void testCoexistenceWindowsCutoffMidActivity() throws SchedulingInterrupt // GOAL WINDOW: [++++-----+++++-+++--+-++++++] // RESULT: [-\\------++----++-------++--] (the first one won't be scheduled, ask Adrien) - FIXED - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(28)); //this boundary is inclusive. final var problem = buildProblemFromFoo(planningHorizon); @@ -927,7 +895,6 @@ public void testCoexistenceWindowsCutoffMidActivity() throws SchedulingInterrupt assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(16, Duration.SECONDS), actTypeB)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(23, Duration.SECONDS), actTypeB)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(25, Duration.SECONDS), actTypeB)); - assertEquals(6, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -942,7 +909,6 @@ public void testCoexistenceWindowsBisect() throws SchedulingInterruptedException RESULT: [++-------|++-] */ - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(12)); final var problem = buildProblemFromFoo(planningHorizon); @@ -994,7 +960,6 @@ public void testCoexistenceWindowsBisect() throws SchedulingInterruptedException assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(1, Duration.SECONDS), actTypeA)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(8, Duration.SECONDS), actTypeA)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(10, Duration.SECONDS), actTypeA)); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -1008,7 +973,6 @@ public void testCoexistenceWindowsBisect2() throws SchedulingInterruptedExceptio RESULT: [++--++--++---++-] */ - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(16)); final var problem = buildProblemFromFoo(planningHorizon); @@ -1060,7 +1024,6 @@ public void testCoexistenceWindowsBisect2() throws SchedulingInterruptedExceptio assertFalse(TestUtility.activityStartingAtTime(plan.get(), Duration.of(5, Duration.SECONDS), actTypeA)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(9, Duration.SECONDS), actTypeA)); assertFalse(TestUtility.activityStartingAtTime(plan.get(), Duration.of(14, Duration.SECONDS), actTypeA)); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -1068,11 +1031,6 @@ public void testCoexistenceUncontrollableJustFits() throws SchedulingInterrupted Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(13, Duration.SECONDS)); - var periodTre = new TimeRangeExpression.Builder() - .from(new Windows(false).set(period, true)) - .build(); - - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); final var problem = buildProblemFromFoo(planningHorizon); @@ -1113,29 +1071,13 @@ public void testCoexistenceUncontrollableJustFits() throws SchedulingInterrupted logger.debug(a.startOffset().toString() + ", " + a.duration().toString()); } assertEquals(5, plan.get().getActivitiesByTime().size()); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test public void testCoexistenceExternalResource() throws SchedulingInterruptedException { Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(25, Duration.SECONDS)); - - final var fooMissionModel = SimulationUtility.getFooMissionModel(); - final var fooSchedulerMissionModel = SimulationUtility.getFooSchedulerModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); - - final var simulationFacade = new SimulationFacade( - planningHorizon, - fooMissionModel, - fooSchedulerMissionModel, - ()-> false); - final var problem = new Problem( - fooMissionModel, - planningHorizon, - simulationFacade, - fooSchedulerMissionModel - ); - + final var problem = SimulationUtility.buildProblemFromFoo(planningHorizon); final var r3Value = Map.of("amountInMicroseconds", SerializedValue.of(6)); final var r1 = new LinearProfile(new Segment<>(Interval.between(Duration.ZERO, Duration.SECONDS.times(5)), new LinearEquation(Duration.ZERO, 5, 1))); final var r2 = new DiscreteProfile(new Segment<>(Interval.FOREVER, SerializedValue.of(5))); @@ -1183,7 +1125,6 @@ public void testCoexistenceExternalResource() throws SchedulingInterruptedExcept assertEquals(act.duration(), Duration.of(r3Value.get("amountInMicroseconds").asInt().get(), Duration.MICROSECONDS)); assertEquals(startOfActivity, Duration.of(1, Duration.SECONDS)); assertEquals(act.startOffset(), startOfActivity); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -1193,10 +1134,12 @@ public void testCoexistenceWithAnchors() throws SchedulingInterruptedException { final var bananaMissionModel = SimulationUtility.getBananaMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochHours(0), TestUtility.timeFromEpochHours(20)); - final var simulationFacade = new SimulationFacade( - planningHorizon, + final var simulationFacade = new CheckpointSimulationFacade( bananaMissionModel, SimulationUtility.getBananaSchedulerModel(), + new InMemoryCachedEngineStore(10), + planningHorizon, + new SimulationEngineConfiguration(Map.of(), Instant.now(), new MissionModelId(0)), () -> false); final var problem = new Problem( bananaMissionModel, @@ -1316,7 +1259,6 @@ public void changingForAllTimeIn() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(13, Duration.SECONDS), activityTypeDependent)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(15, Duration.SECONDS), activityTypeDependent)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(17, Duration.SECONDS), activityTypeDependent)); - assertEquals(11, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -1391,7 +1333,6 @@ public void changingForAllTimeInCutoff() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(13, Duration.SECONDS), activityTypeDependent)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(15, Duration.SECONDS), activityTypeDependent)); assertFalse(TestUtility.activityStartingAtTime(plan.get(), Duration.of(17, Duration.SECONDS), activityTypeDependent)); - assertEquals(10, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -1473,6 +1414,5 @@ public void changingForAllTimeInAlternativeCutoff() throws SchedulingInterrupted assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(13, Duration.SECONDS), activityTypeDependent)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(15, Duration.SECONDS), activityTypeDependent)); assertFalse(TestUtility.activityStartingAtTime(plan.get(), Duration.of(17, Duration.SECONDS), activityTypeDependent)); - assertEquals(10, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestCardinalityGoal.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestCardinalityGoal.java index df9919cfa3..19bacdbfc9 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestCardinalityGoal.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestCardinalityGoal.java @@ -14,7 +14,6 @@ import java.util.List; -import static gov.nasa.jpl.aerie.scheduler.SimulationUtility.buildProblemFromFoo; import static org.junit.jupiter.api.Assertions.assertEquals; public class TestCardinalityGoal { @@ -24,8 +23,7 @@ public void testone() throws SchedulingInterruptedException { Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(20, Duration.SECONDS)); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); - final var problem = buildProblemFromFoo(planningHorizon); - + final var problem = SimulationUtility.buildProblemFromFoo(planningHorizon); CardinalityGoal goal = new CardinalityGoal.Builder() .duration(Interval.between(Duration.of(12, Duration.SECONDS), Duration.of(15, Duration.SECONDS))) @@ -48,6 +46,5 @@ public void testone() throws SchedulingInterruptedException { assertEquals(plan.get().getActivitiesByTime().stream() .map(SchedulingActivityDirective::duration) .reduce(Duration.ZERO, Duration::plus), Duration.of(12, Duration.SECOND)); - assertEquals(7, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestPersistentAnchor.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestPersistentAnchor.java index 19a6ee5c34..3d065f6021 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestPersistentAnchor.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestPersistentAnchor.java @@ -10,6 +10,8 @@ import gov.nasa.jpl.aerie.constraints.tree.Expression; import gov.nasa.jpl.aerie.constraints.tree.ForEachActivitySpans; import gov.nasa.jpl.aerie.constraints.tree.WindowsWrapperExpression; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; @@ -17,13 +19,15 @@ import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeExpressionRelative; import gov.nasa.jpl.aerie.scheduler.goals.CoexistenceGoal; import gov.nasa.jpl.aerie.scheduler.model.*; -import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; +import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; +import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import org.apache.commons.lang3.function.TriFunction; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.time.Instant; import java.util.*; import java.util.stream.Collectors; @@ -430,10 +434,12 @@ public TestData createTestCaseStartsAt(final PersistentTimeAnchor persistentAnch final var bananaMissionModel = SimulationUtility.getBananaMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochHours(0), TestUtility.timeFromEpochHours(20)); - final var simulationFacade = new SimulationFacade( - planningHorizon, + final var simulationFacade = new CheckpointSimulationFacade( bananaMissionModel, SimulationUtility.getBananaSchedulerModel(), + new InMemoryCachedEngineStore(10), + planningHorizon, + new SimulationEngineConfiguration(Map.of(), Instant.now(), new MissionModelId(0)), () -> false); final var problem = new Problem( bananaMissionModel, diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestRecurrenceGoal.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestRecurrenceGoal.java index d7bcfabb9e..e6c1890d7c 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestRecurrenceGoal.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestRecurrenceGoal.java @@ -13,7 +13,6 @@ import java.util.List; import static gov.nasa.jpl.aerie.scheduler.SimulationUtility.buildProblemFromFoo; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -45,7 +44,6 @@ public void testRecurrence() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -54,7 +52,7 @@ public void testRecurrenceNegative() { final var problem = buildProblemFromFoo(planningHorizon); try { final var activityType = problem.getActivityType("ControllableDurationActivity"); - final var goal = new RecurrenceGoal.Builder() + new RecurrenceGoal.Builder() .named("Test recurrence goal") .forAllTimeIn(new WindowsWrapperExpression(new Windows(false).set(Interval.betweenClosedOpen(Duration.of(1, Duration.SECONDS), Duration.of(20, Duration.SECONDS)), true))) @@ -65,6 +63,7 @@ public void testRecurrenceNegative() { .repeatingEvery(Duration.of(-1, Duration.SECONDS)) .withinPlanHorizon(planningHorizon) .build(); + fail(); } catch (IllegalArgumentException e) { //minimum is checked first so that's the output, even though the value for repeatingEvery is set as both the min @@ -75,7 +74,6 @@ public void testRecurrenceNegative() { catch (Exception e) { fail(e.getMessage()); } - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestRecurrenceGoalExtended.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestRecurrenceGoalExtended.java index a68978ad4b..59e1bcf018 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestRecurrenceGoalExtended.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestRecurrenceGoalExtended.java @@ -13,7 +13,6 @@ import java.util.List; import static gov.nasa.jpl.aerie.scheduler.SimulationUtility.buildProblemFromFoo; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class TestRecurrenceGoalExtended { @@ -47,7 +46,6 @@ public void testRecurrence() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } /** @@ -76,7 +74,6 @@ public void testRecurrenceSecondGoalOutOfWindowAndPlanHorizon() throws Schedulin var plan = solver.getNextSolution().orElseThrow(); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(1, Duration.SECONDS), activityType) && (plan.getActivities().size() == 1)); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } /** @@ -105,7 +102,6 @@ public void testRecurrenceRepeatIntervalLargerThanGoalWindow() throws Scheduling var plan = solver.getNextSolution().orElseThrow(); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(1, Duration.SECONDS), activityType)); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } /** @@ -140,7 +136,6 @@ public void testGoalWindowLargerThanPlanHorizon() throws SchedulingInterruptedEx var plan = solver.getNextSolution().orElseThrow(); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(1, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(8, Duration.SECONDS), activityType)); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @@ -170,7 +165,6 @@ public void testGoalDurationLargerGoalWindow() throws SchedulingInterruptedExcep var plan = solver.getNextSolution().orElseThrow(); assertTrue(TestUtility.emptyPlan(plan)); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @@ -200,7 +194,6 @@ public void testGoalDurationLargerRepeatInterval() throws SchedulingInterruptedE var plan = solver.getNextSolution().orElseThrow(); assertTrue(TestUtility.emptyPlan(plan)); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @@ -248,6 +241,5 @@ public void testAddActivityNonEmptyPlan() throws SchedulingInterruptedException assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(5, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(10, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(15, Duration.SECONDS), activityType)); - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestUnsatisfiableCompositeGoals.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestUnsatisfiableCompositeGoals.java index 6a538b1b92..83dbb41db2 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestUnsatisfiableCompositeGoals.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestUnsatisfiableCompositeGoals.java @@ -20,8 +20,12 @@ import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import java.util.List; +import java.util.stream.Stream; import static gov.nasa.jpl.aerie.scheduler.SimulationUtility.buildProblemFromFoo; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -44,6 +48,10 @@ private static Problem makeTestMissionAB() { return SimulationUtility.buildProblemFromFoo(h); } + private static Problem makeTestMissionABWithCache() { + return SimulationUtility.buildProblemFromFoo(h, 15); + } + private static PlanInMemory makePlanA12(Problem problem) { final var plan = new PlanInMemory(); final var actTypeA = problem.getActivityType("ControllableDurationActivity"); @@ -70,9 +78,13 @@ public CoexistenceGoal BForEachAGoal(ActivityType A, ActivityType B){ .build(); } - @Test - public void testAndWithoutBackTrack() throws SchedulingInterruptedException { - final var problem = makeTestMissionAB(); + static Stream testAndWithoutBackTrack() { + return Stream.of(Arguments.of(makeTestMissionAB()), + Arguments.of(makeTestMissionABWithCache())); + } + @ParameterizedTest + @MethodSource + public void testAndWithoutBackTrack(Problem problem) throws SchedulingInterruptedException { problem.setInitialPlan(makePlanA12(problem)); final var actTypeControllable = problem.getActivityType("ControllableDurationActivity"); final var actTypeBasic = problem.getActivityType("BasicActivity"); @@ -102,7 +114,6 @@ public void testAndWithoutBackTrack() throws SchedulingInterruptedException { Assertions.assertTrue(TestUtility.activityStartingAtTime(plan, t2hr, actTypeBar)); Assertions.assertTrue(TestUtility.activityStartingAtTime(plan, t2hr, actTypeBasic)); Assertions.assertEquals(plan.getActivities().size(), 5); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -135,7 +146,6 @@ public void testAndWithBackTrack() throws SchedulingInterruptedException { Assertions.assertTrue(TestUtility.activityStartingAtTime(plan, t1hr, actTypeControllable)); Assertions.assertTrue(TestUtility.activityStartingAtTime(plan, t2hr, actTypeControllable)); Assertions.assertEquals(plan.getActivities().size(), 2); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -175,7 +185,6 @@ public void testOrWithoutBacktrack() throws SchedulingInterruptedException { Assertions.assertTrue(TestUtility.activityStartingAtTime(plan, t2hr, actTypeBar)); Assertions.assertTrue(TestUtility.activityStartingAtTime(plan, t2hr, actTypeBasic)); Assertions.assertEquals(plan.getActivities().size(), 4); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -210,7 +219,6 @@ public void testOrWithBacktrack() throws SchedulingInterruptedException { Assertions.assertTrue(TestUtility.activityStartingAtTime(plan, t1hr, actTypeControllable)); Assertions.assertTrue(TestUtility.activityStartingAtTime(plan, t2hr, actTypeControllable)); Assertions.assertEquals(plan.getActivities().size(), 2); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -249,6 +257,5 @@ public void testCardinalityBacktrack() throws SchedulingInterruptedException { var plan = solver.getNextSolution().orElseThrow(); assertEquals(0, plan.getActivities().size()); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/UncontrollableDurationTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/UncontrollableDurationTest.java index 7dca6799e1..bf965c5161 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/UncontrollableDurationTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/UncontrollableDurationTest.java @@ -23,7 +23,6 @@ import java.util.List; import static gov.nasa.jpl.aerie.scheduler.SimulationUtility.buildProblemFromFoo; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class UncontrollableDurationTest { @@ -93,7 +92,6 @@ public void testNonLinear() throws SchedulingInterruptedException { assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT0S"), planningHorizon.fromStart("PT1M29S"), problem.getActivityType("SolarPanelNonLinear"))); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT16M40S"), planningHorizon.fromStart("PT18M9S"), problem.getActivityType("SolarPanelNonLinear"))); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT33M20S"), planningHorizon.fromStart("PT34M49S"), problem.getActivityType("SolarPanelNonLinear"))); - assertEquals(11, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -146,7 +144,6 @@ public void testTimeDependent() throws SchedulingInterruptedException { assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT33M20S"), planningHorizon.fromStart("PT36M47S"), problem.getActivityType("SolarPanelNonLinearTimeDependent"))); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT0S"), planningHorizon.fromStart("PT2M21S"), problem.getActivityType("SolarPanelNonLinearTimeDependent"))); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT16M40S"), planningHorizon.fromStart("PT17M18S"), problem.getActivityType("SolarPanelNonLinearTimeDependent"))); - assertEquals(21, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -184,7 +181,6 @@ public void testBug() throws SchedulingInterruptedException { planningHorizon.fromStart("PT0.000004S"), planningHorizon.fromStart("PT0.000004S"), problem.getActivityType("ZeroDurationUncontrollableActivity"))); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -220,7 +216,6 @@ public void testScheduleExceptionThrowingTask() throws SchedulingInterruptedExce planningHorizon.fromStart("PT1M38.886061S"), planningHorizon.fromStart("PT1M38.886061S"), problem.getActivityType("LateRiser"))); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacadeTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacadeTest.java new file mode 100644 index 0000000000..36636ae180 --- /dev/null +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacadeTest.java @@ -0,0 +1,119 @@ +package gov.nasa.jpl.aerie.scheduler.simulation; + +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; +import gov.nasa.jpl.aerie.merlin.framework.ThreadedTask; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.scheduler.SchedulingInterruptedException; +import gov.nasa.jpl.aerie.scheduler.SimulationUtility; +import gov.nasa.jpl.aerie.scheduler.TimeUtility; +import gov.nasa.jpl.aerie.scheduler.model.ActivityType; +import gov.nasa.jpl.aerie.scheduler.model.PlanInMemory; +import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; +import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class CheckpointSimulationFacadeTest { + private SimulationFacade newSimulationFacade; + private final static PlanningHorizon H = new PlanningHorizon(TimeUtility.fromDOY("2025-001T00:00:00.000"), TimeUtility.fromDOY("2025-005T00:00:00.000")); + + private final static Duration t0 = H.getStartAerie(); + private final static Duration d1min = Duration.of(1, Duration.MINUTE); + private final static Duration d1hr = Duration.of(1, Duration.HOUR); + private final static Duration t1hr = t0.plus(d1hr); + private final static Duration t2hr = t0.plus(d1hr.times(2)); + private final static Duration t3hr = t0.plus(d1hr.times(3)); + private static PlanInMemory makePlanA012(Map activityTypeMap) { + final var plan = new PlanInMemory(); + final var actTypeA = activityTypeMap.get("BasicActivity"); + plan.add(SchedulingActivityDirective.of(actTypeA, t0, null, null, true)); + plan.add(SchedulingActivityDirective.of(actTypeA, t1hr, null, null, true)); + plan.add(SchedulingActivityDirective.of(actTypeA, t2hr, null, null, true)); + return plan; + } + @BeforeEach + public void before(){ + ThreadedTask.CACHE_READS = true; + } + @Test + public void planUpdateTest() throws SimulationFacade.SimulationException, SchedulingInterruptedException { + final var fooMissionModel = SimulationUtility.getFooMissionModel(); + final Map activityTypes = new HashMap<>(); + for(var taskType : fooMissionModel.getDirectiveTypes().directiveTypes().entrySet()){ + activityTypes.put(taskType.getKey(), new ActivityType(taskType.getKey(), taskType.getValue(), SimulationUtility.getFooSchedulerModel().getDurationTypes().get(taskType.getKey()))); + } + final var plan = makePlanA012(activityTypes); + newSimulationFacade = new SimulationFacade( + fooMissionModel, + SimulationUtility.getFooSchedulerModel(), + new InMemoryCachedEngineStore(10), + H, + new SimulationEngineConfiguration(Map.of(), Instant.EPOCH, new MissionModelId(1)), + () -> false); + newSimulationFacade.addActivityTypes(activityTypes.values()); + newSimulationFacade.simulateNoResults(plan, t2hr); + //we are stopping at 2hr, at the start of the last activity so it will not have a duraiton in the plan + assertNull(plan.getActivities().stream().filter(a -> a.startOffset().isEqualTo(t2hr)).findFirst().get().duration()); + } + + @Test + public void planUpdateTest2() throws SimulationFacade.SimulationException, SchedulingInterruptedException { + final var fooMissionModel = SimulationUtility.getFooMissionModel(); + final Map activityTypes = new HashMap<>(); + for(var taskType : fooMissionModel.getDirectiveTypes().directiveTypes().entrySet()){ + activityTypes.put(taskType.getKey(), new ActivityType(taskType.getKey(), taskType.getValue(), SimulationUtility.getFooSchedulerModel().getDurationTypes().get(taskType.getKey()))); + } + final var plan = makePlanA012(activityTypes); + newSimulationFacade = new SimulationFacade( + fooMissionModel, + SimulationUtility.getFooSchedulerModel(), + new InMemoryCachedEngineStore(10), + H, + new SimulationEngineConfiguration(Map.of(),Instant.EPOCH, new MissionModelId(1)), + () -> false); + newSimulationFacade.addActivityTypes(activityTypes.values()); + newSimulationFacade.simulateNoResults(plan, t3hr); + //we are stopping at 2hr, at the start of the last activity so it will not have a duraiton in the plan + assertNotNull(plan + .getActivities() + .stream() + .filter(a -> a.startOffset().isEqualTo(t2hr)) + .findFirst() + .get() + .duration()); + } + + @Test + public void secondIteration() throws SimulationFacade.SimulationException, SchedulingInterruptedException { + final var fooMissionModel = SimulationUtility.getFooMissionModel(); + final Map activityTypes = new HashMap<>(); + for(var taskType : fooMissionModel.getDirectiveTypes().directiveTypes().entrySet()){ + activityTypes.put(taskType.getKey(), new ActivityType(taskType.getKey(), taskType.getValue(), SimulationUtility.getFooSchedulerModel().getDurationTypes().get(taskType.getKey()))); + } + final var plan = makePlanA012(activityTypes); + final var cachedEngines = new InMemoryCachedEngineStore(10); + newSimulationFacade = new SimulationFacade( + fooMissionModel, + SimulationUtility.getFooSchedulerModel(), + cachedEngines, + H, + new SimulationEngineConfiguration(Map.of(),Instant.EPOCH, new MissionModelId(1)), + () -> false + ); + newSimulationFacade.addActivityTypes(activityTypes.values()); + final var ret = newSimulationFacade.simulateWithResults(plan, t2hr); + final var ret2 = newSimulationFacade.simulateWithResults(plan, t2hr); + //TODO: equality on two checkpoint is difficult, although the plan are the same and the startoffset is the same, they are not equal (cells...) + //so when saving, we don't know if we already have the same checkpoint + //we are stopping at 2hr, at the start of the last activity so it will not have a duraiton in the plan + SimulationResultsComparisonUtils.assertEqualsSimulationResults(ret.driverResults(), ret2.driverResults()); + } +} diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationResultsComparisonUtils.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationResultsComparisonUtils.java new file mode 100644 index 0000000000..25da0a83d4 --- /dev/null +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationResultsComparisonUtils.java @@ -0,0 +1,189 @@ +package gov.nasa.jpl.aerie.scheduler.simulation; + +import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivity; +import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; +import gov.nasa.jpl.aerie.merlin.driver.engine.ProfileSegment; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.RealDynamics; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class SimulationResultsComparisonUtils { + + public static void assertEqualsSimulationResults(final SimulationResults expected, final SimulationResults simulationResults) + { + assertEquals(expected.unfinishedActivities, simulationResults.unfinishedActivities); + assertEquals(expected.topics, simulationResults.topics); + assertEqualsTSA(convertSimulatedActivitiesToTree(expected), convertSimulatedActivitiesToTree(simulationResults)); + final var differencesDiscrete = new HashMap>(); + for(final var discreteProfile: simulationResults.discreteProfiles.entrySet()){ + final var differences = equalsDiscreteProfile(expected.discreteProfiles.get(discreteProfile.getKey()).getRight(), discreteProfile.getValue().getRight()); + if(!differences.isEmpty()){ + differencesDiscrete.put(discreteProfile.getKey(), differences); + } + } + final var differencesReal = new HashMap>(); + for(final var realProfile: simulationResults.realProfiles.entrySet()){ + final var profileElements = realProfile.getValue().getRight(); + final var expectedProfileElements = expected.realProfiles.get(realProfile.getKey()).getRight(); + final var differences = equalsRealProfile(expectedProfileElements, profileElements); + if(!differences.isEmpty()) { + differencesReal.put(realProfile.getKey(), differences); + } + } + if(!differencesDiscrete.isEmpty() || !differencesReal.isEmpty()){ + fail("Differences in real profiles: " + differencesReal + "\n Differences in discrete profiles " + differencesDiscrete); + } + } + + public record RealProfileDifference(ProfileSegment expected, ProfileSegment actual){} + public record DiscreteProfileDifference(ProfileSegment expected, ProfileSegment actual){} + + public static Map equalsRealProfile(List> expected, List> actual){ + final var differences = new HashMap(); + for(int i = 0; i < expected.size(); i++){ + if(!actual.get(i).equals(expected.get(i))){ + differences.put(i, new RealProfileDifference(expected.get(i), actual.get(i))); + } + } + return differences; + } + public static Map equalsDiscreteProfile(List> expected, List> actual){ + final var differences = new HashMap(); + for(int i = 0; i < expected.size(); i++){ + if(!actual.get(i).equals(expected.get(i))){ + differences.put(i, new DiscreteProfileDifference(expected.get(i), actual.get(i))); + } + } + return differences; + } + + /** + * Recursively removes all fields with specific names from a SerializedValue + * @param serializedValue the serialized value + * @param fieldsToRemove the names of the fields to remove + * @return a serialized value without the removed fields + */ + public static SerializedValue removeFieldsFromSerializedValue( + final SerializedValue serializedValue, + final Collection fieldsToRemove){ + final var visitor = new SerializedValue.Visitor(){ + @Override + public SerializedValue onNull() { + return SerializedValue.NULL; + } + + @Override + public SerializedValue onNumeric(final BigDecimal value) { + return SerializedValue.of(value); + } + + @Override + public SerializedValue onBoolean(final boolean value) { + return SerializedValue.of(value); + } + + @Override + public SerializedValue onString(final String value) { + return SerializedValue.of(value); + } + + @Override + public SerializedValue onMap(final Map value) { + final var newVal = new HashMap(); + for(final var entry: value.entrySet()){ + if(!fieldsToRemove.contains(entry.getKey())){ + newVal.put(entry.getKey(), removeFieldsFromSerializedValue(entry.getValue(), fieldsToRemove)); + } + } + return SerializedValue.of(newVal); + } + + @Override + public SerializedValue onList(final List value) { + final var newList = new ArrayList(); + for(final var val : value){ + newList.add(removeFieldsFromSerializedValue(val, fieldsToRemove)); + } + return SerializedValue.of(newList); + } + }; + return serializedValue.match(visitor); + } + + /** + * Converts the activity instances from a SimulationResults object into a set of tree structure representing parent-child activities for comparison purposes + * @param simulationResults the simulation results + * @return a set of trees + */ + public static Set convertSimulatedActivitiesToTree(final SimulationResults simulationResults){ + return simulationResults.simulatedActivities.values().stream().map(simulatedActivity -> TreeSimulatedActivity.fromSimulatedActivity( + simulatedActivity, + simulationResults)).collect(Collectors.toSet()); + } + + /** + * Asserts whether two sets of activity instances are equal. + * @param expected the expected set of activities (as trees) + * @param actual the actual set of activities (as trees) + */ + public static void assertEqualsTSA(final Set expected, + final Set actual){ + assertEquals(expected.size(), actual.size()); + final var copyExpected = new HashSet<>(expected); + for(final var inB: actual){ + if(!copyExpected.contains(inB)){ + fail(); + } + //make sure identical trees are not used to validate twice + copyExpected.remove(inB); + } + } + + // Representation of simulated activities as trees of activities + public record TreeSimulatedActivity(StrippedSimulatedActivity activity, + Set children){ + public static TreeSimulatedActivity fromSimulatedActivity(SimulatedActivity simulatedActivity, SimulationResults simulationResults){ + final var stripped = StrippedSimulatedActivity.fromSimulatedActivity(simulatedActivity); + final HashSet children = new HashSet<>(); + for(final var childId: simulatedActivity.childIds()) { + final var child = fromSimulatedActivity(simulationResults.simulatedActivities.get(childId), simulationResults); + children.add(child); + } + return new TreeSimulatedActivity(stripped, children); + } + } + + //Representation of SimulatedActivity stripped of parent/child/directive id information + //used for comparison purposes + public record StrippedSimulatedActivity( + String type, + Map arguments, + Instant start, + Duration duration, + SerializedValue computedAttributes + ){ + public static StrippedSimulatedActivity fromSimulatedActivity(SimulatedActivity simulatedActivity){ + return new StrippedSimulatedActivity( + simulatedActivity.type(), + simulatedActivity.arguments(), + simulatedActivity.start(), + simulatedActivity.duration(), + simulatedActivity.computedAttributes() + ); + } + } +} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulerAgent.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulerAgent.java index 2b9870b73a..889abf0d2b 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulerAgent.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulerAgent.java @@ -23,6 +23,7 @@ public interface SchedulerAgent { void schedule( ScheduleRequest request, ResultsProtocol.WriterRole writer, - Supplier canceledListener + Supplier canceledListener, + int sizeCachedEngineStore ) throws InterruptedException; } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ThreadedSchedulerAgent.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ThreadedSchedulerAgent.java index 19114b6919..d48cd6ac33 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ThreadedSchedulerAgent.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ThreadedSchedulerAgent.java @@ -25,7 +25,8 @@ private ThreadedSchedulerAgent(final BlockingQueue requestQue public void schedule( final ScheduleRequest request, final ResultsProtocol.WriterRole writer, - final Supplier canceledListener + final Supplier canceledListener, + final int sizeCachedEngineStore ) throws InterruptedException { this.requestQueue.put(new SchedulingRequest.Schedule(request, writer, canceledListener)); @@ -65,7 +66,7 @@ public void run() { if (request instanceof SchedulingRequest.Schedule req) { try { - this.schedulerAgent.schedule(req.request(), req.writer(), req.canceledListener); + this.schedulerAgent.schedule(req.request(), req.writer(), req.canceledListener, 0); } catch (final Throwable ex) { ex.printStackTrace(System.err); req.writer().failWith(b -> b diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java index a782e46611..b5fb6c27d8 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java @@ -99,7 +99,11 @@ public static void main(String[] args) throws Exception { final var revisionData = new SpecificationRevisionData(specificationRevision, planRevision); final ResultsProtocol.WriterRole writer = owner.get(); try { - scheduleAgent.schedule(new ScheduleRequest(specificationId, revisionData), writer, canceledListener); + scheduleAgent.schedule( + new ScheduleRequest(specificationId, revisionData), + writer, + canceledListener, + config.maxCachedSimulationEngines()); } catch (final Throwable ex) { ex.printStackTrace(System.err); writer.failWith(b -> b @@ -133,7 +137,8 @@ private static WorkerAppConfiguration loadConfiguration() { Path.of(getEnv("MERLIN_LOCAL_STORE", "/usr/src/app/merlin_file_store")), Path.of(getEnv("SCHEDULER_RULES_JAR", "/usr/src/app/merlin_file_store/scheduler_rules.jar")), PlanOutputMode.valueOf((getEnv("SCHEDULER_OUTPUT_MODE", "CreateNewOutputPlan"))), - getEnv("HASURA_GRAPHQL_ADMIN_SECRET", "") + getEnv("HASURA_GRAPHQL_ADMIN_SECRET", ""), + Integer.parseInt(getEnv("MAX_NB_CACHED_SIMULATION_ENGINE", "30")) ); } } diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/WorkerAppConfiguration.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/WorkerAppConfiguration.java index 217c0f051b..84b094cfc0 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/WorkerAppConfiguration.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/WorkerAppConfiguration.java @@ -11,5 +11,6 @@ public record WorkerAppConfiguration( Path merlinFileStore, Path missionRuleJarPath, PlanOutputMode outputMode, - String hasuraGraphQlAdminSecret + String hasuraGraphQlAdminSecret, + int maxCachedSimulationEngines ) { } diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java index 9fcec891bc..a600581b02 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java @@ -22,7 +22,9 @@ import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; import gov.nasa.jpl.aerie.merlin.driver.MissionModelLoader; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerPlugin; @@ -66,6 +68,7 @@ import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleResults; import gov.nasa.jpl.aerie.scheduler.server.services.SchedulerAgent; import gov.nasa.jpl.aerie.scheduler.server.services.SpecificationService; +import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import org.apache.commons.collections4.BidiMap; @@ -111,9 +114,10 @@ public record SynchronousSchedulerAgent( public void schedule( final ScheduleRequest request, final ResultsProtocol.WriterRole writer, - final Supplier canceledListener + final Supplier canceledListener, + final int sizeCachedEngineStore ) { - try { + try(final var cachedEngineStore = new InMemoryCachedEngineStore(sizeCachedEngineStore)) { //confirm requested plan to schedule from/into still exists at targeted version (request could be stale) //TODO: maybe some kind of high level db transaction wrapping entire read/update of target plan revision @@ -127,11 +131,16 @@ public void schedule( specification.horizonStartTimestamp().toInstant(), specification.horizonEndTimestamp().toInstant() ); - try(final var simulationFacade = new SimulationFacade( - planningHorizon, + final var simulationFacade = new SimulationFacade( schedulerMissionModel.missionModel(), schedulerMissionModel.schedulerModel(), - canceledListener)) { + cachedEngineStore, + planningHorizon, + new SimulationEngineConfiguration( + planMetadata.modelConfiguration(), + planMetadata.horizon().getStartInstant(), + new MissionModelId(planMetadata.modelId())), + canceledListener); final var problem = new Problem( schedulerMissionModel.missionModel(), planningHorizon, @@ -217,10 +226,10 @@ public void schedule( } problem.setGoals(orderedGoals); - final var scheduler = new PrioritySolver(problem, specification.analysisOnly()); - //run the scheduler to find a solution to the posed problem, if any - final var solutionPlan = scheduler.getNextSolution().orElseThrow( - () -> new ResultsProtocolFailure("scheduler returned no solution")); + final var scheduler = new PrioritySolver(problem, specification.analysisOnly()); + //run the scheduler to find a solution to the posed problem, if any + final var solutionPlan = scheduler.getNextSolution().orElseThrow( + () -> new ResultsProtocolFailure("scheduler returned no solution")); final var activityToGoalId = new HashMap(); for (final var entry : solutionPlan.getEvaluation().getGoalEvaluations().entrySet()) { @@ -239,17 +248,14 @@ public void schedule( activityToGoalId, schedulerMissionModel.schedulerModel() ); - List updatedActs = updateEverythingWithNewAnchorIds(solutionPlan, instancesToIds); - merlinService.updatePlanActivityDirectiveAnchors(specification.planId(), updatedActs, instancesToIds); + List updatedActs = updateEverythingWithNewAnchorIds(solutionPlan, instancesToIds); + merlinService.updatePlanActivityDirectiveAnchors(specification.planId(), updatedActs, instancesToIds); - final var planMetadataAfterChanges = merlinService.getPlanMetadata(specification.planId()); + final var planMetadataAfterChanges = merlinService.getPlanMetadata(specification.planId()); final var datasetId = storeSimulationResults(planningHorizon, simulationFacade, planMetadataAfterChanges, instancesToIds); //collect results and notify subscribers of success final var results = collectResults(solutionPlan, instancesToIds, goals); writer.succeedWith(results, datasetId); - } catch (SchedulingInterruptedException e) { - writer.reportCanceled(e); - } } catch (final SpecificationLoadException e) { writer.failWith(b -> b .type("SPECIFICATION_LOAD_EXCEPTION") @@ -283,6 +289,13 @@ public void schedule( .type("IO_EXCEPTION") .message(e.toString()) .trace(e)); + } catch (SchedulingInterruptedException e) { + writer.reportCanceled(e); + } catch (Exception e) { + writer.failWith(b -> b + .type("OTHER_EXCEPTION") + .message(e.toString()) + .trace(e)); } } @@ -318,36 +331,38 @@ private ExternalProfiles loadExternalProfiles(final PlanId planId) return merlinService.getExternalProfiles(planId); } - private Optional storeSimulationResults(PlanningHorizon planningHorizon, SimulationFacade simulationFacade, PlanMetadata planMetadata, - final Map schedDirectiveToMerlinId) - throws MerlinServiceException, IOException, SchedulingInterruptedException { - if(!simulationFacade.areInitialSimulationResultsStale()) return Optional.empty(); + private Optional storeSimulationResults( + final Plan plan, + PlanningHorizon planningHorizon, + SimulationFacade simulationFacade, + PlanMetadata planMetadata, + final Map schedDirectiveToMerlinId) + throws MerlinServiceException, IOException, SchedulingInterruptedException + { //finish simulation until end of horizon before posting results try { - simulationFacade.computeSimulationResultsUntil(planningHorizon.getEndAerie()); + final var simulationData = simulationFacade.simulateWithResults(plan, planningHorizon.getEndAerie()); + final var schedID_to_MerlinID = + schedDirectiveToMerlinId.entrySet().stream() + .collect(Collectors.toMap( + (a) -> new SchedulingActivityDirectiveId(a.getKey().id().id()), Map.Entry::getValue)); + final var schedID_to_simID = + simulationData.mapping(); + final var simID_to_MerlinID = + schedID_to_simID.entrySet().stream().collect(Collectors.toMap( + Map.Entry::getValue, + (a) -> schedID_to_MerlinID.get(a.getKey()))); + if(simID_to_MerlinID.values().containsAll(schedDirectiveToMerlinId.values()) && schedDirectiveToMerlinId.values().containsAll(simID_to_MerlinID.values())){ + return Optional.of(merlinService.storeSimulationResults(planMetadata, + simulationData.driverResults(), + simID_to_MerlinID)); + } else{ + //schedule in simulation is inconsistent with current state of the plan (user probably disabled simulation for some of the goals) + return Optional.empty(); + } } catch (SimulationFacade.SimulationException e) { throw new RuntimeException("Error while running simulation before storing simulation results after scheduling", e); } - final var schedID_to_MerlinID = - schedDirectiveToMerlinId.entrySet().stream() - .collect(Collectors.toMap( - (a) -> new SchedulingActivityDirectiveId(a.getKey().id().id()), Map.Entry::getValue)); - final var temp_SchedID_to_simID = simulationFacade.getBidiActivityIdCorrespondence(); - if(temp_SchedID_to_simID.isEmpty()) - return Optional.empty(); - final var schedID_to_simID = new HashMap<>(temp_SchedID_to_simID.get()); - final var simID_to_MerlinID = - schedID_to_simID.entrySet().stream().collect(Collectors.toMap( - Map.Entry::getValue, - (a) -> schedID_to_MerlinID.get(a.getKey()))); - if(simID_to_MerlinID.values().containsAll(schedDirectiveToMerlinId.values()) && schedDirectiveToMerlinId.values().containsAll(simID_to_MerlinID.values())){ - return Optional.of(merlinService.storeSimulationResults(planMetadata, - simulationFacade.getLatestDriverSimulationResults().get(), - simID_to_MerlinID)); - } else{ - //schedule in simulation is inconsistent with current state of the plan (user probably disabled simulation for some of the goals) - return Optional.empty(); - } } private static SchedulingDSLCompilationService.SchedulingDSLCompilationResult compileGoalDefinition( diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java index 6a94206f25..ca1ce8ad30 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java @@ -58,6 +58,7 @@ import gov.nasa.jpl.aerie.scheduler.model.Plan; import gov.nasa.jpl.aerie.scheduler.server.services.SpecificationService; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; @@ -144,7 +145,7 @@ export default () => Goal.ActivityRecurrenceGoal({ """, true)), PLANNING_HORIZON); fail(); } - catch (IllegalArgumentException e) { + catch (AssertionError e) { assertTrue(e.getMessage().contains("Duration passed to RecurrenceGoal as the goal's minimum recurrence interval cannot be negative!")); } catch (Exception e) { @@ -303,7 +304,6 @@ export default () => Goal.CoexistenceGoal({ assertEquals(Duration.of(5, Duration.HOUR), peelBanana.startOffset()); } - @Test void testSingleActivityPlanSimpleRecurrenceGoal() { final var results = runScheduler( @@ -466,7 +466,7 @@ void testCoexistenceGoalWithAnchors() { ), List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ - persistentAnchor: PersistentTimeAnchor.START, + persistentAnchor: PersistentTimeAnchor.START, forEach: ActivityExpression.ofType(ActivityTypes.BiteBanana), activityFinder: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: (interval) => ActivityTemplates.GrowBanana({quantity: 10, growingDuration: Temporal.Duration.from({minutes:1}) }), @@ -486,6 +486,50 @@ export default () => Goal.CoexistenceGoal({ } } + @Test + void testCoexistenceGoalWithAnchorsCreation() { + final var results = runScheduler( + BANANANATION, + List.of( + new ActivityDirective( + Duration.ZERO, + "BiteBanana", + Map.of("biteSize", SerializedValue.of(1)), + null, + true + ) + ), + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ + export default () => Goal.CoexistenceGoal({ + persistentAnchor: PersistentTimeAnchor.START, + forEach: ActivityExpression.ofType(ActivityTypes.BiteBanana), + activityFinder: ActivityExpression.ofType(ActivityTypes.GrowBanana), + activityTemplate: (interval) => ActivityTemplates.GrowBanana({quantity: 10, growingDuration: Temporal.Duration.from({minutes:1}) }), + startsAt: TimingConstraint.singleton(WindowProperty.END).plus(Temporal.Duration.from({ minutes : 5})) + }) + """, true)), + PLANNING_HORIZON); + + assertEquals(1, results.scheduleResults.goalResults().size()); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); + + assertTrue(goalResult.satisfied()); + assertEquals(1, goalResult.createdActivities().size()); + assertEquals(1, goalResult.satisfyingActivities().size()); + for (final var activity : goalResult.satisfyingActivities()) { + assertNotNull(activity); + } + + final var planByActivityType = partitionByActivityType(results.updatedPlan); + final var allBiteBanana = planByActivityType.get("BiteBanana"); + for(final var activity : planByActivityType.get("GrowBanana")){ + assertNotNull(activity.anchorId()); + final var reference = results.idToAct().get(activity.anchorId()); + assertTrue(allBiteBanana.contains(reference)); + allBiteBanana.remove(reference); + } + } + @Test void testCoexistencePartialActWithParameter() { final var expectedSatisfactionAct = new ActivityDirective( @@ -1579,6 +1623,7 @@ private static List onePickEveryTenMinutes(final Interval int } @Test + @Disabled void testBigCoexistence(){ final var growBananaDuration = Duration.of(1, Duration.HOUR); final var results = runScheduler( @@ -2029,7 +2074,16 @@ private SchedulingRunResults runScheduler( final MissionModelDescription desc, final List plannedActivities, final Iterable goals, - final PlanningHorizon planningHorizon + final PlanningHorizon planningHorizon){ + return runScheduler(desc, plannedActivities, goals, planningHorizon, 30); + } + + private SchedulingRunResults runScheduler( + final MissionModelDescription desc, + final List plannedActivities, + final Iterable goals, + final PlanningHorizon planningHorizon, + final int cachedEngineStoreCapacity ) { final var activities = new HashMap(); @@ -2037,7 +2091,7 @@ private SchedulingRunResults runScheduler( for (final var activityDirective : plannedActivities) { activities.put(new ActivityDirectiveId(id++), activityDirective); } - return runScheduler(desc, activities, goals, List.of(), planningHorizon, Optional.empty()); + return runScheduler(desc, activities, goals, List.of(), planningHorizon, Optional.empty(), cachedEngineStoreCapacity); } private SchedulingRunResults runScheduler( @@ -2047,7 +2101,7 @@ private SchedulingRunResults runScheduler( final PlanningHorizon planningHorizon ) { - return runScheduler(desc, plannedActivities, goals, List.of(), planningHorizon, Optional.empty()); + return runScheduler(desc, plannedActivities, goals, List.of(), planningHorizon, Optional.empty(), 30); } private SchedulingRunResults runScheduler( @@ -2073,7 +2127,7 @@ private SchedulingRunResults runScheduler( for (final var activityDirective : plannedActivities) { activities.put(new ActivityDirectiveId(id++), activityDirective); } - return runScheduler(desc, activities, goals, globalSchedulingConditions, planningHorizon, externalProfiles); + return runScheduler(desc, activities, goals, globalSchedulingConditions, planningHorizon, externalProfiles, 30); } private SchedulingRunResults runScheduler( @@ -2082,7 +2136,8 @@ private SchedulingRunResults runScheduler( final Iterable goals, final List globalSchedulingConditions, final PlanningHorizon planningHorizon, - final Optional externalProfiles + final Optional externalProfiles, + final int cachedEngineStoreCapacity ) { final var mockMerlinService = new MockMerlinService(); mockMerlinService.setMissionModel(getMissionModelInfo(desc)); @@ -2115,7 +2170,7 @@ private SchedulingRunResults runScheduler( schedulingDSLCompiler); // Scheduling Goals -> Scheduling Specification final var writer = new MockResultsProtocolWriter(); - agent.schedule(new ScheduleRequest(new SpecificationId(1L), new SpecificationRevisionData(1L, 1L)), writer, () -> false); + agent.schedule(new ScheduleRequest(new SpecificationId(1L), new SpecificationRevisionData(1L, 1L)), writer, () -> false, cachedEngineStoreCapacity); assertEquals(1, writer.results.size()); final var result = writer.results.get(0); if (result instanceof MockResultsProtocolWriter.Result.Failure e) { @@ -2123,10 +2178,18 @@ private SchedulingRunResults runScheduler( System.err.println(serializedReason); fail(serializedReason); } - return new SchedulingRunResults(((MockResultsProtocolWriter.Result.Success) result).results(), mockMerlinService.updatedPlan, mockMerlinService.plan, plannedActivities); + return new SchedulingRunResults( + ((MockResultsProtocolWriter.Result.Success) result).results(), + mockMerlinService.updatedPlan, + mockMerlinService.plan, + plannedActivities); } - record SchedulingRunResults(ScheduleResults scheduleResults, Collection updatedPlan, Plan plan, Map idToAct) {} + record SchedulingRunResults( + ScheduleResults scheduleResults, + Collection updatedPlan, + Plan plan, + Map idToAct) {} static MerlinService.MissionModelTypes loadMissionModelTypesFromJar( final String jarPath, @@ -2362,12 +2425,7 @@ export default function myGoal() { planningHorizon); final var planByActivityType = partitionByActivityType(results.updatedPlan()); final var biteBanana = planByActivityType.get("BiteBanana").stream().map((bb) -> bb.startOffset()).toList(); - final var childs = planByActivityType.get("child"); - assertEquals(childs.size(), biteBanana.size()); - assertEquals(childs.size(), 2); - for(final var childAct: childs){ - assertTrue(biteBanana.contains(childAct.startOffset())); - } + assertEquals(biteBanana.size(), 2); } @Test @@ -3104,69 +3162,6 @@ export default () => Goal.CoexistenceGoal({ assertEquals(SerializedValue.of(2), growBanana2.serializedActivity().getArguments().get("quantity")); } - /** - * Test the option to turn off simulation in between goals. - * - * Goal 0 places `PeelBanana`s. Goal 1 looks for `PeelBanana`s and places `BananaNap`s. - * If it doesn't resimulate in between, Goal 1 should place no activities. - * If it does resimulate, it should place one activity. - * - * Both options are tested here. - */ - @Test - void testOptionalSimulationAfterGoal_unsimulatedActivities() { - final var activityDuration = Duration.of(1, Duration.HOUR); - final var configs = Map.of( - false, 0, // don't simulate, expect 0 activities - true, 1 // do simulate, expect 1 activity - ); - for (final var config: configs.entrySet()) { - final var results = runScheduler( - BANANANATION, - Map.of( - new ActivityDirectiveId(1L), - new ActivityDirective( - Duration.ZERO, - "GrowBanana", - Map.of( - "quantity", SerializedValue.of(1), - "growingDuration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))), - null, - true) - ), - List.of( - new SchedulingGoal(new GoalId(0L, 0L), """ - export default () => Goal.CoexistenceGoal({ - forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), - activityTemplate: ActivityTemplates.BananaNap(), - startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes: 5 })) - }) - """, true, config.getKey() - ), - new SchedulingGoal(new GoalId(1L, 0L), """ - export default () => Goal.CoexistenceGoal({ - forEach: ActivityExpression.ofType(ActivityTypes.BananaNap), - activityTemplate: ActivityTemplates.DownloadBanana({connection: "DSL"}), - startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes: 5 })) - }) - """, true, true) - ), - PLANNING_HORIZON); - - assertEquals(2, results.scheduleResults.goalResults().size()); - final var goalResult1 = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); - final var goalResult2 = results.scheduleResults.goalResults().get(new GoalId(1L, 0L)); - - assertTrue(goalResult1.satisfied()); - assertTrue(goalResult2.satisfied()); - assertEquals(1, goalResult1.createdActivities().size()); - assertEquals(config.getValue(), goalResult2.createdActivities().size()); - for (final var activity : goalResult1.createdActivities()) { - assertNotNull(activity); - } - } - } - /** * Test the option to turn off simulation in between goals. * @@ -3200,7 +3195,7 @@ void testOptionalSimulationAfterGoal_staleResources() { List.of( new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ - forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), + forEach: Real.Resource("/fruit").greaterThan(4), activityTemplate: ActivityTemplates.DownloadBanana({connection: "DSL"}), startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes: 5 })) }) From d04cc85637a82464a1d5f94234b14f0efe72da64 Mon Sep 17 00:00:00 2001 From: maillard Date: Wed, 7 Feb 2024 11:20:05 -0800 Subject: [PATCH 26/43] Update e2e tests --- .../nasa/jpl/aerie/e2e/SchedulingTests.java | 35 +++++++++---------- .../aerie/e2e/types/SimulationDataset.java | 6 ++-- .../gov/nasa/jpl/aerie/e2e/utils/GQL.java | 2 ++ 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java index 43f9f4486d..4b2b45de67 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java @@ -6,6 +6,7 @@ import gov.nasa.jpl.aerie.e2e.types.Plan; import gov.nasa.jpl.aerie.e2e.types.ProfileSegment; import gov.nasa.jpl.aerie.e2e.types.SchedulingRequest.SchedulingStatus; +import gov.nasa.jpl.aerie.e2e.types.SimulationDataset; import gov.nasa.jpl.aerie.e2e.types.ValueSchema; import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; import gov.nasa.jpl.aerie.e2e.utils.HasuraRequests; @@ -381,22 +382,17 @@ void schedulingGoalPostsSimResults() throws IOException { final var plan = hasura.getPlan(planId); final var simResults = hasura.getSimulationDatasetByDatasetId(datasetId); - // All directive have their simulated activity - final var planActivities = plan.activityDirectives(); - final var simActivities = simResults.activities(); - assertEquals(4, planActivities.size()); - assertEquals(planActivities.size(), simActivities.size()); - for(int i = 0; i activities) { + List activities, + Integer datasetId) { public record SimulatedActivity( int spanId, Integer directiveId, @@ -58,7 +59,8 @@ public static SimulationDataset fromJSON(JsonObject json) { json.getBoolean("canceled"), json.getString("simulation_start_time"), json.getString("simulation_end_time"), - simActivities); + simActivities, + json.getInt("dataset_id")); } public enum SimulationStatus{ pending, incomplete, failed, success } diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java index 08079cc25b..ed7105974d 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java @@ -434,6 +434,7 @@ query GetSimulationDataset($id: Int!) { status reason canceled + dataset_id simulation_start_time simulation_end_time simulated_activities { @@ -453,6 +454,7 @@ query GetSimulationDataset($id: Int!) { status reason canceled + dataset_id simulation_start_time simulation_end_time simulated_activities { From ed0b88bfe6fed9ca9cf964b70113f9dd4b809bf8 Mon Sep 17 00:00:00 2001 From: maillard Date: Thu, 8 Feb 2024 15:39:10 -0800 Subject: [PATCH 27/43] Close the last interval of constraint simulation results When we exit the simulation at time t, the engine has finished all work at time t and this has updated the value of resources. Consequently, the last interval should not be closed/open but closed/closed to be consistent with the state of the simulation engine. --- .../aerie/constraints/model/DiscreteProfile.java | 12 ++++++++---- .../jpl/aerie/constraints/model/LinearProfile.java | 13 ++++++++----- .../merlin/server/services/ConstraintAction.java | 2 +- .../simulation/SimulationResultsConverter.java | 2 +- .../jpl/aerie/scheduler/SimulationFacadeTest.java | 11 +++++------ 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/DiscreteProfile.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/DiscreteProfile.java index 57e0b0abb9..1de8d492c0 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/DiscreteProfile.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/DiscreteProfile.java @@ -144,33 +144,37 @@ public Optional valueAt(final Duration timepoint) { } public static DiscreteProfile fromSimulatedProfile(final List> simulatedProfile) { - return fromProfileHelper(Duration.ZERO, simulatedProfile, Optional::of); + return fromProfileHelper(Duration.ZERO, simulatedProfile, Optional::of, true); } public static DiscreteProfile fromExternalProfile(final Duration offsetFromPlanStart, final List>> externalProfile) { - return fromProfileHelper(offsetFromPlanStart, externalProfile, $ -> $); + return fromProfileHelper(offsetFromPlanStart, externalProfile, $ -> $, false); } private static DiscreteProfile fromProfileHelper( final Duration offsetFromPlanStart, final List> profile, - final Function> transform + final Function> transform, + final boolean close ) { final var result = new IntervalMap.Builder(); var cursor = offsetFromPlanStart; + var c = 0; for (final var pair: profile) { final var nextCursor = cursor.plus(pair.extent()); final var value = transform.apply(pair.dynamics()); final Duration finalCursor = cursor; + final var isLast = c == profile.size() - 1; value.ifPresent( $ -> result.set( - Interval.between(finalCursor, Inclusive, nextCursor, Exclusive), + Interval.between(finalCursor, Inclusive, nextCursor, (close && isLast) ? Inclusive : Exclusive), $ ) ); cursor = nextCursor; + c++; } return new DiscreteProfile(result.build()); diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/LinearProfile.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/LinearProfile.java index fb5b75e7d5..1bedf03ca2 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/LinearProfile.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/LinearProfile.java @@ -175,28 +175,30 @@ public LinearProfile shiftBy(final Duration duration) { } public static LinearProfile fromSimulatedProfile(final List> simulatedProfile) { - return fromProfileHelper(Duration.ZERO, simulatedProfile, Optional::of); + return fromProfileHelper(Duration.ZERO, simulatedProfile, Optional::of, true); } public static LinearProfile fromExternalProfile(final Duration offsetFromPlanStart, final List>> externalProfile) { - return fromProfileHelper(offsetFromPlanStart, externalProfile, $ -> $); + return fromProfileHelper(offsetFromPlanStart, externalProfile, $ -> $, false); } private static LinearProfile fromProfileHelper( final Duration offsetFromPlanStart, final List> profile, - final Function> transform + final Function> transform, + final boolean close ) { final var result = new IntervalMap.Builder(); var cursor = offsetFromPlanStart; + var c = 0; for (final var pair: profile) { final var nextCursor = cursor.plus(pair.extent()); - + final var isLast = c == profile.size() - 1; final var value = transform.apply(pair.dynamics()); final Duration finalCursor = cursor; value.ifPresent( $ -> result.set( - Interval.between(finalCursor, Inclusive, nextCursor, Exclusive), + Interval.between(finalCursor, Inclusive, nextCursor, (close && isLast) ? Inclusive :Exclusive), new LinearEquation( finalCursor, $.initial, @@ -206,6 +208,7 @@ private static LinearProfile fromProfileHelper( ); cursor = nextCursor; + c++; } return new LinearProfile(result.build()); diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java index c0de789a34..cdcf464832 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java @@ -212,7 +212,7 @@ public Map> getViolations(final PlanId planId, final Opt } } - final Interval bounds = Interval.betweenClosedOpen(Duration.ZERO, simDuration); + final Interval bounds = Interval.between(Duration.ZERO, simDuration); final var preparedResults = new gov.nasa.jpl.aerie.constraints.model.SimulationResults( simStartTime, bounds, diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationResultsConverter.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationResultsConverter.java index 0be3a6cead..9e253315be 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationResultsConverter.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationResultsConverter.java @@ -28,7 +28,7 @@ public static gov.nasa.jpl.aerie.constraints.model.SimulationResults convertToCo .collect(Collectors.toList()); return new gov.nasa.jpl.aerie.constraints.model.SimulationResults( driverResults.startTime, - Interval.betweenClosedOpen(Duration.ZERO, driverResults.duration), + Interval.between(Duration.ZERO, driverResults.duration), activities, Maps.transformValues(driverResults.realProfiles, $ -> LinearProfile.fromSimulatedProfile($.getRight())), Maps.transformValues(driverResults.discreteProfiles, $ -> DiscreteProfile.fromSimulatedProfile($.getRight())) diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationFacadeTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationFacadeTest.java index 5d23080cc0..02f1fb6e40 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationFacadeTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationFacadeTest.java @@ -31,7 +31,6 @@ import static gov.nasa.jpl.aerie.constraints.time.Interval.Inclusivity.Exclusive; import static gov.nasa.jpl.aerie.constraints.time.Interval.Inclusivity.Inclusive; import static gov.nasa.jpl.aerie.constraints.time.Interval.interval; -import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECONDS; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -171,7 +170,7 @@ public void whenValueAboveDoubleOnSimplePlan() var actual = new GreaterThan(getFruitRes(), new RealValue(2.9)).evaluate(results.constraintsResults()); var expected = new Windows( Segment.of(interval(0, Inclusive, 2, Exclusive, SECONDS), true), - Segment.of(interval(2, Inclusive,5, Exclusive, SECONDS), false) + Segment.of(interval(2, Inclusive,5, Inclusive, SECONDS), false) ); assertEquals(expected, actual); } @@ -184,7 +183,7 @@ public void whenValueBelowDoubleOnSimplePlan() var actual = new LessThan(getFruitRes(), new RealValue(3.0)).evaluate(results.constraintsResults()); var expected = new Windows( Segment.of(interval(0, Inclusive, 2, Exclusive, SECONDS), false), - Segment.of(interval(2, Inclusive, 5, Exclusive, SECONDS), true) + Segment.of(interval(2, Inclusive, 5, Inclusive, SECONDS), true) ); assertEquals(expected, actual); } @@ -198,7 +197,7 @@ public void whenValueBetweenDoubleOnSimplePlan() var expected = new Windows( Segment.of(interval(0, Inclusive, 1, Exclusive, SECONDS), false), Segment.of(interval(1, Inclusive, 2, Exclusive, SECONDS), true), - Segment.of(interval(2, Inclusive, 5, Exclusive, SECONDS), false) + Segment.of(interval(2, Inclusive, 5, Inclusive, SECONDS), false) ); assertEquals(expected, actual); } @@ -212,7 +211,7 @@ public void whenValueEqualDoubleOnSimplePlan() var expected = new Windows( Segment.of(interval(0, Inclusive, 1, Exclusive, SECONDS), false), Segment.of(interval(1, Inclusive, 2, Exclusive, SECONDS), true), - Segment.of(interval(2, Inclusive, 5, Exclusive, SECONDS), false) + Segment.of(interval(2, Inclusive, 5, Inclusive, SECONDS), false) ); assertEquals(expected, actual); } @@ -226,7 +225,7 @@ public void whenValueNotEqualDoubleOnSimplePlan() var expected = new Windows( Segment.of(interval(0, Inclusive, 1, Exclusive, SECONDS), true), Segment.of(interval(1, Inclusive, 2, Exclusive, SECONDS), false), - Segment.of(interval(2, Inclusive, 5, Exclusive, SECONDS), true) + Segment.of(interval(2, Inclusive, 5, Inclusive, SECONDS), true) ); assertEquals(expected, actual); } From 1fe93886c49f2055303511d69e504446bbb1e754 Mon Sep 17 00:00:00 2001 From: maillard Date: Thu, 8 Feb 2024 16:14:14 -0800 Subject: [PATCH 28/43] Mock simulation results if no resources are needed --- .../services/LocalMissionModelService.java | 11 +-- .../scheduler/model/SchedulePlanGrounder.java | 72 ++++++++++++++ .../model/SchedulingActivityDirective.java | 11 +++ .../scheduler/solver/PrioritySolver.java | 98 +++++++++++++------ .../scheduler/SchedulingGrounderTest.java | 96 ++++++++++++++++++ 5 files changed, 249 insertions(+), 39 deletions(-) create mode 100644 scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulePlanGrounder.java create mode 100644 scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SchedulingGrounderTest.java diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java index 2e8deca06e..bd71ea6a4e 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java @@ -1,17 +1,12 @@ package gov.nasa.jpl.aerie.merlin.server.services; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; -import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.DirectiveTypeRegistry; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; import gov.nasa.jpl.aerie.merlin.driver.MissionModelLoader; import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; -import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; -import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; -import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.InputType.Parameter; import gov.nasa.jpl.aerie.merlin.protocol.model.InputType.ValidationNotice; import gov.nasa.jpl.aerie.merlin.protocol.model.ModelType; @@ -21,7 +16,6 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.jpl.aerie.merlin.server.models.ActivityDirectiveForValidation; import gov.nasa.jpl.aerie.merlin.server.models.ActivityType; -import gov.nasa.jpl.aerie.merlin.server.models.Constraint; import gov.nasa.jpl.aerie.merlin.server.models.MissionModelId; import gov.nasa.jpl.aerie.merlin.server.models.MissionModelJar; import gov.nasa.jpl.aerie.merlin.server.remotes.MissionModelRepository; @@ -37,8 +31,8 @@ import java.util.Map; import java.util.Optional; import java.util.function.Consumer; -import java.util.stream.Collectors; import java.util.function.Supplier; +import java.util.stream.Collectors; /** * Implements the missionModel service {@link MissionModelService} interface on a set of local domain objects. @@ -283,9 +277,6 @@ public Map getModelEffectiveArguments(final String miss .getEffectiveArguments(arguments); } - static Map> missionModelCache = new HashMap<>(); - static Map> cachedEngines = new HashMap<>(); - /** * Validate that a set of activity parameters conforms to the expectations of a named mission model. * diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulePlanGrounder.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulePlanGrounder.java new file mode 100644 index 0000000000..7cf9e621cb --- /dev/null +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulePlanGrounder.java @@ -0,0 +1,72 @@ +package gov.nasa.jpl.aerie.scheduler.model; + +import gov.nasa.jpl.aerie.constraints.model.ActivityInstance; +import gov.nasa.jpl.aerie.constraints.time.Interval; +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; +import gov.nasa.jpl.aerie.merlin.driver.StartOffsetReducer; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class SchedulePlanGrounder { + public static Optional> groundSchedule( + final List schedulingActivityDirectiveList, + final Duration planDuration + ){ + final var grounded = new HashMap(); + + final var idMap = schedulingActivityDirectiveList + .stream() + .map(a -> Pair.of(new ActivityDirectiveId(a.getId().id()), a)) + .collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); + + final var converted = schedulingActivityDirectiveList + .stream() + .map(a -> Pair.of( + new ActivityDirectiveId(a.id().id()), + new ActivityDirective( + a.startOffset(), + a.type().getName(), + a.arguments(), + (a.anchorId() == null) ? null : new ActivityDirectiveId(a.anchorId().id()), + a.anchoredToStart() + ))) + .collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); + final var converter = new StartOffsetReducer(planDuration, converted); + var computed = converter.compute(); + computed = StartOffsetReducer.filterOutNegativeStartOffset(computed); + + for(final var directive: computed.entrySet()){ + Duration offset = Duration.ZERO; + final var idActivity = directive.getKey(); + if(idActivity != null){ + if(grounded.get(idActivity) == null){ + return Optional.empty(); + } else { + final var alreadyGroundedAct = grounded.get(idActivity); + offset = alreadyGroundedAct.interval.end; + } + } + for(final Pair dependentDirective : directive.getValue()) { + final var dependentId = dependentDirective.getKey(); + final var dependentOriginalActivity = idMap.get(dependentId); + final var startTime = offset.plus(dependentDirective.getValue()); + //happens only in tests + if(dependentOriginalActivity.duration() == null){ + return Optional.empty(); + } + grounded.put(dependentId, new ActivityInstance( + dependentId.id(), + dependentOriginalActivity.type().getName(), + dependentOriginalActivity.arguments(), + Interval.between(startTime, startTime.plus(dependentOriginalActivity.duration())))); + } + } + return Optional.of(grounded.values().stream().toList()); + } +} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingActivityDirective.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingActivityDirective.java index ee8e2b275c..22b56018f1 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingActivityDirective.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingActivityDirective.java @@ -85,6 +85,17 @@ public static SchedulingActivityDirective of(ActivityType type, Duration startOf Map.of(), null, anchorId, anchoredToStart); } + public static SchedulingActivityDirective of(SchedulingActivityDirectiveId id, ActivityType type, Duration startOffset, Duration duration, SchedulingActivityDirectiveId anchorId, boolean anchoredToStart) { + return new SchedulingActivityDirective( + id, + type, + startOffset, + duration, + Map.of(), + null, + anchorId, + anchoredToStart); + } public static SchedulingActivityDirective of(ActivityType type, Duration startOffset, Duration duration, Map parameters, SchedulingActivityDirectiveId anchorId, boolean anchoredToStart) { return new SchedulingActivityDirective(new SchedulingActivityDirectiveId(uniqueId.getAndIncrement()), type, diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java index 0115ea68cb..c0bac6d815 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java @@ -6,7 +6,6 @@ import gov.nasa.jpl.aerie.constraints.time.Segment; import gov.nasa.jpl.aerie.constraints.time.Windows; import gov.nasa.jpl.aerie.constraints.tree.Expression; -import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; @@ -27,11 +26,12 @@ import gov.nasa.jpl.aerie.scheduler.model.Plan; import gov.nasa.jpl.aerie.scheduler.model.PlanInMemory; import gov.nasa.jpl.aerie.scheduler.model.Problem; +import gov.nasa.jpl.aerie.scheduler.model.SchedulePlanGrounder; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; -import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; +import gov.nasa.jpl.aerie.scheduler.simulation.SimulationData; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.stn.TaskNetworkAdapter; -import org.apache.commons.collections4.BidiMap; +import org.apache.commons.collections4.bidimap.DualHashBidiMap; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,6 +41,7 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -62,7 +63,9 @@ public class PrioritySolver implements Solver { private boolean checkSimBeforeEvaluatingGoal; - private SimulationResults lastSimulationResults; + private boolean atLeastOneSimulateAfter; + + private SimulationData cachedSimulationResultsBeforeGoalEvaluation; /** * boolean stating whether only conflict analysis should be performed or not @@ -136,6 +139,7 @@ public PrioritySolver(final Problem problem, final boolean analysisOnly) { checkNotNull(problem, "creating solver with null input problem descriptor"); this.checkSimBeforeInsertingActivities = true; this.checkSimBeforeEvaluatingGoal = true; + this.atLeastOneSimulateAfter = false; this.problem = problem; this.simulationFacade = problem.getSimulationFacade(); this.analysisOnly = analysisOnly; @@ -189,7 +193,7 @@ public record InsertActivityResult(boolean success, List acts, boolean actsFromInitialPlan) throws SchedulingInterruptedException{ + private InsertActivityResult checkAndInsertActs(Collection acts) throws SchedulingInterruptedException{ // TODO: When anchors are allowed to be added by Scheduling goals, inserting the new activities one at a time should be reconsidered boolean allGood = true; logger.info("Inserting new activities in the plan to check plan validity"); @@ -205,7 +209,7 @@ private InsertActivityResult checkAndInsertActs(Collection getGoalQueue() { final var rawGoals = problem.getGoals(); assert rawGoals != null; + this.atLeastOneSimulateAfter = rawGoals.stream().filter(g -> g.simulateAfter).findFirst().isPresent(); + //create queue container using comparator and pre-sized for all goals final var capacity = rawGoals.size(); assert capacity >= 0; @@ -340,7 +346,7 @@ private void satisfyOptionGoal(OptionGoal goal) throws SchedulingInterruptedExce //we do not care about ownership here as it is not really a piggyback but just the validation of the supergoal plan.getEvaluation().forGoal(goal).associate(act, false); } - final var insertionResult = checkAndInsertActs(actsToInsert, false); + final var insertionResult = checkAndInsertActs(actsToInsert); if(insertionResult.success()) { for(var act: insertionResult.activitiesInserted()){ plan.getEvaluation().forGoal(goal).associate(act, false); @@ -482,7 +488,7 @@ private void satisfyGoalGeneral(Goal goal) throws SchedulingInterruptedException //add the activities to the output plan if (!acts.isEmpty()) { logger.info("Found activity to satisfy missing activity instance conflict"); - final var insertionResult = checkAndInsertActs(acts, false); + final var insertionResult = checkAndInsertActs(acts); if(insertionResult.success){ plan.getEvaluation().forGoal(goal).associate(insertionResult.activitiesInserted(), true); itConflicts.remove(); @@ -507,7 +513,7 @@ else if(!analysisOnly && (missing instanceof MissingActivityTemplateConflict mi //add the activities to the output plan if (!acts.isEmpty()) { logger.info("Found activity to satisfy missing activity template conflict"); - final var insertionResult = checkAndInsertActs(acts, false); + final var insertionResult = checkAndInsertActs(acts); if(insertionResult.success()){ plan.getEvaluation().forGoal(goal).associate(insertionResult.activitiesInserted(), true); @@ -554,9 +560,9 @@ else if(!analysisOnly && (missing instanceof MissingActivityTemplateConflict mi missingAssociationConflict.getAnchorToStart().get(), startOffset ); - replaceActivity(act,replacementAct); + plan.replace(act,replacementAct); //decision-making here, we choose the first satisfying activity - evaluation.forGoal(goal).associate(replacementAct, false); + plan.getEvaluation().forGoal(goal).associate(replacementAct, false); itConflicts.remove(); logger.info("Activity " + replacementAct + " has been associated to goal " + goal.getName() +" to satisfy conflict " + i); break; @@ -610,10 +616,14 @@ private Collection getConflicts(Goal goal) throws SchedulingInterrupte logger.debug("Computing simulation results until "+ this.problem.getPlanningHorizon().getEndAerie() + " (planning horizon end) in order to compute conflicts"); final var resources = new HashSet(); goal.extractResources(resources); - this.getLatestSimResultsUpTo(this.problem.getPlanningHorizon().getEndAerie(), resources); + final var simulationResults = this.getLatestSimResultsUpTo(this.problem.getPlanningHorizon().getEndAerie(), resources); final var evaluationEnvironment = new EvaluationEnvironment(this.problem.getRealExternalProfiles(), this.problem.getDiscreteExternalProfiles()); - final Optional> mapSchedulingIdsToActivityIds = simulationFacade.getBidiActivityIdCorrespondence(); - final var rawConflicts = goal.getConflicts(plan, lastSimulationResults, mapSchedulingIdsToActivityIds, evaluationEnvironment, this.problem.getSchedulerModel()); + final var rawConflicts = goal.getConflicts( + plan, + simulationResults.constraintsResults(), + simulationResults.mapSchedulingIdsToActivityIds(), + evaluationEnvironment, + this.problem.getSchedulerModel()); assert rawConflicts != null; return rawConflicts; } @@ -792,7 +802,7 @@ private Windows narrowByResourceConstraints( final var evaluationEnvironment = new EvaluationEnvironment(this.problem.getRealExternalProfiles(), this.problem.getDiscreteExternalProfiles()); for (final var constraint : constraints) { //REVIEW: loop through windows more efficient than enveloppe(windows) ? - final var validity = constraint.evaluate(latestSimulationResults, totalDomain, evaluationEnvironment); + final var validity = constraint.evaluate(latestSimulationResults.constraintsResults(), totalDomain, evaluationEnvironment); ret = ret.and(validity); //short-circuit if no possible windows left if (ret.stream().noneMatch(Segment::value)) { @@ -802,14 +812,45 @@ private Windows narrowByResourceConstraints( return ret; } - - private SimulationResults getLatestSimResultsUpTo(final Duration time, final Set resourceNames) throws SchedulingInterruptedException { + private SimulationData getLatestSimResultsUpTo(final Duration time, final Set resourceNames) throws SchedulingInterruptedException { + //if no resource is needed, build the results from the current plan + if(resourceNames.isEmpty()) { + final var groundedPlan = SchedulePlanGrounder.groundSchedule( + this.plan.getActivities().stream().toList(), + this.problem.getPlanningHorizon().getEndAerie()); + if (groundedPlan.isPresent()) { + return new SimulationData( + plan, + null, + new SimulationResults( + problem.getPlanningHorizon().getStartInstant(), + problem.getPlanningHorizon().getHor(), + groundedPlan.get(), + Map.of(), + Map.of()), + Optional.of(new DualHashBidiMap())); + } else { + logger.debug( + "Tried mocking simulation results with a grounded plan but could not because of the activity cannot be grounded."); + } + } try { - if(checkSimBeforeEvaluatingGoal || lastSimulationResults == null) - lastSimulationResults = simulationFacade - .simulateWithResults(plan, time.plus(Duration.of(1, Duration.MICROSECONDS)), resourceNames) - .constraintsResults(); - return lastSimulationResults; + var resources = new HashSet(resourceNames); + var resourcesAreMissing = false; + if(cachedSimulationResultsBeforeGoalEvaluation != null){ + final var allResources = new HashSet<>(cachedSimulationResultsBeforeGoalEvaluation.constraintsResults().realProfiles.keySet()); + allResources.addAll(cachedSimulationResultsBeforeGoalEvaluation.constraintsResults().discreteProfiles.keySet()); + resourcesAreMissing = !allResources.containsAll(resourceNames); + } + //if at least one goal needs the simulateAfter, we can't compute partial resources to avoid future recomputations + if(atLeastOneSimulateAfter){ + resources.clear(); + resources.addAll(this.problem.getMissionModel().getResources().keySet()); + } + if(checkSimBeforeEvaluatingGoal || cachedSimulationResultsBeforeGoalEvaluation == null || cachedSimulationResultsBeforeGoalEvaluation.constraintsResults().bounds.end.shorterThan(time) || resourcesAreMissing) + cachedSimulationResultsBeforeGoalEvaluation = simulationFacade + .simulateWithResults(plan, time, resources); + return cachedSimulationResultsBeforeGoalEvaluation; } catch (SimulationFacade.SimulationException e) { throw new RuntimeException("Exception while running simulation before evaluating conflicts", e); } @@ -835,7 +876,7 @@ private Windows narrowGlobalConstraints( plan, tmp, mac, - latestSimulationResults, + latestSimulationResults.constraintsResults(), evaluationEnvironment); } return tmp; @@ -906,13 +947,12 @@ public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History< SchedulingActivityDirective.instantiateArguments( activityExpression.arguments(), start, - latestConstraintsSimulationResults, + latestConstraintsSimulationResults.constraintsResults(), evaluationEnvironment, activityExpression.type()), null, null, true); - final var lastInsertion = history.getLastEvent(); Duration computedDuration = null; try { final var duplicatePlan = plan.duplicate(); @@ -942,7 +982,7 @@ public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History< final var instantiatedArguments = SchedulingActivityDirective.instantiateArguments( activityExpression.arguments(), earliestStart, - getLatestSimResultsUpTo(earliestStart, resourceNames), + getLatestSimResultsUpTo(earliestStart, resourceNames).constraintsResults(), evaluationEnvironment, activityExpression.type()); @@ -986,7 +1026,7 @@ public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History< SchedulingActivityDirective.instantiateArguments( activityExpression.arguments(), earliestStart, - getLatestSimResultsUpTo(earliestStart, resourceNames), + getLatestSimResultsUpTo(earliestStart, resourceNames).constraintsResults(), evaluationEnvironment, activityExpression.type()), null, @@ -1001,7 +1041,7 @@ public Duration valueAt(final Duration start, final EquationSolvingAlgorithms.Hi final var instantiatedArgs = SchedulingActivityDirective.instantiateArguments( activityExpression.arguments(), start, - getLatestSimResultsUpTo(start, resourceNames), + getLatestSimResultsUpTo(start, resourceNames).constraintsResults(), evaluationEnvironment, activityExpression.type() ); diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SchedulingGrounderTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SchedulingGrounderTest.java new file mode 100644 index 0000000000..eb9dc05041 --- /dev/null +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SchedulingGrounderTest.java @@ -0,0 +1,96 @@ +package gov.nasa.jpl.aerie.scheduler; + +import gov.nasa.jpl.aerie.constraints.model.ActivityInstance; +import gov.nasa.jpl.aerie.constraints.time.Interval; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.scheduler.model.ActivityType; +import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; +import gov.nasa.jpl.aerie.scheduler.model.SchedulePlanGrounder; +import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; +import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SchedulingGrounderTest { + private final static PlanningHorizon h = new PlanningHorizon(TimeUtility.fromDOY("2025-001T01:01:01.001"), TimeUtility.fromDOY("2025-005T01:01:01.001")); + private final static Duration t0 = h.getStartAerie(); + private final static Duration d1min = Duration.of(1, Duration.MINUTE); + private final static Duration d1hr = Duration.of(1, Duration.HOUR); + private final static Duration t1hr = t0.plus(d1hr); + private final static Duration t2hr = t0.plus(d1hr.times(2)); + + @Test + public void testChainAnchors(){ + final var at = new ActivityType("at"); + final var id1 = new SchedulingActivityDirectiveId(1); + final var id2 = new SchedulingActivityDirectiveId(2); + final var id3 = new SchedulingActivityDirectiveId(3); + final var act1 = SchedulingActivityDirective.of(id1, at, t0, d1min, null, true); + final var act2 = SchedulingActivityDirective.of(id2, at, t1hr, d1min, act1.id(), false); + final var act3 = SchedulingActivityDirective.of(id3, at, t2hr, d1min, act2.id(), false); + final var acts = List.of(act1, act3, act2); + final var result = SchedulePlanGrounder.groundSchedule(acts, h.getEndAerie()); + //act 1 should start at 0 min into the plan + //act 2 should start 61 min into the plan + //act 3 should be [62 min, 63 min] + final var act1expt = new ActivityInstance(id1.id(), at.getName(), Map.of(), Interval.between(t0, t0.plus(d1min))); + final var act2expt = new ActivityInstance(id2.id(), at.getName(), Map.of(), Interval.between(t1hr.plus(t0).plus(d1min), t1hr.plus(t0).plus(d1min).plus(d1min))); + final var act3expt = new ActivityInstance(id3.id(), at.getName(), Map.of(), Interval.between(t0.plus(Duration.of(182, Duration.MINUTES)), t0.plus(Duration.of(183, Duration.MINUTES)))); + assertTrue(result.get().contains(act1expt)); + assertTrue(result.get().contains(act2expt)); + assertTrue(result.get().contains(act3expt)); + } + + @Test + public void testEmptyDueToEmptyDuration(){ + final var at = new ActivityType("at"); + final var id1 = new SchedulingActivityDirectiveId(1); + final var act1 = SchedulingActivityDirective.of(id1, at, t0, null, null, true); + final var result = SchedulePlanGrounder.groundSchedule(List.of(act1), h.getEndAerie()); + assertTrue(result.isEmpty()); + } + + @Test + public void testAnchoredToPlanEnd(){ + final var at = new ActivityType("at"); + final var id1 = new SchedulingActivityDirectiveId(1); + final var act1 = SchedulingActivityDirective.of(id1, at, Duration.negate(d1hr), d1min, null, false); + final var result = SchedulePlanGrounder.groundSchedule(List.of(act1), h.getEndAerie()); + final var act1expt = new ActivityInstance(id1.id(), at.getName(), Map.of(), Interval.between(h.getEndAerie().minus(d1hr), h.getEndAerie().minus(d1hr).plus(d1min))); + assertEquals(act1expt, result.get().get(0)); + } + + + @Test + public void noAnchor(){ + final var at = new ActivityType("at"); + final var id1 = new SchedulingActivityDirectiveId(1); + final var id2 = new SchedulingActivityDirectiveId(2); + final var act1 = SchedulingActivityDirective.of(id1, at, t0, d1min, null, true); + final var act2 = SchedulingActivityDirective.of(id2, at, t1hr, d1min, null, true); + final var result = SchedulePlanGrounder.groundSchedule(List.of(act1, act2), h.getEndAerie()); + final var act1expt = new ActivityInstance(id1.id(), at.getName(), Map.of(), Interval.between(t0, t0.plus(d1min))); + final var act2expt = new ActivityInstance(id2.id(), at.getName(), Map.of(), Interval.between(t1hr, t1hr.plus(d1min))); + assertTrue(result.get().contains(act1expt)); + assertTrue(result.get().contains(act2expt)); + } + + @Test + public void startTimeAnchor(){ + final var at = new ActivityType("at"); + final var id1 = new SchedulingActivityDirectiveId(1); + final var id2 = new SchedulingActivityDirectiveId(2); + final var act1 = SchedulingActivityDirective.of(id1, at, t1hr, d1min, null, true); + final var act2 = SchedulingActivityDirective.of(id2, at, t1hr, d1min, act1.id(), true); + final var result = SchedulePlanGrounder.groundSchedule(List.of(act1, act2), h.getEndAerie()); + final var act1expt = new ActivityInstance(id1.id(), at.getName(), Map.of(), Interval.between(t1hr, t1hr.plus(d1min))); + final var act2expt = new ActivityInstance(id2.id(), at.getName(), Map.of(), Interval.between(t2hr, t2hr.plus(d1min))); + assertTrue(result.get().contains(act1expt)); + assertTrue(result.get().contains(act2expt)); + } +} From a184a4c53b979f09e43a9e442bb3983d3936b7ea Mon Sep 17 00:00:00 2001 From: maillard Date: Thu, 14 Mar 2024 15:05:48 -0700 Subject: [PATCH 29/43] useless import --- .../aerie/scheduler/simulation/ResumableSimulationDriver.java | 1 - 1 file changed, 1 deletion(-) diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java index 71a31394e1..872d87e4d6 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java @@ -29,7 +29,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.Executor; import java.util.Set; import java.util.function.Supplier; From 02219889fb76136783439de7605dd4cf6dde63cb Mon Sep 17 00:00:00 2001 From: maillard Date: Thu, 14 Mar 2024 15:13:21 -0700 Subject: [PATCH 30/43] Activate thread task cache in scheduling, keep it deactivated in merlin --- deployment/docker-compose.yml | 2 ++ docker-compose.yml | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index bfeca65141..75a0f788e5 100755 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -54,6 +54,7 @@ services: MERLIN_DB_PASSWORD: "${MERLIN_PASSWORD}" MERLIN_WORKER_LOCAL_STORE: /usr/src/app/merlin_file_store SIMULATION_PROGRESS_POLL_PERIOD_MILLIS: 2000 + THREADED_TASK_CACHE_READS: false JAVA_OPTS: > -Dorg.slf4j.simpleLogger.defaultLogLevel=INFO -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=WARN @@ -100,6 +101,7 @@ services: SCHEDULER_OUTPUT_MODE: UpdateInputPlanWithNewActivities MERLIN_LOCAL_STORE: /usr/src/app/merlin_file_store SCHEDULER_RULES_JAR: /usr/src/app/merlin_file_store/scheduler_rules.jar + THREADED_TASK_CACHE_READS: true JAVA_OPTS: > -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=INFO -Dorg.slf4j.simpleLogger.logFile=System.err diff --git a/docker-compose.yml b/docker-compose.yml index a234d87666..3398098a51 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -127,6 +127,7 @@ services: MERLIN_DB_PASSWORD: "${MERLIN_PASSWORD}" MERLIN_WORKER_LOCAL_STORE: /usr/src/app/merlin_file_store SIMULATION_PROGRESS_POLL_PERIOD_MILLIS: 2000 + THREADED_TASK_CACHE_READS: false JAVA_OPTS: > -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG @@ -151,6 +152,7 @@ services: MERLIN_DB_PASSWORD: "${MERLIN_PASSWORD}" MERLIN_WORKER_LOCAL_STORE: /usr/src/app/merlin_file_store SIMULATION_PROGRESS_POLL_PERIOD_MILLIS: 2000 + THREADED_TASK_CACHE_READS: false JAVA_OPTS: > -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG @@ -178,6 +180,7 @@ services: SCHEDULER_OUTPUT_MODE: UpdateInputPlanWithNewActivities MERLIN_LOCAL_STORE: /usr/src/app/merlin_file_store SCHEDULER_RULES_JAR: /usr/src/app/merlin_file_store/scheduler_rules.jar + THREADED_TASK_CACHE_READS: true JAVA_OPTS: > -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG @@ -204,6 +207,7 @@ services: SCHEDULER_OUTPUT_MODE: UpdateInputPlanWithNewActivities MERLIN_LOCAL_STORE: /usr/src/app/merlin_file_store SCHEDULER_RULES_JAR: /usr/src/app/merlin_file_store/scheduler_rules.jar + THREADED_TASK_CACHE_READS: true JAVA_OPTS: > -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG From 7b71fde0901a98af8c1324d2184686c0d9ca390a Mon Sep 17 00:00:00 2001 From: maillard Date: Fri, 15 Mar 2024 15:10:31 -0700 Subject: [PATCH 31/43] Refactor equals() of SimulationResults --- .../merlin/driver/SimulationResults.java | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResults.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResults.java index 4309434865..0c5fc342bc 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResults.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResults.java @@ -59,18 +59,16 @@ public String toString() { @Override public boolean equals(final Object o) { if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (!(o instanceof SimulationResults that)) return false; - SimulationResults that = (SimulationResults) o; - - if (!startTime.equals(that.startTime)) return false; - if (!duration.isEqualTo(that.duration)) return false; - if (!realProfiles.equals(that.realProfiles)) return false; - if (!discreteProfiles.equals(that.discreteProfiles)) return false; - if (!simulatedActivities.equals(that.simulatedActivities)) return false; - if (!unfinishedActivities.equals(that.unfinishedActivities)) return false; - if (!topics.equals(that.topics)) return false; - return events.equals(that.events); + return startTime.equals(that.startTime) + && duration.isEqualTo(that.duration) + && realProfiles.equals(that.realProfiles) + && discreteProfiles.equals(that.discreteProfiles) + && simulatedActivities.equals(that.simulatedActivities) + && unfinishedActivities.equals(that.unfinishedActivities) + && topics.equals(that.topics) + && events.equals(that.events); } @Override From e70bb9bdbb9be7980712c878caab5698fa5d40a0 Mon Sep 17 00:00:00 2001 From: maillard Date: Fri, 15 Mar 2024 15:10:47 -0700 Subject: [PATCH 32/43] Rename test --- .../SimulationDuplicationTest.java | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java index 65ecfbe3c5..63c78466f7 100644 --- a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java +++ b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java @@ -165,7 +165,7 @@ void testTrivialDuplicate() { } @Test - void testFooDuplicateEmptyPlan() { + void testCompareCheckpointOnEmptyPlan() { final MissionModel missionModel = makeMissionModel( new MissionModelBuilder(), Instant.EPOCH, @@ -196,8 +196,8 @@ void testFooNonEmptyPlan() { Instant.EPOCH, new Configuration()); final Map schedule = Map.ofEntries( - activity(1, MINUTE, "foo", Map.of("z", SerializedValue.of(123))), - activity(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) + activityFrom(1, MINUTE, "foo", Map.of("z", SerializedValue.of(123))), + activityFrom(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) ); final var results = simulateWithCheckpoints( missionModel, @@ -238,8 +238,8 @@ void testFooNonEmptyPlanMultipleResumes() { Instant.EPOCH, new Configuration()); final Map schedule = Map.ofEntries( - activity(1, MINUTE, "foo", Map.of("z", SerializedValue.of(123))), - activity(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) + activityFrom(1, MINUTE, "foo", Map.of("z", SerializedValue.of(123))), + activityFrom(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) ); final var results = simulateWithCheckpoints( missionModel, @@ -291,8 +291,8 @@ void testFooNonEmptyPlanMultipleCheckpointsMultipleResumes() { Instant.EPOCH, new Configuration()); final Map schedule = Map.ofEntries( - activity(1, MINUTE, "foo", Map.of("z", SerializedValue.of(123))), - activity(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) + activityFrom(1, MINUTE, "foo", Map.of("z", SerializedValue.of(123))), + activityFrom(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) ); final var results = simulateWithCheckpoints( missionModel, @@ -343,18 +343,18 @@ void testFooNonEmptyPlanMultipleCheckpointsMultipleResumesWithEdits() { new MissionModelBuilder(), Instant.EPOCH, new Configuration()); - final Pair activity1 = activity( + final Pair activity1 = activityFrom( 1, MINUTE, "foo", Map.of("z", SerializedValue.of(123))); final Map schedule1 = Map.ofEntries( activity1, - activity(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) + activityFrom(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) ); final Map schedule2 = Map.ofEntries( activity1, - activity(390, SECONDS, "foo", Map.of("z", SerializedValue.of(999))) + activityFrom(390, SECONDS, "foo", Map.of("z", SerializedValue.of(999))) ); final var results = simulateWithCheckpoints( missionModel, @@ -411,18 +411,11 @@ void testFooNonEmptyPlanMultipleCheckpointsMultipleResumesWithEdits() { private static long nextActivityDirectiveId = 0L; - private static Pair activity(final long quantity, final Duration unit, final String type) { - return activity(quantity, unit, type, Map.of()); - } - private static Pair activity(final Duration startOffset, final String type) { - return activity(startOffset, type, Map.of()); - } - - private static Pair activity(final long quantity, final Duration unit, final String type, final Map args) { - return activity(Duration.of(quantity, unit), type, args); + private static Pair activityFrom(final long quantity, final Duration unit, final String type, final Map args) { + return activityFrom(Duration.of(quantity, unit), type, args); } - private static Pair activity(final Duration startOffset, final String type, final Map args) { + private static Pair activityFrom(final Duration startOffset, final String type, final Map args) { return Pair.of(new ActivityDirectiveId(nextActivityDirectiveId++), new ActivityDirective(startOffset, type, args, null, true)); } From 25bb277c645baefc4f0c597a74ec002698b75943 Mon Sep 17 00:00:00 2001 From: maillard Date: Fri, 15 Mar 2024 15:13:31 -0700 Subject: [PATCH 33/43] Remove unused imports --- .../nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java index 2130cae61b..15be47752f 100644 --- a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java @@ -1,9 +1,7 @@ package gov.nasa.jpl.aerie.merlin.driver; -import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; import gov.nasa.jpl.aerie.merlin.driver.timeline.CausalEventSource; import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; -import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; From 9c5239147b29d160c89f578fc66df8d1b548ee3b Mon Sep 17 00:00:00 2001 From: maillard Date: Fri, 15 Mar 2024 15:27:14 -0700 Subject: [PATCH 34/43] Put test mission model in its own class --- .../SimulationDuplicationTest.java | 121 ---- .../merlin/driver/AnchorSimulationTest.java | 678 ++++++++++-------- .../driver/SimulationDuplicationTest.java | 198 +---- .../driver/TemporalSubsetSimulationTests.java | 38 +- .../aerie/merlin/driver/TestMissionModel.java | 212 ++++++ 5 files changed, 632 insertions(+), 615 deletions(-) create mode 100644 merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TestMissionModel.java diff --git a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java index 63c78466f7..38b26a7dcc 100644 --- a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java +++ b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java @@ -94,76 +94,6 @@ private static MissionModel makeMissionModel(final MissionModelBuilder return builder.build(model, registry); } - @Test - void emptyPlanTest() { - final SimulationResults results = SimulationDriver.simulate( - missionModel, - Map.of(), - Instant.EPOCH, - Duration.HOUR, - Instant.EPOCH, - Duration.HOUR, - () -> false); - final List> standardTopics = List.of( - Triple.of( - 0, - "ActivityType.Input.DelayActivityDirective", - ValueSchema.ofStruct(Map.of())), - Triple.of( - 1, - "ActivityType.Output.DelayActivityDirective", - ValueSchema.ofStruct(Map.of())), - Triple.of( - 2, - "ActivityType.Input.DecomposingActivityDirective", - ValueSchema.ofStruct(Map.of())), - Triple.of( - 3, - "ActivityType.Output.DecomposingActivityDirective", - ValueSchema.ofStruct(Map.of()))); - final SimulationResults expected = new SimulationResults( - Map.of(), - Map.of(), - Map.of(), - Map.of(), - Instant.EPOCH, - Duration.HOUR, - standardTopics, - new TreeMap<>()); - assertResultsEqual(expected, results); - } - - @Test - void testTrivialDuplicate() { - final SimulationResults results = simulateWithCheckpoints( - missionModel, - CheckpointSimulationDriver.CachedSimulationEngine.empty(missionModel), - List.of(Duration.of(5, MINUTES)), - Map.of(), - store, - mockConfiguration() - ); - final SimulationResults expected = SimulationDriver.simulate( - missionModel, - Map.of(), - Instant.EPOCH, - Duration.HOUR, - Instant.EPOCH, - Duration.HOUR, - () -> false, - $ -> {}); - assertResultsEqual(expected, results); - final var newResults = simulateWithCheckpoints( - missionModel, - store.getCachedEngines(mockConfiguration()).get(0), - List.of(), - Map.of(), - store, - mockConfiguration() - ); - assertResultsEqual(expected, newResults); - } - @Test void testCompareCheckpointOnEmptyPlan() { final MissionModel missionModel = makeMissionModel( @@ -624,55 +554,4 @@ public TaskFactory getTaskFactory(final Object o, final Object o2) { }); } }; - - static MissionModel missionModel = new MissionModel<>( - new Object(), - new LiveCells(new CausalEventSource()), - Map.of(), - List.of( - new MissionModel.SerializableTopic<>( - "ActivityType.Input.DelayActivityDirective", - delayedActivityDirectiveInputTopic, - testModelOutputType), - new MissionModel.SerializableTopic<>( - "ActivityType.Output.DelayActivityDirective", - delayedActivityDirectiveOutputTopic, - testModelOutputType), - new MissionModel.SerializableTopic<>( - "ActivityType.Input.DecomposingActivityDirective", - decomposingActivityDirectiveInputTopic, - testModelOutputType), - new MissionModel.SerializableTopic<>( - "ActivityType.Output.DecomposingActivityDirective", - decomposingActivityDirectiveOutputTopic, - testModelOutputType)), - List.of(), - DirectiveTypeRegistry.extract( - new ModelType<>() { - - @Override - public Map> getDirectiveTypes() { - return Map.of( - "DelayActivityDirective", - delayedActivityDirective, - "DecomposingActivityDirective", - decomposingActivityDirective); - } - - @Override - public InputType getConfigurationType() { - return testModelInputType; - } - - @Override - public Object instantiate( - final Instant planStart, - final Object configuration, - final Initializer builder) - { - return new Object(); - } - } - ) - ); } diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/AnchorSimulationTest.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/AnchorSimulationTest.java index c52749321e..9f300abc16 100644 --- a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/AnchorSimulationTest.java +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/AnchorSimulationTest.java @@ -1,39 +1,20 @@ package gov.nasa.jpl.aerie.merlin.driver; -import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; -import gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType; -import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; -import gov.nasa.jpl.aerie.merlin.protocol.model.ModelType; -import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; -import gov.nasa.jpl.aerie.merlin.protocol.model.Task; -import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; -import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; -import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; -import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; -import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import org.apache.commons.lang3.tuple.Pair; -import org.apache.commons.lang3.tuple.Triple; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.TreeMap; -import java.util.concurrent.Executor; -import java.util.function.Function; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -475,24 +456,28 @@ public void filterTest() { } @Nested public final class AnchorsSimulationDriverTests { - private final SerializedActivity serializedDelayDirective = new SerializedActivity("DelayActivityDirective", arguments); - private final SerializedActivity serializedDecompositionDirective = new SerializedActivity("DecomposingActivityDirective", arguments); + private final SerializedActivity serializedDelayDirective = new SerializedActivity( + "DelayActivityDirective", + arguments); + private final SerializedActivity serializedDecompositionDirective = new SerializedActivity( + "DecomposingActivityDirective", + arguments); private final SerializedValue computedAttributes = new SerializedValue.MapValue(Map.of()); private final Instant planStart = Instant.EPOCH; /** * Asserts equality based on the following fields of SimulationResults: - * - startTime - * - simulatedActivities - * - unfinishedActivities (asserted to be empty in actual) - * - topics - * Any resource profiles and events are not checked. + * - startTime + * - simulatedActivities + * - unfinishedActivities (asserted to be empty in actual) + * - topics + * Any resource profiles and events are not checked. */ - private static void assertEqualsSimulationResults(SimulationResults expected, SimulationResults actual){ + private static void assertEqualsSimulationResults(SimulationResults expected, SimulationResults actual) { assertEquals(expected.startTime, actual.startTime); assertEquals(expected.duration, actual.duration); assertEquals(expected.simulatedActivities.size(), actual.simulatedActivities.size()); - for(final var entry : expected.simulatedActivities.entrySet()){ + for (final var entry : expected.simulatedActivities.entrySet()) { final var key = entry.getKey(); final var expectedValue = entry.getValue(); final var actualValue = actual.simulatedActivities.get(key); @@ -501,15 +486,21 @@ private static void assertEqualsSimulationResults(SimulationResults expected, Si } assertTrue(actual.unfinishedActivities.isEmpty()); assertEquals(expected.topics.size(), actual.topics.size()); - for(int i = 0; i < expected.topics.size(); ++i){ + for (int i = 0; i < expected.topics.size(); ++i) { assertEquals(expected.topics.get(i), actual.topics.get(i)); } } - private void constructFullComplete5AryTree(int maxLevel, int currentLevel, long parentNode, Map activitiesToSimulate, Map simulatedActivities){ - if(currentLevel > maxLevel) return; - for(int i = 1; i <= 5; i++) { - long curElement = parentNode*5+i; + private void constructFullComplete5AryTree( + int maxLevel, + int currentLevel, + long parentNode, + Map activitiesToSimulate, + Map simulatedActivities) + { + if (currentLevel > maxLevel) return; + for (int i = 1; i <= 5; i++) { + long curElement = parentNode * 5 + i; activitiesToSimulate.put( new ActivityDirectiveId(curElement), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(parentNode), false)); @@ -523,11 +514,16 @@ private void constructFullComplete5AryTree(int maxLevel, int currentLevel, long null, List.of(), Optional.of(new ActivityDirectiveId(curElement)), computedAttributes)); - constructFullComplete5AryTree(maxLevel, currentLevel+1, curElement, activitiesToSimulate, simulatedActivities); + constructFullComplete5AryTree( + maxLevel, + currentLevel + 1, + curElement, + activitiesToSimulate, + simulatedActivities); } } - private static void assertEqualsAsideFromChildren(SimulatedActivity expected, SimulatedActivity actual){ + private static void assertEqualsAsideFromChildren(SimulatedActivity expected, SimulatedActivity actual) { assertEquals(expected.type(), actual.type()); assertEquals(expected.arguments(), actual.arguments()); assertEquals(expected.start(), actual.start()); @@ -559,14 +555,19 @@ public void activitiesAnchoredToPlan() { List.of(), Optional.of(activityDirectiveId), computedAttributes - )); + )); } // Anchored to Plan End (only negative will be simulated) for (long l = 10; l < 15; l++) { final var activityDirectiveId = new ActivityDirectiveId(l); resolveToPlanStartAnchors.put( activityDirectiveId, - new ActivityDirective(Duration.of(-l, Duration.MINUTES), serializedDelayDirective, null, false)); // Minutes so they finish by simulation end + new ActivityDirective( + // Minutes so they finish by simulation end + Duration.of(-l, Duration.MINUTES), + serializedDelayDirective, + null, + false)); simulatedActivities.put(new SimulatedActivityId(l), new SimulatedActivity( serializedDelayDirective.getTypeName(), Map.of(), @@ -635,11 +636,11 @@ public void activitiesAnchoredToPlan() { Map.of(), //unfinished planStart, tenDays, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), resolveToPlanStartAnchors, planStart, tenDays, @@ -703,7 +704,7 @@ public void activitiesAnchoredToOtherActivities() { new SimulatedActivity( serializedDelayDirective.getTypeName(), Map.of(), - Instant.EPOCH.plus((2*l)+1, ChronoUnit.MINUTES), + Instant.EPOCH.plus((2 * l) + 1, ChronoUnit.MINUTES), oneMinute, null, List.of(), @@ -733,7 +734,7 @@ public void activitiesAnchoredToOtherActivities() { new SimulatedActivity( serializedDelayDirective.getTypeName(), Map.of(), - Instant.EPOCH.plus(l+c, ChronoUnit.MINUTES), + Instant.EPOCH.plus(l + c, ChronoUnit.MINUTES), oneMinute, null, List.of(), @@ -752,11 +753,11 @@ public void activitiesAnchoredToOtherActivities() { Map.of(), //unfinished planStart, tenDays, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesToSimulate, planStart, tenDays, @@ -769,7 +770,7 @@ public void activitiesAnchoredToOtherActivities() { @Test @DisplayName("Decomposition and anchors do not interfere with each other") - public void decomposingActivitiesAndAnchors(){ + public void decomposingActivitiesAndAnchors() { // Given positions Left, Center, Right in an anchor chain, where each position can either contain a Non-Decomposition (ND) activity or a Decomposition (D) activity, // and the connection between Center and Left and Right and Center can be either Start (<-s-) or End (<-e-), // and two NDs cannot be adjacent to each other, there are 20 permutations. @@ -782,140 +783,436 @@ public void decomposingActivitiesAndAnchors(){ final var threeMinutes = Duration.of(3, Duration.MINUTES); // ND <-s- D <-s- D - activitiesToSimulate.put(new ActivityDirectiveId(1), new ActivityDirective(Duration.ZERO, serializedDelayDirective, null, true)); - activitiesToSimulate.put(new ActivityDirectiveId(2), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(1), true)); - activitiesToSimulate.put(new ActivityDirectiveId(3), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(2), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(1), + new ActivityDirective(Duration.ZERO, serializedDelayDirective, null, true)); + activitiesToSimulate.put( + new ActivityDirectiveId(2), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(1), + true)); + activitiesToSimulate.put( + new ActivityDirectiveId(3), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(2), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(1), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH, oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(1)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH, + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(1)), + computedAttributes)); topLevelSimulatedActivities.put( new ActivityDirectiveId(2), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH, threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(2)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH, + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(2)), + computedAttributes)); topLevelSimulatedActivities.put( new ActivityDirectiveId(3), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH, threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(3)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH, + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(3)), + computedAttributes)); // ND <-s- D <-e- D - activitiesToSimulate.put(new ActivityDirectiveId(4), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(2), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(4), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(2), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(4), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(3, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(4)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(3, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(4)), + computedAttributes)); // ND <-s- D <-s- ND - activitiesToSimulate.put(new ActivityDirectiveId(5), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(2), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(5), + new ActivityDirective(Duration.ZERO, + serializedDelayDirective, + new ActivityDirectiveId(2), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(5), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH, oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(5)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH, + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(5)), + computedAttributes)); // ND <-s- D <-e- ND - activitiesToSimulate.put(new ActivityDirectiveId(6), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(2), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(6), + new ActivityDirective(Duration.ZERO, + serializedDelayDirective, + new ActivityDirectiveId(2), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(6), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(3, ChronoUnit.MINUTES), oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(6)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(3, ChronoUnit.MINUTES), + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(6)), + computedAttributes)); // ND <-e- D <-s- D - activitiesToSimulate.put(new ActivityDirectiveId(7), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(1), false)); - activitiesToSimulate.put(new ActivityDirectiveId(8), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(7), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(7), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(1), + false)); + activitiesToSimulate.put( + new ActivityDirectiveId(8), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(7), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(7), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(1, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(7)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(1, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(7)), + computedAttributes)); topLevelSimulatedActivities.put( new ActivityDirectiveId(8), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(1, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(8)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(1, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(8)), + computedAttributes)); // ND <-e- D <-e- D - activitiesToSimulate.put(new ActivityDirectiveId(9), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(7), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(9), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(7), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(9), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(4, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(9)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(4, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(9)), + computedAttributes)); // ND <-e- D <-s- ND - activitiesToSimulate.put(new ActivityDirectiveId(10), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(7), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(10), + new ActivityDirective(Duration.ZERO, + serializedDelayDirective, + new ActivityDirectiveId(7), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(10), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(1, ChronoUnit.MINUTES), oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(10)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(1, ChronoUnit.MINUTES), + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(10)), + computedAttributes)); // ND <-e- D <-e- ND - activitiesToSimulate.put(new ActivityDirectiveId(11), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(7), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(11), + new ActivityDirective(Duration.ZERO, + serializedDelayDirective, + new ActivityDirectiveId(7), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(11), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(4, ChronoUnit.MINUTES), oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(11)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(4, ChronoUnit.MINUTES), + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(11)), + computedAttributes)); // D <-s- D <-s- D - activitiesToSimulate.put(new ActivityDirectiveId(12), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(3), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(12), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(3), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(12), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH, threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(12)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH, + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(12)), + computedAttributes)); // D <-s- D <-e- D - activitiesToSimulate.put(new ActivityDirectiveId(13), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(3), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(13), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(3), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(13), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(3, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(13)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(3, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(13)), + computedAttributes)); // D <-s- D <-s- ND - activitiesToSimulate.put(new ActivityDirectiveId(14), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(3), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(14), + new ActivityDirective(Duration.ZERO, + serializedDelayDirective, + new ActivityDirectiveId(3), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(14), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH, oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(14)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH, + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(14)), + computedAttributes)); // D <-s- D <-e- ND - activitiesToSimulate.put(new ActivityDirectiveId(15), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(3), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(15), + new ActivityDirective(Duration.ZERO, + serializedDelayDirective, + new ActivityDirectiveId(3), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(15), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(3, ChronoUnit.MINUTES), oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(15)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(3, ChronoUnit.MINUTES), + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(15)), + computedAttributes)); // D <-e- D <-s- D - activitiesToSimulate.put(new ActivityDirectiveId(16), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(4), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(16), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(4), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(16), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(3, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(16)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(3, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(16)), + computedAttributes)); // D <-e- D <-e- D - activitiesToSimulate.put(new ActivityDirectiveId(17), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(4), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(17), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(4), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(17), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(6, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(17)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(6, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(17)), + computedAttributes)); // D <-e- D <-s- ND - activitiesToSimulate.put(new ActivityDirectiveId(18), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(4), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(18), + new ActivityDirective(Duration.ZERO, + serializedDelayDirective, + new ActivityDirectiveId(4), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(18), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(3, ChronoUnit.MINUTES), oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(18)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(3, ChronoUnit.MINUTES), + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(18)), + computedAttributes)); // D <-e- D <-e- ND - activitiesToSimulate.put(new ActivityDirectiveId(19), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(4), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(19), + new ActivityDirective(Duration.ZERO, + serializedDelayDirective, + new ActivityDirectiveId(4), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(19), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(6, ChronoUnit.MINUTES), oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(19)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(6, ChronoUnit.MINUTES), + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(19)), + computedAttributes)); // D <-s- ND <-s- D - activitiesToSimulate.put(new ActivityDirectiveId(20), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(14), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(20), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(14), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(20), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH, threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(20)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH, + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(20)), + computedAttributes)); // D <-s- ND <-e- D - activitiesToSimulate.put(new ActivityDirectiveId(21), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(14), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(21), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(14), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(21), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(1, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(21)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(1, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(21)), + computedAttributes)); // D <-e- ND <-s- D - activitiesToSimulate.put(new ActivityDirectiveId(22), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(15), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(22), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(15), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(22), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(3, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(22)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(3, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(22)), + computedAttributes)); // D <-e- ND <-e- D - activitiesToSimulate.put(new ActivityDirectiveId(23), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(15), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(23), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(15), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(23), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(4, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(23)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(4, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(23)), + computedAttributes)); // Custom assertion, as Decomposition children can end up simulated in different positions between runs final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesToSimulate, planStart, tenDays, @@ -925,8 +1222,9 @@ public void decomposingActivitiesAndAnchors(){ assertEquals(planStart, actualSimResults.startTime); assertTrue(actualSimResults.unfinishedActivities.isEmpty()); + final var modelTopicList = TestMissionModel.getModelTopicList(); assertEquals(modelTopicList.size(), actualSimResults.topics.size()); - for(int i = 0; i < modelTopicList.size(); ++i){ + for (int i = 0; i < modelTopicList.size(); ++i) { assertEquals(modelTopicList.get(i), actualSimResults.topics.get(i)); } @@ -934,23 +1232,22 @@ public void decomposingActivitiesAndAnchors(){ final var otherSimulatedActivities = new HashMap(23); assertEquals(51, actualSimResults.simulatedActivities.size()); // 23 + 2*(14 Decomposing activities) - for(final var entry : actualSimResults.simulatedActivities.entrySet()) { - if(entry.getValue().parentId()==null){ + for (final var entry : actualSimResults.simulatedActivities.entrySet()) { + if (entry.getValue().parentId() == null) { otherSimulatedActivities.put(entry.getKey(), entry.getValue()); - } - else { + } else { childSimulatedActivities.put(entry.getKey(), entry.getValue()); } } assertEquals(23, otherSimulatedActivities.size()); assertEquals(28, childSimulatedActivities.size()); - for(final var entry : otherSimulatedActivities.entrySet()){ + for (final var entry : otherSimulatedActivities.entrySet()) { assertTrue(entry.getValue().directiveId().isPresent()); final ActivityDirectiveId topLevelKey = entry.getValue().directiveId().get(); assertEqualsAsideFromChildren(topLevelSimulatedActivities.get(topLevelKey), entry.getValue()); // For decompositions, examine the children - if(entry.getValue().type().equals(serializedDecompositionDirective.getTypeName())){ + if (entry.getValue().type().equals(serializedDecompositionDirective.getTypeName())) { assertEquals(2, entry.getValue().childIds().size()); final var firstChild = childSimulatedActivities.remove(entry.getValue().childIds().get(0)); final var secondChild = childSimulatedActivities.remove(entry.getValue().childIds().get(1)); @@ -961,7 +1258,7 @@ public void decomposingActivitiesAndAnchors(){ assertTrue(firstChild.childIds().isEmpty()); assertTrue(secondChild.childIds().isEmpty()); - if(firstChild.start().isBefore(secondChild.start())){ + if (firstChild.start().isBefore(secondChild.start())) { assertEqualsAsideFromChildren( new SimulatedActivity( serializedDelayDirective.getTypeName(), @@ -1049,11 +1346,11 @@ public void naryTreeAnchorChain() { Map.of(), //unfinished planStart, tenDays, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesToSimulate, planStart, tenDays, @@ -1064,184 +1361,5 @@ public void naryTreeAnchorChain() { assertEquals(3906, expectedSimResults.simulatedActivities.size()); assertEqualsSimulationResults(expectedSimResults, actualSimResults); } - - //region Mission Model - /* package-private */static final List> modelTopicList = Arrays.asList( - Triple.of(0, "ActivityType.Input.DelayActivityDirective", new ValueSchema.StructSchema(Map.of())), - Triple.of(1, "ActivityType.Output.DelayActivityDirective", new ValueSchema.StructSchema(Map.of())), - Triple.of(2, "ActivityType.Input.DecomposingActivityDirective", new ValueSchema.StructSchema(Map.of())), - Triple.of(3, "ActivityType.Output.DecomposingActivityDirective", new ValueSchema.StructSchema(Map.of()))); - - private static final Topic delayedActivityDirectiveInputTopic = new Topic<>(); - private static final Topic delayedActivityDirectiveOutputTopic = new Topic<>(); - /* package-private*/ static final DirectiveType delayedActivityDirective = new DirectiveType<>() { - @Override - public InputType getInputType() { - return testModelInputType; - } - - @Override - public OutputType getOutputType() { - return testModelOutputType; - } - - @Override - public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> oneShotTask($ -> { - $.emit(this, delayedActivityDirectiveInputTopic); - return TaskStatus.delayed(oneMinute, oneShotTask($$ -> { - $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); - return TaskStatus.completed(Unit.UNIT); - })); - }); - } - }; - - private static final Topic decomposingActivityDirectiveInputTopic = new Topic<>(); - private static final Topic decomposingActivityDirectiveOutputTopic = new Topic<>(); - /* package-private */ static final DirectiveType decomposingActivityDirective = new DirectiveType<>() { - @Override - public InputType getInputType() { - return testModelInputType; - } - - @Override - public OutputType getOutputType() { - return testModelOutputType; - } - - @Override - public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> oneShotTask(scheduler -> { - scheduler.emit(this, decomposingActivityDirectiveInputTopic); - return TaskStatus.delayed( - Duration.ZERO, - oneShotTask($ -> { - try { - $.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); - } catch (final InstantiationException ex) { - throw new Error("Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( - ex.toString())); - } - return TaskStatus.delayed(Duration.of(120, Duration.SECOND), oneShotTask($$ -> { - try { - $$.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); - } catch (final InstantiationException ex) { - throw new Error( - "Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( - ex.toString())); - } - $$.emit(Unit.UNIT, decomposingActivityDirectiveOutputTopic); - return TaskStatus.completed(Unit.UNIT); - })); - })); - }); - } - }; - - private static Task oneShotTask(Function> f) { - return new Task<>() { - @Override - public TaskStatus step(final Scheduler scheduler) { - return f.apply(scheduler); - } - - @Override - public Task duplicate(Executor executor) { - return this; - } - }; - } - - private static final InputType testModelInputType = new InputType<>() { - @Override - public List getParameters() { - return List.of(); - } - - @Override - public List getRequiredParameters() { - return List.of(); - } - - @Override - public Object instantiate(final Map arguments) { - return new Object(); - } - - @Override - public Map getArguments(final Object value) { - return Map.of(); - } - - @Override - public List getValidationFailures(final Object value) { - return List.of(); - } - }; - - private static final OutputType testModelOutputType = new OutputType<>() { - @Override - public ValueSchema getSchema() { - return ValueSchema.ofStruct(Map.of()); - } - - @Override - public SerializedValue serialize(final Object value) { - return SerializedValue.of(Map.of()); - } - }; - - /* package-private */ static final MissionModel AnchorTestModel = new MissionModel<>( - new Object(), - new LiveCells(null), - Map.of(), - List.of( - new MissionModel.SerializableTopic<>( - "ActivityType.Input.DelayActivityDirective", - delayedActivityDirectiveInputTopic, - testModelOutputType), - new MissionModel.SerializableTopic<>( - "ActivityType.Output.DelayActivityDirective", - delayedActivityDirectiveOutputTopic, - testModelOutputType), - new MissionModel.SerializableTopic<>( - "ActivityType.Input.DecomposingActivityDirective", - decomposingActivityDirectiveInputTopic, - testModelOutputType), - new MissionModel.SerializableTopic<>( - "ActivityType.Output.DecomposingActivityDirective", - decomposingActivityDirectiveOutputTopic, - testModelOutputType)), - List.of(), - DirectiveTypeRegistry.extract( - new ModelType<>() { - - @Override - public Map> getDirectiveTypes() { - return Map.of( - "DelayActivityDirective", - delayedActivityDirective, - "DecomposingActivityDirective", - decomposingActivityDirective); - } - - @Override - public InputType getConfigurationType() { - return testModelInputType; - } - - @Override - public Object instantiate( - final Instant planStart, - final Object configuration, - final Initializer builder) - { - return new Object(); - } - } - ) - ); - //endregion } } diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java index 15be47752f..e179c1a4c9 100644 --- a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java @@ -1,22 +1,6 @@ package gov.nasa.jpl.aerie.merlin.driver; -import gov.nasa.jpl.aerie.merlin.driver.timeline.CausalEventSource; -import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; -import gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType; -import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; -import gov.nasa.jpl.aerie.merlin.protocol.model.ModelType; -import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; -import gov.nasa.jpl.aerie.merlin.protocol.model.Task; -import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; -import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; -import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; -import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; -import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import org.apache.commons.lang3.tuple.Triple; import org.junit.jupiter.api.BeforeEach; @@ -28,8 +12,6 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; -import java.util.concurrent.Executor; -import java.util.function.Function; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTES; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -69,7 +51,7 @@ void beforeEach(){ @Test void emptyPlanTest() { final SimulationResults results = SimulationDriver.simulate( - missionModel, + TestMissionModel.missionModel(), Map.of(), Instant.EPOCH, Duration.HOUR, @@ -108,11 +90,11 @@ void emptyPlanTest() { @Test void testDuplicate() { final var results = simulateWithCheckpoints( - CheckpointSimulationDriver.CachedSimulationEngine.empty(missionModel), + CheckpointSimulationDriver.CachedSimulationEngine.empty(TestMissionModel.missionModel()), List.of(Duration.of(5, MINUTES)), store); final SimulationResults expected = SimulationDriver.simulate( - missionModel, + TestMissionModel.missionModel(), Map.of(), Instant.EPOCH, Duration.HOUR, @@ -131,7 +113,7 @@ static SimulationResults simulateWithCheckpoints( final CachedEngineStore engineStore ) { return CheckpointSimulationDriver.computeResults(CheckpointSimulationDriver.simulateWithCheckpoints( - missionModel, + TestMissionModel.missionModel(), Map.of(), Instant.EPOCH, Duration.HOUR, @@ -146,176 +128,4 @@ static SimulationResults simulateWithCheckpoints( mockConfiguration() )); } - - private static final Topic delayedActivityDirectiveInputTopic = new Topic<>(); - private static final Topic delayedActivityDirectiveOutputTopic = new Topic<>(); - - private static final InputType testModelInputType = new InputType<>() { - @Override - public List getParameters() { - return List.of(); - } - - @Override - public List getRequiredParameters() { - return List.of(); - } - - @Override - public Object instantiate(final Map arguments) { - return new Object(); - } - - @Override - public Map getArguments(final Object value) { - return Map.of(); - } - - @Override - public List getValidationFailures(final Object value) { - return List.of(); - } - }; - - private static final OutputType testModelOutputType = new OutputType<>() { - @Override - public ValueSchema getSchema() { - return ValueSchema.ofStruct(Map.of()); - } - - @Override - public SerializedValue serialize(final Object value) { - return SerializedValue.of(Map.of()); - } - }; - - /* package-private*/ static final DirectiveType delayedActivityDirective = new DirectiveType<>() { - @Override - public InputType getInputType() { - return testModelInputType; - } - - @Override - public OutputType getOutputType() { - return testModelOutputType; - } - - @Override - public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> oneShotTask($ -> { - $.emit(this, delayedActivityDirectiveInputTopic); - return TaskStatus.delayed(Duration.MINUTE, oneShotTask($$ -> { - $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); - return TaskStatus.completed(Unit.UNIT); - })); - }); - } - }; - - private static final Topic decomposingActivityDirectiveInputTopic = new Topic<>(); - private static final Topic decomposingActivityDirectiveOutputTopic = new Topic<>(); - /* package-private */ static final DirectiveType decomposingActivityDirective = new DirectiveType<>() { - @Override - public InputType getInputType() { - return testModelInputType; - } - - @Override - public OutputType getOutputType() { - return testModelOutputType; - } - - @Override - public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> oneShotTask(scheduler -> { - scheduler.emit(this, decomposingActivityDirectiveInputTopic); - return TaskStatus.delayed( - Duration.ZERO, - oneShotTask($ -> { - try { - $.spawn(InSpan.Parent, delayedActivityDirective.getTaskFactory(null, null)); - } catch (final InstantiationException ex) { - throw new Error("Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( - ex.toString())); - } - return TaskStatus.delayed(Duration.of(120, Duration.SECOND), oneShotTask($$ -> { - try { - $$.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); - } catch (final InstantiationException ex) { - throw new Error( - "Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( - ex.toString())); - } - $$.emit(Unit.UNIT, decomposingActivityDirectiveOutputTopic); - return TaskStatus.completed(Unit.UNIT); - })); - })); - }); - } - }; - - static MissionModel missionModel = new MissionModel<>( - new Object(), - new LiveCells(new CausalEventSource()), - Map.of(), - List.of( - new MissionModel.SerializableTopic<>( - "ActivityType.Input.DelayActivityDirective", - delayedActivityDirectiveInputTopic, - testModelOutputType), - new MissionModel.SerializableTopic<>( - "ActivityType.Output.DelayActivityDirective", - delayedActivityDirectiveOutputTopic, - testModelOutputType), - new MissionModel.SerializableTopic<>( - "ActivityType.Input.DecomposingActivityDirective", - decomposingActivityDirectiveInputTopic, - testModelOutputType), - new MissionModel.SerializableTopic<>( - "ActivityType.Output.DecomposingActivityDirective", - decomposingActivityDirectiveOutputTopic, - testModelOutputType)), - List.of(), - DirectiveTypeRegistry.extract( - new ModelType<>() { - - @Override - public Map> getDirectiveTypes() { - return Map.of( - "DelayActivityDirective", - delayedActivityDirective, - "DecomposingActivityDirective", - decomposingActivityDirective); - } - - @Override - public InputType getConfigurationType() { - return testModelInputType; - } - - @Override - public Object instantiate( - final Instant planStart, - final Object configuration, - final Initializer builder) - { - return new Object(); - } - } - ) - ); - - private static Task oneShotTask(Function> f) { - return new Task<>() { - @Override - public TaskStatus step(final Scheduler scheduler) { - return f.apply(scheduler); - } - - @Override - public Task duplicate(Executor executor) { - return this; - } - }; - } } diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TemporalSubsetSimulationTests.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TemporalSubsetSimulationTests.java index 28358ed4b2..fb31916f7f 100644 --- a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TemporalSubsetSimulationTests.java +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TemporalSubsetSimulationTests.java @@ -13,8 +13,6 @@ import java.util.Optional; import java.util.TreeMap; -import static gov.nasa.jpl.aerie.merlin.driver.AnchorSimulationTest.AnchorsSimulationDriverTests.AnchorTestModel; -import static gov.nasa.jpl.aerie.merlin.driver.AnchorSimulationTest.AnchorsSimulationDriverTests.modelTopicList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -117,11 +115,11 @@ public void simulateFirstHalf(){ unfinishedActivities, planStart, fiveDays, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart, fiveDays, @@ -169,11 +167,11 @@ public void simulateSecondHalf(){ Map.of(), //The last activity starts an hour before the simulation ends planStart.plus(5, ChronoUnit.DAYS), fiveDays, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart.plus(5, ChronoUnit.DAYS), fiveDays, @@ -239,11 +237,11 @@ void simulateMiddle() { unfinishedActivities, planStart.plus(3, ChronoUnit.DAYS), fiveDays, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart.plus(3, ChronoUnit.DAYS), fiveDays, @@ -303,11 +301,11 @@ void simulateBeforePlanStart() { unfinishedActivities, planStart.plus(-2, ChronoUnit.DAYS), fiveDays, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart.plus(-2, ChronoUnit.DAYS), fiveDays, @@ -356,11 +354,11 @@ void simulateAfterPlanEnd() { unfinishedActivities, planStart.plus(8, ChronoUnit.DAYS), fiveDays, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart.plus(8, ChronoUnit.DAYS), fiveDays, @@ -597,11 +595,11 @@ void simulateAroundAnchors() { unfinishedActivities, planStart.plus(3, ChronoUnit.HOURS), fourAndAHalfHours, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart.plus(3, ChronoUnit.HOURS), fourAndAHalfHours, @@ -758,11 +756,11 @@ void simulateStartBetweenAnchors() { unfinishedActivities, planStart.plus(3, ChronoUnit.HOURS), fourAndAHalfHours, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart.plus(3, ChronoUnit.HOURS), fourAndAHalfHours, @@ -953,11 +951,11 @@ void simulateEndBetweenAnchors() { unfinishedActivities, planStart.plus(3, ChronoUnit.HOURS), fourAndAHalfHours, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart.plus(3, ChronoUnit.HOURS), fourAndAHalfHours, @@ -998,11 +996,11 @@ void simulateNoDuration() { unfinishedActivities, planStart.plus(12, ChronoUnit.HOURS), Duration.ZERO, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart.plus(12, ChronoUnit.HOURS), Duration.ZERO, diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TestMissionModel.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TestMissionModel.java new file mode 100644 index 0000000000..ce6a6ed600 --- /dev/null +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TestMissionModel.java @@ -0,0 +1,212 @@ +package gov.nasa.jpl.aerie.merlin.driver; + +import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; +import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; +import gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType; +import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; +import gov.nasa.jpl.aerie.merlin.protocol.model.ModelType; +import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; +import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; +import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; +import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; +import org.apache.commons.lang3.tuple.Triple; + +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.function.Function; +public class TestMissionModel { + private final static Duration oneMinute = Duration.of(60, Duration.SECONDS); + private static final Topic delayedActivityDirectiveInputTopic = new Topic<>(); + private static final Topic delayedActivityDirectiveOutputTopic = new Topic<>(); + + static List> getModelTopicList() { + return Arrays.asList( + Triple.of(0, "ActivityType.Input.DelayActivityDirective", new ValueSchema.StructSchema(Map.of())), + Triple.of(1, "ActivityType.Output.DelayActivityDirective", new ValueSchema.StructSchema(Map.of())), + Triple.of(2, "ActivityType.Input.DecomposingActivityDirective", new ValueSchema.StructSchema(Map.of())), + Triple.of(3, "ActivityType.Output.DecomposingActivityDirective", new ValueSchema.StructSchema(Map.of()))); + } + + /* package-private*/ static final DirectiveType delayedActivityDirective = new DirectiveType<>() { + @Override + public InputType getInputType() { + return testModelInputType; + } + + @Override + public OutputType getOutputType() { + return testModelOutputType; + } + + @Override + public TaskFactory getTaskFactory(final Object o, final Object o2) { + return executor -> oneShotTask($ -> { + $.emit(this, delayedActivityDirectiveInputTopic); + return TaskStatus.delayed(oneMinute, oneShotTask($$ -> { + $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); + return TaskStatus.completed(Unit.UNIT); + })); + }); + } + }; + + private static final Topic decomposingActivityDirectiveInputTopic = new Topic<>(); + private static final Topic decomposingActivityDirectiveOutputTopic = new Topic<>(); + /* package-private */ static final DirectiveType decomposingActivityDirective = new DirectiveType<>() { + @Override + public InputType getInputType() { + return testModelInputType; + } + + @Override + public OutputType getOutputType() { + return testModelOutputType; + } + + @Override + public TaskFactory getTaskFactory(final Object o, final Object o2) { + return executor -> oneShotTask(scheduler -> { + scheduler.emit(this, decomposingActivityDirectiveInputTopic); + return TaskStatus.delayed( + Duration.ZERO, + oneShotTask($ -> { + try { + $.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); + } catch (final InstantiationException ex) { + throw new Error("Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( + ex.toString())); + } + return TaskStatus.delayed(Duration.of(120, Duration.SECOND), oneShotTask($$ -> { + try { + $$.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); + } catch (final InstantiationException ex) { + throw new Error( + "Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( + ex.toString())); + } + $$.emit(Unit.UNIT, decomposingActivityDirectiveOutputTopic); + return TaskStatus.completed(Unit.UNIT); + })); + })); + }); + } + }; + + private static Task oneShotTask(Function> f) { + return new Task<>() { + @Override + public TaskStatus step(final Scheduler scheduler) { + return f.apply(scheduler); + } + + @Override + public Task duplicate(Executor executor) { + return this; + } + }; + } + + private static final InputType testModelInputType = new InputType<>() { + @Override + public List getParameters() { + return List.of(); + } + + @Override + public List getRequiredParameters() { + return List.of(); + } + + @Override + public Object instantiate(final Map arguments) { + return new Object(); + } + + @Override + public Map getArguments(final Object value) { + return Map.of(); + } + + @Override + public List getValidationFailures(final Object value) { + return List.of(); + } + }; + + private static final OutputType testModelOutputType = new OutputType<>() { + @Override + public ValueSchema getSchema() { + return ValueSchema.ofStruct(Map.of()); + } + + @Override + public SerializedValue serialize(final Object value) { + return SerializedValue.of(Map.of()); + } + }; + + public static MissionModel missionModel() { + return new MissionModel<>( + new Object(), + new LiveCells(new TemporalEventSource()), + Map.of(), + List.of( + new MissionModel.SerializableTopic<>( + "ActivityType.Input.DelayActivityDirective", + delayedActivityDirectiveInputTopic, + testModelOutputType), + new MissionModel.SerializableTopic<>( + "ActivityType.Output.DelayActivityDirective", + delayedActivityDirectiveOutputTopic, + testModelOutputType), + new MissionModel.SerializableTopic<>( + "ActivityType.Input.DecomposingActivityDirective", + decomposingActivityDirectiveInputTopic, + testModelOutputType), + new MissionModel.SerializableTopic<>( + "ActivityType.Output.DecomposingActivityDirective", + decomposingActivityDirectiveOutputTopic, + testModelOutputType)), + List.of(), + DirectiveTypeRegistry.extract( + new ModelType<>() { + + @Override + public Map> getDirectiveTypes() { + return Map.of( + "DelayActivityDirective", + delayedActivityDirective, + "DecomposingActivityDirective", + decomposingActivityDirective); + } + + @Override + public InputType getConfigurationType() { + return testModelInputType; + } + + @Override + public Object instantiate( + final Instant planStart, + final Object configuration, + final Initializer builder) + { + return new Object(); + } + } + ) + ); + } +} From 1a05eb6bef5dc449a8694463976052b453271a3f Mon Sep 17 00:00:00 2001 From: maillard Date: Fri, 15 Mar 2024 15:41:33 -0700 Subject: [PATCH 35/43] Move OneShotTask in itw own class --- .../SimulationDuplicationTest.java | 40 +- .../driver/CheckpointSimulationDriver.java | 138 +++--- .../jpl/aerie/merlin/driver/OneStepTask.java | 20 + .../driver/SimulationDuplicationTest.java | 5 +- .../aerie/merlin/driver/TestMissionModel.java | 29 +- .../model/SchedulingActivityDirective.java | 1 + .../CheckpointSimulationFacade.java | 295 ++++++++++++ .../ResourceAwareSpreadCheckpointPolicy.java | 6 +- .../simulation/ResumableSimulationDriver.java | 1 + .../simulation/SimulationFacade.java | 433 ++---------------- .../simulation/SimulationFacadeUtils.java | 168 +++++++ .../aerie/scheduler/FixedDurationTest.java | 6 +- .../scheduler/ParametricDurationTest.java | 6 +- .../aerie/scheduler/PrioritySolverTest.java | 23 +- .../aerie/scheduler/SimulationFacadeTest.java | 3 +- .../aerie/scheduler/SimulationUtility.java | 10 +- .../simulation/AnchorSchedulerTest.java | 29 +- .../CheckpointSimulationFacadeTest.java | 105 ++--- .../simulation/InstantiateArgumentsTest.java | 23 +- .../server/services/GraphQLMerlinService.java | 6 +- .../server/services/MerlinService.java | 7 +- .../services/SynchronousSchedulerAgent.java | 36 +- .../worker/services/MockMerlinService.java | 2 +- .../SchedulingDSLCompilationServiceTests.java | 4 +- 24 files changed, 715 insertions(+), 681 deletions(-) create mode 100644 merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/OneStepTask.java create mode 100644 scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacade.java create mode 100644 scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacadeUtils.java diff --git a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java index 38b26a7dcc..69a845d67f 100644 --- a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java +++ b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java @@ -9,20 +9,20 @@ import gov.nasa.jpl.aerie.merlin.driver.MissionModelBuilder; import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; +import gov.nasa.jpl.aerie.merlin.driver.OneStepTask; import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; +import gov.nasa.jpl.aerie.merlin.driver.SimulationResultsComputerInputs; import gov.nasa.jpl.aerie.merlin.driver.timeline.CausalEventSource; import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.framework.ThreadedTask; import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType; import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; import gov.nasa.jpl.aerie.merlin.protocol.model.ModelType; import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; -import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; @@ -43,8 +43,6 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; -import java.util.concurrent.Executor; -import java.util.function.Function; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTE; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTES; @@ -395,7 +393,7 @@ static SimulationResults simulateWithCheckpoints( final CachedEngineStore cachedEngineStore, final SimulationEngineConfiguration simulationEngineConfiguration ) { - return CheckpointSimulationDriver.computeResults(CheckpointSimulationDriver.simulateWithCheckpoints( + return SimulationResultsComputerInputs.computeResults(CheckpointSimulationDriver.simulateWithCheckpoints( missionModel, schedule, Instant.EPOCH, @@ -408,7 +406,8 @@ static SimulationResults simulateWithCheckpoints( CheckpointSimulationDriver.desiredCheckpoints(desiredCheckpoints), CheckpointSimulationDriver.noCondition(), cachedEngineStore, - simulationEngineConfiguration)); + simulationEngineConfiguration, + false)); } static SimulationResults simulateWithCheckpoints( @@ -418,7 +417,7 @@ static SimulationResults simulateWithCheckpoints( final CachedEngineStore cachedEngineStore, final SimulationEngineConfiguration simulationEngineConfiguration ) { - return CheckpointSimulationDriver.computeResults(CheckpointSimulationDriver.simulateWithCheckpoints( + return SimulationResultsComputerInputs.computeResults(CheckpointSimulationDriver.simulateWithCheckpoints( missionModel, schedule, Instant.EPOCH, @@ -431,7 +430,8 @@ static SimulationResults simulateWithCheckpoints( CheckpointSimulationDriver.desiredCheckpoints(desiredCheckpoints), CheckpointSimulationDriver.noCondition(), cachedEngineStore, - simulationEngineConfiguration)); + simulationEngineConfiguration, + false)); } private static final Topic delayedActivityDirectiveInputTopic = new Topic<>(); @@ -489,9 +489,9 @@ public OutputType getOutputType() { @Override public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> oneShotTask($ -> { + return executor -> new OneStepTask<>($ -> { $.emit(this, delayedActivityDirectiveInputTopic); - return TaskStatus.delayed(Duration.MINUTE, oneShotTask($$ -> { + return TaskStatus.delayed(Duration.MINUTE, new OneStepTask<>($$ -> { $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); return TaskStatus.completed(Unit.UNIT); })); @@ -499,20 +499,6 @@ public TaskFactory getTaskFactory(final Object o, final Object o2) { } }; - public static Task oneShotTask(Function> f) { - return new Task<>() { - @Override - public TaskStatus step(final Scheduler scheduler) { - return f.apply(scheduler); - } - - @Override - public Task duplicate(Executor executor) { - return this; - } - }; - } - private static final Topic decomposingActivityDirectiveInputTopic = new Topic<>(); private static final Topic decomposingActivityDirectiveOutputTopic = new Topic<>(); /* package-private */ static final DirectiveType decomposingActivityDirective = new DirectiveType<>() { @@ -528,18 +514,18 @@ public OutputType getOutputType() { @Override public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> oneShotTask(scheduler -> { + return executor -> new OneStepTask<>(scheduler -> { scheduler.emit(this, decomposingActivityDirectiveInputTopic); return TaskStatus.delayed( Duration.ZERO, - oneShotTask($ -> { + new OneStepTask<>($ -> { try { $.spawn(InSpan.Parent, delayedActivityDirective.getTaskFactory(null, null)); } catch (final InstantiationException ex) { throw new Error("Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( ex.toString())); } - return TaskStatus.delayed(Duration.of(120, Duration.SECOND), oneShotTask($$ -> { + return TaskStatus.delayed(Duration.of(120, Duration.SECOND), new OneStepTask<>($$ -> { try { $$.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); } catch (final InstantiationException ex) { diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java index 99650dc94a..52eb685377 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java @@ -3,12 +3,10 @@ import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; import gov.nasa.jpl.aerie.merlin.driver.engine.SlabList; import gov.nasa.jpl.aerie.merlin.driver.engine.SpanId; -import gov.nasa.jpl.aerie.merlin.driver.engine.TaskId; import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import org.apache.commons.lang3.function.TriFunction; import org.apache.commons.lang3.mutable.MutableLong; import org.apache.commons.lang3.mutable.MutableObject; import org.apache.commons.lang3.tuple.Pair; @@ -19,30 +17,22 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; -import java.util.function.BiFunction; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; import static gov.nasa.jpl.aerie.merlin.driver.SimulationDriver.scheduleActivities; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MAX_VALUE; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.min; public class CheckpointSimulationDriver { private static final Logger LOGGER = LoggerFactory.getLogger(CheckpointSimulationDriver.class); - public record SimulationResultsComputerInputs( - SimulationEngine engine, - Instant simulationStartTime, - Duration elapsedTime, - Topic activityTopic, - TemporalEventSource timeline, - Iterable> serializableTopics, - Map activityDirectiveIdTaskIdMap){} - public record CachedSimulationEngine( Duration endsAt, Map activityDirectives, @@ -52,7 +42,7 @@ public record CachedSimulationEngine( Topic activityTopic, MissionModel missionModel ) { - public CachedSimulationEngine { + public void freeze() { cells.freeze(); timePoints.freeze(); simulationEngine.close(); @@ -80,8 +70,7 @@ public static CachedSimulationEngine empty(final MissionModel missionModel) { timeline.add(commit); } } - - return new CachedSimulationEngine( + final var emptyCachedEngine = new CachedSimulationEngine( Duration.MIN_VALUE, Map.of(), engine, @@ -90,6 +79,7 @@ public static CachedSimulationEngine empty(final MissionModel missionModel) { new Topic<>(), missionModel ); + return emptyCachedEngine; } } @@ -129,7 +119,7 @@ public static Optional System.out.println("Re-using simulation engine at " + bestCandidate.ifPresent(cachedSimulationEngine -> LOGGER.info("Re-using simulation engine at " + cachedSimulationEngine.endsAt())); return bestCandidate.map(cachedSimulationEngine -> Pair.of(cachedSimulationEngine, correspondenceMap)); } @@ -168,10 +158,10 @@ private static TemporalEventSource makeCombinedTimeline(List desiredCheckpoints(final List desiredCheckpoints) { - return (elapsedTime, nextTime) -> { + public static Function desiredCheckpoints(final List desiredCheckpoints) { + return simulationState -> { for (final var desiredCheckpoint : desiredCheckpoints) { - if (elapsedTime.noLongerThan(desiredCheckpoint) && nextTime.longerThan(desiredCheckpoint)) { + if (simulationState.currentTime().noLongerThan(desiredCheckpoint) && simulationState.nextTime().longerThan(desiredCheckpoint)) { return true; } } @@ -179,13 +169,13 @@ public static BiFunction desiredCheckpoints(final L }; } - public static BiFunction wallClockCheckpoints(final long thresholdSeconds) { + public static Function wallClockCheckpoints(final long thresholdSeconds) { MutableLong lastCheckpointRealTime = new MutableLong(System.nanoTime()); MutableObject lastCheckpointSimTime = new MutableObject<>(Duration.ZERO); - return (elapsedTime, nextTime) -> { - if (nextTime.longerThan(elapsedTime) && System.nanoTime() - lastCheckpointRealTime.getValue() > (thresholdSeconds * 1000 * 1000 * 1000)) { + return simulationState -> { + if (simulationState.nextTime().longerThan(simulationState.currentTime()) && System.nanoTime() - lastCheckpointRealTime.getValue() > (thresholdSeconds * 1000 * 1000 * 1000)) { lastCheckpointRealTime.setValue(System.nanoTime()); - lastCheckpointSimTime.setValue(elapsedTime); + lastCheckpointSimTime.setValue(simulationState.currentTime()); return true; } else { return false; @@ -193,6 +183,18 @@ public static BiFunction wallClockCheckpoints(final }; } + public static Function checkpointAtEnd(Function stoppingCondition) { + return simulationState -> stoppingCondition.apply(simulationState) || simulationState.nextTime.isEqualTo(MAX_VALUE); + } + + public record SimulationState( + Duration currentTime, + Duration nextTime, + SimulationEngine simulationEngine, + Map schedule, + Map activityDirectiveIdSpanIdMap + ){} + public static SimulationResultsComputerInputs simulateWithCheckpoints( final MissionModel missionModel, final Map schedule, @@ -203,17 +205,18 @@ public static SimulationResultsComputerInputs simulateWithCheckpoints( final Consumer simulationExtentConsumer, final Supplier simulationCanceled, final CachedSimulationEngine cachedEngine, - final BiFunction shouldTakeCheckpoint, - final TriFunction, Map, Boolean> stopConditionOnPlan, + final Function shouldTakeCheckpoint, + final Function stopConditionOnPlan, final CachedEngineStore cachedEngineStore, - final SimulationEngineConfiguration configuration) { - final var activityToSpan = new HashMap(); + final SimulationEngineConfiguration configuration, + final boolean avoidDuplication) { + final var activityToSpan = new LinkedHashMap(); final var activityTopic = cachedEngine.activityTopic(); final var timelines = new ArrayList(); timelines.add(new TemporalEventSource(cachedEngine.timePoints)); - var engine = cachedEngine.simulationEngine.duplicate(); + var engine = avoidDuplication ? cachedEngine.simulationEngine : cachedEngine.simulationEngine.duplicate(); engine.unscheduleAfter(cachedEngine.endsAt); - try (var ignored = cachedEngine.simulationEngine) { + var timeline = new TemporalEventSource(); var cells = new LiveCells(timeline, cachedEngine.cells()); /* The current real time. */ @@ -261,26 +264,28 @@ public static SimulationResultsComputerInputs simulateWithCheckpoints( // TERMINATION: Actually, we might never break if real time never progresses forward. while (elapsedTime.noLongerThan(simulationDuration) && !simulationCanceled.get()) { final var nextTime = engine.peekNextTime().orElse(Duration.MAX_VALUE); - if (shouldTakeCheckpoint.apply(elapsedTime, nextTime)) { - cells.freeze(); + if (shouldTakeCheckpoint.apply(new SimulationState(elapsedTime, nextTime, engine, schedule, activityToSpan))) { + if(!avoidDuplication) cells.freeze(); LOGGER.info("Saving a simulation engine in memory"); - cachedEngineStore.save( - new CachedSimulationEngine( + final var newCachedEngine = new CachedSimulationEngine( elapsedTime, schedule, engine, cells, makeCombinedTimeline(timelines, timeline).points(), activityTopic, - missionModel), + missionModel); + if(!avoidDuplication) newCachedEngine.freeze(); + cachedEngineStore.save( + newCachedEngine, configuration); timelines.add(timeline); - engine = engine.duplicate(); + engine = avoidDuplication ? engine : engine.duplicate(); timeline = new TemporalEventSource(); cells = new LiveCells(timeline, cells); } //break before changing the state of the engine - if (simulationCanceled.get() || stopConditionOnPlan.apply(engine, schedule, activityToSpan)) { + if (simulationCanceled.get() || stopConditionOnPlan.apply(new SimulationState(elapsedTime, nextTime, engine, schedule, activityToSpan))) { break; } @@ -306,7 +311,7 @@ public static SimulationResultsComputerInputs simulateWithCheckpoints( } catch (Throwable ex) { throw new SimulationException(elapsedTime, simulationStartTime, ex); } - + if(!avoidDuplication) engine.close(); return new SimulationResultsComputerInputs( engine, simulationStartTime, @@ -315,62 +320,21 @@ public static SimulationResultsComputerInputs simulateWithCheckpoints( makeCombinedTimeline(timelines, timeline), missionModel.getTopics(), activityToSpan); - } } - public static TriFunction,Map, Boolean> - stopOnceAllActivitiessAreFinished(){ - return (engine, schedule, actIdToTaskId) -> actIdToTaskId + public static Function stopOnceAllActivitiessAreFinished(){ + return simulationState -> simulationState.activityDirectiveIdSpanIdMap() .values() .stream() - .allMatch(engine::spanIsComplete); - } - - public static TriFunction,Map, Boolean> - noCondition(){ - return (engine, schedule, actIdToSpanId) -> false; - } - - public static TriFunction,Map, Boolean> - stopOnceActivityHasFinished(final ActivityDirectiveId activityDirectiveId){ - return (engine, schedule, actIdToSpanId) -> (actIdToSpanId.containsKey(activityDirectiveId) - && engine.spanIsComplete(actIdToSpanId.get(activityDirectiveId))); - } - - public static SimulationResults computeResults( - final SimulationResultsComputerInputs simulationResultsInputs, - final Set resourceNames){ - return SimulationEngine.computeResults( - simulationResultsInputs.engine(), - simulationResultsInputs.simulationStartTime(), - simulationResultsInputs.elapsedTime(), - simulationResultsInputs.activityTopic(), - simulationResultsInputs.timeline(), - simulationResultsInputs.serializableTopics(), - resourceNames - ); + .allMatch(simulationState.simulationEngine()::spanIsComplete); } - public static SimulationResults computeResults( - final SimulationResultsComputerInputs simulationResultsInputs){ - return SimulationEngine.computeResults( - simulationResultsInputs.engine(), - simulationResultsInputs.simulationStartTime(), - simulationResultsInputs.elapsedTime(), - simulationResultsInputs.activityTopic(), - simulationResultsInputs.timeline(), - simulationResultsInputs.serializableTopics() - ); + public static Function noCondition(){ + return simulationState -> false; } - public static SimulationEngine.SimulationActivityExtract computeActivitySimulationResults( - final SimulationResultsComputerInputs simulationResultsInputs){ - return SimulationEngine.computeActivitySimulationResults( - simulationResultsInputs.engine(), - simulationResultsInputs.simulationStartTime(), - simulationResultsInputs.elapsedTime(), - simulationResultsInputs.activityTopic(), - simulationResultsInputs.timeline(), - simulationResultsInputs.serializableTopics()); + public static Function stopOnceActivityHasFinished(final ActivityDirectiveId activityDirectiveId){ + return simulationState -> (simulationState.activityDirectiveIdSpanIdMap().containsKey(activityDirectiveId) + && simulationState.simulationEngine.spanIsComplete(simulationState.activityDirectiveIdSpanIdMap().get(activityDirectiveId))); } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/OneStepTask.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/OneStepTask.java new file mode 100644 index 0000000000..5e0f8087e2 --- /dev/null +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/OneStepTask.java @@ -0,0 +1,20 @@ +package gov.nasa.jpl.aerie.merlin.driver; + +import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; +import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; + +import java.util.concurrent.Executor; +import java.util.function.Function; + +public record OneStepTask(Function> f) implements Task { + @Override + public TaskStatus step(final Scheduler scheduler) { + return f.apply(scheduler); + } + + @Override + public Task duplicate(Executor executor) { + return this; + } +} diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java index e179c1a4c9..b85a2006fc 100644 --- a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java @@ -112,7 +112,7 @@ static SimulationResults simulateWithCheckpoints( final List desiredCheckpoints, final CachedEngineStore engineStore ) { - return CheckpointSimulationDriver.computeResults(CheckpointSimulationDriver.simulateWithCheckpoints( + return SimulationResultsComputerInputs.computeResults(CheckpointSimulationDriver.simulateWithCheckpoints( TestMissionModel.missionModel(), Map.of(), Instant.EPOCH, @@ -125,7 +125,8 @@ static SimulationResults simulateWithCheckpoints( CheckpointSimulationDriver.desiredCheckpoints(desiredCheckpoints), CheckpointSimulationDriver.noCondition(), engineStore, - mockConfiguration() + mockConfiguration(), + false )); } } diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TestMissionModel.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TestMissionModel.java index ce6a6ed600..7f71ba267c 100644 --- a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TestMissionModel.java +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TestMissionModel.java @@ -3,13 +3,11 @@ import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType; import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; import gov.nasa.jpl.aerie.merlin.protocol.model.ModelType; import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; -import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; @@ -24,8 +22,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.concurrent.Executor; -import java.util.function.Function; + public class TestMissionModel { private final static Duration oneMinute = Duration.of(60, Duration.SECONDS); private static final Topic delayedActivityDirectiveInputTopic = new Topic<>(); @@ -52,9 +49,9 @@ public OutputType getOutputType() { @Override public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> oneShotTask($ -> { + return executor -> new OneStepTask<>($ -> { $.emit(this, delayedActivityDirectiveInputTopic); - return TaskStatus.delayed(oneMinute, oneShotTask($$ -> { + return TaskStatus.delayed(oneMinute, new OneStepTask<>($$ -> { $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); return TaskStatus.completed(Unit.UNIT); })); @@ -77,18 +74,18 @@ public OutputType getOutputType() { @Override public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> oneShotTask(scheduler -> { + return executor -> new OneStepTask<>(scheduler -> { scheduler.emit(this, decomposingActivityDirectiveInputTopic); return TaskStatus.delayed( Duration.ZERO, - oneShotTask($ -> { + new OneStepTask<>($ -> { try { $.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); } catch (final InstantiationException ex) { throw new Error("Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( ex.toString())); } - return TaskStatus.delayed(Duration.of(120, Duration.SECOND), oneShotTask($$ -> { + return TaskStatus.delayed(Duration.of(120, Duration.SECOND), new OneStepTask<>($$ -> { try { $$.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); } catch (final InstantiationException ex) { @@ -104,20 +101,6 @@ public TaskFactory getTaskFactory(final Object o, final Object o2) { } }; - private static Task oneShotTask(Function> f) { - return new Task<>() { - @Override - public TaskStatus step(final Scheduler scheduler) { - return f.apply(scheduler); - } - - @Override - public Task duplicate(Executor executor) { - return this; - } - }; - } - private static final InputType testModelInputType = new InputType<>() { @Override public List getParameters() { diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingActivityDirective.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingActivityDirective.java index 22b56018f1..87550b9011 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingActivityDirective.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingActivityDirective.java @@ -288,6 +288,7 @@ public boolean equalsInProperties(final SchedulingActivityDirective that){ && duration.isEqualTo(that.duration) && startOffset.isEqualTo(that.startOffset) && arguments.equals(that.arguments) + && Objects.equals(topParent, that.topParent) && Objects.equals(anchorId, that.anchorId) && (anchoredToStart == that.anchoredToStart); } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacade.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacade.java new file mode 100644 index 0000000000..db32850ea1 --- /dev/null +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacade.java @@ -0,0 +1,295 @@ +package gov.nasa.jpl.aerie.scheduler.simulation; + +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; +import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; +import gov.nasa.jpl.aerie.merlin.driver.SimulationResultsComputerInputs; +import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.scheduler.SchedulingInterruptedException; +import gov.nasa.jpl.aerie.scheduler.model.ActivityType; +import gov.nasa.jpl.aerie.scheduler.model.Plan; +import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; +import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Instant; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +import static gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver.checkpointAtEnd; +import static gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacadeUtils.scheduleFromPlan; +import static gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacadeUtils.updatePlanWithChildActivities; + +public class CheckpointSimulationFacade implements SimulationFacade { + private static final Logger LOGGER = LoggerFactory.getLogger(CheckpointSimulationFacade.class); + private final MissionModel missionModel; + private final InMemoryCachedEngineStore cachedEngines; + private final PlanningHorizon planningHorizon; + private final Map activityTypes; + private final SimulationEngineConfiguration configuration; + private SimulationData initialSimulationResults; + private final Supplier canceledListener; + private final SchedulerModel schedulerModel; + private Duration totalSimulationTime = Duration.ZERO; + private boolean resumableSimulationBehavior; + + /** + * Loads initial simulation results into the simulation. They will be served until initialSimulationResultsAreStale() + * is called. + * @param simulationData the initial simulation results + */ + @Override + public void setInitialSimResults(final SimulationData simulationData){ + this.initialSimulationResults = simulationData; + } + + + public CheckpointSimulationFacade( + final MissionModel missionModel, + final SchedulerModel schedulerModel, + final InMemoryCachedEngineStore cachedEngines, + final PlanningHorizon planningHorizon, + final SimulationEngineConfiguration simulationEngineConfiguration, + final Supplier canceledListener){ + this.missionModel = missionModel; + this.schedulerModel = schedulerModel; + this.cachedEngines = cachedEngines; + this.planningHorizon = planningHorizon; + this.activityTypes = new HashMap<>(); + this.configuration = simulationEngineConfiguration; + this.canceledListener = canceledListener; + this.resumableSimulationBehavior = cachedEngines.capacity() == 1; + } + + /** + * Returns the total simulated time + * @return + */ + @Override + public Duration totalSimulationTime(){ + return totalSimulationTime; + } + + public CheckpointSimulationFacade( + final PlanningHorizon planningHorizon, + final MissionModel missionModel, + final SchedulerModel schedulerModel + ){ + this.missionModel = missionModel; + this.cachedEngines = new InMemoryCachedEngineStore(0); + this.planningHorizon = planningHorizon; + this.activityTypes = new HashMap<>(); + this.configuration = new SimulationEngineConfiguration(Map.of(), Instant.now(), new MissionModelId(1)); + this.canceledListener = () -> false; + this.schedulerModel = schedulerModel; + } + + @Override + public Supplier getCanceledListener(){ + return this.canceledListener; + } + + @Override + public void addActivityTypes(final Collection activityTypes){ + activityTypes.forEach(at -> this.activityTypes.put(at.getName(), at)); + } + + private void replaceValue(final Map map, final V value, final V replacement){ + for (final Map.Entry entry : map.entrySet()) { + if (entry.getValue().equals(value)) { + entry.setValue(replacement); + break; + } + } + } + + private void replaceIds( + final PlanSimCorrespondence planSimCorrespondence, + final Map updates){ + for(final var replacements : updates.entrySet()){ + replaceValue(planSimCorrespondence.planActDirectiveIdToSimulationActivityDirectiveId(),replacements.getKey(), replacements.getValue()); + if(planSimCorrespondence.directiveIdActivityDirectiveMap().containsKey(replacements.getKey())){ + final var value = planSimCorrespondence.directiveIdActivityDirectiveMap().remove(replacements.getKey()); + planSimCorrespondence.directiveIdActivityDirectiveMap().put(replacements.getValue(), value); + } + } + } + + /** + * Simulates until the end of the last activity of a plan. Updates the input plan with child activities and activity durations. + * @param plan the plan to simulate + * @return the inputs needed to compute simulation results + * @throws SimulationException if an exception happens during simulation + */ + @Override + public SimulationResultsComputerInputs simulateNoResultsUntilEndPlan(final Plan plan) + throws SimulationException, SchedulingInterruptedException + { + return simulateNoResults(plan, planningHorizon.getEndAerie(), null).simulationResultsComputerInputs(); + } + + /** + * Simulates a plan until the end of one of its activities + * Do not use to update the plan as decomposing activities may not finish + * @param plan + * @param activity + * @return + * @throws SimulationException + */ + + @Override + public SimulationResultsComputerInputs simulateNoResultsUntilEndAct( + final Plan plan, + final SchedulingActivityDirective activity) throws SimulationException, SchedulingInterruptedException + { + return simulateNoResults(plan, null, activity).simulationResultsComputerInputs(); + } + + public AugmentedSimulationResultsComputerInputs simulateNoResults( + final Plan plan, + final Duration until) throws SimulationException, SchedulingInterruptedException + { + return simulateNoResults(plan, until, null); + } + + + /** + * Simulates and updates plan + * @param plan + * @param until can be null + * @param activity can be null + */ + private AugmentedSimulationResultsComputerInputs simulateNoResults( + final Plan plan, + final Duration until, + final SchedulingActivityDirective activity) throws SimulationException, SchedulingInterruptedException + { + final var planSimCorrespondence = scheduleFromPlan(plan, this.schedulerModel); + + final var best = CheckpointSimulationDriver.bestCachedEngine( + planSimCorrespondence.directiveIdActivityDirectiveMap(), + cachedEngines.getCachedEngines(configuration)); + CheckpointSimulationDriver.CachedSimulationEngine engine = null; + Duration from = Duration.ZERO; + if(best.isPresent()){ + engine = best.get().getKey(); + replaceIds(planSimCorrespondence, best.get().getRight()); + from = engine.endsAt(); + } + + //Configuration + //Three modes : (1) until a specific end time (2) until end of one specific activity (3) until end of last activity in plan + Duration simulationDuration; + Function + stoppingCondition; + //(1) + if(until != null && activity == null){ + simulationDuration = until; + stoppingCondition = CheckpointSimulationDriver.noCondition(); + LOGGER.info("Simulation mode: until specific time " + simulationDuration); + } + //(2) + else if(activity != null && until == null){ + simulationDuration = planningHorizon.getEndAerie(); + stoppingCondition = CheckpointSimulationDriver.stopOnceActivityHasFinished(planSimCorrespondence.planActDirectiveIdToSimulationActivityDirectiveId().get(activity)); + LOGGER.info("Simulation mode: until activity ends " + activity); + //(3) + } else if(activity == null && until == null){ + simulationDuration = planningHorizon.getEndAerie(); + stoppingCondition = CheckpointSimulationDriver.stopOnceAllActivitiessAreFinished(); + LOGGER.info("Simulation mode: until all activities end "); + } else { + throw new SimulationException("Bad configuration", null); + } + + if(engine == null) engine = CheckpointSimulationDriver.CachedSimulationEngine.empty(missionModel); + + if(best.isPresent()) cachedEngines.registerUsed(engine); + try { + final var simulation = CheckpointSimulationDriver.simulateWithCheckpoints( + missionModel, + planSimCorrespondence.directiveIdActivityDirectiveMap(), + planningHorizon.getStartInstant(), + simulationDuration, + planningHorizon.getStartInstant(), + planningHorizon.getEndAerie(), + $ -> {}, + canceledListener, + engine, + resumableSimulationBehavior ? checkpointAtEnd(stoppingCondition): new ResourceAwareSpreadCheckpointPolicy( + cachedEngines.capacity(), + Duration.ZERO, + planningHorizon.getEndAerie(), + Duration.max(engine.endsAt(), Duration.ZERO), + simulationDuration, + 1, + true), + stoppingCondition, + cachedEngines, + configuration, + resumableSimulationBehavior); + if(canceledListener.get()) throw new SchedulingInterruptedException("simulating"); + this.totalSimulationTime = this.totalSimulationTime.plus(simulation.elapsedTime().minus(from)); + final var activityResults = SimulationResultsComputerInputs.computeActivitySimulationResults(simulation); + + updatePlanWithChildActivities( + activityResults, + activityTypes, + plan, + planSimCorrespondence, + planningHorizon); + + SimulationFacadeUtils.pullActivityDurationsIfNecessary( + plan, + planSimCorrespondence, + activityResults + ); + //plan has been updated + return new AugmentedSimulationResultsComputerInputs(simulation, planSimCorrespondence); + } catch(Exception e){ + if(e instanceof SchedulingInterruptedException sie){ + throw sie; + } + throw new SimulationException("An exception happened during simulation", e); + } + } + + @Override + public SimulationData simulateWithResults( + final Plan plan, + final Duration until) throws SimulationException, SchedulingInterruptedException + { + return simulateWithResults(plan, until, missionModel.getResources().keySet()); + } + + @Override + public SimulationData simulateWithResults( + final Plan plan, + final Duration until, + final Set resourceNames) throws SimulationException, SchedulingInterruptedException + { + if(this.initialSimulationResults != null) { + final var inputPlan = scheduleFromPlan(plan, schedulerModel); + final var initialPlanA = scheduleFromPlan(this.initialSimulationResults.plan(), schedulerModel); + if (initialPlanA.equals(inputPlan)) { + return new SimulationData( + plan, + initialSimulationResults.driverResults(), + SimulationResultsConverter.convertToConstraintModelResults(initialSimulationResults.driverResults()), + this.initialSimulationResults.mapping()); + } + } + final var resultsInput = simulateNoResults(plan, until); + final var driverResults = SimulationResultsComputerInputs.computeResults(resultsInput.simulationResultsComputerInputs(), resourceNames); + return new SimulationData(plan, driverResults, SimulationResultsConverter.convertToConstraintModelResults(driverResults), resultsInput.planSimCorrespondence().planActDirectiveIdToSimulationActivityDirectiveId()); + } + +} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResourceAwareSpreadCheckpointPolicy.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResourceAwareSpreadCheckpointPolicy.java index 709c4e03c2..da265d2d83 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResourceAwareSpreadCheckpointPolicy.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResourceAwareSpreadCheckpointPolicy.java @@ -5,7 +5,7 @@ import java.util.ArrayList; import java.util.List; -import java.util.function.BiFunction; +import java.util.function.Function; /** * Policy for saving simulation checkpoints in a cache. @@ -39,7 +39,7 @@ public ResourceAwareSpreadCheckpointPolicy( } @Override - public Boolean apply(final Duration duration, final Duration duration2) { - return function.apply(duration, duration2); + public Boolean apply(final CheckpointSimulationDriver.SimulationState simulationState) { + return function.apply(simulationState); } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java index 872d87e4d6..3cae844eac 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java @@ -3,6 +3,7 @@ import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.OneStepTask; import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; import gov.nasa.jpl.aerie.merlin.driver.StartOffsetReducer; diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java index 64e5a6199a..cbae355dac 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java @@ -2,303 +2,65 @@ import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; -import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; -import gov.nasa.jpl.aerie.merlin.driver.MissionModel; -import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; -import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; -import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivity; -import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivityId; -import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; -import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; -import gov.nasa.jpl.aerie.merlin.driver.engine.SpanId; -import gov.nasa.jpl.aerie.merlin.driver.engine.TaskId; -import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; +import gov.nasa.jpl.aerie.merlin.driver.SimulationResultsComputerInputs; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; import gov.nasa.jpl.aerie.scheduler.SchedulingInterruptedException; import gov.nasa.jpl.aerie.scheduler.model.ActivityType; import gov.nasa.jpl.aerie.scheduler.model.Plan; -import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; import org.apache.commons.collections4.BidiMap; -import org.apache.commons.collections4.bidimap.DualHashBidiMap; -import org.apache.commons.lang3.function.TriFunction; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.time.Instant; import java.util.Collection; -import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Supplier; -public class SimulationFacade { - private static final Logger LOGGER = LoggerFactory.getLogger(SimulationFacade.class); - private final MissionModel missionModel; - private final InMemoryCachedEngineStore cachedEngines; - private final PlanningHorizon planningHorizon; - private final Map activityTypes; - private int itSimActivityId; - private final SimulationEngineConfiguration configuration; - private SimulationData initialSimulationResults; - private final Supplier canceledListener; - private final SchedulerModel schedulerModel; - private Duration totalSimulationTime = Duration.ZERO; +public interface SimulationFacade { + void setInitialSimResults(SimulationData simulationData); - /** - * Loads initial simulation results into the simulation. They will be served until initialSimulationResultsAreStale() - * is called. - * @param simulationData the initial simulation results - */ - public void setInitialSimResults(final SimulationData simulationData){ - this.initialSimulationResults = simulationData; - } + Duration totalSimulationTime(); + Supplier getCanceledListener(); - public SimulationFacade( - final MissionModel missionModel, - final SchedulerModel schedulerModel, - final InMemoryCachedEngineStore cachedEngines, - final PlanningHorizon planningHorizon, - final SimulationEngineConfiguration simulationEngineConfiguration, - final Supplier canceledListener){ - this.itSimActivityId = 0; - this.missionModel = missionModel; - this.schedulerModel = schedulerModel; - this.cachedEngines = cachedEngines; - this.planningHorizon = planningHorizon; - this.activityTypes = new HashMap<>(); - this.configuration = simulationEngineConfiguration; - this.canceledListener = canceledListener; - } + void addActivityTypes(Collection activityTypes); - /** - * Returns the total simulated time - * @return - */ - public Duration totalSimulationTime(){ - return totalSimulationTime; - } + SimulationResultsComputerInputs simulateNoResultsAllActivities(Plan plan) + throws SimulationException, SchedulingInterruptedException; - public SimulationFacade( - final PlanningHorizon planningHorizon, - final MissionModel missionModel, - final SchedulerModel schedulerModel - ){ - this.itSimActivityId = 0; - this.missionModel = missionModel; - this.cachedEngines = new InMemoryCachedEngineStore(0); - this.planningHorizon = planningHorizon; - this.activityTypes = new HashMap<>(); - this.configuration = new SimulationEngineConfiguration(Map.of(), Instant.now(), new MissionModelId(1)); - this.canceledListener = () -> false; - this.schedulerModel = schedulerModel; - } + SimulationResultsComputerInputs simulateNoResultsUntilEndAct( + Plan plan, + SchedulingActivityDirective activity) throws SimulationException, SchedulingInterruptedException; - public Supplier getCanceledListener(){ - return this.canceledListener; - } + AugmentedSimulationResultsComputerInputs simulateNoResults( + Plan plan, + Duration until) throws SimulationException, SchedulingInterruptedException; - public void addActivityTypes(final Collection activityTypes){ - activityTypes.forEach(at -> this.activityTypes.put(at.getName(), at)); - } + SimulationData simulateWithResults( + Plan plan, + Duration until) throws SimulationException, SchedulingInterruptedException; - private void replaceValue(final Map map, final V value, final V replacement){ - for (final Map.Entry entry : map.entrySet()) { - if (entry.getValue().equals(value)) { - entry.setValue(replacement); - break; - } - } - } + SimulationData simulateWithResults( + Plan plan, + Duration until, + Set resourceNames) throws SimulationException, SchedulingInterruptedException; - private void replaceIds( - final PlanSimCorrespondence planSimCorrespondence, - final Map updates){ - for(final var replacements : updates.entrySet()){ - replaceValue(planSimCorrespondence.planActDirectiveIdToSimulationActivityDirectiveId,replacements.getKey(), replacements.getValue()); - if(planSimCorrespondence.directiveIdActivityDirectiveMap.containsKey(replacements.getKey())){ - final var value = planSimCorrespondence.directiveIdActivityDirectiveMap.remove(replacements.getKey()); - planSimCorrespondence.directiveIdActivityDirectiveMap.put(replacements.getValue(), value); - } - } - } - - /** - * Simulates until the end of the last activity of a plan. Updates the input plan with child activities and activity durations. - * @param plan the plan to simulate - * @return the inputs needed to compute simulation results - * @throws SimulationException if an exception happens during simulation - */ - public CheckpointSimulationDriver.SimulationResultsComputerInputs simulateNoResultsUntilEndPlan(final Plan plan) - throws SimulationException, SchedulingInterruptedException - { - return simulateNoResults(plan, planningHorizon.getEndAerie(), null).simulationResultsComputerInputs(); - } + Optional getLatestSimulationData(); - public static class SimulationException extends Exception { + class SimulationException extends Exception { SimulationException(final String message, final Throwable cause) { super(message, cause); } } - /** - * Simulates a plan until the end of one of its activities - * Do not use to update the plan as decomposing activities may not finish - * @param plan - * @param activity - * @return - * @throws SimulationException - */ - - public CheckpointSimulationDriver.SimulationResultsComputerInputs simulateNoResultsUntilEndAct( - final Plan plan, - final SchedulingActivityDirective activity) throws SimulationException, SchedulingInterruptedException - { - return simulateNoResults(plan, null, activity).simulationResultsComputerInputs(); - } - - public record AugmentedSimulationResultsComputerInputs( - CheckpointSimulationDriver.SimulationResultsComputerInputs simulationResultsComputerInputs, - PlanSimCorrespondence planSimCorrespondence){} + record AugmentedSimulationResultsComputerInputs( + SimulationResultsComputerInputs simulationResultsComputerInputs, + SimulationFacade.PlanSimCorrespondence planSimCorrespondence + ) {} - public AugmentedSimulationResultsComputerInputs simulateNoResults( - final Plan plan, - final Duration until) throws SimulationException, SchedulingInterruptedException - { - return simulateNoResults(plan, until, null); - } - - - /** - * Simulates and updates plan - * @param plan - * @param until can be null - * @param activity can be null - */ - private AugmentedSimulationResultsComputerInputs simulateNoResults( - final Plan plan, - final Duration until, - final SchedulingActivityDirective activity) throws SimulationException, SchedulingInterruptedException - { - final var planSimCorrespondence = scheduleFromPlan(plan, planningHorizon.getEndAerie(), planningHorizon.getStartInstant(), planningHorizon.getStartInstant()); - - final var best = CheckpointSimulationDriver.bestCachedEngine( - planSimCorrespondence.directiveIdActivityDirectiveMap(), - cachedEngines.getCachedEngines(configuration)); - CheckpointSimulationDriver.CachedSimulationEngine engine = null; - Duration from = Duration.ZERO; - if(best.isPresent()){ - engine = best.get().getKey(); - replaceIds(planSimCorrespondence, best.get().getRight()); - from = engine.endsAt(); - } - - //Configuration - //Three modes : (1) until a specific end time (2) until end of one specific activity (3) until end of last activity in plan - Duration simulationDuration; - TriFunction, Map, Boolean> - stoppingCondition; - //(1) - if(until != null && activity == null){ - simulationDuration = until; - stoppingCondition = CheckpointSimulationDriver.noCondition(); - } - //(2) - else if(activity != null && until == null){ - simulationDuration = planningHorizon.getEndAerie(); - stoppingCondition = CheckpointSimulationDriver.stopOnceActivityHasFinished(planSimCorrespondence.planActDirectiveIdToSimulationActivityDirectiveId.get(activity)); - //(3) - } else if(activity == null && until == null){ - simulationDuration = planningHorizon.getEndAerie(); - stoppingCondition = CheckpointSimulationDriver.stopOnceAllActivitiessAreFinished(); - } else { - throw new SimulationException("Bad configuration", null); - } - - if(engine == null) engine = CheckpointSimulationDriver.CachedSimulationEngine.empty(missionModel); - - if(best.isPresent()) cachedEngines.registerUsed(engine); - try { - final var simulation = CheckpointSimulationDriver.simulateWithCheckpoints( - missionModel, - planSimCorrespondence.directiveIdActivityDirectiveMap(), - planningHorizon.getStartInstant(), - simulationDuration, - planningHorizon.getStartInstant(), - planningHorizon.getEndAerie(), - $ -> {}, - canceledListener, - engine, - new ResourceAwareSpreadCheckpointPolicy( - cachedEngines.capacity(), - Duration.ZERO, - planningHorizon.getEndAerie(), - Duration.max(engine.endsAt(), Duration.ZERO), - simulationDuration, - 1, - true), - stoppingCondition, - cachedEngines, - configuration); - if(canceledListener.get()) throw new SchedulingInterruptedException("simulating"); - this.totalSimulationTime = this.totalSimulationTime.plus(simulation.elapsedTime().minus(from)); - final var activityResults = - CheckpointSimulationDriver.computeActivitySimulationResults(simulation); - - updatePlanWithChildActivities( - activityResults, - activityTypes, - plan, - planSimCorrespondence); - pullActivityDurationsIfNecessary( - plan, - planSimCorrespondence, - activityResults - ); - //plan has been updated - return new AugmentedSimulationResultsComputerInputs(simulation, planSimCorrespondence); - } catch(Exception e){ - if(e instanceof SchedulingInterruptedException sie){ - throw sie; - } - throw new SimulationException("An exception happened during simulation", e); - } - } - - public SimulationData simulateWithResults( - final Plan plan, - final Duration until) throws SimulationException, SchedulingInterruptedException - { - return simulateWithResults(plan, until, missionModel.getResources().keySet()); - } - - public SimulationData simulateWithResults( - final Plan plan, - final Duration until, - final Set resourceNames) throws SimulationException, SchedulingInterruptedException - { - if(this.initialSimulationResults != null) { - final var inputPlan = scheduleFromPlan(plan, planningHorizon.getEndAerie(), planningHorizon.getStartInstant(), planningHorizon.getStartInstant()); - final var initialPlanA = scheduleFromPlan(this.initialSimulationResults.plan(), planningHorizon.getEndAerie(), planningHorizon.getStartInstant(), planningHorizon.getStartInstant()); - if (initialPlanA.equals(inputPlan)) { - return new SimulationData( - plan, - initialSimulationResults.driverResults(), - SimulationResultsConverter.convertToConstraintModelResults(initialSimulationResults.driverResults()), - this.initialSimulationResults.mapping()); - } - } - final var resultsInput = simulateNoResults(plan, until); - final var driverResults = CheckpointSimulationDriver.computeResults(resultsInput.simulationResultsComputerInputs, resourceNames); - return new SimulationData(plan, driverResults, SimulationResultsConverter.convertToConstraintModelResults(driverResults), resultsInput.planSimCorrespondence.planActDirectiveIdToSimulationActivityDirectiveId); - } - - private record PlanSimCorrespondence( + record PlanSimCorrespondence( BidiMap planActDirectiveIdToSimulationActivityDirectiveId, Map directiveIdActivityDirectiveMap){ @Override @@ -310,143 +72,4 @@ public boolean equals(Object other){ return false; } } - - private PlanSimCorrespondence scheduleFromPlan( - final Plan plan, - final Duration planDuration, - final Instant planStartTime, - final Instant simulationStartTime){ - final var activities = plan.getActivities(); - final var planActDirectiveIdToSimulationActivityDirectiveId = new DualHashBidiMap(); - if(activities.isEmpty()) return new PlanSimCorrespondence(new DualHashBidiMap<>(), Map.of()); - //filter out child activities - final var activitiesWithoutParent = activities.stream().filter(a -> a.topParent() == null).toList(); - final Map directivesToSimulate = new HashMap<>(); - - for(final var activity : activitiesWithoutParent){ - final var activityIdSim = new ActivityDirectiveId(itSimActivityId++); - planActDirectiveIdToSimulationActivityDirectiveId.put(activity.getId(), activityIdSim); - } - - for(final var activity : activitiesWithoutParent) { - final var activityDirective = schedulingActToActivityDir(activity, planActDirectiveIdToSimulationActivityDirectiveId); - directivesToSimulate.put( - planActDirectiveIdToSimulationActivityDirectiveId.get(activity.getId()), - activityDirective); - } - return new PlanSimCorrespondence(planActDirectiveIdToSimulationActivityDirectiveId, directivesToSimulate); - } - - /** - * For activities that have a null duration (in an initial plan for example) and that have been simulated, we pull the duration and - * replace the original instance with a new instance that includes the duration, both in the plan and the simulation facade - */ - private void pullActivityDurationsIfNecessary( - final Plan plan, - final PlanSimCorrespondence correspondence, - final SimulationEngine.SimulationActivityExtract activityExtract - ) { - final var toReplace = new HashMap(); - for (final var activity : plan.getActivities()) { - if (activity.duration() == null) { - final var activityDirective = findSimulatedActivityById( - activityExtract.simulatedActivities().values(), - correspondence.planActDirectiveIdToSimulationActivityDirectiveId.get(activity.getId())); - if (activityDirective.isPresent()) { - final var replacementAct = SchedulingActivityDirective.copyOf( - activity, - activityDirective.get().duration() - ); - toReplace.put(activity, replacementAct); - } - //if not, maybe the activity is not finished - } - } - toReplace.forEach(plan::replace); - } - - private final Optional findSimulatedActivityById(Collection simulatedActivities, final ActivityDirectiveId activityDirectiveId){ - return simulatedActivities.stream().filter(a -> a.directiveId().isPresent() && a.directiveId().get().equals(activityDirectiveId)).findFirst(); - } - - private void updatePlanWithChildActivities( - final SimulationEngine.SimulationActivityExtract activityExtract, - final Map activityTypes, - final Plan plan, - final PlanSimCorrespondence planSimCorrespondence) - { - //remove all activities with parents - final var toRemove = plan.getActivities().stream().filter(a -> a.topParent() != null).toList(); - toRemove.forEach(plan::remove); - //pull child activities - activityExtract.simulatedActivities().forEach( (activityInstanceId, activity) -> { - if (activity.parentId() == null) return; - final var rootParent = getIdOfRootParent(activityExtract, activityInstanceId); - if(rootParent.isPresent()) { - final var activityInstance = SchedulingActivityDirective.of( - activityTypes.get(activity.type()), - planningHorizon.toDur(activity.start()), - activity.duration(), - activity.arguments(), - planSimCorrespondence.planActDirectiveIdToSimulationActivityDirectiveId.getKey(rootParent.get()), - null, - true); - plan.add(activityInstance); - } - }); - //no need to replace in Evaluation because child activities are not referenced in it - } - - private static Optional getIdOfRootParent( - final SimulationEngine.SimulationActivityExtract results, - final SimulatedActivityId instanceId){ - if(!results.simulatedActivities().containsKey(instanceId)){ - if(!results.unfinishedActivities().containsKey(instanceId)){ - LOGGER.debug("The simulation of the parent of activity with id "+ instanceId.id() + " has been finished"); - } - return Optional.empty(); - } - final var act = results.simulatedActivities().get(instanceId); - if(act.parentId() == null){ - // SAFETY: any activity that has no parent must have a directive id. - return Optional.of(act.directiveId().get()); - } else { - return getIdOfRootParent(results, act.parentId()); - } - } - - private static Optional getActivityDuration( - final ActivityDirectiveId activityDirectiveId, - final CheckpointSimulationDriver.SimulationResultsComputerInputs simulationResultsInputs){ - return simulationResultsInputs.engine().getSpan(simulationResultsInputs.activityDirectiveIdTaskIdMap().get(activityDirectiveId)).duration(); - } - - private ActivityDirective schedulingActToActivityDir( - final SchedulingActivityDirective activity, - final Map planActDirectiveIdToSimulationActivityDirectiveId) { - if(activity.getParentActivity().isPresent()) { - throw new Error("This method should not be called with a generated activity but with its top-level parent."); - } - final var arguments = new HashMap<>(activity.arguments()); - if (activity.duration() != null) { - final var durationType = activity.getType().getDurationType(); - if (durationType instanceof DurationType.Controllable dt) { - arguments.put(dt.parameterName(), this.schedulerModel.serializeDuration(activity.duration())); - } else if ( - durationType instanceof DurationType.Uncontrollable - || durationType instanceof DurationType.Fixed - || durationType instanceof DurationType.Parametric - ) { - // If an activity has already been simulated, it will have a duration, even if its DurationType is Uncontrollable. - } else { - throw new Error("Unhandled variant of DurationType: " + durationType); - } - } - final var serializedActivity = new SerializedActivity(activity.getType().getName(), arguments); - return new ActivityDirective( - activity.startOffset(), - serializedActivity, - planActDirectiveIdToSimulationActivityDirectiveId.get(activity.anchorId()), - activity.anchoredToStart()); - } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacadeUtils.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacadeUtils.java new file mode 100644 index 0000000000..e1e38cb036 --- /dev/null +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacadeUtils.java @@ -0,0 +1,168 @@ +package gov.nasa.jpl.aerie.scheduler.simulation; + +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; +import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; +import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivity; +import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivityId; +import gov.nasa.jpl.aerie.merlin.driver.SimulationResultsComputerInputs; +import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; +import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; +import gov.nasa.jpl.aerie.scheduler.model.ActivityType; +import gov.nasa.jpl.aerie.scheduler.model.Plan; +import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; +import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; +import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; +import org.apache.commons.collections4.bidimap.DualHashBidiMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class SimulationFacadeUtils { + private static final Logger LOGGER = LoggerFactory.getLogger(SimulationFacadeUtils.class); + private static int itSimActivityId = 0; + + public static SimulationFacade.PlanSimCorrespondence scheduleFromPlan(final Plan plan, final SchedulerModel schedulerModel){ + final var activities = plan.getActivities(); + final var planActDirectiveIdToSimulationActivityDirectiveId = new DualHashBidiMap(); + if(activities.isEmpty()) return new SimulationFacade.PlanSimCorrespondence(new DualHashBidiMap<>(), Map.of()); + //filter out child activities + final var activitiesWithoutParent = activities.stream().filter(a -> a.topParent() == null).toList(); + final Map directivesToSimulate = new HashMap<>(); + + for(final var activity : activitiesWithoutParent){ + final var activityIdSim = new ActivityDirectiveId(itSimActivityId++); + planActDirectiveIdToSimulationActivityDirectiveId.put(activity.getId(), activityIdSim); + } + + for(final var activity : activitiesWithoutParent) { + final var activityDirective = schedulingActToActivityDir(activity, planActDirectiveIdToSimulationActivityDirectiveId, schedulerModel); + directivesToSimulate.put( + planActDirectiveIdToSimulationActivityDirectiveId.get(activity.getId()), + activityDirective); + } + return new SimulationFacade.PlanSimCorrespondence(planActDirectiveIdToSimulationActivityDirectiveId, directivesToSimulate); + } + + /** + * For activities that have a null duration (in an initial plan for example) and that have been simulated, we pull the duration and + * replace the original instance with a new instance that includes the duration, both in the plan and the simulation facade + */ + public static void pullActivityDurationsIfNecessary( + final Plan plan, + final SimulationFacade.PlanSimCorrespondence correspondence, + final SimulationEngine.SimulationActivityExtract activityExtract + ) { + final var toReplace = new HashMap(); + for (final var activity : plan.getActivities()) { + if (activity.duration() == null) { + final var activityDirective = findSimulatedActivityById( + activityExtract.simulatedActivities().values(), + correspondence.planActDirectiveIdToSimulationActivityDirectiveId().get(activity.getId())); + if (activityDirective.isPresent()) { + final var replacementAct = SchedulingActivityDirective.copyOf( + activity, + activityDirective.get().duration() + ); + toReplace.put(activity, replacementAct); + } + //if not, maybe the activity is not finished + } + } + toReplace.forEach(plan::replace); + } + + private static Optional findSimulatedActivityById(Collection simulatedActivities, final ActivityDirectiveId activityDirectiveId){ + return simulatedActivities.stream().filter(a -> a.directiveId().isPresent() && a.directiveId().get().equals(activityDirectiveId)).findFirst(); + } + + public static void updatePlanWithChildActivities( + final SimulationEngine.SimulationActivityExtract activityExtract, + final Map activityTypes, + final Plan plan, + final SimulationFacade.PlanSimCorrespondence planSimCorrespondence, + final PlanningHorizon planningHorizon) + { + //remove all activities with parents + final var toRemove = plan.getActivities().stream().filter(a -> a.topParent() != null).toList(); + toRemove.forEach(plan::remove); + //pull child activities + activityExtract.simulatedActivities().forEach( (activityInstanceId, activity) -> { + if (activity.parentId() == null) return; + final var rootParent = getIdOfRootParent(activityExtract, activityInstanceId); + if(rootParent.isPresent()) { + final var activityInstance = SchedulingActivityDirective.of( + activityTypes.get(activity.type()), + planningHorizon.toDur(activity.start()), + activity.duration(), + activity.arguments(), + planSimCorrespondence.planActDirectiveIdToSimulationActivityDirectiveId().getKey(rootParent.get()), + null, + true); + plan.add(activityInstance); + } + }); + //no need to replace in Evaluation because child activities are not referenced in it + } + + private static Optional getIdOfRootParent( + final SimulationEngine.SimulationActivityExtract results, + final SimulatedActivityId instanceId){ + if(!results.simulatedActivities().containsKey(instanceId)){ + if(!results.unfinishedActivities().containsKey(instanceId)){ + LOGGER.debug("The simulation of the parent of activity with id "+ instanceId.id() + " has been finished"); + } + return Optional.empty(); + } + final var act = results.simulatedActivities().get(instanceId); + if(act.parentId() == null){ + // SAFETY: any activity that has no parent must have a directive id. + return Optional.of(act.directiveId().get()); + } else { + return getIdOfRootParent(results, act.parentId()); + } + } + + private static Optional getActivityDuration( + final ActivityDirectiveId activityDirectiveId, + final SimulationResultsComputerInputs simulationResultsInputs){ + return simulationResultsInputs.engine().getSpan(simulationResultsInputs.activityDirectiveIdTaskIdMap().get(activityDirectiveId)).duration(); + } + + public static ActivityDirective schedulingActToActivityDir( + final SchedulingActivityDirective activity, + final Map planActDirectiveIdToSimulationActivityDirectiveId, + final SchedulerModel schedulerModel) { + if(activity.getParentActivity().isPresent()) { + throw new Error("This method should not be called with a generated activity but with its top-level parent."); + } + final var arguments = new HashMap<>(activity.arguments()); + if (activity.duration() != null) { + final var durationType = activity.getType().getDurationType(); + if (durationType instanceof DurationType.Controllable dt) { + arguments.put(dt.parameterName(), schedulerModel.serializeDuration(activity.duration())); + } else if ( + durationType instanceof DurationType.Uncontrollable + || durationType instanceof DurationType.Fixed + || durationType instanceof DurationType.Parametric + ) { + // If an activity has already been simulated, it will have a duration, even if its DurationType is Uncontrollable. + } else { + throw new Error("Unhandled variant of DurationType: " + durationType); + } + } + final var serializedActivity = new SerializedActivity(activity.getType().getName(), arguments); + return new ActivityDirective( + activity.startOffset(), + serializedActivity, + planActDirectiveIdToSimulationActivityDirectiveId.get(activity.anchorId()), + activity.anchoredToStart()); + } +} diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/FixedDurationTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/FixedDurationTest.java index 28c859d731..828b7ff81e 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/FixedDurationTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/FixedDurationTest.java @@ -14,7 +14,7 @@ import gov.nasa.jpl.aerie.scheduler.model.Problem; import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; -import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; +import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -23,8 +23,6 @@ import java.util.List; import java.util.Map; -import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOUR; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class FixedDurationTest { @@ -39,7 +37,7 @@ void setUp(){ problem = new Problem( bananaMissionModel, planningHorizon, - new SimulationFacade( + new CheckpointSimulationFacade( bananaMissionModel, SimulationUtility.getBananaSchedulerModel(), new InMemoryCachedEngineStore(10), diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/ParametricDurationTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/ParametricDurationTest.java index 796beae3de..bcc1632f89 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/ParametricDurationTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/ParametricDurationTest.java @@ -15,7 +15,7 @@ import gov.nasa.jpl.aerie.scheduler.goals.CoexistenceGoal; import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; import gov.nasa.jpl.aerie.scheduler.model.Problem; -import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; +import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -24,8 +24,6 @@ import java.util.List; import java.util.Map; -import static gov.nasa.jpl.aerie.scheduler.TestApplyWhen.dur; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class ParametricDurationTest { @@ -37,7 +35,7 @@ public class ParametricDurationTest { void setUp(){ planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochDays(3)); MissionModel bananaMissionModel = SimulationUtility.getBananaMissionModel(); - problem = new Problem(bananaMissionModel, planningHorizon, new SimulationFacade( + problem = new Problem(bananaMissionModel, planningHorizon, new CheckpointSimulationFacade( bananaMissionModel, SimulationUtility.getBananaSchedulerModel(), new InMemoryCachedEngineStore(15), diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java index 9d54160f06..46f27304b6 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java @@ -20,6 +20,7 @@ import gov.nasa.jpl.aerie.scheduler.model.Problem; import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; +import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.Evaluation; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; @@ -31,6 +32,7 @@ import java.util.Optional; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOURS; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; import static gov.nasa.jpl.aerie.scheduler.TestUtility.assertSetEquality; import static org.junit.jupiter.api.Assertions.*; @@ -42,7 +44,7 @@ private static PrioritySolver makeEmptyProblemSolver() { new Problem( bananaMissionModel, h, - new SimulationFacade( + new CheckpointSimulationFacade( bananaMissionModel, schedulerModel, new InMemoryCachedEngineStore(15), @@ -129,6 +131,23 @@ private static PlanInMemory makePlanAB012(Problem problem) { return plan; } + @Test + public void test(){ + final var problem = makeTestMissionAB(); + final var plan = new PlanInMemory(); + final var actTypeA = problem.getActivityType("ControllableDurationActivity"); + final var first =SchedulingActivityDirective.of(actTypeA, t1hr, d1min, null, true); + final var second = SchedulingActivityDirective.of(actTypeA, t2hr, d1min, null, true); + plan.add(first); + plan.add(second); + final var changeFirst = SchedulingActivityDirective.copyOf(first, ZERO); + final var plan3 = new PlanInMemory(); + plan3.add(changeFirst); + final var plan2 = new PlanInMemory(); + //final var diffs = PlanDiff.diff(plan, plan2); + //final var diff2 = PlanDiff.diff(plan2, plan); + } + @Test public void getNextSolution_initialPlanInOutput() throws SchedulingInterruptedException { final var problem = makeTestMissionAB(); @@ -249,7 +268,7 @@ public void getNextSolution_coexistenceGoalOnActivityWorks_withInitialSimResults throws SimulationFacade.SimulationException, SchedulingInterruptedException { final var problem = makeTestMissionAB(); - final var adHocFacade = new SimulationFacade( + final var adHocFacade = new CheckpointSimulationFacade( problem.getMissionModel(), problem.getSchedulerModel(), new InMemoryCachedEngineStore(10), diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationFacadeTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationFacadeTest.java index 02f1fb6e40..9852e6f1df 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationFacadeTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationFacadeTest.java @@ -16,6 +16,7 @@ import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; import gov.nasa.jpl.aerie.scheduler.model.Problem; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; +import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import org.junit.jupiter.api.AfterEach; @@ -81,7 +82,7 @@ private DiscreteResource getPlantRes() { public void setUp() { missionModel = SimulationUtility.getBananaMissionModel(); final var schedulerModel = SimulationUtility.getBananaSchedulerModel(); - facade = new SimulationFacade(horizon, missionModel, schedulerModel); + facade = new CheckpointSimulationFacade(horizon, missionModel, schedulerModel); problem = new Problem(missionModel, horizon, facade, schedulerModel); } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationUtility.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationUtility.java index 2e4cf44fa0..b2e03d19c6 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationUtility.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationUtility.java @@ -11,7 +11,7 @@ import gov.nasa.jpl.aerie.scheduler.model.Problem; import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; -import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; +import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; import java.nio.file.Path; import java.time.Instant; @@ -37,7 +37,7 @@ private static MissionModel makeMissionModel(final MissionModelBuilder builde } public static Problem buildProblemFromFoo(final PlanningHorizon planningHorizon) { - return buildProblemFromFoo(planningHorizon, 0); + return buildProblemFromFoo(planningHorizon, 1); } public static Problem buildProblemFromFoo(final PlanningHorizon planningHorizon, final int simulationCacheSize){ @@ -46,7 +46,7 @@ public static Problem buildProblemFromFoo(final PlanningHorizon planningHorizon, return new Problem( fooMissionModel, planningHorizon, - new SimulationFacade( + new CheckpointSimulationFacade( fooMissionModel, fooSchedulerModel, new InMemoryCachedEngineStore(simulationCacheSize), @@ -55,7 +55,7 @@ public static Problem buildProblemFromFoo(final PlanningHorizon planningHorizon, Map.of(), Instant.EPOCH, new MissionModelId(1)), - ()->false), + () -> false), fooSchedulerModel); } @@ -65,7 +65,7 @@ public static Problem buildProblemFromBanana(final PlanningHorizon planningHoriz return new Problem( bananaMissionModel, planningHorizon, - new SimulationFacade( + new CheckpointSimulationFacade( bananaMissionModel, bananaSchedulerModel, new InMemoryCachedEngineStore(15), diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java index d0889c721a..7d07671b50 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java @@ -4,19 +4,18 @@ import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; import gov.nasa.jpl.aerie.merlin.driver.DirectiveTypeRegistry; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.OneStepTask; import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivity; import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivityId; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType; import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; import gov.nasa.jpl.aerie.merlin.protocol.model.ModelType; import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; -import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; @@ -40,8 +39,6 @@ import java.util.Map; import java.util.Optional; import java.util.TreeMap; -import java.util.concurrent.Executor; -import java.util.function.Function; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -670,9 +667,9 @@ public OutputType getOutputType() { @Override public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> oneShotTask($ -> { + return executor -> new OneStepTask<>($ -> { $.emit(this, delayedActivityDirectiveInputTopic); - return TaskStatus.delayed(oneMinute, oneShotTask($$ -> { + return TaskStatus.delayed(oneMinute, new OneStepTask<>($$ -> { $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); return TaskStatus.completed(Unit.UNIT); })); @@ -695,18 +692,18 @@ public OutputType getOutputType() { @Override public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> oneShotTask(scheduler -> { + return executor -> new OneStepTask<>(scheduler -> { scheduler.emit(this, decomposingActivityDirectiveInputTopic); return TaskStatus.delayed( Duration.ZERO, - oneShotTask($ -> { + new OneStepTask<>($ -> { try { $.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); } catch (final InstantiationException ex) { throw new Error("Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( ex.toString())); } - return TaskStatus.delayed(Duration.of(120, Duration.SECOND), oneShotTask($$ -> { + return TaskStatus.delayed(Duration.of(120, Duration.SECOND), new OneStepTask<>($$ -> { try { $$.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); } catch (final InstantiationException ex) { @@ -812,18 +809,4 @@ public Object instantiate( ) ); //endregion - - private static Task oneShotTask(Function> f) { - return new Task<>() { - @Override - public TaskStatus step(final Scheduler scheduler) { - return f.apply(scheduler); - } - - @Override - public Task duplicate(Executor executor) { - return this; - } - }; - } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacadeTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacadeTest.java index 36636ae180..983b80b5e8 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacadeTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacadeTest.java @@ -18,19 +18,18 @@ import java.util.HashMap; import java.util.Map; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOUR; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; public class CheckpointSimulationFacadeTest { private SimulationFacade newSimulationFacade; private final static PlanningHorizon H = new PlanningHorizon(TimeUtility.fromDOY("2025-001T00:00:00.000"), TimeUtility.fromDOY("2025-005T00:00:00.000")); - + private Map activityTypes; private final static Duration t0 = H.getStartAerie(); - private final static Duration d1min = Duration.of(1, Duration.MINUTE); - private final static Duration d1hr = Duration.of(1, Duration.HOUR); + private final static Duration d1hr = Duration.of(1, HOUR); private final static Duration t1hr = t0.plus(d1hr); private final static Duration t2hr = t0.plus(d1hr.times(2)); - private final static Duration t3hr = t0.plus(d1hr.times(3)); private static PlanInMemory makePlanA012(Map activityTypeMap) { final var plan = new PlanInMemory(); final var actTypeA = activityTypeMap.get("BasicActivity"); @@ -42,16 +41,12 @@ private static PlanInMemory makePlanA012(Map activityTypeM @BeforeEach public void before(){ ThreadedTask.CACHE_READS = true; - } - @Test - public void planUpdateTest() throws SimulationFacade.SimulationException, SchedulingInterruptedException { final var fooMissionModel = SimulationUtility.getFooMissionModel(); - final Map activityTypes = new HashMap<>(); + activityTypes = new HashMap<>(); for(var taskType : fooMissionModel.getDirectiveTypes().directiveTypes().entrySet()){ activityTypes.put(taskType.getKey(), new ActivityType(taskType.getKey(), taskType.getValue(), SimulationUtility.getFooSchedulerModel().getDurationTypes().get(taskType.getKey()))); } - final var plan = makePlanA012(activityTypes); - newSimulationFacade = new SimulationFacade( + newSimulationFacade = new CheckpointSimulationFacade( fooMissionModel, SimulationUtility.getFooSchedulerModel(), new InMemoryCachedEngineStore(10), @@ -59,61 +54,57 @@ public void planUpdateTest() throws SimulationFacade.SimulationException, Schedu new SimulationEngineConfiguration(Map.of(), Instant.EPOCH, new MissionModelId(1)), () -> false); newSimulationFacade.addActivityTypes(activityTypes.values()); - newSimulationFacade.simulateNoResults(plan, t2hr); - //we are stopping at 2hr, at the start of the last activity so it will not have a duraiton in the plan - assertNull(plan.getActivities().stream().filter(a -> a.startOffset().isEqualTo(t2hr)).findFirst().get().duration()); } + /** + * This is to check that one of the simulation interfaces, the one simulating a plan until a given time, is actually stopping + * at the given time. It does that by checking that calling the simulation did not update the activity's duration. + */ @Test - public void planUpdateTest2() throws SimulationFacade.SimulationException, SchedulingInterruptedException { - final var fooMissionModel = SimulationUtility.getFooMissionModel(); - final Map activityTypes = new HashMap<>(); - for(var taskType : fooMissionModel.getDirectiveTypes().directiveTypes().entrySet()){ - activityTypes.put(taskType.getKey(), new ActivityType(taskType.getKey(), taskType.getValue(), SimulationUtility.getFooSchedulerModel().getDurationTypes().get(taskType.getKey()))); - } + public void simulateUntilTime() throws SimulationFacade.SimulationException, SchedulingInterruptedException { final var plan = makePlanA012(activityTypes); - newSimulationFacade = new SimulationFacade( - fooMissionModel, - SimulationUtility.getFooSchedulerModel(), - new InMemoryCachedEngineStore(10), - H, - new SimulationEngineConfiguration(Map.of(),Instant.EPOCH, new MissionModelId(1)), - () -> false); - newSimulationFacade.addActivityTypes(activityTypes.values()); - newSimulationFacade.simulateNoResults(plan, t3hr); - //we are stopping at 2hr, at the start of the last activity so it will not have a duraiton in the plan - assertNotNull(plan - .getActivities() - .stream() - .filter(a -> a.startOffset().isEqualTo(t2hr)) - .findFirst() - .get() - .duration()); + newSimulationFacade.simulateNoResults(plan, t2hr); + //we are stopping at 2hr, at the start of the last activity so it will not have a duration in the plan + assertNull(plan.getActivities().stream().filter(a -> a.startOffset().isEqualTo(t2hr)).findFirst().get().duration()); } + /** + * Simulating the same plan on a smaller horizon leads to re-using the simulation data + */ @Test - public void secondIteration() throws SimulationFacade.SimulationException, SchedulingInterruptedException { - final var fooMissionModel = SimulationUtility.getFooMissionModel(); - final Map activityTypes = new HashMap<>(); - for(var taskType : fooMissionModel.getDirectiveTypes().directiveTypes().entrySet()){ - activityTypes.put(taskType.getKey(), new ActivityType(taskType.getKey(), taskType.getValue(), SimulationUtility.getFooSchedulerModel().getDurationTypes().get(taskType.getKey()))); - } + public void noNeedForResimulation() throws SimulationFacade.SimulationException, SchedulingInterruptedException { final var plan = makePlanA012(activityTypes); - final var cachedEngines = new InMemoryCachedEngineStore(10); - newSimulationFacade = new SimulationFacade( - fooMissionModel, - SimulationUtility.getFooSchedulerModel(), - cachedEngines, - H, - new SimulationEngineConfiguration(Map.of(),Instant.EPOCH, new MissionModelId(1)), - () -> false - ); - newSimulationFacade.addActivityTypes(activityTypes.values()); final var ret = newSimulationFacade.simulateWithResults(plan, t2hr); - final var ret2 = newSimulationFacade.simulateWithResults(plan, t2hr); - //TODO: equality on two checkpoint is difficult, although the plan are the same and the startoffset is the same, they are not equal (cells...) - //so when saving, we don't know if we already have the same checkpoint - //we are stopping at 2hr, at the start of the last activity so it will not have a duraiton in the plan + final var ret2 = newSimulationFacade.simulateWithResults(plan, t1hr); SimulationResultsComparisonUtils.assertEqualsSimulationResults(ret.driverResults(), ret2.driverResults()); } + + /** + * Simulating the same plan on a smaller horizon via a different request (no-results vs with-results) leads to the same + * simulation data + */ + @Test + public void simulationResultsTest() throws SchedulingInterruptedException, SimulationFacade.SimulationException { + final var plan = makePlanA012(activityTypes); + final var simResults = newSimulationFacade.simulateNoResultsAllActivities(plan).computeResults(); + final var simResults2 = newSimulationFacade.simulateWithResults(plan, t1hr); + SimulationResultsComparisonUtils.assertEqualsSimulationResults(simResults, simResults2.driverResults()); + } + + /** + * Tests that the simulation stops at the end of the planning horizon even if the plan we are trying to simulate + * is supposed to last longer. + */ + @Test + public void testStopsAtEndOfPlanningHorizon() + throws SchedulingInterruptedException, SimulationFacade.SimulationException + { + final var plan = new PlanInMemory(); + final var actTypeA = activityTypes.get("ControllableDurationActivity"); + plan.add(SchedulingActivityDirective.of(actTypeA, t0, HOUR.times(200), null, true)); + final var results = newSimulationFacade.simulateNoResultsAllActivities(plan).computeResults(); + assertEquals(H.getEndAerie(), newSimulationFacade.totalSimulationTime()); + assert(results.unfinishedActivities.size() == 1); + } + } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InstantiateArgumentsTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InstantiateArgumentsTest.java index ae87e50e8c..57a45b7c18 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InstantiateArgumentsTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InstantiateArgumentsTest.java @@ -11,12 +11,11 @@ import gov.nasa.jpl.aerie.constraints.tree.RealValue; import gov.nasa.jpl.aerie.constraints.tree.StructExpressionAt; import gov.nasa.jpl.aerie.constraints.tree.ValueAt; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; +import gov.nasa.jpl.aerie.merlin.driver.OneStepTask; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType; import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; -import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; @@ -31,8 +30,6 @@ import java.time.Instant; import java.util.List; import java.util.Map; -import java.util.concurrent.Executor; -import java.util.function.Function; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOUR; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECONDS; @@ -145,9 +142,9 @@ public OutputType getOutputType() { @Override public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> oneShotTask($ -> { + return executor -> new OneStepTask<>($ -> { $.emit(this, delayedActivityDirectiveInputTopic); - return TaskStatus.delayed(oneMinute, oneShotTask($$ -> { + return TaskStatus.delayed(oneMinute, new OneStepTask<>($$ -> { $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); return TaskStatus.completed(Unit.UNIT); })); @@ -199,18 +196,4 @@ public SerializedValue serialize(final Object value) { return SerializedValue.of(Map.of()); } }; - - private static Task oneShotTask(Function> f) { - return new Task<>() { - @Override - public TaskStatus step(final Scheduler scheduler) { - return f.apply(scheduler); - } - - @Override - public Task duplicate(Executor executor) { - return this; - } - }; - } } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinService.java index 7f4d10bb5c..b467cf4073 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinService.java @@ -906,7 +906,7 @@ private Map getSpans(DatasetId datasetI } @Override - public Optional getSimulationResults(PlanMetadata planMetadata) + public Optional> getSimulationResults(PlanMetadata planMetadata) throws MerlinServiceException, IOException { final var simulationDatasetId = getSuitableSimulationResults(planMetadata); @@ -929,7 +929,7 @@ public Optional getSimulationResults(PlanMetadata planMetadat final var simulationEndTime = planMetadata.horizon().getEndInstant(); final var micros = java.time.Duration.between(simulationStartTime, simulationEndTime).toNanos() / 1000; final var duration = Duration.of(micros, MICROSECOND); - return Optional.of(new SimulationResults( + return Optional.of(Pair.of(new SimulationResults( unwrappedProfiles.realProfiles(), unwrappedProfiles.discreteProfiles(), simulatedActivities, @@ -938,7 +938,7 @@ public Optional getSimulationResults(PlanMetadata planMetadat duration, List.of(), new TreeMap<>() - )); + ), simulationDatasetId.get().datasetId)); } catch (InterruptedException | ExecutionException e) { return Optional.empty(); } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MerlinService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MerlinService.java index 6bed6ca691..e2ec0db998 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MerlinService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MerlinService.java @@ -84,12 +84,13 @@ void ensurePlanExists(final PlanId planId) throws IOException, NoSuchPlanException, MerlinServiceException; /** - * Gets existing simulation results for current plan if they exist and are suitable for scheduling purposes (current revision, covers the entire planning horizon) + * Gets existing simulation results for current plan if they exist and are suitable for scheduling purposes (current + * revision, covers the entire planning horizon) * These simulation results do not include events and topics. * @param planMetadata the plan metadata - * @return simulation results, optionally + * @return optionally: simulation results and its dataset id */ - Optional getSimulationResults(PlanMetadata planMetadata) throws MerlinServiceException, IOException, InvalidJsonException; + Optional> getSimulationResults(PlanMetadata planMetadata) throws MerlinServiceException, IOException, InvalidJsonException; /** diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java index a600581b02..3ea1917c6e 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java @@ -68,12 +68,16 @@ import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleResults; import gov.nasa.jpl.aerie.scheduler.server.services.SchedulerAgent; import gov.nasa.jpl.aerie.scheduler.server.services.SpecificationService; +import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import org.apache.commons.collections4.BidiMap; import org.apache.commons.collections4.bidimap.DualHashBidiMap; import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.HashSet; /** * agent that handles posed scheduling requests by blocking the requester thread until scheduling is complete @@ -94,6 +98,8 @@ public record SynchronousSchedulerAgent( ) implements SchedulerAgent { + private static final Logger LOGGER = LoggerFactory.getLogger(SynchronousSchedulerAgent.class); + public SynchronousSchedulerAgent { Objects.requireNonNull(merlinService); Objects.requireNonNull(modelJarsDir); @@ -131,7 +137,7 @@ public void schedule( specification.horizonStartTimestamp().toInstant(), specification.horizonEndTimestamp().toInstant() ); - final var simulationFacade = new SimulationFacade( + final var simulationFacade = new CheckpointSimulationFacade( schedulerMissionModel.missionModel(), schedulerMissionModel.schedulerModel(), cachedEngineStore, @@ -148,10 +154,11 @@ public void schedule( schedulerMissionModel.schedulerModel() ); final var externalProfiles = loadExternalProfiles(planMetadata.planId()); - final var initialSimulationResults = loadSimulationResults(planMetadata); + final var initialSimulationResultsAndDatasetId = loadSimulationResults(planMetadata); //seed the problem with the initial plan contents - final var loadedPlanComponents = loadInitialPlan(planMetadata, problem, initialSimulationResults); - problem.setInitialPlan(loadedPlanComponents.schedulerPlan(), initialSimulationResults, loadedPlanComponents.mapSchedulingIdsToActivityIds); + final var loadedPlanComponents = loadInitialPlan(planMetadata, problem, + initialSimulationResultsAndDatasetId.map(Pair::getKey)); + problem.setInitialPlan(loadedPlanComponents.schedulerPlan(), initialSimulationResultsAndDatasetId.map(Pair::getKey), loadedPlanComponents.mapSchedulingIdsToActivityIds); problem.setExternalProfile(externalProfiles.realProfiles(), externalProfiles.discreteProfiles()); //apply constraints/goals to the problem final var compiledGlobalSchedulingConditions = new ArrayList(); @@ -252,10 +259,19 @@ public void schedule( merlinService.updatePlanActivityDirectiveAnchors(specification.planId(), updatedActs, instancesToIds); final var planMetadataAfterChanges = merlinService.getPlanMetadata(specification.planId()); - final var datasetId = storeSimulationResults(planningHorizon, simulationFacade, planMetadataAfterChanges, instancesToIds); - //collect results and notify subscribers of success - final var results = collectResults(solutionPlan, instancesToIds, goals); - writer.succeedWith(results, datasetId); + Optional datasetId = initialSimulationResultsAndDatasetId.map(Pair::getRight); + if(planMetadataAfterChanges.planRev() != specification.planRevision()) { + datasetId = storeSimulationResults( + solutionPlan, + planningHorizon, + simulationFacade, + planMetadataAfterChanges, + instancesToIds); + } + //collect results and notify subscribers of success + final var results = collectResults(solutionPlan, instancesToIds, goals); + LOGGER.info("Simulation cache saved " + cachedEngineStore.getTotalSavedSimulationTime() + " in simulation time"); + writer.succeedWith(results, datasetId); } catch (final SpecificationLoadException e) { writer.failWith(b -> b .type("SPECIFICATION_LOAD_EXCEPTION") @@ -317,7 +333,7 @@ public List updateEverythingWithNewAnchorIds(Plan s } - private Optional loadSimulationResults(final PlanMetadata planMetadata){ + private Optional> loadSimulationResults(final PlanMetadata planMetadata){ try { return merlinService.getSimulationResults(planMetadata); } catch (MerlinServiceException | IOException | InvalidJsonException e) { @@ -347,7 +363,7 @@ private Optional storeSimulationResults( .collect(Collectors.toMap( (a) -> new SchedulingActivityDirectiveId(a.getKey().id().id()), Map.Entry::getValue)); final var schedID_to_simID = - simulationData.mapping(); + simulationData.mapSchedulingIdsToActivityIds().get(); final var simID_to_MerlinID = schedID_to_simID.entrySet().stream().collect(Collectors.toMap( Map.Entry::getValue, diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockMerlinService.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockMerlinService.java index a7cb47459b..4ba9057b68 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockMerlinService.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockMerlinService.java @@ -151,7 +151,7 @@ public void ensurePlanExists(final PlanId planId) { } @Override - public Optional getSimulationResults(final PlanMetadata planMetadata) + public Optional> getSimulationResults(final PlanMetadata planMetadata) { return Optional.empty(); } diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java index a187e0656c..830ab890ad 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java @@ -32,6 +32,7 @@ import gov.nasa.jpl.aerie.scheduler.model.Problem; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchPlanException; import gov.nasa.jpl.aerie.scheduler.server.http.InvalidJsonException; +import gov.nasa.jpl.aerie.scheduler.server.models.DatasetId; import gov.nasa.jpl.aerie.scheduler.server.models.ExternalProfiles; import gov.nasa.jpl.aerie.scheduler.server.models.MerlinPlan; import gov.nasa.jpl.aerie.scheduler.server.models.MissionModelId; @@ -41,6 +42,7 @@ import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingDSL; import gov.nasa.jpl.aerie.scheduler.server.services.MerlinService; import gov.nasa.jpl.aerie.scheduler.server.services.MerlinServiceException; +import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -101,7 +103,7 @@ public void ensurePlanExists(final PlanId planId) throws IOException, NoSuchPlan } @Override - public Optional getSimulationResults(final PlanMetadata planMetadata) + public Optional> getSimulationResults(final PlanMetadata planMetadata) throws MerlinServiceException, IOException, InvalidJsonException { return Optional.empty(); From e7beb426544b17e7b91448dcc686c4e40ee0d4a2 Mon Sep 17 00:00:00 2001 From: maillard Date: Fri, 29 Mar 2024 11:47:16 -0700 Subject: [PATCH 36/43] Update default configuration --- docker-compose.yml | 6 ++---- .../jpl/aerie/merlin/driver/CheckpointSimulationDriver.java | 2 +- .../gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java | 2 +- scheduler-driver/build.gradle | 2 ++ .../scheduler/simulation/CheckpointSimulationFacade.java | 2 ++ .../aerie/scheduler/worker/SchedulerWorkerAppDriver.java | 2 +- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3398098a51..169b0db1e9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -127,7 +127,6 @@ services: MERLIN_DB_PASSWORD: "${MERLIN_PASSWORD}" MERLIN_WORKER_LOCAL_STORE: /usr/src/app/merlin_file_store SIMULATION_PROGRESS_POLL_PERIOD_MILLIS: 2000 - THREADED_TASK_CACHE_READS: false JAVA_OPTS: > -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG @@ -152,7 +151,6 @@ services: MERLIN_DB_PASSWORD: "${MERLIN_PASSWORD}" MERLIN_WORKER_LOCAL_STORE: /usr/src/app/merlin_file_store SIMULATION_PROGRESS_POLL_PERIOD_MILLIS: 2000 - THREADED_TASK_CACHE_READS: false JAVA_OPTS: > -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG @@ -180,7 +178,7 @@ services: SCHEDULER_OUTPUT_MODE: UpdateInputPlanWithNewActivities MERLIN_LOCAL_STORE: /usr/src/app/merlin_file_store SCHEDULER_RULES_JAR: /usr/src/app/merlin_file_store/scheduler_rules.jar - THREADED_TASK_CACHE_READS: true + MAX_NB_CACHED_SIMULATION_ENGINE: 1 JAVA_OPTS: > -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG @@ -207,7 +205,7 @@ services: SCHEDULER_OUTPUT_MODE: UpdateInputPlanWithNewActivities MERLIN_LOCAL_STORE: /usr/src/app/merlin_file_store SCHEDULER_RULES_JAR: /usr/src/app/merlin_file_store/scheduler_rules.jar - THREADED_TASK_CACHE_READS: true + MAX_NB_CACHED_SIMULATION_ENGINE: 1 JAVA_OPTS: > -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java index 52eb685377..d7b3fa778d 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java @@ -266,7 +266,7 @@ public static SimulationResultsComputerInputs simulateWithCheckpoints( final var nextTime = engine.peekNextTime().orElse(Duration.MAX_VALUE); if (shouldTakeCheckpoint.apply(new SimulationState(elapsedTime, nextTime, engine, schedule, activityToSpan))) { if(!avoidDuplication) cells.freeze(); - LOGGER.info("Saving a simulation engine in memory"); + LOGGER.info("Saving a simulation engine in memory at time " + elapsedTime + " (next time: " + nextTime + ")"); final var newCachedEngine = new CachedSimulationEngine( elapsedTime, schedule, diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java index bab6058c51..a52320625a 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java @@ -19,7 +19,7 @@ import java.util.function.Supplier; public final class ThreadedTask implements Task { - public static boolean CACHE_READS = Boolean.parseBoolean(getEnv("THREADED_TASK_CACHE_READS", "true")); + public static boolean CACHE_READS = false; private final boolean cacheReads = CACHE_READS; private final Scoped rootContext; diff --git a/scheduler-driver/build.gradle b/scheduler-driver/build.gradle index c62f98bc9b..9204c93f1b 100644 --- a/scheduler-driver/build.gradle +++ b/scheduler-driver/build.gradle @@ -34,6 +34,8 @@ dependencies { implementation 'org.jgrapht:jgrapht-core:1.5.2' implementation 'org.slf4j:slf4j-simple:2.0.7' implementation 'org.apache.commons:commons-collections4:4.4' + implementation project(':merlin-framework') + testImplementation project(':merlin-framework-junit') testImplementation project(':constraints') testImplementation project(':examples:banananation') diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacade.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacade.java index db32850ea1..b086cc01ca 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacade.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacade.java @@ -6,6 +6,7 @@ import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.merlin.driver.SimulationResultsComputerInputs; +import gov.nasa.jpl.aerie.merlin.framework.ThreadedTask; import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.scheduler.SchedulingInterruptedException; @@ -59,6 +60,7 @@ public CheckpointSimulationFacade( final PlanningHorizon planningHorizon, final SimulationEngineConfiguration simulationEngineConfiguration, final Supplier canceledListener){ + if(cachedEngines.capacity() > 1) ThreadedTask.CACHE_READS = true; this.missionModel = missionModel; this.schedulerModel = schedulerModel; this.cachedEngines = cachedEngines; diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java index b5fb6c27d8..0ff8f840c6 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java @@ -138,7 +138,7 @@ private static WorkerAppConfiguration loadConfiguration() { Path.of(getEnv("SCHEDULER_RULES_JAR", "/usr/src/app/merlin_file_store/scheduler_rules.jar")), PlanOutputMode.valueOf((getEnv("SCHEDULER_OUTPUT_MODE", "CreateNewOutputPlan"))), getEnv("HASURA_GRAPHQL_ADMIN_SECRET", ""), - Integer.parseInt(getEnv("MAX_NB_CACHED_SIMULATION_ENGINE", "30")) + Integer.parseInt(getEnv("MAX_NB_CACHED_SIMULATION_ENGINE", "1")) ); } } From 1d9aeeae8ddbd16650ea393a1f0662f9652040e6 Mon Sep 17 00:00:00 2001 From: maillard Date: Fri, 29 Mar 2024 15:38:41 -0700 Subject: [PATCH 37/43] Hand-schedule dependents to have a reference to their taskId --- ...java => FooSimulationDuplicationTest.java} | 146 +------ .../driver/CheckpointSimulationDriver.java | 385 +++++++++++------ .../driver/SimulationDuplicationTest.java | 53 +-- .../CheckpointSimulationFacade.java | 135 ++++-- .../simulation/ResumableSimulationDriver.java | 407 ------------------ .../simulation/SimulationFacadeUtils.java | 20 +- .../aerie/scheduler/PrioritySolverTest.java | 20 - .../simulation/AnchorSchedulerTest.java | 53 ++- .../simulation/ResumableSimulationTest.java | 112 ----- 9 files changed, 410 insertions(+), 921 deletions(-) rename examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/{SimulationDuplicationTest.java => FooSimulationDuplicationTest.java} (73%) delete mode 100644 scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java delete mode 100644 scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationTest.java diff --git a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/FooSimulationDuplicationTest.java similarity index 73% rename from examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java rename to examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/FooSimulationDuplicationTest.java index 69a845d67f..7e0305b90b 100644 --- a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/SimulationDuplicationTest.java +++ b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/FooSimulationDuplicationTest.java @@ -4,35 +4,18 @@ import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; import gov.nasa.jpl.aerie.merlin.driver.CachedEngineStore; +import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.DirectiveTypeRegistry; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; import gov.nasa.jpl.aerie.merlin.driver.MissionModelBuilder; -import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; -import gov.nasa.jpl.aerie.merlin.driver.OneStepTask; import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; -import gov.nasa.jpl.aerie.merlin.driver.SimulationResultsComputerInputs; -import gov.nasa.jpl.aerie.merlin.driver.timeline.CausalEventSource; -import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.framework.ThreadedTask; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; -import gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType; -import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; -import gov.nasa.jpl.aerie.merlin.protocol.model.ModelType; -import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; -import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; -import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; -import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; -import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; -import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import org.apache.commons.lang3.tuple.Pair; -import org.apache.commons.lang3.tuple.Triple; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -42,14 +25,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.TreeMap; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTE; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTES; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; -public class SimulationDuplicationTest { +public class FooSimulationDuplicationTest { CachedEngineStore store; final private class InfiniteCapacityEngineStore implements CachedEngineStore{ private final Map> store = new HashMap<>(); @@ -65,6 +47,11 @@ public void save( public List getCachedEngines(final SimulationEngineConfiguration configuration) { return store.get(configuration); } + + @Override + public int capacity() { + return Integer.MAX_VALUE; + } } public static SimulationEngineConfiguration mockConfiguration(){ @@ -393,7 +380,7 @@ static SimulationResults simulateWithCheckpoints( final CachedEngineStore cachedEngineStore, final SimulationEngineConfiguration simulationEngineConfiguration ) { - return SimulationResultsComputerInputs.computeResults(CheckpointSimulationDriver.simulateWithCheckpoints( + return CheckpointSimulationDriver.simulateWithCheckpoints( missionModel, schedule, Instant.EPOCH, @@ -406,8 +393,8 @@ static SimulationResults simulateWithCheckpoints( CheckpointSimulationDriver.desiredCheckpoints(desiredCheckpoints), CheckpointSimulationDriver.noCondition(), cachedEngineStore, - simulationEngineConfiguration, - false)); + simulationEngineConfiguration + ).computeResults(); } static SimulationResults simulateWithCheckpoints( @@ -417,7 +404,7 @@ static SimulationResults simulateWithCheckpoints( final CachedEngineStore cachedEngineStore, final SimulationEngineConfiguration simulationEngineConfiguration ) { - return SimulationResultsComputerInputs.computeResults(CheckpointSimulationDriver.simulateWithCheckpoints( + return CheckpointSimulationDriver.simulateWithCheckpoints( missionModel, schedule, Instant.EPOCH, @@ -430,114 +417,7 @@ static SimulationResults simulateWithCheckpoints( CheckpointSimulationDriver.desiredCheckpoints(desiredCheckpoints), CheckpointSimulationDriver.noCondition(), cachedEngineStore, - simulationEngineConfiguration, - false)); + simulationEngineConfiguration + ).computeResults(); } - - private static final Topic delayedActivityDirectiveInputTopic = new Topic<>(); - private static final Topic delayedActivityDirectiveOutputTopic = new Topic<>(); - - private static final InputType testModelInputType = new InputType<>() { - @Override - public List getParameters() { - return List.of(); - } - - @Override - public List getRequiredParameters() { - return List.of(); - } - - @Override - public Object instantiate(final Map arguments) { - return new Object(); - } - - @Override - public Map getArguments(final Object value) { - return Map.of(); - } - - @Override - public List getValidationFailures(final Object value) { - return List.of(); - } - }; - - private static final OutputType testModelOutputType = new OutputType<>() { - @Override - public ValueSchema getSchema() { - return ValueSchema.ofStruct(Map.of()); - } - - @Override - public SerializedValue serialize(final Object value) { - return SerializedValue.of(Map.of()); - } - }; - - /* package-private*/ static final DirectiveType delayedActivityDirective = new DirectiveType<>() { - @Override - public InputType getInputType() { - return testModelInputType; - } - - @Override - public OutputType getOutputType() { - return testModelOutputType; - } - - @Override - public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> new OneStepTask<>($ -> { - $.emit(this, delayedActivityDirectiveInputTopic); - return TaskStatus.delayed(Duration.MINUTE, new OneStepTask<>($$ -> { - $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); - return TaskStatus.completed(Unit.UNIT); - })); - }); - } - }; - - private static final Topic decomposingActivityDirectiveInputTopic = new Topic<>(); - private static final Topic decomposingActivityDirectiveOutputTopic = new Topic<>(); - /* package-private */ static final DirectiveType decomposingActivityDirective = new DirectiveType<>() { - @Override - public InputType getInputType() { - return testModelInputType; - } - - @Override - public OutputType getOutputType() { - return testModelOutputType; - } - - @Override - public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> new OneStepTask<>(scheduler -> { - scheduler.emit(this, decomposingActivityDirectiveInputTopic); - return TaskStatus.delayed( - Duration.ZERO, - new OneStepTask<>($ -> { - try { - $.spawn(InSpan.Parent, delayedActivityDirective.getTaskFactory(null, null)); - } catch (final InstantiationException ex) { - throw new Error("Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( - ex.toString())); - } - return TaskStatus.delayed(Duration.of(120, Duration.SECOND), new OneStepTask<>($$ -> { - try { - $$.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); - } catch (final InstantiationException ex) { - throw new Error( - "Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( - ex.toString())); - } - $$.emit(Unit.UNIT, decomposingActivityDirectiveOutputTopic); - return TaskStatus.completed(Unit.UNIT); - })); - })); - }); - } - }; } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java index d7b3fa778d..5aff6f7e6b 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java @@ -2,13 +2,14 @@ import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; import gov.nasa.jpl.aerie.merlin.driver.engine.SlabList; +import gov.nasa.jpl.aerie.merlin.driver.engine.SpanException; import gov.nasa.jpl.aerie.merlin.driver.engine.SpanId; import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; +import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import org.apache.commons.lang3.mutable.MutableLong; -import org.apache.commons.lang3.mutable.MutableObject; +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,15 +19,17 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import static gov.nasa.jpl.aerie.merlin.driver.SimulationDriver.scheduleActivities; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MAX_VALUE; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECONDS; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.min; @@ -67,10 +70,10 @@ public static CachedSimulationEngine empty(final MissionModel missionModel) { { final var batch = engine.extractNextJobs(Duration.MAX_VALUE); final var commit = engine.performJobs(batch.jobs(), cells, Duration.ZERO, Duration.MAX_VALUE); - timeline.add(commit); + timeline.add(commit.getKey()); } } - final var emptyCachedEngine = new CachedSimulationEngine( + return new CachedSimulationEngine( Duration.MIN_VALUE, Map.of(), engine, @@ -79,7 +82,6 @@ public static CachedSimulationEngine empty(final MissionModel missionModel) { new Topic<>(), missionModel ); - return emptyCachedEngine; } } @@ -91,9 +93,11 @@ public static CachedSimulationEngine empty(final MissionModel missionModel) { */ public static Optional>> bestCachedEngine( final Map schedule, - final List cachedEngines) { + final List cachedEngines, + final Duration planDuration) { Optional bestCandidate = Optional.empty(); final Map correspondenceMap = new HashMap<>(); + final var minimumStartTimes = getMinimumStartTimes(schedule, planDuration); for (final var cachedEngine : cachedEngines) { if (bestCandidate.isPresent() && cachedEngine.endsAt().noLongerThan(bestCandidate.get().endsAt())) continue; @@ -103,17 +107,29 @@ public static Optional(schedule); for (final var activity : scheduledActivities.entrySet()) { - if (activityDirectivesInCache.values().contains(activity.getValue())) { - final var removedEntry = - removeValueInMap(activityDirectivesInCache, activity.getValue()); - correspondenceMap.put(activity.getKey(), removedEntry.get().getKey()); + final var entryToRemove = activityDirectivesInCache.entrySet() + .stream() + .filter(e -> e.getValue().equals(activity.getValue())) + .findFirst(); + if(entryToRemove.isPresent()) { + final var entry = entryToRemove.get(); + activityDirectivesInCache.remove(entry.getKey()); + correspondenceMap.put(activity.getKey(), entry.getKey()); } else { - invalidationTime = min(invalidationTime, activity.getValue().startOffset()); + invalidationTime = min(invalidationTime, minimumStartTimes.get(activity.getKey())); } } - for (final var activity : activityDirectivesInCache.values()) { - invalidationTime = min(invalidationTime, activity.startOffset()); + final var allActs = new HashMap(); + allActs.putAll(cachedEngine.activityDirectives()); + allActs.putAll(scheduledActivities); + final var minimumStartTimeOfActsInCache = getMinimumStartTimes(allActs, planDuration); + for (final var activity : activityDirectivesInCache.entrySet()) { + invalidationTime = min(invalidationTime, minimumStartTimeOfActsInCache.get(activity.getKey())); } + // (1) cachedEngine ends strictly after bestCandidate as per first line of this loop + // and they both end before the invalidation time: (2) the bestCandidate has already passed its invalidation time + // test below (3) cacheEngine is before its invalidation time too per the test below. + // (1) + (3) -> cachedEngine is strictly better than bestCandidate if (cachedEngine.endsAt().shorterThan(invalidationTime)) { bestCandidate = Optional.of(cachedEngine); } @@ -124,18 +140,6 @@ public static Optional Pair.of(cachedSimulationEngine, correspondenceMap)); } - private static Optional> removeValueInMap(final Map map, V value){ - final var it = map.entrySet().iterator(); - while(it.hasNext()){ - final var entry = it.next(); - if(entry.getValue().equals(value)){ - it.remove(); - return Optional.of(entry); - } - } - return Optional.empty(); - } - private static TemporalEventSource makeCombinedTimeline(List timelines, TemporalEventSource timeline) { final TemporalEventSource combinedTimeline = new TemporalEventSource(); for (final var entry : timelines) { @@ -169,24 +173,34 @@ public static Function desiredCheckpoints(final List wallClockCheckpoints(final long thresholdSeconds) { - MutableLong lastCheckpointRealTime = new MutableLong(System.nanoTime()); - MutableObject lastCheckpointSimTime = new MutableObject<>(Duration.ZERO); - return simulationState -> { - if (simulationState.nextTime().longerThan(simulationState.currentTime()) && System.nanoTime() - lastCheckpointRealTime.getValue() > (thresholdSeconds * 1000 * 1000 * 1000)) { - lastCheckpointRealTime.setValue(System.nanoTime()); - lastCheckpointSimTime.setValue(simulationState.currentTime()); - return true; - } else { - return false; - } - }; - } - public static Function checkpointAtEnd(Function stoppingCondition) { return simulationState -> stoppingCondition.apply(simulationState) || simulationState.nextTime.isEqualTo(MAX_VALUE); } + private static Map getMinimumStartTimes(final Map schedule, final Duration planDuration){ + //For an anchored activity, it's minimum invalidationTime would be the sum of all startOffsets in its anchor chain + // (plus or minus the plan duration depending on whether the root is anchored to plan start or plan end). + // If it's a start anchor chain (as in, all anchors have anchoredToStart set to true), + // this will give you its exact start time, but if there are any end-time anchors, this will give you the minimum time the activity could start at. + final var minimumStartTimes = new HashMap(); + for(final var activity : schedule.entrySet()){ + var curInChain = activity; + var curSum = ZERO; + while(true){ + if(curInChain.getValue().anchorId() == null){ + curSum = curSum.plus(curInChain.getValue().startOffset()); + curSum = !curInChain.getValue().anchoredToStart() ? curSum.plus(planDuration) : curSum; + minimumStartTimes.put(activity.getKey(), curSum); + break; + } else{ + curSum = curSum.plus(curInChain.getValue().startOffset()); + curInChain = Map.entry(curInChain.getValue().anchorId(), schedule.get(curInChain.getValue().anchorId())); + } + } + } + return minimumStartTimes; + } + public record SimulationState( Duration currentTime, Duration nextTime, @@ -195,6 +209,23 @@ public record SimulationState( Map activityDirectiveIdSpanIdMap ){} + /** + * Simulates a plan/schedule while using and creating simulation checkpoints. + * @param missionModel the mission model + * @param schedule the plan/schedule + * @param simulationStartTime the start time of the simulation + * @param simulationDuration the simulation duration + * @param planStartTime the plan overall start time + * @param planDuration the plan overall duration + * @param simulationExtentConsumer consumer to report simulation progress + * @param simulationCanceled provider of an external stop signal + * @param cachedEngine the simulation engine that is going to be used + * @param shouldTakeCheckpoint a function from state of the simulation to boolean deciding when to take checkpoints + * @param stopConditionOnPlan a function from state of the simulation to boolean deciding when to stop simulation + * @param cachedEngineStore a store for simulation engine checkpoints taken. If capacity is 1, the simulation will behave like a resumable simulation. + * @param configuration the simulation configuration + * @return all the information to compute simulation results if needed + */ public static SimulationResultsComputerInputs simulateWithCheckpoints( final MissionModel missionModel, final Map schedule, @@ -208,65 +239,86 @@ public static SimulationResultsComputerInputs simulateWithCheckpoints( final Function shouldTakeCheckpoint, final Function stopConditionOnPlan, final CachedEngineStore cachedEngineStore, - final SimulationEngineConfiguration configuration, - final boolean avoidDuplication) { - final var activityToSpan = new LinkedHashMap(); + final SimulationEngineConfiguration configuration) + { + final boolean duplicationIsOk = cachedEngineStore.capacity() > 1; + final var activityToSpan = new HashMap(); final var activityTopic = cachedEngine.activityTopic(); final var timelines = new ArrayList(); timelines.add(new TemporalEventSource(cachedEngine.timePoints)); - var engine = avoidDuplication ? cachedEngine.simulationEngine : cachedEngine.simulationEngine.duplicate(); + var engine = !duplicationIsOk ? cachedEngine.simulationEngine : cachedEngine.simulationEngine.duplicate(); engine.unscheduleAfter(cachedEngine.endsAt); - var timeline = new TemporalEventSource(); - var cells = new LiveCells(timeline, cachedEngine.cells()); - /* The current real time. */ - var elapsedTime = Duration.max(ZERO, cachedEngine.endsAt()); - - simulationExtentConsumer.accept(elapsedTime); - - // Specify a topic on which tasks can log the activity they're associated with. - - try { - final var filteredSchedule = new HashMap(); - for (final var entry : schedule.entrySet()) { - if (entry.getValue().startOffset().longerThan(cachedEngine.endsAt())) { - filteredSchedule.put(entry.getKey(), entry.getValue()); - } + var timeline = new TemporalEventSource(); + var cells = new LiveCells(timeline, cachedEngine.cells()); + /* The current real time. */ + var elapsedTime = Duration.max(ZERO, cachedEngine.endsAt()); + + simulationExtentConsumer.accept(elapsedTime); + + try { + // Get all activities as close as possible to absolute time + // Schedule all activities. + // Using HashMap explicitly because it allows `null` as a key. + // `null` key means that an activity is not waiting on another activity to finish to know its start time + HashMap>> resolved = new StartOffsetReducer(planDuration, schedule).compute(); + if(!resolved.isEmpty()) { + resolved.put( + null, + StartOffsetReducer.adjustStartOffset( + resolved.get(null), + Duration.of( + planStartTime.until(simulationStartTime, ChronoUnit.MICROS), + Duration.MICROSECONDS))); + } + // Filter out activities that are before simulationStartTime + resolved = StartOffsetReducer.filterOutStartOffsetBefore(resolved, Duration.max(ZERO, cachedEngine.endsAt().plus(MICROSECONDS))); + final var toSchedule = new LinkedHashSet(); + toSchedule.add(null); + final HashMap>> finalResolved = resolved; + final var activitiesToBeScheduledNow = new HashMap(); + if(finalResolved.get(null) != null) { + for (final var r : finalResolved.get(null)) { + activitiesToBeScheduledNow.put(r.getKey(), schedule.get(r.getKey())); } - - // Get all activities as close as possible to absolute time - // Schedule all activities. - // Using HashMap explicitly because it allows `null` as a key. - // `null` key means that an activity is not waiting on another activity to finish to know its start time - HashMap>> resolved = new StartOffsetReducer(planDuration, filteredSchedule).compute(); - if(resolved.size() != 0) { - resolved.put( - null, - StartOffsetReducer.adjustStartOffset( - resolved.get(null), - Duration.of( - planStartTime.until(simulationStartTime, ChronoUnit.MICROS), - Duration.MICROSECONDS))); + } + var toCheckForDependencyScheduling = scheduleActivities( + toSchedule, + activitiesToBeScheduledNow, + resolved, + missionModel, + engine, + elapsedTime, + activityToSpan, + activityTopic); + + // Drive the engine until we're out of time. + // TERMINATION: Actually, we might never break if real time never progresses forward. + while (elapsedTime.noLongerThan(simulationDuration) && !simulationCanceled.get()) { + final var nextTime = engine.peekNextTime().orElse(Duration.MAX_VALUE); + if (duplicationIsOk && shouldTakeCheckpoint.apply(new SimulationState(elapsedTime, nextTime, engine, schedule, activityToSpan))) { + cells.freeze(); + LOGGER.info("Saving a simulation engine in memory at time " + elapsedTime + " (next time: " + nextTime + ")"); + final var newCachedEngine = new CachedSimulationEngine( + elapsedTime, + schedule, + engine, + cells, + makeCombinedTimeline(timelines, timeline).points(), + activityTopic, + missionModel); + newCachedEngine.freeze(); + cachedEngineStore.save( + newCachedEngine, + configuration); + timelines.add(timeline); + engine = engine.duplicate(); + timeline = new TemporalEventSource(); + cells = new LiveCells(timeline, cells); } - // Filter out activities that are before simulationStartTime - resolved = StartOffsetReducer.filterOutNegativeStartOffset(resolved); - - activityToSpan.putAll(scheduleActivities( - filteredSchedule, - resolved, - missionModel, - engine, - activityTopic - ) - ); - - // Drive the engine until we're out of time. - // TERMINATION: Actually, we might never break if real time never progresses forward. - while (elapsedTime.noLongerThan(simulationDuration) && !simulationCanceled.get()) { - final var nextTime = engine.peekNextTime().orElse(Duration.MAX_VALUE); - if (shouldTakeCheckpoint.apply(new SimulationState(elapsedTime, nextTime, engine, schedule, activityToSpan))) { - if(!avoidDuplication) cells.freeze(); - LOGGER.info("Saving a simulation engine in memory at time " + elapsedTime + " (next time: " + nextTime + ")"); + //break before changing the state of the engine + if (simulationCanceled.get() || stopConditionOnPlan.apply(new SimulationState(elapsedTime, nextTime, engine, schedule, activityToSpan))) { + if(!duplicationIsOk){ final var newCachedEngine = new CachedSimulationEngine( elapsedTime, schedule, @@ -275,61 +327,138 @@ public static SimulationResultsComputerInputs simulateWithCheckpoints( makeCombinedTimeline(timelines, timeline).points(), activityTopic, missionModel); - if(!avoidDuplication) newCachedEngine.freeze(); cachedEngineStore.save( newCachedEngine, configuration); timelines.add(timeline); - engine = avoidDuplication ? engine : engine.duplicate(); - timeline = new TemporalEventSource(); - cells = new LiveCells(timeline, cells); - } - //break before changing the state of the engine - if (simulationCanceled.get() || stopConditionOnPlan.apply(new SimulationState(elapsedTime, nextTime, engine, schedule, activityToSpan))) { - break; } + break; + } - final var batch = engine.extractNextJobs(simulationDuration); - // Increment real time, if necessary. - final var delta = batch.offsetFromStart().minus(elapsedTime); - elapsedTime = batch.offsetFromStart(); - timeline.add(delta); - // TODO: Advance a dense time counter so that future tasks are strictly ordered relative to these, - // even if they occur at the same real time. + final var batch = engine.extractNextJobs(simulationDuration); + // Increment real time, if necessary. + final var delta = batch.offsetFromStart().minus(elapsedTime); + elapsedTime = batch.offsetFromStart(); + timeline.add(delta); + // TODO: Advance a dense time counter so that future tasks are strictly ordered relative to these, + // even if they occur at the same real time. - simulationExtentConsumer.accept(elapsedTime); + simulationExtentConsumer.accept(elapsedTime); - //this break depends on the state of the batch: this is the soonest we can exist for that reason - if (batch.jobs().isEmpty() && (batch.offsetFromStart().isEqualTo(simulationDuration))) { - break; - } + //this break depends on the state of the batch: this is the soonest we can exit for that reason + if (batch.jobs().isEmpty() && (batch.offsetFromStart().isEqualTo(simulationDuration))) { + break; + } - // Run the jobs in this batch. - final var commit = engine.performJobs(batch.jobs(), cells, elapsedTime, simulationDuration); - timeline.add(commit); + // Run the jobs in this batch. + final var commit = engine.performJobs(batch.jobs(), cells, elapsedTime, simulationDuration); + timeline.add(commit.getLeft()); + if (commit.getRight().isPresent()) { + throw commit.getRight().get(); } - } catch (Throwable ex) { - throw new SimulationException(elapsedTime, simulationStartTime, ex); + + toCheckForDependencyScheduling.putAll(scheduleActivities( + getSuccessorsToSchedule(engine, toCheckForDependencyScheduling), + schedule, + resolved, + missionModel, + engine, + elapsedTime, + activityToSpan, + activityTopic)); } - if(!avoidDuplication) engine.close(); - return new SimulationResultsComputerInputs( - engine, - simulationStartTime, - elapsedTime, - activityTopic, - makeCombinedTimeline(timelines, timeline), - missionModel.getTopics(), - activityToSpan); + } catch (SpanException ex) { + // Swallowing the spanException as the internal `spanId` is not user meaningful info. + final var topics = missionModel.getTopics(); + final var directiveId = SimulationEngine.getDirectiveIdFromSpan(engine, activityTopic, timeline, topics, ex.spanId); + if(directiveId.isPresent()) { + throw new SimulationException(elapsedTime, simulationStartTime, directiveId.get(), ex.cause); + } + throw new SimulationException(elapsedTime, simulationStartTime, ex.cause); + } catch (Throwable ex) { + throw new SimulationException(elapsedTime, simulationStartTime, ex); + } + return new SimulationResultsComputerInputs( + engine, + simulationStartTime, + elapsedTime, + activityTopic, + makeCombinedTimeline(timelines, timeline), + missionModel.getTopics(), + activityToSpan); + } + + + private static Set getSuccessorsToSchedule( + final SimulationEngine engine, + final Map toCheckForDependencyScheduling) { + final var toSchedule = new LinkedHashSet(); + final var iterator = toCheckForDependencyScheduling.entrySet().iterator(); + while(iterator.hasNext()){ + final var taskToCheck = iterator.next(); + if(engine.spanIsComplete(taskToCheck.getValue())){ + toSchedule.add(taskToCheck.getKey()); + iterator.remove(); + } + } + return toSchedule; + } + + private static Map scheduleActivities( + final Set toScheduleNow, + final Map completeSchedule, + final HashMap>> resolved, + final MissionModel missionModel, + final SimulationEngine engine, + final Duration curTime, + final Map activityToTask, + final Topic activityTopic){ + final var toCheckForDependencyScheduling = new HashMap(); + for(final var predecessor: toScheduleNow) { + if(!resolved.containsKey(predecessor)) continue; + for (final var directivePair : resolved.get(predecessor)) { + final var offset = directivePair.getRight(); + final var directiveIdToSchedule = directivePair.getLeft(); + final var serializedDirective = completeSchedule.get(directiveIdToSchedule).serializedActivity(); + final TaskFactory task; + try { + task = missionModel.getTaskFactory(serializedDirective); + } catch (final InstantiationException ex) { + // All activity instantiations are assumed to be validated by this point + throw new Error("Unexpected state: activity instantiation %s failed with: %s" + .formatted(serializedDirective.getTypeName(), ex.toString())); + } + Duration computedStartTime = offset; + if (predecessor != null) { + computedStartTime = (curTime.isEqualTo(Duration.MIN_VALUE) ? Duration.ZERO : curTime).plus(offset); + } + final var taskId = engine.scheduleTask( + computedStartTime, + makeTaskFactory(directiveIdToSchedule, task, activityTopic)); + activityToTask.put(directiveIdToSchedule, taskId); + if (resolved.containsKey(directiveIdToSchedule)) { + toCheckForDependencyScheduling.put(directiveIdToSchedule, taskId); + } + } + } + return toCheckForDependencyScheduling; + } + + private static TaskFactory makeTaskFactory( + final ActivityDirectiveId directiveId, + final TaskFactory task, + final Topic activityTopic) { + return executor -> new DuplicatableTaskFactory<>(directiveId, task, activityTopic, executor); } - public static Function stopOnceAllActivitiessAreFinished(){ + public static Function onceAllActivitiesAreFinished(){ return simulationState -> simulationState.activityDirectiveIdSpanIdMap() .values() .stream() .allMatch(simulationState.simulationEngine()::spanIsComplete); } - public static Function noCondition(){ + public static Function noCondition(){ return simulationState -> false; } diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java index b85a2006fc..f7f28361e5 100644 --- a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java @@ -1,8 +1,6 @@ package gov.nasa.jpl.aerie.merlin.driver; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; -import org.apache.commons.lang3.tuple.Triple; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -11,7 +9,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.TreeMap; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTES; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -33,6 +30,10 @@ public void save( public List getCachedEngines(final SimulationEngineConfiguration configuration) { return store.get(configuration); } + + public int capacity() { + return Integer.MAX_VALUE; + } } public static SimulationEngineConfiguration mockConfiguration(){ @@ -48,45 +49,6 @@ void beforeEach(){ this.store = new InfiniteCapacityEngineStore(); } - @Test - void emptyPlanTest() { - final SimulationResults results = SimulationDriver.simulate( - TestMissionModel.missionModel(), - Map.of(), - Instant.EPOCH, - Duration.HOUR, - Instant.EPOCH, - Duration.HOUR, - () -> false); - final List> standardTopics = List.of( - Triple.of( - 0, - "ActivityType.Input.DelayActivityDirective", - ValueSchema.ofStruct(Map.of())), - Triple.of( - 1, - "ActivityType.Output.DelayActivityDirective", - ValueSchema.ofStruct(Map.of())), - Triple.of( - 2, - "ActivityType.Input.DecomposingActivityDirective", - ValueSchema.ofStruct(Map.of())), - Triple.of( - 3, - "ActivityType.Output.DecomposingActivityDirective", - ValueSchema.ofStruct(Map.of()))); - final SimulationResults expected = new SimulationResults( - Map.of(), - Map.of(), - Map.of(), - Map.of(), - Instant.EPOCH, - Duration.HOUR, - standardTopics, - new TreeMap<>()); - assertEquals(expected, results); - } - @Test void testDuplicate() { final var results = simulateWithCheckpoints( @@ -112,7 +74,7 @@ static SimulationResults simulateWithCheckpoints( final List desiredCheckpoints, final CachedEngineStore engineStore ) { - return SimulationResultsComputerInputs.computeResults(CheckpointSimulationDriver.simulateWithCheckpoints( + return CheckpointSimulationDriver.simulateWithCheckpoints( TestMissionModel.missionModel(), Map.of(), Instant.EPOCH, @@ -125,8 +87,7 @@ static SimulationResults simulateWithCheckpoints( CheckpointSimulationDriver.desiredCheckpoints(desiredCheckpoints), CheckpointSimulationDriver.noCondition(), engineStore, - mockConfiguration(), - false - )); + mockConfiguration() + ).computeResults(); } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacade.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacade.java index b086cc01ca..df66f6a55e 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacade.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacade.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.scheduler.simulation; +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; @@ -21,11 +22,12 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.function.Supplier; -import static gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver.checkpointAtEnd; +import static gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver.onceAllActivitiesAreFinished; import static gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacadeUtils.scheduleFromPlan; import static gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacadeUtils.updatePlanWithChildActivities; @@ -40,7 +42,7 @@ public class CheckpointSimulationFacade implements SimulationFacade { private final Supplier canceledListener; private final SchedulerModel schedulerModel; private Duration totalSimulationTime = Duration.ZERO; - private boolean resumableSimulationBehavior; + private SimulationData latestSimulationData; /** * Loads initial simulation results into the simulation. They will be served until initialSimulationResultsAreStale() @@ -68,7 +70,22 @@ public CheckpointSimulationFacade( this.activityTypes = new HashMap<>(); this.configuration = simulationEngineConfiguration; this.canceledListener = canceledListener; - this.resumableSimulationBehavior = cachedEngines.capacity() == 1; + this.latestSimulationData = null; + } + + public CheckpointSimulationFacade( + final PlanningHorizon planningHorizon, + final MissionModel missionModel, + final SchedulerModel schedulerModel + ){ + this( + missionModel, + schedulerModel, + new InMemoryCachedEngineStore(1), + planningHorizon, + new SimulationEngineConfiguration(Map.of(), Instant.now(), new MissionModelId(1)), + ()-> false + ); } /** @@ -80,20 +97,6 @@ public Duration totalSimulationTime(){ return totalSimulationTime; } - public CheckpointSimulationFacade( - final PlanningHorizon planningHorizon, - final MissionModel missionModel, - final SchedulerModel schedulerModel - ){ - this.missionModel = missionModel; - this.cachedEngines = new InMemoryCachedEngineStore(0); - this.planningHorizon = planningHorizon; - this.activityTypes = new HashMap<>(); - this.configuration = new SimulationEngineConfiguration(Map.of(), Instant.now(), new MissionModelId(1)); - this.canceledListener = () -> false; - this.schedulerModel = schedulerModel; - } - @Override public Supplier getCanceledListener(){ return this.canceledListener; @@ -122,6 +125,18 @@ private void replaceIds( final var value = planSimCorrespondence.directiveIdActivityDirectiveMap().remove(replacements.getKey()); planSimCorrespondence.directiveIdActivityDirectiveMap().put(replacements.getValue(), value); } + //replace the anchor ids in the plan + final var replacementMap = new HashMap(); + for(final var act : planSimCorrespondence.directiveIdActivityDirectiveMap().entrySet()){ + if(act.getValue().anchorId() != null && act.getValue().anchorId().equals(replacements.getKey())){ + final var replacementActivity = new ActivityDirective(act.getValue().startOffset(), act.getValue().serializedActivity(), replacements.getValue(), act.getValue().anchoredToStart()); + replacementMap.put(act.getKey(), replacementActivity); + } + } + for(final var replacement: replacementMap.entrySet()){ + planSimCorrespondence.directiveIdActivityDirectiveMap().remove(replacement.getKey()); + planSimCorrespondence.directiveIdActivityDirectiveMap().put(replacement.getKey(), replacement.getValue()); + } } } @@ -132,10 +147,10 @@ private void replaceIds( * @throws SimulationException if an exception happens during simulation */ @Override - public SimulationResultsComputerInputs simulateNoResultsUntilEndPlan(final Plan plan) + public SimulationResultsComputerInputs simulateNoResultsAllActivities(final Plan plan) throws SimulationException, SchedulingInterruptedException { - return simulateNoResults(plan, planningHorizon.getEndAerie(), null).simulationResultsComputerInputs(); + return simulateNoResults(plan, null, null).simulationResultsComputerInputs(); } /** @@ -178,7 +193,8 @@ private AugmentedSimulationResultsComputerInputs simulateNoResults( final var best = CheckpointSimulationDriver.bestCachedEngine( planSimCorrespondence.directiveIdActivityDirectiveMap(), - cachedEngines.getCachedEngines(configuration)); + cachedEngines.getCachedEngines(configuration), + planningHorizon.getEndAerie()); CheckpointSimulationDriver.CachedSimulationEngine engine = null; Duration from = Duration.ZERO; if(best.isPresent()){ @@ -201,12 +217,13 @@ private AugmentedSimulationResultsComputerInputs simulateNoResults( //(2) else if(activity != null && until == null){ simulationDuration = planningHorizon.getEndAerie(); - stoppingCondition = CheckpointSimulationDriver.stopOnceActivityHasFinished(planSimCorrespondence.planActDirectiveIdToSimulationActivityDirectiveId().get(activity)); + stoppingCondition = CheckpointSimulationDriver.stopOnceActivityHasFinished( + planSimCorrespondence.planActDirectiveIdToSimulationActivityDirectiveId().get(activity.id())); LOGGER.info("Simulation mode: until activity ends " + activity); - //(3) + //(3) } else if(activity == null && until == null){ simulationDuration = planningHorizon.getEndAerie(); - stoppingCondition = CheckpointSimulationDriver.stopOnceAllActivitiessAreFinished(); + stoppingCondition = CheckpointSimulationDriver.onceAllActivitiesAreFinished(); LOGGER.info("Simulation mode: until all activities end "); } else { throw new SimulationException("Bad configuration", null); @@ -214,6 +231,19 @@ else if(activity != null && until == null){ if(engine == null) engine = CheckpointSimulationDriver.CachedSimulationEngine.empty(missionModel); + Function checkpointPolicy = new ResourceAwareSpreadCheckpointPolicy( + cachedEngines.capacity(), + Duration.ZERO, + planningHorizon.getEndAerie(), + Duration.max(engine.endsAt(), Duration.ZERO), + simulationDuration, + 1, + true); + + if(stoppingCondition.equals(CheckpointSimulationDriver.onceAllActivitiesAreFinished())){ + checkpointPolicy = or(checkpointPolicy, onceAllActivitiesAreFinished()); + } + if(best.isPresent()) cachedEngines.registerUsed(engine); try { final var simulation = CheckpointSimulationDriver.simulateWithCheckpoints( @@ -226,21 +256,14 @@ else if(activity != null && until == null){ $ -> {}, canceledListener, engine, - resumableSimulationBehavior ? checkpointAtEnd(stoppingCondition): new ResourceAwareSpreadCheckpointPolicy( - cachedEngines.capacity(), - Duration.ZERO, - planningHorizon.getEndAerie(), - Duration.max(engine.endsAt(), Duration.ZERO), - simulationDuration, - 1, - true), + checkpointPolicy, stoppingCondition, cachedEngines, - configuration, - resumableSimulationBehavior); + configuration + ); if(canceledListener.get()) throw new SchedulingInterruptedException("simulating"); this.totalSimulationTime = this.totalSimulationTime.plus(simulation.elapsedTime().minus(from)); - final var activityResults = SimulationResultsComputerInputs.computeActivitySimulationResults(simulation); + final var activityResults = simulation.computeActivitySimulationResults(); updatePlanWithChildActivities( activityResults, @@ -256,15 +279,29 @@ else if(activity != null && until == null){ ); //plan has been updated return new AugmentedSimulationResultsComputerInputs(simulation, planSimCorrespondence); - } catch(Exception e){ - if(e instanceof SchedulingInterruptedException sie){ - throw sie; - } + } catch (SchedulingInterruptedException e) { + throw e; + } catch (Exception e) { throw new SimulationException("An exception happened during simulation", e); } } - @Override + @SafeVarargs + private static Function or( + final Function... functions) + { + return (simulationState) -> { + for(final var function: functions){ + if(function.apply(simulationState)){ + return true; + } + } + return false; + }; + } + + + @Override public SimulationData simulateWithResults( final Plan plan, final Duration until) throws SimulationException, SchedulingInterruptedException @@ -282,16 +319,22 @@ public SimulationData simulateWithResults( final var inputPlan = scheduleFromPlan(plan, schedulerModel); final var initialPlanA = scheduleFromPlan(this.initialSimulationResults.plan(), schedulerModel); if (initialPlanA.equals(inputPlan)) { - return new SimulationData( - plan, - initialSimulationResults.driverResults(), - SimulationResultsConverter.convertToConstraintModelResults(initialSimulationResults.driverResults()), - this.initialSimulationResults.mapping()); + return initialSimulationResults; } } final var resultsInput = simulateNoResults(plan, until); - final var driverResults = SimulationResultsComputerInputs.computeResults(resultsInput.simulationResultsComputerInputs(), resourceNames); - return new SimulationData(plan, driverResults, SimulationResultsConverter.convertToConstraintModelResults(driverResults), resultsInput.planSimCorrespondence().planActDirectiveIdToSimulationActivityDirectiveId()); + final var driverResults = resultsInput.simulationResultsComputerInputs().computeResults(resourceNames); + this.latestSimulationData = new SimulationData( + plan, + driverResults, + SimulationResultsConverter.convertToConstraintModelResults(driverResults), + Optional.ofNullable(resultsInput.planSimCorrespondence().planActDirectiveIdToSimulationActivityDirectiveId())); + return this.latestSimulationData; + } + + @Override + public Optional getLatestSimulationData() { + return Optional.ofNullable(this.latestSimulationData); } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java deleted file mode 100644 index 3cae844eac..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java +++ /dev/null @@ -1,407 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.simulation; - -import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; -import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; -import gov.nasa.jpl.aerie.merlin.driver.MissionModel; -import gov.nasa.jpl.aerie.merlin.driver.OneStepTask; -import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; -import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; -import gov.nasa.jpl.aerie.merlin.driver.StartOffsetReducer; -import gov.nasa.jpl.aerie.merlin.driver.engine.JobSchedule; -import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; -import gov.nasa.jpl.aerie.merlin.driver.engine.SpanId; -import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; -import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; -import gov.nasa.jpl.aerie.merlin.protocol.model.Task; -import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; -import gov.nasa.jpl.aerie.scheduler.NotNull; -import gov.nasa.jpl.aerie.scheduler.SchedulingInterruptedException; -import org.apache.commons.lang3.tuple.Pair; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.time.Instant; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Supplier; - -public class ResumableSimulationDriver implements AutoCloseable { - private final Supplier canceledListener; - - public long durationSinceRestart = 0; - - private static final Logger logger = LoggerFactory.getLogger(ResumableSimulationDriver.class); - /* The current real time. All the tasks before and at this time have been performed. - Simulation has not started so it is set to MIN_VALUE. */ - private Duration curTime = Duration.MIN_VALUE; - private SimulationEngine engine = new SimulationEngine(); - private LiveCells cells; - private TemporalEventSource timeline = new TemporalEventSource(); - private final MissionModel missionModel; - private final Duration planDuration; - private JobSchedule.Batch batch; - - private final Topic activityTopic = new Topic<>(); - - //mapping each activity name to its task id (in String form) in the simulation engine - private final Map plannedDirectiveToTask; - - //subset of plannedDirectiveToTask to check for scheduling dependent tasks - private final Map toCheckForDependencyScheduling; - - //simulation results so far - private SimulationResults lastSimResults; - //cached simulation results cover the period [Duration.ZERO, lastSimResultsEnd] - private Duration lastSimResultsEnd = Duration.ZERO; - - //List of activities simulated since the last reset - private final Map activitiesInserted = new HashMap<>(); - - //counts the number of simulation restarts, used as performance metric in the scheduler - //effectively counting the number of calls to initSimulation() - private int countSimulationRestarts; - - public ResumableSimulationDriver( - MissionModel missionModel, - Duration planDuration, - Supplier canceledListener - ){ - this.missionModel = missionModel; - plannedDirectiveToTask = new HashMap<>(); - toCheckForDependencyScheduling = new HashMap<>(); - this.planDuration = planDuration; - countSimulationRestarts = 0; - this.canceledListener = canceledListener; - initSimulation(); - } - - - private void printTimeSpent(){ - final var dur = durationSinceRestart/1_000_000_000.; - final var average = curTime.shorterThan(Duration.of(1, Duration.SECONDS)) ? 0 : dur/curTime.in(Duration.SECONDS); - if(dur != 0) { - logger.info("Time spent in the last sim " + dur + "s, average per simulation second " + average + "s. Simulated until " + curTime); - } - } - - // This method is currently only used in one test. - /*package-private*/ void clearActivitiesInserted() {activitiesInserted.clear();} - - /*package-private*/ void initSimulation(){ - logger.info("Reinitialization of the scheduling simulation"); - printTimeSpent(); - durationSinceRestart = 0; - plannedDirectiveToTask.clear(); - toCheckForDependencyScheduling.clear(); - lastSimResults = null; - lastSimResultsEnd = Duration.ZERO; - long before = System.nanoTime(); - if (this.engine != null) this.engine.close(); - this.engine = new SimulationEngine(); - batch = null; - /* The top-level simulation timeline. */ - this.timeline = new TemporalEventSource(); - this.cells = new LiveCells(timeline, missionModel.getInitialCells()); - curTime = Duration.MIN_VALUE; - - // Begin tracking all resources. - for (final var entry : missionModel.getResources().entrySet()) { - final var name = entry.getKey(); - final var resource = entry.getValue(); - engine.trackResource(name, resource, Duration.ZERO); - } - - // Start daemon task(s) immediately, before anything else happens. - { - if(missionModel.hasDaemons()) { - engine.scheduleTask(Duration.ZERO, missionModel.getDaemon()); - batch = engine.extractNextJobs(Duration.MAX_VALUE); - } - } - this.durationSinceRestart += System.nanoTime() - before; - countSimulationRestarts++; - } - - /** - * Return the number of simulation restarts - * @return the number of simulation restarts - */ - public int getCountSimulationRestarts(){ - return countSimulationRestarts; - } - - @Override - public void close() { - logger.debug("Closing sim"); - printTimeSpent(); - this.engine.close(); - } - - private void simulateUntil(Duration endTime) throws SchedulingInterruptedException { - long before = System.nanoTime(); - logger.info("Simulating until "+endTime); - assert(endTime.noShorterThan(curTime)); - if(batch == null){ - batch = engine.extractNextJobs(Duration.MAX_VALUE); - } - // Increment real time, if necessary. - while(!batch.offsetFromStart().longerThan(endTime) && !endTime.isEqualTo(Duration.MAX_VALUE)) { - if(canceledListener.get()) throw new SchedulingInterruptedException("simulating"); - //by default, curTime is negative to signal we have not started simulation yet. We set it to 0 when we start. - final var delta = batch.offsetFromStart().minus(curTime.isNegative() ? Duration.ZERO : curTime); - curTime = batch.offsetFromStart(); - timeline.add(delta); - // Run the jobs in this batch. - final var commit = engine.performJobs(batch.jobs(), cells, curTime, Duration.MAX_VALUE); - timeline.add(commit.getLeft()); - if (commit.getRight().isPresent()) { - throw new RuntimeException(commit.getRight().get()); - } - - batch = engine.extractNextJobs(Duration.MAX_VALUE); - } - lastSimResults = null; - this.durationSinceRestart += (System.nanoTime() - before); - } - - - /** - * Simulate an activity directive. - * @param activity the serialized type and arguments of the activity directive to be simulated - * @param startOffset the start offset from the activity's anchor - * @param anchorId the activity id of the anchor (or null if the activity is anchored to the plan) - * @param anchoredToStart toggle for if the activity is anchored to the start or end of its anchor - * @param activityId the activity id for the activity to simulate - */ - public void simulateActivity( - final Duration startOffset, - final SerializedActivity activity, - final ActivityDirectiveId anchorId, - final boolean anchoredToStart, - final ActivityDirectiveId activityId - ) throws SchedulingInterruptedException { - simulateActivity(new ActivityDirective(startOffset, activity, anchorId, anchoredToStart), activityId); - } - - /** - * Simulate an activity directive. - * @param activityToSimulate the activity directive to simulate - * @param activityId the ActivityDirectiveId for the activity to simulate - */ - public void simulateActivity(ActivityDirective activityToSimulate, ActivityDirectiveId activityId) - throws SchedulingInterruptedException { - simulateActivities(Map.of(activityId, activityToSimulate)); - } - - public void simulateActivities(@NotNull Map activitiesToSimulate) - throws SchedulingInterruptedException { - if(activitiesToSimulate.isEmpty()) return; - - activitiesInserted.putAll(activitiesToSimulate); - - final HashMap>> resolved = new StartOffsetReducer(planDuration, activitiesToSimulate).compute(); - resolved.get(null).sort(Comparator.comparing(Pair::getRight)); - final var earliestStartOffset = resolved.get(null).get(0); - - if(earliestStartOffset.getRight().noLongerThan(curTime)){ - logger.info("Restarting simulation because earliest start of activity to simulate " + earliestStartOffset.getRight() + " is before current sim time " + curTime); - initSimulation(); - simulateSchedule(activitiesInserted); - } else { - simulateSchedule(activitiesToSimulate); - } - } - - - /** - * Get the simulation results from the Duration.ZERO to the current simulation time point - * @param startTimestamp the timestamp for the start of the planning horizon. Used as epoch for computing SimulationResults. - * @return the simulation results - */ - public SimulationResults getSimulationResults(Instant startTimestamp) throws SchedulingInterruptedException { - return getSimulationResultsUpTo(startTimestamp, curTime); - } - - public Duration getCurrentSimulationEndTime(){ - return curTime; - } - - /** - * Get the simulation results from the Duration.ZERO to a specified end time point. - * The provided simulation results might cover more than the required time period. - * @param startTimestamp the timestamp for the start of the planning horizon. Used as epoch for computing SimulationResults. - * @param endTime the end timepoint. The simulation results will be computed up to this point. - * @return the simulation results - */ - public SimulationResults getSimulationResultsUpTo(Instant startTimestamp, Duration endTime) - throws SchedulingInterruptedException { - //if previous results cover a bigger period, we return do not regenerate - if(endTime.longerThan(curTime)){ - logger.info("Simulating from " + curTime + " to " + endTime + " to get simulation results"); - simulateUntil(endTime); - } else{ - logger.info("Not simulating because asked endTime "+endTime+" is before current sim time " + curTime); - } - final var before = System.nanoTime(); - if(lastSimResults == null || endTime.longerThan(lastSimResultsEnd) || startTimestamp.compareTo(lastSimResults.startTime) != 0) { - if(canceledListener.get()) throw new SchedulingInterruptedException("computing simulation results"); - lastSimResults = SimulationEngine.computeResults( - engine, - startTimestamp, - endTime, - activityTopic, - timeline, - missionModel.getTopics()); - lastSimResultsEnd = endTime; - //while sim results may not be up to date with curTime, a regeneration has taken place after the last insertion - } - this.durationSinceRestart += System.nanoTime() - before; - - return lastSimResults; - } - - private void simulateSchedule(final Map schedule) - throws SchedulingInterruptedException { - final var before = System.nanoTime(); - if (schedule.isEmpty()) { - throw new IllegalArgumentException("simulateSchedule() called with empty schedule, use simulateUntil() instead"); - } - - // Get all activities as close as possible to absolute time, then schedule all activities. - // Using HashMap explicitly because it allows `null` as a key. - // `null` key means that an activity is not waiting on another activity to finish to know its start time - HashMap>> resolved = new StartOffsetReducer( - planDuration, - schedule).compute(); - // Filter out activities that are before the plan start - resolved = StartOffsetReducer.filterOutNegativeStartOffset(resolved); - final var toSchedule = new HashSet(); - toSchedule.add(null); - scheduleActivities( - toSchedule, - schedule, - resolved, - missionModel, - engine - ); - - var allTaskFinished = false; - - if (batch == null) { - batch = engine.extractNextJobs(Duration.MAX_VALUE); - } - //by default, curTime is negative to signal we have not started simulation yet. We set it to 0 when we start. - Duration delta = batch.offsetFromStart().minus(curTime.isNegative() ? Duration.ZERO : curTime); - - //once all tasks are finished, we need to wait for events triggered at the same time - while (!allTaskFinished || delta.isZero()) { - if(canceledListener.get()) throw new SchedulingInterruptedException("simulating"); - - curTime = batch.offsetFromStart(); - timeline.add(delta); - // TODO: Advance a dense time counter so that future tasks are strictly ordered relative to these, - // even if they occur at the same real time. - - // Run the jobs in this batch. - final var commit = engine.performJobs(batch.jobs(), cells, curTime, Duration.MAX_VALUE); - timeline.add(commit.getLeft()); - if(commit.getRight().isPresent()) { - throw new RuntimeException(commit.getRight().get()); - } - - scheduleActivities(getSuccessorsToSchedule(engine), schedule, resolved, missionModel, engine); - - // all tasks are complete : do not exit yet, there might be event triggered at the same time - if (!plannedDirectiveToTask.isEmpty() && plannedDirectiveToTask - .values() - .stream() - .allMatch($ -> engine.getSpan($).isComplete())) { - allTaskFinished = true; - } - - // Update batch and increment real time, if necessary. - batch = engine.extractNextJobs(Duration.MAX_VALUE); - delta = batch.offsetFromStart().minus(curTime); - if(batch.offsetFromStart().longerThan(planDuration)){ - break; - } - } - lastSimResults = null; - this.durationSinceRestart+= System.nanoTime() - before; - } - - /** - * Returns the duration of a terminated simulated activity - * @param activityDirectiveId the activity id - * @return its duration if the activity has been simulated and has finished simulating, an IllegalArgumentException otherwise - */ - public Optional getActivityDuration(ActivityDirectiveId activityDirectiveId){ - //potential cause of non presence: (1) activity is outside plan bounds (2) activity has not been simulated yet - if(!plannedDirectiveToTask.containsKey(activityDirectiveId)) return Optional.empty(); - return engine.getSpan(plannedDirectiveToTask.get(activityDirectiveId)).duration(); - } - - private Set getSuccessorsToSchedule(final SimulationEngine engine) { - final var toSchedule = new HashSet(); - final var iterator = toCheckForDependencyScheduling.entrySet().iterator(); - while(iterator.hasNext()){ - final var taskToCheck = iterator.next(); - if(engine.getSpan(taskToCheck.getValue()).isComplete()){ - toSchedule.add(taskToCheck.getKey()); - iterator.remove(); - } - } - return toSchedule; - } - - private void scheduleActivities( - final Set toScheduleNow, - final Map completeSchedule, - final HashMap>> resolved, - final MissionModel missionModel, - final SimulationEngine engine){ - for(final var predecessor: toScheduleNow) { - for (final var directivePair : resolved.get(predecessor)) { - final var offset = directivePair.getRight(); - final var directiveIdToSchedule = directivePair.getLeft(); - final var serializedDirective = completeSchedule.get(directiveIdToSchedule).serializedActivity(); - final TaskFactory task; - try { - task = missionModel.getTaskFactory(serializedDirective); - } catch (final InstantiationException ex) { - // All activity instantiations are assumed to be validated by this point - throw new Error("Unexpected state: activity instantiation %s failed with: %s" - .formatted(serializedDirective.getTypeName(), ex.toString())); - } - Duration computedStartTime = offset; - if (predecessor != null) { - computedStartTime = (curTime.isEqualTo(Duration.MIN_VALUE) ? Duration.ZERO : curTime).plus(offset); - } - final var taskId = engine.scheduleTask( - computedStartTime, - makeTaskFactory(directiveIdToSchedule, task, activityTopic)); - plannedDirectiveToTask.put(directiveIdToSchedule, taskId); - if (resolved.containsKey(directiveIdToSchedule)) { - toCheckForDependencyScheduling.put(directiveIdToSchedule, taskId); - } - } - } - } - - private static TaskFactory makeTaskFactory( - final ActivityDirectiveId directiveId, - final TaskFactory task, - final Topic activityTopic) { - return executor -> - Task.run($ -> $.emit(directiveId, activityTopic)) - .andThen(task.create(executor)); - } -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacadeUtils.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacadeUtils.java index e1e38cb036..3ce06e0bd0 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacadeUtils.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacadeUtils.java @@ -2,7 +2,6 @@ import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; -import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivity; import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivityId; @@ -79,8 +78,13 @@ public static void pullActivityDurationsIfNecessary( toReplace.forEach(plan::replace); } - private static Optional findSimulatedActivityById(Collection simulatedActivities, final ActivityDirectiveId activityDirectiveId){ - return simulatedActivities.stream().filter(a -> a.directiveId().isPresent() && a.directiveId().get().equals(activityDirectiveId)).findFirst(); + private static Optional findSimulatedActivityById( + Collection simulatedActivities, + final ActivityDirectiveId activityDirectiveId + ){ + return simulatedActivities.stream() + .filter(a -> a.directiveId().isPresent() && a.directiveId().get().equals(activityDirectiveId)) + .findFirst(); } public static void updatePlanWithChildActivities( @@ -130,10 +134,14 @@ private static Optional getIdOfRootParent( } } - private static Optional getActivityDuration( + public static Optional getActivityDuration( final ActivityDirectiveId activityDirectiveId, - final SimulationResultsComputerInputs simulationResultsInputs){ - return simulationResultsInputs.engine().getSpan(simulationResultsInputs.activityDirectiveIdTaskIdMap().get(activityDirectiveId)).duration(); + final SimulationResultsComputerInputs simulationResultsInputs + ){ + return simulationResultsInputs.engine() + .getSpan(simulationResultsInputs.activityDirectiveIdTaskIdMap() + .get(activityDirectiveId)) + .duration(); } public static ActivityDirective schedulingActToActivityDir( diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java index 46f27304b6..60fa027bf3 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java @@ -31,8 +31,6 @@ import java.util.Map; import java.util.Optional; -import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOURS; -import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; import static gov.nasa.jpl.aerie.scheduler.TestUtility.assertSetEquality; import static org.junit.jupiter.api.Assertions.*; @@ -131,23 +129,6 @@ private static PlanInMemory makePlanAB012(Problem problem) { return plan; } - @Test - public void test(){ - final var problem = makeTestMissionAB(); - final var plan = new PlanInMemory(); - final var actTypeA = problem.getActivityType("ControllableDurationActivity"); - final var first =SchedulingActivityDirective.of(actTypeA, t1hr, d1min, null, true); - final var second = SchedulingActivityDirective.of(actTypeA, t2hr, d1min, null, true); - plan.add(first); - plan.add(second); - final var changeFirst = SchedulingActivityDirective.copyOf(first, ZERO); - final var plan3 = new PlanInMemory(); - plan3.add(changeFirst); - final var plan2 = new PlanInMemory(); - //final var diffs = PlanDiff.diff(plan, plan2); - //final var diff2 = PlanDiff.diff(plan2, plan); - } - @Test public void getNextSolution_initialPlanInOutput() throws SchedulingInterruptedException { final var problem = makeTestMissionAB(); @@ -177,7 +158,6 @@ public void getNextSolution_proceduralGoalCreatesActivities() throws SchedulingI final var plan = solver.getNextSolution().orElseThrow(); assertSetEquality(plan.getActivitiesByTime(), expectedPlan.getActivitiesByTime()); - //1 full sim at the beginning + 3 sims for each act insertion } @Test diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java index 7d07671b50..8a75f8790f 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java @@ -2,13 +2,17 @@ import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; +import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.DirectiveTypeRegistry; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; import gov.nasa.jpl.aerie.merlin.driver.OneStepTask; import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivity; import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivityId; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; +import gov.nasa.jpl.aerie.merlin.driver.SimulationResultsComputerInputs; import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; @@ -26,7 +30,6 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.jpl.aerie.scheduler.SchedulingInterruptedException; import org.apache.commons.lang3.tuple.Triple; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -48,12 +51,6 @@ public class AnchorSchedulerTest { private final Duration tenDays = Duration.duration(10 * 60 * 60 * 24, Duration.SECONDS); private final static Duration oneMinute = Duration.of(60, Duration.SECONDS); private final Map arguments = Map.of("unusedArg", SerializedValue.of("test-param")); - private ResumableSimulationDriver driver; - - @BeforeEach - void beforeEach() { - driver = new ResumableSimulationDriver<>(AnchorTestModel, tenDays, () -> false); - } @Nested public final class AnchorsSimulationDriverTests { @@ -62,6 +59,25 @@ public final class AnchorsSimulationDriverTests { private final SerializedValue computedAttributes = new SerializedValue.MapValue(Map.of()); private final Instant planStart = Instant.EPOCH; + + public SimulationResultsComputerInputs simulateActivities( final Map schedule){ + return CheckpointSimulationDriver.simulateWithCheckpoints( + AnchorTestModel, + schedule, + planStart, + tenDays, + planStart, + tenDays, + $ -> {}, + () -> false, + CheckpointSimulationDriver.CachedSimulationEngine.empty(AnchorTestModel), + (a) -> false, + CheckpointSimulationDriver.onceAllActivitiesAreFinished(), + new InMemoryCachedEngineStore(1), + new SimulationEngineConfiguration(Map.of(), planStart, new MissionModelId(1)) + ); + } + /** * Asserts equality based on the following fields of SimulationResults: * - startTime @@ -216,16 +232,14 @@ public void activitiesAnchoredToPlan() throws SchedulingInterruptedException { simulatedActivities, Map.of(), //unfinished planStart, - tenDays, // simulation duration + tenDays.minus(Duration.of(9, Duration.MINUTE)), // simulation duration modelTopicList, new TreeMap<>() //events ); - driver.simulateActivities(resolveToPlanStartAnchors); - final var actualSimResults = driver.getSimulationResultsUpTo(planStart, tenDays); + final var actualSimResults = simulateActivities(resolveToPlanStartAnchors).computeResults(); assertEqualsSimulationResults(expectedSimResults, actualSimResults); - assertEquals(1, driver.getCountSimulationRestarts()); } @Test @@ -334,11 +348,9 @@ public void activitiesAnchoredToOtherActivities() throws SchedulingInterruptedEx new TreeMap<>() //events ); - driver.simulateActivities(activitiesToSimulate); - final var actualSimResults = driver.getSimulationResults(planStart); + final var actualSimResults = simulateActivities(activitiesToSimulate).computeResults(); assertEqualsSimulationResults(expectedSimResults, actualSimResults); - assertEquals(1, driver.getCountSimulationRestarts()); } @Test @@ -351,8 +363,8 @@ public void activitiesAnchoredToOtherActivitiesSimple() throws SchedulingInterru activitiesToSimulate.put( new ActivityDirectiveId(1), new ActivityDirective(oneMinute, serializedDelayDirective, new ActivityDirectiveId(0), false)); - driver.simulateActivities(activitiesToSimulate); - final var durationOfAnchoredActivity = driver.getActivityDuration(new ActivityDirectiveId(1)); + final var simulationResults = simulateActivities(activitiesToSimulate); + final var durationOfAnchoredActivity = SimulationFacadeUtils.getActivityDuration(new ActivityDirectiveId(1), simulationResults); assertTrue(durationOfAnchoredActivity.isPresent()); } @@ -502,9 +514,7 @@ public void decomposingActivitiesAndAnchors() throws SchedulingInterruptedExcept new ActivityDirectiveId(23), new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(4, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(23)), computedAttributes)); - // Custom assertion, as Decomposition children can end up simulated in different positions between runs - driver.simulateActivities(activitiesToSimulate); - final var actualSimResults = driver.getSimulationResults(planStart); + final var actualSimResults = simulateActivities(activitiesToSimulate).computeResults(); assertEquals(planStart, actualSimResults.startTime); assertTrue(actualSimResults.unfinishedActivities.isEmpty()); @@ -596,7 +606,6 @@ public void decomposingActivitiesAndAnchors() throws SchedulingInterruptedExcept // We have examined all the children assertTrue(childSimulatedActivities.isEmpty()); - assertEquals(1, driver.getCountSimulationRestarts()); } @Test @@ -636,12 +645,10 @@ public void naryTreeAnchorChain() throws SchedulingInterruptedException{ modelTopicList, new TreeMap<>() //events ); - driver.simulateActivities(activitiesToSimulate); - final var actualSimResults = driver.getSimulationResults(planStart); + final var actualSimResults = simulateActivities(activitiesToSimulate).computeResults(); assertEquals(3906, expectedSimResults.simulatedActivities.size()); assertEqualsSimulationResults(expectedSimResults, actualSimResults); - assertEquals(1, driver.getCountSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationTest.java deleted file mode 100644 index 4f40990a76..0000000000 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.simulation; - -import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; -import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.scheduler.SchedulingInterruptedException; -import gov.nasa.jpl.aerie.scheduler.SimulationUtility; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.Map; - -import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECONDS; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class ResumableSimulationTest { - ResumableSimulationDriver resumableSimulationDriver; - Duration endOfLastAct; - - private final Duration tenHours = Duration.of(10, Duration.HOURS); - private final Duration fiveHours = Duration.of(5, Duration.HOURS); - - @BeforeEach - public void init() throws SchedulingInterruptedException { - final var acts = getActivities(); - final var fooMissionModel = SimulationUtility.getFooMissionModel(); - resumableSimulationDriver = new ResumableSimulationDriver<>(fooMissionModel,tenHours, ()-> false); - for (var act : acts) { - resumableSimulationDriver.simulateActivity(act.start, act.activity, null, true, act.id); - } - } - @Test - public void simulationResultsTest() throws SchedulingInterruptedException { - final var now = Instant.now(); - //ensures that simulation results are generated until the end of the last act; - var simResults = resumableSimulationDriver.getSimulationResults(now); - assert(simResults.realProfiles.get("/utcClock").getRight().get(0).extent().isEqualTo(endOfLastAct)); - /* ensures that when current simulation results cover more than the asked period and that nothing has happened - between two requests, the same results are returned */ - var simResults2 = resumableSimulationDriver.getSimulationResultsUpTo(now, Duration.of(7, SECONDS)); - assertEquals(simResults, simResults2); - } - - @Test - public void simulationResultsTest2() throws SchedulingInterruptedException{ - /* ensures that when the passed start epoch is not equal to the one used for previously computed results, the results are re-computed */ - var simResults = resumableSimulationDriver.getSimulationResults(Instant.now()); - assert(simResults.realProfiles.get("/utcClock").getRight().get(0).extent().isEqualTo(endOfLastAct)); - var simResults2 = resumableSimulationDriver.getSimulationResultsUpTo(Instant.now(), Duration.of(7, SECONDS)); - assertNotEquals(simResults, simResults2); - } - - @Test - public void simulationResultsTest3() throws SchedulingInterruptedException{ - /* ensures that when current simulation results cover less than the asked period and that nothing has happened - between two requests, the results are re-computed */ - final var now = Instant.now(); - var simResults2 = resumableSimulationDriver.getSimulationResultsUpTo(now, Duration.of(7, SECONDS)); - var simResults = resumableSimulationDriver.getSimulationResults(now); - assert(simResults.realProfiles.get("/utcClock").getRight().get(0).extent().isEqualTo(endOfLastAct)); - assertNotEquals(simResults, simResults2); - } - - @Test - public void durationTest(){ - final var acts = getActivities(); - var act1Dur = resumableSimulationDriver.getActivityDuration(acts.get(0).id()); - var act2Dur = resumableSimulationDriver.getActivityDuration(acts.get(1).id()); - assertTrue(act1Dur.isPresent() && act2Dur.isPresent()); - assertTrue(act1Dur.get().isEqualTo(Duration.of(2, SECONDS))); - assertTrue(act2Dur.get().isEqualTo(Duration.of(2, SECONDS))); - } - - @Test - public void testStopsAtEndOfPlanningHorizon() throws SchedulingInterruptedException { - final var fooSchedulerModel = SimulationUtility.getFooSchedulerModel(); - final var activity = new TestSimulatedActivity( - Duration.of(0, SECONDS), - new SerializedActivity("ControllableDurationActivity", Map.of("duration", fooSchedulerModel.serializeDuration(tenHours))), - new ActivityDirectiveId(1)); - final var fooMissionModel = SimulationUtility.getFooMissionModel(); - resumableSimulationDriver = new ResumableSimulationDriver<>(fooMissionModel, fiveHours, ()-> false); - resumableSimulationDriver.initSimulation(); - resumableSimulationDriver.clearActivitiesInserted(); - resumableSimulationDriver.simulateActivity(activity.start, activity.activity, null, true, activity.id); - assertEquals(fiveHours, resumableSimulationDriver.getCurrentSimulationEndTime()); - assert(resumableSimulationDriver.getSimulationResults(Instant.now()).unfinishedActivities.size() == 1); - } - - private ArrayList getActivities(){ - final var acts = new ArrayList(); - var act1 = new TestSimulatedActivity( - Duration.of(0, SECONDS), - new SerializedActivity("BasicActivity", Map.of()), - new ActivityDirectiveId(1)); - acts.add(act1); - var act2 = new TestSimulatedActivity( - Duration.of(14, SECONDS), - new SerializedActivity("BasicActivity", Map.of()), - new ActivityDirectiveId(2)); - acts.add(act2); - - endOfLastAct = Duration.of(16,SECONDS); - return acts; - } - - record TestSimulatedActivity(Duration start, SerializedActivity activity, ActivityDirectiveId id){} -} From 164a913920c1d94f59ae00fcb1a79b7dbfef464a Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Thu, 4 Apr 2024 21:53:04 -0700 Subject: [PATCH 38/43] Inline makeTaskFactory --- .../merlin/driver/CheckpointSimulationDriver.java | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java index 5aff6f7e6b..3b0c263a8f 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java @@ -7,6 +7,7 @@ import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; @@ -18,7 +19,6 @@ import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -434,7 +434,9 @@ private static Map scheduleActivities( } final var taskId = engine.scheduleTask( computedStartTime, - makeTaskFactory(directiveIdToSchedule, task, activityTopic)); + executor -> + Task.run(scheduler -> scheduler.emit(directiveIdToSchedule, activityTopic)) + .andThen(task.create(executor))); activityToTask.put(directiveIdToSchedule, taskId); if (resolved.containsKey(directiveIdToSchedule)) { toCheckForDependencyScheduling.put(directiveIdToSchedule, taskId); @@ -444,13 +446,6 @@ private static Map scheduleActivities( return toCheckForDependencyScheduling; } - private static TaskFactory makeTaskFactory( - final ActivityDirectiveId directiveId, - final TaskFactory task, - final Topic activityTopic) { - return executor -> new DuplicatableTaskFactory<>(directiveId, task, activityTopic, executor); - } - public static Function onceAllActivitiesAreFinished(){ return simulationState -> simulationState.activityDirectiveIdSpanIdMap() .values() From 0fcb51a01d09989fc077c961d8441b1da68efb00 Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Thu, 11 Apr 2024 09:33:43 -0700 Subject: [PATCH 39/43] Add default implementation of duplicate that throws UnsupportedOperationException --- .../java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java index 8696126e24..71f7b519ff 100644 --- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java +++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java @@ -41,7 +41,9 @@ default void release() {} * @param executor the executor to use for the new Task * @return a copy of this Task that can be stepped independently from this Task */ - Task duplicate(Executor executor); + default Task duplicate(Executor executor) { + throw new UnsupportedOperationException("Tasks must implement duplicate in order to be used in a simulation checkpoint"); + } default Task andThen(Task task2) { return new Task<>() { From 542057936ef27bcf224b5024109bda35f397b1ac Mon Sep 17 00:00:00 2001 From: Matthew Dailis Date: Thu, 11 Apr 2024 09:50:40 -0700 Subject: [PATCH 40/43] Document MAX_NB_CACHED_SIMULATION_ENGINES and set minimum to 1 --- deployment/Environment.md | 1 + deployment/docker-compose.yml | 3 +-- docker-compose.yml | 4 ++-- .../scheduler/worker/SchedulerWorkerAppDriver.java | 11 ++++++++++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/deployment/Environment.md b/deployment/Environment.md index c00906e9d2..d8556be552 100644 --- a/deployment/Environment.md +++ b/deployment/Environment.md @@ -69,6 +69,7 @@ See the [environment variables document](https://github.com/NASA-AMMOS/aerie-gat | `SCHEDULER_DB_PASSWORD` | Password of the Scheduler DB User | `string` | | | `SCHEDULER_OUTPUT_MODE` | How scheduler output is sent back to Aerie | `string` | UpdateInputPlanWithNewActivities | | `SCHEDULER_RULES_JAR` | Jar file to load scheduling rules from (until user input to database) | `string` | /usr/src/app/merlin_file_store/scheduler_rules.jar | +| `MAX_NB_CACHED_SIMULATION_ENGINES` | The maximum number of simulation engines to cache in memory during a scheduling run. Must be at least 1 | `number` | 1 | ## Aerie Sequencing diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 75a0f788e5..00a09583d7 100755 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -54,7 +54,6 @@ services: MERLIN_DB_PASSWORD: "${MERLIN_PASSWORD}" MERLIN_WORKER_LOCAL_STORE: /usr/src/app/merlin_file_store SIMULATION_PROGRESS_POLL_PERIOD_MILLIS: 2000 - THREADED_TASK_CACHE_READS: false JAVA_OPTS: > -Dorg.slf4j.simpleLogger.defaultLogLevel=INFO -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=WARN @@ -101,7 +100,7 @@ services: SCHEDULER_OUTPUT_MODE: UpdateInputPlanWithNewActivities MERLIN_LOCAL_STORE: /usr/src/app/merlin_file_store SCHEDULER_RULES_JAR: /usr/src/app/merlin_file_store/scheduler_rules.jar - THREADED_TASK_CACHE_READS: true + MAX_NB_CACHED_SIMULATION_ENGINES: 1 JAVA_OPTS: > -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=INFO -Dorg.slf4j.simpleLogger.logFile=System.err diff --git a/docker-compose.yml b/docker-compose.yml index 169b0db1e9..a4e51528e0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -178,7 +178,7 @@ services: SCHEDULER_OUTPUT_MODE: UpdateInputPlanWithNewActivities MERLIN_LOCAL_STORE: /usr/src/app/merlin_file_store SCHEDULER_RULES_JAR: /usr/src/app/merlin_file_store/scheduler_rules.jar - MAX_NB_CACHED_SIMULATION_ENGINE: 1 + MAX_NB_CACHED_SIMULATION_ENGINES: 1 JAVA_OPTS: > -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG @@ -205,7 +205,7 @@ services: SCHEDULER_OUTPUT_MODE: UpdateInputPlanWithNewActivities MERLIN_LOCAL_STORE: /usr/src/app/merlin_file_store SCHEDULER_RULES_JAR: /usr/src/app/merlin_file_store/scheduler_rules.jar - MAX_NB_CACHED_SIMULATION_ENGINE: 1 + MAX_NB_CACHED_SIMULATION_ENGINES: 1 JAVA_OPTS: > -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java index 0ff8f840c6..e90dff569c 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java @@ -25,8 +25,12 @@ import gov.nasa.jpl.aerie.scheduler.worker.services.SchedulingDSLCompilationService; import gov.nasa.jpl.aerie.scheduler.worker.services.SynchronousSchedulerAgent; import io.javalin.Javalin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public final class SchedulerWorkerAppDriver { + private static final Logger logger = LoggerFactory.getLogger(SchedulerWorkerAppDriver.class); + public static void main(String[] args) throws Exception { final var config = loadConfiguration(); @@ -127,6 +131,11 @@ private static String getEnv(final String key, final String fallback){ } private static WorkerAppConfiguration loadConfiguration() { + int maxNbCachedSimulationEngine = Integer.parseInt(getEnv("MAX_NB_CACHED_SIMULATION_ENGINES", "1")); + if (maxNbCachedSimulationEngine < 1) { + logger.warn("MAX_NB_CACHED_SIMULATION_ENGINES is " + maxNbCachedSimulationEngine + " but minimum is 1. Setting to 1."); + maxNbCachedSimulationEngine = 1; + } return new WorkerAppConfiguration( new PostgresStore(getEnv("AERIE_DB_SERVER", "postgres"), getEnv("SCHEDULER_DB_USER", ""), @@ -138,7 +147,7 @@ private static WorkerAppConfiguration loadConfiguration() { Path.of(getEnv("SCHEDULER_RULES_JAR", "/usr/src/app/merlin_file_store/scheduler_rules.jar")), PlanOutputMode.valueOf((getEnv("SCHEDULER_OUTPUT_MODE", "CreateNewOutputPlan"))), getEnv("HASURA_GRAPHQL_ADMIN_SECRET", ""), - Integer.parseInt(getEnv("MAX_NB_CACHED_SIMULATION_ENGINE", "1")) + maxNbCachedSimulationEngine ); } } From 9e7442cd2d146759382167cb73722f2514ca2e91 Mon Sep 17 00:00:00 2001 From: maillard Date: Thu, 11 Apr 2024 17:16:12 -0700 Subject: [PATCH 41/43] The error lambda is now the first one of the class. --- .../src/test/java/gov/nasa/jpl/aerie/e2e/SimulationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SimulationTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SimulationTests.java index 75bd63ff71..2ee2202b6f 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SimulationTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SimulationTests.java @@ -564,7 +564,7 @@ void noDirectiveIdOnDaemon() throws IOException { // The trace starts at the original exception and doesn't include the intermediary SimulationException final var expectedStart = """ java.lang.RuntimeException: Daemon task exception raised. - \tat gov.nasa.jpl.aerie.foomissionmodel.Mission.lambda$new$1(Mission.java"""; + \tat gov.nasa.jpl.aerie.foomissionmodel.Mission.lambda$new$0(Mission.java"""; assertTrue(reason.trace().startsWith(expectedStart)); } From bc4ce3bb9ffa281c0333905e780ed9dab2973c98 Mon Sep 17 00:00:00 2001 From: maillard Date: Tue, 4 Jun 2024 10:49:55 -0700 Subject: [PATCH 42/43] Update StartOffsetReducer --- .../jpl/aerie/merlin/driver/StartOffsetReducer.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/StartOffsetReducer.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/StartOffsetReducer.java index d0a4f76618..3611545ea9 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/StartOffsetReducer.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/StartOffsetReducer.java @@ -137,6 +137,13 @@ public static List> adjustStartOffset(List

>> filterOutNegativeStartOffset(HashMap>> toFilter) { + return filterOutStartOffsetBefore(toFilter, Duration.ZERO); + } + + public static HashMap>> filterOutStartOffsetBefore( + final HashMap>> toFilter, + final Duration duration) + { if(toFilter == null) return null; // Create a deep copy of toFilter (The Pairs are immutable, so they do not need to be copied) @@ -155,16 +162,16 @@ public static HashMap(toFilter .get(null) .stream() - .filter(pair -> pair.getValue().isNegative()) + .filter(pair -> pair.getValue().shorterThan(duration)) .toList()); while(!beforeStartTime.isEmpty()){ - final Pair currentPair = beforeStartTime.remove(beforeStartTime.size() - 1); + final Pair currentPair = beforeStartTime.removeLast(); if(filtered.containsKey(currentPair.getLeft())) { beforeStartTime.addAll(filtered.get(currentPair.getLeft())); filtered.remove(currentPair.getLeft()); } } - filtered.get(null).removeIf(pair -> pair.getValue().isNegative()); + filtered.get(null).removeIf(pair -> pair.getValue().shorterThan(duration)); return filtered; } } From 50ec917b42e28fee12d9ac8a177b976c3c742a3a Mon Sep 17 00:00:00 2001 From: maillard Date: Mon, 17 Jun 2024 14:23:14 +0200 Subject: [PATCH 43/43] Remove duplicates in Plan and Evaluation --- .../nasa/jpl/aerie/scheduler/model/Plan.java | 17 +++++------------ .../jpl/aerie/scheduler/model/PlanInMemory.java | 9 +-------- .../simulation/SimulationFacadeUtils.java | 2 +- .../jpl/aerie/scheduler/solver/Evaluation.java | 13 ------------- .../aerie/scheduler/solver/PrioritySolver.java | 2 +- 5 files changed, 8 insertions(+), 35 deletions(-) diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Plan.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Plan.java index 7d5a99942c..c263617651 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Plan.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Plan.java @@ -19,13 +19,6 @@ */ public interface Plan { - /** - * Replace a directive by another in the plan and its attached Evaluation - * @param toBeReplaced the activity to be replaced - * @param replacement the replacement activity - */ - void replace(SchedulingActivityDirective toBeReplaced, SchedulingActivityDirective replacement); - /** * Duplicates a plan * @return the duplicate plan @@ -67,11 +60,6 @@ public interface Plan { */ void remove(SchedulingActivityDirective act); - /** - * fetches activities in the plan ordered by start time - * - * @return set of all activities in the plan ordered by start time - */ /** * replace and old activity by a new one * @param oldAct Old Activity @@ -79,6 +67,11 @@ public interface Plan { */ void replaceActivity(SchedulingActivityDirective oldAct, SchedulingActivityDirective newAct); + /** + * fetches activities in the plan ordered by start time + * + * @return set of all activities in the plan ordered by start time + */ List getActivitiesByTime(); /** diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/PlanInMemory.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/PlanInMemory.java index 53a862ac54..a7a3c440a1 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/PlanInMemory.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/PlanInMemory.java @@ -46,13 +46,6 @@ public PlanInMemory() { this.actsByTime = new TreeMap<>(); } - @Override - public void replace(final SchedulingActivityDirective toBeReplaced, final SchedulingActivityDirective replacement) { - if(evaluation != null) evaluation.replace(toBeReplaced, replacement); - remove(toBeReplaced); - add(replacement); - } - public PlanInMemory(final PlanInMemory other){ if(other.evaluation != null) this.evaluation = other.evaluation.duplicate(); this.actsByTime = new TreeMap<>(); @@ -137,7 +130,7 @@ public List getActivitiesByTime() { public void replaceActivity(SchedulingActivityDirective oldAct, SchedulingActivityDirective newAct){ this.remove(oldAct); this.add(newAct); - this.evaluation.updateGoalEvals(oldAct, newAct); + if(evaluation != null) this.evaluation.updateGoalEvals(oldAct, newAct); } /** diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacadeUtils.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacadeUtils.java index 3ce06e0bd0..bb72056c17 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacadeUtils.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacadeUtils.java @@ -75,7 +75,7 @@ public static void pullActivityDurationsIfNecessary( //if not, maybe the activity is not finished } } - toReplace.forEach(plan::replace); + toReplace.forEach(plan::replaceActivity); } private static Optional findSimulatedActivityById( diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/Evaluation.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/Evaluation.java index 7f90e24e50..2370c467a7 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/Evaluation.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/Evaluation.java @@ -166,19 +166,6 @@ public java.util.Collection getGoals() { return goalEvals.keySet(); } - /** - * Replaces an activity in the Evaluation by another activity - * @param toBeReplaced the activity to be replaced - * @param replacement the replacement activity - */ - public void replace( - final SchedulingActivityDirective toBeReplaced, - final SchedulingActivityDirective replacement){ - for(final var goalEval: goalEvals.entrySet()){ - goalEval.getValue().replace(toBeReplaced, replacement); - } - } - /** * Duplicates the Evaluation * @return the duplicate evaluation diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java index c0bac6d815..058b227499 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java @@ -560,7 +560,7 @@ else if(!analysisOnly && (missing instanceof MissingActivityTemplateConflict mi missingAssociationConflict.getAnchorToStart().get(), startOffset ); - plan.replace(act,replacementAct); + plan.replaceActivity(act,replacementAct); //decision-making here, we choose the first satisfying activity plan.getEvaluation().forGoal(goal).associate(replacementAct, false); itConflicts.remove();