Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature activity type value mappers #1294

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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.AutoValueMapper;
import gov.nasa.jpl.aerie.merlin.framework.annotations.Export;
import gov.nasa.jpl.aerie.merlin.protocol.types.Duration;

import java.util.List;
import java.util.Optional;

import static gov.nasa.jpl.aerie.banananation.generated.ActivityActions.call;
import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.delay;

/**
* RussianNestingBanana nests activity types as parameters inside an activity
*
* This activity tests the use of activity types as parameters and within compound type parameters. There are a few use cases:
* 1. Basic activity type parameter which is passed through and called without parent level modeling
* 2. List of activity types which are just iterated through and called
* 3. Using a parent level parameter to override information in the call to a child from an activity type parameter
*
* @subsystem fruit
* @contact John Doe
*/
@ActivityType("RussianNestingBanana")
public final class RussianNestingBanana {

/** Record encapsulating an activity type **/
@AutoValueMapper.Record
public record pickBananaWithId(
int id,
PickBananaActivity pickBananaActivity
) {}

/** Record type parameter encapsulating an activity type **/
@Export.Parameter
public pickBananaWithId pickBananaActivityRecord;

/** Parent level override parameter, in this case to override call to peel banana
* found in the pickBannaActivityRecord parameter **/
@Export.Parameter
public int pickBananaQuantityOverride = 0;

/** List of activity type parameter example **/
@Export.Parameter
public List<BiteBananaActivity> biteBananaActivity;

/** Vanilla activity type parameter **/
@Export.Parameter
public PeelBananaActivity peelBananaActivity;


@ActivityType.EffectModel
public void run(final Mission mission) {
// if the pickBananaQuantityOverride is preset use that integer instead of the pickBananaActivityRecord
if(pickBananaQuantityOverride != 0) {
PickBananaActivity pickBananaActivity = pickBananaActivityRecord.pickBananaActivity;
pickBananaActivity.quantity = pickBananaQuantityOverride;
call(mission, pickBananaActivity);
} else { // else use the record type parameter supplied
call(mission, pickBananaActivityRecord.pickBananaActivity());
}
// call a bite banana for each element in the list of biteBanana activities
for (final var bite : biteBananaActivity) {
call(mission, bite);
delay(Duration.of(30, Duration.MINUTE));
}
// call peel banana activity
call(mission, peelBananaActivity);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
@WithActivityType(ControllableDurationActivity.class)
@WithActivityType(RipenBananaActivity.class)
@WithActivityType(ExceptionActivity.class)

@WithActivityType(RussianNestingBanana.class)
package gov.nasa.jpl.aerie.banananation;

import gov.nasa.jpl.aerie.banananation.activities.BakeBananaBreadActivity;
Expand All @@ -46,6 +46,7 @@
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.RussianNestingBanana;
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
@@ -0,0 +1,81 @@
package gov.nasa.jpl.aerie.banananation.activities;

import gov.nasa.jpl.aerie.banananation.SimulationUtility;
import gov.nasa.jpl.aerie.banananation.generated.activities.RussianNestingBananaMapper;
import gov.nasa.jpl.aerie.merlin.driver.SerializedActivity;
import gov.nasa.jpl.aerie.merlin.framework.junit.MerlinExtension;
import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;

import java.util.List;
import java.util.Map;

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.SECONDS;
import static gov.nasa.jpl.aerie.merlin.protocol.types.Duration.duration;

@ExtendWith(MerlinExtension.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class RussianNestingActivityTest {
private final RussianNestingBananaMapper mapper;

public RussianNestingActivityTest() {
this.mapper = new RussianNestingBananaMapper();
}

@Test
public void testDefaultSimulationDoesNotThrow() {
final var schedule = SimulationUtility.buildSchedule(
Pair.of(
duration(1100, MILLISECONDS),
new SerializedActivity("RussianNestingBanana",
Map.of("pickBananaActivityRecord",
SerializedValue.of(Map.of("id",
SerializedValue.of(2),
"pickBananaActivity",
SerializedValue.of(Map.of("quantity",
SerializedValue.of(10))))),
"pickBananaQuantityOverride", SerializedValue.of(3),
"biteBananaActivity", SerializedValue.of(List.of(SerializedValue.of(Map.of("biteSize",
SerializedValue.of(7.0))),
SerializedValue.of(Map.of("biteSize",
SerializedValue.of(1.0))))),
"peelBananaActivity", SerializedValue.of(Map.of("peelDirection",
SerializedValue.of("fromStem")))))));

final var simulationDuration = duration(5, SECONDS);

var simulationResults = SimulationUtility.simulate(schedule, simulationDuration);

// verify results
// 1. Plant count decreased by 3 (200 -> 197) from the pickBananaActivity call using pickBananaQuantityOverride
var finalPlantCount = simulationResults.discreteProfiles.get("/plant").getValue().get(simulationResults.discreteProfiles.get("/plant").getValue().size()-1).dynamics().asInt().orElseThrow();
assert(finalPlantCount == 197);

// 2. Verify first bite banana activity ran, decrementing "/fruit" by 7 (from 4 to -3) in total and setting flag to flag B
var finalFruitCount = simulationResults.realProfiles.get("/fruit").getValue().get(simulationResults.realProfiles.get("/fruit").getValue().size()-1).dynamics().initial;
var finalFlagState = simulationResults.discreteProfiles.get("/flag").getValue().get(simulationResults.discreteProfiles.get("/flag").getValue().size()-1).dynamics().asString().orElseThrow();

assert(finalFruitCount == 4.0 - 7.0);
assert(finalFlagState == "B");

// 3. Verify second bite banana activity ran, decrementing "/fruit" by 1 (from -3 to -4) in total and setting flag to flag A
simulationResults = SimulationUtility.simulate(schedule, duration(35, MINUTE));
finalFruitCount = simulationResults.realProfiles.get("/fruit").getValue().get(simulationResults.realProfiles.get("/fruit").getValue().size()-1).dynamics().initial;
finalFlagState = simulationResults.discreteProfiles.get("/flag").getValue().get(simulationResults.discreteProfiles.get("/flag").getValue().size()-1).dynamics().asString().orElseThrow();
assert(finalFruitCount == 4.0 - 8.0);
assert(finalFlagState == "A");

simulationResults = SimulationUtility.simulate(schedule, duration(65, MINUTE));
// 4. Verify peel banana activity ran educing frruit by 1.0 and peel by 1.0 to -5.0 and 3.0 respectively
finalFruitCount = simulationResults.realProfiles.get("/fruit").getValue().get(simulationResults.realProfiles.get("/fruit").getValue().size()-1).dynamics().initial;
var finalPeelState = simulationResults.discreteProfiles.get("/peel").getValue().get(simulationResults.discreteProfiles.get("/peel").getValue().size()-1).dynamics().asReal().orElseThrow();
assert(finalFruitCount == 4.0 - 9.0);
assert(finalPeelState == 3.0);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import gov.nasa.jpl.aerie.contrib.serialization.mappers.RecordValueMapper;
import gov.nasa.jpl.aerie.merlin.framework.Result;
import gov.nasa.jpl.aerie.merlin.framework.ValueMapper;
import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType;
import gov.nasa.jpl.aerie.merlin.framework.annotations.AutoValueMapper;
import gov.nasa.jpl.aerie.merlin.processor.metamodel.MissionModelRecord;
import gov.nasa.jpl.aerie.merlin.processor.metamodel.TypeRule;
Expand Down Expand Up @@ -61,6 +62,24 @@ static TypeRule recordTypeRule(final Element autoValueMapperElement, final Class
ClassName.get((TypeElement) autoValueMapperElement).canonicalName().replace(".", "_"));
}

static TypeRule activityTypeRule(final Element activityTypeElement, final ClassName generatedClassName) throws InvalidMissionModelException {
if (!(activityTypeElement.getKind().equals(ElementKind.CLASS) || activityTypeElement.getKind().equals(ElementKind.RECORD))) { //todo: check if activities are constrained to class and record
throw new InvalidMissionModelException(
"@%s is only allowed on classes and records".formatted(
ActivityType.class.getSimpleName()),
activityTypeElement);
}

return new TypeRule(
new TypePattern.ClassPattern(
ClassName.get(ValueMapper.class),
List.of(TypePattern.from(activityTypeElement.asType()))),
Set.of(),
List.of(),
generatedClassName,
ClassName.get((TypeElement) activityTypeElement).canonicalName().replace("activities", "generated_activitiesValueMappers").replace(".", "_") + "ValueMapper");
}

static TypeRule annotationTypeRule(final Element autoValueMapperElement, final ClassName generatedClassName) throws InvalidMissionModelException {
if (!autoValueMapperElement.getKind().equals(ElementKind.ANNOTATION_TYPE)) {
throw new InvalidMissionModelException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
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;
import gov.nasa.jpl.aerie.merlin.processor.metamodel.ActivityValueMapperRecord;
import gov.nasa.jpl.aerie.merlin.processor.metamodel.InputTypeRecord;
import gov.nasa.jpl.aerie.merlin.processor.metamodel.EffectModelRecord;
import gov.nasa.jpl.aerie.merlin.processor.metamodel.ExportDefaultsStyle;
import gov.nasa.jpl.aerie.merlin.processor.metamodel.MapperRecord;
import gov.nasa.jpl.aerie.merlin.processor.metamodel.ActivityMapperRecord;
import gov.nasa.jpl.aerie.merlin.processor.metamodel.MissionModelRecord;
import gov.nasa.jpl.aerie.merlin.processor.metamodel.ParameterRecord;
import gov.nasa.jpl.aerie.merlin.processor.metamodel.ParameterValidationRecord;
Expand Down Expand Up @@ -183,9 +184,10 @@ private Optional<InputTypeRecord> getMissionModelConfigurationType(final Package
final var name = declaration.getSimpleName().toString();
final var parameters = getExportParameters(declaration);
final var validations = this.getExportValidations(declaration, parameters);
final var mapper = getExportMapper(missionModelElement, declaration);
final var activityMapper = getExportActivityMapper(missionModelElement, declaration);
final var valueMapper = getExportValueMapper(missionModelElement, declaration);
final var defaultsStyle = getExportDefaultsStyle(declaration);
return Optional.of(new InputTypeRecord(name, declaration, parameters, validations, mapper, defaultsStyle));
return Optional.of(new InputTypeRecord(name, declaration, parameters, validations, activityMapper, valueMapper, defaultsStyle));
}

private List<TypeElement> getMissionModelMapperClasses(final PackageElement missionModelElement)
Expand Down Expand Up @@ -381,7 +383,8 @@ private ActivityTypeRecord parseActivityType(final PackageElement missionModelEl
{
final var fullyQualifiedClassName = activityTypeElement.getQualifiedName();
final var name = this.getActivityTypeName(activityTypeElement);
final var mapper = this.getExportMapper(missionModelElement, activityTypeElement);
final var activityMapper = getExportActivityMapper(missionModelElement, activityTypeElement);
final var valueMapper = getExportValueMapper(missionModelElement, activityTypeElement);
final var parameters = this.getExportParameters(activityTypeElement);
final var validations = this.getExportValidations(activityTypeElement, parameters);
final var effectModel = this.getActivityEffectModel(activityTypeElement);
Expand All @@ -405,7 +408,7 @@ class (old-style) or as a record (new-style) by determining
return new ActivityTypeRecord(
fullyQualifiedClassName.toString(),
name,
new InputTypeRecord(name, activityTypeElement, parameters, validations, mapper, defaultsStyle),
new InputTypeRecord(name, activityTypeElement, parameters, validations, activityMapper, valueMapper, defaultsStyle),
effectModel);
}

Expand Down Expand Up @@ -471,12 +474,12 @@ private String getActivityTypeName(final TypeElement activityTypeElement)
return (String) nameAttribute.getValue();
}

private MapperRecord getExportMapper(final PackageElement missionModelElement, final TypeElement exportTypeElement)
private ActivityMapperRecord getExportActivityMapper(final PackageElement missionModelElement, final TypeElement exportTypeElement)
throws InvalidMissionModelException
{
final var annotationMirror = this.getAnnotationMirrorByType(exportTypeElement, ActivityType.WithMapper.class);
if (annotationMirror.isEmpty()) {
return MapperRecord.generatedFor(
return ActivityMapperRecord.generatedFor(
ClassName.get(exportTypeElement),
missionModelElement);
}
Expand All @@ -488,7 +491,27 @@ private MapperRecord getExportMapper(final PackageElement missionModelElement, f
annotationMirror.get()))
.getValue();

return MapperRecord.custom(
return ActivityMapperRecord.custom(
ClassName.get((TypeElement) mapperType.asElement()));
}
private ActivityValueMapperRecord getExportValueMapper(final PackageElement missionModelElement, final TypeElement exportTypeElement)
throws InvalidMissionModelException
{
final var annotationMirror = this.getAnnotationMirrorByType(exportTypeElement, ActivityType.WithMapper.class);
if (annotationMirror.isEmpty()) {
return ActivityValueMapperRecord.generatedFor(
ClassName.get(exportTypeElement),
missionModelElement);
}

final var mapperType = (DeclaredType) getAnnotationAttribute(annotationMirror.get(), "value")
.orElseThrow(() -> new InvalidMissionModelException(
"Unable to get value attribute of annotation",
exportTypeElement,
annotationMirror.get()))
.getValue();

return ActivityValueMapperRecord.custom(
ClassName.get((TypeElement) mapperType.asElement()));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package gov.nasa.jpl.aerie.merlin.processor;

import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.TypeName;
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.generator.MissionModelGenerator;
import gov.nasa.jpl.aerie.merlin.processor.metamodel.MissionModelRecord;
Expand All @@ -26,6 +29,7 @@
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -92,9 +96,10 @@ public boolean process(final Set<? extends TypeElement> annotations, final Round
for (final var element : roundEnv.getElementsAnnotatedWith(MissionModel.class)) {
final var recordAutoValueMapperRequests = roundEnv.getElementsAnnotatedWith(AutoValueMapper.Record.class);
final var annotationAutoValueMapperRequests = roundEnv.getElementsAnnotatedWith(AutoValueMapper.Annotation.class);

final var packageElement = (PackageElement) element;
try {
final var missionModelRecord$ = missionModelParser.parseMissionModel(packageElement);
final var missionModelRecord$ = missionModelParser.parseMissionModel(packageElement); //todo: add typerules for activity parameters

final var concatenatedTypeRules = new ArrayList<>(missionModelRecord$.typeRules());
for (final var request : recordAutoValueMapperRequests) {
Expand All @@ -103,6 +108,9 @@ public boolean process(final Set<? extends TypeElement> annotations, final Round
for (final var request : annotationAutoValueMapperRequests) {
concatenatedTypeRules.add(AutoValueMappers.annotationTypeRule(request, missionModelRecord$.getAutoValueMappersName()));
}
for(final var request : this.foundActivityTypes) {
concatenatedTypeRules.add(AutoValueMappers.activityTypeRule(request, missionModelRecord$.getActivityValueMappers()));
}

final var missionModelRecord = new MissionModelRecord(
missionModelRecord$.$package(),
Expand All @@ -113,6 +121,7 @@ public boolean process(final Set<? extends TypeElement> annotations, final Round
missionModelRecord$.activityTypes()
);


final var generatedFiles = new ArrayList<>(List.of(
missionModelGen.generateMerlinPlugin(missionModelRecord),
missionModelGen.generateSchedulerPlugin(missionModelRecord)));
Expand All @@ -134,13 +143,16 @@ public boolean process(final Set<? extends TypeElement> annotations, final Round
annotationAutoValueMapperRequests);
generatedFiles.add(autoValueMappers);


for (final var activityRecord : missionModelRecord.activityTypes()) {
this.ownedActivityTypes.add(activityRecord.inputType().declaration());
if (!activityRecord.inputType().mapper().isCustom) {
if (!activityRecord.inputType().activityMapper().isCustom) {
missionModelGen.generateActivityMapper(missionModelRecord, activityRecord).ifPresent(generatedFiles::add);
}
}

generatedFiles.add(missionModelGen.generateActivityValueMappers(missionModelRecord));

for (final var generatedFile : generatedFiles) {
this.messager.printMessage(
Diagnostic.Kind.NOTE,
Expand Down
Loading