Source code for pydsol.core.interfaces

"""
The interfaces module defines common interfaces for the major classes 
in the pydsol framework. The use of interfaces (aka abstract base classes
in Python) avoids circular references in the import of modules. 

As an example, the Simulator class refers to a Replication and a Model in 
its initialize method. Both the Replication classes and the Model class
have references to, and use methods from the Simulator class. The interfaces
module that defines the core 'contract' for the Simulator, Model, Experiment,
Replication and Statistics helps to avoid circular references, but also 
defines the core functionalities of these central classes in the pydscol
framework.

Instead of combining all classes in one huge pydsol module with 
thousands of lines of code, the interfaces nicely decouple the definition
of the classes and their implementation, and they avoid circular referencing 
of modules to each other. Think of the use of this particular interface 
module as the .h files in C++.
"""

from abc import ABC, abstractmethod
from typing import TypeVar, Generic, Dict

from pydsol.core.pubsub import EventType
from pydsol.core.utils import get_module_logger

__all__ = [
    "SimulatorInterface",
    "ReplicationInterface",
    "ExperimentInterface",
    "InputParameterInterface",
    "ModelInterface",
    "StatisticsInterface",
    "SimStatisticsInterface",
    "StatEvents",
]

logger = get_module_logger('interfaces')

# ----------------------------------------------------------------------------
# SIMULATOR INTERFACES
# ----------------------------------------------------------------------------

# The TypeVar for time is used for type hinting for simulator time types
TIME = TypeVar("TIME", float, int)


