diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/DiscreteProfile.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/DiscreteProfile.java index 57e0b0abb9..1de8d492c0 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/DiscreteProfile.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/DiscreteProfile.java @@ -144,33 +144,37 @@ public Optional valueAt(final Duration timepoint) { } public static DiscreteProfile fromSimulatedProfile(final List> simulatedProfile) { - return fromProfileHelper(Duration.ZERO, simulatedProfile, Optional::of); + return fromProfileHelper(Duration.ZERO, simulatedProfile, Optional::of, true); } public static DiscreteProfile fromExternalProfile(final Duration offsetFromPlanStart, final List>> externalProfile) { - return fromProfileHelper(offsetFromPlanStart, externalProfile, $ -> $); + return fromProfileHelper(offsetFromPlanStart, externalProfile, $ -> $, false); } private static DiscreteProfile fromProfileHelper( final Duration offsetFromPlanStart, final List> profile, - final Function> transform + final Function> transform, + final boolean close ) { final var result = new IntervalMap.Builder(); var cursor = offsetFromPlanStart; + var c = 0; for (final var pair: profile) { final var nextCursor = cursor.plus(pair.extent()); final var value = transform.apply(pair.dynamics()); final Duration finalCursor = cursor; + final var isLast = c == profile.size() - 1; value.ifPresent( $ -> result.set( - Interval.between(finalCursor, Inclusive, nextCursor, Exclusive), + Interval.between(finalCursor, Inclusive, nextCursor, (close && isLast) ? Inclusive : Exclusive), $ ) ); cursor = nextCursor; + c++; } return new DiscreteProfile(result.build()); diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/LinearProfile.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/LinearProfile.java index fb5b75e7d5..1bedf03ca2 100644 --- a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/LinearProfile.java +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/model/LinearProfile.java @@ -175,28 +175,30 @@ public LinearProfile shiftBy(final Duration duration) { } public static LinearProfile fromSimulatedProfile(final List> simulatedProfile) { - return fromProfileHelper(Duration.ZERO, simulatedProfile, Optional::of); + return fromProfileHelper(Duration.ZERO, simulatedProfile, Optional::of, true); } public static LinearProfile fromExternalProfile(final Duration offsetFromPlanStart, final List>> externalProfile) { - return fromProfileHelper(offsetFromPlanStart, externalProfile, $ -> $); + return fromProfileHelper(offsetFromPlanStart, externalProfile, $ -> $, false); } private static LinearProfile fromProfileHelper( final Duration offsetFromPlanStart, final List> profile, - final Function> transform + final Function> transform, + final boolean close ) { final var result = new IntervalMap.Builder(); var cursor = offsetFromPlanStart; + var c = 0; for (final var pair: profile) { final var nextCursor = cursor.plus(pair.extent()); - + final var isLast = c == profile.size() - 1; final var value = transform.apply(pair.dynamics()); final Duration finalCursor = cursor; value.ifPresent( $ -> result.set( - Interval.between(finalCursor, Inclusive, nextCursor, Exclusive), + Interval.between(finalCursor, Inclusive, nextCursor, (close && isLast) ? Inclusive :Exclusive), new LinearEquation( finalCursor, $.initial, @@ -206,6 +208,7 @@ private static LinearProfile fromProfileHelper( ); cursor = nextCursor; + c++; } return new LinearProfile(result.build()); diff --git a/deployment/Environment.md b/deployment/Environment.md index c00906e9d2..d8556be552 100644 --- a/deployment/Environment.md +++ b/deployment/Environment.md @@ -69,6 +69,7 @@ See the [environment variables document](https://github.com/NASA-AMMOS/aerie-gat | `SCHEDULER_DB_PASSWORD` | Password of the Scheduler DB User | `string` | | | `SCHEDULER_OUTPUT_MODE` | How scheduler output is sent back to Aerie | `string` | UpdateInputPlanWithNewActivities | | `SCHEDULER_RULES_JAR` | Jar file to load scheduling rules from (until user input to database) | `string` | /usr/src/app/merlin_file_store/scheduler_rules.jar | +| `MAX_NB_CACHED_SIMULATION_ENGINES` | The maximum number of simulation engines to cache in memory during a scheduling run. Must be at least 1 | `number` | 1 | ## Aerie Sequencing diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index bfeca65141..00a09583d7 100755 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -100,6 +100,7 @@ services: SCHEDULER_OUTPUT_MODE: UpdateInputPlanWithNewActivities MERLIN_LOCAL_STORE: /usr/src/app/merlin_file_store SCHEDULER_RULES_JAR: /usr/src/app/merlin_file_store/scheduler_rules.jar + MAX_NB_CACHED_SIMULATION_ENGINES: 1 JAVA_OPTS: > -Dorg.slf4j.simpleLogger.log.com.zaxxer.hikari=INFO -Dorg.slf4j.simpleLogger.logFile=System.err diff --git a/docker-compose.yml b/docker-compose.yml index a234d87666..a4e51528e0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -178,6 +178,7 @@ services: SCHEDULER_OUTPUT_MODE: UpdateInputPlanWithNewActivities MERLIN_LOCAL_STORE: /usr/src/app/merlin_file_store SCHEDULER_RULES_JAR: /usr/src/app/merlin_file_store/scheduler_rules.jar + MAX_NB_CACHED_SIMULATION_ENGINES: 1 JAVA_OPTS: > -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG @@ -204,6 +205,7 @@ services: SCHEDULER_OUTPUT_MODE: UpdateInputPlanWithNewActivities MERLIN_LOCAL_STORE: /usr/src/app/merlin_file_store SCHEDULER_RULES_JAR: /usr/src/app/merlin_file_store/scheduler_rules.jar + MAX_NB_CACHED_SIMULATION_ENGINES: 1 JAVA_OPTS: > -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -Dorg.slf4j.simpleLogger.defaultLogLevel=DEBUG diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java index 43f9f4486d..4b2b45de67 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/SchedulingTests.java @@ -6,6 +6,7 @@ import gov.nasa.jpl.aerie.e2e.types.Plan; import gov.nasa.jpl.aerie.e2e.types.ProfileSegment; import gov.nasa.jpl.aerie.e2e.types.SchedulingRequest.SchedulingStatus; +import gov.nasa.jpl.aerie.e2e.types.SimulationDataset; import gov.nasa.jpl.aerie.e2e.types.ValueSchema; import gov.nasa.jpl.aerie.e2e.utils.GatewayRequests; import gov.nasa.jpl.aerie.e2e.utils.HasuraRequests; @@ -381,22 +382,17 @@ void schedulingGoalPostsSimResults() throws IOException { final var plan = hasura.getPlan(planId); final var simResults = hasura.getSimulationDatasetByDatasetId(datasetId); - // All directive have their simulated activity - final var planActivities = plan.activityDirectives(); - final var simActivities = simResults.activities(); - assertEquals(4, planActivities.size()); - assertEquals(planActivities.size(), simActivities.size()); - for(int i = 0; i activities) { + List activities, + Integer datasetId) { public record SimulatedActivity( int spanId, Integer directiveId, @@ -58,7 +59,8 @@ public static SimulationDataset fromJSON(JsonObject json) { json.getBoolean("canceled"), json.getString("simulation_start_time"), json.getString("simulation_end_time"), - simActivities); + simActivities, + json.getInt("dataset_id")); } public enum SimulationStatus{ pending, incomplete, failed, success } diff --git a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java index 08079cc25b..ed7105974d 100644 --- a/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java +++ b/e2e-tests/src/test/java/gov/nasa/jpl/aerie/e2e/utils/GQL.java @@ -434,6 +434,7 @@ query GetSimulationDataset($id: Int!) { status reason canceled + dataset_id simulation_start_time simulation_end_time simulated_activities { @@ -453,6 +454,7 @@ query GetSimulationDataset($id: Int!) { status reason canceled + dataset_id simulation_start_time simulation_end_time simulated_activities { diff --git a/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/Mission.java b/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/Mission.java index 3cd7cdaae8..b56922bc22 100644 --- a/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/Mission.java +++ b/examples/foo-missionmodel/src/main/java/gov/nasa/jpl/aerie/foomissionmodel/Mission.java @@ -41,6 +41,8 @@ public final class Mission { public final TimeTrackerDaemon timeTrackerDaemon = new TimeTrackerDaemon(); + public final Counter counter = Counter.ofInteger(); + public Mission(final Registrar registrar, final Instant planStart, final Configuration config) { this.cachedRegistrar = registrar; @@ -74,13 +76,20 @@ public Mission(final Registrar registrar, final Instant planStart, final Configu registrar.real("/simple_data/b/rate", this.simpleData.b.rate); registrar.real("/simple_data/total_volume", this.simpleData.totalVolume); + registrar.discrete("/counter", this.counter, new IntegerValueMapper()); + spawn(timeTrackerDaemon::run); - spawn(() -> { // Register a never-ending daemon task - while (true) { - ModelActions.delay(Duration.SECOND); + spawn(replaying(new Runnable() { + @Override + public void run() { // Register a never-ending daemon task + for (int i = 0; i < 1000; i++) { + ModelActions.delay(Duration.SECOND); + } + counter.add(1); + spawn(replaying(this)); } - }); + })); if(config.raiseException) { spawn(() -> { diff --git a/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/FooSimulationDuplicationTest.java b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/FooSimulationDuplicationTest.java new file mode 100644 index 0000000000..7e0305b90b --- /dev/null +++ b/examples/foo-missionmodel/src/test/java/gov/nasa/jpl/aerie/foomissionmodel/FooSimulationDuplicationTest.java @@ -0,0 +1,423 @@ +package gov.nasa.jpl.aerie.foomissionmodel; + +import gov.nasa.jpl.aerie.foomissionmodel.generated.GeneratedModelType; +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; +import gov.nasa.jpl.aerie.merlin.driver.CachedEngineStore; +import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.DirectiveTypeRegistry; +import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelBuilder; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; +import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; +import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; +import gov.nasa.jpl.aerie.merlin.framework.ThreadedTask; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTE; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTES; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class FooSimulationDuplicationTest { + CachedEngineStore store; + final private class InfiniteCapacityEngineStore implements CachedEngineStore{ + private final Map> store = new HashMap<>(); + @Override + public void save( + final CheckpointSimulationDriver.CachedSimulationEngine cachedSimulationEngine, + final SimulationEngineConfiguration configuration) { + store.computeIfAbsent(configuration, conf -> new ArrayList<>()); + store.get(configuration).add(cachedSimulationEngine); + } + + @Override + public List getCachedEngines(final SimulationEngineConfiguration configuration) { + return store.get(configuration); + } + + @Override + public int capacity() { + return Integer.MAX_VALUE; + } + } + + public static SimulationEngineConfiguration mockConfiguration(){ + return new SimulationEngineConfiguration( + Map.of(), + Instant.EPOCH, + new MissionModelId(0) + ); + } + + @BeforeEach + void beforeEach(){ + this.store = new InfiniteCapacityEngineStore(); + } + + @BeforeAll + static void beforeAll() { + ThreadedTask.CACHE_READS = true; + } + + private static MissionModel makeMissionModel(final MissionModelBuilder builder, final Instant planStart, final Configuration config) { + final var factory = new GeneratedModelType(); + final var registry = DirectiveTypeRegistry.extract(factory); + final var model = factory.instantiate(planStart, config, builder); + return builder.build(model, registry); + } + + @Test + void testCompareCheckpointOnEmptyPlan() { + final MissionModel missionModel = makeMissionModel( + new MissionModelBuilder(), + Instant.EPOCH, + new Configuration()); + final var results = simulateWithCheckpoints( + missionModel, + List.of(Duration.of(5, MINUTES)), + Map.of(), + store, + mockConfiguration() + ); + final SimulationResults expected = SimulationDriver.simulate( + missionModel, + Map.of(), + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false, + $ -> {}); + assertResultsEqual(expected, results); + } + + @Test + void testFooNonEmptyPlan() { + final MissionModel missionModel = makeMissionModel( + new MissionModelBuilder(), + Instant.EPOCH, + new Configuration()); + final Map schedule = Map.ofEntries( + activityFrom(1, MINUTE, "foo", Map.of("z", SerializedValue.of(123))), + activityFrom(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) + ); + final var results = simulateWithCheckpoints( + missionModel, + List.of(Duration.of(5, MINUTES)), + schedule, + store, + mockConfiguration() + ); + final SimulationResults expected = SimulationDriver.simulate( + missionModel, + schedule, + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false, + $ -> {}); + assertResultsEqual(expected, results); + + assertEquals(Duration.of(5, MINUTES), store.getCachedEngines(mockConfiguration()).getFirst().endsAt()); + + final var results2 = simulateWithCheckpoints( + missionModel, + store.getCachedEngines(mockConfiguration()).get(0), + List.of(Duration.of(5, MINUTES)), + schedule, + store, + mockConfiguration() + ); + + assertResultsEqual(expected, results2); + } + + @Test + void testFooNonEmptyPlanMultipleResumes() { + final MissionModel missionModel = makeMissionModel( + new MissionModelBuilder(), + Instant.EPOCH, + new Configuration()); + final Map schedule = Map.ofEntries( + activityFrom(1, MINUTE, "foo", Map.of("z", SerializedValue.of(123))), + activityFrom(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) + ); + final var results = simulateWithCheckpoints( + missionModel, + List.of(Duration.of(5, MINUTES)), + schedule, + store, + mockConfiguration() + ); + final SimulationResults expected = SimulationDriver.simulate( + missionModel, + schedule, + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false, + $ -> {}); + assertResultsEqual(expected, results); + + assertEquals(Duration.of(5, MINUTES), store.getCachedEngines(mockConfiguration()).getFirst().endsAt()); + + final var results2 = simulateWithCheckpoints( + missionModel, + store.getCachedEngines(mockConfiguration()).getFirst(), + List.of(Duration.of(5, MINUTES)), + schedule, + store, + mockConfiguration() + ); + + assertResultsEqual(expected, results2); + + final var results3 = simulateWithCheckpoints( + missionModel, + store.getCachedEngines(mockConfiguration()).getFirst(), + List.of(Duration.of(5, MINUTES)), + schedule, + store, + mockConfiguration() + ); + + assertResultsEqual(expected, results3); + } + + @Test + void testFooNonEmptyPlanMultipleCheckpointsMultipleResumes() { + final MissionModel missionModel = makeMissionModel( + new MissionModelBuilder(), + Instant.EPOCH, + new Configuration()); + final Map schedule = Map.ofEntries( + activityFrom(1, MINUTE, "foo", Map.of("z", SerializedValue.of(123))), + activityFrom(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) + ); + final var results = simulateWithCheckpoints( + missionModel, + List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), + schedule, + store, + mockConfiguration() + ); + final SimulationResults expected = SimulationDriver.simulate( + missionModel, + schedule, + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false, + $ -> {}); + assertResultsEqual(expected, results); + + assertEquals(Duration.of(5, MINUTES), store.getCachedEngines(mockConfiguration()).getFirst().endsAt()); + + final var results2 = simulateWithCheckpoints( + missionModel, + store.getCachedEngines(mockConfiguration()).getFirst(), + List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), + schedule, + store, + mockConfiguration() + ); + + assertResultsEqual(expected, results2); + + final var results3 = simulateWithCheckpoints( + missionModel, + store.getCachedEngines(mockConfiguration()).get(1), + List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), + schedule, + store, + mockConfiguration() + ); + + assertResultsEqual(expected, results3); + } + + @Test + void testFooNonEmptyPlanMultipleCheckpointsMultipleResumesWithEdits() { + final MissionModel missionModel = makeMissionModel( + new MissionModelBuilder(), + Instant.EPOCH, + new Configuration()); + final Pair activity1 = activityFrom( + 1, + MINUTE, + "foo", + Map.of("z", SerializedValue.of(123))); + final Map schedule1 = Map.ofEntries( + activity1, + activityFrom(7, MINUTES, "foo", Map.of("z", SerializedValue.of(999))) + ); + final Map schedule2 = Map.ofEntries( + activity1, + activityFrom(390, SECONDS, "foo", Map.of("z", SerializedValue.of(999))) + ); + final var results = simulateWithCheckpoints( + missionModel, + List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), + schedule1, + store, + mockConfiguration() + ); + final SimulationResults expected1 = SimulationDriver.simulate( + missionModel, + schedule1, + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false, + $ -> {}); + + final SimulationResults expected2 = SimulationDriver.simulate( + missionModel, + schedule2, + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false, + $ -> {}); + + assertResultsEqual(expected1, results); + + assertEquals(Duration.of(5, MINUTES), store.getCachedEngines(mockConfiguration()).getFirst().endsAt()); + + final var results2 = simulateWithCheckpoints( + missionModel, + store.getCachedEngines(mockConfiguration()).getFirst(), + List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), + schedule2, + store, + mockConfiguration() + ); + assertResultsEqual(expected2, results2); + + final SimulationResults results3 = simulateWithCheckpoints( + missionModel, + store.getCachedEngines(mockConfiguration()).get(1), + List.of(Duration.of(5, MINUTES), Duration.of(6, MINUTES)), + schedule2, + store, + mockConfiguration() + ); + + assertResultsEqual(expected2, results3); + } + + private static long nextActivityDirectiveId = 0L; + + private static Pair activityFrom(final long quantity, final Duration unit, final String type, final Map args) { + return activityFrom(Duration.of(quantity, unit), type, args); + } + + private static Pair activityFrom(final Duration startOffset, final String type, final Map args) { + return Pair.of(new ActivityDirectiveId(nextActivityDirectiveId++), new ActivityDirective(startOffset, type, args, null, true)); + } + + + static void assertResultsEqual(SimulationResults expected, SimulationResults actual) { + if (expected.equals(actual)) return; + final var differences = new ArrayList(); + if (!expected.duration.isEqualTo(actual.duration)) { + differences.add("duration"); + } + if (!expected.realProfiles.equals(actual.realProfiles)) { + differences.add("realProfiles"); + } + if (!expected.discreteProfiles.equals(actual.discreteProfiles)) { + differences.add("discreteProfiles"); + } + if (!expected.simulatedActivities.equals(actual.simulatedActivities)) { + differences.add("simulatedActivities"); + } + if (!expected.unfinishedActivities.equals(actual.unfinishedActivities)) { + differences.add("unfinishedActivities"); + } + if (!expected.startTime.equals(actual.startTime)) { + differences.add("startTime"); + } + if (!expected.duration.isEqualTo(actual.duration)) { + differences.add("duration"); + } + if (!expected.topics.equals(actual.topics)) { + differences.add("topics"); + } + if (!expected.events.equals(actual.events)) { + differences.add("events"); + } + if (!differences.isEmpty()) { + System.out.println(); + } + System.out.println(differences); + assertEquals(expected, actual); + } + + static SimulationResults simulateWithCheckpoints( + final MissionModel missionModel, + final CheckpointSimulationDriver.CachedSimulationEngine cachedSimulationEngine, + final List desiredCheckpoints, + final Map schedule, + final CachedEngineStore cachedEngineStore, + final SimulationEngineConfiguration simulationEngineConfiguration + ) { + return CheckpointSimulationDriver.simulateWithCheckpoints( + missionModel, + schedule, + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + $ -> {}, + () -> false, + cachedSimulationEngine, + CheckpointSimulationDriver.desiredCheckpoints(desiredCheckpoints), + CheckpointSimulationDriver.noCondition(), + cachedEngineStore, + simulationEngineConfiguration + ).computeResults(); + } + + static SimulationResults simulateWithCheckpoints( + final MissionModel missionModel, + final List desiredCheckpoints, + final Map schedule, + final CachedEngineStore cachedEngineStore, + final SimulationEngineConfiguration simulationEngineConfiguration + ) { + return CheckpointSimulationDriver.simulateWithCheckpoints( + missionModel, + schedule, + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + $ -> {}, + () -> false, + CheckpointSimulationDriver.CachedSimulationEngine.empty(missionModel), + CheckpointSimulationDriver.desiredCheckpoints(desiredCheckpoints), + CheckpointSimulationDriver.noCondition(), + cachedEngineStore, + simulationEngineConfiguration + ).computeResults(); + } +} diff --git a/merlin-driver/build.gradle b/merlin-driver/build.gradle index 0703239dd3..c6ad4e9c7d 100644 --- a/merlin-driver/build.gradle +++ b/merlin-driver/build.gradle @@ -40,6 +40,7 @@ dependencies { api project(':merlin-sdk') api 'org.glassfish:javax.json:1.1.4' implementation 'it.unimi.dsi:fastutil:8.5.12' + implementation 'org.slf4j:slf4j-simple:2.0.7' testImplementation project(':merlin-framework') testImplementation project(':merlin-framework-junit') diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CachedEngineStore.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CachedEngineStore.java new file mode 100644 index 0000000000..c1a069d412 --- /dev/null +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CachedEngineStore.java @@ -0,0 +1,12 @@ +package gov.nasa.jpl.aerie.merlin.driver; + +import java.util.List; + +public interface CachedEngineStore { + void save(final CheckpointSimulationDriver.CachedSimulationEngine cachedSimulationEngine, + final SimulationEngineConfiguration configuration); + List getCachedEngines( + final SimulationEngineConfiguration configuration); + + int capacity(); +} diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java new file mode 100644 index 0000000000..3b0c263a8f --- /dev/null +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/CheckpointSimulationDriver.java @@ -0,0 +1,464 @@ +package gov.nasa.jpl.aerie.merlin.driver; + +import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; +import gov.nasa.jpl.aerie.merlin.driver.engine.SlabList; +import gov.nasa.jpl.aerie.merlin.driver.engine.SpanException; +import gov.nasa.jpl.aerie.merlin.driver.engine.SpanId; +import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; +import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; +import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MAX_VALUE; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECONDS; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.min; + +public class CheckpointSimulationDriver { + private static final Logger LOGGER = LoggerFactory.getLogger(CheckpointSimulationDriver.class); + + public record CachedSimulationEngine( + Duration endsAt, + Map activityDirectives, + SimulationEngine simulationEngine, + LiveCells cells, + SlabList timePoints, + Topic activityTopic, + MissionModel missionModel + ) { + public void freeze() { + cells.freeze(); + timePoints.freeze(); + simulationEngine.close(); + } + + public static CachedSimulationEngine empty(final MissionModel missionModel) { + final SimulationEngine engine = new SimulationEngine(); + final TemporalEventSource timeline = new TemporalEventSource(); + final LiveCells cells = new LiveCells(timeline, missionModel.getInitialCells()); + + // Begin tracking all resources. + for (final var entry : missionModel.getResources().entrySet()) { + final var name = entry.getKey(); + final var resource = entry.getValue(); + + engine.trackResource(name, resource, Duration.ZERO); + } + + { + // Start daemon task(s) immediately, before anything else happens. + engine.scheduleTask(Duration.ZERO, missionModel.getDaemon()); + { + final var batch = engine.extractNextJobs(Duration.MAX_VALUE); + final var commit = engine.performJobs(batch.jobs(), cells, Duration.ZERO, Duration.MAX_VALUE); + timeline.add(commit.getKey()); + } + } + return new CachedSimulationEngine( + Duration.MIN_VALUE, + Map.of(), + engine, + cells, + timeline.points(), + new Topic<>(), + missionModel + ); + } + } + + /** + * Selects the best cached engine for simulating a given plan. + * @param schedule the schedule/plan + * @param cachedEngines a list of cached engines + * @return the best cached engine as well as the map of corresponding activity ids for this engine + */ + public static Optional>> bestCachedEngine( + final Map schedule, + final List cachedEngines, + final Duration planDuration) { + Optional bestCandidate = Optional.empty(); + final Map correspondenceMap = new HashMap<>(); + final var minimumStartTimes = getMinimumStartTimes(schedule, planDuration); + for (final var cachedEngine : cachedEngines) { + if (bestCandidate.isPresent() && cachedEngine.endsAt().noLongerThan(bestCandidate.get().endsAt())) + continue; + + final var activityDirectivesInCache = new HashMap<>(cachedEngine.activityDirectives()); + // Find the invalidation time + var invalidationTime = Duration.MAX_VALUE; + final var scheduledActivities = new HashMap<>(schedule); + for (final var activity : scheduledActivities.entrySet()) { + final var entryToRemove = activityDirectivesInCache.entrySet() + .stream() + .filter(e -> e.getValue().equals(activity.getValue())) + .findFirst(); + if(entryToRemove.isPresent()) { + final var entry = entryToRemove.get(); + activityDirectivesInCache.remove(entry.getKey()); + correspondenceMap.put(activity.getKey(), entry.getKey()); + } else { + invalidationTime = min(invalidationTime, minimumStartTimes.get(activity.getKey())); + } + } + final var allActs = new HashMap(); + allActs.putAll(cachedEngine.activityDirectives()); + allActs.putAll(scheduledActivities); + final var minimumStartTimeOfActsInCache = getMinimumStartTimes(allActs, planDuration); + for (final var activity : activityDirectivesInCache.entrySet()) { + invalidationTime = min(invalidationTime, minimumStartTimeOfActsInCache.get(activity.getKey())); + } + // (1) cachedEngine ends strictly after bestCandidate as per first line of this loop + // and they both end before the invalidation time: (2) the bestCandidate has already passed its invalidation time + // test below (3) cacheEngine is before its invalidation time too per the test below. + // (1) + (3) -> cachedEngine is strictly better than bestCandidate + if (cachedEngine.endsAt().shorterThan(invalidationTime)) { + bestCandidate = Optional.of(cachedEngine); + } + } + + bestCandidate.ifPresent(cachedSimulationEngine -> LOGGER.info("Re-using simulation engine at " + + cachedSimulationEngine.endsAt())); + return bestCandidate.map(cachedSimulationEngine -> Pair.of(cachedSimulationEngine, correspondenceMap)); + } + + private static TemporalEventSource makeCombinedTimeline(List timelines, TemporalEventSource timeline) { + final TemporalEventSource combinedTimeline = new TemporalEventSource(); + for (final var entry : timelines) { + for (final var timePoint : entry.points()) { + if (timePoint instanceof TemporalEventSource.TimePoint.Delta t) { + combinedTimeline.add(t.delta()); + } else if (timePoint instanceof TemporalEventSource.TimePoint.Commit t) { + combinedTimeline.add(t.events()); + } + } + } + + for (final var timePoint : timeline) { + if (timePoint instanceof TemporalEventSource.TimePoint.Delta t) { + combinedTimeline.add(t.delta()); + } else if (timePoint instanceof TemporalEventSource.TimePoint.Commit t) { + combinedTimeline.add(t.events()); + } + } + return combinedTimeline; + } + + public static Function desiredCheckpoints(final List desiredCheckpoints) { + return simulationState -> { + for (final var desiredCheckpoint : desiredCheckpoints) { + if (simulationState.currentTime().noLongerThan(desiredCheckpoint) && simulationState.nextTime().longerThan(desiredCheckpoint)) { + return true; + } + } + return false; + }; + } + + public static Function checkpointAtEnd(Function stoppingCondition) { + return simulationState -> stoppingCondition.apply(simulationState) || simulationState.nextTime.isEqualTo(MAX_VALUE); + } + + private static Map getMinimumStartTimes(final Map schedule, final Duration planDuration){ + //For an anchored activity, it's minimum invalidationTime would be the sum of all startOffsets in its anchor chain + // (plus or minus the plan duration depending on whether the root is anchored to plan start or plan end). + // If it's a start anchor chain (as in, all anchors have anchoredToStart set to true), + // this will give you its exact start time, but if there are any end-time anchors, this will give you the minimum time the activity could start at. + final var minimumStartTimes = new HashMap(); + for(final var activity : schedule.entrySet()){ + var curInChain = activity; + var curSum = ZERO; + while(true){ + if(curInChain.getValue().anchorId() == null){ + curSum = curSum.plus(curInChain.getValue().startOffset()); + curSum = !curInChain.getValue().anchoredToStart() ? curSum.plus(planDuration) : curSum; + minimumStartTimes.put(activity.getKey(), curSum); + break; + } else{ + curSum = curSum.plus(curInChain.getValue().startOffset()); + curInChain = Map.entry(curInChain.getValue().anchorId(), schedule.get(curInChain.getValue().anchorId())); + } + } + } + return minimumStartTimes; + } + + public record SimulationState( + Duration currentTime, + Duration nextTime, + SimulationEngine simulationEngine, + Map schedule, + Map activityDirectiveIdSpanIdMap + ){} + + /** + * Simulates a plan/schedule while using and creating simulation checkpoints. + * @param missionModel the mission model + * @param schedule the plan/schedule + * @param simulationStartTime the start time of the simulation + * @param simulationDuration the simulation duration + * @param planStartTime the plan overall start time + * @param planDuration the plan overall duration + * @param simulationExtentConsumer consumer to report simulation progress + * @param simulationCanceled provider of an external stop signal + * @param cachedEngine the simulation engine that is going to be used + * @param shouldTakeCheckpoint a function from state of the simulation to boolean deciding when to take checkpoints + * @param stopConditionOnPlan a function from state of the simulation to boolean deciding when to stop simulation + * @param cachedEngineStore a store for simulation engine checkpoints taken. If capacity is 1, the simulation will behave like a resumable simulation. + * @param configuration the simulation configuration + * @return all the information to compute simulation results if needed + */ + public static SimulationResultsComputerInputs simulateWithCheckpoints( + final MissionModel missionModel, + final Map schedule, + final Instant simulationStartTime, + final Duration simulationDuration, + final Instant planStartTime, + final Duration planDuration, + final Consumer simulationExtentConsumer, + final Supplier simulationCanceled, + final CachedSimulationEngine cachedEngine, + final Function shouldTakeCheckpoint, + final Function stopConditionOnPlan, + final CachedEngineStore cachedEngineStore, + final SimulationEngineConfiguration configuration) + { + final boolean duplicationIsOk = cachedEngineStore.capacity() > 1; + final var activityToSpan = new HashMap(); + final var activityTopic = cachedEngine.activityTopic(); + final var timelines = new ArrayList(); + timelines.add(new TemporalEventSource(cachedEngine.timePoints)); + var engine = !duplicationIsOk ? cachedEngine.simulationEngine : cachedEngine.simulationEngine.duplicate(); + engine.unscheduleAfter(cachedEngine.endsAt); + + var timeline = new TemporalEventSource(); + var cells = new LiveCells(timeline, cachedEngine.cells()); + /* The current real time. */ + var elapsedTime = Duration.max(ZERO, cachedEngine.endsAt()); + + simulationExtentConsumer.accept(elapsedTime); + + try { + // Get all activities as close as possible to absolute time + // Schedule all activities. + // Using HashMap explicitly because it allows `null` as a key. + // `null` key means that an activity is not waiting on another activity to finish to know its start time + HashMap>> resolved = new StartOffsetReducer(planDuration, schedule).compute(); + if(!resolved.isEmpty()) { + resolved.put( + null, + StartOffsetReducer.adjustStartOffset( + resolved.get(null), + Duration.of( + planStartTime.until(simulationStartTime, ChronoUnit.MICROS), + Duration.MICROSECONDS))); + } + // Filter out activities that are before simulationStartTime + resolved = StartOffsetReducer.filterOutStartOffsetBefore(resolved, Duration.max(ZERO, cachedEngine.endsAt().plus(MICROSECONDS))); + final var toSchedule = new LinkedHashSet(); + toSchedule.add(null); + final HashMap>> finalResolved = resolved; + final var activitiesToBeScheduledNow = new HashMap(); + if(finalResolved.get(null) != null) { + for (final var r : finalResolved.get(null)) { + activitiesToBeScheduledNow.put(r.getKey(), schedule.get(r.getKey())); + } + } + var toCheckForDependencyScheduling = scheduleActivities( + toSchedule, + activitiesToBeScheduledNow, + resolved, + missionModel, + engine, + elapsedTime, + activityToSpan, + activityTopic); + + // Drive the engine until we're out of time. + // TERMINATION: Actually, we might never break if real time never progresses forward. + while (elapsedTime.noLongerThan(simulationDuration) && !simulationCanceled.get()) { + final var nextTime = engine.peekNextTime().orElse(Duration.MAX_VALUE); + if (duplicationIsOk && shouldTakeCheckpoint.apply(new SimulationState(elapsedTime, nextTime, engine, schedule, activityToSpan))) { + cells.freeze(); + LOGGER.info("Saving a simulation engine in memory at time " + elapsedTime + " (next time: " + nextTime + ")"); + final var newCachedEngine = new CachedSimulationEngine( + elapsedTime, + schedule, + engine, + cells, + makeCombinedTimeline(timelines, timeline).points(), + activityTopic, + missionModel); + newCachedEngine.freeze(); + cachedEngineStore.save( + newCachedEngine, + configuration); + timelines.add(timeline); + engine = engine.duplicate(); + timeline = new TemporalEventSource(); + cells = new LiveCells(timeline, cells); + } + //break before changing the state of the engine + if (simulationCanceled.get() || stopConditionOnPlan.apply(new SimulationState(elapsedTime, nextTime, engine, schedule, activityToSpan))) { + if(!duplicationIsOk){ + final var newCachedEngine = new CachedSimulationEngine( + elapsedTime, + schedule, + engine, + cells, + makeCombinedTimeline(timelines, timeline).points(), + activityTopic, + missionModel); + cachedEngineStore.save( + newCachedEngine, + configuration); + timelines.add(timeline); + } + break; + } + + final var batch = engine.extractNextJobs(simulationDuration); + // Increment real time, if necessary. + final var delta = batch.offsetFromStart().minus(elapsedTime); + elapsedTime = batch.offsetFromStart(); + timeline.add(delta); + // TODO: Advance a dense time counter so that future tasks are strictly ordered relative to these, + // even if they occur at the same real time. + + simulationExtentConsumer.accept(elapsedTime); + + //this break depends on the state of the batch: this is the soonest we can exit for that reason + if (batch.jobs().isEmpty() && (batch.offsetFromStart().isEqualTo(simulationDuration))) { + break; + } + + // Run the jobs in this batch. + final var commit = engine.performJobs(batch.jobs(), cells, elapsedTime, simulationDuration); + timeline.add(commit.getLeft()); + if (commit.getRight().isPresent()) { + throw commit.getRight().get(); + } + + toCheckForDependencyScheduling.putAll(scheduleActivities( + getSuccessorsToSchedule(engine, toCheckForDependencyScheduling), + schedule, + resolved, + missionModel, + engine, + elapsedTime, + activityToSpan, + activityTopic)); + } + } catch (SpanException ex) { + // Swallowing the spanException as the internal `spanId` is not user meaningful info. + final var topics = missionModel.getTopics(); + final var directiveId = SimulationEngine.getDirectiveIdFromSpan(engine, activityTopic, timeline, topics, ex.spanId); + if(directiveId.isPresent()) { + throw new SimulationException(elapsedTime, simulationStartTime, directiveId.get(), ex.cause); + } + throw new SimulationException(elapsedTime, simulationStartTime, ex.cause); + } catch (Throwable ex) { + throw new SimulationException(elapsedTime, simulationStartTime, ex); + } + return new SimulationResultsComputerInputs( + engine, + simulationStartTime, + elapsedTime, + activityTopic, + makeCombinedTimeline(timelines, timeline), + missionModel.getTopics(), + activityToSpan); + } + + + private static Set getSuccessorsToSchedule( + final SimulationEngine engine, + final Map toCheckForDependencyScheduling) { + final var toSchedule = new LinkedHashSet(); + final var iterator = toCheckForDependencyScheduling.entrySet().iterator(); + while(iterator.hasNext()){ + final var taskToCheck = iterator.next(); + if(engine.spanIsComplete(taskToCheck.getValue())){ + toSchedule.add(taskToCheck.getKey()); + iterator.remove(); + } + } + return toSchedule; + } + + private static Map scheduleActivities( + final Set toScheduleNow, + final Map completeSchedule, + final HashMap>> resolved, + final MissionModel missionModel, + final SimulationEngine engine, + final Duration curTime, + final Map activityToTask, + final Topic activityTopic){ + final var toCheckForDependencyScheduling = new HashMap(); + for(final var predecessor: toScheduleNow) { + if(!resolved.containsKey(predecessor)) continue; + for (final var directivePair : resolved.get(predecessor)) { + final var offset = directivePair.getRight(); + final var directiveIdToSchedule = directivePair.getLeft(); + final var serializedDirective = completeSchedule.get(directiveIdToSchedule).serializedActivity(); + final TaskFactory task; + try { + task = missionModel.getTaskFactory(serializedDirective); + } catch (final InstantiationException ex) { + // All activity instantiations are assumed to be validated by this point + throw new Error("Unexpected state: activity instantiation %s failed with: %s" + .formatted(serializedDirective.getTypeName(), ex.toString())); + } + Duration computedStartTime = offset; + if (predecessor != null) { + computedStartTime = (curTime.isEqualTo(Duration.MIN_VALUE) ? Duration.ZERO : curTime).plus(offset); + } + final var taskId = engine.scheduleTask( + computedStartTime, + executor -> + Task.run(scheduler -> scheduler.emit(directiveIdToSchedule, activityTopic)) + .andThen(task.create(executor))); + activityToTask.put(directiveIdToSchedule, taskId); + if (resolved.containsKey(directiveIdToSchedule)) { + toCheckForDependencyScheduling.put(directiveIdToSchedule, taskId); + } + } + } + return toCheckForDependencyScheduling; + } + + public static Function onceAllActivitiesAreFinished(){ + return simulationState -> simulationState.activityDirectiveIdSpanIdMap() + .values() + .stream() + .allMatch(simulationState.simulationEngine()::spanIsComplete); + } + + public static Function noCondition(){ + return simulationState -> false; + } + + public static Function stopOnceActivityHasFinished(final ActivityDirectiveId activityDirectiveId){ + return simulationState -> (simulationState.activityDirectiveIdSpanIdMap().containsKey(activityDirectiveId) + && simulationState.simulationEngine.spanIsComplete(simulationState.activityDirectiveIdSpanIdMap().get(activityDirectiveId))); + } +} diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModel.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModel.java index 03ae26c4ee..fc9b477e34 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModel.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModel.java @@ -1,9 +1,11 @@ package gov.nasa.jpl.aerie.merlin.driver; import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; import gov.nasa.jpl.aerie.merlin.protocol.model.Resource; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; @@ -14,6 +16,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.Executor; public final class MissionModel { private final Model model; @@ -55,9 +58,17 @@ public TaskFactory getTaskFactory(final SerializedActivity specification) thr } public TaskFactory getDaemon() { - return executor -> scheduler -> { - MissionModel.this.daemons.forEach($ -> scheduler.spawn(InSpan.Fresh, $)); - return TaskStatus.completed(Unit.UNIT); + return executor -> new Task<>() { + @Override + public TaskStatus step(final Scheduler scheduler) { + MissionModel.this.daemons.forEach($ -> scheduler.spawn(InSpan.Fresh, $)); + return TaskStatus.completed(Unit.UNIT); + } + + @Override + public Task duplicate(final Executor executor) { + return this; + } }; } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModelId.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModelId.java new file mode 100644 index 0000000000..5f3d95bd8c --- /dev/null +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/MissionModelId.java @@ -0,0 +1,3 @@ +package gov.nasa.jpl.aerie.merlin.driver; + +public record MissionModelId(long id) {} diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/OneStepTask.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/OneStepTask.java new file mode 100644 index 0000000000..5e0f8087e2 --- /dev/null +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/OneStepTask.java @@ -0,0 +1,20 @@ +package gov.nasa.jpl.aerie.merlin.driver; + +import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; +import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; + +import java.util.concurrent.Executor; +import java.util.function.Function; + +public record OneStepTask(Function> f) implements Task { + @Override + public TaskStatus step(final Scheduler scheduler) { + return f.apply(scheduler); + } + + @Override + public Task duplicate(Executor executor) { + return this; + } +} diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDriver.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDriver.java index bf0a8cbe3c..900e7da930 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDriver.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDriver.java @@ -5,16 +5,16 @@ import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; -import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; import org.apache.commons.lang3.tuple.Pair; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -209,30 +209,23 @@ void simulateTask(final MissionModel missionModel, final TaskFactory void scheduleActivities( final Map schedule, final HashMap>> resolved, final MissionModel missionModel, final SimulationEngine engine, final Topic activityTopic - ) - { - if(resolved.get(null) == null) { return; } // Nothing to simulate - + ) { + if (resolved.get(null) == null) { + // Nothing to simulate + return; + } for (final Pair directivePair : resolved.get(null)) { final var directiveId = directivePair.getLeft(); final var startOffset = directivePair.getRight(); final var serializedDirective = schedule.get(directiveId).serializedActivity(); - final TaskFactory task; - try { - task = missionModel.getTaskFactory(serializedDirective); - } catch (final InstantiationException ex) { - // All activity instantiations are assumed to be validated by this point - throw new Error("Unexpected state: activity instantiation %s failed with: %s" - .formatted(serializedDirective.getTypeName(), ex.toString())); - } + final TaskFactory task = deserializeActivity(missionModel, serializedDirective); engine.scheduleTask(startOffset, makeTaskFactory( directiveId, @@ -247,52 +240,54 @@ private static void scheduleActivities( private static TaskFactory makeTaskFactory( final ActivityDirectiveId directiveId, - final TaskFactory task, + final TaskFactory taskFactory, final Map schedule, final HashMap>> resolved, final MissionModel missionModel, final Topic activityTopic - ) - { - // Emit the current activity (defined by directiveId) - return executor -> scheduler0 -> TaskStatus.calling(InSpan.Fresh, (TaskFactory) (executor1 -> scheduler1 -> { - scheduler1.emit(directiveId, activityTopic); - return task.create(executor1).step(scheduler1); - }), scheduler2 -> { - // When the current activity finishes, get the list of the activities that needed this activity to finish to know their start time - final List> dependents = resolved.get(directiveId) == null ? List.of() : resolved.get(directiveId); - // Iterate over the dependents - for (final var dependent : dependents) { - scheduler2.spawn(InSpan.Parent, executor2 -> scheduler3 -> - // Delay until the dependent starts - TaskStatus.delayed(dependent.getRight(), scheduler4 -> { - final var dependentDirectiveId = dependent.getLeft(); - final var serializedDependentDirective = schedule.get(dependentDirectiveId).serializedActivity(); - - // Initialize the Task for the dependent - final TaskFactory dependantTask; - try { - dependantTask = missionModel.getTaskFactory(serializedDependentDirective); - } catch (final InstantiationException ex) { - // All activity instantiations are assumed to be validated by this point - throw new Error("Unexpected state: activity instantiation %s failed with: %s" - .formatted(serializedDependentDirective.getTypeName(), ex.toString())); - } - - // Schedule the dependent - // When it finishes, it will schedule the activities depending on it to know their start time - scheduler4.spawn(InSpan.Parent, makeTaskFactory( - dependentDirectiveId, - dependantTask, - schedule, - resolved, - missionModel, - activityTopic - )); - return TaskStatus.completed(Unit.UNIT); - })); - } - return TaskStatus.completed(Unit.UNIT); - }); + ) { + record Dependent(Duration offset, TaskFactory task) {} + + final List dependents = new ArrayList<>(); + for (final var pair : resolved.getOrDefault(directiveId, List.of())) { + dependents.add(new Dependent( + pair.getRight(), + makeTaskFactory( + pair.getLeft(), + deserializeActivity(missionModel, schedule.get(pair.getLeft()).serializedActivity()), + schedule, + resolved, + missionModel, + activityTopic))); + } + + return executor -> { + final var task = taskFactory.create(executor); + return Task + .callingWithSpan( + Task.emitting(directiveId, activityTopic) + .andThen(task)) + .andThen( + Task.spawning( + dependents + .stream() + .map( + dependent -> + TaskFactory.delaying(dependent.offset()) + .andThen(dependent.task())) + .toList())); + }; + } + + private static TaskFactory deserializeActivity(MissionModel missionModel, SerializedActivity serializedDirective) { + final TaskFactory task; + try { + task = missionModel.getTaskFactory(serializedDirective); + } catch (final InstantiationException ex) { + // All activity instantiations are assumed to be validated by this point + throw new Error("Unexpected state: activity instantiation %s failed with: %s" + .formatted(serializedDirective.getTypeName(), ex.toString())); + } + return task; } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationEngineConfiguration.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationEngineConfiguration.java new file mode 100644 index 0000000000..3f1f4e4294 --- /dev/null +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationEngineConfiguration.java @@ -0,0 +1,12 @@ +package gov.nasa.jpl.aerie.merlin.driver; + +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; + +import java.time.Instant; +import java.util.Map; + +public record SimulationEngineConfiguration( + Map simulationConfiguration, + Instant simStartTime, + MissionModelId missionModelId +) {} diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResults.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResults.java index bbe2bcdd0e..0c5fc342bc 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResults.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResults.java @@ -55,4 +55,32 @@ public String toString() { + ", unfinishedActivities=" + this.unfinishedActivities + " }"; } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (!(o instanceof SimulationResults that)) return false; + + return startTime.equals(that.startTime) + && duration.isEqualTo(that.duration) + && realProfiles.equals(that.realProfiles) + && discreteProfiles.equals(that.discreteProfiles) + && simulatedActivities.equals(that.simulatedActivities) + && unfinishedActivities.equals(that.unfinishedActivities) + && topics.equals(that.topics) + && events.equals(that.events); + } + + @Override + public int hashCode() { + int result = startTime.hashCode(); + result = 31 * result + duration.hashCode(); + result = 31 * result + realProfiles.hashCode(); + result = 31 * result + discreteProfiles.hashCode(); + result = 31 * result + simulatedActivities.hashCode(); + result = 31 * result + unfinishedActivities.hashCode(); + result = 31 * result + topics.hashCode(); + result = 31 * result + events.hashCode(); + return result; + } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResultsComputerInputs.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResultsComputerInputs.java new file mode 100644 index 0000000000..8a152a2cea --- /dev/null +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/SimulationResultsComputerInputs.java @@ -0,0 +1,54 @@ +package gov.nasa.jpl.aerie.merlin.driver; + +import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; +import gov.nasa.jpl.aerie.merlin.driver.engine.SpanId; +import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.time.Instant; +import java.util.Map; +import java.util.Set; + +public record SimulationResultsComputerInputs( + SimulationEngine engine, + Instant simulationStartTime, + Duration elapsedTime, + Topic activityTopic, + TemporalEventSource timeline, + Iterable> serializableTopics, + Map activityDirectiveIdTaskIdMap){ + + public SimulationResults computeResults(final Set resourceNames){ + return SimulationEngine.computeResults( + this.engine(), + this.simulationStartTime(), + this.elapsedTime(), + this.activityTopic(), + this.timeline(), + this.serializableTopics(), + resourceNames + ); + } + + public SimulationResults computeResults(){ + return SimulationEngine.computeResults( + this.engine(), + this.simulationStartTime(), + this.elapsedTime(), + this.activityTopic(), + this.timeline(), + this.serializableTopics() + ); + } + + public SimulationEngine.SimulationActivityExtract computeActivitySimulationResults(){ + return SimulationEngine.computeActivitySimulationResults( + this.engine(), + this.simulationStartTime(), + this.elapsedTime(), + this.activityTopic(), + this.timeline(), + this.serializableTopics()); + } +} diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/StartOffsetReducer.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/StartOffsetReducer.java index d0a4f76618..3611545ea9 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/StartOffsetReducer.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/StartOffsetReducer.java @@ -137,6 +137,13 @@ public static List> adjustStartOffset(List

>> filterOutNegativeStartOffset(HashMap>> toFilter) { + return filterOutStartOffsetBefore(toFilter, Duration.ZERO); + } + + public static HashMap>> filterOutStartOffsetBefore( + final HashMap>> toFilter, + final Duration duration) + { if(toFilter == null) return null; // Create a deep copy of toFilter (The Pairs are immutable, so they do not need to be copied) @@ -155,16 +162,16 @@ public static HashMap(toFilter .get(null) .stream() - .filter(pair -> pair.getValue().isNegative()) + .filter(pair -> pair.getValue().shorterThan(duration)) .toList()); while(!beforeStartTime.isEmpty()){ - final Pair currentPair = beforeStartTime.remove(beforeStartTime.size() - 1); + final Pair currentPair = beforeStartTime.removeLast(); if(filtered.containsKey(currentPair.getLeft())) { beforeStartTime.addAll(filtered.get(currentPair.getLeft())); filtered.remove(currentPair.getLeft()); } } - filtered.get(null).removeIf(pair -> pair.getValue().isNegative()); + filtered.get(null).removeIf(pair -> pair.getValue().shorterThan(duration)); return filtered; } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java index 4f7f7bda02..a1f2dd061b 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/JobSchedule.java @@ -6,6 +6,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentSkipListMap; @@ -57,5 +58,19 @@ public void clear() { this.queue.clear(); } + public Optional peekNextTime() { + if(this.queue.isEmpty()) return Optional.empty(); + return Optional.ofNullable(this.queue.firstKey()).map(SchedulingInstant::offsetFromStart); + } + public record Batch(Duration offsetFromStart, Set jobs) {} + + public JobSchedule duplicate() { + final JobSchedule jobSchedule = new JobSchedule<>(); + for (final var entry : this.queue.entrySet()) { + jobSchedule.queue.put(entry.getKey(), new HashSet<>(entry.getValue())); + } + jobSchedule.scheduledJobs.putAll(this.scheduledJobs); + return jobSchedule; + } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/Profile.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/Profile.java index f35a8c87e1..df7ea415b3 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/Profile.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/Profile.java @@ -20,4 +20,8 @@ public void append(final Duration currentTime, final Dynamics dynamics) { public Iterator> iterator() { return this.segments.iterator(); } + + public Profile duplicate() { + return new Profile<>(segments.duplicate()); + } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/ProfilingState.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/ProfilingState.java index 74709fef5e..d34294e329 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/ProfilingState.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/ProfilingState.java @@ -14,4 +14,8 @@ ProfilingState create(final Resource resource) { public void append(final Duration currentTime, final Querier querier) { this.profile.append(currentTime, this.resource.getDynamics(querier)); } + + public ProfilingState duplicate() { + return new ProfilingState<>(resource, profile.duplicate()); + } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java index 8bd15a193e..67488e7782 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SimulationEngine.java @@ -39,6 +39,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -46,42 +47,100 @@ import java.util.Optional; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; +import java.util.stream.Collectors; /** * A representation of the work remaining to do during a simulation, and its accumulated results. */ public final class SimulationEngine implements AutoCloseable { + private static int numActiveSimulationEngines = 0; + private boolean closed = false; + + public static int getNumActiveSimulationEngines() { + return numActiveSimulationEngines; + } + /** The set of all jobs waiting for time to pass. */ - private final JobSchedule scheduledJobs = new JobSchedule<>(); + private final JobSchedule scheduledJobs; /** The set of all jobs waiting on a condition. */ - private final Map waitingTasks = new HashMap<>(); + private final Map waitingTasks; /** The set of all tasks blocked on some number of subtasks. */ - private final Map blockedTasks = new HashMap<>(); + private final Map blockedTasks; /** The set of conditions depending on a given set of topics. */ - private final Subscriptions, ConditionId> waitingConditions = new Subscriptions<>(); + private final Subscriptions, ConditionId> waitingConditions; /** The set of queries depending on a given set of topics. */ - private final Subscriptions, ResourceId> waitingResources = new Subscriptions<>(); + private final Subscriptions, ResourceId> waitingResources; /** The execution state for every task. */ - private final Map> tasks = new HashMap<>(); + private final Map> tasks; /** The getter for each tracked condition. */ - private final Map conditions = new HashMap<>(); + private final Map conditions; /** The profiling state for each tracked resource. */ - private final Map> resources = new HashMap<>(); + private final Map> resources; + + /** Tasks that have been scheduled, but not started */ + private final Map unstartedTasks; /** The set of all spans of work contributed to by modeled tasks. */ - private final Map spans = new HashMap<>(); + private final Map spans; /** A count of the direct contributors to each span, including child spans and tasks. */ - private final Map spanContributorCount = new HashMap<>(); + private final Map spanContributorCount; /** A thread pool that modeled tasks can use to keep track of their state between steps. */ - private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + private final ExecutorService executor; + + public SimulationEngine() { + numActiveSimulationEngines++; + scheduledJobs = new JobSchedule<>(); + waitingTasks = new LinkedHashMap<>(); + blockedTasks = new LinkedHashMap<>(); + waitingConditions = new Subscriptions<>(); + waitingResources = new Subscriptions<>(); + tasks = new LinkedHashMap<>(); + conditions = new LinkedHashMap<>(); + resources = new LinkedHashMap<>(); + unstartedTasks = new LinkedHashMap<>(); + spans = new LinkedHashMap<>(); + spanContributorCount = new LinkedHashMap<>(); + executor = Executors.newVirtualThreadPerTaskExecutor(); + } + + private SimulationEngine(SimulationEngine other) { + numActiveSimulationEngines++; + // New Executor allows other SimulationEngine to be closed + executor = Executors.newVirtualThreadPerTaskExecutor(); + scheduledJobs = other.scheduledJobs.duplicate(); + waitingTasks = new LinkedHashMap<>(other.waitingTasks); + blockedTasks = new LinkedHashMap<>(); + for (final var entry : other.blockedTasks.entrySet()) { + blockedTasks.put(entry.getKey(), new MutableInt(entry.getValue())); + } + waitingConditions = other.waitingConditions.duplicate(); + waitingResources = other.waitingResources.duplicate(); + tasks = new LinkedHashMap<>(); + for (final var entry : other.tasks.entrySet()) { + tasks.put(entry.getKey(), entry.getValue().duplicate(executor)); + } + conditions = new LinkedHashMap<>(other.conditions); + resources = new LinkedHashMap<>(); + for (final var entry : other.resources.entrySet()) { + resources.put(entry.getKey(), entry.getValue().duplicate()); + } + unstartedTasks = new LinkedHashMap<>(other.unstartedTasks); + spans = new LinkedHashMap<>(other.spans); + spanContributorCount = new LinkedHashMap<>(); + for (final var entry : other.spanContributorCount.entrySet()) { + spanContributorCount.put(entry.getKey(), new MutableInt(entry.getValue().getValue())); + } + } /** Schedule a new task to be performed at the given time. */ public SpanId scheduleTask(final Duration startTime, final TaskFactory state) { + if (this.closed) throw new IllegalStateException("Cannot schedule task on closed simulation engine"); if (startTime.isNegative()) throw new IllegalArgumentException("Cannot schedule a task before the start time of the simulation"); final var span = SpanId.generate(); @@ -92,12 +151,15 @@ public SpanId scheduleTask(final Duration startTime, final TaskFactory< this.tasks.put(task, new ExecutionState<>(span, Optional.empty(), state.create(this.executor))); this.scheduledJobs.schedule(JobId.forTask(task), SubInstant.Tasks.at(startTime)); + this.unstartedTasks.put(task, startTime); + return span; } /** Register a resource whose profile should be accumulated over time. */ public void trackResource(final String name, final Resource resource, final Duration nextQueryTime) { + if (this.closed) throw new IllegalStateException("Cannot track resource on closed simulation engine"); final var id = new ResourceId(name); this.resources.put(id, ProfilingState.create(resource)); @@ -106,6 +168,7 @@ void trackResource(final String name, final Resource resource, final D /** Schedules any conditions or resources dependent on the given topic to be re-checked at the given time. */ public void invalidateTopic(final Topic topic, final Duration invalidationTime) { + if (this.closed) throw new IllegalStateException("Cannot invalidate topic on closed simulation engine"); final var resources = this.waitingResources.invalidateTopic(topic); for (final var resource : resources) { this.scheduledJobs.schedule(JobId.forResource(resource), SubInstant.Resources.at(invalidationTime)); @@ -122,6 +185,7 @@ public void invalidateTopic(final Topic topic, final Duration invalidationTim /** Removes and returns the next set of jobs to be performed concurrently. */ public JobSchedule.Batch extractNextJobs(final Duration maximumTime) { + if (this.closed) throw new IllegalStateException("Cannot extract next jobs on closed simulation engine"); final var batch = this.scheduledJobs.extractNextJobs(maximumTime); // If we're signaling based on a condition, we need to untrack the condition before any tasks run. @@ -145,6 +209,7 @@ public Pair, Optional> performJobs( final Duration currentTime, final Duration maximumTime ) throws SpanException { + if (this.closed) throw new IllegalStateException("Cannot perform jobs on closed simulation engine"); var tip = EventGraph.empty(); Mutable> exception = new MutableObject<>(Optional.empty()); for (final var job$ : jobs) { @@ -184,7 +249,9 @@ public void performJob( } /** Perform the next step of a modeled task. */ - public void stepTask(final TaskId task, final TaskFrame frame, final Duration currentTime) throws SpanException { + public void stepTask(final TaskId task, final TaskFrame frame, final Duration currentTime) throws SpanException { + if (this.closed) throw new IllegalStateException("Cannot step task on closed simulation engine"); + this.unstartedTasks.remove(task); // The handler for the next status of the task is responsible // for putting an updated state back into the task set. var state = this.tasks.remove(task); @@ -287,6 +354,7 @@ public void updateCondition( final Duration currentTime, final Duration horizonTime ) { + if (this.closed) throw new IllegalStateException("Cannot update condition on closed simulation engine"); final var querier = new EngineQuerier(frame); final var prediction = this.conditions .get(condition) @@ -311,6 +379,7 @@ public void updateResource( final TaskFrame frame, final Duration currentTime ) { + if (this.closed) throw new IllegalStateException("Cannot update resource on closed simulation engine"); final var querier = new EngineQuerier(frame); this.resources.get(resource).append(currentTime, querier); @@ -325,11 +394,23 @@ public void updateResource( /** Resets all tasks (freeing any held resources). The engine should not be used after being closed. */ @Override public void close() { + numActiveSimulationEngines--; for (final var task : this.tasks.values()) { task.state().release(); } this.executor.shutdownNow(); + this.closed = true; + } + + public void unscheduleAfter(final Duration duration) { + if (this.closed) throw new IllegalStateException("Cannot unschedule jobs on closed simulation engine"); + for (final var taskId : new ArrayList<>(this.tasks.keySet())) { + if (this.unstartedTasks.containsKey(taskId) && this.unstartedTasks.get(taskId).longerThan(duration)) { + this.tasks.remove(taskId); + this.scheduledJobs.unschedule(JobId.forTask(taskId)); + } + } } private record SpanInfo( @@ -444,19 +525,15 @@ public static Optional getDirectiveIdFromSpan( return directiveSpanId.map(spanInfo::getDirective); } - /** Compute a set of results from the current state of simulation. */ - // TODO: Move result extraction out of the SimulationEngine. - // The Engine should only need to stream events of interest to a downstream consumer. - // The Engine cannot be cognizant of all downstream needs. - // TODO: Whatever mechanism replaces `computeResults` also ought to replace `isTaskComplete`. - // TODO: Produce results for all tasks, not just those that have completed. - // Planners need to be aware of failed or unfinished tasks. - public static SimulationResults computeResults( - final SimulationEngine engine, - final Instant startTime, - final Duration elapsedTime, - final Topic activityTopic, + public record SimulationActivityExtract( + Instant startTime, + Duration duration, + Map simulatedActivities, + Map unfinishedActivities){} + + private static SpanInfo computeTaskInfo( final TemporalEventSource timeline, + final Topic activityTopic, final Iterable> serializableTopics ) { // Collect per-span information from the event graph. @@ -468,37 +545,34 @@ public static SimulationResults computeResults( final var trait = new SpanInfo.Trait(serializableTopics, activityTopic); p.events().evaluate(trait, trait::atom).accept(spanInfo); } + return spanInfo; + } - // Extract profiles for every resource. - final var realProfiles = new HashMap>>>(); - final var discreteProfiles = new HashMap>>>(); - - for (final var entry : engine.resources.entrySet()) { - final var id = entry.getKey(); - final var state = entry.getValue(); - - final var name = id.id(); - final var resource = state.resource(); - - switch (resource.getType()) { - case "real" -> realProfiles.put( - name, - Pair.of( - resource.getOutputType().getSchema(), - serializeProfile(elapsedTime, state, SimulationEngine::extractRealDynamics))); - - case "discrete" -> discreteProfiles.put( - name, - Pair.of( - resource.getOutputType().getSchema(), - serializeProfile(elapsedTime, state, SimulationEngine::extractDiscreteDynamics))); - - default -> - throw new IllegalArgumentException( - "Resource `%s` has unknown type `%s`".formatted(name, resource.getType())); - } - } + public static SimulationActivityExtract computeActivitySimulationResults( + final SimulationEngine engine, + final Instant startTime, + final Duration elapsedTime, + final Topic activityTopic, + final TemporalEventSource timeline, + final Iterable> serializableTopics + ){ + return computeActivitySimulationResults( + engine, + startTime, + elapsedTime, + computeTaskInfo(timeline, activityTopic, serializableTopics) + ); + } + /** + * Computes only activity-related results when resources are not needed + */ + public static SimulationActivityExtract computeActivitySimulationResults( + final SimulationEngine engine, + final Instant startTime, + final Duration elapsedTime, + final SpanInfo spanInfo + ){ // Identify the nearest ancestor *activity* (excluding intermediate anonymous tasks). final var activityParents = new HashMap(); final var activityDirectiveIds = new HashMap(); @@ -572,6 +646,80 @@ public static SimulationResults computeResults( )); } }); + return new SimulationActivityExtract(startTime, elapsedTime, simulatedActivities, unfinishedActivities); + } + + public static SimulationResults computeResults( + final SimulationEngine engine, + final Instant startTime, + final Duration elapsedTime, + final Topic activityTopic, + final TemporalEventSource timeline, + final Iterable> serializableTopics + ) { + return computeResults( + engine, + startTime, + elapsedTime, + activityTopic, + timeline, + serializableTopics, + engine.resources.keySet() + .stream() + .map(ResourceId::id) + .collect(Collectors.toSet())); + } + + /** Compute a set of results from the current state of simulation. */ + // TODO: Move result extraction out of the SimulationEngine. + // The Engine should only need to stream events of interest to a downstream consumer. + // The Engine cannot be cognizant of all downstream needs. + // TODO: Whatever mechanism replaces `computeResults` also ought to replace `isTaskComplete`. + // TODO: Produce results for all tasks, not just those that have completed. + // Planners need to be aware of failed or unfinished tasks. + public static SimulationResults computeResults( + final SimulationEngine engine, + final Instant startTime, + final Duration elapsedTime, + final Topic activityTopic, + final TemporalEventSource timeline, + final Iterable> serializableTopics, + final Set resourceNames + ) { + // Collect per-task information from the event graph. + final var taskInfo = computeTaskInfo(timeline, activityTopic, serializableTopics); + + // Extract profiles for every resource. + final var realProfiles = new HashMap>>>(); + final var discreteProfiles = new HashMap>>>(); + + for (final var entry : engine.resources.entrySet()) { + final var id = entry.getKey(); + final var state = entry.getValue(); + + final var name = id.id(); + final var resource = state.resource(); + if(!resourceNames.contains(name)) continue; + switch (resource.getType()) { + case "real" -> realProfiles.put( + name, + Pair.of( + resource.getOutputType().getSchema(), + serializeProfile(elapsedTime, state, SimulationEngine::extractRealDynamics))); + + case "discrete" -> discreteProfiles.put( + name, + Pair.of( + resource.getOutputType().getSchema(), + serializeProfile(elapsedTime, state, SimulationEngine::extractDiscreteDynamics))); + + default -> + throw new IllegalArgumentException( + "Resource `%s` has unknown type `%s`".formatted(name, resource.getType())); + } + } + + final var activityResults = computeActivitySimulationResults(engine, startTime, elapsedTime, taskInfo); final List> topics = new ArrayList<>(); final var serializableTopicToId = new HashMap, Integer>(); @@ -608,8 +756,8 @@ public static SimulationResults computeResults( return new SimulationResults(realProfiles, discreteProfiles, - simulatedActivities, - unfinishedActivities, + activityResults.simulatedActivities, + activityResults.unfinishedActivities, startTime, elapsedTime, topics, @@ -803,6 +951,10 @@ private record ExecutionState(SpanId span, Optional caller, Task public ExecutionState continueWith(final Task newState) { return new ExecutionState<>(this.span, this.caller, newState); } + + public ExecutionState duplicate(Executor executor) { + return new ExecutionState<>(span, caller, state.duplicate(executor)); + } } /** The span of time over which a subtree of tasks has acted. */ @@ -821,4 +973,16 @@ public boolean isComplete() { return this.endOffset.isPresent(); } } + + public boolean spanIsComplete(SpanId spanId) { + return this.spans.get(spanId).isComplete(); + } + + public SimulationEngine duplicate() { + return new SimulationEngine(this); + } + + public Optional peekNextTime() { + return this.scheduledJobs.peekNextTime(); + } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SlabList.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SlabList.java index 82fda9b742..1ed9b8b4ed 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SlabList.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/SlabList.java @@ -24,8 +24,12 @@ public final class SlabList implements Iterable { private Slab tail = this.head; /*derived*/ private int size = 0; + private boolean frozen = false; public void append(final T element) { + if (this.frozen) { + throw new IllegalStateException("Cannot append to frozen SlabList"); + } this.tail.elements().add(element); this.size += 1; @@ -99,4 +103,16 @@ public Slab() { this(new ArrayList<>(SLAB_SIZE), new MutableObject<>(null)); } } + + public SlabList duplicate() { + final SlabList slabList = new SlabList<>(); + for (T t : this) { + slabList.append(t); + } + return slabList; + } + + public void freeze() { + this.frozen = true; + } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/Subscriptions.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/Subscriptions.java index e533a37802..6f7866355e 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/Subscriptions.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/engine/Subscriptions.java @@ -50,4 +50,14 @@ public void clear() { this.topicsByQuery.clear(); this.queriesByTopic.clear(); } + + public Subscriptions duplicate() { + final Subscriptions subscriptions = new Subscriptions<>(); + for (final var entry : this.topicsByQuery.entrySet()) { + final var query = entry.getKey(); + final var topics = entry.getValue(); + subscriptions.subscribeQuery(query, new HashSet<>(topics)); + } + return subscriptions; + } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/CausalEventSource.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/CausalEventSource.java index 21c2670080..f436f226f0 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/CausalEventSource.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/CausalEventSource.java @@ -5,8 +5,12 @@ public final class CausalEventSource implements EventSource { private Event[] points = new Event[2]; private int size = 0; + private boolean frozen = false; public void add(final Event point) { + if (this.frozen) { + throw new IllegalStateException("Cannot add to frozen CausalEventSource"); + } if (this.size == this.points.length) { this.points = Arrays.copyOf(this.points, 3 * this.size / 2); } @@ -41,4 +45,9 @@ public void stepUp(final Cell cell) { this.index = size; } } + + @Override + public void freeze() { + this.frozen = true; + } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/EventSource.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/EventSource.java index 7357695d54..cb2b5f0ed8 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/EventSource.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/EventSource.java @@ -3,6 +3,8 @@ public interface EventSource { Cursor cursor(); + void freeze(); + interface Cursor { void stepUp(Cell cell); } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/LiveCells.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/LiveCells.java index 41661543d1..781c3dc547 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/LiveCells.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/LiveCells.java @@ -57,4 +57,9 @@ private Optional> getCell(final Query query) { return Optional.of(cell.get()); } + + public void freeze() { + if (this.parent != null) this.parent.freeze(); + this.source.freeze(); + } } diff --git a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/TemporalEventSource.java b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/TemporalEventSource.java index 9f4ec53af5..6964c9217d 100644 --- a/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/TemporalEventSource.java +++ b/merlin-driver/src/main/java/gov/nasa/jpl/aerie/merlin/driver/timeline/TemporalEventSource.java @@ -86,4 +86,8 @@ public sealed interface TimePoint { record Delta(Duration delta) implements TimePoint {} record Commit(EventGraph events, Set> topics) implements TimePoint {} } + + public void freeze() { + this.points.freeze(); + } } diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/AnchorSimulationTest.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/AnchorSimulationTest.java index d7e8984d81..9f300abc16 100644 --- a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/AnchorSimulationTest.java +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/AnchorSimulationTest.java @@ -1,29 +1,14 @@ package gov.nasa.jpl.aerie.merlin.driver; -import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; -import gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType; -import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; -import gov.nasa.jpl.aerie.merlin.protocol.model.ModelType; -import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; -import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; -import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; -import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; -import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; -import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import org.apache.commons.lang3.tuple.Pair; -import org.apache.commons.lang3.tuple.Triple; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -471,24 +456,28 @@ public void filterTest() { } @Nested public final class AnchorsSimulationDriverTests { - private final SerializedActivity serializedDelayDirective = new SerializedActivity("DelayActivityDirective", arguments); - private final SerializedActivity serializedDecompositionDirective = new SerializedActivity("DecomposingActivityDirective", arguments); + private final SerializedActivity serializedDelayDirective = new SerializedActivity( + "DelayActivityDirective", + arguments); + private final SerializedActivity serializedDecompositionDirective = new SerializedActivity( + "DecomposingActivityDirective", + arguments); private final SerializedValue computedAttributes = new SerializedValue.MapValue(Map.of()); private final Instant planStart = Instant.EPOCH; /** * Asserts equality based on the following fields of SimulationResults: - * - startTime - * - simulatedActivities - * - unfinishedActivities (asserted to be empty in actual) - * - topics - * Any resource profiles and events are not checked. + * - startTime + * - simulatedActivities + * - unfinishedActivities (asserted to be empty in actual) + * - topics + * Any resource profiles and events are not checked. */ - private static void assertEqualsSimulationResults(SimulationResults expected, SimulationResults actual){ + private static void assertEqualsSimulationResults(SimulationResults expected, SimulationResults actual) { assertEquals(expected.startTime, actual.startTime); assertEquals(expected.duration, actual.duration); assertEquals(expected.simulatedActivities.size(), actual.simulatedActivities.size()); - for(final var entry : expected.simulatedActivities.entrySet()){ + for (final var entry : expected.simulatedActivities.entrySet()) { final var key = entry.getKey(); final var expectedValue = entry.getValue(); final var actualValue = actual.simulatedActivities.get(key); @@ -497,15 +486,21 @@ private static void assertEqualsSimulationResults(SimulationResults expected, Si } assertTrue(actual.unfinishedActivities.isEmpty()); assertEquals(expected.topics.size(), actual.topics.size()); - for(int i = 0; i < expected.topics.size(); ++i){ + for (int i = 0; i < expected.topics.size(); ++i) { assertEquals(expected.topics.get(i), actual.topics.get(i)); } } - private void constructFullComplete5AryTree(int maxLevel, int currentLevel, long parentNode, Map activitiesToSimulate, Map simulatedActivities){ - if(currentLevel > maxLevel) return; - for(int i = 1; i <= 5; i++) { - long curElement = parentNode*5+i; + private void constructFullComplete5AryTree( + int maxLevel, + int currentLevel, + long parentNode, + Map activitiesToSimulate, + Map simulatedActivities) + { + if (currentLevel > maxLevel) return; + for (int i = 1; i <= 5; i++) { + long curElement = parentNode * 5 + i; activitiesToSimulate.put( new ActivityDirectiveId(curElement), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(parentNode), false)); @@ -519,11 +514,16 @@ private void constructFullComplete5AryTree(int maxLevel, int currentLevel, long null, List.of(), Optional.of(new ActivityDirectiveId(curElement)), computedAttributes)); - constructFullComplete5AryTree(maxLevel, currentLevel+1, curElement, activitiesToSimulate, simulatedActivities); + constructFullComplete5AryTree( + maxLevel, + currentLevel + 1, + curElement, + activitiesToSimulate, + simulatedActivities); } } - private static void assertEqualsAsideFromChildren(SimulatedActivity expected, SimulatedActivity actual){ + private static void assertEqualsAsideFromChildren(SimulatedActivity expected, SimulatedActivity actual) { assertEquals(expected.type(), actual.type()); assertEquals(expected.arguments(), actual.arguments()); assertEquals(expected.start(), actual.start()); @@ -555,14 +555,19 @@ public void activitiesAnchoredToPlan() { List.of(), Optional.of(activityDirectiveId), computedAttributes - )); + )); } // Anchored to Plan End (only negative will be simulated) for (long l = 10; l < 15; l++) { final var activityDirectiveId = new ActivityDirectiveId(l); resolveToPlanStartAnchors.put( activityDirectiveId, - new ActivityDirective(Duration.of(-l, Duration.MINUTES), serializedDelayDirective, null, false)); // Minutes so they finish by simulation end + new ActivityDirective( + // Minutes so they finish by simulation end + Duration.of(-l, Duration.MINUTES), + serializedDelayDirective, + null, + false)); simulatedActivities.put(new SimulatedActivityId(l), new SimulatedActivity( serializedDelayDirective.getTypeName(), Map.of(), @@ -631,11 +636,11 @@ public void activitiesAnchoredToPlan() { Map.of(), //unfinished planStart, tenDays, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), resolveToPlanStartAnchors, planStart, tenDays, @@ -699,7 +704,7 @@ public void activitiesAnchoredToOtherActivities() { new SimulatedActivity( serializedDelayDirective.getTypeName(), Map.of(), - Instant.EPOCH.plus((2*l)+1, ChronoUnit.MINUTES), + Instant.EPOCH.plus((2 * l) + 1, ChronoUnit.MINUTES), oneMinute, null, List.of(), @@ -729,7 +734,7 @@ public void activitiesAnchoredToOtherActivities() { new SimulatedActivity( serializedDelayDirective.getTypeName(), Map.of(), - Instant.EPOCH.plus(l+c, ChronoUnit.MINUTES), + Instant.EPOCH.plus(l + c, ChronoUnit.MINUTES), oneMinute, null, List.of(), @@ -748,11 +753,11 @@ public void activitiesAnchoredToOtherActivities() { Map.of(), //unfinished planStart, tenDays, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesToSimulate, planStart, tenDays, @@ -765,7 +770,7 @@ public void activitiesAnchoredToOtherActivities() { @Test @DisplayName("Decomposition and anchors do not interfere with each other") - public void decomposingActivitiesAndAnchors(){ + public void decomposingActivitiesAndAnchors() { // Given positions Left, Center, Right in an anchor chain, where each position can either contain a Non-Decomposition (ND) activity or a Decomposition (D) activity, // and the connection between Center and Left and Right and Center can be either Start (<-s-) or End (<-e-), // and two NDs cannot be adjacent to each other, there are 20 permutations. @@ -778,140 +783,436 @@ public void decomposingActivitiesAndAnchors(){ final var threeMinutes = Duration.of(3, Duration.MINUTES); // ND <-s- D <-s- D - activitiesToSimulate.put(new ActivityDirectiveId(1), new ActivityDirective(Duration.ZERO, serializedDelayDirective, null, true)); - activitiesToSimulate.put(new ActivityDirectiveId(2), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(1), true)); - activitiesToSimulate.put(new ActivityDirectiveId(3), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(2), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(1), + new ActivityDirective(Duration.ZERO, serializedDelayDirective, null, true)); + activitiesToSimulate.put( + new ActivityDirectiveId(2), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(1), + true)); + activitiesToSimulate.put( + new ActivityDirectiveId(3), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(2), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(1), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH, oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(1)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH, + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(1)), + computedAttributes)); topLevelSimulatedActivities.put( new ActivityDirectiveId(2), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH, threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(2)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH, + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(2)), + computedAttributes)); topLevelSimulatedActivities.put( new ActivityDirectiveId(3), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH, threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(3)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH, + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(3)), + computedAttributes)); // ND <-s- D <-e- D - activitiesToSimulate.put(new ActivityDirectiveId(4), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(2), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(4), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(2), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(4), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(3, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(4)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(3, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(4)), + computedAttributes)); // ND <-s- D <-s- ND - activitiesToSimulate.put(new ActivityDirectiveId(5), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(2), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(5), + new ActivityDirective(Duration.ZERO, + serializedDelayDirective, + new ActivityDirectiveId(2), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(5), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH, oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(5)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH, + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(5)), + computedAttributes)); // ND <-s- D <-e- ND - activitiesToSimulate.put(new ActivityDirectiveId(6), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(2), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(6), + new ActivityDirective(Duration.ZERO, + serializedDelayDirective, + new ActivityDirectiveId(2), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(6), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(3, ChronoUnit.MINUTES), oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(6)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(3, ChronoUnit.MINUTES), + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(6)), + computedAttributes)); // ND <-e- D <-s- D - activitiesToSimulate.put(new ActivityDirectiveId(7), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(1), false)); - activitiesToSimulate.put(new ActivityDirectiveId(8), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(7), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(7), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(1), + false)); + activitiesToSimulate.put( + new ActivityDirectiveId(8), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(7), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(7), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(1, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(7)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(1, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(7)), + computedAttributes)); topLevelSimulatedActivities.put( new ActivityDirectiveId(8), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(1, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(8)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(1, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(8)), + computedAttributes)); // ND <-e- D <-e- D - activitiesToSimulate.put(new ActivityDirectiveId(9), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(7), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(9), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(7), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(9), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(4, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(9)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(4, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(9)), + computedAttributes)); // ND <-e- D <-s- ND - activitiesToSimulate.put(new ActivityDirectiveId(10), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(7), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(10), + new ActivityDirective(Duration.ZERO, + serializedDelayDirective, + new ActivityDirectiveId(7), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(10), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(1, ChronoUnit.MINUTES), oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(10)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(1, ChronoUnit.MINUTES), + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(10)), + computedAttributes)); // ND <-e- D <-e- ND - activitiesToSimulate.put(new ActivityDirectiveId(11), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(7), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(11), + new ActivityDirective(Duration.ZERO, + serializedDelayDirective, + new ActivityDirectiveId(7), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(11), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(4, ChronoUnit.MINUTES), oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(11)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(4, ChronoUnit.MINUTES), + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(11)), + computedAttributes)); // D <-s- D <-s- D - activitiesToSimulate.put(new ActivityDirectiveId(12), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(3), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(12), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(3), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(12), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH, threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(12)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH, + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(12)), + computedAttributes)); // D <-s- D <-e- D - activitiesToSimulate.put(new ActivityDirectiveId(13), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(3), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(13), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(3), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(13), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(3, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(13)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(3, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(13)), + computedAttributes)); // D <-s- D <-s- ND - activitiesToSimulate.put(new ActivityDirectiveId(14), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(3), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(14), + new ActivityDirective(Duration.ZERO, + serializedDelayDirective, + new ActivityDirectiveId(3), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(14), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH, oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(14)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH, + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(14)), + computedAttributes)); // D <-s- D <-e- ND - activitiesToSimulate.put(new ActivityDirectiveId(15), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(3), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(15), + new ActivityDirective(Duration.ZERO, + serializedDelayDirective, + new ActivityDirectiveId(3), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(15), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(3, ChronoUnit.MINUTES), oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(15)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(3, ChronoUnit.MINUTES), + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(15)), + computedAttributes)); // D <-e- D <-s- D - activitiesToSimulate.put(new ActivityDirectiveId(16), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(4), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(16), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(4), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(16), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(3, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(16)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(3, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(16)), + computedAttributes)); // D <-e- D <-e- D - activitiesToSimulate.put(new ActivityDirectiveId(17), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(4), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(17), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(4), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(17), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(6, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(17)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(6, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(17)), + computedAttributes)); // D <-e- D <-s- ND - activitiesToSimulate.put(new ActivityDirectiveId(18), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(4), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(18), + new ActivityDirective(Duration.ZERO, + serializedDelayDirective, + new ActivityDirectiveId(4), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(18), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(3, ChronoUnit.MINUTES), oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(18)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(3, ChronoUnit.MINUTES), + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(18)), + computedAttributes)); // D <-e- D <-e- ND - activitiesToSimulate.put(new ActivityDirectiveId(19), new ActivityDirective(Duration.ZERO, serializedDelayDirective, new ActivityDirectiveId(4), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(19), + new ActivityDirective(Duration.ZERO, + serializedDelayDirective, + new ActivityDirectiveId(4), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(19), - new SimulatedActivity(serializedDelayDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(6, ChronoUnit.MINUTES), oneMinute, null, List.of(), Optional.of(new ActivityDirectiveId(19)), computedAttributes)); + new SimulatedActivity( + serializedDelayDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(6, ChronoUnit.MINUTES), + oneMinute, + null, + List.of(), + Optional.of(new ActivityDirectiveId(19)), + computedAttributes)); // D <-s- ND <-s- D - activitiesToSimulate.put(new ActivityDirectiveId(20), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(14), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(20), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(14), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(20), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH, threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(20)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH, + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(20)), + computedAttributes)); // D <-s- ND <-e- D - activitiesToSimulate.put(new ActivityDirectiveId(21), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(14), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(21), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(14), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(21), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(1, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(21)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(1, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(21)), + computedAttributes)); // D <-e- ND <-s- D - activitiesToSimulate.put(new ActivityDirectiveId(22), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(15), true)); + activitiesToSimulate.put( + new ActivityDirectiveId(22), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(15), + true)); topLevelSimulatedActivities.put( new ActivityDirectiveId(22), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(3, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(22)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(3, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(22)), + computedAttributes)); // D <-e- ND <-e- D - activitiesToSimulate.put(new ActivityDirectiveId(23), new ActivityDirective(Duration.ZERO, serializedDecompositionDirective, new ActivityDirectiveId(15), false)); + activitiesToSimulate.put( + new ActivityDirectiveId(23), + new ActivityDirective(Duration.ZERO, + serializedDecompositionDirective, + new ActivityDirectiveId(15), + false)); topLevelSimulatedActivities.put( new ActivityDirectiveId(23), - new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(4, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(23)), computedAttributes)); + new SimulatedActivity( + serializedDecompositionDirective.getTypeName(), + Map.of(), + Instant.EPOCH.plus(4, ChronoUnit.MINUTES), + threeMinutes, + null, + List.of(), + Optional.of(new ActivityDirectiveId(23)), + computedAttributes)); // Custom assertion, as Decomposition children can end up simulated in different positions between runs final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesToSimulate, planStart, tenDays, @@ -921,8 +1222,9 @@ public void decomposingActivitiesAndAnchors(){ assertEquals(planStart, actualSimResults.startTime); assertTrue(actualSimResults.unfinishedActivities.isEmpty()); + final var modelTopicList = TestMissionModel.getModelTopicList(); assertEquals(modelTopicList.size(), actualSimResults.topics.size()); - for(int i = 0; i < modelTopicList.size(); ++i){ + for (int i = 0; i < modelTopicList.size(); ++i) { assertEquals(modelTopicList.get(i), actualSimResults.topics.get(i)); } @@ -930,23 +1232,22 @@ public void decomposingActivitiesAndAnchors(){ final var otherSimulatedActivities = new HashMap(23); assertEquals(51, actualSimResults.simulatedActivities.size()); // 23 + 2*(14 Decomposing activities) - for(final var entry : actualSimResults.simulatedActivities.entrySet()) { - if(entry.getValue().parentId()==null){ + for (final var entry : actualSimResults.simulatedActivities.entrySet()) { + if (entry.getValue().parentId() == null) { otherSimulatedActivities.put(entry.getKey(), entry.getValue()); - } - else { + } else { childSimulatedActivities.put(entry.getKey(), entry.getValue()); } } assertEquals(23, otherSimulatedActivities.size()); assertEquals(28, childSimulatedActivities.size()); - for(final var entry : otherSimulatedActivities.entrySet()){ + for (final var entry : otherSimulatedActivities.entrySet()) { assertTrue(entry.getValue().directiveId().isPresent()); final ActivityDirectiveId topLevelKey = entry.getValue().directiveId().get(); assertEqualsAsideFromChildren(topLevelSimulatedActivities.get(topLevelKey), entry.getValue()); // For decompositions, examine the children - if(entry.getValue().type().equals(serializedDecompositionDirective.getTypeName())){ + if (entry.getValue().type().equals(serializedDecompositionDirective.getTypeName())) { assertEquals(2, entry.getValue().childIds().size()); final var firstChild = childSimulatedActivities.remove(entry.getValue().childIds().get(0)); final var secondChild = childSimulatedActivities.remove(entry.getValue().childIds().get(1)); @@ -957,7 +1258,7 @@ public void decomposingActivitiesAndAnchors(){ assertTrue(firstChild.childIds().isEmpty()); assertTrue(secondChild.childIds().isEmpty()); - if(firstChild.start().isBefore(secondChild.start())){ + if (firstChild.start().isBefore(secondChild.start())) { assertEqualsAsideFromChildren( new SimulatedActivity( serializedDelayDirective.getTypeName(), @@ -1045,11 +1346,11 @@ public void naryTreeAnchorChain() { Map.of(), //unfinished planStart, tenDays, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesToSimulate, planStart, tenDays, @@ -1060,170 +1361,5 @@ public void naryTreeAnchorChain() { assertEquals(3906, expectedSimResults.simulatedActivities.size()); assertEqualsSimulationResults(expectedSimResults, actualSimResults); } - - //region Mission Model - /* package-private */static final List> modelTopicList = Arrays.asList( - Triple.of(0, "ActivityType.Input.DelayActivityDirective", new ValueSchema.StructSchema(Map.of())), - Triple.of(1, "ActivityType.Output.DelayActivityDirective", new ValueSchema.StructSchema(Map.of())), - Triple.of(2, "ActivityType.Input.DecomposingActivityDirective", new ValueSchema.StructSchema(Map.of())), - Triple.of(3, "ActivityType.Output.DecomposingActivityDirective", new ValueSchema.StructSchema(Map.of()))); - - private static final Topic delayedActivityDirectiveInputTopic = new Topic<>(); - private static final Topic delayedActivityDirectiveOutputTopic = new Topic<>(); - /* package-private*/ static final DirectiveType delayedActivityDirective = new DirectiveType<>() { - @Override - public InputType getInputType() { - return testModelInputType; - } - - @Override - public OutputType getOutputType() { - return testModelOutputType; - } - - @Override - public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> $ -> { - $.emit(this, delayedActivityDirectiveInputTopic); - return TaskStatus.delayed(oneMinute, $$ -> { - $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); - return TaskStatus.completed(Unit.UNIT); - }); - }; - } - }; - - private static final Topic decomposingActivityDirectiveInputTopic = new Topic<>(); - private static final Topic decomposingActivityDirectiveOutputTopic = new Topic<>(); - /* package-private */ static final DirectiveType decomposingActivityDirective = new DirectiveType<>() { - @Override - public InputType getInputType() { - return testModelInputType; - } - - @Override - public OutputType getOutputType() { - return testModelOutputType; - } - - @Override - public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> scheduler -> { - scheduler.emit(this, decomposingActivityDirectiveInputTopic); - return TaskStatus.delayed( - Duration.ZERO, - $ -> { - try { - $.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); - } catch (final InstantiationException ex) { - throw new Error("Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( - ex.toString())); - } - return TaskStatus.delayed(Duration.of(120, Duration.SECOND), $$ -> { - try { - $$.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); - } catch (final InstantiationException ex) { - throw new Error( - "Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( - ex.toString())); - } - $$.emit(Unit.UNIT, decomposingActivityDirectiveOutputTopic); - return TaskStatus.completed(Unit.UNIT); - }); - }); - }; - } - }; - - private static final InputType testModelInputType = new InputType<>() { - @Override - public List getParameters() { - return List.of(); - } - - @Override - public List getRequiredParameters() { - return List.of(); - } - - @Override - public Object instantiate(final Map arguments) { - return new Object(); - } - - @Override - public Map getArguments(final Object value) { - return Map.of(); - } - - @Override - public List getValidationFailures(final Object value) { - return List.of(); - } - }; - - private static final OutputType testModelOutputType = new OutputType<>() { - @Override - public ValueSchema getSchema() { - return ValueSchema.ofStruct(Map.of()); - } - - @Override - public SerializedValue serialize(final Object value) { - return SerializedValue.of(Map.of()); - } - }; - - /* package-private */ static final MissionModel AnchorTestModel = new MissionModel<>( - new Object(), - new LiveCells(null), - Map.of(), - List.of( - new MissionModel.SerializableTopic<>( - "ActivityType.Input.DelayActivityDirective", - delayedActivityDirectiveInputTopic, - testModelOutputType), - new MissionModel.SerializableTopic<>( - "ActivityType.Output.DelayActivityDirective", - delayedActivityDirectiveOutputTopic, - testModelOutputType), - new MissionModel.SerializableTopic<>( - "ActivityType.Input.DecomposingActivityDirective", - decomposingActivityDirectiveInputTopic, - testModelOutputType), - new MissionModel.SerializableTopic<>( - "ActivityType.Output.DecomposingActivityDirective", - decomposingActivityDirectiveOutputTopic, - testModelOutputType)), - List.of(), - DirectiveTypeRegistry.extract( - new ModelType<>() { - - @Override - public Map> getDirectiveTypes() { - return Map.of( - "DelayActivityDirective", - delayedActivityDirective, - "DecomposingActivityDirective", - decomposingActivityDirective); - } - - @Override - public InputType getConfigurationType() { - return testModelInputType; - } - - @Override - public Object instantiate( - final Instant planStart, - final Object configuration, - final Initializer builder) - { - return new Object(); - } - } - ) - ); - //endregion } } diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java new file mode 100644 index 0000000000..f7f28361e5 --- /dev/null +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/SimulationDuplicationTest.java @@ -0,0 +1,93 @@ +package gov.nasa.jpl.aerie.merlin.driver; + +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTES; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class SimulationDuplicationTest { + CachedEngineStore store; + final private class InfiniteCapacityEngineStore implements CachedEngineStore{ + private Map> store = new HashMap<>(); + @Override + public void save( + final CheckpointSimulationDriver.CachedSimulationEngine cachedSimulationEngine, + final SimulationEngineConfiguration configuration) + { + store.computeIfAbsent(configuration, conf -> new ArrayList<>()); + store.get(configuration).add(cachedSimulationEngine); + } + + @Override + public List getCachedEngines(final SimulationEngineConfiguration configuration) { + return store.get(configuration); + } + + public int capacity() { + return Integer.MAX_VALUE; + } + } + + public static SimulationEngineConfiguration mockConfiguration(){ + return new SimulationEngineConfiguration( + Map.of(), + Instant.EPOCH, + new MissionModelId(0) + ); + } + + @BeforeEach + void beforeEach(){ + this.store = new InfiniteCapacityEngineStore(); + } + + @Test + void testDuplicate() { + final var results = simulateWithCheckpoints( + CheckpointSimulationDriver.CachedSimulationEngine.empty(TestMissionModel.missionModel()), + List.of(Duration.of(5, MINUTES)), + store); + final SimulationResults expected = SimulationDriver.simulate( + TestMissionModel.missionModel(), + Map.of(), + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + () -> false, + $ -> {}); + assertEquals(expected, results); + final var newResults = simulateWithCheckpoints(store.getCachedEngines(mockConfiguration()).get(0), List.of(), store); + assertEquals(expected, newResults); + } + + static SimulationResults simulateWithCheckpoints( + final CheckpointSimulationDriver.CachedSimulationEngine cachedEngine, + final List desiredCheckpoints, + final CachedEngineStore engineStore + ) { + return CheckpointSimulationDriver.simulateWithCheckpoints( + TestMissionModel.missionModel(), + Map.of(), + Instant.EPOCH, + Duration.HOUR, + Instant.EPOCH, + Duration.HOUR, + $ -> {}, + () -> false, + cachedEngine, + CheckpointSimulationDriver.desiredCheckpoints(desiredCheckpoints), + CheckpointSimulationDriver.noCondition(), + engineStore, + mockConfiguration() + ).computeResults(); + } +} diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TemporalSubsetSimulationTests.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TemporalSubsetSimulationTests.java index 28358ed4b2..fb31916f7f 100644 --- a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TemporalSubsetSimulationTests.java +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TemporalSubsetSimulationTests.java @@ -13,8 +13,6 @@ import java.util.Optional; import java.util.TreeMap; -import static gov.nasa.jpl.aerie.merlin.driver.AnchorSimulationTest.AnchorsSimulationDriverTests.AnchorTestModel; -import static gov.nasa.jpl.aerie.merlin.driver.AnchorSimulationTest.AnchorsSimulationDriverTests.modelTopicList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -117,11 +115,11 @@ public void simulateFirstHalf(){ unfinishedActivities, planStart, fiveDays, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart, fiveDays, @@ -169,11 +167,11 @@ public void simulateSecondHalf(){ Map.of(), //The last activity starts an hour before the simulation ends planStart.plus(5, ChronoUnit.DAYS), fiveDays, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart.plus(5, ChronoUnit.DAYS), fiveDays, @@ -239,11 +237,11 @@ void simulateMiddle() { unfinishedActivities, planStart.plus(3, ChronoUnit.DAYS), fiveDays, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart.plus(3, ChronoUnit.DAYS), fiveDays, @@ -303,11 +301,11 @@ void simulateBeforePlanStart() { unfinishedActivities, planStart.plus(-2, ChronoUnit.DAYS), fiveDays, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart.plus(-2, ChronoUnit.DAYS), fiveDays, @@ -356,11 +354,11 @@ void simulateAfterPlanEnd() { unfinishedActivities, planStart.plus(8, ChronoUnit.DAYS), fiveDays, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart.plus(8, ChronoUnit.DAYS), fiveDays, @@ -597,11 +595,11 @@ void simulateAroundAnchors() { unfinishedActivities, planStart.plus(3, ChronoUnit.HOURS), fourAndAHalfHours, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart.plus(3, ChronoUnit.HOURS), fourAndAHalfHours, @@ -758,11 +756,11 @@ void simulateStartBetweenAnchors() { unfinishedActivities, planStart.plus(3, ChronoUnit.HOURS), fourAndAHalfHours, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart.plus(3, ChronoUnit.HOURS), fourAndAHalfHours, @@ -953,11 +951,11 @@ void simulateEndBetweenAnchors() { unfinishedActivities, planStart.plus(3, ChronoUnit.HOURS), fourAndAHalfHours, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart.plus(3, ChronoUnit.HOURS), fourAndAHalfHours, @@ -998,11 +996,11 @@ void simulateNoDuration() { unfinishedActivities, planStart.plus(12, ChronoUnit.HOURS), Duration.ZERO, - modelTopicList, + TestMissionModel.getModelTopicList(), new TreeMap<>() //events ); final var actualSimResults = SimulationDriver.simulate( - AnchorTestModel, + TestMissionModel.missionModel(), activitiesInPlan, planStart.plus(12, ChronoUnit.HOURS), Duration.ZERO, diff --git a/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TestMissionModel.java b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TestMissionModel.java new file mode 100644 index 0000000000..7f71ba267c --- /dev/null +++ b/merlin-driver/src/test/java/gov/nasa/jpl/aerie/merlin/driver/TestMissionModel.java @@ -0,0 +1,195 @@ +package gov.nasa.jpl.aerie.merlin.driver; + +import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; +import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; +import gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType; +import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; +import gov.nasa.jpl.aerie.merlin.protocol.model.ModelType; +import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; +import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; +import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; +import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; +import org.apache.commons.lang3.tuple.Triple; + +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class TestMissionModel { + private final static Duration oneMinute = Duration.of(60, Duration.SECONDS); + private static final Topic delayedActivityDirectiveInputTopic = new Topic<>(); + private static final Topic delayedActivityDirectiveOutputTopic = new Topic<>(); + + static List> getModelTopicList() { + return Arrays.asList( + Triple.of(0, "ActivityType.Input.DelayActivityDirective", new ValueSchema.StructSchema(Map.of())), + Triple.of(1, "ActivityType.Output.DelayActivityDirective", new ValueSchema.StructSchema(Map.of())), + Triple.of(2, "ActivityType.Input.DecomposingActivityDirective", new ValueSchema.StructSchema(Map.of())), + Triple.of(3, "ActivityType.Output.DecomposingActivityDirective", new ValueSchema.StructSchema(Map.of()))); + } + + /* package-private*/ static final DirectiveType delayedActivityDirective = new DirectiveType<>() { + @Override + public InputType getInputType() { + return testModelInputType; + } + + @Override + public OutputType getOutputType() { + return testModelOutputType; + } + + @Override + public TaskFactory getTaskFactory(final Object o, final Object o2) { + return executor -> new OneStepTask<>($ -> { + $.emit(this, delayedActivityDirectiveInputTopic); + return TaskStatus.delayed(oneMinute, new OneStepTask<>($$ -> { + $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); + return TaskStatus.completed(Unit.UNIT); + })); + }); + } + }; + + private static final Topic decomposingActivityDirectiveInputTopic = new Topic<>(); + private static final Topic decomposingActivityDirectiveOutputTopic = new Topic<>(); + /* package-private */ static final DirectiveType decomposingActivityDirective = new DirectiveType<>() { + @Override + public InputType getInputType() { + return testModelInputType; + } + + @Override + public OutputType getOutputType() { + return testModelOutputType; + } + + @Override + public TaskFactory getTaskFactory(final Object o, final Object o2) { + return executor -> new OneStepTask<>(scheduler -> { + scheduler.emit(this, decomposingActivityDirectiveInputTopic); + return TaskStatus.delayed( + Duration.ZERO, + new OneStepTask<>($ -> { + try { + $.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); + } catch (final InstantiationException ex) { + throw new Error("Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( + ex.toString())); + } + return TaskStatus.delayed(Duration.of(120, Duration.SECOND), new OneStepTask<>($$ -> { + try { + $$.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); + } catch (final InstantiationException ex) { + throw new Error( + "Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( + ex.toString())); + } + $$.emit(Unit.UNIT, decomposingActivityDirectiveOutputTopic); + return TaskStatus.completed(Unit.UNIT); + })); + })); + }); + } + }; + + private static final InputType testModelInputType = new InputType<>() { + @Override + public List getParameters() { + return List.of(); + } + + @Override + public List getRequiredParameters() { + return List.of(); + } + + @Override + public Object instantiate(final Map arguments) { + return new Object(); + } + + @Override + public Map getArguments(final Object value) { + return Map.of(); + } + + @Override + public List getValidationFailures(final Object value) { + return List.of(); + } + }; + + private static final OutputType testModelOutputType = new OutputType<>() { + @Override + public ValueSchema getSchema() { + return ValueSchema.ofStruct(Map.of()); + } + + @Override + public SerializedValue serialize(final Object value) { + return SerializedValue.of(Map.of()); + } + }; + + public static MissionModel missionModel() { + return new MissionModel<>( + new Object(), + new LiveCells(new TemporalEventSource()), + Map.of(), + List.of( + new MissionModel.SerializableTopic<>( + "ActivityType.Input.DelayActivityDirective", + delayedActivityDirectiveInputTopic, + testModelOutputType), + new MissionModel.SerializableTopic<>( + "ActivityType.Output.DelayActivityDirective", + delayedActivityDirectiveOutputTopic, + testModelOutputType), + new MissionModel.SerializableTopic<>( + "ActivityType.Input.DecomposingActivityDirective", + decomposingActivityDirectiveInputTopic, + testModelOutputType), + new MissionModel.SerializableTopic<>( + "ActivityType.Output.DecomposingActivityDirective", + decomposingActivityDirectiveOutputTopic, + testModelOutputType)), + List.of(), + DirectiveTypeRegistry.extract( + new ModelType<>() { + + @Override + public Map> getDirectiveTypes() { + return Map.of( + "DelayActivityDirective", + delayedActivityDirective, + "DecomposingActivityDirective", + decomposingActivityDirective); + } + + @Override + public InputType getConfigurationType() { + return testModelInputType; + } + + @Override + public Object instantiate( + final Instant planStart, + final Object configuration, + final Initializer builder) + { + return new Object(); + } + } + ) + ); + } +} diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java index e8760293db..9d71dca7f5 100644 --- a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java @@ -24,6 +24,7 @@ import gov.nasa.jpl.aerie.merlin.processor.metamodel.InputTypeRecord; import gov.nasa.jpl.aerie.merlin.processor.metamodel.MissionModelRecord; import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; import gov.nasa.jpl.aerie.merlin.protocol.model.MerlinPlugin; @@ -31,6 +32,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.OutputType; import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerPlugin; +import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; @@ -50,6 +52,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.Executor; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -859,12 +862,19 @@ public Optional generateActivityMapper(final MissionModelRecord missio .orElseGet(() -> CodeBlock .builder() .add( - "return executor -> scheduler -> {$>\n$L$<};\n", + "return executor -> new $T<>() { public $T<$T> step($T scheduler) {$>\n$L$<} public $T<$T> duplicate($T executor) { return this; }};\n", + Task.class, + TaskStatus.class, + Unit.class, + Scheduler.class, CodeBlock.builder() - .addStatement("scheduler.emit($L, this.$L)", "activity", "inputTopic") - .addStatement("scheduler.emit($T.UNIT, this.$L)", Unit.class, "outputTopic") + .addStatement("scheduler.emit($L, $L.this.$L)", "activity", activityType.inputType().mapper().name, "inputTopic") + .addStatement("scheduler.emit($T.UNIT, $L.this.$L)", Unit.class, activityType.inputType().mapper().name, "outputTopic") .addStatement("return $T.completed($T.UNIT)", TaskStatus.class, Unit.class) - .build()) + .build(), + Task.class, + Unit.class, + Executor.class) .build())) .build()) .build(); diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingTask.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingTask.java index a89e4048b3..daae4a274e 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingTask.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ReplayingTask.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.Objects; +import java.util.concurrent.Executor; import java.util.function.Supplier; public final class ReplayingTask implements Task { @@ -69,4 +70,12 @@ public Scheduler await(final gov.nasa.jpl.aerie.merlin.protocol.model.Condition // (most notably the call stack snapshotting). private static final class Yield extends RuntimeException {} private static final Yield Yield = new Yield(); + + @Override + public Task duplicate(Executor executor) { + final ReplayingTask replayingTask = new ReplayingTask<>(rootContext, task); + replayingTask.memory.reads().addAll(this.memory.reads()); + replayingTask.memory.writes().setValue(this.memory.writes()); + return replayingTask; + } } diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java index d4bc94e559..1fa4efb3f5 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedReactionContext.java @@ -9,6 +9,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import java.util.Objects; +import java.util.function.Consumer; import java.util.function.Function; /* package-local */ @@ -16,15 +17,18 @@ final class ThreadedReactionContext implements Context { private final Scoped rootContext; private final TaskHandle handle; private Scheduler scheduler; + private final Consumer readLogger; public ThreadedReactionContext( final Scoped rootContext, final Scheduler scheduler, - final TaskHandle handle) + final TaskHandle handle, + final Consumer readLog) { this.rootContext = Objects.requireNonNull(rootContext); this.scheduler = scheduler; this.handle = handle; + this.readLogger = readLog; } @Override @@ -34,7 +38,9 @@ public ContextType getContextType() { @Override public State ask(final CellId cellId) { - return this.scheduler.get(cellId); + final State state = this.scheduler.get(cellId); + this.readLogger.accept(state); + return state; } @Override diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java index fec837e0c2..a52320625a 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/ThreadedTask.java @@ -1,18 +1,27 @@ package gov.nasa.jpl.aerie.merlin.framework; +import gov.nasa.jpl.aerie.merlin.protocol.driver.CellId; import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.Task; import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; import java.util.function.Supplier; public final class ThreadedTask implements Task { + public static boolean CACHE_READS = false; + private final boolean cacheReads = CACHE_READS; + private final Scoped rootContext; private final Supplier task; private final Executor executor; @@ -22,6 +31,8 @@ public final class ThreadedTask implements Task { private Lifecycle lifecycle = Lifecycle.Inactive; private Return returnValue; + private final List readLog = new ArrayList<>(); + private int stepCount = 0; public ThreadedTask(final Executor executor, final Scoped rootContext, final Supplier task) { this.rootContext = Objects.requireNonNull(rootContext); @@ -31,6 +42,7 @@ public ThreadedTask(final Executor executor, final Scoped rootContext, @Override public TaskStatus step(final Scheduler scheduler) { + this.stepCount++; try { if (this.lifecycle == Lifecycle.Terminated) { return TaskStatus.completed(this.returnValue); @@ -88,6 +100,10 @@ public TaskStatus step(final Scheduler scheduler) { private void beginAsync() { final var handle = new ThreadedTaskHandle(); + if (((ExecutorService) this.executor).isShutdown()) { + throw new RuntimeException("Executor is shut down!"); + } + this.executor.execute(() -> { final TaskRequest request; try { @@ -136,7 +152,8 @@ public TaskResponse run(final TaskRequest request) { if (request instanceof TaskRequest.Resume resume) { final var scheduler = resume.scheduler; - final var context = new ThreadedReactionContext(ThreadedTask.this.rootContext, scheduler, this); + final Consumer readLogger = cacheReads ? ThreadedTask.this.readLog::add : $ -> {}; + final var context = new ThreadedReactionContext(ThreadedTask.this.rootContext, scheduler, this, readLogger); try (final var restore = ThreadedTask.this.rootContext.set(context)) { return new TaskResponse.Success<>(TaskStatus.completed(ThreadedTask.this.task.get())); @@ -246,4 +263,38 @@ public TaskAbort() { super(null, null, /* capture suppressed exceptions? */ true, /* capture stack trace? */ false); } } + + @Override + public Task duplicate(Executor executor) { + if (!cacheReads) { + throw new RuntimeException("Cannot duplicate threaded task without cached reads"); + } + final ThreadedTask threadedTask = new ThreadedTask<>(executor, rootContext, task); + final var readIterator = readLog.iterator(); + final Scheduler scheduler = new Scheduler() { + @Override + public State get(final CellId cellId) { + return (State) readIterator.next(); + } + + @Override + public void emit(final Event event, final Topic topic) { + + } + + @Override + public void spawn(final InSpan childSpan, final TaskFactory task) { + + } + }; + for (int i = 0; i < stepCount; i++) { + threadedTask.step(scheduler); + } + return threadedTask; + } + + private static String getEnv(final String key, final String fallback) { + final var env = System.getenv(key); + return env == null ? fallback : env; + } } diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java index 577cc3aeea..71f7b519ff 100644 --- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java +++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/Task.java @@ -1,16 +1,25 @@ package gov.nasa.jpl.aerie.merlin.protocol.model; import gov.nasa.jpl.aerie.merlin.protocol.driver.Scheduler; +import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InSpan; import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus; +import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; -public interface Task { +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.Consumer; +import java.util.function.Function; + +public interface Task { /** * Perform one step of the task, returning the next step of the task and the conditions under which to perform it. * *

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

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

*/ default void release() {} + + /** + * Produce a copy of this Task that can be stepped independently from this Task + * + *

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

+ * @param executor the executor to use for the new Task + * @return a copy of this Task that can be stepped independently from this Task + */ + default Task duplicate(Executor executor) { + throw new UnsupportedOperationException("Tasks must implement duplicate in order to be used in a simulation checkpoint"); + } + + default Task andThen(Task task2) { + return new Task<>() { + @Override + public TaskStatus step(final Scheduler scheduler) { + switch (Task.this.step(scheduler)) { + case TaskStatus.Completed s -> { + return task2.step(scheduler); + } + case TaskStatus.AwaitingCondition s -> { + return new TaskStatus.AwaitingCondition<>(s.condition(), s.continuation().andThen(task2)); + } + case TaskStatus.CallingTask s -> { + return new TaskStatus.CallingTask<>(s.childSpan(), s.child(), s.continuation().andThen(task2)); + } + case TaskStatus.Delayed s -> { + return new TaskStatus.Delayed<>(s.delay(), s.continuation().andThen(task2)); + } + } + } + + @Override + public Task duplicate(final Executor executor) { + return Task.this.duplicate(executor).andThen(task2.duplicate(executor)); + } + }; + } + + default Task dropOutput() { + return new Task<>() { + @Override + public TaskStatus step(final Scheduler scheduler) { + switch (this.step(scheduler)) { + case TaskStatus.Completed s -> { + return TaskStatus.completed(Unit.UNIT); + } + case TaskStatus.AwaitingCondition s -> { + return new TaskStatus.AwaitingCondition<>(s.condition(), s.continuation().dropOutput()); + } + case TaskStatus.CallingTask s -> { + return new TaskStatus.CallingTask<>(s.childSpan(), s.child(), s.continuation().dropOutput()); + } + case TaskStatus.Delayed s -> { + return new TaskStatus.Delayed<>(s.delay(), s.continuation().dropOutput()); + } + } + } + + @Override + public Task duplicate(final Executor executor) { + return Task.this.duplicate(executor).dropOutput(); + } + }; + } + + static Task calling(Task task) { + return new Task() { + @Override + public TaskStatus step(final Scheduler scheduler) { + return TaskStatus.calling(InSpan.Parent, (TaskFactory < Output >)executor -> task, Task.empty()); + } + + @Override + public Task duplicate(final Executor executor) { + return calling(task.duplicate(executor)); + } + }; + } + + static Task callingWithSpan(Task task) { + return new Task() { + @Override + public TaskStatus step(final Scheduler scheduler) { + return TaskStatus.calling(InSpan.Fresh, (TaskFactory) executor -> task, Task.empty()); + } + + @Override + public Task duplicate(final Executor executor) { + return callingWithSpan(task.duplicate(executor)); + } + }; + } + + static Task delaying(Duration duration) { + return Task.of($ -> TaskStatus.delayed(duration, Task.empty())); + } + + static Task emitting(EventType eventType, Topic topic) { + return Task.run($ -> $.emit(eventType, topic)); + } + + static Task spawning(TaskFactory taskFactory) { + return Task.run($ -> $.spawn(InSpan.Parent, taskFactory)); + } + + static Task spawning(Consumer f) { + return Task.run($ -> $.spawn(InSpan.Parent, (TaskFactory) executor -> Task.run(f))); + } + + static Task spawningWithSpan(TaskFactory taskFactory) { + return Task.run($ -> $.spawn(InSpan.Fresh, taskFactory)); + } + + static Task spawningWithSpan(Consumer f) { + return Task.run($ -> $.spawn(InSpan.Fresh, (TaskFactory) executor -> Task.run(f))); + } + + static Task spawning(List> tasks) { + return Task.run($ -> { + for (final var task : tasks) { + $.spawn(InSpan.Fresh, task); + } + }); + } + + /** + * @param f Must not yield + * @return + */ + static Task run(Consumer f) { + return Task.evaluate(scheduler -> { + f.accept(scheduler); + return Unit.UNIT; + }); + } + + static Task evaluate(Function f) { + return new Task<>() { + @Override + public TaskStatus step(final Scheduler scheduler) { + return TaskStatus.completed(f.apply(scheduler)); + } + + @Override + public Task duplicate(final Executor executor) { + return this; + } + }; + } + + static Task empty() { + return new Task<>() { + @Override + public TaskStatus step(final Scheduler scheduler) { + return TaskStatus.completed(Unit.UNIT); + } + + @Override + public Task duplicate(final Executor executor) { + return this; + } + }; + } + + static Task of(Function> f) { + return new Task() { + @Override + public TaskStatus step(final Scheduler scheduler) { + return f.apply(scheduler); + } + + @Override + public Task duplicate(final Executor executor) { + return this; + } + }; + } } diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/TaskFactory.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/TaskFactory.java index 4ec05c866c..1de8ab53bc 100644 --- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/TaskFactory.java +++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/TaskFactory.java @@ -1,13 +1,29 @@ package gov.nasa.jpl.aerie.merlin.protocol.model; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.Unit; + import java.util.concurrent.Executor; /** * A factory for creating fresh copies of a task. All tasks created by a factory must be observationally equivalent. * - * @param + * @param * The type of data returned by a task created by this factory. */ -public interface TaskFactory { - Task create(Executor executor); +public interface TaskFactory { + Task create(Executor executor); + + static TaskFactory delaying(Duration duration) { + return executor -> Task.delaying(duration); + } + + default TaskFactory andThen(TaskFactory task) { + return executor -> { + final var task1 = this.create(executor); + final var task2 = task.create(executor); + + return task1.andThen(task2); + }; + } } diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java index c0de789a34..cdcf464832 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/ConstraintAction.java @@ -212,7 +212,7 @@ public Map> getViolations(final PlanId planId, final Opt } } - final Interval bounds = Interval.betweenClosedOpen(Duration.ZERO, simDuration); + final Interval bounds = Interval.between(Duration.ZERO, simDuration); final var preparedResults = new gov.nasa.jpl.aerie.constraints.model.SimulationResults( simStartTime, bounds, diff --git a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java index 6c1c2b4c65..bd71ea6a4e 100644 --- a/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java +++ b/merlin-server/src/main/java/gov/nasa/jpl/aerie/merlin/server/services/LocalMissionModelService.java @@ -16,7 +16,6 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.jpl.aerie.merlin.server.models.ActivityDirectiveForValidation; import gov.nasa.jpl.aerie.merlin.server.models.ActivityType; -import gov.nasa.jpl.aerie.merlin.server.models.Constraint; import gov.nasa.jpl.aerie.merlin.server.models.MissionModelId; import gov.nasa.jpl.aerie.merlin.server.models.MissionModelJar; import gov.nasa.jpl.aerie.merlin.server.remotes.MissionModelRepository; @@ -32,8 +31,8 @@ import java.util.Map; import java.util.Optional; import java.util.function.Consumer; -import java.util.stream.Collectors; import java.util.function.Supplier; +import java.util.stream.Collectors; /** * Implements the missionModel service {@link MissionModelService} interface on a set of local domain objects. diff --git a/scheduler-driver/build.gradle b/scheduler-driver/build.gradle index c62f98bc9b..9204c93f1b 100644 --- a/scheduler-driver/build.gradle +++ b/scheduler-driver/build.gradle @@ -34,6 +34,8 @@ dependencies { implementation 'org.jgrapht:jgrapht-core:1.5.2' implementation 'org.slf4j:slf4j-simple:2.0.7' implementation 'org.apache.commons:commons-collections4:4.4' + implementation project(':merlin-framework') + testImplementation project(':merlin-framework-junit') testImplementation project(':constraints') testImplementation project(':examples:banananation') diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityExpression.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityExpression.java index 75b32eb809..64e5eebbbf 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityExpression.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityExpression.java @@ -445,7 +445,13 @@ public String prettyPrint(final String prefix) { ); } @Override - public void extractResources(final Set names) { } + public void extractResources(final Set names) { + if(this.durationRange != null) { + this.durationRange.getLeft().extractResources(names); + this.durationRange.getRight().extractResources(names); + } + this.arguments.forEach((name, pe)-> pe.extractResources(names)); + } public Interval instantiateDurationInterval( final PlanningHorizon planningHorizon, diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpression.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpression.java deleted file mode 100644 index 0589e7a4c5..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpression.java +++ /dev/null @@ -1,19 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.durationexpressions; - -import gov.nasa.jpl.aerie.constraints.model.SimulationResults; -import gov.nasa.jpl.aerie.constraints.time.Interval; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; - -public interface DurationExpression { - - enum DurationAnchorEnum { - WindowDuration - } - - Duration compute(final Interval interval, final SimulationResults simulationResults); - - default DurationExpression minus(DurationExpression other){ - return new DurationExpressionMinus(this, other); - } - -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionDur.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionDur.java deleted file mode 100644 index bb2c5d7025..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionDur.java +++ /dev/null @@ -1,21 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.durationexpressions; - -import gov.nasa.jpl.aerie.constraints.model.SimulationResults; -import gov.nasa.jpl.aerie.constraints.time.Interval; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; - -public class DurationExpressionDur implements DurationExpression { - - - final Duration dur; - - public DurationExpressionDur(Duration dur){ - this.dur = dur; - } - - - @Override - public Duration compute(final Interval interval, final SimulationResults simulationResults) { - return dur; - } -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionMax.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionMax.java deleted file mode 100644 index 78a903e013..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionMax.java +++ /dev/null @@ -1,28 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.durationexpressions; - -import gov.nasa.jpl.aerie.constraints.model.SimulationResults; -import gov.nasa.jpl.aerie.constraints.time.Interval; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; - -import java.util.List; - -public class DurationExpressionMax implements DurationExpression{ - - final List exprs; - - public DurationExpressionMax(DurationExpression... exprs){ - this.exprs = List.of(exprs); - } - - @Override - public Duration compute(final Interval interval, final SimulationResults simulationResults) { - var computed = new Duration[exprs.size()]; - int i = 0; - for(var expr: exprs){ - computed[i++]=expr.compute(interval, simulationResults); - } - - return Duration.max(computed); - - } -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionMinus.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionMinus.java deleted file mode 100644 index bd706bf986..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionMinus.java +++ /dev/null @@ -1,22 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.durationexpressions; - -import gov.nasa.jpl.aerie.constraints.model.SimulationResults; -import gov.nasa.jpl.aerie.constraints.time.Interval; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; - -public class DurationExpressionMinus implements DurationExpression{ - - final DurationExpression expr1; - final DurationExpression expr2; - - public DurationExpressionMinus(DurationExpression expr1, DurationExpression expr2){ - this.expr1 = expr1; - this.expr2 = expr2; - - } - - @Override - public Duration compute(final Interval interval, final SimulationResults simulationResults) { - return expr1.compute(interval, simulationResults).minus(expr2.compute(interval, simulationResults)); - } -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionRelative.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionRelative.java deleted file mode 100644 index c624aa1450..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionRelative.java +++ /dev/null @@ -1,24 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.durationexpressions; - -import gov.nasa.jpl.aerie.constraints.model.SimulationResults; -import gov.nasa.jpl.aerie.constraints.time.Interval; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; - -public class DurationExpressionRelative implements DurationExpression { - - - final DurationAnchorEnum anchor; - - public DurationExpressionRelative(DurationAnchorEnum anchor){ - this.anchor = anchor; - } - - - @Override - public Duration compute(final Interval interval, final SimulationResults simulationResults) { - if(anchor==DurationAnchorEnum.WindowDuration){ - return interval.duration(); - } - throw new IllegalArgumentException("Not implemented: Duration anchor different than WindowDuration"); - } -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionState.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionState.java deleted file mode 100644 index e52b8fa271..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressionState.java +++ /dev/null @@ -1,20 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.durationexpressions; - -import gov.nasa.jpl.aerie.constraints.model.SimulationResults; -import gov.nasa.jpl.aerie.constraints.time.Interval; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.scheduler.constraints.resources.StateQueryParam; - -public class DurationExpressionState implements DurationExpression { - - final StateQueryParam state; - - public DurationExpressionState(StateQueryParam state){ - this.state = state; - } - - @Override - public Duration compute(final Interval interval, final SimulationResults simulationResults) { - return Duration.of(state.getValue(simulationResults, null, interval).asInt().orElseThrow(), Duration.MICROSECONDS); - } -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressions.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressions.java deleted file mode 100644 index e49f2eb6f4..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/durationexpressions/DurationExpressions.java +++ /dev/null @@ -1,19 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.durationexpressions; - -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; - -public class DurationExpressions { - - public static DurationExpression constant(Duration dur){ - return new DurationExpressionDur(dur); - } - - public static DurationExpression windowDuration(){ - return new DurationExpressionRelative(DurationExpression.DurationAnchorEnum.WindowDuration); - } - - public static DurationExpression max(DurationExpression... expr){ - return new DurationExpressionMax(expr); - } - -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/ConstraintState.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/ConstraintState.java deleted file mode 100644 index 85453e64a1..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/ConstraintState.java +++ /dev/null @@ -1,33 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.scheduling; - -import gov.nasa.jpl.aerie.constraints.time.Windows; - -/** - * Class similar to Conflict but for GlobalConstraints - */ -public class ConstraintState { - - /** - * constraint concerned by this state - */ - final public GlobalConstraint constraint; - - /** - * boolean stating whether the constraint is violated or not - */ - public boolean isViolation = true; - - /** - * intervals during which the constraint is violated - */ - final public Windows violationWindows; - - //readable explanation when possible - public String cause; - - public ConstraintState(GlobalConstraint constraint, boolean isViolation, Windows violationWindows) { - this.isViolation = isViolation; - this.violationWindows = violationWindows; - this.constraint = constraint; - } -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/GlobalConstraint.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/GlobalConstraint.java deleted file mode 100644 index d12c19a32a..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/GlobalConstraint.java +++ /dev/null @@ -1,19 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.constraints.scheduling; - -import gov.nasa.jpl.aerie.constraints.model.SimulationResults; -import gov.nasa.jpl.aerie.constraints.time.Windows; -import gov.nasa.jpl.aerie.scheduler.model.Plan; - -/** - * Interface defining methods that must be implemented by global constraints such as mutex or cardinality - * Also provides a directory for creating these constraints - */ -public interface GlobalConstraint { - - //todo: probably needs a domain - - //is the constraint enforced on its domain - ConstraintState isEnforced(Plan plan, Windows windows, SimulationResults simulationResults); - - -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/GlobalConstraintWithIntrospection.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/GlobalConstraintWithIntrospection.java index aaac4a1bed..e8d0eb55cd 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/GlobalConstraintWithIntrospection.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/GlobalConstraintWithIntrospection.java @@ -6,15 +6,15 @@ import gov.nasa.jpl.aerie.scheduler.model.Plan; import gov.nasa.jpl.aerie.scheduler.conflicts.Conflict; +import java.util.Set; + /** * Interface defining methods that must be implemented by global constraints such as mutex or cardinality * Also provides a directory for creating these constraints */ -public interface GlobalConstraintWithIntrospection extends GlobalConstraint { - +public interface GlobalConstraintWithIntrospection { //specific to introspectable constraint : find the windows in which we can insert activities without violating //the constraint Windows findWindows(Plan plan, Windows windows, Conflict conflict, SimulationResults simulationResults, EvaluationEnvironment evaluationEnvironment); - - + void extractResources(Set names); } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelative.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelative.java index 0f5ac45567..844e07a96a 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelative.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelative.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Set; /** * class allowing to define dynamic expressions of timepoints, relative to time anchors @@ -25,6 +26,7 @@ public abstract class TimeExpressionRelative { */ public abstract Interval computeTime(final SimulationResults simulationResults, final Plan plan, final Interval interval); public abstract Optional getAnchor(); + public abstract void extractResources(final Set names); protected final List> operations = new ArrayList<>(); diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeBefore.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeBefore.java index c64fc8bcc5..344f726836 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeBefore.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeBefore.java @@ -7,6 +7,7 @@ import gov.nasa.jpl.aerie.scheduler.model.Plan; import java.util.Optional; +import java.util.Set; public class TimeExpressionRelativeBefore extends TimeExpressionRelative { @@ -38,4 +39,9 @@ public Interval computeTime(final SimulationResults simulationResults, final Pla public Optional getAnchor() { return Optional.empty(); } + + @Override + public void extractResources(final Set names) { + expr.extractResources(names); + } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeBinary.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeBinary.java index d444f4eb84..f15adb4a84 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeBinary.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeBinary.java @@ -4,6 +4,7 @@ import gov.nasa.jpl.aerie.constraints.time.Interval; import gov.nasa.jpl.aerie.scheduler.model.Plan; +import java.util.Set; import java.util.Optional; public class TimeExpressionRelativeBinary extends TimeExpressionRelative { @@ -27,4 +28,10 @@ public Optional getAnchor(){ return Optional.empty(); } + + @Override + public void extractResources(final Set names) { + lowerBound.extractResources(names); + upperBound.extractResources(names); + } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeSimple.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeSimple.java index 2d010e42d6..b0bf30a98f 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeSimple.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/timeexpressions/TimeExpressionRelativeSimple.java @@ -6,6 +6,7 @@ import gov.nasa.jpl.aerie.scheduler.TimeUtility; import gov.nasa.jpl.aerie.scheduler.model.Plan; +import java.util.Set; import java.util.Optional; public class TimeExpressionRelativeSimple extends TimeExpressionRelative { @@ -57,4 +58,7 @@ public Interval computeTimeRelativeAbsolute(final Interval interval) { return retRange; } + + @Override + public void extractResources(final Set names) {} } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/ActivityTemplateGoal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/ActivityTemplateGoal.java index 6c84075509..55b80f8c73 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/ActivityTemplateGoal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/ActivityTemplateGoal.java @@ -1,15 +1,10 @@ package gov.nasa.jpl.aerie.scheduler.goals; -import gov.nasa.jpl.aerie.constraints.model.EvaluationEnvironment; import gov.nasa.jpl.aerie.constraints.time.Windows; import gov.nasa.jpl.aerie.constraints.tree.Expression; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; -import gov.nasa.jpl.aerie.scheduler.model.Plan; -import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; -import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; -import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; -import java.util.Optional; +import java.util.Set; /** * describes the desired existence of an activity matching a given template/preset @@ -74,9 +69,6 @@ protected ActivityTemplateGoal fill(ActivityTemplateGoal goal) { } else { goal.matchActTemplate = matchingActTemplate; } - - goal.initiallyEvaluatedTemporalContext = null; - return goal; } @@ -116,12 +108,10 @@ protected ActivityTemplateGoal() { } */ protected ActivityExpression matchActTemplate; - - /** - * checked by getConflicts every time it is invoked to see if the Window(s) - * corresponding to when this goal has changed, which is unexpected behavior - * that needs to be caught - */ - protected Windows initiallyEvaluatedTemporalContext; - + @Override + public void extractResources(final Set names) { + super.extractResources(names); + matchActTemplate.extractResources(names); + desiredActTemplate.extractResources(names); + } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CardinalityGoal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CardinalityGoal.java index 5a16b0edc1..9b0b7055e1 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CardinalityGoal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CardinalityGoal.java @@ -136,14 +136,6 @@ public Collection getConflicts( //unwrap temporalContext final var windows = getTemporalContext().evaluate(simulationResults, evaluationEnvironment); - //make sure it hasn't changed - if (this.initiallyEvaluatedTemporalContext != null && !windows.equals(this.initiallyEvaluatedTemporalContext)) { - throw new UnexpectedTemporalContextChangeException("The temporalContext Windows has changed from: " + this.initiallyEvaluatedTemporalContext.toString() + " to " + windows.toString()); - } - else if (this.initiallyEvaluatedTemporalContext == null) { - this.initiallyEvaluatedTemporalContext = windows; - } - //iterate through it and then within each iteration do exactly what you did before final var conflicts = new LinkedList(); diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CoexistenceGoal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CoexistenceGoal.java index 15f5752ae9..4ba2f1ec2c 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CoexistenceGoal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CoexistenceGoal.java @@ -14,7 +14,6 @@ import gov.nasa.jpl.aerie.scheduler.conflicts.MissingActivityTemplateConflict; import gov.nasa.jpl.aerie.scheduler.conflicts.MissingAssociationConflict; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; -import gov.nasa.jpl.aerie.scheduler.constraints.durationexpressions.DurationExpression; import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeAnchor; import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeExpressionRelative; import gov.nasa.jpl.aerie.scheduler.model.PersistentTimeAnchor; @@ -22,15 +21,13 @@ import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; import org.apache.commons.collections4.BidiMap; -import gov.nasa.jpl.aerie.scheduler.solver.stn.TaskNetworkAdapter; import java.util.ArrayList; import java.util.HashMap; import java.util.Objects; import java.util.List; import java.util.Optional; - -import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; +import java.util.Set; /** * describes the desired coexistence of an activity with another @@ -39,7 +36,6 @@ public class CoexistenceGoal extends ActivityTemplateGoal { private TimeExpressionRelative startExpr; private TimeExpressionRelative endExpr; - private DurationExpression durExpr; private String alias; private PersistentTimeAnchor persistentAnchor; /** @@ -47,10 +43,6 @@ public class CoexistenceGoal extends ActivityTemplateGoal { */ protected Expression expr; - /** - * used to check this hasn't changed, as if it did, that's probably unanticipated behavior - */ - protected Spans evaluatedExpr; /** * the builder can construct goals piecemeal via a series of method calls */ @@ -68,12 +60,6 @@ public Builder startsAt(TimeExpressionRelative TimeExpressionRelative) { return getThis(); } - protected DurationExpression durExpression; - public Builder durationIn(DurationExpression durExpr){ - this.durExpression = durExpr; - return getThis(); - } - protected TimeExpressionRelative startExpr; public Builder endsAt(TimeExpressionRelative TimeExpressionRelative) { @@ -171,8 +157,6 @@ protected CoexistenceGoal fill(CoexistenceGoal goal) { goal.endExpr = endExpr; - goal.durExpr = durExpression; - goal.alias = alias; goal.persistentAnchor = Objects.requireNonNullElse(persistentAnchor, PersistentTimeAnchor.DISABLED); @@ -202,34 +186,11 @@ public java.util.Collection getConflicts( final EvaluationEnvironment evaluationEnvironment, final SchedulerModel schedulerModel) { //TODO: check if interval gets split and if so, notify user? - //NOTE: temporalContext IS A WINDOWS OVER WHICH THE GOAL APPLIES, USUALLY SOMETHING BROAD LIKE A MISSION PHASE - //NOTE: expr IS A WINDOWS OVER WHICH A COEXISTENCEGOAL APPLIES, FOR EXAMPLE THE WINDOWS CORRESPONDING TO 5 SECONDS AFTER EVERY BASICACTIVITY IS SCHEDULED - //NOTE: IF temporalContext IS SMALLER THAN expr OR SOMEHOW BISECTS IT, ODDS ARE THIS ISN'T ANTICIPATED USER BEHAVIOR. GENERALLY, ANALYZEWHEN SHOULDN'T BE PROVIDING - // A SMALLER WINDOW, AND HONESTLY DOESN'T MAKE SENSE TO USE ON TOP BUT IS SUPPORTED TO MAKE CODE MORE CONSISTENT. IF ONE NEEDS TO USE ANALYZEWHEN ON TOP - // OF COEXISTENCEGOAL THEY SHOULD PROBABLY REFACTOR THEIR COEXISTENCE GOAL. ONE SUCH USE WOULD BE IF THE COEXISTENCEGOAL WAS SPECIFIED IN TERMS OF - // AN ACTIVITYEXPRESSION AND THEN ANALYZEWHEN WAS A MISSION PHASE, ALTHOUGH IT IS POSSIBLE TO JUST SPECIFY AN EXPRESSION THAT COMBINES THOSE. - //unwrap temporalContext final var windows = getTemporalContext().evaluate(simulationResults, evaluationEnvironment); - //make sure it hasn't changed - if (this.initiallyEvaluatedTemporalContext != null && !windows.includes(this.initiallyEvaluatedTemporalContext)) { - throw new UnexpectedTemporalContextChangeException("The temporalContext Windows has changed from: " + this.initiallyEvaluatedTemporalContext.toString() + " to " + windows); - } - else if (this.initiallyEvaluatedTemporalContext == null) { - this.initiallyEvaluatedTemporalContext = windows; - } - final var anchors = expr.evaluate(simulationResults, evaluationEnvironment).intersectWith(windows); - //make sure expr hasn't changed either as that could yield unexpected behavior - if (this.evaluatedExpr != null && !anchors.isCollectionSubsetOf(this.evaluatedExpr)) { - throw new UnexpectedTemporalContextChangeException("The expr Windows has changed from: " + this.expr.toString() + " to " + anchors); - } - else if (this.initiallyEvaluatedTemporalContext == null) { - this.evaluatedExpr = anchors; - } - // can only check if bisection has happened if you can extract the interval from expr like you do in computeRange but without the final windows parameter, // then use that and compare it to local variable windows to check for bisection; // I can add that, but it doesn't seem necessary for now. @@ -266,12 +227,6 @@ else if (this.initiallyEvaluatedTemporalContext == null) { activityFinder.endsIn(endTimeRange); activityCreationTemplate.endsIn(endTimeRange); } - /* this will override whatever might be already present in the template */ - if (durExpr != null) { - var durRange = this.durExpr.compute(window.interval(), simulationResults); - activityFinder.durationIn(durRange); - activityCreationTemplate.durationIn(durRange); - } final var activitiesFound = plan.find( activityFinder.build(), @@ -412,6 +367,14 @@ private EvaluationEnvironment createEvaluationEnvironmentFromAnchor(EvaluationEn } } + @Override + public void extractResources(final Set names) { + super.extractResources(names); + this.expr.extractResources(names); + if(this.startExpr != null) this.startExpr.extractResources(names); + if(this.endExpr != null) this.endExpr.extractResources(names); + } + /** * ctor creates an empty goal without details * diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CompositeAndGoal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CompositeAndGoal.java index cb23dc9e3e..600d52ce31 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CompositeAndGoal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CompositeAndGoal.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; /** * Class representing a conjunction of goal as a goal @@ -38,5 +39,11 @@ public List getSubgoals() { return goals; } - + @Override + public void extractResources(final Set names) { + super.extractResources(names); + for(final var goal: goals){ + goal.extractResources(names); + } + } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Goal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Goal.java index ac375ec1a0..8d10f333a0 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Goal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Goal.java @@ -18,6 +18,7 @@ import java.util.LinkedList; import java.util.List; +import java.util.Set; import java.util.Optional; /** @@ -253,6 +254,10 @@ protected Goal fill(Goal goal) { }//Builder + public void extractResources(Set names) { + temporalContext.extractResources(names); + if(resourceConstraints != null) resourceConstraints.extractResources(names); + } /** * fetches the human-legible identifier of the goal diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/OptionGoal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/OptionGoal.java index 4cac7cca10..50f4d57f49 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/OptionGoal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/OptionGoal.java @@ -13,6 +13,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.Optional; public class OptionGoal extends Goal { @@ -43,6 +44,14 @@ public java.util.Collection getConflicts( throw new NotImplementedException("Conflict detection is performed at solver level"); } + @Override + public void extractResources(final Set names) { + super.extractResources(names); + for(final var goal: goals){ + goal.extractResources(names); + } + } + public static class Builder extends Goal.Builder { final List goals = new ArrayList<>(); diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/RecurrenceGoal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/RecurrenceGoal.java index c5168ad05f..36995145e4 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/RecurrenceGoal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/RecurrenceGoal.java @@ -133,14 +133,6 @@ public java.util.Collection getConflicts( } } - //make sure it hasn't changed - if (this.initiallyEvaluatedTemporalContext != null && !windows.includes(this.initiallyEvaluatedTemporalContext)) { - throw new UnexpectedTemporalContextChangeException("The temporalContext Windows has changed from: " + this.initiallyEvaluatedTemporalContext.toString() + " to " + windows); - } - else if (this.initiallyEvaluatedTemporalContext == null) { - this.initiallyEvaluatedTemporalContext = windows; - } - //iterate through it and then within each iteration do exactly what you did before for (Interval subInterval : windows.iterateEqualTo(true)) { //collect all matching target acts ordered by start time diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Plan.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Plan.java index 2d6963aec0..c263617651 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Plan.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Plan.java @@ -19,6 +19,12 @@ */ public interface Plan { + /** + * Duplicates a plan + * @return the duplicate plan + */ + Plan duplicate(); + /** * adds the given activity instances to the scheduled plan solution * @@ -54,11 +60,6 @@ public interface Plan { */ void remove(SchedulingActivityDirective act); - /** - * fetches activities in the plan ordered by start time - * - * @return set of all activities in the plan ordered by start time - */ /** * replace and old activity by a new one * @param oldAct Old Activity @@ -66,6 +67,11 @@ public interface Plan { */ void replaceActivity(SchedulingActivityDirective oldAct, SchedulingActivityDirective newAct); + /** + * fetches activities in the plan ordered by start time + * + * @return set of all activities in the plan ordered by start time + */ List getActivitiesByTime(); /** diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/PlanInMemory.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/PlanInMemory.java index f15d9e9503..a7a3c440a1 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/PlanInMemory.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/PlanInMemory.java @@ -6,6 +6,7 @@ import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; import gov.nasa.jpl.aerie.scheduler.solver.Evaluation; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -32,35 +33,30 @@ public class PlanInMemory implements Plan { */ protected Evaluation evaluation; - /** - * container of all activity instances in plan, indexed by name - */ - private final HashMap actsById - = new HashMap<>(); - - /** - * container of all activity instances in plan, indexed by type - */ - private final HashMap> actsByType - = new HashMap<>(); - /** * container of all activity instances in plan, indexed by start time */ - private final TreeMap> actsByTime - = new TreeMap<>(); - - /** - * container of all activity instances in plan - */ - private final HashSet actsSet - = new HashSet<>(); + private final TreeMap> actsByTime; /** * ctor creates a new empty solution plan * */ public PlanInMemory() { + this.actsByTime = new TreeMap<>(); + } + + public PlanInMemory(final PlanInMemory other){ + if(other.evaluation != null) this.evaluation = other.evaluation.duplicate(); + this.actsByTime = new TreeMap<>(); + for(final var entry: other.actsByTime.entrySet()){ + this.actsByTime.put(entry.getKey(), new ArrayList<>(entry.getValue())); + } + } + + @Override + public Plan duplicate() { + return new PlanInMemory(this); } /** @@ -73,12 +69,19 @@ public void add(Collection acts) { } } + public int size(){ + int size = 0; + for(final var entry: this.actsByTime.entrySet()){ + size += entry.getValue().size(); + } + return size; + } /** * {@inheritDoc} */ @Override - public void add(SchedulingActivityDirective act) { + public void add(final SchedulingActivityDirective act) { if (act == null) { throw new IllegalArgumentException( "adding null activity to plan"); @@ -90,20 +93,8 @@ public void add(SchedulingActivityDirective act) { } final var id = act.getId(); assert id != null; - if (actsById.containsKey(id)) { - throw new IllegalArgumentException( - "adding activity with duplicate name=" + id + " to plan"); - } - final var type = act.getType(); - assert type != null; - - actsById.put(id, act); - //REVIEW: use a cleaner multimap? maybe guava actsByTime.computeIfAbsent(startT, k -> new LinkedList<>()) .add(act); - actsByType.computeIfAbsent(type, k -> new LinkedList<>()) - .add(act); - actsSet.add(act); } @Override @@ -115,13 +106,8 @@ public void remove(Collection acts) { @Override public void remove(SchedulingActivityDirective act) { - //TODO: handle ownership. Constraint propagation ? - actsById.remove(act.getId()); var acts = actsByTime.get(act.startOffset()); if (acts != null) acts.remove(act); - acts = actsByType.get(act.getType()); - if (acts != null) acts.remove(act); - actsSet.remove(act); } /** @@ -144,7 +130,7 @@ public List getActivitiesByTime() { public void replaceActivity(SchedulingActivityDirective oldAct, SchedulingActivityDirective newAct){ this.remove(oldAct); this.add(newAct); - this.evaluation.updateGoalEvals(oldAct, newAct); + if(evaluation != null) this.evaluation.updateGoalEvals(oldAct, newAct); } /** @@ -152,17 +138,29 @@ public void replaceActivity(SchedulingActivityDirective oldAct, SchedulingActivi */ @Override public Map> getActivitiesByType() { - return Collections.unmodifiableMap(actsByType); + final var map = new HashMap>(); + for(final var entry: this.actsByTime.entrySet()){ + for(final var activity : entry.getValue()){ + map.computeIfAbsent(activity.type(), t -> new ArrayList<>()).add(activity); + } + } + return Collections.unmodifiableMap(map); } @Override public Map getActivitiesById() { - return Collections.unmodifiableMap(actsById); + final var map = new HashMap(); + for(final var entry: this.actsByTime.entrySet()){ + for(final var activity : entry.getValue()){ + map.put(activity.id(), activity); + } + } + return Collections.unmodifiableMap(map); } @Override public Set getAnchorIds() { - return actsSet.stream() + return getActivities().stream() .map(SchedulingActivityDirective::anchorId) .collect(Collectors.toSet()); } @@ -172,7 +170,11 @@ public Set getAnchorIds() { */ @Override public Set getActivities() { - return Collections.unmodifiableSet(actsSet); + final var set = new HashSet(); + for(final var entry: this.actsByTime.entrySet()){ + set.addAll(entry.getValue()); + } + return Collections.unmodifiableSet(set); } /** diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Problem.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Problem.java index 8b9f39bf61..c0a6774aa7 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Problem.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/Problem.java @@ -6,10 +6,10 @@ import gov.nasa.jpl.aerie.merlin.driver.MissionModel; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; -import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.GlobalConstraint; +import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.GlobalConstraintWithIntrospection; import gov.nasa.jpl.aerie.scheduler.goals.Goal; -import gov.nasa.jpl.aerie.scheduler.simulation.SimulationData; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; +import gov.nasa.jpl.aerie.scheduler.simulation.SimulationData; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationResultsConverter; import org.apache.commons.collections4.BidiMap; @@ -44,7 +44,7 @@ public class Problem { /** * global constraints in the mission model, indexed by name */ - private final List globalConstraints + private final List globalConstraints = new java.util.LinkedList<>(); private final Map realExternalProfiles = new HashMap<>(); @@ -76,7 +76,11 @@ public class Problem { * * @param mission IN the mission model that this problem is based on */ - public Problem(MissionModel mission, PlanningHorizon planningHorizon, SimulationFacade simulationFacade, SchedulerModel schedulerModel) { + public Problem( + MissionModel mission, + PlanningHorizon planningHorizon, + SimulationFacade simulationFacade, + SchedulerModel schedulerModel) { this.missionModel = mission; this.schedulerModel = schedulerModel; this.initialPlan = new PlanInMemory(); @@ -89,7 +93,7 @@ public Problem(MissionModel mission, PlanningHorizon planningHorizon, Simulat } this.simulationFacade = simulationFacade; if(this.simulationFacade != null) { - this.simulationFacade.setActivityTypes(this.getActivityTypes()); + this.simulationFacade.addActivityTypes(this.getActivityTypes()); } this.initialSimulationResults = Optional.empty(); } @@ -109,11 +113,11 @@ public PlanningHorizon getPlanningHorizon(){ * * @param globalConstraint IN the global constraint */ - public void add(GlobalConstraint globalConstraint) { + public void add(GlobalConstraintWithIntrospection globalConstraint) { this.globalConstraints.add(globalConstraint); } - public List getGlobalConstraints() { + public List getGlobalConstraints() { return this.globalConstraints; } @@ -140,12 +144,16 @@ public Plan getInitialPlan() { * @param initialSimulationResults optional initial simulation results associated to the initial plan * @param plan the initial seed plan that schedulers may start from */ - public void setInitialPlan(final Plan plan, final Optional initialSimulationResults, final BidiMap mapSchedulingIdsToActivityIds) { + public void setInitialPlan( + final Plan plan, + final Optional initialSimulationResults, + final BidiMap mapSchedulingIdsToActivityIds) { initialPlan = plan; this.initialSimulationResults = initialSimulationResults.map(simulationResults -> new SimulationData( + plan, simulationResults, - SimulationResultsConverter.convertToConstraintModelResults( - simulationResults), Optional.ofNullable(mapSchedulingIdsToActivityIds))); + SimulationResultsConverter.convertToConstraintModelResults(simulationResults), + Optional.ofNullable(mapSchedulingIdsToActivityIds))); } /** diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulePlanGrounder.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulePlanGrounder.java new file mode 100644 index 0000000000..7cf9e621cb --- /dev/null +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulePlanGrounder.java @@ -0,0 +1,72 @@ +package gov.nasa.jpl.aerie.scheduler.model; + +import gov.nasa.jpl.aerie.constraints.model.ActivityInstance; +import gov.nasa.jpl.aerie.constraints.time.Interval; +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; +import gov.nasa.jpl.aerie.merlin.driver.StartOffsetReducer; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class SchedulePlanGrounder { + public static Optional> groundSchedule( + final List schedulingActivityDirectiveList, + final Duration planDuration + ){ + final var grounded = new HashMap(); + + final var idMap = schedulingActivityDirectiveList + .stream() + .map(a -> Pair.of(new ActivityDirectiveId(a.getId().id()), a)) + .collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); + + final var converted = schedulingActivityDirectiveList + .stream() + .map(a -> Pair.of( + new ActivityDirectiveId(a.id().id()), + new ActivityDirective( + a.startOffset(), + a.type().getName(), + a.arguments(), + (a.anchorId() == null) ? null : new ActivityDirectiveId(a.anchorId().id()), + a.anchoredToStart() + ))) + .collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); + final var converter = new StartOffsetReducer(planDuration, converted); + var computed = converter.compute(); + computed = StartOffsetReducer.filterOutNegativeStartOffset(computed); + + for(final var directive: computed.entrySet()){ + Duration offset = Duration.ZERO; + final var idActivity = directive.getKey(); + if(idActivity != null){ + if(grounded.get(idActivity) == null){ + return Optional.empty(); + } else { + final var alreadyGroundedAct = grounded.get(idActivity); + offset = alreadyGroundedAct.interval.end; + } + } + for(final Pair dependentDirective : directive.getValue()) { + final var dependentId = dependentDirective.getKey(); + final var dependentOriginalActivity = idMap.get(dependentId); + final var startTime = offset.plus(dependentDirective.getValue()); + //happens only in tests + if(dependentOriginalActivity.duration() == null){ + return Optional.empty(); + } + grounded.put(dependentId, new ActivityInstance( + dependentId.id(), + dependentOriginalActivity.type().getName(), + dependentOriginalActivity.arguments(), + Interval.between(startTime, startTime.plus(dependentOriginalActivity.duration())))); + } + } + return Optional.of(grounded.values().stream().toList()); + } +} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingActivityDirective.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingActivityDirective.java index aaeb28c314..87550b9011 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingActivityDirective.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingActivityDirective.java @@ -85,6 +85,17 @@ public static SchedulingActivityDirective of(ActivityType type, Duration startOf Map.of(), null, anchorId, anchoredToStart); } + public static SchedulingActivityDirective of(SchedulingActivityDirectiveId id, ActivityType type, Duration startOffset, Duration duration, SchedulingActivityDirectiveId anchorId, boolean anchoredToStart) { + return new SchedulingActivityDirective( + id, + type, + startOffset, + duration, + Map.of(), + null, + anchorId, + anchoredToStart); + } public static SchedulingActivityDirective of(ActivityType type, Duration startOffset, Duration duration, Map parameters, SchedulingActivityDirectiveId anchorId, boolean anchoredToStart) { return new SchedulingActivityDirective(new SchedulingActivityDirectiveId(uniqueId.getAndIncrement()), type, @@ -264,7 +275,7 @@ public ActivityType getType() { } public String toString() { - return "[" + this.type.getName() + ","+ this.id + "," + startOffset + "," + ((duration != null) ? getEndTime() : "no duration") + ", "+anchorId+", "+anchoredToStart+"]"; + return "[" + this.type.getName() + ","+ this.id + "," + startOffset + "," + ((duration != null) ? getEndTime() : "no duration") + ", "+ topParent + ", " + anchorId+", "+anchoredToStart+"]"; } /** @@ -277,6 +288,7 @@ public boolean equalsInProperties(final SchedulingActivityDirective that){ && duration.isEqualTo(that.duration) && startOffset.isEqualTo(that.startOffset) && arguments.equals(that.arguments) + && Objects.equals(topParent, that.topParent) && Objects.equals(anchorId, that.anchorId) && (anchoredToStart == that.anchoredToStart); } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingCondition.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingCondition.java index 29903aeb01..65a5567759 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingCondition.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/model/SchedulingCondition.java @@ -7,10 +7,10 @@ import gov.nasa.jpl.aerie.scheduler.conflicts.Conflict; import gov.nasa.jpl.aerie.scheduler.conflicts.MissingActivityInstanceConflict; import gov.nasa.jpl.aerie.scheduler.conflicts.MissingActivityTemplateConflict; -import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.ConstraintState; import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.GlobalConstraintWithIntrospection; import java.util.List; +import java.util.Set; public record SchedulingCondition( Expression expression, @@ -41,18 +41,11 @@ public Windows findWindows( } @Override - public ConstraintState isEnforced( - final Plan plan, - final Windows windows, - final SimulationResults simulationResults) - { - // A SchedulingCondition is never "violated" per se - if there are no windows in which - // activities can be placed, that does not mean that it has been violated. - // TODO: As of writing isEnforced is unused. Either remove or come up with a more coherent plan for GlobalConstraints. - return new ConstraintState(this, false, null); + public void extractResources(final Set names) { + this.expression.extractResources(names); } - static boolean anyMatch(final List activityTypes, final ActivityType type) { + private static boolean anyMatch(final List activityTypes, final ActivityType type) { // TODO we may want to handle more complex activity expressions, not just type. for (final var activityType : activityTypes) { if (type.equals(activityType)) { diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacade.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacade.java new file mode 100644 index 0000000000..df66f6a55e --- /dev/null +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacade.java @@ -0,0 +1,340 @@ +package gov.nasa.jpl.aerie.scheduler.simulation; + +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; +import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; +import gov.nasa.jpl.aerie.merlin.driver.SimulationResultsComputerInputs; +import gov.nasa.jpl.aerie.merlin.framework.ThreadedTask; +import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.scheduler.SchedulingInterruptedException; +import gov.nasa.jpl.aerie.scheduler.model.ActivityType; +import gov.nasa.jpl.aerie.scheduler.model.Plan; +import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; +import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Instant; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +import static gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver.onceAllActivitiesAreFinished; +import static gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacadeUtils.scheduleFromPlan; +import static gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacadeUtils.updatePlanWithChildActivities; + +public class CheckpointSimulationFacade implements SimulationFacade { + private static final Logger LOGGER = LoggerFactory.getLogger(CheckpointSimulationFacade.class); + private final MissionModel missionModel; + private final InMemoryCachedEngineStore cachedEngines; + private final PlanningHorizon planningHorizon; + private final Map activityTypes; + private final SimulationEngineConfiguration configuration; + private SimulationData initialSimulationResults; + private final Supplier canceledListener; + private final SchedulerModel schedulerModel; + private Duration totalSimulationTime = Duration.ZERO; + private SimulationData latestSimulationData; + + /** + * Loads initial simulation results into the simulation. They will be served until initialSimulationResultsAreStale() + * is called. + * @param simulationData the initial simulation results + */ + @Override + public void setInitialSimResults(final SimulationData simulationData){ + this.initialSimulationResults = simulationData; + } + + + public CheckpointSimulationFacade( + final MissionModel missionModel, + final SchedulerModel schedulerModel, + final InMemoryCachedEngineStore cachedEngines, + final PlanningHorizon planningHorizon, + final SimulationEngineConfiguration simulationEngineConfiguration, + final Supplier canceledListener){ + if(cachedEngines.capacity() > 1) ThreadedTask.CACHE_READS = true; + this.missionModel = missionModel; + this.schedulerModel = schedulerModel; + this.cachedEngines = cachedEngines; + this.planningHorizon = planningHorizon; + this.activityTypes = new HashMap<>(); + this.configuration = simulationEngineConfiguration; + this.canceledListener = canceledListener; + this.latestSimulationData = null; + } + + public CheckpointSimulationFacade( + final PlanningHorizon planningHorizon, + final MissionModel missionModel, + final SchedulerModel schedulerModel + ){ + this( + missionModel, + schedulerModel, + new InMemoryCachedEngineStore(1), + planningHorizon, + new SimulationEngineConfiguration(Map.of(), Instant.now(), new MissionModelId(1)), + ()-> false + ); + } + + /** + * Returns the total simulated time + * @return + */ + @Override + public Duration totalSimulationTime(){ + return totalSimulationTime; + } + + @Override + public Supplier getCanceledListener(){ + return this.canceledListener; + } + + @Override + public void addActivityTypes(final Collection activityTypes){ + activityTypes.forEach(at -> this.activityTypes.put(at.getName(), at)); + } + + private void replaceValue(final Map map, final V value, final V replacement){ + for (final Map.Entry entry : map.entrySet()) { + if (entry.getValue().equals(value)) { + entry.setValue(replacement); + break; + } + } + } + + private void replaceIds( + final PlanSimCorrespondence planSimCorrespondence, + final Map updates){ + for(final var replacements : updates.entrySet()){ + replaceValue(planSimCorrespondence.planActDirectiveIdToSimulationActivityDirectiveId(),replacements.getKey(), replacements.getValue()); + if(planSimCorrespondence.directiveIdActivityDirectiveMap().containsKey(replacements.getKey())){ + final var value = planSimCorrespondence.directiveIdActivityDirectiveMap().remove(replacements.getKey()); + planSimCorrespondence.directiveIdActivityDirectiveMap().put(replacements.getValue(), value); + } + //replace the anchor ids in the plan + final var replacementMap = new HashMap(); + for(final var act : planSimCorrespondence.directiveIdActivityDirectiveMap().entrySet()){ + if(act.getValue().anchorId() != null && act.getValue().anchorId().equals(replacements.getKey())){ + final var replacementActivity = new ActivityDirective(act.getValue().startOffset(), act.getValue().serializedActivity(), replacements.getValue(), act.getValue().anchoredToStart()); + replacementMap.put(act.getKey(), replacementActivity); + } + } + for(final var replacement: replacementMap.entrySet()){ + planSimCorrespondence.directiveIdActivityDirectiveMap().remove(replacement.getKey()); + planSimCorrespondence.directiveIdActivityDirectiveMap().put(replacement.getKey(), replacement.getValue()); + } + } + } + + /** + * Simulates until the end of the last activity of a plan. Updates the input plan with child activities and activity durations. + * @param plan the plan to simulate + * @return the inputs needed to compute simulation results + * @throws SimulationException if an exception happens during simulation + */ + @Override + public SimulationResultsComputerInputs simulateNoResultsAllActivities(final Plan plan) + throws SimulationException, SchedulingInterruptedException + { + return simulateNoResults(plan, null, null).simulationResultsComputerInputs(); + } + + /** + * Simulates a plan until the end of one of its activities + * Do not use to update the plan as decomposing activities may not finish + * @param plan + * @param activity + * @return + * @throws SimulationException + */ + + @Override + public SimulationResultsComputerInputs simulateNoResultsUntilEndAct( + final Plan plan, + final SchedulingActivityDirective activity) throws SimulationException, SchedulingInterruptedException + { + return simulateNoResults(plan, null, activity).simulationResultsComputerInputs(); + } + + public AugmentedSimulationResultsComputerInputs simulateNoResults( + final Plan plan, + final Duration until) throws SimulationException, SchedulingInterruptedException + { + return simulateNoResults(plan, until, null); + } + + + /** + * Simulates and updates plan + * @param plan + * @param until can be null + * @param activity can be null + */ + private AugmentedSimulationResultsComputerInputs simulateNoResults( + final Plan plan, + final Duration until, + final SchedulingActivityDirective activity) throws SimulationException, SchedulingInterruptedException + { + final var planSimCorrespondence = scheduleFromPlan(plan, this.schedulerModel); + + final var best = CheckpointSimulationDriver.bestCachedEngine( + planSimCorrespondence.directiveIdActivityDirectiveMap(), + cachedEngines.getCachedEngines(configuration), + planningHorizon.getEndAerie()); + CheckpointSimulationDriver.CachedSimulationEngine engine = null; + Duration from = Duration.ZERO; + if(best.isPresent()){ + engine = best.get().getKey(); + replaceIds(planSimCorrespondence, best.get().getRight()); + from = engine.endsAt(); + } + + //Configuration + //Three modes : (1) until a specific end time (2) until end of one specific activity (3) until end of last activity in plan + Duration simulationDuration; + Function + stoppingCondition; + //(1) + if(until != null && activity == null){ + simulationDuration = until; + stoppingCondition = CheckpointSimulationDriver.noCondition(); + LOGGER.info("Simulation mode: until specific time " + simulationDuration); + } + //(2) + else if(activity != null && until == null){ + simulationDuration = planningHorizon.getEndAerie(); + stoppingCondition = CheckpointSimulationDriver.stopOnceActivityHasFinished( + planSimCorrespondence.planActDirectiveIdToSimulationActivityDirectiveId().get(activity.id())); + LOGGER.info("Simulation mode: until activity ends " + activity); + //(3) + } else if(activity == null && until == null){ + simulationDuration = planningHorizon.getEndAerie(); + stoppingCondition = CheckpointSimulationDriver.onceAllActivitiesAreFinished(); + LOGGER.info("Simulation mode: until all activities end "); + } else { + throw new SimulationException("Bad configuration", null); + } + + if(engine == null) engine = CheckpointSimulationDriver.CachedSimulationEngine.empty(missionModel); + + Function checkpointPolicy = new ResourceAwareSpreadCheckpointPolicy( + cachedEngines.capacity(), + Duration.ZERO, + planningHorizon.getEndAerie(), + Duration.max(engine.endsAt(), Duration.ZERO), + simulationDuration, + 1, + true); + + if(stoppingCondition.equals(CheckpointSimulationDriver.onceAllActivitiesAreFinished())){ + checkpointPolicy = or(checkpointPolicy, onceAllActivitiesAreFinished()); + } + + if(best.isPresent()) cachedEngines.registerUsed(engine); + try { + final var simulation = CheckpointSimulationDriver.simulateWithCheckpoints( + missionModel, + planSimCorrespondence.directiveIdActivityDirectiveMap(), + planningHorizon.getStartInstant(), + simulationDuration, + planningHorizon.getStartInstant(), + planningHorizon.getEndAerie(), + $ -> {}, + canceledListener, + engine, + checkpointPolicy, + stoppingCondition, + cachedEngines, + configuration + ); + if(canceledListener.get()) throw new SchedulingInterruptedException("simulating"); + this.totalSimulationTime = this.totalSimulationTime.plus(simulation.elapsedTime().minus(from)); + final var activityResults = simulation.computeActivitySimulationResults(); + + updatePlanWithChildActivities( + activityResults, + activityTypes, + plan, + planSimCorrespondence, + planningHorizon); + + SimulationFacadeUtils.pullActivityDurationsIfNecessary( + plan, + planSimCorrespondence, + activityResults + ); + //plan has been updated + return new AugmentedSimulationResultsComputerInputs(simulation, planSimCorrespondence); + } catch (SchedulingInterruptedException e) { + throw e; + } catch (Exception e) { + throw new SimulationException("An exception happened during simulation", e); + } + } + + @SafeVarargs + private static Function or( + final Function... functions) + { + return (simulationState) -> { + for(final var function: functions){ + if(function.apply(simulationState)){ + return true; + } + } + return false; + }; + } + + + @Override + public SimulationData simulateWithResults( + final Plan plan, + final Duration until) throws SimulationException, SchedulingInterruptedException + { + return simulateWithResults(plan, until, missionModel.getResources().keySet()); + } + + @Override + public SimulationData simulateWithResults( + final Plan plan, + final Duration until, + final Set resourceNames) throws SimulationException, SchedulingInterruptedException + { + if(this.initialSimulationResults != null) { + final var inputPlan = scheduleFromPlan(plan, schedulerModel); + final var initialPlanA = scheduleFromPlan(this.initialSimulationResults.plan(), schedulerModel); + if (initialPlanA.equals(inputPlan)) { + return initialSimulationResults; + } + } + final var resultsInput = simulateNoResults(plan, until); + final var driverResults = resultsInput.simulationResultsComputerInputs().computeResults(resourceNames); + this.latestSimulationData = new SimulationData( + plan, + driverResults, + SimulationResultsConverter.convertToConstraintModelResults(driverResults), + Optional.ofNullable(resultsInput.planSimCorrespondence().planActDirectiveIdToSimulationActivityDirectiveId())); + return this.latestSimulationData; + } + + @Override + public Optional getLatestSimulationData() { + return Optional.ofNullable(this.latestSimulationData); + } + +} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStore.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStore.java new file mode 100644 index 0000000000..9c565bb38f --- /dev/null +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStore.java @@ -0,0 +1,125 @@ +package gov.nasa.jpl.aerie.scheduler.simulation; + +import gov.nasa.jpl.aerie.merlin.driver.CachedEngineStore; +import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.SimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import org.apache.commons.collections4.map.ListOrderedMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class InMemoryCachedEngineStore implements AutoCloseable, CachedEngineStore { + private record CachedEngineMetadata( + SimulationEngineConfiguration configuration, + Instant creationDate){} + + private static final Logger LOGGER = LoggerFactory.getLogger(InMemoryCachedEngineStore.class); + private final ListOrderedMap cachedEngines; + private final int capacity; + private Duration savedSimulationTime; + + /** + * + * @param capacity the maximum number of engines that can be stored in memory + */ + public InMemoryCachedEngineStore(final int capacity) { + if(capacity <= 0) throw new IllegalArgumentException("Capacity of the cached engine store must be greater than 0"); + this.cachedEngines = new ListOrderedMap<>(); + this.capacity = capacity; + this.savedSimulationTime = Duration.ZERO; + } + + public Duration getTotalSavedSimulationTime(){ + return savedSimulationTime; + } + + @Override + public void close() { + cachedEngines.forEach((cachedEngine, metadata) -> cachedEngine.simulationEngine().close()); + cachedEngines.clear(); + } + + /** + * Register a re-use for a saved cached simulation engine. Will decrease likelihood of this engine being deleted. + * @param cachedSimulationEngine the simulation engine + */ + public void registerUsed(final CheckpointSimulationDriver.CachedSimulationEngine cachedSimulationEngine){ + final var engineMetadata = this.cachedEngines.remove(cachedSimulationEngine); + if(engineMetadata != null){ + this.cachedEngines.put(0, cachedSimulationEngine, engineMetadata); + this.savedSimulationTime = this.savedSimulationTime.plus(cachedSimulationEngine.endsAt()); + } + } + + public void save( + final CheckpointSimulationDriver.CachedSimulationEngine engine, + final SimulationEngineConfiguration configuration) { + if (shouldWeSave(engine, configuration)) { + if (cachedEngines.size() + 1 > capacity) { + removeLast(); + } + final var metadata = new CachedEngineMetadata(configuration, Instant.now()); + cachedEngines.put(cachedEngines.size(), engine, metadata); + LOGGER.info("Added a cached simulation engine to the store. Current occupation ratio: " + cachedEngines.size() + "/" + this.capacity); + } + } + + @Override + public int capacity(){ + return capacity; + } + + public List getCachedEngines( + final SimulationEngineConfiguration configuration){ + return cachedEngines + .entrySet() + .stream() + .filter(ce -> configuration.equals(ce.getValue().configuration)) + .map(Map.Entry::getKey) + .toList(); + } + + public Optional> getMissionModel( + final Map configuration, + final Instant simulationStartTime){ + for(final var entry: cachedEngines.entrySet()){ + if(entry.getValue().configuration.simulationConfiguration().equals(configuration) && + entry.getValue().configuration.simStartTime().equals(simulationStartTime)){ + return Optional.of(entry.getKey().missionModel()); + } + } + return Optional.empty(); + } + + private boolean shouldWeSave(final CheckpointSimulationDriver.CachedSimulationEngine engine, + final SimulationEngineConfiguration configuration){ + //avoid duplicates + for(final var cached: cachedEngines.entrySet()){ + final var savedEngine = cached.getKey(); + final var metadata = cached.getValue(); + if(engine.endsAt().isEqualTo(savedEngine.endsAt()) && + engine.activityDirectives().equals(savedEngine.activityDirectives()) && + metadata.configuration.equals(configuration)){ + return false; + } + } + return true; + } + + /** + * Least-recently-used removal policy + */ + private void removeLast(){ + LOGGER.info("Cleaning cached simulation engine from the store"); + this.cachedEngines.remove(this.cachedEngines.size() - 1); + } +} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResourceAwareSpreadCheckpointPolicy.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResourceAwareSpreadCheckpointPolicy.java new file mode 100644 index 0000000000..da265d2d83 --- /dev/null +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResourceAwareSpreadCheckpointPolicy.java @@ -0,0 +1,45 @@ +package gov.nasa.jpl.aerie.scheduler.simulation; + +import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * Policy for saving simulation checkpoints in a cache. + * The number of checkpoint saved is equal to the capacity of the cache multiplied by a discount factor. + * The resulting number of cache events are then spread over the whole planning horizon. + */ +public class ResourceAwareSpreadCheckpointPolicy implements Function{ + final Function function; + + public ResourceAwareSpreadCheckpointPolicy( + final int resourceCapacity, + final Duration planningHorizonStart, + final Duration planningHorizonEnd, + final Duration subHorizonStart, + final Duration subHorizonEnd, + final double discount, + final boolean endForSure){ + final List desiredCheckpoints = new ArrayList<>(); + if(resourceCapacity > 0){ + final var period = planningHorizonEnd.minus(planningHorizonStart).dividedBy((int) (resourceCapacity * discount)); + //for a given planning horizon, we try always hitting the same checkpoint times to increase the probability of + //already having it saved in the cache + for (Duration cur = planningHorizonStart.plus(period); + cur.longerThan(subHorizonStart) && cur.shorterThan(subHorizonEnd); + cur = cur.plus(period)) { + desiredCheckpoints.add(cur); + } + if (endForSure && !desiredCheckpoints.contains(subHorizonEnd)) desiredCheckpoints.add(subHorizonEnd); + } + this.function = CheckpointSimulationDriver.desiredCheckpoints(desiredCheckpoints); + } + + @Override + public Boolean apply(final CheckpointSimulationDriver.SimulationState simulationState) { + return function.apply(simulationState); + } +} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java deleted file mode 100644 index 4e3c47f9e9..0000000000 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationDriver.java +++ /dev/null @@ -1,406 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.simulation; - -import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; -import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; -import gov.nasa.jpl.aerie.merlin.driver.MissionModel; -import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; -import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; -import gov.nasa.jpl.aerie.merlin.driver.StartOffsetReducer; -import gov.nasa.jpl.aerie.merlin.driver.engine.JobSchedule; -import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; -import gov.nasa.jpl.aerie.merlin.driver.engine.SpanId; -import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; -import gov.nasa.jpl.aerie.merlin.driver.timeline.TemporalEventSource; -import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; -import gov.nasa.jpl.aerie.merlin.protocol.model.TaskFactory; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; -import gov.nasa.jpl.aerie.scheduler.NotNull; -import gov.nasa.jpl.aerie.scheduler.SchedulingInterruptedException; -import org.apache.commons.lang3.tuple.Pair; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.time.Instant; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Supplier; - -public class ResumableSimulationDriver implements AutoCloseable { - private final Supplier canceledListener; - - public long durationSinceRestart = 0; - - private static final Logger logger = LoggerFactory.getLogger(ResumableSimulationDriver.class); - /* The current real time. All the tasks before and at this time have been performed. - Simulation has not started so it is set to MIN_VALUE. */ - private Duration curTime = Duration.MIN_VALUE; - private SimulationEngine engine = new SimulationEngine(); - private LiveCells cells; - private TemporalEventSource timeline = new TemporalEventSource(); - private final MissionModel missionModel; - private final Duration planDuration; - private JobSchedule.Batch batch; - - private final Topic activityTopic = new Topic<>(); - - //mapping each activity name to its task id (in String form) in the simulation engine - private final Map plannedDirectiveToTask; - - //subset of plannedDirectiveToTask to check for scheduling dependent tasks - private final Map toCheckForDependencyScheduling; - - //simulation results so far - private SimulationResults lastSimResults; - //cached simulation results cover the period [Duration.ZERO, lastSimResultsEnd] - private Duration lastSimResultsEnd = Duration.ZERO; - - //List of activities simulated since the last reset - private final Map activitiesInserted = new HashMap<>(); - - //counts the number of simulation restarts, used as performance metric in the scheduler - //effectively counting the number of calls to initSimulation() - private int countSimulationRestarts; - - public ResumableSimulationDriver( - MissionModel missionModel, - Duration planDuration, - Supplier canceledListener - ){ - this.missionModel = missionModel; - plannedDirectiveToTask = new HashMap<>(); - toCheckForDependencyScheduling = new HashMap<>(); - this.planDuration = planDuration; - countSimulationRestarts = 0; - this.canceledListener = canceledListener; - initSimulation(); - } - - - private void printTimeSpent(){ - final var dur = durationSinceRestart/1_000_000_000.; - final var average = curTime.shorterThan(Duration.of(1, Duration.SECONDS)) ? 0 : dur/curTime.in(Duration.SECONDS); - if(dur != 0) { - logger.info("Time spent in the last sim " + dur + "s, average per simulation second " + average + "s. Simulated until " + curTime); - } - } - - // This method is currently only used in one test. - /*package-private*/ void clearActivitiesInserted() {activitiesInserted.clear();} - - /*package-private*/ void initSimulation(){ - logger.info("Reinitialization of the scheduling simulation"); - printTimeSpent(); - durationSinceRestart = 0; - plannedDirectiveToTask.clear(); - toCheckForDependencyScheduling.clear(); - lastSimResults = null; - lastSimResultsEnd = Duration.ZERO; - long before = System.nanoTime(); - if (this.engine != null) this.engine.close(); - this.engine = new SimulationEngine(); - batch = null; - /* The top-level simulation timeline. */ - this.timeline = new TemporalEventSource(); - this.cells = new LiveCells(timeline, missionModel.getInitialCells()); - curTime = Duration.MIN_VALUE; - - // Begin tracking all resources. - for (final var entry : missionModel.getResources().entrySet()) { - final var name = entry.getKey(); - final var resource = entry.getValue(); - engine.trackResource(name, resource, Duration.ZERO); - } - - // Start daemon task(s) immediately, before anything else happens. - { - if(missionModel.hasDaemons()) { - engine.scheduleTask(Duration.ZERO, missionModel.getDaemon()); - batch = engine.extractNextJobs(Duration.MAX_VALUE); - } - } - this.durationSinceRestart += System.nanoTime() - before; - countSimulationRestarts++; - } - - /** - * Return the number of simulation restarts - * @return the number of simulation restarts - */ - public int getCountSimulationRestarts(){ - return countSimulationRestarts; - } - - @Override - public void close() { - logger.debug("Closing sim"); - printTimeSpent(); - this.engine.close(); - } - - private void simulateUntil(Duration endTime) throws SchedulingInterruptedException { - long before = System.nanoTime(); - logger.info("Simulating until "+endTime); - assert(endTime.noShorterThan(curTime)); - if(batch == null){ - batch = engine.extractNextJobs(Duration.MAX_VALUE); - } - // Increment real time, if necessary. - while(!batch.offsetFromStart().longerThan(endTime) && !endTime.isEqualTo(Duration.MAX_VALUE)) { - if(canceledListener.get()) throw new SchedulingInterruptedException("simulating"); - //by default, curTime is negative to signal we have not started simulation yet. We set it to 0 when we start. - final var delta = batch.offsetFromStart().minus(curTime.isNegative() ? Duration.ZERO : curTime); - curTime = batch.offsetFromStart(); - timeline.add(delta); - // Run the jobs in this batch. - final var commit = engine.performJobs(batch.jobs(), cells, curTime, Duration.MAX_VALUE); - timeline.add(commit.getLeft()); - if (commit.getRight().isPresent()) { - throw new RuntimeException(commit.getRight().get()); - } - - batch = engine.extractNextJobs(Duration.MAX_VALUE); - } - lastSimResults = null; - this.durationSinceRestart += (System.nanoTime() - before); - } - - - /** - * Simulate an activity directive. - * @param activity the serialized type and arguments of the activity directive to be simulated - * @param startOffset the start offset from the activity's anchor - * @param anchorId the activity id of the anchor (or null if the activity is anchored to the plan) - * @param anchoredToStart toggle for if the activity is anchored to the start or end of its anchor - * @param activityId the activity id for the activity to simulate - */ - public void simulateActivity( - final Duration startOffset, - final SerializedActivity activity, - final ActivityDirectiveId anchorId, - final boolean anchoredToStart, - final ActivityDirectiveId activityId - ) throws SchedulingInterruptedException { - simulateActivity(new ActivityDirective(startOffset, activity, anchorId, anchoredToStart), activityId); - } - - /** - * Simulate an activity directive. - * @param activityToSimulate the activity directive to simulate - * @param activityId the ActivityDirectiveId for the activity to simulate - */ - public void simulateActivity(ActivityDirective activityToSimulate, ActivityDirectiveId activityId) - throws SchedulingInterruptedException { - simulateActivities(Map.of(activityId, activityToSimulate)); - } - - public void simulateActivities(@NotNull Map activitiesToSimulate) - throws SchedulingInterruptedException { - if(activitiesToSimulate.isEmpty()) return; - - activitiesInserted.putAll(activitiesToSimulate); - - final HashMap>> resolved = new StartOffsetReducer(planDuration, activitiesToSimulate).compute(); - resolved.get(null).sort(Comparator.comparing(Pair::getRight)); - final var earliestStartOffset = resolved.get(null).get(0); - - if(earliestStartOffset.getRight().noLongerThan(curTime)){ - logger.info("Restarting simulation because earliest start of activity to simulate " + earliestStartOffset.getRight() + " is before current sim time " + curTime); - initSimulation(); - simulateSchedule(activitiesInserted); - } else { - simulateSchedule(activitiesToSimulate); - } - } - - - /** - * Get the simulation results from the Duration.ZERO to the current simulation time point - * @param startTimestamp the timestamp for the start of the planning horizon. Used as epoch for computing SimulationResults. - * @return the simulation results - */ - public SimulationResults getSimulationResults(Instant startTimestamp) throws SchedulingInterruptedException { - return getSimulationResultsUpTo(startTimestamp, curTime); - } - - public Duration getCurrentSimulationEndTime(){ - return curTime; - } - - /** - * Get the simulation results from the Duration.ZERO to a specified end time point. - * The provided simulation results might cover more than the required time period. - * @param startTimestamp the timestamp for the start of the planning horizon. Used as epoch for computing SimulationResults. - * @param endTime the end timepoint. The simulation results will be computed up to this point. - * @return the simulation results - */ - public SimulationResults getSimulationResultsUpTo(Instant startTimestamp, Duration endTime) - throws SchedulingInterruptedException { - //if previous results cover a bigger period, we return do not regenerate - if(endTime.longerThan(curTime)){ - logger.info("Simulating from " + curTime + " to " + endTime + " to get simulation results"); - simulateUntil(endTime); - } else{ - logger.info("Not simulating because asked endTime "+endTime+" is before current sim time " + curTime); - } - final var before = System.nanoTime(); - if(lastSimResults == null || endTime.longerThan(lastSimResultsEnd) || startTimestamp.compareTo(lastSimResults.startTime) != 0) { - if(canceledListener.get()) throw new SchedulingInterruptedException("computing simulation results"); - lastSimResults = SimulationEngine.computeResults( - engine, - startTimestamp, - endTime, - activityTopic, - timeline, - missionModel.getTopics()); - lastSimResultsEnd = endTime; - //while sim results may not be up to date with curTime, a regeneration has taken place after the last insertion - } - this.durationSinceRestart += System.nanoTime() - before; - - return lastSimResults; - } - - private void simulateSchedule(final Map schedule) - throws SchedulingInterruptedException { - final var before = System.nanoTime(); - if (schedule.isEmpty()) { - throw new IllegalArgumentException("simulateSchedule() called with empty schedule, use simulateUntil() instead"); - } - - // Get all activities as close as possible to absolute time, then schedule all activities. - // Using HashMap explicitly because it allows `null` as a key. - // `null` key means that an activity is not waiting on another activity to finish to know its start time - HashMap>> resolved = new StartOffsetReducer( - planDuration, - schedule).compute(); - // Filter out activities that are before the plan start - resolved = StartOffsetReducer.filterOutNegativeStartOffset(resolved); - final var toSchedule = new HashSet(); - toSchedule.add(null); - scheduleActivities( - toSchedule, - schedule, - resolved, - missionModel, - engine - ); - - var allTaskFinished = false; - - if (batch == null) { - batch = engine.extractNextJobs(Duration.MAX_VALUE); - } - //by default, curTime is negative to signal we have not started simulation yet. We set it to 0 when we start. - Duration delta = batch.offsetFromStart().minus(curTime.isNegative() ? Duration.ZERO : curTime); - - //once all tasks are finished, we need to wait for events triggered at the same time - while (!allTaskFinished || delta.isZero()) { - if(canceledListener.get()) throw new SchedulingInterruptedException("simulating"); - - curTime = batch.offsetFromStart(); - timeline.add(delta); - // TODO: Advance a dense time counter so that future tasks are strictly ordered relative to these, - // even if they occur at the same real time. - - // Run the jobs in this batch. - final var commit = engine.performJobs(batch.jobs(), cells, curTime, Duration.MAX_VALUE); - timeline.add(commit.getLeft()); - if(commit.getRight().isPresent()) { - throw new RuntimeException(commit.getRight().get()); - } - - scheduleActivities(getSuccessorsToSchedule(engine), schedule, resolved, missionModel, engine); - - // all tasks are complete : do not exit yet, there might be event triggered at the same time - if (!plannedDirectiveToTask.isEmpty() && plannedDirectiveToTask - .values() - .stream() - .allMatch($ -> engine.getSpan($).isComplete())) { - allTaskFinished = true; - } - - // Update batch and increment real time, if necessary. - batch = engine.extractNextJobs(Duration.MAX_VALUE); - delta = batch.offsetFromStart().minus(curTime); - if(batch.offsetFromStart().longerThan(planDuration)){ - break; - } - } - lastSimResults = null; - this.durationSinceRestart+= System.nanoTime() - before; - } - - /** - * Returns the duration of a terminated simulated activity - * @param activityDirectiveId the activity id - * @return its duration if the activity has been simulated and has finished simulating, an IllegalArgumentException otherwise - */ - public Optional getActivityDuration(ActivityDirectiveId activityDirectiveId){ - //potential cause of non presence: (1) activity is outside plan bounds (2) activity has not been simulated yet - if(!plannedDirectiveToTask.containsKey(activityDirectiveId)) return Optional.empty(); - return engine.getSpan(plannedDirectiveToTask.get(activityDirectiveId)).duration(); - } - - private Set getSuccessorsToSchedule(final SimulationEngine engine) { - final var toSchedule = new HashSet(); - final var iterator = toCheckForDependencyScheduling.entrySet().iterator(); - while(iterator.hasNext()){ - final var taskToCheck = iterator.next(); - if(engine.getSpan(taskToCheck.getValue()).isComplete()){ - toSchedule.add(taskToCheck.getKey()); - iterator.remove(); - } - } - return toSchedule; - } - - private void scheduleActivities( - final Set toScheduleNow, - final Map completeSchedule, - final HashMap>> resolved, - final MissionModel missionModel, - final SimulationEngine engine){ - for(final var predecessor: toScheduleNow) { - for (final var directivePair : resolved.get(predecessor)) { - final var offset = directivePair.getRight(); - final var directiveIdToSchedule = directivePair.getLeft(); - final var serializedDirective = completeSchedule.get(directiveIdToSchedule).serializedActivity(); - final TaskFactory task; - try { - task = missionModel.getTaskFactory(serializedDirective); - } catch (final InstantiationException ex) { - // All activity instantiations are assumed to be validated by this point - throw new Error("Unexpected state: activity instantiation %s failed with: %s" - .formatted(serializedDirective.getTypeName(), ex.toString())); - } - Duration computedStartTime = offset; - if (predecessor != null) { - computedStartTime = (curTime.isEqualTo(Duration.MIN_VALUE) ? Duration.ZERO : curTime).plus(offset); - } - final var taskId = engine.scheduleTask( - computedStartTime, - makeTaskFactory(directiveIdToSchedule, task, activityTopic)); - plannedDirectiveToTask.put(directiveIdToSchedule, taskId); - if (resolved.containsKey(directiveIdToSchedule)) { - toCheckForDependencyScheduling.put(directiveIdToSchedule, taskId); - } - } - } - } - - private static TaskFactory makeTaskFactory( - final ActivityDirectiveId directiveId, - final TaskFactory task, - final Topic activityTopic) { - return executor -> scheduler -> { - scheduler.emit(directiveId, activityTopic); - return task.create(executor).step(scheduler); - }; - } -} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationData.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationData.java index 09684c8ff8..5f1517f6ab 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationData.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationData.java @@ -2,13 +2,14 @@ import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; +import gov.nasa.jpl.aerie.scheduler.model.Plan; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; import org.apache.commons.collections4.BidiMap; -import java.util.Collection; import java.util.Optional; public record SimulationData( + Plan plan, SimulationResults driverResults, gov.nasa.jpl.aerie.constraints.model.SimulationResults constraintsResults, Optional> mapSchedulingIdsToActivityIds diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java index 9870fd59e7..cbae355dac 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java @@ -2,384 +2,74 @@ import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; -import gov.nasa.jpl.aerie.merlin.driver.MissionModel; -import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; -import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivityId; -import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; -import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; +import gov.nasa.jpl.aerie.merlin.driver.SimulationResultsComputerInputs; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; import gov.nasa.jpl.aerie.scheduler.SchedulingInterruptedException; -import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; import gov.nasa.jpl.aerie.scheduler.model.ActivityType; -import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; +import gov.nasa.jpl.aerie.scheduler.model.Plan; +import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.commons.collections4.BidiMap; -import java.util.ArrayList; import java.util.Collection; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; +import java.util.HashSet; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.function.Supplier; -import org.apache.commons.collections4.BidiMap; -import org.apache.commons.collections4.bidimap.DualHashBidiMap; - -/** - * A facade for simulating plans and processing simulation results. - */ -public class SimulationFacade implements AutoCloseable{ - - private static final Logger logger = LoggerFactory.getLogger(SimulationFacade.class); - private final Supplier canceledListener; - - private final MissionModel missionModel; - private final SchedulerModel schedulerModel; - - // planning horizon - private final PlanningHorizon planningHorizon; - private Map activityTypes; - private ResumableSimulationDriver driver; - private int itSimActivityId; - private final BidiMap mapSchedulingIdsToActivityIds; - - private final Map insertedActivities; - //counts the total number of simulation restarts, used as performance metric in the scheduler - private int pastSimulationRestarts; - - public SimulationData lastSimulationData; - - /** - * state boolean stating whether the initial plan has been modified to allow initial simulation results to be used - */ - private boolean initialPlanHasBeenModified = false; - - /* External initial simulation results that will be served only if initialPlanHasBeenModified is equal to false*/ - private Optional initialSimulationResults; - - /** - * The set of activities to be added to the first simulation. - * Used to potentially delay the first simulation until the loaded results are stale. - * The only way to add activities to the facade is to simulate them. But sometimes, we have initial sim results and we - * do not need to simulate before the first activity insertion. This initial plan allows the facade to "load" the activities in simulation - * and wait until the first needed simulation to simulate them. - */ - private List initialPlan; - - /** - * 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; - } - - /** - * @return true if initial simulation results are stale, false otherwise - */ - public boolean areInitialSimulationResultsStale(){ - return this.initialPlanHasBeenModified; - } - - public Optional getLatestConstraintSimulationResults(){ - if(!initialPlanHasBeenModified && initialSimulationResults.isPresent()) return Optional.of(this.initialSimulationResults.get().constraintsResults()); - if(lastSimulationData == null) return Optional.empty(); - return Optional.of(lastSimulationData.constraintsResults()); - } - - public Optional getLatestDriverSimulationResults(){ - if(!initialPlanHasBeenModified && initialSimulationResults.isPresent()) return Optional.of(this.initialSimulationResults.get().driverResults()); - if(lastSimulationData == null) return Optional.empty(); - return Optional.of(lastSimulationData.driverResults()); - } - - public Supplier getCanceledListener() {return canceledListener;} - - public SimulationFacade( - final PlanningHorizon planningHorizon, - final MissionModel missionModel, - final SchedulerModel schedulerModel, - Supplier canceledListener - ) { - this.missionModel = missionModel; - this.planningHorizon = planningHorizon; - this.driver = new ResumableSimulationDriver<>(missionModel, planningHorizon.getAerieHorizonDuration(), canceledListener); - this.itSimActivityId = 0; - this.insertedActivities = new HashMap<>(); - this.mapSchedulingIdsToActivityIds = new DualHashBidiMap<>(); - this.activityTypes = new HashMap<>(); - this.pastSimulationRestarts = 0; - this.initialPlan = new ArrayList<>(); - this.initialSimulationResults = Optional.empty(); - this.schedulerModel = schedulerModel; - this.canceledListener = canceledListener; - } - - @Override - 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)); - } - - public Optional> getBidiActivityIdCorrespondence(){ - if(initialSimulationResults.isEmpty() || initialPlanHasBeenModified) - return Optional.ofNullable(mapSchedulingIdsToActivityIds); - else return initialSimulationResults.get().mapSchedulingIdsToActivityIds(); - } - +public interface SimulationFacade { + void setInitialSimResults(SimulationData simulationData); + Duration totalSimulationTime(); - /** - * Fetches activity instance durations from last simulation - * - * @param schedulingActivityDirective the activity instance we want the duration for - * @return the duration if found in the last simulation, null otherwise - */ - public Optional getActivityDuration(final SchedulingActivityDirective schedulingActivityDirective) { - if(!mapSchedulingIdsToActivityIds.containsKey(schedulingActivityDirective.getId())){ - return Optional.empty(); - } - final var duration = driver.getActivityDuration(mapSchedulingIdsToActivityIds.get( - schedulingActivityDirective.getId())); - return duration; - } - - private ActivityDirectiveId getIdOfRootParent(SimulationResults results, SimulatedActivityId instanceId){ - final var act = results.simulatedActivities.get(instanceId); - if(act.parentId() == null){ - // SAFETY: any activity that has no parent must have a directive id. - return act.directiveId().get(); - } else { - return getIdOfRootParent(results, act.parentId()); - } - } - - public Map getAllChildActivities(final Duration endTime) - throws SimulationException, SchedulingInterruptedException - { - logger.info("Need to compute simulation results until "+ endTime + " for getting child activities"); - var latestSimulationData = this.getLatestDriverSimulationResults(); - //if no initial sim results and no sim has been performed, perform a sim and get the sim results - if(latestSimulationData.isEmpty()){ - //useful only if there are activities to simulate for this case of getting child activities - if(insertedActivities.size() == 0) return Map.of(); - computeSimulationResultsUntil(endTime); - latestSimulationData = this.getLatestDriverSimulationResults(); - } - final Map childActivities = new HashMap<>(); - latestSimulationData.get().simulatedActivities.forEach( (activityInstanceId, activity) -> { - if (activity.parentId() == null) return; - final var rootParent = getIdOfRootParent(this.lastSimulationData.driverResults(), activityInstanceId); - final var schedulingActId = mapSchedulingIdsToActivityIds.inverseBidiMap().get(rootParent); - final var activityInstance = SchedulingActivityDirective.of( - activityTypes.get(activity.type()), - this.planningHorizon.toDur(activity.start()), - activity.duration(), - activity.arguments(), - schedulingActId, - null, - true); - childActivities.put(activityInstance, schedulingActId); - }); - return childActivities; - } + Supplier getCanceledListener(); - public void removeAndInsertActivitiesFromSimulation( - final Collection activitiesToRemove, - final Collection activitiesToAdd - ) throws SimulationException, SchedulingInterruptedException { - if (canceledListener.get()) throw new SchedulingInterruptedException("removing/adding activities"); - logger.debug("Removing("+activitiesToRemove.size()+")/Adding("+activitiesToAdd.size()+") activities from simulation"); - activitiesToRemove.stream().forEach(remove -> logger.debug("Removing act starting at " + remove.startOffset())); - activitiesToAdd.stream().forEach(adding -> logger.debug("Adding act starting at " + adding.startOffset())); - var atLeastOneActualRemoval = false; - for(final var act: activitiesToRemove){ - if(insertedActivities.containsKey(act)){ - atLeastOneActualRemoval = true; - insertedActivities.remove(act); - } - } - 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()); - } - if(allActivitiesToSimulate.isEmpty() && !atLeastOneActualRemoval) return; - //reset resumable simulation - if(atLeastOneActualRemoval || earliestActStartTime.noLongerThan(this.driver.getCurrentSimulationEndTime())){ - allActivitiesToSimulate.addAll(insertedActivities.keySet()); - insertedActivities.clear(); - mapSchedulingIdsToActivityIds.clear(); - logger.info("(Re)creating simulation driver because at least one removal("+atLeastOneActualRemoval+") or insertion in the past ("+earliestActStartTime+")"); - if (driver != null) { - this.pastSimulationRestarts += driver.getCountSimulationRestarts(); - driver.close(); - } - logger.info("Number of simulation restarts so far: " + this.pastSimulationRestarts); - driver = new ResumableSimulationDriver<>(missionModel, planningHorizon.getAerieHorizonDuration(), canceledListener); - } - simulateActivities(allActivitiesToSimulate); - } - - public void removeActivitiesFromSimulation(final Collection activities) - throws SimulationException, SchedulingInterruptedException - { - removeAndInsertActivitiesFromSimulation(activities, List.of()); - } + void addActivityTypes(Collection activityTypes); - /** - * Returns the total number of simulation restarts - * @return the number of simulation restarts - */ - public int countSimulationRestarts(){ - return this.driver.getCountSimulationRestarts() + this.pastSimulationRestarts; - } + SimulationResultsComputerInputs simulateNoResultsAllActivities(Plan plan) + throws SimulationException, SchedulingInterruptedException; - public void insertActivitiesIntoSimulation(final Collection activities) - throws SimulationException, SchedulingInterruptedException - { - removeAndInsertActivitiesFromSimulation(List.of(), activities); - } + SimulationResultsComputerInputs simulateNoResultsUntilEndAct( + Plan plan, + SchedulingActivityDirective activity) throws SimulationException, SchedulingInterruptedException; - /** - * Replaces an activity instance with another, strictly when they have the same id - * @param toBeReplaced the activity to be replaced - * @param replacement the replacement activity - */ - public void replaceActivityFromSimulation(final SchedulingActivityDirective toBeReplaced, final SchedulingActivityDirective replacement){ - if(toBeReplaced.type() != replacement.type() || - ((toBeReplaced.anchorId() == replacement.anchorId()) && toBeReplaced.startOffset() != replacement.startOffset()) || - !(toBeReplaced.arguments().equals(replacement.arguments()))) { - throw new IllegalArgumentException("When replacing an activity, you can only update the duration"); - } - if(!insertedActivities.containsKey(toBeReplaced)){ - throw new IllegalArgumentException("Trying to replace an activity that has not been previously simulated"); - } - final var associated = insertedActivities.get(toBeReplaced); - insertedActivities.remove(toBeReplaced); - insertedActivities.put(replacement, associated); - final var simulationId = this.mapSchedulingIdsToActivityIds.get(toBeReplaced.id()); - mapSchedulingIdsToActivityIds.remove(toBeReplaced.id()); - mapSchedulingIdsToActivityIds.put(replacement.id(), simulationId); - } + AugmentedSimulationResultsComputerInputs simulateNoResults( + Plan plan, + Duration until) throws SimulationException, SchedulingInterruptedException; - private void simulateActivities(final Collection activities) - throws SimulationException, SchedulingInterruptedException { - final var activitiesSortedByStartTime = - activities.stream().filter(activity -> !(insertedActivities.containsKey(activity))) - .sorted(Comparator.comparing(SchedulingActivityDirective::startOffset)).toList(); - if(activitiesSortedByStartTime.isEmpty()) return; - final Map directivesToSimulate = new HashMap<>(); + SimulationData simulateWithResults( + Plan plan, + Duration until) throws SimulationException, SchedulingInterruptedException; - for(final var activity : activitiesSortedByStartTime){ - final var activityIdSim = new ActivityDirectiveId(itSimActivityId++); - mapSchedulingIdsToActivityIds.put(activity.getId(), activityIdSim); - } + SimulationData simulateWithResults( + Plan plan, + Duration until, + Set resourceNames) throws SimulationException, SchedulingInterruptedException; - for(final var activity : activitiesSortedByStartTime) { - final var activityDirective = schedulingActToActivityDir(activity); - directivesToSimulate.put(mapSchedulingIdsToActivityIds.get(activity.getId()), - activityDirective); - insertedActivities.put(activity, activityDirective); - } - try { - driver.simulateActivities(directivesToSimulate); - } catch (SchedulingInterruptedException e) { - throw e; //pass interruption up - } catch (Exception e){ - throw new SimulationException("An exception happened during simulation", e); - } - this.lastSimulationData = null; - } + Optional getLatestSimulationData(); - public static class SimulationException extends Exception { + class SimulationException extends Exception { SimulationException(final String message, final Throwable cause) { super(message, cause); } } - public void computeSimulationResultsUntil(final Duration endTime) - throws SimulationException, SchedulingInterruptedException { - if(!initialPlan.isEmpty()){ - final var toSimulate = new ArrayList<>(this.initialPlan); - this.initialPlan.clear(); - this.insertActivitiesIntoSimulation(toSimulate); - } - try { - final var results = driver.getSimulationResultsUpTo(this.planningHorizon.getStartInstant(), endTime); - //compare references - if(lastSimulationData == null || results != lastSimulationData.driverResults()) { - //simulation results from the last simulation, as converted for use by the constraint evaluation engine - this.lastSimulationData = new SimulationData(results, SimulationResultsConverter.convertToConstraintModelResults(results), Optional.ofNullable(mapSchedulingIdsToActivityIds)); - } - } catch (SchedulingInterruptedException e){ - throw e; //pass interruption up - } catch (Exception e){ - throw new SimulationException("An exception happened during simulation", e); - } - } + record AugmentedSimulationResultsComputerInputs( + SimulationResultsComputerInputs simulationResultsComputerInputs, + SimulationFacade.PlanSimCorrespondence planSimCorrespondence + ) {} - public Duration getCurrentSimulationEndTime(){ - return driver.getCurrentSimulationEndTime(); - } - - private ActivityDirective schedulingActToActivityDir(SchedulingActivityDirective activity) { - if(activity.getParentActivity().isPresent()) { - throw new Error("This method should not be called with a generated activity but with its top-level parent."); - } - final var arguments = new HashMap<>(activity.arguments()); - if (activity.duration() != null) { - final var durationType = activity.getType().getDurationType(); - if (durationType instanceof DurationType.Controllable dt) { - arguments.put(dt.parameterName(), this.schedulerModel.serializeDuration(activity.duration())); - } else if ( - durationType instanceof DurationType.Uncontrollable - || durationType instanceof DurationType.Fixed - || durationType instanceof DurationType.Parametric - ) { - // If an activity has already been simulated, it will have a duration, even if its DurationType is Uncontrollable. - } else { - throw new Error("Unhandled variant of DurationType: " + durationType); + record PlanSimCorrespondence( + BidiMap planActDirectiveIdToSimulationActivityDirectiveId, + Map directiveIdActivityDirectiveMap){ + @Override + public boolean equals(Object other){ + if(other instanceof PlanSimCorrespondence planSimCorrespondenceAs){ + return directiveIdActivityDirectiveMap.size() == planSimCorrespondenceAs.directiveIdActivityDirectiveMap.size() && + new HashSet<>(directiveIdActivityDirectiveMap.values()).containsAll(new HashSet<>(((PlanSimCorrespondence) other).directiveIdActivityDirectiveMap.values())); } + return false; } - final var serializedActivity = new SerializedActivity(activity.getType().getName(), arguments); - if(activity.anchorId()!= null && !mapSchedulingIdsToActivityIds.containsKey(activity.anchorId())){ - throw new RuntimeException("Activity with id "+ activity.anchorId() + " referenced as an anchor by activity " + activity.toString() + " is not present in the plan"); - } - return new ActivityDirective( - activity.startOffset(), - serializedActivity, - mapSchedulingIdsToActivityIds.get(activity.anchorId()), - activity.anchoredToStart()); } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacadeUtils.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacadeUtils.java new file mode 100644 index 0000000000..bb72056c17 --- /dev/null +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacadeUtils.java @@ -0,0 +1,176 @@ +package gov.nasa.jpl.aerie.scheduler.simulation; + +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; +import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; +import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivity; +import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivityId; +import gov.nasa.jpl.aerie.merlin.driver.SimulationResultsComputerInputs; +import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; +import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; +import gov.nasa.jpl.aerie.scheduler.model.ActivityType; +import gov.nasa.jpl.aerie.scheduler.model.Plan; +import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; +import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; +import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; +import org.apache.commons.collections4.bidimap.DualHashBidiMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class SimulationFacadeUtils { + private static final Logger LOGGER = LoggerFactory.getLogger(SimulationFacadeUtils.class); + private static int itSimActivityId = 0; + + public static SimulationFacade.PlanSimCorrespondence scheduleFromPlan(final Plan plan, final SchedulerModel schedulerModel){ + final var activities = plan.getActivities(); + final var planActDirectiveIdToSimulationActivityDirectiveId = new DualHashBidiMap(); + if(activities.isEmpty()) return new SimulationFacade.PlanSimCorrespondence(new DualHashBidiMap<>(), Map.of()); + //filter out child activities + final var activitiesWithoutParent = activities.stream().filter(a -> a.topParent() == null).toList(); + final Map directivesToSimulate = new HashMap<>(); + + for(final var activity : activitiesWithoutParent){ + final var activityIdSim = new ActivityDirectiveId(itSimActivityId++); + planActDirectiveIdToSimulationActivityDirectiveId.put(activity.getId(), activityIdSim); + } + + for(final var activity : activitiesWithoutParent) { + final var activityDirective = schedulingActToActivityDir(activity, planActDirectiveIdToSimulationActivityDirectiveId, schedulerModel); + directivesToSimulate.put( + planActDirectiveIdToSimulationActivityDirectiveId.get(activity.getId()), + activityDirective); + } + return new SimulationFacade.PlanSimCorrespondence(planActDirectiveIdToSimulationActivityDirectiveId, directivesToSimulate); + } + + /** + * For activities that have a null duration (in an initial plan for example) and that have been simulated, we pull the duration and + * replace the original instance with a new instance that includes the duration, both in the plan and the simulation facade + */ + public static void pullActivityDurationsIfNecessary( + final Plan plan, + final SimulationFacade.PlanSimCorrespondence correspondence, + final SimulationEngine.SimulationActivityExtract activityExtract + ) { + final var toReplace = new HashMap(); + for (final var activity : plan.getActivities()) { + if (activity.duration() == null) { + final var activityDirective = findSimulatedActivityById( + activityExtract.simulatedActivities().values(), + correspondence.planActDirectiveIdToSimulationActivityDirectiveId().get(activity.getId())); + if (activityDirective.isPresent()) { + final var replacementAct = SchedulingActivityDirective.copyOf( + activity, + activityDirective.get().duration() + ); + toReplace.put(activity, replacementAct); + } + //if not, maybe the activity is not finished + } + } + toReplace.forEach(plan::replaceActivity); + } + + private static Optional findSimulatedActivityById( + Collection simulatedActivities, + final ActivityDirectiveId activityDirectiveId + ){ + return simulatedActivities.stream() + .filter(a -> a.directiveId().isPresent() && a.directiveId().get().equals(activityDirectiveId)) + .findFirst(); + } + + public static void updatePlanWithChildActivities( + final SimulationEngine.SimulationActivityExtract activityExtract, + final Map activityTypes, + final Plan plan, + final SimulationFacade.PlanSimCorrespondence planSimCorrespondence, + final PlanningHorizon planningHorizon) + { + //remove all activities with parents + final var toRemove = plan.getActivities().stream().filter(a -> a.topParent() != null).toList(); + toRemove.forEach(plan::remove); + //pull child activities + activityExtract.simulatedActivities().forEach( (activityInstanceId, activity) -> { + if (activity.parentId() == null) return; + final var rootParent = getIdOfRootParent(activityExtract, activityInstanceId); + if(rootParent.isPresent()) { + final var activityInstance = SchedulingActivityDirective.of( + activityTypes.get(activity.type()), + planningHorizon.toDur(activity.start()), + activity.duration(), + activity.arguments(), + planSimCorrespondence.planActDirectiveIdToSimulationActivityDirectiveId().getKey(rootParent.get()), + null, + true); + plan.add(activityInstance); + } + }); + //no need to replace in Evaluation because child activities are not referenced in it + } + + private static Optional getIdOfRootParent( + final SimulationEngine.SimulationActivityExtract results, + final SimulatedActivityId instanceId){ + if(!results.simulatedActivities().containsKey(instanceId)){ + if(!results.unfinishedActivities().containsKey(instanceId)){ + LOGGER.debug("The simulation of the parent of activity with id "+ instanceId.id() + " has been finished"); + } + return Optional.empty(); + } + final var act = results.simulatedActivities().get(instanceId); + if(act.parentId() == null){ + // SAFETY: any activity that has no parent must have a directive id. + return Optional.of(act.directiveId().get()); + } else { + return getIdOfRootParent(results, act.parentId()); + } + } + + public static Optional getActivityDuration( + final ActivityDirectiveId activityDirectiveId, + final SimulationResultsComputerInputs simulationResultsInputs + ){ + return simulationResultsInputs.engine() + .getSpan(simulationResultsInputs.activityDirectiveIdTaskIdMap() + .get(activityDirectiveId)) + .duration(); + } + + public static ActivityDirective schedulingActToActivityDir( + final SchedulingActivityDirective activity, + final Map planActDirectiveIdToSimulationActivityDirectiveId, + final SchedulerModel schedulerModel) { + if(activity.getParentActivity().isPresent()) { + throw new Error("This method should not be called with a generated activity but with its top-level parent."); + } + final var arguments = new HashMap<>(activity.arguments()); + if (activity.duration() != null) { + final var durationType = activity.getType().getDurationType(); + if (durationType instanceof DurationType.Controllable dt) { + arguments.put(dt.parameterName(), schedulerModel.serializeDuration(activity.duration())); + } else if ( + durationType instanceof DurationType.Uncontrollable + || durationType instanceof DurationType.Fixed + || durationType instanceof DurationType.Parametric + ) { + // If an activity has already been simulated, it will have a duration, even if its DurationType is Uncontrollable. + } else { + throw new Error("Unhandled variant of DurationType: " + durationType); + } + } + final var serializedActivity = new SerializedActivity(activity.getType().getName(), arguments); + return new ActivityDirective( + activity.startOffset(), + serializedActivity, + planActDirectiveIdToSimulationActivityDirectiveId.get(activity.anchorId()), + activity.anchoredToStart()); + } +} diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationResultsConverter.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationResultsConverter.java index 0be3a6cead..9e253315be 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationResultsConverter.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationResultsConverter.java @@ -28,7 +28,7 @@ public static gov.nasa.jpl.aerie.constraints.model.SimulationResults convertToCo .collect(Collectors.toList()); return new gov.nasa.jpl.aerie.constraints.model.SimulationResults( driverResults.startTime, - Interval.betweenClosedOpen(Duration.ZERO, driverResults.duration), + Interval.between(Duration.ZERO, driverResults.duration), activities, Maps.transformValues(driverResults.realProfiles, $ -> LinearProfile.fromSimulatedProfile($.getRight())), Maps.transformValues(driverResults.discreteProfiles, $ -> DiscreteProfile.fromSimulatedProfile($.getRight())) diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/Evaluation.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/Evaluation.java index a7b096f47c..2370c467a7 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/Evaluation.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/Evaluation.java @@ -83,6 +83,31 @@ public Optional getNbConflictsDetected() { */ public void associate(SchedulingActivityDirective act, boolean createdByThisGoal) { acts.put(act, createdByThisGoal);} + /** + * Replaces an activity in the goal evaluation by another activity + * @param toBeReplaced the activity to be replaced + * @param replacement the replacement activity + */ + public void replace(final SchedulingActivityDirective toBeReplaced, final SchedulingActivityDirective replacement){ + final var found = acts.get(toBeReplaced); + if(found != null){ + acts.remove(toBeReplaced); + acts.put(replacement, found); + } + } + + /** + * Duplicates the GoalEvaluation + * @return the duplicate + */ + public GoalEvaluation duplicate(){ + final var duplicate = new GoalEvaluation(); + duplicate.acts.putAll(this.acts); + duplicate.nbConflictsDetected = this.nbConflictsDetected; + duplicate.score = this.score; + return duplicate; + } + /** * flags all given activities as contributing to the goal's (dis)satisfaction * @@ -141,6 +166,18 @@ public java.util.Collection getGoals() { return goalEvals.keySet(); } + /** + * Duplicates the Evaluation + * @return the duplicate evaluation + */ + public Evaluation duplicate(){ + final var duplicate = new Evaluation(); + for(final var goalEvaluation : goalEvals.entrySet()){ + duplicate.goalEvals.put(goalEvaluation.getKey(), goalEvaluation.getValue().duplicate()); + } + return duplicate; + } + /** * fetch all goals and their current individual evaluation * diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java index f4a9cfe3b5..058b227499 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java @@ -6,7 +6,6 @@ import gov.nasa.jpl.aerie.constraints.time.Segment; import gov.nasa.jpl.aerie.constraints.time.Windows; import gov.nasa.jpl.aerie.constraints.tree.Expression; -import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; @@ -19,7 +18,6 @@ import gov.nasa.jpl.aerie.scheduler.conflicts.MissingActivityTemplateConflict; import gov.nasa.jpl.aerie.scheduler.conflicts.MissingAssociationConflict; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; -import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.GlobalConstraint; import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.GlobalConstraintWithIntrospection; import gov.nasa.jpl.aerie.scheduler.goals.ActivityTemplateGoal; import gov.nasa.jpl.aerie.scheduler.goals.CompositeAndGoal; @@ -28,22 +26,24 @@ import gov.nasa.jpl.aerie.scheduler.model.Plan; import gov.nasa.jpl.aerie.scheduler.model.PlanInMemory; import gov.nasa.jpl.aerie.scheduler.model.Problem; +import gov.nasa.jpl.aerie.scheduler.model.SchedulePlanGrounder; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; -import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; +import gov.nasa.jpl.aerie.scheduler.simulation.SimulationData; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.stn.TaskNetworkAdapter; -import org.apache.commons.collections4.BidiMap; +import org.apache.commons.collections4.bidimap.DualHashBidiMap; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; @@ -59,40 +59,37 @@ public class PrioritySolver implements Solver { private static final Logger logger = LoggerFactory.getLogger(PrioritySolver.class); - boolean checkSimBeforeInsertingActivities; - boolean checkSimBeforeEvaluatingGoal; + private boolean checkSimBeforeInsertingActivities; + + private boolean checkSimBeforeEvaluatingGoal; + + private boolean atLeastOneSimulateAfter; + + private SimulationData cachedSimulationResultsBeforeGoalEvaluation; /** * boolean stating whether only conflict analysis should be performed or not */ - final boolean analysisOnly; + private final boolean analysisOnly; /** * description of the planning problem to solve * * remains constant throughout solver lifetime */ - final Problem problem; + private final Problem problem; /** * the single-shot priority-ordered greedy solution devised by the solver * * this object is null until first call to getNextSolution() */ - Plan plan; - - List> generatedActivityInstances = new ArrayList<>(); - - /** - * tracks how well this solver thinks it has satisfied goals - * - * including which activities were created to satisfy each goal - */ - Evaluation evaluation; + private Plan plan; private final SimulationFacade simulationFacade; public record ActivityMetadata(SchedulingActivityDirective activityDirective){} + public static class HistoryWithActivity implements EquationSolvingAlgorithms.History { List, Optional>> events; @@ -142,6 +139,7 @@ public PrioritySolver(final Problem problem, final boolean analysisOnly) { checkNotNull(problem, "creating solver with null input problem descriptor"); this.checkSimBeforeInsertingActivities = true; this.checkSimBeforeEvaluatingGoal = true; + this.atLeastOneSimulateAfter = false; this.problem = problem; this.simulationFacade = problem.getSimulationFacade(); this.analysisOnly = analysisOnly; @@ -167,7 +165,7 @@ public Optional getNextSolution() throws SchedulingInterruptedException { initializePlan(); if(problem.getInitialSimulationResults().isPresent()) { logger.debug("Loading initial simulation results from the DB"); - simulationFacade.loadInitialSimResults(problem.getInitialSimulationResults().get()); + simulationFacade.setInitialSimResults(problem.getInitialSimulationResults().get()); } } catch (SimulationFacade.SimulationException e) { logger.error("Tried to initializePlan but at least one activity could not be instantiated", e); @@ -195,7 +193,7 @@ public record InsertActivityResult(boolean success, List acts, boolean actsFromInitialPlan) throws SchedulingInterruptedException{ + private InsertActivityResult checkAndInsertActs(Collection acts) throws SchedulingInterruptedException{ // TODO: When anchors are allowed to be added by Scheduling goals, inserting the new activities one at a time should be reconsidered boolean allGood = true; logger.info("Inserting new activities in the plan to check plan validity"); @@ -203,81 +201,24 @@ private InsertActivityResult checkAndInsertActs(Collection(); - - if(allGood) { - logger.info("New activities have been inserted in the plan successfully"); - if(!acts.isEmpty() && !actsFromInitialPlan) simulationFacade.initialSimulationResultsAreStale(); - //update plan with regard to simulation - for(var act: acts) { - plan.add(act); - finalSetOfActsInserted.add(act); - } - final var replaced = synchronizeSimulationWithSchedulerPlan(); - for(final var actReplaced : replaced.entrySet()){ - if(finalSetOfActsInserted.contains(actReplaced.getKey())){ - finalSetOfActsInserted.remove(actReplaced.getKey()); - finalSetOfActsInserted.add(actReplaced.getValue()); - } - } - } else{ - logger.info("New activities could not be inserted in the plan, see error just above"); - //update simulation with regard to plan - try { - simulationFacade.removeActivitiesFromSimulation(acts); - } catch(SimulationFacade.SimulationException e){ - throw new RuntimeException("Removing activities from the simulation should not result in exception being thrown but one was thrown", e); - } - } - return new InsertActivityResult(allGood, finalSetOfActsInserted); - } - - /** - * Pulls all the child activities from the simulation + fills in activity durations - * This method should be called only when the state of the plan is considered safe, i.e. not during rootfinding - * @return a map of scheduling activity directives (old -> new) that have been replaced in the plan due to updated durations - */ - private Map synchronizeSimulationWithSchedulerPlan() - throws SchedulingInterruptedException { - final Map replacedInPlan; + final var planWithAddedActivities = plan.duplicate(); + planWithAddedActivities.add(acts); try { - final var allGeneratedActivities = - simulationFacade.getAllChildActivities(simulationFacade.getCurrentSimulationEndTime()); - processNewGeneratedActivities(allGeneratedActivities); - replacedInPlan = pullActivityDurationsIfNecessary(); + if(checkSimBeforeInsertingActivities) simulationFacade.simulateNoResultsAllActivities(planWithAddedActivities); + plan = planWithAddedActivities; } catch (SimulationFacade.SimulationException e) { - throw new RuntimeException("Exception while simulating to get child activities", e); + allGood = false; + logger.error("Tried to simulate the plan {} but a simulation exception happened", planWithAddedActivities, e); } - return replacedInPlan; + return new InsertActivityResult(allGood, acts.stream().map(act -> plan.getActivitiesById().get(act.getId())).toList()); } + /** * creates internal storage space to build up partial solutions in **/ @@ -287,89 +228,12 @@ public void initializePlan() throws SimulationFacade.SimulationException, Schedu //turn off simulation checking for initial plan contents (must accept user input regardless) final var prevCheckFlag = this.checkSimBeforeInsertingActivities; this.checkSimBeforeInsertingActivities = false; - checkAndInsertActs(problem.getInitialPlan().getActivitiesByTime(), true); + checkAndInsertActs(problem.getInitialPlan().getActivitiesByTime()); this.checkSimBeforeInsertingActivities = prevCheckFlag; - evaluation = new Evaluation(); - plan.addEvaluation(evaluation); - if(simulationFacade != null) simulationFacade.addInitialPlan(this.plan.getActivitiesByTime()); - } - - /** - * For activities that have a null duration (in an initial plan for example) and that have been simulated, we pull the duration and - * replace the original instance with a new instance that includes the duration, both in the plan and the simulation facade - */ - public Map pullActivityDurationsIfNecessary() { - final var toRemoveFromPlan = new ArrayList(); - final var toAddToPlan = new ArrayList(); - final var replaced = new HashMap(); - for (final var activity : plan.getActivities()) { - if (activity.duration() == null) { - final var duration = simulationFacade.getActivityDuration(activity); - if (duration.isPresent()) { - final var replacementAct = SchedulingActivityDirective.copyOf( - activity, - duration.get() - ); - simulationFacade.replaceActivityFromSimulation(activity, replacementAct); - toAddToPlan.add(replacementAct); - toRemoveFromPlan.add(activity); - generatedActivityInstances = generatedActivityInstances.stream().map(pair -> pair.getLeft().equals(activity) ? Pair.of(replacementAct, pair.getRight()): pair).collect(Collectors.toList()); - generatedActivityInstances = generatedActivityInstances.stream().map(pair -> pair.getRight().equals(activity) ? Pair.of(pair.getLeft(), replacementAct): pair).collect(Collectors.toList()); - replaced.put(activity, replacementAct); - } - } - } - plan.remove(toRemoveFromPlan); - plan.add(toAddToPlan); - return replaced; - } - - /** - * For activities that have a null duration (in an initial plan for example) and that have been simulated, we pull the duration and - * replace the original instance with a new instance that includes the duration, both in the plan and the simulation facade - */ - public void replaceActivity(SchedulingActivityDirective actOld, SchedulingActivityDirective actNew) { - simulationFacade.replaceActivityFromSimulation(actOld, actNew); - generatedActivityInstances = generatedActivityInstances.stream().map(pair -> pair.getLeft().equals(actOld) ? Pair.of(actNew, pair.getRight()): pair).collect(Collectors.toList()); - generatedActivityInstances = generatedActivityInstances.stream().map(pair -> pair.getRight().equals(actOld) ? Pair.of(pair.getLeft(), actNew): pair).collect(Collectors.toList()); - plan.remove(actOld); - plan.add(actNew); + plan.addEvaluation(new Evaluation()); } - /** - * Filters generated activities and makes sure that simulations are only adding activities and not removing them - * @param allNewGeneratedActivities all the generated activities from the last simulation results. - */ - private void processNewGeneratedActivities(Map allNewGeneratedActivities) { - final var activitiesById = plan.getActivitiesById(); - final var formattedNewGeneratedActivities = new ArrayList>(); - allNewGeneratedActivities.entrySet().forEach(entry -> formattedNewGeneratedActivities.add(Pair.of(entry.getKey(), activitiesById.get(entry.getValue())))); - - final var copyOld = new ArrayList<>(this.generatedActivityInstances); - final var copyNew = new ArrayList<>(formattedNewGeneratedActivities); - - for(final var pairOld: this.generatedActivityInstances){ - for (final var pairNew : formattedNewGeneratedActivities){ - if(pairOld.getLeft().equalsInProperties(pairNew.getLeft()) && - pairNew.getRight().equals(pairOld.getRight())){ - copyNew.remove(pairNew); - copyOld.remove(pairOld); - //break at first occurrence. there may be several activities equal in properties. - break; - } - } - } - - //TODO: continuous goal satisfaction - //copyNew contains only things that are new - //copyOld contains only present in old but absent in new - //if(copyOld.size() != 0){ - //throw new Error("Activities have disappeared from simulation, failing"); - //} - this.generatedActivityInstances.addAll(copyNew); - this.plan.add(copyNew.stream().map(Pair::getLeft).toList()); - } /** * iteratively fills in output plan to satisfy input problem description @@ -423,6 +287,8 @@ private LinkedList getGoalQueue() { final var rawGoals = problem.getGoals(); assert rawGoals != null; + this.atLeastOneSimulateAfter = rawGoals.stream().filter(g -> g.simulateAfter).findFirst().isPresent(); + //create queue container using comparator and pre-sized for all goals final var capacity = rawGoals.size(); assert capacity >= 0; @@ -458,9 +324,9 @@ private void satisfyOptionGoal(OptionGoal goal) throws SchedulingInterruptedExce Collection actsToAssociateWith = null; for (var subgoal : goal.getSubgoals()) { satisfyGoal(subgoal); - if(evaluation.forGoal(subgoal).getScore() == 0 || !subgoal.shouldRollbackIfUnsatisfied()) { - var associatedActivities = evaluation.forGoal(subgoal).getAssociatedActivities(); - var insertedActivities = evaluation.forGoal(subgoal).getInsertedActivities(); + if(plan.getEvaluation().forGoal(subgoal).getScore() == 0 || !subgoal.shouldRollbackIfUnsatisfied()) { + var associatedActivities = plan.getEvaluation().forGoal(subgoal).getAssociatedActivities(); + var insertedActivities = plan.getEvaluation().forGoal(subgoal).getInsertedActivities(); var aggregatedActivities = new ArrayList(); aggregatedActivities.addAll(associatedActivities); aggregatedActivities.addAll(insertedActivities); @@ -478,27 +344,28 @@ private void satisfyOptionGoal(OptionGoal goal) throws SchedulingInterruptedExce if (currentSatisfiedGoal != null) { for(var act: actsToAssociateWith){ //we do not care about ownership here as it is not really a piggyback but just the validation of the supergoal - evaluation.forGoal(goal).associate(act, false); + plan.getEvaluation().forGoal(goal).associate(act, false); } - final var insertionResult = checkAndInsertActs(actsToInsert, false); + final var insertionResult = checkAndInsertActs(actsToInsert); if(insertionResult.success()) { for(var act: insertionResult.activitiesInserted()){ - evaluation.forGoal(goal).associate(act, false); + plan.getEvaluation().forGoal(goal).associate(act, false); } - evaluation.forGoal(goal).setScore(0); + plan.getEvaluation().forGoal(goal).setScore(0); } else{ //this should not happen because we have already tried to insert the same set of activities in the plan and it //did not fail throw new IllegalStateException("Had satisfied subgoal but (1) simulation or (2) association with supergoal failed"); } } else { - evaluation.forGoal(goal).setScore(-1); + plan.getEvaluation().forGoal(goal).setScore(-1); } } else { var atLeastOneSatisfied = false; //just satisfy any goal for (var subgoal : goal.getSubgoals()) { satisfyGoal(subgoal); + final var evaluation = plan.getEvaluation(); final var subgoalIsSatisfied = (evaluation.forGoal(subgoal).getScore() == 0); evaluation.forGoal(goal).associate(evaluation.forGoal(subgoal).getAssociatedActivities(), false); evaluation.forGoal(goal).associate(evaluation.forGoal(subgoal).getInsertedActivities(), true); @@ -510,9 +377,9 @@ private void satisfyOptionGoal(OptionGoal goal) throws SchedulingInterruptedExce logger.info("OR goal " + goal.getName() + ": subgoal " + subgoal.getName() + " could not be satisfied, moving on to next subgoal"); } if(atLeastOneSatisfied){ - evaluation.forGoal(goal).setScore(0); + plan.getEvaluation().forGoal(goal).setScore(0); } else { - evaluation.forGoal(goal).setScore(-1); + plan.getEvaluation().forGoal(goal).setScore(-1); if(goal.shouldRollbackIfUnsatisfied()) { for (var subgoal : goal.getSubgoals()) { rollback(subgoal); @@ -523,7 +390,7 @@ private void satisfyOptionGoal(OptionGoal goal) throws SchedulingInterruptedExce } private void rollback(Goal goal){ - var evalForGoal = evaluation.forGoal(goal); + var evalForGoal = plan.getEvaluation().forGoal(goal); var associatedActivities = evalForGoal.getAssociatedActivities(); var insertedActivities = evalForGoal.getInsertedActivities(); plan.remove(insertedActivities); @@ -539,7 +406,7 @@ private void satisfyCompositeGoal(CompositeAndGoal goal) throws SchedulingInterr var nbGoalSatisfied = 0; for (var subgoal : goal.getSubgoals()) { satisfyGoal(subgoal); - if (evaluation.forGoal(subgoal).getScore() == 0) { + if (plan.getEvaluation().forGoal(subgoal).getScore() == 0) { logger.info("AND goal " + goal.getName() + ": subgoal " + subgoal.getName() + " has been satisfied, moving on to next subgoal"); nbGoalSatisfied++; } else { @@ -553,9 +420,9 @@ private void satisfyCompositeGoal(CompositeAndGoal goal) throws SchedulingInterr } final var goalIsSatisfied = (nbGoalSatisfied == goal.getSubgoals().size()); if (goalIsSatisfied) { - evaluation.forGoal(goal).setScore(0); + plan.getEvaluation().forGoal(goal).setScore(0); } else { - evaluation.forGoal(goal).setScore(-1); + plan.getEvaluation().forGoal(goal).setScore(-1); } if(!goalIsSatisfied && goal.shouldRollbackIfUnsatisfied()){ @@ -565,6 +432,7 @@ private void satisfyCompositeGoal(CompositeAndGoal goal) throws SchedulingInterr } if(goalIsSatisfied) { for (var subgoal : goal.getSubgoals()) { + final var evaluation = plan.getEvaluation(); evaluation.forGoal(goal).associate(evaluation.forGoal(subgoal).getAssociatedActivities(), false); evaluation.forGoal(goal).associate(evaluation.forGoal(subgoal).getInsertedActivities(), true); } @@ -603,7 +471,7 @@ private void satisfyGoalGeneral(Goal goal) throws SchedulingInterruptedException var missingConflicts = getConflicts(goal); logger.info("Found "+ missingConflicts.size() +" conflicts in conflict detection"); //setting the number of conflicts detected at first evaluation, will be used at backtracking - evaluation.forGoal(goal).setNbConflictsDetected(missingConflicts.size()); + plan.getEvaluation().forGoal(goal).setNbConflictsDetected(missingConflicts.size()); assert missingConflicts != null; final var itConflicts = missingConflicts.iterator(); @@ -620,9 +488,9 @@ private void satisfyGoalGeneral(Goal goal) throws SchedulingInterruptedException //add the activities to the output plan if (!acts.isEmpty()) { logger.info("Found activity to satisfy missing activity instance conflict"); - final var insertionResult = checkAndInsertActs(acts, false); + final var insertionResult = checkAndInsertActs(acts); if(insertionResult.success){ - evaluation.forGoal(goal).associate(insertionResult.activitiesInserted(), true); + plan.getEvaluation().forGoal(goal).associate(insertionResult.activitiesInserted(), true); itConflicts.remove(); //REVIEW: really association should be via the goal's own query... } else{ @@ -645,10 +513,10 @@ else if(!analysisOnly && (missing instanceof MissingActivityTemplateConflict mi //add the activities to the output plan if (!acts.isEmpty()) { logger.info("Found activity to satisfy missing activity template conflict"); - final var insertionResult = checkAndInsertActs(acts, false); + final var insertionResult = checkAndInsertActs(acts); if(insertionResult.success()){ - evaluation.forGoal(goal).associate(insertionResult.activitiesInserted(), true); + plan.getEvaluation().forGoal(goal).associate(insertionResult.activitiesInserted(), true); //REVIEW: really association should be via the goal's own query... cardinalityLeft--; durationLeft = durationLeft.minus(insertionResult @@ -692,9 +560,9 @@ else if(!analysisOnly && (missing instanceof MissingActivityTemplateConflict mi missingAssociationConflict.getAnchorToStart().get(), startOffset ); - replaceActivity(act,replacementAct); + plan.replaceActivity(act,replacementAct); //decision-making here, we choose the first satisfying activity - evaluation.forGoal(goal).associate(replacementAct, false); + plan.getEvaluation().forGoal(goal).associate(replacementAct, false); itConflicts.remove(); logger.info("Activity " + replacementAct + " has been associated to goal " + goal.getName() +" to satisfy conflict " + i); break; @@ -705,7 +573,7 @@ else if(!analysisOnly && (missing instanceof MissingActivityTemplateConflict mi } else { //decision-making here, we choose the first satisfying activity - evaluation.forGoal(goal).associate(act, false); + plan.getEvaluation().forGoal(goal).associate(act, false); itConflicts.remove(); logger.info("Activity " + act @@ -728,7 +596,7 @@ else if(!analysisOnly && (missing instanceof MissingActivityTemplateConflict mi rollback(goal); } logger.info("Finishing goal satisfaction for goal " + goal.getName() +":"+ (missingConflicts.size() == 0 ? "SUCCESS" : "FAILURE. Number of conflicts that could not be addressed: " + missingConflicts.size())); - evaluation.forGoal(goal).setScore(-missingConflicts.size()); + plan.getEvaluation().forGoal(goal).setScore(-missingConflicts.size()); } /** @@ -746,11 +614,16 @@ private Collection getConflicts(Goal goal) throws SchedulingInterrupte assert plan != null; //REVIEW: maybe should have way to request only certain kinds of conflicts logger.debug("Computing simulation results until "+ this.problem.getPlanningHorizon().getEndAerie() + " (planning horizon end) in order to compute conflicts"); - final var lastSimulationResults = this.getLatestSimResultsUpTo(this.problem.getPlanningHorizon().getEndAerie()); - synchronizeSimulationWithSchedulerPlan(); + final var resources = new HashSet(); + goal.extractResources(resources); + final var simulationResults = this.getLatestSimResultsUpTo(this.problem.getPlanningHorizon().getEndAerie(), resources); final var evaluationEnvironment = new EvaluationEnvironment(this.problem.getRealExternalProfiles(), this.problem.getDiscreteExternalProfiles()); - final Optional> mapSchedulingIdsToActivityIds = simulationFacade.getBidiActivityIdCorrespondence(); - final var rawConflicts = goal.getConflicts(plan, lastSimulationResults, mapSchedulingIdsToActivityIds, evaluationEnvironment, this.problem.getSchedulerModel()); + final var rawConflicts = goal.getConflicts( + plan, + simulationResults.constraintsResults(), + simulationResults.mapSchedulingIdsToActivityIds(), + evaluationEnvironment, + this.problem.getSchedulerModel()); assert rawConflicts != null; return rawConflicts; } @@ -921,14 +794,15 @@ private Windows narrowByResourceConstraints( final var totalDomain = Interval.between(windows.minTrueTimePoint().get().getKey(), windows.maxTrueTimePoint().get().getKey()); //make sure the simulation results cover the domain logger.debug("Computing simulation results until "+ totalDomain.end + " in order to compute resource constraints"); - final var latestSimulationResults = this.getLatestSimResultsUpTo(totalDomain.end); - synchronizeSimulationWithSchedulerPlan(); + final var resourceNames = new HashSet(); + constraints.forEach(c -> c.extractResources(resourceNames)); + final var latestSimulationResults = this.getLatestSimResultsUpTo(totalDomain.end, resourceNames); //iteratively narrow the windows from each constraint //REVIEW: could be some optimization in constraint ordering (smallest domain first to fail fast) final var evaluationEnvironment = new EvaluationEnvironment(this.problem.getRealExternalProfiles(), this.problem.getDiscreteExternalProfiles()); for (final var constraint : constraints) { //REVIEW: loop through windows more efficient than enveloppe(windows) ? - final var validity = constraint.evaluate(latestSimulationResults, totalDomain, evaluationEnvironment); + final var validity = constraint.evaluate(latestSimulationResults.constraintsResults(), totalDomain, evaluationEnvironment); ret = ret.and(validity); //short-circuit if no possible windows left if (ret.stream().noneMatch(Segment::value)) { @@ -938,47 +812,73 @@ private Windows narrowByResourceConstraints( return ret; } - - private SimulationResults getLatestSimResultsUpTo(Duration time) throws SchedulingInterruptedException { - var lastSimResultsFromFacade = this.simulationFacade.getLatestConstraintSimulationResults(); - if (lastSimResultsFromFacade.isEmpty() || lastSimResultsFromFacade.get().bounds.end.shorterThan(time)) { - try { - this.simulationFacade.computeSimulationResultsUntil(time); - } catch (SimulationFacade.SimulationException e) { - throw new RuntimeException("Exception while running simulation before evaluating conflicts", e); + private SimulationData getLatestSimResultsUpTo(final Duration time, final Set resourceNames) throws SchedulingInterruptedException { + //if no resource is needed, build the results from the current plan + if(resourceNames.isEmpty()) { + final var groundedPlan = SchedulePlanGrounder.groundSchedule( + this.plan.getActivities().stream().toList(), + this.problem.getPlanningHorizon().getEndAerie()); + if (groundedPlan.isPresent()) { + return new SimulationData( + plan, + null, + new SimulationResults( + problem.getPlanningHorizon().getStartInstant(), + problem.getPlanningHorizon().getHor(), + groundedPlan.get(), + Map.of(), + Map.of()), + Optional.of(new DualHashBidiMap())); + } else { + logger.debug( + "Tried mocking simulation results with a grounded plan but could not because of the activity cannot be grounded."); } } - return this.simulationFacade.getLatestConstraintSimulationResults().get(); + try { + var resources = new HashSet(resourceNames); + var resourcesAreMissing = false; + if(cachedSimulationResultsBeforeGoalEvaluation != null){ + final var allResources = new HashSet<>(cachedSimulationResultsBeforeGoalEvaluation.constraintsResults().realProfiles.keySet()); + allResources.addAll(cachedSimulationResultsBeforeGoalEvaluation.constraintsResults().discreteProfiles.keySet()); + resourcesAreMissing = !allResources.containsAll(resourceNames); + } + //if at least one goal needs the simulateAfter, we can't compute partial resources to avoid future recomputations + if(atLeastOneSimulateAfter){ + resources.clear(); + resources.addAll(this.problem.getMissionModel().getResources().keySet()); + } + if(checkSimBeforeEvaluatingGoal || cachedSimulationResultsBeforeGoalEvaluation == null || cachedSimulationResultsBeforeGoalEvaluation.constraintsResults().bounds.end.shorterThan(time) || resourcesAreMissing) + cachedSimulationResultsBeforeGoalEvaluation = simulationFacade + .simulateWithResults(plan, time, resources); + return cachedSimulationResultsBeforeGoalEvaluation; + } catch (SimulationFacade.SimulationException e) { + throw new RuntimeException("Exception while running simulation before evaluating conflicts", e); + } } private Windows narrowGlobalConstraints( - Plan plan, - MissingActivityConflict mac, - Windows windows, - Collection constraints, - EvaluationEnvironment evaluationEnvironment - ) throws SchedulingInterruptedException { + final Plan plan, + final MissingActivityConflict mac, + final Windows windows, + final Collection constraints, + final EvaluationEnvironment evaluationEnvironment) throws SchedulingInterruptedException { Windows tmp = windows; if(tmp.stream().noneMatch(Segment::value)){ return tmp; } //make sure the simulation results cover the domain logger.debug("Computing simulation results until "+ tmp.maxTrueTimePoint().get().getKey() + " in order to compute global scheduling conditions"); - final var latestSimulationResults = this.getLatestSimResultsUpTo(tmp.maxTrueTimePoint().get().getKey()); - synchronizeSimulationWithSchedulerPlan(); - for (GlobalConstraint gc : constraints) { - if (gc instanceof GlobalConstraintWithIntrospection c) { - tmp = c.findWindows( - plan, - tmp, - mac, - latestSimulationResults, - evaluationEnvironment); - } else { - throw new Error("Unhandled variant of GlobalConstraint: %s".formatted(gc)); - } + final var resourceNames = new HashSet(); + constraints.forEach(c -> c.extractResources(resourceNames)); + final var latestSimulationResults = this.getLatestSimResultsUpTo(tmp.maxTrueTimePoint().get().getKey(), resourceNames); + for (final var gc : constraints) { + tmp = gc.findWindows( + plan, + tmp, + mac, + latestSimulationResults.constraintsResults(), + evaluationEnvironment); } - return tmp; } @@ -1025,6 +925,10 @@ private Optional instantiateActivity( if(reduced.isEmpty()) return Optional.empty(); final var solved = reduced.get(); + //Extract resource names to lighten the computation of simulation results + final var resourceNames = new HashSet(); + activityExpression.extractResources(resourceNames); + //the domain of user/scheduling temporal constraints have been reduced with the STN, //now it is time to find an assignment compatible //CASE 1: activity has an uncontrollable duration @@ -1035,7 +939,7 @@ private Optional instantiateActivity( public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History history) throws EquationSolvingAlgorithms.DiscontinuityException, SchedulingInterruptedException { - final var latestConstraintsSimulationResults = getLatestSimResultsUpTo(start); + final var latestConstraintsSimulationResults = getLatestSimResultsUpTo(start, resourceNames); final var actToSim = SchedulingActivityDirective.of( activityExpression.type(), start, @@ -1043,21 +947,20 @@ public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History< SchedulingActivityDirective.instantiateArguments( activityExpression.arguments(), start, - latestConstraintsSimulationResults, + latestConstraintsSimulationResults.constraintsResults(), evaluationEnvironment, activityExpression.type()), null, null, true); - final var lastInsertion = history.getLastEvent(); - Optional computedDuration = Optional.empty(); - final var toRemove = new ArrayList(); - lastInsertion.ifPresent(eventWithActivity -> toRemove.add(eventWithActivity.getValue().get().activityDirective())); + Duration computedDuration = null; try { - simulationFacade.removeAndInsertActivitiesFromSimulation(toRemove, List.of(actToSim)); - computedDuration = simulationFacade.getActivityDuration(actToSim); - if(computedDuration.isPresent()) { - history.add(new EquationSolvingAlgorithms.FunctionCoordinate<>(start, start.plus(computedDuration.get())), new ActivityMetadata(actToSim)); + final var duplicatePlan = plan.duplicate(); + duplicatePlan.add(actToSim); + simulationFacade.simulateNoResultsUntilEndAct(duplicatePlan, actToSim); + computedDuration = duplicatePlan.getActivitiesById().get(actToSim.getId()).duration(); + if(computedDuration != null) { + history.add(new EquationSolvingAlgorithms.FunctionCoordinate<>(start, start.plus(computedDuration)), new ActivityMetadata(SchedulingActivityDirective.copyOf(actToSim, computedDuration))); } else{ logger.debug("No simulation error but activity duration could not be found in simulation, likely caused by unfinished activity or activity outside plan bounds."); history.add(new EquationSolvingAlgorithms.FunctionCoordinate<>(start, null), new ActivityMetadata(actToSim)); @@ -1066,7 +969,8 @@ public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History< logger.debug("Simulation error while trying to simulate activities: " + e); history.add(new EquationSolvingAlgorithms.FunctionCoordinate<>(start, null), new ActivityMetadata(actToSim)); } - return computedDuration.map(start::plus).orElseThrow(EquationSolvingAlgorithms.DiscontinuityException::new); + if(computedDuration == null) throw new EquationSolvingAlgorithms.DiscontinuityException(); + return start.plus(computedDuration); } }; @@ -1078,7 +982,7 @@ public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History< final var instantiatedArguments = SchedulingActivityDirective.instantiateArguments( activityExpression.arguments(), earliestStart, - getLatestSimResultsUpTo(earliestStart), + getLatestSimResultsUpTo(earliestStart, resourceNames).constraintsResults(), evaluationEnvironment, activityExpression.type()); @@ -1103,12 +1007,7 @@ public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History< activityExpression.type(), earliestStart, setActivityDuration, - SchedulingActivityDirective.instantiateArguments( - activityExpression.arguments(), - earliestStart, - getLatestSimResultsUpTo(earliestStart), - evaluationEnvironment, - activityExpression.type()), + instantiatedArguments, null, null, true)); @@ -1119,7 +1018,6 @@ public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History< } final var earliestStart = solved.start().start; - // TODO: When scheduling is allowed to create activities with anchors, this constructor should pull from an expanded creation template return Optional.of(SchedulingActivityDirective.of( activityExpression.type(), @@ -1128,7 +1026,7 @@ public Duration valueAt(Duration start, final EquationSolvingAlgorithms.History< SchedulingActivityDirective.instantiateArguments( activityExpression.arguments(), earliestStart, - getLatestSimResultsUpTo(earliestStart), + getLatestSimResultsUpTo(earliestStart, resourceNames).constraintsResults(), evaluationEnvironment, activityExpression.type()), null, @@ -1143,7 +1041,7 @@ public Duration valueAt(final Duration start, final EquationSolvingAlgorithms.Hi final var instantiatedArgs = SchedulingActivityDirective.instantiateArguments( activityExpression.arguments(), start, - getLatestSimResultsUpTo(start), + getLatestSimResultsUpTo(start, resourceNames).constraintsResults(), evaluationEnvironment, activityExpression.type() ); @@ -1211,19 +1109,13 @@ private Optional rootFindingHelper( } catch (EquationSolvingAlgorithms.NoSolutionException e) { logger.info("Rootfinding found no solution"); } - if(!history.events.isEmpty()) { - try { - simulationFacade.removeActivitiesFromSimulation(List.of(history.getLastEvent().get().getRight().get().activityDirective())); - } catch (SimulationFacade.SimulationException e) { - throw new RuntimeException("Exception while simulating original plan after activity insertion failure" ,e); - } - } logger.info("Finished rootfinding: FAILURE"); history.logHistory(); return Optional.empty(); } public void printEvaluation() { + final var evaluation = plan.getEvaluation(); logger.warn("Remaining conflicts for goals "); for (var goalEval : evaluation.getGoals()) { logger.warn(goalEval.getName() + " -> " + evaluation.forGoal(goalEval).score); diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/FixedDurationTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/FixedDurationTest.java index 483a2a39ac..828b7ff81e 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/FixedDurationTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/FixedDurationTest.java @@ -5,19 +5,24 @@ import gov.nasa.jpl.aerie.constraints.tree.SpansFromWindows; import gov.nasa.jpl.aerie.constraints.tree.WindowsWrapperExpression; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeExpressionRelative; import gov.nasa.jpl.aerie.scheduler.goals.CoexistenceGoal; -import gov.nasa.jpl.aerie.scheduler.model.*; -import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; +import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; +import gov.nasa.jpl.aerie.scheduler.model.Problem; +import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; +import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.time.Instant; import java.util.List; +import java.util.Map; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class FixedDurationTest { @@ -29,7 +34,17 @@ public class FixedDurationTest { void setUp(){ planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochDays(3)); MissionModel bananaMissionModel = SimulationUtility.getBananaMissionModel(); - problem = new Problem(bananaMissionModel, planningHorizon, new SimulationFacade(planningHorizon, bananaMissionModel, SimulationUtility.getBananaSchedulerModel(), ()-> false), SimulationUtility.getBananaSchedulerModel()); + problem = new Problem( + bananaMissionModel, + planningHorizon, + new CheckpointSimulationFacade( + bananaMissionModel, + SimulationUtility.getBananaSchedulerModel(), + new InMemoryCachedEngineStore(10), + planningHorizon, + new SimulationEngineConfiguration(Map.of(), Instant.EPOCH, new MissionModelId(1)), + ()-> false), + SimulationUtility.getBananaSchedulerModel()); } @Test @@ -58,7 +73,6 @@ public void testFieldAnnotation() throws SchedulingInterruptedException { final var plan = solver.getNextSolution().get(); solver.printEvaluation(); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT1M"), planningHorizon.fromStart("PT1H1M"), problem.getActivityType("BananaNap"))); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @@ -88,7 +102,6 @@ public void testMethodAnnotation() throws SchedulingInterruptedException { final var plan = solver.getNextSolution().get(); solver.printEvaluation(); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT1M"), planningHorizon.fromStart("P2DT1M"), problem.getActivityType("RipenBanana"))); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/LongDurationPlanTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/LongDurationPlanTest.java index a86d2c79e3..89d8305338 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/LongDurationPlanTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/LongDurationPlanTest.java @@ -14,7 +14,6 @@ import java.util.List; import static gov.nasa.jpl.aerie.scheduler.TestUtility.assertSetEquality; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class LongDurationPlanTest { @@ -59,7 +58,6 @@ public void getNextSolution_initialPlanInOutput() throws SchedulingInterruptedEx assertTrue(plan.isPresent()); assertSetEquality(plan.get().getActivitiesByTime(), expectedPlan.getActivitiesByTime()); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -78,6 +76,5 @@ public void getNextSolution_proceduralGoalCreatesActivities() throws SchedulingI final var plan = solver.getNextSolution().orElseThrow(); assertSetEquality(plan.getActivitiesByTime(), expectedPlan.getActivitiesByTime()); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/ParametricDurationTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/ParametricDurationTest.java index f058ec4bd9..bcc1632f89 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/ParametricDurationTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/ParametricDurationTest.java @@ -4,7 +4,10 @@ import gov.nasa.jpl.aerie.constraints.time.Windows; import gov.nasa.jpl.aerie.constraints.tree.SpansFromWindows; import gov.nasa.jpl.aerie.constraints.tree.WindowsWrapperExpression; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; +import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; @@ -12,14 +15,15 @@ import gov.nasa.jpl.aerie.scheduler.goals.CoexistenceGoal; import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; import gov.nasa.jpl.aerie.scheduler.model.Problem; -import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; +import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.time.Instant; import java.util.List; +import java.util.Map; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class ParametricDurationTest { @@ -31,7 +35,13 @@ public class ParametricDurationTest { void setUp(){ planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochDays(3)); MissionModel bananaMissionModel = SimulationUtility.getBananaMissionModel(); - problem = new Problem(bananaMissionModel, planningHorizon, new SimulationFacade(planningHorizon, bananaMissionModel, SimulationUtility.getBananaSchedulerModel(), ()-> false), SimulationUtility.getBananaSchedulerModel()); + problem = new Problem(bananaMissionModel, planningHorizon, new CheckpointSimulationFacade( + bananaMissionModel, + SimulationUtility.getBananaSchedulerModel(), + new InMemoryCachedEngineStore(15), + planningHorizon, + new SimulationEngineConfiguration(Map.of(), Instant.EPOCH, new MissionModelId(1)), + ()-> false), SimulationUtility.getBananaSchedulerModel()); } @Test @@ -61,7 +71,6 @@ public void testStartConstraint() throws SchedulingInterruptedException { final var plan = solver.getNextSolution().get(); solver.printEvaluation(); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT1M"), planningHorizon.fromStart("PT2M"), problem.getActivityType("DownloadBanana"))); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -91,6 +100,5 @@ public void testEndConstraint() throws SchedulingInterruptedException { final var plan = solver.getNextSolution().get(); solver.printEvaluation(); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT2M"), planningHorizon.fromStart("PT12M"), problem.getActivityType("DownloadBanana"))); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java index f365af3730..60fa027bf3 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/PrioritySolverTest.java @@ -5,6 +5,7 @@ import gov.nasa.jpl.aerie.constraints.time.Windows; import gov.nasa.jpl.aerie.constraints.tree.WindowsWrapperExpression; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeAnchor; @@ -17,12 +18,17 @@ import gov.nasa.jpl.aerie.scheduler.model.PlanInMemory; import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; import gov.nasa.jpl.aerie.scheduler.model.Problem; +import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; +import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.Evaluation; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import org.junit.jupiter.api.Test; +import java.time.Instant; import java.util.List; +import java.util.Map; import java.util.Optional; import static gov.nasa.jpl.aerie.scheduler.TestUtility.assertSetEquality; @@ -36,7 +42,13 @@ private static PrioritySolver makeEmptyProblemSolver() { new Problem( bananaMissionModel, h, - new SimulationFacade(h, bananaMissionModel, schedulerModel, () -> false), + new CheckpointSimulationFacade( + bananaMissionModel, + schedulerModel, + new InMemoryCachedEngineStore(15), + h, + new SimulationEngineConfiguration(Map.of(),Instant.EPOCH, new MissionModelId(1)), + () -> false), schedulerModel)); } @@ -77,9 +89,7 @@ public void getNextSolution_givesNoSolutionOnSubsequentCall() throws SchedulingI //test mission with two primitive activity types private static Problem makeTestMissionAB() { - final var fooMissionModel = SimulationUtility.getFooMissionModel(); - final var fooSchedulerModel = SimulationUtility.getFooSchedulerModel(); - return new Problem(fooMissionModel, h, new SimulationFacade(h, fooMissionModel, fooSchedulerModel, ()-> false), fooSchedulerModel); + return SimulationUtility.buildProblemFromFoo(h, 15); } private final static PlanningHorizon h = new PlanningHorizon(TimeUtility.fromDOY("2025-001T01:01:01.001"), TimeUtility.fromDOY("2025-005T01:01:01.001")); @@ -130,7 +140,6 @@ public void getNextSolution_initialPlanInOutput() throws SchedulingInterruptedEx assertTrue(plan.isPresent()); assertSetEquality(plan.get().getActivitiesByTime(), expectedPlan.getActivitiesByTime()); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -149,7 +158,6 @@ public void getNextSolution_proceduralGoalCreatesActivities() throws SchedulingI final var plan = solver.getNextSolution().orElseThrow(); assertSetEquality(plan.getActivitiesByTime(), expectedPlan.getActivitiesByTime()); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -171,7 +179,6 @@ public void getNextSolution_proceduralGoalAttachesActivitiesToEvaluation() throw final var eval = plan.getEvaluation().forGoal(goal); assertNotNull(eval); assertSetEquality(eval.getAssociatedActivities().stream().toList(), expectedPlan.getActivitiesByTime()); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -199,7 +206,6 @@ public void getNextSolution_recurrenceGoalWorks() throws SchedulingInterruptedEx //TODO: may want looser expectation (eg allow flexibility as long as right repeat pattern met) assertTrue(plan.getActivitiesByTime().get(0).equalsInProperties(expectedPlan.getActivitiesByTime().get(0))); assertSetEquality(plan.getActivitiesByTime(), expectedPlan.getActivitiesByTime()); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -231,7 +237,6 @@ public void getNextSolution_coexistenceGoalOnActivityWorks() throws SchedulingIn //TODO: evaluation should have association of instances to goal //TODO: should ensure no other spurious acts yet need to ignore special interval activities assertSetEquality(plan.getActivitiesByTime(), expectedPlan.getActivitiesByTime()); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); } /** @@ -243,20 +248,15 @@ public void getNextSolution_coexistenceGoalOnActivityWorks_withInitialSimResults throws SimulationFacade.SimulationException, SchedulingInterruptedException { final var problem = makeTestMissionAB(); - - final var adHocFacade = new SimulationFacade( - problem.getPlanningHorizon(), + final var adHocFacade = new CheckpointSimulationFacade( problem.getMissionModel(), problem.getSchedulerModel(), - ()-> false); - adHocFacade.insertActivitiesIntoSimulation(makePlanA012(problem).getActivities()); - adHocFacade.computeSimulationResultsUntil(problem.getPlanningHorizon().getEndAerie()); - final var simResults = adHocFacade.getLatestDriverSimulationResults().get(); - if(adHocFacade.getBidiActivityIdCorrespondence().isPresent()) - problem.setInitialPlan(makePlanA012(problem), Optional.of(simResults), adHocFacade.getBidiActivityIdCorrespondence().get()); - else - problem.setInitialPlan(makePlanA012(problem), Optional.of(simResults), null); - + new InMemoryCachedEngineStore(10), + problem.getPlanningHorizon(), + new SimulationEngineConfiguration(Map.of(),Instant.EPOCH, new MissionModelId(1)), + () -> false); + final var simResults = adHocFacade.simulateWithResults(makePlanA012(problem), h.getEndAerie()); + problem.setInitialPlan(makePlanA012(problem), Optional.of(simResults.driverResults()), simResults.mapSchedulingIdsToActivityIds().get()); final var actTypeA = problem.getActivityType("ControllableDurationActivity"); final var actTypeB = problem.getActivityType("OtherControllableDurationActivity"); final var goal = new CoexistenceGoal.Builder() @@ -278,24 +278,11 @@ public void getNextSolution_coexistenceGoalOnActivityWorks_withInitialSimResults final var plan = solver.getNextSolution().orElseThrow(); final var expectedPlan = makePlanAB012(problem); assertSetEquality(plan.getActivitiesByTime(), expectedPlan.getActivitiesByTime()); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test public void testCardGoalWithApplyWhen() throws SchedulingInterruptedException { - var planningHorizon = h; - - final var fooMissionModel = SimulationUtility.getFooMissionModel(); - final var fooSchedulerModel = SimulationUtility.getFooSchedulerModel(); - Problem problem = new Problem( - fooMissionModel, - planningHorizon, - new SimulationFacade( - planningHorizon, - fooMissionModel, - fooSchedulerModel, - ()-> false), - SimulationUtility.getFooSchedulerModel()); + final var problem = SimulationUtility.buildProblemFromFoo(h); final var activityType = problem.getActivityType("ControllableDurationActivity"); //act at t=1hr and at t=2hrs @@ -326,9 +313,5 @@ public void testCardGoalWithApplyWhen() throws SchedulingInterruptedException { var plan = solver.getNextSolution().orElseThrow(); //will insert an activity at the beginning of the plan in addition of the two already-present activities assertEquals(3, plan.getActivities().size()); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } - - - } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SchedulingGrounderTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SchedulingGrounderTest.java new file mode 100644 index 0000000000..eb9dc05041 --- /dev/null +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SchedulingGrounderTest.java @@ -0,0 +1,96 @@ +package gov.nasa.jpl.aerie.scheduler; + +import gov.nasa.jpl.aerie.constraints.model.ActivityInstance; +import gov.nasa.jpl.aerie.constraints.time.Interval; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.scheduler.model.ActivityType; +import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; +import gov.nasa.jpl.aerie.scheduler.model.SchedulePlanGrounder; +import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; +import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SchedulingGrounderTest { + private final static PlanningHorizon h = new PlanningHorizon(TimeUtility.fromDOY("2025-001T01:01:01.001"), TimeUtility.fromDOY("2025-005T01:01:01.001")); + private final static Duration t0 = h.getStartAerie(); + private final static Duration d1min = Duration.of(1, Duration.MINUTE); + private final static Duration d1hr = Duration.of(1, Duration.HOUR); + private final static Duration t1hr = t0.plus(d1hr); + private final static Duration t2hr = t0.plus(d1hr.times(2)); + + @Test + public void testChainAnchors(){ + final var at = new ActivityType("at"); + final var id1 = new SchedulingActivityDirectiveId(1); + final var id2 = new SchedulingActivityDirectiveId(2); + final var id3 = new SchedulingActivityDirectiveId(3); + final var act1 = SchedulingActivityDirective.of(id1, at, t0, d1min, null, true); + final var act2 = SchedulingActivityDirective.of(id2, at, t1hr, d1min, act1.id(), false); + final var act3 = SchedulingActivityDirective.of(id3, at, t2hr, d1min, act2.id(), false); + final var acts = List.of(act1, act3, act2); + final var result = SchedulePlanGrounder.groundSchedule(acts, h.getEndAerie()); + //act 1 should start at 0 min into the plan + //act 2 should start 61 min into the plan + //act 3 should be [62 min, 63 min] + final var act1expt = new ActivityInstance(id1.id(), at.getName(), Map.of(), Interval.between(t0, t0.plus(d1min))); + final var act2expt = new ActivityInstance(id2.id(), at.getName(), Map.of(), Interval.between(t1hr.plus(t0).plus(d1min), t1hr.plus(t0).plus(d1min).plus(d1min))); + final var act3expt = new ActivityInstance(id3.id(), at.getName(), Map.of(), Interval.between(t0.plus(Duration.of(182, Duration.MINUTES)), t0.plus(Duration.of(183, Duration.MINUTES)))); + assertTrue(result.get().contains(act1expt)); + assertTrue(result.get().contains(act2expt)); + assertTrue(result.get().contains(act3expt)); + } + + @Test + public void testEmptyDueToEmptyDuration(){ + final var at = new ActivityType("at"); + final var id1 = new SchedulingActivityDirectiveId(1); + final var act1 = SchedulingActivityDirective.of(id1, at, t0, null, null, true); + final var result = SchedulePlanGrounder.groundSchedule(List.of(act1), h.getEndAerie()); + assertTrue(result.isEmpty()); + } + + @Test + public void testAnchoredToPlanEnd(){ + final var at = new ActivityType("at"); + final var id1 = new SchedulingActivityDirectiveId(1); + final var act1 = SchedulingActivityDirective.of(id1, at, Duration.negate(d1hr), d1min, null, false); + final var result = SchedulePlanGrounder.groundSchedule(List.of(act1), h.getEndAerie()); + final var act1expt = new ActivityInstance(id1.id(), at.getName(), Map.of(), Interval.between(h.getEndAerie().minus(d1hr), h.getEndAerie().minus(d1hr).plus(d1min))); + assertEquals(act1expt, result.get().get(0)); + } + + + @Test + public void noAnchor(){ + final var at = new ActivityType("at"); + final var id1 = new SchedulingActivityDirectiveId(1); + final var id2 = new SchedulingActivityDirectiveId(2); + final var act1 = SchedulingActivityDirective.of(id1, at, t0, d1min, null, true); + final var act2 = SchedulingActivityDirective.of(id2, at, t1hr, d1min, null, true); + final var result = SchedulePlanGrounder.groundSchedule(List.of(act1, act2), h.getEndAerie()); + final var act1expt = new ActivityInstance(id1.id(), at.getName(), Map.of(), Interval.between(t0, t0.plus(d1min))); + final var act2expt = new ActivityInstance(id2.id(), at.getName(), Map.of(), Interval.between(t1hr, t1hr.plus(d1min))); + assertTrue(result.get().contains(act1expt)); + assertTrue(result.get().contains(act2expt)); + } + + @Test + public void startTimeAnchor(){ + final var at = new ActivityType("at"); + final var id1 = new SchedulingActivityDirectiveId(1); + final var id2 = new SchedulingActivityDirectiveId(2); + final var act1 = SchedulingActivityDirective.of(id1, at, t1hr, d1min, null, true); + final var act2 = SchedulingActivityDirective.of(id2, at, t1hr, d1min, act1.id(), true); + final var result = SchedulePlanGrounder.groundSchedule(List.of(act1, act2), h.getEndAerie()); + final var act1expt = new ActivityInstance(id1.id(), at.getName(), Map.of(), Interval.between(t1hr, t1hr.plus(d1min))); + final var act2expt = new ActivityInstance(id2.id(), at.getName(), Map.of(), Interval.between(t2hr, t2hr.plus(d1min))); + assertTrue(result.get().contains(act1expt)); + assertTrue(result.get().contains(act2expt)); + } +} diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationFacadeTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationFacadeTest.java index 6a6fbc9507..9852e6f1df 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationFacadeTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationFacadeTest.java @@ -16,6 +16,7 @@ import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; import gov.nasa.jpl.aerie.scheduler.model.Problem; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; +import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import org.junit.jupiter.api.AfterEach; @@ -81,7 +82,7 @@ private DiscreteResource getPlantRes() { public void setUp() { missionModel = SimulationUtility.getBananaMissionModel(); final var schedulerModel = SimulationUtility.getBananaSchedulerModel(); - facade = new SimulationFacade(horizon, missionModel, schedulerModel, ()-> false); + facade = new CheckpointSimulationFacade(horizon, missionModel, schedulerModel); problem = new Problem(missionModel, horizon, facade, schedulerModel); } @@ -160,19 +161,17 @@ public void associationToExistingSatisfyingActivity() throws SchedulingInterrupt final var actAssociatedInSecondRun = plan2.get().getEvaluation().forGoal(goal).getAssociatedActivities(); assertEquals(1, actAssociatedInSecondRun.size()); assertTrue(actAssociatedInFirstRun.iterator().next().equalsInProperties(actAssociatedInSecondRun.iterator().next())); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test public void whenValueAboveDoubleOnSimplePlan() throws SimulationFacade.SimulationException, SchedulingInterruptedException { - facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); - facade.computeSimulationResultsUntil(tEnd); - var actual = new GreaterThan(getFruitRes(), new RealValue(2.9)).evaluate(facade.getLatestConstraintSimulationResults().get()); + final var results = facade.simulateWithResults(makeTestPlanP0B1(), tEnd); + var actual = new GreaterThan(getFruitRes(), new RealValue(2.9)).evaluate(results.constraintsResults()); var expected = new Windows( Segment.of(interval(0, Inclusive, 2, Exclusive, SECONDS), true), - Segment.of(interval(2, Inclusive,5, Exclusive, SECONDS), false) + Segment.of(interval(2, Inclusive,5, Inclusive, SECONDS), false) ); assertEquals(expected, actual); } @@ -181,12 +180,11 @@ public void whenValueAboveDoubleOnSimplePlan() public void whenValueBelowDoubleOnSimplePlan() throws SimulationFacade.SimulationException, SchedulingInterruptedException { - facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); - facade.computeSimulationResultsUntil(tEnd); - var actual = new LessThan(getFruitRes(), new RealValue(3.0)).evaluate(facade.getLatestConstraintSimulationResults().get()); + final var results = facade.simulateWithResults(makeTestPlanP0B1(), tEnd); + var actual = new LessThan(getFruitRes(), new RealValue(3.0)).evaluate(results.constraintsResults()); var expected = new Windows( Segment.of(interval(0, Inclusive, 2, Exclusive, SECONDS), false), - Segment.of(interval(2, Inclusive, 5, Exclusive, SECONDS), true) + Segment.of(interval(2, Inclusive, 5, Inclusive, SECONDS), true) ); assertEquals(expected, actual); } @@ -195,13 +193,12 @@ public void whenValueBelowDoubleOnSimplePlan() public void whenValueBetweenDoubleOnSimplePlan() throws SimulationFacade.SimulationException, SchedulingInterruptedException { - facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); - facade.computeSimulationResultsUntil(tEnd); - var actual = new And(new GreaterThanOrEqual(getFruitRes(), new RealValue(3.0)), new LessThanOrEqual(getFruitRes(), new RealValue(3.99))).evaluate(facade.getLatestConstraintSimulationResults().get()); + final var results = facade.simulateWithResults(makeTestPlanP0B1(), tEnd); + var actual = new And(new GreaterThanOrEqual(getFruitRes(), new RealValue(3.0)), new LessThanOrEqual(getFruitRes(), new RealValue(3.99))).evaluate(results.constraintsResults()); var expected = new Windows( Segment.of(interval(0, Inclusive, 1, Exclusive, SECONDS), false), Segment.of(interval(1, Inclusive, 2, Exclusive, SECONDS), true), - Segment.of(interval(2, Inclusive, 5, Exclusive, SECONDS), false) + Segment.of(interval(2, Inclusive, 5, Inclusive, SECONDS), false) ); assertEquals(expected, actual); } @@ -210,13 +207,12 @@ public void whenValueBetweenDoubleOnSimplePlan() public void whenValueEqualDoubleOnSimplePlan() throws SimulationFacade.SimulationException, SchedulingInterruptedException { - facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); - facade.computeSimulationResultsUntil(tEnd); - var actual = new Equal<>(getFruitRes(), new RealValue(3.0)).evaluate(facade.getLatestConstraintSimulationResults().get()); + final var results = facade.simulateWithResults(makeTestPlanP0B1(), tEnd); + var actual = new Equal<>(getFruitRes(), new RealValue(3.0)).evaluate(results.constraintsResults()); var expected = new Windows( Segment.of(interval(0, Inclusive, 1, Exclusive, SECONDS), false), Segment.of(interval(1, Inclusive, 2, Exclusive, SECONDS), true), - Segment.of(interval(2, Inclusive, 5, Exclusive, SECONDS), false) + Segment.of(interval(2, Inclusive, 5, Inclusive, SECONDS), false) ); assertEquals(expected, actual); } @@ -225,13 +221,12 @@ public void whenValueEqualDoubleOnSimplePlan() public void whenValueNotEqualDoubleOnSimplePlan() throws SimulationFacade.SimulationException, SchedulingInterruptedException { - facade.insertActivitiesIntoSimulation(makeTestPlanP0B1().getActivities()); - facade.computeSimulationResultsUntil(tEnd); - var actual = new NotEqual<>(getFruitRes(), new RealValue(3.0)).evaluate(facade.getLatestConstraintSimulationResults().get()); + final var results = facade.simulateWithResults(makeTestPlanP0B1(), tEnd); + var actual = new NotEqual<>(getFruitRes(), new RealValue(3.0)).evaluate(results.constraintsResults()); var expected = new Windows( Segment.of(interval(0, Inclusive, 1, Exclusive, SECONDS), true), Segment.of(interval(1, Inclusive, 2, Exclusive, SECONDS), false), - Segment.of(interval(2, Inclusive, 5, Exclusive, SECONDS), true) + Segment.of(interval(2, Inclusive, 5, Inclusive, SECONDS), true) ); assertEquals(expected, actual); } @@ -273,7 +268,6 @@ public void testCoexistenceGoalWithResourceConstraint() throws SchedulingInterru final var solver = new PrioritySolver(this.problem); final var plan = solver.getNextSolution().orElseThrow(); assertTrue(TestUtility.containsActivity(plan, t2, t2, actTypePeel)); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -315,7 +309,6 @@ public void testProceduralGoalWithResourceConstraint() throws SchedulingInterrup assertTrue(TestUtility.containsExactlyActivity(plan, act2)); assertTrue(TestUtility.doesNotContainActivity(plan, act1)); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -357,6 +350,5 @@ public void testActivityTypeWithResourceConstraint() throws SchedulingInterrupte final var plan = solver.getNextSolution().orElseThrow(); assertTrue(TestUtility.containsExactlyActivity(plan, act2)); assertTrue(TestUtility.doesNotContainActivity(plan, act1)); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationUtility.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationUtility.java index 468fb7bcde..b2e03d19c6 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationUtility.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/SimulationUtility.java @@ -5,13 +5,17 @@ import gov.nasa.jpl.aerie.merlin.driver.DirectiveTypeRegistry; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; import gov.nasa.jpl.aerie.merlin.driver.MissionModelBuilder; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; import gov.nasa.jpl.aerie.scheduler.model.Problem; -import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; +import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; +import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; import java.nio.file.Path; import java.time.Instant; +import java.util.Map; public final class SimulationUtility { @@ -32,32 +36,46 @@ private static MissionModel makeMissionModel(final MissionModelBuilder builde return builder.build(model, registry); } - public static Problem buildProblemFromFoo(final PlanningHorizon planningHorizon){ + public static Problem buildProblemFromFoo(final PlanningHorizon planningHorizon) { + return buildProblemFromFoo(planningHorizon, 1); + } + + public static Problem buildProblemFromFoo(final PlanningHorizon planningHorizon, final int simulationCacheSize){ final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var fooSchedulerModel = SimulationUtility.getFooSchedulerModel(); return new Problem( fooMissionModel, planningHorizon, - new SimulationFacade( - planningHorizon, + new CheckpointSimulationFacade( fooMissionModel, fooSchedulerModel, - ()->false), + new InMemoryCachedEngineStore(simulationCacheSize), + planningHorizon, + new SimulationEngineConfiguration( + Map.of(), + Instant.EPOCH, + new MissionModelId(1)), + () -> false), fooSchedulerModel); } public static Problem buildProblemFromBanana(final PlanningHorizon planningHorizon){ - final var fooMissionModel = SimulationUtility.getBananaMissionModel(); - final var fooSchedulerModel = SimulationUtility.getBananaSchedulerModel(); + final var bananaMissionModel = SimulationUtility.getBananaMissionModel(); + final var bananaSchedulerModel = SimulationUtility.getBananaSchedulerModel(); return new Problem( - fooMissionModel, + bananaMissionModel, planningHorizon, - new SimulationFacade( + new CheckpointSimulationFacade( + bananaMissionModel, + bananaSchedulerModel, + new InMemoryCachedEngineStore(15), planningHorizon, - fooMissionModel, - fooSchedulerModel, + new SimulationEngineConfiguration( + Map.of(), + Instant.EPOCH, + new MissionModelId(1)), ()->false), - fooSchedulerModel); + bananaSchedulerModel); } public static SchedulerModel getFooSchedulerModel(){ diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestApplyWhen.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestApplyWhen.java index 504df48b49..11657ff701 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestApplyWhen.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestApplyWhen.java @@ -23,37 +23,42 @@ import gov.nasa.jpl.aerie.constraints.tree.SpansWrapperExpression; import gov.nasa.jpl.aerie.constraints.tree.ValueAt; import gov.nasa.jpl.aerie.constraints.tree.WindowsWrapperExpression; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; -import gov.nasa.jpl.aerie.scheduler.constraints.TimeRangeExpression; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeAnchor; import gov.nasa.jpl.aerie.scheduler.goals.CardinalityGoal; import gov.nasa.jpl.aerie.scheduler.goals.ChildCustody; import gov.nasa.jpl.aerie.scheduler.goals.CoexistenceGoal; import gov.nasa.jpl.aerie.scheduler.goals.RecurrenceGoal; +import gov.nasa.jpl.aerie.scheduler.model.Problem; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; import gov.nasa.jpl.aerie.scheduler.model.PlanInMemory; import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; -import gov.nasa.jpl.aerie.scheduler.model.Problem; -import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; +import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; +import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.time.Instant; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Map; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOURS; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTE; import static gov.nasa.jpl.aerie.scheduler.SimulationUtility.buildProblemFromFoo; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertFalse; public class TestApplyWhen { - private static final Logger logger = LoggerFactory.getLogger(TestApplyWhen.class); ////////////////////////////////////////////RECURRENCE//////////////////////////////////////////// @@ -87,7 +92,6 @@ public void testRecurrenceCutoff1() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -120,7 +124,6 @@ public void testRecurrenceCutoff2() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -153,7 +156,6 @@ public void testRecurrenceShorterWindow() throws SchedulingInterruptedException assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -186,7 +188,6 @@ public void testRecurrenceLongerWindow() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -232,7 +233,6 @@ public void testRecurrenceBabyWindow() throws SchedulingInterruptedException { assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -275,7 +275,6 @@ public void testRecurrenceWindows() throws SchedulingInterruptedException { assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -318,7 +317,6 @@ public void testRecurrenceWindowsCutoffMidInterval() throws SchedulingInterrupte assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -363,7 +361,6 @@ public void testRecurrenceWindowsGlobalCheck() throws SchedulingInterruptedExcep assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(8, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(17, Duration.SECONDS), activityType)); //interval (len 4) needs to be 2 longer than the recurrence repeatingEvery (len 3) - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -408,7 +405,6 @@ public void testRecurrenceWindowsCutoffMidActivity() throws SchedulingInterrupte assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); //assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); //should fail, won't work if cutoff mid-activity - interval expected to be longer than activity duration!!! assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -440,7 +436,6 @@ public void testRecurrenceCutoffUncontrollable() throws SchedulingInterruptedExc assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } @@ -450,7 +445,6 @@ public void testRecurrenceCutoffUncontrollable() throws SchedulingInterruptedExc public void testCardinality() throws SchedulingInterruptedException { Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(5, Duration.SECONDS)); - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); final var problem = buildProblemFromFoo(planningHorizon); @@ -482,7 +476,6 @@ public void testCardinality() throws SchedulingInterruptedException { assertEquals(plan.get().getActivitiesByTime().stream() .map(SchedulingActivityDirective::duration) .reduce(Duration.ZERO, Duration::plus), Duration.of(4, Duration.SECOND)); //1 gets added, then throws 4 warnings meaning it tried to schedule 5 in total, not the expected 8... - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -529,7 +522,6 @@ public void testCardinalityWindows() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(3, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(13, Duration.SECONDS), activityType)); - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -577,7 +569,6 @@ public void testCardinalityWindowsCutoffMidActivity() throws SchedulingInterrupt //assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(7, Duration.SECONDS), activityType)); assertFalse(TestUtility.activityStartingAtTime(plan,Duration.of(9, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -587,7 +578,6 @@ public void testCardinalityUncontrollable() throws SchedulingInterruptedExceptio */ Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(20, Duration.SECONDS)); - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); final var problem = buildProblemFromFoo(planningHorizon); @@ -621,7 +611,6 @@ public void testCardinalityUncontrollable() throws SchedulingInterruptedExceptio .reduce(Duration.ZERO, Duration::plus); assertTrue(size >= 3 && size <= 10); assertTrue(totalDuration.dividedBy(Duration.SECOND) >= 16 && totalDuration.dividedBy(Duration.SECOND) <= 19); - assertEquals(9, problem.getSimulationFacade().countSimulationRestarts()); } @@ -632,7 +621,6 @@ public void testCoexistenceWindowCutoff() throws SchedulingInterruptedException Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(12, Duration.SECONDS)); - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); final var problem = buildProblemFromFoo(planningHorizon); @@ -673,19 +661,11 @@ public void testCoexistenceWindowCutoff() throws SchedulingInterruptedException logger.debug(a.startOffset().toString() + ", " + a.duration().toString()); } assertEquals(4, plan.get().getActivitiesByTime().size()); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test public void testCoexistenceJustFits() throws SchedulingInterruptedException { - Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(13, Duration.SECONDS));//13, so it just fits in - - var periodTre = new TimeRangeExpression.Builder() - .from(new Windows(false).set(period, true)) - .build(); - - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); final var problem = buildProblemFromFoo(planningHorizon); @@ -726,7 +706,6 @@ public void testCoexistenceJustFits() throws SchedulingInterruptedException { logger.debug(a.startOffset().toString() + ", " + a.duration().toString()); } assertEquals(5, plan.get().getActivitiesByTime().size()); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -741,11 +720,6 @@ public void testCoexistenceUncontrollableCutoff() throws SchedulingInterruptedEx Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(13, Duration.SECONDS)); - var periodTre = new TimeRangeExpression.Builder() - .from(new Windows(false).set(period, true)) - .build(); - - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); final var problem = buildProblemFromFoo(planningHorizon); @@ -765,7 +739,7 @@ public void testCoexistenceUncontrollableCutoff() throws SchedulingInterruptedEx .ofType(actTypeA) .build(); - //and cut off in the middle of one of the already present activities (period ends at 18) + //and cut off in the middle of one of the already present activities (period ends at 13) final var actTypeB = problem.getActivityType("BasicActivity"); CoexistenceGoal goal = new CoexistenceGoal.Builder() .forAllTimeIn(new WindowsWrapperExpression(new Windows(false).set(period, true))) @@ -785,10 +759,7 @@ public void testCoexistenceUncontrollableCutoff() throws SchedulingInterruptedEx for(SchedulingActivityDirective a : plan.get().getActivitiesByTime()){ logger.debug(a.startOffset().toString() + ", " + a.duration().toString()); } - assertEquals(2, plan.get().getActivitiesByTime() - .stream().filter($ -> $.duration().dividedBy(Duration.SECOND) == 2).toList() - .size()); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); + assertEquals(2, plan.get().getActivitiesByType().get(actTypeB).size()); } @Test @@ -800,7 +771,6 @@ public void testCoexistenceWindows() throws SchedulingInterruptedException { // GOAL WINDOW: [++++-------+++++++----] // RESULT: [++-----------++-------] - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); final var problem = buildProblemFromFoo(planningHorizon); @@ -853,7 +823,6 @@ public void testCoexistenceWindows() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(1, Duration.SECONDS), actTypeA)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(14, Duration.SECONDS), actTypeA)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(12, Duration.SECONDS), actTypeA)); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -865,7 +834,6 @@ public void testCoexistenceWindowsCutoffMidActivity() throws SchedulingInterrupt // GOAL WINDOW: [++++-----+++++-+++--+-++++++] // RESULT: [-\\------++----++-------++--] (the first one won't be scheduled, ask Adrien) - FIXED - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(28)); //this boundary is inclusive. final var problem = buildProblemFromFoo(planningHorizon); @@ -927,7 +895,6 @@ public void testCoexistenceWindowsCutoffMidActivity() throws SchedulingInterrupt assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(16, Duration.SECONDS), actTypeB)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(23, Duration.SECONDS), actTypeB)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(25, Duration.SECONDS), actTypeB)); - assertEquals(6, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -942,7 +909,6 @@ public void testCoexistenceWindowsBisect() throws SchedulingInterruptedException RESULT: [++-------|++-] */ - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(12)); final var problem = buildProblemFromFoo(planningHorizon); @@ -994,7 +960,6 @@ public void testCoexistenceWindowsBisect() throws SchedulingInterruptedException assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(1, Duration.SECONDS), actTypeA)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(8, Duration.SECONDS), actTypeA)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(10, Duration.SECONDS), actTypeA)); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -1008,7 +973,6 @@ public void testCoexistenceWindowsBisect2() throws SchedulingInterruptedExceptio RESULT: [++--++--++---++-] */ - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(16)); final var problem = buildProblemFromFoo(planningHorizon); @@ -1060,7 +1024,6 @@ public void testCoexistenceWindowsBisect2() throws SchedulingInterruptedExceptio assertFalse(TestUtility.activityStartingAtTime(plan.get(), Duration.of(5, Duration.SECONDS), actTypeA)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(9, Duration.SECONDS), actTypeA)); assertFalse(TestUtility.activityStartingAtTime(plan.get(), Duration.of(14, Duration.SECONDS), actTypeA)); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -1068,11 +1031,6 @@ public void testCoexistenceUncontrollableJustFits() throws SchedulingInterrupted Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(13, Duration.SECONDS)); - var periodTre = new TimeRangeExpression.Builder() - .from(new Windows(false).set(period, true)) - .build(); - - final var fooMissionModel = SimulationUtility.getFooMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); final var problem = buildProblemFromFoo(planningHorizon); @@ -1113,29 +1071,13 @@ public void testCoexistenceUncontrollableJustFits() throws SchedulingInterrupted logger.debug(a.startOffset().toString() + ", " + a.duration().toString()); } assertEquals(5, plan.get().getActivitiesByTime().size()); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test public void testCoexistenceExternalResource() throws SchedulingInterruptedException { Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(25, Duration.SECONDS)); - - final var fooMissionModel = SimulationUtility.getFooMissionModel(); - final var fooSchedulerMissionModel = SimulationUtility.getFooSchedulerModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); - - final var simulationFacade = new SimulationFacade( - planningHorizon, - fooMissionModel, - fooSchedulerMissionModel, - ()-> false); - final var problem = new Problem( - fooMissionModel, - planningHorizon, - simulationFacade, - fooSchedulerMissionModel - ); - + final var problem = SimulationUtility.buildProblemFromFoo(planningHorizon); final var r3Value = Map.of("amountInMicroseconds", SerializedValue.of(6)); final var r1 = new LinearProfile(new Segment<>(Interval.between(Duration.ZERO, Duration.SECONDS.times(5)), new LinearEquation(Duration.ZERO, 5, 1))); final var r2 = new DiscreteProfile(new Segment<>(Interval.FOREVER, SerializedValue.of(5))); @@ -1183,7 +1125,6 @@ public void testCoexistenceExternalResource() throws SchedulingInterruptedExcept assertEquals(act.duration(), Duration.of(r3Value.get("amountInMicroseconds").asInt().get(), Duration.MICROSECONDS)); assertEquals(startOfActivity, Duration.of(1, Duration.SECONDS)); assertEquals(act.startOffset(), startOfActivity); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -1193,10 +1134,12 @@ public void testCoexistenceWithAnchors() throws SchedulingInterruptedException { final var bananaMissionModel = SimulationUtility.getBananaMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochHours(0), TestUtility.timeFromEpochHours(20)); - final var simulationFacade = new SimulationFacade( - planningHorizon, + final var simulationFacade = new CheckpointSimulationFacade( bananaMissionModel, SimulationUtility.getBananaSchedulerModel(), + new InMemoryCachedEngineStore(10), + planningHorizon, + new SimulationEngineConfiguration(Map.of(), Instant.now(), new MissionModelId(0)), () -> false); final var problem = new Problem( bananaMissionModel, @@ -1316,7 +1259,6 @@ public void changingForAllTimeIn() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(13, Duration.SECONDS), activityTypeDependent)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(15, Duration.SECONDS), activityTypeDependent)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(17, Duration.SECONDS), activityTypeDependent)); - assertEquals(11, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -1391,7 +1333,6 @@ public void changingForAllTimeInCutoff() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(13, Duration.SECONDS), activityTypeDependent)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(15, Duration.SECONDS), activityTypeDependent)); assertFalse(TestUtility.activityStartingAtTime(plan.get(), Duration.of(17, Duration.SECONDS), activityTypeDependent)); - assertEquals(10, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -1473,6 +1414,5 @@ public void changingForAllTimeInAlternativeCutoff() throws SchedulingInterrupted assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(13, Duration.SECONDS), activityTypeDependent)); assertTrue(TestUtility.activityStartingAtTime(plan.get(), Duration.of(15, Duration.SECONDS), activityTypeDependent)); assertFalse(TestUtility.activityStartingAtTime(plan.get(), Duration.of(17, Duration.SECONDS), activityTypeDependent)); - assertEquals(10, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestCardinalityGoal.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestCardinalityGoal.java index df9919cfa3..19bacdbfc9 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestCardinalityGoal.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestCardinalityGoal.java @@ -14,7 +14,6 @@ import java.util.List; -import static gov.nasa.jpl.aerie.scheduler.SimulationUtility.buildProblemFromFoo; import static org.junit.jupiter.api.Assertions.assertEquals; public class TestCardinalityGoal { @@ -24,8 +23,7 @@ public void testone() throws SchedulingInterruptedException { Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(20, Duration.SECONDS)); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); - final var problem = buildProblemFromFoo(planningHorizon); - + final var problem = SimulationUtility.buildProblemFromFoo(planningHorizon); CardinalityGoal goal = new CardinalityGoal.Builder() .duration(Interval.between(Duration.of(12, Duration.SECONDS), Duration.of(15, Duration.SECONDS))) @@ -48,6 +46,5 @@ public void testone() throws SchedulingInterruptedException { assertEquals(plan.get().getActivitiesByTime().stream() .map(SchedulingActivityDirective::duration) .reduce(Duration.ZERO, Duration::plus), Duration.of(12, Duration.SECOND)); - assertEquals(7, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestPersistentAnchor.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestPersistentAnchor.java index 19a6ee5c34..3d065f6021 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestPersistentAnchor.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestPersistentAnchor.java @@ -10,6 +10,8 @@ import gov.nasa.jpl.aerie.constraints.tree.Expression; import gov.nasa.jpl.aerie.constraints.tree.ForEachActivitySpans; import gov.nasa.jpl.aerie.constraints.tree.WindowsWrapperExpression; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; @@ -17,13 +19,15 @@ import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeExpressionRelative; import gov.nasa.jpl.aerie.scheduler.goals.CoexistenceGoal; import gov.nasa.jpl.aerie.scheduler.model.*; -import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; +import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; +import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import org.apache.commons.lang3.function.TriFunction; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.time.Instant; import java.util.*; import java.util.stream.Collectors; @@ -430,10 +434,12 @@ public TestData createTestCaseStartsAt(final PersistentTimeAnchor persistentAnch final var bananaMissionModel = SimulationUtility.getBananaMissionModel(); final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochHours(0), TestUtility.timeFromEpochHours(20)); - final var simulationFacade = new SimulationFacade( - planningHorizon, + final var simulationFacade = new CheckpointSimulationFacade( bananaMissionModel, SimulationUtility.getBananaSchedulerModel(), + new InMemoryCachedEngineStore(10), + planningHorizon, + new SimulationEngineConfiguration(Map.of(), Instant.now(), new MissionModelId(0)), () -> false); final var problem = new Problem( bananaMissionModel, diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestRecurrenceGoal.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestRecurrenceGoal.java index d7bcfabb9e..e6c1890d7c 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestRecurrenceGoal.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestRecurrenceGoal.java @@ -13,7 +13,6 @@ import java.util.List; import static gov.nasa.jpl.aerie.scheduler.SimulationUtility.buildProblemFromFoo; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -45,7 +44,6 @@ public void testRecurrence() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -54,7 +52,7 @@ public void testRecurrenceNegative() { final var problem = buildProblemFromFoo(planningHorizon); try { final var activityType = problem.getActivityType("ControllableDurationActivity"); - final var goal = new RecurrenceGoal.Builder() + new RecurrenceGoal.Builder() .named("Test recurrence goal") .forAllTimeIn(new WindowsWrapperExpression(new Windows(false).set(Interval.betweenClosedOpen(Duration.of(1, Duration.SECONDS), Duration.of(20, Duration.SECONDS)), true))) @@ -65,6 +63,7 @@ public void testRecurrenceNegative() { .repeatingEvery(Duration.of(-1, Duration.SECONDS)) .withinPlanHorizon(planningHorizon) .build(); + fail(); } catch (IllegalArgumentException e) { //minimum is checked first so that's the output, even though the value for repeatingEvery is set as both the min @@ -75,7 +74,6 @@ public void testRecurrenceNegative() { catch (Exception e) { fail(e.getMessage()); } - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestRecurrenceGoalExtended.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestRecurrenceGoalExtended.java index a68978ad4b..59e1bcf018 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestRecurrenceGoalExtended.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestRecurrenceGoalExtended.java @@ -13,7 +13,6 @@ import java.util.List; import static gov.nasa.jpl.aerie.scheduler.SimulationUtility.buildProblemFromFoo; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class TestRecurrenceGoalExtended { @@ -47,7 +46,6 @@ public void testRecurrence() throws SchedulingInterruptedException { assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(6, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(11, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(16, Duration.SECONDS), activityType)); - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } /** @@ -76,7 +74,6 @@ public void testRecurrenceSecondGoalOutOfWindowAndPlanHorizon() throws Schedulin var plan = solver.getNextSolution().orElseThrow(); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(1, Duration.SECONDS), activityType) && (plan.getActivities().size() == 1)); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } /** @@ -105,7 +102,6 @@ public void testRecurrenceRepeatIntervalLargerThanGoalWindow() throws Scheduling var plan = solver.getNextSolution().orElseThrow(); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(1, Duration.SECONDS), activityType)); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } /** @@ -140,7 +136,6 @@ public void testGoalWindowLargerThanPlanHorizon() throws SchedulingInterruptedEx var plan = solver.getNextSolution().orElseThrow(); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(1, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(8, Duration.SECONDS), activityType)); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @@ -170,7 +165,6 @@ public void testGoalDurationLargerGoalWindow() throws SchedulingInterruptedExcep var plan = solver.getNextSolution().orElseThrow(); assertTrue(TestUtility.emptyPlan(plan)); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @@ -200,7 +194,6 @@ public void testGoalDurationLargerRepeatInterval() throws SchedulingInterruptedE var plan = solver.getNextSolution().orElseThrow(); assertTrue(TestUtility.emptyPlan(plan)); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @@ -248,6 +241,5 @@ public void testAddActivityNonEmptyPlan() throws SchedulingInterruptedException assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(5, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(10, Duration.SECONDS), activityType)); assertTrue(TestUtility.activityStartingAtTime(plan,Duration.of(15, Duration.SECONDS), activityType)); - assertEquals(5, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestUnsatisfiableCompositeGoals.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestUnsatisfiableCompositeGoals.java index 6a538b1b92..83dbb41db2 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestUnsatisfiableCompositeGoals.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/TestUnsatisfiableCompositeGoals.java @@ -20,8 +20,12 @@ import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import java.util.List; +import java.util.stream.Stream; import static gov.nasa.jpl.aerie.scheduler.SimulationUtility.buildProblemFromFoo; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -44,6 +48,10 @@ private static Problem makeTestMissionAB() { return SimulationUtility.buildProblemFromFoo(h); } + private static Problem makeTestMissionABWithCache() { + return SimulationUtility.buildProblemFromFoo(h, 15); + } + private static PlanInMemory makePlanA12(Problem problem) { final var plan = new PlanInMemory(); final var actTypeA = problem.getActivityType("ControllableDurationActivity"); @@ -70,9 +78,13 @@ public CoexistenceGoal BForEachAGoal(ActivityType A, ActivityType B){ .build(); } - @Test - public void testAndWithoutBackTrack() throws SchedulingInterruptedException { - final var problem = makeTestMissionAB(); + static Stream testAndWithoutBackTrack() { + return Stream.of(Arguments.of(makeTestMissionAB()), + Arguments.of(makeTestMissionABWithCache())); + } + @ParameterizedTest + @MethodSource + public void testAndWithoutBackTrack(Problem problem) throws SchedulingInterruptedException { problem.setInitialPlan(makePlanA12(problem)); final var actTypeControllable = problem.getActivityType("ControllableDurationActivity"); final var actTypeBasic = problem.getActivityType("BasicActivity"); @@ -102,7 +114,6 @@ public void testAndWithoutBackTrack() throws SchedulingInterruptedException { Assertions.assertTrue(TestUtility.activityStartingAtTime(plan, t2hr, actTypeBar)); Assertions.assertTrue(TestUtility.activityStartingAtTime(plan, t2hr, actTypeBasic)); Assertions.assertEquals(plan.getActivities().size(), 5); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -135,7 +146,6 @@ public void testAndWithBackTrack() throws SchedulingInterruptedException { Assertions.assertTrue(TestUtility.activityStartingAtTime(plan, t1hr, actTypeControllable)); Assertions.assertTrue(TestUtility.activityStartingAtTime(plan, t2hr, actTypeControllable)); Assertions.assertEquals(plan.getActivities().size(), 2); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -175,7 +185,6 @@ public void testOrWithoutBacktrack() throws SchedulingInterruptedException { Assertions.assertTrue(TestUtility.activityStartingAtTime(plan, t2hr, actTypeBar)); Assertions.assertTrue(TestUtility.activityStartingAtTime(plan, t2hr, actTypeBasic)); Assertions.assertEquals(plan.getActivities().size(), 4); - assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -210,7 +219,6 @@ public void testOrWithBacktrack() throws SchedulingInterruptedException { Assertions.assertTrue(TestUtility.activityStartingAtTime(plan, t1hr, actTypeControllable)); Assertions.assertTrue(TestUtility.activityStartingAtTime(plan, t2hr, actTypeControllable)); Assertions.assertEquals(plan.getActivities().size(), 2); - assertEquals(1, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -249,6 +257,5 @@ public void testCardinalityBacktrack() throws SchedulingInterruptedException { var plan = solver.getNextSolution().orElseThrow(); assertEquals(0, plan.getActivities().size()); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/UncontrollableDurationTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/UncontrollableDurationTest.java index 7dca6799e1..bf965c5161 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/UncontrollableDurationTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/UncontrollableDurationTest.java @@ -23,7 +23,6 @@ import java.util.List; import static gov.nasa.jpl.aerie.scheduler.SimulationUtility.buildProblemFromFoo; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class UncontrollableDurationTest { @@ -93,7 +92,6 @@ public void testNonLinear() throws SchedulingInterruptedException { assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT0S"), planningHorizon.fromStart("PT1M29S"), problem.getActivityType("SolarPanelNonLinear"))); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT16M40S"), planningHorizon.fromStart("PT18M9S"), problem.getActivityType("SolarPanelNonLinear"))); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT33M20S"), planningHorizon.fromStart("PT34M49S"), problem.getActivityType("SolarPanelNonLinear"))); - assertEquals(11, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -146,7 +144,6 @@ public void testTimeDependent() throws SchedulingInterruptedException { assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT33M20S"), planningHorizon.fromStart("PT36M47S"), problem.getActivityType("SolarPanelNonLinearTimeDependent"))); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT0S"), planningHorizon.fromStart("PT2M21S"), problem.getActivityType("SolarPanelNonLinearTimeDependent"))); assertTrue(TestUtility.containsActivity(plan, planningHorizon.fromStart("PT16M40S"), planningHorizon.fromStart("PT17M18S"), problem.getActivityType("SolarPanelNonLinearTimeDependent"))); - assertEquals(21, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -184,7 +181,6 @@ public void testBug() throws SchedulingInterruptedException { planningHorizon.fromStart("PT0.000004S"), planningHorizon.fromStart("PT0.000004S"), problem.getActivityType("ZeroDurationUncontrollableActivity"))); - assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); } @Test @@ -220,7 +216,6 @@ public void testScheduleExceptionThrowingTask() throws SchedulingInterruptedExce planningHorizon.fromStart("PT1M38.886061S"), planningHorizon.fromStart("PT1M38.886061S"), problem.getActivityType("LateRiser"))); - assertEquals(4, problem.getSimulationFacade().countSimulationRestarts()); } } diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java index c6c47c1a72..8a75f8790f 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/AnchorSchedulerTest.java @@ -2,12 +2,17 @@ import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; +import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; import gov.nasa.jpl.aerie.merlin.driver.DirectiveTypeRegistry; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; +import gov.nasa.jpl.aerie.merlin.driver.OneStepTask; import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivity; import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivityId; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; +import gov.nasa.jpl.aerie.merlin.driver.SimulationResultsComputerInputs; import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; @@ -25,7 +30,6 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.jpl.aerie.scheduler.SchedulingInterruptedException; import org.apache.commons.lang3.tuple.Triple; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -47,12 +51,6 @@ public class AnchorSchedulerTest { private final Duration tenDays = Duration.duration(10 * 60 * 60 * 24, Duration.SECONDS); private final static Duration oneMinute = Duration.of(60, Duration.SECONDS); private final Map arguments = Map.of("unusedArg", SerializedValue.of("test-param")); - private ResumableSimulationDriver driver; - - @BeforeEach - void beforeEach() { - driver = new ResumableSimulationDriver<>(AnchorTestModel, tenDays, () -> false); - } @Nested public final class AnchorsSimulationDriverTests { @@ -61,6 +59,25 @@ public final class AnchorsSimulationDriverTests { private final SerializedValue computedAttributes = new SerializedValue.MapValue(Map.of()); private final Instant planStart = Instant.EPOCH; + + public SimulationResultsComputerInputs simulateActivities( final Map schedule){ + return CheckpointSimulationDriver.simulateWithCheckpoints( + AnchorTestModel, + schedule, + planStart, + tenDays, + planStart, + tenDays, + $ -> {}, + () -> false, + CheckpointSimulationDriver.CachedSimulationEngine.empty(AnchorTestModel), + (a) -> false, + CheckpointSimulationDriver.onceAllActivitiesAreFinished(), + new InMemoryCachedEngineStore(1), + new SimulationEngineConfiguration(Map.of(), planStart, new MissionModelId(1)) + ); + } + /** * Asserts equality based on the following fields of SimulationResults: * - startTime @@ -215,16 +232,14 @@ public void activitiesAnchoredToPlan() throws SchedulingInterruptedException { simulatedActivities, Map.of(), //unfinished planStart, - tenDays, // simulation duration + tenDays.minus(Duration.of(9, Duration.MINUTE)), // simulation duration modelTopicList, new TreeMap<>() //events ); - driver.simulateActivities(resolveToPlanStartAnchors); - final var actualSimResults = driver.getSimulationResultsUpTo(planStart, tenDays); + final var actualSimResults = simulateActivities(resolveToPlanStartAnchors).computeResults(); assertEqualsSimulationResults(expectedSimResults, actualSimResults); - assertEquals(1, driver.getCountSimulationRestarts()); } @Test @@ -333,11 +348,9 @@ public void activitiesAnchoredToOtherActivities() throws SchedulingInterruptedEx new TreeMap<>() //events ); - driver.simulateActivities(activitiesToSimulate); - final var actualSimResults = driver.getSimulationResults(planStart); + final var actualSimResults = simulateActivities(activitiesToSimulate).computeResults(); assertEqualsSimulationResults(expectedSimResults, actualSimResults); - assertEquals(1, driver.getCountSimulationRestarts()); } @Test @@ -350,8 +363,8 @@ public void activitiesAnchoredToOtherActivitiesSimple() throws SchedulingInterru activitiesToSimulate.put( new ActivityDirectiveId(1), new ActivityDirective(oneMinute, serializedDelayDirective, new ActivityDirectiveId(0), false)); - driver.simulateActivities(activitiesToSimulate); - final var durationOfAnchoredActivity = driver.getActivityDuration(new ActivityDirectiveId(1)); + final var simulationResults = simulateActivities(activitiesToSimulate); + final var durationOfAnchoredActivity = SimulationFacadeUtils.getActivityDuration(new ActivityDirectiveId(1), simulationResults); assertTrue(durationOfAnchoredActivity.isPresent()); } @@ -501,9 +514,7 @@ public void decomposingActivitiesAndAnchors() throws SchedulingInterruptedExcept new ActivityDirectiveId(23), new SimulatedActivity(serializedDecompositionDirective.getTypeName(), Map.of(), Instant.EPOCH.plus(4, ChronoUnit.MINUTES), threeMinutes, null, List.of(), Optional.of(new ActivityDirectiveId(23)), computedAttributes)); - // Custom assertion, as Decomposition children can end up simulated in different positions between runs - driver.simulateActivities(activitiesToSimulate); - final var actualSimResults = driver.getSimulationResults(planStart); + final var actualSimResults = simulateActivities(activitiesToSimulate).computeResults(); assertEquals(planStart, actualSimResults.startTime); assertTrue(actualSimResults.unfinishedActivities.isEmpty()); @@ -595,7 +606,6 @@ public void decomposingActivitiesAndAnchors() throws SchedulingInterruptedExcept // We have examined all the children assertTrue(childSimulatedActivities.isEmpty()); - assertEquals(1, driver.getCountSimulationRestarts()); } @Test @@ -635,12 +645,10 @@ public void naryTreeAnchorChain() throws SchedulingInterruptedException{ modelTopicList, new TreeMap<>() //events ); - driver.simulateActivities(activitiesToSimulate); - final var actualSimResults = driver.getSimulationResults(planStart); + final var actualSimResults = simulateActivities(activitiesToSimulate).computeResults(); assertEquals(3906, expectedSimResults.simulatedActivities.size()); assertEqualsSimulationResults(expectedSimResults, actualSimResults); - assertEquals(1, driver.getCountSimulationRestarts()); } } @@ -666,13 +674,13 @@ public OutputType getOutputType() { @Override public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> $ -> { + return executor -> new OneStepTask<>($ -> { $.emit(this, delayedActivityDirectiveInputTopic); - return TaskStatus.delayed(oneMinute, $$ -> { + return TaskStatus.delayed(oneMinute, new OneStepTask<>($$ -> { $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); return TaskStatus.completed(Unit.UNIT); - }); - }; + })); + }); } }; @@ -691,18 +699,18 @@ public OutputType getOutputType() { @Override public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> scheduler -> { + return executor -> new OneStepTask<>(scheduler -> { scheduler.emit(this, decomposingActivityDirectiveInputTopic); return TaskStatus.delayed( Duration.ZERO, - $ -> { + new OneStepTask<>($ -> { try { $.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); } catch (final InstantiationException ex) { throw new Error("Unexpected state: activity instantiation of DelayedActivityDirective failed with: %s".formatted( ex.toString())); } - return TaskStatus.delayed(Duration.of(120, Duration.SECOND), $$ -> { + return TaskStatus.delayed(Duration.of(120, Duration.SECOND), new OneStepTask<>($$ -> { try { $$.spawn(InSpan.Fresh, delayedActivityDirective.getTaskFactory(null, null)); } catch (final InstantiationException ex) { @@ -712,9 +720,9 @@ public TaskFactory getTaskFactory(final Object o, final Object o2) { } $$.emit(Unit.UNIT, decomposingActivityDirectiveOutputTopic); return TaskStatus.completed(Unit.UNIT); - }); - }); - }; + })); + })); + }); } }; diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacadeTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacadeTest.java new file mode 100644 index 0000000000..983b80b5e8 --- /dev/null +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/CheckpointSimulationFacadeTest.java @@ -0,0 +1,110 @@ +package gov.nasa.jpl.aerie.scheduler.simulation; + +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; +import gov.nasa.jpl.aerie.merlin.framework.ThreadedTask; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.scheduler.SchedulingInterruptedException; +import gov.nasa.jpl.aerie.scheduler.SimulationUtility; +import gov.nasa.jpl.aerie.scheduler.TimeUtility; +import gov.nasa.jpl.aerie.scheduler.model.ActivityType; +import gov.nasa.jpl.aerie.scheduler.model.PlanInMemory; +import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; +import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOUR; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class CheckpointSimulationFacadeTest { + private SimulationFacade newSimulationFacade; + private final static PlanningHorizon H = new PlanningHorizon(TimeUtility.fromDOY("2025-001T00:00:00.000"), TimeUtility.fromDOY("2025-005T00:00:00.000")); + private Map activityTypes; + private final static Duration t0 = H.getStartAerie(); + private final static Duration d1hr = Duration.of(1, HOUR); + private final static Duration t1hr = t0.plus(d1hr); + private final static Duration t2hr = t0.plus(d1hr.times(2)); + private static PlanInMemory makePlanA012(Map activityTypeMap) { + final var plan = new PlanInMemory(); + final var actTypeA = activityTypeMap.get("BasicActivity"); + plan.add(SchedulingActivityDirective.of(actTypeA, t0, null, null, true)); + plan.add(SchedulingActivityDirective.of(actTypeA, t1hr, null, null, true)); + plan.add(SchedulingActivityDirective.of(actTypeA, t2hr, null, null, true)); + return plan; + } + @BeforeEach + public void before(){ + ThreadedTask.CACHE_READS = true; + final var fooMissionModel = SimulationUtility.getFooMissionModel(); + activityTypes = new HashMap<>(); + for(var taskType : fooMissionModel.getDirectiveTypes().directiveTypes().entrySet()){ + activityTypes.put(taskType.getKey(), new ActivityType(taskType.getKey(), taskType.getValue(), SimulationUtility.getFooSchedulerModel().getDurationTypes().get(taskType.getKey()))); + } + newSimulationFacade = new CheckpointSimulationFacade( + fooMissionModel, + SimulationUtility.getFooSchedulerModel(), + new InMemoryCachedEngineStore(10), + H, + new SimulationEngineConfiguration(Map.of(), Instant.EPOCH, new MissionModelId(1)), + () -> false); + newSimulationFacade.addActivityTypes(activityTypes.values()); + } + + /** + * This is to check that one of the simulation interfaces, the one simulating a plan until a given time, is actually stopping + * at the given time. It does that by checking that calling the simulation did not update the activity's duration. + */ + @Test + public void simulateUntilTime() throws SimulationFacade.SimulationException, SchedulingInterruptedException { + final var plan = makePlanA012(activityTypes); + newSimulationFacade.simulateNoResults(plan, t2hr); + //we are stopping at 2hr, at the start of the last activity so it will not have a duration in the plan + assertNull(plan.getActivities().stream().filter(a -> a.startOffset().isEqualTo(t2hr)).findFirst().get().duration()); + } + + /** + * Simulating the same plan on a smaller horizon leads to re-using the simulation data + */ + @Test + public void noNeedForResimulation() throws SimulationFacade.SimulationException, SchedulingInterruptedException { + final var plan = makePlanA012(activityTypes); + final var ret = newSimulationFacade.simulateWithResults(plan, t2hr); + final var ret2 = newSimulationFacade.simulateWithResults(plan, t1hr); + SimulationResultsComparisonUtils.assertEqualsSimulationResults(ret.driverResults(), ret2.driverResults()); + } + + /** + * Simulating the same plan on a smaller horizon via a different request (no-results vs with-results) leads to the same + * simulation data + */ + @Test + public void simulationResultsTest() throws SchedulingInterruptedException, SimulationFacade.SimulationException { + final var plan = makePlanA012(activityTypes); + final var simResults = newSimulationFacade.simulateNoResultsAllActivities(plan).computeResults(); + final var simResults2 = newSimulationFacade.simulateWithResults(plan, t1hr); + SimulationResultsComparisonUtils.assertEqualsSimulationResults(simResults, simResults2.driverResults()); + } + + /** + * Tests that the simulation stops at the end of the planning horizon even if the plan we are trying to simulate + * is supposed to last longer. + */ + @Test + public void testStopsAtEndOfPlanningHorizon() + throws SchedulingInterruptedException, SimulationFacade.SimulationException + { + final var plan = new PlanInMemory(); + final var actTypeA = activityTypes.get("ControllableDurationActivity"); + plan.add(SchedulingActivityDirective.of(actTypeA, t0, HOUR.times(200), null, true)); + final var results = newSimulationFacade.simulateNoResultsAllActivities(plan).computeResults(); + assertEquals(H.getEndAerie(), newSimulationFacade.totalSimulationTime()); + assert(results.unfinishedActivities.size() == 1); + } + +} diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStoreTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStoreTest.java new file mode 100644 index 0000000000..52d6afa6d4 --- /dev/null +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InMemoryCachedEngineStoreTest.java @@ -0,0 +1,116 @@ +package gov.nasa.jpl.aerie.scheduler.simulation; + +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; +import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; +import gov.nasa.jpl.aerie.merlin.driver.CheckpointSimulationDriver; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; +import gov.nasa.jpl.aerie.merlin.driver.engine.SimulationEngine; +import gov.nasa.jpl.aerie.merlin.driver.engine.SlabList; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; +import gov.nasa.jpl.aerie.merlin.driver.timeline.CausalEventSource; +import gov.nasa.jpl.aerie.merlin.driver.timeline.LiveCells; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.scheduler.SimulationUtility; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class InMemoryCachedEngineStoreTest { + SimulationEngineConfiguration simulationEngineConfiguration; + MissionModelId missionModelId; + InMemoryCachedEngineStore store; + + @BeforeEach + void beforeEach(){ + this.missionModelId = new MissionModelId(1); + this.simulationEngineConfiguration = new SimulationEngineConfiguration(Map.of(), Instant.EPOCH, this.missionModelId); + this.store = new InMemoryCachedEngineStore(2); + } + + @AfterEach + void afterEach() { + this.store.close(); + } + + public static CheckpointSimulationDriver.CachedSimulationEngine getCachedEngine1(){ + return new CheckpointSimulationDriver.CachedSimulationEngine( + Duration.SECOND, + Map.of( + new ActivityDirectiveId(1), new ActivityDirective(Duration.HOUR, "ActivityType1", Map.of(), null, true), + new ActivityDirectiveId(2), new ActivityDirective(Duration.HOUR, "ActivityType2", Map.of(), null, true) + ), + new SimulationEngine(), + new LiveCells(new CausalEventSource()), + new SlabList<>(), + null, + SimulationUtility.getFooMissionModel() + ); + } + + public static CheckpointSimulationDriver.CachedSimulationEngine getCachedEngine2(){ + return new CheckpointSimulationDriver.CachedSimulationEngine( + Duration.SECOND, + Map.of( + new ActivityDirectiveId(3), new ActivityDirective(Duration.HOUR, "ActivityType3", Map.of(), null, true), + new ActivityDirectiveId(4), new ActivityDirective(Duration.HOUR, "ActivityType4", Map.of(), null, true) + ), + new SimulationEngine(), + new LiveCells(new CausalEventSource()), + new SlabList<>(), + null, + SimulationUtility.getFooMissionModel() + ); + } + + public static CheckpointSimulationDriver.CachedSimulationEngine getCachedEngine3(){ + return new CheckpointSimulationDriver.CachedSimulationEngine( + Duration.SECOND, + Map.of( + new ActivityDirectiveId(5), new ActivityDirective(Duration.HOUR, "ActivityType5", Map.of(), null, true), + new ActivityDirectiveId(6), new ActivityDirective(Duration.HOUR, "ActivityType6", Map.of(), null, true) + ), + new SimulationEngine(), + new LiveCells(new CausalEventSource()), + new SlabList<>(), + null, + SimulationUtility.getFooMissionModel() + ); + } + + @Test + public void duplicateTest(){ + final var store = new InMemoryCachedEngineStore(2); + store.save(CheckpointSimulationDriver.CachedSimulationEngine.empty(SimulationUtility.getFooMissionModel()), this.simulationEngineConfiguration); + store.save(CheckpointSimulationDriver.CachedSimulationEngine.empty(SimulationUtility.getFooMissionModel()), this.simulationEngineConfiguration); + store.save(CheckpointSimulationDriver.CachedSimulationEngine.empty(SimulationUtility.getFooMissionModel()), this.simulationEngineConfiguration); + assertEquals(1, store.getCachedEngines(this.simulationEngineConfiguration).size()); + } + + @Test + public void order(){ + final var cachedEngine1 = getCachedEngine1(); + final var cachedEngine2 = getCachedEngine2(); + final var cachedEngine3 = getCachedEngine3(); + store.save(cachedEngine1, this.simulationEngineConfiguration); + store.save(cachedEngine2, this.simulationEngineConfiguration); + final var cachedBeforeRegister = store.getCachedEngines(this.simulationEngineConfiguration); + // no engines have been used, so the cache is ordered in descending creation date + assertEquals(cachedBeforeRegister.get(0).activityDirectives(), cachedEngine1.activityDirectives()); + assertEquals(cachedBeforeRegister.get(1).activityDirectives(), cachedEngine2.activityDirectives()); + //engine2 has been used so it goes first in the list + store.registerUsed(cachedEngine2); + final var cachedAfterRegister = store.getCachedEngines(this.simulationEngineConfiguration); + assertEquals(cachedAfterRegister.get(0).activityDirectives(), cachedEngine2.activityDirectives()); + assertEquals(cachedAfterRegister.get(1).activityDirectives(), cachedEngine1.activityDirectives()); + store.save(cachedEngine3, this.simulationEngineConfiguration); + //to store cachedEngine3, we had to remove the last element of the list, engine 1 and the order is still most recently used + final var cachedAfterRemoveLast = store.getCachedEngines(this.simulationEngineConfiguration); + assertEquals(cachedAfterRemoveLast.get(0).activityDirectives(), cachedEngine2.activityDirectives()); + assertEquals(cachedAfterRemoveLast.get(1).activityDirectives(), cachedEngine3.activityDirectives()); + } +} diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InstantiateArgumentsTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InstantiateArgumentsTest.java index b9ab4b11bc..57a45b7c18 100644 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InstantiateArgumentsTest.java +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/InstantiateArgumentsTest.java @@ -11,6 +11,7 @@ import gov.nasa.jpl.aerie.constraints.tree.RealValue; import gov.nasa.jpl.aerie.constraints.tree.StructExpressionAt; import gov.nasa.jpl.aerie.constraints.tree.ValueAt; +import gov.nasa.jpl.aerie.merlin.driver.OneStepTask; import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic; import gov.nasa.jpl.aerie.merlin.protocol.model.DirectiveType; import gov.nasa.jpl.aerie.merlin.protocol.model.InputType; @@ -141,13 +142,13 @@ public OutputType getOutputType() { @Override public TaskFactory getTaskFactory(final Object o, final Object o2) { - return executor -> $ -> { + return executor -> new OneStepTask<>($ -> { $.emit(this, delayedActivityDirectiveInputTopic); - return TaskStatus.delayed(oneMinute, $$ -> { + return TaskStatus.delayed(oneMinute, new OneStepTask<>($$ -> { $$.emit(Unit.UNIT, delayedActivityDirectiveOutputTopic); return TaskStatus.completed(Unit.UNIT); - }); - }; + })); + }); } }; diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationTest.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationTest.java deleted file mode 100644 index 4f40990a76..0000000000 --- a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/ResumableSimulationTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.simulation; - -import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; -import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity; -import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.scheduler.SchedulingInterruptedException; -import gov.nasa.jpl.aerie.scheduler.SimulationUtility; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.time.Instant; -import java.util.ArrayList; -import java.util.Map; - -import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECONDS; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class ResumableSimulationTest { - ResumableSimulationDriver resumableSimulationDriver; - Duration endOfLastAct; - - private final Duration tenHours = Duration.of(10, Duration.HOURS); - private final Duration fiveHours = Duration.of(5, Duration.HOURS); - - @BeforeEach - public void init() throws SchedulingInterruptedException { - final var acts = getActivities(); - final var fooMissionModel = SimulationUtility.getFooMissionModel(); - resumableSimulationDriver = new ResumableSimulationDriver<>(fooMissionModel,tenHours, ()-> false); - for (var act : acts) { - resumableSimulationDriver.simulateActivity(act.start, act.activity, null, true, act.id); - } - } - @Test - public void simulationResultsTest() throws SchedulingInterruptedException { - final var now = Instant.now(); - //ensures that simulation results are generated until the end of the last act; - var simResults = resumableSimulationDriver.getSimulationResults(now); - assert(simResults.realProfiles.get("/utcClock").getRight().get(0).extent().isEqualTo(endOfLastAct)); - /* ensures that when current simulation results cover more than the asked period and that nothing has happened - between two requests, the same results are returned */ - var simResults2 = resumableSimulationDriver.getSimulationResultsUpTo(now, Duration.of(7, SECONDS)); - assertEquals(simResults, simResults2); - } - - @Test - public void simulationResultsTest2() throws SchedulingInterruptedException{ - /* ensures that when the passed start epoch is not equal to the one used for previously computed results, the results are re-computed */ - var simResults = resumableSimulationDriver.getSimulationResults(Instant.now()); - assert(simResults.realProfiles.get("/utcClock").getRight().get(0).extent().isEqualTo(endOfLastAct)); - var simResults2 = resumableSimulationDriver.getSimulationResultsUpTo(Instant.now(), Duration.of(7, SECONDS)); - assertNotEquals(simResults, simResults2); - } - - @Test - public void simulationResultsTest3() throws SchedulingInterruptedException{ - /* ensures that when current simulation results cover less than the asked period and that nothing has happened - between two requests, the results are re-computed */ - final var now = Instant.now(); - var simResults2 = resumableSimulationDriver.getSimulationResultsUpTo(now, Duration.of(7, SECONDS)); - var simResults = resumableSimulationDriver.getSimulationResults(now); - assert(simResults.realProfiles.get("/utcClock").getRight().get(0).extent().isEqualTo(endOfLastAct)); - assertNotEquals(simResults, simResults2); - } - - @Test - public void durationTest(){ - final var acts = getActivities(); - var act1Dur = resumableSimulationDriver.getActivityDuration(acts.get(0).id()); - var act2Dur = resumableSimulationDriver.getActivityDuration(acts.get(1).id()); - assertTrue(act1Dur.isPresent() && act2Dur.isPresent()); - assertTrue(act1Dur.get().isEqualTo(Duration.of(2, SECONDS))); - assertTrue(act2Dur.get().isEqualTo(Duration.of(2, SECONDS))); - } - - @Test - public void testStopsAtEndOfPlanningHorizon() throws SchedulingInterruptedException { - final var fooSchedulerModel = SimulationUtility.getFooSchedulerModel(); - final var activity = new TestSimulatedActivity( - Duration.of(0, SECONDS), - new SerializedActivity("ControllableDurationActivity", Map.of("duration", fooSchedulerModel.serializeDuration(tenHours))), - new ActivityDirectiveId(1)); - final var fooMissionModel = SimulationUtility.getFooMissionModel(); - resumableSimulationDriver = new ResumableSimulationDriver<>(fooMissionModel, fiveHours, ()-> false); - resumableSimulationDriver.initSimulation(); - resumableSimulationDriver.clearActivitiesInserted(); - resumableSimulationDriver.simulateActivity(activity.start, activity.activity, null, true, activity.id); - assertEquals(fiveHours, resumableSimulationDriver.getCurrentSimulationEndTime()); - assert(resumableSimulationDriver.getSimulationResults(Instant.now()).unfinishedActivities.size() == 1); - } - - private ArrayList getActivities(){ - final var acts = new ArrayList(); - var act1 = new TestSimulatedActivity( - Duration.of(0, SECONDS), - new SerializedActivity("BasicActivity", Map.of()), - new ActivityDirectiveId(1)); - acts.add(act1); - var act2 = new TestSimulatedActivity( - Duration.of(14, SECONDS), - new SerializedActivity("BasicActivity", Map.of()), - new ActivityDirectiveId(2)); - acts.add(act2); - - endOfLastAct = Duration.of(16,SECONDS); - return acts; - } - - record TestSimulatedActivity(Duration start, SerializedActivity activity, ActivityDirectiveId id){} -} diff --git a/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationResultsComparisonUtils.java b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationResultsComparisonUtils.java new file mode 100644 index 0000000000..25da0a83d4 --- /dev/null +++ b/scheduler-driver/src/test/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationResultsComparisonUtils.java @@ -0,0 +1,189 @@ +package gov.nasa.jpl.aerie.scheduler.simulation; + +import gov.nasa.jpl.aerie.merlin.driver.SimulatedActivity; +import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; +import gov.nasa.jpl.aerie.merlin.driver.engine.ProfileSegment; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.RealDynamics; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; + +import java.math.BigDecimal; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +public class SimulationResultsComparisonUtils { + + public static void assertEqualsSimulationResults(final SimulationResults expected, final SimulationResults simulationResults) + { + assertEquals(expected.unfinishedActivities, simulationResults.unfinishedActivities); + assertEquals(expected.topics, simulationResults.topics); + assertEqualsTSA(convertSimulatedActivitiesToTree(expected), convertSimulatedActivitiesToTree(simulationResults)); + final var differencesDiscrete = new HashMap>(); + for(final var discreteProfile: simulationResults.discreteProfiles.entrySet()){ + final var differences = equalsDiscreteProfile(expected.discreteProfiles.get(discreteProfile.getKey()).getRight(), discreteProfile.getValue().getRight()); + if(!differences.isEmpty()){ + differencesDiscrete.put(discreteProfile.getKey(), differences); + } + } + final var differencesReal = new HashMap>(); + for(final var realProfile: simulationResults.realProfiles.entrySet()){ + final var profileElements = realProfile.getValue().getRight(); + final var expectedProfileElements = expected.realProfiles.get(realProfile.getKey()).getRight(); + final var differences = equalsRealProfile(expectedProfileElements, profileElements); + if(!differences.isEmpty()) { + differencesReal.put(realProfile.getKey(), differences); + } + } + if(!differencesDiscrete.isEmpty() || !differencesReal.isEmpty()){ + fail("Differences in real profiles: " + differencesReal + "\n Differences in discrete profiles " + differencesDiscrete); + } + } + + public record RealProfileDifference(ProfileSegment expected, ProfileSegment actual){} + public record DiscreteProfileDifference(ProfileSegment expected, ProfileSegment actual){} + + public static Map equalsRealProfile(List> expected, List> actual){ + final var differences = new HashMap(); + for(int i = 0; i < expected.size(); i++){ + if(!actual.get(i).equals(expected.get(i))){ + differences.put(i, new RealProfileDifference(expected.get(i), actual.get(i))); + } + } + return differences; + } + public static Map equalsDiscreteProfile(List> expected, List> actual){ + final var differences = new HashMap(); + for(int i = 0; i < expected.size(); i++){ + if(!actual.get(i).equals(expected.get(i))){ + differences.put(i, new DiscreteProfileDifference(expected.get(i), actual.get(i))); + } + } + return differences; + } + + /** + * Recursively removes all fields with specific names from a SerializedValue + * @param serializedValue the serialized value + * @param fieldsToRemove the names of the fields to remove + * @return a serialized value without the removed fields + */ + public static SerializedValue removeFieldsFromSerializedValue( + final SerializedValue serializedValue, + final Collection fieldsToRemove){ + final var visitor = new SerializedValue.Visitor(){ + @Override + public SerializedValue onNull() { + return SerializedValue.NULL; + } + + @Override + public SerializedValue onNumeric(final BigDecimal value) { + return SerializedValue.of(value); + } + + @Override + public SerializedValue onBoolean(final boolean value) { + return SerializedValue.of(value); + } + + @Override + public SerializedValue onString(final String value) { + return SerializedValue.of(value); + } + + @Override + public SerializedValue onMap(final Map value) { + final var newVal = new HashMap(); + for(final var entry: value.entrySet()){ + if(!fieldsToRemove.contains(entry.getKey())){ + newVal.put(entry.getKey(), removeFieldsFromSerializedValue(entry.getValue(), fieldsToRemove)); + } + } + return SerializedValue.of(newVal); + } + + @Override + public SerializedValue onList(final List value) { + final var newList = new ArrayList(); + for(final var val : value){ + newList.add(removeFieldsFromSerializedValue(val, fieldsToRemove)); + } + return SerializedValue.of(newList); + } + }; + return serializedValue.match(visitor); + } + + /** + * Converts the activity instances from a SimulationResults object into a set of tree structure representing parent-child activities for comparison purposes + * @param simulationResults the simulation results + * @return a set of trees + */ + public static Set convertSimulatedActivitiesToTree(final SimulationResults simulationResults){ + return simulationResults.simulatedActivities.values().stream().map(simulatedActivity -> TreeSimulatedActivity.fromSimulatedActivity( + simulatedActivity, + simulationResults)).collect(Collectors.toSet()); + } + + /** + * Asserts whether two sets of activity instances are equal. + * @param expected the expected set of activities (as trees) + * @param actual the actual set of activities (as trees) + */ + public static void assertEqualsTSA(final Set expected, + final Set actual){ + assertEquals(expected.size(), actual.size()); + final var copyExpected = new HashSet<>(expected); + for(final var inB: actual){ + if(!copyExpected.contains(inB)){ + fail(); + } + //make sure identical trees are not used to validate twice + copyExpected.remove(inB); + } + } + + // Representation of simulated activities as trees of activities + public record TreeSimulatedActivity(StrippedSimulatedActivity activity, + Set children){ + public static TreeSimulatedActivity fromSimulatedActivity(SimulatedActivity simulatedActivity, SimulationResults simulationResults){ + final var stripped = StrippedSimulatedActivity.fromSimulatedActivity(simulatedActivity); + final HashSet children = new HashSet<>(); + for(final var childId: simulatedActivity.childIds()) { + final var child = fromSimulatedActivity(simulationResults.simulatedActivities.get(childId), simulationResults); + children.add(child); + } + return new TreeSimulatedActivity(stripped, children); + } + } + + //Representation of SimulatedActivity stripped of parent/child/directive id information + //used for comparison purposes + public record StrippedSimulatedActivity( + String type, + Map arguments, + Instant start, + Duration duration, + SerializedValue computedAttributes + ){ + public static StrippedSimulatedActivity fromSimulatedActivity(SimulatedActivity simulatedActivity){ + return new StrippedSimulatedActivity( + simulatedActivity.type(), + simulatedActivity.arguments(), + simulatedActivity.start(), + simulatedActivity.duration(), + simulatedActivity.computedAttributes() + ); + } + } +} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinService.java index 7f4d10bb5c..b467cf4073 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GraphQLMerlinService.java @@ -906,7 +906,7 @@ private Map getSpans(DatasetId datasetI } @Override - public Optional getSimulationResults(PlanMetadata planMetadata) + public Optional> getSimulationResults(PlanMetadata planMetadata) throws MerlinServiceException, IOException { final var simulationDatasetId = getSuitableSimulationResults(planMetadata); @@ -929,7 +929,7 @@ public Optional getSimulationResults(PlanMetadata planMetadat final var simulationEndTime = planMetadata.horizon().getEndInstant(); final var micros = java.time.Duration.between(simulationStartTime, simulationEndTime).toNanos() / 1000; final var duration = Duration.of(micros, MICROSECOND); - return Optional.of(new SimulationResults( + return Optional.of(Pair.of(new SimulationResults( unwrappedProfiles.realProfiles(), unwrappedProfiles.discreteProfiles(), simulatedActivities, @@ -938,7 +938,7 @@ public Optional getSimulationResults(PlanMetadata planMetadat duration, List.of(), new TreeMap<>() - )); + ), simulationDatasetId.get().datasetId)); } catch (InterruptedException | ExecutionException e) { return Optional.empty(); } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MerlinService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MerlinService.java index 6bed6ca691..e2ec0db998 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MerlinService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MerlinService.java @@ -84,12 +84,13 @@ void ensurePlanExists(final PlanId planId) throws IOException, NoSuchPlanException, MerlinServiceException; /** - * Gets existing simulation results for current plan if they exist and are suitable for scheduling purposes (current revision, covers the entire planning horizon) + * Gets existing simulation results for current plan if they exist and are suitable for scheduling purposes (current + * revision, covers the entire planning horizon) * These simulation results do not include events and topics. * @param planMetadata the plan metadata - * @return simulation results, optionally + * @return optionally: simulation results and its dataset id */ - Optional getSimulationResults(PlanMetadata planMetadata) throws MerlinServiceException, IOException, InvalidJsonException; + Optional> getSimulationResults(PlanMetadata planMetadata) throws MerlinServiceException, IOException, InvalidJsonException; /** diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulerAgent.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulerAgent.java index 2b9870b73a..889abf0d2b 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulerAgent.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulerAgent.java @@ -23,6 +23,7 @@ public interface SchedulerAgent { void schedule( ScheduleRequest request, ResultsProtocol.WriterRole writer, - Supplier canceledListener + Supplier canceledListener, + int sizeCachedEngineStore ) throws InterruptedException; } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ThreadedSchedulerAgent.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ThreadedSchedulerAgent.java index 19114b6919..d48cd6ac33 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ThreadedSchedulerAgent.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ThreadedSchedulerAgent.java @@ -25,7 +25,8 @@ private ThreadedSchedulerAgent(final BlockingQueue requestQue public void schedule( final ScheduleRequest request, final ResultsProtocol.WriterRole writer, - final Supplier canceledListener + final Supplier canceledListener, + final int sizeCachedEngineStore ) throws InterruptedException { this.requestQueue.put(new SchedulingRequest.Schedule(request, writer, canceledListener)); @@ -65,7 +66,7 @@ public void run() { if (request instanceof SchedulingRequest.Schedule req) { try { - this.schedulerAgent.schedule(req.request(), req.writer(), req.canceledListener); + this.schedulerAgent.schedule(req.request(), req.writer(), req.canceledListener, 0); } catch (final Throwable ex) { ex.printStackTrace(System.err); req.writer().failWith(b -> b diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java index a782e46611..e90dff569c 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java @@ -25,8 +25,12 @@ import gov.nasa.jpl.aerie.scheduler.worker.services.SchedulingDSLCompilationService; import gov.nasa.jpl.aerie.scheduler.worker.services.SynchronousSchedulerAgent; import io.javalin.Javalin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public final class SchedulerWorkerAppDriver { + private static final Logger logger = LoggerFactory.getLogger(SchedulerWorkerAppDriver.class); + public static void main(String[] args) throws Exception { final var config = loadConfiguration(); @@ -99,7 +103,11 @@ public static void main(String[] args) throws Exception { final var revisionData = new SpecificationRevisionData(specificationRevision, planRevision); final ResultsProtocol.WriterRole writer = owner.get(); try { - scheduleAgent.schedule(new ScheduleRequest(specificationId, revisionData), writer, canceledListener); + scheduleAgent.schedule( + new ScheduleRequest(specificationId, revisionData), + writer, + canceledListener, + config.maxCachedSimulationEngines()); } catch (final Throwable ex) { ex.printStackTrace(System.err); writer.failWith(b -> b @@ -123,6 +131,11 @@ private static String getEnv(final String key, final String fallback){ } private static WorkerAppConfiguration loadConfiguration() { + int maxNbCachedSimulationEngine = Integer.parseInt(getEnv("MAX_NB_CACHED_SIMULATION_ENGINES", "1")); + if (maxNbCachedSimulationEngine < 1) { + logger.warn("MAX_NB_CACHED_SIMULATION_ENGINES is " + maxNbCachedSimulationEngine + " but minimum is 1. Setting to 1."); + maxNbCachedSimulationEngine = 1; + } return new WorkerAppConfiguration( new PostgresStore(getEnv("AERIE_DB_SERVER", "postgres"), getEnv("SCHEDULER_DB_USER", ""), @@ -133,7 +146,8 @@ private static WorkerAppConfiguration loadConfiguration() { Path.of(getEnv("MERLIN_LOCAL_STORE", "/usr/src/app/merlin_file_store")), Path.of(getEnv("SCHEDULER_RULES_JAR", "/usr/src/app/merlin_file_store/scheduler_rules.jar")), PlanOutputMode.valueOf((getEnv("SCHEDULER_OUTPUT_MODE", "CreateNewOutputPlan"))), - getEnv("HASURA_GRAPHQL_ADMIN_SECRET", "") + getEnv("HASURA_GRAPHQL_ADMIN_SECRET", ""), + maxNbCachedSimulationEngine ); } } diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/WorkerAppConfiguration.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/WorkerAppConfiguration.java index 217c0f051b..84b094cfc0 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/WorkerAppConfiguration.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/WorkerAppConfiguration.java @@ -11,5 +11,6 @@ public record WorkerAppConfiguration( Path merlinFileStore, Path missionRuleJarPath, PlanOutputMode outputMode, - String hasuraGraphQlAdminSecret + String hasuraGraphQlAdminSecret, + int maxCachedSimulationEngines ) { } diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java index 9fcec891bc..3ea1917c6e 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SynchronousSchedulerAgent.java @@ -22,7 +22,9 @@ import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; import gov.nasa.jpl.aerie.merlin.driver.MissionModel; +import gov.nasa.jpl.aerie.merlin.driver.MissionModelId; import gov.nasa.jpl.aerie.merlin.driver.MissionModelLoader; +import gov.nasa.jpl.aerie.merlin.driver.SimulationEngineConfiguration; import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerPlugin; @@ -66,11 +68,16 @@ import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleResults; import gov.nasa.jpl.aerie.scheduler.server.services.SchedulerAgent; import gov.nasa.jpl.aerie.scheduler.server.services.SpecificationService; +import gov.nasa.jpl.aerie.scheduler.simulation.CheckpointSimulationFacade; +import gov.nasa.jpl.aerie.scheduler.simulation.InMemoryCachedEngineStore; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; import gov.nasa.jpl.aerie.scheduler.solver.PrioritySolver; import org.apache.commons.collections4.BidiMap; import org.apache.commons.collections4.bidimap.DualHashBidiMap; import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.util.HashSet; /** * agent that handles posed scheduling requests by blocking the requester thread until scheduling is complete @@ -91,6 +98,8 @@ public record SynchronousSchedulerAgent( ) implements SchedulerAgent { + private static final Logger LOGGER = LoggerFactory.getLogger(SynchronousSchedulerAgent.class); + public SynchronousSchedulerAgent { Objects.requireNonNull(merlinService); Objects.requireNonNull(modelJarsDir); @@ -111,9 +120,10 @@ public record SynchronousSchedulerAgent( public void schedule( final ScheduleRequest request, final ResultsProtocol.WriterRole writer, - final Supplier canceledListener + final Supplier canceledListener, + final int sizeCachedEngineStore ) { - try { + try(final var cachedEngineStore = new InMemoryCachedEngineStore(sizeCachedEngineStore)) { //confirm requested plan to schedule from/into still exists at targeted version (request could be stale) //TODO: maybe some kind of high level db transaction wrapping entire read/update of target plan revision @@ -127,11 +137,16 @@ public void schedule( specification.horizonStartTimestamp().toInstant(), specification.horizonEndTimestamp().toInstant() ); - try(final var simulationFacade = new SimulationFacade( - planningHorizon, + final var simulationFacade = new CheckpointSimulationFacade( schedulerMissionModel.missionModel(), schedulerMissionModel.schedulerModel(), - canceledListener)) { + cachedEngineStore, + planningHorizon, + new SimulationEngineConfiguration( + planMetadata.modelConfiguration(), + planMetadata.horizon().getStartInstant(), + new MissionModelId(planMetadata.modelId())), + canceledListener); final var problem = new Problem( schedulerMissionModel.missionModel(), planningHorizon, @@ -139,10 +154,11 @@ public void schedule( schedulerMissionModel.schedulerModel() ); final var externalProfiles = loadExternalProfiles(planMetadata.planId()); - final var initialSimulationResults = loadSimulationResults(planMetadata); + final var initialSimulationResultsAndDatasetId = loadSimulationResults(planMetadata); //seed the problem with the initial plan contents - final var loadedPlanComponents = loadInitialPlan(planMetadata, problem, initialSimulationResults); - problem.setInitialPlan(loadedPlanComponents.schedulerPlan(), initialSimulationResults, loadedPlanComponents.mapSchedulingIdsToActivityIds); + final var loadedPlanComponents = loadInitialPlan(planMetadata, problem, + initialSimulationResultsAndDatasetId.map(Pair::getKey)); + problem.setInitialPlan(loadedPlanComponents.schedulerPlan(), initialSimulationResultsAndDatasetId.map(Pair::getKey), loadedPlanComponents.mapSchedulingIdsToActivityIds); problem.setExternalProfile(externalProfiles.realProfiles(), externalProfiles.discreteProfiles()); //apply constraints/goals to the problem final var compiledGlobalSchedulingConditions = new ArrayList(); @@ -217,10 +233,10 @@ public void schedule( } problem.setGoals(orderedGoals); - final var scheduler = new PrioritySolver(problem, specification.analysisOnly()); - //run the scheduler to find a solution to the posed problem, if any - final var solutionPlan = scheduler.getNextSolution().orElseThrow( - () -> new ResultsProtocolFailure("scheduler returned no solution")); + final var scheduler = new PrioritySolver(problem, specification.analysisOnly()); + //run the scheduler to find a solution to the posed problem, if any + final var solutionPlan = scheduler.getNextSolution().orElseThrow( + () -> new ResultsProtocolFailure("scheduler returned no solution")); final var activityToGoalId = new HashMap(); for (final var entry : solutionPlan.getEvaluation().getGoalEvaluations().entrySet()) { @@ -239,17 +255,23 @@ public void schedule( activityToGoalId, schedulerMissionModel.schedulerModel() ); - List updatedActs = updateEverythingWithNewAnchorIds(solutionPlan, instancesToIds); - merlinService.updatePlanActivityDirectiveAnchors(specification.planId(), updatedActs, instancesToIds); - - final var planMetadataAfterChanges = merlinService.getPlanMetadata(specification.planId()); - final var datasetId = storeSimulationResults(planningHorizon, simulationFacade, planMetadataAfterChanges, instancesToIds); - //collect results and notify subscribers of success - final var results = collectResults(solutionPlan, instancesToIds, goals); - writer.succeedWith(results, datasetId); - } catch (SchedulingInterruptedException e) { - writer.reportCanceled(e); + List updatedActs = updateEverythingWithNewAnchorIds(solutionPlan, instancesToIds); + merlinService.updatePlanActivityDirectiveAnchors(specification.planId(), updatedActs, instancesToIds); + + final var planMetadataAfterChanges = merlinService.getPlanMetadata(specification.planId()); + Optional datasetId = initialSimulationResultsAndDatasetId.map(Pair::getRight); + if(planMetadataAfterChanges.planRev() != specification.planRevision()) { + datasetId = storeSimulationResults( + solutionPlan, + planningHorizon, + simulationFacade, + planMetadataAfterChanges, + instancesToIds); } + //collect results and notify subscribers of success + final var results = collectResults(solutionPlan, instancesToIds, goals); + LOGGER.info("Simulation cache saved " + cachedEngineStore.getTotalSavedSimulationTime() + " in simulation time"); + writer.succeedWith(results, datasetId); } catch (final SpecificationLoadException e) { writer.failWith(b -> b .type("SPECIFICATION_LOAD_EXCEPTION") @@ -283,6 +305,13 @@ public void schedule( .type("IO_EXCEPTION") .message(e.toString()) .trace(e)); + } catch (SchedulingInterruptedException e) { + writer.reportCanceled(e); + } catch (Exception e) { + writer.failWith(b -> b + .type("OTHER_EXCEPTION") + .message(e.toString()) + .trace(e)); } } @@ -304,7 +333,7 @@ public List updateEverythingWithNewAnchorIds(Plan s } - private Optional loadSimulationResults(final PlanMetadata planMetadata){ + private Optional> loadSimulationResults(final PlanMetadata planMetadata){ try { return merlinService.getSimulationResults(planMetadata); } catch (MerlinServiceException | IOException | InvalidJsonException e) { @@ -318,36 +347,38 @@ private ExternalProfiles loadExternalProfiles(final PlanId planId) return merlinService.getExternalProfiles(planId); } - private Optional storeSimulationResults(PlanningHorizon planningHorizon, SimulationFacade simulationFacade, PlanMetadata planMetadata, - final Map schedDirectiveToMerlinId) - throws MerlinServiceException, IOException, SchedulingInterruptedException { - if(!simulationFacade.areInitialSimulationResultsStale()) return Optional.empty(); + private Optional storeSimulationResults( + final Plan plan, + PlanningHorizon planningHorizon, + SimulationFacade simulationFacade, + PlanMetadata planMetadata, + final Map schedDirectiveToMerlinId) + throws MerlinServiceException, IOException, SchedulingInterruptedException + { //finish simulation until end of horizon before posting results try { - simulationFacade.computeSimulationResultsUntil(planningHorizon.getEndAerie()); + final var simulationData = simulationFacade.simulateWithResults(plan, planningHorizon.getEndAerie()); + final var schedID_to_MerlinID = + schedDirectiveToMerlinId.entrySet().stream() + .collect(Collectors.toMap( + (a) -> new SchedulingActivityDirectiveId(a.getKey().id().id()), Map.Entry::getValue)); + final var schedID_to_simID = + simulationData.mapSchedulingIdsToActivityIds().get(); + final var simID_to_MerlinID = + schedID_to_simID.entrySet().stream().collect(Collectors.toMap( + Map.Entry::getValue, + (a) -> schedID_to_MerlinID.get(a.getKey()))); + if(simID_to_MerlinID.values().containsAll(schedDirectiveToMerlinId.values()) && schedDirectiveToMerlinId.values().containsAll(simID_to_MerlinID.values())){ + return Optional.of(merlinService.storeSimulationResults(planMetadata, + simulationData.driverResults(), + simID_to_MerlinID)); + } else{ + //schedule in simulation is inconsistent with current state of the plan (user probably disabled simulation for some of the goals) + return Optional.empty(); + } } catch (SimulationFacade.SimulationException e) { throw new RuntimeException("Error while running simulation before storing simulation results after scheduling", e); } - final var schedID_to_MerlinID = - schedDirectiveToMerlinId.entrySet().stream() - .collect(Collectors.toMap( - (a) -> new SchedulingActivityDirectiveId(a.getKey().id().id()), Map.Entry::getValue)); - final var temp_SchedID_to_simID = simulationFacade.getBidiActivityIdCorrespondence(); - if(temp_SchedID_to_simID.isEmpty()) - return Optional.empty(); - final var schedID_to_simID = new HashMap<>(temp_SchedID_to_simID.get()); - final var simID_to_MerlinID = - schedID_to_simID.entrySet().stream().collect(Collectors.toMap( - Map.Entry::getValue, - (a) -> schedID_to_MerlinID.get(a.getKey()))); - if(simID_to_MerlinID.values().containsAll(schedDirectiveToMerlinId.values()) && schedDirectiveToMerlinId.values().containsAll(simID_to_MerlinID.values())){ - return Optional.of(merlinService.storeSimulationResults(planMetadata, - simulationFacade.getLatestDriverSimulationResults().get(), - simID_to_MerlinID)); - } else{ - //schedule in simulation is inconsistent with current state of the plan (user probably disabled simulation for some of the goals) - return Optional.empty(); - } } private static SchedulingDSLCompilationService.SchedulingDSLCompilationResult compileGoalDefinition( diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockMerlinService.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockMerlinService.java index a7cb47459b..4ba9057b68 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockMerlinService.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/MockMerlinService.java @@ -151,7 +151,7 @@ public void ensurePlanExists(final PlanId planId) { } @Override - public Optional getSimulationResults(final PlanMetadata planMetadata) + public Optional> getSimulationResults(final PlanMetadata planMetadata) { return Optional.empty(); } diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java index a187e0656c..830ab890ad 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java @@ -32,6 +32,7 @@ import gov.nasa.jpl.aerie.scheduler.model.Problem; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchPlanException; import gov.nasa.jpl.aerie.scheduler.server.http.InvalidJsonException; +import gov.nasa.jpl.aerie.scheduler.server.models.DatasetId; import gov.nasa.jpl.aerie.scheduler.server.models.ExternalProfiles; import gov.nasa.jpl.aerie.scheduler.server.models.MerlinPlan; import gov.nasa.jpl.aerie.scheduler.server.models.MissionModelId; @@ -41,6 +42,7 @@ import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingDSL; import gov.nasa.jpl.aerie.scheduler.server.services.MerlinService; import gov.nasa.jpl.aerie.scheduler.server.services.MerlinServiceException; +import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -101,7 +103,7 @@ public void ensurePlanExists(final PlanId planId) throws IOException, NoSuchPlan } @Override - public Optional getSimulationResults(final PlanMetadata planMetadata) + public Optional> getSimulationResults(final PlanMetadata planMetadata) throws MerlinServiceException, IOException, InvalidJsonException { return Optional.empty(); diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java index 6a94206f25..ca1ce8ad30 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java @@ -58,6 +58,7 @@ import gov.nasa.jpl.aerie.scheduler.model.Plan; import gov.nasa.jpl.aerie.scheduler.server.services.SpecificationService; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; @@ -144,7 +145,7 @@ export default () => Goal.ActivityRecurrenceGoal({ """, true)), PLANNING_HORIZON); fail(); } - catch (IllegalArgumentException e) { + catch (AssertionError e) { assertTrue(e.getMessage().contains("Duration passed to RecurrenceGoal as the goal's minimum recurrence interval cannot be negative!")); } catch (Exception e) { @@ -303,7 +304,6 @@ export default () => Goal.CoexistenceGoal({ assertEquals(Duration.of(5, Duration.HOUR), peelBanana.startOffset()); } - @Test void testSingleActivityPlanSimpleRecurrenceGoal() { final var results = runScheduler( @@ -466,7 +466,7 @@ void testCoexistenceGoalWithAnchors() { ), List.of(new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ - persistentAnchor: PersistentTimeAnchor.START, + persistentAnchor: PersistentTimeAnchor.START, forEach: ActivityExpression.ofType(ActivityTypes.BiteBanana), activityFinder: ActivityExpression.ofType(ActivityTypes.GrowBanana), activityTemplate: (interval) => ActivityTemplates.GrowBanana({quantity: 10, growingDuration: Temporal.Duration.from({minutes:1}) }), @@ -486,6 +486,50 @@ export default () => Goal.CoexistenceGoal({ } } + @Test + void testCoexistenceGoalWithAnchorsCreation() { + final var results = runScheduler( + BANANANATION, + List.of( + new ActivityDirective( + Duration.ZERO, + "BiteBanana", + Map.of("biteSize", SerializedValue.of(1)), + null, + true + ) + ), + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ + export default () => Goal.CoexistenceGoal({ + persistentAnchor: PersistentTimeAnchor.START, + forEach: ActivityExpression.ofType(ActivityTypes.BiteBanana), + activityFinder: ActivityExpression.ofType(ActivityTypes.GrowBanana), + activityTemplate: (interval) => ActivityTemplates.GrowBanana({quantity: 10, growingDuration: Temporal.Duration.from({minutes:1}) }), + startsAt: TimingConstraint.singleton(WindowProperty.END).plus(Temporal.Duration.from({ minutes : 5})) + }) + """, true)), + PLANNING_HORIZON); + + assertEquals(1, results.scheduleResults.goalResults().size()); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); + + assertTrue(goalResult.satisfied()); + assertEquals(1, goalResult.createdActivities().size()); + assertEquals(1, goalResult.satisfyingActivities().size()); + for (final var activity : goalResult.satisfyingActivities()) { + assertNotNull(activity); + } + + final var planByActivityType = partitionByActivityType(results.updatedPlan); + final var allBiteBanana = planByActivityType.get("BiteBanana"); + for(final var activity : planByActivityType.get("GrowBanana")){ + assertNotNull(activity.anchorId()); + final var reference = results.idToAct().get(activity.anchorId()); + assertTrue(allBiteBanana.contains(reference)); + allBiteBanana.remove(reference); + } + } + @Test void testCoexistencePartialActWithParameter() { final var expectedSatisfactionAct = new ActivityDirective( @@ -1579,6 +1623,7 @@ private static List onePickEveryTenMinutes(final Interval int } @Test + @Disabled void testBigCoexistence(){ final var growBananaDuration = Duration.of(1, Duration.HOUR); final var results = runScheduler( @@ -2029,7 +2074,16 @@ private SchedulingRunResults runScheduler( final MissionModelDescription desc, final List plannedActivities, final Iterable goals, - final PlanningHorizon planningHorizon + final PlanningHorizon planningHorizon){ + return runScheduler(desc, plannedActivities, goals, planningHorizon, 30); + } + + private SchedulingRunResults runScheduler( + final MissionModelDescription desc, + final List plannedActivities, + final Iterable goals, + final PlanningHorizon planningHorizon, + final int cachedEngineStoreCapacity ) { final var activities = new HashMap(); @@ -2037,7 +2091,7 @@ private SchedulingRunResults runScheduler( for (final var activityDirective : plannedActivities) { activities.put(new ActivityDirectiveId(id++), activityDirective); } - return runScheduler(desc, activities, goals, List.of(), planningHorizon, Optional.empty()); + return runScheduler(desc, activities, goals, List.of(), planningHorizon, Optional.empty(), cachedEngineStoreCapacity); } private SchedulingRunResults runScheduler( @@ -2047,7 +2101,7 @@ private SchedulingRunResults runScheduler( final PlanningHorizon planningHorizon ) { - return runScheduler(desc, plannedActivities, goals, List.of(), planningHorizon, Optional.empty()); + return runScheduler(desc, plannedActivities, goals, List.of(), planningHorizon, Optional.empty(), 30); } private SchedulingRunResults runScheduler( @@ -2073,7 +2127,7 @@ private SchedulingRunResults runScheduler( for (final var activityDirective : plannedActivities) { activities.put(new ActivityDirectiveId(id++), activityDirective); } - return runScheduler(desc, activities, goals, globalSchedulingConditions, planningHorizon, externalProfiles); + return runScheduler(desc, activities, goals, globalSchedulingConditions, planningHorizon, externalProfiles, 30); } private SchedulingRunResults runScheduler( @@ -2082,7 +2136,8 @@ private SchedulingRunResults runScheduler( final Iterable goals, final List globalSchedulingConditions, final PlanningHorizon planningHorizon, - final Optional externalProfiles + final Optional externalProfiles, + final int cachedEngineStoreCapacity ) { final var mockMerlinService = new MockMerlinService(); mockMerlinService.setMissionModel(getMissionModelInfo(desc)); @@ -2115,7 +2170,7 @@ private SchedulingRunResults runScheduler( schedulingDSLCompiler); // Scheduling Goals -> Scheduling Specification final var writer = new MockResultsProtocolWriter(); - agent.schedule(new ScheduleRequest(new SpecificationId(1L), new SpecificationRevisionData(1L, 1L)), writer, () -> false); + agent.schedule(new ScheduleRequest(new SpecificationId(1L), new SpecificationRevisionData(1L, 1L)), writer, () -> false, cachedEngineStoreCapacity); assertEquals(1, writer.results.size()); final var result = writer.results.get(0); if (result instanceof MockResultsProtocolWriter.Result.Failure e) { @@ -2123,10 +2178,18 @@ private SchedulingRunResults runScheduler( System.err.println(serializedReason); fail(serializedReason); } - return new SchedulingRunResults(((MockResultsProtocolWriter.Result.Success) result).results(), mockMerlinService.updatedPlan, mockMerlinService.plan, plannedActivities); + return new SchedulingRunResults( + ((MockResultsProtocolWriter.Result.Success) result).results(), + mockMerlinService.updatedPlan, + mockMerlinService.plan, + plannedActivities); } - record SchedulingRunResults(ScheduleResults scheduleResults, Collection updatedPlan, Plan plan, Map idToAct) {} + record SchedulingRunResults( + ScheduleResults scheduleResults, + Collection updatedPlan, + Plan plan, + Map idToAct) {} static MerlinService.MissionModelTypes loadMissionModelTypesFromJar( final String jarPath, @@ -2362,12 +2425,7 @@ export default function myGoal() { planningHorizon); final var planByActivityType = partitionByActivityType(results.updatedPlan()); final var biteBanana = planByActivityType.get("BiteBanana").stream().map((bb) -> bb.startOffset()).toList(); - final var childs = planByActivityType.get("child"); - assertEquals(childs.size(), biteBanana.size()); - assertEquals(childs.size(), 2); - for(final var childAct: childs){ - assertTrue(biteBanana.contains(childAct.startOffset())); - } + assertEquals(biteBanana.size(), 2); } @Test @@ -3104,69 +3162,6 @@ export default () => Goal.CoexistenceGoal({ assertEquals(SerializedValue.of(2), growBanana2.serializedActivity().getArguments().get("quantity")); } - /** - * Test the option to turn off simulation in between goals. - * - * Goal 0 places `PeelBanana`s. Goal 1 looks for `PeelBanana`s and places `BananaNap`s. - * If it doesn't resimulate in between, Goal 1 should place no activities. - * If it does resimulate, it should place one activity. - * - * Both options are tested here. - */ - @Test - void testOptionalSimulationAfterGoal_unsimulatedActivities() { - final var activityDuration = Duration.of(1, Duration.HOUR); - final var configs = Map.of( - false, 0, // don't simulate, expect 0 activities - true, 1 // do simulate, expect 1 activity - ); - for (final var config: configs.entrySet()) { - final var results = runScheduler( - BANANANATION, - Map.of( - new ActivityDirectiveId(1L), - new ActivityDirective( - Duration.ZERO, - "GrowBanana", - Map.of( - "quantity", SerializedValue.of(1), - "growingDuration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))), - null, - true) - ), - List.of( - new SchedulingGoal(new GoalId(0L, 0L), """ - export default () => Goal.CoexistenceGoal({ - forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), - activityTemplate: ActivityTemplates.BananaNap(), - startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes: 5 })) - }) - """, true, config.getKey() - ), - new SchedulingGoal(new GoalId(1L, 0L), """ - export default () => Goal.CoexistenceGoal({ - forEach: ActivityExpression.ofType(ActivityTypes.BananaNap), - activityTemplate: ActivityTemplates.DownloadBanana({connection: "DSL"}), - startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes: 5 })) - }) - """, true, true) - ), - PLANNING_HORIZON); - - assertEquals(2, results.scheduleResults.goalResults().size()); - final var goalResult1 = results.scheduleResults.goalResults().get(new GoalId(0L, 0L)); - final var goalResult2 = results.scheduleResults.goalResults().get(new GoalId(1L, 0L)); - - assertTrue(goalResult1.satisfied()); - assertTrue(goalResult2.satisfied()); - assertEquals(1, goalResult1.createdActivities().size()); - assertEquals(config.getValue(), goalResult2.createdActivities().size()); - for (final var activity : goalResult1.createdActivities()) { - assertNotNull(activity); - } - } - } - /** * Test the option to turn off simulation in between goals. * @@ -3200,7 +3195,7 @@ void testOptionalSimulationAfterGoal_staleResources() { List.of( new SchedulingGoal(new GoalId(0L, 0L), """ export default () => Goal.CoexistenceGoal({ - forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), + forEach: Real.Resource("/fruit").greaterThan(4), activityTemplate: ActivityTemplates.DownloadBanana({connection: "DSL"}), startsAt: TimingConstraint.singleton(WindowProperty.START).plus(Temporal.Duration.from({ minutes: 5 })) })