Modeling Constructs

This section describes the constructs that can be used to build Dymodetron models.

The modeling constructs are summarized in the table below.

Modeling Constructs Summary

Modeling construct

Description

Model

An entire model containing other model constructs.

Model Description

The name of the model and a textual summary.

Model Parameters

The scalar values, lookup table, and parameterized random distributions that affect the behavior of the model.

Scalar Parameter

A scalar value that affects the behavior of the model.

Lookup Table

A lookup table that affects the behavior of the model.

Random Distribution

A parameterized random distribution that affects the behavior of the model.

Entity Type

A kind of object described by the model.

Entity Attribute

A measurable characteristic of an entity type.

Entity Instance

An individual, identifiable realization of an entity type, with concrete values of each entity attribute and state.

Entity Set

A collection of entity instances.

State Machine

A description of the behavior of an entity type, in the form of individual states and reactions to events.

State

A binary (true, false) description of an entity as having some condition or being in some mode or circumstance.

Event Type

A type of event that can occur, causing a state machine to transition between states.

Event

An individual realization of an event type, that can cause specific entity instances to transition between states.

Transition

A reaction to an event, that causes an entity to stop being in one state, and start being in another state.

Entry Action

A sequence of action statements that occurs upon entry to a state.

Exit Action

A sequence of action statements that occurs prior to exit from a state.

Action Statement

An expression that generates events on entity instances, or modifies entity attributes, or intermediate logic leading up to one of those outcomes.

Overview

The figure below provides an overview of how the modeling constructs fit together. The boxes are dymodetron constructs. The lines indicate relationships between constructs. The “diamonds” indicate containment: the construct on the “diamond” side contains the construct on the other side of the relationship. Where the arrows have labels, read from the base of the arrow, across the relationship, and to the other side. For example State Machine describes behavior of EntityType. Quantifiers such as 0..*, 1, 2, indicate the multiplicity of the relationship: one-to-one, one-to-many, zero-or-more, etc.

%%{init: { 'theme': 'neutral' } }%% classDiagram Model *-- "1" ModelDescription Model *-- "*" EntityInstance Model *-- "*" StateMachine Model *-- "1" ModelParameters Model *-- "1" StateTracking EntitySet "*" o-- "*" EntityInstance ModelParameters *-- "0..*" LookupTable ModelParameters *-- "0..*" RandomDistribution ModelParameters *-- "0..*" ScalarParameter StateMachine *-- "1" TransitionTable TransitionTable *-- "1..*" Transition StateMachine *-- "*" State StateMachine *-- "1" TransitionTable State *-- EntryAction State *-- ExitAction StateMachine *-- "*" EventType StateMachine *--> "*" State : composed of StateMachine --> "1" EntityType : describes behavior of Transition --> "2" State : has source, target Transition --> Event : triggered by EntryAction --> "*" ActionStatement ExitAction --> "*" ActionStatement Event --> EventType : has type Event --> "1" EntitySet : targeted at ActionStatement --> "0..*" Event : generates ActionStatement --> "0..*" EntitySet : gathers up ActionStatement --> "0..*" EntityAttribute : updates EntityType *--> "0..*" EntityAttribute : quantified by Transition --> EntityInstance : changes state of EntityInstance --> "1" EntityType : is one of class ModelDescription { model_name docstring } class EntityType { name } class EntityAttribute { name attribute_type initial_value } class Event { event_time }

Model

A Dymodetron model is a single self-contained set of Dymodetron model constructs used to describe some system in order to ask some questions.

A Dymodetron Model is a python file containing instances of all the rest of the Dymodetron constructs. Here’s an example.

Entity Type

An entity type is a kind of thing that your model describes. Measurable characteristics of an entity type are quantified by one or more entity attributes. The behavior of an entity type is described by one or more state machines. Individual realizations of an entity type are called entity instances. A simulation of a model can have many instances of the same entity type. Each instance has its own unique identifier, its own set of values for each of the entity type’s entity attributes, and its own “running copy” of each of the entity type’s state machines.

You create an entity type by creating a class that sub-classes the Dymodetron EntityType class.

from dymodetron import EntityType

# An entity is defined by creating a class that sub-classes 'EntityType'.
class Cat(EntityType):
    # It doesn't hurt to put a docstring on your new entity type.
    """Cats are a type of animal that is neither dog, duck, nor lizard."""

Entity Instance

An entity instance is an individual realization of a given entity type. Each entity instance has its own unique identifier, its own copy of each of the entity attributes for the entity type, and its own “running copy” of each of the state machines for the entity type.

Normally you don’t need to create entity instances yourself. The Dymodetron engine will take care of this, after you tell it how many you want.

Entity Attribute

An entity attribute quantifies some measurable aspect of an entity.

You declare the entity attributes within your entity type class, by using EntityAttribute. An entity attribute has a name, a type, and an initial value, as shown in the example below. Here we are adding an entity attribute named height_inches, of type scalar_float, with default initial value 9.0.

The available types are ‘scalar_float’, ‘scalar_int’, and ‘scalar_obj’.

from dymodetron import EntityType, EntityAttribute, types

# An entity is defined by creating a class that sub-classes 'EntityType'.
class Cat(EntityType):
    # You can have docstrings on any of the elements of your model.
    """Cats are a type of animal that is neither dog, duck, nor lizard."""

    # Here we declare the entity attributes of Cat.
    # The entity attribute name is on the left-hand side.
    # On the right-hand side, we specify the type of the attribute, and the default
    # initial value.
    height_inches = EntityAttribute(attribute_type=types.scalar_float, initial_value=9.0)

Entity Set

An entity set is a collection of entity instances that is ‘calculated’ at runtime, determined by applying a selection criteria to a larger set of entity instances, to choose a subset of them.

A common thing you will do in a Dymodetron model is select and gather up subsets of all the entities in your model, so that you can generate events targeting just those selected entities, or so that you can update their entity attribute values. You do this by filtering down entity sets into subsets, based on criteria that you provide. The criteria examine whether or not an individual entity instance should be in the resultant entity set or not.

This process is illustrated below. Every state entry/exit action is passed a set of entities that are entering/exiting the state. Using the action.entity_match(...) expression, you can filter this set down to a smaller set of filtered entities. Then, you can generate events on those filtered entities. Or, you can modify the attributes of the filtered entities.

%%{init: { 'theme': 'neutral' } }%% graph TB subgraph entry_action [state entry/exit action] A(entities entering/exiting state) -- entity_set --> B(action.entity_match... as filtered_entities) B -- filtered_entities --> C(generate events on filtered_entities) B -- filtered_entities --> D(modify attributes on filtered_entities) end

For example, you might select all Cat entities that have their height_inches attribute larger than some threshold. Then you might generate events that target just those cats.

Entity sets are created within action statements by using with action.entity_match(...) as name_of_entity_set: ... expressions. In the example below, we select all cats more than 24 inches tall, and generate an event targeted at them.

from dymodetron import action

# Select all the cats that are more than 24 inches tall.
# The 'cat' entity set is provided to the action statement in which
# this entity_match() is used, and now we are filtering down the cats within that entity set
# based on the value of the entity attribute 'height_inches'.
with action.entity_match(
    entity_type=Cat(),                            # What entity type?
    entity_set=cat,                               # What entity set to start with?
    criteria=lambda c: c.height_inches > 24.0     # Which entity instances should be included in the resultant entity set?
) as giant_cat:                                   # The name of the resultant entity set follows the 'as' keyword.

    # In this section, you can refer to 'giant_cat' anywhere an entity set is called for.
    # It will refer to all entities matching the criteria you provided above.

    # Generate hungry event on giant cats at 5 time ticks in the future.
    action.generate_event_rel(
        entity_type=Cat(),
        entity_set=giant_cat,
        event=hungry_event(),
        time_ticks_rel=5
    )

Naming an entity set

Notice above that the with ... as expression is assigning a name giant_cat to all the cats that match the specified criteria. The as clause is how the entity set is named.

That entity set name is then used within the with ... as block to generate an event that targets just the cats in the giant_cat entity set. You might instead manipulate the giant_cat attributes, or filter them down even further with additional entity_match() expressions.

Entity match criteria expressions

Within the entity_match() expression above, you’ll notice a criteria argument. This is how individual entity instances are evaluated to determine if they belong in the entity set.

The criteria is an expression that takes an entity as an argument, and evaluates the entity’s attributes and/or states in order to return a true/false condition indicating whether or not it should be included in the set. True = include in set, False = exclude from set.

Note

What is this lambda thing?

The python lambda syntax is a way of defining functions in-line in the code. This makes it so that we can specify the criteria for the entity set right there in the definition of the entity set. You can learn more about lambda expressions in the python documentation. The short version is lambdas are “syntactic sugar for a normal function definition”.

The value of the criteria argument should be a lambda expression, or else a separate function that takes entity sets as an argument.

The example below applies the same criteria as the last example, but instead of using a lambda, we pull the criteria out into a standalone function named my_criteria. This has the benefit that you can write unit tests against the standalone criteria function, and you can re-use it in other places. The drawback is that it’s not as easy to look at the action.entity_match() expression and know what the criteria are.

from dymodetron import action

def my_criteria(c):
    result = c.height_inches > 24.0
    return result

with action.entity_match(
    entity_type=Cat(),
    entity_set=cat,
    criteria=my_criteria                          # Reference a standalone function, instead of using a lambda.
) as giant_cat:

    # ... etc ...

Both approaches are equivalent, so do what works best for you. Generally speaking, if the criteria are pretty simple, and you can be pretty sure it’s correct by visual inspection, then the inline lambda approach works well. If the criteria are complicated, or if you have the same criteria that you will use in multiple places, or if you want to gain additional confidence by writing unit tests against your criteria, then it’s probably worth pulling the criteria out into a standalone function.

