diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/SpansWrapperExpression.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/SpansWrapperExpression.java new file mode 100644 index 0000000000..8d4d7c7977 --- /dev/null +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/SpansWrapperExpression.java @@ -0,0 +1,29 @@ +package gov.nasa.jpl.aerie.constraints.tree; +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.Spans; + +import java.util.Set; + +public record SpansWrapperExpression(Spans spans) implements Expression { + @Override + public Spans evaluate( + final SimulationResults results, + final Interval bounds, + final EvaluationEnvironment environment) + { + return spans; + } + + @Override + public String prettyPrint(final String prefix) { + return String.format( + "\n%s(spans-wrapper-of %s)", + prefix, + this.spans + ); } + + @Override + public void extractResources(final Set names) { } +} diff --git a/e2e-tests/src/tests/bindings.test.ts b/e2e-tests/src/tests/bindings.test.ts index 59605ff17d..5fcea8bf18 100644 --- a/e2e-tests/src/tests/bindings.test.ts +++ b/e2e-tests/src/tests/bindings.test.ts @@ -641,6 +641,61 @@ test.describe.serial('Scheduler Bindings', () => { status: 'failure', reason: 'No mission model exists with id `MissionModelId[id=-1]`' }); + // Returns a 200 with a failure status if a invalid plan id is passed + // reason is "No plan exists with id `PlanId[id=-1]`" + response = await request.post(`${urls.SCHEDULER_URL}/schedulingDslTypescript`, { + data: { + action: {name: "schedulingDslTypescript"}, + input: {missionModelId: mission_model_id, planId:-1}, + request_query: "", + session_variables: admin.session}}); + expect(response.status()).toEqual(200); + expect(await response.json()).toEqual({ + status: 'failure', + reason: 'No plan exists with id `PlanId[id=-1]`' + }); + + //verify that when inserting an external dataset, the resource is generated in the constraints edsl + await request.post(`${urls.MERLIN_URL}/addExternalDataset`, { + data: { + action: {name: "addExternalDataset"}, + input: { + planId: plan_id, + datasetStart:'2021-001T06:00:00.000', + profileSet: {'/my_other_boolean':{schema:{type:'boolean'},segments:[{duration:3600000000,dynamics:true}],type:'discrete'}}, + simulationDatasetId: null + }, + request_query: "", + session_variables: admin.session}}); + + let resourceTypesWithExternalResource = `export type Resource = { + "/peel": number, + "/fruit": {initial: number, rate: number, }, + "/data/line_count": number, + "/my_other_boolean": boolean, + "/flag/conflicted": boolean, + "/plant": number, + "/flag": ( | "A" | "B"), + "/producer": string, +};`; + // Returns a 200 with a success status and the resource types containing the external type + response = await request.post(`${urls.SCHEDULER_URL}/schedulingDslTypescript`, { + data: { + action: {name: "schedulingDslTypescript"}, + input: {missionModelId: mission_model_id, planId:plan_id}, + request_query: "", + session_variables: admin.session}}); + let respBody = await response.json(); + let found = false; + for(let file of respBody.typescriptFiles){ + if(file.filePath == "file:///mission-model-generated-code.ts"){ + expect(file.content.includes(resourceTypesWithExternalResource)).toEqual(true); + found = true; + } + } + expect(found).toEqual(true); + expect(response.status()).toEqual(200); + expect(respBody.status).toEqual('success'); // Returns a 200 with a success status if the ID is valid response = await request.post(`${urls.SCHEDULER_URL}/schedulingDslTypescript`, { @@ -649,7 +704,7 @@ test.describe.serial('Scheduler Bindings', () => { input: {missionModelId: mission_model_id}, request_query: "", session_variables: admin.session}}); - let respBody = await response.json(); + respBody = await response.json(); expect(response.status()).toEqual(200); expect(respBody.status).toEqual('success'); expect(respBody.typescriptFiles).not.toBeNull(); diff --git a/e2e-tests/src/tests/scheduler-external-datasets.test.ts b/e2e-tests/src/tests/scheduler-external-datasets.test.ts new file mode 100644 index 0000000000..627aa8f59b --- /dev/null +++ b/e2e-tests/src/tests/scheduler-external-datasets.test.ts @@ -0,0 +1,109 @@ + +import {expect, test} from "@playwright/test"; +import req, {awaitScheduling, awaitSimulation} from "../utilities/requests.js"; +import time from "../utilities/time.js"; + +/* + This test uploads an external dataset and checks that it is possible to use an external resource in a scheduling goal +*/ +test.describe.serial('Scheduling with external dataset', () => { + 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 profile_set = { + '/my_boolean': { + type: 'discrete', + schema: { + type: 'boolean', + }, + segments: [ + { duration: 3600000000, dynamics: false }, + { duration: 3600000000, dynamics: true }, + { duration: 3600000000, dynamics: false }, + ], + }, + }; + + const externalDatasetInput: ExternalDatasetInsertInput = { + plan_id, + dataset_start: plan_start_timestamp, + profile_set, + }; + + await req.insertExternalDataset(request, externalDatasetInput); + + await awaitSimulation(request, plan_id); + + const schedulingGoal1: SchedulingGoalInsertInput = + { + description: "Test goal", + model_id: mission_model_id, + name: "ForEachGrowPeel" + rd, + definition: `export default function myGoal() { + return Goal.CoexistenceGoal({ + forEach: Discrete.Resource("/my_boolean").equal(true).assignGaps(false), + 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); + + await awaitScheduling(request, scheduling_specification_id); + const plan = await req.getPlan(request, plan_id) + expect(plan.activity_directives.length).toEqual(1); + expect(plan.activity_directives[0].startTime == "2021-001T02:00:00.000") + await req.deletePlan(request, plan_id); + await req.deleteMissionModel(request, mission_model_id) + }); +}); + +function delay(ms: number) { + return new Promise( resolve => setTimeout(resolve, ms) ); +} 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 5209da8a0d..d166d0c2b8 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 @@ -121,10 +121,10 @@ protected CardinalityGoal fill(CardinalityGoal goal) { * should probably be created!) */ @Override - public Collection getConflicts(Plan plan, final SimulationResults simulationResults) { + public Collection getConflicts(Plan plan, final SimulationResults simulationResults, final EvaluationEnvironment evaluationEnvironment) { //unwrap temporalContext - final var windows = getTemporalContext().evaluate(simulationResults); + final var windows = getTemporalContext().evaluate(simulationResults, evaluationEnvironment); //make sure it hasn't changed if (this.initiallyEvaluatedTemporalContext != null && !windows.equals(this.initiallyEvaluatedTemporalContext)) { @@ -142,7 +142,7 @@ else if (this.initiallyEvaluatedTemporalContext == null) { final var actTB = new ActivityExpression.Builder().basedOn(this.matchActTemplate).startsOrEndsIn(subIntervalWindows).build(); - final var acts = new LinkedList<>(plan.find(actTB, simulationResults, new EvaluationEnvironment())); + final var acts = new LinkedList<>(plan.find(actTB, simulationResults, evaluationEnvironment)); acts.sort(Comparator.comparing(SchedulingActivityDirective::startOffset)); int nbActs = 0; @@ -201,7 +201,7 @@ else if (this.initiallyEvaluatedTemporalContext == null) { this, subIntervalWindows, this.desiredActTemplate, - new EvaluationEnvironment(), + 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 ac7b83db11..d3b276ec44 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 @@ -19,7 +19,7 @@ import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; import java.util.ArrayList; -import java.util.Map; +import java.util.HashMap; import java.util.Optional; /** @@ -172,7 +172,8 @@ protected CoexistenceGoal fill(CoexistenceGoal goal) { * should probably be created!) */ @SuppressWarnings({"unchecked", "rawtypes"}) - public java.util.Collection getConflicts(Plan plan, final SimulationResults simulationResults) { //TODO: check if interval gets split and if so, notify user? + @Override + public java.util.Collection getConflicts(Plan plan, final SimulationResults simulationResults, final EvaluationEnvironment evaluationEnvironment) { //TODO: check if interval gets split and if so, notify user? //NOTE: temporalContext IS A WINDOWS OVER WHICH THE GOAL APPLIES, USUALLY SOMETHING BROAD LIKE A MISSION PHASE //NOTE: expr IS A WINDOWS OVER WHICH A COEXISTENCEGOAL APPLIES, FOR EXAMPLE THE WINDOWS CORRESPONDING TO 5 SECONDS AFTER EVERY BASICACTIVITY IS SCHEDULED @@ -182,7 +183,7 @@ public java.util.Collection getConflicts(Plan plan, final SimulationRe // AN ACTIVITYEXPRESSION AND THEN ANALYZEWHEN WAS A MISSION PHASE, ALTHOUGH IT IS POSSIBLE TO JUST SPECIFY AN EXPRESSION THAT COMBINES THOSE. //unwrap temporalContext - final var windows = getTemporalContext().evaluate(simulationResults); + final var windows = getTemporalContext().evaluate(simulationResults, evaluationEnvironment); //make sure it hasn't changed if (this.initiallyEvaluatedTemporalContext != null && !windows.includes(this.initiallyEvaluatedTemporalContext)) { @@ -192,7 +193,7 @@ else if (this.initiallyEvaluatedTemporalContext == null) { this.initiallyEvaluatedTemporalContext = windows; } - final var anchors = expr.evaluate(simulationResults).intersectWith(windows); + final var anchors = expr.evaluate(simulationResults, evaluationEnvironment).intersectWith(windows); //make sure expr hasn't changed either as that could yield unexpected behavior if (this.evaluatedExpr != null && !anchors.isCollectionSubsetOf(this.evaluatedExpr)) { @@ -242,7 +243,10 @@ else if (this.initiallyEvaluatedTemporalContext == null) { activityCreationTemplate.durationIn(durRange); } - final var existingActs = plan.find(activityFinder.build(), simulationResults, createEvaluationEnvironmentFromAnchor(window)); + final var existingActs = plan.find( + activityFinder.build(), + simulationResults, + createEvaluationEnvironmentFromAnchor(evaluationEnvironment, window)); var missingActAssociations = new ArrayList(); var planEvaluation = plan.getEvaluation(); @@ -272,7 +276,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), 1, Optional.empty())); + conflicts.add(new MissingActivityTemplateConflict(this, this.temporalContext.evaluate(simulationResults, evaluationEnvironment), temp, createEvaluationEnvironmentFromAnchor(evaluationEnvironment, window), 1, Optional.empty())); } else { conflicts.add(new MissingAssociationConflict(this, missingActAssociations)); } @@ -283,24 +287,28 @@ else if (this.initiallyEvaluatedTemporalContext == null) { return conflicts; } - private EvaluationEnvironment createEvaluationEnvironmentFromAnchor(Segment> span){ + private EvaluationEnvironment createEvaluationEnvironmentFromAnchor(EvaluationEnvironment existingEnvironment, Segment> span){ if(span.value().isPresent()){ final var metadata = span.value().get(); + final var activityInstances = new HashMap<>(existingEnvironment.activityInstances()); + activityInstances.put(this.alias, metadata.activityInstance()); return new EvaluationEnvironment( - Map.of(this.alias, metadata.activityInstance()), - Map.of(), - Map.of(), - Map.of(), - Map.of() + activityInstances, + existingEnvironment.spansInstances(), + existingEnvironment.intervals(), + existingEnvironment.realExternalProfiles(), + existingEnvironment.discreteExternalProfiles() ); } else{ assert this.alias != null; + final var intervals = new HashMap<>(existingEnvironment.intervals()); + intervals.put(this.alias, span.interval()); return new EvaluationEnvironment( - Map.of(), - Map.of(), - Map.of(this.alias, span.interval()), - Map.of(), - Map.of() + existingEnvironment.activityInstances(), + existingEnvironment.spansInstances(), + intervals, + existingEnvironment.realExternalProfiles(), + existingEnvironment.discreteExternalProfiles() ); } } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Goal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Goal.java index 8e8514b699..1dc294ad46 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Goal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/Goal.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.scheduler.goals; +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.Windows; @@ -283,7 +284,11 @@ public String getName() { * @param simulationResults * @return a list of issues in the plan that diminish goal satisfaction */ - public java.util.Collection getConflicts(Plan plan, final SimulationResults simulationResults) { + public java.util.Collection getConflicts( + Plan plan, + final SimulationResults simulationResults, + final EvaluationEnvironment evaluationEnvironment + ) { return java.util.Collections.emptyList(); } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/OptionGoal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/OptionGoal.java index 7afad635e2..948ae518db 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/OptionGoal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/OptionGoal.java @@ -1,5 +1,6 @@ package gov.nasa.jpl.aerie.scheduler.goals; +import gov.nasa.jpl.aerie.constraints.model.EvaluationEnvironment; import gov.nasa.jpl.aerie.constraints.model.SimulationResults; import gov.nasa.jpl.aerie.scheduler.conflicts.Conflict; import gov.nasa.jpl.aerie.scheduler.model.Plan; @@ -28,7 +29,9 @@ public Optimizer getOptimizer(){ } @Override - public java.util.Collection getConflicts(Plan plan, final SimulationResults simulationResults) { + public java.util.Collection getConflicts(Plan plan, + final SimulationResults simulationResults, + final EvaluationEnvironment evaluationEnvironment) { throw new NotImplementedException("Conflict detection is performed at solver level"); } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/ProceduralCreationGoal.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/ProceduralCreationGoal.java index bf6b3ee6e2..d5a88765b9 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/ProceduralCreationGoal.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/ProceduralCreationGoal.java @@ -108,12 +108,13 @@ protected ProceduralCreationGoal fill(ProceduralCreationGoal goal) { * the plan (and should probably be created). The matching is strict: all * arguments must be identical. */ - public Collection getConflicts(Plan plan, final SimulationResults simulationResults) { + @Override + public Collection getConflicts(Plan plan, final SimulationResults simulationResults, final EvaluationEnvironment evaluationEnvironment) { final var conflicts = new java.util.LinkedList(); //run the generator to see what acts are still desired //REVIEW: maybe some caching on plan hash here? - final var requestedActs = getRelevantGeneratedActivities(plan, simulationResults); + final var requestedActs = getRelevantGeneratedActivities(plan, simulationResults, evaluationEnvironment); //walk each requested act and try to find an exact match in the plan for (final var requestedAct : requestedActs) { @@ -202,13 +203,13 @@ protected ProceduralCreationGoal() { } * are deemed relevant to this goal (eg within the temporal context * of this goal) */ - private Collection getRelevantGeneratedActivities(Plan plan, SimulationResults simulationResults) { + private Collection getRelevantGeneratedActivities(Plan plan, SimulationResults simulationResults, EvaluationEnvironment evaluationEnvironment) { //run the generator in the plan context final var allActs = generator.apply(plan); //filter out acts that don't have a start time within the goal purview - final var evaluatedGoalContext = getTemporalContext().evaluate(simulationResults); + final var evaluatedGoalContext = getTemporalContext().evaluate(simulationResults, evaluationEnvironment); final var filteredActs = allActs.stream().filter( act -> ((act.startOffset() != null) && evaluatedGoalContext.includes(Interval.at(0, act.startOffset()))) 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 abb958e08c..7f15465378 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 @@ -104,12 +104,13 @@ else if (every.max.isNegative()) { * exist over a timespan longer than the allowed range (and one should * probably be created!) */ - public java.util.Collection getConflicts(@NotNull Plan plan, final SimulationResults simulationResults) { + @Override + public java.util.Collection getConflicts(@NotNull Plan plan, final SimulationResults simulationResults, final EvaluationEnvironment evaluationEnvironment) { final var conflicts = new java.util.LinkedList(); //unwrap temporalContext final var tempWindowPlanHorizon = new Windows(false).set(List.of(this.planHorizon.getHor()), true); - final var windows = tempWindowPlanHorizon.and(this.getTemporalContext().evaluate(simulationResults)); + final var windows = tempWindowPlanHorizon.and(this.getTemporalContext().evaluate(simulationResults, evaluationEnvironment)); //check repeat is larger than activity duration if(this.getActTemplate().getType().getDurationType() instanceof DurationType.Fixed act){ @@ -153,7 +154,7 @@ else if (this.initiallyEvaluatedTemporalContext == null) { final var strideDur = actStartT.minus(prevStartT); if (strideDur.compareTo(this.recurrenceInterval.max) > 0) { //fill conflicts for all the missing activities in that long span - conflicts.addAll(makeRecurrenceConflicts(prevStartT, actStartT)); + conflicts.addAll(makeRecurrenceConflicts(prevStartT, actStartT, evaluationEnvironment)); } else { /*TODO: right now, we associate with all the activities that are satisfying but we should aim for the minimum @@ -171,7 +172,7 @@ else if (this.initiallyEvaluatedTemporalContext == null) { //fill in conflicts for all missing activities in the last span up to the //goal's own end time (also handles case of no matching acts at all) - conflicts.addAll(makeRecurrenceConflicts(prevStartT, lastStartT)); + conflicts.addAll(makeRecurrenceConflicts(prevStartT, lastStartT, evaluationEnvironment)); } return conflicts; @@ -202,7 +203,7 @@ protected RecurrenceGoal() { } * @param start IN the start time of the span to fill with conflicts (inclusive) * @param end IN the end time of the span to fill with conflicts (exclusive) */ - private java.util.Collection makeRecurrenceConflicts(Duration start, Duration end) + private java.util.Collection makeRecurrenceConflicts(Duration start, Duration end, final EvaluationEnvironment evaluationEnvironment) { final var conflicts = new java.util.LinkedList(); @@ -212,7 +213,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(), 1, Optional.empty())); + conflicts.add(new MissingActivityTemplateConflict(this, windows, this.getActTemplate(), 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 624160e6ec..c2feb377e0 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,5 +1,7 @@ package gov.nasa.jpl.aerie.scheduler.model; +import gov.nasa.jpl.aerie.constraints.model.DiscreteProfile; +import gov.nasa.jpl.aerie.constraints.model.LinearProfile; 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; @@ -12,7 +14,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; /** @@ -40,6 +44,10 @@ public class Problem { */ private final List globalConstraints = new java.util.LinkedList<>(); + + private final Map realExternalProfiles = new HashMap<>(); + private final Map discreteExternalProfiles = new HashMap<>(); + /** * the initial seed plan to start scheduling from */ @@ -148,6 +156,20 @@ public void setInitialPlan(final Plan plan) { public Optional getInitialSimulationResults(){ return initialSimulationResults; } + public void setExternalProfile(final Map realExternalProfiles, + final Map discreteExternalProfiles){ + this.realExternalProfiles.putAll(realExternalProfiles); + this.discreteExternalProfiles.putAll(discreteExternalProfiles); + } + + public Map getRealExternalProfiles(){ + return this.realExternalProfiles; + } + + public Map getDiscreteExternalProfiles(){ + return this.discreteExternalProfiles; + } + public void setGoals(List goals){ goalsOrderedByPriority.clear(); goalsOrderedByPriority.addAll(goals); 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 4daad48a52..e775a5ae4b 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 @@ -613,7 +613,8 @@ else if(!analysisOnly && (missing instanceof MissingActivityTemplateConflict mi assert plan != null; //REVIEW: maybe should have way to request only certain kinds of conflicts final var lastSimulationResults = this.getLatestSimResultsUpTo(this.problem.getPlanningHorizon().getEndAerie()); - final var rawConflicts = goal.getConflicts(plan, lastSimulationResults); + final var evaluationEnvironment = new EvaluationEnvironment(this.problem.getRealExternalProfiles(), this.problem.getDiscreteExternalProfiles()); + final var rawConflicts = goal.getConflicts(plan, lastSimulationResults, evaluationEnvironment); assert rawConflicts != null; return rawConflicts; } @@ -765,9 +766,10 @@ private Windows narrowByResourceConstraints(Windows windows, 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) + final var evaluationEnvironment = new EvaluationEnvironment(this.problem.getRealExternalProfiles(), this.problem.getDiscreteExternalProfiles()); for (final var constraint : constraints) { //REVIEW: loop through windows more efficient than enveloppe(windows) ? - final var validity = constraint.evaluate(latestSimulationResults, totalDomain); + final var validity = constraint.evaluate(latestSimulationResults, totalDomain, evaluationEnvironment); ret = ret.and(validity); //short-circuit if no possible windows left if (ret.stream().noneMatch(Segment::value)) { 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 00c3e0dbce..dada4b93e5 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 @@ -1,13 +1,29 @@ package gov.nasa.jpl.aerie.scheduler; +import gov.nasa.jpl.aerie.constraints.model.DiscreteProfile; +import gov.nasa.jpl.aerie.constraints.model.EvaluationEnvironment; +import gov.nasa.jpl.aerie.constraints.model.LinearEquation; +import gov.nasa.jpl.aerie.constraints.model.LinearProfile; +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.Spans; import gov.nasa.jpl.aerie.constraints.time.Windows; import gov.nasa.jpl.aerie.constraints.tree.And; +import gov.nasa.jpl.aerie.constraints.tree.AssignGaps; +import gov.nasa.jpl.aerie.constraints.tree.DiscreteResource; +import gov.nasa.jpl.aerie.constraints.tree.DiscreteValue; +import gov.nasa.jpl.aerie.constraints.tree.Equal; import gov.nasa.jpl.aerie.constraints.tree.Expression; import gov.nasa.jpl.aerie.constraints.tree.GreaterThanOrEqual; +import gov.nasa.jpl.aerie.constraints.tree.ProfileExpression; import gov.nasa.jpl.aerie.constraints.tree.RealResource; import gov.nasa.jpl.aerie.constraints.tree.RealValue; +import gov.nasa.jpl.aerie.constraints.tree.SpansFromWindows; +import gov.nasa.jpl.aerie.constraints.tree.SpansWrapperExpression; +import gov.nasa.jpl.aerie.constraints.tree.ValueAt; import gov.nasa.jpl.aerie.constraints.tree.WindowsWrapperExpression; +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.scheduler.constraints.TimeRangeExpression; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityCreationTemplate; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; @@ -30,6 +46,8 @@ import java.util.Arrays; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -1153,6 +1171,66 @@ public void testCoexistenceUncontrollableJustFits() { assertEquals(3, problem.getSimulationFacade().countSimulationRestarts()); } + @Test + public void testCoexistenceExternalResource() { + Interval period = Interval.betweenClosedOpen(Duration.of(0, Duration.SECONDS), Duration.of(25, Duration.SECONDS)); + + final var fooMissionModel = SimulationUtility.getFooMissionModel(); + final var planningHorizon = new PlanningHorizon(TestUtility.timeFromEpochSeconds(0), TestUtility.timeFromEpochSeconds(25)); + Problem problem = new Problem(fooMissionModel, planningHorizon, new SimulationFacade( + planningHorizon, + fooMissionModel), SimulationUtility.getFooSchedulerModel()); + + final var r3Value = 6; + final var r1 = new LinearProfile(new Segment<>(Interval.between(Duration.ZERO, Duration.SECONDS.times(5)), new LinearEquation(Duration.ZERO, 5, 1))); + final var r2 = new DiscreteProfile(new Segment<>(Interval.FOREVER, SerializedValue.of(5))); + final var r3 = new DiscreteProfile(new Segment<>(Interval.FOREVER, SerializedValue.of(r3Value))); + final var externalRealProfiles = Map.of("/real/R1", r1); + final var externalDiscreteProfiles = Map.of( + "/discrete/R2", r2, + "/discrete/R3", r3 + ); + problem.setExternalProfile( + externalRealProfiles, + externalDiscreteProfiles + ); + + final var profEx = new ProfileExpression<>(new ValueAt<>( + new ProfileExpression<>(new DiscreteResource("/discrete/R3")), + new SpansWrapperExpression(new Spans(Interval.at(Duration.of(0, Duration.MICROSECONDS)))))); + final var cond = new And( + new GreaterThanOrEqual(new RealResource("/real/R1"), new RealValue(6)), + new Equal<>(new DiscreteResource("/discrete/R2"), new DiscreteValue(SerializedValue.of(5)))); + final var actTypeB = problem.getActivityType("ControllableDurationActivity"); + CoexistenceGoal goal = new CoexistenceGoal.Builder() + .forAllTimeIn(new WindowsWrapperExpression(new Windows(period, true).assignGaps(new Windows(Interval.FOREVER, false)))) + .forEach(new SpansFromWindows(new AssignGaps<>(cond, new WindowsWrapperExpression(new Windows(Interval.FOREVER, false))))) + .thereExistsOne(new ActivityCreationTemplate.Builder() + .ofType(actTypeB) + .withArgument("duration", profEx) + .build()) + .startsAt(TimeAnchor.START) + .withinPlanHorizon(planningHorizon) + .aliasForAnchors("a") + .build(); + + problem.setGoals(List.of(goal)); + + final var solver = new PrioritySolver(problem); + var plan = solver.getNextSolution(); + for(SchedulingActivityDirective a : plan.get().getActivitiesByTime()){ + logger.debug(a.startOffset().toString() + ", " + a.duration().toString()); + } + final var emptySimulationResults = new SimulationResults(null, null, List.of(), Map.of(), Map.of()); + final var startOfActivity = cond.evaluate(emptySimulationResults, Interval.FOREVER, new EvaluationEnvironment(externalRealProfiles, externalDiscreteProfiles)).iterateEqualTo(true).iterator().next().start; + assertEquals(1, plan.get().getActivitiesByTime().size()); + final var act = plan.get().getActivitiesByTime().get(0); + assertEquals(act.duration(), Duration.of(r3Value, Duration.MICROSECONDS)); + assertEquals(startOfActivity, Duration.of(1, Duration.SECONDS)); + assertEquals(act.startOffset(), startOfActivity); + assertEquals(2, problem.getSimulationFacade().countSimulationRestarts()); + } + @Test public void changingForAllTimeIn() { diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ActivityTemplateJsonParser.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ActivityTemplateJsonParser.java index 2e1a9b59ec..9f39ec076f 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ActivityTemplateJsonParser.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/ActivityTemplateJsonParser.java @@ -9,17 +9,18 @@ import gov.nasa.jpl.aerie.json.JsonParseResult; import gov.nasa.jpl.aerie.json.JsonParser; import gov.nasa.jpl.aerie.json.SchemaCache; +import gov.nasa.jpl.aerie.scheduler.server.models.ActivityType; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingDSL; -import gov.nasa.jpl.aerie.scheduler.server.services.MissionModelService; +import gov.nasa.jpl.aerie.scheduler.server.services.MerlinService; import static gov.nasa.jpl.aerie.constraints.json.ConstraintParsers.profileExpressionP; import static gov.nasa.jpl.aerie.constraints.json.ConstraintParsers.structExpressionF; public class ActivityTemplateJsonParser implements JsonParser { - private final Map activityTypesByName = new HashMap<>(); + private final Map activityTypesByName = new HashMap<>(); - public ActivityTemplateJsonParser(MissionModelService.MissionModelTypes activityTypes){ + public ActivityTemplateJsonParser(MerlinService.MissionModelTypes activityTypes){ activityTypes.activityTypes().forEach((actType)-> activityTypesByName.put(actType.name(), actType)); } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerBindings.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerBindings.java index 241872d2c7..b919f4e13e 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerBindings.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerBindings.java @@ -118,8 +118,8 @@ private void getSchedulingDslTypescript(final Context ctx) { try { final var body = parseJson(ctx.body(), hasuraMissionModelIdActionP); final var missionModelId = body.input().missionModelId(); - - final var response = this.generateSchedulingLibAction.run(missionModelId); + final var planId = body.input().planId(); + final var response = this.generateSchedulingLibAction.run(missionModelId, planId); final String resultString; if (response instanceof GenerateSchedulingLibAction.Response.Success r) { var files = Json.createArrayBuilder(); diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerParsers.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerParsers.java index 5383d1d8bc..cd61ec5acf 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerParsers.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/http/SchedulerParsers.java @@ -3,14 +3,17 @@ import gov.nasa.jpl.aerie.json.JsonParser; import gov.nasa.jpl.aerie.scheduler.server.models.HasuraAction; 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.SpecificationId; import gov.nasa.jpl.aerie.scheduler.server.models.Timestamp; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleFailure; -import org.apache.commons.lang3.tuple.Pair; import java.util.Optional; -import static gov.nasa.jpl.aerie.json.BasicParsers.*; +import static gov.nasa.jpl.aerie.json.BasicParsers.anyP; +import static gov.nasa.jpl.aerie.json.BasicParsers.longP; +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.scheduler.server.remotes.postgres.PostgresParsers.pgTimestampP; @@ -34,6 +37,11 @@ private SchedulerParsers() {} . map( MissionModelId::new, MissionModelId::id); + public static final JsonParser planIdP + = longP + . map( + PlanId::new, + PlanId::id); public static final JsonParser scheduleFailureP = productP .field("type", stringP) @@ -67,29 +75,34 @@ private SchedulerParsers() {} * @return a parser that accepts hasura action / session details along with specified input type into a tuple * of the form (name,input,session) ready for application of a mapping */ - private static JsonParser, HasuraAction.Session>, String>> hasuraActionP(final JsonParser inputP) { + private static JsonParser> hasuraActionF(final JsonParser inputP) { return productP .field("action", productP.field("name", stringP)) .field("input", inputP) .field("session_variables", hasuraActionSessionP) - .field("request_query", stringP); + .field("request_query", stringP) + .map( + untuple((name, input, session, requestQuery) -> new HasuraAction<>(name, input, session)), + $ -> tuple($.name(), $.input(), $.session(), "")); } /** * parser for a hasura action that accepts a plan id as its sole input, along with normal hasura session details */ public static final JsonParser> hasuraSpecificationActionP - = hasuraActionP(productP.field("specificationId", specificationIdP)) + = hasuraActionF(productP.field("specificationId", specificationIdP) .map( - untuple((name, specificationId, session, requestQuery) -> new HasuraAction<>(name, new HasuraAction.SpecificationInput(specificationId), session)), - action -> tuple(action.name(), action.input().specificationId(), action.session(), "")); + untuple(HasuraAction.SpecificationInput::new), + HasuraAction.SpecificationInput::specificationId)); /** * parser for a hasura action that accepts a mission model id as its sole input, along with normal hasura session details */ public static final JsonParser> hasuraMissionModelIdActionP - = hasuraActionP(productP.field("missionModelId", missionModelIdP)) + = hasuraActionF(productP + .field("missionModelId", missionModelIdP) + .optionalField("planId", planIdP) .map( - untuple((name, missionModelId, session, requestQuery) -> new HasuraAction<>(name, new HasuraAction.MissionModelIdInput(missionModelId), session)), - action -> tuple(action.name(), action.input().missionModelId(), action.session(), "")); + untuple(HasuraAction.MissionModelIdInput::new), + input -> tuple(input.missionModelId(), input.planId()))); } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ActivityType.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ActivityType.java new file mode 100644 index 0000000000..5bd8d78859 --- /dev/null +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ActivityType.java @@ -0,0 +1,8 @@ +package gov.nasa.jpl.aerie.scheduler.server.models; + +import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; + +import java.util.Map; + +public record ActivityType(String name, Map parameters, Map> presets) {} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ExternalProfiles.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ExternalProfiles.java new file mode 100644 index 0000000000..0edfddf829 --- /dev/null +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ExternalProfiles.java @@ -0,0 +1,12 @@ +package gov.nasa.jpl.aerie.scheduler.server.models; + +import gov.nasa.jpl.aerie.constraints.model.DiscreteProfile; +import gov.nasa.jpl.aerie.constraints.model.LinearProfile; + +import java.util.Collection; +import java.util.Map; + +public record ExternalProfiles( + Map realProfiles, + Map discreteProfiles, + Collection resourceTypes) {} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/HasuraAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/HasuraAction.java index ada8556d80..8c111b7e64 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/HasuraAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/HasuraAction.java @@ -1,5 +1,7 @@ package gov.nasa.jpl.aerie.scheduler.server.models; +import java.util.Optional; + public record HasuraAction(String name, I input, Session session) { public record Session(String hasuraRole, String hasuraUserId) { } @@ -7,5 +9,5 @@ public record Session(String hasuraRole, String hasuraUserId) { } public sealed interface Input permits SpecificationInput, MissionModelIdInput { } public record SpecificationInput(SpecificationId specificationId) implements Input { } - public record MissionModelIdInput(MissionModelId missionModelId) implements Input { } + public record MissionModelIdInput(MissionModelId missionModelId, Optional planId) implements Input { } } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ResourceType.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ResourceType.java new file mode 100644 index 0000000000..246de7afbf --- /dev/null +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/ResourceType.java @@ -0,0 +1,5 @@ +package gov.nasa.jpl.aerie.scheduler.server.models; + +import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; + +public record ResourceType(String name, ValueSchema schema) {} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/SchedulingDSL.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/SchedulingDSL.java index 8fef7b3b97..3f235bc5a2 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/SchedulingDSL.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/SchedulingDSL.java @@ -12,7 +12,7 @@ import gov.nasa.jpl.aerie.scheduler.TimeUtility; import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeAnchor; import gov.nasa.jpl.aerie.scheduler.server.http.ActivityTemplateJsonParser; -import gov.nasa.jpl.aerie.scheduler.server.services.MissionModelService; +import gov.nasa.jpl.aerie.scheduler.server.services.MerlinService; import org.apache.commons.lang3.tuple.Pair; import java.util.List; @@ -57,7 +57,7 @@ public class SchedulingDSL { $ -> tuple($.duration(), $.occurrence())); private static JsonObjectParser recurrenceGoalDefinitionP( - MissionModelService.MissionModelTypes activityTypes) + MerlinService.MissionModelTypes activityTypes) { return productP .field("activityTemplate", new ActivityTemplateJsonParser(activityTypes)) @@ -112,7 +112,7 @@ private static JsonObjectParser recurren (TimingConstraint.ActivityTimingConstraintFlexibleRange $) -> tuple($.lowerBound(), $.upperBound(), $.singleton())); private static final JsonObjectParser coexistenceGoalDefinitionP( - MissionModelService.MissionModelTypes activityTypes) + MerlinService.MissionModelTypes activityTypes) { return productP @@ -146,7 +146,7 @@ private static final JsonObjectParser c } private static final JsonObjectParser cardinalityGoalDefinitionP( - MissionModelService.MissionModelTypes activityTypes) { + MerlinService.MissionModelTypes activityTypes) { return productP .field("activityTemplate", new ActivityTemplateJsonParser(activityTypes)) @@ -195,7 +195,7 @@ private static JsonObjectParser goalApplyWhenF(fina } - private static JsonParser goalSpecifierF(MissionModelService.MissionModelTypes missionModelTypes) { + private static JsonParser goalSpecifierF(MerlinService.MissionModelTypes missionModelTypes) { return recursiveP(self -> SumParsers.sumP("kind", GoalSpecifier.class, List.of( SumParsers.variant( "ActivityRecurrenceGoal", @@ -243,7 +243,7 @@ private static JsonObjectParser conditionAndF(f globalSchedulingConditionP) ))); - public static final JsonParser schedulingJsonP(MissionModelService.MissionModelTypes missionModelTypes){ + public static final JsonParser schedulingJsonP(MerlinService.MissionModelTypes missionModelTypes){ return goalSpecifierF(missionModelTypes); } diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/UnwrappedProfileSet.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/UnwrappedProfileSet.java new file mode 100644 index 0000000000..3a5c5deffb --- /dev/null +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/models/UnwrappedProfileSet.java @@ -0,0 +1,15 @@ +package gov.nasa.jpl.aerie.scheduler.server.models; + +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 java.util.Map; + +public record UnwrappedProfileSet( + Map>>> realProfiles, + Map>>> discreteProfiles +){} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ConstraintsTypescriptCodeGenerationHelper.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ConstraintsTypescriptCodeGenerationHelper.java index 55b0920c94..79469321c4 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ConstraintsTypescriptCodeGenerationHelper.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/ConstraintsTypescriptCodeGenerationHelper.java @@ -9,7 +9,7 @@ public final class ConstraintsTypescriptCodeGenerationHelper { private ConstraintsTypescriptCodeGenerationHelper() { } - public static Map activityTypes(final MissionModelService.MissionModelTypes missionModelTypes) { + public static Map activityTypes(final MerlinService.MissionModelTypes missionModelTypes) { return missionModelTypes .activityTypes() .stream() @@ -27,7 +27,7 @@ public static Map activity .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } - public static Map resources(final MissionModelService.MissionModelTypes missionModelTypes) { + public static Map resources(final MerlinService.MissionModelTypes missionModelTypes) { return missionModelTypes .resourceTypes() .stream() diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GenerateSchedulingLibAction.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GenerateSchedulingLibAction.java index 0684bfedd1..d1c61dfaef 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GenerateSchedulingLibAction.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/GenerateSchedulingLibAction.java @@ -5,15 +5,19 @@ import java.io.InputStreamReader; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; + 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.models.MissionModelId; +import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; public record GenerateSchedulingLibAction( - MissionModelService missionModelService + MerlinService.ReaderRole merlinService ) { public GenerateSchedulingLibAction { - Objects.requireNonNull(missionModelService); + Objects.requireNonNull(merlinService); } /** @@ -25,12 +29,14 @@ record Success(Map files) implements Response {} } /** - * execute the scheduling operation on the target plan (or retrieve existing scheduling results) + * generates the scheduling typescript files * * @param missionModelId the id of the mission model for which to generate a scheduling library + * @param planId the optional id of the plan concerned by this code generation, if plan id is provided, code will be generated for external resources + * associated with the plan * @return a response object wrapping the results of generating the code (either successful or not) */ - public Response run(final MissionModelId missionModelId) { + public Response run(final MissionModelId missionModelId, final Optional planId) { try { final var schedulingDsl = getTypescriptResource("scheduler-edsl-fluent-api.ts"); final var schedulerAst = getTypescriptResource("scheduler-ast.ts"); @@ -38,7 +44,12 @@ public Response run(final MissionModelId missionModelId) { final var windowsAst = getTypescriptResource("constraints/constraints-ast.ts"); final var temporalPolyfillTypes = getTypescriptResource("constraints/TemporalPolyfillTypes.ts"); - final var missionModelTypes = missionModelService.getMissionModelTypes(missionModelId); + + var missionModelTypes = merlinService.getMissionModelTypes(missionModelId); + if(planId.isPresent()) { + final var allResourceTypes = merlinService.getResourceTypes(planId.get()); + missionModelTypes = new MerlinService.MissionModelTypes(missionModelTypes.activityTypes(), allResourceTypes); + } final var generatedSchedulerCode = TypescriptCodeGenerationService.generateTypescriptTypesFromMissionModel(missionModelTypes); final var generatedConstraintsCode = gov.nasa.jpl.aerie.constraints.TypescriptCodeGenerationService @@ -54,7 +65,8 @@ public Response run(final MissionModelId missionModelId) { "file:///mission-model-generated-code.ts", generatedConstraintsCode, "file:///%s".formatted(temporalPolyfillTypes.basename), temporalPolyfillTypes.source )); - } catch (final NoSuchMissionModelException | IOException | MissionModelService.MissionModelServiceException e) { + } catch (final IOException | MerlinServiceException | + NoSuchPlanException | NoSuchMissionModelException e) { return new Response.Failure(e.getMessage()); } } 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 b8438142dc..eb4a9638e4 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 @@ -1,5 +1,7 @@ package gov.nasa.jpl.aerie.scheduler.server.services; +import gov.nasa.jpl.aerie.constraints.model.DiscreteProfile; +import gov.nasa.jpl.aerie.constraints.model.LinearProfile; import gov.nasa.jpl.aerie.json.BasicParsers; import gov.nasa.jpl.aerie.json.JsonParser; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; @@ -28,13 +30,17 @@ import gov.nasa.jpl.aerie.scheduler.server.http.InvalidEntityException; import gov.nasa.jpl.aerie.scheduler.server.http.InvalidJsonException; import gov.nasa.jpl.aerie.scheduler.server.models.ActivityAttributesRecord; +import gov.nasa.jpl.aerie.scheduler.server.models.ActivityType; import gov.nasa.jpl.aerie.scheduler.server.models.DatasetId; +import gov.nasa.jpl.aerie.scheduler.server.models.ExternalProfiles; import gov.nasa.jpl.aerie.scheduler.server.models.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.models.ProfileSet; +import gov.nasa.jpl.aerie.scheduler.server.models.ResourceType; +import gov.nasa.jpl.aerie.scheduler.server.models.UnwrappedProfileSet; import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Triple; @@ -67,7 +73,6 @@ 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; @@ -88,15 +93,38 @@ * * @param merlinGraphqlURI endpoint of the merlin graphql service that should be used to access all plan data */ -public record GraphQLMerlinService(URI merlinGraphqlURI, String hasuraGraphQlAdminSecret) implements PlanService.OwnerRole, - MissionModelService -{ +public record GraphQLMerlinService(URI merlinGraphqlURI, String hasuraGraphQlAdminSecret) implements MerlinService.OwnerRole { /** * timeout for http graphql requests issued to aerie */ private static final java.time.Duration httpTimeout = java.time.Duration.ofSeconds(60); + public record DatasetMetadata(DatasetId datasetId, Duration offsetFromPlanStart){}; + + private record SimulationId(long id){} + + private record ProfileRecord( + long id, + long datasetId, + String name, + Pair type, + Duration duration + ) {} + + private record SpanRecord( + String type, + Instant start, + Optional duration, + Optional parentId, + List childIds, + ActivityAttributesRecord attributes + ) {} + + public record SimulationDatasetId(int id){} + + public record DatasetIds(DatasetId datasetId, SimulationDatasetId simulationDatasetId){} + /** * dispatch the given graphql request to aerie and collect the results * @@ -106,7 +134,7 @@ public record GraphQLMerlinService(URI merlinGraphqlURI, String hasuraGraphQlAdm * @param gqlStr the graphQL query or mutation to send to aerie * @return the json response returned by aerie, or an empty optional in case of io errors */ - protected Optional postRequest(final String gqlStr) throws IOException, PlanServiceException { + protected Optional postRequest(final String gqlStr) throws IOException, MerlinServiceException { try { //TODO: (mem optimization) use streams here to avoid several copies of strings final var reqBody = Json.createObjectBuilder().add("query", gqlStr).build(); @@ -127,7 +155,7 @@ protected Optional postRequest(final String gqlStr) throws IOExcepti } final var respBody = Json.createReader(httpResp.body()).readObject(); if (respBody.containsKey("errors")) { - throw new PlanServiceException(respBody.toString()); + throw new MerlinServiceException(respBody.toString()); } return Optional.of(respBody); } catch (final InterruptedException e) { @@ -138,7 +166,9 @@ protected Optional postRequest(final String gqlStr) throws IOExcepti } } - protected Optional postRequest(final String query, final JsonObject variables) throws IOException, PlanServiceException { + protected Optional postRequest(final String query, final JsonObject variables) throws IOException, + MerlinServiceException + { try { //TODO: (mem optimization) use streams here to avoid several copies of strings final var reqBody = Json @@ -163,7 +193,7 @@ protected Optional postRequest(final String query, final JsonObject } final var respBody = Json.createReader(httpResp.body()).readObject(); if (respBody.containsKey("errors")) { - throw new PlanServiceException(respBody.toString()); + throw new MerlinServiceException(respBody.toString()); } return Optional.of(respBody); } catch (final InterruptedException e) { @@ -181,7 +211,7 @@ protected Optional postRequest(final String query, final JsonObject * {@inheritDoc} */ @Override - public long getPlanRevision(final PlanId planId) throws IOException, NoSuchPlanException, PlanServiceException { + public long getPlanRevision(final PlanId planId) throws IOException, NoSuchPlanException, MerlinServiceException { final var query = """ query GetPlanRevision($id: Int!) { plan_by_pk(id: $id) { @@ -206,7 +236,7 @@ query GetPlanRevision($id: Int!) { */ @Override public PlanMetadata getPlanMetadata(final PlanId planId) - throws IOException, NoSuchPlanException, PlanServiceException + throws IOException, NoSuchPlanException, MerlinServiceException { final var request = ( "query getPlanMetadata { " @@ -272,7 +302,7 @@ public PlanMetadata getPlanMetadata(final PlanId planId) */ @Override public MerlinPlan getPlanActivityDirectives(final PlanMetadata planMetadata, final Problem problem) - throws IOException, NoSuchPlanException, PlanServiceException, InvalidJsonException, InstantiationException + throws IOException, NoSuchPlanException, MerlinServiceException, InvalidJsonException, InstantiationException { final var merlinPlan = new MerlinPlan(); final var request = @@ -332,7 +362,7 @@ public Pair> creat final Plan plan, final Map activityToGoalId ) - throws IOException, NoSuchPlanException, PlanServiceException + throws IOException, NoSuchPlanException, MerlinServiceException { final var planName = getNextPlanName(); final var planId = createEmptyPlan( @@ -348,7 +378,7 @@ public Pair> creat */ @Override public PlanId createEmptyPlan(final String name, final long modelId, final Instant startTime, final Duration duration) - throws IOException, NoSuchPlanException, PlanServiceException + throws IOException, NoSuchPlanException, MerlinServiceException { final var requestFormat = ( "mutation createEmptyPlan { insert_plan_one( object: { " @@ -385,7 +415,7 @@ public Map updatePlanActivityD final Plan plan, final Map activityToGoalId ) - throws IOException, NoSuchPlanException, PlanServiceException + throws IOException, NoSuchPlanException, MerlinServiceException { final var ids = new HashMap(); //creation are done in batch as that's what the scheduler does the most @@ -411,7 +441,7 @@ public Map updatePlanActivityD ); final var activityDirectiveId = idsFromInitialPlan.get(activity.getId()); if (!activityDirectiveFromSchedulingDirective.equals(actFromInitialPlan.get())) { - throw new PlanServiceException("The scheduler should not be updating activity instances"); + throw new MerlinServiceException("The scheduler should not be updating activity instances"); //updateActivityDirective(planId, schedulerActIntoMerlinAct, activityDirectiveId, activityToGoalId.get(activity)); } ids.put(activity, activityDirectiveId); @@ -423,7 +453,7 @@ public Map updatePlanActivityD final var actsFromNewPlan = plan.getActivitiesById(); for (final var idActInInitialPlan : idsFromInitialPlan.entrySet()) { if (!actsFromNewPlan.containsKey(idActInInitialPlan.getKey())) { - throw new PlanServiceException("The scheduler should not be deleting activity instances"); + throw new MerlinServiceException("The scheduler should not be deleting activity instances"); //deleteActivityDirective(idActInInitialPlan.getValue()); } } @@ -438,7 +468,7 @@ public Map updatePlanActivityD * {@inheritDoc} */ @Override - public void ensurePlanExists(final PlanId planId) throws IOException, NoSuchPlanException, PlanServiceException { + public void ensurePlanExists(final PlanId planId) throws IOException, NoSuchPlanException, MerlinServiceException { final Supplier exceptionFactory = () -> new NoSuchPlanException(planId); final var request = "query ensurePlanExists { plan_by_pk( id: %s ) { id } }" .formatted(planId.id()); @@ -465,7 +495,9 @@ public void ensurePlanExists(final PlanId planId) throws IOException, NoSuchPlan */ //TODO: (error cleanup) more diverse exceptions for failed operations @Override - public void clearPlanActivityDirectives(final PlanId planId) throws IOException, NoSuchPlanException, PlanServiceException { + public void clearPlanActivityDirectives(final PlanId planId) throws IOException, NoSuchPlanException, + MerlinServiceException + { ensurePlanExists(planId); final var request = ( "mutation clearPlanActivities {" @@ -492,7 +524,7 @@ public Map createAllPlanActivi final Plan plan, final Map activityToGoalId ) - throws IOException, NoSuchPlanException, PlanServiceException + throws IOException, NoSuchPlanException, MerlinServiceException { return createActivityDirectives(planId, plan.getActivitiesByTime(), activityToGoalId); } @@ -502,7 +534,7 @@ public Map createActivityDirec final List orderedActivities, final Map activityToGoalId ) - throws IOException, NoSuchPlanException, PlanServiceException + throws IOException, NoSuchPlanException, MerlinServiceException { ensurePlanExists(planId); final var query = """ @@ -575,8 +607,8 @@ mutation createAllPlanActivityDirectives($activities: [activity_directive_insert } @Override - public MissionModelTypes getMissionModelTypes(final PlanId planId) - throws IOException, MissionModelServiceException + public MerlinService.MissionModelTypes getMissionModelTypes(final PlanId planId) + throws IOException, MerlinServiceException { final var request = """ query GetActivityTypesForPlan { @@ -596,11 +628,8 @@ public MissionModelTypes getMissionModelTypes(final PlanId planId) } """.formatted(planId.id()); final JsonObject response; - try { - response = postRequest(request).get(); - } catch (PlanServiceException e) { - throw new MissionModelServiceException("Failed to get mission model types for plan id %s".formatted(planId), e); - } + response = postRequest(request).get(); + final var activityTypesJsonArray = response.getJsonObject("data") .getJsonObject("plan_by_pk") @@ -613,7 +642,7 @@ public MissionModelTypes getMissionModelTypes(final PlanId planId) .getJsonObject("mission_model") .getInt("id")); - return new MissionModelTypes(activityTypes, getResourceTypes(missionModelId)); + return new MerlinService.MissionModelTypes(activityTypes, getResourceTypes(missionModelId)); } private static List parseActivityTypes(final JsonArray activityTypesJsonArray) { @@ -654,8 +683,8 @@ private static List parseActivityTypes(final JsonArray activityTyp } @Override - public MissionModelTypes getMissionModelTypes(final MissionModelId missionModelId) - throws IOException, MissionModelServiceException, NoSuchMissionModelException + public MerlinService.MissionModelTypes getMissionModelTypes(final MissionModelId missionModelId) + throws IOException, NoSuchMissionModelException, MerlinServiceException { final var request = """ query GetActivityTypesFromMissionModel{ @@ -672,11 +701,7 @@ public MissionModelTypes getMissionModelTypes(final MissionModelId missionModelI } """.formatted(missionModelId.id()); final JsonObject response; - try { - response = postRequest(request).get(); - } catch (PlanServiceException e) { - throw new MissionModelServiceException("Failed to get mission model types for model id %s".formatted(missionModelId), e); - } + response = postRequest(request).get(); final var data = response.getJsonObject("data"); if (data.get("mission_model_by_pk").getValueType().equals(JsonValue.ValueType.NULL)) throw new NoSuchMissionModelException(missionModelId); final var activityTypesJsonArray = data @@ -684,11 +709,11 @@ public MissionModelTypes getMissionModelTypes(final MissionModelId missionModelI .getJsonArray("activity_types"); final var activityTypes = parseActivityTypes(activityTypesJsonArray); - return new MissionModelTypes(activityTypes, getResourceTypes(missionModelId)); + return new MerlinService.MissionModelTypes(activityTypes, getResourceTypes(missionModelId)); } public Collection getResourceTypes(final MissionModelId missionModelId) - throws IOException, MissionModelServiceException + throws IOException, MerlinServiceException { final var request = """ query GetResourceTypes { @@ -699,11 +724,7 @@ public Collection getResourceTypes(final MissionModelId missionMod } """.formatted(missionModelId.id()); final JsonObject response; - try { - response = postRequest(request).get(); - } catch (PlanServiceException e) { - throw new MissionModelServiceException("Failed to get mission model types for model id %s".formatted(missionModelId), e); - } + response = postRequest(request).get(); final var data = response.getJsonObject("data"); final var resourceTypesJsonArray = data.getJsonArray("resourceTypes"); @@ -720,8 +741,33 @@ public Collection getResourceTypes(final MissionModelId missionMod return resourceTypes; } + /** + * Gets resource types associated to a plan, those coming from the mission model as well as those coming from external dataset resources + * @param planId the plan id + * @return + * @throws IOException + * @throws MissionModelServiceException + * @throws MerlinServiceException + * @throws NoSuchPlanException + */ + @Override + public Collection getResourceTypes(final PlanId planId) + throws IOException, MerlinServiceException, NoSuchPlanException + { + final var missionModelId = this.getPlanMetadata(planId).modelId(); + final var missionModelResourceTypes = getResourceTypes(new MissionModelId(missionModelId)); + final var allResourceTypes = new ArrayList<>(missionModelResourceTypes); + final var associatedDataset = getExternalDatasets(planId); + if(associatedDataset.isPresent()) { + for(final var datasetMetada: associatedDataset.get()) { + final var profileSet = getProfileTypes(datasetMetada.datasetId()); + allResourceTypes.addAll(extractResourceTypes(profileSet)); + } + } + return allResourceTypes; + } -public SimulationId getSimulationId(PlanId planId) throws PlanServiceException, IOException { + public SimulationId getSimulationId(PlanId planId) throws MerlinServiceException, IOException { final var request = """ query { simulation(where: {plan_id: {_eq: %d}}) { @@ -736,29 +782,11 @@ public SimulationId getSimulationId(PlanId planId) throws PlanServiceException, return new SimulationId(simulationId); } - private record SimulationId(long id){} - - private record ProfileRecord( - long id, - long datasetId, - String name, - Pair type, - Duration duration - ) {} - - private record SpanRecord( - String type, - Instant start, - Optional duration, - Optional parentId, - List childIds, - ActivityAttributesRecord attributes - ) {} - @Override public DatasetId storeSimulationResults(final PlanMetadata planMetadata, final SimulationResults results, - final Map simulationActivityDirectiveIdToMerlinActivityDirectiveId) throws PlanServiceException, IOException + final Map simulationActivityDirectiveIdToMerlinActivityDirectiveId) throws + MerlinServiceException, IOException { final var simulationId = getSimulationId(planMetadata.planId()); final var datasetIds = createSimulationDataset(simulationId, planMetadata); @@ -776,7 +804,7 @@ public DatasetId storeSimulationResults(final PlanMetadata planMetadata, } private Map getSimulatedActivities(SimulationDatasetId datasetId, Instant startSimulation) - throws PlanServiceException, IOException, InvalidJsonException + throws MerlinServiceException, IOException, InvalidJsonException { final var request = """ query{ @@ -803,7 +831,22 @@ private Map getSimulatedActivities(Simul return parseSimulatedActivities(data, startSimulation); } -private Profiles getProfiles(DatasetId datasetId) throws PlanServiceException, IOException { + private ProfileSet getProfileTypes(DatasetId datasetId) throws MerlinServiceException, IOException { + final var request = """ + query{ + profile(where: {dataset_id: {_eq: %d}}){ + type + name + } + } + """.formatted(datasetId.id()); + final JsonObject response; + response = postRequest(request).get(); + final var data = response.getJsonObject("data").getJsonArray("profile"); + return parseProfiles(data); + } + + private ProfileSet getProfilesWithSegments(DatasetId datasetId) throws MerlinServiceException, IOException { final var request = """ query{ profile(where: {dataset_id: {_eq: %d}}){ @@ -812,6 +855,7 @@ private Profiles getProfiles(DatasetId datasetId) throws PlanServiceException, I profile_segments { start_offset dynamics + is_gap } name } @@ -821,9 +865,10 @@ private Profiles getProfiles(DatasetId datasetId) throws PlanServiceException, I 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 { + private Map getSpans(DatasetId datasetId, Instant startTime) throws + MerlinServiceException, IOException { final var request = """ query{ span(where: {duration: {_is_null: true}, dataset_id: {_eq: %d}}) { @@ -839,11 +884,11 @@ private Map getSpans(DatasetId datasetI 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 + throws MerlinServiceException, IOException { final var simulationDatasetId = getSuitableSimulationResults(planMetadata); if(simulationDatasetId.isEmpty()) return Optional.empty(); @@ -854,18 +899,20 @@ public Optional getSimulationResults(PlanMetadata planMetadat Future> futureSpans = executorService.submit(() -> getSpans( simulationDatasetId.get().datasetId(), planMetadata.horizon().getStartInstant())); - Future futureProfiles = executorService.submit(() -> getProfiles(simulationDatasetId.get().datasetId())); + Future futureProfiles = executorService.submit(() -> getProfilesWithSegments(simulationDatasetId.get().datasetId())); try { final var simulatedActivities = futureSimulatedActivities.get(); final var unfinishedActivities = futureSpans.get(); final var profiles = futureProfiles.get(); + //verify that there is no gap and convert + final var unwrappedProfiles = unwrapProfiles(profiles); 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, + unwrappedProfiles.realProfiles(), + unwrappedProfiles.discreteProfiles(), simulatedActivities, unfinishedActivities, simulationStartTime, @@ -879,6 +926,73 @@ public Optional getSimulationResults(PlanMetadata planMetadat } } + public Optional> getExternalDatasets(final PlanId planId) + throws MerlinServiceException, IOException + { + final var datasets = new ArrayList(); + final var request = """ + query { + plan_dataset(where: {plan_id: {_eq: %d}, simulation_dataset_id: {_is_null: true}}, order_by: {dataset_id:asc}) { + dataset_id + offset_from_plan_start + } + } + """.formatted(planId.id()); + final var response = postRequest(request).get(); + final var data = response.getJsonObject("data").getJsonArray("plan_dataset"); + if (data.size() == 0) { + return Optional.empty(); + } + for(final var dataset:data){ + final var datasetId = new DatasetId(dataset.asJsonObject().getInt("dataset_id")); + final var offsetFromPlanStart = durationFromPGInterval(dataset + .asJsonObject() + .getString("offset_from_plan_start")); + datasets.add(new DatasetMetadata(datasetId, offsetFromPlanStart)); + } + return Optional.of(datasets); + } + + @Override + public ExternalProfiles getExternalProfiles(final PlanId planId) + throws MerlinServiceException, IOException + { + final Map realProfiles = new HashMap<>(); + final Map discreteProfiles = new HashMap<>(); + final var resourceTypes = new ArrayList(); + final var datasetMetadatas = getExternalDatasets(planId); + if(datasetMetadatas.isPresent()) { + for(final var datasetMetadata: datasetMetadatas.get()) { + final var profiles = getProfilesWithSegments(datasetMetadata.datasetId()); + profiles.realProfiles().forEach((name, profile) -> { + realProfiles.put(name, + LinearProfile.fromExternalProfile( + datasetMetadata.offsetFromPlanStart, + profile.getRight())); + }); + profiles.discreteProfiles().forEach((name, profile) -> { + discreteProfiles.put(name, + DiscreteProfile.fromExternalProfile( + datasetMetadata.offsetFromPlanStart, + profile.getRight())); + }); + resourceTypes.addAll(extractResourceTypes(profiles)); + } + } + return new ExternalProfiles(realProfiles, discreteProfiles, resourceTypes); +} + + private Collection extractResourceTypes(final ProfileSet profileSet){ + final var resourceTypes = new ArrayList(); + profileSet.realProfiles().forEach((name, profile) -> { + resourceTypes.add(new ResourceType(name, profile.getLeft())); + }); + profileSet.discreteProfiles().forEach((name, profile) -> { + resourceTypes.add(new ResourceType(name, profile.getLeft())); + }); + return resourceTypes; + } + private Map parseUnfinishedActivities(JsonArray unfinishedActivitiesJson, Instant simulationStart){ final var unfinishedActivities = new HashMap(); for(final var unfinishedActivityJson: unfinishedActivitiesJson){ @@ -908,15 +1022,30 @@ private Map parseUnfinishedActivities(J return unfinishedActivities; } - private record Profiles( - Map>>> realProfiles, - Map>>> discreteProfiles - ){} + private UnwrappedProfileSet unwrapProfiles(final ProfileSet profileSet) throws MerlinServiceException { + return new UnwrappedProfileSet(unwrapProfiles(profileSet.realProfiles()), unwrapProfiles(profileSet.discreteProfiles())); + } + + private HashMap>>> unwrapProfiles(Map>>>> profiles) + throws MerlinServiceException + { + final var unwrapped = new HashMap>>>(); + for(final var profile: profiles.entrySet()) { + final var unwrappedSegments = new ArrayList>(); + for (final var segment : profile.getValue().getRight()) { + if (segment.dynamics().isPresent()) { + unwrappedSegments.add(new ProfileSegment<>(segment.extent(), segment.dynamics().get())); + } + } + unwrapped.put(profile.getKey(), Pair.of(profile.getValue().getLeft(), unwrappedSegments)); + } + return unwrapped; + } - private Profiles parseProfiles(JsonArray dataset){ - Map>>> realProfiles = new HashMap<>(); - Map>>> discreteProfiles = new HashMap<>(); - for(final var profile:dataset){ + private ProfileSet 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"); @@ -929,33 +1058,50 @@ private Profiles parseProfiles(JsonArray dataset){ discreteProfiles.put(name, discreteProfile); } } - return new Profiles(realProfiles, discreteProfiles); + return new ProfileSet(realProfiles, discreteProfiles); } - public Pair>> parseProfile(JsonObject profile, JsonParser dynamicsParser){ + 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()) { + final var segments = new ArrayList>>(); + if(profile.containsKey("profile_segments")) { + final var resultSet = profile.getJsonArray("profile_segments").iterator(); + JsonValue curProfileSegment = null; + if (resultSet.hasNext()) { + final var profileExtent = durationFromPGInterval(profile.asJsonObject().getString("duration")); curProfileSegment = resultSet.next(); - final var nextOffset = durationFromPGInterval(curProfileSegment.asJsonObject().getString("start_offset")); - final var duration = nextOffset.minus(offset); + var offset = durationFromPGInterval(curProfileSegment.asJsonObject().getString("start_offset")); + var isGap = curProfileSegment.asJsonObject().getBoolean("is_gap"); + Optional dynamics; + if (!isGap) { + dynamics = Optional.of(dynamicsParser + .parse(curProfileSegment.asJsonObject().get("dynamics")) + .getSuccessOrThrow()); + } else { + dynamics = Optional.empty(); + } + + 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)); + isGap = curProfileSegment.asJsonObject().getBoolean("is_gap"); + offset = nextOffset; + if (!isGap) { + dynamics = Optional.of(dynamicsParser + .parse(curProfileSegment.asJsonObject().get("dynamics")) + .getSuccessOrThrow()); + } else { + dynamics = Optional.empty(); + } + } + + final var duration = profileExtent.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); } @@ -1005,7 +1151,7 @@ private Map parseSimulatedActivities(Jso * @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 { + public Optional getSuitableSimulationResults(PlanMetadata planMetadata) throws MerlinServiceException, IOException { final var request = """ { @@ -1045,7 +1191,7 @@ public Optional getSuitableSimulationResults(PlanMetadata planMetada } private SimulationId createSimulation(final PlanId planId, final Map arguments) - throws PlanServiceException, IOException + throws MerlinServiceException, IOException { final var request = """ mutation { @@ -1066,7 +1212,7 @@ private SimulationId createSimulation(final PlanId planId, final Map Duration sumDurations(final List>> private HashMap postResourceProfiles(DatasetId datasetId, final Map>>>> realProfiles, final Map>>>> discreteProfiles) - throws PlanServiceException, IOException + throws MerlinServiceException, IOException { final var req = """ mutation($profiles: [profile_insert_input!]!) { @@ -1189,7 +1331,7 @@ private HashMap postResourceProfiles(DatasetId datasetId, final var id = dataReturned.asJsonObject().getInt("id"); final var nameResourceReturned = dataReturned.asJsonObject().getString("name"); if(!nameResourceReturned.equals(resource)){ - throw new PlanServiceException("Resource do not match"); + throw new MerlinServiceException("Resource do not match"); } profileRecords.put(resource, new ProfileRecord( id, @@ -1202,13 +1344,11 @@ private HashMap postResourceProfiles(DatasetId datasetId, return profileRecords; } - - private void postProfileSegments( final DatasetId datasetId, final Map records, final ProfileSet profileSet - ) throws PlanServiceException, IOException + ) throws MerlinServiceException, IOException { final var realProfiles = profileSet.realProfiles(); final var discreteProfiles = profileSet.discreteProfiles(); @@ -1234,7 +1374,7 @@ private void postProfileSegment( final ProfileRecord profileRecord, final List>> segments, final JsonParser dynamicsP - ) throws PlanServiceException, IOException + ) throws MerlinServiceException, IOException { final var req = """ mutation($profileSegments:[profile_segment_insert_input!]!) { @@ -1275,19 +1415,19 @@ private void postProfileSegment( final JsonObject response; try { response = postRequest(req, arguments).get(); - } catch (PlanServiceException e) { - throw new PlanServiceException(e.toString()); + } catch (MerlinServiceException e) { + throw new MerlinServiceException(e.toString()); } final var affected_rows = response.getJsonObject("data").getJsonObject("insert_profile_segment").getInt("affected_rows"); if(affected_rows!=segments.size()) { - throw new PlanServiceException("not the same size"); + throw new MerlinServiceException("not the same size"); } } private void postRealProfileSegments(final DatasetId datasetId, final ProfileRecord profileRecord, final List>> segments) - throws PlanServiceException, IOException + throws MerlinServiceException, IOException { postProfileSegment(datasetId, profileRecord, segments, realDynamicsP); } @@ -1295,14 +1435,14 @@ private void postRealProfileSegments(final DatasetId datasetId, private void postDiscreteProfileSegments(final DatasetId datasetId, final ProfileRecord profileRecord, final List>> segments) - throws PlanServiceException, IOException + throws MerlinServiceException, IOException { postProfileSegment(datasetId, profileRecord, segments, serializedValueP); } private void insertSimulationTopics( DatasetId datasetId, - final List> topics) throws PlanServiceException, IOException + final List> topics) throws MerlinServiceException, IOException { final var req = """ mutation($topics:[topic_insert_input!]!) { @@ -1330,7 +1470,7 @@ private void insertSimulationTopics( private void insertSimulationEvents( DatasetId datasetId, - Map>>> eventPoints) throws PlanServiceException, IOException + Map>>> eventPoints) throws MerlinServiceException, IOException { final var req = """ mutation($events:[event_insert_input!]!){ @@ -1385,7 +1525,7 @@ private void postActivities( final Map unfinishedActivities, final Instant simulationStart, final Map simulationActivityDirectiveIdToMerlinActivityDirectiveId - ) throws PlanServiceException, IOException + ) throws MerlinServiceException, IOException { final var simulatedActivityRecords = simulatedActivities.entrySet().stream() .collect(Collectors.toMap( @@ -1410,7 +1550,7 @@ public void updateSimulatedActivityParentsAction( final DatasetId datasetId, final Map simulatedActivities, final Map simIdToPgId -) throws PlanServiceException, IOException +) throws MerlinServiceException, IOException { final var req = """ mutation($updates:[span_updates!]!) { @@ -1449,7 +1589,7 @@ public void updateSimulatedActivityParentsAction( affected_rows = response.getJsonObject("data").getJsonObject("update_span_many").getInt("affected_rows"); } if(affected_rows != updateCounter) { - throw new PlanServiceException("not the same size"); + throw new MerlinServiceException("not the same size"); } } @@ -1484,7 +1624,7 @@ private static SpanRecord unfinishedActivityToRecord(final UnfinishedActivity ac public HashMap postSpans(final DatasetId datasetId, final Map spans, final Instant simulationStart - ) throws PlanServiceException, IOException + ) throws MerlinServiceException, IOException { final var req = """ mutation($spans:[span_insert_input!]!) { 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/MerlinService.java similarity index 74% rename from scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/PlanService.java rename to scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MerlinService.java index b3d7ce07e1..c6aea10d5f 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/MerlinService.java @@ -9,22 +9,37 @@ import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchActivityInstanceException; +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.http.InvalidJsonException; +import gov.nasa.jpl.aerie.scheduler.server.models.ActivityType; import gov.nasa.jpl.aerie.scheduler.server.models.DatasetId; +import gov.nasa.jpl.aerie.scheduler.server.models.ExternalProfiles; import gov.nasa.jpl.aerie.scheduler.server.models.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.models.ResourceType; import org.apache.commons.lang3.tuple.Pair; import java.io.IOException; import java.time.Instant; +import java.util.Collection; import java.util.Map; import java.util.Optional; -public interface PlanService { +public interface MerlinService { + record MissionModelTypes(Collection activityTypes, Collection resourceTypes) {} + interface ReaderRole { + + MerlinService.MissionModelTypes getMissionModelTypes(final PlanId planId) + throws IOException, MerlinServiceException; + MerlinService.MissionModelTypes getMissionModelTypes(final MissionModelId missionModelId) + throws IOException, MerlinServiceException, + NoSuchMissionModelException; + /** * fetch current revision number of the target plan stored in aerie * @@ -33,7 +48,7 @@ interface ReaderRole { * @throws NoSuchPlanException when the plan container does not exist in aerie */ long getPlanRevision(final PlanId planId) - throws IOException, NoSuchPlanException, PlanServiceException; + throws IOException, NoSuchPlanException, MerlinServiceException; /** * fetch current metadata of the target plan (not the activity instance content) @@ -43,7 +58,7 @@ long getPlanRevision(final PlanId planId) * @throws NoSuchPlanException when the plan container does not exist in aerie */ PlanMetadata getPlanMetadata(final PlanId planId) - throws IOException, NoSuchPlanException, PlanServiceException; + throws IOException, NoSuchPlanException, MerlinServiceException; /** * create an in-memory snapshot of the target plan's activity contents from aerie @@ -54,7 +69,7 @@ PlanMetadata getPlanMetadata(final PlanId planId) * @throws NoSuchPlanException when the plan container does not exist in aerie */ MerlinPlan getPlanActivityDirectives(final PlanMetadata planMetadata, final Problem mission) - throws IOException, NoSuchPlanException, PlanServiceException, InvalidJsonException, InstantiationException; + throws IOException, NoSuchPlanException, MerlinServiceException, InvalidJsonException, InstantiationException; /** * confirms that the specified plan exists in the aerie database, throwing exception if not @@ -64,7 +79,7 @@ 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; + throws IOException, NoSuchPlanException, MerlinServiceException; /** * Gets existing simulation results for current plan if they exist and are suitable for scheduling purposes (current revision, covers the entire planning horizon) @@ -72,7 +87,27 @@ void ensurePlanExists(final PlanId planId) * @param planMetadata the plan metadata * @return simulation results, optionally */ - Optional getSimulationResults(PlanMetadata planMetadata) throws PlanServiceException, IOException, InvalidJsonException; + Optional getSimulationResults(PlanMetadata planMetadata) throws MerlinServiceException, IOException, InvalidJsonException; + + + /** + * Gets external profiles associated to a plan, including segments + * @param planId the plan id + * @throws MerlinServiceException + * @throws IOException + */ + ExternalProfiles getExternalProfiles(final PlanId planId) + throws MerlinServiceException, IOException; + + /** + * Gets resource types associated to a plan, those coming from the mission model as well as those coming from external dataset resources + * @param planId the plan id + * @throws IOException + * @throws MerlinServiceException + * @throws NoSuchPlanException + */ + Collection getResourceTypes(final PlanId planId) + throws IOException, MerlinServiceException, NoSuchPlanException; } interface WriterRole { @@ -91,7 +126,7 @@ Pair> createNewPla final Plan plan, final Map activityToGoalId ) - throws IOException, NoSuchPlanException, PlanServiceException; + throws IOException, NoSuchPlanException, MerlinServiceException; /** * create a new empty plan container based on specifications @@ -106,7 +141,7 @@ Pair> createNewPla * @throws NoSuchPlanException when the plan container could not be found in aerie after creation */ PlanId createEmptyPlan(final String name, final long modelId, final Instant startTime, final Duration duration) - throws IOException, NoSuchPlanException, PlanServiceException; + throws IOException, NoSuchPlanException, MerlinServiceException; /** * synchronize the in-memory plan back over to aerie data stores via update operations @@ -125,7 +160,7 @@ Map updatePlanActivityDirectiv Plan plan, Map activityToGoalId ) - throws IOException, NoSuchPlanException, PlanServiceException, NoSuchActivityInstanceException; + throws IOException, NoSuchPlanException, MerlinServiceException, NoSuchActivityInstanceException; /** * delete all the activity instances stored in the target plan container @@ -136,7 +171,7 @@ Map updatePlanActivityDirectiv * @throws NoSuchPlanException when the plan container does not exist in aerie */ void clearPlanActivityDirectives(final PlanId planId) - throws IOException, NoSuchPlanException, PlanServiceException; + throws IOException, NoSuchPlanException, MerlinServiceException; /** * create activity instances in the target plan container for each activity in the input plan @@ -155,7 +190,7 @@ Map createAllPlanActivityDirec final Plan plan, final Map activityToGoalId ) - throws IOException, NoSuchPlanException, PlanServiceException; + throws IOException, NoSuchPlanException, MerlinServiceException; /** * Stores the simulation results produced during scheduling @@ -165,11 +200,12 @@ Map createAllPlanActivityDirec * @param simulationActivityDirectiveIdToMerlinActivityDirectiveId the translation between activity ids in the * local simulation and the merlin activity ids * @return - * @throws PlanServiceException + * @throws MerlinServiceException * @throws IOException */ DatasetId storeSimulationResults(final PlanMetadata planMetadata, final SimulationResults results, - final Map simulationActivityDirectiveIdToMerlinActivityDirectiveId) throws PlanServiceException, IOException; + final Map simulationActivityDirectiveIdToMerlinActivityDirectiveId) throws + MerlinServiceException, IOException; } interface OwnerRole extends ReaderRole, WriterRole {} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MerlinServiceException.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MerlinServiceException.java new file mode 100644 index 0000000000..55f37bd35c --- /dev/null +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MerlinServiceException.java @@ -0,0 +1,7 @@ +package gov.nasa.jpl.aerie.scheduler.server.services; + +public class MerlinServiceException extends Exception { + MerlinServiceException(final String message) { + super(message); + } +} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MissionModelService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MissionModelService.java deleted file mode 100644 index 9a36a3a209..0000000000 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/MissionModelService.java +++ /dev/null @@ -1,35 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.server.services; - -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.exceptions.NoSuchMissionModelException; -import gov.nasa.jpl.aerie.scheduler.server.models.MissionModelId; -import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; - -import java.io.IOException; -import java.util.Collection; -import java.util.Map; - -public interface MissionModelService { - MissionModelTypes getMissionModelTypes(final PlanId planId) - throws IOException, MissionModelServiceException; - MissionModelTypes getMissionModelTypes(final MissionModelId missionModelId) - throws IOException, MissionModelServiceException, - NoSuchMissionModelException; - - class MissionModelServiceException extends Exception { - MissionModelServiceException(final String message) { - super(message); - } - - public MissionModelServiceException(final String message, final Throwable cause) { - super(message, cause); - } - } - - record ActivityType(String name, Map parameters, Map> presets) {} - - record ResourceType(String name, ValueSchema schema) {} - - record MissionModelTypes(Collection activityTypes, Collection resourceTypes) {} -} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/PlanServiceException.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/PlanServiceException.java deleted file mode 100644 index 955db8cccc..0000000000 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/PlanServiceException.java +++ /dev/null @@ -1,7 +0,0 @@ -package gov.nasa.jpl.aerie.scheduler.server.services; - -public class PlanServiceException extends Exception { - PlanServiceException(final String message) { - super(message); - } -} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/TypescriptCodeGenerationService.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/TypescriptCodeGenerationService.java index ef816b1951..ab93b554a6 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/TypescriptCodeGenerationService.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/TypescriptCodeGenerationService.java @@ -8,6 +8,8 @@ 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.ActivityType; +import gov.nasa.jpl.aerie.scheduler.server.models.ResourceType; import org.apache.commons.lang3.tuple.Pair; import static gov.nasa.jpl.aerie.merlin.driver.json.SerializedValueJsonParser.serializedValueP; @@ -15,7 +17,7 @@ public final class TypescriptCodeGenerationService { private TypescriptCodeGenerationService() { } - public static String generateTypescriptTypesFromMissionModel(final MissionModelService.MissionModelTypes missionModelTypes) { + public static String generateTypescriptTypesFromMissionModel(final MerlinService.MissionModelTypes missionModelTypes) { final var activityTypeCodes = new ArrayList(); for (final var activityType : missionModelTypes.activityTypes()) { activityTypeCodes.add(getActivityTypeInformation(activityType)); @@ -49,7 +51,7 @@ public static String generateTypescriptTypesFromMissionModel(final MissionModelS return joinLines(result); } - private static String generateResourceTypes(final Collection resourceTypes) { + private static String generateResourceTypes(final Collection resourceTypes) { final var result = new ArrayList(); result.add("export enum Resource {"); for (final var resourceType : resourceTypes) { @@ -216,11 +218,11 @@ static String toString(final TypescriptType type, boolean topLevelStructIsProfil } } - private static ActivityTypeCode getActivityTypeInformation(final MissionModelService.ActivityType activityType) { + private static ActivityTypeCode getActivityTypeInformation(final ActivityType activityType) { return new ActivityTypeCode(activityType.name(), generateActivityParameterTypes(activityType), activityType.presets()); } - private static List generateActivityParameterTypes(final MissionModelService.ActivityType activityType) { + private static List generateActivityParameterTypes(final ActivityType activityType) { return activityType .parameters() .entrySet() diff --git a/scheduler-server/src/testFixtures/java/gov/nasa/jpl/aerie/scheduler/server/services/TypescriptCodeGenerationServiceTestFixtures.java b/scheduler-server/src/testFixtures/java/gov/nasa/jpl/aerie/scheduler/server/services/TypescriptCodeGenerationServiceTestFixtures.java index 659b209212..c64557a89a 100644 --- a/scheduler-server/src/testFixtures/java/gov/nasa/jpl/aerie/scheduler/server/services/TypescriptCodeGenerationServiceTestFixtures.java +++ b/scheduler-server/src/testFixtures/java/gov/nasa/jpl/aerie/scheduler/server/services/TypescriptCodeGenerationServiceTestFixtures.java @@ -5,13 +5,15 @@ 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.ActivityType; +import gov.nasa.jpl.aerie.scheduler.server.models.ResourceType; public final class TypescriptCodeGenerationServiceTestFixtures { - public static final MissionModelService.MissionModelTypes MISSION_MODEL_TYPES = - new MissionModelService.MissionModelTypes( + public static final MerlinService.MissionModelTypes MISSION_MODEL_TYPES = + new MerlinService.MissionModelTypes( List.of( - new MissionModelService.ActivityType( + new ActivityType( "SampleActivity1", Map.of( "variant", @@ -35,7 +37,7 @@ public final class TypescriptCodeGenerationServiceTestFixtures { ), Map.of() ), - new MissionModelService.ActivityType( + new ActivityType( "SampleActivity2", Map.of( "quantity", @@ -46,7 +48,7 @@ public final class TypescriptCodeGenerationServiceTestFixtures { Map.of("quantity", SerializedValue.of(5)) ) ), - new MissionModelService.ActivityType( + new ActivityType( "SampleActivity3", Map.of( "variant", @@ -60,15 +62,15 @@ public final class TypescriptCodeGenerationServiceTestFixtures { Map.of("variant", SerializedValue.of("option1")) ) ), - new MissionModelService.ActivityType( + new ActivityType( "SampleActivityEmpty", Map.of(), Map.of() ) ), List.of( - new MissionModelService.ResourceType("/sample/resource/1", ValueSchema.REAL), - new MissionModelService.ResourceType("/sample/resource/3", ValueSchema.ofVariant(List.of( + new ResourceType("/sample/resource/1", ValueSchema.REAL), + new ResourceType("/sample/resource/3", ValueSchema.ofVariant(List.of( new ValueSchema.Variant( "option1", "option1" ), @@ -76,7 +78,7 @@ public final class TypescriptCodeGenerationServiceTestFixtures { "option2", "option2" ) ))), - new MissionModelService.ResourceType("/sample/resource/2", ValueSchema.ofStruct( + new ResourceType("/sample/resource/2", ValueSchema.ofStruct( Map.of( "field1", ValueSchema.BOOLEAN, "field2", ValueSchema.ofVariant(List.of( diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java index 0cc7cbd3df..f1682b818b 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/SchedulerWorkerAppDriver.java @@ -63,7 +63,6 @@ public static void main(String[] args) throws Exception { final var specificationService = new LocalSpecificationService(stores.specifications()); final var scheduleAgent = new SynchronousSchedulerAgent(specificationService, - merlinService, merlinService, config.merlinFileStore(), config.missionRuleJarPath(), diff --git a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationService.java b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationService.java index a59af3868f..3703e48db1 100644 --- a/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationService.java +++ b/scheduler-worker/src/main/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationService.java @@ -1,26 +1,28 @@ package gov.nasa.jpl.aerie.scheduler.worker.services; -import javax.json.Json; -import javax.json.JsonObject; -import javax.json.stream.JsonParsingException; -import java.io.File; -import java.io.IOException; -import java.io.StringReader; -import java.util.List; -import java.util.Objects; -import static gov.nasa.jpl.aerie.constraints.json.ConstraintParsers.windowsExpressionP; -import gov.nasa.jpl.aerie.constraints.time.Windows; -import gov.nasa.jpl.aerie.constraints.tree.Expression; import gov.nasa.jpl.aerie.json.JsonParser; import gov.nasa.jpl.aerie.scheduler.server.http.InvalidEntityException; import gov.nasa.jpl.aerie.scheduler.server.http.InvalidJsonException; import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; +import gov.nasa.jpl.aerie.scheduler.server.models.ResourceType; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingCompilationError; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingDSL; import gov.nasa.jpl.aerie.scheduler.server.services.ConstraintsTypescriptCodeGenerationHelper; -import gov.nasa.jpl.aerie.scheduler.server.services.MissionModelService; +import gov.nasa.jpl.aerie.scheduler.server.services.MerlinService; +import gov.nasa.jpl.aerie.scheduler.server.services.MerlinServiceException; import gov.nasa.jpl.aerie.scheduler.server.services.TypescriptCodeGenerationService; +import javax.json.Json; +import javax.json.JsonObject; +import javax.json.stream.JsonParsingException; +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + public class SchedulingDSLCompilationService { private final Process nodeProcess; @@ -48,30 +50,48 @@ public void close() { this.nodeProcess.destroy(); } - public SchedulingDSLCompilationResult compileGlobalSchedulingCondition(final MissionModelService missionModelService, final PlanId planId, final String conditionTypescript) { + public SchedulingDSLCompilationResult compileGlobalSchedulingCondition(final MerlinService.ReaderRole merlinService, final PlanId planId, final String conditionTypescript, final + Collection additionalResourceTypes) { try{ - final var missionModelTypes = missionModelService.getMissionModelTypes(planId); - return compile(missionModelTypes, conditionTypescript, SchedulingDSL.conditionSpecifierP, "GlobalSchedulingCondition"); - } catch (IOException | MissionModelService.MissionModelServiceException e) { + final var missionModelTypes = merlinService.getMissionModelTypes(planId); + final var aggregatedResourceTypes = new ArrayList<>(missionModelTypes.resourceTypes()); + aggregatedResourceTypes.addAll(additionalResourceTypes); + final var planTypes = new MerlinService.MissionModelTypes(missionModelTypes.activityTypes(), aggregatedResourceTypes); + return compile(planTypes, conditionTypescript, SchedulingDSL.conditionSpecifierP, "GlobalSchedulingCondition"); + } catch (IOException | MerlinServiceException e) { throw new Error(e); } } + public SchedulingDSLCompilationResult compileSchedulingGoalDSL( + final MerlinService.ReaderRole merlinService, + final PlanId planId, + final String goalTypescript){ + return compileSchedulingGoalDSL(merlinService, planId, goalTypescript, List.of()); + } + /** * NOTE: This method is not re-entrant (assumes only one call to this method is running at any given time) */ - public SchedulingDSLCompilationResult compileSchedulingGoalDSL(final MissionModelService missionModelService, final PlanId planId, final String goalTypescript) + public SchedulingDSLCompilationResult compileSchedulingGoalDSL( + final MerlinService.ReaderRole merlinService, + final PlanId planId, + final String goalTypescript, + final Collection additionalResourceTypes) { try { - final var missionModelTypes = missionModelService.getMissionModelTypes(planId); - return compile(missionModelTypes, goalTypescript, SchedulingDSL.schedulingJsonP(missionModelTypes), "Goal"); - } catch (IOException | MissionModelService.MissionModelServiceException e) { + final var missionModelTypes = merlinService.getMissionModelTypes(planId); + final var aggregatedResourceTypes = new ArrayList<>(missionModelTypes.resourceTypes()); + aggregatedResourceTypes.addAll(additionalResourceTypes); + final var augmentedMissionModelTypes = new MerlinService.MissionModelTypes(missionModelTypes.activityTypes(), aggregatedResourceTypes); + return compile(augmentedMissionModelTypes, goalTypescript, SchedulingDSL.schedulingJsonP(augmentedMissionModelTypes), "Goal"); + } catch (IOException | MerlinServiceException e) { throw new Error(e); } } private SchedulingDSLCompilationResult compile( - final MissionModelService.MissionModelTypes missionModelTypes, + final MerlinService.MissionModelTypes missionModelTypes, final String goalTypescript, final JsonParser parser, final String expectedReturnType) 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 d5f84df40d..5d055ba59f 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 @@ -10,6 +10,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -46,19 +47,20 @@ 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.ExternalProfiles; import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.GoalRecord; import gov.nasa.jpl.aerie.scheduler.server.models.GoalSource; import gov.nasa.jpl.aerie.scheduler.server.models.MerlinPlan; 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.models.ResourceType; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingCompilationError; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingDSL; import gov.nasa.jpl.aerie.scheduler.server.models.Specification; import gov.nasa.jpl.aerie.scheduler.server.remotes.postgres.GoalBuilder; -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 gov.nasa.jpl.aerie.scheduler.server.services.MerlinService; +import gov.nasa.jpl.aerie.scheduler.server.services.MerlinServiceException; import gov.nasa.jpl.aerie.scheduler.server.services.RevisionData; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleRequest; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleResults; @@ -72,8 +74,7 @@ /** * agent that handles posed scheduling requests by blocking the requester thread until scheduling is complete * - * @param planService interface for querying plan details from merlin - * @param missionModelService interface for querying mission model details from merlin + * @param merlinService interface for querying plan and mission model details from merlin * @param modelJarsDir path to parent directory for mission model jars (interim backdoor jar file access) * @param goalsJarPath path to jar file to load scheduling goals from (interim solution for user input goals) * @param outputMode how the scheduling output should be returned to aerie (eg overwrite or new container) @@ -81,8 +82,7 @@ //TODO: will eventually need scheduling goal service arg to pull goals from scheduler's own data store public record SynchronousSchedulerAgent( SpecificationService specificationService, - PlanService.OwnerRole planService, - MissionModelService missionModelService, + MerlinService.OwnerRole merlinService, Path modelJarsDir, Path goalsJarPath, PlanOutputMode outputMode, @@ -91,8 +91,7 @@ public record SynchronousSchedulerAgent( implements SchedulerAgent { public SynchronousSchedulerAgent { - Objects.requireNonNull(planService); - Objects.requireNonNull(missionModelService); + Objects.requireNonNull(merlinService); Objects.requireNonNull(modelJarsDir); Objects.requireNonNull(goalsJarPath); Objects.requireNonNull(schedulingDSLCompilationService); @@ -114,7 +113,7 @@ public void schedule(final ScheduleRequest request, final ResultsProtocol.Writer //TODO: maybe some kind of high level db transaction wrapping entire read/update of target plan revision final var specification = specificationService.getSpecification(request.specificationId()); - final var planMetadata = planService.getPlanMetadata(specification.planId()); + final var planMetadata = merlinService.getPlanMetadata(specification.planId()); ensureRequestIsCurrent(request); ensurePlanRevisionMatch(specification, planMetadata.planRev()); //create scheduler problem seeded with initial plan @@ -130,20 +129,22 @@ public void schedule(final ScheduleRequest request, final ResultsProtocol.Writer simulationFacade, schedulerMissionModel.schedulerModel() ); + final var externalProfiles = loadExternalProfiles(planMetadata.planId()); final var initialSimulationResults = loadSimulationResults(planMetadata); //seed the problem with the initial plan contents final var loadedPlanComponents = loadInitialPlan(planMetadata, problem); problem.setInitialPlan(loadedPlanComponents.schedulerPlan(), initialSimulationResults); - + problem.setExternalProfile(externalProfiles.realProfiles(), externalProfiles.discreteProfiles()); //apply constraints/goals to the problem final var compiledGlobalSchedulingConditions = new ArrayList(); final var failedGlobalSchedulingConditions = new ArrayList>(); specification.globalSchedulingConditions().forEach($ -> { if (!$.enabled()) return; final var result = schedulingDSLCompilationService.compileGlobalSchedulingCondition( - missionModelService, + merlinService, planMetadata.planId(), - $.source().source()); + $.source().source(), + externalProfiles.resourceTypes()); if (result instanceof SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Success r) { compiledGlobalSchedulingConditions.addAll(conditionBuilder(r.value(), problem)); } else if (result instanceof SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Error r) { @@ -174,10 +175,11 @@ public void schedule(final ScheduleRequest request, final ResultsProtocol.Writer for (final var goalRecord : specification.goalsByPriority()) { if (!goalRecord.enabled()) continue; final var result = compileGoalDefinition( - missionModelService, + merlinService, planMetadata.planId(), goalRecord.definition(), - schedulingDSLCompilationService); + schedulingDSLCompilationService, + externalProfiles.resourceTypes()); if (result instanceof SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Success r) { compiledGoals.add(Pair.of(goalRecord, r.value())); } else if (result instanceof SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Error r) { @@ -229,7 +231,7 @@ public void schedule(final ScheduleRequest request, final ResultsProtocol.Writer solutionPlan, activityToGoalId ); - final var planMetadataAfterChanges = planService.getPlanMetadata(specification.planId()); + final var planMetadataAfterChanges = merlinService.getPlanMetadata(specification.planId()); final var datasetId = storeSimulationResults(planningHorizon, simulationFacade, planMetadataAfterChanges, instancesToIds); //collect results and notify subscribers of success final var results = collectResults(solutionPlan, instancesToIds, goals); @@ -258,7 +260,7 @@ public void schedule(final ScheduleRequest request, final ResultsProtocol.Writer .message(e.toString()) .data(ResponseSerializers.serializeNoSuchPlanException(e)) .trace(e)); - } catch (final PlanServiceException e) { + } catch (final MerlinServiceException e) { writer.failWith(b -> b .type("PLAN_SERVICE_EXCEPTION") .message(e.toString()) @@ -273,15 +275,21 @@ 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) { + return merlinService.getSimulationResults(planMetadata); + } catch (MerlinServiceException | IOException | InvalidJsonException e) { throw new ResultsProtocolFailure(e); } } + private ExternalProfiles loadExternalProfiles(final PlanId planId) + throws MerlinServiceException, IOException + { + return merlinService.getExternalProfiles(planId); + } + private Optional storeSimulationResults(PlanningHorizon planningHorizon, SimulationFacade simulationFacade, PlanMetadata planMetadata, final Map schedDirectiveToMerlinId) - throws PlanServiceException, IOException + throws MerlinServiceException, IOException { //finish simulation until end of horizon before posting results try { @@ -300,9 +308,9 @@ private Optional storeSimulationResults(PlanningHorizon planningHoriz Map.Entry::getValue, (a) -> schedID_to_MerlinID.get(a.getKey()))); if(simID_to_MerlinID.values().containsAll(schedDirectiveToMerlinId.values()) && schedDirectiveToMerlinId.values().containsAll(simID_to_MerlinID.values())){ - return Optional.of(planService.storeSimulationResults(planMetadata, - simulationFacade.getLatestDriverSimulationResults(), - simID_to_MerlinID)); + return Optional.of(merlinService.storeSimulationResults(planMetadata, + simulationFacade.getLatestDriverSimulationResults(), + simID_to_MerlinID)); } else{ //schedule in simulation is inconsistent with current state of the plan (user probably disabled simulation for some of the goals) return Optional.empty(); @@ -310,15 +318,17 @@ private Optional storeSimulationResults(PlanningHorizon planningHoriz } private static SchedulingDSLCompilationService.SchedulingDSLCompilationResult compileGoalDefinition( - final MissionModelService missionModelService, + final MerlinService.ReaderRole merlinService, final PlanId planId, final GoalSource goalDefinition, - final SchedulingDSLCompilationService schedulingDSLCompilationService) + final SchedulingDSLCompilationService schedulingDSLCompilationService, + final Collection additionalResourceTypes) { return schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, planId, - goalDefinition.source() + goalDefinition.source(), + additionalResourceTypes ); } @@ -336,9 +346,9 @@ private void ensurePlanRevisionMatch(final Specification specification, final lo * @throws ResultsProtocolFailure when the requested plan cannot be found, or aerie could not be reached */ private long getMerlinPlanRev(final PlanId planId) - throws PlanServiceException, NoSuchPlanException, IOException + throws MerlinServiceException, NoSuchPlanException, IOException { - return planService.getPlanRevision(planId); + return merlinService.getPlanRevision(planId); } /** * confirms that specification revision still matches that expected by the scheduling request @@ -397,7 +407,7 @@ private Solver createScheduler(final PlanMetadata planMetadata, final Problem pr private PlanComponents loadInitialPlan(final PlanMetadata planMetadata, final Problem problem) { //TODO: maybe paranoid check if plan rev has changed since original metadata? try { - final var merlinPlan = planService.getPlanActivityDirectives(planMetadata, problem); + final var merlinPlan = merlinService.getPlanActivityDirectives(planMetadata, problem); final Map schedulingIdToDirectiveId = new HashMap<>(); final var plan = new PlanInMemory(); final var activityTypes = problem.getActivityTypes().stream().collect(Collectors.toMap(ActivityType::getName, at -> at)); @@ -560,10 +570,10 @@ private Map storeFinalPlan( try { switch (this.outputMode) { case CreateNewOutputPlan -> { - return planService.createNewPlanWithActivityDirectives(planMetadata, newPlan, goalToActivity).getValue(); + return merlinService.createNewPlanWithActivityDirectives(planMetadata, newPlan, goalToActivity).getValue(); } case UpdateInputPlanWithNewActivities -> { - return planService.updatePlanActivityDirectives( + return merlinService.updatePlanActivityDirectives( planMetadata.planId(), idsFromInitialPlan, initialPlan, 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 cc99078002..bd5b005f66 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 @@ -12,32 +12,36 @@ 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.ExternalProfiles; 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.MissionModelService; -import gov.nasa.jpl.aerie.scheduler.server.services.PlanService; -import gov.nasa.jpl.aerie.scheduler.server.services.PlanServiceException; +import gov.nasa.jpl.aerie.scheduler.server.models.ResourceType; +import gov.nasa.jpl.aerie.scheduler.server.services.MerlinService; 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.List; import java.util.Map; import java.util.Optional; -class MockMerlinService implements MissionModelService, PlanService.OwnerRole { +class MockMerlinService implements MerlinService.OwnerRole { private Optional planningHorizon; + private ExternalProfiles externalProfiles = new ExternalProfiles(Map.of(), Map.of(), List.of()); - record MissionModelInfo(Path libPath, Path modelPath, String modelName, MissionModelTypes types, Map config) {} + public void setExternalDataset(ExternalProfiles externalProfiles) { + this.externalProfiles = externalProfiles; + } + + record MissionModelInfo(Path libPath, Path modelPath, String modelName, MerlinService.MissionModelTypes types, Map config) {} private Optional missionModelInfo = Optional.empty(); private MerlinPlan initialPlan; @@ -143,6 +147,17 @@ public Optional getSimulationResults(final PlanMetadata planM return Optional.empty(); } + @Override + public ExternalProfiles getExternalProfiles(final PlanId planId) { + return externalProfiles; + } + + @Override + public Collection getResourceTypes(final PlanId planId) + { + return null; + } + @Override public void clearPlanActivityDirectives(final PlanId planId) { @@ -169,14 +184,14 @@ public DatasetId storeSimulationResults( } @Override - public MissionModelTypes getMissionModelTypes(final PlanId planId) + public MerlinService.MissionModelTypes getMissionModelTypes(final PlanId planId) { if (this.missionModelInfo.isEmpty()) throw new RuntimeException("Make sure to call setMissionModel before running a test"); return this.missionModelInfo.get().types(); } @Override - public MissionModelTypes getMissionModelTypes(final MissionModelId missionModelId) + public MerlinService.MissionModelTypes getMissionModelTypes(final MissionModelId missionModelId) { if (this.missionModelInfo.isEmpty()) throw new RuntimeException("Make sure to call setMissionModel before running a test"); return this.missionModelInfo.get().types(); diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java index 2cd4c35afc..3333ba940e 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingDSLCompilationServiceTests.java @@ -22,20 +22,31 @@ import gov.nasa.jpl.aerie.constraints.tree.StructExpressionAt; import gov.nasa.jpl.aerie.constraints.tree.ValueAt; import gov.nasa.jpl.aerie.constraints.tree.WindowsFromSpans; +import gov.nasa.jpl.aerie.merlin.driver.SimulationResults; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.InstantiationException; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.scheduler.TimeUtility; import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeAnchor; +import gov.nasa.jpl.aerie.scheduler.model.Problem; +import gov.nasa.jpl.aerie.scheduler.server.exceptions.NoSuchPlanException; +import gov.nasa.jpl.aerie.scheduler.server.http.InvalidJsonException; +import gov.nasa.jpl.aerie.scheduler.server.models.ExternalProfiles; +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.models.ResourceType; import gov.nasa.jpl.aerie.scheduler.server.models.SchedulingDSL; -import gov.nasa.jpl.aerie.scheduler.server.services.MissionModelService; +import gov.nasa.jpl.aerie.scheduler.server.services.MerlinService; +import gov.nasa.jpl.aerie.scheduler.server.services.MerlinServiceException; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import java.io.IOException; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Optional; @@ -51,18 +62,61 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class SchedulingDSLCompilationServiceTests { private static final PlanId PLAN_ID = new PlanId(1L); - private static final MissionModelService missionModelService = new MissionModelService() { + private static final MerlinService.ReaderRole merlinService = new MerlinService.ReaderRole() { @Override - public MissionModelTypes getMissionModelTypes(final PlanId missionModelId) + public MerlinService.MissionModelTypes getMissionModelTypes(final PlanId missionModelId) { return MISSION_MODEL_TYPES; } @Override - public MissionModelTypes getMissionModelTypes(final MissionModelId missionModelId) + public MerlinService.MissionModelTypes getMissionModelTypes(final MissionModelId missionModelId) { return MISSION_MODEL_TYPES; } + + @Override + public long getPlanRevision(final PlanId planId) throws IOException, NoSuchPlanException, MerlinServiceException { + return 0; + } + + @Override + public PlanMetadata getPlanMetadata(final PlanId planId) + throws IOException, NoSuchPlanException, MerlinServiceException + { + return null; + } + + @Override + public MerlinPlan getPlanActivityDirectives(final PlanMetadata planMetadata, final Problem mission) + throws IOException, NoSuchPlanException, MerlinServiceException, InvalidJsonException, InstantiationException + { + return null; + } + + @Override + public void ensurePlanExists(final PlanId planId) throws IOException, NoSuchPlanException, MerlinServiceException { + + } + + @Override + public Optional getSimulationResults(final PlanMetadata planMetadata) + throws MerlinServiceException, IOException, InvalidJsonException + { + return Optional.empty(); + } + + @Override + public ExternalProfiles getExternalProfiles(final PlanId planId) throws MerlinServiceException, IOException { + return null; + } + + @Override + public Collection getResourceTypes(final PlanId planId) + throws IOException, MerlinServiceException, NoSuchPlanException + { + return null; + } }; SchedulingDSLCompilationService schedulingDSLCompilationService; @@ -80,12 +134,13 @@ void tearDown() { void testSchedulingDSL_mutex() { final var result = schedulingDSLCompilationService.compileGlobalSchedulingCondition( - missionModelService, + merlinService, PLAN_ID, """ export default function myCondition() { return GlobalSchedulingCondition.mutex([ActivityTypes.SampleActivity2], [ActivityTypes.SampleActivity1]) } - """); + """, + List.of()); final var expectedGoalDefinition = new SchedulingDSL.ConditionSpecifier.AndCondition(List.of( new SchedulingDSL.ConditionSpecifier.GlobalSchedulingCondition( new Not( @@ -164,7 +219,7 @@ private static StructExpressionAt getSampleActivity3PresetParameters() { void testSchedulingDSL_basic() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function myGoal() { return Goal.ActivityRecurrenceGoal({ @@ -196,7 +251,7 @@ export default function myGoal() { void testSchedulingDSL_recurrence_activityFinder() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function myGoal() { return Goal.ActivityRecurrenceGoal({ @@ -231,7 +286,7 @@ export default function myGoal() { void testSchedulingDSL_partial() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function myGoal() { return Goal.ActivityRecurrenceGoal({ @@ -263,7 +318,7 @@ export default function myGoal() { void testSchedulingDSL_helper_function() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function myGoal() { return myHelper(ActivityTemplates.SampleActivity1({ @@ -298,7 +353,7 @@ function myHelper(activityTemplate) { void testSchedulingDSL_variable_not_defined() { final SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Error actualErrors; actualErrors = (SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Error) schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function myGoal() { const x = 4 - 2 @@ -326,7 +381,7 @@ function myHelper(activityTemplate) { void testSchedulingDSL_applyWhen() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function myGoal() { return Goal.ActivityRecurrenceGoal({ @@ -365,7 +420,7 @@ export default function myGoal() { void testSchedulingDSL_wrong_return_type() { final SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Error actualErrors; actualErrors = (SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Error) schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function myGoal() { return 5 @@ -382,7 +437,7 @@ export default function myGoal() { void testSchedulingDSL_temporal() { final SchedulingDSLCompilationService.SchedulingDSLCompilationResult result; result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default () => Goal.ActivityRecurrenceGoal({ @@ -415,7 +470,7 @@ export default () => Goal.ActivityRecurrenceGoal({ void testHugeGoal() { // This test is intended to create a Goal that is bigger than the node subprocess's standard input buffer final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function myGoal() { return Goal.ActivityRecurrenceGoal({ @@ -447,7 +502,7 @@ export default function myGoal() { @Test void testCoexistenceGoalActivityExpression() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function() { return Goal.CoexistenceGoal({ @@ -485,7 +540,7 @@ export default function() { @Test void testCoexistenceGoalFlexibleTimingConstraint() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function() { return Goal.CoexistenceGoal({ @@ -529,7 +584,7 @@ export default function() { @Test void testCoexistenceGoalActivityFinder() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function() { return Goal.CoexistenceGoal({ @@ -570,7 +625,7 @@ export default function() { @Test void testCoexistenceGoalParameterReference() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function() { return Goal.CoexistenceGoal({ @@ -617,7 +672,7 @@ export default function() { @Test void testCoexistenceGoalParameterReferenceValueAt() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function() { return Goal.CoexistenceGoal({ @@ -664,7 +719,7 @@ export default function() { @Test void strictTypeCheckingTest_astNode() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ interface FakeGoal { @@ -697,7 +752,7 @@ export default function() { @Test void strictTypeCheckingTest_transition() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function() { @@ -726,7 +781,7 @@ export default function() { void testSchedulingDSL_emptyActivityCorrect() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function myGoal() { return Goal.ActivityRecurrenceGoal({ @@ -755,7 +810,7 @@ export default function myGoal() { void testSchedulingDSL_emptyActivityBogus() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function myGoal() { return Goal.ActivityRecurrenceGoal({ @@ -779,7 +834,7 @@ else if (result instanceof SchedulingDSLCompilationService.SchedulingDSLCompilat @Test void testCoexistenceGoalStateConstraint() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ const micro = (m: number) => Temporal.Duration.from({microseconds: m}); @@ -819,7 +874,7 @@ export default function() { @Test void testCoexistenceGoalReferenceWindowDuration() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ const micro = (m: number) => Temporal.Duration.from({microseconds: m}); @@ -859,13 +914,14 @@ export default function() { @Test void testWindowsExpression() { final var result = schedulingDSLCompilationService.compileGlobalSchedulingCondition( - missionModelService, + merlinService, PLAN_ID, """ export default function() { return GlobalSchedulingCondition.scheduleOnlyWhen([], Real.Resource("/sample/resource/1").lessThan(5.0)); } - """); + """, + List.of()); if (result instanceof SchedulingDSLCompilationService.SchedulingDSLCompilationResult.Success r) { assertEquals( new SchedulingDSL.ConditionSpecifier.GlobalSchedulingCondition( @@ -882,7 +938,7 @@ export default function() { @Test void testAndGoal(){ final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function myGoal() { return Goal.ActivityRecurrenceGoal({ @@ -927,7 +983,7 @@ export default function myGoal() { @Test void testOrGoal(){ final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default function myGoal() { return Goal.ActivityRecurrenceGoal({ @@ -972,7 +1028,7 @@ export default function myGoal() { @Test void testActivityPreset() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default (): Goal => { return Goal.ActivityRecurrenceGoal({ @@ -1000,7 +1056,7 @@ export default (): Goal => { void testSchedulingDSLMutatingPreset() { final var result1 = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default (): Goal => { let preset = ActivityPresets.SampleActivity2["my preset"]; @@ -1026,7 +1082,7 @@ export default (): Goal => { } final var result2 = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default (): Goal => { let preset = { @@ -1059,7 +1115,7 @@ export default (): Goal => { @Test void testPresetWithEnum() { final var result = schedulingDSLCompilationService.compileSchedulingGoalDSL( - missionModelService, + merlinService, PLAN_ID, """ export default (): Goal => { return Goal.ActivityRecurrenceGoal({ diff --git a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java index 41e2648c67..825388ea1f 100644 --- a/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java +++ b/scheduler-worker/src/test/java/gov/nasa/jpl/aerie/scheduler/worker/services/SchedulingIntegrationTests.java @@ -12,8 +12,11 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; + +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOUR; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOURS; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECOND; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTE; @@ -21,8 +24,9 @@ import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECOND; import static org.junit.jupiter.api.Assertions.*; +import gov.nasa.jpl.aerie.constraints.model.DiscreteProfile; import gov.nasa.jpl.aerie.constraints.time.Interval; -import gov.nasa.jpl.aerie.foomissionmodel.mappers.FooValueMappers; +import gov.nasa.jpl.aerie.constraints.time.Segment; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirective; import gov.nasa.jpl.aerie.merlin.driver.ActivityDirectiveId; import gov.nasa.jpl.aerie.merlin.driver.MissionModelLoader; @@ -31,20 +35,23 @@ import gov.nasa.jpl.aerie.merlin.protocol.model.InputType.Parameter; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.jpl.aerie.scheduler.TimeUtility; import gov.nasa.jpl.aerie.scheduler.model.PlanningHorizon; import gov.nasa.jpl.aerie.scheduler.server.config.PlanOutputMode; import gov.nasa.jpl.aerie.scheduler.server.http.SchedulerParsers; +import gov.nasa.jpl.aerie.scheduler.server.models.ExternalProfiles; import gov.nasa.jpl.aerie.scheduler.server.models.GlobalSchedulingConditionRecord; import gov.nasa.jpl.aerie.scheduler.server.models.GlobalSchedulingConditionSource; import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.GoalRecord; import gov.nasa.jpl.aerie.scheduler.server.models.GoalSource; import gov.nasa.jpl.aerie.scheduler.server.models.PlanId; +import gov.nasa.jpl.aerie.scheduler.server.models.ResourceType; import gov.nasa.jpl.aerie.scheduler.server.models.Specification; import gov.nasa.jpl.aerie.scheduler.server.models.SpecificationId; import gov.nasa.jpl.aerie.scheduler.server.models.Timestamp; -import gov.nasa.jpl.aerie.scheduler.server.services.MissionModelService; +import gov.nasa.jpl.aerie.scheduler.server.services.MerlinService; import gov.nasa.jpl.aerie.scheduler.server.services.RevisionData; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleRequest; import gov.nasa.jpl.aerie.scheduler.server.services.ScheduleResults; @@ -1705,6 +1712,42 @@ export default (): Goal => { assertEquals("Fyffes", changeProducer.serializedActivity().getArguments().get("producer").asString().get()); } + @Test + void testExternalResource() { + + final var myBooleanResource = new DiscreteProfile( + List.of( + new Segment<>(Interval.between(HOUR.times(2), HOUR.times(4)), SerializedValue.of(true)) + ) + ).assignGaps(new DiscreteProfile(List.of(new Segment(Interval.FOREVER, SerializedValue.of(false))))); + + final var results = runScheduler( + BANANANATION, + List.of(), + List.of(new SchedulingGoal(new GoalId(0L), """ + export default (): Goal => { + return Goal.CoexistenceGoal({ + activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), + forEach: Discrete.Resource(Resources["/my_boolean"]).equal(true), + startsAt: TimingConstraint.singleton(WindowProperty.START) + }) + }""", true)), + List.of(), + PLANNING_HORIZON, + Optional.of( + new ExternalProfiles( + Map.of(), + Map.of("/my_boolean", myBooleanResource), + List.of(new ResourceType("/my_boolean", new ValueSchema.BooleanSchema())))) + ); + + assertEquals(1, results.scheduleResults.goalResults().size()); + assertEquals(1, results.updatedPlan().size()); + final var planByActivityType = partitionByActivityType(results.updatedPlan()); + final var peelBanana = planByActivityType.get("PeelBanana").iterator().next(); + assertEquals(HOUR.times(2), peelBanana.startOffset()); + } + @Test void testApplyWhen() { final var growBananaDuration = Duration.of(1, Duration.SECONDS); @@ -1908,7 +1951,7 @@ private SchedulingRunResults runScheduler( for (final var activityDirective : plannedActivities) { activities.put(new ActivityDirectiveId(id++), activityDirective); } - return runScheduler(desc, activities, goals, List.of(), planningHorizon); + return runScheduler(desc, activities, goals, List.of(), planningHorizon, Optional.empty()); } private SchedulingRunResults runScheduler( @@ -1918,23 +1961,33 @@ private SchedulingRunResults runScheduler( final PlanningHorizon planningHorizon ) { - return runScheduler(desc, plannedActivities, goals, List.of(), planningHorizon); + return runScheduler(desc, plannedActivities, goals, List.of(), planningHorizon, Optional.empty()); } - private SchedulingRunResults runScheduler( final MissionModelDescription desc, final List plannedActivities, final Iterable goals, final List globalSchedulingConditions, final PlanningHorizon planningHorizon + ){ + return runScheduler(desc, plannedActivities, goals, globalSchedulingConditions, planningHorizon, Optional.empty()); + } + + private SchedulingRunResults runScheduler( + final MissionModelDescription desc, + final List plannedActivities, + final Iterable goals, + final List globalSchedulingConditions, + final PlanningHorizon planningHorizon, + final Optional externalProfiles ){ final var activities = new HashMap(); long id = 1; for (final var activityDirective : plannedActivities) { activities.put(new ActivityDirectiveId(id++), activityDirective); } - return runScheduler(desc, activities, goals, globalSchedulingConditions, planningHorizon); + return runScheduler(desc, activities, goals, globalSchedulingConditions, planningHorizon, externalProfiles); } private SchedulingRunResults runScheduler( @@ -1942,12 +1995,16 @@ private SchedulingRunResults runScheduler( final Map plannedActivities, final Iterable goals, final List globalSchedulingConditions, - final PlanningHorizon planningHorizon + final PlanningHorizon planningHorizon, + final Optional externalProfiles ) { final var mockMerlinService = new MockMerlinService(); mockMerlinService.setMissionModel(getMissionModelInfo(desc)); mockMerlinService.setInitialPlan(plannedActivities); mockMerlinService.setPlanningHorizon(planningHorizon); + if(externalProfiles.isPresent()) { + mockMerlinService.setExternalDataset(externalProfiles.get()); + } final var planId = new PlanId(1L); final var goalsByPriority = new ArrayList(); @@ -1966,7 +2023,6 @@ private SchedulingRunResults runScheduler( final var agent = new SynchronousSchedulerAgent( specificationService, mockMerlinService, - mockMerlinService, desc.libPath(), Path.of(""), PlanOutputMode.UpdateInputPlanWithNewActivities, @@ -1986,7 +2042,7 @@ private SchedulingRunResults runScheduler( record SchedulingRunResults(ScheduleResults scheduleResults, Collection updatedPlan, Plan plan, Map idToAct) {} - static MissionModelService.MissionModelTypes loadMissionModelTypesFromJar( + static MerlinService.MissionModelTypes loadMissionModelTypesFromJar( final String jarPath, final Map configuration) throws MissionModelLoader.MissionModelLoadException @@ -1998,11 +2054,11 @@ static MissionModelService.MissionModelTypes loadMissionModelTypesFromJar( "", ""); final Map> taskSpecTypes = missionModel.getDirectiveTypes().directiveTypes(); - final var activityTypes = new ArrayList(); + final var activityTypes = new ArrayList(); for (final var entry : taskSpecTypes.entrySet()) { final var activityTypeName = entry.getKey(); final var taskSpecType = entry.getValue(); - activityTypes.add(new MissionModelService.ActivityType( + activityTypes.add(new gov.nasa.jpl.aerie.scheduler.server.models.ActivityType( activityTypeName, taskSpecType .getInputType() @@ -2013,14 +2069,14 @@ static MissionModelService.MissionModelTypes loadMissionModelTypesFromJar( )); } - final var resourceTypes = new ArrayList(); + final var resourceTypes = new ArrayList(); for (final var entry : missionModel.getResources().entrySet()) { final var name = entry.getKey(); final var resource = entry.getValue(); - resourceTypes.add(new MissionModelService.ResourceType(name, resource.getOutputType().getSchema())); + resourceTypes.add(new ResourceType(name, resource.getOutputType().getSchema())); } - return new MissionModelService.MissionModelTypes(activityTypes, resourceTypes); + return new MerlinService.MissionModelTypes(activityTypes, resourceTypes); } @Test