From 222b021bde9033b1e3435dc1d128240544634ff0 Mon Sep 17 00:00:00 2001 From: maillard Date: Mon, 24 Jul 2023 10:11:54 -0700 Subject: [PATCH 01/10] Changes in scheduler simulation - Harmonize condition between SimulationFacade and ResumableSimulationDriver for restart - Initialize sim time at negative value to avoid useless simulation when an activity starts at Duration.ZERO. Also, avoid actually launching daemon task at initialization, wait for the real start of simulation. - Remove need for buffer in simulation results of scheduler The need for buffer was due to a bound checking in Windows.intoSpans failing if one of the bound not included. And, the last segment of all profiles (ending at the end of the simulation horizon) are always open. I have included the special case in the checking of bounds. --- .../jpl/aerie/constraints/time/Interval.java | 8 ++++++ .../jpl/aerie/constraints/time/Windows.java | 12 +++++--- .../jpl/aerie/merlin/driver/MissionModel.java | 4 +++ .../simulation/ResumableSimulationDriver.java | 28 +++++++++---------- .../simulation/SimulationFacade.java | 9 ++---- .../SimulationResultsConverter.java | 2 +- .../aerie/scheduler/SimulationFacadeTest.java | 10 +++---- 7 files changed, 42 insertions(+), 31 deletions(-) diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Interval.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Interval.java index 0767bd7302..b70fda430b 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Interval.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Interval.java @@ -288,6 +288,14 @@ public static int compareEndToEnd(final Interval x, final Interval y) { return 0; } + public static boolean hasSameStart(Interval x, Interval y){ + return compareStartToStart(x,y) == 0; + } + + public static boolean hasSameEnd(Interval x, Interval y){ + return compareEndToEnd(x,y) == 0; + } + public static int compareStartToEnd(final Interval x, final Interval y) { // First, order by absolute time. if (!x.start.isEqualTo(y.end)) { diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Windows.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Windows.java index d7ab1ca3ab..651faafd54 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Windows.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/time/Windows.java @@ -496,14 +496,18 @@ public Spans intoSpans(final Interval bounds) { boolean boundsStartContained = false; boolean boundsEndContained = false; if(this.segments.size() == 1){ - if (segments.get(0).interval().contains(bounds.start)) boundsStartContained = true; - if (segments.get(0).interval().contains(bounds.end)) boundsEndContained = true; + if (segments.get(0).interval().contains(bounds.start) || + Interval.hasSameStart(segments.get(0).interval(), bounds)) boundsStartContained = true; + if (segments.get(0).interval().contains(bounds.end) || + Interval.hasSameEnd(segments.get(0).interval(), bounds)) boundsEndContained = true; } for (int i = 0; i < this.segments.size() - 1; i++) { final var leftInterval = this.segments.get(i).interval(); final var rightInterval = this.segments.get(i+1).interval(); - if (leftInterval.contains(bounds.start) || rightInterval.contains(bounds.start)) boundsStartContained = true; - if (leftInterval.contains(bounds.end) || rightInterval.contains(bounds.end)) boundsEndContained = true; + if((leftInterval.contains(bounds.start) || rightInterval.contains(bounds.start)) || + Interval.hasSameStart(leftInterval, bounds) || Interval.hasSameStart(rightInterval, bounds)) boundsStartContained = true; + if((leftInterval.contains(bounds.end) || rightInterval.contains(bounds.end)) || + Interval.hasSameEnd(leftInterval, bounds) || Interval.hasSameEnd(rightInterval, bounds)) boundsEndContained = true; if (leftInterval.isStrictlyBefore(bounds)) continue; if (rightInterval.isStrictlyAfter(bounds)) continue; if (!leftInterval.adjacent(rightInterval)) { 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 977eb106ab..d53ee219a4 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 @@ -72,6 +72,10 @@ public Iterable> getTopics() { return this.topics; } + public boolean hasDaemons(){ + return !this.daemons.isEmpty(); + } + public record SerializableTopic ( String name, Topic topic, 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 a507c74927..09a7605368 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 @@ -31,7 +31,9 @@ public class ResumableSimulationDriver implements AutoCloseable { private static final Logger logger = LoggerFactory.getLogger(ResumableSimulationDriver.class); - private Duration curTime = Duration.ZERO; + /* 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(); @@ -62,7 +64,6 @@ public ResumableSimulationDriver(MissionModel missionModel, Duration plan this.planDuration = planDuration; countSimulationRestarts = 0; initSimulation(); - batch = null; } // This method is currently only used in one test. @@ -75,27 +76,25 @@ public ResumableSimulationDriver(MissionModel missionModel, Duration plan lastSimResultsEnd = Duration.ZERO; 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()); - /* The current real time. */ - curTime = Duration.ZERO; + 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, curTime); + engine.trackResource(name, resource, Duration.ZERO); } // Start daemon task(s) immediately, before anything else happens. { - engine.scheduleTask(Duration.ZERO, missionModel.getDaemon()); - - batch = engine.extractNextJobs(Duration.MAX_VALUE); - final var commit = engine.performJobs(batch.jobs(), cells, curTime, Duration.MAX_VALUE); - timeline.add(commit); + if(missionModel.hasDaemons()) { + engine.scheduleTask(Duration.ZERO, missionModel.getDaemon()); + batch = engine.extractNextJobs(Duration.MAX_VALUE); + } } countSimulationRestarts++; } @@ -120,7 +119,8 @@ private void simulateUntil(Duration endTime){ } // Increment real time, if necessary. while(!batch.offsetFromStart().longerThan(endTime) && !endTime.isEqualTo(Duration.MAX_VALUE)) { - final var delta = batch.offsetFromStart().minus(curTime); + //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. @@ -241,8 +241,8 @@ private void simulateSchedule(final Map if (batch == null) { batch = engine.extractNextJobs(Duration.MAX_VALUE); } - // Increment real time, if necessary. - Duration delta = batch.offsetFromStart().minus(curTime); + //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()) { 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 ca37c801c3..7fadfb931e 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 @@ -47,7 +47,6 @@ public class SimulationFacade implements AutoCloseable{ private final Map planActDirectiveIdToSimulationActivityDirectiveId = new HashMap<>(); private final Map insertedActivities; - private static final Duration MARGIN = Duration.of(5, MICROSECONDS); //counts the total number of simulation restarts, used as performance metric in the scheduler private int pastSimulationRestarts; @@ -149,7 +148,7 @@ public void removeAndInsertActivitiesFromSimulation( } final var allActivitiesToSimulate = new ArrayList<>(activitiesToAdd); //reset resumable simulation - if(atLeastOneActualRemoval || earliestActStartTime.shorterThan(this.driver.getCurrentSimulationEndTime())){ + if(atLeastOneActualRemoval || earliestActStartTime.noLongerThan(this.driver.getCurrentSimulationEndTime())){ allActivitiesToSimulate.addAll(insertedActivities.keySet()); insertedActivities.clear(); planActDirectiveIdToSimulationActivityDirectiveId.clear(); @@ -236,12 +235,8 @@ public void simulateActivity(final SchedulingActivityDirective activity) throws } public void computeSimulationResultsUntil(final Duration endTime) throws SimulationException { - var endTimeWithMargin = endTime; - if(endTime.noLongerThan(Duration.MAX_VALUE.minus(MARGIN))){ - endTimeWithMargin = endTime.plus(MARGIN); - } try { - final var results = driver.getSimulationResultsUpTo(this.planningHorizon.getStartInstant(), endTimeWithMargin); + final var results = driver.getSimulationResultsUpTo(this.planningHorizon.getStartInstant(), endTime); //compare references if(results != lastSimDriverResults) { //simulation results from the last simulation, as converted for use by the constraint evaluation engine 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 bf64c29df7..d547aa2285 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 @@ -30,7 +30,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.between(Duration.ZERO, planDuration), + Interval.betweenClosedOpen(Duration.ZERO, planDuration), 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 48d52a0096..2eb765414a 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 @@ -203,7 +203,7 @@ public void whenValueAboveDoubleOnSimplePlan() throws SimulationFacade.Simulatio var actual = new GreaterThan(getFruitRes(), new RealValue(2.9)).evaluate(facade.getLatestConstraintSimulationResults()); var expected = new Windows( Segment.of(interval(0, Inclusive, 2, Exclusive, SECONDS), true), - Segment.of(interval(2, 5, SECONDS), false) + Segment.of(interval(2, Inclusive,5, Exclusive, SECONDS), false) ); assertThat(actual).isEqualTo(expected); } @@ -215,7 +215,7 @@ public void whenValueBelowDoubleOnSimplePlan() throws SimulationFacade.Simulatio var actual = new LessThan(getFruitRes(), new RealValue(3.0)).evaluate(facade.getLatestConstraintSimulationResults()); var expected = new Windows( Segment.of(interval(0, Inclusive, 2, Exclusive, SECONDS), false), - Segment.of(interval(2, 5, SECONDS), true) + Segment.of(interval(2, Inclusive, 5, Exclusive, SECONDS), true) ); assertThat(actual).isEqualTo(expected); } @@ -228,7 +228,7 @@ public void whenValueBetweenDoubleOnSimplePlan() throws SimulationFacade.Simulat 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, Inclusive, SECONDS), false) + Segment.of(interval(2, Inclusive, 5, Exclusive, SECONDS), false) ); assertThat(actual).isEqualTo(expected); } @@ -241,7 +241,7 @@ public void whenValueEqualDoubleOnSimplePlan() throws SimulationFacade.Simulatio 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, Inclusive, SECONDS), false) + Segment.of(interval(2, Inclusive, 5, Exclusive, SECONDS), false) ); assertThat(actual).isEqualTo(expected); } @@ -254,7 +254,7 @@ public void whenValueNotEqualDoubleOnSimplePlan() throws SimulationFacade.Simula 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, Inclusive, SECONDS), true) + Segment.of(interval(2, Inclusive, 5, Exclusive, SECONDS), true) ); assertThat(actual).isEqualTo(expected); } From 4df68e8cf2714c38c973e708e0e111be43ef1da6 Mon Sep 17 00:00:00 2001 From: maillard Date: Mon, 24 Jul 2023 10:35:43 -0700 Subject: [PATCH 02/10] Exit getAllChildActivities before simulation if there is no activity in the plan --- .../nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java | 1 + 1 file changed, 1 insertion(+) 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 7fadfb931e..b41f1f9c7a 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 @@ -110,6 +110,7 @@ private ActivityDirectiveId getIdOfRootParent(SimulationResults results, Simulat public Map getAllChildActivities(final Duration endTime) throws SimulationException { + if(insertedActivities.size() == 0) return Map.of(); computeSimulationResultsUntil(endTime); final Map childActivities = new HashMap<>(); this.lastSimDriverResults.simulatedActivities.forEach( (activityInstanceId, activity) -> { From bd556243090230d4dc9b809b9384b6b76efb461b Mon Sep 17 00:00:00 2001 From: maillard Date: Mon, 24 Jul 2023 10:37:07 -0700 Subject: [PATCH 03/10] Input simulation results imply domain of converted results --- .../jpl/aerie/scheduler/simulation/SimulationFacade.java | 2 +- .../scheduler/simulation/SimulationResultsConverter.java | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) 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 b41f1f9c7a..c786fdd030 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 @@ -241,7 +241,7 @@ public void computeSimulationResultsUntil(final Duration endTime) throws Simulat //compare references if(results != lastSimDriverResults) { //simulation results from the last simulation, as converted for use by the constraint evaluation engine - lastSimConstraintResults = SimulationResultsConverter.convertToConstraintModelResults(results, planningHorizon.getAerieHorizonDuration()); + lastSimConstraintResults = SimulationResultsConverter.convertToConstraintModelResults(results); lastSimDriverResults = results; } } catch (Exception e){ 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 d547aa2285..d7f1bf34eb 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 @@ -20,17 +20,15 @@ public class SimulationResultsConverter { * convert a simulation driver SimulationResult to a constraint evaluation engine SimulationResult * * @param driverResults the recorded results of a simulation run from the simulation driver - * @param planDuration the duration of the plan * @return the same results rearranged to be suitable for use by the constraint evaluation engine */ - public static gov.nasa.jpl.aerie.constraints.model.SimulationResults convertToConstraintModelResults( - SimulationResults driverResults, Duration planDuration){ + public static gov.nasa.jpl.aerie.constraints.model.SimulationResults convertToConstraintModelResults(SimulationResults driverResults){ final var activities = driverResults.simulatedActivities.entrySet().stream() .map(e -> convertToConstraintModelActivityInstance(e.getKey().id(), e.getValue(), driverResults.startTime)) .collect(Collectors.toList()); return new gov.nasa.jpl.aerie.constraints.model.SimulationResults( driverResults.startTime, - Interval.betweenClosedOpen(Duration.ZERO, planDuration), + Interval.betweenClosedOpen(Duration.ZERO, driverResults.duration), activities, Maps.transformValues(driverResults.realProfiles, $ -> LinearProfile.fromSimulatedProfile($.getRight())), Maps.transformValues(driverResults.discreteProfiles, $ -> DiscreteProfile.fromSimulatedProfile($.getRight())) From 90f06c95dfaed8790092271e335b23e47e807ff9 Mon Sep 17 00:00:00 2001 From: maillard Date: Mon, 24 Jul 2023 15:44:18 -0700 Subject: [PATCH 04/10] Generalize MissingActivityTemplateConflict and remove conflict iteration in scheduler solving alg --- .../MissingActivityTemplateConflict.java | 34 +++++-- .../scheduler/goals/CardinalityGoal.java | 21 ++-- .../scheduler/goals/CoexistenceGoal.java | 2 +- .../aerie/scheduler/goals/RecurrenceGoal.java | 3 +- .../scheduler/solver/PrioritySolver.java | 98 ++++++++++++------- 5 files changed, 105 insertions(+), 53 deletions(-) diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/conflicts/MissingActivityTemplateConflict.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/conflicts/MissingActivityTemplateConflict.java index 09344d1a80..a4760f0eb3 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/conflicts/MissingActivityTemplateConflict.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/conflicts/MissingActivityTemplateConflict.java @@ -2,12 +2,14 @@ import gov.nasa.jpl.aerie.constraints.model.EvaluationEnvironment; import gov.nasa.jpl.aerie.constraints.time.Windows; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityCreationTemplate; import gov.nasa.jpl.aerie.scheduler.goals.ActivityTemplateGoal; +import java.util.Optional; + /** * describes plan problem due to lack of a matching instance for a template - * * such conflicts are typically addressed by scheduling additional activities * using the corresponding creation template */ @@ -16,16 +18,20 @@ public class MissingActivityTemplateConflict extends MissingActivityConflict { /** * ctor creates a new conflict regarding a missing activity * - * @param goal IN STORED the dissatisfied goal that issued the conflict - * @param temporalContext IN STORED the times in the plan when the goal was - * disatisfied enough to induce this conflict (including just the - * desired start times of the activity, not necessarily the end time) + * @param goal the dissatisfied goal that issued the conflict + * @param temporalContext the times in the plan when the goal was + * @param template desired activity template + * @param evaluationEnvironment the evaluation environment at the time of creation so variables can be retrieved later at instantiation + * @param cardinality the desired number of times the activity template should be inserted + * @param totalDuration the desired total duration */ public MissingActivityTemplateConflict( ActivityTemplateGoal goal, Windows temporalContext, ActivityCreationTemplate template, - EvaluationEnvironment evaluationEnvironment) + EvaluationEnvironment evaluationEnvironment, + int cardinality, + Optional totalDuration) { super(goal, evaluationEnvironment); @@ -35,6 +41,22 @@ public MissingActivityTemplateConflict( } this.temporalContext = temporalContext; this.template = template; + this.cardinality = cardinality; + this.totalDuration = totalDuration; + } + + //the number of times the activity needs to be inserted + int cardinality; + + //the desired total duration over the number of activities needed + Optional totalDuration; + + public int getCardinality(){ + return cardinality; + } + + public Optional getTotalDuration(){ + return totalDuration; } /** 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 1dd01c4cfc..5209da8a0d 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 @@ -23,6 +23,7 @@ import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Optional; import java.util.Set; /** @@ -194,17 +195,15 @@ else if (this.initiallyEvaluatedTemporalContext == null) { conflicts.add(new MissingAssociationConflict(this, List.of(act))); } } - //1) solve occurence part, we just need a certain number of activities - for (int i = 0; i < nbToSchedule; i++) { - conflicts.add(new MissingActivityTemplateConflict(this, subIntervalWindows, this.desiredActTemplate, new EvaluationEnvironment())); - } - /* - * 2) solve duration part: we can't assume stuff about duration, we post one conflict. The scheduler will solve this conflict by inserting one - * activity then the conflict will be reevaluated and if the scheduled duration so far is less than needed, another - * conflict will be posted and so on - * */ - if (nbToSchedule == 0 && durToSchedule.isPositive()) { - conflicts.add(new MissingActivityTemplateConflict(this, subIntervalWindows, this.desiredActTemplate, new EvaluationEnvironment())); + + if(nbToSchedule>0 || durToSchedule.isPositive()) { + conflicts.add(new MissingActivityTemplateConflict( + this, + subIntervalWindows, + this.desiredActTemplate, + new EvaluationEnvironment(), + nbToSchedule, + durToSchedule.isPositive() ? Optional.of(durToSchedule) : Optional.empty())); } } 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 4e4d4a51b0..ac7b83db11 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 @@ -272,7 +272,7 @@ else if (this.initiallyEvaluatedTemporalContext == null) { if (!alreadyOneActivityAssociated) { //create conflict if no matching target activity found if (existingActs.isEmpty()) { - conflicts.add(new MissingActivityTemplateConflict(this, this.temporalContext.evaluate(simulationResults), temp, createEvaluationEnvironmentFromAnchor(window))); + conflicts.add(new MissingActivityTemplateConflict(this, this.temporalContext.evaluate(simulationResults), temp, createEvaluationEnvironmentFromAnchor(window), 1, Optional.empty())); } else { conflicts.add(new MissingAssociationConflict(this, missingActAssociations)); } 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 125dfbfbb4..abb958e08c 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 @@ -16,6 +16,7 @@ import org.jetbrains.annotations.NotNull; import java.util.List; +import java.util.Optional; /** * describes the desired recurrence of an activity every time period @@ -211,7 +212,7 @@ private java.util.Collection makeRecurrenceConflicts(Du ) { final var windows = new Windows(false).set(Interval.betweenClosedOpen(intervalT.minus(recurrenceInterval.max), Duration.min(intervalT, end)), true); if(windows.iterateEqualTo(true).iterator().hasNext()){ - conflicts.add(new MissingActivityTemplateConflict(this, windows, this.getActTemplate(), new EvaluationEnvironment())); + conflicts.add(new MissingActivityTemplateConflict(this, windows, this.getActTemplate(), new EvaluationEnvironment(), 1, Optional.empty())); } else{ System.out.println(); 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 944e6a1a15..8cc7c4043b 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 @@ -5,6 +5,7 @@ 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.protocol.types.Duration; import gov.nasa.jpl.aerie.scheduler.conflicts.Conflict; import gov.nasa.jpl.aerie.scheduler.conflicts.MissingActivityConflict; import gov.nasa.jpl.aerie.scheduler.conflicts.MissingActivityInstanceConflict; @@ -516,59 +517,88 @@ private void satisfyGoalGeneral(Goal goal) { //setting the number of conflicts detected at first evaluation, will be used at backtracking evaluation.forGoal(goal).setNbConflictsDetected(missingConflicts.size()); assert missingConflicts != null; - boolean madeProgress = true; + final var itConflicts = missingConflicts.iterator(); - while (!missingConflicts.isEmpty() && madeProgress) { - madeProgress = false; + //create new activity instances for each missing conflict + while (itConflicts.hasNext()) { + final var missing = itConflicts.next(); + assert missing != null; - //create new activity instances for each missing conflict - for (final var missing : missingConflicts) { - assert missing != null; + //determine the best activities to satisfy the conflict + if (!analysisOnly && (missing instanceof MissingActivityInstanceConflict missingActivityInstanceConflict)) { + final var acts = getBestNewActivities(missingActivityInstanceConflict); + assert acts != null; + //add the activities to the output plan + if (!acts.isEmpty()) { + final var insertionResult = checkAndInsertActs(acts); + if(insertionResult.success){ - //determine the best activities to satisfy the conflict - if (!analysisOnly && (missing instanceof MissingActivityInstanceConflict || missing instanceof MissingActivityTemplateConflict)) { - final var acts = getBestNewActivities((MissingActivityConflict) missing); + evaluation.forGoal(goal).associate(insertionResult.activitiesInserted(), true); + itConflicts.remove(); + //REVIEW: really association should be via the goal's own query... + + //NB: repropagation of new activity effects occurs on demand + // at next constraint query, if relevant + } + } + } + else if(!analysisOnly && (missing instanceof MissingActivityTemplateConflict missingActivityTemplateConflict)){ + var cardinalityLeft = missingActivityTemplateConflict.getCardinality(); + var durationToAccomplish = missingActivityTemplateConflict.getTotalDuration(); + var durationLeft = Duration.ZERO; + if(durationToAccomplish.isPresent()) { + durationLeft = durationToAccomplish.get(); + } + while(cardinalityLeft > 0 || durationLeft.longerThan(Duration.ZERO)){ + final var acts = getBestNewActivities(missingActivityTemplateConflict); assert acts != null; //add the activities to the output plan if (!acts.isEmpty()) { final var insertionResult = checkAndInsertActs(acts); - if(insertionResult.success){ - madeProgress = true; + if(insertionResult.success()){ evaluation.forGoal(goal).associate(insertionResult.activitiesInserted(), true); //REVIEW: really association should be via the goal's own query... //NB: repropagation of new activity effects occurs on demand // at next constraint query, if relevant + cardinalityLeft--; + durationLeft = durationLeft.minus(insertionResult + .activitiesInserted() + .stream() + .map(SchedulingActivityDirective::duration) + .reduce(Duration.ZERO, Duration::plus)); } + } else{ + break; } - } else if(missing instanceof MissingAssociationConflict missingAssociationConflict){ - var actToChooseFrom = missingAssociationConflict.getActivityInstancesToChooseFrom(); - //no act type constraint to consider as the activities have been scheduled - //no global constraint for the same reason above mentioned - //only the target goal state constraints to consider - for(var act : actToChooseFrom){ - var actWindow = new Windows(false).set(Interval.between(act.startOffset(), act.getEndTime()), true); - var stateConstraints = goal.getResourceConstraints(); - var narrowed = actWindow; - if(stateConstraints!= null) { - narrowed = narrowByResourceConstraints(actWindow, List.of(stateConstraints)); - } - if(narrowed.includes(actWindow)){ - //decision-making here, we choose the first satisfying activity - evaluation.forGoal(goal).associate(act, false); - madeProgress = true; - break; - } + } + if(cardinalityLeft <= 0 && durationLeft.noLongerThan(Duration.ZERO)){ + itConflicts.remove(); + } + } else if(missing instanceof MissingAssociationConflict missingAssociationConflict){ + var actToChooseFrom = missingAssociationConflict.getActivityInstancesToChooseFrom(); + //no act type constraint to consider as the activities have been scheduled + //no global constraint for the same reason above mentioned + //only the target goal state constraints to consider + for(var act : actToChooseFrom){ + var actWindow = new Windows(false).set(Interval.between(act.startOffset(), act.getEndTime()), true); + var stateConstraints = goal.getResourceConstraints(); + var narrowed = actWindow; + if(stateConstraints!= null) { + narrowed = narrowByResourceConstraints(actWindow, List.of(stateConstraints)); + } + if(narrowed.includes(actWindow)){ + //decision-making here, we choose the first satisfying activity + evaluation.forGoal(goal).associate(act, false); + itConflicts.remove(); + break; } } - }//for(missing) - - if (madeProgress) { - missingConflicts = getConflicts(goal); } - }//while(missingConflicts&&madeProgress) + }//for(missing) + if(!missingConflicts.isEmpty() && goal.shouldRollbackIfUnsatisfied()){ rollback(goal); From 09c943ab98a556f303569643cb3684e2503e5c7c Mon Sep 17 00:00:00 2001 From: maillard Date: Thu, 27 Jul 2023 17:48:57 -0700 Subject: [PATCH 05/10] Allow for use of external initial simulation results in solver --- .../activities/ActivityCreationTemplate.java | 25 ++++- .../jpl/aerie/scheduler/model/Problem.java | 35 +++++-- .../scheduler/simulation/SimulationData.java | 11 +++ .../simulation/SimulationFacade.java | 96 +++++++++++++++---- .../scheduler/solver/PrioritySolver.java | 69 ++++++------- .../aerie/scheduler/SimulationFacadeTest.java | 37 ++++--- 6 files changed, 184 insertions(+), 89 deletions(-) create mode 100644 scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationData.java diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityCreationTemplate.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityCreationTemplate.java index d70203d13c..0c1f61adbd 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityCreationTemplate.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityCreationTemplate.java @@ -2,6 +2,7 @@ import gov.nasa.jpl.aerie.constraints.model.EvaluationEnvironment; import gov.nasa.jpl.aerie.constraints.model.Profile; +import gov.nasa.jpl.aerie.constraints.model.SimulationResults; import gov.nasa.jpl.aerie.constraints.time.Interval; import gov.nasa.jpl.aerie.constraints.time.Spans; import gov.nasa.jpl.aerie.constraints.time.Windows; @@ -271,6 +272,7 @@ public boolean isApproximation(){ @Override public Duration valueAt(Duration start, HistoryWithActivity history) { + final var latestConstraintsSimulationResults = getLatestSimulationResults(facade, start); final var actToSim = SchedulingActivityDirective.of( type, start, @@ -278,7 +280,7 @@ public Duration valueAt(Duration start, HistoryWithActivity history) { SchedulingActivityDirective.instantiateArguments( arguments, start, - facade.getLatestConstraintSimulationResults(), + latestConstraintsSimulationResults, evaluationEnvironment, type), null, @@ -313,7 +315,7 @@ public Duration valueAt(Duration start, HistoryWithActivity history) { final var instantiatedArguments = SchedulingActivityDirective.instantiateArguments( this.arguments, earliestStart, - facade.getLatestConstraintSimulationResults(), + getLatestSimulationResults(facade, earliestStart), evaluationEnvironment, type); @@ -343,7 +345,7 @@ public Duration valueAt(Duration start, HistoryWithActivity history) { SchedulingActivityDirective.instantiateArguments( this.arguments, earliestStart, - facade.getLatestConstraintSimulationResults(), + getLatestSimulationResults(facade, earliestStart), evaluationEnvironment, type), null, @@ -365,7 +367,7 @@ public Duration valueAt(Duration start, HistoryWithActivity history) { SchedulingActivityDirective.instantiateArguments( this.arguments, earliestStart, - facade.getLatestConstraintSimulationResults(), + getLatestSimulationResults(facade, earliestStart), evaluationEnvironment, type), null, @@ -384,7 +386,7 @@ public Duration valueAt(final Duration start, final HistoryWithActivity history) final var instantiatedArgs = SchedulingActivityDirective.instantiateArguments( arguments, start, - facade.getLatestConstraintSimulationResults(), + getLatestSimulationResults(facade, start), evaluationEnvironment, type ); @@ -482,5 +484,18 @@ private Optional rootFindingHelper( return Optional.empty(); } + private SimulationResults getLatestSimulationResults(final SimulationFacade facade, final Duration until){ + final var latestConstraintsSimulationResults = facade.getLatestConstraintSimulationResults(); + if(latestConstraintsSimulationResults.isEmpty()){ + try { + facade.computeSimulationResultsUntil(until); + return facade.getLatestConstraintSimulationResults().get(); + } catch(SimulationFacade.SimulationException e){ + throw new RuntimeException("Simulation is in irreparable state"); + } + } else{ + return latestConstraintsSimulationResults.get(); + } + } } 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 0f8b062067..624160e6ec 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 @@ -1,16 +1,19 @@ package gov.nasa.jpl.aerie.scheduler.model; 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.NotNull; import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.GlobalConstraint; 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.SimulationResultsConverter; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Optional; /** * description of a planning problem to be solved @@ -42,6 +45,11 @@ public class Problem { */ private Plan initialPlan; + /** + * initial simulation results loaded from the DB + */ + private Optional initialSimulationResults; + /** * container of all goals in the problem, indexed by name */ @@ -73,10 +81,7 @@ public Problem(MissionModel mission, PlanningHorizon planningHorizon, Simulat if(this.simulationFacade != null) { this.simulationFacade.setActivityTypes(this.getActivityTypes()); } - } - - public Problem(PlanningHorizon planningHorizon){ - this(null, planningHorizon, null, null); + this.initialSimulationResults = Optional.empty(); } public SimulationFacade getSimulationFacade(){ @@ -120,13 +125,29 @@ public Plan getInitialPlan() { /** * sets the initial seed plan that schedulers may start from - * + * @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(Plan plan) { + public void setInitialPlan(final Plan plan, final Optional initialSimulationResults) { initialPlan = plan; + this.initialSimulationResults = initialSimulationResults.map(simulationResults -> new SimulationData( + simulationResults, + SimulationResultsConverter.convertToConstraintModelResults( + simulationResults), + plan.getActivities())); + } + + /** + * sets the initial seed plan that schedulers may start from + * + * @param plan the initial seed plan that schedulers may start from + */ + public void setInitialPlan(final Plan plan) { + setInitialPlan(plan, Optional.empty()); } + public Optional getInitialSimulationResults(){ return initialSimulationResults; } + public void setGoals(List goals){ goalsOrderedByPriority.clear(); goalsOrderedByPriority.addAll(goals); 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 new file mode 100644 index 0000000000..72b9986a7c --- /dev/null +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationData.java @@ -0,0 +1,11 @@ +package gov.nasa.jpl.aerie.scheduler.simulation; + +import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; +import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; + +import java.util.Collection; + +public record SimulationData( + SimulationResults driverResults, + gov.nasa.jpl.aerie.constraints.model.SimulationResults constraintsResults, + Collection activitiesInPlan){} 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 c786fdd030..e26658423f 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 @@ -9,6 +9,7 @@ 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.SerializedValue; +import gov.nasa.jpl.aerie.scheduler.model.Problem; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; import gov.nasa.jpl.aerie.scheduler.model.ActivityType; import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; @@ -24,8 +25,6 @@ import java.util.Map; import java.util.Optional; -import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECONDS; - /** * A facade for simulating plans and processing simulation results. */ @@ -41,21 +40,57 @@ public class SimulationFacade implements AutoCloseable{ private ResumableSimulationDriver driver; private int itSimActivityId; - //simulation results from the last simulation, as output directly by simulation driver - private SimulationResults lastSimDriverResults; - private gov.nasa.jpl.aerie.constraints.model.SimulationResults lastSimConstraintResults; private final Map planActDirectiveIdToSimulationActivityDirectiveId = new HashMap<>(); private final Map insertedActivities; //counts the total number of simulation restarts, used as performance metric in the scheduler private int pastSimulationRestarts; - public gov.nasa.jpl.aerie.constraints.model.SimulationResults getLatestConstraintSimulationResults(){ - return lastSimConstraintResults; + 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; + + /** + * 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); + } + + /** + * Signals to the facade that the initial simulation results are stale and should not be used anymore + */ + public void initialSimulationResultsAreStale(){ + this.initialPlanHasBeenModified = true; + } + + 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 SimulationResults getLatestDriverSimulationResults(){ - return lastSimDriverResults; + if(!initialPlanHasBeenModified && initialSimulationResults.isPresent()) return this.initialSimulationResults.get().driverResults(); + return lastSimulationData.driverResults(); } public SimulationFacade(final PlanningHorizon planningHorizon, final MissionModel missionModel) { @@ -66,6 +101,8 @@ public SimulationFacade(final PlanningHorizon planningHorizon, final MissionMode this.insertedActivities = new HashMap<>(); this.activityTypes = new HashMap<>(); this.pastSimulationRestarts = 0; + this.initialPlan = new ArrayList<>(); + this.initialSimulationResults = Optional.empty(); } @Override @@ -73,6 +110,16 @@ public void close(){ driver.close(); } + /** + * 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 setActivityTypes(final Collection activityTypes){ this.activityTypes = new HashMap<>(); activityTypes.forEach(at -> this.activityTypes.put(at.getName(), at)); @@ -113,9 +160,9 @@ public Map getAllChi if(insertedActivities.size() == 0) return Map.of(); computeSimulationResultsUntil(endTime); final Map childActivities = new HashMap<>(); - this.lastSimDriverResults.simulatedActivities.forEach( (activityInstanceId, activity) -> { + this.lastSimulationData.driverResults().simulatedActivities.forEach( (activityInstanceId, activity) -> { if (activity.parentId() == null) return; - final var rootParent = getIdOfRootParent(this.lastSimDriverResults, activityInstanceId); + final var rootParent = getIdOfRootParent(this.lastSimulationData.driverResults(), activityInstanceId); final var schedulingActId = planActDirectiveIdToSimulationActivityDirectiveId.entrySet().stream().filter( entry -> entry.getValue().equals(rootParent) ).findFirst().get().getKey(); @@ -143,11 +190,15 @@ public void removeAndInsertActivitiesFromSimulation( insertedActivities.remove(act); } } + 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()); } - final var allActivitiesToSimulate = new ArrayList<>(activitiesToAdd); + if(allActivitiesToSimulate.isEmpty() && !atLeastOneActualRemoval) return; //reset resumable simulation if(atLeastOneActualRemoval || earliestActStartTime.noLongerThan(this.driver.getCurrentSimulationEndTime())){ allActivitiesToSimulate.addAll(insertedActivities.keySet()); @@ -176,6 +227,12 @@ public int countSimulationRestarts(){ return this.driver.getCountSimulationRestarts() + this.pastSimulationRestarts; } + public void insertActivitiesIntoSimulation(final Collection activities) + throws SimulationException + { + removeAndInsertActivitiesFromSimulation(List.of(), activities); + } + /** * Replaces an activity instance with another, strictly when they have the same id * @param toBeReplaced the activity to be replaced @@ -198,7 +255,7 @@ public void replaceActivityFromSimulation(final SchedulingActivityDirective toBe this.planActDirectiveIdToSimulationActivityDirectiveId.put(replacement.id(), simulationId); } - public void simulateActivities(final Collection activities) throws SimulationException { + private void simulateActivities(final Collection activities) throws SimulationException { final var activitiesSortedByStartTime = activities.stream().filter(activity -> !(insertedActivities.containsKey(activity))) .sorted(Comparator.comparing(SchedulingActivityDirective::startOffset)).toList(); @@ -230,19 +287,18 @@ public static class SimulationException extends Exception { } } - public void simulateActivity(final SchedulingActivityDirective activity) throws SimulationException { - if(insertedActivities.containsKey(activity)) return; - simulateActivities(List.of(activity)); - } - public void computeSimulationResultsUntil(final Duration endTime) throws SimulationException { + 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(results != lastSimDriverResults) { + if(lastSimulationData == null || results != lastSimulationData.driverResults()) { //simulation results from the last simulation, as converted for use by the constraint evaluation engine - lastSimConstraintResults = SimulationResultsConverter.convertToConstraintModelResults(results); - lastSimDriverResults = results; + this.lastSimulationData = new SimulationData(results, SimulationResultsConverter.convertToConstraintModelResults(results), this.insertedActivities.keySet()); } } catch (Exception e){ throw new SimulationException("An exception happened during simulation", e); 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 8cc7c4043b..4daad48a52 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 @@ -1,6 +1,7 @@ package gov.nasa.jpl.aerie.scheduler.solver; import gov.nasa.jpl.aerie.constraints.model.EvaluationEnvironment; +import gov.nasa.jpl.aerie.constraints.model.SimulationResults; import gov.nasa.jpl.aerie.constraints.time.Interval; import gov.nasa.jpl.aerie.constraints.time.Segment; import gov.nasa.jpl.aerie.constraints.time.Windows; @@ -114,6 +115,9 @@ public Optional getNextSolution() { //on first call to solver; setup fresh solution workspace for problem try { initializePlan(); + if(problem.getInitialSimulationResults().isPresent()) { + simulationFacade.loadInitialSimResults(problem.getInitialSimulationResults().get()); + } } catch (SimulationFacade.SimulationException e) { logger.error("Tried to initializePlan but at least one activity could not be instantiated", e); return Optional.empty(); @@ -155,7 +159,7 @@ private InsertActivityResult checkAndInsertActs(Collection(); if(allGood) { + if(!acts.isEmpty()) simulationFacade.initialSimulationResultsAreStale(); //update plan with regard to simulation for(var act: acts) { plan.add(act); @@ -222,14 +227,7 @@ public void initializePlan() throws SimulationFacade.SimulationException { evaluation = new Evaluation(); plan.addEvaluation(evaluation); - - //if backed by real models, initialize the simulation states/resources/profiles for the plan so state queries work - if (problem.getMissionModel() != null) { - simulationFacade.simulateActivities(plan.getActivities()); - final var allGeneratedActivities = simulationFacade.getAllChildActivities(problem.getPlanningHorizon().getEndAerie()); - processNewGeneratedActivities(allGeneratedActivities); - pullActivityDurationsIfNecessary(); - } + if(simulationFacade != null) simulationFacade.addInitialPlan(this.plan.getActivitiesByTime()); } /** @@ -528,7 +526,6 @@ private void satisfyGoalGeneral(Goal goal) { //determine the best activities to satisfy the conflict if (!analysisOnly && (missing instanceof MissingActivityInstanceConflict missingActivityInstanceConflict)) { final var acts = getBestNewActivities(missingActivityInstanceConflict); - assert acts != null; //add the activities to the output plan if (!acts.isEmpty()) { final var insertionResult = checkAndInsertActs(acts); @@ -537,9 +534,6 @@ private void satisfyGoalGeneral(Goal goal) { evaluation.forGoal(goal).associate(insertionResult.activitiesInserted(), true); itConflicts.remove(); //REVIEW: really association should be via the goal's own query... - - //NB: repropagation of new activity effects occurs on demand - // at next constraint query, if relevant } } } @@ -560,9 +554,6 @@ else if(!analysisOnly && (missing instanceof MissingActivityTemplateConflict mi evaluation.forGoal(goal).associate(insertionResult.activitiesInserted(), true); //REVIEW: really association should be via the goal's own query... - - //NB: repropagation of new activity effects occurs on demand - // at next constraint query, if relevant cardinalityLeft--; durationLeft = durationLeft.minus(insertionResult .activitiesInserted() @@ -621,16 +612,8 @@ else if(!analysisOnly && (missing instanceof MissingActivityTemplateConflict mi assert goal != null; assert plan != null; //REVIEW: maybe should have way to request only certain kinds of conflicts - var lastSimResults = this.simulationFacade.getLatestConstraintSimulationResults(); - if (lastSimResults == null || this.checkSimBeforeEvaluatingGoal) { - try { - this.simulationFacade.computeSimulationResultsUntil(this.problem.getPlanningHorizon().getEndAerie()); - } catch (SimulationFacade.SimulationException e) { - throw new RuntimeException("Exception while running simulation before evaluating conflicts", e); - } - lastSimResults = this.simulationFacade.getLatestConstraintSimulationResults(); - } - final var rawConflicts = goal.getConflicts(plan, lastSimResults); + final var lastSimulationResults = this.getLatestSimResultsUpTo(this.problem.getPlanningHorizon().getEndAerie()); + final var rawConflicts = goal.getConflicts(plan, lastSimulationResults); assert rawConflicts != null; return rawConflicts; } @@ -779,16 +762,12 @@ private Windows narrowByResourceConstraints(Windows windows, final var totalDomain = Interval.between(windows.minTrueTimePoint().get().getKey(), windows.maxTrueTimePoint().get().getKey()); //make sure the simulation results cover the domain - try { - simulationFacade.computeSimulationResultsUntil(totalDomain.end); - } catch (SimulationFacade.SimulationException e) { - throw new RuntimeException("Exception while running simulation before evaluating resource constraints", e); - } + final var latestSimulationResults = this.getLatestSimResultsUpTo(totalDomain.end); //iteratively narrow the windows from each constraint //REVIEW: could be some optimization in constraint ordering (smallest domain first to fail fast) for (final var constraint : constraints) { //REVIEW: loop through windows more efficient than enveloppe(windows) ? - final var validity = constraint.evaluate(simulationFacade.getLatestConstraintSimulationResults(), totalDomain); + final var validity = constraint.evaluate(latestSimulationResults, totalDomain); ret = ret.and(validity); //short-circuit if no possible windows left if (ret.stream().noneMatch(Segment::value)) { @@ -798,6 +777,24 @@ private Windows narrowByResourceConstraints(Windows windows, return ret; } + + private SimulationResults getLatestSimResultsUpTo(Duration time){ + SimulationResults lastSimulationResults = null; + var lastSimResultsFromFacade = this.simulationFacade.getLatestConstraintSimulationResults(); + if (lastSimResultsFromFacade.isEmpty() || lastSimResultsFromFacade.get().bounds.end.shorterThan(time)) { + try { + this.simulationFacade.computeSimulationResultsUntil(time); + final var allGeneratedActivities = simulationFacade.getAllChildActivities(time); + processNewGeneratedActivities(allGeneratedActivities); + pullActivityDurationsIfNecessary(); + } catch (SimulationFacade.SimulationException e) { + throw new RuntimeException("Exception while running simulation before evaluating conflicts", e); + } + } + lastSimulationResults = this.simulationFacade.getLatestConstraintSimulationResults().get(); + return lastSimulationResults; + } + private Windows narrowGlobalConstraints( Plan plan, MissingActivityConflict mac, @@ -809,18 +806,14 @@ private Windows narrowGlobalConstraints( return tmp; } //make sure the simulation results cover the domain - try { - simulationFacade.computeSimulationResultsUntil(tmp.maxTrueTimePoint().get().getKey()); - } catch(SimulationFacade.SimulationException e){ - throw new RuntimeException("Exception while running simulation before evaluating global constraints", e); - } + final var latestSimulationResults = this.getLatestSimResultsUpTo(tmp.maxTrueTimePoint().get().getKey()); for (GlobalConstraint gc : constraints) { if (gc instanceof GlobalConstraintWithIntrospection c) { tmp = c.findWindows( plan, tmp, mac, - simulationFacade.getLatestConstraintSimulationResults(), + latestSimulationResults, evaluationEnvironment); } else { throw new Error("Unhandled variant of GlobalConstraint: %s".formatted(gc)); 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 2eb765414a..39d44578c0 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 @@ -40,8 +40,8 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Function; -import java.util.function.Supplier; import static com.google.common.truth.Truth.assertThat; import static gov.nasa.jpl.aerie.constraints.time.Interval.Inclusivity.Exclusive; @@ -179,28 +179,28 @@ public void associationToExistingSatisfyingActivity(){ @Test public void getValueAtTimeDoubleOnSimplePlanMidpoint() throws SimulationFacade.SimulationException { - facade.simulateActivities(makeTestPlanP0B1().getActivities()); + facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); facade.computeSimulationResultsUntil(tEnd); final var stateQuery = new StateQueryParam(getFruitRes().name, new TimeExpressionConstant(t1_5)); - final var actual = stateQuery.getValue(facade.getLatestConstraintSimulationResults(), null, horizon.getHor()); + final var actual = stateQuery.getValue(facade.getLatestConstraintSimulationResults().get(), null, horizon.getHor()); assertThat(actual).isEqualTo(SerializedValue.of(3.0)); } @Test public void getValueAtTimeDoubleOnSimplePlan() throws SimulationFacade.SimulationException { - facade.simulateActivities(makeTestPlanP0B1().getActivities()); + facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); facade.computeSimulationResultsUntil(tEnd); final var stateQuery = new StateQueryParam(getFruitRes().name, new TimeExpressionConstant(t2)); - final var actual = stateQuery.getValue(facade.getLatestConstraintSimulationResults(), null, horizon.getHor()); + final var actual = stateQuery.getValue(facade.getLatestConstraintSimulationResults().get(), null, horizon.getHor()); assertThat(actual).isEqualTo(SerializedValue.of(2.9)); assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @Test public void whenValueAboveDoubleOnSimplePlan() throws SimulationFacade.SimulationException { - facade.simulateActivities(makeTestPlanP0B1().getActivities()); + facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); facade.computeSimulationResultsUntil(tEnd); - var actual = new GreaterThan(getFruitRes(), new RealValue(2.9)).evaluate(facade.getLatestConstraintSimulationResults()); + var actual = new GreaterThan(getFruitRes(), new RealValue(2.9)).evaluate(facade.getLatestConstraintSimulationResults().get()); var expected = new Windows( Segment.of(interval(0, Inclusive, 2, Exclusive, SECONDS), true), Segment.of(interval(2, Inclusive,5, Exclusive, SECONDS), false) @@ -210,9 +210,9 @@ public void whenValueAboveDoubleOnSimplePlan() throws SimulationFacade.Simulatio @Test public void whenValueBelowDoubleOnSimplePlan() throws SimulationFacade.SimulationException { - facade.simulateActivities(makeTestPlanP0B1().getActivities()); + facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); facade.computeSimulationResultsUntil(tEnd); - var actual = new LessThan(getFruitRes(), new RealValue(3.0)).evaluate(facade.getLatestConstraintSimulationResults()); + var actual = new LessThan(getFruitRes(), new RealValue(3.0)).evaluate(facade.getLatestConstraintSimulationResults().get()); var expected = new Windows( Segment.of(interval(0, Inclusive, 2, Exclusive, SECONDS), false), Segment.of(interval(2, Inclusive, 5, Exclusive, SECONDS), true) @@ -222,9 +222,9 @@ public void whenValueBelowDoubleOnSimplePlan() throws SimulationFacade.Simulatio @Test public void whenValueBetweenDoubleOnSimplePlan() throws SimulationFacade.SimulationException { - facade.simulateActivities(makeTestPlanP0B1().getActivities()); + 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()); + var actual = new And(new GreaterThanOrEqual(getFruitRes(), new RealValue(3.0)), new LessThanOrEqual(getFruitRes(), new RealValue(3.99))).evaluate(facade.getLatestConstraintSimulationResults().get()); var expected = new Windows( Segment.of(interval(0, Inclusive, 1, Exclusive, SECONDS), false), Segment.of(interval(1, Inclusive, 2, Exclusive, SECONDS), true), @@ -235,9 +235,9 @@ public void whenValueBetweenDoubleOnSimplePlan() throws SimulationFacade.Simulat @Test public void whenValueEqualDoubleOnSimplePlan() throws SimulationFacade.SimulationException { - facade.simulateActivities(makeTestPlanP0B1().getActivities()); + facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); facade.computeSimulationResultsUntil(tEnd); - var actual = new Equal<>(getFruitRes(), new RealValue(3.0)).evaluate(facade.getLatestConstraintSimulationResults()); + var actual = new Equal<>(getFruitRes(), new RealValue(3.0)).evaluate(facade.getLatestConstraintSimulationResults().get()); var expected = new Windows( Segment.of(interval(0, Inclusive, 1, Exclusive, SECONDS), false), Segment.of(interval(1, Inclusive, 2, Exclusive, SECONDS), true), @@ -248,9 +248,9 @@ public void whenValueEqualDoubleOnSimplePlan() throws SimulationFacade.Simulatio @Test public void whenValueNotEqualDoubleOnSimplePlan() throws SimulationFacade.SimulationException { - facade.simulateActivities(makeTestPlanP0B1().getActivities()); + facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); facade.computeSimulationResultsUntil(tEnd); - var actual = new NotEqual<>(getFruitRes(), new RealValue(3.0)).evaluate(facade.getLatestConstraintSimulationResults()); + var actual = new NotEqual<>(getFruitRes(), new RealValue(3.0)).evaluate(facade.getLatestConstraintSimulationResults().get()); var expected = new Windows( Segment.of(interval(0, Inclusive, 1, Exclusive, SECONDS), true), Segment.of(interval(1, Inclusive, 2, Exclusive, SECONDS), false), @@ -261,7 +261,7 @@ public void whenValueNotEqualDoubleOnSimplePlan() throws SimulationFacade.Simula @Test public void testCoexistenceGoalWithResourceConstraint() throws SimulationFacade.SimulationException { - facade.simulateActivities(makeTestPlanP0B1().getActivities()); + facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); /** * reminder for PB1 @@ -299,10 +299,9 @@ public void testCoexistenceGoalWithResourceConstraint() throws SimulationFacade. assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } - @Test public void testProceduralGoalWithResourceConstraint() throws SimulationFacade.SimulationException { - facade.simulateActivities(makeTestPlanP0B1().getActivities()); + facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); final var constraint = new And( new LessThanOrEqual(new RealResource("/peel"), new RealValue(3.0)), @@ -344,7 +343,7 @@ public void testProceduralGoalWithResourceConstraint() throws SimulationFacade.S @Test public void testActivityTypeWithResourceConstraint() throws SimulationFacade.SimulationException { - facade.simulateActivities(makeTestPlanP0B1().getActivities()); + facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); final var constraint = new And( new LessThanOrEqual(new RealResource("/peel"), new RealValue(3.0)), From 54d03211681541d2b4b7ed94159895f0ba254fef Mon Sep 17 00:00:00 2001 From: maillard Date: Thu, 27 Jul 2023 17:50:39 -0700 Subject: [PATCH 06/10] Pull initial simulation results from DB for use in scheduling --- .../server/graphql/GraphQLParsers.java | 92 +++--- .../server/graphql/ProfileParsers.java | 79 +++++ .../server/services/GraphQLMerlinService.java | 304 ++++++++++++++++-- .../server/services/PlanService.java | 9 + .../server/graphql/GraphQLParsersTest.java | 12 +- .../services/SynchronousSchedulerAgent.java | 16 +- .../worker/services/MockMerlinService.java | 31 +- 7 files changed, 455 insertions(+), 88 deletions(-) create mode 100644 scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/graphql/ProfileParsers.java diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/graphql/GraphQLParsers.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/graphql/GraphQLParsers.java index 77588a6cff..03e79be087 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/graphql/GraphQLParsers.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/graphql/GraphQLParsers.java @@ -2,32 +2,32 @@ import gov.nasa.jpl.aerie.json.JsonParser; import gov.nasa.jpl.aerie.json.Unit; +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 gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.jpl.aerie.scheduler.server.models.ActivityAttributesRecord; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.scheduler.server.models.Timestamp; import org.apache.commons.lang3.tuple.Pair; +import org.postgresql.util.PGInterval; +import java.sql.SQLException; +import java.time.Instant; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; import java.time.temporal.ChronoUnit; import java.util.Map; -import java.util.Optional; -import java.util.regex.Pattern; import static gov.nasa.jpl.aerie.json.BasicParsers.doubleP; import static gov.nasa.jpl.aerie.json.BasicParsers.literalP; import static gov.nasa.jpl.aerie.json.BasicParsers.longP; import static gov.nasa.jpl.aerie.json.BasicParsers.mapP; import static gov.nasa.jpl.aerie.json.BasicParsers.productP; +import static gov.nasa.jpl.aerie.json.BasicParsers.stringP; import static gov.nasa.jpl.aerie.json.Uncurry.tuple; import static gov.nasa.jpl.aerie.json.Uncurry.untuple; import static gov.nasa.jpl.aerie.merlin.driver.json.SerializedValueJsonParser.serializedValueP; import static gov.nasa.jpl.aerie.merlin.driver.json.ValueSchemaJsonParser.valueSchemaP; - import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECONDS; /** @@ -43,17 +43,6 @@ public class GraphQLParsers { //TODO: inconsistent with DOY format in Timestamp.fromString, MerlinParsers.timestampP, etc used in merlin-server public static final DateTimeFormatter timestampFormat = DateTimeFormatter.ISO_OFFSET_DATE_TIME; - /** - * the formatting expected in interval scalars returned by graphql queries - */ - //TODO: inconsistent with bare microseconds used elsewhere - public static final Pattern intervalPattern = Pattern.compile( - "^(?[+-])?" //optional sign prefix, as in +322:21:15 - + "(((?
\\d+):)?" //optional hours field, as in 322:21:15 - + "(?\\d+):)?" //optional minutes field, as in 22:15 - + "(?\\d+" //required seconds field, as in 15 - + "(\\.\\d*)?)$"); //optional decimal sub-seconds, as in 15. or 15.111 - /** * parse the given graphQL formatted timestamptz scalar string (eg 2021-01-01T00:00:00+00:00) * @@ -65,34 +54,6 @@ public static Timestamp parseGraphQLTimestamp(final String in) { return new Timestamp(ZonedDateTime.parse(in, timestampFormat).toInstant()); } - /** - * parse the given graphQL formatted interval scalar string (eg 322:21:15.250) - * - * supports up to microsecond precision - * - * @param in the input graphql formatted interval scalar string to parse - * @return the interval object represented by the input string - */ - public static Duration parseGraphQLInterval(final String in) { - - final var matcher = intervalPattern.matcher(in); - if (!matcher.matches()) { - throw new DateTimeParseException("unable to parse HH:MM:SS.sss duration from \"" + in + "\"", in, 0); - } - final var signValues = Map.of("+", 1, "-", -1); - final var sign = Optional.ofNullable(matcher.group("sign")).map(signValues::get).orElse(1); - final var hr = Optional.ofNullable(matcher.group("hr")).map(Integer::parseInt) - .map(java.time.Duration::ofHours).orElse(java.time.Duration.ZERO); - final var min = Optional.ofNullable(matcher.group("min")).map(Integer::parseInt) - .map(java.time.Duration::ofMinutes).orElse(java.time.Duration.ZERO); - final var sec = Optional.ofNullable(matcher.group("sec")).map(Double::parseDouble) - .map(s -> (long) (s * 1000 * 1000))//seconds->millis->micros - .map(us -> java.time.Duration.of(us, ChronoUnit.MICROS)) - .orElse(java.time.Duration.ZERO); - final var total = hr.plus(min).plus(sec).multipliedBy(sign); - return Duration.of((total.getNano() / 1000) + (total.getSeconds() * 1000_000), MICROSECONDS); - } - public static final JsonParser> simulationArgumentsP = mapP(serializedValueP); public static final JsonParser realDynamicsP @@ -130,4 +91,47 @@ public static Duration parseGraphQLInterval(final String in) { untuple(ActivityAttributesRecord::new), $ -> tuple($.directiveId(), $.arguments(), $.computedAttributes())); + public static final JsonParser durationP = + stringP + .map( + GraphQLParsers::durationFromPGInterval, + duration -> graphQLIntervalFromDuration(duration).getValue()); + + public static Duration durationFromPGInterval(final String pgInterval) { + try { + final PGInterval asInterval = new PGInterval(pgInterval); + if(asInterval.getYears() != 0 || + asInterval.getMonths() != 0) throw new RuntimeException("Years or months found in a pginterval"); + final var asDuration = java.time.Duration.ofDays(asInterval.getDays()) + .plusHours(asInterval.getHours()) + .plusMinutes(asInterval.getMinutes()) + .plusSeconds(asInterval.getWholeSeconds()) + .plusNanos(asInterval.getMicroSeconds()*1000); + return Duration.of(asDuration.toNanos()/1000, MICROSECONDS); + }catch(SQLException e){ + throw new RuntimeException(e); + } + } + + public static PGInterval graphQLIntervalFromDuration(final Duration duration) { + try { + final var micros = duration.in(MICROSECONDS); + return new PGInterval("PT%d.%06dS".formatted(micros / 1_000_000, micros % 1_000_000)); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + public static PGInterval graphQLIntervalFromDuration(final Instant instant1, final Instant instant2) { + try { + final var micros = java.time.Duration.between(instant1, instant2).toNanos() / 1000; + return new PGInterval("PT%d.%06dS".formatted(micros / 1_000_000, micros % 1_000_000)); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public static Instant instantFromStart(Instant start, Duration duration){ + return start.plus(java.time.Duration.of(duration.in(Duration.MICROSECONDS), ChronoUnit.MICROS)); + } + } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/graphql/ProfileParsers.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/graphql/ProfileParsers.java new file mode 100644 index 0000000000..e05d41da14 --- /dev/null +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/graphql/ProfileParsers.java @@ -0,0 +1,79 @@ +package gov.nasa.jpl.aerie.scheduler.server.graphql; + +import gov.nasa.jpl.aerie.json.JsonParser; +import gov.nasa.jpl.aerie.json.Unit; +import gov.nasa.jpl.aerie.merlin.driver.engine.ProfileSegment; +import gov.nasa.jpl.aerie.merlin.protocol.types.RealDynamics; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.List; + +import static gov.nasa.jpl.aerie.json.BasicParsers.doubleP; +import static gov.nasa.jpl.aerie.json.BasicParsers.listP; +import static gov.nasa.jpl.aerie.json.BasicParsers.literalP; +import static gov.nasa.jpl.aerie.json.BasicParsers.productP; +import static gov.nasa.jpl.aerie.json.BasicParsers.stringP; +import static gov.nasa.jpl.aerie.json.Uncurry.tuple; +import static gov.nasa.jpl.aerie.json.Uncurry.untuple; +import static gov.nasa.jpl.aerie.merlin.driver.json.SerializedValueJsonParser.serializedValueP; +import static gov.nasa.jpl.aerie.merlin.driver.json.ValueSchemaJsonParser.valueSchemaP; +import static gov.nasa.jpl.aerie.scheduler.server.graphql.GraphQLParsers.durationP; + +public final class ProfileParsers { + public static final JsonParser realDynamicsP + = productP + . field("initial", doubleP) + . field("rate", doubleP) + . map( + untuple(RealDynamics::linear), + $ -> tuple($.initial, $.rate)); + + public static final JsonParser> realProfileSegmentP + = productP + . field("start_offset", durationP) + . field("dynamics", realDynamicsP) + . map( + untuple((start_offset, dynamics) -> new ProfileSegment(start_offset, dynamics)), + $ -> tuple($.extent(), $.dynamics())); + + public static final JsonParser> discreteProfileSegmentP + = productP + . field("start_offset", durationP) + . field("dynamics", serializedValueP) + . map( + untuple( ProfileSegment::new), + $ -> tuple($.extent(), $.dynamics())); + + public static final JsonParser discreteValueSchemaTypeP = productP + .field("type", literalP("discrete")) + .field("schema", valueSchemaP) + .map(untuple((type, schema) -> schema), + $ -> tuple(Unit.UNIT, $)); + + public static final JsonParser realValueSchemaTypeP = productP + .field("type", literalP("real")) + .field("schema", valueSchemaP) + .map(untuple((type, schema) -> schema), + $ -> tuple(Unit.UNIT, $)); + + public static final JsonParser>>> realProfileP + = productP + . field("name", stringP) + . field("type", realValueSchemaTypeP) + . field("profile_segments", listP(realProfileSegmentP)) + . map( + untuple((name, type, segments) -> Pair.of(type, segments)), + $ -> tuple("", $.getLeft(), $.getRight())); + + public static final JsonParser>>> discreteProfileP + = productP + . field("name", stringP) + . field("type", discreteValueSchemaTypeP) + . field("profile_segments", listP(discreteProfileSegmentP)) + . map( + untuple((name, type, segments) -> Pair.of(type, segments)), + $ -> tuple("", $.getLeft(), $.getRight())); + +} 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 508a23dd67..b8438142dc 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 @@ -23,6 +23,7 @@ import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchMissionModelException; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchPlanException; +import gov.nasa.jpl.aerie.scheduler.server.graphql.GraphQLParsers; import gov.nasa.jpl.aerie.scheduler.server.http.EventGraphFlattener; import gov.nasa.jpl.aerie.scheduler.server.http.InvalidEntityException; import gov.nasa.jpl.aerie.scheduler.server.http.InvalidJsonException; @@ -36,7 +37,6 @@ import gov.nasa.jpl.aerie.scheduler.server.models.ProfileSet; import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Triple; -import org.postgresql.util.PGInterval; import javax.json.Json; import javax.json.JsonArray; @@ -50,7 +50,6 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.file.Path; -import java.sql.SQLException; import java.time.Instant; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @@ -60,19 +59,29 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.TreeMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.function.Supplier; import java.util.stream.Collectors; +import static gov.nasa.jpl.aerie.json.BasicParsers.chooseP; +import static gov.nasa.jpl.aerie.json.BasicParsers.stringP; import static gov.nasa.jpl.aerie.merlin.driver.json.SerializedValueJsonParser.serializedValueP; import static gov.nasa.jpl.aerie.merlin.driver.json.ValueSchemaJsonParser.valueSchemaP; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECOND; import static gov.nasa.jpl.aerie.scheduler.server.graphql.GraphQLParsers.activityAttributesP; import static gov.nasa.jpl.aerie.scheduler.server.graphql.GraphQLParsers.discreteProfileTypeP; -import static gov.nasa.jpl.aerie.scheduler.server.graphql.GraphQLParsers.parseGraphQLInterval; +import static gov.nasa.jpl.aerie.scheduler.server.graphql.GraphQLParsers.durationFromPGInterval; +import static gov.nasa.jpl.aerie.scheduler.server.graphql.GraphQLParsers.graphQLIntervalFromDuration; +import static gov.nasa.jpl.aerie.scheduler.server.graphql.GraphQLParsers.instantFromStart; import static gov.nasa.jpl.aerie.scheduler.server.graphql.GraphQLParsers.parseGraphQLTimestamp; import static gov.nasa.jpl.aerie.scheduler.server.graphql.GraphQLParsers.realDynamicsP; import static gov.nasa.jpl.aerie.scheduler.server.graphql.GraphQLParsers.realProfileTypeP; import static gov.nasa.jpl.aerie.scheduler.server.graphql.GraphQLParsers.simulationArgumentsP; +import static gov.nasa.jpl.aerie.scheduler.server.graphql.ProfileParsers.discreteValueSchemaTypeP; +import static gov.nasa.jpl.aerie.scheduler.server.graphql.ProfileParsers.realValueSchemaTypeP; /** * {@inheritDoc} @@ -217,7 +226,7 @@ public PlanMetadata getPlanMetadata(final PlanId planId) final long planPK = plan.getJsonNumber("id").longValue(); final long planRev = plan.getJsonNumber("revision").longValue(); final var startTime = parseGraphQLTimestamp(plan.getString("start_time")); - final var duration = parseGraphQLInterval(plan.getString("duration")); + final var duration = durationFromPGInterval(plan.getString("duration")); final var model = plan.getJsonObject("mission_model"); final var modelId = model.getJsonNumber("id").longValue(); @@ -289,7 +298,7 @@ public MerlinPlan getPlanActivityDirectives(final PlanMetadata planMetadata, fin .getInputType() .getEffectiveArguments(deserializedArguments); final var merlinActivity = new ActivityDirective( - parseGraphQLInterval(start), + durationFromPGInterval(start), type, effectiveArguments, (anchorId != null) ? new ActivityDirectiveId(anchorId) : null, @@ -766,6 +775,275 @@ public DatasetId storeSimulationResults(final PlanMetadata planMetadata, return datasetIds.datasetId(); } + private Map getSimulatedActivities(SimulationDatasetId datasetId, Instant startSimulation) + throws PlanServiceException, IOException, InvalidJsonException + { + final var request = """ + query{ + simulated_activity(where: {simulation_dataset_id: {_eq: %d}}) { + activity_directive { + id + arguments + type + anchored_to_start + anchor_id + } + activity_type_name + duration + id + parent_id + start_offset + attributes + } + } + """.formatted(datasetId.id()); + final JsonObject response; + response = postRequest(request).get(); + final var data = response.getJsonObject("data").getJsonArray("simulated_activity"); + return parseSimulatedActivities(data, startSimulation); + } + +private Profiles getProfiles(DatasetId datasetId) throws PlanServiceException, IOException { + final var request = """ + query{ + profile(where: {dataset_id: {_eq: %d}}){ + type + duration + profile_segments { + start_offset + dynamics + } + name + } + } + """.formatted(datasetId.id()); + final JsonObject response; + response = postRequest(request).get(); + final var data = response.getJsonObject("data").getJsonArray("profile"); + return parseProfiles(data); +} + +private Map getSpans(DatasetId datasetId, Instant startTime) throws PlanServiceException, IOException { + final var request = """ + query{ + span(where: {duration: {_is_null: true}, dataset_id: {_eq: %d}}) { + attributes + parent_id + type + start_offset + id + } + } + """.formatted(datasetId.id()); + final JsonObject response; + response = postRequest(request).get(); + final var data = response.getJsonObject("data").getJsonArray("span"); + return parseUnfinishedActivities(data, startTime); +} + + @Override + public Optional getSimulationResults(PlanMetadata planMetadata) + throws PlanServiceException, IOException + { + final var simulationDatasetId = getSuitableSimulationResults(planMetadata); + if(simulationDatasetId.isEmpty()) return Optional.empty(); + try(var executorService = Executors.newFixedThreadPool(3)) { + Future> futureSimulatedActivities = executorService.submit(() -> getSimulatedActivities( + simulationDatasetId.get().simulationDatasetId(), + planMetadata.horizon().getStartInstant())); + Future> futureSpans = executorService.submit(() -> getSpans( + simulationDatasetId.get().datasetId(), + planMetadata.horizon().getStartInstant())); + Future futureProfiles = executorService.submit(() -> getProfiles(simulationDatasetId.get().datasetId())); + try { + final var simulatedActivities = futureSimulatedActivities.get(); + final var unfinishedActivities = futureSpans.get(); + final var profiles = futureProfiles.get(); + final var simulationStartTime = planMetadata.horizon().getStartInstant(); + 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( + profiles.realProfiles, + profiles.discreteProfiles, + simulatedActivities, + unfinishedActivities, + simulationStartTime, + duration, + List.of(), + new TreeMap<>() + )); + } catch (InterruptedException | ExecutionException e) { + return Optional.empty(); + } + } + } + + private Map parseUnfinishedActivities(JsonArray unfinishedActivitiesJson, Instant simulationStart){ + final var unfinishedActivities = new HashMap(); + for(final var unfinishedActivityJson: unfinishedActivitiesJson){ + final var activityAttributes = activityAttributesP.parse(unfinishedActivityJson.asJsonObject().getJsonObject("attributes")).getSuccessOrThrow(); + SimulatedActivityId parentId = null; + if(!unfinishedActivityJson.asJsonObject().isNull("parent_id")){ + parentId = new SimulatedActivityId(unfinishedActivityJson.asJsonObject().getJsonNumber("parent_id").longValue()); + } + final var activityType = unfinishedActivityJson.asJsonObject().getJsonString("type").getString(); + final var start = instantFromStart(simulationStart, + durationFromPGInterval(unfinishedActivityJson.asJsonObject().getJsonString("start_offset").getString())); + final var id = new SimulatedActivityId(unfinishedActivityJson.asJsonObject().getJsonNumber("id").longValue()); + Optional actDirectiveId = Optional.empty(); + if(activityAttributes.directiveId().isPresent()){ + actDirectiveId = Optional.of(new ActivityDirectiveId(activityAttributes.directiveId().get())); + } + final var unfinishedActivity = new UnfinishedActivity( + activityType, + activityAttributes.arguments(), + start, + parentId, + List.of(), + actDirectiveId + ); + unfinishedActivities.put(id, unfinishedActivity); + } + return unfinishedActivities; + } + + private record Profiles( + Map>>> realProfiles, + Map>>> discreteProfiles + ){} + + private Profiles parseProfiles(JsonArray dataset){ + Map>>> realProfiles = new HashMap<>(); + Map>>> discreteProfiles = new HashMap<>(); + for(final var profile:dataset){ + final var name = profile.asJsonObject().getString("name"); + final var type = profile.asJsonObject().getJsonObject("type"); + final var typetype = type.getString("type"); + final boolean isReal = typetype.equals("real"); + if(isReal){ + final var realProfile = parseProfile(profile.asJsonObject(), realDynamicsP); + realProfiles.put(name, realProfile); + } else { + final var discreteProfile = parseProfile(profile.asJsonObject(), serializedValueP); + discreteProfiles.put(name, discreteProfile); + } + } + return new Profiles(realProfiles, discreteProfiles); + } + + public Pair>> parseProfile(JsonObject profile, JsonParser dynamicsParser){ + // Profile segments are stored with their start offset relative to simulation start + // We must convert these to durations describing how long each segment lasts + final var profileExtent = durationFromPGInterval(profile.asJsonObject().getString("duration")); + final var type = chooseP(discreteValueSchemaTypeP, realValueSchemaTypeP).parse(profile.getJsonObject("type")).getSuccessOrThrow(); + final var resultSet = profile.getJsonArray("profile_segments").iterator(); + JsonValue curProfileSegment = null; + final var segments = new ArrayList>(); + if (resultSet.hasNext()) { + curProfileSegment = resultSet.next(); + var offset = durationFromPGInterval(curProfileSegment.asJsonObject().getString("start_offset")); + var dynamics = dynamicsParser.parse(curProfileSegment.asJsonObject().get("dynamics")).getSuccessOrThrow(); + + while (resultSet.hasNext()) { + curProfileSegment = resultSet.next(); + final var nextOffset = durationFromPGInterval(curProfileSegment.asJsonObject().getString("start_offset")); + final var duration = nextOffset.minus(offset); + segments.add(new ProfileSegment<>(duration, dynamics)); + offset = nextOffset; + dynamics = dynamicsParser.parse(curProfileSegment.asJsonObject().getJsonObject("dynamics")).getSuccessOrThrow(); + } + + final var duration = profileExtent.minus(offset); + segments.add(new ProfileSegment(duration, dynamics)); + } + return Pair.of(type, segments); + } + + private Map parseSimulatedActivities(JsonArray simulatedActivitiesArray, Instant simulationStart) + throws InvalidJsonException + { + final var simulatedActivities = new HashMap(); + for(final var simulatedActivityJson: simulatedActivitiesArray) { + //if no duration, this is an unfinished activity + if(simulatedActivityJson.asJsonObject().isNull("duration")) continue; + final var activityDuration = GraphQLParsers.durationP.parse(simulatedActivityJson.asJsonObject().get("duration")).getSuccessOrThrow(); + final var activityId = simulatedActivityJson.asJsonObject().getJsonNumber("id").longValue(); + SimulatedActivityId parentId = null; + if(!simulatedActivityJson.asJsonObject().isNull("parent_id")){ + parentId = new SimulatedActivityId(simulatedActivityJson.asJsonObject().getJsonNumber("parent_id").longValue()); + } + final var startOffset = instantFromStart(simulationStart,durationFromPGInterval(simulatedActivityJson.asJsonObject().getString("start_offset"))); + final var computedAttributes = serializedValueP.parse(simulatedActivityJson.asJsonObject().get("attributes")).getSuccessOrThrow(); + final var activityDirective = simulatedActivityJson.asJsonObject().getJsonObject("activity_directive"); + final var activityDirectiveId = new ActivityDirectiveId(activityDirective.getInt("id")); + final var activityDirectiveArguments = activityDirective.getJsonObject("arguments"); + final var deserializedArguments = BasicParsers + .mapP(serializedValueP) + .parse(activityDirectiveArguments) + .getSuccessOrThrow((reason) -> new InvalidJsonException(new InvalidEntityException(List.of(reason)))); + final var activityType = activityDirective.getString("type"); + final var simulatedActivity = new SimulatedActivity( + activityType, + deserializedArguments, + startOffset, + activityDuration, + parentId, + List.of(), + Optional.of(activityDirectiveId), + computedAttributes + ); + simulatedActivities.put(new SimulatedActivityId(activityId), simulatedActivity); + } + return simulatedActivities; + } + + /** + * Returns the simulation dataset id if the simulation + * - covers the entire planning horizon + * - corresponds to the plan revision + * @param planMetadata the plan metadata containing the planning horizon and plan revision + * @return optionally a simulation dataset id + */ + public Optional getSuitableSimulationResults(PlanMetadata planMetadata) throws PlanServiceException, IOException { + final var request = + """ + { + simulation_dataset( + where: { + status: {_eq: "success"}, + plan_revision: {_eq: %d}, + simulation_start_time: {_eq: "%s"}, + simulation_end_time: {_eq: "%s"}, + simulation: {plan_id: {_eq: %d}} + }) { + id + dataset_id + arguments + simulation { + arguments + } + } + }""".formatted( + planMetadata.planRev(), + planMetadata.horizon().getStartInstant(), + planMetadata.horizon().getEndInstant(), + planMetadata.planId().id()); + final JsonObject response; + response = postRequest(request).get(); + final var data = response.getJsonObject("data"); + final var simulationDatasets = data.getJsonArray("simulation_dataset"); + for(final var simulationDataset : simulationDatasets){ + final var simulationDatasetId = simulationDataset.asJsonObject().getInt("id"); + final var datasetId = simulationDataset.asJsonObject().getInt("dataset_id"); + final var simulationDatasetArguments = simulationArgumentsP.parse(simulationDataset.asJsonObject().getJsonObject("arguments")).getSuccessOrThrow(); + final var simulationArguments = simulationArgumentsP.parse(simulationDataset.asJsonObject().getJsonObject("simulation").getJsonObject("arguments")).getSuccessOrThrow(); + if(!simulationDatasetArguments.equals(simulationArguments)) continue; + return Optional.of(new DatasetIds(new DatasetId(datasetId), new SimulationDatasetId(simulationDatasetId))); + } + return Optional.empty(); + } + private SimulationId createSimulation(final PlanId planId, final Map arguments) throws PlanServiceException, IOException { @@ -924,23 +1202,7 @@ private HashMap postResourceProfiles(DatasetId datasetId, return profileRecords; } - public PGInterval graphQLIntervalFromDuration(final Duration duration) { - try { - final var micros = duration.in(MICROSECOND); - return new PGInterval("PT%d.%06dS".formatted(micros / 1_000_000, micros % 1_000_000)); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } - public PGInterval graphQLIntervalFromDuration(final Instant instant1, final Instant instant2) { - try { - final var micros = java.time.Duration.between(instant1, instant2).toNanos() / 1000; - return new PGInterval("PT%d.%06dS".formatted(micros / 1_000_000, micros % 1_000_000)); - } catch (SQLException e) { - throw new RuntimeException(e); - } - } private void postProfileSegments( final DatasetId datasetId, diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/PlanService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/PlanService.java index 967ef4cf34..b3d7ce07e1 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/PlanService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/PlanService.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.time.Instant; import java.util.Map; +import java.util.Optional; public interface PlanService { interface ReaderRole { @@ -64,6 +65,14 @@ MerlinPlan getPlanActivityDirectives(final PlanMetadata planMetadata, final Prob //TODO: (defensive) should combine such checks into the mutations they are guarding, but not possible in graphql? void ensurePlanExists(final PlanId planId) throws IOException, NoSuchPlanException, PlanServiceException; + + /** + * 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 + */ + Optional getSimulationResults(PlanMetadata planMetadata) throws PlanServiceException, IOException, InvalidJsonException; } interface WriterRole { diff --git a/scheduler-server/src/test/java/gov/nasa/jpl/aerie/scheduler/server/graphql/GraphQLParsersTest.java b/scheduler-server/src/test/java/gov/nasa/jpl/aerie/scheduler/server/graphql/GraphQLParsersTest.java index f6d2f92754..7afe66fa4a 100644 --- a/scheduler-server/src/test/java/gov/nasa/jpl/aerie/scheduler/server/graphql/GraphQLParsersTest.java +++ b/scheduler-server/src/test/java/gov/nasa/jpl/aerie/scheduler/server/graphql/GraphQLParsersTest.java @@ -28,15 +28,7 @@ public static Stream parseGraphQLInterval() { Arguments.of("322:21:15.", parseDurationISO8601("PT322H21M15S")), Arguments.of("322:21:15", parseDurationISO8601("PT322H21M15S")), Arguments.of("+322:21:15", parseDurationISO8601("PT322H21M15S")), - Arguments.of("-322:21:15", parseDurationISO8601("PT-322H-21M-15S")), - Arguments.of("21:15.111", parseDurationISO8601("PT21M15.111S")), - Arguments.of("+21:15.111", parseDurationISO8601("PT21M15.111S")), - Arguments.of("-21:15.111", parseDurationISO8601("PT-21M-15.111S")), - Arguments.of("15.111", parseDurationISO8601("PT15.111S")), - Arguments.of("15.", parseDurationISO8601("PT15S")), - Arguments.of("15", parseDurationISO8601("PT15S")), - Arguments.of("-15", parseDurationISO8601("PT-15S")), - Arguments.of("+15", parseDurationISO8601("PT15S")) + Arguments.of("-322:21:15", parseDurationISO8601("PT-322H-21M-15S")) ); } @@ -50,7 +42,7 @@ void parseGraphQLTimestamp(String input, Timestamp expected) { @ParameterizedTest @MethodSource void parseGraphQLInterval(String input, Duration expected) { - final var actual = GraphQLParsers.parseGraphQLInterval(input); + final var actual = GraphQLParsers.durationFromPGInterval(input); assertEquals(expected, actual); } 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 bced3af91f..d5f84df40d 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 @@ -21,6 +21,7 @@ import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; import gov.nasa.jpl.aerie.merlin.driver.MissionModelLoader; +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; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; @@ -42,6 +43,7 @@ import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchSpecificationException; import gov.nasa.jpl.aerie.scheduler.server.exceptions.ResultsProtocolFailure; import gov.nasa.jpl.aerie.scheduler.server.exceptions.SpecificationLoadException; +import gov.nasa.jpl.aerie.scheduler.server.http.InvalidJsonException; import gov.nasa.jpl.aerie.scheduler.server.http.ResponseSerializers; import gov.nasa.jpl.aerie.scheduler.server.models.DatasetId; import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; @@ -128,9 +130,10 @@ public void schedule(final ScheduleRequest request, final ResultsProtocol.Writer simulationFacade, schedulerMissionModel.schedulerModel() ); + final var initialSimulationResults = loadSimulationResults(planMetadata); //seed the problem with the initial plan contents final var loadedPlanComponents = loadInitialPlan(planMetadata, problem); - problem.setInitialPlan(loadedPlanComponents.schedulerPlan()); + problem.setInitialPlan(loadedPlanComponents.schedulerPlan(), initialSimulationResults); //apply constraints/goals to the problem final var compiledGlobalSchedulingConditions = new ArrayList(); @@ -226,7 +229,8 @@ public void schedule(final ScheduleRequest request, final ResultsProtocol.Writer solutionPlan, activityToGoalId ); - final var datasetId = storeSimulationResults(planningHorizon, simulationFacade, planMetadata, instancesToIds); + final var planMetadataAfterChanges = planService.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); @@ -267,6 +271,14 @@ public void schedule(final ScheduleRequest request, final ResultsProtocol.Writer } } + private Optional loadSimulationResults(final PlanMetadata planMetadata){ + try { + return planService.getSimulationResults(planMetadata); + } catch (PlanServiceException | IOException | InvalidJsonException e) { + throw new ResultsProtocolFailure(e); + } + } + private Optional storeSimulationResults(PlanningHorizon planningHorizon, SimulationFacade simulationFacade, PlanMetadata planMetadata, final Map schedDirectiveToMerlinId) throws PlanServiceException, IOException 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 567795aecd..597751b875 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 @@ -1,14 +1,5 @@ package gov.nasa.jpl.aerie.scheduler.worker.services; -import java.io.IOException; -import java.nio.file.Path; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; @@ -16,20 +7,32 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.scheduler.TimeUtility; -import gov.nasa.jpl.aerie.scheduler.model.*; +import gov.nasa.jpl.aerie.scheduler.model.Plan; +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.model.SchedulingActivityDirectiveId; +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.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.MerlinPlan; import gov.nasa.jpl.aerie.scheduler.server.models.MissionModelId; import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; import gov.nasa.jpl.aerie.scheduler.server.models.PlanMetadata; -import gov.nasa.jpl.aerie.scheduler.server.services.GraphQLMerlinService; import gov.nasa.jpl.aerie.scheduler.server.services.MissionModelService; import gov.nasa.jpl.aerie.scheduler.server.services.PlanService; import gov.nasa.jpl.aerie.scheduler.server.services.PlanServiceException; import org.apache.commons.lang3.tuple.Pair; +import java.io.IOException; +import java.nio.file.Path; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + class MockMerlinService implements MissionModelService, PlanService.OwnerRole { private Optional planningHorizon; @@ -132,6 +135,12 @@ public void ensurePlanExists(final PlanId planId) { } + @Override + public Optional getSimulationResults(final PlanMetadata planMetadata) + { + return Optional.empty(); + } + @Override public void clearPlanActivityDirectives(final PlanId planId) { From 5d6717d5676cfc3a31ea42288fa71da741851fdc Mon Sep 17 00:00:00 2001 From: maillard Date: Tue, 1 Aug 2023 09:37:09 -0700 Subject: [PATCH 07/10] Fix way of loading initial plan in tests Tests were running correctly but the proper call path was not taken --- .../aerie/scheduler/SimulationFacadeTest.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) 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 39d44578c0..3dc318b97c 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 @@ -28,7 +28,10 @@ import gov.nasa.jpl.aerie.scheduler.constraints.resources.StateQueryParam; import gov.nasa.jpl.aerie.scheduler.goals.ChildCustody; import gov.nasa.jpl.aerie.scheduler.goals.ProceduralCreationGoal; -import gov.nasa.jpl.aerie.scheduler.model.*; +import gov.nasa.jpl.aerie.scheduler.model.Plan; +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.model.SchedulingActivityDirective; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; @@ -40,7 +43,6 @@ import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.function.Function; import static com.google.common.truth.Truth.assertThat; @@ -260,8 +262,8 @@ public void whenValueNotEqualDoubleOnSimplePlan() throws SimulationFacade.Simula } @Test - public void testCoexistenceGoalWithResourceConstraint() throws SimulationFacade.SimulationException { - facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); + public void testCoexistenceGoalWithResourceConstraint() { + problem.setInitialPlan(makeTestPlanP0B1()); /** * reminder for PB1 @@ -300,8 +302,8 @@ public void testCoexistenceGoalWithResourceConstraint() throws SimulationFacade. } @Test - public void testProceduralGoalWithResourceConstraint() throws SimulationFacade.SimulationException { - facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); + public void testProceduralGoalWithResourceConstraint() { + problem.setInitialPlan(makeTestPlanP0B1()); final var constraint = new And( new LessThanOrEqual(new RealResource("/peel"), new RealValue(3.0)), @@ -342,8 +344,8 @@ public void testProceduralGoalWithResourceConstraint() throws SimulationFacade.S } @Test - public void testActivityTypeWithResourceConstraint() throws SimulationFacade.SimulationException { - facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); + public void testActivityTypeWithResourceConstraint() { + problem.setInitialPlan(makeTestPlanP0B1()); final var constraint = new And( new LessThanOrEqual(new RealResource("/peel"), new RealValue(3.0)), From cc00d182eb8cc360f6808a4536b27ea4814a8b6a Mon Sep 17 00:00:00 2001 From: maillard Date: Tue, 1 Aug 2023 09:38:23 -0700 Subject: [PATCH 08/10] Test whether passing initial simulation results decrease the number of sims --- .../aerie/scheduler/PrioritySolverTest.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) 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 1d76ac1600..30278ce884 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 @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import java.util.List; +import java.util.Optional; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth8.assertThat; @@ -248,6 +249,47 @@ public void getNextSolution_coexistenceGoalOnActivityWorks() { assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } + /** + * This test is the same as getNextSolution_coexistenceGoalOnActivityWorks except for the initial simulation results that + * are loaded with the initial plan. This results in 1 less simulation as the initial results are used for generating conflicts. + */ + @Test + public void getNextSolution_coexistenceGoalOnActivityWorks_withInitialSimResults() + throws SimulationFacade.SimulationException + { + final var problem = makeTestMissionAB(); + + final var adHocFacade = new SimulationFacade(problem.getPlanningHorizon(), problem.getMissionModel()); + adHocFacade.insertActivitiesIntoSimulation(makePlanA012(problem).getActivities()); + adHocFacade.computeSimulationResultsUntil(problem.getPlanningHorizon().getEndAerie()); + final var simResults = adHocFacade.getLatestDriverSimulationResults(); + problem.setInitialPlan(makePlanA012(problem), Optional.of(simResults)); + + final var actTypeA = problem.getActivityType("ControllableDurationActivity"); + final var actTypeB = problem.getActivityType("OtherControllableDurationActivity"); + final var goal = new CoexistenceGoal.Builder() + .named("g0") + .forAllTimeIn(new WindowsWrapperExpression(new Windows(false).set(h.getHor(), true))) + .forEach(new ActivityExpression.Builder() + .ofType(actTypeA) + .build()) + .thereExistsOne(new ActivityCreationTemplate.Builder() + .ofType(actTypeB) + .duration(d1min) + .build()) + .startsAt(TimeAnchor.START) + .aliasForAnchors("Bond. James Bond") + .withinPlanHorizon(h) + .build(); + problem.setGoals(List.of(goal)); + final var solver = makeProblemSolver(problem); + final var plan = solver.getNextSolution().orElseThrow(); + final var expectedPlan = makePlanAB012(problem); + assertThat(plan.getActivitiesByTime()) + .comparingElementsUsing(equalExceptInName) + .containsAtLeastElementsIn(expectedPlan.getActivitiesByTime()); + assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); + } @Test public void testCardGoalWithApplyWhen(){ From 0d192ac3510b67a0ed06ea289d9c4c0949a52b50 Mon Sep 17 00:00:00 2001 From: maillard Date: Tue, 1 Aug 2023 11:22:33 -0700 Subject: [PATCH 09/10] Update simulation performance results for scheduler tests --- .../aerie/scheduler/LongDurationPlanTest.java | 2 +- .../jpl/aerie/scheduler/PrioritySolverTest.java | 6 +++--- .../nasa/jpl/aerie/scheduler/TestApplyWhen.java | 16 ++++++++-------- .../scheduler/UncontrollableDurationTest.java | 6 +++--- .../simulation/AnchorSchedulerTest.java | 6 +++--- 5 files changed, 18 insertions(+), 18 deletions(-) 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 7c59c2925e..a28a538f4d 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 @@ -79,7 +79,7 @@ public void getNextSolution_initialPlanInOutput() { Truth.assertThat(plan.get().getActivitiesByTime()) .comparingElementsUsing(equalExceptInName) .containsExactlyElementsIn(expectedPlan.getActivitiesByTime()); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @Test 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 30278ce884..9f155f4163 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 @@ -136,7 +136,7 @@ public void getNextSolution_initialPlanInOutput() { assertThat(plan.get().getActivitiesByTime()) .comparingElementsUsing(equalExceptInName) .containsExactlyElementsIn(expectedPlan.getActivitiesByTime()); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -246,7 +246,7 @@ public void getNextSolution_coexistenceGoalOnActivityWorks() { assertThat(plan.getActivitiesByTime()) .comparingElementsUsing(equalExceptInName) .containsAtLeastElementsIn(expectedPlan.getActivitiesByTime()); - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); } /** @@ -288,7 +288,7 @@ public void getNextSolution_coexistenceGoalOnActivityWorks_withInitialSimResults assertThat(plan.getActivitiesByTime()) .comparingElementsUsing(equalExceptInName) .containsAtLeastElementsIn(expectedPlan.getActivitiesByTime()); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test 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 da8921b913..00c3e0dbce 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 @@ -453,7 +453,7 @@ public void testRecurrenceCutoffUncontrollable() { 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(13, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(8, problem.getSimulationFacade().countSimulationRestarts()); } @@ -644,7 +644,7 @@ public void testCardinalityUncontrollable() { //ruled unpredictable for now .reduce(Duration.ZERO, Duration::plus); assertTrue(size >= 3 && size <= 10); assertTrue(totalDuration.dividedBy(Duration.SECOND) >= 16 && totalDuration.dividedBy(Duration.SECOND) <= 19); - assertEquals(17, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(9, problem.getSimulationFacade().countSimulationRestarts()); } @@ -698,7 +698,7 @@ public void testCoexistenceWindowCutoff() { logger.debug(a.startOffset().toString() + ", " + a.duration().toString()); } assertEquals(4, plan.get().getActivitiesByTime().size()); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -753,7 +753,7 @@ public void testCoexistenceJustFits() { logger.debug(a.startOffset().toString() + ", " + a.duration().toString()); } assertEquals(5, plan.get().getActivitiesByTime().size()); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -817,7 +817,7 @@ public void testCoexistenceUncontrollableCutoff() { //ruled unpredictable for no assertEquals(2, plan.get().getActivitiesByTime() .stream().filter($ -> $.duration().dividedBy(Duration.SECOND) == 2).toList() .size()); - assertEquals(6, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -1027,7 +1027,7 @@ public void testCoexistenceWindowsBisect() { //bad, should fail completely. wort 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(4, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -1095,7 +1095,7 @@ public void testCoexistenceWindowsBisect2() { //corrected. Bisection does work s 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(4, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -1150,7 +1150,7 @@ public void testCoexistenceUncontrollableJustFits() { logger.debug(a.startOffset().toString() + ", " + a.duration().toString()); } assertEquals(5, plan.get().getActivitiesByTime().size()); - assertEquals(6, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test 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 f9a8acb51d..5db8f2a8f3 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 @@ -96,7 +96,7 @@ public void testNonLinear(){ 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(31, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(13, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -149,7 +149,7 @@ public void testTimeDependent(){ 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(41, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(21, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -223,7 +223,7 @@ public void testScheduleExceptionThrowingTask(){ planningHorizon.fromStart("PT120S"), planningHorizon.fromStart("PT120S"), problem.getActivityType("LateRiser"))); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } } 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 c58c7d8cc6..100ca7c8bc 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 @@ -222,7 +222,7 @@ public void activitiesAnchoredToPlan() { final var actualSimResults = driver.getSimulationResultsUpTo(planStart, tenDays); assertEqualsSimulationResults(expectedSimResults, actualSimResults); - assertEquals(2, driver.getCountSimulationRestarts()); + assertEquals(1, driver.getCountSimulationRestarts()); } @Test @@ -578,7 +578,7 @@ public void decomposingActivitiesAndAnchors(){ // We have examined all the children assertTrue(childSimulatedActivities.isEmpty()); - assertEquals(2, driver.getCountSimulationRestarts()); + assertEquals(1, driver.getCountSimulationRestarts()); } @Test @@ -623,7 +623,7 @@ public void naryTreeAnchorChain() { assertEquals(3906, expectedSimResults.simulatedActivities.size()); assertEqualsSimulationResults(expectedSimResults, actualSimResults); - assertEquals(2, driver.getCountSimulationRestarts()); + assertEquals(1, driver.getCountSimulationRestarts()); } } From 674837ef023529b82d2ad2f4a613dc736208ccd1 Mon Sep 17 00:00:00 2001 From: maillard Date: Mon, 14 Aug 2023 09:22:08 -0700 Subject: [PATCH 10/10] Add e2e-test --- .../src/tests/scheduler-with-sim.test.ts | 319 ++++++++++++++++++ e2e-tests/src/tests/scheduler.test.ts | 33 +- e2e-tests/src/utilities/gql.ts | 40 +++ e2e-tests/src/utilities/requests.ts | 84 +++++ 4 files changed, 449 insertions(+), 27 deletions(-) create mode 100644 e2e-tests/src/tests/scheduler-with-sim.test.ts diff --git a/e2e-tests/src/tests/scheduler-with-sim.test.ts b/e2e-tests/src/tests/scheduler-with-sim.test.ts new file mode 100644 index 0000000000..e62e783e0a --- /dev/null +++ b/e2e-tests/src/tests/scheduler-with-sim.test.ts @@ -0,0 +1,319 @@ + +import {expect, test} from "@playwright/test"; +import req, {awaitScheduling, awaitSimulation} from "../utilities/requests.js"; +import time from "../utilities/time.js"; + +/* +This is testing the check on Plan Revision before loading initial simulation results. We inject results associated with an old plan revision. +In these results, there is only one GrowBanana activity instead of the actual 2 present in the latest plan revision. +A coexistence goal attaching to GrowBanana activities shows that the scheduler did not use the stale sim results. +*/ +test.describe.serial('Scheduling with initial sim results', () => { + const rd = Math.random() * 100; + const plan_start_timestamp = "2021-001T00:00:00.000"; + const plan_end_timestamp = "2021-001T12:00:00.000"; + + test('Main', async ({ request }) => { + //upload bananation jar + const jar_id = await req.uploadJarFile(request); + + const model: MissionModelInsertInput = { + jar_id, + mission: 'aerie_e2e_tests' + rd, + name: 'Banananation (e2e tests)' + rd, + version: '0.0.0' + rd, + }; + const mission_model_id = await req.createMissionModel(request, model); + //delay for generation + await delay(2000); + const plan_input : CreatePlanInput = { + model_id : mission_model_id, + name : 'test_plan' + rd, + start_time : plan_start_timestamp, + duration : time.getIntervalFromDoyRange(plan_start_timestamp, plan_end_timestamp) + }; + const plan_id = await req.createPlan(request, plan_input); + + const firstGrowBananaToInsert : ActivityInsertInput = + { + //no arguments to ensure that the scheduler is getting effective arguments + arguments : {}, + plan_id: plan_id, + type : "GrowBanana", + start_offset : "1h" + }; + + await req.insertActivity(request, firstGrowBananaToInsert); + + await awaitSimulation(request, plan_id); + + const secondGrowBananaToInsert : ActivityInsertInput = + { + //no arguments to ensure that the scheduler is getting effective arguments + arguments : {}, + plan_id: plan_id, + type : "GrowBanana", + start_offset : "2h" + }; + + await req.insertActivity(request, secondGrowBananaToInsert); + + const schedulingGoal1 : SchedulingGoalInsertInput = + { + description: "Test goal", + model_id: mission_model_id, + name: "ForEachGrowPeel"+rd, + definition: `export default function myGoal() { + return Goal.CoexistenceGoal({ + forEach: ActivityExpression.ofType(ActivityType.GrowBanana), + activityTemplate: ActivityTemplates.BiteBanana({ + biteSize: 1, + }), + startsAt:TimingConstraint.singleton(WindowProperty.END) + }) + }` + }; + + const first_goal_id = await req.insertSchedulingGoal(request, schedulingGoal1); + + let plan_revision = await req.getPlanRevision(request, plan_id); + + const schedulingSpecification : SchedulingSpecInsertInput = { + // @ts-ignore + horizon_end: plan_end_timestamp, + horizon_start: plan_start_timestamp, + plan_id : plan_id, + plan_revision : plan_revision, + simulation_arguments : {}, + analysis_only: false + } + const scheduling_specification_id = await req.insertSchedulingSpecification(request, schedulingSpecification); + + const priority = 0; + const specGoal: SchedulingSpecGoalInsertInput = { + goal_id: first_goal_id, + priority: priority, + specification_id: scheduling_specification_id, + }; + await req.createSchedulingSpecGoal(request, specGoal); + + const { status, datasetId } = await awaitScheduling(request, scheduling_specification_id); + + expect(status).toEqual("complete") + expect(datasetId).not.toBeNull(); + + const plan = await req.getPlan(request, plan_id) + expect(plan.activity_directives.length).toEqual(4); + + //delete plan + const deleted_plan_id = await req.deletePlan(request, plan_id); + expect(deleted_plan_id).not.toBeNull(); + expect(deleted_plan_id).toBeDefined(); + expect(deleted_plan_id).toEqual(plan_id); + + //delete mission model + const deleted_mission_model_id = await req.deleteMissionModel(request, mission_model_id) + expect(deleted_mission_model_id).not.toBeNull(); + expect(deleted_mission_model_id).toBeDefined(); + expect(deleted_mission_model_id).toEqual(mission_model_id); + }); + + + /* In this test, we load simulation results with the current plan revision but with a different sim config. If the + injected results are picked up, the goal will not be satisfied and the number of activities will stay at its original value. + */ + test('Scheduling sim results 2', async ({ request }) => { + const rd = Math.random() * 100; + const plan_start_timestamp = "2021-001T00:00:00.000"; + const plan_end_timestamp = "2021-001T12:00:00.000"; + + //upload bananation jar + const jar_id = await req.uploadJarFile(request); + + const model: MissionModelInsertInput = { + jar_id, + mission: 'aerie_e2e_tests' + rd, + name: 'Banananation (e2e tests)' + rd, + version: '0.0.0' + rd, + }; + const mission_model_id = await req.createMissionModel(request, model); + //delay for generation + await delay(2000); + const plan_input : CreatePlanInput = { + model_id : mission_model_id, + name : 'test_plan' + rd, + start_time : plan_start_timestamp, + duration : time.getIntervalFromDoyRange(plan_start_timestamp, plan_end_timestamp) + }; + const plan_id = await req.createPlan(request, plan_input); + + const simulation_id = await req.getSimulationId(request, plan_id); + + const simulation_template : InsertSimulationTemplateInput = { + model_id: mission_model_id, + arguments: { + "initialPlantCount": 400, + }, + description: 'Template for Plan ' +plan_id + }; + + await req.insertAndAssociateSimulationTemplate(request, simulation_template, simulation_id); + + await awaitSimulation(request, plan_id); + + const empty_simulation_template : InsertSimulationTemplateInput = { + model_id: mission_model_id, + arguments: { + }, + description: 'Template for Plan ' +plan_id + }; + + await req.insertAndAssociateSimulationTemplate(request, empty_simulation_template, simulation_id); + + const schedulingGoal1 : SchedulingGoalInsertInput = + { + description: "Test goal", + model_id: mission_model_id, + name: "ForEachPlanLessThan300"+rd, + definition: `export default () => Goal.CoexistenceGoal({ + forEach: Real.Resource("/plant").lessThan(300), + activityTemplate: ActivityTemplates.GrowBanana({quantity: 10, growingDuration: Temporal.Duration.from({minutes:1}) }), + startsAt: TimingConstraint.singleton(WindowProperty.START) + })` + }; + + const first_goal_id = await req.insertSchedulingGoal(request, schedulingGoal1); + + const plan_revision = await req.getPlanRevision(request, plan_id); + + const schedulingSpecification : SchedulingSpecInsertInput = { + // @ts-ignore + horizon_end: plan_end_timestamp, + horizon_start: plan_start_timestamp, + plan_id : plan_id, + plan_revision : plan_revision, + simulation_arguments : {}, + analysis_only: false + } + const specification_id = await req.insertSchedulingSpecification(request, schedulingSpecification); + + const priority = 0; + const specGoal: SchedulingSpecGoalInsertInput = { + goal_id: first_goal_id, + priority: priority, + specification_id: specification_id, + }; + await req.createSchedulingSpecGoal(request, specGoal); + + await awaitScheduling(request, specification_id); + + const plan = await req.getPlan(request, plan_id) + expect(plan.activity_directives.length).toEqual(1); + + //delete plan + const deleted_plan_id = await req.deletePlan(request, plan_id); + expect(deleted_plan_id).not.toBeNull(); + expect(deleted_plan_id).toBeDefined(); + expect(deleted_plan_id).toEqual(plan_id); + + //delete mission model + const deleted_mission_model_id = await req.deleteMissionModel(request, mission_model_id) + expect(deleted_mission_model_id).not.toBeNull(); + expect(deleted_mission_model_id).toBeDefined(); + expect(deleted_mission_model_id).toEqual(mission_model_id); + }); + + /* In this test, we inject simulation results to test that the scheduler is loading them properly. If they are picked up, + there should be no activity in the plan (plant>300). Otherwise, there should be one activity. + */ + test('Scheduling sim results 3', async ({ request }) => { + const rd = Math.random() * 100; + const plan_start_timestamp = "2021-001T00:00:00.000"; + const plan_end_timestamp = "2021-001T12:00:00.000"; + //upload bananation jar + const jar_id = await req.uploadJarFile(request); + + const model: MissionModelInsertInput = { + jar_id, + mission: 'aerie_e2e_tests' + rd, + name: 'Banananation (e2e tests)' + rd, + version: '0.0.0' + rd, + }; + const mission_model_id = await req.createMissionModel(request, model); + //delay for generation + await delay(2000); + const plan_input : CreatePlanInput = { + model_id : mission_model_id, + name : 'test_plan' + rd, + start_time : plan_start_timestamp, + duration : time.getIntervalFromDoyRange(plan_start_timestamp, plan_end_timestamp) + }; + const plan_id = await req.createPlan(request, plan_input); + + const simId = await req.getSimulationId(request, plan_id); + let plan_revision = await req.getPlanRevision(request, plan_id); + + const datasetId = await req.insertSimulationDataset( + request, + simId, + plan_start_timestamp, + plan_end_timestamp, + "success", + {}, + plan_revision); + + const profileId = await req.insertProfile(request, datasetId, "12h", "/plant", { "type": "discrete", "schema": { "type": "int" } }); + await req.insertProfileSegment(request, datasetId, 400, false, profileId, "0h"); + + const schedulingGoal1 : SchedulingGoalInsertInput = + { + description: "Test goal", + model_id: mission_model_id, + name: "ForEachPlanLessThan300"+rd, + definition: `export default () => Goal.CoexistenceGoal({ + forEach: Real.Resource("/plant").lessThan(300), + activityTemplate: ActivityTemplates.GrowBanana({quantity: 10, growingDuration: Temporal.Duration.from({minutes:1}) }), + startsAt: TimingConstraint.singleton(WindowProperty.START) + })` + }; + + const first_goal_id = await req.insertSchedulingGoal(request, schedulingGoal1); + + plan_revision = await req.getPlanRevision(request, plan_id); + + const schedulingSpecification : SchedulingSpecInsertInput = { + // @ts-ignore + horizon_end: plan_end_timestamp, + horizon_start: plan_start_timestamp, + plan_id : plan_id, + plan_revision : plan_revision, + simulation_arguments : {}, + analysis_only: false + } + const specification_id = await req.insertSchedulingSpecification(request, schedulingSpecification); + + const priority = 0; + const specGoal: SchedulingSpecGoalInsertInput = { + goal_id: first_goal_id, + priority: priority, + specification_id: specification_id, + }; + await req.createSchedulingSpecGoal(request, specGoal); + + await awaitScheduling(request, specification_id); + + const plan = await req.getPlan(request, plan_id) + expect(plan.activity_directives.length).toEqual(0); + + //delete plan + await req.deletePlan(request, plan_id); + + //delete mission model + await req.deleteMissionModel(request, mission_model_id) + }); +}); + + +function delay(ms: number) { + return new Promise( resolve => setTimeout(resolve, ms) ); +} diff --git a/e2e-tests/src/tests/scheduler.test.ts b/e2e-tests/src/tests/scheduler.test.ts index 0aa6fec393..64949d3bec 100644 --- a/e2e-tests/src/tests/scheduler.test.ts +++ b/e2e-tests/src/tests/scheduler.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import req from '../utilities/requests.js'; +import req, {awaitScheduling} from '../utilities/requests.js'; import time from '../utilities/time.js'; const eqSet = (xs, ys) => @@ -173,34 +173,13 @@ test.describe.serial('Scheduling', () => { }); test('Run scheduling', async ({ request }) => { - let status_local: string; - let analysisId_local: number; - const { reason, status, analysisId } = await req.schedule(request, specification_id); - expect(status).not.toBeNull(); - expect(status).toBeDefined(); + const schedulingResults = await awaitScheduling(request, specification_id); + const { analysisId, status, datasetId } = schedulingResults; + dataset_id = datasetId + expect(status).toEqual("complete") expect(analysisId).not.toBeNull(); - expect(analysisId).toBeDefined(); + expect(datasetId).not.toBeNull(); expect(typeof analysisId).toEqual("number") - analysisId_local = analysisId; - const max_it = 10; - let it = 0; - let reason_local: string; - while (it++ < max_it && (status == 'pending' || status == 'incomplete')) { - const { reason, status, analysisId, datasetId } = await req.schedule(request, specification_id); - status_local = status; - reason_local = reason; - expect(status).not.toBeNull(); - expect(status).toBeDefined(); - dataset_id = datasetId - await delay(1000); - } - if (status_local == "failed") { - console.error(reason_local) - throw new Error(reason_local); - } - expect(status_local).toEqual("complete") - expect(analysisId_local).toEqual(analysisId) - expect(dataset_id).not.toBeNull(); }); test('Verify posting of simulation results', async ({ request }) => { diff --git a/e2e-tests/src/utilities/gql.ts b/e2e-tests/src/utilities/gql.ts index 86ccd069bf..fde180557f 100644 --- a/e2e-tests/src/utilities/gql.ts +++ b/e2e-tests/src/utilities/gql.ts @@ -273,6 +273,46 @@ const gql = { } `, + INSERT_SPAN:`#graphql + mutation InsertSpan( + $parentId: Int!, + $duration: interval, + $datasetId: Int!, + $type: String, + $startOffset: interval, + $attributes: jsonb + ){ + insert_span_one(object: {parent_id: $parentId, duration: $duration, dataset_id: $datasetId, type: $type, start_offset: $startOffset, attributes: $attributes}) { + id + } +} +`, + +INSERT_SIMULATION_DATASET:`#graphql + mutation InsertSimulationDataset($simulationDatasetInsertInput:simulation_dataset_insert_input! + ){ + insert_simulation_dataset_one(object: $simulationDatasetInsertInput) { + dataset_id + } + } + `, + + INSERT_PROFILE: `#graphql + mutation insertProfile($datasetId: Int!, $duration:interval, $name:String, $type:jsonb){ + insert_profile_one(object: {dataset_id: $datasetId, duration: $duration, name: $name, type: $type}) { + id + } + } + `, + + INSERT_PROFILE_SEGMENT:`#graphql + mutation insertProfileSegment($datasetId: Int!, $dynamics:jsonb, $isGap: Boolean, $profileId:Int!, $startOffset:interval){ + insert_profile_segment_one(object: {dataset_id: $datasetId, dynamics: $dynamics, is_gap: $isGap, profile_id: $profileId, start_offset: $startOffset}){ + dataset_id + } + } + `, + INSERT_SIMULATION_TEMPLATE: `#graphql mutation CreateSimulationTemplate($simulationTemplateInsertInput: simulation_template_insert_input!) { insert_simulation_template_one(object: $simulationTemplateInsertInput) { diff --git a/e2e-tests/src/utilities/requests.ts b/e2e-tests/src/utilities/requests.ts index e0c82e92a7..d38352ee35 100644 --- a/e2e-tests/src/utilities/requests.ts +++ b/e2e-tests/src/utilities/requests.ts @@ -152,6 +152,53 @@ const req = { return simulation_dataset[0] as SimulationDataset; }, + async insertSpan( + request: APIRequestContext, + parentId: number, + duration: string, + simulationDatasetId: number, + type: string, + startOffset: string, + attributes: any){ + //note the empty headers: required to act as hasura admin role to be able to insert in these tables + const data = await req.hasura(request, gql.INSERT_SPAN, { + parentId: parentId, + duration: duration, + datasetId: simulationDatasetId, + type: type, + startOffset: startOffset, + attributes: attributes},{}); + const { insert_span_one } = data; + const { id } = insert_span_one; + return id; + }, + + async insertSimulationDataset( + request: APIRequestContext, + simulationId: number, + simulationStartTime: string, + simulationEndTime:string, + status:string, + simulationArguments:ArgumentsMap, + planRevision: number + + ){ + //note the empty headers: required to act as hasura admin role to be able to insert in these tables + const data = await req.hasura(request, gql.INSERT_SIMULATION_DATASET, { + simulationDatasetInsertInput : { + simulation_id: simulationId, + simulation_start_time:simulationStartTime, + simulation_end_time: simulationEndTime, + status:status, + arguments: simulationArguments, + plan_revision: planRevision + } + },{}); + const { insert_simulation_dataset_one } = data; + const { dataset_id : datasetId } = insert_simulation_dataset_one; + return datasetId; + }, + async insertAndAssociateSimulationTemplate( request: APIRequestContext, template: InsertSimulationTemplateInput, @@ -330,6 +377,22 @@ const req = { return id; }, + async insertProfile(request: APIRequestContext, datasetId: number, duration:string, name: string, type:object): Promise { + //note the empty headers: required to act as hasura admin role to be able to insert in these tables + const data = await req.hasura(request, gql.INSERT_PROFILE, { datasetId, duration, name, type }, {}); + const { insert_profile_one } = data; + const { id } = insert_profile_one; + return id; + }, + + async insertProfileSegment(request: APIRequestContext, datasetId: number, dynamics:number, isGap: boolean, profileId:number, startOffset:string): Promise { + //note the empty headers: required to act as hasura admin role to be able to insert in these tables + const data = await req.hasura(request, gql.INSERT_PROFILE_SEGMENT, { datasetId, dynamics, isGap, profileId, startOffset }, {}); + const { insert_profile_segment_one } = data; + const { dataset_id } = insert_profile_segment_one; + return dataset_id; + }, + async updateConstraint( request: APIRequestContext, constraintId: number, @@ -450,4 +513,25 @@ export async function awaitSimulation(request: APIRequestContext, plan_id: numbe throw Error(`Simulation timed out after ${max_iter} iterations`); } +export async function awaitScheduling(request: APIRequestContext, scheduling_specification_id: number): Promise { + const max_iter = 10; + for (let i = 0; i < max_iter; i++) { + const resp = await req.schedule(request, scheduling_specification_id); + const { reason, status } = resp; + + switch (status) { + case 'pending': + case 'incomplete': + await time.delay(1000); + break; + case 'complete': + return resp; + default: + throw Error(`Scheduling returned bad status: ${status} with reason ${reason}`); + } + } + + throw Error(`Scheduling timed out after ${max_iter} iterations`); +} + export default req;