Entity match criteria expressions - a closer look

The criteria argument for entity_match has the following form, where you replace the portions indicated with angle brackets < > (there should be no angle brackets in your resulting expression).

criteria=lambda <entity_argument_name>: <entity-argument-boolean-expression>

entity_argument_name

This is any name of your choosing. You will refer to this name in the entity-argument-boolean-expression.

entity-argument-boolean-expression

This is a function that evaluates the entity attributes and/or states of an entity instance, to determine whether or not the entity should be included in the entity set.

Using entity_argument_name, you access entity attributes using ‘dot’ notation, and construct boolean expressions bsaed on the entity attributes. For example: entity_argument_name.entity_attribute_name > 0 would select entity instances where the entity_attribute_name is larger than zero.

The types of expressions you can use are listed in this table and described in the subsequent sections.

expression type

Description

value comparisons

Use python value comparison operators.

numpy numerical expressions

Use numpy numerical expressions to perform calculations.

dymodetron action expressions

Use dymodetron action expressions to evaluate expressions involving state machines.

Entity match criteria expressions - examples

Value comparisons expressions using entity attributes

You can use expressions involving python value comparison operators.

Here we look for cats in the entity set cat having height_inches > 24.0.

from dymodetron import action

# Here suppose that entity type 'Cat' has an entity attribute 'height_inches'.
with action.entity_match(
    entity_type=Cat(),
    entity_set=cat,
    criteria=lambda c: c.height_inches > 24.0
) as giant_cat:
    # ... do something with 'giant_cat'

Naming the lambda expression argument

In the ‘criteria’, the lambda argument name can be whatever you want. It’s best to make it either meaningful or a single letter. The entity_match block below will end up with the identical giant_cat entity set as the last example, we’ve just used a stranger name for the lambda expression argument.

from dymodetron import action

with action.entity_match(
    entity_type=Cat(),
    entity_set=cat,
    criteria=lambda whatever_name_you_want_to_use: whatever_name_you_want_to_use.height_inches > 24.0
) as giant_cat:
    # ... do something with 'giant_cat'

Using a numpy expression

You can use expressions involving numpy mathematical functions in order to do calculations. Below, we are creating a set of cats that have taken an even number of naps.

from dymodetron import action
import numpy

# Here suppose that entity type 'Cat' has an entity attribute 'naps_taken'.
with action.entity_match(
    entity_type=Cat(),
    entity_set=cat,
    criteria=lambda c: numpy.mod(c.naps_taken) == 0
) as evenly_napped_cat:
    # ... do something with 'evenly_napped_cat'

Using Dymodetron ‘action’ expressions

Dymodetron has a number of ‘action’ expressions which support evaluation of entity instances in your entity set criteria.

action

Description

Example

in_state(entity,
state_machine_name.state_name())
Determine if entity is currently in the referenced state.
You must include the state machine name and the state name
in the second argument.
Also, note the closed parens () at the end of the state name.
# Find cats in state eating of state machine
# cat_sleeping_eating_drinking.
action.in_state(c, cat_sleeping_eating_drinking.eating() )
not_in_state(entity,
state_machine_name.state_name())
Determine if entity is not currently in the referenced state.
You must include the state machine name and the state name
in the second argument.
Also, note the closed parens () at the end of the state name.
# Find cats not in state eating of state machine
# cat_sleeping_eating_drinking.
action.not_in_state(c,
cat_sleeping_eating_drinking.eating() )
get_state_entry_counter(entity,
state_machine_name.state_name())
Determine the number of times entity has entered
the referenced state.
# Find cats that have never eaten.
action.get_state_entry_counter(c,
cat_sleeping_eating_drinking.eating()) == 0

Matching multiple criteria

You can match on multiple criteria by using the & and | operators for logical-and and logical-or, respectively.

For example, the entity_match below picks out all cats that are currently in state sleeping, and have never been in state eating.

from dymodetron import action

with action.entity_match(
    entity_type=Cat(),
    entity_set=cat,
    criteria=lambda c:
        action.in_state(c, be_a_cat.sleeping())
        &
        action.get_state_entry_counter(c, be_a_cat.eating()) == 0
) as hungry_sleeping_cat:
    # ... do something with 'hungry_sleeping_cat'

Entity set nested filtering

By using nested entity_match() blocks, you can apply model logic to pick out different subsets of the entity instance population and then manipulate their entity attributes and/or generate events on them depending on which criteria they satisfy.

This process is illustrated below.

%%{init: { 'theme': 'neutral' } }%% graph TB subgraph entry_action [state entry/exit action] A(entities entering/exiting state) -- entity_set --> B(action.entity_match... as filtered_entities) B -- filtered_entities --> E(action.entity_match... as additionally_filtered_entities) E -- additionally_filtered_entities --> F(generate events on additionally_filtered_entities) E -- additionally_filtered_entities --> G(modify attributes on additionally_filtered_entities) end

For example, below, we include two sub-filtering entity_match blocks nested inside the top-level entity_match. We generate an event on all the entity instances matching the top-level criteria. Then we filter down further into two groups, depending on additional criteria, to manipulate the entity attributes differently for different subsets of entities.

from dymodetron import action

# Select all the cats that are more than 24 inches tall.
with action.entity_match(
    entity_type=Cat(),
    entity_set=cat,
    criteria=lambda c: c.height_inches > 24.0
) as giant_cat:

    # All the giant cats will be seen by a predator in 10 time ticks.
    action.generate_event_rel(
        entity_type=Cat(),
        entity_set=giant_cat,
        event=observed_by_predator(),
        time_ticks_rel=10
    )

    # Here we are filtering down giant_cat, to pick out heavy giant cats.
    with action.entity_match(
        entity_type=Cat(),
        entity_set=giant_cat,
        criteria=lambda c: c.weight_lbm > 40.0
    ) as giant_heavy_cat:
        # Suppose we have a 'maximum_velocity_mph' entity attribute defined on Cats.
        # Giant heavy cats don't move very fast.
        giant_heavy_cat.maximum_velocity_mph = 1

    # Here we are filtering down giant_cat, to pick out giant cats that aren't as heavy.
    with action.entity_match(
        entity_type=Cat(),
        entity_set=giant_cat,
        criteria=lambda c: c.weight_lbm <= 40.0
    ) as giant_lighter_cat:
        # Giant lighter cats can move a little bit faster.
        giant_lighter_cat.maximum_velocity_mph = 15

Model Description

The model description construct contains a textual description of the model. You can put whatever you want in it.

You define the model description by creating a class that sub-classes the Dymodetron ModelDescription class. Use whatever name you want for the class. Add a docstring within the class to describe your model.

The name of the model description class will be taken to be the name of the model. This means that the name of python file that contains the model definition is not necessarily the name of the model. The model name is used when generating code and diagrams.

from dymodetron import ModelDescription

# The model description is defined by creating a class that sub-classes 'ModelDescription'.
class cat_model_description(ModelDescription):
    # A model description just has a docstring explaining what you are up to with this model.
    """We model the behaviors of a certain kind of cat."""

Model Parameters

The model parameters construct contains all of the scalar values, random distributions, and lookup tables that influence the behavior of the model simulation when executed.

You create the model parameters construct by defining a class that sub-classes the Dymodetron Params class. It is recommended that you name your model parameters class ModelParameters, although you can name it whatever you want.

The example model parameters below contain a random distribution, a lookup table, and a scalar.

You should put all the values that influence the behavior of your model into the ModelParameters construct.

from dymodetron import Params, random as dyrandom, LookupTable, LookupMethod

##########################################################################################
# Define model parameters.
##########################################################################################
class ModelParameters(Params):

    # A random distribution to be used for event generation times:
    # cat nap durations.
    nap_length_hours_distribution = dyrandom.NormalDistribution(
        mean=12.0,
        std=2.0
    )

    # Data for a lookup table:
    # cat max jumping height lookup table data.
    cat_max_jumping_height_data = dict(
        age_cutoffs       = numpy.array([0,        1,      2,          3,        4,       5,      10,     15,     20]),
        jump_height_feet  = numpy.array([3.0,      8.0,    10.0,   12.00,    13.00,   14.00,   12.00,   3.00,   1.00])
    )

    # Lookup table to be used for intermediate calculations in model:
    # cat max jumping height lookup table.
    cat_max_jump_height_lookup = LookupTable(
        breakpoints=cat_max_jumping_height_data['age_cutoffs'],
        values=cat_max_jumping_height_data['jump_height_feet'],
        lookup_method=LookupMethod.Previous
    )

    # For this model, we have all cats take the same amount of time to drink water.
    cat_drinking_time_hours = 0.0167

Scalar Model Parameter

A scalar model parameter is a value with a name.

You create a scalar model parameter by writing scalar_param_name = value in your model parameters block. For example, see cat_drinking_time_hours in the last section.

You use a scalar model parameter by referencing it in an action statement. In the example below, we have an entity set named drinking_cats, and we want to generate events on them at some time in the future. We use a model scalar parameter to specify the time in the future (relative to now) at which the events should be scheduled.

from dymodetron import action

# Generate event on 'drinking_cats' at a time in the future.
# We get the time from a model parameter.
action.generate_event_rel(entity_type=Cat(),
                          entity_set=drinking_cats,
                          event=not_thirsty_event(),
                          time_ticks_rel=ModelParameters.cat_drinking_time_hours
                          )

Lookup Table

A lookup table is a function y = f(x), where given an input value x, the output value y is determined based on the lookup table data.

Defining a lookup table

You define a lookup table by providing 3 pieces of information:

  • breakpoints - a monotonically increasing array of lookup input values.

  • values - the lookup output values associated with each lookup input value.

  • lookup_method - a rule for how to calculate the output value when an input value is in between two breakpoints.

