Skip to content

Commit

Permalink
Merge pull request #1124 from NASA-AMMOS/1071-scheduling-can-use-exte…
Browse files Browse the repository at this point in the history
…rnal-profiles

Scheduling can use external profiles
  • Loading branch information
adrienmaillard authored Sep 26, 2023
2 parents a18c037 + e4e595d commit 2f8c8c3
Show file tree
Hide file tree
Showing 36 changed files with 1,052 additions and 370 deletions.
Original file line number Diff line number Diff line change
@@ -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<Spans> {
@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<String> names) { }
}
57 changes: 56 additions & 1 deletion e2e-tests/src/tests/bindings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`, {
Expand All @@ -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();
Expand Down
109 changes: 109 additions & 0 deletions e2e-tests/src/tests/scheduler-external-datasets.test.ts
Original file line number Diff line number Diff line change
@@ -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) );
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,10 @@ protected CardinalityGoal fill(CardinalityGoal goal) {
* should probably be created!)
*/
@Override
public Collection<Conflict> getConflicts(Plan plan, final SimulationResults simulationResults) {
public Collection<Conflict> 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)) {
Expand All @@ -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;
Expand Down Expand Up @@ -201,7 +201,7 @@ else if (this.initiallyEvaluatedTemporalContext == null) {
this,
subIntervalWindows,
this.desiredActTemplate,
new EvaluationEnvironment(),
evaluationEnvironment,
nbToSchedule,
durToSchedule.isPositive() ? Optional.of(durToSchedule) : Optional.empty()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -172,7 +172,8 @@ protected CoexistenceGoal fill(CoexistenceGoal goal) {
* should probably be created!)
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public java.util.Collection<Conflict> getConflicts(Plan plan, final SimulationResults simulationResults) { //TODO: check if interval gets split and if so, notify user?
@Override
public java.util.Collection<Conflict> 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
Expand All @@ -182,7 +183,7 @@ public java.util.Collection<Conflict> getConflicts(Plan plan, final SimulationRe
// AN ACTIVITYEXPRESSION AND THEN ANALYZEWHEN WAS A MISSION PHASE, ALTHOUGH IT IS POSSIBLE TO JUST SPECIFY AN EXPRESSION<WINDOWS> 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)) {
Expand All @@ -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)) {
Expand Down Expand Up @@ -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<SchedulingActivityDirective>();
var planEvaluation = plan.getEvaluation();
Expand Down Expand Up @@ -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));
}
Expand All @@ -283,24 +287,28 @@ else if (this.initiallyEvaluatedTemporalContext == null) {
return conflicts;
}

private EvaluationEnvironment createEvaluationEnvironmentFromAnchor(Segment<Optional<Spans.Metadata>> span){
private EvaluationEnvironment createEvaluationEnvironmentFromAnchor(EvaluationEnvironment existingEnvironment, Segment<Optional<Spans.Metadata>> 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()
);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<Conflict> getConflicts(Plan plan, final SimulationResults simulationResults) {
public java.util.Collection<Conflict> getConflicts(
Plan plan,
final SimulationResults simulationResults,
final EvaluationEnvironment evaluationEnvironment
) {
return java.util.Collections.emptyList();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -28,7 +29,9 @@ public Optimizer getOptimizer(){
}

@Override
public java.util.Collection<Conflict> getConflicts(Plan plan, final SimulationResults simulationResults) {
public java.util.Collection<Conflict> getConflicts(Plan plan,
final SimulationResults simulationResults,
final EvaluationEnvironment evaluationEnvironment) {
throw new NotImplementedException("Conflict detection is performed at solver level");
}

Expand Down
Loading

0 comments on commit 2f8c8c3

Please sign in to comment.