Overview

DY NAMIC MODE L TRON

Dymodetron is for building and analyzing dynamic models efficiently.

What do you mean by “dynamic models”?

  • Models involving time-varying states/attributes and rules/logic/equations for changing state and attributes.

  • Models having a set of identifiable entities, usually interacting with one another.

  • Models where you want to quantify how those entities interact with one another and how their states and attributes progress and change over time.

  • Often with some random processes involved in the model.

How does one use Dymodetron?

The figure below illustrates the Dymodetron user workflow. There are two loops oriented around the Build Model activity. Each piece is described in the subsequent sections.

%%{init: { 'theme': 'neutral' } }%% graph TB subgraph t1 [" "] direction TB A(Build Model) end subgraph s1 [" "] direction TB C(Generate Model Diagrams) F(View Model Diagrams) A(Build Model) -- model source --> C(Generate Model Diagrams) C(Generate Model Diagrams) -- model diagrams --> F(View Model Diagrams) F(View Model Diagrams) -- Iterate On Model Definition --> A(Build Model) end subgraph u1 [" "] direction TB B(Generate Model Simulation Code) D(Run Simulations) E(Analyze Results) A(Build Model) -- model source --> B(Generate Model Simulation Code) B(Generate Model Simulation Code) -- generated sim --> D(Run Simulations) D(Run Simulations) -- sim results --> E(Analyze Results) E(Analyze Results) -- Iterate On Model Definition --> A(Build Model) E(Analyze Results) -- Iterate On Model Parameters --> D(Run Simulations) end classDef class1 fill: white, stroke: black classDef class2 fill: white, stroke: white class t1 class2 class s1,u1 class1 style A fill:#aca,stroke:#333,stroke-width:4px

1. Build Model

  • A Dymodetron user builds or modifies a model using the Dymodetron modeling constructs.

  • The user does this in a python source code file, using a number of modeling constructs provided by Dymodetron.

  • This is done in order to ask the model some questions.

Note

If you are using a python integrated development environment (IDE), all the features of auto-complete, code navigation, identifier renaming, code refactoring, etc., are there to help while building your model. This is why we’ve ‘borrowed’ python as the Dymodetron model definition language.

2. Generate Model Simulation Code

  • The user invokes the Dymodetron code generator to create a simulation of the model.

3. Generate Model Visualizations

  • Optional, recommended: the user invokes the Dymodetron diagram generator(s) to create visualizations of the model, and uses these visualizations as a central element of communication/collaboration with others around the questions and answers associated with #1 and #5. These are not visualizations of the results of simulations of the model. Instead, these are visualizations of the model structure and behavior.

4. Run Simulations

  • The user runs the simulation from the command line or IDE.

  • The simulation creates result data files.

5. Analyze Results

  • The user views the result data or writes additional scripts to process/analyze the results.

  • This is where the user gets some answers to those questions in #1.

6. Iterate

  • GOTO #1.

Note

When the model involves one or more random processes, then steps #2,4,5 are complicated by the need to run many copies of the simulation each having different random number seeds, and then collect, aggregate, analyze the results across the many simulation runs. For now, the user has to do all that work by themselves, but Dymodetron has aspirations to make this easier for the user.

What does a dymodetron model look like?

Dymodetron enables modeling of behavior in the form of state machines. Entities being modeled are described as existing in one or more states at any given time. The model generates events based on certain criteria being satisfied; entities then react to the arrival of those events by transitioning between states. Model logic is executed upon entry to or exit from a state, often resulting in generation of additional events, which continues the propagation of the model. The user builds a model like this because they want to ask questions about e.g. how many entities enter a particular state over a certain window of time, or how long entities stay in certain states, and other questions of this nature.

We discuss the concepts underpinning Dymodetron state machines models in another section.

Example Model Diagram (generated)

Here’s a visualization of an example Dymodetron state machine model. The model captures a crude description of the behavior of cats. The boxes are states. The arrows are transitions. The labels on the transition arrows indicate the event that triggers the transition.

%%{init: { 'theme': 'base' } }%% stateDiagram-v2 awake sleeping [*] --> sleeping : initial_event sleeping --> awake : not_tired_event awake --> sleeping : tired_event state awake { drinking eating staring_out_window [*] --> staring_out_window : initial_event staring_out_window --> eating : hungry_event eating --> staring_out_window : not_hungry_event staring_out_window --> drinking : thirsty_event drinking --> staring_out_window : not_thirsty_event }

This diagram is generated by the Dymodetron state machine diagram generator, which takes the code for a model as its input. The code for this particular model is below. The diagram generator is described here. Generally speaking, whenever you think you’re done with some model definition changes, you’ll want to re-run the diagram generator to review the latest diagram(s). This increases the likelihood, but doesn’t guarantee, that Dymodetron understands what you had in your head. It’s a good idea to check the diagrams in along with your code, so that other people can more rapidly come up to speed on your model.

The logic for the state machine model shown above is as follows:

  1. The state machine starts at the black dot on the left-hand side of the diagram.

    • The black dot is the ‘initial state’ for the state machine.

  2. Immediately, the ‘initial_event’ transition is taken, putting all the cats in the simulation of the model into state ‘sleeping’.

  3. When a ‘not_tired_event’ happens to a cat, it will transition to ‘awake’.

  4. The ‘awake’ state has a sub-state-machine with additional logic.

  5. An ‘awake’ cat immediately transitions to ‘staring_out_window’.

  6. Additional events take the cat in and out of ‘eating’ and ‘drinking’ states while the cat is ‘awake’.

  7. At some point, a ‘tired_event’ may occur, taking the cat back into state ‘sleeping’.

    • In this model, a cat may transition to ‘sleeping’ while ‘staring_out_window’, or while ‘eating’, or while ‘drinking’.

    • If a cat subsequently transitions again back to ‘awake’, it will again immediately transition to ‘staring_out_window’.

Multiple interacting state machines

A typical model will have multiple state machines, describing different aspects of the behavior of the entities that you care about. Our ‘cat’ model above might be augmented with another, separate, state machine that describes e.g. the internal biological processes of our cats as they go about their daily activities. This separate state machine will have a completely separate set of states. It can use none, some, or all of the same events used in the state machine above. The 2nd state machine can generate events that cause transitions in 1st state machine, and vice versa. For example, the 2nd state machine might generate ‘hungry_event’ and ‘thirsty_event’, triggering the associated transitions in the 1st state machine shown above. Or, the 2nd state machine might have transitions also triggered by the ‘tired_event’ and ‘not_tired_event’, which means that the same events can cause state transitions to occur simultaneously in both state machines.

In this way, the behaviors of the modeled entities can be decomposed into separate pieces but also linked together where necessary.

Multiple interacting entities

A typical model will have a number of entities. Each entity has its own ‘copy’ of the state machine(s) in the model. For example, our ‘cat’ model may have 100,000 cats, each of which is ‘running’ its own ‘copy’ of the state machine shown above. At any point in simulated time, each individual cat will have its own individual state. Individual entities are called entity instances.

Individual entities can generate events targeted at themselves or at other entities. In this way, the modeled entities can interact with one another.

Where do events come from?

Not shown in the diagram above are the mechanisms by which the events are generated. Those happen within state entry actions, which can be seen in the model code, where you will see action statements similar to the following:

# This action statement generates the named event on all the cats in the named entity set.
# The time of the event will be 7 time ticks in in the future, relative to the simulated time
# that the action statement is executed.
#
# Not shown in this example: how to construct the entity set 'cat_with_odd_times_staring_out_window'.
# See the section on entity sets for more information on that.
#
action.generate_event_rel(
    entity_type=Cat(),
    entity_set=cat_with_odd_times_staring_out_window,
    event=thirsty_event(),
    time_ticks_rel=7
)

Targeting events at entities

When you run a model simulation, it will typically have a large number of entity instances ‘running’ in the simulation. In the example above, those will all represent cats. Each individual cat has its own “copy” of the state machine running, and so all the cats may be in different states at any given simulated time. Individual events are typically targeted to a subset of the cat population: anything from one cat, to a handful of cats, to all the cats.

Subsets of the entities in the model are gathered into entity sets, by specifying filter criteria to pick out the entities that should belong to the set. The targeting of events in your model is designed around entity filter criteria that you provide.

In our cat example, the ‘cat criteria’ can be based on what state a cat is in. For example we might have some logic that does something special just for cats that are currently in the ‘eating’ state.