The number of entries in breakpoints and values must be identical.

Below is an example of a lookup table definition. This illustrates the syntax for creating the breakpoint and value arrays. You must define lookup tables within your model parameters block.

from dymodetron import LookupTable, LookupMethod, Params

##########################################################################################
# Define model parameters.
##########################################################################################
class ModelParameters(Params):

    # Lookup table to be used for intermediate calculations in model:
    # cat max jumping height lookup table.
    cat_max_jump_height_feet_lookup_by_age_years = LookupTable(
        breakpoints   = numpy.array([0,        1,      2,          3,        4,       5,      10,     15,     20]),
        values        = numpy.array([3,        8,     10,         12,       13,      14,      12,      3,      1]),
        lookup_method = LookupMethod.Previous
    )

The available options for lookup_method are described below.

lookup_method

Description

LookupMethod.NearestLow

Uses the nearest breakpoint to the input value. Rounds down when interpolating half-integers.

LookupMethod.NearestHigh

Uses the nearest breakpoint to the input value. Rounds up when interpolating half-integers.

LookupMethod.Previous

Use the next lowest breakpoint.

LookupMethod.Next

Use the next largest breakpoint.

Using a lookup table

You use a lookup table within an entry action by invoking the action.lookup action statement.

In the example below, recall that cat is the entity set passed to the entry action. What the action.lookup statement is doing is looking up the ‘max jump height’ for each entity in the entity set cat. Then, the lookup output value is being written to the entity attribute altitude_feet, again one for each entity in the entity set cat.

from dymodetron import State, dymaction, action

# In this example model, we have a state that a cat gets into when it jumps.
class jump(State):
    @dymaction
    # This is the entry action for the state 'jump'.
    def entry_action(cat: Cat):
        # Remember that 'cat' is an entity set. We are looking up a value for each cat in the entity set,
        # and assigning the entity attribute on the left-hand side of the expression, for each cat.
        cat.altitude_feet = action.lookup(cat.age_years, ModelParameters.cat_max_jump_height_feet_lookup_by_age_years)

        # Now, each entity in the entity set 'cat' has an altitude_feet value that was determined by the lookup
        # table and the cat's 'age_years' entity attribute.

If you look up an input value that is outside the range of the provided breakpoints, the output value will be extrapolated per the behavior of scipy.interpolate.interp1d.

Random Distribution

A random distribution is used to model a random process by giving random values to quantities in your model.

Defining a random distribution

Below is an example of a random distribution being defined. You must define your random distributions within the model parameters block.

Note that when importing the dymodetron.random package, we alias it to dyrandom. You don’t have to do this but it helps to clarify that we’re working with Dymodetron’s random package and not one of the many other random packages one might encounter.

from dymodetron import random as dyrandom

##########################################################################################
# Define model parameters.
##########################################################################################
class ModelParameters(Params):

    # A random distribution to be used for event generation times:
    # cat nap durations.
    nap_length_hours_distribution = dyrandom.NormalDistribution(
        mean=12.0,
        std=2.0
    )

The random distribution types available in Dymodetron, and their parameters, are listed below.

random distribution type

Description

Parameters

UniformDistribution

Random real number uniformly distributed in the provided interval.

low, high

NormalDistribution

Random real number normally distributed with provided mean and standard deviation.

mean, std

NormalPositiveDistribution

Absolute value of NormalDistribution.

mean, std

NormalIntDistribution

NormalDistribution, rounded to produce integers.

mean, std

NormalPositiveIntDistribution

Absolute value of NormalDistribution, rounded to produce integers.

mean, std

ExponentialDistribution

Random real number Exponential distributed with provided rate parameter.

rate

PoissonDistribution

Random real number Poisson distributed with provided rate parameter.

rate

LogNormalDistribution

Random real number whose logarithm is normally distributed.
The provided mean and standard deviation are of the log-normal
distribution itself, not the underlying normal distribution.

mean, std

LogNormalIntDistribution

LogNormalDistribution, rounded to produce integers.

mean, std

Using a random distribution

You use random distributions by assigning the values sampled from them to entity variables, and then using those entity variables.

In the example below, whenever a set of cats begins to sleep, we calculate the time at which they are not tired anymore as a random process using the random distribution defined above.

We first declare an entity variable, which is a set of values, one for each entity in the entity set cat. Then we assign to those values from the random distribution. Then we use those values to determine the event times for new events that we generate on the entities.

from dymodetron import action, dymaction, State

class sleeping(State):
    @dymaction
    def entry_action(cat: Cat):

        # Declare entity variable: one variable value for each entity in entity set 'cat'.
        t_nap_finished_hours = action.declare(entity_set=cat, var_type=numpy.float)

        # Assign entity variable from random distribution: each entity in entity set 'cat'
        # gets a sample from 'ModelParameters.nap_length_hours_distribution', stored
        # in the entity variable we declared above.
        action.assign(entity_set=cat,
                      entity_var=t_nap_finished_hours,
                      dist=ModelParameters.nap_length_hours_distribution)

        # Use random data in entity variable: each entity in entity set 'cat' gets a not_tired_event(),
        # at a time determined by the random distribution we just used above.
        action.generate_event_rel(entity_type=Cat(),
                                  entity_set=cat,
                                  event=not_tired_event(),
                                  time_ticks_rel=t_nap_finished_hours.value())

State Machine

The state machine model of computation is described here.

A state machine describes behaviors of an entity type. The state machine consists of a number of states. Every state machine is associated with a single entity type. Every entity instance of that type has its own running ‘copy’ of the state machine. For a given entity instance, each state is either active or inactive at any given simulated time. When a state is active, we say that the entity instance is ‘in’ the state. When the state is inactive, we say that the entity instance is ‘not in’ the state.

States become activated and de-activated by transitions, which are triggered by events. As events occur, entity instances go in and out of various states.

After modeling entity behavior using state machines, the user can ask questions of the model in terms of counting how many times certain states are entered/exited, or how long entities stay in certain states, etc.

State machines can be hierarchical. A state ‘S’ in a top-level state machine can contain its own sub-state-machine. When such a state ‘S’ becomes active, the sub-state-machine begins execution. Here’s an example of a hierarchical state machine.

Define state machine

These are the steps to defining a state machine:

  1. Declare state machine

  2. Specify state machine entity type

  3. Define state machine event types

  4. Define state machine states

  5. Define state machine transition table

  6. (Optional) Define state machine initialization time

These steps are described in the next sections.

Declare state machine

To declare a state machine, create a class that subclasses from Dymodetron’s class StateMachine.

The name of the class is the name of the state machine. It is recommended that you provide a docstring for the state machine.

It is recommended that you give your state machine a meaningful name that describes the behaviors that it models. In our example below, we’ve provided a state machine name that is indicative of the behaviors that it captures.

Your model can have multiple state machines.

from dymodetron import StateMachine

class minimal_cat_sleeping_eating_drinking(StateMachine):
    """Defines the sleeping patterns of cats, as far as this model is concerned."""

Specify state machine entity type

Every state machine is associated with a single entity type. You indicate which one with an expression of the form entity_type = XYZ() within your state machine class. Replace XYZ with the name of the entity type you want the state machine to be associated with.

from dymodetron import StateMachine

class minimal_cat_sleeping_eating_drinking(StateMachine):
    """Defines the sleeping patterns of cats, as far as this model is concerned."""

    # Every state machine must declare the type of entity it is associated with.
    # The entity type is declared elsewhere in the model, and we are referencing it here.
    entity_type = Cat()

Define state machine initialization time

By default every state machine initializes at simulation time zero. You can change this by specifying the initial time for the state machine.

You do this by including a variable t_initial within your state machine. In the example below, we specify an initial time for the state machine at t_initial = -30.0. Using negative initial times can be useful for building state machines that implement initialization logic for your model, to arrange for the simulation to be in a particular state at t=0.

from dymodetron import StateMachine

class minimal_cat_sleeping_eating_drinking(StateMachine):
    """Defines the sleeping patterns of cats, as far as this model is concerned."""

    t_initial = -30.0

    # Every state machine must declare the type of entity it is associated with.
    # The entity type is declared elsewhere in the model, and we are referencing it here.
    entity_type = Cat()

State

The concept of a state in a state machine is described here.

Define state machine states

Every state machine consists of a number of states. You populate the state machine class with states by placing a nested class inside the state machine. The name of the nested class is the name of the state. The nested state class needs to sub-class the Dymodetron State class.

In the example below, we have two states sleeping and awake.

from dymodetron import Event, State, StateMachine

class minimal_cat_sleeping_eating_drinking(StateMachine):
    """Defines the sleeping patterns of cats, as far as this model is concerned."""

    # Every state machine must declare the type of entity it is associated with.
    # The entity type is declared elsewhere in the model, and we are referencing it here.
    entity_type = Cat()

    class sleeping(State):
        pass

    class awake(State):
        pass

State Entry and Exit Actions

State entry and exit actions are used to generate events, modify entity attributes, or measure and report on the state of the simulation.

When an entity instance enters a state, the entry action is executed. Similarly for exit actions when a state is exited.

A state entry action is defined by placing a function named entry_action in the state class, as shown in the example below. You must also decorate the function with the @dymaction decorator.

from dymodetron import Event, State, StateMachine, dymaction

class minimal_cat_sleeping_eating_drinking(StateMachine):
    """Defines the sleeping patterns of cats, as far as this model is concerned."""

    # Every state machine must declare the type of entity it is associated with.
    # The entity type is declared elsewhere in the model, and we are referencing it here.
    entity_type = Cat()

    class sleeping(State):
        @dymaction
        def entry_action(cat: Cat):
            time_ticks = action.get_time_ticks()
            action.log(f'cats sleeping at time = {time_ticks}')

    class awake(State):
        pass

