Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Binder

In this chapter we discuss some basic classifications of models and modelling approaches.

Objectives:

  • What is a top-level classification for modelling and simulation approaches?

  • What does continuous/discrete, stochastic/deterministic and microscopic/macroscopic mean?

import numpy as np
from matplotlib import pyplot as plt
from matplotlib.ticker import MultipleLocator

Three Systems

As described in the chapters Basics and Modelling Circle, a modelling and simulation experiment runs through (at least) three different system levels (see picture below, losely from Sargent (2010), Figure 2). In the modelling process, a problem is abstracted from the real system using a suitable modelling approach. To be precise, this is actually done based from our perception of the real-system via our senses, our logic or from domain knowledge of others, condensed to what is relevant for the modelling purpose - we will refer to this as the perceived real system. Therefore, as soon as our perception or the real system changes, we need to question the validity of any model developed based on our old views. Anyhow, the resulting conceptual model describes the formalised system and can be formally examined at this level using analytic methods. In the implementation process, the conceptual model is converted into an implemented model using computational and numerical methods. It describes the computerised system and can be examined using computational methods, in particular by performing simulation experiments. All three systems depict the same object or circumstance in their own way.

An important observation here is that a process of abstraction and simplification takes place during the transition from one system to the other. This is obvious in the transition from the real to the formal system which is called conceptual modelling. However, the fact that this must also take place in the transition from the conceptual to computerised system is perhaps not so clear ad hoc, as the respective description languages (mathematical/formal vs. code) are very similar. In many cases, a conceptual model must be further simplified in order to be executable by a computer, for example, when a numerical algorithm is needed to solve an equation.

In the following sections, we will describe various characteristics that the systems mentioned may have and give examples.

Dynamic / Static

A system/model/modelling-approach is regarded dynamic if its time-evolution is under investigation. A dynamic formal system is characterised by a model which uses the free variable tt. A computerised dynamic model is usually identified by requiring some sort of time-loop to advance.

Most systems where modelling and simulation is applied as a method are dynamic, mainly because corresponding methods center around the evolution of time. Examples for static application usually focus on analysis of time-equilibria (steady-states),such as computing the deformation of certain body under stress (elasticity theory), break-even analysis in eqonomic applications, or orbital computations in quantum dynamics. In these cases, however, it can be argued that the corresponding systems are actually dynamic ones which are evaluated at a very specific point in time, enabling a special time-independent analysis.

Also, with regard to dynamic systems, we can differentiate in terms of structure whether modelling and simulation is actually a suitable method. Suppose, a formalized dynamic model can be written as a function ff with

y(t)=f(t,u(t),p(t))y(t)=f(t,u(t),p(t))

whereas y(t)y(t) describes the time-dependent output and u(t),p(t)u(t),p(t) input and parameters of the model, development of a computerised model and performing simulation is usually not necessary - at least as long as ff is sufficiently simple. Even though the model is clearly dynamic, its structure is sufficiently simple that a wide range of analyses can be carried out using formal methods alone without the aid of a computer or numerical algorithm, in particular, the direct evaluation of the model for an arbitrary point in time.

As soon as systems become implicit, e.g. (t)=f(t,u(t),p(t),y(t))(t)=f(t,u(t),p(t),y(t)), or rely on the description of time-dynamics instead y(t)=f(t,u(t),p(t),y(t))y'(t)=f(t,u(t),p(t),y(t)), incorporate stochasticty, y(t)=f(t,u(t),p(t),y(t),X(t))y'(t)=f(t,u(t),p(t),y(t),X(t)) or cannot be properly expressed in terms of analytic functions, the computerised system and its features for simulations becomes relevant.

Time-Continuous / Discrete

Time flows — not only proverbially, but also in our usual perception of the world. We perceive the present moment as infinitesimally short and have the idea or feeling that something around us is constantly changing — or could change — at any moment. That means, we would expect that it is reasonable to investigate the state of a system at any possible point in time, i.e. on a continuous scale. This perception might change when we condense the real system to what is relevant for a modelling task. E.g. the clock on your desktop will (likely) show the same time for precisely one minute, at which point it instantaneously switches to a new state. Is it reasonable to say, that the clock updates continuously? Well, all processes around might, but the clock itself does not, it updates in a discrete way.