In the example below illustrates two different mechanisms for selecting subsets of entities to be targeted for events. First, we select all the cats currently in the ‘eating’ state. This is done using a construct for filtering entity sets based on criteria you provide.

Then we model that cats currently in state ‘eating’ will have a 30% chance of getting tired in 5 seconds. The example below does this using a construct for generating events on a randomly chosen subset of an entity set.

# This 'entity_match' statement filters down from the set of all cats, to the set of all cats
# currently in state 'eating'.
with action.entity_match(
    # Type of entity to match.
    entity_type=Cat(),

    # The entity instance(s) to look through. This comes from the context in which
    # this 'entity_match' statement is being executed.
    entity_set=cat,

    # The criteria to filter on: cats that are currently in the 'eating' sub-state
    # of the 'awake' state within the 'cat_sleeping_eating_drinking' state machine.
    # The 'lambda' syntax is how we build an expression describing the criteria we are looking for.
    # Each entity in the entity_set 'cat' will be evaluated against this criteria, to determine if it
    # should be included in the entity_set 'eating_cats' used below.
    criteria= lambda c: action.in_state(c, cat_sleeping_eating_drinking.awake.eating())
) as eating_cats:

    # In this section, we can now reference 'eating_cats', which is the set of all entities
    # in the entity_set 'cat' that are also in state 'eating'.
    #
    # The action statement below randomly selects entities from entity set 'eating_cats'.
    #
    # Each entity instance has a 30% chance of being selected.
    #
    # The event 'tired_event' will be generated on the selected entities, at simulation time 5 ticks
    # in the future relative to the simulated time at which the action statement is executed.
    action.generate_event_binomial(
        entity_type=Cat(),
        entity_set=eating_cats,
        event_a=tired_event(),
        probability_event_a=0.3,
        time_ticks_rel=5
    )

The ‘cat criteria’ can also be based on cat attributes. Attributes are ways of quantifying the measurable aspects of entities, separate from the state(s) that an entity is in. (Attributes are not shown in the diagram above). For example, a cat might have attributes such as weight, height, eye color.

The ‘cat criteria’ can also be based on a random selection process. You can use random distributions to model random processes that result in some entities becoming part of an entity set, and others not. Sometimes the ‘cat criteria’ is a combination of states, attributes, and random processes.

Entity criteria are described further in the section on entity sets.

Example Model Source

This model associated with the diagram above is listed below. There are a number of things about the model that are not represented in the state chart diagram. The various elements of a Dymodetron model are described in the section on modeling constructs.

  1from dymodetron import \
  2    EntityType, \
  3    EntityAttribute, \
  4    StateMachine, \
  5    SubStateMachine, \
  6    ModelDescription, \
  7    Params, \
  8    State, \
  9    Event, \
 10    Transition, \
 11    Transitions, \
 12    types, \
 13    action, \
 14    dymaction, \
 15    dymodetrack, \
 16    initial_state, \
 17    initial_event
 18
 19import numpy
 20
 21##########################################################################################
 22# Model overview.
 23#
 24# This model is a simple example of a Dymodetron state machine. We describe
 25# the behaviors of a cat. The main point of the model is to illustrate how some of
 26# the Dymodetron model definition elements work.
 27#
 28# A Dymodetron model consists of entity types, entity attributes, event types,
 29# state machines, states, and transition tables consisting of individual transitions.
 30#
 31# We use python classes as the atomic unit for many of the elements of the model definition.
 32# These should not be interpreted as python classes in the usual sense. Instead, we are
 33# co-opting the python language as a model definition language. (Instead of creating a new
 34# model definition language). We use python classes in many places as an atomic unit for the
 35# construction of our model definition.
 36#
 37# You will notice that we use a mixture of capitalization conventions for classes, some of
 38# which are not conventional. Specifically, we use CamelCase for the names of the Dymodetron
 39# model constructs such as StateMachine, Event, etc. We also use CamelCase for the entity
 40# type 'Cat'. We use a lower_case_naming_convention for names of events, state machines,
 41# states, entity attributes.
 42#
 43# The unconventional capitalizations arguably make certain parts of the model definition
 44# easier on the eyes when the class names are used. You can use whatever capitalization
 45# conventions you want.
 46##########################################################################################
 47
 48##########################################################################################
 49# Provide the model description.
 50##########################################################################################
 51
 52
 53# The model description is defined by creating a class that sub-classes 'ModelDescription'.
 54class cat_model(ModelDescription):
 55    # A model description just has a docstring explaining what you are up to with this model.
 56    """We model the behaviors of a certain kind of cat."""
 57
 58
 59##########################################################################################
 60# Define entity types.
 61##########################################################################################
 62
 63# An entity is defined by creating a class that sub-classes 'EntityType'.
 64class Cat(EntityType):
 65    # You can have docstrings on any of the elements of your model.
 66    """Cats are a type of animal that is neither dog, duck, nor lizard."""
 67
 68    # Here we declare the entity attributes of Cat.
 69    # The entity name is on the left-hand side.
 70    # On the right-hand side, we specify the type of the attribute, and the default
 71    # initial value.
 72    height_inches = EntityAttribute(attribute_type=types.scalar_float, initial_value=9.0)
 73    weight_pounds = EntityAttribute(attribute_type=types.scalar_float, initial_value=10.0)
 74
 75
 76##########################################################################################
 77# Define model parameters.
 78##########################################################################################
 79
 80# Your model parameters live inside a class that sub-classes 'Params'.
 81class ModelParameters(Params):
 82    # No parameters for this model.
 83    pass
 84
 85##########################################################################################
 86# Define event types.  It is recommended, but not required, to end event names
 87# with '_event'.
 88#
 89# The meaning of each event type is defined by its usage in the state machines
 90# further below in the model. The event name is a useful label, but it's just a label.
 91# If you really want to know what an event "means", you have to look at where it is used
 92# to cause transitions in the model.
 93##########################################################################################
 94
 95# An event type is defined by creating a class that sub-classes Event.
 96class tired_event(Event):
 97    pass
 98
 99class not_tired_event(Event):
