Skip to content

Core

The dissmodel.core module provides the simulation clock and execution lifecycle, built on top of Salabim's discrete event engine.

All models and visualization components must be instantiated after the Environment — they register themselves automatically on creation.

Environment  →  Model  →  Visualization  →  env.run()
     ↑             ↑            ↑                ↑
  first         second        third           fourth

Usage

from dissmodel.core import Environment, Model

env = Environment(start_time=1, end_time=10)

class MyModel(Model):
    def setup(self):
        pass

    def execute(self):
        print(f"step {self.env.now()}")

MyModel()
env.run()

Object-Oriented Modeling

Object-oriented modeling is a core feature of DisSModel, inherited directly from Python's class system. Just as TerraME defines agents as objects with encapsulated attributes and behaviours, DisSModel uses class inheritance to build structured, reusable, and modular models.

Every model is a subclass of Model, which guarantees automatic registration with the active Environment. This means the simulation clock, the execution lifecycle, and any visualization components are wired together without any boilerplate.

from dissmodel.core import Model, Environment

class SIR(Model):

    def setup(self, susceptible=9998, infected=2, recovered=0,
              duration=2, contacts=6, probability=0.25):
        self.susceptible  = susceptible
        self.infected     = infected
        self.recovered    = recovered
        self.duration     = duration
        self.contacts     = contacts
        self.probability  = probability

    def execute(self):
        total       = self.susceptible + self.infected + self.recovered
        alpha       = self.contacts * self.probability
        new_inf     = self.infected * alpha * (self.susceptible / total)
        new_rec     = self.infected / self.duration
        self.susceptible -= new_inf
        self.infected    += new_inf - new_rec
        self.recovered   += new_rec

Instantiation is clean and parametric:

env = Environment(end_time=30)
SIR(susceptible=9998, infected=2, recovered=0,
    duration=2, contacts=6, probability=0.25)
env.run()

!!! tip "Why subclass Model?" - Automatic clock integrationself.env.now() is always available inside execute(). - Encapsulation — each model owns its state; multiple instances can run in the same environment independently. - Extensibility — override setup() to add parameters, execute() to define the transition rule. Nothing else is required. - Composability — models can read each other's state, enabling coupled CA + SysDyn simulations within a single env.run().

Each model can define its own start_time and end_time, independent of the environment interval. This allows different parts of a simulation to be active at different periods within the same run.

from dissmodel.core import Model, Environment

class ModelA(Model):
    def execute(self):
        print(f"[A] t={self.env.now()}")

class ModelB(Model):
    def execute(self):
        print(f"[B] t={self.env.now()}")

class ModelC(Model):
    def execute(self):
        print(f"[C] t={self.env.now()}")

env = Environment(start_time=2010, end_time=2016)

ModelA(start_time=2012)        # active from 2012 to end
ModelB(end_time=2013)          # active from start to 2013
ModelC()                       # active throughout

env.run()

Expected output:

Running from 2010 to 2016 (duration: 6)
[B] t=2010.0
[C] t=2010.0
[B] t=2011.0
[C] t=2011.0
[A] t=2012.0
[B] t=2012.0
[C] t=2012.0
[A] t=2013.0
[C] t=2013.0
[A] t=2014.0
[C] t=2014.0
[A] t=2015.0
[C] t=2015.0
[A] t=2016.0
[C] t=2016.0

!!! note Models with no start_time / end_time inherit the environment's interval. Models are synchronised — all active models execute at each time step before the clock advances.


API Reference

dissmodel.core.Environment

Bases: Environment

Simulation environment with support for a custom time window.

Extends :class:salabim.Environment with start_time and end_time to define the simulation boundaries explicitly.

Parameters:

Name Type Description Default
start_time float

Simulation start time, by default 0.

0
end_time float

Simulation end time. Can also be set via till in :meth:run.

None
*args Any

Extra positional arguments forwarded to :class:salabim.Environment.