State exit actions are defined similarly, but using a function named exit_action.

Within an entry or exit action, you use action statements to select sets of entities and then generate events on them and/or modify their entity attributes.

The argument to entry_action is an entity set. This is because events, which are what cause a transition resulting in a state being entered, are targeted at an entity set. The entry_action argument is the set of all entity instances entering a state together at some simulation time. It may contain one, many, or all of the entity instances in the simulation.

The syntax for the entry_action argument is as follows (similar for exit_action):

def entry_action(<ENTITY_SET_NAME>: <ENTITY_TYPE>):

You can use whatever name you want for ENTITY_SET_NAME. Then reference that name in the body of the entry_action. The ENTITY_TYPE should be the same type that you set with the state machine’s entity_type declaration.

Including the : <ENTITY_TYPE> is optional but recommended. You could instead do the following:

def entry_action(<ENTITY_SET_NAME>):

Event Type

Events are described here. In the model definition you define types of events. Using action statements you define the model logic that results in generation of events when the simulation runs. Each individual instance of an event is a realization of the event type.

Define state machine event types

State machines react to events. Part of the state machine model is a set of events that it will react to.

You define an event by creating a class that sub-classes from the Dymodetron Event class.

In the example below, we’re defining two event types tired_event and not_tired_event.

from dymodetron import Event, State, StateMachine

# An event type is defined by creating a class that sub-classes Event.
class tired_event(Event):
    pass

class not_tired_event(Event):
    pass

class minimal_cat_sleeping_eating_drinking(StateMachine):
    """Defines the sleeping patterns of cats, as far as this model is concerned."""

    # Every state machine must declare the type of entity it is associated with.
    # The entity type is declared elsewhere in the model, and we are referencing it here.
    entity_type = Cat()

    class sleeping(State):
        pass

    class awake(State):
        pass

State Transition

State transitions are described here.

A state transition is defined by the following:

  • event_type - the type of event that causes the transition.

  • source_state - if an entity is in this state when the event arrives, the transition will occur.

  • target_state - the state that an entity will go into after the transition.

Transitions are defined within the state transition table, see next section.

When a transition occurs, an entity leaves the source state and enters the target state. If the target state has a sub-state-machine, then that sub-state-machine will initialize and start responding to events.

State Transition Table

Every state machine has one state transition table. The state transition table is a list of state transitions.

Define state machine transition table

A transition table is defined within the state machine class by declaring a variable transitions and then using the Dymodetron Transitions and Transition constructs as illustrated in the example below.

from dymodetron import Event, State, StateMachine, Transition, Transitions

# An event type is defined by creating a class that sub-classes Event.
class tired_event(Event):
    pass

class not_tired_event(Event):
    pass

class minimal_cat_sleeping_eating_drinking(StateMachine):
    """Defines the sleeping patterns of cats, as far as this model is concerned."""

    # Every state machine must declare the type of entity it is associated with.
    # The entity type is declared elsewhere in the model, and we are referencing it here.
    entity_type = Cat()

    class sleeping(State):
        pass

    class awake(State):
        pass

    # Here, we define the transition table for state-machine 'cat_sleeping_eating_drinking'.
    #
    # The state machine initializes into the 'sleeping' state. Then, events toggle it back and forth
    # between 'sleeping' and 'awake'.
    transitions = Transitions([
        Transition(event_type=initial_event(),   source_state=initial_state(), target_state=sleeping()),
        Transition(           not_tired_event(),              sleeping(),                   awake()),
        Transition(           tired_event(),                  awake(),                      sleeping())
    ])
Initial state, initial event, initial transition

Every state machine has an initial state. You don’t define this state explicitly in your model, it is implied by the existence of the state machine. The initial state is represented by a black dot in a state machine diagram.

When a state machine initializes, it is in the initial state. At the time of state machine initialization, a special event fires, of type initial_event.

In your transition table, you include an entry that describes what state the state machine should go into the initial_event fires. This is called the initial transition. The initial transition should have event_type=initial_event() and source_state=initial_state().

See the transition table in the example above for an illustration of how to specify the initial transition.

State Tracking

When you simulate a model, you’ll often want to count how many entities enter a particular state over the course of the simulation, or examine how the number of entities in a state changes over time.

You can configure the model on a per-state basis to enable tracking of the metrics listed in the table below. When the simulation completes, it will write out a state tracking report describing the state tracking metrics for the enabled states.

State Tracking Options

State Tracking Option

Description

track_n

Track the number of entities in a state at each event time.

track_new

Track the number of entities newly entering a state at each event time.

track_cumulative

Track the total number of entities that entered a state over the course of a simulation.

You configure state tracking on a state by using the @dymodetrack decorator. You pass it the arguments in the table listed above in order to enable to respective state tracking feature.

In the example below, we have indicated that we want to enable track_cumulative and track_n on state awake.

from dymodetron import State, StateMachine, dymodetrack

class minimal_cat_sleeping_eating_drinking(StateMachine):
    """Defines the sleeping patterns of cats, as far as this model is concerned."""

    # Every state machine must declare the type of entity it is associated with.
    # The entity type is declared elsewhere in the model, and we are referencing it here.
    entity_type = Cat()

    class sleeping(State):
        pass

    @dymodetrack(track_cumulative=True, track_n=True)
    class awake(State):
        pass

Actions

Action statements are used to implement model logic in state entry and exit actions.

One key characteristic of Dymodetron action statements is that they operate on entity sets. For example, when you generate events, you generate events on all the entity instances an entity set. When you declare a variable, the variable contains a separate value for each entity instance in an entity set. When you assign to that variable, you can assign a different value for each entity instance. When you calculate expressions using that variable, the calculation is performed for all the entity instances, using the appropriate value for each one.

The table below lists the available action commands.

Dymodetron action statement commands

Action statement name

Action statement command

Description

Entity Match

action.entity_match()

Filter an entity set down to a subset of entities based on evaluation criteria.

Declare Entity Variable

action.declare()

Declare a variable that will hold a separate value for each entity instance in an entity set.

Assign to Entity Variable

action.assign()

Assign a value to an entity variable; can be a different value for each entity instance in the associated entity set.

Generate Events

action.generate_event_...()

Generate events targeted to entity instances in an entity set.

In State or Not?

action.in_state(), .not_in_state()

Evaluate whether entity instances in an entity set are in a particular state or not.

Last Time State Was Entered

action.get_last_state_entry_time_ticks()

Determine the last time a particular state was entered by entity instances in an entity set.

State Entry Counter

action.get_state_entry_counter()

Determine the number of times a state has been entered by any entity instance.

Call

action.call()

Call a function on all the entity instances in an entity set.

Measurements

action.measurements()

Evaluate metrics such as count, average, etc., on entity instances in an entity set matching provided criteria.

Lookup in Lookup Table

action.lookup()

Look up values in a lookup table, one value per entity instance in an entity set.

Get Current Simulation Time

action.get_time_ticks()

Retrieve the current simulation time.

Number Crunching (numpy)

numpy.*

Evaluate numerical computations involving entity attribute and entity variables. Computations are performed once for each entity instance in an entity set.

Entity Match

The entity_match action is described in the section on entity sets.

You use this action in a with .. as .. construct, as shown below.

from dymodetron import action

with action.entity_match(
    entity_type=<ENTITY TYPE>,                    # What entity type?
    entity_set=<INPUT ENTITY SET>,                # What entity set to start with?
    criteria=lambda E: <EXPRESSION INVOLVING E>   # Which entity instances should be included in the resultant entity set?
) as <RESULT ENTITY SET>:                         # The name of the resultant entity set follows the 'as' keyword.

    # Use <RESULT ENTITY SET> here.

The action.entity_match() arguments are described below.

entity_match arguments

Argument name

Description

entity_type

The type of entities in the entity set.

entity_set

The name of the entity set containing entity instances to evaluate.

criteria

Expression evaluating to True/False that determines which entity instances to include in the resultant entity set.

When using lambda expressions for criteria, the lambda argument name can be whatever you choose (it doesn’t have to be E).

In the example below, we have an entity_match that picks all cats whose height exceeds a given threshold.

from dymodetron import State, StateMachine, action

class minimal_cat_sleeping_eating_drinking(StateMachine):
    """Defines the sleeping patterns of cats, as far as this model is concerned."""

    # Every state machine must declare the type of entity it is associated with.
    # The entity type is declared elsewhere in the model, and we are referencing it here.
    entity_type = Cat()

    class sleeping(State):
        @dymaction
        def entry_action(cat: Cat):
            # The entry action takes an entity set as an argument.
            # In this case, we have named the entity set 'cat', and each entity instance
            # has entity type 'Cat'.

            # Select all the cats that are more than 24 inches tall.
            # The 'cat' entity set is provided to the action statement in which
            # this entity_match() is used, and now we are filtering down the cats within that entity set
            # based on the value of the entity attribute 'height_inches'.
            with action.entity_match(
                entity_type=Cat(),                            # What entity type?
                entity_set=cat,                               # What entity set to start with?
                criteria=lambda c: c.height_inches > 24.0     # Which entity instances should be included in the resultant entity set?
            ) as giant_cat:                                   # The name of the resultant entity set follows the 'as' keyword.

                # In this section, you can refer to 'giant_cat' anywhere an entity set is called for.
                # It will refer to all entities matching the criteria you provided above.

                # Generate hungry event on giant cats at 5 time ticks in the future.
                action.generate_event_rel(
                    entity_type=Cat(),
                    entity_set=giant_cat,
                    event=hungry_event(),
                    time_ticks_rel=5
                )

Note

What is the argument that gets passed to the criteria function?