100    pass
101
102class hungry_event(Event):
103    pass
104
105class not_hungry_event(Event):
106    pass
107
108class thirsty_event(Event):
109    pass
110
111class not_thirsty_event(Event):
112    pass
113
114
115##########################################################################################
116# Define state machines.
117##########################################################################################
118#
119# The Cat model has one state machine.  (You can have more than one if you want).
120#
121# State machines can be hard to visualize solely by looking at this model code. What you want
122# to do is use the dymodetron state machine diagram generator to create the diagram for this
123# state machine.
124#
125# Do this by running the following command, from the dymodetron root folder:
126#
127#     python -m dymodetron.generators.state_machine_diagrams --model_definition_file=examples/cat.py --overwrite_existing=1
128#
129# That will create a file:
130#
131#     generated/state_machine_diagrams/cat_sleeping_eating_drinking.html
132#
133# Open that file in a browser, and you will see the diagram for this state machine.
134#
135# A state machine is defined by creating a class that sub-classes StateMachine.
136#
137class cat_sleeping_eating_drinking(StateMachine):
138    """Defines the sleeping, eating, and drinking patterns of cats, as far as this model is concerned."""
139
140    # Every state machine must declare the type of entity it is associated with.
141    # In this model, there is only one entity type, 'Cat'.
142    entity_type = Cat()
143
144    # A state is defined by creating a nested class that sub-classes State.
145    class sleeping(State):
146        # We're going to define an entry action for this state. All entry action functions
147        # need to be annotated with '@dymaction'.
148        @dymaction
149        def entry_action(cat: Cat):
150            # The entry action takes an argument 'cat' of type 'Cat'. 'Cat' is the entity type we
151            # defined above.
152
153            # We want to model all cats as remaining in state 'sleeping' for 10 time ticks.
154            # So we schedule the 'not_tired_event' to fire in 10 time ticks.
155            # Time 'ticks' are whatever unit you want them to be. They could be seconds, minutes,
156            # hours, days. You just have to make sure that you are consistently treating them as
157            # whatever unit you have chosen.
158
159            # This action statement generates an event at a time relative to the 'current' time
160            # when the action is executed. For an entry action, the 'current' time is the time that
161            # this state is entered.
162            action.generate_event_rel(
163                entity_type=Cat(),          # The type of entity to schedule the event on.
164                entity_set=cat,                 # The entity instance(s) to schedule the event on.
165                event=not_tired_event(),    # The event to schedule.
166                time_ticks_rel=10           # The time in the future, relative to 'now', that the event should occur.
167            )
168
169    # A state can contain its own sub-state-machine. Whenever the state is entered,
170    # the sub-state-machine will start executing. If you want a state to contain a state machine,
171    # you subclass from SubStateMachine instead of from State.
172    class awake(SubStateMachine):
173
174        # States within the sub-state-machine are defined in the same way all states are defined.
175        class staring_out_window(State):
176            @dymaction
177            def entry_action(cat: Cat):
178
179                # The logic we want to implement is:
180                #
181                #   Cats who have been staring out the window an even number of times get hungry in 5 ticks.
182                #   Cats who have been staring out the window an odd number of times get thirsty in 7 ticks.
183
184                # This action statement retrieves the number of times that this state has been entered by
185                # the entity instance(s) represented by 'cat'.
186                staring_out_window_count = action.get_state_entry_counter(cat, cat_sleeping_eating_drinking.awake.staring_out_window())
187
188                # Generally speaking, you use the library of 'action.xyz()' statements to implement a lot
189                # of the logic of your state machine model. The states, events, and transition tables provide
190                # the skeleton of the model. The action statements are the meat on the bones, and fill in the
191                # detail of how the state machine should propagate.
192
193                # This 'entity_match' statement picks out the cats with an even 'staring_out_window_count'.
194                with action.entity_match(
195                    entity_type=Cat(),  # Type of entity to match.
196                    entity_set=cat,         # The entity instance(s) to look through.
197                    criteria=           # The criteria to match on.  It will be passed entity instance(s) to look at,
198                                        # i.e. the cat(s).  Note that for this criteria, we don't actually look at the
199                                        # cat(s), we are just looking at the 'staring_out_window_count'.
200                        lambda c: numpy.mod(staring_out_window_count, 2) == 0
201                ) as cat_with_even_times_staring_out_window:
202
203                    # The 'with' statement above picked out all the cats such that their 'staring_out_window_count'
204                    # is an even number. The 'as' clause labeled all those as 'cat_with_even_times_staring_out_window'.
205                    # We use that label next in the 'entity' parameter, to target just those specific cats with the
206                    # event we are generating.
207
208                    action.generate_event_rel(
209                        entity_type=Cat(),
210                        entity_set=cat_with_even_times_staring_out_window,
211                        event=hungry_event(),
212                        time_ticks_rel=5
213                    )
214
215                # This 'entity_match' statement picks out the cats with an odd 'staring_out_window_count'.
216                with action.entity_match(
217                    entity_type=Cat(),
218                    entity_set=cat,
219                    criteria=lambda c: numpy.mod(staring_out_window_count, 2) == 1
220                ) as cat_with_odd_times_staring_out_window:
221
222                    action.generate_event_rel(
223                        entity_type=Cat(),
224                        entity_set=cat_with_odd_times_staring_out_window,
225                        event=thirsty_event(),
226                        time_ticks_rel=7
227                    )
228
229        @dymodetrack(track_cumulative=True)
230        class eating(State):
231
232            @dymaction
233            def entry_action(cat: Cat):
234                # In 10 ticks the cat isn't hungry anymore.
235                action.generate_event_rel(
236                    entity_type=Cat(),
237                    entity_set=cat,
238                    event=not_hungry_event(),
239                    time_ticks_rel=10
240                )
241
242            @dymaction
243            def exit_action(cat: Cat):
244                # Cats gain a little weight every time they are done eating.
245                cat.weight_pounds += 0.5
246
247
248        class drinking(State):
249
250            @dymaction
251            def entry_action(cat: Cat):
252                # We are modeling a world of giant cats: cats get a little taller every
253                # time they take a drink of water.
254                cat.height_inches += 0.1
255
256                # In 3 ticks the cat isn't thirsty anymore.
257                action.generate_event_rel(
258                    entity_type=Cat(),
259                    entity_set=cat,
260                    event=not_thirsty_event(),
261                    time_ticks_rel=3
262                )
263
264        # Here, we define the transition table for the sub-state-machine within state 'awake'.
265        # Transition tables describe how events cause transitions between states.
266        #
267        # You define a transition table with a class variable named 'transitions', of type 'Transitions'.
268        # As you can see below, 'Transitions' takes an array of 'Transition' objects.
269        #
270        # Each row in the transition table is a 'Transition' object defining the event type that triggers
271        # the transition, and the source/target states for the transition.
272        #
273        # This 'awake' sub-state-machine initializes into the 'staring_out_window' state. Then, events toggle it
274        # back and forth to/from the 'eating' and 'drinking' states.
275        transitions = Transitions([
276            # Every state machine has a magic 'initial_event' and 'initial_state' automatically defined for you.
277            # You don't have to write them into the model definition above, but you do have to include
278            # them in the transition table here, so that the simulation knows what state to initialize the state
279            # machine into.
280            #
281            # The 'initial_event' fires when the state machine starts executing. At that time, the state machine
282            # transitions from the magic 'initial_state' to whichever state you tell it to transition to here
283            # in the transition table.
284            Transition(event_type=initial_event(),   source_state=initial_state(), target_state=staring_out_window()),
285            Transition(           hungry_event(),                 staring_out_window(),         eating()),
286            Transition(           not_hungry_event(),             eating(),                     staring_out_window()),
287            Transition(           thirsty_event(),                staring_out_window(),         drinking()),
288            Transition(           not_thirsty_event(),            drinking(),                   staring_out_window())
289        ])
290
291    # Here, we define the transition table for the top-level state-machine 'cat_sleeping_eating_drinking'.
292    #
293    # The state machine initializes into the 'sleeping' state. Then, events toggle it back and forth
294    # between 'sleeping' and 'awake'.
295    transitions = Transitions([
296        Transition(event_type=initial_event(),   source_state=initial_state(), target_state=sleeping()),
297        Transition(           not_tired_event(),              sleeping(),                   awake()),
298        Transition(           tired_event(),                  awake(),                      sleeping())
299    ])

Model Simulation Code (generated)

If you want to use Dymodetron to build and simulate models, you may not ever need to look at the generated simulation code. The exception might be for debugging the simulation of your model. In any case, this section illustrates what the generated simulation code for a model looks like.

The Dymodetron code generator consumes a model definition, and generates code to simulate the model. The generated code for the model above is listed below. The model entry actions embedded within the model definition above are also invoked during the simulation execution. You will see that the generated code also relies upon some Dymodetron package pieces.

  1
  2import numpy
  3import numpy_indexed
  4from scipy.interpolate import interp1d
  5from dataclasses import dataclass, field
  6from typing import Any, Type, Union, List, Dict
  7from heapq import heappop, heappush
  8from functools import partial
  9import logging
 10import beeprint
 11import sys
 12import os
 13import argparse
 14
 15from dymodetron import \
 16    Event, \
 17    State, \
 18    StateMachine, \
 19    action, \
 20    ActionStateHooks, \
 21    ActionEntityTypeHooks, \
 22    ActionLookupTableHooks, \
 23    ActionDistributionHooks, \
 24    TimedValue, \
 25    StateTracking
 26
 27
 28sys.path.append(os.path.join('..','..'))
 29import examples.cat as model
 30
 31
 32log = logging.getLogger(__name__)
 33
 34
 35@dataclass(order=True)
 36class ScheduledEvent:
 37    # The heapq will sort on time_ticks and event_id.
 38    # Uniqueness and monotonicity (handled elsewhere) of scheduled_event.event_id
 39    # gives us the tie-breaker we want, so we get time order and then
 40    # schedule order.
 41    time_ticks: numpy.float64 = numpy.float64(0.0)
 42    event_id: numpy.uint64 = 0
 43    event_obj: Any = field(compare=False, default=None)
 44    # entity_ids = None means all
 45    entity_ids: 'numpy.array[numpy.uint64]' = field(compare=False, default=None)
 46
 47
 48@dataclass
 49class SourceTargetPair:
 50    source: 'numpy.array[numpy.int64]'
 51    target: 'numpy.array[numpy.int64]'
 52    target_state_last_entry_time_ticks: 'numpy.array[numpy.float64]' = field(compare=False, default=None)
 53    source_state_definition: State = field(default=None) #TODO: default None is temporary
 54    target_state_definition: State = field(default=None) #TODO: default None is temporary
 55
 56
 57@dataclass
 58class StateData:
 59    state: Type
 60    state_entry_counters: 'numpy.array[numpy.uint64]' = field(compare=False, default=None)
 61    last_state_entry_time_ticks: 'numpy.array[numpy.float64]' = field(compare=False, default=None)
 62    state_tracking: StateTracking = field(compare=False, default=None)
 63    is_state_machine: bool = field(default=None)
 64    substate_initialization_event_type: Type = field(default=None)
 65
 66
 67#######################################################################
 68# State machine initial event types.
 69#######################################################################
 70
 71
 72class initial_event_types:
 73    class initial_event_cat_sleeping_eating_drinking(Event):
 74        pass
 75    class initial_event_cat_sleeping_eating_drinking__awake(Event):
 76        pass
 77    
 78#######################################################################
 79# Entity attributes.
 80# Entity type: Cat
 81#######################################################################
 82
 83
 84class Entities_Cat_attributes:
 85    def __init__(self, num_entities):
 86        self.num_entities = num_entities
 87        self.height_inches = numpy.full(self.num_entities, 9.0, numpy.float32)
 88        self.weight_pounds = numpy.full(self.num_entities, 10.0, numpy.float32)
 89
 90
 91#######################################################################
 92# Entity states.
 93# Entity type: Cat
 94#######################################################################
 95
 96
 97class Entities_Cat_states:
 98    def __init__(self, num_entities):
 99        self.num_entities = num_entities
