Skip to content

Commit

Permalink
Merge pull request #908 from NASA-AMMOS/904-parameterizedduration-ann…
Browse files Browse the repository at this point in the history
…otation

ParametricDuration annotation
  • Loading branch information
JoelCourtney authored May 10, 2023
2 parents 8c36661 + c3ebfe6 commit 8da423c
Show file tree
Hide file tree
Showing 12 changed files with 328 additions and 76 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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.framework.annotations.ActivityType.ParametricDuration;
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.framework.ModelActions.delay;

/**
* Internet technology has come a long way.
*
* @subsystem fruit
* @contact Jane Doe
*/
@ActivityType("DownloadBanana")
public final class DownloadBananaActivity {

public enum ConnectionType {
DSL,
FiberOptic,
DietaryFiberOptic
}

@Parameter
public ConnectionType connection = ConnectionType.DSL;

@ParametricDuration
public Duration duration() {
return switch (this.connection) {
case DSL -> Duration.HOUR;
case FiberOptic -> Duration.of(10, Duration.MINUTE);
case DietaryFiberOptic -> Duration.MINUTE;
};
}

@EffectModel
public void run(final Mission mission) {
delay(duration());
mission.fruit.add(1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
@WithActivityType(DecomposingActivity.GrandchildActivity.class)
@WithActivityType(DecomposingSpawnActivity.DecomposingSpawnParentActivity.class)
@WithActivityType(DecomposingSpawnActivity.DecomposingSpawnChildActivity.class)
@WithActivityType(DownloadBananaActivity.class)
@WithActivityType(BakeBananaBreadActivity.class)
@WithActivityType(BananaNapActivity.class)
@WithActivityType(DurationParameterActivity.class)
Expand All @@ -32,6 +33,7 @@
import gov.nasa.jpl.aerie.banananation.activities.ControllableDurationActivity;
import gov.nasa.jpl.aerie.banananation.activities.DecomposingActivity;
import gov.nasa.jpl.aerie.banananation.activities.DecomposingSpawnActivity;
import gov.nasa.jpl.aerie.banananation.activities.DownloadBananaActivity;
import gov.nasa.jpl.aerie.banananation.activities.DurationParameterActivity;
import gov.nasa.jpl.aerie.banananation.activities.GrowBananaActivity;
import gov.nasa.jpl.aerie.banananation.activities.LineCountBananaActivity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.squareup.javapoet.ClassName;
import gov.nasa.jpl.aerie.merlin.framework.Registrar;
import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType;
import gov.nasa.jpl.aerie.merlin.framework.annotations.AutoValueMapper;
import gov.nasa.jpl.aerie.merlin.framework.annotations.Export;
import gov.nasa.jpl.aerie.merlin.framework.annotations.MissionModel;
import gov.nasa.jpl.aerie.merlin.processor.metamodel.ActivityTypeRecord;
Expand Down Expand Up @@ -472,23 +471,38 @@ private Optional<EffectModelRecord> getActivityEffectModel(final TypeElement act
throws InvalidMissionModelException
{
Optional<String> fixedDuration = Optional.empty();
Optional<String> parameterizedDuration = Optional.empty();
for (final var element: activityTypeElement.getEnclosedElements()) {
if (element.getAnnotation(ActivityType.FixedDuration.class) == null) continue;
if (element.getAnnotation(ActivityType.FixedDuration.class) != null) {
if (fixedDuration.isPresent()) throw new InvalidMissionModelException(
"FixedDuration annotation cannot be applied multiple times in one activity type."
);

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."
);

if (element.getKind() == ElementKind.METHOD) {
if (!(element instanceof ExecutableElement executableElement)) throw new InvalidMissionModelException("FixedDuration method annotation must be an executable element.");
fixedDuration = Optional.of(executableElement.getSimpleName().toString() + "()");
} else if (element.getKind() == ElementKind.FIELD) {
fixedDuration = Optional.of(element.getSimpleName().toString());
}
} else if (element.getAnnotation(ActivityType.ParametricDuration.class) != null) {
if (parameterizedDuration.isPresent()) throw new InvalidMissionModelException(
"ParametricDuration annotation cannot be applied multiple times in one activity type."
);

if (!(element instanceof ExecutableElement executableElement)) throw new InvalidMissionModelException(
"ParametricDuration 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."
"ParametricDuration method 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());
parameterizedDuration = Optional.of(executableElement.getSimpleName().toString());
}
}

Expand All @@ -503,13 +517,21 @@ 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");
if (
(durationParameter.isPresent() ? 1 : 0)
+ (fixedDuration.isPresent() ? 1 : 0)
+ (parameterizedDuration.isPresent() ? 1 : 0)
> 1
) {
throw new InvalidMissionModelException("Only one duration annotation can be applied to an activity type at a time.");
}

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, fixedDuration));
return Optional.of(new EffectModelRecord(element.getSimpleName().toString(), executorAnnotation.value(), nonVoidReturnType, durationParameter, fixedDuration, parameterizedDuration));
}

return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import com.squareup.javapoet.WildcardTypeName;
import gov.nasa.jpl.aerie.contrib.serialization.mappers.RecordValueMapper;
import gov.nasa.jpl.aerie.merlin.framework.ActivityMapper;
import gov.nasa.jpl.aerie.merlin.framework.EmptyInputType;
import gov.nasa.jpl.aerie.merlin.framework.ModelActions;
Expand All @@ -24,7 +23,6 @@
import gov.nasa.jpl.aerie.merlin.processor.metamodel.EffectModelRecord;
import gov.nasa.jpl.aerie.merlin.processor.metamodel.InputTypeRecord;
import gov.nasa.jpl.aerie.merlin.processor.metamodel.MissionModelRecord;
import gov.nasa.jpl.aerie.merlin.processor.metamodel.TypeRule;
import gov.nasa.jpl.aerie.merlin.protocol.driver.Initializer;
import gov.nasa.jpl.aerie.merlin.protocol.driver.Topic;
import gov.nasa.jpl.aerie.merlin.protocol.model.InputType;
Expand All @@ -40,24 +38,18 @@
import gov.nasa.jpl.aerie.merlin.protocol.types.TaskStatus;
import gov.nasa.jpl.aerie.merlin.protocol.types.Unit;
import gov.nasa.jpl.aerie.merlin.protocol.types.ValueSchema;
import org.apache.commons.lang3.tuple.Pair;

import javax.annotation.processing.Generated;
import javax.annotation.processing.Messager;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.RecordComponentElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -301,6 +293,7 @@ public JavaFile generateSchedulerModel(final MissionModelRecord missionModel) {
.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 if ($.parametricDuration().isPresent()) return CodeBlock.of("parametric($$ -> (new $L().new InputMapper()).instantiate($$).$L())", activityTypeRecord.inputType().mapper().name, $.parametricDuration().get());
else return CodeBlock.of("uncontrollable()");
})
.orElse(CodeBlock.of("uncontrollable()"))))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public record EffectModelRecord(
ActivityType.Executor executor,
Optional<TypeMirror> returnType,
Optional<String> durationParameter,
Optional<String> fixedDurationExpr
Optional<String> fixedDurationExpr,
Optional<String> parametricDuration
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ enum Executor { Threaded, Replaying }
* }</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.
* They must be exactly equal as above. If that is not true, use {@link ParametricDuration} instead.
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
Expand Down Expand Up @@ -100,4 +100,38 @@ enum Executor { Threaded, Replaying }
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.FIELD, ElementType.METHOD })
@interface FixedDuration {}