[docs]class SimulatorInterface(ABC, Generic[TIME]): """ The SimulatorInterface defines the key methods for any Simulator to be used in the pydsol-framework. Different types of Simulators can be used, e.g., fixed time increment simulators (time ticks) for ABM and for solving differential equations, and variable time increments for discrete-event models. Simulators can run as-fast-as-possible or be synchronized with the wall clock time, etc. Event Types ----------- STARTING_EVENT: EventType Will be fired when the simulator has been instructed to start. The actual start might not have happened yet. START_EVENT: EventType Will be fired when the simulator has actually started. STOPPING_EVENT: EventType Will be fired when the simulator has been instructed to stop or pause. The actual stop might not have happened yet. START_EVENT: EventType Will be fired when the simulator has actually paused or stopped. TIME_CHANGED_EVENT: EventType Will be fired when the time of the simulation has changed. This event can be very useful, for instance, to draw time-dependent graphs. """ STARTING_EVENT: EventType = EventType("STARTING_EVENT") START_EVENT: EventType = EventType("START_EVENT") STOPPING_EVENT: EventType = EventType("STOPPING_EVENT") STOP_EVENT: EventType = EventType("STOP_EVENT") TIME_CHANGED_EVENT: EventType = EventType("TIME_CHANGED_EVENT") @property @abstractmethod def name(self) -> str: """Return the name of the simulator.""" @property @abstractmethod def time_type(self) -> type: """Return the time type of the simulator.""" @property @abstractmethod def simulator_time(self) -> TIME: """Return the current absolute time of the simulator.""" @property @abstractmethod def replication(self) -> 'ReplicationInterface': """Return the replication with which the simulator has been initialized, or None when initialize has not yet been called.""" @property @abstractmethod def model(self) -> 'ModelInterface': """Return the model that is being simulated, or None when initialize for a model has not yet been called."""
[docs] @abstractmethod def initialize(self, model: 'ModelInterface', replication: 'ReplicationInterface'): """Initialize the simulator with a replication for a model."""
[docs] @abstractmethod def add_initial_method(self, target, method: str, **kwargs): """Add a method call that has to be performed at the end if initialize, and before the model starts. This can, for instance, be used to schedule the execution of simulation events before initialize has been called, and solved the problem that, for discrete event simulators, the scheduleEvent(...) methods cannot be called before initialize()."""
[docs] @abstractmethod def cleanup(self): """Clean up after a replication has finished, and prepare for the next replication to run."""
[docs] @abstractmethod def start(self): """Starts the simulator, and fire a START_EVENT when the simulator is started. The start method uses the RunUntil property with a value of the end time of the replication when starting the simulator. Note ---- Note that when the simulator was already started, an exception will be raised, and no event will be fired."""
[docs] @abstractmethod def step(self): """Steps the simulator, and fire a START_EVENT before the execution of the event, and a STOP_EVENT after the execution of the event to indicate the simulator made a step. Note ---- Note that when the simulator is already running, an exception will be raised, and no event will be fired."""
[docs] @abstractmethod def stop(self): """Stops or pauses the simulator, and fire a STOP_EVENT when the simulator is stopped. Note ---- Note that when the simulator was already stopped, an exception will be raised, and no event will be fired."""
[docs] @abstractmethod def run_up_to(self, stop_time: TIME): """Runs the simulator up to a certain time; any events at that time, or the solving of the differential equation at that timestep, will not yet be executed."""
[docs] @abstractmethod def run_up_to_including(self, stop_time: TIME): """Runs the simulator up to a certain time; all events at that time, or the solving of the differential equation at that timestep, will be executed."""
[docs] @abstractmethod def is_initialized(self) -> bool: """Return whether the simulator has been initialized with a replication for a model."""
[docs] @abstractmethod def is_starting_or_running(self) -> bool: """Return whether the simulator is starting or has started."""
[docs] @abstractmethod def is_stopping_or_stopped(self) -> bool: """Return whether the simulator is stopping or has been stopped. This method also returns True when the simulator has not yet been initialized, or when the model has not yet started."""
[docs]class ReplicationInterface(ABC, Generic[TIME]): """ The ReplicationInterface defines the method that an Replication needs to implement. It also defines the events that will be fired to indicate that the execution of a replication on the simulator has started, and that a replication on the simulator has ended. A replication provides a start time, warmup time, and duration to the simulator and model, and it is related to a unique set of seed values for the random streams used in the stochastic simulation. Event Types ----------- START_REPLICATION_EVENT: EventType Will be fired when the execution of the replication has started. END_REPLICATION_EVENT: EventType Will be fired when the execution of the replication has completed. WARMUP_EVENT: EventType Will be fired when the warmup period has been reached, and the defined statistics in the model will be cleared. """ START_REPLICATION_EVENT: EventType = EventType("START_REPLICATION_EVENT") END_REPLICATION_EVENT: EventType = EventType("END_REPLICATION_EVENT") WARMUP_EVENT: EventType = EventType("WARMUP_EVENT") @property @abstractmethod def start_sim_time(self) -> TIME: """Return the absolute start time of the replication""" @property @abstractmethod def warmup_sim_time(self) -> TIME: """Return the absolute warmup time of the replication""" @property @abstractmethod def end_sim_time(self) -> TIME: """Return the absolute end time of the replication"""
[docs]class ExperimentInterface(ABC, Generic[TIME]): """ The ExperimentInterface defines the method that an Experiment needs to implement. It also defines the events that will be fired to indicate that the execution of an experiment on the simulator has started, and that an experiment on the simulator has ended. An experiment consists of a number of replications for the model that will be executed with the same start time, warmup time, and duration. Event Types ----------- START_EXPERIMENT_EVENT: EventType Will be fired when the execution of the experiment has started. END_EXPERIMENT_EVENT: EventType Will be fired when the execution of the experiment has completed. """ START_EXPERIMENT_EVENT: EventType = EventType("START_EXPERIMENT_EVENT") END_EXPERIMENT_EVENT: EventType = EventType("END_EXPERIMENT_EVENT")
[docs]class InputParameterInterface(ABC): """ Input parameters describe different types of input parameters for the model. All parameters for a model are contained in a hierarchical map where successive keys can be retrieved using a dot-notation between the key elements. Suppose a model has two servers: server1 and server2. Each of the servers has an average service time and a number of resources. This can be coded using keys ‘server1.avg_serice_time’, ‘server1.nr_resources’, ‘server2.avg_serice_time’, ‘server2.nr_resources’. This means that the key ‘server1’ contains an `InputParameterMap` with an instance of InputParameterFloat for the service time, and `InputParameterInt` for the number of resources. Readers for the input parameter map can read the model parameters, e.g., from the screen, a web page, an Excel file, a properties file, or a JSON file. This generic interface describes the minimum set of methods and properties that an InputParameter should have. The definition of the interface avoids circular references. """ @property @abstractmethod def key(self) -> str: """ Return the key of the parameter that can be a part of the dot-notation to uniquely identify the model parameter. The key does not contain the name of the parent. The key is set at time of construction and it is immutable. """
[docs] @abstractmethod def extended_key(self): """ Return the extended key of this InputParameter including parents with a dot-notation. The name of this parameter is the last entry in the dot notation. """
@property @abstractmethod def name(self) -> str: """ Returns the concise description of the input parameter, which can be used in a GUI to identify the parameter to the user. """ @property @abstractmethod def description(self) -> str: """ Returns description or explanation of the InputParameter. For instance, an indication of the bounds or the type. This value is purely there for the user interface. """ @property @abstractmethod def default_value(self) -> object: """ Returns the default (initial) value of the parameter. The actual return type will be defined in subclasses of `InputParameter`. The default value is immutable. """ @property @abstractmethod def value(self) -> object: """ Returns the actual value of the parameter. The value is initialized with default_value and is updated based on user input or data input. The actual type will be defined in subclasses of `InputParameter`. """
[docs] @abstractmethod def set_value(self, value: object): """ Provides a new value for the parameter. The actual type of `value` will be defined in subclasses of `InputParameter`. This is actually a method and not a setter property because it can raise errors based on the validity of the value. """
@property @abstractmethod def display_priority(self) -> float: """ Return the number indicating the order of display of the parameter in the parent parameter map. Floats make it easy to insert an extra parameter between parameters that have already been allocated subsequent integer values. """ @property @abstractmethod def read_only(self) -> bool: """ Return whether a user is prohibited from changing the value of the parameter or not. """ @property @abstractmethod def parent(self): """ Return the parent map in which the parameter can be retrieved using its key. Typically, only the root InputParameterMap has no parent, and all other parameters have an InputParameterMap as parent. """
[docs]class ModelInterface(ABC): """ The ModelInterface defines the minimum set of methods that a simulation model in the pydsol-framework should implement. Every model consists of the business logic (state transitions initialized in the `construct_model` method), input parameters, output statistics, and a reference to the simulator that executes the model. The most important method for the Model is the `construct_model` method. This method is called for each replication to initialize the model to its initial state. The state of the model should be the same every time after the `construct_model` method has been called. Constant parts of the model that might be expensive to calculate (e.g., maps, large graphs, information from databases) does not have to be calculated every time in the `construct_model` method, but can be defined once in the `__init__` method instead. """
[docs] @abstractmethod def construct_model(self): """ Code to construct the model logic for each replication. This method is called for each replication to initialize the model to its initial state. The state of the model should be the same every time after the `construct_model` method has been called. Constant parts of the model that might be expensive to calculate (e.g., maps, large graphs, information from databases) does not have to be calculated every time in the `construct_model` method, but can be defined once in the `__init__` method instead. """
@property @abstractmethod def simulator(self) -> SimulatorInterface: """Return the simulator for this model."""
[docs] @abstractmethod def add_parameter(self, input_parameter): """Add an input parameter to the input parameter map."""
[docs] @abstractmethod def set_parameter(self, key: str, value: object): """set the parameter value of an input parameter."""
[docs] @abstractmethod def get_parameter(self, key: str) -> object: """return the value of an input parameter."""
[docs] @abstractmethod def output_statistics(self) -> Dict[str, "StatisticsInterface"]: """return the output statistics map."""
[docs] @abstractmethod def add_output_statistic(self, key: str, statistic: "StatisticsInterface"): """add an output statistic to the output statistics map."""
[docs] @abstractmethod def get_output_statistic(self, key: str) -> "StatisticsInterface": """retrieve an output statistic from the output statistics map."""
# ---------------------------------------------------------------------------- # STATISTICS INTERFACES # ----------------------------------------------------------------------------
[docs]class StatisticsInterface(ABC): """ The StatisticsInterface is a tagging interface for statistics classes. It defines the minimum set of method that any statistic in the pydsol-framework needs to implement. """
[docs] @abstractmethod def initialize(self) -> None: """Initialize the statistic. This can happen at a the start and/or at a simulation replication warmup event."""
@property @abstractmethod def name(self) -> str: """Return the descriptive name of the statistic."""
[docs] @abstractmethod def n(self) -> int: """Return the number of observations."""
[docs]class SimStatisticsInterface(StatisticsInterface): """ The SimStatisticsInterface is a tagging interface for statistics classes that are aware of the Simulator, and that can listen to events such as the WARMUP_EVENT to (re)initialize the statistics. """
[docs] @abstractmethod def notify(self, event) -> None: """EventListener behavior, so the statistic can be subscribed to events like WARMUP_EVENT and END_REPLICATION_EVENT. """
@property @abstractmethod def simulator(self) -> SimulatorInterface: """Return the simulator."""
# ---------------------------------------------------------------------------- # STATISTICS EVENTS # ----------------------------------------------------------------------------
[docs]class StatEvents: """ StatEvents contains the set of events that different statistics in the statistics module can fire. To avoid circular references between the statistics module that fires the events, and modules that listen to events, all events are defines in one place as `StatEvents.XXX_EVENT`. The events that can be used are listed below. """ DATA_EVENT: EventType = EventType("DATA_EVENT") """ The DATA_EVENT is the incoming event for EventBased statistics that contains a new value for the statistics. The payload is a single float. This event can be used by the EventBasedCounter and EventBasedTally and its subclasses. The event is fired from outside to these statistics. """ WEIGHT_DATA_EVENT: EventType = EventType("WEIGHT_DATA_EVENT") """The WEIGHT_DATA_EVENT is the incoming event for weighted EventBased statistics that contains a new weight-value pair for the statistics. The payload is a tuple (weight, value). This event can be used by the EventBasedWeightedTally and its subclasses.The event is fired from outside to the statistics.""" TIMESTAMP_DATA_EVENT: EventType = EventType("TIMESTAMP_DATA_EVENT") """The TIMESTAMP_DATA_EVENT is the incoming event for weighted EventBased statistics that contains a new timestamp-value pair for the statistics. The payload is a tuple (timestamp, value). This event can be used by the EventBasedTimestampWeightedTally and its subclasses.The event is fired from outside to the statistics.""" INITIALIZED_EVENT: EventType = EventType("INITIALIZED_EVENT") """INITIALIZED_EVENT indicates that the statistic has been (re)initialized, and all counters have been reset to the original values. The event is fired by the statistic to its listeners.""" OBSERVATION_ADDED_EVENT: EventType = EventType("OBSERVATION_ADDED_EVENT") """OBSERVATION_ADDED_EVENT indicates that an observation has been received, and contains the value of the observation as the payload. For weight-bsaed and timestamp-based observations, the payload is a tuple. For value-based statistics, the payload is just the (float) value. The event is fired by the statistic to its listeners.""" N_EVENT: EventType = EventType("N_EVENT") """N_EVENT indicates that the number of observations has been increased, and contains the new number of observation as the payload. The event is fired by the statistic to its listeners.""" COUNT_EVENT: EventType = EventType("COUNT_EVENT") """COUNT_EVENT is an event of the Counter statistic and is subclasses. The event indicates that the count has changed, and contains the new count as the payload. The event is fired by the statistic to its listeners.""" MIN_EVENT: EventType = EventType("MIN_EVENT") """MIN_EVENT indicates that the minimum of the observations has changed, and contains the new minimum as the payload. This event is fired by the EventBasedTally, its Weighted and Timestamped variant, and its subclasses to the listeners.""" MAX_EVENT: EventType = EventType("MAX_EVENT") """MAX_EVENT indicates that the maximum of the observations has changed, and contains the new maximum as the payload. This event is fired by the EventBasedTally, its Weighted and Timestamped variants, and their subclasses to the listeners.""" SUM_EVENT: EventType = EventType("SUM_EVENT") """SUM_EVENT indicates that the sum of the observations has changed, and contains the new sum as the payload. This event is fired by the EventBasedTally and its subclasses to the listeners.""" MEAN_EVENT: EventType = EventType("MEAN_EVENT") """MEAN_EVENT indicates that the mean of the observations has changed, and contains the new mean as the payload. This event is fired by the EventBasedTally and its subclasses to the listeners.""" POPULATION_STDEV_EVENT: EventType = EventType("POPULATION_STDEV_EVENT") """POPULATION_STDEV_EVENT indicates that the population standard deviation of the observations has changed, and contains the new standard deviation as the payload. This event is fired by the EventBasedTally and its subclasses to the listeners.""" POPULATION_VARIANCE_EVENT: EventType = EventType("POPULATION_VARIANCE_EVENT") """POPULATION_VARIANCE_EVENT indicates that the population variance of the observations has changed, and contains the new variance as the payload. This event is fired by the EventBasedTally and its subclasses to the listeners.""" POPULATION_SKEWNESS_EVENT: EventType = EventType("POPULATION_SKEWNESS_EVENT") """POPULATION_SKEWNESS_EVENT indicates that the population skewness of the observations has changed, and contains the new skewness as the payload. This event is fired by the EventBasedTally and its subclasses to the listeners.""" POPULATION_KURTOSIS_EVENT: EventType = EventType("POPULATION_KURTOSIS_EVENT") """POPULATION_KURTOSIS_EVENT indicates that the population kurtosis of the observations has changed, and contains the new kurtosis as the payload. This event is fired by the EventBasedTally and its subclasses to the listeners.""" POPULATION_EXCESS_K_EVENT: EventType = EventType("POPULATION_EXCESS_K_EVENT") """POPULATION_EXCESS_K_EVENT indicates that the population exces kurtosis of the observations has changed, and contains the new excess kurtosis as the payload. This event is fired by the EventBasedTally and its subclasses to the listeners.""" SAMPLE_STDEV_EVENT: EventType = EventType("SAMPLE_STDEV_EVENT") """SAMPLE_STDEV_EVENT indicates that the sample standard deviation of the observations has changed, and contains the new standard deviation as the payload. This event is fired by the EventBasedTally and its subclasses to the listeners.""" SAMPLE_VARIANCE_EVENT: EventType = EventType("SAMPLE_VARIANCE_EVENT") """SAMPLE_VARIANCE_EVENT indicates that the sample variance of the observations has changed, and contains the new variance as the payload. This event is fired by the EventBasedTally and its subclasses to the listeners.""" SAMPLE_SKEWNESS_EVENT: EventType = EventType("SAMPLE_SKEWNESS_EVENT") """SAMPLE_SKEWNESS_EVENT indicates that the sample skewness of the observations has changed, and contains the new skewness as the payload. This event is fired by the EventBasedTally and its subclasses to the listeners.""" SAMPLE_KURTOSIS_EVENT: EventType = EventType("SAMPLE_KURTOSIS_EVENT") """SAMPLE_KURTOSIS_EVENT indicates that the sample kurtosis of the observations has changed, and contains the new kurtosis as the payload. This event is fired by the EventBasedTally and its subclasses to the listeners.""" SAMPLE_EXCESS_K_EVENT: EventType = EventType("SAMPLE_EXCESS_K_EVENT") """SAMPLE_EXCESS_K_EVENT indicates that the sample excess kurtosis of the observations has changed, and contains the new excess kurtosis as the payload. This event is fired by the EventBasedTally and its subclasses to the listeners.""" WEIGHTED_SUM_EVENT: EventType = EventType("WEIGHTED_SUM_EVENT") """WEIGHTED_SUM_EVENT indicates that the weighted sum of the observations has changed, and contains the new weighted sum as the payload. This event is fired by the EventBasedWeightedTally and its subclasses to the listeners.""" WEIGHTED_MEAN_EVENT: EventType = EventType("WEIGHTED_MEAN_EVENT") """WEIGHTED_MEAN_EVENT indicates that the weighted mean of the observations has changed, and contains the new weighted mean as the payload. This event is fired by the EventBasedWeightedTally and its subclasses to the listeners.""" WEIGHTED_POPULATION_STDEV_EVENT: EventType = \ EventType("WEIGHTED_POPULATION_STDEV_EVENT") """WEIGHTED_POPULATION_STDEV_EVENT indicates that the weighted population standard deviation of the observations has changed, and contains the new weighted standard deviation as the payload. This event is fired by the EventBasedWeightedTally and its subclasses to the listeners.""" WEIGHTED_POPULATION_VARIANCE_EVENT: EventType = \ EventType("WEIGHTED_POPULATION_VARIANCE_EVENT") """WEIGHTED_POPULATION_VARIANCE_EVENT indicates that the weighted population variance of the observations has changed, and contains the new weighted variance as the payload. This event is fired by the EventBasedWeightedTally and its subclasses to the listeners.""" WEIGHTED_SAMPLE_STDEV_EVENT: EventType = \ EventType("WEIGHTED_SAMPLE_STDEV_EVENT") """WEIGHTED_SAMPLE_STDEV_EVENT indicates that the weighted sample standard deviation of the observations has changed, and contains the new weighted standard deviation as the payload. This event is fired by the EventBasedWeightedTally and its subclasses to the listeners.""" WEIGHTED_SAMPLE_VARIANCE_EVENT: EventType = \ EventType("WEIGHTED_SAMPLE_VARIANCE_EVENT") """WEIGHTED_SAMPLE_VARIANCE_EVENT indicates that the weighted sample variance of the observations has changed, and contains the new weighted variance as the payload. This event is fired by the EventBasedWeightedTally and its subclasses to the listeners."""