The argument that gets passed to the criteria function is an entity set. It is actually the same entity set that you pass to the entity_set argument in your entity_match() invocation.

However, in the criteria expression, you treat this argument as an individual object. For example, as shown above, we access an entity attributes using ‘dot’ notation: criteria=lambda c: c.height_inches > 24.0.

What is happening here is that Dymodetron sets things up under the hood so that you can write the code as if you are examining a single entity instance’s attributes. Behind the scenes, Dymodetron will apply the criteria evaluation to all of the entities in the provided entity_set.

Similarly, when you use the numpy and Dymodetron action statements described in the last section, Dymodetron will apply these across the entire set of instances in the input entity_set.

When you are writing the criteria, you write it as if you are examining a single entity instance. The Dymodetron action syntax is set up to make this the case, so that the criteria expressions can be easily read and written without worrying about the complexity of managing and iterating through collections of entity instances. All that is done behind the scenes.

The main thing in keep in mind is that it’s really an entire set of entities that are being operated on when these criteria expressions are evaluated, even though the criteria expressions are written and read as if a single entity is being evaluated.

Entity Variables

In action statements, it is often necessary to perform intermediate calculations in the process of generating events or modifying entity attributes. For example, we may need to perform unit conversions or other arithmetic on entity attributes before using them.

Entity variables are variables that store a value for every entity instance in an entity set. Entity variables are always declared and used in the context of some entity set.

When the value of an entity variable is calculated, it is calculated for each entity in the entity set.

Entity variables are similar to entity attributes: they have a name, a type, and a separate value for each entity instance. The main difference between entity variables and entity attributes is that entity variables are transient and only exist in the scope of the action statement where they are declared. Entity attributes, on the other hand, are attached to the entity instances and continue to ‘exist’ and maintain state irrespective of any state or state machine (although their values are typically read and written by state entry/exit actions).

A more subtle distinction between entity variables and entity attributes is that every entity instance in a simulation has a value for every defined entity attribute. On the other hand, entity variables are defined in terms of an entity set which may or may not include every entity instance in the simulation. The relationship between entity variables and entity sets is described further in the sections below.

There are three actions you can perform with entity variables: declare entity variable, assign to entity variable, and access entity variable values. These are listed in the table below with links to additional information.

Entity variable actions

Action

Description

Declare Entity Variable

Declare an entity variable (name and data type) in association with a given entity set.

Assign to Entity Variable

Assign values to an entity variable, separate values associated with each entity instance in the entity set.

Access Entity Variable Value

Retrieve the values of an entity variable to be used in Dymodetron expressions.

In the example below, we modify the example from the last section, and convert the cat height from inches to feet before using it. We implement a crude conversion that rounds down the the nearest integer number of feet. This conversion calculation is calculated separately for each entity instance in entity_set=cat.

We then modify the entity_match to use the converted value in determining which cats to include in the giant_cat entity set. This is done using the expression .value(...) tacked onto the entity variable name, within the criteria expression.

The expressions action.declare(), action.assign(), and .value() are described further in the following sections.

from dymodetron import State, StateMachine, action

class minimal_cat_sleeping_eating_drinking(StateMachine):
    """Defines the sleeping patterns of cats, as far as this model is concerned."""

    entity_type = Cat()

    class sleeping(State):
        @dymaction
        def entry_action(cat: Cat):

            # For each entity instance in entity_set=cat, we'll have a value in variable height_feet,
            # initialized to 0.0 for each entity instance.
            height_feet = action.declare(entity_set=cat, var_type=numpy.float, default_value=0.0)

            # For each entity instance in entity_set=cat, we'll calculate a value in variable height_feet.
            # We convert to feet, and roud the result down to the next lowest integer.
            action.assign(entity_set=cat, entity_var=height_feet, expr=lambda c: numpy.floor(c.height_feet / 12.0))

            # Select all the cats that are more than 2 feet tall, per the conversion above.
            # The 'cat' entity set is provided to the action statement in which
            # this entity_match() is used, and now we are filtering down the cats within that entity set
            # based on the value of the entity variable 'height_feet'.
            with action.entity_match(
                entity_type=Cat(),                            # What entity type?
                entity_set=cat,                               # What entity set to start with?
                criteria=lambda c: height_feet.value(c) > 2.0 # Which entity instances should be included in the resultant entity set?
            ) as giant_cat:                                   # The name of the resultant entity set follows the 'as' keyword.

                ...
Declare Entity Variable

An entity variable is declared with an expression of the following form:

from dymodetron import action

<ENTITY_VARIABLE_NAME> = action.declare(entity_set, var_type, default_value)

The result is an entity variable. The entity variable is a set of values, one for each entity instance in the provided entity set. Each value will be equal to the provided default_value, until the entity variable is assigned to using action.assign().

The action.declare() arguments are described below.

declare arguments

Argument name

Description

entity_set

The entity set for which we are creating an entity variable that will store a separate value for each entity instance.

var_type

The type of the variable. The supported types are the numpy types.

default_value

The default value to be used for each entity instance, if an assignment doesn’t set it.

Assign to Entity Variable

Typically your model will assign values to entity variables. When assigning values to an entity variable you specify the subset (one, many, or all) of entity instances that go along with the values to assign. Values can be assigned by providing a constant, an expression, or a random distribution. These options are described further below.

Entity variable assignment options

Assignment type

Description

Value

Provide a literal constant value to be used for all the entity instances.

Expression

Provide an expression that will be calculated independently for each entity instance.

Random Distribution

Provide a random distribution that will be sampled once for each entity instance.

Assign to Entity Variable - Value

You can assign a fixed scalar value to an entity variable. This results in each entity instance in the entity variable’s entity set having the same value for the variable.

An entity variable is assigned in ‘value’ mode with an expression of the following form:

from dymodetron import action

action.assign(entity_set, entity_variable, value=<VALUE>)
Arguments for entity variable assignment from value

Argument name

Description

entity_set

The name of the entity set for which entity instances should be assigned variable values. This must be a subset of the entity_set originally provided to action.declare() when creating the entity variable.

entity_variable

The entity variable that was returned from the call to action.declare().

<VALUE>

A scalar value. This value become the entity variable associated with each entity instance in entity_set.

In the example below, we declare an entity variable heart_rate_bpm. This entity variable has a separate value for each entity instance in the entity set cat. The values are floating point numbers initialized to 80.

Then, we use action.entity_match() to select the subset of all cats in state sleeping, and assign a different value for heart_rate_bpm for those cats.

from dymodetron import action

# Declare an entity variable.
#
# The name of the entity variable is on the left-hand side of the equal sign.
#
# The entity set 'cat' comes from the surrounding context.
#
# The variable type is floating point number.
#
# The entity variable will contain a separate value for each entity instance in entity set 'cat'.
#
# To start with, the value of 'heart_rate_bpm' for all the entity instances will be 80.
heart_rate_bpm = action.declare(entity_set=cat, var_type=numpy.float, default_value=80)

# Select all cats that are in state 'sleeping'.
with action.entity_match(
        entity_type=Cat(),
        entity_set=cat,
        criteria=lambda c: action.in_state(c, minimal_cat_sleeping_eating_drinking.sleeping())
) as sleeping_cat:

    # Assign value 40 to 'heart_rate_bpm' for all sleeping cats.
    action.assign(entity_set=sleeping_cat, entity_var=heart_rate_bpm, value=40)
Assign to Entity Variable - Expression

You can assign an entity variable based on an expression. The expression is evaluated separately for each entity instance in the entity variable’s entity set. Usually you’ll use entity attributes in the expression. Each individual entity instance’s value for the entity attribute is used, resulting in potentially different resulting entity variable values for each entity instance.

An entity variable is assigned in ‘expression’ mode as follows:

from dymodetron import action

action.assign(entity_set, entity_variable, expr=lambda E: <EXPRESSION INVOLVING E>)
Arguments for entity variable assignment from expression

Argument name

Description

entity_set

The name of the entity set for which entity instances should be assigned variable values. This must be a subset of the entity_set originally provided to action.declare() when creating the entity variable.

entity_variable

The entity variable that was returned from the call to action.declare().

<EXPRESSION>

An expression involving entity attributes.

Note

What is this lambda thing again?

As discussed in the section on entity match criteria, the python lambda syntax is a way of defining functions in-line in the code. For action.assign(), this makes it so that we can specify the expression for calculating entity variable values right there in the assignment expression. You can learn more about lambda expressions in the python documentation. The short version is lambdas are “syntactic sugar for a normal function definition”.

In your lambda expression, the lambda argument name can be whatever you choose (it doesn’t have to be E).

Similar to the criteria argument for action.entity_match, the <EXPRESSION INVOLVING E> can access entity attributes.

In the example below, assume that cats have an entity attribute weight_lbm. We assign cat heart rate using a calculation based on cat weight. For each entity instance in entity set cat, the associated value for heart_rate_bpm will be calculated using that entity instance’s individual entity attribute value for weight_lbm.

from dymodetron import action

# Declare an entity variable.
#
# The name of the entity variable is on the left-hand side of the equal sign.
#
# The entity set 'cat' comes from the surrounding context.
#
# The variable type is floating point number.
#
# The entity variable will contain a separate value for each entity instance in entity set 'cat'.
#
# To start with, the value of 'heart_rate_bpm' for all the entity instances will be 80.
heart_rate_bpm = action.declare(entity_set=cat, var_type=numpy.float, default_value=80)

# Assign heart rate based on weight: 40 bpm plus 1/2 bpm per pound.
action.assign(
    entity_set=cat,
    entity_var=heart_rate_bpm,
    expr=lambda c: 40.0 + (c.weight_lbm * 0.5)
)
Assign to Entity Variable - Distribution

You can assign to an entity variable from a random distribution. The random distribution is sampled separately for each entity instance in the entity variable’s entity set.

