Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scheduling can use external profiles #1124

Merged
merged 4 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`, {
Mythicaeda marked this conversation as resolved.
Show resolved Hide resolved
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
Mythicaeda marked this conversation as resolved.
Show resolved Hide resolved
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
Loading