Create a Time Aware Simulator

This tutorial will help you to create a simulators that keeps track of the current time using demod. It assumes that you have followed the previous tutorial about creating a demod simulator.

Demod helps handling time in the simulation when required. It can do that for simulator inheriting from TimeAwareSimulator.

class TimeAwareExampleSimulator(TimeAwareSimulator):
    """Adds Time functionality to our previous example Simulator."""

The first thing we have to handle now is the constructor of the simulator. Let’s first check the one of the TimeAwareSimulator.

TimeAwareSimulator.__init__(n_households, start_datetime=datetime.datetime(2014, 1, 1, 4, 0), step_size=datetime.timedelta(seconds=60), logger=None, **kwargs)

Create a Time Aware Simulator.

Parameters
  • n_households (int) – The number of households

  • start_datetime (datetime.datetime) – The start of the simulation. Defaults starts at 4 am on the 1rs January 2014.

  • step_size (datetime.timedelta) – The duration of a step. Defaults to 1 minute.

  • logger (Optional[demod.simulators.base_simulators.SimLogger]) – An optional logger object. Defaults to None.

Raises

TypeError – If the types of the inputs does not match.

Return type

None

There are two new parameters that are mandatory. The step_size which will tell how long a step of the simulator last, and start_datetime which specifies when the simulation starts.

Note

Demod uses the standard python library datetime for handling the time, where datetime.datetime represent a time stamp and datetime.timedelta represent time intervals.

Let’s adapt the __init__ and methods:

def __init__(
        self, n_households, max_residents, *args,
        initialization_algo='random', **kwargs
    ):

    # The new parameters will simply be passed through  *args, **kwargs to
    # TimeAwareSimulator, we don't need any change about that.
    super().__init__(n_households, *args, **kwargs)
    self.occupants = np.empty(n_households, dtype=int)

    # Initialize the simulator
    self.initialize_starting_state(max_residents)

Note

Our example simulator can accept any step_size, but some simulator might allow only one step size, which can be specified in the __init__ method. You can also add a condition on the start_datetime, if for example the simulator is used for retrieving old climate data, the start_datetime must be included in the dataset.

We did not need to change many things, excepted that we added a new initialization_algo argument with a default value.

Now let’s see how we can use initialize_starting_state.

TimeAwareSimulator.initialize_starting_state(*args, initialization_time=None, **kwargs)

Initialize the starting state of a time aware simulator.

This method will compute how many initialization steps are required to get to current_time. The steps are calculated based on step_size and initialization_time. The initial datetime will be current_time or if it is not possible due to step_size constraints, the first datetime reachable by the simulator before current_time.

Parameters
  • *args – Any arg that must be passed to the step method during the initialization.

  • initialization_time (Optional[Union[datetime.datetime, datetime.timedelta]]) – The time or datetime for which the simulator is initalized. Defaults to None (no initialization steps, initialized at the current datetime).

  • *kwargs – Any keyword arg that must be passed to the step method during the initialization.

Raises
  • ValueError – If the initialization_time is not reachable by the simulator.

  • NotImplementedError – If the type of the initialization_time is not recognized.

Return type

None

We can implement our initialization like this:

def initialize_starting_state(self, max_residents, initialization_algo):
    """This is the function initializing the starting state.

    Different methods, depending on the algorithm.
    """
    if initialization_algo == 'random':
        # Same as previous initialization
        self.occupants = np.random.randint(
            0, max_residents, size=self.n_households
        )
        super().initialize_starting_state(
            # Will not run any step
            # self.current_time tracks the time during simulation
            initialization_time=self.current_time
        )
    elif initialization_algo == 'all_inside_at_4am':
        # Now assume that all the residents are at home at 4 AM

        # Sets the residents to be all there
        self.occupants = max_residents * np.ones(self.n_households)

        # Call to the parent initialization
        super().initialize_starting_state(
            # Specifies that the method has been initialized for 4 AM
            initialization_time=datetime.time(4, 0, 0),
            # Sets dummy variables for the step function during
            # the initial steps
            arriving=np.zeros(self.n_households),
            leaving=np.zeros(self.n_households)
        )
    else:
        # Import a specific error message
        from demod.utils.error_messages import UNIMPLEMENTED_ALGO_IN_METHOD
        # Raise the error message
        raise NotImplementedError(UNIMPLEMENTED_ALGO_IN_METHOD.format(
            algo=initialization_algo,
            method=self.initialize_starting_state,
        ))

Note

Demod provides support for error messages, you can learn more here.

Finally we can look at the step function:

TimeAwareSimulator.step()

Perform a time aware step.

Update the current_time according to the step_size.

Return type

None

The interesting thing is that you can add a callback to the step function that will trigger another method when being called.

from demod.simulators.base_simulators import Callbacks

@ Callbacks.before_next_day_4am
def step(self, arriving, leaving):
    """Step function of the simulator.
    """
    self.occupants += arriving
    self.occupants -= leaving
    super().step()

def on_before_next_day_4am(self):
    """This function is called by the Callbacks.before_next_day_4am

    It will be called every day at 4 am, right before the step
    function is called.
    """
    # We want to print the percentage households with no one at home.
    print("There is {} percent of the households that are empty".format(
        np.mean(self.occupants==0) * 100
    ))

You can see all the available callbacks function, and their corresponding methods in demod.simulators.base_simulators.Callbacks.

Finally as a bonus, you can try to pass logger=SimLogger('current_datetime', 'your_getter) to your new simulator, to have a plot that used the time in the x coordinates !

You can continue the tutorial by