/**
* Use when an activity's duration is indirectly determined only by its arguments.
*
* Apply to a getter method that returns this activity's duration. For correctness, it is recommended
* that you use the getter in the effect model to ensure the duration is what you say it is. Apply like this:
*
* <pre>{@code
* @ActivityType("ParametricDurationActivity")
* public record ParametricDurationActivity(boolean goFast) {
* @ParametricDuration
* public Duration duration() {
* if (goFast) {
* return Duration.MINUTE;
* } else {
* return Duration.HOUR;
* }
* }
*
* @EffectModel
* public void run(Mission mission) {
* // ...
* delay(duration);
* // ...
* }
* }
* }</pre>
*
* If the duration of the activity is exactly equal to one of the arguments, it is recommended to use the
* {@link ControllableDuration} annotation instead.
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
@interface ParametricDuration {}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package gov.nasa.jpl.aerie.merlin.protocol.types;

import java.util.Map;

public sealed interface DurationType {
record Controllable(String parameterName) implements DurationType {}
record Uncontrollable() implements DurationType {}
record Fixed(Duration duration) implements DurationType {}
record Parametric(ThrowingDurationFunction durationFunction) implements DurationType {}

static DurationType uncontrollable() {
return new Uncontrollable();
Expand All @@ -16,4 +19,12 @@ static DurationType controllable(final String parameterName) {
static DurationType fixed(final Duration duration) {
return new Fixed(duration);
}

static DurationType parametric(final ThrowingDurationFunction durationFunction) {
return new Parametric(durationFunction);
}

interface ThrowingDurationFunction {
Duration apply(final Map<String, SerializedValue> arguments) throws InstantiationException;
}
}
Loading

0 comments on commit 8da423c

Please sign in to comment.