Skip to content

Commit

Permalink
Add @MaximumDuration annotation and use it in scheduling
Browse files Browse the repository at this point in the history
  • Loading branch information
adrienmaillard committed Apr 3, 2024
1 parent 08d482f commit a9bf4cf
Show file tree
Hide file tree
Showing 16 changed files with 320 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,7 @@ private Optional<EffectModelRecord> getActivityEffectModel(final TypeElement act
{
Optional<String> fixedDuration = Optional.empty();
Optional<String> parameterizedDuration = Optional.empty();
Optional<String> maximumDuration = Optional.empty();
for (final var element: activityTypeElement.getEnclosedElements()) {
if (element.getAnnotation(ActivityType.FixedDuration.class) != null) {
if (fixedDuration.isPresent()) throw new InvalidMissionModelException(
Expand Down Expand Up @@ -581,6 +582,22 @@ private Optional<EffectModelRecord> 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());
}
}
}

Expand Down Expand Up @@ -609,7 +626,7 @@ private Optional<EffectModelRecord> getActivityEffectModel(final TypeElement act
? Optional.<TypeMirror>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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public record EffectModelRecord(
Optional<TypeMirror> returnType,
Optional<String> durationParameter,
Optional<String> fixedDurationExpr,
Optional<String> parametricDuration
Optional<String> parametricDuration,
Optional<String> maximumDuration
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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:
* <pre>{@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);
* }
* }
* }
* }</pre>
*
* Or like the following on a static method:
*
* <pre>{@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);
* }
* }
* }
* }</pre>
*
* 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 {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ public interface SchedulerModel {
Map<String, DurationType> getDurationTypes();
SerializedValue serializeDuration(final Duration duration);
Duration deserializeDuration(final SerializedValue serializedValue);
Map<String, Duration> getMaximumDurations();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -58,7 +67,7 @@ public record ActivityExpression(
Interval endRange,
Pair<Expression<? extends Profile<?>>, Expression<? extends Profile<?>>> durationRange,
ActivityType type,
java.util.regex.Pattern nameRe,
Pattern nameRe,
Map<String, ProfileExpression<?>> arguments
) implements Expression<Spans> {

Expand Down Expand Up @@ -86,7 +95,7 @@ public static class Builder {
protected @Nullable Interval startsIn;
protected @Nullable Interval endsIn;
protected @Nullable Pair<Expression<? extends Profile<?>>, Expression<? extends Profile<?>>> 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)));
Expand Down Expand Up @@ -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) {
Expand All @@ -318,11 +327,11 @@ public boolean matches(
final var dur = act.interval.duration();
final Optional<Duration> durRequirementLower = this.durationRange.getLeft()
.evaluate(simulationResults, evaluationEnvironment)
.valueAt(Duration.ZERO)
.valueAt(ZERO)
.flatMap($ -> $.asInt().map(i -> Duration.of(i, Duration.MICROSECOND)));
final Optional<Duration> 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");
Expand Down Expand Up @@ -378,6 +387,66 @@ public String prettyPrint(final String prefix) {
@Override
public void extractResources(final Set<String> names) { }

public Interval instantiateDurationInterval(
final PlanningHorizon planningHorizon,
final EvaluationEnvironment evaluationEnvironment
){
if(durationRange == null) return null;
Optional<Duration> durRequirementLower = Optional.empty();
Optional<Duration> 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<TaskNetworkAdapter.TNActData> reduceTemporalConstraints(
final PlanningHorizon planningHorizon,
final SchedulerModel schedulerModel,
final EvaluationEnvironment evaluationEnvironment,
final List<Interval> 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<Interval>(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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -123,7 +124,11 @@ protected CardinalityGoal fill(CardinalityGoal goal) {
* should probably be created!)
*/
@Override
public Collection<Conflict> getConflicts(Plan plan, final SimulationResults simulationResults, final EvaluationEnvironment evaluationEnvironment) {
public Collection<Conflict> getConflicts(
final Plan plan,
final SimulationResults simulationResults,
final EvaluationEnvironment evaluationEnvironment,
final SchedulerModel schedulerModel) {

//unwrap temporalContext
final var windows = getTemporalContext().evaluate(simulationResults, evaluationEnvironment);
Expand Down
Loading

0 comments on commit a9bf4cf

Please sign in to comment.