It is surprisingly difficult to grasp the intuitive concepts of discrete vs. continuous mathematically correctly. In Kaliyadan & Kulkarni (2019) a variable is called continuous if, for any two values aa and bb also a<c<ba<c<b can be taken. While this definition might be intuitive, it does not regard potential differences between the real and rational numbers and the subtle differences between the two number formats with respect to analysis (e.g. differentiation, integration, etc.). In Joshi (1989) a variable is called discrete, if between any two values a<ba<b taken by the variable, the difference bab-a is non-infinitesimal. Note, that a variable must not always be the one or the other, e.g. by being discrete in some places and continuous in others.

We generally call a system time-discrete if time is represented by a discrete variable and time-continuous if time is represented by a continuous variable. Most literature simplify this slightly, so that a system which state xx can be expressed as a time function x:TX:tx(t)x:T\rightarrow X: t\mapsto x(t) is considered discrete, if TZT\subseteq \mathbb{Z} and continuous, if TRT\subseteq\mathbb{R} Khalil & Grizzle (2002). This definition is more practical and more intuitive, however comes with limitations, e.g. when examining systems which use non-equidistant time-steps.

Value-Continuous / Discrete

Analogously, systems can also be classified in terms of the values that their state variables can take. In general, a system is referred to as a value-discrete system if all (relevant) state variables are discrete, and as a value-continuous if they are continuous. Note, that time itself must not be considered as state variable in this regard.

It should be noted, however, that the aim here is not so much to make a formally correct and precise statement, but rather to describe and communicate a system as effectively as possible. Accordingly, in many cases it makes sense to prioritize a classification that facilitates understanding over one that is strictly formally correct .

We give four simple examples for systems/models which span all combiations of time and value continuous and discrete, respectively to illustrate that any combination of these is sensible.

Case Study: (a) Rope Pendulum - Time-Continuous/Value-Continuous

We aim to model the trajedoctry of the bob of a rope pendulum. Perceived Real System. We face a very well known Physical system which is perceived as something entriely continuous: the position of the bob changes smoothly with time and observing it for any two consecutive points in time it is a nontrivial question to ask where it has been in between. Formalised System. Systems like these are best modelled using ordinary differential equations. One finds

ϕ˙(t)=mgϕ(t),ϕ(0)=ϕ0\dot{\phi}(t)=-mg\phi(t),\quad \phi(0)=\phi_0

as a (somewhat) reasonable model for the time-dynamics of the angle of the pendulum in radiants, measured from its vertical steady-state. Hereby, mm stands for the mass of the bob and gg for the magnitude of the gravitational field. Note that we do not question validity or simplifying assumptions here. Anyhow, due to its simplicity the equation can be solved analytically to

ϕ(t)=ϕ0cos(mgt).\phi(t)=\phi_0\cos\left(\sqrt{mg}t\right).

Case Study: (b) Supermarket-Queue - Time-Continuous/Value-Discrete

We aim to model the length of a queue (in persons) in front of a supermarket’s cashier desk. Perceived Real System. Alike any accumultion of objects, the length of a queue must be considered discrete. It changes its length by percisely one when a new customer enters and when a queueing customer is served. Since there is no limitation when this might be, we need to consider the system as time-continous. Formalised System. Systems like these are usually modelled using a so-called discrete-event approach. That means, the model considers the state of the system, in this case the length of the queue QQ, and evaluates when the state changes by sampling corresponding events, in this case, event E1E_1 when a new customer comes (i.e. Q+=1Q+=1) and E2E_2 when the first-in-line customer is served and leaves the queue (i.e. Q=1Q-=1). Arrival of customers is modelled as a Poisson process meaning that the time in between any E1E_1 events is exponentially distributed with rate λ\lambda (i.e. the arrival rate). For the time in between any E2E_2 events (i.e. the service time) we consider a uniform distribution U(a,b)U(a,b). Although the system only evaluates the state and the change of QQ at the time of the events, the state of the queue is precisely known at any point in time in between which makes the model time-continuous.

Case Study: (c) Turnover with a New Product - Time-Discrete/Value-Continuous