Recall that random distributions are defined in your model parameters block.

An entity variable is assigned in ‘random distribution’ mode with an expression of the following form:

from dymodetron import action

action.assign(entity_set, entity_variable, dist=ModelParameters.<DISTRIBUTION_NAME>)
Arguments for entity variable assignment from random distribution

Argument name

Description

entity_set

The name of the entity set for which entity instances should be assigned variable values. This must be a subset of the entity_set originally provided to action.declare() when creating the entity variable.

entity_variable

The entity variable that was returned from the call to action.declare().

ModelParameters

The name of the class you’ve used to define your model parameters. If you used a different name for your model parameters class, use that name instead.

<DISTRIBUTION_NAME>

The name of a random distribution that you have defined in your model parameters block.

In the example below, we define a random distribution for heart rate in our model parameters block. Then, we use the distribution in the assignment to the entity variable. Each entity instance in the entity set cat gets a separately sampled value from the distribution heart_rate_bpm_dist defined in ModelParameters.

from dymodetron import Params, random as dyrandom, action

##########################################################################################
# Define model parameters.
##########################################################################################
class ModelParameters(Params):

    # A random distribution to be used for cat heart rates:
    # normal distribution with mean = 60, standard deviation = 10.
    heart_rate_bpm_dist = dyrandom.NormalDistribution(
        mean=60.0,
        std=10.0
    )

# ...
# ... later in model, in an action statement ...
# ...

# Declare an entity variable.
#
# The name of the entity variable is on the left-hand side of the equal sign.
#
# The entity set 'cat' comes from the surrounding context.
#
# The variable type is floating point number.
#
# The entity variable will contain a separate value for each entity instance in entity set 'cat'.
#
# To start with, the value of 'heart_rate_bpm' for all the entity instances will be 80.
heart_rate_bpm = action.declare(entity_set=cat, var_type=numpy.float, default_value=80)

# Assign heart rate based on random distribution from model parameters.
# Each entity instance in entity set 'cat' gets a sampled value from the referenced
# distribution.
action.assign(
    entity_set=cat,
    entity_var=heart_rate_bpm,
    dist=ModelParameters.heart_rate_bpm_dist
)
Access Entity Variable Value

After going to all the work of declaring entity variables and assigning to entity variables, you usually want to use the values for something.

Recall that entity variables are associated with an entity set, and that they store a separate value for each entity instance in the entity set. When you use an entity variable, you use it in the context of an expression involving the entity variable’s entity set, or a subset of it.

Entity variable values are accessed as follows:

from dymodetron import action

# Access entity variable values in the context of some expression.
... <ENTITY_VARIABLE_NAME>.value(<ENTITY_SET>) ...
Arguments for accessing entity variable values

Argument name

Description

<ENTITY_VARIABLE_NAME>

The name you gave the entity variable when you declared it.

<ENTITY_SET>

An entity set containing the entity instances for which you wish to access the variable values. Must be a subset of the entity set you provided when you declared the entity variable.

In the example below, we update the last example by picking out cats where the value of entity variable heart_rate_bpm is less than some threshold, and then generate an event just on those cats. In this example, we are looking at entity variable values for all the entities in the entity set that we used when we declared the entity variable.

from dymodetron import Params, random as dyrandom, action

##########################################################################################
# Define model parameters.
##########################################################################################
class ModelParameters(Params):

    # A random distribution to be used for cat heart rates:
    # normal distribution with mean = 60, standard deviation = 10.
    heart_rate_bpm_dist = dyrandom.NormalDistribution(
        mean=60.0,
        std=10.0
    )

# ...
# ... later in model, in an action statement ...
# ...

# Declare an entity variable.
#
# The name of the entity variable is on the left-hand side of the equal sign.
#
# The entity set 'cat' comes from the surrounding context.
#
# The variable type is floating point number.
#
# The entity variable will contain a separate value for each entity instance in entity set 'cat'.
#
# To start with, the value of 'heart_rate_bpm' for all the entity instances will be 80.
heart_rate_bpm = action.declare(entity_set=cat, var_type=numpy.float, default_value=80)

# Assign heart rate based on random distribution from model parameters.
# Each entity instance in entity set 'cat' gets a sampled value from the referenced
# distribution.
action.assign(
    entity_set=cat,
    entity_var=heart_rate_bpm,
    dist=ModelParameters.heart_rate_bpm_dist
)

# Pick out the cats with a low heart rate.
with action.entity_match(
        entity_type=Cat(),
        entity_set=cat,
        criteria=lambda c: heart_rate_bpm_dist.value(c) < 30
) as cat_with_low_heart_rate:

    # Generate event on cats with low heart rate.
    cat_with_low_heart_rate.generate_event_rel(
        entity_type=Cat(),
        entity_set=cat_with_low_heart_rate,
        event=tired_event(),
        time_ticks_rel=0,
    )

Sometimes you will access entity variable values on just a subset of entities. In the example below, we assign heart_rate_bpm on all the cats, but we access the value only for the sleeping subset of cats.

from dymodetron import Params, random as dyrandom, action

##########################################################################################
# Define model parameters.
##########################################################################################
class ModelParameters(Params):

    # A random distribution to be used for cat heart rates:
    # normal distribution with mean = 60, standard deviation = 10.
    heart_rate_bpm_dist = dyrandom.NormalDistribution(
        mean=60.0,
        std=10.0
    )

# ...
# ... later in model, in an action statement ...
# ...

# Declare an entity variable.
#
# The name of the entity variable is on the left-hand side of the equal sign.
#
# The entity set 'cat' comes from the surrounding context.
#
# The variable type is floating point number.
#
# The entity variable will contain a separate value for each entity instance in entity set 'cat'.
#
# To start with, the value of 'heart_rate_bpm' for all the entity instances will be 80.
heart_rate_bpm = action.declare(entity_set=cat, var_type=numpy.float, default_value=80)

# Assign heart rate based on random distribution from model parameters.
# Each entity instance in entity set 'cat' gets a sampled value from the referenced
# distribution.
action.assign(
    entity_set=cat,
    entity_var=heart_rate_bpm,
    dist=ModelParameters.heart_rate_bpm_dist
)

# Select all cats that are in state 'sleeping'.
with action.entity_match(
        entity_type=Cat(),
        entity_set=cat,
        criteria=lambda c: action.in_state(c, minimal_cat_sleeping_eating_drinking.sleeping())
) as sleeping_cat:

    # Pick out the sleeping cats with a high heart rate.
    with action.entity_match(
            entity_type=Cat(),
            entity_set=sleeping_cat,
            criteria=lambda c: sleeping_cat.value(c) >= 30
    ) as sleeping_cat_with_high_heart_rate:

        # Generate event on sleeping cats with high heart rate.
        sleeping_cat_with_high_heart_rate.generate_event_rel(
            entity_type=Cat(),
            entity_set=sleeping_cat_with_high_heart_rate,
            event=not_tired_event(),
            time_ticks_rel=0,
        )

Generate Events

Events are described here. The action statements for generating events are listed below.

Event generating action statements.

Action

Action statement command

Description

Generate event

generate_event_rel, generate_event_abs

Generate events on entities at a given simulation time.

Generate event w/ binomial probability

generate_event_binomial_rel, generate_event_binomial_abs

Generate events, or not, on entities at a given simulation time, according to a binomial probability distribution with given parameters.

Generate event w/ given probability

generate_event_trials_rel, generate_event_trials_abs

Generate events, or not, on entities with a given probability for each entity.

Generate event

Events are generated at an absolute simulation time with expressions of the following form:

from dymodetron import action

# ... within entry or exit action ...

# Generate events at absolute sim time.
action.generate_event_abs(
    entity_type=<ENTITY_TYPE>(),
    entity_set=<ENTITY_SET>,
    event=<EVENT_TYPE>(),
    time_ticks_abs=<SIMULATION_TIME_TICKS>
)

# Generate events at sim time, relative to sim time at which this action is executed.
action.generate_event_rel(
    entity_type=<ENTITY_TYPE>(),
    entity_set=<ENTITY_SET>,
    event=<EVENT_TYPE>(),
    time_ticks_rel=<SIMULATION_TIME_TICKS>
)

Note the parentheses () following the entity type and the event type.

Arguments for generate_event_...

Argument name

Description

entity_type

The type of entity you are generating events on.

entity_set

The set of entity instances (one, many, or all) that should receive the event.

event

The type of event to generate. This should reference an event type that you defined elsewhere in the model definition.

time_ticks_abs

Applicable only to generate_event_abs(). The absolute simulation time at which to generate the event, measured in ticks. Can also be an array of times, one for each entity instance in entity_set.

time_ticks_rel

Applicable only to generate_event_rel(). The relative simulation time at which to generate the event. Relative to the simulation time at which the action was executed. Can also be an array of times, one for each entity instance in entity_set.

Generate event w/ binomial probability

Sometimes you may want to generate an event probabilistically on a set of entities, where a random process determines whether or not each entity gets an event or not. With this action statement, for each entity instance in entity_set, a random draw from a binomial distribution will be taken. For a given entity instance, if the “success” outcome is achieved, then event_a will be triggered for that entity instance. If the “failure” outcome is achieved, then event_b will be triggered instead. You can also set event_b=None, in which case “failure” outcomes result in no event being scheduled for the associated entity instance.

from dymodetron import action

# ... within entry or exit action ...

# Generate events probabilistically at absolute simulation time.
action.generate_event_binomial_abs(
    entity_type=<ENTITY_TYPE>(),
    entity_set=<ENTITY_SET>,
    event_a=<EVENT_TYPE>(),
    event_b=<EVENT_TYPE>(),
    p_a=<PROBABILITY>,
    time_ticks_abs=<SIMULATION_TIME_TICKS>
)


