Skip to content

Commit

Permalink
Merge pull request #858 from NASA-AMMOS/831-investigate-fixing-schedu…
Browse files Browse the repository at this point in the history
…ling-running-too-many-simulations

Add @FixedDuration annotation
  • Loading branch information
JoelCourtney authored Apr 13, 2023
2 parents 38a775e + 06ceaa7 commit 26a7d5d
Show file tree
Hide file tree
Showing 14 changed files with 271 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import gov.nasa.jpl.aerie.banananation.Mission;
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.protocol.types.Duration;

import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.*;

/**
* Nap time [banana style]!!!!
Expand All @@ -13,8 +16,11 @@
*/
@ActivityType("BananaNap")
public final class BananaNapActivity {
@ActivityType.FixedDuration
public static final Duration DURATION = Duration.HOUR;

@EffectModel
public void run(final Mission mission) {

delay(DURATION);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package gov.nasa.jpl.aerie.banananation.activities;

import gov.nasa.jpl.aerie.banananation.Mission;
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.protocol.types.Duration;

import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.delay;

/**
* Monke is patient.
*
* Waits two days for bananas to ripen. Ripeness is not modelled.
*
* @subsystem fruit
* @contact Jane Doe
*/
@ActivityType("RipenBanana")
public final class RipenBananaActivity {

@ActivityType.FixedDuration
public static Duration duration() {
return Duration.of(48, Duration.HOUR);
}

@EffectModel
public void run(final Mission mission) {
delay(duration());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
@WithActivityType(BananaNapActivity.class)
@WithActivityType(DurationParameterActivity.class)
@WithActivityType(ControllableDurationActivity.class)
@WithActivityType(RipenBananaActivity.class)

package gov.nasa.jpl.aerie.banananation;

Expand All @@ -37,6 +38,7 @@
import gov.nasa.jpl.aerie.banananation.activities.ParameterTestActivity;
import gov.nasa.jpl.aerie.banananation.activities.PeelBananaActivity;
import gov.nasa.jpl.aerie.banananation.activities.PickBananaActivity;
import gov.nasa.jpl.aerie.banananation.activities.RipenBananaActivity;
import gov.nasa.jpl.aerie.banananation.activities.ThrowBananaActivity;
import gov.nasa.jpl.aerie.contrib.serialization.rulesets.BasicValueMappers;
import gov.nasa.jpl.aerie.merlin.framework.annotations.MissionModel;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ private List<TypeElement> getMissionModelActivityTypes(final PackageElement miss
private ActivityTypeRecord parseActivityType(final PackageElement missionModelElement, final TypeElement activityTypeElement)
throws InvalidMissionModelException
{
final var fullyQualifiedClassName = activityTypeElement.getQualifiedName();
final var name = this.getActivityTypeName(activityTypeElement);
final var mapper = this.getExportMapper(missionModelElement, activityTypeElement);
final var parameters = this.getExportParameters(activityTypeElement);
Expand All @@ -345,6 +346,7 @@ class (old-style) or as a record (new-style) by determining
final var defaultsStyle = this.getExportDefaultsStyle(activityTypeElement);

return new ActivityTypeRecord(
fullyQualifiedClassName.toString(),
name,
new InputTypeRecord(name, activityTypeElement, parameters, validations, mapper, defaultsStyle),
effectModel);
Expand Down Expand Up @@ -484,7 +486,29 @@ private List<ParameterRecord> getExportParameters(final TypeElement exportTypeEl
}

private Optional<EffectModelRecord> getActivityEffectModel(final TypeElement activityTypeElement)
throws InvalidMissionModelException
{
Optional<String> fixedDuration = Optional.empty();
for (final var element: activityTypeElement.getEnclosedElements()) {
if (element.getAnnotation(ActivityType.FixedDuration.class) == null) continue;

if (fixedDuration.isPresent()) throw new InvalidMissionModelException(
"FixedDuration annotation cannot be applied multiple times in one activity type."
);

if (element.getKind() == ElementKind.METHOD) {
if (!(element instanceof ExecutableElement executableElement)) throw new InvalidMissionModelException("FixedDuration method annotation must be an executable element.");

if (!executableElement.getParameters().isEmpty()) throw new InvalidMissionModelException(
"FixedDuration annotation must be applied to a method with no arguments."
);

fixedDuration = Optional.of(executableElement.getSimpleName().toString() + "()");
} else if (element.getKind() == ElementKind.FIELD) {
fixedDuration = Optional.of(element.getSimpleName().toString());
}
}

for (final var element : activityTypeElement.getEnclosedElements()) {
if (element.getKind() != ElementKind.METHOD) continue;

Expand All @@ -495,13 +519,14 @@ private Optional<EffectModelRecord> getActivityEffectModel(final TypeElement act

final var durationTypeAnnotation = element.getAnnotation(ActivityType.ControllableDuration.class);
final var durationParameter = Optional.ofNullable(durationTypeAnnotation).map(ActivityType.ControllableDuration::parameterName);
if (durationParameter.isPresent() && fixedDuration.isPresent()) throw new InvalidMissionModelException("Activity cannot have both FixedDuration and ControllableDuration annotations");

final var returnType = executableElement.getReturnType();
final var nonVoidReturnType = returnType.getKind() == TypeKind.VOID
? Optional.<TypeMirror>empty()
: Optional.of(returnType);

return Optional.of(new EffectModelRecord(element.getSimpleName().toString(), executorAnnotation.value(), nonVoidReturnType, durationParameter));
return Optional.of(new EffectModelRecord(element.getSimpleName().toString(), executorAnnotation.value(), nonVoidReturnType, durationParameter, fixedDuration));
}

return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,11 @@ public JavaFile generateSchedulerModel(final MissionModelRecord missionModel) {
DurationType.class,
activityTypeRecord
.effectModel()
.flatMap(EffectModelRecord::durationParameter)
.map(durationParameter -> CodeBlock.of("controllable(\"$L\")", durationParameter))
.map($ -> {
if ($.durationParameter().isPresent()) return CodeBlock.of("controllable(\"$L\")", $.durationParameter().get());
else if ($.fixedDurationExpr().isPresent()) return CodeBlock.of("fixed($L.$L)", activityTypeRecord.fullyQualifiedClass(), $.fixedDurationExpr().get());
else return CodeBlock.of("uncontrollable()");
})
.orElse(CodeBlock.of("uncontrollable()"))))
.reduce((x, y) -> x.add("$L", y.build()))
.orElse(CodeBlock.builder()).build())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.Optional;

public record ActivityTypeRecord(
String fullyQualifiedClass,
String name,
InputTypeRecord inputType,
Optional<EffectModelRecord> effectModel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public record EffectModelRecord(
String methodName,
ActivityType.Executor executor,
Optional<TypeMirror> returnType,
Optional<String> durationParameter
Optional<String> durationParameter,
Optional<String> fixedDurationExpr
) {
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gov.nasa.jpl.aerie.merlin.framework.annotations;

import gov.nasa.jpl.aerie.merlin.framework.ActivityMapper;
import gov.nasa.jpl.aerie.merlin.protocol.types.Duration;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
Expand All @@ -26,9 +27,77 @@ enum Executor { Threaded, Replaying }
Executor value() default Executor.Threaded;
}

/**
* Apply to the effect model when the activity has a parameter that sets the activity's duration.
*
* Apply like the following:
*
* <pre>{@code
* @ActivityType("ControllableDurationActivity")
* public record ControllableDurationActivity(Duration duration) {
*
* @EffectModel
* @ActivityType.ControllableDuration(parameterName = "duration")
* public void run(Mission mission) {
* delay(duration);
* }
* }
* }</pre>
*
* Keep in mind that it is not enough for the activity duration to be *determined* by the duration parameter.
* They must be exactly equal as above.
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
@interface ControllableDuration {
String parameterName();
}

/**
* Use when an activity has a constant statically-known duration.
*
* Apply to either a static {@link Duration} field or a static no-arg method
* that returns {@link Duration}. For correctness, it is recommended that you use the field or method
* in the effect model to ensure that the duration is what you say it is - the duration is verified
* by the scheduler, but only after a (potentially expensive) simulation.
*
* Apply either like the following on a static field:
*
* <pre>{@code
* @ActivityType("FixedDurationActivity")
* public record FixedDurationActivity() {
* @FixedDuration
* public static final Duration DURATION = Duration.HOUR;
*
* @EffectModel
* public void run(Mission mission) {
* delay(DURATION);
* }
* }
* }</pre>
*
* Or like the following on a static method:
*
* <pre>{@code
* @ActivityType("FixedDurationActivity")
* public record FixedDurationActivity() {
* @FixedDuration
* public static Duration duration() {
* return Duration.HOUR;
* }
*
* @EffectModel
* public void run(Mission mission) {
* delay(duration());
* }
* }
* }</pre>
*
* This annotation is optional, but it is highly recommended if applicable. The scheduler will assume
* your activity has an uncontrollable variable duration if no duration annotation is present, which
* will cause extra simulations in scheduling.
*/
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.FIELD, ElementType.METHOD })
@interface FixedDuration {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
public sealed interface DurationType {
record Controllable(String parameterName) implements DurationType {}
record Uncontrollable() implements DurationType {}
record Fixed(Duration duration) implements DurationType {}

static DurationType uncontrollable() {
return new Uncontrollable();
Expand All @@ -11,4 +12,8 @@ static DurationType uncontrollable() {
static DurationType controllable(final String parameterName) {
return new Controllable(parameterName);
}

static DurationType fixed(final Duration duration) {
return new Fixed(duration);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package gov.nasa.jpl.aerie.scheduler.constraints.activities;

import gov.nasa.jpl.aerie.constraints.model.DiscreteProfile;
import gov.nasa.jpl.aerie.constraints.model.EvaluationEnvironment;
import gov.nasa.jpl.aerie.constraints.model.Profile;
import gov.nasa.jpl.aerie.constraints.time.Interval;
Expand All @@ -11,7 +10,6 @@
import gov.nasa.jpl.aerie.constraints.tree.Expression;
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.EquationSolvingAlgorithms;
import gov.nasa.jpl.aerie.scheduler.NotNull;
import gov.nasa.jpl.aerie.scheduler.model.SchedulingActivityDirective;
Expand Down Expand Up @@ -341,14 +339,17 @@ public Duration valueAt(final Duration start) {
type);

final var durationParameterName = dt.parameterName();
//handle variable duration parameter here
//handle variable duration parameter here
final Duration setActivityDuration;
if (instantiatedArguments.containsKey(durationParameterName)) {
final var argumentDuration = Duration.of(instantiatedArguments.get(durationParameterName).asInt().get(), Duration.MICROSECOND);
final var argumentDuration = Duration.of(
instantiatedArguments.get(durationParameterName).asInt().get(),
Duration.MICROSECOND);
if (solved.duration().contains(argumentDuration)) {
setActivityDuration = argumentDuration;
} else{
logger.debug("Controllable duration set by user is incompatible with temporal constraints associated to the activity template");
} else {
logger.debug(
"Controllable duration set by user is incompatible with temporal constraints associated to the activity template");
return Optional.empty();
}
} else {
Expand All @@ -369,8 +370,26 @@ public Duration valueAt(final Duration start) {
null,
null,
true));
} else{
throw new UnsupportedOperationException("Duration type other than Uncontrollable and Controllable are not suppoerted");
} else if (this.type.getDurationType() instanceof DurationType.Fixed dt) {
//select earliest start time, STN guarantees satisfiability
final var earliestStart = solved.start().start;

// TODO: When scheduling is allowed to create activities with anchors, this constructor should pull from an expanded creation template
return Optional.of(SchedulingActivityDirective.of(
type,
earliestStart,
dt.duration(),
SchedulingActivityDirective.instantiateArguments(
this.arguments,
earliestStart,
facade.getLatestConstraintSimulationResults(),
evaluationEnvironment,
type),
null,
null,
true));
} else {
throw new UnsupportedOperationException("Duration types other than Uncontrollable, Controllable, and Fixed are not supported: " + this.type.getDurationType());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.MICROSECONDS;

Expand Down Expand Up @@ -233,7 +232,7 @@ private ActivityDirective schedulingActToActivityDir(SchedulingActivityDirective
final var durationType = activity.getType().getDurationType();
if (durationType instanceof DurationType.Controllable dt) {
arguments.put(dt.parameterName(), SerializedValue.of(activity.duration().in(Duration.MICROSECONDS)));
} else if (durationType instanceof DurationType.Uncontrollable) {
} else if (durationType instanceof DurationType.Uncontrollable || durationType instanceof DurationType.Fixed) {
// If an activity has already been simulated, it will have a duration, even if its DurationType is Uncontrollable.
} else {
throw new Error("Unhandled variant of DurationType: " + durationType);
Expand Down
Loading

0 comments on commit 26a7d5d

Please sign in to comment.