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