diff --git a/README.md b/README.md index f70e1f5..37c7df8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Aerie Template -This repo provides an Aerie mission model template, which is meant as a starting point for building a new mission model in Aerie. -Included in this repo is all the basic infrastructure required to generate a mission model `.jar` file. +This repo provides a basic template for getting started with mission modeling and scheduling within the Aerie framework. + +Included in this repo is all the basic infrastructure and scaffolding required to generate a mission model `.jar` file, as well as scheduling procedure `.jar`s that can be uploaded and run within Aerie. #### Interested in learning how to develop a model yourself? Check out the [Aerie Mission Modeling Tutorial](https://nasa-ammos.github.io/aerie-docs/tutorials/mission-modeling/introduction/) @@ -12,7 +13,6 @@ Try out the following models: - [Tutorial Model](https://github.com/NASA-AMMOS/aerie-modeling-tutorial) - Simple Data Model (coming soon) - ## Prerequisites - Install [OpenJDK Temurin LTS](https://adoptium.net/temurin/releases/?version=21). If you're on macOS, you can install [brew](https://brew.sh/) instead and then use the following command to install JDK 21: @@ -35,6 +35,7 @@ Try out the following models: ## Building +### Mission Model To build a mission model JAR you can do: ```sh @@ -45,6 +46,43 @@ This will create the file `'missionmodel/build/libs/missionmodel.jar`, which you +### Scheduling Procedures +To build scheduling procedures, first you will need a completed mission model. You can accomplish this by following the [Aerie Mission Modeling Tutorial](https://nasa-ammos.github.io/aerie-docs/tutorials/mission-modeling/introduction/), or by using the included `complete-model-tutorial.patch`: + +```sh +git apply complete-model-tutorial.patch +``` + +This is required since scheduling procedures will reference activity types (e.g. when placing activity directives), and the model in this repo, out of the box, has no registered activities. + +Then, copy an example scheduling procedure into the procedures folder. + +```sh +cp scheduling/examples/SampleProcedure.java scheduling/src/main/java/scheduling/procedures +``` + +(For a more involved example procedure, take a look at some [procedures in the Aerie repo](https://github.com/NASA-AMMOS/aerie/blob/develop/procedural/examples/foo-procedures/src/main/java/gov/nasa/ammos/aerie/procedural/examples/fooprocedures/procedures/StayWellFed.java)) + +The following will be your process every time you iterate on these procedures + +```sh +./gradlew scheduling:compileJava +./gradlew scheduling:buildAllSchedulingProcedureJars +``` + +The first `gradle` command will expand `@SchedulingProcedure` annotations into new, verbose source code files that Aerie can process down the line. +The second `gradle` command then looks for those generated files, creates a task to build a `.jar` for each file, and then runs all those tasks. + +Your procedure jars will then be in `scheduling/build/libs/OriginalSourceCodeFileName.jar`, which in this case will be `scheduling/build/libs/SampleProcedure.jar`. + +## Running Procedures + +Now that you have `.jar`'s, we need to upload them to Aerie so you can run them against plans. + +The quickest way to upload a single JAR is to use the `aerie-ui`. On the `/scheduling/goals/new` page, you should now see a new tab option for creating a `jar` procedural goal. Once created, you will need to register the goal with a specific plan, just like you do with EDSL goals. + +Then, from the manage goals page on your plan, you can pass arguments to your procedure using the drop down menu, and run your procedures using the schedule button. You can also right click to manage invocations (duplicate, delete, etc) + ## Testing To run unit tests under [./missionmodel/src/test](./missionmodel/src/test) against your mission model you can do: diff --git a/complete-model-tutorial.patch b/complete-model-tutorial.patch new file mode 100644 index 0000000..a81c9a2 --- /dev/null +++ b/complete-model-tutorial.patch @@ -0,0 +1,140 @@ +diff --git a/missionmodel/src/main/java/missionmodel/CollectData.java b/missionmodel/src/main/java/missionmodel/CollectData.java +new file mode 100644 +index 0000000..4e61911 +--- /dev/null ++++ b/missionmodel/src/main/java/missionmodel/CollectData.java +@@ -0,0 +1,45 @@ ++package missionmodel; ++ ++import gov.nasa.jpl.aerie.contrib.metadata.Unit; ++import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteEffects; ++import gov.nasa.jpl.aerie.merlin.framework.annotations.ActivityType; ++import gov.nasa.jpl.aerie.merlin.framework.annotations.Export.Parameter; ++import gov.nasa.jpl.aerie.merlin.framework.annotations.Export.Validation; ++ ++ ++import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; ++ ++import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.delay; ++ ++/* Example Activity Type Definition ++ If this activity is moved over to main/java/missionmodel along with the DataModel class and the example model ++ declaration in the Mission class is uncommented, the model should compile. ++ */ ++@ActivityType("CollectData") ++public class CollectData { ++ ++ @Parameter ++ @Unit("Mbps") ++ public double rate = 10.0; // Mbps ++ ++ @Parameter ++ public Duration duration = Duration.duration(1, Duration.HOURS); ++ ++ @Validation("Collection rate is beyond buffer limit of 100.0 Mbps") ++ @Validation.Subject("rate") ++ public boolean validateCollectionRate() { ++ return rate <= 100.0; ++ } ++ ++ @ActivityType.EffectModel ++ public void run(Mission model) { ++ ++ /* ++ Collect data at fixed rate over duration of activity ++ */ ++ DiscreteEffects.increase(model.dataModel.RecordingRate, this.rate); ++ delay(duration); ++ DiscreteEffects.decrease(model.dataModel.RecordingRate, this.rate); ++ ++ } ++} +diff --git a/missionmodel/src/main/java/missionmodel/DataModel.java b/missionmodel/src/main/java/missionmodel/DataModel.java +new file mode 100644 +index 0000000..b0df0d4 +--- /dev/null ++++ b/missionmodel/src/main/java/missionmodel/DataModel.java +@@ -0,0 +1,42 @@ ++package missionmodel; ++ ++import gov.nasa.jpl.aerie.contrib.serialization.mappers.DoubleValueMapper; ++import gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource; ++import gov.nasa.jpl.aerie.contrib.streamline.modeling.Registrar; ++import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete; ++import gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.DiscreteEffects; ++import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; ++ ++import static gov.nasa.jpl.aerie.contrib.metadata.UnitRegistrar.withUnit; ++import static gov.nasa.jpl.aerie.contrib.streamline.core.MutableResource.resource; ++import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentValue; ++import static gov.nasa.jpl.aerie.contrib.streamline.modeling.discrete.Discrete.discrete; ++import static gov.nasa.jpl.aerie.merlin.framework.ModelActions.delay; ++ ++/* Example Mission Model delegate class ++ This class includes two resource declarations and a method that can be spawned via a daemon task ++ If this activity is moved over to main/java/missionmodel and the example model declaration in the Mission class ++ is uncommented, the model should compile. ++ */ ++public class DataModel { ++ ++ public MutableResource> RecordingRate; // Megabits/s ++ ++ public MutableResource> SSR_Volume_Sampled; // Gigabits ++ ++ public DataModel(Registrar registrar, Configuration config) { ++ RecordingRate = resource(discrete(0.0)); ++ registrar.discrete("RecordingRate", RecordingRate, withUnit("Mbps", new DoubleValueMapper())); ++ } ++ ++ public void integrateDataRate() { ++ Duration INTEGRATION_SAMPLE_INTERVAL = Duration.duration(60, Duration.SECONDS); ++ while(true) { ++ delay(INTEGRATION_SAMPLE_INTERVAL); ++ Double currentRecordingRate = currentValue(RecordingRate); ++ DiscreteEffects.increase(SSR_Volume_Sampled, currentRecordingRate * ++ INTEGRATION_SAMPLE_INTERVAL.ratioOver(Duration.SECONDS) / 1000.0); // Mbit -> Gbit ++ } ++ } ++ ++} +diff --git a/missionmodel/src/main/java/missionmodel/Mission.java b/missionmodel/src/main/java/missionmodel/Mission.java +index d7eb103..e2b3da5 100644 +--- a/missionmodel/src/main/java/missionmodel/Mission.java ++++ b/missionmodel/src/main/java/missionmodel/Mission.java +@@ -25,7 +25,7 @@ public final class Mission { + // public MutableResource> ExampleResource; + + // Example model declaration +- //public final DataModel dataModel; ++ public final DataModel dataModel; + + public Mission(final gov.nasa.jpl.aerie.merlin.framework.Registrar registrar, final Configuration config) { + this.errorRegistrar = new Registrar(registrar, Registrar.ErrorBehavior.Log); +@@ -35,10 +35,10 @@ public final class Mission { + // errorRegistrar.discrete("ExampleResource", ExampleResource, new DoubleValueMapper()); + + // Example model instantiation +- //this.dataModel = new DataModel(this.errorRegistrar, config); ++ this.dataModel = new DataModel(this.errorRegistrar, config); + + // Example daemon task call +- // spawn(dataModel::integrateDataRate); ++ spawn(dataModel::integrateDataRate); + + } + } +diff --git a/missionmodel/src/main/java/missionmodel/package-info.java b/missionmodel/src/main/java/missionmodel/package-info.java +index 314ae30..f0d67da 100644 +--- a/missionmodel/src/main/java/missionmodel/package-info.java ++++ b/missionmodel/src/main/java/missionmodel/package-info.java +@@ -1,8 +1,8 @@ + @MissionModel(model = Mission.class) + @WithMappers(BasicValueMappers.class) + @WithConfiguration(Configuration.class) +-// @WithActivityType(ActivityType.class) // for new activity type +-// @WithMetadata(name = "unit", annotation = gov.nasa.jpl.aerie.contrib.metadata.Unit.class) // for unit support ++@WithActivityType(CollectData.class) // for new activity type ++@WithMetadata(name = "unit", annotation = gov.nasa.jpl.aerie.contrib.metadata.Unit.class) // for unit support + package missionmodel; + + import gov.nasa.jpl.aerie.contrib.serialization.rulesets.BasicValueMappers; diff --git a/missionmodel/src/main/java/missionmodel/Utils.java b/missionmodel/src/main/java/missionmodel/Utils.java new file mode 100644 index 0000000..9d73fd9 --- /dev/null +++ b/missionmodel/src/main/java/missionmodel/Utils.java @@ -0,0 +1,7 @@ +package missionmodel; + +public class Utils { + public static String getCollectDataActivityName() { + return "CollectData"; + } +} diff --git a/scheduling/build.gradle b/scheduling/build.gradle new file mode 100644 index 0000000..f0f9c4b --- /dev/null +++ b/scheduling/build.gradle @@ -0,0 +1,100 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + id 'java' + id 'io.github.goooler.shadow' version '8.1.7' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +dependencies { + // pull in any helper / utils from the mission model + implementation project(":missionmodel") + + // procedural scheduling libraries + annotationProcessor "gov.nasa.ammos.aerie.procedural:processor:" + project.aerieVersion + implementation "gov.nasa.ammos.aerie.procedural:scheduling:" + project.aerieVersion + implementation "gov.nasa.ammos.aerie.procedural:timeline:" + project.aerieVersion + implementation "gov.nasa.ammos.aerie.procedural:constraints:" + project.aerieVersion + + // standard aerie deps + implementation 'gov.nasa.jpl.aerie:merlin-framework:' + project.aerieVersion + implementation 'gov.nasa.jpl.aerie:contrib:' + project.aerieVersion + implementation 'gov.nasa.jpl.aerie:type-utils:' + project.aerieVersion + + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0' +} + +test { + useJUnitPlatform() +} + +tasks.register('buildAllSchedulingProcedureJars') { + group = 'SchedulingProcedureJars' + + dependsOn "generateSchedulingProcedureJarTasks" + dependsOn { + tasks.findAll { task -> task.name.startsWith('buildSchedulingProcedureJar_') } + } +} + +tasks.create("generateSchedulingProcedureJarTasks") { + group = 'SchedulingProcedureJars' + + final proceduresDir = findFirstMatchingBuildDir("generated/procedures") + + if (proceduresDir == null) { + println "No generated procedures found yet, make sure you have procedure source files in src/.../procedures" + return + } + println "Generating jar tasks for the following procedures directory: ${proceduresDir}" + + final files = file(proceduresDir).listFiles() + if (files.length == 0) { + println "No procedures available within folder ${proceduresDir}" + return + } + + files.toList().each { file -> + final nameWithoutExtension = file.name.replace(".java", "") + final taskName = "buildSchedulingProcedureJar_${nameWithoutExtension}" + + println "Generating ${taskName} task, which will build ${nameWithoutExtension}.jar" + + tasks.create(taskName, ShadowJar) { + group = 'SchedulingProcedureJars' + configurations = [project.configurations.compileClasspath] + from sourceSets.main.output + archiveBaseName = "" // clear + archiveClassifier.set(nameWithoutExtension) // set output jar name + manifest { + attributes 'Main-Class': getMainClassFromGeneratedFile(file) + } + minimize() + dependencies { + // exclude project(':procedural:timeline') + // exclude dependency(":kotlin.*") + } + } + } +} + +private String findFirstMatchingBuildDir(String pattern) { + String found = null + final generatedDir = file("build/generated/sources") + generatedDir.mkdirs() + generatedDir.eachDirRecurse { dir -> if (dir.path.contains(pattern)) found = dir.path } + return found +} + +private static String getMainClassFromGeneratedFile(File file) { + final fileString = file.toString() + final prefix = "build/generated/sources/annotationProcessor/java/main/" + final index = fileString.indexOf(prefix) + prefix.length() + final trimmed = fileString.substring(index).replace(".java", "") + return trimmed.replace("/", ".") +} diff --git a/scheduling/examples/SampleProcedure.java b/scheduling/examples/SampleProcedure.java new file mode 100644 index 0000000..3c01425 --- /dev/null +++ b/scheduling/examples/SampleProcedure.java @@ -0,0 +1,33 @@ +package scheduling.procedures; + +import gov.nasa.ammos.aerie.procedural.scheduling.Goal; +import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan; +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.SchedulingProcedure; +import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart; +import gov.nasa.jpl.aerie.merlin.protocol.types.Duration; + +import missionmodel.Utils; + +import java.util.Map; + +@SchedulingProcedure +public record SampleProcedure(int quantity) implements Goal { + @Override + public void run(EditablePlan plan) { + final var firstTime = Duration.hours(0); + final var step = Duration.hours(6); + + var currentTime = firstTime; + for (var i = 0; i < quantity; i++) { + plan.create( + Utils.getCollectDataActivityName(), + new DirectiveStart.Absolute(currentTime), + Map.of() + ); + currentTime = currentTime.plus(step); + } + plan.commit(); +// var results = plan.simulate(new SimulateOptions()); +// var size = results.instances().collect().size(); + } +} diff --git a/scheduling/src/main/java/scheduling/package-info.java b/scheduling/src/main/java/scheduling/package-info.java new file mode 100644 index 0000000..dbf8bc5 --- /dev/null +++ b/scheduling/src/main/java/scheduling/package-info.java @@ -0,0 +1,5 @@ +@WithMappers(BasicValueMappers.class) +package scheduling; + +import gov.nasa.jpl.aerie.contrib.serialization.rulesets.BasicValueMappers; +import gov.nasa.ammos.aerie.procedural.scheduling.annotations.WithMappers; diff --git a/scheduling/src/main/java/scheduling/procedures/.gitkeep b/scheduling/src/main/java/scheduling/procedures/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/settings.gradle b/settings.gradle index 6da1093..122f4e8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,3 @@ rootProject.name = 'aerie-template' include('missionmodel') +include('scheduling')