We aim to model the monthly turnover from a new procuct. Perceived Real System. Although actual sales in the retail sector occur continuously, it is neither possible nor practical to monitor the corresponding dynamics continuously, as both production and pricing must be planned over specific periods (e.g. weeks, months or years). Consequently, only a discrete-time view of the system makes sense. However, the corresponding turnover lies on a continuous spectrum. Formalised System A system like this can be modelled using the concept of difference equations. In this approach, the state of the system at the next observation point is computed from is previous one(s). For example, the discretised Bass Diffusion model states

xt+1=xt+p(Mxt)+qxtM(Mxt),x0=0x_{t+1}=x_t+p(M-x_{t})+q\frac{x_t}{M}(M-x_{t}),\quad x_0=0

whereas xtx_t is the total number of customers after month tt and MM is the total market potential. Furthermore, pp and qq are adoption parameters for the new product. Monthly turnover for month t+1t+1 is computed by c(xt+1xt)c(x_{t+1}-x_{t}) whereas cc denotes the price of the product. Clearly, the formalised system is time-discrete since it is compued from an iteration, its value, however, can take an arbitrary positive value.

Case Study: (d) Roulette Table - Time-Discrete/Value-Discrete

We aim to model the earnings and losses of a person playing Roulette in a casino. Perceived Real System. Roulette as well as most other games is played in rounds. Moreover wins and losses are multiples of the value of the smallest chip. As a result, the system can be considered discrete in both dimensions. Formalised System. Models of this kind fall in the domain of game theory and usually consist of two component. One component simulates the game and evaluates the round using random variables. The second component, which is usually considerably more complex than the first, models the strategy pursued by the player. In this case study, we simply want to allow the player to place a random whole number of chips, say ntNt1n_t\leq N_{t-1} for round tt, on random single fields. The game is evaluated by computing a random integer between 1 and 37. In case of a loss, the stack decreases Nt=Nt1ntN_t=N_{t-1}-n_t, otherwise it is incremented via Nt=Nt1nt+35N_t=N_{t-1}-n_t+35.

Case Studies: (a)-(d) - Computerised Models

Below you find implementations of the models (a)-(d) and some representative simulation results. We would like to put the emphasis on the structure and shape of the results and the result plot.

class SimplePendulumModel:
    def __init__(self, m: float, phi_0: float):
        """Simple model of a rope pendulum
        :param m: mass of the bob in kg
        :param phi_0: initial angle in radians
        """
        self.m = m
        self.g = 9.81
        self.phi_0 = phi_0

    def run(self, t_end: float) -> tuple[np.ndarray, np.ndarray]:
        """Runs the model until a given end-time
        :param t_end: simulation end time in seconds
        :return: array of representative time-instants and corresponding angles
        """
        t = np.arange(0, t_end, t_end / 100)
        phi = np.array([self.phi_0 * np.cos(np.sqrt(self.m * self.g) * t) for t in t])
        return t, phi


class SimpleQueueModel:
    def __init__(self, lam: float, a: float, b: float):
        """Simple model of a supermarket queue.
        :param lam: average time interval between two customers entering the queue [sec]
        :param a: minimum service time at the cashiers desk per person [sec]
        :param b: maximum service time at the cashiers desk per person [sec]
        """
        self.lam = lam
        self.a = a
        self.b = b

    def draw_interarrival_time(self) -> float:
        """Draws a random time (seconds) between two arrivals
        :return: inter arrival time
        """
        return np.random.exponential(self.lam)

    def draw_service_time(self) -> float:
        """Draws a random sevice time (seconds)
        :return: service time
        """
        return self.a + (self.b - self.a) * np.random.random()

    def run(self, t_end: float, seed: int = 12345) -> tuple[np.ndarray, np.ndarray]:
        """Runs the model until a given end-time
        :param t_end: simulation end time in seconds
        :param seed: optional seed for the random number generation
        :return: time-instants of all events and corresponding queue lengths right before and after event execution
        """
        np.random.seed(seed)
        t = [0]
        q = [0]
        busy = False
        evs = [(0, 0)]
        while t[-1] < t_end:
            ev = evs.pop(0)
            t.append(ev[0])  # before event execution
            q.append(q[-1])
            if ev[1] == 0:  # arrival
                t.append(ev[0])
                q.append(q[-1] + 1)  # make the queue longer
                evs.append(
                    (ev[0] + self.draw_interarrival_time(), 0)
                )  # add a new arrival event
                if not busy:  # start with service
                    evs.append(
                        (ev[0] + self.draw_service_time(), 1)
                    )  # add a new end-service event
                    busy = True
            if ev[1] == 1:
                t.append(ev[0])
                q.append(q[-1] - 1)  # make the queue shorter
                busy = False
                if q[-1] > 0:  # start with the next service
                    evs.append(
                        (ev[0] + self.draw_service_time(), 1)
                    )  # add a new end-service event
                    busy = True
            evs.sort()  # sort the list to get the correct next event
        return np.array(t), np.array(q)