100        self.cat_sleeping_eating_drinking = Entities_Cat_states_cat_sleeping_eating_drinking(num_entities=self.num_entities)
101        self.cat_sleeping_eating_drinking__awake = Entities_Cat_states_cat_sleeping_eating_drinking__awake(num_entities=self.num_entities)
102
103
104class Entities_Cat_states_cat_sleeping_eating_drinking:
105    def __init__(self, num_entities):
106        self.num_entities = num_entities
107        self.initial_state = numpy.full(self.num_entities, 1, dtype=numpy.int64)
108        self.awake = numpy.full(self.num_entities, 0, dtype=numpy.int64)
109        self.sleeping = numpy.full(self.num_entities, 0, dtype=numpy.int64)
110
111
112class Entities_Cat_states_cat_sleeping_eating_drinking__awake:
113    def __init__(self, num_entities):
114        self.num_entities = num_entities
115        self.initial_state = numpy.full(self.num_entities, 1, dtype=numpy.int64)
116        self.drinking = numpy.full(self.num_entities, 0, dtype=numpy.int64)
117        self.eating = numpy.full(self.num_entities, 0, dtype=numpy.int64)
118        self.staring_out_window = numpy.full(self.num_entities, 0, dtype=numpy.int64)
119
120
121class Entities_Cat_state_tracking:
122    def __init__(self):
123        self.cat_sleeping_eating_drinking__awake__eating = StateTracking(
124            track_n=None,
125            track_new=None,
126            track_cumulative=0
127        )
128        self.cat_sleeping_eating_drinking__awake = StateTracking(
129            track_n=None,
130            track_new=None,
131            track_cumulative=None
132        )
133        self.cat_sleeping_eating_drinking__awake__drinking = StateTracking(
134            track_n=None,
135            track_new=None,
136            track_cumulative=None
137        )
138        self.cat_sleeping_eating_drinking__awake__staring_out_window = StateTracking(
139            track_n=None,
140            track_new=None,
141            track_cumulative=None
142        )
143        self.cat_sleeping_eating_drinking__sleeping = StateTracking(
144            track_n=None,
145            track_new=None,
146            track_cumulative=None
147        )
148
149
150#######################################################################
151# Entity last state entry times.
152# Entity type: Cat
153#######################################################################
154
155
156class Entities_Cat_last_state_entry_times_cat_sleeping_eating_drinking:
157    def __init__(self, num_entities):
158        self.num_entities = num_entities
159        self.awake = numpy.full(self.num_entities, numpy.nan, dtype=numpy.int64)
160        self.initial_state = numpy.full(self.num_entities, numpy.nan, dtype=numpy.int64)
161        self.sleeping = numpy.full(self.num_entities, numpy.nan, dtype=numpy.int64)
162
163
164class Entities_Cat_last_state_entry_times_cat_sleeping_eating_drinking__awake:
165    def __init__(self, num_entities):
166        self.num_entities = num_entities
167        self.drinking = numpy.full(self.num_entities, numpy.nan, dtype=numpy.int64)
168        self.eating = numpy.full(self.num_entities, numpy.nan, dtype=numpy.int64)
169        self.initial_state = numpy.full(self.num_entities, numpy.nan, dtype=numpy.int64)
170        self.staring_out_window = numpy.full(self.num_entities, numpy.nan, dtype=numpy.int64)
171
172
173class Entities_Cat_last_state_entry_times:
174    def __init__(self, num_entities):
175        self.num_entities = num_entities
176        self.cat_sleeping_eating_drinking = Entities_Cat_last_state_entry_times_cat_sleeping_eating_drinking(num_entities=self.num_entities)
177        self.cat_sleeping_eating_drinking__awake = Entities_Cat_last_state_entry_times_cat_sleeping_eating_drinking__awake(num_entities=self.num_entities)
178
179
180#######################################################################
181# Entity state entry counters.
182# Entity type: Cat
183#######################################################################
184
185
186class Entities_Cat_state_entry_counters_cat_sleeping_eating_drinking:
187    def __init__(self, num_entities):
188        self.num_entities = num_entities
189        self.awake = numpy.full(self.num_entities, 0, dtype=numpy.uint64)
190        self.initial_state = numpy.full(self.num_entities, 0, dtype=numpy.uint64)
191        self.sleeping = numpy.full(self.num_entities, 0, dtype=numpy.uint64)
192
193
194class Entities_Cat_state_entry_counters_cat_sleeping_eating_drinking__awake:
195    def __init__(self, num_entities):
196        self.num_entities = num_entities
197        self.drinking = numpy.full(self.num_entities, 0, dtype=numpy.uint64)
198        self.eating = numpy.full(self.num_entities, 0, dtype=numpy.uint64)
199        self.initial_state = numpy.full(self.num_entities, 0, dtype=numpy.uint64)
200        self.staring_out_window = numpy.full(self.num_entities, 0, dtype=numpy.uint64)
201
202
203class Entities_Cat_state_entry_counters:
204    def __init__(self, num_entities):
205        self.num_entities = num_entities
206        self.cat_sleeping_eating_drinking = Entities_Cat_state_entry_counters_cat_sleeping_eating_drinking(num_entities=self.num_entities)
207        self.cat_sleeping_eating_drinking__awake = Entities_Cat_state_entry_counters_cat_sleeping_eating_drinking__awake(num_entities=self.num_entities)
208
209
210#######################################################################
211# Entity instances.
212# Entity type: Cat
213#######################################################################
214
215
216class Entities_Cat:
217
218    def __init__(self, num_entities: numpy.uint64, enable_logging: bool=None, info_interval_ticks: numpy.float64=None):
219        if enable_logging is None:
220            enable_logging = False
221        if info_interval_ticks is None:
222            info_interval_ticks = 100.0
223        self.enable_logging = enable_logging
224        self.info_interval_ticks = info_interval_ticks
225        self.last_info_time_ticks = numpy.finfo(numpy.float64).min
226        self.num_entities = num_entities
227        self.initialize_q()
228        self.initialize_rng()
229        self.initialize_state()
230        self.initialize_event_type_transition_lookup()
231        self.initialize_action_mappings()
232        self.initialize_entity_type()
233        self.initialize_entity_type_mappings()
234        self.initialize_lookup_tables()
235        self.register_lookup_tables()
236        self.initialize_distributions()
237        self.register_distributions()
238        return
239
240    def initialize_state(self):
241        self.initialize_identity()
242        self.initialize_entity_attributes()
243        self.initialize_entity_states()
244        self.initialize_entity_state_tracking()
245        self.initialize_time()
246        self.initialize_state_machines()
247        self.initialize_state_data_hooks()
248
249    def initialize_identity(self):
250        self.entity_id = numpy.arange(self.num_entities, dtype=numpy.uint64)
251        pass
252
253    def initialize_time(self):
254        min_time = numpy.finfo(numpy.float64).min
255        self.set_time_ticks(time_ticks=min_time)
256
257    def initialize_q(self):
258        self.q = []
259        self.next_event_id = numpy.uint64(0)
260
261    def set_seed(self, seed):
262        self.initialize_rng(seed=seed)
263
264    def get_seed(self):
265        # TODO: this might not be the best value for 'high'.
266        return self.seed_rng.integers(low=0, high=numpy.iinfo(numpy.int32).max, size=1)
267
268    def initialize_rng(self, seed=12345):        
269        self.seed_rng = numpy.random.default_rng(seed=seed)        
270        
271        # The events rng is used internally in schedule_event_zzz() methods.
272        events_seed = self.get_seed()
273        self.events_rng = numpy.random.default_rng(events_seed)
274        
275        # Now that the seed rng is set up, we have to re-initialize all the distribution rngs,
276        # they will take new seeds from the seed rng. And then they have to be re-registered
277        # with the action hooks.
278        self.initialize_distributions()
279        self.register_distributions()
280
281    def get_next_event_id(self):
282        event_id = self.next_event_id
283        self.next_event_id = self.next_event_id + numpy.uint64(1)
284        return event_id
285
286    def schedule_event_rel(self, event_obj: Event, time_ticks_rel: numpy.float64 = numpy.float64(0.0), entity_ids: 'numpy.array[numpy.uint64]' = None):
287        event_time_ticks_abs = self.get_time_ticks() + time_ticks_rel
288        self.schedule_event_abs(event_obj=event_obj, time_ticks_abs=event_time_ticks_abs, entity_ids=entity_ids)
289
290    def schedule_events_abs(self,
291                            event_obj: Event,
292                            times_ticks_abs: 'numpy.array[numpy.float64]',
293                            entity_ids: 'numpy.array[numpy.uint64]' = None):
294        # Break the entity_ids into sub-groups that share the same times_ticks_abs.
295        # And then schedule the sub-groups together.
296
297        # This creates effectively a list of arrays of indices, where each array contains indices that
298        # have the same times_ticks_abs.
299        #
300        # [ array([5, 13, 16]), array([1, 8]), array([0, 3, 12, 19, 21]), ... ]
301        #
302        # Subsequent .split() operations pick out the values at these indices.
303        time_ticks_group_by = numpy_indexed.group_by(times_ticks_abs)
304
305        # The two lists created below by contract will have the same number of entries.
306        #
307        # The unique time ticks values.
308        time_ticks = time_ticks_group_by.unique
309        # The entity ids for each corresponding time ticks value.
310        entity_id_groups = time_ticks_group_by.split(entity_ids)
311
312        # Schedule the appropriate entities for each unique time ticks value.
313        for i, (time, this_time_entity_ids) in enumerate(zip(time_ticks, entity_id_groups)):
314            self.schedule_event_abs(event_obj=event_obj,
315                                    time_ticks_abs=time,
316                                    entity_ids=this_time_entity_ids)
317
318    def _schedule_event_abs(self, event_obj: Event, time_ticks_abs: numpy.float64 = numpy.float64(0.0), entity_ids: 'numpy.array[numpy.uint64]' = None):
319        # We ignore an empty list for entity_ids.
320        if entity_ids is not None and len(entity_ids) == 0:
321            if self.enable_logging: log.debug(f'Event was scheduled with no target entity_ids: [{event_obj} @ {time_ticks_abs}]')
322            return
323
324        if entity_ids is None:
325            # We pass it along as None; this is handled as a special case in dispatch_event().
326            pass
327
328        event_id = self.get_next_event_id()
329        scheduled_event = ScheduledEvent(time_ticks=numpy.float64(time_ticks_abs),
330                                         event_id=event_id,
331                                         event_obj=event_obj,
332                                         entity_ids=entity_ids)
333        if self.enable_logging: log.debug(f'Scheduling event: [{scheduled_event}]')
334        heappush(self.q, scheduled_event)
335
336    def schedule_event_abs(self,
337                           event_obj: Event,
338                           time_ticks_abs: numpy.float64 = numpy.float64(0.0),
339                           entity_ids: 'numpy.array[numpy.uint64]' = None):
340
341        if isinstance(time_ticks_abs, numpy.ndarray):
342           self.schedule_events_abs(event_obj=event_obj,
343                                    times_ticks_abs=time_ticks_abs,
344                                    entity_ids=entity_ids)
345        else:
346            self._schedule_event_abs(event_obj=event_obj,
347                                     time_ticks_abs=time_ticks_abs,
348                                     entity_ids=entity_ids)
349
350    def schedule_event_binomial(self,
351                                time_ticks_abs: Union[numpy.float64, 'numpy.array[numpy.float64]'],
352                                event_a: Event,
353                                event_b: Event,
354                                p_a: numpy.float64,
355                                entity_ids: 'numpy.array[numpy.uint64]'):
356
357        # An empty list for 'entity_ids' here is ignored.
358        if entity_ids is not None and len(entity_ids) == 0:
359            if self.enable_logging: log.debug(f'Binomial event was scheduled with no target entity_ids: [({event_a}, {event_b}) @ {time_ticks_abs}]')
360            return
361
362        # None is interpreted as all.
363        if entity_ids is None:
364            entity_ids = numpy.arange(self.num_entities)
365
366        if event_a is None and event_b is None:
367            raise RuntimeError('At least one of of event_a or event_b must be provided to schedule_event_binomial()')
368
369        trials = self.events_rng.binomial(1, p_a, len(entity_ids))
370        entity_inds_a = numpy.nonzero(trials == 1)[0]
371        entity_inds_b = numpy.nonzero(trials == 0)[0]
372        entity_ids_a = entity_ids[entity_inds_a]
373        entity_ids_b = entity_ids[entity_inds_b]
374
375        if len(entity_ids_a) > 0 and event_a is not None:
376            if isinstance(time_ticks_abs, numpy.ndarray):
377                time_ticks_abs_a = time_ticks_abs[entity_inds_a]
378                self.schedule_events_abs(event_obj=event_a,
379                                         times_ticks_abs=time_ticks_abs_a,
380                                         entity_ids=entity_ids_a)
381            else:
382                self.schedule_event_abs(event_obj=event_a,
383                                        time_ticks_abs=time_ticks_abs,
384                                        entity_ids=entity_ids_a)
385
386        if len(entity_ids_b) > 0 and event_b is not None:
387            if isinstance(time_ticks_abs, numpy.ndarray):
388                time_ticks_abs_b = time_ticks_abs[entity_inds_b]
389                self.schedule_events_abs(event_obj=event_b,
390                                         times_ticks_abs=time_ticks_abs_b,
391                                         entity_ids=entity_ids_b)
392            else:
393                self.schedule_event_abs(event_obj=event_b,
394                                        time_ticks_abs=time_ticks_abs,
395                                        entity_ids=entity_ids_b)
396
397    def schedule_event_trials(self,
398                              time_ticks_abs: Union[numpy.float64, 'numpy.array[numpy.float64]'],
399                              event: Event,
400                              p: 'numpy.array[numpy.float64]',
401                              entity_ids: 'numpy.array[numpy.uint64]'):
402
403        if entity_ids is not None and len(entity_ids) == 0:
404            if self.enable_logging: log.debug(f'Trials event was scheduled with no target entity_ids: [({event}) @ {time_ticks_abs}]')
405            return
406
407        # None is interpreted as all.
408        if entity_ids is None:
409            entity_ids = numpy.arange(self.num_entities)
410
411        if event is None:
412            raise RuntimeError('The event parameter must be provided to schedule_event_trials()')
413
414        draws = self.events_rng.random(size=len(entity_ids))
415        trials = draws < p
416        # Indices into entity_ids for trials resulting in the outcome as specified by the probability.
417        outcome_trial_indices = trials.nonzero()[0]
418        if len(outcome_trial_indices) > 0:
419            outcome_trial_entity_ids = entity_ids[outcome_trial_indices]
420
421            if isinstance(time_ticks_abs, numpy.ndarray):
422                # Pull the times associated with each entity_id, which we find by outcome_trial_indices.
423                time_ticks_abs_a = time_ticks_abs[outcome_trial_indices]
424                self.schedule_events_abs(event_obj=event,
425                                         times_ticks_abs=time_ticks_abs_a,
426                                         entity_ids=outcome_trial_entity_ids)
427            else:
428                self.schedule_event_abs(event_obj=event,
429                                        time_ticks_abs=time_ticks_abs,
430                                        entity_ids=outcome_trial_entity_ids)
431
432    def set_time_ticks(self, time_ticks: numpy.int64):
433        action.time_ticks = time_ticks
434
435    def get_time_ticks(self) -> numpy.float64:
436        return action.time_ticks
437
438    def get_events_at_next_time(self, q: List[ScheduledEvent]) -> Dict[Type, List[ScheduledEvent]]:
439        '''Returns a map of lists of events that are co-scheduled for the next scheduled event time, keyed by event
440        type.'''
441        event_map = dict()
442        if len(q) == 0:
443            return None
444
445        next_event = heappop(q)
446        next_event_type = type(next_event.event_obj)
447        event_map[next_event_type] = [next_event]
448        while len(q) > 0 and q[0].time_ticks == next_event.time_ticks:
449            event: ScheduledEvent = heappop(q)
450            event_type = type(event.event_obj)
451            if event_type in event_map.keys():
452                event_map[event_type].append(event)
453            else:
454                event_map[event_type] = [event]
455
456        return event_map
457
458
459    def step(self, to_time_ticks: numpy.float64):
460        '''Run state machines up to time to_time_ticks. Returns False if there are no more events left in the
461        queue, otherwise True.'''
462        self.check_multiprocessing_shenanigans()
463
464        if len(self.q) == 0:
465            return False
466
467        next_event_time = self.q[0].time_ticks
468        while next_event_time <= to_time_ticks:
469
470            if next_event_time - self.last_info_time_ticks >= self.info_interval_ticks:
471                if self.enable_logging: log.info(f't = {next_event_time}')
472                self.last_info_time_ticks = next_event_time
473
474            next_events = self.get_events_at_next_time(self.q)
475
476            next_event: ScheduledEvent
477            for next_event_type in next_events.keys():
478                events = next_events[next_event_type]
479                # Merge all the events of the same type together.
480                if len(events) > 1:
481                    if self.enable_logging: log.debug(f'Merging event ids: [{f"{[e.event_id for e in events]},"}]')
482                    merge_target = events[0]
483                    if merge_target.entity_ids is None:
484                        # This indicates 'all entity_ids', so we don't need to merge, just leave it alone.
485                        pass
486                    else:
487                        for merge_source in events[1:]:
488                            merge_target.entity_ids = numpy.append(merge_target.entity_ids, merge_source.entity_ids)
489                    next_event = merge_target
490                    if next_event.entity_ids is not None:
491                        # TODO: should we be using a set for entity_ids ?
492                        numpy.ndarray.sort(next_event.entity_ids)
493                else:
494                    next_event = events[0]
495
496                if self.enable_logging: log.debug(f'Processing event: [{next_event}]')
497
498                event_time_ticks = next_event.time_ticks
499                self.set_time_ticks(time_ticks=event_time_ticks)
500
501                self.dispatch_event(next_event)
502
503            if len(self.q) == 0:
504                return False
505
506            next_event_time = self.q[0].time_ticks
507
508        return True
509
510    def dispatch_event(self, scheduled_event: ScheduledEvent):
511
512        time_ticks = scheduled_event.time_ticks
513
514        # Figure out which applicable transitions.
515        all_transitions = self.event_type_transition_lookup
516        event_obj = scheduled_event.event_obj
517        applicable_transitions = all_transitions[type(event_obj)]
518
519        # For each applicable transition:
520        transition: SourceTargetPair
521        for transition in applicable_transitions:
522
523            # Build mask for targeting scheduled_event.entity_ids
524            if scheduled_event.entity_ids is None:
525                entity_id_mask = numpy.full(self.num_entities, 1)
526            else:
527                entity_id_mask = numpy.full(self.num_entities, 0)
528                # This masking relies on entity_id is also the index into the arrays.
529                entity_id_mask[scheduled_event.entity_ids] = 1
530
531            # Figure out which entity ids are currently in the appropriate source state,
532            # and are in the entity_id_mask.
533            applicable_entity_ids = numpy.nonzero((transition.source == 1)
534                                                  &
535                                                  (entity_id_mask == 1)
536                                                  )[0]
537
538            # Get the target state data hooks.
539            target_state_data_hooks: StateData = self.state_data_hooks[type(transition.target_state_definition)]
540            target_state_entry_counters = target_state_data_hooks.state_entry_counters
541            target_state_last_entry_time_ticks = target_state_data_hooks.last_state_entry_time_ticks
542            target_state_tracking = target_state_data_hooks.state_tracking
543
544            # Transition state.  In addition to updating the state tracking variables, we also: 
545            #   - Run the source state exit actions, with the appropriate entity ids that are transitioning.
546            #   - Run the target state entry actions, again with the appropriate entity ids.
547            #   - Update the associated state hooks.
548
549            transitioning_entities: action.entity_match
550
551            with action.entity_match(entity_type=self.entity_type(),
552                                     entity_set=applicable_entity_ids,
553                                     criteria=None
554            ) as transitioning_entities:
555
556                # Take the applicable entities out of the source state.
557                transition.source_state_definition.exit_action(transitioning_entities)
558                transition.source[transitioning_entities.filtered_entity_ids] = 0
559
560                # Put the applicable entities into the target state.
561                transition.target[transitioning_entities.filtered_entity_ids] = 1
562
563                # Update state hooks.
564                target_state_entry_counters[transitioning_entities.filtered_entity_ids] += 1
565                target_state_last_entry_time_ticks[transitioning_entities.filtered_entity_ids] = time_ticks
566
567                # Update state tracking
568                if target_state_tracking.track_n is not None:
569                    entities_in_target_state_now = numpy.nonzero(transition.target==1)[0]
570                    num_entities_in_target_state_now = len(entities_in_target_state_now)
571                    target_state_tracking.track_n.append(TimedValue(
572                        time_ticks=time_ticks,
573                        value=num_entities_in_target_state_now
574                    ))
575
576                if target_state_tracking.track_new is not None:
577                    num_new_entities_in_state = len(transitioning_entities.filtered_entity_ids)
578                    target_state_tracking.track_new.append(TimedValue(
579                        time_ticks=time_ticks,
580                        value=num_new_entities_in_state
581                    ))
582
583                if target_state_tracking.track_cumulative is not None:
584                    num_new_entities_in_state = len(transitioning_entities.filtered_entity_ids)
585                    target_state_tracking.track_cumulative += num_new_entities_in_state
586
587                # Execute target state entry action.
588                transition.target_state_definition.entry_action(transitioning_entities)
589
590                # If the target state has a sub-state-machine, schedule the initialization event for it now.
591                if target_state_data_hooks.is_state_machine:
592                    self.schedule_event_rel(event_obj=target_state_data_hooks.substate_initialization_event_type(),
593                                            time_ticks_rel=numpy.float64(0.0),
594                                            entity_ids=transitioning_entities.filtered_entity_ids)
595
596    def check_multiprocessing_shenanigans(self):
597        # This is a workaround for module-level stuff doesn't survive sporking on windows
598        # when using multiprocessing.
599        # We're using the item in the 'if' statement as the 'canary' to detect that things need to be re-initialized.
600        if len(action.action_state_hooks.keys()) == 0:
601            self.initialize_action_mappings()
602            self.register_lookup_tables()
603            self.initialize_time()
604            self.register_distributions()
605            self.initialize_entity_type_mappings()
606        
607    def initialize_entity_attributes(self):
608        self.attributes = Entities_Cat_attributes(num_entities=self.num_entities)
609
610    def initialize_entity_states(self):
611        self.states = Entities_Cat_states(num_entities=self.num_entities)
612        self.last_state_entry_time_ticks = Entities_Cat_last_state_entry_times(num_entities=self.num_entities)
613        self.state_entry_counters = Entities_Cat_state_entry_counters(num_entities=self.num_entities)
614          
615
616    def initialize_entity_state_tracking(self):
617        self.state_tracking = Entities_Cat_state_tracking()
618
619    def initialize_state_data_hooks(self):
620        self.state_data_hooks = dict()
621
622        # State machine: cat_sleeping_eating_drinking
623        self.state_data_hooks[model.cat_sleeping_eating_drinking.awake] = StateData(
624            state=model.cat_sleeping_eating_drinking.awake,
625            state_entry_counters=self.state_entry_counters.cat_sleeping_eating_drinking.awake,
626            last_state_entry_time_ticks=self.last_state_entry_time_ticks.cat_sleeping_eating_drinking.awake,
627            state_tracking=self.state_tracking.cat_sleeping_eating_drinking__awake,
628            is_state_machine=True,
629            substate_initialization_event_type=initial_event_types.initial_event_cat_sleeping_eating_drinking__awake
630        )
631
632        # State machine: cat_sleeping_eating_drinking.awake
633        self.state_data_hooks[model.cat_sleeping_eating_drinking.awake.drinking] = StateData(
634            state=model.cat_sleeping_eating_drinking.awake.drinking,
635            state_entry_counters=self.state_entry_counters.cat_sleeping_eating_drinking__awake.drinking,
636            last_state_entry_time_ticks=self.last_state_entry_time_ticks.cat_sleeping_eating_drinking__awake.drinking,
637            state_tracking=self.state_tracking.cat_sleeping_eating_drinking__awake__drinking,
638            is_state_machine=False,
639            substate_initialization_event_type=None
640        )
641        self.state_data_hooks[model.cat_sleeping_eating_drinking.awake.eating] = StateData(
642            state=model.cat_sleeping_eating_drinking.awake.eating,
643            state_entry_counters=self.state_entry_counters.cat_sleeping_eating_drinking__awake.eating,
644            last_state_entry_time_ticks=self.last_state_entry_time_ticks.cat_sleeping_eating_drinking__awake.eating,
645            state_tracking=self.state_tracking.cat_sleeping_eating_drinking__awake__eating,
646            is_state_machine=False,
647            substate_initialization_event_type=None
648        )
649        self.state_data_hooks[model.cat_sleeping_eating_drinking.awake.staring_out_window] = StateData(
650            state=model.cat_sleeping_eating_drinking.awake.staring_out_window,
651            state_entry_counters=self.state_entry_counters.cat_sleeping_eating_drinking__awake.staring_out_window,
652            last_state_entry_time_ticks=self.last_state_entry_time_ticks.cat_sleeping_eating_drinking__awake.staring_out_window,
653            state_tracking=self.state_tracking.cat_sleeping_eating_drinking__awake__staring_out_window,
654            is_state_machine=False,
655            substate_initialization_event_type=None
656        )
657        self.state_data_hooks[model.cat_sleeping_eating_drinking.sleeping] = StateData(
658            state=model.cat_sleeping_eating_drinking.sleeping,
659            state_entry_counters=self.state_entry_counters.cat_sleeping_eating_drinking.sleeping,
660            last_state_entry_time_ticks=self.last_state_entry_time_ticks.cat_sleeping_eating_drinking.sleeping,
661            state_tracking=self.state_tracking.cat_sleeping_eating_drinking__sleeping,
662            is_state_machine=False,
663            substate_initialization_event_type=None
664        )
665
666    def initialize_event_type_transition_lookup(self):
667        self.event_type_transition_lookup: Dict[type, List[SourceTargetPair]] = dict()
668
669        self.event_type_transition_lookup[model.not_tired_event] = [
670            SourceTargetPair(source=self.states.cat_sleeping_eating_drinking.sleeping,
671                             target=self.states.cat_sleeping_eating_drinking.awake,
672                             source_state_definition=model.cat_sleeping_eating_drinking.sleeping(),
673                             target_state_definition=model.cat_sleeping_eating_drinking.awake()
674            ),
675        ]
676
677        self.event_type_transition_lookup[model.tired_event] = [
678            SourceTargetPair(source=self.states.cat_sleeping_eating_drinking.awake,
679                             target=self.states.cat_sleeping_eating_drinking.sleeping,
680                             source_state_definition=model.cat_sleeping_eating_drinking.awake(),
681                             target_state_definition=model.cat_sleeping_eating_drinking.sleeping()
682            ),
683        ]
684
685        self.event_type_transition_lookup[model.hungry_event] = [
686            SourceTargetPair(source=self.states.cat_sleeping_eating_drinking__awake.staring_out_window,
687                             target=self.states.cat_sleeping_eating_drinking__awake.eating,
688                             source_state_definition=model.cat_sleeping_eating_drinking.awake.staring_out_window(),
689                             target_state_definition=model.cat_sleeping_eating_drinking.awake.eating()
690            ),
691        ]
692
693        self.event_type_transition_lookup[model.not_hungry_event] = [
694            SourceTargetPair(source=self.states.cat_sleeping_eating_drinking__awake.eating,
695                             target=self.states.cat_sleeping_eating_drinking__awake.staring_out_window,
696                             source_state_definition=model.cat_sleeping_eating_drinking.awake.eating(),
697                             target_state_definition=model.cat_sleeping_eating_drinking.awake.staring_out_window()
698            ),
699        ]
700
701        self.event_type_transition_lookup[model.not_thirsty_event] = [
702            SourceTargetPair(source=self.states.cat_sleeping_eating_drinking__awake.drinking,
703                             target=self.states.cat_sleeping_eating_drinking__awake.staring_out_window,
704                             source_state_definition=model.cat_sleeping_eating_drinking.awake.drinking(),
705                             target_state_definition=model.cat_sleeping_eating_drinking.awake.staring_out_window()
706            ),
707        ]
708
709        self.event_type_transition_lookup[model.thirsty_event] = [
710            SourceTargetPair(source=self.states.cat_sleeping_eating_drinking__awake.staring_out_window,
711                             target=self.states.cat_sleeping_eating_drinking__awake.drinking,
712                             source_state_definition=model.cat_sleeping_eating_drinking.awake.staring_out_window(),
713                             target_state_definition=model.cat_sleeping_eating_drinking.awake.drinking()
714            ),
715        ]
716
717        self.event_type_transition_lookup[initial_event_types.initial_event_cat_sleeping_eating_drinking__awake] = [
718            SourceTargetPair(source=self.states.cat_sleeping_eating_drinking__awake.initial_state,
719                             target=self.states.cat_sleeping_eating_drinking__awake.staring_out_window,
720                             source_state_definition=model.cat_sleeping_eating_drinking.awake.initial_state(),
721                             target_state_definition=model.cat_sleeping_eating_drinking.awake.staring_out_window()
722            ),
723        ]
724
725        self.event_type_transition_lookup[initial_event_types.initial_event_cat_sleeping_eating_drinking] = [
726            SourceTargetPair(source=self.states.cat_sleeping_eating_drinking.initial_state,
727                             target=self.states.cat_sleeping_eating_drinking.sleeping,
728                             source_state_definition=model.cat_sleeping_eating_drinking.initial_state(),
729                             target_state_definition=model.cat_sleeping_eating_drinking.sleeping()
730            ),
731        ]
732
733        self.event_type_transition_lookup[initial_event_types.initial_event_cat_sleeping_eating_drinking__awake] = [
734            SourceTargetPair(source=self.states.cat_sleeping_eating_drinking__awake.initial_state,
735                             target=self.states.cat_sleeping_eating_drinking__awake.staring_out_window,
736                             source_state_definition=model.cat_sleeping_eating_drinking.awake.initial_state(),
737                             target_state_definition=model.cat_sleeping_eating_drinking.awake.staring_out_window()
738            ),
739        ]
740
741    def initialize_action_mappings(self):
742
743        # State machine: cat_sleeping_eating_drinking
744        # State: cat_sleeping_eating_drinking.awake
745        action.register_action_state_hooks(ActionStateHooks(
746            state=model.cat_sleeping_eating_drinking.awake(),
747            generated_state=self.states.cat_sleeping_eating_drinking.awake,
748            generated_last_entry_time_ticks=self.last_state_entry_time_ticks.cat_sleeping_eating_drinking.awake,
749            generated_state_entry_counters=self.state_entry_counters.cat_sleeping_eating_drinking.awake,
750            state_tracking=self.state_tracking.cat_sleeping_eating_drinking__awake
751        ))
752
753        # State machine: cat_sleeping_eating_drinking.awake
754        # State: cat_sleeping_eating_drinking.awake.drinking
755        action.register_action_state_hooks(ActionStateHooks(
756            state=model.cat_sleeping_eating_drinking.awake.drinking(),
757            generated_state=self.states.cat_sleeping_eating_drinking__awake.drinking,
758            generated_last_entry_time_ticks=self.last_state_entry_time_ticks.cat_sleeping_eating_drinking__awake.drinking,
759            generated_state_entry_counters=self.state_entry_counters.cat_sleeping_eating_drinking__awake.drinking,
760            state_tracking=self.state_tracking.cat_sleeping_eating_drinking__awake__drinking
761        ))
762        # State: cat_sleeping_eating_drinking.awake.eating
763        action.register_action_state_hooks(ActionStateHooks(
764            state=model.cat_sleeping_eating_drinking.awake.eating(),
765            generated_state=self.states.cat_sleeping_eating_drinking__awake.eating,
766            generated_last_entry_time_ticks=self.last_state_entry_time_ticks.cat_sleeping_eating_drinking__awake.eating,
767            generated_state_entry_counters=self.state_entry_counters.cat_sleeping_eating_drinking__awake.eating,
768            state_tracking=self.state_tracking.cat_sleeping_eating_drinking__awake__eating
769        ))
770        # State: StateMachine.initial_state
771        action.register_action_state_hooks(ActionStateHooks(
772            state=model.cat_sleeping_eating_drinking.awake.initial_state(),
773            generated_state=self.states.cat_sleeping_eating_drinking__awake.initial_state,
774            generated_last_entry_time_ticks=self.last_state_entry_time_ticks.cat_sleeping_eating_drinking__awake.initial_state,
775            generated_state_entry_counters=self.state_entry_counters.cat_sleeping_eating_drinking__awake.initial_state,
776            state_tracking=None
777        ))
778        # State: cat_sleeping_eating_drinking.awake.staring_out_window
779        action.register_action_state_hooks(ActionStateHooks(
780            state=model.cat_sleeping_eating_drinking.awake.staring_out_window(),
781            generated_state=self.states.cat_sleeping_eating_drinking__awake.staring_out_window,
782            generated_last_entry_time_ticks=self.last_state_entry_time_ticks.cat_sleeping_eating_drinking__awake.staring_out_window,
783            generated_state_entry_counters=self.state_entry_counters.cat_sleeping_eating_drinking__awake.staring_out_window,
784            state_tracking=self.state_tracking.cat_sleeping_eating_drinking__awake__staring_out_window
785        ))
786        # State: StateMachine.initial_state
787        action.register_action_state_hooks(ActionStateHooks(
788            state=model.cat_sleeping_eating_drinking.initial_state(),
789            generated_state=self.states.cat_sleeping_eating_drinking.initial_state,
790            generated_last_entry_time_ticks=self.last_state_entry_time_ticks.cat_sleeping_eating_drinking.initial_state,
791            generated_state_entry_counters=self.state_entry_counters.cat_sleeping_eating_drinking.initial_state,
792            state_tracking=None
793        ))
794        # State: cat_sleeping_eating_drinking.sleeping
795        action.register_action_state_hooks(ActionStateHooks(
796            state=model.cat_sleeping_eating_drinking.sleeping(),
797            generated_state=self.states.cat_sleeping_eating_drinking.sleeping,
798            generated_last_entry_time_ticks=self.last_state_entry_time_ticks.cat_sleeping_eating_drinking.sleeping,
799            generated_state_entry_counters=self.state_entry_counters.cat_sleeping_eating_drinking.sleeping,
800            state_tracking=self.state_tracking.cat_sleeping_eating_drinking__sleeping
801        ))
802
803    def initialize_entity_type_mappings(self):
804        action.register_action_entity_type_hooks(ActionEntityTypeHooks(
805            entity_type=model.Cat(),
806            entity_attributes=self.attributes,
807            schedule_event_method=self.schedule_event_abs,
808            schedule_event_binomial_method=self.schedule_event_binomial,
809            schedule_event_trials_method=self.schedule_event_trials
810        ))
811
812    def initialize_entity_type(self):
813        self.entity_type = model.Cat
814
815    def initialize_state_machines(self):
816        """Initialize the top-level state machines. Note that we do not initialize sub-state-machines here."""
817        self.schedule_event_abs(
818            event_obj=initial_event_types.initial_event_cat_sleeping_eating_drinking(), 
819            time_ticks_abs=0, 
820            entity_ids=None
821        )
822
823    def initialize_lookup_tables(self):
824        """Initialize lookup tables."""
825
826    def register_lookup_tables(self):
827        """Register lookup tables w/ action handling."""
828
829    def initialize_distributions(self):
830        """Initialize distributions."""
831
832    def register_distributions(self):
833        """Register distributions with action handling."""
834        
835        def uniform(numpy_rng, low, high, num_samples):
836            return numpy_rng.uniform(low,high,num_samples)
837
838        def exponential(numpy_rng, rate, num_samples):
839            scale = 1 / rate
840            return numpy_rng.exponential(scale, num_samples)
841
842        def poisson(numpy_rng, rate, num_samples):
843            return numpy_rng.poisson(rate, num_samples)
844        
845        def normal(numpy_rng, mean, std, num_samples):
846            return numpy_rng.normal(mean, std, num_samples)
847        
848        def normal_positive(numpy_rng, mean, std, num_samples):
849            return numpy.abs(numpy_rng.normal(mean, std, num_samples))
850        
851        def normal_int(numpy_rng, mean, std, num_samples):
852            return numpy.round(numpy_rng.normal(mean, std, num_samples))
853        
854        def normal_positive_int(numpy_rng, mean, std, num_samples):
855            return numpy.abs(numpy.round(numpy_rng.normal(mean, std, num_samples)))
856        
857        def calculate_lognormal_underlying_parameters(mean, std):
858            """Given the mean and sigma (std deviation) for a lognormal distribution, calculates the mean
859            and sigma of the underlying normal distribution."""
860            underlying_mean = numpy.log((mean ** 2) / (numpy.sqrt(mean ** 2 + std ** 2)))
861            underlying_sigma = numpy.sqrt(numpy.log(1 + (std ** 2 / mean ** 2)))
862            return underlying_mean, underlying_sigma
863        
864        def log_normal(numpy_rng, mean, std, num_samples):
865            underlying_mean, underlying_sigma = calculate_lognormal_underlying_parameters(mean, std)
866            return numpy_rng.lognormal(underlying_mean, underlying_sigma, num_samples)
867        
868        def log_normal_int(numpy_rng, mean, std, num_samples):
869            underlying_mean, underlying_sigma = calculate_lognormal_underlying_parameters(mean, std)
870            return numpy.round(numpy_rng.lognormal(underlying_mean, underlying_sigma, num_samples))
871        
872
873    def finalize(self):
874        applicable_entity_ids = numpy.arange(self.num_entities)
875        with action.entity_match(entity_type=self.entity_type(),
876                                 entity_set=applicable_entity_ids,
877                                 criteria=None
878        ) as finalizing_entities:
879            model.cat_sleeping_eating_drinking.finalize_action(finalizing_entities)
880        
881
882        action.log('-----------------------------------')
883        action.log('State Tracking')
884        action.log('-----------------------------------')
885        action.log(beeprint.pp(self.state_tracking, output=False))
886
887if __name__ == '__main__':
888
889    logging.basicConfig(stream=sys.stdout, level=os.environ.get("LOGLEVEL", "INFO"))
890    
891    arg_parser = argparse.ArgumentParser()
892    arg_parser.add_argument("--num_entities", type=int, default=100, help='Number of entity instances.')
893    arg_parser.add_argument("--enable_logging", type=int, default=0, help='Enable logging, or not.')
894    arg_parser.add_argument("--rand_seed", type=int, default=1, help='Random number seed.')
895    arg_parser.add_argument("--end_time_ticks", type=float, default=10, help='Simulation end time (ticks).')
896    arg_parser.add_argument("--progress_interval_ticks", type=float, default=100, help='Interval between reporting sim progress (ticks).')
897    args = arg_parser.parse_args()
898
899    enable_logging = True if args.enable_logging > 0 else False
900    
901    if enable_logging:
902        print('Sim Arguments:')
903        beeprint.pp(vars(args))
904
905    entities_Cat = Entities_Cat(
906        num_entities=args.num_entities,
907        enable_logging=enable_logging,
908        info_interval_ticks=args.progress_interval_ticks
909    )
910
911    entities_Cat.set_seed(args.rand_seed)
912
913    entities_Cat.step(to_time_ticks=args.end_time_ticks)
914
915    entities_Cat.finalize()