# Generate events probabilistically at sim time, relative to sim time at which this action is executed.
action.generate_event_binomial_rel(
    entity_type=<ENTITY_TYPE>(),
    entity_set=<ENTITY_SET>,
    event_a=<EVENT_TYPE>(),
    event_b=<EVENT_TYPE>(),
    p_a=<PROBABILITY>,
    time_ticks_rel=<SIMULATION_TIME_TICKS>
)
Arguments for generate_event_binomial_...

Argument name

Description

entity_type

The type of entity you are generating events on.

entity_set

The set of entity instances (one, many, or all) that should receive events.

event_a

The type of event to generate for “success” outcome of n trials with probability p_a. This should reference an event type that you defined elsewhere in the model definition.

event_b

The type of event to generate for “failure” outcome of n trials with probability p_a. Can also be None, in which case no event is generated in the “failure” outcome case.

p_a

The binomial distribution ‘probability’ parameter.

n

The binomial distribution ‘number of trials’ parameter. (NOT CURRENTLY IMPLEMENTED, hard-coded to n=1, sorry!).

time_ticks_abs

Applicable only to generate_event_binomial_abs. The simulation time at which to generate the events, measured in ticks. Can also be an array of times, one for each entity instance in entity_set.

time_ticks_rel

Applicable only to generate_event_binomial_rel. The simulation time at which to generate the events, measured in ticks. Can also be an array of times, one for each entity instance in entity_set.

Generate event w/ given probability

This action is similar to the last section, but instead of drawing from the same distribution for each entity instance, you explicitly provide the probabilities yourself for each entity instance as an array argument.

from dymodetron import action

# ... within entry or exit action ...

# Generate events probabilistically at absolute simulation time.
action.generate_event_trials_abs(
    entity_type=<ENTITY_TYPE>(),
    entity_set=<ENTITY_SET>,
    event=<EVENT_TYPE>(),
    p=<PROBABILITY>,
    time_ticks_abs=<SIMULATION_TIME_TICKS>
)


# Generate events probabilistically at sim time, relative to sim time at which this action is executed.
action.generate_event_trials_rel(
    entity_type=<ENTITY_TYPE>(),
    entity_set=<ENTITY_SET>,
    event=<EVENT_TYPE>(),
    p=<PROBABILITY>,
    time_ticks_rel=<SIMULATION_TIME_TICKS>
)
Arguments for generate_event_trials_...

Argument name

Description

entity_type

The type of entity you are generating events on.

entity_set

The set of entity instances (one, many, or all) that should receive events.

event

The type of event to generate with probability p. This should reference an event type that you defined elsewhere in the model definition.

p

Array of probabilities of event being generated, one entry for each individual entity instance.

time_ticks_abs

Applicable only to generate_event_trials_abs. The simulation time at which to generate the events, measured in ticks. Can also be an array of times, one for each entity instance in entity_set.

time_ticks_rel

Applicable only to generate_event_trials_rel. The simulation time at which to generate the events, measured in ticks. Can also be an array of times, one for each entity instance in entity_set.

In State or Not?

These actions are used in the criteria clause of the entity_match expression. They let you you determine whether entity instances are in a specified state.

from dymodetron import action

# ... within entry or exit action ...

with action.entity_match(
        entity_type=<ENTITY_TYPE>,
        entity_set=<ENTITY_SET>,
        criteria=lambda e: action.in_state(e, <STATE_NAME>)
) as entities_in_state:

    # Do something with entities_in_state ...

You can also check if an entity instance is not in a specified state:

from dymodetron import action

# ... within entry or exit action ...

with action.entity_match(
        entity_type=<ENTITY_TYPE>,
        entity_set=<ENTITY_SET>,
        criteria=lambda e: action.not_in_state(e, <STATE_NAME>())
) as entities_not_in_state:

    # Do something with entities_not_in_state ...

State Entry Counter

This action provides the number of times that that a specified state has been entered, for each entity instance in a specified entity set.

from dymodetron import action

with action.entity_match(
    entity_type=<ENTITY_TYPE>(),
    entity_set=<ENTITY_SET>,
    criteria=lambda e: action.get_state_entry_counter(e, <STATE_NAME>()) > 0
) as entities_that_have_entered_state:

    # Do something with entities_that_have_entered_state ...

Last Time State Was Entered

This action provides the last time that that a specified state was entered, for each entity instance in a specified entity set.

from dymodetron import action

with action.entity_match(
    entity_type=<ENTITY_TYPE>(),
    entity_set=<ENTITY_SET>,
    criteria=lambda e: action.get_state_entry_counter(e, <STATE_NAME>()) > 0
) as entities_that_have_entered_state:

    # Get the current time (a scalar).
    t_now = action.get_time_ticks()

    # Get the last time STATE_NAME was entered by the entities in question.
    _, t_last_state_entry_time = action.get_last_state_entry_time_ticks(entities_that_have_entered_state, <STATE_NAME>())

    # Declare entity variable to store result of calculation for each entity instance.
    time_since_last_state_entry = action.declare(entity_set=entities_that_have_entered_state, var_type=numpy.float)

    # Calculate the simulation time that has passed since these entities were last in the state.
    time_since_last_state_entry.assign(
        entity_set=entities_that_have_entered_state,
        entity_var=time_since_last_state_entry,
        # This expression relies on the numpy behavior of 'broadcasting' the scalar 't_now'
        # so that we take the different between t_now and the value of each entry in t_last_state_entry_time.
        expr=lambda e: numpy.maximum(1, t_now - t_last_state_entry_time)
    )

Call

It may be useful sometimes to carve a set of action statements out into its own function. Once you’ve done that, you can call the function from an action statement using the call action. This is done with expressions of the following form:

from dymodetron import action

# ... within entry or exit action ...

action.call(<ENTITY_SET>, <FUNCTION>)
Arguments for call()

Argument name

Description

ENTITY_SET

The entity set to pass to the function.

FUNCTION

The function to call.

from dymodetron import action, dymaction

# ... within a state machine definition ...

# In this example model, we have a state that a cat gets into when it jumps.
class jump(State):
    @dymaction
    def entry_action(cat: Cat):
        action.call(cat, update_cat_energy)

# A function consisting of action statements on an entity set.
@dymaction
def update_cat_energy(cat: Cat):
    # Calculate the energy of each entity instance in entity set 'cat',
    # and save the value to entity attributes.
    cat.energy = 0.5 * cat.mass_lbm * numpy.power(cat.velocity, 2)

A function that will be called by the call statement must take a single argument: an entity set that it will operate on. The function also needs the @dymaction decorator applied to it, similar to state entry and exit actions.

Measurements

These actions are for quantifying aggregate metrics across entity instances at the simulated time that the action statements are executed.

You create measurements with action statements as illustrated in the example below.

from dymodetron import action

# ... within entry or exit action ...

with action.measurements(
    entity_type=Cat(),
    entity_set=cat
) as measuring_cats:

    # Count the number of cats under a given weight.
    # The measurement will be attached to the 'measuring_cats' object.
    measuring_cats.count(
        # The name of the measurement.
        'Number of light cats',
        # The criteria that must be satisfied to be counted.
        criteria=lambda c: c.weight_lbm < 40.0
    )

    # Measure the mean cat height.
    measuring_cats.mean(
        # The name of the measurement.
        'Mean cat height (inches)',
        # Take the mean of the weight.
        value=lambda c: c.height_inches
        # No criteria provided, means use all the cats in the entity set provided
        # in the original expression above (entity_set=cat).
    )

    # Measure the mean cat height, but only the light cats.
    measuring_cats.mean(
        # The name of the measurement.
        'Mean height (inches) of light cats',
        # Take the mean of the weight.
        value=lambda c: c.height_inches
        # Only the entity instances that match this criteria will be included in taking the mean.
        criteria=lambda c: c.weight_lbm < 40.0
    )

    # Apply a numpy function. In this case we're getting the median height of light cats.
    measuring_cats.apply(
        # The name of the measurement.
        'Median height (inches) of light cats',
        # Take the mean of the weight.
        value=lambda c: c.height_inches,
        f=numpy.median
        # The criteria that must be satisfied to be included in taking the median.
        criteria=lambda c: c.weight_lbm < 40.0
    )

    # Count and store the number of heavy cats.
    heavy_cat_count = measuring_cats.count(
        # The name of the measurement.
        'Number of heavy cats',
        # The criteria that must be satisfied to be counted.
        criteria=lambda c: c.weight_lbm >= 40.0
    )

    # Count and store the total number of cats.
    total_cat_count = measuring_cats.count(
        # The name of the measurement.
        'Number of cats',
        # The criteria that must be satisfied to be counted.
        # criteria=None means count them all.
        criteria=None
    )

    # This is the same as the last one, but a different way to do it.
    # Count and store the total number of cats.
    total_cat_count = measuring_cats.count(
        # The name of the measurement.
        'Number of cats',
        # Not providing any criteria means count them all.
    )

    # Store the proportion of heavy cats as a measurement.
    measuring_cats.attach(
        'Proportion of heavy cats',
        heavy_cat_count / total_cat_count
    )

    # Now log the measurements.
    action.log(measuring_cats)

The last statement above writes the measurements to the log. For this example, the output might be as follows:

{'Number of light cats': 2946,
 'Mean cat height (inches)': 13.2,
 'Mean height (inches) of light cats': 11.9,
 'Median height (inches) of light cats': 8.4,
 'Number of heavy cats': 2054,
 'Number of cats': 5000,
 'Proportion of heavy cats': 0.4108,
}
Measurement functions

Measurement function name

Description

count

Count the number of entity instances matching some provided criteria.

mean

Take the mean of an expression involving entity attributes, optionally subject to some criteria.