class SimpleTurnoverModel:
    def __init__(self, M: int, p: float, q: float, c: float):
        """Simple model for the mothly turnover from a new product
        :param M: market potential (potential customers)
        :param p: advertising effect
        :param q: imitation (word of mouth) effect
        :param c: turnover per sale
        """
        self.M = M
        self.p = p
        self.q = q
        self.c = c

    def run(self, t_end: int) -> tuple[np.ndarray, np.ndarray]:
        """Runs the model until a given end-time
        :param t_end: simulated number of months
        :return: time series of months and corresponding turnover
        """
        t = [0]
        x = [0.0]
        while t[-1] < t_end:
            t.append(t[-1] + 1)  # incement time by 1
            y = self.M - x[-1]  # potential customers left
            s1 = self.p * y  # sales by advertising
            s2 = self.q * y * x[-1] / self.M  # sales by imitation / word of mouth
            x.append(x[-1] + s1 + s2)  # update sales
        to = [
            self.c * (b - a) for a, b in zip(x[:-1], x[1:])
        ]  # compute monthly turnover
        return np.array(t[1:]), np.array(to)


class SimpleRouletteModel:
    def __init__(self, chips_0: int):
        """Simple model of a roulette player
        :param chips_0: initial number of available chips
        """
        self.chips_0 = chips_0
        self.chips = chips_0
        self.bet: dict[int, int] = dict()

    def place_bet(self) -> None:
        """Places a random bet for a round
        :return:
        """
        self.bet = dict.fromkeys(range(37), 0)
        while self.chips > 0:
            self.bet[np.random.randint(1, 37)] += 1
            self.chips -= 1
            if np.random.random() < 0.25:
                break  # simply continue betting until chips are spent or with chances 1:4

    def run(self, t_end: int, seed: int = 12345) -> tuple[np.ndarray, np.ndarray]:
        """Runs the model until a given end-time
        :param t_end: number of rounds to play
        :param seed: optional seed for the random number generation
        :return: time series of rounds and corresponding chip-stack
        """
        np.random.seed(seed)
        t = [0]
        x = [self.chips]
        while t[-1] < t_end:
            t.append(t[-1] + 1)  # incement round by 1
            self.place_bet()
            res = np.random.randint(0, 37)
            self.chips += self.bet[res] * 35  # increment chips
            x.append(self.chips)
        return np.array(t), np.array(x)


plt.figure(figsize=(10, 10))
plt.subplot(2, 2, 1)
mdl = SimplePendulumModel(1, 0.7)
t, x = mdl.run(10)
plt.plot(t, x)
plt.ylabel("angle [rad]")
plt.xlabel("time [s]")
plt.title("(a) simple pendulum model")

plt.subplot(2, 2, 2)
mdl = SimpleQueueModel(10, 5, 15)
t, x = mdl.run(300)
plt.plot(t, x)
plt.ylabel("queue-length [persons]")
plt.xlabel("time [s]")
plt.grid(axis="y")
plt.title("(b) simple queueing model")

plt.subplot(2, 2, 3)
mdl = SimpleTurnoverModel(10000, 0.1, 0.3, 39.90)
t, x = mdl.run(12)
plt.plot(t, x, "o")
plt.xticks(t)
plt.grid(axis="x")
plt.ylabel("turnover [euro]")
plt.xlabel("time [months]")
plt.title("(c) simple turnover model")

