Skip to content

Commit

Permalink
Mission Modeling Tutorial (#121)
Browse files Browse the repository at this point in the history
* Prereq and resource types documentation

* Added docs to build your first activity

* Add discussion on state and derived resources

* Split up tutorial into separate pages

* Restructured tutorial hierarchy. Got titles to show up right

* Fix broken links

* Added two more sections to the tutorial to build second activity and test it out

* Added section on ways to integrate resources

* Added additional sections on integrating rate and simulation configuration
  • Loading branch information
ewferg authored Feb 2, 2024
1 parent a84ee19 commit 2835b33
Show file tree
Hide file tree
Showing 17 changed files with 1,447 additions and 0 deletions.
3 changes: 3 additions & 0 deletions docs/tutorials/mission-modeling/assets/Simulation_Config.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions docs/tutorials/mission-modeling/assets/Tutorial_Plan_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions docs/tutorials/mission-modeling/assets/Tutorial_Plan_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions docs/tutorials/mission-modeling/assets/Tutorial_Plan_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions docs/tutorials/mission-modeling/current-value.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Using Current Value in an Effect Model

Now that we have our magnetometer resources, we need to build an activity that changes the `MagDataMode` for us (since `MagDataRate` is a derived resource, we shouldn't have to touch it) and changes the overall SSR `RecordingRate` to reflect the magnetometer's data rate change. This activity, which we'll call `ChangeMagMode`, only needs one parameter of type `MagDataCollectionMode` to allow the user to request a change to the mode. Let's give that parameter a default value of `LOW_RATE`.

In the effect model for this activity (which we'll call `run()` by convention), we can use the `set()` method in the `DiscreteEffects` class to change the `MagDataMode` to the value provided by our mode parameter. The computation of the change to the `RecordingRate` caused by the mode change is a little tricky because we need to know both the value of the `MagDataRate` before and after the mode change. Once we know those value, we can subtract the old value from the new value to get the net increase to the `RecordingRate`. If the new value happens to be less than the old value, our answer will be negative, but that should be ok as long as we use the `increase()` method when effecting the `RecordingRate` resource.

We can get the current value of a resource with a static method called `currentValue()` available in the `Resources` class. For our case here, we want to get the current value of the `MagDataRate` **before** we actually change the mode to the requested value, so we have to be a little careful about the order of operations within our effect model. The resulting activity type and its effect model should look something like this:

```java
package missionmodel;

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;

import static gov.nasa.jpl.aerie.contrib.streamline.core.Resources.currentValue;

@ActivityType("ChangeMagMode")
public class ChangeMagMode {

@Export.Parameter
public MagDataCollectionMode mode = MagDataCollectionMode.LOW_RATE;

@ActivityType.EffectModel
public void run(Mission model) {
double currentRate = currentValue(model.dataModel.MagDataRate);
double newRate = mode.getDataRate();
// Divide by 10^3 for kbps->Mbps conversion
DiscreteEffects.increase(model.dataModel.RecordingRate, (newRate-currentRate)/1.0e3);
DiscreteEffects.set(model.dataModel.MagDataMode, mode);
}
}
```

Looking at our new activity definition, you can see how we use the `increase()` effect on `RecordingRate` to "increase" the data rate based on the net data change from the old rate. You may also notice a magic number where we do a unit conversion from `kbps` to `Mbps`, which isn't ideal. Later on in this tutorial, we will introduce a "Unit Aware" resource framework that will help a bit with conversions like these if desired.

As a final step, make sure you add this new activity to our `package-info.java` file

```java
@ActivityType("ChangeMagMode")
```
59 changes: 59 additions & 0 deletions docs/tutorials/mission-modeling/enum-derived-resource.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Enumerated and Derived Resources

In addition to our on-board camera, let's imagine that we also have an instrument on-board that is continuously collecting data, say a magnetometer, based on a data collection mode. Perhaps at especially interesting times in the mission, the magnetometer is placed in a high rate collection mode and at other times remains in a low rate collection mode. For our model, we want to be able to track the collection mode over time along with the associated data collection rate of that mode.

The first thing we'll do to accomplish this is create a Java enumeration called `MagDataCollectionMode` that gives us the list of available collection modes along with a mapping of those modes to data collection rates using [enum fields](https://issac88.medium.com/java-enum-fields-methods-constructors-3a19256f58b). We will also add a getter method to get the data rate based on the mode. Let's say that we have three modes, `OFF`, `LOW_RATE`, and `HIGH_RATE` with values `0.0`, `500.0`, and `5000.0`, respectively. After coding this up, our enum should look like this:

```java
package missionmodel;

public enum MagDataCollectionMode {
OFF(0.0), // kbps
LOW_RATE(500.0), // kbps
HIGH_RATE(5000.0); // kbps

private final double magDataRate;

MagDataCollectionMode(double magDataRate) {
this.magDataRate = magDataRate;
}

public double getDataRate() {
return magDataRate;
}
}
```

With our enumeration built, we can now add a couple of new resources to our `DataModel` class. The first resource, which we'll call `MagDataMode`, will track the current data collection mode for the magnetometer. Declare this resource as a discrete `MutableResource` of type `MagDataCollectionMode`

```java
public MutableResource<Discrete<MagDataCollectionMode>> MagDataMode;
```

and then add the following lines of code to the constructor to initialize the resource to `OFF` and register it with the UI.

```java
MagDataMode = resource(discrete(MagDataCollectionMode.OFF));
registrar.discrete("MagDataMode",MagDataMode, new EnumValueMapper<>(MagDataCollectionMode.class));
```

As you can see, declaring and defining this resource was not much different than when we built `RecordingRate` except instead of referencing the `Double` type, we are referencing our enumerated type `MagDataCollectionMode`.

Another resource we can add is one to track the numerical value of the data collection rate of the magnetometer, which is based on the collection mode. In other words, we can derive the value of the rate from the mode. Since we are deriving this value and don't intend to emit effects directly onto this resource, we can declare it as a discrete `Resource` of type `Double` instead of a `MutableResource`.

```java
public Resource<Discrete<Double>> MagDataRate; // bps
```

When we go to define this resource in the constructor, we need to tell the resource to get its value by mapping the `MagDataMode` to its corresponding rate. A special static method in the `DiscreteResourceMonad` class called `map()` allows us to define a function that operates on the value of a resource to get a derived resource value. In this case, that function is simply the getter function we added to the `MagDataCollectionMode`. The resulting definition and registration code for `MagDataRate` then becomes

```java
MagDataRate = map(MagDataMode, MagDataCollectionMode::getDataRate);
registrar.discrete("MagDataRate", MagDataRate, new DoubleValueMapper());
```

:::info

Instead of deriving a resource value from a function using `map()`, there are a number of static methods in the `DiscreteResources` class, which you can use to `add()`, `multiply()`, `divide()`, etc. resources. For example, you could have a `Total` resource that simple used `add()` to sum some resources together.

:::
127 changes: 127 additions & 0 deletions docs/tutorials/mission-modeling/first-activity.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Your First Activity

Now that we have a resource, let's build an activity called `CollectData` that emits effects on that resource. We can imagine this activity representing a camera on-board a spacecraft that collects data over a short period of time. Activities in Aerie follow the general definition given in the [CCSDS Mission Planning and Scheduling Green Book](https://public.ccsds.org/Pubs/529x0g1.pdf)

> "An activity is a meaningful unit of what can be planned… The granularity of a Planning Activity depends on the use case; It can be hierarchical"
Essentially, activities are the building blocks for generating your plan. Activities in Aerie follow a class/object relationship where [activity types](https://nasa-ammos.github.io/aerie-docs/mission-modeling/activity-types/introduction/) - defined as a class in Java - describe the structure, properties, and behavior of an object and activity instances are the actual objects that exist within a plan.

Since activity types are classes in Java, create a new class called `CreateData` and add the following Java annotation above that class, which allows Aerie to recognize this class as an activity type.

```java
@ActivityType("CollectData")
```

Within this activity type, let's define two [parameters](https://nasa-ammos.github.io/aerie-docs/mission-modeling/activity-types/parameters/), `rate` and `duration`, and give them default arguments. When an activity instance is placed into a plan, operators can modify these default arguments prior to simulation if desired. Activity parameters are simply member variables of the activity type class with an annotation above the member variable:

```java
@Export.Parameter
```

:::info

In reality, there are a variety of [parameter annotations](https://nasa-ammos.github.io/aerie-docs/mission-modeling/parameters/) you can use to tell Aerie about activity parameters and their defaults. In fact, if all member variables are intended to be parameters, you don't even need to include an annotation. For this tutorial, however, we want to be explicit with our parameter definition and will be using annotations even if they aren't technically required.

:::

For our activity, we will make `rate` a `double` with a default value of `10.0` megabits per second and `duration` a `Duration` type built into Aerie with a default value of `1` hour. That translates to the following code:

```java
@Parameter
public double rate = 10.0; // Mbps

@Parameter
public Duration duration = Duration.duration(1, Duration.HOURS);
```

Right now, if an activity of this type was added to a plan, an operator could alter the parameter defaults to any value allowed by the parameter's type. Let's say that due to buffer limitations of our camera, it can only collect data at a rate of `100.0` megabits per second, and we want to notify the operator that any rate above this range is invalid. We can do this with [parameter validations](https://nasa-ammos.github.io/aerie-docs/mission-modeling/activity-types/parameters/#validations) by adding a method to our class with a couple of annotations:

```java
@Validation("Collection rate is beyond buffer limit of 100.0 Mbps")
@Validation.Subject("rate")
public boolean validateCollectionRate() {
return rate <= 100.0;
}
```

The `@Validation` annotation specifies the message to present to the operator when the validation fails. The `@Validation.Subject` annotation specifies the parameter(s) with which the validation is associated. Now, as you will see soon, when an operator specifies a data rate above `100.0`, Aerie will show a validation error and message in the UI.

Next, we need to tell our activity how and when to effect change on the `RecordingRate` resource, which is done in an [Activity Effect Model](https://nasa-ammos.github.io/aerie-docs/mission-modeling/activity-types/effect-model/). Just like with validations, an effect model is built by adding a method to our class, but with a different annotation, `@ActivityType.EffectModel`. Unlike validations, there can only be one of these methods per activity and the method should accept the top-level mission model class as a parameter (which in our case is just `Mission`). Conventionally, the method name given to the effect model is `run()`.

For our activity, we simply want to model data collection at a fixed rate specified by the `rate` parameter over the full duration of the activity. Within the `run()` method, we can add the follow code to get that behavior:

```java
DiscreteEffects.increase(model.dataModel.RecordingRate, this.rate);
delay(duration);
DiscreteEffects.decrease(model.dataModel.RecordingRate, this.rate);
```

Effects on resources are accomplished by using one of the many static methods available in the class associated with your resource type. In this case, `RecordingRate` is a discrete resource, and therefore we are using methods from the `DiscreteEffects` class. If you peruse the static methods in `DiscreteEffects`, you'll see methods like `set()`, `increase()`, `decrease()`, `consume()`, `restore()`,`using()`, etc. Since discrete resources can be of many primitive types (e.g. `Double`,`Boolean`), there are specific methods for each type. Most of these effects change the value of the resource at one time point instantaneously, but some, like `using()`, allow you to specify an [action](https://nasa-ammos.github.io/aerie-docs/mission-modeling/activity-types/effect-model/#actions) to run like `delay()`. Prior to executing the action, the resource changes just like other effects, but once the action is complete, the effect on the resource is reversed. These resource effects are sometimes called "renewable" in contrast to the other style of effects, which are often called "consumable".

In our effect model for this activity, we are using the "consumable" effects `increase()` and `decrease()`, which as you would predict, increase and decrease the value of the `RecordingRate` by the `rate` parameter. The `run()` method is executed at the start of the activity, so the increase occurs right at the activity start time. We then perform the `delay()` action for the user-specified activity `duration`, which moves time forward within this activity before finally reversing the rate increase. Since there are no other actions after the rate decrease, we know we have reached the end of the activity.

If we wanted to save a line of code, we could have the "renewable" effect `using()` to achieve the same result:

```java
DiscreteEffects.using(model.dataModel.RecordingRate, -this.rate, () -> delay(duration) );
delay(duration);
```

:::info

For the case where we use `using()`, you'll notice we have to use the `delay()` action twice. This is because the first action within `using()` is spawned, which allows the execution of the effect model to continue as the `using()` effect waits for the end of its `delay()` action. This allows you to have many `using()` effects, perhaps on different resources, running concurrently within an activity. The second `delay()` actually moves time forward for the activity.

:::

With our effect model in place, we are done coding up the `CollectData` activity and the final result should look something like this:

```java
package missionmodel;

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

@ActivityType("CollectData")
public class CollectData {

@Parameter
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
*/
// Approach 1 - Modify rate at start/end of activity
DiscreteEffects.increase(model.dataModel.RecordingRate, this.rate);
delay(duration);
DiscreteEffects.decrease(model.dataModel.RecordingRate, this.rate);

}
}
```

The last thing we need to do before giving our model a test drive is add a line to the `package-info.java` file to help Aerie find our newly built activity type

```java
@WithActivityType(CollectData.class)
```

Ok! Now we are all set to give this a spin.
20 changes: 20 additions & 0 deletions docs/tutorials/mission-modeling/first-build.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Download and Build the Mission Model Template

Mission models require a couple of standard items for Aerie to process the model once it has been built:

1. A [`package-info.java`](https://nasa-ammos.github.io/aerie-docs/mission-modeling/introduction/#the-package-infojava-file) file containing a reference to the top-level mission model class, annotations referencing any activities defined in the model, an annotation referencing a configuration class that can expose configuration parameters that can be varied prior to simulation, and import statements to the Aerie modeling framework to bridge the framework to the model.
2. The top-level [mission model class](https://ammos.nasa.gov/aerie-docs/mission-modeling/introduction/#the-mission-model-class) that defines or delegates the behavior of the system being described in the model. Any quantity or state that you would like to track over the course of the simulation - which we define as a [**Resource**](https://ammos.nasa.gov/aerie-docs/mission-modeling/resources-and-models/) - should be declared and defined in this class or its delegates. The name of the top-level mission class can be anything as long as it matches the reference in `package-info.java`.

Fortunately, to save you some trouble, we've created a [mission model template repository](https://github.com/NASA-AMMOS/aerie-mission-model-template) that already has these items included for you along with a gradle build setup that takes care of including the right Aerie dependencies to get your mission model `.jar` file built hassle-free. In this repository, if you take a look in [`src/main/java/missionmodel`](https://github.com/NASA-AMMOS/aerie-mission-model-template/tree/main/src/main/java/firesat), you'll see the `package-info.java` file along with the top-level `Mission` and `Configuration` classes already defined for you.

On the main page for the [mission model template repository](https://github.com/NASA-AMMOS/aerie-mission-model-template), click the "Use this template" button on the top right of the page and select "Create a new repository" to create a new repository for your SSR model. Clone your new repository and follow the instructions in the [`README.md`](https://github.com/NASA-AMMOS/aerie-mission-model-template/blob/main/README.md) to setup your environment and test out building a mission model `.jar` from the model. You'll find the `.jar` you built within a `build/libs` directory generated as part of the gradle build.

At this point, we could pull up the Aerie UI and load the `.jar` file into Aerie as a model, but there is nothing really interesting in the model yet. So before we bring our model into Aerie, let's give it some content.

TODO:

- Update mission model template to align with David's new framework
- Account for his register in the Mission class
- Update dependencies in build.gradle
- Change name from firesat to missionmodel
- Remove activities/resources from the template (If they want content they could use the tutorial repo or other examples we provide)
Loading

0 comments on commit 2835b33

Please sign in to comment.