From fd6e586b1cad9ef5e64c59e8b7756fb4520f2ccd Mon Sep 17 00:00:00 2001 From: maillard Date: Wed, 20 Sep 2023 16:09:48 -0700 Subject: [PATCH] Add justAfter and justBefore timing constraints in scheduling edsl --- .../src/libs/scheduler-edsl-fluent-api.ts | 33 +++++ .../services/SchedulingIntegrationTests.java | 125 +++++++++++++++++- 2 files changed, 157 insertions(+), 1 deletion(-) diff --git a/scheduler-worker/scheduling-dsl-compiler/src/libs/scheduler-edsl-fluent-api.ts b/scheduler-worker/scheduling-dsl-compiler/src/libs/scheduler-edsl-fluent-api.ts index 8df710466c..0726a6fbca 100644 --- a/scheduler-worker/scheduling-dsl-compiler/src/libs/scheduler-edsl-fluent-api.ts +++ b/scheduler-worker/scheduling-dsl-compiler/src/libs/scheduler-edsl-fluent-api.ts @@ -592,6 +592,9 @@ export class ActivityExpression { } export class TimingConstraint { + + public static defaultPadding: Temporal.Duration = Temporal.Duration.from({microseconds:1}); + /** @internal **/ private constructor() {} /** @@ -633,6 +636,23 @@ export class TimingConstraint { singleton: false }) } + + /** + * Represents a precise time point at a defined offset (default: 1 microseconds, can be modified) from the start or end of a window. + * Equivalent to TimingConstraint.singleton(windowProperty).plus(TimingConstraint.defaultPadding) + * @param windowProperty either WindowProperty.START or WindowProperty.END + */ + public static justAfter(windowProperty: WindowProperty): SingletonTimingConstraint { + return this.singleton(windowProperty).plus(this.defaultPadding); + } + /** + * Represents a precise time point at a defined offset (default: -1 microseconds, can be modified) from the start or end of a window. + * Equivalent to TimingConstraint.singleton(windowProperty).minus(TimingConstraint.defaultPadding) + * @param windowProperty either WindowProperty.START or WindowProperty.END + */ + public static justBefore(windowProperty: WindowProperty): SingletonTimingConstraint { + return this.singleton(windowProperty).minus(this.defaultPadding); + } } /** @@ -837,6 +857,7 @@ declare global { matchingArgs: WindowsEDSL.Gen.ActivityTypeParameterMapWithUndefined[T]): ActivityExpression } class TimingConstraint { + public static defaultPadding: Temporal.Duration; /** * The singleton timing constraint represents a precise time point * at some offset from either the start or end of a window. @@ -862,6 +883,18 @@ declare global { * @param upperBound represents the (inclusive) upper bound of the time interval */ public static bounds(lowerBound: SingletonTimingConstraint, upperBound: SingletonTimingConstraint): FlexibleRangeTimingConstraint + /** + * Represents a precise time point at a defined offset (default: 1 microseconds, can be modified) from the start or end of a window. + * Equivalent to TimingConstraint.singleton(windowProperty).plus(TimingConstraint.defaultPadding) + * @param windowProperty either WindowProperty.START or WindowProperty.END + */ + public static justAfter(windowProperty: WindowProperty): SingletonTimingConstraint + /** + * Represents a precise time point at a defined offset (default: -1 microseconds, can be modified) from the start or end of a window. + * Equivalent to TimingConstraint.singleton(windowProperty).minus(TimingConstraint.defaultPadding) + * @param windowProperty either WindowProperty.START or WindowProperty.END + */ + public static justBefore(windowProperty: WindowProperty): SingletonTimingConstraint } var WindowProperty: typeof AST.WindowProperty var Operator: typeof AST.TimingConstraintOperator 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 825388ea1f..7ca1804d89 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 @@ -19,6 +19,8 @@ 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.MICROSECONDS; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MILLISECONDS; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTE; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MINUTES; import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.SECOND; @@ -2744,7 +2746,128 @@ export default () => Goal.CoexistenceGoal({ assertEquals(SerializedValue.of("fromStem"), peelBanana.serializedActivity().getArguments().get("peelDirection")); } - /** + @Test + void testJustAfter() { + /* + Start with a plan with B anchored to A + Goal: for each B, place a C + And make sure that C ends up in the right place + */ + final var activityDuration = Duration.of(1, Duration.HOUR); + final var tenMinutes = Duration.of(10, MINUTES); + final var results = runScheduler( + BANANANATION, + Map.of( + new ActivityDirectiveId(2L), + new ActivityDirective( + tenMinutes, + "GrowBanana", + Map.of( + "quantity", SerializedValue.of(1), + "growingDuration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))), + new ActivityDirectiveId(1L), + true)), + List.of(new SchedulingGoal(new GoalId(0L), """ + export default function(){ + TimingConstraint.defaultPadding = Temporal.Duration.from({milliseconds:1}) + return Goal.CoexistenceGoal({ + forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), + activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), + startsAt: TimingConstraint.justAfter(WindowProperty.START) + }) + } + """, true)), + PLANNING_HORIZON); + + assertEquals(1, results.scheduleResults.goalResults().size()); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + + assertTrue(goalResult.satisfied()); + assertEquals(1, goalResult.createdActivities().size()); + for (final var activity : goalResult.createdActivities()) { + assertNotNull(activity); + } + for (final var activity : goalResult.satisfyingActivities()) { + assertNotNull(activity); + } + + final var planByActivityType = partitionByActivityType(results.updatedPlan()); + final var peelBananas = planByActivityType.get("PeelBanana"); + final var growBananas = planByActivityType.get("GrowBanana"); + + assertEquals(1, peelBananas.size()); + assertEquals(1, growBananas.size()); + final var peelBanana = peelBananas.iterator().next(); + final var growBanana = growBananas.iterator().next(); + + + assertEquals(tenMinutes, growBanana.startOffset()); + assertEquals(SerializedValue.of(1), growBanana.serializedActivity().getArguments().get("quantity")); + + assertEquals(Duration.of(10, Duration.MINUTES).plus(Duration.of(1, MILLISECONDS)), peelBanana.startOffset()); + assertEquals(SerializedValue.of("fromStem"), peelBanana.serializedActivity().getArguments().get("peelDirection")); + } + + @Test + void testJustBefore() { + /* + Start with a plan with B anchored to A + Goal: for each B, place a C + And make sure that C ends up in the right place + */ + final var activityDuration = Duration.of(1, Duration.HOUR); + final var tenMinutes = Duration.of(10, MINUTES); + final var results = runScheduler( + BANANANATION, + Map.of( + new ActivityDirectiveId(2L), + new ActivityDirective( + tenMinutes, + "GrowBanana", + Map.of( + "quantity", SerializedValue.of(1), + "growingDuration", SerializedValue.of(activityDuration.in(Duration.MICROSECONDS))), + new ActivityDirectiveId(1L), + true)), + List.of(new SchedulingGoal(new GoalId(0L), """ + export default () => Goal.CoexistenceGoal({ + forEach: ActivityExpression.ofType(ActivityTypes.GrowBanana), + activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), + startsAt: TimingConstraint.justBefore(WindowProperty.START) + }) + """, true)), + PLANNING_HORIZON); + + assertEquals(1, results.scheduleResults.goalResults().size()); + final var goalResult = results.scheduleResults.goalResults().get(new GoalId(0L)); + + assertTrue(goalResult.satisfied()); + assertEquals(1, goalResult.createdActivities().size()); + for (final var activity : goalResult.createdActivities()) { + assertNotNull(activity); + } + for (final var activity : goalResult.satisfyingActivities()) { + assertNotNull(activity); + } + + final var planByActivityType = partitionByActivityType(results.updatedPlan()); + final var peelBananas = planByActivityType.get("PeelBanana"); + final var growBananas = planByActivityType.get("GrowBanana"); + + assertEquals(1, peelBananas.size()); + assertEquals(1, growBananas.size()); + final var peelBanana = peelBananas.iterator().next(); + final var growBanana = growBananas.iterator().next(); + + + assertEquals(tenMinutes, growBanana.startOffset()); + assertEquals(SerializedValue.of(1), growBanana.serializedActivity().getArguments().get("quantity")); + + assertEquals(Duration.of(10, Duration.MINUTES).minus(Duration.of(1, MICROSECONDS)), peelBanana.startOffset()); + assertEquals(SerializedValue.of("fromStem"), peelBanana.serializedActivity().getArguments().get("peelDirection")); + } + + /** * Test that the scheduler can correctly place activities off of activities anchored to start after the start * of another activity. */