plt.subplot(2, 2, 4)
mdl = SimpleRouletteModel(20)
t, x = mdl.run(15)
plt.plot(t, x, "o")
plt.xticks(t)
plt.gca().yaxis.set_minor_locator(
    MultipleLocator(1)
)  # make sure that there are grid-marks for every integer
plt.gca().xaxis.grid(True)
plt.gca().yaxis.grid(True, which="minor")
plt.ylabel("chips count")
plt.xlabel("time [rounds]")
plt.title("(d) simple roulette model")

plt.show()
<Figure size 1000x1000 with 4 Axes>

In the four stated case studies, the interpretation of continuous / discrete remained unchanged in the course of the process from perceived real, formalised to computerised system, i.e. the systems were modelled and implemented precisely as they were perceived.

It is important to notice that this is not necessary. Examples for which systems perceived as discrete/continuous are modelled precisely the other way round are manifold. A beautiful example for this is given by the Predator-Prey case study in the Simulation Circle chapter. Here a clearly discrete number of prey- and predator-individuals are modelled as a continuous number since the used System Dynamics approach which is both time- and value-continuous. In order to solve the differential equations numerically, however, the computer needed to apply a numerical approximation method which eventually made the computerised model time-discrete. As a result, a time-continuous/value-discrete perceived real system is formally modelled as a time-continuous/value-continuous approach and simulated by a time-discrete/value-continuous computerised model.

In general, the term of simplifying a system by interpreting a continuous variable as a discrete one is called discretisation.

Deterministic / Stochastic

A system is called deterministic if identical conditions lead to identical behaviour, otherwise it is called stochastic. Deterministic systems are perfectly predictable, whereas stochastic ones are not.

Determinism is not usually an inherent property of the real system, but rather a matter of interpretation/perfecption that is heavily driven by the purpose of the analysis. Tossing a coin is often cited as the prime example of a random experiment: the outcome – heads or tails – is unpredictable. ... or at least so it seems. The coin is a body with mass and inertia that follows a trajectory governed by the laws of physics. It stands to reason that a complete knowledge of all relevant system components – from the force profile applied to the coin, the flow of the air, to the nature of the surface on which the coin lands – makes the system predictable and thus deterministic. The question of whether the system is perceived in one way or another ultimately depends on what the coin toss means in this context: is it used to deterine who gets to start a game, or is it a relevant component in the motor of an airplane? In the former case, we neither ask for nor intend to predict the result, in the latter case, we might want to manipulate all conditions to ensure that the coinflip result is predictable so that the plane does not crash.

When it comes to modelling, additional considerations come into play that make it preferable/necessary to represent a deterministic system stochastically instead. Even if a real system generally behaves in a deterministic manner, information relevant for deterministic modelling is either missing or is only available with a degree of uncertainty. This applies equally to values (parameters, inputs) as well to system processes (causalities). In this situation, it is up to the modeller to judge whether omitting this information is acceptable for a deterministic model or whether it would lead to invalid results. In the latter case, it makes sense to account for the uncertainties using stochasticity. In this way, the model’s predictive power is at least partially preserved, although it must always be considered in relation to the uncertainty. In the given example, the necessary measurements for development of a deterministic coin-flip model are usually not available with sufficient accuracy. We may add stochastic noise to the involved parameters and inputs to incorporate for this uncertainy and may still find answers like: “With this configuration the coin lands on heads with 90% certainty”.

In a formalised system, stochasticity can be identified by the use of distributions and random variables. Of the examples described above, both the Queuing model (b) and the Roulette model (d) are stochastic, as an element of chance is incorporated into both. The other two models are deterministic, as the outcome can be unambiguously derived from the model parameters and the input. Analogous, stochastic computerised systems are identified by the use of random numbers, which are drawn from so-called Pseudo Random Number Generators (PRNG), e.g. np.random.random() for drawing U(0,1)U(0,1) uniformly distributed numbers between 0 and 1. It should be noted that sequence of numbers they produce only appears to be random, but is not truly random. The computer or software generates these numbers using iterative algorithms which produce the same numbers for the same configuration. On the one hand, this means that the computer is (practically) incapable of producing real stochasticity (i.e. to be precise, there are no stochastic computerised systems), on the other hand, we may exploit this property to make sure that experiments are always reproducible. The experiments carried out above for models (b) and (d) always produce the same results when repeated, despite the use of random variables. This is because the command np.seed(xxxx) fixes the configuration of the PRNG. If you change the parameter of this command to a different integer, you will obtain different values.