()
**kwargs Any

Extra keyword arguments forwarded to :class:salabim.Environment.

{}

Examples:

>>> env = Environment(start_time=0, end_time=10)
>>> env.start_time
0
>>> env.end_time
10
Source code in dissmodel/core/environment.py
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
class Environment(sim.Environment):
    """
    Simulation environment with support for a custom time window.

    Extends :class:`salabim.Environment` with ``start_time`` and ``end_time``
    to define the simulation boundaries explicitly.

    Parameters
    ----------
    start_time : float, optional
        Simulation start time, by default 0.
    end_time : float, optional
        Simulation end time. Can also be set via ``till`` in :meth:`run`.
    *args :
        Extra positional arguments forwarded to :class:`salabim.Environment`.
    **kwargs :
        Extra keyword arguments forwarded to :class:`salabim.Environment`.

    Examples
    --------
    >>> env = Environment(start_time=0, end_time=10)
    >>> env.start_time
    0
    >>> env.end_time
    10
    """

    def __init__(
        self,
        start_time: float = 0,
        end_time: Optional[float] = None,
        *args: Any,
        **kwargs: Any,
    ) -> None:
        kwargs.pop("animation", None)
        kwargs.pop("trace", False)
        super().__init__(*args, trace=False, **kwargs)
        self.start_time = start_time
        self.end_time = end_time

    def run(self, till: Optional[float] = None) -> None:
        """
        Run the simulation over the configured time window.

        Parameters
        ----------
        till : float, optional
            Duration to run from ``start_time``. If provided, overrides
            ``end_time``. If omitted, ``end_time`` must be set.

        Raises
        ------
        ValueError
            If neither ``till`` nor ``end_time`` is defined.

        Examples
        --------
        >>> env = Environment(start_time=0, end_time=10)
        >>> env.run()
        Running from 0 to 10 (duration: 10)
        """
        self.reset()

        if till is not None:
            self.end_time = self.start_time + till
        elif self.end_time is not None:
            till = self.end_time - self.start_time
        else:
            raise ValueError("Provide 'till' or set 'end_time' before calling run().")

        print(f"Running from {self.start_time} to {self.end_time} (duration: {till})")
        super().run(till=till)

    def reset(self) -> None:
        """
        Clear accumulated plot data.

        This method is called automatically at the start of :meth:`run` to
        ensure charts start fresh on each simulation run.

        Examples
        --------
        >>> env = Environment()
        >>> env._plot_metadata = {"x": {"data": [1, 2, 3]}}
        >>> env.reset()
        >>> env._plot_metadata["x"]["data"]
        []
        """
        if hasattr(self, "_plot_metadata"):
            for item in self._plot_metadata.values():
                item["data"].clear()

    def now(self) -> float:
        """
        Return the current simulation time adjusted by ``start_time``.

        Returns
        -------
        float
            Current time as ``salabim.now() + start_time``.

        Examples
        --------
        >>> env = Environment(start_time=5)
        >>> env.now()
        5.0
        """
        return super().now() + self.start_time

now()

Return the current simulation time adjusted by start_time.

Returns:

Type Description
float

Current time as salabim.now() + start_time.

Examples:

>>> env = Environment(start_time=5)
>>> env.now()
5.0
Source code in dissmodel/core/environment.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def now(self) -> float:
    """
    Return the current simulation time adjusted by ``start_time``.

    Returns
    -------
    float
        Current time as ``salabim.now() + start_time``.

    Examples
    --------
    >>> env = Environment(start_time=5)
    >>> env.now()
    5.0
    """
    return super().now() + self.start_time

reset()

Clear accumulated plot data.

This method is called automatically at the start of :meth:run to ensure charts start fresh on each simulation run.

Examples:

>>> env = Environment()
>>> env._plot_metadata = {"x": {"data": [1, 2, 3]}}
>>> env.reset()
>>> env._plot_metadata["x"]["data"]
[]
Source code in dissmodel/core/environment.py
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def reset(self) -> None:
    """
    Clear accumulated plot data.

    This method is called automatically at the start of :meth:`run` to
    ensure charts start fresh on each simulation run.

    Examples
    --------
    >>> env = Environment()
    >>> env._plot_metadata = {"x": {"data": [1, 2, 3]}}
    >>> env.reset()
    >>> env._plot_metadata["x"]["data"]
    []
    """
    if hasattr(self, "_plot_metadata"):
        for item in self._plot_metadata.values():
            item["data"].clear()

run(till=None)

Run the simulation over the configured time window.

Parameters:

Name Type Description Default
till float

Duration to run from start_time. If provided, overrides end_time. If omitted, end_time must be set.

None

Raises:

Type Description
ValueError

If neither till nor end_time is defined.

Examples:

>>> env = Environment(start_time=0, end_time=10)
>>> env.run()
Running from 0 to 10 (duration: 10)
Source code in dissmodel/core/environment.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def run(self, till: Optional[float] = None) -> None:
    """
    Run the simulation over the configured time window.

    Parameters
    ----------
    till : float, optional
        Duration to run from ``start_time``. If provided, overrides
        ``end_time``. If omitted, ``end_time`` must be set.

    Raises
    ------
    ValueError
        If neither ``till`` nor ``end_time`` is defined.

    Examples
    --------
    >>> env = Environment(start_time=0, end_time=10)
    >>> env.run()
    Running from 0 to 10 (duration: 10)
    """
    self.reset()

    if till is not None:
        self.end_time = self.start_time + till
    elif self.end_time is not None:
        till = self.end_time - self.start_time
    else:
        raise ValueError("Provide 'till' or set 'end_time' before calling run().")

    print(f"Running from {self.start_time} to {self.end_time} (duration: {till})")
    super().run(till=till)

dissmodel.core.Model

Bases: Component

Base class for simulation models backed by a salabim Component.

Provides a time-stepped execution loop and automatic tracking of attributes marked for plotting via the :func:~dissmodel.visualization.track_plot decorator.

Parameters:

Name Type Description Default
step float

Time increment between successive :meth:execute calls, by default 1.

1
start_time float

Time at which the model starts executing, by default 0.

0
end_time float

Time at which the model stops executing, by default math.inf.

inf
name str

Component name, by default "".

''
*args Any

Extra positional arguments forwarded to :class:salabim.Component.

()
**kwargs Any

Extra keyword arguments forwarded to :class:salabim.Component.

{}

Examples:

>>> class MyModel(Model):
...     def execute(self):
...         print (self.env.now())
>>> env = Environment()
>>> model = MyModel(step=1, start_time=0, end_time=5)
>>> env.run(5)
Running from 0 to 5 (duration: 5)
0.0
1.0
2.0
3.0
4.0
Source code in dissmodel/core/model.py
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
class Model(sim.Component):
    """
    Base class for simulation models backed by a salabim Component.

    Provides a time-stepped execution loop and automatic tracking of
    attributes marked for plotting via the :func:`~dissmodel.visualization.track_plot`
    decorator.

    Parameters
    ----------
    step : float, optional
        Time increment between successive :meth:`execute` calls, by default 1.
    start_time : float, optional
        Time at which the model starts executing, by default 0.
    end_time : float, optional
        Time at which the model stops executing, by default ``math.inf``.
    name : str, optional
        Component name, by default ``""``.
    *args :
        Extra positional arguments forwarded to :class:`salabim.Component`.
    **kwargs :
        Extra keyword arguments forwarded to :class:`salabim.Component`.

    Examples
    --------
    >>> class MyModel(Model):
    ...     def execute(self):
    ...         print (self.env.now())
    >>> env = Environment()
    >>> model = MyModel(step=1, start_time=0, end_time=5)
    >>> env.run(5)
    Running from 0 to 5 (duration: 5)
    0.0
    1.0
    2.0
    3.0
    4.0
    """

    def __init__(
        self,
        step: float = 1,
        start_time: float = 0,
        end_time: float = math.inf,
        name: str = "",
        *args: Any,
        **kwargs: Any,
    ) -> None:
        super().__init__(*args, **kwargs)
        self._step = step
        self.start_time = start_time
        self.end_time = end_time

    def process(self) -> None:
        """
        salabim process loop.

        Waits until ``start_time``, then calls :meth:`execute` every
        ``step`` time units until ``end_time``.
        """
        if self.env.now() < self.start_time:
            self.hold(self.start_time - self.env.now())

        while self.env.now() < self.end_time:
            self.execute()
            self.hold(self._step)

    def execute(self) -> None:
        """
        Called once per time step.

        Override in subclasses to define model behaviour.
        """
        pass

    def __setattr__(self, name: str, value: Any) -> None:
        """
        Intercept attribute assignment to record values marked for plotting.

        If the class defines ``_plot_info`` (via the
        :func:`~dissmodel.visualization.track_plot` decorator) and ``name``
        matches a tracked attribute, the value is appended to the plot data
        buffer and registered in ``env._plot_metadata``.

        Parameters
        ----------
        name : str
            Attribute name being set.
        value : Any
            Value being assigned.
        """
        cls = self.__class__

        if hasattr(cls, "_plot_info") and name.lower() in cls._plot_info:
            plot_info: dict[str, Any] = cls._plot_info[name.lower()]
            plot_info["data"].append(value)

            if not hasattr(self.env, "_plot_metadata"):
                self.env._plot_metadata = {}

            if plot_info["label"] not in self.env._plot_metadata:
                self.env._plot_metadata[plot_info["label"]] = plot_info

        super().__setattr__(name, value)

__setattr__(name, value)

Intercept attribute assignment to record values marked for plotting.

If the class defines _plot_info (via the :func:~dissmodel.visualization.track_plot decorator) and name matches a tracked attribute, the value is appended to the plot data buffer and registered in env._plot_metadata.

Parameters:

Name Type Description Default
name str

Attribute name being set.

required
value Any

Value being assigned.

required
Source code in dissmodel/core/model.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def __setattr__(self, name: str, value: Any) -> None:
    """
    Intercept attribute assignment to record values marked for plotting.

    If the class defines ``_plot_info`` (via the
    :func:`~dissmodel.visualization.track_plot` decorator) and ``name``
    matches a tracked attribute, the value is appended to the plot data
    buffer and registered in ``env._plot_metadata``.

    Parameters
    ----------
    name : str
        Attribute name being set.
    value : Any
        Value being assigned.
    """
    cls = self.__class__

    if hasattr(cls, "_plot_info") and name.lower() in cls._plot_info:
        plot_info: dict[str, Any] = cls._plot_info[name.lower()]
        plot_info["data"].append(value)

        if not hasattr(self.env, "_plot_metadata"):
            self.env._plot_metadata = {}

        if plot_info["label"] not in self.env._plot_metadata:
            self.env._plot_metadata[plot_info["label"]] = plot_info

    super().__setattr__(name, value)

execute()

Called once per time step.

Override in subclasses to define model behaviour.

Source code in dissmodel/core/model.py
76
77
78
79
80
81
82
def execute(self) -> None:
    """
    Called once per time step.

    Override in subclasses to define model behaviour.
    """
    pass

process()

salabim process loop.

Waits until start_time, then calls :meth:execute every step time units until end_time.

Source code in dissmodel/core/model.py
62
63
64
65
66
67
68
69
70
71
72
73
74
def process(self) -> None:
    """
    salabim process loop.

    Waits until ``start_time``, then calls :meth:`execute` every
    ``step`` time units until ``end_time``.
    """
    if self.env.now() < self.start_time:
        self.hold(self.start_time - self.env.now())

    while self.env.now() < self.end_time:
        self.execute()
        self.hold(self._step)