diff --git a/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/activities/PeelBananaActivity.java b/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/activities/PeelBananaActivity.java index 4436e29bb0..709f5b76a5 100644 --- a/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/activities/PeelBananaActivity.java +++ b/examples/banananation/src/main/java/gov/nasa/jpl/aerie/banananation/activities/PeelBananaActivity.java @@ -5,6 +5,9 @@ import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType; import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType.EffectModel; import gov.nasa.jpl.aerie.merlin.framework.annotations.Export.Parameter; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.HOURS; /** * Peel a banana, in preparation for consumption. @@ -29,6 +32,9 @@ public enum PeelDirectionEnum { @Unit("direction") public PeelDirectionEnum peelDirection = PeelDirectionEnum.fromStem; + @ActivityType.MaximumDuration + public static final Duration DURATION_UPPER_BOUND = Duration.of(1, HOURS); + @EffectModel public void run(final Mission mission) { if (peelDirection.equals(PeelDirectionEnum.fromStem)) { diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/MissionModelParser.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/MissionModelParser.java index 06742b0d1b..22d5ca72d6 100644 --- a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/MissionModelParser.java +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/MissionModelParser.java @@ -550,6 +550,7 @@ private Optional getActivityEffectModel(final TypeElement act { Optional fixedDuration = Optional.empty(); Optional parameterizedDuration = Optional.empty(); + Optional maximumDuration = Optional.empty(); for (final var element: activityTypeElement.getEnclosedElements()) { if (element.getAnnotation(ActivityType.FixedDuration.class) != null) { if (fixedDuration.isPresent()) throw new InvalidMissionModelException( @@ -581,6 +582,22 @@ private Optional getActivityEffectModel(final TypeElement act ); parameterizedDuration = Optional.of(executableElement.getSimpleName().toString()); + } else if (element.getAnnotation(ActivityType.MaximumDuration.class) != null){ + if (maximumDuration.isPresent()) throw new InvalidMissionModelException( + "MaximumDuration annotation cannot be applied multiple times in one activity type." + ); + + if (element.getKind() == ElementKind.METHOD) { + if (!(element instanceof ExecutableElement executableElement)) throw new InvalidMissionModelException( + "MaximumDuration method annotation must be an executable element."); + + if (!executableElement.getParameters().isEmpty()) throw new InvalidMissionModelException( + "MaximumDuration annotation must be applied to a method with no arguments." + ); + maximumDuration = Optional.of(executableElement.getSimpleName().toString() + "()"); + } else if (element.getKind() == ElementKind.FIELD) { + maximumDuration = Optional.of(element.getSimpleName().toString()); + } } } @@ -609,7 +626,7 @@ private Optional getActivityEffectModel(final TypeElement act ? Optional.empty() : Optional.of(returnType); - return Optional.of(new EffectModelRecord(element.getSimpleName().toString(), executorAnnotation.value(), nonVoidReturnType, durationParameter, fixedDuration, parameterizedDuration)); + return Optional.of(new EffectModelRecord(element.getSimpleName().toString(), executorAnnotation.value(), nonVoidReturnType, durationParameter, fixedDuration, parameterizedDuration, maximumDuration)); } return Optional.empty(); diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java index a09bffff68..e8760293db 100644 --- a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/generator/MissionModelGenerator.java @@ -301,6 +301,32 @@ public JavaFile generateSchedulerModel(final MissionModelRecord missionModel) { .orElse(CodeBlock.builder()).build()) .addStatement("return result") .build()) + .addMethod(MethodSpec + .methodBuilder("getMaximumDurations") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(ParameterizedTypeName.get(Map.class, String.class, Duration.class)) + .addStatement("final var result = new $T()", ParameterizedTypeName.get(HashMap.class, String.class, Duration.class)) + .addCode( + missionModel + .activityTypes() + .stream() + .filter(a -> a.effectModel().isPresent()) + .filter(a -> a.effectModel().get().maximumDuration().isPresent()) + .map( + activityTypeRecord -> + CodeBlock + .builder() + .addStatement("result.put(\"$L\", $L)", + activityTypeRecord.name(), + activityTypeRecord + .effectModel() + .map($ -> CodeBlock.of("$L.$L", activityTypeRecord.fullyQualifiedClass(), $.maximumDuration().get())) + .get())) + .reduce((x, y) -> x.add("$L", y.build())) + .orElse(CodeBlock.builder()).build()) + .addStatement("return result") + .build()) .addMethod(MethodSpec .methodBuilder("serializeDuration") .addModifiers(Modifier.PUBLIC) diff --git a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/EffectModelRecord.java b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/EffectModelRecord.java index d4151ab9a3..20ca56bffa 100644 --- a/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/EffectModelRecord.java +++ b/merlin-framework-processor/src/main/java/gov/nasa/jpl/aerie/merlin/processor/metamodel/EffectModelRecord.java @@ -11,6 +11,7 @@ public record EffectModelRecord( Optional returnType, Optional durationParameter, Optional fixedDurationExpr, - Optional parametricDuration + Optional parametricDuration, + Optional maximumDuration ) { } diff --git a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/annotations/ActivityType.java b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/annotations/ActivityType.java index cc5139d1d5..61f89bec99 100644 --- a/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/annotations/ActivityType.java +++ b/merlin-framework/src/main/java/gov/nasa/jpl/aerie/merlin/framework/annotations/ActivityType.java @@ -134,4 +134,57 @@ enum Executor { Threaded, Replaying } @Retention(RetentionPolicy.CLASS) @Target(ElementType.METHOD) @interface ParametricDuration {} + + /** + * Use when the duration of an activity can be upper-bounded, + * Apply to either a static {@link Duration} field or a static no-arg method + * that returns {@link Duration}. + * + * This annotation can be used when the activity has an uncontrollable or parametric duration (see @ParametricDuration). + * + * Apply either like the following on a static field: + *
{@code
+   * @ActivityType("Activity")
+   * public record Activity() {
+   *   @MaximumDuration
+   *   public static final Duration MAXIMUM_DURATION = Duration.HOUR;
+   *
+   *   @EffectModel
+   *   public void run(Mission mission) {
+   *     if(mission.resourceA.equals(true){
+   *       delay(MAXIMUM_DURATION)
+   *     } else{
+   *       delay(Duration.MINUTE);
+   *     }
+   *   }
+   * }
+   * }
+ * + * Or like the following on a static method: + * + *
{@code
+   * @ActivityType("Activity")
+   * public record Activity() {
+   *   @MaximumDuration
+   *   public static Duration maximumDuration() {
+   *     return Duration.HOUR;
+   *   }
+   *
+   *   @EffectModel
+   *   public void run(Mission mission) {
+   *     if(mission.resourceA.equals(true){
+   *       delay(maximumDuration())
+   *     } else{
+   *       delay(Duration.MINUTE);
+   *     }
+   *   }
+   * }
+   * }
+ * + * This annotation is optional, but it is highly recommended if applicable. This annotation allows to perform less simulations. + * + */ + @Retention(RetentionPolicy.CLASS) + @Target({ ElementType.FIELD, ElementType.METHOD }) + @interface MaximumDuration {} } diff --git a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/SchedulerModel.java b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/SchedulerModel.java index f846973011..639b7a2c5c 100644 --- a/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/SchedulerModel.java +++ b/merlin-sdk/src/main/java/gov/nasa/jpl/aerie/merlin/protocol/model/SchedulerModel.java @@ -10,4 +10,5 @@ public interface SchedulerModel { Map getDurationTypes(); SerializedValue serializeDuration(final Duration duration); Duration deserializeDuration(final SerializedValue serializedValue); + Map getMaximumDurations(); } diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityExpression.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityExpression.java index 119f0bc58e..c299f9b78e 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityExpression.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/constraints/activities/ActivityExpression.java @@ -13,21 +13,30 @@ import gov.nasa.jpl.aerie.constraints.tree.DurationLiteral; import gov.nasa.jpl.aerie.constraints.tree.Expression; import gov.nasa.jpl.aerie.constraints.tree.ProfileExpression; +import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; 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.model.PlanningHorizon; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; import gov.nasa.jpl.aerie.scheduler.model.ActivityType; import gov.nasa.jpl.aerie.scheduler.NotNull; import gov.nasa.jpl.aerie.scheduler.Nullable; +import gov.nasa.jpl.aerie.scheduler.solver.stn.TaskNetworkAdapter; +import kotlin.DeepRecursiveFunction; import org.apache.commons.lang3.tuple.Pair; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.regex.Pattern; + +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; /** * the criteria used to identify activity instances in scheduling goals @@ -58,7 +67,7 @@ public record ActivityExpression( Interval endRange, Pair>, Expression>> durationRange, ActivityType type, - java.util.regex.Pattern nameRe, + Pattern nameRe, Map> arguments ) implements Expression { @@ -86,7 +95,7 @@ public static class Builder { protected @Nullable Interval startsIn; protected @Nullable Interval endsIn; protected @Nullable Pair>, Expression>> durationIn; - protected java.util.regex.Pattern nameRe; + protected Pattern nameRe; public Builder withArgument(String argument, SerializedValue val) { arguments.put(argument, new ProfileExpression<>(new DiscreteValue(val))); @@ -298,7 +307,7 @@ public boolean matches( } public boolean matches( - final @NotNull gov.nasa.jpl.aerie.constraints.model.ActivityInstance act, + final @NotNull ActivityInstance act, final SimulationResults simulationResults, final EvaluationEnvironment evaluationEnvironment, final boolean matchArgumentsExactly) { @@ -318,11 +327,11 @@ public boolean matches( final var dur = act.interval.duration(); final Optional durRequirementLower = this.durationRange.getLeft() .evaluate(simulationResults, evaluationEnvironment) - .valueAt(Duration.ZERO) + .valueAt(ZERO) .flatMap($ -> $.asInt().map(i -> Duration.of(i, Duration.MICROSECOND))); final Optional durRequirementUpper = this.durationRange.getRight() .evaluate(simulationResults, evaluationEnvironment) - .valueAt(Duration.ZERO) + .valueAt(ZERO) .flatMap($ -> $.asInt().map(i -> Duration.of(i, Duration.MICROSECOND))); if(durRequirementLower.isEmpty() && durRequirementUpper.isEmpty()){ throw new RuntimeException("ActivityExpression is malformed, duration bounds are absent but the range is not null"); @@ -378,6 +387,66 @@ public String prettyPrint(final String prefix) { @Override public void extractResources(final Set names) { } + public Interval instantiateDurationInterval( + final PlanningHorizon planningHorizon, + final EvaluationEnvironment evaluationEnvironment + ){ + if(durationRange == null) return null; + Optional durRequirementLower = Optional.empty(); + Optional durRequirementUpper = Optional.empty(); + try { + durRequirementLower = durationRange().getLeft() + .evaluate(null, planningHorizon.getHor(), evaluationEnvironment) + .valueAt(ZERO) + .flatMap($ -> $.asInt().map(i -> Duration.of(i, Duration.MICROSECOND))); + durRequirementUpper = durationRange().getRight() + .evaluate(null, planningHorizon.getHor(), evaluationEnvironment) + .valueAt(ZERO) + .flatMap($ -> $.asInt().map(i -> Duration.of(i, Duration.MICROSECOND))); + } catch (NullPointerException e) { + throw new UnsupportedOperationException("Activity creation duration arguments cannot depend on simulation results.", e); + } + if(durRequirementLower.isPresent() && durRequirementUpper.isPresent()) { + return Interval.between(durRequirementLower.get(), durRequirementUpper.get()); + } + return null; + } + + public Optional reduceTemporalConstraints( + final PlanningHorizon planningHorizon, + final SchedulerModel schedulerModel, + final EvaluationEnvironment evaluationEnvironment, + final List enveloppes){ + + var maximumDuration = Duration.MAX_VALUE; + final var activityTypeMaximumDuration = schedulerModel.getMaximumDurations().get(this.type().getName()); + if(activityTypeMaximumDuration != null){ + maximumDuration = Duration.min(maximumDuration, activityTypeMaximumDuration); + } + + final var durationType = schedulerModel.getDurationTypes().get(this.type.getName()); + if(durationType instanceof DurationType.Fixed fixed){ + maximumDuration = Duration.min(maximumDuration, fixed.duration()); + } + + var instantiateDurationInterval = this.instantiateDurationInterval(planningHorizon, evaluationEnvironment); + var minimumDuration = ZERO; + if(instantiateDurationInterval != null){ + minimumDuration = Duration.max(minimumDuration, instantiateDurationInterval.start); + maximumDuration = Duration.min(maximumDuration, instantiateDurationInterval.end); + } + + final var durationInterval = Interval.between(minimumDuration, maximumDuration); + + final var allEnveloppes = new ArrayList(enveloppes); + allEnveloppes.add(planningHorizon.getHor()); + return TaskNetworkAdapter.reduceActivityTemporalConstraints( + startRange(), + endRange(), + durationInterval, + allEnveloppes); + } + /** * Evaluates whether a SerializedValue can be qualified as the subset of another SerializedValue or not * @param superset the proposed superset 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 cc50a40435..3cdba1bce4 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 @@ -4,6 +4,7 @@ import gov.nasa.jpl.aerie.constraints.model.SimulationResults; import gov.nasa.jpl.aerie.constraints.time.Interval; import gov.nasa.jpl.aerie.constraints.time.Windows; +import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.scheduler.conflicts.UnsatisfiableGoalConflict; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; @@ -123,7 +124,11 @@ protected CardinalityGoal fill(CardinalityGoal goal) { * should probably be created!) */ @Override - public Collection getConflicts(Plan plan, final SimulationResults simulationResults, final EvaluationEnvironment evaluationEnvironment) { + public Collection getConflicts( + final Plan plan, + final SimulationResults simulationResults, + final EvaluationEnvironment evaluationEnvironment, + final SchedulerModel schedulerModel) { //unwrap temporalContext final var windows = getTemporalContext().evaluate(simulationResults, evaluationEnvironment); 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 5740b9ed93..98c9ce146d 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 @@ -5,7 +5,10 @@ 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.Expression; +import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.scheduler.conflicts.Conflict; import gov.nasa.jpl.aerie.scheduler.conflicts.MissingActivityTemplateConflict; import gov.nasa.jpl.aerie.scheduler.conflicts.MissingAssociationConflict; @@ -15,11 +18,15 @@ import gov.nasa.jpl.aerie.scheduler.constraints.timeexpressions.TimeExpression; import gov.nasa.jpl.aerie.scheduler.model.Plan; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; +import gov.nasa.jpl.aerie.scheduler.solver.stn.TaskNetworkAdapter; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Optional; +import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.ZERO; + /** * describes the desired coexistence of an activity with another */ @@ -175,7 +182,11 @@ protected CoexistenceGoal fill(CoexistenceGoal goal) { */ @SuppressWarnings({"unchecked", "rawtypes"}) @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? + public java.util.Collection getConflicts( + final Plan plan, + final SimulationResults simulationResults, + final EvaluationEnvironment evaluationEnvironment, + final SchedulerModel schedulerModel) { //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 @@ -266,11 +277,19 @@ else if (this.initiallyEvaluatedTemporalContext == null) { if (!alreadyOneActivityAssociated) { //create conflict if no matching target activity found if (existingActs.isEmpty()) { + var temporalContext = this.temporalContext.evaluate(simulationResults, evaluationEnvironment); + final var activityCreationTemplateBuilt = activityCreationTemplate.build(); + final var newEvaluationEnvironment = createEvaluationEnvironmentFromAnchor(evaluationEnvironment, window); + final var newTemporalContext = boundTemporalContextWithSchedulerModel( + temporalContext, + schedulerModel, + activityCreationTemplateBuilt, + newEvaluationEnvironment); conflicts.add(new MissingActivityTemplateConflict( this, - this.temporalContext.evaluate(simulationResults, evaluationEnvironment), - activityCreationTemplate.build(), - createEvaluationEnvironmentFromAnchor(evaluationEnvironment, window), + newTemporalContext, + activityCreationTemplateBuilt, + newEvaluationEnvironment, 1, Optional.empty())); } else { @@ -283,6 +302,22 @@ else if (this.initiallyEvaluatedTemporalContext == null) { return conflicts; } + private Windows boundTemporalContextWithSchedulerModel( + final Windows baseTemporalContext, + final SchedulerModel schedulerModel, + final ActivityExpression activityTemplate, + final EvaluationEnvironment evaluationEnvironment){ + var boundedTemporalContext = new Windows().add(baseTemporalContext); + var currentTemporalContextUpperBound = baseTemporalContext.maxTrueTimePoint().get().getKey(); + final var reduced = activityTemplate.reduceTemporalConstraints(planHorizon, schedulerModel, evaluationEnvironment, List.of()); + if(reduced.isPresent()) { + currentTemporalContextUpperBound = Duration.min(currentTemporalContextUpperBound, reduced.get().end().end); + //invalidate the end + boundedTemporalContext = boundedTemporalContext.and(new Windows(true).set(Interval.between(currentTemporalContextUpperBound, Interval.Inclusivity.Exclusive, Duration.MAX_VALUE, Interval.Inclusivity.Exclusive), false)); + } + return boundedTemporalContext; + } + private EvaluationEnvironment createEvaluationEnvironmentFromAnchor(EvaluationEnvironment existingEnvironment, Segment> span){ if(span.value().isPresent()){ final var metadata = span.value().get(); 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 1dc294ad46..2fd21d76ef 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 @@ -7,6 +7,7 @@ import gov.nasa.jpl.aerie.constraints.tree.And; import gov.nasa.jpl.aerie.constraints.tree.Expression; import gov.nasa.jpl.aerie.constraints.tree.WindowsWrapperExpression; +import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.scheduler.conflicts.Conflict; import gov.nasa.jpl.aerie.scheduler.model.Plan; @@ -287,8 +288,9 @@ public String getName() { public java.util.Collection getConflicts( Plan plan, final SimulationResults simulationResults, - final EvaluationEnvironment evaluationEnvironment - ) { + final EvaluationEnvironment evaluationEnvironment, + final SchedulerModel schedulerModel + ) { 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 948ae518db..fdfc792a27 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 @@ -2,6 +2,7 @@ import gov.nasa.jpl.aerie.constraints.model.EvaluationEnvironment; import gov.nasa.jpl.aerie.constraints.model.SimulationResults; +import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; import gov.nasa.jpl.aerie.scheduler.conflicts.Conflict; import gov.nasa.jpl.aerie.scheduler.model.Plan; import gov.nasa.jpl.aerie.scheduler.solver.optimizers.Optimizer; @@ -29,9 +30,11 @@ public Optimizer getOptimizer(){ } @Override - public java.util.Collection getConflicts(Plan plan, - final SimulationResults simulationResults, - final EvaluationEnvironment evaluationEnvironment) { + public java.util.Collection getConflicts( + final Plan plan, + final SimulationResults simulationResults, + final EvaluationEnvironment evaluationEnvironment, + final SchedulerModel schedulerModel) { 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 d5a88765b9..d92cdd9ea4 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 @@ -3,6 +3,7 @@ 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.merlin.protocol.model.SchedulerModel; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; import gov.nasa.jpl.aerie.scheduler.conflicts.Conflict; @@ -109,7 +110,11 @@ protected ProceduralCreationGoal fill(ProceduralCreationGoal goal) { * arguments must be identical. */ @Override - public Collection getConflicts(Plan plan, final SimulationResults simulationResults, final EvaluationEnvironment evaluationEnvironment) { + public Collection getConflicts( + final Plan plan, + final SimulationResults simulationResults, + final EvaluationEnvironment evaluationEnvironment, + final SchedulerModel schedulerModel) { final var conflicts = new java.util.LinkedList(); //run the generator to see what acts are still desired 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 4e3c6c5749..5130000230 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 @@ -4,6 +4,7 @@ import gov.nasa.jpl.aerie.constraints.model.SimulationResults; import gov.nasa.jpl.aerie.constraints.time.Interval; import gov.nasa.jpl.aerie.constraints.time.Windows; +import gov.nasa.jpl.aerie.merlin.protocol.model.SchedulerModel; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import gov.nasa.jpl.aerie.merlin.protocol.types.DurationType; import gov.nasa.jpl.aerie.scheduler.constraints.activities.ActivityExpression; @@ -109,7 +110,11 @@ else if (every.max.isNegative()) { * probably be created!) */ @Override - public java.util.Collection getConflicts(@NotNull Plan plan, final SimulationResults simulationResults, final EvaluationEnvironment evaluationEnvironment) { + public java.util.Collection getConflicts( + @NotNull final Plan plan, + final SimulationResults simulationResults, + final EvaluationEnvironment evaluationEnvironment, + final SchedulerModel schedulerModel) { final var conflicts = new java.util.LinkedList(); //unwrap temporalContext @@ -219,9 +224,6 @@ private java.util.Collection makeRecurrenceConflicts(Du if(windows.iterateEqualTo(true).iterator().hasNext()){ conflicts.add(new MissingActivityTemplateConflict(this, windows, this.getActTemplate(), evaluationEnvironment, 1, Optional.empty())); } - else{ - System.out.println(); - } if(intervalT.compareTo(end) >= 0){ break; } 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 9fe3b6f2d4..1d3fe7d34a 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 @@ -30,7 +30,6 @@ import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective; import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirectiveId; import gov.nasa.jpl.aerie.scheduler.simulation.SimulationFacade; -import gov.nasa.jpl.aerie.scheduler.solver.stn.TaskNetwork; import gov.nasa.jpl.aerie.scheduler.solver.stn.TaskNetworkAdapter; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; @@ -706,7 +705,7 @@ private Collection getConflicts(Goal goal) throws SchedulingInterrupte final var lastSimulationResults = this.getLatestSimResultsUpTo(this.problem.getPlanningHorizon().getEndAerie()); synchronizeSimulationWithSchedulerPlan(); final var evaluationEnvironment = new EvaluationEnvironment(this.problem.getRealExternalProfiles(), this.problem.getDiscreteExternalProfiles()); - final var rawConflicts = goal.getConflicts(plan, lastSimulationResults, evaluationEnvironment); + final var rawConflicts = goal.getConflicts(plan, lastSimulationResults, evaluationEnvironment, this.problem.getSchedulerModel()); assert rawConflicts != null; return rawConflicts; } @@ -947,52 +946,16 @@ private Optional instantiateActivity( final EvaluationEnvironment evaluationEnvironment ) throws SchedulingInterruptedException { final var planningHorizon = this.problem.getPlanningHorizon(); - final var taskNetwork = new TaskNetworkAdapter(new TaskNetwork()); - taskNetwork.addAct(name); - if (interval != null) { - taskNetwork.addEnveloppe(name, "interval", interval.start, interval.end); - } - taskNetwork.addEnveloppe(name, "planningHorizon", planningHorizon.getStartAerie(), planningHorizon.getEndAerie()); - if (activityExpression.startRange() != null) { - taskNetwork.addStartInterval(name, activityExpression.startRange().start, activityExpression.startRange().end); - } - if (activityExpression.endRange() != null) { - taskNetwork.addEndInterval(name, activityExpression.endRange().start, activityExpression.endRange().end); - } - Optional durRequirementLower = Optional.empty(); - Optional durRequirementUpper = Optional.empty();; - if (activityExpression.durationRange() != null) { - try { - durRequirementLower = activityExpression.durationRange().getLeft() - .evaluate(null, planningHorizon.getHor(), evaluationEnvironment) - .valueAt(Duration.ZERO) - .flatMap($ -> $.asInt().map(i -> Duration.of(i, Duration.MICROSECOND))); - durRequirementUpper = activityExpression.durationRange().getRight() - .evaluate(null, planningHorizon.getHor(), evaluationEnvironment) - .valueAt(Duration.ZERO) - .flatMap($ -> $.asInt().map(i -> Duration.of(i, Duration.MICROSECOND))); - - } catch (NullPointerException e) { - throw new UnsupportedOperationException("Activity creation duration arguments cannot depend on simulation results.", e); - } - if(durRequirementLower.isPresent() && durRequirementUpper.isPresent()) { - taskNetwork.addDurationInterval(name, durRequirementLower.get(), durRequirementUpper.get()); - } - } - final var success = taskNetwork.solveConstraints(); - if (!success) { - logger.debug("Inconsistent static temporal constraints, cannot place activity in interval"); - logger.debug("Start range " + activityExpression.startRange()); - logger.debug("End range " + activityExpression.endRange()); - logger.debug( - "Duration range [" + - (durRequirementLower.isPresent() ? durRequirementLower.get() : "-inf") + - ", " + - (durRequirementUpper.isPresent() ? durRequirementUpper.get() : "+inf")); - logger.debug("Interval range " + interval); - return Optional.empty(); - } - final var solved = taskNetwork.getAllData(name); + final var envelopes = new ArrayList(); + if(interval != null) envelopes.add(interval); + final var reduced = activityExpression.reduceTemporalConstraints( + planningHorizon, + this.problem.getSchedulerModel(), + evaluationEnvironment, + envelopes); + + if(reduced.isEmpty()) return Optional.empty(); + final var solved = reduced.get(); //the domain of user/scheduling temporal constraints have been reduced with the STN, //now it is time to find an assignment compatible diff --git a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/stn/TaskNetworkAdapter.java b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/stn/TaskNetworkAdapter.java index 65e41fa243..c867c31427 100644 --- a/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/stn/TaskNetworkAdapter.java +++ b/scheduler-driver/src/main/java/gov/nasa/jpl/aerie/scheduler/solver/stn/TaskNetworkAdapter.java @@ -3,11 +3,17 @@ import gov.nasa.jpl.aerie.constraints.time.Interval; import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Optional; /** * Adapter for TaskNetwork for use with Interval and Duration */ public class TaskNetworkAdapter { + private static final Logger logger = LoggerFactory.getLogger(TaskNetworkAdapter.class); private final TaskNetwork tw; @@ -89,26 +95,37 @@ private Interval toWin(Pair pair){ return toWin(pair.getLeft(), pair.getRight()); } - public static TNActData solve(Interval start, Interval end, Interval dur, Interval enveloppe){ - TaskNetwork tw = new TaskNetwork(); - String actName = "ACT"; - TaskNetworkAdapter tnw = new TaskNetworkAdapter(tw); - if(start!=null){ - tnw.addStartInterval(actName, start.start, start.end); + public static Optional reduceActivityTemporalConstraints( + final Interval startInterval, + final Interval endInterval, + final Interval durationInterval, + final Collection envelopes){ + final TaskNetwork tw = new TaskNetwork(); + final String actName = "ACT"; + final TaskNetworkAdapter tnw = new TaskNetworkAdapter(tw); + tnw.addAct(actName); + if(startInterval != null){ + tnw.addStartInterval(actName, startInterval.start, startInterval.end); } - if(end!=null){ - tnw.addStartInterval(actName, end.start, end.end); + if(endInterval != null){ + tnw.addEndInterval(actName, endInterval.start, endInterval.end); } - if(dur!=null){ - tnw.addDurationInterval(actName, dur.start, dur.end); + if(durationInterval != null){ + tnw.addDurationInterval(actName, durationInterval.start, durationInterval.end); } - if(enveloppe!=null){ - tnw.addEnveloppe(actName, "ENV", enveloppe.start, enveloppe.end); + var i = 0; + for(final var enveloppe: envelopes){ + tnw.addEnveloppe(actName, "ENV"+(i++), enveloppe.start, enveloppe.end); } if(tnw.solveConstraints()){ - return tnw.getAllData(actName); + return Optional.of(tnw.getAllData(actName)); } else{ - return null; + logger.debug("Inconsistent static temporal constraints, cannot place activity in interval"); + logger.debug("Start range " + startInterval); + logger.debug("End range " + endInterval); + logger.debug("Duration range " + durationInterval); + logger.debug("Envelopes: " + envelopes); + return Optional.empty(); } } 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 e78247d804..0a1ce17620 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 @@ -1519,6 +1519,37 @@ export default (): Goal => { assertEquals(Duration.of(4, HOURS), growBanana.startOffset()); } + private static List onePickEveryTenMinutes(final Interval interval){ + final var plan = new ArrayList(); + for(var cur = interval.start; cur.shorterThan(interval.end); cur = cur.plus(Duration.of(10, MINUTES))){ + plan.add(new ActivityDirective( + cur, + "PickBanana", + Map.of("quantity", SerializedValue.of(100)), + null, + true)); + } + return plan; + } + + @Test + void testBigCoexistence(){ + final var growBananaDuration = Duration.of(1, Duration.HOUR); + final var results = runScheduler( + BANANANATION, + onePickEveryTenMinutes(PLANNING_HORIZON.getHor()), + List.of(new SchedulingGoal(new GoalId(0L, 0L), """ + export default (): Goal => { + return Goal.CoexistenceGoal({ + activityTemplate: ActivityTemplates.PeelBanana({peelDirection: "fromStem"}), + forEach: ActivityExpression.ofType(ActivityTypes.PickBanana), + startsAt: TimingConstraint.singleton(WindowProperty.START) + }) + }""", true)), + PLANNING_HORIZON); + assertEquals(1152, results.updatedPlan().size()); + } + @Test void testNotEqualTo_satisfied() { // Initial plant count is 200 in default configuration