Case Study: (b2) Supermarket-Queue - Deterministic

Instead of incorporating uncertainty regarding the arrival of new customers or the time required for service of customers as done in the original (b) Supermarket-Queue model we want to conceptualise a deterministic model. The conceptual and compouterised model structure will be retained with the exception that the time between arivals is always equal to λ\lambda and the time required for service is always (b+a)/2(b+a)/2 - both equal to the mean values of the distributions before.

class SimpleDeterministicQueueModel(SimpleQueueModel):
    def draw_interarrival_time(self) -> float:
        """Draws a time (seconds) between two arrivals
        :return: inter arrival time
        """
        return self.lam

    def draw_service_time(self) -> float:
        """Draws a sevice time (seconds)
        :return: service time
        """
        return (self.b + self.a) / 2


plt.figure(figsize=(10, 10))
plt.subplot(2, 2, 1)
mdl = SimpleQueueModel(10, 5, 15)
t, x = mdl.run(300)
plt.plot(t, x)
plt.ylabel("queue-length [persons]")
plt.xlabel("time [s]")
plt.grid(axis="y")
plt.title("(b) simple queueing model")

plt.subplot(2, 2, 2)
mdl = SimpleDeterministicQueueModel(10, 5, 15)
t, x = mdl.run(300)
plt.plot(t, x)
plt.ylabel("queue-length [persons]")
plt.xlabel("time [s]")
plt.grid(axis="y")
plt.title("(b2) simple deterministic queueing model")

plt.subplot(2, 2, 3)
mdl = SimpleQueueModel(10, 5, 15)
for i in range(100):
    t, x = mdl.run(300, i)
    plt.plot(t, x, "k", linewidth=1)
plt.ylabel("queue-length [persons]")
plt.xlabel("time [s]")
plt.grid(axis="y")
plt.title("(b) simple queueing model (100 runs)")

plt.subplot(2, 2, 4)
mdl = SimpleDeterministicQueueModel(10, 5, 15)
for i in range(100):
    t, x = mdl.run(300, i)
    plt.plot(t, x, "k", linewidth=1)
plt.ylabel("queue-length [persons]")
plt.xlabel("time [s]")
plt.grid(axis="y")
plt.title("(b2) simple deterministic queueing model (100 runs)")

plt.show()
<Figure size 1000x1000 with 4 Axes>

Not only does the stochastic queueing model offer possibilities to quantify the system’s inherent uncertainty, it also shows an entirely different qualitative behaviour.

Microscopic / Macroscopic

We describe a system as microscopic if it is perceived as a collection of similar sub-systems (compare, Bicher (2020)). Well-known examples of these include populations, with animals or people as sub-systems, or traffic, with cars as sub-systems. Just like all classifications introduced earlier, this one is also a matter of perspective and purpose. For example, even though a liquid consists of a large number of individual molecules, one would probably only regard it as such if one wished to analyse a very small volume of it in great detail. In other words, even though the term has nothing to do with the actual physical concept of “microscale”, it does indeed refer to how many components the overall system has. If this number is too large, the system will likely not be perceived as microscopic (compare Fitzpatrick, 2006).

More importantly we need to ask how a such a system is represented in the (formalised or computerised) model. If the state of a model consists of the states of similar sub-models, the model is referred to as microscopic. If, however, the system is modelled without taking the subsystems direcly into account, the model is referred to as macroscopic. It should be emphasised here that the term macroscopic only makes sense if the real system is perceived as microscopic. E.g. the Pendulum (a) model does not regard any sub-systems, still, we would not label it as macroscopic, since there are no (obvious) sub-systems in the real system. This is different e.g. for the Queueing (b) model. Here, the system clearly consists of individual customers which are simplified to an aggregated queue state.

Historically speaking, microscopic models are a more recent development than macroscopic ones, as their implementation generally requires more memory and computing power.

Case Study: (b3) Supermarket-Queue - Microscopic

