Skip to content

Commit

Permalink
Initial implementation of spawn and call for non-activity subtasks
Browse files Browse the repository at this point in the history
  • Loading branch information
mattdailis committed Jul 13, 2024
1 parent 8b5dec9 commit 6f974e3
Show file tree
Hide file tree
Showing 78 changed files with 6,989 additions and 75 deletions.
300 changes: 300 additions & 0 deletions docs-src/1_tutorials/getting-started/1-gettingstarted.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
# Getting Started

:::{warning}
This page is under construction. Please bear with us as we port
our [Java tutorial](https://nasa-ammos.github.io/aerie-docs/tutorials/mission-modeling/introduction/) to python.
:::

Welcome Aerie modeling padawans! For your training today, you will be learning the basics of mission modeling in Aerie
by building your own simple model of an on-board spacecraft solid state recorder (SSR). This model will track the
recording rate into the recorder from a couple instruments along with the integrated data volume over time. Through the
process of building this model, you'll learn about the fundamental objects of a model, activities and resources, and
their structure. You'll be introduced to the different categories of resources and learn how you define and implement
each along with restrictions on when you can/can't modify them. As a bonus, we will also cover how you can make your
resources "unit aware" to prevent those pesky issues that come along with performing unit conversions and how you can
test your model without having to pull your model into an Aerie deployment.

Let the training begin!

## Installing pymerlin

If you haven't already, go to the [quickstart](../../quickstart.md) guide to get set up with pymerlin.

## Creating a Mission Model

Start by creating a `mission.py` file with the following contents:

```python
from pymerlin import MissionModel


@MissionModel
class Model:
def __init__(self, registrar):
self.data_model = DataModel(registrar)


class DataModel:
def __init__(self, registrar):
"YOUR CODE HERE"
```

## Your First Resource

We will begin building our SSR model by creating a single resource, `recording_rate`, to track the rate at which data is
being written to the SSR over time. As a reminder, a **Resource** is any measurable quantity whose behavior we want to
track over the course of a plan. Then, we will create a simple activity, `collect_data`, that updates the `recording_rate`
by a user-specified rate for a user-specified duration. This activity is intended to represent an on-board camera taking
images and writing data to the spacecraft SSR.

Although we could define the `recording_rate` resource directly in the pre-provided top-level `Mission` class, we'd like
to keep that class as simple as possible and delegate most of model's behavior definition to other, more focused
classes. With this in mind, let's create a new class within the `missionmodel` package called `DataModel`, which we will
eventually instantiate within the `Mission` class.

In the `DataModel` class, declare the `recording_rate` resource by replacing `"YOUR CODE HERE"` with the following line
of code:

```python
self.recording_rate = registrar.cell(0) # Megabits/s
```

<!--:::{tip}
As you are coding, take advantage of your IDE to auto import the modeling framework classes you need like `MutableResource`.
:::-->

Let's tease apart this line of code and use it as an opportunity to provide a brief overview of the various types of
resources available to you as a modeler. The mission modeling framework provides two primary classes from which to
define resources:

1. `MutableResource` - resource whose value can be explicitly updated by activities or other modeling code after it has
been defined. Updates to the resource take the form of "Effects" such as `increase`, `decrease`, or `set`. The values
of this category of resource are explicitly tracked in objects called "Cells" within Aerie, which you can read about
in detail in
the [Aerie Software Design Document](https://ammos.nasa.gov/aerie-docs/overview/software-design-document/#cells) if
you are interested.
2. `Resource` - resource whose value cannot be explicitly updated after it has been defined. In other words, these
resources cannot be updated via "Effects". The most common use of these resources are to create "derived" resources
that are fully defined by the values of other resources (we will have some examples of these later). Since these
resources get their value from other resources, they actually don't need to store their own value within a "Cell".
Interestingly, the `MutableResource` class extends the `Resource` class and includes additional logic to ensure
values are correctly stored in these "Cells".

From these classes, there are a few different types of resources provided, which are primarily distinguished by how the
value of the resource progresses between computed points:

- `Discrete` - resource that maintains a constant value between computed points (i.e. a step function or piecewise
constant function). Discrete resources can be defined as many different types such as `Boolean`, `Integer`, `Double`,
or an enumeration. These types of resources are what you traditionally find in discrete event simulators and are the
easiest to define and "effect".
- `Linear` - resource that has a linear profile between computed points. When computing the value of such resources you
have to specify both the value of the resource at a given time along with a rate so that the resource knows how it
should change until the next point is computed. The resource does not have to be strictly continuous. In other words,
the linear segments that are computed for the resource do not have to match up. Unlike discrete resources, a linear
resource is implicitly defined as a `Double`.
- `Polynomial` - generalized version of the linear resource that allows you to define resources that evolve over time
based on polynomial functions.
- `Clock` - special resource type to provide "stopwatch" like functionality that allows you to track the time since an
event occurred.

TODO: Add more content on `Clock`

:::{note}
Polynomial resources currently cannot be rendered in the Aerie UI and must be transformed to a linear resource (an
example of this is shown later in the tutorial)
:::

Looking back at our resource declaration, you can see that `recording_rate` is a `MutableResource` (we will emit effects
on this resource in our first activity) of the type `Discrete<Double>`, so the value of the resource will stay constant
until the next time we compute effects on it.

Next, we must define and initialize our `recording_rate` resource, which we can do in a class constructor that takes one
parameter we'll called `registrar` of type `Registrar`. You can think of the `Registrar` class as your link to what will
ultimately get exposed in the UI and in a second we will use this class to register `recording_rate`. But first, let's
add the following line to the constructor we just made to fully define our resource.

Both the `MutableResource` and `Discrete` classes have static helper functions for initializing resources of their type.
If you included those functions via `import static` statements, you get the simple line above. The `discrete()` function
expects an initial value for the resource, which we have specified as `0.0`.

The last thing to do is to register `recording_rate` to the UI so we can view the resource as a timeline along with our
activity plan. This is accomplished with the following line of code:

```python
registrar.resource("recording_rate", self.recording_rate.get);
```

:::{note}
Notice that `self.recording_rate.get` does not have parenthesies at the end. This is because we are registering
the `get`
function itself as a resource. Resources are functions that perform computations on cells
:::

The first argument to this `resource` function is the string name of the resource you want to appear in the simulation
results,
and the second argument is the resource itself.

You have now declared, defined, and registered your first resource and your `DataModel` class should look something like
this:

```python
class DataModel:
def __init__(self, registrar):
self.recording_rate = registrar.cell(0)
registrar.resource("recording_rate", self.recording_rate.get)
```

With our `DataModel` class built, we can now instantiate it within the top-level `Model` class as a member variable of
that class. The `Registrar` that we are passing to `DataModel` is unique in that it can log simulation errors as a
resource, so we also need to instantiate one of these special error registrars as well. After these additions,
the `Mission` class should look like this:

```python
from pymerlin import MissionModel


@MissionModel
class Model:
def __init__(self, registrar):
self.data_model = DataModel(registrar)


class DataModel:
def __init__(self, registrar):
self.recording_rate = registrar.cell(0)
registrar.resource("recording_rate", self.recording_rate.get)
```

## Your First Activity

Now that we have a resource, let's build an activity called `collect_data` 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 implemented by async functions in python, create a new function called `collect_data` and add the
following decorator above that function, which allows pymerlin to recognize this function as an activity type.

```python
@Model.ActivityType
async def collect_data(model):
pass
```

:::{note}
The `async` keyword allows pymerlin to interleave the execution of your new activity with other activities, which is
important when activities can pause and resume at various times
:::

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. Parameters allow the behavior of an activity to be modified by an
operator without modifying its code.

```python
@Model.ActivityType
async def collect_data(model, rate=0.0, duration=Duration.from_string("01:00:00")):
pass
```

For our activity, we will give `rate` a default value of `10.0` megabits per second and `duration` a default value of
`1` hour using pymerlin's built-in `Duration` type.

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:

```python
@Model.ActivityType
@Validation(lambda rate: rate < 100.0, "Collection rate is beyond buffer limit of 100.0 Mbps")
async def collect_data(model, rate=0.0, duration=Duration.from_string("01:00:00")):
pass
```

The `@Validation` decorator specifies a function to validate one or more parameters, and a message to present to the
operator when the validation fails. Now, as you will see soon, when an operator specifies a data rate above `100.0`,
Aerie will show a validation error and message.

Next, we need to tell our activity how and when to effect change on the `recording_rate` resource, which is done in
an [Activity Effect Model](https://nasa-ammos.github.io/aerie-docs/mission-modeling/activity-types/effect-model/). We do
this by filling out the body of the `collect_data` function.

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:

```python
@Model.ActivityType
@Validation(lambda rate: rate < 100.0, "Collection rate is beyond buffer limit of 100.0 Mbps")
async def collect_data(model, rate=0.0, duration=Duration.from_string("01:00:00")):
model.data_model.recording_rate += rate
await delay(duration);
model.data_model.recording_rate -= 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, `recording_rate` 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 `recording_rate` 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:

```python
with using(model.data_model.recording_rate, rate):
await delay(duration)
```

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

```python
from pymerlin import MissionModel, Duration
from pymerlin._internal._decorators import Validation
from pymerlin.model_actions import delay


@MissionModel
class Model:
def __init__(self, registrar):
self.data_model = DataModel(registrar)
registrar.resource("counter", self.counter.get)

class DataModel:
def __init__(self, registrar):
self.recording_rate = registrar.cell(0)
registrar.resource("recording_rate", self.recording_rate.get)

@Model.ActivityType
@Validation(lambda rate: rate < 100.0, "Collection rate is beyond buffer limit of 100.0 Mbps")
async def collect_data(model, rate=0.0, duration="01:00:00"):
model.data_model.recording_rate += rate
await delay(Duration.from_string(duration))
model.data_model.recording_rate -= rate
```

Ok! Now we are all set to give this a spin.

28 changes: 28 additions & 0 deletions docs-src/1_tutorials/getting-started/2-model-test-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Model Test Drive

:::{warning}
This page is under construction. Please bear with us as we port
our [Java tutorial](https://nasa-ammos.github.io/aerie-docs/tutorials/mission-modeling/introduction/) to python.
:::

:::{warning}
This page is a stub, since python mission models cannot yet be uploaded to Aerie.
:::

Within your IDE, compile the model (`./gradlew assemble` should do the trick) and make sure it built successfully by checking `build/lib` for a new `missionmodel.jar` file.

Follow [these instructions](https://ammos.nasa.gov/aerie-docs/planning/upload-mission-model/) to upload your `.jar` file, and give your model a name and version number (e.g. SSR Model version 1.0). Next, you can follow [these instructions](https://ammos.nasa.gov/aerie-docs/planning/create-plan-and-simulate/#instructions) to create a new plan. Pick the model you just compiled to build your plan off of and name your plan `Mission Plan 1` and give it a duration of `1 day`. Click "Create" and click on the newly created plan to open it, which should take you to a view with the plan timeline in the center view panel.

On the left panel, you should see your `collect_data` activity type, which you can drag and drop onto the "Activities" row in the timeline. Your `recording_rate` resource should also appear as a row in the timeline, but with no values applied yet since we haven't simulated. Put two activities in your plan and click on the second one (the first one we will leave alone and let it use default values). On the right panel, you should now see detailed information about your activity. Look for the "Parameters" section and you will see your rate and duration parameters, which you can modify. Try modifying the rate above `100.0` and you will see a warning icon appear, which you can hover over and see the message we wrote into a Validation earlier. Modify the rate back to `20` and change the default duration to `2 hours`.

:::info

When activity types are initially added to the plan, they are shown with a play button icon and don't have a duration. We call these "activity directives", and it is these activities that you are allowed to modify by changing parameters, timing, etc. Once a simulation has been performed, one or more activities will appear below the directive, which are the activity instances. These actually have a duration (based on their effect model) and are the result of the simulation run.

:::

On the top menu bar, click "Simulation" and then "Simulate". After you see a green checkmark, `recording_rate` should be populated with a value profile. The value should begin at 0.0 (since we initialized it that way in the model) and pop up to `10` for the first activity and `20` for the second. You'll also see that the activity instances below the activity directives (see note above) have durations that match the arguments we provided. At this point, your view will look similar to the screenshot below.

![Tutorial Plan 1](assets/Tutorial_Plan_1.png)

At this point, we could go into more detail about how you can [edit the timeline](https://nasa-ammos.github.io/aerie-docs/planning/timeline-editing/), edit your UI view, or view simulation history, but instead we will move back to our IDE and add some more complexity to our model.
Loading

0 comments on commit 6f974e3

Please sign in to comment.