nanmean

Take the mean of an expression involving entity attributes, optionally subject to some criteria, ignoring nans.

apply

Use a provided function to calculate an aggregate measurement.

attach

Attach your own computed value to the measurements object.

Creating measurement object

You create the measurement object with expressions of the following form.

from dymodetron import action

# ... within entry or exit action ...

with action.measurements(
    entity_type=<ENTITY_TYPE>>,
    entity_set=<ENTITY_SET>
) as <MEASUREMENT_OBJECT>:

    # ... use <MEASUREMENT_OBJECT> ...
Arguments for measurements()

Argument name

Description

entity_type

The type of entity to measure.

entity_set

The entity set containing the entity instances to measure.

See example.

Count
from dymodetron import action

# ... within entry or exit action ...

with action.measurements(
    entity_type=<ENTITY_TYPE>>,
    entity_set=<ENTITY_SET>
) as <MEASUREMENT_OBJECT>:

    # Count the number of entity instances in <ENTITY_SET> that match
    # the criteria.
    count_value = <MEASUREMENT_OBJECT>.count(
        label=<STRING>,
        criteria=lambda E: <EXPRESSION_INVOLVING_E>
    )
Arguments for count()

Argument name

Description

label

The string to use to label the measurement.

criteria

A boolean expression on an entity indicating whether or not the entity should be counted.

See example.

Capturing the return value count_value is optional.

Mean
from dymodetron import action

# ... within entry or exit action ...

with action.measurements(
    entity_type=<ENTITY_TYPE>>,
    entity_set=<ENTITY_SET>
) as <MEASUREMENT_OBJECT>:

    # Calculate the mean of 'value' across all entity instances in <ENTITY_SET> that match
    # the criteria.
    mean_value = <MEASUREMENT_OBJECT>.mean(
        label=<STRING>,
        value=lambda E: <EXPRESSION_INVOLVING_E>,
        criteria=lambda E: <EXPRESSION_INVOLVING_E>
    )
Arguments for mean()

Argument name

Description

label

The string to use to label the measurement.

value

An expression returning the value to use for each entity instance. The measurement calculates the mean of this value over all entity instances that satisfy criteria.

criteria

A boolean expression on an entity indicating whether or not the entity should be included in the calculation.

See example.

Capturing the return value mean_value is optional.

Nanmean

Same as mean, but ignores nan values in the calculation.

from dymodetron import action

# ... within entry or exit action ...

with action.measurements(
    entity_type=<ENTITY_TYPE>>,
    entity_set=<ENTITY_SET>
) as <MEASUREMENT_OBJECT>:

    # Calculate the mean of 'value' across all entity instances in <ENTITY_SET> that match
    # the criteria.  Ignore an entity instance when 'value' is nan.
    mean_value = <MEASUREMENT_OBJECT>.nanmean(
        label=<STRING>,
        value=lambda E: <EXPRESSION_INVOLVING_E>,
        criteria=lambda E: <EXPRESSION_INVOLVING_E>
    )

See example.

Apply

Apply a function of your choice to calculate the aggregate. The function should take an array-like of numbers and return a scalar value.

from dymodetron import action

# ... within entry or exit action ...

with action.measurements(
    entity_type=<ENTITY_TYPE>>,
    entity_set=<ENTITY_SET>
) as <MEASUREMENT_OBJECT>:

    # Apply 'f' to the array of 'value' across all entity instances matching 'criteria'.
    measurement_value = <MEASUREMENT_OBJECT>.apply(
        label=<STRING>,
        value=lambda E: <EXPRESSION_INVOLVING_E>,
        f=<FUNCTION>,
        criteria=lambda E: <EXPRESSION_INVOLVING_E>
    )
Arguments for apply()

Argument name

Description

label

The string to use to label the measurement.

value

An expression returning the value to use for each entity instance. The measurement calculates the mean of this value over all entity instances that satisfy criteria.

f

A function taking an array-like of numbers, and returning a scalar. The resulting measurement is the calculation of this function being passed an array-like containing value for all entity instances that satisfy criteria.

criteria

A boolean expression on an entity indicating whether or not the entity should be included in the calculation.

See example.

Capturing the return value measurement_value is optional.

Attach

Sometimes you may want to perform some intermediate calculations and attach them to the measurement object. For example, you may calculate a ratio of counts, and add that as a labeled measurement.

from dymodetron import action

# ... within entry or exit action ...

with action.measurements(
    entity_type=<ENTITY_TYPE>>,
    entity_set=<ENTITY_SET>
) as <MEASUREMENT_OBJECT>:

    # ... calculate a scalar value, usually out of other measurements ...
    v = 1 + 2

    # Attach 'v'
    <MEASUREMENT_OBJECT>.attach(
        label=<STRING>,
        value=v
    )
Arguments for apply()

Argument name

Description

label

The string to use to label the measurement.

value

A scalar value to store as a measurement.

See example.

Lookup in Lookup Table

If you’ve defined a lookup table in your model, you can look up values in it using action statements of the following form. This assigns a looked-up ‘output’ entity attribute value for each entity instance in the given entity set, given the input lookup_values.

from dymodetron import action

# ... within entry or exit action ...

# Assign entity attribute from lookup table.
<ENTITY_SET>.<OUTPUT_ENTITY_ATTRIBUTE> = action.lookup(
    lookup_values=<ENTITY_SET>.<LOOKUP_ENTITY_ATTRIBUTE>,
    lookup_table=<LOOKUP_TABLE>
)

Here’s an example.

Alternatively, you can assign the lookup output to an entity variable, with the following.

from dymodetron import action

# Assign entity variable from lookup table.
my_variable = action.declare(entity_set=<ENTITY_SET>, var_type=<VAR_TYPE>)

action.assign(
    entity_set=<ENTITY_SET>,
    entity_var=my_variable,
    values=action.lookup(<ENTITY_SET>.<LOOKUP_ENTITY_ATTRIBUTE>, <LOOKUP_TABLE>)
)

You can also use an entity variable as the input to the lookup, as follows.

from dymodetron import action

# Assign entity variable from lookup table.
my_variable = action.declare(entity_set=<ENTITY_SET>, var_type=<VAR_TYPE>)

# Suppose model parameters has a random distribution defined named 'my_random_distribution'.
# We'll assign to my_variable by sampling from the random distribution.
action.assign(
    entity_set=<ENTITY_SET>,
    entity_var=my_variable,
    dist=ModelParameters.my_random_distribution
)

# Now assign to an entity attribute using looked-up values with 'my_variable' as the input.
<ENTITY_SET>.<OUTPUT_ENTITY_ATTRIBUTE> = action.lookup(my_variable.value(), <LOOKUP_TABLE>)

Get Current Simulation Time

You can retrieve the current simulation time with the following expression.

from dymodetron import action

# Retrieve current simulation time.
t_now_ticks = action.get_time_ticks()

# ... use 't_now_ticks' in expression ...

Here’s an example.

Number Crunching (numpy)

Often your model will need to calculate numerical expressions involving entity attributes and entity variables. You can do this using numpy mathematical functions.

Some examples are shown below.

from dymodetron import action, State, dymaction

# In this example model, we have a state that a cat gets into when it jumps.
class jump(State):
    @dymaction
    # This is the entry action for the state 'jump'.
    def entry_action(cat: Cat):

        # Put a floor on cat altitude.
        capped_altitude_feet = numpy.max(1, cat.altitude_feet)

        # Calculate the air pressure for all the cats.
        air_pressure_pascals = 101325 * numpy.power(1 - 2.25577e-5 * cat.altitude_feet, 5.25588)

        # Calculate the cats' kinetic energy.
        cat_energy = 0.5 * cat.mass_kg * numpy.power(cat.velocity_m_s, 2)

Full minimal state machine example

Below is a full minimal example of a model with a state machine. This example illustrates the use of states, events, and transitions. This example does not include state entry/exit actions, action statements, model parameters, or sub-state-machines.

from dymodetron import \
    EntityType, \
    StateMachine, \
    ModelDescription, \
    Params, \
    State, \
    Event, \
    Transition, \
    Transitions, \
    initial_state, \
    initial_event

class minimal_cat(ModelDescription):
    # A model description just has a docstring explaining what you are up to with this model.
    """We model the behaviors of a certain kind of cat."""


class Cat(EntityType):
    pass

class ModelParameters(Params):
    pass


# An event type is defined by creating a class that sub-classes Event.
class tired_event(Event):
    pass


class not_tired_event(Event):
    pass


class minimal_cat_sleeping_eating_drinking(StateMachine):
    """Defines the sleeping patterns of cats, as far as this model is concerned."""

    # Every state machine must declare the type of entity it is associated with.
    # The entity type is declared elsewhere in the model, and we are referencing it here.
    entity_type = Cat()

    class sleeping(State):
        pass

    class awake(State):
        pass

    # Here, we define the transition table for state-machine 'minimal_cat_sleeping_eating_drinking'.
    #
    # The state machine initializes into the 'sleeping' state. Then, events toggle it back and forth
    # between 'sleeping' and 'awake'.
    transitions = Transitions([
        Transition(event_type=initial_event(),   source_state=initial_state(), target_state=sleeping()),
        Transition(           not_tired_event(),              sleeping(),                   awake()),
        Transition(           tired_event(),                  awake(),                      sleeping())
    ])

Below is the generated diagram for the minimal state machine example.

%%{init: { 'theme': 'base' } }%% stateDiagram-v2 awake sleeping [*] --> sleeping : initial_event sleeping --> awake : not_tired_event awake --> sleeping : tired_event

The diagram is generated by running the following command from the root of the Dymodetron folder:

# python -m dymodetron.generators.state_machine_diagrams --model_definition_file=examples/minimal_cat.py --overwrite_existing=1