Instead of “hiding” individuals in a scalar queue parameter, as done in the original (b) Supermarket-Queue model, we want to explicitly model the individual customers. That means, the queue consists of a list of customer entities.

class CustomerEntity:
    def __init__(self, t_now: float) -> None:
        """Class for a customer entity
        :param t_now: time at which the entity starts queueing
        """
        self.t_queue_start = t_now
        self.t_end_service = None

    def get_time_in_system(self, t_now: float) -> float:
        """Returns the total time the entity spent in the system
        :param t_now: current time
        :return: total time spent in the system
        """
        return t_now - self.t_queue_start


class SimpleMicroscopicQueueModel(SimpleQueueModel):
    def run(
        self, t_end: float, seed: int = 12345
    ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
        """Runs the model until a given end-time
        :param t_end: simulation end time in seconds
        :param seed: optional seed for the random number generation
        :return: time-instants of all events and corresponding queue lengths right before and after event execution, and a vector of  times customers spent in the system
        """
        np.random.seed(seed)
        queue = list()
        t = [0]
        q = [len(queue)]
        current_customer = None
        evs = [(0, 0)]
        times = list()
        while t[-1] < t_end:
            ev = evs.pop(0)
            t_now = ev[0]
            t.append(t_now)  # before event execution
            q.append(len(queue))
            if ev[1] == 0:  # arrival
                customer = CustomerEntity(t_now)  # create a new customer
                queue.append(customer)
                evs.append(
                    (ev[0] + self.draw_interarrival_time(), 0)
                )  # add a new arrival event
                if current_customer is None:  # start with service
                    current_customer = queue.pop(0)  # get the next queueing customer
                    evs.append(
                        (ev[0] + self.draw_service_time(), 1)
                    )  # add a new end-service event
            if ev[1] == 1:
                times.append(current_customer.get_time_in_system(t_now))
                current_customer = None
                if len(queue) > 0:  # start with the next service
                    current_customer = queue.pop(0)  # get the next queueing customer
                    evs.append(
                        (ev[0] + self.draw_service_time(), 1)
                    )  # add a new end-service event
            t.append(t_now)  # after event execution
            q.append(len(queue))
            evs.sort()  # sort the list to get the correct next event
        return np.array(t), np.array(q), np.array(times)


plt.figure(figsize=(14, 5))
plt.subplot(1, 3, 1)
mdl = SimpleQueueModel(10, 5, 15)
t, x = mdl.run(300)
plt.plot(t, x)
plt.ylabel("queue-length [persons]")
plt.xlabel("time [s]")
plt.grid(axis="y")
plt.title("(b) simple macroscopic queueing model")

plt.subplot(1, 3, 2)
mdl = SimpleMicroscopicQueueModel(10, 5, 15)
t, x, tms = mdl.run(300)
plt.plot(t, x)
plt.ylabel("queue-length [persons]")
plt.xlabel("time [s]")
plt.grid(axis="y")
plt.title("(b3) simple microscopic queueing model")

plt.subplot(1, 3, 3)
plt.violinplot(tms)
plt.ylabel("time [s]")
plt.title("(b3) simple microscopic queueing model\n(total waiting time)")

plt.show()
<Figure size 1400x500 with 3 Axes>

As seen, the model is very easily changed to a microscopic approach even preserving the exact results. The object oriented programming concept of Python is perfectly suited for these approaches. One also immidiately sees a clear benefit from the approach, since we are now able to track individuals through the system and measure their queueing time.

References
  1. Sargent, R. G. (2010). Verification and validation of simulation models. Proceedings of the 2010 Winter Simulation Conference, 166–183. 10.1109/wsc.2010.5679166
  2. Kaliyadan, F., & Kulkarni, V. (2019). Types of variables, descriptive statistics, and sample size. Indian Dermatology Online Journal, 10(1), 82–86.
  3. Joshi, K. D. (1989). Foundations of discrete mathematics. New Age International.
  4. Khalil, H. K., & Grizzle, J. W. (2002). Nonlinear systems (Vol. 3). Prentice hall Upper Saddle River, NJ.
  5. Bicher, M. (2020). Classification of Microscopic Models with Respect to Aggregated System Behaviour. ARGESIM. 10.11128/fbs.29