diff --git a/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/WindowsOf.java b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/WindowsOf.java new file mode 100644 index 0000000000..af6217d540 --- /dev/null +++ b/constraints/src/main/java/gov/nasa/jpl/aerie/constraints/tree/WindowsOf.java @@ -0,0 +1,53 @@ +package gov.nasa.jpl.aerie.constraints.tree; + +import gov.nasa.jpl.aerie.constraints.model.ActivityInstance; +import gov.nasa.jpl.aerie.constraints.model.SimulationResults; +import gov.nasa.jpl.aerie.constraints.model.Violation; +import gov.nasa.jpl.aerie.constraints.time.Window; +import gov.nasa.jpl.aerie.constraints.time.Windows; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public final class WindowsOf implements Expression { + public final Expression> expression; + + public WindowsOf(Expression> expression) { + this.expression = expression; + } + + @Override + public Windows evaluate(SimulationResults results, final Window bounds, Map environment) { + final var ret = new Windows(bounds); + final var unsatisfiedWindows = this.expression.evaluate(results, bounds, environment); + for(var unsatisfiedWindow : unsatisfiedWindows){ + ret.intersectWith(unsatisfiedWindow.violationWindows); + } + return ret; + } + + @Override + public void extractResources(final Set names) { + this.expression.extractResources(names); + } + + @Override + public String prettyPrint(final String prefix) { + return this.expression.prettyPrint(prefix); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof WindowsOf)) return false; + final var o = (WindowsOf)obj; + + return Objects.equals(this.expression, o.expression); + } + + @Override + public int hashCode() { + return Objects.hash(this.expression); + } +} diff --git a/scheduler-server/build.gradle b/scheduler-server/build.gradle index 91a08f81fd..49e8537cce 100644 --- a/scheduler-server/build.gradle +++ b/scheduler-server/build.gradle @@ -70,7 +70,7 @@ dependencies { implementation project(':constraints') implementation project(':scheduler') - runtimeOnly project(':merlin-framework') + implementation project(':merlin-framework') implementation 'org.apache.commons:commons-lang3:3.12.0' implementation 'io.javalin:javalin:4.1.1' diff --git a/scheduler-server/scheduling-dsl-compiler/src/libs/scheduler-ast.ts b/scheduler-server/scheduling-dsl-compiler/src/libs/scheduler-ast.ts index d182e43e0b..378e05ad0c 100644 --- a/scheduler-server/scheduling-dsl-compiler/src/libs/scheduler-ast.ts +++ b/scheduler-server/scheduling-dsl-compiler/src/libs/scheduler-ast.ts @@ -5,9 +5,20 @@ export interface ActivityTemplate { args: {[key: string]: any}, } +export interface ClosedOpenInterval { + start: number + end: number +} + +export type CardinalityGoalArguments = + | { duration: number, occurrence: number } + | { duration: number } + | { occurrence: number } + export enum NodeKind { ActivityRecurrenceGoal = 'ActivityRecurrenceGoal', ActivityCoexistenceGoal = 'ActivityCoexistenceGoal', + ActivityCardinalityGoal = 'ActivityCardinalityGoal', GoalAnd = 'GoalAnd', GoalOr = 'GoalOr' } @@ -22,8 +33,8 @@ export enum NodeKind { export type Goal = | ActivityRecurrenceGoal | ActivityCoexistenceGoal + | ActivityCardinalityGoal ; -// TODO cardinality goal export interface ActivityRecurrenceGoal { kind: NodeKind.ActivityRecurrenceGoal, @@ -31,6 +42,13 @@ export interface ActivityRecurrenceGoal { interval: number, } +export interface ActivityCardinalityGoal { + kind: NodeKind.ActivityCardinalityGoal, + activityTemplate: ActivityTemplate, + specification: CardinalityGoalArguments, + inPeriod: ClosedOpenInterval, +} + export interface ActivityCoexistenceGoal { kind: NodeKind.ActivityCoexistenceGoal, activityTemplate: ActivityTemplate, diff --git a/scheduler-server/scheduling-dsl-compiler/src/libs/scheduler-edsl-fluent-api.ts b/scheduler-server/scheduling-dsl-compiler/src/libs/scheduler-edsl-fluent-api.ts index 3482e75c52..091a33d2de 100644 --- a/scheduler-server/scheduling-dsl-compiler/src/libs/scheduler-edsl-fluent-api.ts +++ b/scheduler-server/scheduling-dsl-compiler/src/libs/scheduler-edsl-fluent-api.ts @@ -3,6 +3,7 @@ import * as WindowsEDSL from './windows-edsl-fluent-api' interface ActivityRecurrenceGoal extends Goal {} interface ActivityCoexistenceGoal extends Goal {} +interface ActivityCardinalityGoal extends Goal {} export class Goal { public readonly __astNode: AST.GoalSpecifier; @@ -49,6 +50,14 @@ export class Goal { forEach: opts.forEach.__astnode, }); } + public static CardinalityGoal(opts: { activityTemplate: ActivityTemplate, specification: AST.CardinalityGoalArguments, inPeriod: ClosedOpenInterval }): ActivityCardinalityGoal { + return Goal.new({ + kind: AST.NodeKind.ActivityCardinalityGoal, + activityTemplate: opts.activityTemplate, + specification: opts.specification, + inPeriod : opts.inPeriod + }); + } } declare global { @@ -61,12 +70,15 @@ declare global { public static ActivityRecurrenceGoal(opts: { activityTemplate: ActivityTemplate, interval: Duration }): ActivityRecurrenceGoal public static CoexistenceGoal(opts: { activityTemplate: ActivityTemplate, forEach: WindowsEDSL.WindowSet }): ActivityCoexistenceGoal + + public static CardinalityGoal(opts: { activityTemplate: ActivityTemplate, specification: AST.CardinalityGoalArguments, inPeriod: ClosedOpenInterval }): ActivityCardinalityGoal } type Duration = number; type Double = number; type Integer = number; } +export interface ClosedOpenInterval extends AST.ClosedOpenInterval {} export interface ActivityTemplate extends AST.ActivityTemplate {} // Make Goal available on the global object 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 a79a5afcaa..017b923738 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 @@ -9,7 +9,9 @@ import java.util.List; import java.util.Map; +import java.util.Optional; +import static gov.nasa.jpl.aerie.json.BasicParsers.intP; import static gov.nasa.jpl.aerie.json.BasicParsers.doubleP; import static gov.nasa.jpl.aerie.json.BasicParsers.listP; import static gov.nasa.jpl.aerie.json.BasicParsers.longP; @@ -38,6 +40,21 @@ public class SchedulingDSL { microseconds -> Duration.of(microseconds, Duration.MICROSECONDS), duration -> duration.in(Duration.MICROSECONDS))); + private static final JsonParser intervalP = + productP + .field("start", durationP) + .field("end", durationP) + .map(Iso.of( + untuple(ClosedOpenInterval::new), + $ -> tuple($.start(), $.end()))); + + private static final JsonParser cardinalitySpecificationJsonParser = + productP + .optionalField("duration", durationP) + .optionalField("occurrence", intP) + .map(Iso.of( + untuple(CardinalitySpecification::new), + $ -> tuple($.duration(), $.occurrence()))); private static final ProductParsers.JsonObjectParser recurrenceGoalDefinitionP = productP .field("activityTemplate", activityTemplateP) @@ -147,6 +164,18 @@ private static ProductParsers.JsonObjectParser windowsO goalDefinition.activityTemplate(), goalDefinition.forEach()))); + private static final ProductParsers.JsonObjectParser cardinalityGoalDefinitionP = + productP + .field("activityTemplate", activityTemplateP) + .field("specification", cardinalitySpecificationJsonParser) + .field("inPeriod", intervalP) + .map(Iso.of( + untuple(GoalSpecifier.CardinalityGoalDefinition::new), + goalDefinition -> tuple( + goalDefinition.activityTemplate(), + goalDefinition.specification(), + goalDefinition.inPeriod()))); + private static ProductParsers.JsonObjectParser goalAndF(final JsonParser goalSpecifierP) { return productP .field("goals", listP(goalSpecifierP)) @@ -166,6 +195,7 @@ private static ProductParsers.JsonObjectParser goalOrF(fin recursiveP(self -> SumParsers.sumP("kind", GoalSpecifier.class, List.of( SumParsers.variant("ActivityRecurrenceGoal", GoalSpecifier.RecurrenceGoalDefinition.class, recurrenceGoalDefinitionP), SumParsers.variant("ActivityCoexistenceGoal", GoalSpecifier.CoexistenceGoalDefinition.class, coexistenceGoalDefinitionP), + SumParsers.variant("ActivityCardinalityGoal", GoalSpecifier.CardinalityGoalDefinition.class, cardinalityGoalDefinitionP), SumParsers.variant("GoalAnd", GoalSpecifier.GoalAnd.class, goalAndF(self)), SumParsers.variant("GoalOr", GoalSpecifier.GoalOr.class, goalOrF(self)) ))); @@ -183,12 +213,19 @@ record CoexistenceGoalDefinition( ActivityTemplate activityTemplate, ConstraintExpression forEach ) implements GoalSpecifier {} + record CardinalityGoalDefinition( + ActivityTemplate activityTemplate, + CardinalitySpecification specification, + ClosedOpenInterval inPeriod + ) implements GoalSpecifier {} record GoalAnd(List goals) implements GoalSpecifier {} record GoalOr(List goals) implements GoalSpecifier {} } public record LinearResource(String name) {} + public record CardinalitySpecification(Optional duration, Optional occurrence){} + public record ClosedOpenInterval(Duration start, Duration end){} public record ActivityTemplate(String activityType, Map arguments) {} public record ActivityExpression(String type) {} diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GoalBuilder.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GoalBuilder.java index 20f04ac591..065e9ff675 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GoalBuilder.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/remotes/postgres/GoalBuilder.java @@ -12,11 +12,23 @@ import gov.nasa.jpl.aerie.constraints.tree.RealResource; import gov.nasa.jpl.aerie.constraints.tree.RealValue; import gov.nasa.jpl.aerie.constraints.tree.Transition; +import gov.nasa.jpl.aerie.constraints.time.Window; +import gov.nasa.jpl.aerie.constraints.time.Windows; +import gov.nasa.jpl.aerie.constraints.tree.During; +import gov.nasa.jpl.aerie.constraints.tree.ForEachActivity; +import gov.nasa.jpl.aerie.constraints.tree.ViolationsOf; +import gov.nasa.jpl.aerie.constraints.tree.WindowsOf; +import gov.nasa.jpl.aerie.contrib.serialization.mappers.DurationValueMapper; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; +import gov.nasa.jpl.aerie.scheduler.Range; +import gov.nasa.jpl.aerie.scheduler.constraints.TimeRangeExpression; 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; import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeAnchor; +import gov.nasa.jpl.aerie.scheduler.goals.CardinalityGoal; import gov.nasa.jpl.aerie.scheduler.goals.CoexistenceGoal; import gov.nasa.jpl.aerie.scheduler.goals.CompositeAndGoal; import gov.nasa.jpl.aerie.scheduler.goals.Goal; @@ -82,6 +94,20 @@ public static Goal goalOfGoalSpecifier( lookupActivityType)); } return builder.build(); + } else if(goalSpecifier instanceof SchedulingDSL.GoalSpecifier.CardinalityGoalDefinition g){ + final var builder = new CardinalityGoal.Builder() + .thereExistsOne(makeActivityTemplate(g.activityTemplate(), lookupActivityType)) + .forAllTimeIn(hor) + .inPeriod(new TimeRangeExpression.Builder() + .from(new Windows(Window.betweenClosedOpen(g.inPeriod().start(), g.inPeriod().end()))) + .build()); + if(g.specification().duration().isPresent()){ + builder.duration(Window.between(g.specification().duration().get(), Duration.MAX_VALUE)); + } + if(g.specification().occurrence().isPresent()){ + builder.occurences(new Range<>(g.specification().occurrence().get(), Integer.MAX_VALUE)); + } + return builder.build(); } else { throw new Error("Unhandled variant of GoalSpecifier:" + goalSpecifier); } @@ -135,8 +161,16 @@ private static Expression expressionOfConstraintExpression( private static ActivityCreationTemplate makeActivityTemplate( final SchedulingDSL.ActivityTemplate activityTemplate, final Function lookupActivityType) { - var builder = new ActivityCreationTemplate.Builder() - .ofType(lookupActivityType.apply(activityTemplate.activityType())); + var builder = new ActivityCreationTemplate.Builder(); + final var type = lookupActivityType.apply(activityTemplate.activityType()); + if(type.getDurationType() instanceof DurationType.Controllable durationType){ + //detect duration parameter + if(activityTemplate.arguments().containsKey(durationType.parameterName())){ + builder.duration(new DurationValueMapper().deserializeValue(activityTemplate.arguments().get(durationType.parameterName())).getSuccessOrThrow()); + activityTemplate.arguments().remove(durationType.parameterName()); + } + } + builder = builder.ofType(type); for (final var argument : activityTemplate.arguments().entrySet()) { builder = builder.withArgument(argument.getKey(), argument.getValue()); } 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 384b11fc69..d98729dc5b 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,8 +1,10 @@ package gov.nasa.jpl.aerie.scheduler.server.services; +import gov.nasa.jpl.aerie.contrib.serialization.mappers.DurationValueMapper; import gov.nasa.jpl.aerie.json.BasicParsers; import gov.nasa.jpl.aerie.merlin.driver.ActivityInstanceId; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; import gov.nasa.jpl.aerie.scheduler.model.ActivityInstance; @@ -318,6 +320,12 @@ public Map updatePlanActivities( for (final var activity : plan.getActivities()) { final var idActFromInitialPlan = idsFromInitialPlan.get(activity.getId()); if (idActFromInitialPlan != null) { + //add duration to parameters if controllable + if(activity.getType().getDurationType() instanceof DurationType.Controllable durationType){ + if(!activity.getArguments().containsKey(durationType.parameterName())){ + activity.addArgument(durationType.parameterName(), new DurationValueMapper().serializeValue(activity.getDuration())); + } + } final var actFromInitialPlan = initialPlan.getActivityById(idActFromInitialPlan); //if act was present in initial plan final var schedulerActIntoMerlinAct = new MerlinActivityInstance(activity.getType().getName(), activity.getStartTime(), activity.getArguments()); @@ -462,6 +470,12 @@ private Map createActivities(final PlanId for (final var act : orderedActivities) { requestSB.append(actPre.formatted(planId.id(), act.getType().getName(), act.getStartTime().toString())); + //add duration to parameters if controllable + if(act.getType().getDurationType() instanceof DurationType.Controllable durationType){ + if(!act.getArguments().containsKey(durationType.parameterName())){ + requestSB.append(argFormat.formatted(durationType.parameterName(), getGraphQLValueString(act.getDuration()))); + } + } for (final var arg : act.getArguments().entrySet()) { final var name = arg.getKey(); var value = getGraphQLValueString(arg.getValue()); diff --git a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SynchronousSchedulerAgent.java b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SynchronousSchedulerAgent.java index 8411659d4b..90b6d5aa60 100644 --- a/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SynchronousSchedulerAgent.java +++ b/scheduler-server/src/main/java/gov/nasa/jpl/aerie/scheduler/server/services/SynchronousSchedulerAgent.java @@ -8,6 +8,7 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.BinaryMutexConstraint; import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.GlobalConstraint; import gov.nasa.jpl.aerie.scheduler.goals.Goal; import gov.nasa.jpl.aerie.scheduler.model.ActivityInstance; @@ -112,6 +113,10 @@ public void schedule(final ScheduleRequest request, final ResultsProtocol.Writer //apply constraints/goals to the problem loadConstraints(planMetadata, schedulerMissionModel.missionModel()).forEach(problem::add); + + //TODO: workaround to get the Cardinality goal working. To remove once we have global constraints in the eDSL + problem.getActivityTypes().forEach(at -> problem.add(BinaryMutexConstraint.buildMutexConstraint(at, at))); + final var orderedGoals = new ArrayList(); final var goals = new HashMap(); for (final var goalRecord : specification.goalsByPriority()) { diff --git a/scheduler-server/src/test/java/gov/nasa/jpl/aerie/scheduler/server/services/MockMerlinService.java b/scheduler-server/src/test/java/gov/nasa/jpl/aerie/scheduler/server/services/MockMerlinService.java index 464b781842..600bb4027d 100644 --- a/scheduler-server/src/test/java/gov/nasa/jpl/aerie/scheduler/server/services/MockMerlinService.java +++ b/scheduler-server/src/test/java/gov/nasa/jpl/aerie/scheduler/server/services/MockMerlinService.java @@ -1,7 +1,9 @@ package gov.nasa.jpl.aerie.scheduler.server.services; +import gov.nasa.jpl.aerie.contrib.serialization.mappers.DurationValueMapper; import gov.nasa.jpl.aerie.merlin.driver.ActivityInstanceId; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; +import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; import gov.nasa.jpl.aerie.scheduler.TimeUtility; import gov.nasa.jpl.aerie.scheduler.model.ActivityInstance; @@ -163,9 +165,17 @@ record PlannedActivityInstance(String type, Map args, D private static Collection extractPlannedActivityInstances(final Plan plan) { final var plannedActivityInstances = new ArrayList(); for (final var activity : plan.getActivities()) { + final var type = activity.getType(); + final var arguments = new HashMap<>(activity.getArguments()); + if(type.getDurationType() instanceof DurationType.Controllable durationType){ + //detect duration parameter and add it to parameters + if(!arguments.containsKey(durationType.parameterName())){ + arguments.put(durationType.parameterName(), new DurationValueMapper().serializeValue(activity.getDuration())); + } + } plannedActivityInstances.add(new PlannedActivityInstance( activity.getType().getName(), - activity.getArguments(), + arguments, activity.getStartTime())); } return plannedActivityInstances; diff --git a/scheduler-server/src/test/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulingIntegrationTests.java b/scheduler-server/src/test/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulingIntegrationTests.java index 1e405531c4..a57200e917 100644 --- a/scheduler-server/src/test/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulingIntegrationTests.java +++ b/scheduler-server/src/test/java/gov/nasa/jpl/aerie/scheduler/server/services/SchedulingIntegrationTests.java @@ -6,6 +6,9 @@ import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.Parameter; import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue; +import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema; +import gov.nasa.jpl.aerie.scheduler.constraints.scheduling.BinaryMutexConstraint; +import gov.nasa.jpl.aerie.scheduler.model.ActivityInstance; import gov.nasa.jpl.aerie.scheduler.server.config.PlanOutputMode; import gov.nasa.jpl.aerie.scheduler.server.models.GoalId; import gov.nasa.jpl.aerie.scheduler.server.models.GoalRecord; @@ -25,8 +28,11 @@ import java.util.Collection; import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; import java.util.stream.Collectors; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -91,6 +97,92 @@ export default () => Goal.ActivityRecurrenceGoal({ } } + @Test + void testEmptyPlanDurationCardinalityGoal() { + final var results = runScheduler(BANANANATION, + List.of(), + List.of(""" + export default function myGoal() { + return Goal.CardinalityGoal({ + activityTemplate: ActivityTemplates.GrowBanana({ + quantity: 1, + growingDuration: 1000000, + }), + inPeriod: {start :0, end:10000000}, + specification : {duration: 10 * 1000000} + }) + } + """)); + assertEquals(1, results.scheduleResults.goalResults().size()); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + + assertTrue(goalResult.satisfied()); + assertEquals(10, goalResult.createdActivities().size()); + for (final var activity : goalResult.createdActivities()) { + assertNotNull(activity); + } + for (final var activity : goalResult.satisfyingActivities()) { + assertNotNull(activity); + } + + final var activitiesByType = partitionByActivityType(results.updatedPlan()); + + final var growBananas = activitiesByType.get("GrowBanana"); + assertEquals(10, growBananas.size()); + + final var setStartTimes = new HashSet(Stream + .of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) + .map(x -> Duration.of(x, Duration.SECOND)).toList()); + for (final var growBanana : growBananas) { + assertTrue(setStartTimes.remove(growBanana.startTime())); + assertEquals(SerializedValue.of(1), growBanana.args().get("quantity")); + } + } + + @Test + void testEmptyPlanOccurrenceCardinalityGoal() { + final var results = runScheduler(BANANANATION, + List.of(), + List.of(""" + export default function myGoal() { + return Goal.CardinalityGoal({ + activityTemplate: ActivityTemplates.GrowBanana({ + quantity: 1, + growingDuration: 1000000, + }), + inPeriod: {start :0, end:10000000}, + specification : {occurrence: 10} + }) + } + """)); + + assertEquals(1, results.scheduleResults.goalResults().size()); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + + assertTrue(goalResult.satisfied()); + assertEquals(10, goalResult.createdActivities().size()); + for (final var activity : goalResult.createdActivities()) { + assertNotNull(activity); + } + for (final var activity : goalResult.satisfyingActivities()) { + assertNotNull(activity); + } + + final var activitiesByType = partitionByActivityType(results.updatedPlan()); + + final var growBananas = activitiesByType.get("GrowBanana"); + assertEquals(10, growBananas.size()); + + final var setStartTimes = new HashSet(Stream + .of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) + .map(x -> Duration.of(x, Duration.SECOND)).toList()); + for (final var growBanana : growBananas) { + assertTrue(setStartTimes.remove(growBanana.startTime())); + assertEquals(SerializedValue.of(1), growBanana.args().get("quantity")); + } + } + + @Test void testSingleActivityPlanSimpleRecurrenceGoal() { final var results = runScheduler( diff --git a/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/conflicts/UnsatisfiableMissingActivityConflict.java b/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/conflicts/UnsatisfiableGoalConflict.java similarity index 59% rename from scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/conflicts/UnsatisfiableMissingActivityConflict.java rename to scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/conflicts/UnsatisfiableGoalConflict.java index 2a179fc6e9..e66ce37c8f 100644 --- a/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/conflicts/UnsatisfiableMissingActivityConflict.java +++ b/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/conflicts/UnsatisfiableGoalConflict.java @@ -3,16 +3,19 @@ import gov.nasa.jpl.aerie.constraints.time.Windows; import gov.nasa.jpl.aerie.scheduler.goals.Goal; -public class UnsatisfiableMissingActivityConflict extends Conflict { +public class UnsatisfiableGoalConflict extends Conflict { + final private String reason; /** * ctor creates a new conflict * * @param goal IN STORED the dissatisfied goal that issued the conflict + * @param reason IN the reason why the goal issued the conflict */ - public UnsatisfiableMissingActivityConflict(Goal goal) { + public UnsatisfiableGoalConflict(final Goal goal, final String reason) { super(goal); + this.reason = reason; } @Override diff --git a/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/BinaryMutexConstraint.java b/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/BinaryMutexConstraint.java index 9d21c0ad23..5bc3b906dc 100644 --- a/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/BinaryMutexConstraint.java +++ b/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/scheduling/BinaryMutexConstraint.java @@ -42,11 +42,11 @@ public Windows findWindows(Plan plan, Windows windows, Conflict conflict, Simula private Windows findWindows(Plan plan, Windows windows, ActivityType actToBeScheduled, SimulationResults simulationResults) { - + Windows validWindows = new Windows(windows); if (!(actToBeScheduled.equals(actType) || actToBeScheduled.equals(otherActType))) { - throw new IllegalArgumentException("Activity type must be one of the mutexed types"); + //not concerned by this constraint + return validWindows; } - Windows validWindows = new Windows(windows); ActivityType actToBeSearched = actToBeScheduled.equals(actType) ? otherActType : actType; final var actSearch = new ActivityExpression.Builder() .ofType(actToBeSearched).build(); diff --git a/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CardinalityGoal.java b/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CardinalityGoal.java index c09edc51d8..162a6f06b4 100644 --- a/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CardinalityGoal.java +++ b/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/goals/CardinalityGoal.java @@ -5,7 +5,9 @@ import gov.nasa.jpl.aerie.constraints.time.Windows; import gov.nasa.jpl.aerie.constraints.tree.Expression; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; -import gov.nasa.jpl.aerie.scheduler.conflicts.UnsatisfiableMissingActivityConflict; +import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; +import gov.nasa.jpl.aerie.scheduler.conflicts.UnsatisfiableGoalConflict; +import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityInstanceId; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityCreationTemplate; @@ -20,8 +22,12 @@ import java.util.Collection; import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Set; /** * describes the desired coexistence of an activity with another @@ -45,6 +51,19 @@ public class CardinalityGoal extends ActivityTemplateGoal { */ protected TimeRangeExpression expr; + // Following members are related to diagnosis of the scheduler being stuck with inserting 0-duration activities + /** + * Activities inserted so far to satisfy this goal + */ + private Set insertedSoFar = new HashSet<>(); + /** + * Current number of steps without inserting an activity with non-zero duration + */ + private int stepsWithoutProgress = 0; + /** + * Maximum acceptable number of steps without progress + */ + private static int maxNoProgressSteps = 50; /** * the builder can construct goals piecemeal via a series of method calls @@ -120,19 +139,30 @@ protected CardinalityGoal fill(CardinalityGoal goal) { if (occurrenceRange != null) { goal.occurrenceRange = occurrenceRange; } + if(isUnsatisfiableDurationType()){ + throw new IllegalArgumentException("Goal is incorrectly parametrized: activity creation template has zero-duration while duration objective is non-zero"); + } if(isUnsatisfiable()){ - throw new IllegalArgumentException("Goal is incorrectly parametrized and therefore unsatisfiable : minimum duration x minimum occurence > maximum duration"); + throw new IllegalArgumentException("Goal is incorrectly parametrized: minimum duration x minimum occurence > maximum duration"); } return goal; } + public boolean isUnsatisfiableDurationType(){ + return (thereExists.getDurationRange() != null && + thereExists.getDurationRange().isSingleton() && + thereExists.getDurationRange().start.isZero() && + durationRange.start.longerThan(Duration.ZERO) + ); + } + public boolean isUnsatisfiable(){ - if(this.durationRange != null && occurrenceRange != null && this.thereExists.getDurationRange() != null){ - if(this.thereExists.getDurationRange().start.times(occurrenceRange.getMinimum()).longerThan(durationRange.end)){ - return true; - } - } - return false; + return this.durationRange != null && + occurrenceRange != null && + this.thereExists.getDurationRange() != null && + this.thereExists.getDurationRange().start + .times(occurrenceRange.getMinimum()) + .longerThan(durationRange.end); } }//Builder @@ -145,7 +175,9 @@ public boolean isUnsatisfiable(){ * but there was no corresponding target activity instance (and one * should probably be created!) */ + @Override public Collection getConflicts(Plan plan, final SimulationResults simulationResults) { + Windows timeDomain = this.expr.computeRange(simulationResults, plan, Windows.forever()); ActivityCreationTemplate actTB = new ActivityCreationTemplate.Builder().basedOn(this.desiredActTemplate).startsOrEndsIn(timeDomain).build(); @@ -171,7 +203,8 @@ public Collection getConflicts(Plan plan, final SimulationResults simu durToSchedule = this.durationRange.start.minus(total); } else if (total.compareTo(this.durationRange.end) > 0) { logger.warn("Need to decrease duration of activities from the plan, impossible because scheduler cannot remove activities"); - return List.of(new UnsatisfiableMissingActivityConflict(this)); + return List.of(new UnsatisfiableGoalConflict(this, + "Need to decrease duration of activities from the plan, impossible because scheduler cannot remove activities")); } } if (this.occurrenceRange != null && !this.occurrenceRange.contains(nbActs)) { @@ -179,14 +212,19 @@ public Collection getConflicts(Plan plan, final SimulationResults simu nbToSchedule = this.occurrenceRange.getMinimum() - nbActs; } else if (nbActs > this.occurrenceRange.getMaximum()) { logger.warn("Need to remove activities from the plan to satify cardinality goal, impossible"); - return List.of(new UnsatisfiableMissingActivityConflict(this)); + return List.of(new UnsatisfiableGoalConflict(this, + "Need to remove activities from the plan to satify cardinality goal, impossible")); } } + if(stuckInsertingZeroDurationActivities(plan, this.occurrenceRange == null || nbToSchedule == 0)) return List.of( + new UnsatisfiableGoalConflict(this, + "During "+this.maxNoProgressSteps+" steps, solver has created only 0-duration activities to satisfy duration cardinality goal, exiting. ")); + final var conflicts = new LinkedList(); //at this point, have thrown exception if not satisfiable //compute the missing association conflicts - for(var act:acts){ + for(final var act : acts){ if(!associatedActivitiesToThisGoal.contains(act) && planEvaluation.canAssociateMoreToCreatorOf(act)){ //they ALL have to be associated conflicts.add(new MissingAssociationConflict(this, List.of(act))); @@ -208,6 +246,25 @@ public Collection getConflicts(Plan plan, final SimulationResults simu return conflicts; } + private boolean stuckInsertingZeroDurationActivities(final Plan plan, final boolean occurrencePartIsSatisfied){ + if(this.durationRange != null && occurrencePartIsSatisfied){ + final var inserted = plan.getEvaluation().forGoal(this).getInsertedActivities(); + final var newlyInsertedActivities = inserted.stream().filter(a -> !insertedSoFar.contains(a.getId())).toList(); + final var durationNewlyInserted = newlyInsertedActivities.stream().reduce(Duration.ZERO, (partialSum, activityInstance2) -> partialSum.plus(activityInstance2.getDuration()), Duration::plus); + if(durationNewlyInserted.isZero()) { + this.stepsWithoutProgress++; + //otherwise, reset it, we have made some progress + } else{ + this.stepsWithoutProgress = 0; + } + if(stepsWithoutProgress > maxNoProgressSteps){ + return true; + } + newlyInsertedActivities.forEach(a -> insertedSoFar.add(a.getId())); + } + return false; + } + /** * /** * ctor creates an empty goal without details diff --git a/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java b/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java index 8de53c16ff..626eeccb80 100644 --- a/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java +++ b/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/simulation/SimulationFacade.java @@ -193,9 +193,12 @@ private gov.nasa.jpl.aerie.constraints.model.ActivityInstance convertToConstrain { final var startT = Duration.of(startTime.until(driverActivity.start(), ChronoUnit.MICROS), MICROSECONDS); final var endT = startT.plus(driverActivity.duration()); + final var activityWindow = startT.isEqualTo(endT) + ? Window.between(startT, endT) + : Window.betweenClosedOpen(startT, endT); return new gov.nasa.jpl.aerie.constraints.model.ActivityInstance( id, driverActivity.type(), driverActivity.arguments(), - Window.betweenClosedOpen(startT, endT)); + activityWindow); } /** diff --git a/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/Evaluation.java b/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/Evaluation.java index 7ad993d42f..9a00f91b5b 100644 --- a/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/Evaluation.java +++ b/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/Evaluation.java @@ -91,11 +91,11 @@ public Optional getNbConflictsDetected() { * @param createdByThisGoal IN a boolean stating whether the instance has been created by this goal or not */ public void associate(java.util.Collection acts, boolean createdByThisGoal) { - acts.forEach((a)->this.acts.put(a, createdByThisGoal)); + acts.forEach(a ->this.acts.put(a, createdByThisGoal)); } public void removeAssociation(java.util.Collection acts){ - acts.forEach((a)->this.acts.remove(a)); + this.acts.entrySet().removeIf(act -> acts.contains(act.getKey())); } /** diff --git a/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java b/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java index 39761a60fd..7f5f55cadd 100644 --- a/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java +++ b/scheduler/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/PrioritySolver.java @@ -422,7 +422,7 @@ private void satisfyGoalGeneral(Goal goal) { assert plan != null; //continue creating activities as long as goal wants more and we can do so - var missingConflicts = getMissingConflicts(goal); + var missingConflicts = getConflicts(goal); //setting the number of conflicts detected at first evaluation, will be used at backtracking evaluation.forGoal(goal).setNbConflictsDetected(missingConflicts.size()); assert missingConflicts != null; @@ -474,11 +474,11 @@ private void satisfyGoalGeneral(Goal goal) { }//for(missing) if (madeProgress) { - missingConflicts = getMissingConflicts(goal); + missingConflicts = getConflicts(goal); } }//while(missingConflicts&&madeProgress) - if(missingConflicts.size() > 0 && !goal.isPartiallySatisfiable()){ + if(!missingConflicts.isEmpty() && !goal.isPartiallySatisfiable()){ rollback(goal); } else{ evaluation.forGoal(goal).setScore(-missingConflicts.size()); @@ -495,28 +495,15 @@ private void satisfyGoalGeneral(Goal goal) { * plan due to the specified goal */ private Collection - getMissingConflicts(Goal goal) + getConflicts(Goal goal) { assert goal != null; assert plan != null; - - //find all the reasons this goal is crying //REVIEW: maybe should have way to request only certain kinds of conflicts this.simulationFacade.computeSimulationResultsUntil(this.problem.getPlanningHorizon().getEndAerie()); final var rawConflicts = goal.getConflicts(plan, this.simulationFacade.getLatestConstraintSimulationResults()); assert rawConflicts != null; - - //filter out any issues that this simple algorithm can't deal with (ie - //anything that doesn't just require throwing more instances in the plan) - final var filteredConflicts = new LinkedList(); - for (final var conflict : rawConflicts) { - assert conflict != null; - //if( conflict instanceof MissingActivityConflict ) { - filteredConflicts.add(conflict); - //} - } - - return filteredConflicts; + return rawConflicts; } /**