Getting Started¶
Warning
This page is under construction. Please bear with us as we port our Java tutorial 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 guide to get set up with pymerlin.
Creating a Mission Model¶
Start by creating a mission.py
file with the following contents:
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:
self.recording_rate = registrar.cell(0) # Megabits/s
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:
cell
- 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 asincrease
,decrease
, orset
. 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 if you are interested.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, thecell
class extends theResource
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 asBoolean
,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 aDouble
.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 cell
(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 cell
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:
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:
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:
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
“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 - 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.
@Model.ActivityType
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, 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.
@Model.ActivityType
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
by adding a method to our class with a couple of annotations:
@Model.ActivityType
@Validation(lambda rate: rate < 100.0, "Collection rate is beyond buffer limit of 100.0 Mbps")
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. 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:
@Model.ActivityType
@Validation(lambda rate: rate < 100.0, "Collection rate is beyond buffer limit of 100.0 Mbps")
def collect_data(model, rate=0.0, duration=Duration.from_string("01:00:00")):
model.data_model.recording_rate += rate
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 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:
with using(model.data_model.recording_rate, rate):
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:
# Comment
pymerlin.checkout()
pymerlin checkout successful: All systems GO 🚀
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)
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")
def collect_data(model, rate=0.0, duration="01:00:00"):
model.data_model.recording_rate += rate
delay(Duration.from_string(duration))
model.data_model.recording_rate -= rate
Ok! Now we are all set to give this a spin.