Skip to content

API Reference

This reference is generated from the source code with mkdocstrings, so it stays in step with the implementation. Every public module, class, function, and method is documented with Google-style docstrings.

Core

import_params

Build Network objects from user-supplied parameters.

ServiceCentre

ServiceCentre(
    number_of_servers,
    queueing_capacity,
    class_change_matrix=None,
    priority_preempt=False,
    ps_threshold=1,
    server_priority_function=None,
    service_discipline=None,
)

Store the class-independent information for a service centre.

Attributes:

Name Type Description
number_of_servers int or Schedule

The number of servers, or a schedule describing how it varies over time.

queueing_capacity int or float

The maximum queue length.

class_change_matrix dict

Matrix describing class changes, if any.

priority_preempt bool

Whether priority classes preempt service.

ps_threshold int

Threshold used by the processor-sharing discipline.

server_priority_function callable

Function assigning server priority.

service_discipline callable

The service discipline used at the node.

class_change_time bool

Whether class change times are modelled.

Initialise a ServiceCentre object.

Parameters:

Name Type Description Default
number_of_servers int or Schedule

The number of servers, or a schedule describing how it varies over time.

required
queueing_capacity int or float

The maximum queue length.

required
class_change_matrix dict

Matrix describing class changes, if any.

None
priority_preempt bool

Whether priority classes preempt service. Defaults to False.

False
ps_threshold int

Threshold used by the processor-sharing discipline. Defaults to 1.

1
server_priority_function callable

Function assigning server priority.

None
service_discipline callable

The service discipline used at the node.

None
Source code in hciw/network.py
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
def __init__(  # noqa: PLR0913, PLR0917 - public configuration API
    self,
    number_of_servers,
    queueing_capacity,
    class_change_matrix=None,
    priority_preempt=False,
    ps_threshold=1,
    server_priority_function=None,
    service_discipline=None,
):
    """Initialise a ``ServiceCentre`` object.

    Args:
        number_of_servers (int or Schedule): The number of servers, or a
            schedule describing how it varies over time.
        queueing_capacity (int or float): The maximum queue length.
        class_change_matrix (dict, optional): Matrix describing class
            changes, if any.
        priority_preempt (bool, optional): Whether priority classes
            preempt service. Defaults to ``False``.
        ps_threshold (int, optional): Threshold used by the
            processor-sharing discipline. Defaults to ``1``.
        server_priority_function (callable, optional): Function assigning
            server priority.
        service_discipline (callable, optional): The service discipline
            used at the node.
    """
    self.number_of_servers = number_of_servers
    self.queueing_capacity = queueing_capacity
    self.class_change_matrix = class_change_matrix
    self.priority_preempt = priority_preempt
    self.ps_threshold = ps_threshold
    self.server_priority_function = server_priority_function
    self.service_discipline = service_discipline
    self.class_change_time = False

CustomerClass

CustomerClass(
    arrival_distributions,
    service_distributions,
    routing,
    priority_class,
    baulking_functions,
    batching_distributions,
    reneging_time_distributions,
    class_change_time_distributions,
    benchmark=None,
)

Store the class-dependent information for a customer class.

Attributes:

Name Type Description
arrival_distributions dict

Arrival distributions for each node.

service_distributions dict

Service distributions for each node.

batching_distributions dict

Batching distributions for each node.

routing Routing

The routing object for the class.

priority_class int

The priority class of the customer class.

baulking_functions dict

Baulking functions for each node.

reneging_time_distributions dict

Reneging time distributions for each node.

class_change_time_distributions dict

Class change time distributions for each pair of customer classes.

benchmark float or None

The target maximum waiting time for this customer class, used by the benchmark-aware service disciplines.

Initialise a CustomerClass object.

Parameters:

Name Type Description Default
arrival_distributions dict

Arrival distributions for each node.

required
service_distributions dict

Service distributions for each node.

required
routing Routing

The routing object for the class.

required
priority_class int

The priority class of the customer class.

required
baulking_functions dict

Baulking functions for each node.

required
batching_distributions dict

Batching distributions for each node.

required
reneging_time_distributions dict

Reneging time distributions for each node.

required
class_change_time_distributions dict

Class change time distributions for each pair of customer classes.

required
benchmark float or None

The target maximum waiting time for this class. Defaults to None.

None
Source code in hciw/network.py
 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
def __init__(  # noqa: PLR0913, PLR0917 - public configuration API
    self,
    arrival_distributions,
    service_distributions,
    routing,
    priority_class,
    baulking_functions,
    batching_distributions,
    reneging_time_distributions,
    class_change_time_distributions,
    benchmark=None,
):
    """Initialise a ``CustomerClass`` object.

    Args:
        arrival_distributions (dict): Arrival distributions for each node.
        service_distributions (dict): Service distributions for each node.
        routing (Routing): The routing object for the class.
        priority_class (int): The priority class of the customer class.
        baulking_functions (dict): Baulking functions for each node.
        batching_distributions (dict): Batching distributions for each node.
        reneging_time_distributions (dict): Reneging time distributions for
            each node.
        class_change_time_distributions (dict): Class change time
            distributions for each pair of customer classes.
        benchmark (float or None, optional): The target maximum waiting
            time for this class. Defaults to ``None``.
    """
    self.arrival_distributions = arrival_distributions
    self.service_distributions = service_distributions
    self.batching_distributions = batching_distributions
    self.routing = routing
    self.priority_class = priority_class
    self.baulking_functions = baulking_functions
    self.reneging_time_distributions = reneging_time_distributions
    self.class_change_time_distributions = class_change_time_distributions
    self.benchmark = benchmark

Network

Network(service_centres, customer_classes)

Store the queueing network.

Holds a list of ServiceCentre objects for each service centre and a dictionary of CustomerClass objects keyed by class name.

Attributes:

Name Type Description
service_centres list

The service centres of the network.

customer_classes dict

The customer classes, keyed by class name.

number_of_nodes int

The number of service centres.

number_of_classes int

The number of customer classes.

customer_class_names list

The sorted names of the customer classes.

number_of_priority_classes int

The number of distinct priority classes.

priority_class_mapping dict

Maps class names to priority classes.

system_capacity float

The total capacity of the system.

Initialise a Network object.

Parameters:

Name Type Description Default
service_centres list

The service centres of the network.

required
customer_classes dict

The customer classes, keyed by class name.

required
Source code in hciw/network.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def __init__(self, service_centres, customer_classes):
    """Initialise a ``Network`` object.

    Args:
        service_centres (list): The service centres of the network.
        customer_classes (dict): The customer classes, keyed by class name.
    """
    self.service_centres = service_centres
    self.customer_classes = customer_classes
    self.number_of_nodes = len(service_centres)
    self.number_of_classes = len(customer_classes)
    self.customer_class_names = sorted(customer_classes.keys())
    self.number_of_priority_classes = len(
        {clss.priority_class for clss in customer_classes.values()}
    )
    self.priority_class_mapping = {
        clss: customer_classes[clss].priority_class for clss in customer_classes
    }
    for nd_id, node in enumerate(self.service_centres):
        if all(
            clss.reneging_time_distributions[nd_id] is None
            for clss in self.customer_classes.values()
        ):
            node.reneging = False
        else:
            node.reneging = True
    if any(
        dist is not None
        for clss in customer_classes.values()
        for dist in clss.class_change_time_distributions.values()
    ):
        for node in self.service_centres:
            node.class_change_time = True

NetworkRouting

NetworkRouting(routers)

Hold a number of routing objects, one for each node.

Set up the router objects for each node.

Parameters:

Name Type Description Default
routers list

The routing objects, one for each node.

required
Source code in hciw/routing/routing.py
11
12
13
14
15
16
17
def __init__(self, routers):
    """Set up the router objects for each node.

    Args:
        routers (list): The routing objects, one for each node.
    """
    self.routers = routers

initialise

initialise(simulation)

Give the simulation and node attributes to the routing objects.

Parameters:

Name Type Description Default
simulation Simulation

The simulation the routers belong to.

required
Source code in hciw/routing/routing.py
19
20
21
22
23
24
25
26
27
28
29
30
def initialise(self, simulation):
    """Give the simulation and node attributes to the routing objects.

    Args:
        simulation (hciw.Simulation): The simulation the routers belong
            to.
    """
    self.simulation = simulation
    for router, node in zip(
        self.routers, self.simulation.transitive_nodes, strict=False
    ):
        router.initialise(self.simulation, node)

initialise_individual

initialise_individual(ind)

Run at the arrival node when the individual is spawned.

Parameters:

Name Type Description Default
ind Individual

The individual being spawned.

required
Source code in hciw/routing/routing.py
32
33
34
35
36
37
def initialise_individual(self, ind):
    """Run at the arrival node when the individual is spawned.

    Args:
        ind (hciw.Individual): The individual being spawned.
    """

next_node

next_node(ind, node_id)

Choose the next node.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required
node_id int

The id number of the node the individual is at.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
39
40
41
42
43
44
45
46
47
48
49
def next_node(self, ind, node_id):
    """Choose the next node.

    Args:
        ind (hciw.Individual): The individual being routed.
        node_id (int): The id number of the node the individual is at.

    Returns:
        hciw.Node: The next node.
    """
    return self.routers[node_id - 1].next_node(ind)

next_node_for_rerouting

next_node_for_rerouting(ind, node_id)

Choose the next node when rerouting a preempted customer.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required
node_id int

The id number of the node the individual is at.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
51
52
53
54
55
56
57
58
59
60
61
def next_node_for_rerouting(self, ind, node_id):
    """Choose the next node when rerouting a preempted customer.

    Args:
        ind (hciw.Individual): The individual being routed.
        node_id (int): The id number of the node the individual is at.

    Returns:
        hciw.Node: The next node.
    """
    return self.routers[node_id - 1].next_node_for_rerouting(ind)

next_node_for_jockeying

next_node_for_jockeying(ind, node_id)

Choose the next node when jockeying to another node after reneging.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required
node_id int

The id number of the node the individual is at.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
63
64
65
66
67
68
69
70
71
72
73
def next_node_for_jockeying(self, ind, node_id):
    """Choose the next node when jockeying to another node after reneging.

    Args:
        ind (hciw.Individual): The individual being routed.
        node_id (int): The id number of the node the individual is at.

    Returns:
        hciw.Node: The next node.
    """
    return self.routers[node_id - 1].next_node_for_jockeying(ind)

TransitionMatrix

TransitionMatrix(transition_matrix)

Bases: NetworkRouting

Hold a number of probabilistic routing objects.

Set up the relevant probabilistic router objects for each node.

Parameters:

Name Type Description Default
transition_matrix list

A square matrix where the entry in row i and column j is the probability of moving from node i + 1 to node j + 1.

required
Source code in hciw/routing/routing.py
79
80
81
82
83
84
85
86
87
88
89
90
91
def __init__(self, transition_matrix):
    """Set up the relevant probabilistic router objects for each node.

    Args:
        transition_matrix (list): A square matrix where the entry in row
            ``i`` and column ``j`` is the probability of moving from node
            ``i + 1`` to node ``j + 1``.
    """
    n_destinations = len(transition_matrix)
    self.routers = [
        Probabilistic(destinations=list(range(1, n_destinations + 1)), probs=row)
        for row in transition_matrix
    ]

initialise

initialise(simulation)

Give the simulation to the routing objects.

Parameters:

Name Type Description Default
simulation Simulation

The simulation the routers belong to.

required

Raises:

Type Description
ValueError

If the number of rows in the transition matrix does not equal the number of nodes in the network.

Source code in hciw/routing/routing.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def initialise(self, simulation):
    """Give the simulation to the routing objects.

    Args:
        simulation (hciw.Simulation): The simulation the routers belong
            to.

    Raises:
        ValueError: If the number of rows in the transition matrix does
            not equal the number of nodes in the network.
    """
    super().initialise(simulation)
    if len(self.routers) != simulation.network.number_of_nodes:
        raise ValueError(
            "Ensure a transition matrix is given, and that the number of rows "
            "is equal to the number of nodes in the network."
        )

ProcessBased

ProcessBased(route_function)

Bases: NetworkRouting

Route an individual based on a pre-defined process.

Initialise the routing object.

Parameters:

Name Type Description Default
route_function callable

A function that returns a pre-defined route for an individual.

required
Source code in hciw/routing/routing.py
115
116
117
118
119
120
121
122
def __init__(self, route_function):
    """Initialise the routing object.

    Args:
        route_function (callable): A function that returns a pre-defined
            route for an individual.
    """
    self.route_function = route_function

initialise

initialise(simulation)

Give the simulation and node attributes to the routing objects.

Parameters:

Name Type Description Default
simulation Simulation

The simulation the routers belong to.

required
Source code in hciw/routing/routing.py
124
125
126
127
128
129
130
131
def initialise(self, simulation):
    """Give the simulation and node attributes to the routing objects.

    Args:
        simulation (hciw.Simulation): The simulation the routers belong
            to.
    """
    self.simulation = simulation

initialise_individual

initialise_individual(ind)

Set the route of the individual when they are spawned.

Parameters:

Name Type Description Default
ind Individual

The individual being spawned.

required
Source code in hciw/routing/routing.py
133
134
135
136
137
138
139
def initialise_individual(self, ind):
    """Set the route of the individual when they are spawned.

    Args:
        ind (hciw.Individual): The individual being spawned.
    """
    ind.route = self.route_function(ind, self.simulation)

next_node

next_node(ind, node_id)

Choose the next node from the process-based pre-defined route.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required
node_id int

The id number of the node the individual is at.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
141
142
143
144
145
146
147
148
149
150
151
152
def next_node(self, ind, node_id):
    """Choose the next node from the process-based pre-defined route.

    Args:
        ind (hciw.Individual): The individual being routed.
        node_id (int): The id number of the node the individual is at.

    Returns:
        hciw.Node: The next node.
    """
    node_index = -1 if len(ind.route) == 0 else ind.route.pop(0)
    return self.simulation.nodes[node_index]

next_node_for_rerouting

next_node_for_rerouting(ind, node_id)

Choose the next node when rerouting a preempted customer.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required
node_id int

The id number of the node the individual is at.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
154
155
156
157
158
159
160
161
162
163
164
def next_node_for_rerouting(self, ind, node_id):
    """Choose the next node when rerouting a preempted customer.

    Args:
        ind (hciw.Individual): The individual being routed.
        node_id (int): The id number of the node the individual is at.

    Returns:
        hciw.Node: The next node.
    """
    return self.next_node(ind, node_id)

next_node_for_jockeying

next_node_for_jockeying(ind, node_id)

Choose the next node when jockeying to another node after reneging.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required
node_id int

The id number of the node the individual is at.

required

Returns:

Type Description
Node

The exit node.

Source code in hciw/routing/routing.py
166
167
168
169
170
171
172
173
174
175
176
def next_node_for_jockeying(self, ind, node_id):
    """Choose the next node when jockeying to another node after reneging.

    Args:
        ind (hciw.Individual): The individual being routed.
        node_id (int): The id number of the node the individual is at.

    Returns:
        hciw.Node: The exit node.
    """
    return self.simulation.nodes[-1]

FlexibleProcessBased

FlexibleProcessBased(route_function, rule, choice)

Bases: ProcessBased

Route an individual based on a pre-defined process.

The route is flexible: each step may be a subset of nodes from which the next node is chosen according to a rule and a choice method.

Initialise the routing object.

Parameters:

Name Type Description Default
route_function callable

A function that returns a pre-defined route for an individual.

required
rule str

Either 'any' or 'all'.

required
choice str

Either 'random', 'jsq' or 'lb'.

required

Raises:

Type Description
ValueError

If rule is not 'any' or 'all'.

ValueError

If choice is not 'random', 'jsq' or 'lb'.

Source code in hciw/routing/routing.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def __init__(self, route_function, rule, choice):
    """Initialise the routing object.

    Args:
        route_function (callable): A function that returns a pre-defined
            route for an individual.
        rule (str): Either ``'any'`` or ``'all'``.
        choice (str): Either ``'random'``, ``'jsq'`` or ``'lb'``.

    Raises:
        ValueError: If ``rule`` is not ``'any'`` or ``'all'``.
        ValueError: If ``choice`` is not ``'random'``, ``'jsq'`` or
            ``'lb'``.
    """
    self.route_function = route_function
    if rule not in ["any", "all"]:
        raise ValueError("Flexible routing rules must be one of 'any' or 'all'.")
    if choice not in ["random", "jsq", "lb"]:
        raise ValueError(
            "Flexible routing choices must be one of 'random', 'jsq', or 'lb'."
        )
    self.rule = rule
    self.choice = choice

find_next_node_from_subset

find_next_node_from_subset(subset, ind)

Find the next node within a subset according to choice.

Parameters:

Name Type Description Default
subset list

The subset of node indices to choose from.

required
ind Individual

The individual being routed.

required

Returns:

Type Description
int

The id number of the chosen node.

Source code in hciw/routing/routing.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
def find_next_node_from_subset(self, subset, ind):
    """Find the next node within a subset according to ``choice``.

    Args:
        subset (list): The subset of node indices to choose from.
        ind (hciw.Individual): The individual being routed.

    Returns:
        int: The id number of the chosen node.
    """
    if self.choice == "random":
        return hciw.random_choice(subset)
    if self.choice == "jsq":
        temp_router = JoinShortestQueue(destinations=subset)
        temp_router.initialise(self.simulation, None)
        nd = temp_router.next_node(ind)
        assert nd is not None
        return nd.id_number
    if self.choice == "lb":
        temp_router = LoadBalancing(destinations=subset)
        temp_router.initialise(self.simulation, None)
        nd = temp_router.next_node(ind)
        assert nd is not None
        return nd.id_number
    return None

update_individual_route

update_individual_route(ind, next_node_id)

Remove chosen nodes from the individual's route according to rule.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required
next_node_id int

The id number of the chosen next node.

required
Source code in hciw/routing/routing.py
236
237
238
239
240
241
242
243
244
245
246
247
248
def update_individual_route(self, ind, next_node_id):
    """Remove chosen nodes from the individual's route according to ``rule``.

    Args:
        ind (hciw.Individual): The individual being routed.
        next_node_id (int): The id number of the chosen next node.
    """
    if self.rule == "any":
        ind.route = ind.route[1:]
    if self.rule == "all":
        ind.route[0].remove(next_node_id)
        if len(ind.route[0]) == 0:
            ind.route = ind.route[1:]

next_node

next_node(ind, node_id)

Choose the next node from the process-based pre-defined route.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required
node_id int

The id number of the node the individual is at.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
def next_node(self, ind, node_id):
    """Choose the next node from the process-based pre-defined route.

    Args:
        ind (hciw.Individual): The individual being routed.
        node_id (int): The id number of the node the individual is at.

    Returns:
        hciw.Node: The next node.
    """
    if len(ind.route) == 0:
        node_index = -1
    else:
        node_index = self.find_next_node_from_subset(ind.route[0], ind)
        self.update_individual_route(ind, node_index)
    return self.simulation.nodes[node_index]

NodeRouting

A generic routing class to determine next sampled node.

initialise

initialise(simulation, node)

Give the simulation and node attributes to the routing object.

Parameters:

Name Type Description Default
simulation Simulation

The simulation the router belongs to.

required
node Node

The node the router belongs to.

required
Source code in hciw/routing/routing.py
271
272
273
274
275
276
277
278
279
280
281
def initialise(self, simulation, node):
    """Give the simulation and node attributes to the routing object.

    Args:
        simulation (hciw.Simulation): The simulation the router belongs
            to.
        node (hciw.Node): The node the router belongs to.
    """
    self.simulation = simulation
    self.node = node
    self.error_check_at_initialise()

error_check_at_initialise

error_check_at_initialise()

Check the routing object is valid when it is initialised.

Source code in hciw/routing/routing.py
283
284
def error_check_at_initialise(self):
    """Check the routing object is valid when it is initialised."""

next_node

next_node(ind)

Choose the next node for an individual.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
286
287
288
289
290
291
292
293
294
295
def next_node(self, ind):
    """Choose the next node for an individual.

    Args:
        ind (hciw.Individual): The individual being routed.

    Returns:
        hciw.Node: The next node.
    """
    raise NotImplementedError

next_node_for_rerouting

next_node_for_rerouting(ind)

Choose the next node when rerouting a preempted customer.

By default, the next node for rerouting uses the same method as :meth:next_node.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
297
298
299
300
301
302
303
304
305
306
307
308
309
def next_node_for_rerouting(self, ind):
    """Choose the next node when rerouting a preempted customer.

    By default, the next node for rerouting uses the same method as
    :meth:`next_node`.

    Args:
        ind (hciw.Individual): The individual being routed.

    Returns:
        hciw.Node: The next node.
    """
    return self.next_node(ind)

next_node_for_jockeying

next_node_for_jockeying(ind)

Choose the next node when jockeying to another node after reneging.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The exit node.

Source code in hciw/routing/routing.py
311
312
313
314
315
316
317
318
319
320
def next_node_for_jockeying(self, ind):
    """Choose the next node when jockeying to another node after reneging.

    Args:
        ind (hciw.Individual): The individual being routed.

    Returns:
        hciw.Node: The exit node.
    """
    return self.simulation.nodes[-1]

Probabilistic

Probabilistic(destinations, probs)

Bases: NodeRouting

A router that probabilistically chooses the next node.

Initialise the routing object.

Parameters:

Name Type Description Default
destinations list

A list of node indices.

required
probs list

A list of probabilities associated with each destination.

required

Raises:

Type Description
ValueError

If any probability is not a float.

ValueError

If any probability is less than 0 or greater than 1.

ValueError

If the probabilities sum to more than 1.

Source code in hciw/routing/routing.py
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
def __init__(self, destinations, probs):
    """Initialise the routing object.

    Args:
        destinations (list): A list of node indices.
        probs (list): A list of probabilities associated with each
            destination.

    Raises:
        ValueError: If any probability is not a float.
        ValueError: If any probability is less than 0 or greater than 1.
        ValueError: If the probabilities sum to more than 1.
    """
    for p in probs:
        if not isinstance(p, float):
            raise ValueError(  # noqa: TRY004 - public API raises ValueError
                "Routing probabilities must be a float."
            )
        if p < 0 or p > 1:
            raise ValueError("Routing probabilities must be between 0 and 1.")
    if sum(probs) > 1.0:
        raise ValueError("Routing probabilities must sum to 1 or less.")
    self.destinations = [*destinations, -1]
    self.probs = [*probs, 1 - sum(probs)]

error_check_at_initialise

error_check_at_initialise()

Check the probabilities and destinations are valid.

Raises:

Type Description
ValueError

If the probabilities and destinations differ in length.

ValueError

If the destinations are not a subset of the nodes in the network.

Source code in hciw/routing/routing.py
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
def error_check_at_initialise(self):
    """Check the probabilities and destinations are valid.

    Raises:
        ValueError: If the probabilities and destinations differ in
            length.
        ValueError: If the destinations are not a subset of the nodes in
            the network.
    """
    if len(self.probs) != len(self.destinations):
        raise ValueError(
            "Routing probabilities should correspond to destinations, and so "
            "should be lists of the same length."
        )
    if not set(self.destinations).issubset(
        {nd.id_number for nd in self.simulation.nodes[1:]}
    ):
        raise ValueError(
            "Routing destinations should be a subset of the nodes in the network."
        )

next_node

next_node(ind)

Probabilistically choose the next node from the destinations.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
372
373
374
375
376
377
378
379
380
381
382
def next_node(self, ind):
    """Probabilistically choose the next node from the destinations.

    Args:
        ind (hciw.Individual): The individual being routed.

    Returns:
        hciw.Node: The next node.
    """
    node_index = hciw.random_choice(self.destinations, self.probs)
    return self.simulation.nodes[node_index]

Direct

Direct(to)

Bases: NodeRouting

A router that sends the individual directly to another node.

Initialise the routing object.

Parameters:

Name Type Description Default
to int

The node index to send to.

required
Source code in hciw/routing/routing.py
388
389
390
391
392
393
394
def __init__(self, to):
    """Initialise the routing object.

    Args:
        to (int): The node index to send to.
    """
    self.to = to

next_node

next_node(ind)

Choose the node to with probability 1.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The destination node.

Source code in hciw/routing/routing.py
396
397
398
399
400
401
402
403
404
405
def next_node(self, ind):
    """Choose the node ``to`` with probability 1.

    Args:
        ind (hciw.Individual): The individual being routed.

    Returns:
        hciw.Node: The destination node.
    """
    return self.simulation.nodes[self.to]

Leave

Bases: NodeRouting

A router that sends the individual directly to the exit node.

next_node

next_node(ind)

Choose the exit node with probability 1.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The exit node.

Source code in hciw/routing/routing.py
411
412
413
414
415
416
417
418
419
420
def next_node(self, ind):
    """Choose the exit node with probability 1.

    Args:
        ind (hciw.Individual): The individual being routed.

    Returns:
        hciw.Node: The exit node.
    """
    return self.simulation.nodes[-1]

JoinShortestQueue

JoinShortestQueue(destinations, tie_break='random')

Bases: NodeRouting

A router that sends the individual to the node with the shortest queue.

The node is chosen from a list of destinations.

Initialise the routing object.

Parameters:

Name Type Description Default
destinations list

A list of node indices.

required
tie_break str

The method to deal with ties. One of 'random' (randomly choose between ties) or 'order' (prioritise nodes in the order given by destinations). Defaults to 'random'.

'random'
Source code in hciw/routing/routing.py
429
430
431
432
433
434
435
436
437
438
439
440
def __init__(self, destinations, tie_break="random"):
    """Initialise the routing object.

    Args:
        destinations (list): A list of node indices.
        tie_break (str, optional): The method to deal with ties. One of
            ``'random'`` (randomly choose between ties) or ``'order'``
            (prioritise nodes in the order given by ``destinations``).
            Defaults to ``'random'``.
    """
    self.destinations = destinations
    self.tie_break = tie_break

error_check_at_initialise

error_check_at_initialise()

Check the destinations are a subset of the network's nodes.

Raises:

Type Description
ValueError

If the destinations are not a subset of the nodes in the network.

Source code in hciw/routing/routing.py
442
443
444
445
446
447
448
449
450
451
452
453
454
def error_check_at_initialise(self):
    """Check the destinations are a subset of the network's nodes.

    Raises:
        ValueError: If the destinations are not a subset of the nodes in
            the network.
    """
    if not set(self.destinations).issubset(
        {nd.id_number for nd in self.simulation.nodes[1:]}
    ):
        raise ValueError(
            "Routing destinations should be a subset of the nodes in the network."
        )

get_queue_size

get_queue_size(node_index)

Get the size of the queue at node_index.

Parameters:

Name Type Description Default
node_index int

The index of the node.

required

Returns:

Type Description
int

The number of waiting individuals.

Source code in hciw/routing/routing.py
456
457
458
459
460
461
462
463
464
465
466
467
468
def get_queue_size(self, node_index):
    """Get the size of the queue at ``node_index``.

    Args:
        node_index (int): The index of the node.

    Returns:
        int: The number of waiting individuals.
    """
    return (
        self.simulation.nodes[node_index].number_of_individuals
        - self.simulation.nodes[node_index].number_in_service
    )

next_node

next_node(ind)

Choose the destination node with the shortest queue.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
def next_node(self, ind):
    """Choose the destination node with the shortest queue.

    Args:
        ind (hciw.Individual): The individual being routed.

    Returns:
        hciw.Node: The next node.
    """
    shortest_queues = []
    shortest_queue_size = float("inf")
    for node_index in self.destinations:
        queue_size = self.get_queue_size(node_index)
        if queue_size == shortest_queue_size:
            shortest_queues.append(node_index)
        if queue_size < shortest_queue_size:
            shortest_queues = [node_index]
            shortest_queue_size = queue_size
    if self.tie_break == "random":
        next_node_index = hciw.random_choice(shortest_queues)
        return self.simulation.nodes[next_node_index]
    if self.tie_break == "order":
        next_node_index = shortest_queues[0]
        return self.simulation.nodes[next_node_index]
    return None

LoadBalancing

LoadBalancing(destinations, tie_break='random')

Bases: JoinShortestQueue

A version of JoinShortestQueue that also counts customers in service.

Source code in hciw/routing/routing.py
429
430
431
432
433
434
435
436
437
438
439
440
def __init__(self, destinations, tie_break="random"):
    """Initialise the routing object.

    Args:
        destinations (list): A list of node indices.
        tie_break (str, optional): The method to deal with ties. One of
            ``'random'`` (randomly choose between ties) or ``'order'``
            (prioritise nodes in the order given by ``destinations``).
            Defaults to ``'random'``.
    """
    self.destinations = destinations
    self.tie_break = tie_break

get_queue_size

get_queue_size(node_index)

Get the size of the queue at node_index.

Parameters:

Name Type Description Default
node_index int

The index of the node.

required

Returns:

Type Description
int

The number of individuals at the node.

Source code in hciw/routing/routing.py
500
501
502
503
504
505
506
507
508
509
def get_queue_size(self, node_index):
    """Get the size of the queue at ``node_index``.

    Args:
        node_index (int): The index of the node.

    Returns:
        int: The number of individuals at the node.
    """
    return self.simulation.nodes[node_index].number_of_individuals

Cycle

Cycle(cycle)

Bases: NodeRouting

A router that cycles through destinations, repeating the cycle once ended.

Initialise the routing object.

Parameters:

Name Type Description Default
cycle list

An ordered sequence of node indices.

required
Source code in hciw/routing/routing.py
515
516
517
518
519
520
521
522
def __init__(self, cycle):
    """Initialise the routing object.

    Args:
        cycle (list): An ordered sequence of node indices.
    """
    self.cycle = cycle
    self.generator = itertools.cycle(self.cycle)

error_check_at_initialise

error_check_at_initialise()

Check the cycle is a subset of the network's nodes.

Raises:

Type Description
ValueError

If the cycle is not a subset of the nodes in the network.

Source code in hciw/routing/routing.py
524
525
526
527
528
529
530
531
532
533
534
535
536
def error_check_at_initialise(self):
    """Check the cycle is a subset of the network's nodes.

    Raises:
        ValueError: If the cycle is not a subset of the nodes in the
            network.
    """
    if not set(self.cycle).issubset(
        {nd.id_number for nd in self.simulation.nodes[1:]}
    ):
        raise ValueError(
            "Routing destinations should be a subset of the nodes in the network."
        )

next_node

next_node(ind)

Choose the next node in the cycle.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
538
539
540
541
542
543
544
545
546
547
548
def next_node(self, ind):
    """Choose the next node in the cycle.

    Args:
        ind (hciw.Individual): The individual being routed.

    Returns:
        hciw.Node: The next node.
    """
    next_node_index = next(self.generator)
    return self.simulation.nodes[next_node_index]

Schedule

Schedule(
    numbers_of_servers: list[int],
    shift_end_dates: list[float],
    preemption: bool | str = False,
    offset: float = 0.0,
)

Store server schedule information and generate the next shifts.

Attributes:

Name Type Description
schedule_type str

The type of the schedule.

shift_end_dates List[float]

The end dates of each shift.

numbers_of_servers List[int]

The number of servers for each shift.

preemption Union[bool, str]

The pre-emption option.

cyclelength float

The length of the schedule cycle.

offset float

The offset applied to the schedule.

Initialise a Schedule instance.

Parameters:

Name Type Description Default
numbers_of_servers List[int]

The number of servers working at each shift.

required
shift_end_dates List[float]

The end dates of each shift.

required
preemption Union[bool, str]

The pre-emption option, one of 'resume', 'restart', 'resample', 'reroute' or False. Defaults to False.

False
offset float

A non-negative offset applied to the schedule. Defaults to 0.0.

0.0

Raises:

Type Description
ValueError

If preemption or offset is invalid.

Source code in hciw/schedules.py
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
def __init__(
    self,
    numbers_of_servers: list[int],
    shift_end_dates: list[float],
    preemption: bool | str = False,
    offset: float = 0.0,
) -> None:
    """Initialise a ``Schedule`` instance.

    Args:
        numbers_of_servers (List[int]): The number of servers working at
            each shift.
        shift_end_dates (List[float]): The end dates of each shift.
        preemption (Union[bool, str], optional): The pre-emption option,
            one of ``'resume'``, ``'restart'``, ``'resample'``,
            ``'reroute'`` or ``False``. Defaults to ``False``.
        offset (float, optional): A non-negative offset applied to the
            schedule. Defaults to ``0.0``.

    Raises:
        ValueError: If ``preemption`` or ``offset`` is invalid.
    """
    if preemption not in [False, "resume", "restart", "resample", "reroute"]:
        raise ValueError(
            "Preemption must be False, 'resume', 'restart', "
            "'resample', 'reroute', or False."
        )
    if not isinstance(offset, float):
        raise ValueError(  # noqa: TRY004 - public API raises ValueError
            "Offset should be a positive float."
        )
    if offset < 0.0:
        raise ValueError("Offset should be a positive float.")
    self.schedule_type = "schedule"
    self.shift_end_dates = shift_end_dates
    self.numbers_of_servers = numbers_of_servers
    self.preemption = preemption
    self.cyclelength = self.shift_end_dates[-1]
    self.offset = offset

initialise

initialise() -> None

Initialise the generator at the beginning of a simulation.

Source code in hciw/schedules.py
58
59
60
61
62
63
64
65
def initialise(self) -> None:
    """Initialise the generator at the beginning of a simulation."""
    self.c = 0
    self.next_shift_change_date = self.offset
    self.next_c = self.numbers_of_servers[0]
    self.schedule_generator = self.get_schedule_generator(
        self.shift_end_dates, self.numbers_of_servers, self.offset
    )

get_schedule_generator

get_schedule_generator(
    boundaries: list[float],
    values: list[int],
    offset: float,
) -> Generator[tuple[float, int], None, None]

Yield the next shift date and server count.

Parameters:

Name Type Description Default
boundaries List[float]

The shift boundaries.

required
values List[int]

The number of servers corresponding to each boundary.

required
offset float

The offset applied to the schedule.

required

Yields:

Type Description
Tuple[float, int]

The next shift date and the number of servers.

Source code in hciw/schedules.py
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
def get_schedule_generator(
    self, boundaries: list[float], values: list[int], offset: float
) -> Generator[tuple[float, int], None, None]:
    """Yield the next shift date and server count.

    Args:
        boundaries (List[float]): The shift boundaries.
        values (List[int]): The number of servers corresponding to each
            boundary.
        offset (float): The offset applied to the schedule.

    Yields:
        Tuple[float, int]: The next shift date and the number of servers.
    """
    num_boundaries = len(boundaries)
    index = 0
    date = 0
    while True:
        date = (
            offset
            + boundaries[index % num_boundaries]
            + ((index) // num_boundaries * self.cyclelength)
        )
        index += 1
        yield date, values[index % num_boundaries]

get_next_shift

get_next_shift() -> None

Update the next shift from the generator.

Source code in hciw/schedules.py
93
94
95
96
97
98
def get_next_shift(self) -> None:
    """Update the next shift from the generator."""
    self.c = self.next_c
    date, c = next(self.schedule_generator)
    self.next_shift_change_date = date
    self.next_c = c

Slotted

Slotted(
    slots,
    slot_sizes,
    capacitated=False,
    preemption=False,
    offset=0.0,
)

Bases: Schedule

A schedule divided into slots with a number of servers per slot.

Attributes:

Name Type Description
schedule_type str

The type of the schedule.

offset float

The offset applied to the schedule.

slots list

The slot boundaries.

slot_sizes list

The number of servers in each slot.

next_slot_sizes list

The slot sizes of the next cycle.

capacitated bool

Whether the slots are capacitated.

preemption Union[bool, str]

The pre-emption option.

cyclelength float

The length of the schedule cycle.

c int

The current number of servers.

Initialise a Slotted schedule instance.

Parameters:

Name Type Description Default
slots list

The slot boundaries.

required
slot_sizes list

The number of servers in each slot.

required
capacitated bool

Whether the slots are capacitated. Defaults to False.

False
preemption Union[bool, str]

The pre-emption option, one of 'resume', 'restart', 'resample' or False. Defaults to False.

False
offset float

A non-negative offset applied to the schedule. Defaults to 0.0.

0.0

Raises:

Type Description
ValueError

If preemption or offset is invalid.

Source code in hciw/schedules.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def __init__(
    self, slots, slot_sizes, capacitated=False, preemption=False, offset=0.0
):
    """Initialise a ``Slotted`` schedule instance.

    Args:
        slots (list): The slot boundaries.
        slot_sizes (list): The number of servers in each slot.
        capacitated (bool, optional): Whether the slots are capacitated.
            Defaults to ``False``.
        preemption (Union[bool, str], optional): The pre-emption option,
            one of ``'resume'``, ``'restart'``, ``'resample'`` or
            ``False``. Defaults to ``False``.
        offset (float, optional): A non-negative offset applied to the
            schedule. Defaults to ``0.0``.

    Raises:
        ValueError: If ``preemption`` or ``offset`` is invalid.
    """
    if not capacitated and preemption is not False:
        raise ValueError(
            "Pre-emption options not availale for non-capacitated slots."
        )
    if preemption not in [False, "resume", "restart", "resample"]:
        raise ValueError(
            "Pre-emption options should be either 'resume', 'restart', "
            "'resample', or False."
        )
    if not isinstance(offset, float):
        raise ValueError(  # noqa: TRY004 - public API raises ValueError
            "Offset should be a positive float."
        )
    if offset < 0.0:
        raise ValueError("Offset should be a positive float.")
    self.schedule_type = "slotted"
    self.offset = offset
    self.slots = slots
    self.slot_sizes = slot_sizes
    self.next_slot_sizes = [self.slot_sizes[-1], *self.slot_sizes[:-1]]
    self.capacitated = capacitated
    self.preemption = preemption
    self.cyclelength = self.slots[-1]
    self.c = 0

initialise

initialise()

Initialise the generator at the beginning of a simulation.

Source code in hciw/schedules.py
160
161
162
163
164
165
def initialise(self):
    """Initialise the generator at the beginning of a simulation."""
    self.schedule_generator = self.get_schedule_generator(
        self.slots, self.next_slot_sizes, self.offset
    )
    self.get_next_slot()

get_next_slot

get_next_slot()

Update the next slot date and size from the generator.

Source code in hciw/schedules.py
167
168
169
170
171
def get_next_slot(self):
    """Update the next slot date and size from the generator."""
    date, size = next(self.schedule_generator)
    self.next_slot_date = date
    self.slot_size = size

create_network

create_network(
    arrival_distributions=None,
    baulking_functions=None,
    class_change_matrices=None,
    class_change_time_distributions=None,
    number_of_servers=None,
    priority_classes=None,
    queue_capacities=None,
    service_distributions=None,
    routing=None,
    batching_distributions=None,
    ps_thresholds=None,
    server_priority_functions=None,
    reneging_time_distributions=None,
    service_disciplines=None,
    customer_class_benchmarks=None,
    system_capacity=float("inf"),
)

Create a Network object from keyword arguments.

Parameters:

Name Type Description Default
arrival_distributions dict

Arrival distributions for each customer class and node. Required.

None
baulking_functions dict

Baulking functions for each customer class and node.

None
class_change_matrices list

Class change matrices, one per node.

None
class_change_time_distributions dict

Class change time distributions for each pair of customer classes.

None
number_of_servers list

The number of servers at each node, or schedules. Required.

None
priority_classes dict or tuple

The priority class of each customer class.

None
queue_capacities list

The queue capacity of each node.

None
service_distributions dict

Service distributions for each customer class and node. Required.

None
routing list, dict, or Routing

The routing for each customer class.

None
batching_distributions dict

Batching distributions for each customer class and node.

None
ps_thresholds list

Processor-sharing thresholds, one per node.

None
server_priority_functions list

Server priority functions, one per node.

None
reneging_time_distributions dict

Reneging time distributions for each customer class and node.

None
service_disciplines list

Service disciplines, one per node.

None
customer_class_benchmarks dict

The target maximum waiting time for each customer class, keyed by class name. Read by the benchmark-aware service disciplines such as hciw.disciplines.wait_time_over_benchmark.

None
system_capacity int or float

The maximum number of customers allowed in the system. Defaults to infinity.

float('inf')

Returns:

Type Description
Network

The constructed network object.

Raises:

Type Description
ValueError

If arrival_distributions, service_distributions or number_of_servers are not provided.

Source code in hciw/import_params.py
 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
def create_network(  # noqa: C901, PLR0912, PLR0913, PLR0917 - public config API
    arrival_distributions=None,
    baulking_functions=None,
    class_change_matrices=None,
    class_change_time_distributions=None,
    number_of_servers=None,
    priority_classes=None,
    queue_capacities=None,
    service_distributions=None,
    routing=None,
    batching_distributions=None,
    ps_thresholds=None,
    server_priority_functions=None,
    reneging_time_distributions=None,
    service_disciplines=None,
    customer_class_benchmarks=None,
    system_capacity=float("inf"),
):
    """Create a ``Network`` object from keyword arguments.

    Args:
        arrival_distributions (dict, optional): Arrival distributions for each
            customer class and node. Required.
        baulking_functions (dict, optional): Baulking functions for each
            customer class and node.
        class_change_matrices (list, optional): Class change matrices, one per
            node.
        class_change_time_distributions (dict, optional): Class change time
            distributions for each pair of customer classes.
        number_of_servers (list, optional): The number of servers at each node,
            or schedules. Required.
        priority_classes (dict or tuple, optional): The priority class of each
            customer class.
        queue_capacities (list, optional): The queue capacity of each node.
        service_distributions (dict, optional): Service distributions for each
            customer class and node. Required.
        routing (list, dict, or Routing, optional): The routing for each
            customer class.
        batching_distributions (dict, optional): Batching distributions for
            each customer class and node.
        ps_thresholds (list, optional): Processor-sharing thresholds, one per
            node.
        server_priority_functions (list, optional): Server priority functions,
            one per node.
        reneging_time_distributions (dict, optional): Reneging time
            distributions for each customer class and node.
        service_disciplines (list, optional): Service disciplines, one per
            node.
        customer_class_benchmarks (dict, optional): The target maximum
            waiting time for each customer class, keyed by class name. Read
            by the benchmark-aware service disciplines such as
            ``hciw.disciplines.wait_time_over_benchmark``.
        system_capacity (int or float, optional): The maximum number of
            customers allowed in the system. Defaults to infinity.

    Returns:
        Network: The constructed network object.

    Raises:
        ValueError: If ``arrival_distributions``, ``service_distributions`` or
            ``number_of_servers`` are not provided.
    """
    if (
        arrival_distributions is None
        or number_of_servers is None
        or service_distributions is None
    ):
        raise ValueError(
            "arrival_distributions, service_distributions and number_of_servers are "
            "required arguments."
        )

    params = {
        "arrival_distributions": arrival_distributions,
        "number_of_servers": number_of_servers,
        "service_distributions": service_distributions,
        "system_capacity": system_capacity,
    }

    if baulking_functions is not None:
        params["baulking_functions"] = baulking_functions
    if class_change_matrices is not None:
        params["class_change_matrices"] = class_change_matrices
    if class_change_time_distributions is not None:
        params["class_change_time_distributions"] = class_change_time_distributions
    if priority_classes is not None:
        params["priority_classes"] = priority_classes
    if queue_capacities is not None:
        params["queue_capacities"] = queue_capacities
    if routing is not None:
        params["routing"] = routing
    if batching_distributions is not None:
        params["batching_distributions"] = batching_distributions
    if ps_thresholds is not None:
        params["ps_thresholds"] = ps_thresholds
    if server_priority_functions is not None:
        params["server_priority_functions"] = server_priority_functions
    if reneging_time_distributions is not None:
        params["reneging_time_distributions"] = reneging_time_distributions
    if service_disciplines is not None:
        params["service_disciplines"] = service_disciplines
    if customer_class_benchmarks is not None:
        params["customer_class_benchmarks"] = customer_class_benchmarks

    return create_network_from_dictionary(params)

create_network_from_dictionary

create_network_from_dictionary(params_input)

Create a Network object from a parameters dictionary.

Parameters:

Name Type Description Default
params_input dict

The dictionary of network parameters.

required

Returns:

Type Description
Network

The constructed network object.

Source code in hciw/import_params.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
def create_network_from_dictionary(params_input):
    """Create a ``Network`` object from a parameters dictionary.

    Args:
        params_input (dict): The dictionary of network parameters.

    Returns:
        Network: The constructed network object.
    """
    params = fill_out_dictionary(params_input)
    validify_dictionary(params)
    # Then make the Network object
    number_of_nodes = params["number_of_nodes"]
    if isinstance(params["priority_classes"], dict):
        preempt_priorities = [False] * number_of_nodes
    elif isinstance(params["priority_classes"], tuple):
        preempt_priorities = params["priority_classes"][1]
        params["priority_classes"] = {
            clss: params["priority_classes"][0][clss]
            for clss in params["customer_class_names"]
        }
    else:
        preempt_priorities = [False] * number_of_nodes
    class_change_matrices = params.get(
        "class_change_matrices",
        [None] * number_of_nodes,
    )
    class_change_time_distributions = {
        clss2: dict.fromkeys(params["customer_class_names"])
        for clss2 in params["customer_class_names"]
    }
    if "class_change_time_distributions" in params:
        for clss1 in params["customer_class_names"]:
            if clss1 in params["class_change_time_distributions"]:
                for clss2 in params["customer_class_names"]:
                    if clss2 in params["class_change_time_distributions"][clss1]:
                        class_change_time_distributions[clss1][clss2] = params[
                            "class_change_time_distributions"
                        ][clss1][clss2]

    nodes = [
        ServiceCentre(
            params["number_of_servers"][nd],
            params["queue_capacities"][nd],
            class_change_matrices[nd],
            preempt_priorities[nd],
            params["ps_thresholds"][nd],
            params["server_priority_functions"][nd],
            params["service_disciplines"][nd],
        )
        for nd in range(number_of_nodes)
    ]
    classes = {}
    for clss_name in params["customer_class_names"]:
        classes[clss_name] = CustomerClass(
            params["arrival_distributions"][clss_name],
            params["service_distributions"][clss_name],
            params["routing"][clss_name],
            params["priority_classes"][clss_name],
            params["baulking_functions"][clss_name],
            params["batching_distributions"][clss_name],
            params["reneging_time_distributions"][clss_name],
            class_change_time_distributions[clss_name],
            benchmark=params["customer_class_benchmarks"][clss_name],
        )
    n = Network(nodes, classes)
    n.system_capacity = params["system_capacity"]
    return n

fill_out_dictionary

fill_out_dictionary(params)

Fill out a parameters dictionary with default optional values.

Parameters:

Name Type Description Default
params dict

The parameters dictionary to complete.

required

Returns:

Type Description
dict

The completed parameters dictionary.

Source code in hciw/import_params.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def fill_out_dictionary(params):  # noqa: C901 - linear default-filling branch chain
    """Fill out a parameters dictionary with default optional values.

    Args:
        params (dict): The parameters dictionary to complete.

    Returns:
        dict: The completed parameters dictionary.
    """
    if isinstance(params["arrival_distributions"], list):
        arr_dists = params["arrival_distributions"]
        params["arrival_distributions"] = {"Customer": arr_dists}
    if isinstance(params["service_distributions"], list):
        srv_dists = params["service_distributions"]
        params["service_distributions"] = {"Customer": srv_dists}
    if "routing" in params:
        if isinstance(params["routing"], list):
            transition_matrix = params["routing"]
            params["routing"] = {
                "Customer": routing.TransitionMatrix(
                    transition_matrix=transition_matrix
                )
            }
        elif isinstance(params["routing"], dict):
            for clss in params["routing"]:
                if isinstance(params["routing"][clss], list):
                    transition_matrix = params["routing"][clss]
                    params["routing"][clss] = routing.TransitionMatrix(
                        transition_matrix=transition_matrix
                    )
        else:
            params["routing"] = {"Customer": params["routing"]}
    if "baulking_functions" in params and isinstance(
        params["baulking_functions"], list
    ):
        blk_fncs = params["baulking_functions"]
        params["baulking_functions"] = {"Customer": blk_fncs}
    if "batching_distributions" in params and isinstance(
        params["batching_distributions"], list
    ):
        btch_dists = params["batching_distributions"]
        params["batching_distributions"] = {"Customer": btch_dists}
    if "reneging_time_distributions" in params and isinstance(
        params["reneging_time_distributions"], list
    ):
        reneging_dists = params["reneging_time_distributions"]
        params["reneging_time_distributions"] = {"Customer": reneging_dists}

    class_names = sorted(params["arrival_distributions"].keys())
    params["customer_class_names"] = class_names

    number_of_nodes = len(params["number_of_servers"])
    default_dict = {
        "name": "Simulation",
        "routing": {
            class_name: routing.TransitionMatrix(transition_matrix=[[0.0]])
            for class_name in class_names
        },
        "number_of_nodes": number_of_nodes,
        "number_of_classes": len(class_names),
        "queue_capacities": [float("inf")] * number_of_nodes,
        "priority_classes": dict.fromkeys(class_names, 0),
        "baulking_functions": {
            class_name: [None] * number_of_nodes for class_name in class_names
        },
        "batching_distributions": {
            class_name: [hciw.dists.Deterministic(1)] * number_of_nodes
            for class_name in class_names
        },
        "ps_thresholds": [1] * number_of_nodes,
        "server_priority_functions": [None] * number_of_nodes,
        "reneging_time_distributions": {
            class_name: [None] * number_of_nodes for class_name in class_names
        },
        "service_disciplines": [hciw.disciplines.FIFO for _ in range(number_of_nodes)],
        "customer_class_benchmarks": dict.fromkeys(class_names, None),
        "system_capacity": float("inf"),
    }

    for key, value in default_dict.items():
        params[key] = params.get(key, value)
    return params

validify_dictionary

validify_dictionary(params)

Raise errors if a parameters dictionary is invalid.

Parameters:

Name Type Description Default
params dict

The parameters dictionary to validate.

required

Raises:

Type Description
ValueError

If the parameters are inconsistent or invalid.

Source code in hciw/import_params.py
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
def validify_dictionary(params):  # noqa: C901, PLR0912 - linear validation branch chain
    """Raise errors if a parameters dictionary is invalid.

    Args:
        params (dict): The parameters dictionary to validate.

    Raises:
        ValueError: If the parameters are inconsistent or invalid.
    """
    consistant_num_classes = (
        params["number_of_classes"]
        == len(params["arrival_distributions"])
        == len(params["service_distributions"])
        == len(params["routing"])
        == len(params["batching_distributions"])
        == len(params["reneging_time_distributions"])
    )
    if not consistant_num_classes:
        raise ValueError("Ensure consistant number of classes is used throughout.")
    consistant_class_names = (
        set(params["arrival_distributions"])
        == set(params["service_distributions"])
        == set(params["routing"])
        == set(params["batching_distributions"])
        == set(params["reneging_time_distributions"])
    ) and (
        len(params["arrival_distributions"])
        == len(params["service_distributions"])
        == len(params["batching_distributions"])
        == len(params["reneging_time_distributions"])
    )
    if not consistant_class_names:
        raise ValueError("Ensure consistant names for customer classes.")
    num_nodes_count = (
        [params["number_of_nodes"]]
        + [len(obs) for obs in params["arrival_distributions"].values()]
        + [len(obs) for obs in params["service_distributions"].values()]
        + [len(obs) for obs in params["batching_distributions"].values()]
        + [len(obs) for obs in params["reneging_time_distributions"].values()]
        + [len(params["number_of_servers"])]
        + [len(params["server_priority_functions"])]
        + [len(params["queue_capacities"])]
    )
    if len(set(num_nodes_count)) != 1:
        raise ValueError("Ensure consistant number of nodes is used throughout.")
    neg_numservers = any(
        (isinstance(obs, int) and obs < 0) for obs in params["number_of_servers"]
    )
    valid_capacities = all(
        ((isinstance(obs, int) and obs >= 0) or obs == float("inf") or obs == "Inf")
        for obs in params["queue_capacities"]
    )
    if neg_numservers:
        raise ValueError("Number of servers must be positive integers.")
    for c in params["number_of_servers"]:
        if not (isinstance(c, (int, Schedule)) or c == float("inf")):
            raise ValueError(
                "Number of servers must be positive integers or instances of "
                "hciw.schedules.Schedule."
            )
    if not valid_capacities:
        raise ValueError("Queue capacities must be positive integers or zero.")

    if "class_change_matrices" in params:
        if not isinstance(params["class_change_matrices"], list):
            raise ValueError(
                "class_change_matrices should be a list of dictionaries for each "
                "node in the network."
            )
        num_nodes = len(params["class_change_matrices"]) == params["number_of_nodes"]
        if not num_nodes:
            raise ValueError("Ensure correct nodes used in class_change_matrices.")
        for nd in params["class_change_matrices"]:
            for row in nd.values():
                if (
                    sum(row.values()) > 1.0
                    or min(row.values()) < 0.0
                    or max(row.values()) > 1.0
                ):
                    raise ValueError("Ensure that class change matrix is valid.")
        class_change_names = {
            k for matrix in params["class_change_matrices"] for k in matrix
        }
        if not class_change_names.issubset(set(params["arrival_distributions"])):
            raise ValueError("Ensure consistant names for customer classes.")

    if "class_change_time_distributions" in params:
        class_change_from_names = set(params["class_change_time_distributions"].keys())
        class_change_to_names = {
            clss
            for row in params["class_change_time_distributions"].values()
            for clss in row
        }
        wrong_class_names = (
            not class_change_from_names.issubset(set(params["customer_class_names"]))
        ) or (not class_change_to_names.issubset(set(params["customer_class_names"])))
        if wrong_class_names:
            raise ValueError(
                "Ensure consistant customer classes used in "
                "class_change_time_distributions."
            )

    if not isinstance(params["customer_class_benchmarks"], dict):
        raise ValueError(  # noqa: TRY004 - consistent with the other validations
            "customer_class_benchmarks should be a dictionary mapping customer "
            "class names to benchmark times."
        )
    if not set(params["customer_class_benchmarks"]).issubset(
        set(params["customer_class_names"])
    ):
        raise ValueError(
            "Ensure consistant customer classes used in customer_class_benchmarks."
        )
    for clss, benchmark in params["customer_class_benchmarks"].items():
        if benchmark is not None and (
            isinstance(benchmark, bool) or not isinstance(benchmark, (int, float))
        ):
            raise ValueError(
                f"Benchmark for customer class '{clss}' must be a number or None."
            )

    if not isinstance(params["system_capacity"], int) and params[
        "system_capacity"
    ] != float("inf"):
        raise ValueError("Ensure system capacity is a positive integer.")
    if params["system_capacity"] <= 0:
        raise ValueError("Ensure system capacity is a positive integer.")

simulation

The Simulation engine that drives customers through a network.

ArrivalNode

ArrivalNode(simulation)

Represent the arrival node of the network.

This class handles arrivals, baulking, and rejections of individuals. See hciw.Simulation for the main simulation class.

Initialise the arrival node.

Parameters:

Name Type Description Default
simulation object

The simulation to which this arrival node belongs.

required
Source code in hciw/arrival_node.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def __init__(self, simulation):
    """Initialise the arrival node.

    Args:
        simulation (object): The simulation to which this arrival node
            belongs.
    """
    self.simulation = simulation
    self.number_of_individuals = 0
    self.all_individuals = []
    self.number_of_individuals_per_class = dict.fromkeys(
        self.simulation.network.customer_class_names, 0
    )
    self.number_accepted_individuals = 0
    self.number_accepted_individuals_per_class = dict.fromkeys(
        self.simulation.network.customer_class_names, 0
    )
    self.system_capacity = self.simulation.network.system_capacity
    self.event_dates_dict: dict[int, dict[str, float | bool]] = {
        nd + 1: dict.fromkeys(self.simulation.network.customer_class_names, False)
        for nd in range(self.simulation.network.number_of_nodes)
    }

initialise

initialise()

Initialise the event dates dictionary and next event date.

Source code in hciw/arrival_node.py
36
37
38
39
def initialise(self):
    """Initialise the event dates dictionary and next event date."""
    self.initialise_event_dates_dict()
    self.find_next_event_date()

decide_baulk

decide_baulk(next_node, next_individual)

Either make an individual baulk or send them to the next node.

Parameters:

Name Type Description Default
next_node Node

The next node.

required
next_individual Individual

The individual being considered.

required
Source code in hciw/arrival_node.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def decide_baulk(self, next_node, next_individual):
    """Either make an individual baulk or send them to the next node.

    Args:
        next_node (Node): The next node.
        next_individual (Individual): The individual being considered.
    """
    if next_node.baulking_functions[self.next_class] is None:
        self.send_individual(next_node, next_individual)
    else:
        rnd_num = random()
        if rnd_num < next_node.baulking_functions[self.next_class](
            next_node.number_of_individuals,
            Q=self.simulation,
            next_ind=next_individual,
            next_node=next_node,
        ):
            self.record_baulk(next_node, next_individual)
            self.simulation.nodes[-1].accept(next_individual, completed=False)
        else:
            self.send_individual(next_node, next_individual)

find_next_event_date

find_next_event_date()

Find the time of the next arrival.

Source code in hciw/arrival_node.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def find_next_event_date(self):
    """Find the time of the next arrival."""
    minnd = None
    minclss = None
    mindate = float("Inf")
    for nd in self.event_dates_dict:
        for clss in self.event_dates_dict[nd]:
            if self.event_dates_dict[nd][clss] < mindate:
                minnd = nd
                minclss = clss
                mindate = self.event_dates_dict[nd][clss]
    self.next_node = minnd
    self.next_class = minclss
    self.next_event_date = mindate

have_event

have_event()

Create and send a batch of arriving individuals.

Finds a batch size, creates that many individuals, sends them to the relevant node, then updates the event dates dictionary.

Source code in hciw/arrival_node.py
 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
116
117
118
def have_event(self):
    """Create and send a batch of arriving individuals.

    Finds a batch size, creates that many individuals, sends them to the
    relevant node, then updates the event dates dictionary.
    """
    batch = self.batch_size(self.next_node, self.next_class)
    assert self.next_node is not None
    assert self.next_class is not None
    for _ in range(batch):
        self.number_of_individuals += 1
        self.number_of_individuals_per_class[self.next_class] += 1
        priority_class = self.simulation.network.priority_class_mapping[
            self.next_class
        ]
        next_individual = self.simulation.IndividualType(
            self.number_of_individuals,
            self.next_class,
            priority_class,
            simulation=self.simulation,
        )
        next_node = self.simulation.transitive_nodes[self.next_node - 1]
        next_individual.starting_node = next_node.id_number
        self.simulation.routers[
            next_individual.customer_class
        ].initialise_individual(next_individual)
        self.release_individual(next_node, next_individual)

    self.event_dates_dict[self.next_node][self.next_class] = self.increment_time(
        self.event_dates_dict[self.next_node][self.next_class],
        self.inter_arrival(self.next_node, self.next_class),
    )
    self.find_next_event_date()

increment_time

increment_time(original, increment)

Increment the original time by the increment.

Parameters:

Name Type Description Default
original float

The original time.

required
increment float

The amount to add.

required

Returns:

Type Description

The incremented time.

Source code in hciw/arrival_node.py
120
121
122
123
124
125
126
127
128
129
130
def increment_time(self, original, increment):
    """Increment the original time by the increment.

    Args:
        original (float): The original time.
        increment (float): The amount to add.

    Returns:
        The incremented time.
    """
    return original + increment

initialise_event_dates_dict

initialise_event_dates_dict()

Initialise the event dates dictionary.

Random times are sampled for each node and class.

Source code in hciw/arrival_node.py
132
133
134
135
136
137
138
139
140
141
142
def initialise_event_dates_dict(self):
    """Initialise the event dates dictionary.

    Random times are sampled for each node and class.
    """
    for nd in self.event_dates_dict:
        for clss in self.event_dates_dict[nd]:
            if self.simulation.inter_arrival_times[nd][clss] is not None:
                self.event_dates_dict[nd][clss] = self.inter_arrival(nd, clss)
            else:
                self.event_dates_dict[nd][clss] = float("inf")

inter_arrival

inter_arrival(nd, clss)

Sample the inter-arrival time for the next class and node.

Parameters:

Name Type Description Default
nd int

The destination node.

required
clss str

The customer class.

required

Returns:

Type Description

The sampled inter-arrival time.

Source code in hciw/arrival_node.py
144
145
146
147
148
149
150
151
152
153
154
155
156
def inter_arrival(self, nd, clss):
    """Sample the inter-arrival time for the next class and node.

    Args:
        nd (int): The destination node.
        clss (str): The customer class.

    Returns:
        The sampled inter-arrival time.
    """
    return self.simulation.inter_arrival_times[nd][clss]._sample(
        t=self.simulation.current_time
    )

batch_size

batch_size(nd, clss)

Sample the batch size for the next class and node.

Parameters:

Name Type Description Default
nd int

The destination node.

required
clss str

The customer class.

required

Returns:

Type Description
int

The sampled batch size.

Raises:

Type Description
ValueError

If the sampled batch size is not a non-negative integer.

Source code in hciw/arrival_node.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def batch_size(self, nd, clss):
    """Sample the batch size for the next class and node.

    Args:
        nd (int): The destination node.
        clss (str): The customer class.

    Returns:
        int: The sampled batch size.

    Raises:
        ValueError: If the sampled batch size is not a non-negative
            integer.
    """
    batch = self.simulation.batch_sizes[nd][clss]._sample(
        t=self.simulation.current_time
    )
    if isinstance(batch, int) and batch >= 0:
        return batch
    raise ValueError("Batch sizes must be positive integers.")

record_baulk

record_baulk(next_node, individual)

Write a baulking record for an individual.

Parameters:

Name Type Description Default
next_node Node

The node the individual would have joined.

required
individual Individual

The baulking individual.

required
Source code in hciw/arrival_node.py
179
180
181
182
183
184
185
186
def record_baulk(self, next_node, individual):
    """Write a baulking record for an individual.

    Args:
        next_node (Node): The node the individual would have joined.
        individual (Individual): The baulking individual.
    """
    next_node.write_baulking_or_rejection_record(individual, record_type="baulk")

record_rejection

record_rejection(next_node, individual)

Write a rejection record for an individual.

Parameters:

Name Type Description Default
next_node Node

The node the individual was rejected from.

required
individual Individual

The rejected individual.

required
Source code in hciw/arrival_node.py
188
189
190
191
192
193
194
195
196
197
def record_rejection(self, next_node, individual):
    """Write a rejection record for an individual.

    Args:
        next_node (Node): The node the individual was rejected from.
        individual (Individual): The rejected individual.
    """
    next_node.write_baulking_or_rejection_record(
        individual, record_type="rejection"
    )

release_individual

release_individual(next_node, next_individual)

Release an individual to the next node.

Either rejects the individual due to lack of capacity, or sends that individual to baulk or not.

Parameters:

Name Type Description Default
next_node Node

The next node.

required
next_individual Individual

The individual being released.

required
Source code in hciw/arrival_node.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
def release_individual(self, next_node, next_individual):
    """Release an individual to the next node.

    Either rejects the individual due to lack of capacity, or sends that
    individual to baulk or not.

    Args:
        next_node (Node): The next node.
        next_individual (Individual): The individual being released.
    """
    if (next_node.number_of_individuals >= next_node.node_capacity) or (
        self.simulation.number_of_individuals >= self.system_capacity
    ):
        self.record_rejection(next_node, next_individual)
        self.simulation.nodes[-1].accept(next_individual, completed=False)
    else:
        self.decide_baulk(next_node, next_individual)

send_individual

send_individual(next_node, next_individual)

Send the next individual to the next node.

Parameters:

Name Type Description Default
next_node Node

The next node.

required
next_individual Individual

The individual being sent.

required
Source code in hciw/arrival_node.py
217
218
219
220
221
222
223
224
225
226
def send_individual(self, next_node, next_individual):
    """Send the next individual to the next node.

    Args:
        next_node (Node): The next node.
        next_individual (Individual): The individual being sent.
    """
    self.number_accepted_individuals += 1
    self.number_accepted_individuals_per_class[next_individual.customer_class] += 1
    next_node.accept(next_individual)

update_next_event_date

update_next_event_date()

Do nothing, as the next event update happens at the event time.

Source code in hciw/arrival_node.py
228
229
def update_next_event_date(self):
    """Do nothing, as the next event update happens at the event time."""

ExactArrivalNode

ExactArrivalNode(simulation)

Bases: ArrivalNode

Represent an arrival node with exact numerical precision.

Inherits from ArrivalNode and implements a more precise version of addition to fix discrepancies with floating point numbers.

Source code in hciw/arrival_node.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def __init__(self, simulation):
    """Initialise the arrival node.

    Args:
        simulation (object): The simulation to which this arrival node
            belongs.
    """
    self.simulation = simulation
    self.number_of_individuals = 0
    self.all_individuals = []
    self.number_of_individuals_per_class = dict.fromkeys(
        self.simulation.network.customer_class_names, 0
    )
    self.number_accepted_individuals = 0
    self.number_accepted_individuals_per_class = dict.fromkeys(
        self.simulation.network.customer_class_names, 0
    )
    self.system_capacity = self.simulation.network.system_capacity
    self.event_dates_dict: dict[int, dict[str, float | bool]] = {
        nd + 1: dict.fromkeys(self.simulation.network.customer_class_names, False)
        for nd in range(self.simulation.network.number_of_nodes)
    }

increment_time

increment_time(original, increment) -> Decimal

Increment the original time by the increment.

Parameters:

Name Type Description Default
original float

The original time.

required
increment float

The amount to add.

required

Returns:

Type Description
Decimal

The incremented time.

Source code in hciw/exactnode.py
71
72
73
74
75
76
77
78
79
80
81
def increment_time(self, original, increment) -> Decimal:
    """Increment the original time by the increment.

    Args:
        original (float): The original time.
        increment (float): The amount to add.

    Returns:
        Decimal: The incremented time.
    """
    return Decimal(str(original)) + Decimal(str(increment))

inter_arrival

inter_arrival(nd, clss) -> Decimal

Sample the inter-arrival time for the next class and node.

Parameters:

Name Type Description Default
nd Node

The next node.

required
clss Individual

The individual class to select next.

required

Returns:

Type Description
Decimal

The sampled inter-arrival time.

Source code in hciw/exactnode.py
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def inter_arrival(self, nd, clss) -> Decimal:
    """Sample the inter-arrival time for the next class and node.

    Args:
        nd (Node): The next node.
        clss (Individual): The individual class to select next.

    Returns:
        Decimal: The sampled inter-arrival time.
    """
    return Decimal(
        str(
            self.simulation.inter_arrival_times[nd][clss]._sample(
                self.simulation.current_time
            )
        )
    )

ExactNode

ExactNode(id_, simulation)

Bases: Node

Represent a node with exact numerical precision.

This class inherits from Node and implements a more precise version of addition to fix discrepancies with floating point numbers.

Source code in hciw/node.py
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
def __init__(self, id_, simulation):
    """Initialise a node.

    Args:
        id_ (int): Identifier for the node.
        simulation (object): The simulation to which this node belongs.
    """
    self.simulation = simulation
    node = self.simulation.network.service_centres[id_ - 1]
    self.server_priority_function = node.server_priority_function
    self.service_discipline = node.service_discipline
    self.next_event_type: str | None = None
    self.schedule: Schedule | Slotted | None
    if isinstance(node.number_of_servers, Schedule):
        self.schedule = node.number_of_servers
        self.schedule.initialise()
        self.c: int | float = self.schedule.c
        if isinstance(self.schedule, Slotted):
            self.slotted: bool = True
            self.next_event_date = self.schedule.next_slot_date
            self.next_event_type = "slotted_service"
        else:
            self.slotted = False
            self.next_event_date = self.schedule.next_shift_change_date
            self.next_shift_change = self.schedule.next_shift_change_date
            self.next_event_type = "shift_change"
    else:
        self.c = node.number_of_servers
        self.schedule = None
        self.slotted = False
        self.next_event_date = float("Inf")
        self.next_shift_change = float("Inf")
    self.node_capacity = node.queueing_capacity + self.c
    self.class_change = node.class_change_matrix
    self.individuals = [[] for _ in range(simulation.number_of_priority_classes)]
    self.number_of_individuals = 0
    self.number_in_service = 0
    self.id_number = id_
    self.baulking_functions = {
        clss: self.simulation.network.customer_classes[clss].baulking_functions[
            id_ - 1
        ]
        for clss in self.simulation.network.customer_class_names
    }
    self.overtime = []
    self.blocked_queue = []
    self.len_blocked_queue = 0
    if not isinf(self.c):
        self.servers = self.create_starting_servers()
    self.highest_id = self.c
    self.simulation.deadlock_detector.initialise_at_node(self)
    self.priority_preempt = node.priority_preempt
    self.interrupted_individuals = []
    self.number_interrupted_individuals = 0
    self.all_servers_total = []
    self.all_servers_busy = []
    self.reneging = node.reneging
    self.dynamic_classes = node.class_change_time
    self.next_class_change_date = float("Inf")
    self.next_individual: list[Individual] | Individual | None = None

now property

now: Decimal

Get the current simulation time.

Returns:

Type Description
Decimal

The current simulation time.

create_starting_servers

create_starting_servers() -> list[Server]

Initialise the servers at this node.

Returns:

Type Description
List[Server]

The newly created starting servers.

Source code in hciw/exactnode.py
26
27
28
29
30
31
32
def create_starting_servers(self) -> list[Server]:
    """Initialise the servers at this node.

    Returns:
        List[Server]: The newly created starting servers.
    """
    return [Server(self, i + 1, Decimal("0.0")) for i in range(int(self.c))]

increment_time

increment_time(original, increment) -> Decimal

Increment the original time by the increment.

Parameters:

Name Type Description Default
original float

The original time.

required
increment float

The amount to add.

required

Returns:

Type Description
Decimal

The incremented time.

Source code in hciw/exactnode.py
34
35
36
37
38
39
40
41
42
43
44
def increment_time(self, original, increment) -> Decimal:
    """Increment the original time by the increment.

    Args:
        original (float): The original time.
        increment (float): The amount to add.

    Returns:
        Decimal: The incremented time.
    """
    return Decimal(str(original)) + Decimal(str(increment))

get_service_time

get_service_time(ind) -> Decimal

Return a service time for the given customer class.

Parameters:

Name Type Description Default
ind Individual

The individual being served.

required

Returns:

Type Description
Decimal

The sampled service time.

Source code in hciw/exactnode.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def get_service_time(self, ind) -> Decimal:
    """Return a service time for the given customer class.

    Args:
        ind (Individual): The individual being served.

    Returns:
        Decimal: The sampled service time.
    """
    return Decimal(
        str(
            self.simulation.service_times[self.id_number][
                ind.customer_class
            ]._sample(self.simulation.current_time, ind=ind)
        )
    )

ExitNode

ExitNode()

Represent the exit node of the network.

Initialise the exit node.

Source code in hciw/exit_node.py
 7
 8
 9
10
11
12
13
14
def __init__(self):
    """Initialise the exit node."""
    self.all_individuals = []
    self.number_of_individuals = 0
    self.number_of_completed_individuals = 0
    self.id_number = -1
    self.next_event_date = float("Inf")
    self.node_capacity = float("Inf")

accept

accept(next_individual, completed=True)

Add an individual to the list of completed individuals.

Parameters:

Name Type Description Default
next_individual Individual

The individual to accept.

required
completed bool

Whether the individual completed service.

True
Source code in hciw/exit_node.py
24
25
26
27
28
29
30
31
32
33
34
35
def accept(self, next_individual, completed=True):
    """Add an individual to the list of completed individuals.

    Args:
        next_individual (Individual): The individual to accept.
        completed (bool): Whether the individual completed service.
    """
    self.all_individuals.append(next_individual)
    next_individual.node = -1
    self.number_of_individuals += 1
    if completed:
        self.number_of_completed_individuals += 1

update_next_event_date

update_next_event_date()

Do nothing, as the next event date is always the max time.

Source code in hciw/exit_node.py
37
38
def update_next_event_date(self):
    """Do nothing, as the next event date is always the max time."""

Individual

Individual(
    id_number,
    customer_class="Customer",
    priority_class=0,
    simulation=False,
)

Represent an individual agent in a queueing network simulation.

An Individual models the flow of an agent through a queueing network, tracking its arrival time, service time, priority and other relevant data.

Attributes:

Name Type Description
arrival_date bool or float

Timestamp of the individual's arrival.

service_start_date bool or float

Timestamp the current service started.

service_time bool or float

Duration of the current service.

service_end_date bool or float

Timestamp the current service ends.

exit_date bool or float

Timestamp the individual exits the system.

id_number int

Unique identifier for the individual.

data_records list

The data records attached to the individual.

customer_class str

The customer class of the individual.

previous_class str

The previous customer class of the individual.

priority_class int

The priority class of the individual.

prev_priority_class int

The previous priority class of the individual.

original_class str

The original customer class of the individual.

is_blocked bool

Whether the individual is blocked.

server bool or Server

The server assigned to the individual, if any.

queue_size_at_arrival bool or int

Queue size when the individual arrived.

queue_size_at_departure bool or int

Queue size when the individual departed.

destination bool or int

The destination node in the network.

interrupted bool

Whether the individual's service was interrupted.

node bool or int

The node where the individual is located.

simulation bool or Simulation

The simulation the individual belongs to.

Example

.. code-block:: python

individual = Individual(
    id_number=1, customer_class='Gold', priority_class=1,
    simulation=True,
)

Initialise an Individual.

Parameters:

Name Type Description Default
id_number int

A unique identifier for the individual.

required
customer_class str

The customer class the individual belongs to. Defaults to 'Customer'.

'Customer'
priority_class int

The priority class of the individual. Defaults to 0.

0
simulation bool or Simulation

The simulation the individual is part of. Defaults to False.

False
Source code in hciw/individual.py
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
def __init__(
    self, id_number, customer_class="Customer", priority_class=0, simulation=False
):
    """Initialise an ``Individual``.

    Args:
        id_number (int): A unique identifier for the individual.
        customer_class (str, optional): The customer class the individual
            belongs to. Defaults to ``'Customer'``.
        priority_class (int, optional): The priority class of the
            individual. Defaults to ``0``.
        simulation (bool or Simulation, optional): The simulation the
            individual is part of. Defaults to ``False``.
    """
    self.arrival_date = False
    self.service_start_date = False
    self.service_time: float | bool | str = False
    self.service_end_date = False
    self.exit_date = False
    self.id_number = id_number
    self.data_records = []
    self.customer_class = customer_class
    self.previous_class = customer_class
    self.priority_class = priority_class
    self.prev_priority_class = priority_class
    self.original_class = customer_class
    self.is_blocked = False
    self.server: Server | bool = False
    self.queue_size_at_arrival: int | bool = False
    self.queue_size_at_departure: int | bool = False
    self.destination: int | bool = False
    self.interrupted = False
    self.node = False
    self.simulation = simulation

Node

Node(id_, simulation)

Represent a node in the queueing network.

The node holds a queue of individuals and, where applicable, the servers that process them.

Initialise a node.

Parameters:

Name Type Description Default
id_ int

Identifier for the node.

required
simulation object

The simulation to which this node belongs.

required
Source code in hciw/node.py
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
def __init__(self, id_, simulation):
    """Initialise a node.

    Args:
        id_ (int): Identifier for the node.
        simulation (object): The simulation to which this node belongs.
    """
    self.simulation = simulation
    node = self.simulation.network.service_centres[id_ - 1]
    self.server_priority_function = node.server_priority_function
    self.service_discipline = node.service_discipline
    self.next_event_type: str | None = None
    self.schedule: Schedule | Slotted | None
    if isinstance(node.number_of_servers, Schedule):
        self.schedule = node.number_of_servers
        self.schedule.initialise()
        self.c: int | float = self.schedule.c
        if isinstance(self.schedule, Slotted):
            self.slotted: bool = True
            self.next_event_date = self.schedule.next_slot_date
            self.next_event_type = "slotted_service"
        else:
            self.slotted = False
            self.next_event_date = self.schedule.next_shift_change_date
            self.next_shift_change = self.schedule.next_shift_change_date
            self.next_event_type = "shift_change"
    else:
        self.c = node.number_of_servers
        self.schedule = None
        self.slotted = False
        self.next_event_date = float("Inf")
        self.next_shift_change = float("Inf")
    self.node_capacity = node.queueing_capacity + self.c
    self.class_change = node.class_change_matrix
    self.individuals = [[] for _ in range(simulation.number_of_priority_classes)]
    self.number_of_individuals = 0
    self.number_in_service = 0
    self.id_number = id_
    self.baulking_functions = {
        clss: self.simulation.network.customer_classes[clss].baulking_functions[
            id_ - 1
        ]
        for clss in self.simulation.network.customer_class_names
    }
    self.overtime = []
    self.blocked_queue = []
    self.len_blocked_queue = 0
    if not isinf(self.c):
        self.servers = self.create_starting_servers()
    self.highest_id = self.c
    self.simulation.deadlock_detector.initialise_at_node(self)
    self.priority_preempt = node.priority_preempt
    self.interrupted_individuals = []
    self.number_interrupted_individuals = 0
    self.all_servers_total = []
    self.all_servers_busy = []
    self.reneging = node.reneging
    self.dynamic_classes = node.class_change_time
    self.next_class_change_date = float("Inf")
    self.next_individual: list[Individual] | Individual | None = None

now property

now

Get the current simulation time.

Returns:

Type Description
float

The current simulation time.

all_individuals property

all_individuals

Get all individuals currently at the node.

Returns:

Type Description
list

Every individual at the node, across all priority classes.

accept

accept(next_individual, completed=False)

Accept a new individual into the queue.

Removes any previous exit date and blockage status, begins service if possible, records arrival information, and updates the state tracker.

Parameters:

Name Type Description Default
next_individual Individual

The individual to accept.

required
completed bool

Whether the individual has completed service.

False
Source code in hciw/node.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def accept(self, next_individual, completed=False):
    """Accept a new individual into the queue.

    Removes any previous exit date and blockage status, begins service
    if possible, records arrival information, and updates the state
    tracker.

    Args:
        next_individual (Individual): The individual to accept.
        completed (bool): Whether the individual has completed service.
    """
    next_individual.node = self.id_number
    next_individual.exit_date = False
    next_individual.is_blocked = False
    next_individual.original_class = next_individual.customer_class
    next_individual.queue_size_at_arrival = self.number_of_individuals
    self.individuals[next_individual.priority_class].append(next_individual)
    self.number_of_individuals += 1
    self.begin_service_if_possible_accept(next_individual)
    self.simulation.statetracker.change_state_accept(self, next_individual)

add_new_servers

add_new_servers(num_servers)

Add the appropriate number of servers for the given shift.

Parameters:

Name Type Description Default
num_servers int

The number of servers to add.

required
Source code in hciw/node.py
129
130
131
132
133
134
135
136
137
138
139
def add_new_servers(self, num_servers):
    """Add the appropriate number of servers for the given shift.

    Args:
        num_servers (int): The number of servers to add.
    """
    for _i in range(num_servers):
        self.highest_id += 1
        self.servers.append(
            self.simulation.ServerType(self, self.highest_id, self.now)
        )

attach_server

attach_server(server, individual)

Attach a server to an individual, and vice versa.

Parameters:

Name Type Description Default
server Server

The server to attach.

required
individual Individual

The individual to attach.

required
Source code in hciw/node.py
141
142
143
144
145
146
147
148
149
150
151
152
153
def attach_server(self, server, individual):
    """Attach a server to an individual, and vice versa.

    Args:
        server (Server): The server to attach.
        individual (Individual): The individual to attach.
    """
    server.cust = individual
    server.busy = True
    individual.server = server
    self.simulation.deadlock_detector.action_at_attach_server(
        self, server, individual
    )

begin_service_if_possible_accept

begin_service_if_possible_accept(next_individual)

Begin service for an individual at the acceptance point.

Sets the arrival date to the current time, and if a server is free or there are infinitely many servers, attaches the server, gets the service start time, service time and service end time, and updates the server's end date.

Parameters:

Name Type Description Default
next_individual Individual

The individual to serve.

required
Source code in hciw/node.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def begin_service_if_possible_accept(self, next_individual):
    """Begin service for an individual at the acceptance point.

    Sets the arrival date to the current time, and if a server is free
    or there are infinitely many servers, attaches the server, gets the
    service start time, service time and service end time, and updates
    the server's end date.

    Args:
        next_individual (Individual): The individual to serve.
    """
    next_individual.arrival_date = self.now
    if self.reneging is True:
        next_individual.reneging_date = self.get_reneging_date(next_individual)
    self.decide_class_change(next_individual)

    ind = next_individual if isinf(self.c) else self.choose_next_customer()

    if ind is not None:
        free_server = self.find_free_server(ind)
        if free_server is None and isinf(self.c) is False and self.c > 0:
            self.decide_preempt(ind)
        if free_server is not None or isinf(self.c):
            if isinf(self.c) is False:
                self.attach_server(free_server, ind)
            ind.service_start_date = self.now
            ind.service_time = self.get_service_time(ind)
            ind.service_end_date = self.now + ind.service_time
            self.number_in_service += 1
            self.reset_class_change(ind)
            if not isinf(self.c):
                assert isinstance(free_server, Server)
                free_server.next_end_service_date = ind.service_end_date

begin_interrupted_individuals_service

begin_interrupted_individuals_service(srvr)

Restart an interrupted individual's service.

The individual's service time is resampled.

Parameters:

Name Type Description Default
srvr Server

The server to give to the individual.

required
Source code in hciw/node.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
def begin_interrupted_individuals_service(self, srvr):
    """Restart an interrupted individual's service.

    The individual's service time is resampled.

    Args:
        srvr (Server): The server to give to the individual.
    """
    ind = next(iter(self.interrupted_individuals))
    if ind.is_blocked:
        node_blocked_to = self.simulation.nodes[ind.destination]
        ind.destination = False
        node_blocked_to.blocked_queue.remove((self.id_number, ind.id_number))
        node_blocked_to.len_blocked_queue -= 1
        ind.is_blocked = False
    self.attach_server(srvr, ind)
    self.give_service_time_after_preemption(ind)
    ind.service_start_date = self.now
    ind.service_end_date = self.now + ind.service_time
    ind.interrupted = False
    self.number_in_service += 1
    srvr.next_end_service_date = ind.service_end_date
    self.interrupted_individuals.remove(ind)
    self.number_interrupted_individuals -= 1

begin_service_if_possible_change_shift

begin_service_if_possible_change_shift()

Begin service for customers after a shift change.

If there are free servers, restarts interrupted customers' services and begins service for any waiting customers by giving a start date and end date and attaching servers to individuals.

Source code in hciw/node.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def begin_service_if_possible_change_shift(self):
    """Begin service for customers after a shift change.

    If there are free servers, restarts interrupted customers' services
    and begins service for any waiting customers by giving a start date
    and end date and attaching servers to individuals.
    """
    free_servers = [s for s in self.servers if not s.busy]
    for srvr in free_servers:
        if self.number_interrupted_individuals > 0:
            self.begin_interrupted_individuals_service(srvr)
        else:
            ind = self.choose_next_customer()
            if ind is not None:
                self.attach_server(srvr, ind)
                ind.service_start_date = self.now
                self.give_individual_a_service_time(ind)
                ind.service_end_date = self.increment_time(
                    ind.service_start_date, ind.service_time
                )
                self.number_in_service += 1
                self.reset_class_change(ind)
                srvr.next_end_service_date = ind.service_end_date

begin_service_if_possible_release

begin_service_if_possible_release(
    next_individual, newly_free_server
)

Begin service for an individual after a release.

Checks for interrupted individuals left to restart service, then gives an arrival date and service time, gives a start date and end date, and attaches the server to the individual.

Parameters:

Name Type Description Default
next_individual Individual

The individual being released.

required
newly_free_server Server

The server freed by the release.

required
Source code in hciw/node.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
def begin_service_if_possible_release(self, next_individual, newly_free_server):
    """Begin service for an individual after a release.

    Checks for interrupted individuals left to restart service, then
    gives an arrival date and service time, gives a start date and end
    date, and attaches the server to the individual.

    Args:
        next_individual (Individual): The individual being released.
        newly_free_server (Server): The server freed by the release.
    """
    if newly_free_server is not None and newly_free_server in self.servers:
        if self.number_interrupted_individuals > 0:
            self.begin_interrupted_individuals_service(newly_free_server)
        else:
            ind = self.choose_next_customer()
            if ind is not None:
                self.attach_server(newly_free_server, ind)
                ind.service_start_date = self.now
                self.give_individual_a_service_time(ind)
                ind.service_end_date = self.increment_time(
                    ind.service_start_date, ind.service_time
                )
                self.number_in_service += 1
                self.reset_class_change(ind)
                newly_free_server.next_end_service_date = ind.service_end_date

block_individual

block_individual(individual, next_node)

Block an individual from entering the next node.

Changes the blocked status, updates the state tracker, adds information to the next node's blocked queue, and updates the deadlock detector, informing the simulation that there are unchecked blockages for deadlock detection.

Parameters:

Name Type Description Default
individual Individual

The individual being blocked.

required
next_node Node

The node the individual is blocked from entering.

required
Source code in hciw/node.py
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
def block_individual(self, individual, next_node):
    """Block an individual from entering the next node.

    Changes the blocked status, updates the state tracker, adds
    information to the next node's blocked queue, and updates the
    deadlock detector, informing the simulation that there are unchecked
    blockages for deadlock detection.

    Args:
        individual (Individual): The individual being blocked.
        next_node (Node): The node the individual is blocked from
            entering.
    """
    individual.is_blocked = True
    self.simulation.statetracker.change_state_block(self, next_node, individual)
    next_node.blocked_queue.append((self.id_number, individual.id_number))
    next_node.len_blocked_queue += 1
    self.simulation.deadlock_detector.action_at_blockage(individual, next_node)
    self.simulation.unchecked_blockage = True

change_customer_class

change_customer_class(individual)

Change an individual's customer class.

The new class is chosen according to a probability distribution.

Parameters:

Name Type Description Default
individual Individual

The individual whose class changes.

required
Source code in hciw/node.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
def change_customer_class(self, individual):
    """Change an individual's customer class.

    The new class is chosen according to a probability distribution.

    Args:
        individual (Individual): The individual whose class changes.
    """
    if self.class_change:
        individual.previous_class = individual.customer_class
        individual.customer_class = random_choice(
            self.simulation.network.customer_class_names,
            [
                self.class_change[individual.previous_class][clss_name]
                for clss_name in self.simulation.network.customer_class_names
            ],
        )
        individual.prev_priority_class = individual.priority_class
        individual.priority_class = self.simulation.network.priority_class_mapping[
            individual.customer_class
        ]

change_customer_class_while_waiting

change_customer_class_while_waiting()

Change the next queued individual's class while waiting.

The individual is moved between priority queues and preemption is decided if the priority class changes.

Source code in hciw/node.py
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def change_customer_class_while_waiting(self):
    """Change the next queued individual's class while waiting.

    The individual is moved between priority queues and preemption is
    decided if the priority class changes.
    """
    changing_individual = self.next_individual
    assert isinstance(changing_individual, Individual)
    changing_individual.customer_class = changing_individual.next_class
    changing_individual.priority_class = (
        self.simulation.network.priority_class_mapping[
            changing_individual.next_class
        ]
    )
    if (
        changing_individual.priority_class
        != changing_individual.prev_priority_class
    ):
        self.change_priority_queue(changing_individual)
        self.decide_preempt(changing_individual)
    self.simulation.statetracker.change_state_classchange(self, changing_individual)
    changing_individual.previous_class = changing_individual.next_class
    changing_individual.prev_priority_class = changing_individual.priority_class
    self.decide_class_change(changing_individual)

change_priority_queue

change_priority_queue(individual)

Move an individual to their new priority queue.

Parameters:

Name Type Description Default
individual Individual

The individual to move.

required
Source code in hciw/node.py
332
333
334
335
336
337
338
339
def change_priority_queue(self, individual):
    """Move an individual to their new priority queue.

    Args:
        individual (Individual): The individual to move.
    """
    self.individuals[individual.prev_priority_class].remove(individual)
    self.individuals[individual.priority_class].append(individual)

change_shift

change_shift()

Implement a server shift change.

Adds or deletes servers, or indicates which servers should go off duty, and begins any new services if there are free servers.

Source code in hciw/node.py
341
342
343
344
345
346
347
348
349
350
351
352
353
def change_shift(self):
    """Implement a server shift change.

    Adds or deletes servers, or indicates which servers should go off
    duty, and begins any new services if there are free servers.
    """
    assert self.schedule is not None
    self.schedule.get_next_shift()
    self.next_shift_change = self.schedule.next_shift_change_date
    self.c = self.schedule.c
    self.take_servers_off_duty(preemption=self.schedule.preemption)
    self.add_new_servers(self.schedule.c)
    self.begin_service_if_possible_change_shift()

find_number_of_slotted_services

find_number_of_slotted_services()

Find the number of slotted services to start in this slot.

Returns:

Type Description
int

The number of services to start.

Source code in hciw/node.py
355
356
357
358
359
360
361
362
363
364
365
366
367
def find_number_of_slotted_services(self):
    """Find the number of slotted services to start in this slot.

    Returns:
        int: The number of services to start.
    """
    assert isinstance(self.schedule, Slotted)
    if self.schedule.capacitated:
        return min(
            max(self.schedule.slot_size - self.number_in_service, 0),
            self.number_of_individuals,
        )
    return min(self.schedule.slot_size, self.number_of_individuals)

interrupt_slotted_services

interrupt_slotted_services()

Interrupt services when there is not enough slot capacity.

Finds the customers whose slotted services need to be interrupted because of insufficient capacity at the current slot, and interrupts their services.

Source code in hciw/node.py
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
def interrupt_slotted_services(self):
    """Interrupt services when there is not enough slot capacity.

    Finds the customers whose slotted services need to be interrupted
    because of insufficient capacity at the current slot, and interrupts
    their services.
    """
    assert isinstance(self.schedule, Slotted)
    if self.schedule.capacitated and self.schedule.preemption is not False:
        number_to_interrupt = self.number_in_service - self.schedule.slot_size
        if number_to_interrupt > 0:
            inds_to_interrupt = sorted(
                [
                    ind
                    for ind in self.all_individuals
                    if ind.service_start_date is not False
                ],
                key=lambda x: (x.priority_class, x.arrival_date),
                reverse=True,
            )[:number_to_interrupt]
            for ind in inds_to_interrupt:
                self.interrupt_service(ind)

slotted_service

slotted_service()

Serve a set number of customers at the current time.

Only the number of customers allowed by the current slot may have service at the exact time the method is called.

Source code in hciw/node.py
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
def slotted_service(self):
    """Serve a set number of customers at the current time.

    Only the number of customers allowed by the current slot may have
    service at the exact time the method is called.
    """
    assert isinstance(self.schedule, Slotted)
    number_of_slotted_services = self.find_number_of_slotted_services()
    self.interrupt_slotted_services()
    for _i in range(number_of_slotted_services):
        if self.number_interrupted_individuals > 0:
            ind = self.interrupted_individuals[0]
            self.interrupted_individuals.remove(ind)
            self.number_interrupted_individuals -= 1
        else:
            ind = self.choose_next_customer()
        if ind is not None:
            ind.service_start_date = self.now
            self.give_individual_a_service_time(ind)
            ind.service_end_date = self.now + ind.service_time
            ind.server = True
            self.number_in_service += 1
            self.reset_class_change(ind)
    self.schedule.get_next_slot()

choose_next_customer

choose_next_customer()

Choose which customer will be next to be served.

Returns:

Type Description
Individual

The customer chosen by the service discipline, or None if no customer is waiting.

Source code in hciw/node.py
417
418
419
420
421
422
423
424
425
426
427
428
429
430
def choose_next_customer(self):
    """Choose which customer will be next to be served.

    Returns:
        Individual: The customer chosen by the service discipline, or
            None if no customer is waiting.
    """
    for priority_individuals in self.individuals:
        waiting_individuals = [
            ind for ind in priority_individuals if not ind.server
        ]
        if len(waiting_individuals) > 0:
            return self.service_discipline(waiting_individuals, self.now)
    return None

create_starting_servers

create_starting_servers()

Initialise the servers.

Returns:

Type Description
list

The newly created starting servers.

Source code in hciw/node.py
432
433
434
435
436
437
438
439
440
def create_starting_servers(self):
    """Initialise the servers.

    Returns:
        list: The newly created starting servers.
    """
    return [
        self.simulation.ServerType(self, i + 1, 0.0) for i in range(int(self.c))
    ]

decide_class_change

decide_class_change(next_individual)

Decide the next class and class change date for an individual.

Parameters:

Name Type Description Default
next_individual Individual

The individual to update.

required
Source code in hciw/node.py
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
def decide_class_change(self, next_individual):
    """Decide the next class and class change date for an individual.

    Args:
        next_individual (Individual): The individual to update.
    """
    if self.dynamic_classes is True:
        next_time = float("inf")
        next_class = next_individual.customer_class
        for clss, dist in self.simulation.network.customer_classes[
            next_individual.customer_class
        ].class_change_time_distributions.items():
            if dist is not None:
                t = dist.sample()
                if t < next_time:
                    next_time = t
                    next_class = clss
        next_individual.next_class = next_class
        next_individual.class_change_date = self.increment_time(self.now, next_time)
        self.find_next_class_change()

decide_preempt

decide_preempt(individual)

Decide whether priority preemption is needed.

If needed, finds the individual to preempt and preempts them.

Parameters:

Name Type Description Default
individual Individual

The individual seeking service.

required
Source code in hciw/node.py
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def decide_preempt(self, individual):
    """Decide whether priority preemption is needed.

    If needed, finds the individual to preempt and preempts them.

    Args:
        individual (Individual): The individual seeking service.
    """
    if self.priority_preempt:
        least_priority = max(s.cust.priority_class for s in self.servers)
        if individual.priority_class < least_priority:
            least_prioritised_individuals = [
                s.cust
                for s in self.servers
                if s.cust.priority_class == least_priority
            ]
            individual_to_preempt = max(
                list(least_prioritised_individuals),
                key=lambda cust: cust.service_start_date,
            )
            self.preempt(individual_to_preempt, individual)

detatch_server

detatch_server(server, individual)

Detach a server from an individual, and vice versa.

Parameters:

Name Type Description Default
server Server

The server to detach.

required
individual Individual

The individual to detach.

required
Source code in hciw/node.py
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
def detatch_server(self, server, individual):
    """Detach a server from an individual, and vice versa.

    Args:
        server (Server): The server to detach.
        individual (Individual): The individual to detach.
    """
    self.simulation.deadlock_detector.action_at_detatch_server(server)
    server.cust = False
    server.busy = False
    individual.server = False
    server.busy_time = self.increment_time(
        server.busy_time, individual.exit_date - individual.service_start_date
    )
    server.total_time = self.now - server.start_date
    if server.offduty:
        self.kill_server(server)

find_free_server

find_free_server(ind)

Find a free server.

Parameters:

Name Type Description Default
ind Individual

The individual needing a server.

required

Returns:

Type Description
Server

A free server, or None if none is available.

Source code in hciw/node.py
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def find_free_server(self, ind):
    """Find a free server.

    Args:
        ind (Individual): The individual needing a server.

    Returns:
        Server: A free server, or None if none is available.
    """
    if isinf(self.c):
        return None

    if self.server_priority_function is None:
        all_servers = self.servers
    else:
        all_servers = sorted(
            self.servers, key=lambda srv: self.server_priority_function(srv, ind)
        )

    for svr in all_servers:
        if not svr.busy:
            return svr
    return None

decide_between_simultaneous_individuals

decide_between_simultaneous_individuals()

Find the next individual that should now finish service.

Returns:

Type Description
Individual

A randomly chosen individual when several finish at the same time.

Source code in hciw/node.py
527
528
529
530
531
532
533
534
535
536
537
538
539
def decide_between_simultaneous_individuals(self):
    """Find the next individual that should now finish service.

    Returns:
        Individual: A randomly chosen individual when several finish at
            the same time.
    """
    assert isinstance(self.next_individual, list)
    if len(self.next_individual) > 1:
        next_individual = random_choice(self.next_individual)
    else:
        next_individual = self.next_individual[0]
    return next_individual

find_next_class_change

find_next_class_change()

Update the next class change date and individual.

Source code in hciw/node.py
541
542
543
544
545
546
547
548
def find_next_class_change(self):
    """Update the next class change date and individual."""
    self.next_class_change_date = float("inf")
    self.next_class_change_ind = None
    for ind in self.all_individuals:
        if (ind.class_change_date < self.next_class_change_date) and not ind.server:
            self.next_class_change_ind = ind
            self.next_class_change_date = ind.class_change_date

find_server_utilisation

find_server_utilisation()

Find the overall server utilisation for the node.

Source code in hciw/node.py
550
551
552
553
554
555
556
557
558
559
560
def find_server_utilisation(self):
    """Find the overall server utilisation for the node."""
    if isinf(self.c) or self.c == 0:
        self.server_utilisation = None
    else:
        for server in self.servers:
            self.all_servers_total.append(server.total_time)
            self.all_servers_busy.append(server.busy_time)
        self.server_utilisation = sum(self.all_servers_busy) / sum(
            self.all_servers_total
        )

finish_service

finish_service()

Finish the service of the next individual.

Finds the individual to finish service, checks whether they need to change class, finds their next node, and releases the individual if there is capacity at the destination, otherwise causes blockage.

Source code in hciw/node.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
def finish_service(self):
    """Finish the service of the next individual.

    Finds the individual to finish service, checks whether they need to
    change class, finds their next node, and releases the individual if
    there is capacity at the destination, otherwise causes blockage.
    """
    next_individual = self.decide_between_simultaneous_individuals()
    self.change_customer_class(next_individual)
    next_node = self.next_node(next_individual)
    next_individual.destination = next_node.id_number
    if not isinf(self.c) and self.c > 0:
        assert isinstance(next_individual.server, Server)
        next_individual.server.next_end_service_date = float("Inf")
    if next_node.number_of_individuals < next_node.node_capacity:
        self.release(next_individual, next_node)
    else:
        self.block_individual(next_individual, next_node)

give_individual_a_service_time

give_individual_a_service_time(individual)

Give a service time to an individual.

Either a new service time, or a service time after pre-emption.

Parameters:

Name Type Description Default
individual Individual

The individual to serve.

required
Source code in hciw/node.py
581
582
583
584
585
586
587
588
589
590
591
592
def give_individual_a_service_time(self, individual):
    """Give a service time to an individual.

    Either a new service time, or a service time after pre-emption.

    Args:
        individual (Individual): The individual to serve.
    """
    if individual.service_time is False:
        individual.service_time = self.get_service_time(individual)
    else:
        self.give_service_time_after_preemption(individual)

give_service_time_after_preemption

give_service_time_after_preemption(individual)

Give a service time after preemption.

Either resamples, restarts, or resumes service time from where it was left off.

Parameters:

Name Type Description Default
individual Individual

The individual to serve.

required
Source code in hciw/node.py
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
def give_service_time_after_preemption(self, individual):
    """Give a service time after preemption.

    Either resamples, restarts, or resumes service time from where it was
    left off.

    Args:
        individual (Individual): The individual to serve.
    """
    if individual.service_time == "resample":
        individual.service_time = self.get_service_time(individual)
    if individual.service_time == "restart":
        individual.service_time = individual.original_service_time
    if individual.service_time == "resume":
        individual.service_time = individual.time_left

have_event

have_event()

Trigger the node's next event.

Source code in hciw/node.py
610
611
612
613
614
615
616
617
618
619
620
621
def have_event(self):
    """Trigger the node's next event."""
    if self.next_event_type == "end_service":
        self.finish_service()
    elif self.next_event_type == "shift_change":
        self.change_shift()
    elif self.next_event_type == "renege":
        self.renege()
    elif self.next_event_type == "class_change":
        self.change_customer_class_while_waiting()
    elif self.next_event_type == "slotted_service":
        self.slotted_service()

increment_time

increment_time(original, increment)

Increment the original time by the increment.

Parameters:

Name Type Description Default
original float

The original time.

required
increment float

The amount to add.

required

Returns:

Type Description

The incremented time.

Source code in hciw/node.py
623
624
625
626
627
628
629
630
631
632
633
def increment_time(self, original, increment):
    """Increment the original time by the increment.

    Args:
        original (float): The original time.
        increment (float): The amount to add.

    Returns:
        The incremented time.
    """
    return original + increment

kill_server

kill_server(srvr)

Kill a server when it goes off duty.

Parameters:

Name Type Description Default
srvr Server

The server to remove.

required
Source code in hciw/node.py
635
636
637
638
639
640
641
642
643
644
645
646
def kill_server(self, srvr):
    """Kill a server when it goes off duty.

    Args:
        srvr (Server): The server to remove.
    """
    srvr.total_time = self.increment_time(self.next_event_date, -srvr.start_date)
    self.overtime.append(self.increment_time(self.next_event_date, -srvr.shift_end))
    self.all_servers_busy.append(srvr.busy_time)
    self.all_servers_total.append(srvr.total_time)
    indx = self.servers.index(srvr)
    del self.servers[indx]

next_node

next_node(ind)

Find the next node according to the routing method.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/node.py
648
649
650
651
652
653
654
655
656
657
658
659
def next_node(self, ind):
    """Find the next node according to the routing method.

    Args:
        ind (Individual): The individual being routed.

    Returns:
        Node: The next node.
    """
    return self.simulation.routers[ind.customer_class].next_node(
        ind, self.id_number
    )

next_node_for_rerouting

next_node_for_rerouting(ind)

Find the next node for rerouting according to the routing method.

Parameters:

Name Type Description Default
ind Individual

The individual being rerouted.

required

Returns:

Type Description
Node

The next node for rerouting.

Source code in hciw/node.py
661
662
663
664
665
666
667
668
669
670
671
672
def next_node_for_rerouting(self, ind):
    """Find the next node for rerouting according to the routing method.

    Args:
        ind (Individual): The individual being rerouted.

    Returns:
        Node: The next node for rerouting.
    """
    return self.simulation.routers[ind.customer_class].next_node_for_rerouting(
        ind, self.id_number
    )

next_node_for_jockeying

next_node_for_jockeying(ind)

Find the next node for jockeying according to the routing method.

Parameters:

Name Type Description Default
ind Individual

The jockeying individual.

required

Returns:

Type Description
Node

The next node for jockeying.

Source code in hciw/node.py
674
675
676
677
678
679
680
681
682
683
684
685
def next_node_for_jockeying(self, ind):
    """Find the next node for jockeying according to the routing method.

    Args:
        ind (Individual): The jockeying individual.

    Returns:
        Node: The next node for jockeying.
    """
    return self.simulation.routers[ind.customer_class].next_node_for_jockeying(
        ind, self.id_number
    )

preempt

preempt(individual_to_preempt, next_individual)

Preempt an individual and replace them in service.

Parameters:

Name Type Description Default
individual_to_preempt Individual

The individual removed from service.

required
next_individual Individual

The individual taking their place.

required
Source code in hciw/node.py
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
def preempt(self, individual_to_preempt, next_individual):
    """Preempt an individual and replace them in service.

    Args:
        individual_to_preempt (Individual): The individual removed from
            service.
        next_individual (Individual): The individual taking their place.
    """
    server = individual_to_preempt.server
    individual_to_preempt.original_service_time = individual_to_preempt.service_time
    if self.priority_preempt == "reroute":
        self.reroute(individual_to_preempt)
    else:
        self.write_interruption_record(individual_to_preempt)
        individual_to_preempt.service_start_date = False
        individual_to_preempt.time_left = (
            individual_to_preempt.service_end_date - self.now
        )
        individual_to_preempt.service_time = self.priority_preempt
        individual_to_preempt.service_end_date = False
        self.detatch_server(server, individual_to_preempt)
        self.decide_class_change(individual_to_preempt)
    self.attach_server(server, next_individual)
    next_individual.service_start_date = self.now
    next_individual.service_time = self.get_service_time(next_individual)
    next_individual.service_end_date = self.now + next_individual.service_time
    self.reset_class_change(next_individual)
    server.next_end_service_date = next_individual.service_end_date

release

release(next_individual, next_node, reroute=False)

Release an individual from the node.

Removes the individual from the queue, records relevant information, detaches the server, updates the state tracker, begins service of any waiting customers, sends the individual to their next destination, and releases any individuals blocked by this node.

Parameters:

Name Type Description Default
next_individual Individual

The individual to release.

required
next_node Node

The node receiving the individual.

required
reroute bool

Whether the release is due to rerouting.

False
Source code in hciw/node.py
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
def release(self, next_individual, next_node, reroute=False):
    """Release an individual from the node.

    Removes the individual from the queue, records relevant information,
    detaches the server, updates the state tracker, begins service of any
    waiting customers, sends the individual to their next destination, and
    releases any individuals blocked by this node.

    Args:
        next_individual (Individual): The individual to release.
        next_node (Node): The node receiving the individual.
        reroute (bool): Whether the release is due to rerouting.
    """
    self.individuals[next_individual.prev_priority_class].remove(next_individual)
    self.number_of_individuals -= 1
    self.number_in_service -= 1
    next_individual.queue_size_at_departure = self.number_of_individuals
    next_individual.exit_date = self.now
    if not reroute:
        self.write_individual_record(next_individual)
    newly_free_server = None
    if not isinf(self.c) and not self.slotted:
        newly_free_server = next_individual.server
        self.detatch_server(newly_free_server, next_individual)
    if self.slotted:
        next_individual.server = False
    self.reset_individual_attributes(next_individual)
    self.simulation.statetracker.change_state_release(
        self, next_node, next_individual, next_individual.is_blocked
    )
    if not reroute:
        self.begin_service_if_possible_release(next_individual, newly_free_server)
    next_node.accept(next_individual)
    if not reroute:
        self.release_blocked_individual()

release_blocked_individual

release_blocked_individual()

Release an individual blocked by this node.

Finds the individual who has been blocked the longest, removes them from the blocked queue, restores their service if it was interrupted, and releases them from their node.

Source code in hciw/node.py
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
def release_blocked_individual(self):
    """Release an individual blocked by this node.

    Finds the individual who has been blocked the longest, removes them
    from the blocked queue, restores their service if it was interrupted,
    and releases them from their node.
    """
    if (self.len_blocked_queue > 0) and (
        self.number_of_individuals < self.node_capacity
    ):
        node_to_receive_from = self.simulation.nodes[self.blocked_queue[0][0]]
        individual_to_receive_index = [
            ind.id_number for ind in node_to_receive_from.all_individuals
        ].index(self.blocked_queue[0][1])
        individual_to_receive = node_to_receive_from.all_individuals[
            individual_to_receive_index
        ]
        self.blocked_queue.pop(0)
        self.len_blocked_queue -= 1
        if individual_to_receive.interrupted:
            individual_to_receive.interrupted = False
            individual_to_receive.service_start_date = (
                individual_to_receive.original_service_start_date
            )
            individual_to_receive.service_end_date = (
                individual_to_receive.service_start_date
                + individual_to_receive.original_service_time
            )
            node_to_receive_from.interrupted_individuals.remove(
                individual_to_receive
            )
            node_to_receive_from.number_interrupted_individuals -= 1
        node_to_receive_from.release(individual_to_receive, self)

reset_class_change

reset_class_change(individual)

Reset an individual's class change date when service begins.

Parameters:

Name Type Description Default
individual Individual

The individual beginning service.

required
Source code in hciw/node.py
786
787
788
789
790
791
792
793
794
795
def reset_class_change(self, individual):
    """Reset an individual's class change date when service begins.

    Args:
        individual (Individual): The individual beginning service.
    """
    if self.dynamic_classes:
        individual.class_change_date = float("inf")
        if individual == self.next_class_change_ind:
            self.find_next_class_change()

renege

renege()

Remove an individual from the queue due to reneging.

Resets the individual's reneging date and sends the individual to their reneging destination.

Source code in hciw/node.py
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
def renege(self):
    """Remove an individual from the queue due to reneging.

    Resets the individual's reneging date and sends the individual to
    their reneging destination.
    """
    reneging_individual = self.decide_between_simultaneous_individuals()
    reneging_individual.reneging_date = float("Inf")
    next_node = self.next_node_for_jockeying(reneging_individual)
    self.individuals[reneging_individual.prev_priority_class].remove(
        reneging_individual
    )
    self.number_of_individuals -= 1
    reneging_individual.queue_size_at_departure = self.number_of_individuals
    reneging_individual.exit_date = self.now
    self.write_reneging_record(reneging_individual)
    self.reset_individual_attributes(reneging_individual)
    self.simulation.statetracker.change_state_renege(
        self, next_node, reneging_individual, blocked=False
    )
    next_node.accept(reneging_individual, completed=False)
    self.release_blocked_individual()

get_reneging_date

get_reneging_date(ind)

Return the reneging date for a given individual.

Parameters:

Name Type Description Default
ind Individual

The individual.

required

Returns:

Type Description
float

The reneging date.

Source code in hciw/node.py
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
def get_reneging_date(self, ind):
    """Return the reneging date for a given individual.

    Args:
        ind (Individual): The individual.

    Returns:
        float: The reneging date.
    """
    dist = self.simulation.network.customer_classes[
        ind.customer_class
    ].reneging_time_distributions[self.id_number - 1]
    if dist is None:
        return float("inf")
    return self.now + dist.sample(t=self.now, ind=ind)

get_service_time

get_service_time(ind)

Return a service time for the given customer class.

Parameters:

Name Type Description Default
ind Individual

The individual being served.

required

Returns:

Type Description
float

The sampled service time.

Source code in hciw/node.py
836
837
838
839
840
841
842
843
844
845
846
847
def get_service_time(self, ind):
    """Return a service time for the given customer class.

    Args:
        ind (Individual): The individual being served.

    Returns:
        float: The sampled service time.
    """
    return self.simulation.service_times[self.id_number][ind.customer_class].sample(
        t=self.now, ind=ind
    )

take_servers_off_duty

take_servers_off_duty(preemption: bool | str = False)

Gather the servers that should be deleted.

Parameters:

Name Type Description Default
preemption bool

Whether preemption is active for this shift change.

False
Source code in hciw/node.py
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
def take_servers_off_duty(self, preemption: bool | str = False):
    """Gather the servers that should be deleted.

    Args:
        preemption (bool): Whether preemption is active for this shift
            change.
    """
    if not preemption:
        to_delete = []
        for srvr in self.servers:
            srvr.shift_end = self.next_event_date
            if srvr.busy:
                srvr.offduty = True
            else:
                to_delete.append(srvr)
    else:
        to_delete = self.servers[::1]  # copy
        for s in self.servers:
            s.shift_end = self.next_event_date
            if s.cust is not False:
                self.interrupt_service(s.cust)
        self.sort_interrupted_individuals()
    for obs in to_delete:
        self.kill_server(obs)

interrupt_service

interrupt_service(individual)

Interrupt an individual's service.

Places the individual in the interrupted queue and writes an interruption record for them.

Parameters:

Name Type Description Default
individual Individual

The individual being interrupted.

required
Source code in hciw/node.py
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
def interrupt_service(self, individual):
    """Interrupt an individual's service.

    Places the individual in the interrupted queue and writes an
    interruption record for them.

    Args:
        individual (Individual): The individual being interrupted.
    """
    individual.original_service_time = individual.service_time
    assert self.schedule is not None
    if self.schedule.preemption == "reroute":
        self.reroute(individual)
    else:
        self.interrupted_individuals.append(individual)
        individual.interrupted = True
        self.number_interrupted_individuals += 1
        self.write_interruption_record(individual)
        individual.original_service_start_date = individual.service_start_date
        individual.service_start_date = False
        individual.time_left = individual.service_end_date - self.now
        individual.service_time = self.schedule.preemption
        individual.service_end_date = False
        self.number_in_service -= 1

sort_interrupted_individuals

sort_interrupted_individuals()

Sort interrupted individuals by priority class and arrival date.

Source code in hciw/node.py
899
900
901
902
903
def sort_interrupted_individuals(self):
    """Sort interrupted individuals by priority class and arrival date."""
    self.interrupted_individuals.sort(
        key=lambda x: (x.priority_class, x.arrival_date)
    )

reroute

reroute(individual)

Reroute a preempted individual.

Parameters:

Name Type Description Default
individual Individual

The individual being rerouted.

required
Source code in hciw/node.py
905
906
907
908
909
910
911
912
913
def reroute(self, individual):
    """Reroute a preempted individual.

    Args:
        individual (Individual): The individual being rerouted.
    """
    next_node = self.next_node_for_rerouting(individual)
    self.write_interruption_record(individual, destination=next_node.id_number)
    self.release(individual, next_node, reroute=True)

update_next_end_service_without_server

update_next_end_service_without_server()

Update the next end of a slotted service in possible_next_events.

Source code in hciw/node.py
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
def update_next_end_service_without_server(self):
    """Update the next end of a slotted service in `possible_next_events`."""
    if self.slotted or isinf(self.c):
        next_end_service_date = float("Inf")
        for ind in self.all_individuals:
            if not ind.is_blocked and ind.service_end_date >= self.now:
                if ind.service_end_date < next_end_service_date:
                    self.possible_next_events["end_service"] = (
                        [ind],
                        ind.service_end_date,
                    )
                    next_end_service_date = ind.service_end_date
                elif (ind.service_end_date == next_end_service_date) and (
                    not isinf(next_end_service_date)
                ):
                    self.possible_next_events["end_service"][0].append(ind)

update_next_end_service_with_server

update_next_end_service_with_server()

Update the next end service with a server in possible_next_events.

Source code in hciw/node.py
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
def update_next_end_service_with_server(self):
    """Update the next end service with a server in `possible_next_events`."""
    if not self.slotted and not isinf(self.c):
        next_end_service_date = float("Inf")
        for s in self.servers:
            if s.next_end_service_date < next_end_service_date:
                self.possible_next_events["end_service"] = (
                    [s.cust],
                    s.next_end_service_date,
                )
                next_end_service_date = s.next_end_service_date
            elif (s.next_end_service_date == next_end_service_date) and (
                not isinf(next_end_service_date)
            ):
                self.possible_next_events["end_service"][0].append(s.cust)

update_next_renege_time

update_next_renege_time()

Update the next renege time in possible_next_events.

Source code in hciw/node.py
948
949
950
951
952
953
954
955
956
957
958
959
960
961
def update_next_renege_time(self):
    """Update the next renege time in `possible_next_events`."""
    if not isinf(self.c) and self.reneging is True:
        next_renege_date = float("Inf")
        for ind in self.all_individuals:
            if (ind.reneging_date < next_renege_date) and not ind.server:
                self.possible_next_events["renege"] = ([ind], ind.reneging_date)
                next_renege_date = ind.reneging_date
            elif (
                (ind.reneging_date == next_renege_date)
                and (not ind.server)
                and (not isinf(next_renege_date))
            ):
                self.possible_next_events["renege"][0].append(ind)

update_next_class_change_while_waiting

update_next_class_change_while_waiting()

Update the next class change while waiting in possible_next_events.

Source code in hciw/node.py
963
964
965
966
967
968
969
def update_next_class_change_while_waiting(self):
    """Update the next class change while waiting in `possible_next_events`."""
    if self.dynamic_classes is True and not isinf(self.c):
        self.possible_next_events["class_change"] = (
            self.next_class_change_ind,
            self.next_class_change_date,
        )

update_next_shift_change_or_slot_time

update_next_shift_change_or_slot_time()

Update possible_next_events with the next shift or slot time.

Source code in hciw/node.py
971
972
973
974
975
976
977
978
979
980
981
982
983
def update_next_shift_change_or_slot_time(self):
    """Update `possible_next_events` with the next shift or slot time."""
    if self.schedule is not None:
        if self.schedule.schedule_type == "schedule":
            self.possible_next_events["shift_change"] = (
                None,
                self.next_shift_change,
            )
        if isinstance(self.schedule, Slotted):
            self.possible_next_events["slotted_service"] = (
                None,
                self.schedule.next_slot_date,
            )

update_next_event_date

update_next_event_date()

Find the time of the next event at this node.

With infinite servers this is the time of the next individual to end service, or infinity. Otherwise it is the minimum of the next shift change and the time for the next individual (who is not blocked) to end service, or infinity.

Source code in hciw/node.py
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
def update_next_event_date(self):
    """Find the time of the next event at this node.

    With infinite servers this is the time of the next individual to end
    service, or infinity. Otherwise it is the minimum of the next shift
    change and the time for the next individual (who is not blocked) to
    end service, or infinity.
    """
    self.possible_next_events = {}
    self.update_next_end_service_without_server()
    self.update_next_end_service_with_server()
    self.update_next_renege_time()
    self.update_next_class_change_while_waiting()
    self.update_next_shift_change_or_slot_time()
    if self.reneging or self.dynamic_classes or self.schedule:
        next_event, self.next_event_type = self.decide_next_event()
        self.next_event_date = next_event[1]
        self.next_individual = next_event[0]
    else:
        next_end_service = self.possible_next_events.get(
            "end_service", (None, float("inf"))
        )
        self.next_event_date = next_end_service[1]
        self.next_individual = next_end_service[0]
        self.next_event_type = "end_service"

decide_next_event

decide_next_event()

Decide the next event.

Chooses the next service end, renege, shift change, or class change. In the case of a tie, events are prioritised in the following order:

  1. slotted service
  2. shift change
  3. end service
  4. class change
  5. renege

Returns:

Type Description
tuple

The next event and its event type.

Source code in hciw/node.py
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
def decide_next_event(self):
    """Decide the next event.

    Chooses the next service end, renege, shift change, or class change.
    In the case of a tie, events are prioritised in the following order:

    1. slotted service
    2. shift change
    3. end service
    4. class change
    5. renege

    Returns:
        tuple: The next event and its event type.
    """
    next_date = float("inf")
    next_event = (None, float("inf"))
    next_event_type = None
    for event_type in [
        "slotted_service",
        "shift_change",
        "end_service",
        "class_change",
        "renege",
    ]:
        possible_next_event = self.possible_next_events.get(
            event_type, (None, float("inf"))
        )
        if possible_next_event[1] < next_date:
            next_event = possible_next_event
            next_event_type = event_type
            next_date = next_event[1]
    return next_event, next_event_type

wrap_up_servers

wrap_up_servers(current_time)

Update server total and busy times at the end of the run.

Parameters:

Name Type Description Default
current_time float

The simulation end time.

required
Source code in hciw/node.py
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
def wrap_up_servers(self, current_time):
    """Update server total and busy times at the end of the run.

    Args:
        current_time (float): The simulation end time.
    """
    if not isinf(self.c):
        for srvr in self.servers:
            srvr.total_time = self.increment_time(current_time, -srvr.start_date)
            if srvr.busy:
                srvr.busy_time += self.increment_time(
                    current_time, -srvr.cust.service_start_date
                )

write_individual_record

write_individual_record(individual)

Write a data record for an individual leaving a node.

Parameters:

Name Type Description Default
individual Individual

The individual leaving the node.

required
Source code in hciw/node.py
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
def write_individual_record(self, individual):
    """Write a data record for an individual leaving a node.

    Args:
        individual (Individual): The individual leaving the node.
    """
    if isinf(self.c) or self.slotted:
        server_id = False
    else:
        server_id = individual.server.id_number

    record = DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=individual.arrival_date,
        waiting_time=individual.service_start_date - individual.arrival_date,
        service_start_date=individual.service_start_date,
        service_time=individual.service_end_date - individual.service_start_date,
        service_end_date=individual.service_end_date,
        time_blocked=individual.exit_date - individual.service_end_date,
        exit_date=individual.exit_date,
        destination=individual.destination,
        queue_size_at_arrival=individual.queue_size_at_arrival,
        queue_size_at_departure=individual.queue_size_at_departure,
        server_id=server_id,
        record_type="service",
    )
    individual.data_records.append(record)

write_incomplete_record

write_incomplete_record(individual)

Write an incomplete data record at the end of the run.

Parameters:

Name Type Description Default
individual Individual

The individual still at the node.

required

Returns:

Type Description
DataRecord

The incomplete data record.

Source code in hciw/node.py
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
def write_incomplete_record(self, individual):
    """Write an incomplete data record at the end of the run.

    Args:
        individual (Individual): The individual still at the node.

    Returns:
        DataRecord: The incomplete data record.
    """
    if not individual.service_time:  # Still in queue
        service_start_date = None
        waiting_time = None
        service_time = None
        service_end_date = None
    else:
        service_start_date = individual.service_start_date
        waiting_time = individual.service_start_date - individual.arrival_date
        if individual.service_end_date > self.now:  # Still in service
            service_time = None
            service_end_date = None
        else:  # Still blocked
            service_time = individual.service_time
            service_end_date = individual.service_end_date

    return DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=individual.arrival_date,
        waiting_time=waiting_time,
        service_start_date=service_start_date,
        service_time=service_time,
        service_end_date=service_end_date,
        time_blocked=None,
        exit_date=None,
        destination=None,
        queue_size_at_arrival=individual.queue_size_at_arrival,
        queue_size_at_departure=None,
        server_id=False,
        record_type="incomplete",
    )

write_interruption_record

write_interruption_record(individual, destination=nan)

Write a data record for an individual being interrupted.

Parameters:

Name Type Description Default
individual Individual

The interrupted individual.

required
destination int

The destination node number.

nan
Source code in hciw/node.py
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
def write_interruption_record(self, individual, destination=nan):
    """Write a data record for an individual being interrupted.

    Args:
        individual (Individual): The interrupted individual.
        destination (int): The destination node number.
    """
    server_id = False if self.slotted else individual.server.id_number

    record = DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=individual.arrival_date,
        waiting_time=individual.service_start_date - individual.arrival_date,
        service_start_date=individual.service_start_date,
        service_time=individual.original_service_time,
        service_end_date=nan,
        time_blocked=nan,
        exit_date=self.now,
        destination=destination,
        queue_size_at_arrival=individual.queue_size_at_arrival,
        queue_size_at_departure=individual.queue_size_at_departure,
        server_id=server_id,
        record_type="interrupted service",
    )
    individual.data_records.append(record)

write_reneging_record

write_reneging_record(individual)

Write a data record for an individual when reneging.

Parameters:

Name Type Description Default
individual Individual

The reneging individual.

required
Source code in hciw/node.py
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
def write_reneging_record(self, individual):
    """Write a data record for an individual when reneging.

    Args:
        individual (Individual): The reneging individual.
    """
    record = DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=individual.arrival_date,
        waiting_time=individual.exit_date - individual.arrival_date,
        service_start_date=nan,
        service_time=nan,
        service_end_date=nan,
        time_blocked=nan,
        exit_date=individual.exit_date,
        destination=individual.destination,
        queue_size_at_arrival=individual.queue_size_at_arrival,
        queue_size_at_departure=individual.queue_size_at_departure,
        server_id=nan,
        record_type="renege",
    )
    individual.data_records.append(record)

write_baulking_or_rejection_record

write_baulking_or_rejection_record(individual, record_type)

Write a data record for an individual who baulks or is rejected.

Parameters:

Name Type Description Default
individual Individual

The individual.

required
record_type str

The type of record to write.

required
Source code in hciw/node.py
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
def write_baulking_or_rejection_record(self, individual, record_type):
    """Write a data record for an individual who baulks or is rejected.

    Args:
        individual (Individual): The individual.
        record_type (str): The type of record to write.
    """
    record = DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=self.now,
        waiting_time=nan,
        service_start_date=nan,
        service_time=nan,
        service_end_date=nan,
        time_blocked=nan,
        exit_date=self.now,
        destination=nan,
        queue_size_at_arrival=self.number_of_individuals,
        queue_size_at_departure=nan,
        server_id=nan,
        record_type=record_type,
    )
    individual.data_records.append(record)

reset_individual_attributes

reset_individual_attributes(individual)

Reset the attributes of an individual.

Parameters:

Name Type Description Default
individual Individual

The individual to reset.

required
Source code in hciw/node.py
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
def reset_individual_attributes(self, individual):
    """Reset the attributes of an individual.

    Args:
        individual (Individual): The individual to reset.
    """
    individual.arrival_date = False
    individual.service_time = False
    individual.service_start_date = False
    individual.service_end_date = False
    individual.exit_date = False
    individual.queue_size_at_arrival = False
    individual.queue_size_at_departure = False
    individual.destination = False

Server

Server(node, id_number, start_date: float | Decimal = 0.0)

Contain information about a server.

Initialise the server.

Parameters:

Name Type Description Default
node Node

The node the server belongs to.

required
id_number int

Identifier for the server.

required
start_date float | Decimal

The time at which the server started, and whose type sets the precision of the server's accumulated times.

0.0
Source code in hciw/server.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def __init__(self, node, id_number, start_date: float | Decimal = 0.0):
    """Initialise the server.

    Args:
        node (Node): The node the server belongs to.
        id_number (int): Identifier for the server.
        start_date (float | Decimal): The time at which the server started,
            and whose type sets the precision of the server's accumulated
            times.
    """
    self.node = node
    self.id_number = id_number
    self.cust = False
    self.busy = False
    self.offduty = False
    self.all_time = False
    self.start_date = start_date
    zero = type(start_date)()
    self.busy_time = zero
    self.total_time = zero
    self.shift_end = False
    self.next_end_service_date = float("Inf")

utilisation property

utilisation

Get the server utilisation.

Returns:

Type Description
float | Decimal

The proportion of total time the server was busy.

Simulation

Simulation(
    network,
    exact=False,
    name="Simulation",
    tracker=None,
    deadlock_detector=None,
    node_class=None,
    arrival_node_class=None,
    exit_node_class=None,
    individual_class=None,
    server_class=None,
    initial_individuals=None,
)

The engine that runs a queueing network simulation.

A Simulation drives customers through the network's nodes over simulated time, collecting data records and tracking state.

Attributes:

Name Type Description
current_time float

The current simulated time.

network Network

The network being simulated.

name str

The name of the simulation.

NodeTypes list

The node classes used at each service centre.

ArrivalNodeType type

The class used for the arrival node.

ExitNodeType type

The class used for the exit node.

IndividualType type

The class used for individuals.

ServerType type

The class used for servers.

deadlock_detector object

The detector used to identify deadlocks.

inter_arrival_times dict

Inter-arrival distributions by node and class.

service_times dict

Service distributions by node and class.

batch_sizes dict

Batch size distributions by node and class.

number_of_priority_classes int

The number of priority classes.

transitive_nodes list

The service centre nodes.

nodes list

The arrival, transitive and exit nodes.

active_nodes list

The nodes that are not the exit node.

routers dict

The routing objects by customer class.

statetracker StateTracker

The state tracker in use.

times_dictionary dict

Maps hashed states to the time first seen.

times_to_deadlock dict

The time from each state to deadlock.

unchecked_blockage bool

Whether a blockage needs checking.

Initialise a simulation instance.

Parameters:

Name Type Description Default
network Network

The network to simulate.

required
exact int

If truthy, use exact Decimal arithmetic with this many digits of precision. Defaults to False.

False
name str

The name of the simulation. Defaults to "Simulation".

'Simulation'
tracker StateTracker

The state tracker to use. Defaults to a plain StateTracker.

None
deadlock_detector object

The deadlock detector to use. Defaults to deadlock.NoDetection().

None
node_class type or list

The node class or classes to use. Defaults to Node.

None
arrival_node_class type

The arrival node class to use. Defaults to ArrivalNode.

None
exit_node_class type

The exit node class to use. Defaults to ExitNode.

None
individual_class type

The individual class to use. Defaults to Individual.

None
server_class type

The server class to use. Defaults to Server.

None
initial_individuals list[list]

A list of backlogged individuals to seed the simulation with, in the form [[id, customer_class, arrival_date, node_index], ...] where arrival_date is the individual's historical arrival date (typically negative, meaning it arrived before the simulation began). If provided, these individuals are placed at their specified nodes before the simulation begins. Defaults to None.

None
Source code in hciw/simulation.py
 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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def __init__(  # noqa: PLR0913, PLR0917 - public configuration API
    self,
    network,
    exact=False,
    name="Simulation",
    tracker=None,
    deadlock_detector=None,
    node_class=None,
    arrival_node_class=None,
    exit_node_class=None,
    individual_class=None,
    server_class=None,
    initial_individuals=None,
):
    """Initialise a simulation instance.

    Args:
        network (Network): The network to simulate.
        exact (int, optional): If truthy, use exact ``Decimal`` arithmetic
            with this many digits of precision. Defaults to ``False``.
        name (str, optional): The name of the simulation. Defaults to
            ``"Simulation"``.
        tracker (StateTracker, optional): The state tracker to use.
            Defaults to a plain ``StateTracker``.
        deadlock_detector (object, optional): The deadlock detector to
            use. Defaults to ``deadlock.NoDetection()``.
        node_class (type or list, optional): The node class or classes to
            use. Defaults to ``Node``.
        arrival_node_class (type, optional): The arrival node class to use.
            Defaults to ``ArrivalNode``.
        exit_node_class (type, optional): The exit node class to use.
            Defaults to ``ExitNode``.
        individual_class (type, optional): The individual class to use.
            Defaults to ``Individual``.
        server_class (type, optional): The server class to use. Defaults
            to ``Server``.
        initial_individuals (list[list], optional): A list of backlogged
            individuals to seed the simulation with, in the form
            ``[[id, customer_class, arrival_date, node_index], ...]`` where
            ``arrival_date`` is the individual's historical arrival date
            (typically negative, meaning it arrived before the simulation
            began). If provided, these individuals are placed at their
            specified nodes before the simulation begins. Defaults to
            ``None``.
    """
    self.current_time = 0.0
    self.network = network
    self.set_classes(
        node_class,
        arrival_node_class,
        exit_node_class,
        individual_class,
        server_class,
    )
    if exact:
        self.NodeTypes = [ExactNode for _ in range(network.number_of_nodes)]
        self.ArrivalNodeType = ExactArrivalNode
        getcontext().prec = exact

    self.name = name
    self.deadlock_detector = (
        deadlock.NoDetection() if deadlock_detector is None else deadlock_detector
    )
    self.inter_arrival_times = self.find_arrival_dists()
    self.service_times = self.find_service_dists()
    self.batch_sizes = self.find_batching_dists()
    self.show_simulation_to_distributions()
    self.number_of_priority_classes = self.network.number_of_priority_classes
    self.transitive_nodes = [
        node_type(i + 1, self) for i, node_type in enumerate(self.NodeTypes)
    ]
    self.exit_node = self.ExitNodeType()
    self.nodes = [
        self.ArrivalNodeType(self),
        *self.transitive_nodes,
        self.exit_node,
    ]
    self.active_nodes = self.nodes[:-1]
    self.routers = self.find_and_initialise_routers()
    self.nodes[0].initialise()
    if tracker is None:
        self.statetracker = trackers.StateTracker()
    else:
        self.statetracker = tracker
    self.statetracker.initialise(self)
    if initial_individuals is not None:
        waitlist.create_existing_customers_from_list(initial_individuals, self)
    self.times_dictionary = {self.statetracker.hash_state(): 0.0}
    self.times_to_deadlock = {}
    self.unchecked_blockage = False

number_of_individuals property

number_of_individuals

Return the number of individuals currently in the system.

Returns:

Type Description
int

The number of individuals currently in the system.

find_arrival_dists

find_arrival_dists()

Create the arrival time distribution objects for each node and class.

Returns:

Type Description
dict

Arrival time distributions keyed by node and customer class.

Source code in hciw/simulation.py
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def find_arrival_dists(self):
    """Create the arrival time distribution objects for each node and class.

    Returns:
        dict: Arrival time distributions keyed by node and customer class.
    """
    return {
        node + 1: {
            clss: copy.deepcopy(
                self.network.customer_classes[clss].arrival_distributions[node]
            )
            for clss in self.network.customer_class_names
        }
        for node in range(self.network.number_of_nodes)
    }

find_service_dists

find_service_dists()

Create the service time distribution objects for each node and class.

Returns:

Type Description
dict

Service time distributions keyed by node and customer class.

Source code in hciw/simulation.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def find_service_dists(self):
    """Create the service time distribution objects for each node and class.

    Returns:
        dict: Service time distributions keyed by node and customer class.
    """
    return {
        node + 1: {
            clss: copy.deepcopy(
                self.network.customer_classes[clss].service_distributions[node]
            )
            for clss in self.network.customer_class_names
        }
        for node in range(self.network.number_of_nodes)
    }

find_batching_dists

find_batching_dists()

Create the batch size distribution objects for each node and class.

Returns:

Type Description
dict

Batch size distributions keyed by node and customer class.

Source code in hciw/simulation.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def find_batching_dists(self):
    """Create the batch size distribution objects for each node and class.

    Returns:
        dict: Batch size distributions keyed by node and customer class.
    """
    return {
        node + 1: {
            clss: copy.deepcopy(
                self.network.customer_classes[clss].batching_distributions[node]
            )
            for clss in self.network.customer_class_names
        }
        for node in range(self.network.number_of_nodes)
    }

show_simulation_to_distributions

show_simulation_to_distributions()

Add the simulation as an attribute of the distribution objects.

Source code in hciw/simulation.py
208
209
210
211
212
213
214
215
def show_simulation_to_distributions(self):
    """Add the simulation as an attribute of the distribution objects."""
    for clss in self.network.customer_class_names:
        for nd in range(self.network.number_of_nodes):
            if self.inter_arrival_times[nd + 1][clss] is not None:
                self.inter_arrival_times[nd + 1][clss].simulation = self
                self.service_times[nd + 1][clss].simulation = self
                self.batch_sizes[nd + 1][clss].simulation = self

find_and_initialise_routers

find_and_initialise_routers()

Initialise and return the routing objects.

Returns:

Type Description
dict

Routing objects keyed by customer class.

Source code in hciw/simulation.py
217
218
219
220
221
222
223
224
225
226
227
def find_and_initialise_routers(self):
    """Initialise and return the routing objects.

    Returns:
        dict: Routing objects keyed by customer class.
    """
    routers_dict = {}
    for clss in self.network.customer_class_names:
        routers_dict[clss] = self.network.customer_classes[clss].routing
        routers_dict[clss].initialise(self)
    return routers_dict

find_next_active_node

find_next_active_node()

Return the active node with the earliest next event date.

If several nodes share the earliest date, one is chosen at random.

Returns:

Type Description
Node

The next active node.

Source code in hciw/simulation.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
def find_next_active_node(self):
    """Return the active node with the earliest next event date.

    If several nodes share the earliest date, one is chosen at random.

    Returns:
        Node: The next active node.
    """
    mindate = float("Inf")
    next_active_nodes = []
    for nd in self.active_nodes:
        if nd.next_event_date < mindate:
            mindate = nd.next_event_date
            next_active_nodes = [nd]
        elif nd.next_event_date == mindate:
            next_active_nodes.append(nd)
    if len(next_active_nodes) > 1:
        return random_choice(next_active_nodes)
    return next_active_nodes[0]

get_all_individuals

get_all_individuals()

Return all individuals with at least one data record.

Returns:

Type Description
list

The individuals with at least one data record.

Source code in hciw/simulation.py
249
250
251
252
253
254
255
256
257
258
259
def get_all_individuals(self):
    """Return all individuals with at least one data record.

    Returns:
        list: The individuals with at least one data record.
    """
    return [
        individual
        for node in [*self.transitive_nodes, self.exit_node]
        for individual in node.all_individuals
    ]

get_all_records

get_all_records(only=None, include_incomplete=False)

Get data records from all individuals.

Parameters:

Name Type Description Default
only list

The record types to include. Defaults to service, baulk, rejection, renege and interrupted service records.

None
include_incomplete bool

Whether to include records for individuals still in the system. Defaults to False.

False

Returns:

Type Description
list

The collected data records.

Source code in hciw/simulation.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
def get_all_records(
    self,
    only=None,
    include_incomplete=False,
):
    """Get data records from all individuals.

    Args:
        only (list, optional): The record types to include. Defaults to
            service, baulk, rejection, renege and interrupted service
            records.
        include_incomplete (bool, optional): Whether to include records
            for individuals still in the system. Defaults to ``False``.

    Returns:
        list: The collected data records.
    """
    if only is None:
        only = ["service", "baulk", "rejection", "renege", "interrupted service"]
    records = []
    for individual in self.get_all_individuals():
        records.extend(
            record
            for record in individual.data_records
            if record.record_type in only
        )
        if include_incomplete and individual.node != -1:
            incomplete_record = self.nodes[individual.node].write_incomplete_record(
                individual
            )
            records.append(incomplete_record)
    self.all_records = records
    return records

set_classes

set_classes(
    node_class,
    arrival_node_class,
    exit_node_class,
    individual_class,
    server_class,
)

Set the node, individual and server classes used by the simulation.

Parameters:

Name Type Description Default
node_class type or list

The node class or classes to use. If a list, it must have one entry per node.

required
arrival_node_class type

The arrival node class to use, or None for the default.

required
exit_node_class type

The exit node class to use, or None for the default.

required
individual_class type

The individual class to use, or None for the default.

required
server_class type

The server class to use, or None for the default.

required

Raises:

Type Description
ValueError

If node_class is a list whose length does not match the number of nodes.

Source code in hciw/simulation.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
def set_classes(  # noqa: PLR0912 - per-class selection branch chain
    self,
    node_class,
    arrival_node_class,
    exit_node_class,
    individual_class,
    server_class,
):
    """Set the node, individual and server classes used by the simulation.

    Args:
        node_class (type or list): The node class or classes to use. If a
            list, it must have one entry per node.
        arrival_node_class (type): The arrival node class to use, or
            ``None`` for the default.
        exit_node_class (type): The exit node class to use, or ``None``
            for the default.
        individual_class (type): The individual class to use, or ``None``
            for the default.
        server_class (type): The server class to use, or ``None`` for the
            default.

    Raises:
        ValueError: If ``node_class`` is a list whose length does not match
            the number of nodes.
    """
    if arrival_node_class is not None:
        self.ArrivalNodeType = arrival_node_class
    else:
        self.ArrivalNodeType = ArrivalNode

    if exit_node_class is not None:
        self.ExitNodeType = exit_node_class
    else:
        self.ExitNodeType = ExitNode

    if node_class is not None:
        if not isinstance(node_class, list):
            self.NodeTypes = [
                node_class for _ in range(self.network.number_of_nodes)
            ]
        else:
            if len(node_class) != self.network.number_of_nodes:
                raise ValueError(
                    "Ensure consistant number of nodes is used throughout."
                )
            self.NodeTypes = node_class
    else:
        self.NodeTypes = [Node for _ in range(self.network.number_of_nodes)]

    if individual_class is not None:
        self.IndividualType = individual_class
    else:
        self.IndividualType = Individual

    if server_class is not None:
        self.ServerType = server_class
    else:
        self.ServerType = Server

event_and_return_nextnode

event_and_return_nextnode(next_active_node)

Carry out an event and return the next active node.

Parameters:

Name Type Description Default
next_active_node Node

The node whose event to carry out.

required

Returns:

Type Description
Node

The next active node.

Source code in hciw/simulation.py
355
356
357
358
359
360
361
362
363
364
365
366
367
def event_and_return_nextnode(self, next_active_node):
    """Carry out an event and return the next active node.

    Args:
        next_active_node (Node): The node whose event to carry out.

    Returns:
        Node: The next active node.
    """
    next_active_node.have_event()
    for node in self.transitive_nodes:
        node.update_next_event_date()
    return self.find_next_active_node()

simulate_until_deadlock

simulate_until_deadlock()

Run the simulation until deadlock is reached.

Source code in hciw/simulation.py
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
def simulate_until_deadlock(self):
    """Run the simulation until deadlock is reached."""
    deadlocked = False
    next_active_node = self.find_next_active_node()
    self.current_time = next_active_node.next_event_date
    time_of_deadlock = self.current_time
    while not deadlocked:
        next_active_node = self.event_and_return_nextnode(next_active_node)
        current_state = self.statetracker.hash_state()
        if current_state not in self.times_dictionary:
            self.times_dictionary[current_state] = self.current_time
        if self.unchecked_blockage:
            deadlocked = self.deadlock_detector.detect_deadlock()
            self.unchecked_blockage = False
        if deadlocked:
            time_of_deadlock = self.current_time
        self.current_time = next_active_node.next_event_date

    self.wrap_up_servers(time_of_deadlock)
    self.times_to_deadlock = {
        state: time_of_deadlock - self.times_dictionary[state]
        for state in self.times_dictionary
    }

simulate_until_max_time

simulate_until_max_time(
    max_simulation_time, progress_bar=False
)

Run the simulation until a maximum simulated time is reached.

Parameters:

Name Type Description Default
max_simulation_time float

The time to stop the simulation at.

required
progress_bar bool

Whether to display a progress bar. Defaults to False.

False
Source code in hciw/simulation.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
def simulate_until_max_time(self, max_simulation_time, progress_bar=False):
    """Run the simulation until a maximum simulated time is reached.

    Args:
        max_simulation_time (float): The time to stop the simulation at.
        progress_bar (bool, optional): Whether to display a progress bar.
            Defaults to ``False``.
    """
    next_active_node = self.find_next_active_node()
    self.current_time = next_active_node.next_event_date

    if progress_bar:
        self.progress_bar = tqdm.tqdm(total=max_simulation_time)

    while self.current_time < max_simulation_time:
        next_active_node = self.event_and_return_nextnode(next_active_node)
        self.statetracker.timestamp()

        if progress_bar:
            remaining_time = max_simulation_time - self.progress_bar.n
            time_increment = next_active_node.next_event_date - self.current_time
            self.progress_bar.update(min(time_increment, remaining_time))

        self.current_time = next_active_node.next_event_date
    self.current_time = max_simulation_time

    self.wrap_up_servers(max_simulation_time)
    if progress_bar:
        remaining_time = max(max_simulation_time - self.progress_bar.n, 0)
        self.progress_bar.update(remaining_time)
        self.progress_bar.close()

simulate_until_max_customers

simulate_until_max_customers(
    max_customers, progress_bar=False, method="Complete"
)

Run the simulation until a number of customers is reached.

Parameters:

Name Type Description Default
max_customers int

The number of customers to stop at.

required
progress_bar bool

Whether to display a progress bar. Defaults to False.

False
method str

The measure used to count customers. One of:

  • 'Complete': count customers that reach the Exit Node after completing their journey.
  • 'Finish': count customers that reach the Exit Node, whether or not they completed their journey (including baulkers and renegers).
  • 'Arrive': count customers spawned at the Arrival Node.
  • 'Accept': count customers spawned and accepted (not rejected) at the Arrival Node.

Defaults to 'Complete'.

'Complete'

Raises:

Type Description
ValueError

If method is not one of the valid options.

Source code in hciw/simulation.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
def simulate_until_max_customers(  # noqa: C901 - inherent event-loop branching
    self, max_customers, progress_bar=False, method="Complete"
):
    """Run the simulation until a number of customers is reached.

    Args:
        max_customers (int): The number of customers to stop at.
        progress_bar (bool, optional): Whether to display a progress bar.
            Defaults to ``False``.
        method (str, optional): The measure used to count customers. One
            of:

            - ``'Complete'``: count customers that reach the Exit Node
              after completing their journey.
            - ``'Finish'``: count customers that reach the Exit Node,
              whether or not they completed their journey (including
              baulkers and renegers).
            - ``'Arrive'``: count customers spawned at the Arrival Node.
            - ``'Accept'``: count customers spawned and accepted (not
              rejected) at the Arrival Node.

            Defaults to ``'Complete'``.

    Raises:
        ValueError: If ``method`` is not one of the valid options.
    """
    next_active_node = self.find_next_active_node()
    self.current_time = next_active_node.next_event_date

    if progress_bar:
        self.progress_bar = tqdm.tqdm(total=max_customers)

    if method == "Complete":

        def check():
            return self.exit_node.number_of_completed_individuals
    elif method == "Finish":

        def check():
            return self.exit_node.number_of_individuals
    elif method == "Arrive":

        def check():
            return self.nodes[0].number_of_individuals
    elif method == "Accept":

        def check():
            return self.nodes[0].number_accepted_individuals
    else:
        raise ValueError("Invalid 'method' for 'simulate_until_max_customers'.")

    previous_time = self.current_time
    while check() < max_customers:
        old_check = check()
        next_active_node = self.event_and_return_nextnode(next_active_node)
        self.statetracker.timestamp()

        if progress_bar:
            remaining_time = max_customers - self.progress_bar.n
            time_increment = check() - old_check
            self.progress_bar.update(min(time_increment, remaining_time))

        previous_time = self.current_time
        self.current_time = next_active_node.next_event_date
    self.current_time = previous_time

    self.wrap_up_servers(self.current_time)

    if progress_bar:
        remaining_time = max(max_customers - self.progress_bar.n, 0)
        self.progress_bar.update(remaining_time)
        self.progress_bar.close()

wrap_up_servers

wrap_up_servers(current_time)

Update server times and find the overall server utilisation.

Updates each server's total time and busy time at the end of the simulation run, and finds the overall server utilisation for each node.

Parameters:

Name Type Description Default
current_time float

The time at which the run finished.

required
Source code in hciw/simulation.py
498
499
500
501
502
503
504
505
506
507
508
509
def wrap_up_servers(self, current_time):
    """Update server times and find the overall server utilisation.

    Updates each server's total time and busy time at the end of the
    simulation run, and finds the overall server utilisation for each node.

    Args:
        current_time (float): The time at which the run finished.
    """
    for nd in self.transitive_nodes:
        nd.wrap_up_servers(current_time)
        nd.find_server_utilisation()

seed

seed(z)

Set all seeds used to generate random number streams.

Seeds both the standard library random module and the NumPy random generator used by hciw.

Parameters:

Name Type Description Default
z int

The seed value to use.

required
Source code in hciw/auxiliary.py
14
15
16
17
18
19
20
21
22
23
24
def seed(z):
    """Set all seeds used to generate random number streams.

    Seeds both the standard library ``random`` module and the NumPy
    random generator used by ``hciw``.

    Args:
        z (int): The seed value to use.
    """
    random.seed(z)
    hciw.rng = np.random.default_rng(seed=z)

random_choice

random_choice(array, probs=None)

Select a random element from an array using optional weights.

Parameters:

Name Type Description Default
array list

The values to choose from.

required
probs list

Probability weights corresponding to each value. If omitted, a uniform choice is made.

None

Returns:

Type Description

The randomly selected element of array.

Source code in hciw/auxiliary.py
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
def random_choice(array, probs=None):
    """Select a random element from an array using optional weights.

    Args:
        array (list): The values to choose from.
        probs (list, optional): Probability weights corresponding to each
            value. If omitted, a uniform choice is made.

    Returns:
        The randomly selected element of ``array``.
    """
    # If no pdf provided, assume uniform dist:
    if probs is None:
        index = int(random.random() * len(array))
        return array[index]

    # A common case, guaranteed to reach the Exit node;
    # No need to sample for this:
    if (set(probs[:-1]) == {0.0}) and (probs[-1] == 1.0):
        return array[-1]

    # Sample a random value from using pdf
    rdm_num = random.random()
    i, p = 0, probs[0]
    while rdm_num > p:
        i += 1
        p += probs[i]
    return array[i]

truncated_normal

truncated_normal(mean, sd)

Sample from a Normal distribution truncated at zero.

Values less than or equal to zero are resampled until a positive value is obtained.

Parameters:

Name Type Description Default
mean float

The mean of the Normal distribution.

required
sd float

The standard deviation of the Normal distribution.

required

Returns:

Type Description
float

A positive sample from the truncated Normal distribution.

Source code in hciw/auxiliary.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def truncated_normal(mean, sd):
    """Sample from a Normal distribution truncated at zero.

    Values less than or equal to zero are resampled until a positive
    value is obtained.

    Args:
        mean (float): The mean of the Normal distribution.
        sd (float): The standard deviation of the Normal distribution.

    Returns:
        float: A positive sample from the truncated Normal distribution.
    """
    sample = random.normalvariate(mean, sd)
    while sample <= 0.0:
        sample = random.normalvariate(mean, sd)
    return sample

flatten_list

flatten_list(list_of_lists)

Flatten a list of lists into a single list.

Parameters:

Name Type Description Default
list_of_lists list

A list whose elements are themselves lists.

required

Returns:

Type Description
list

A single list containing all elements of the inner lists.

Source code in hciw/auxiliary.py
76
77
78
79
80
81
82
83
84
85
86
87
88
def flatten_list(list_of_lists):
    """Flatten a list of lists into a single list.

    Args:
        list_of_lists (list): A list whose elements are themselves lists.

    Returns:
        list: A single list containing all elements of the inner lists.
    """
    flat = []
    for a_list in list_of_lists:
        flat += a_list
    return flat

no_routing

no_routing(ind)

Send a customer straight to the exit node.

This process-based routing function is a placeholder used when NoArrivals is used.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
list

An empty list of destinations.

Source code in hciw/auxiliary.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def no_routing(ind):
    """Send a customer straight to the exit node.

    This process-based routing function is a placeholder used when
    ``NoArrivals`` is used.

    Args:
        ind (Individual): The individual being routed.

    Returns:
        list: An empty list of destinations.
    """
    return []

network

Information stores describing the nodes and classes of a network.

ServiceCentre

ServiceCentre(
    number_of_servers,
    queueing_capacity,
    class_change_matrix=None,
    priority_preempt=False,
    ps_threshold=1,
    server_priority_function=None,
    service_discipline=None,
)

Store the class-independent information for a service centre.

Attributes:

Name Type Description
number_of_servers int or Schedule

The number of servers, or a schedule describing how it varies over time.

queueing_capacity int or float

The maximum queue length.

class_change_matrix dict

Matrix describing class changes, if any.

priority_preempt bool

Whether priority classes preempt service.

ps_threshold int

Threshold used by the processor-sharing discipline.

server_priority_function callable

Function assigning server priority.

service_discipline callable

The service discipline used at the node.

class_change_time bool

Whether class change times are modelled.

Initialise a ServiceCentre object.

Parameters:

Name Type Description Default
number_of_servers int or Schedule

The number of servers, or a schedule describing how it varies over time.

required
queueing_capacity int or float

The maximum queue length.

required
class_change_matrix dict

Matrix describing class changes, if any.

None
priority_preempt bool

Whether priority classes preempt service. Defaults to False.

False
ps_threshold int

Threshold used by the processor-sharing discipline. Defaults to 1.

1
server_priority_function callable

Function assigning server priority.

None
service_discipline callable

The service discipline used at the node.

None
Source code in hciw/network.py
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
def __init__(  # noqa: PLR0913, PLR0917 - public configuration API
    self,
    number_of_servers,
    queueing_capacity,
    class_change_matrix=None,
    priority_preempt=False,
    ps_threshold=1,
    server_priority_function=None,
    service_discipline=None,
):
    """Initialise a ``ServiceCentre`` object.

    Args:
        number_of_servers (int or Schedule): The number of servers, or a
            schedule describing how it varies over time.
        queueing_capacity (int or float): The maximum queue length.
        class_change_matrix (dict, optional): Matrix describing class
            changes, if any.
        priority_preempt (bool, optional): Whether priority classes
            preempt service. Defaults to ``False``.
        ps_threshold (int, optional): Threshold used by the
            processor-sharing discipline. Defaults to ``1``.
        server_priority_function (callable, optional): Function assigning
            server priority.
        service_discipline (callable, optional): The service discipline
            used at the node.
    """
    self.number_of_servers = number_of_servers
    self.queueing_capacity = queueing_capacity
    self.class_change_matrix = class_change_matrix
    self.priority_preempt = priority_preempt
    self.ps_threshold = ps_threshold
    self.server_priority_function = server_priority_function
    self.service_discipline = service_discipline
    self.class_change_time = False

CustomerClass

CustomerClass(
    arrival_distributions,
    service_distributions,
    routing,
    priority_class,
    baulking_functions,
    batching_distributions,
    reneging_time_distributions,
    class_change_time_distributions,
    benchmark=None,
)

Store the class-dependent information for a customer class.

Attributes:

Name Type Description
arrival_distributions dict

Arrival distributions for each node.

service_distributions dict

Service distributions for each node.

batching_distributions dict

Batching distributions for each node.

routing Routing

The routing object for the class.

priority_class int

The priority class of the customer class.

baulking_functions dict

Baulking functions for each node.

reneging_time_distributions dict

Reneging time distributions for each node.

class_change_time_distributions dict

Class change time distributions for each pair of customer classes.

benchmark float or None

The target maximum waiting time for this customer class, used by the benchmark-aware service disciplines.

Initialise a CustomerClass object.

Parameters:

Name Type Description Default
arrival_distributions dict

Arrival distributions for each node.

required
service_distributions dict

Service distributions for each node.

required
routing Routing

The routing object for the class.

required
priority_class int

The priority class of the customer class.

required
baulking_functions dict

Baulking functions for each node.

required
batching_distributions dict

Batching distributions for each node.

required
reneging_time_distributions dict

Reneging time distributions for each node.

required
class_change_time_distributions dict

Class change time distributions for each pair of customer classes.

required
benchmark float or None

The target maximum waiting time for this class. Defaults to None.

None
Source code in hciw/network.py
 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
def __init__(  # noqa: PLR0913, PLR0917 - public configuration API
    self,
    arrival_distributions,
    service_distributions,
    routing,
    priority_class,
    baulking_functions,
    batching_distributions,
    reneging_time_distributions,
    class_change_time_distributions,
    benchmark=None,
):
    """Initialise a ``CustomerClass`` object.

    Args:
        arrival_distributions (dict): Arrival distributions for each node.
        service_distributions (dict): Service distributions for each node.
        routing (Routing): The routing object for the class.
        priority_class (int): The priority class of the customer class.
        baulking_functions (dict): Baulking functions for each node.
        batching_distributions (dict): Batching distributions for each node.
        reneging_time_distributions (dict): Reneging time distributions for
            each node.
        class_change_time_distributions (dict): Class change time
            distributions for each pair of customer classes.
        benchmark (float or None, optional): The target maximum waiting
            time for this class. Defaults to ``None``.
    """
    self.arrival_distributions = arrival_distributions
    self.service_distributions = service_distributions
    self.batching_distributions = batching_distributions
    self.routing = routing
    self.priority_class = priority_class
    self.baulking_functions = baulking_functions
    self.reneging_time_distributions = reneging_time_distributions
    self.class_change_time_distributions = class_change_time_distributions
    self.benchmark = benchmark

Network

Network(service_centres, customer_classes)

Store the queueing network.

Holds a list of ServiceCentre objects for each service centre and a dictionary of CustomerClass objects keyed by class name.

Attributes:

Name Type Description
service_centres list

The service centres of the network.

customer_classes dict

The customer classes, keyed by class name.

number_of_nodes int

The number of service centres.

number_of_classes int

The number of customer classes.

customer_class_names list

The sorted names of the customer classes.

number_of_priority_classes int

The number of distinct priority classes.

priority_class_mapping dict

Maps class names to priority classes.

system_capacity float

The total capacity of the system.

Initialise a Network object.

Parameters:

Name Type Description Default
service_centres list

The service centres of the network.

required
customer_classes dict

The customer classes, keyed by class name.

required
Source code in hciw/network.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def __init__(self, service_centres, customer_classes):
    """Initialise a ``Network`` object.

    Args:
        service_centres (list): The service centres of the network.
        customer_classes (dict): The customer classes, keyed by class name.
    """
    self.service_centres = service_centres
    self.customer_classes = customer_classes
    self.number_of_nodes = len(service_centres)
    self.number_of_classes = len(customer_classes)
    self.customer_class_names = sorted(customer_classes.keys())
    self.number_of_priority_classes = len(
        {clss.priority_class for clss in customer_classes.values()}
    )
    self.priority_class_mapping = {
        clss: customer_classes[clss].priority_class for clss in customer_classes
    }
    for nd_id, node in enumerate(self.service_centres):
        if all(
            clss.reneging_time_distributions[nd_id] is None
            for clss in self.customer_classes.values()
        ):
            node.reneging = False
        else:
            node.reneging = True
    if any(
        dist is not None
        for clss in customer_classes.values()
        for dist in clss.class_change_time_distributions.values()
    ):
        for node in self.service_centres:
            node.class_change_time = True

auxiliary

Auxiliary helper functions used throughout the simulation.

This module provides seeding utilities, weighted random sampling, and small list helpers used by the simulation engine.

seed

seed(z)

Set all seeds used to generate random number streams.

Seeds both the standard library random module and the NumPy random generator used by hciw.

Parameters:

Name Type Description Default
z int

The seed value to use.

required
Source code in hciw/auxiliary.py
14
15
16
17
18
19
20
21
22
23
24
def seed(z):
    """Set all seeds used to generate random number streams.

    Seeds both the standard library ``random`` module and the NumPy
    random generator used by ``hciw``.

    Args:
        z (int): The seed value to use.
    """
    random.seed(z)
    hciw.rng = np.random.default_rng(seed=z)

random_choice

random_choice(array, probs=None)

Select a random element from an array using optional weights.

Parameters:

Name Type Description Default
array list

The values to choose from.

required
probs list

Probability weights corresponding to each value. If omitted, a uniform choice is made.

None

Returns:

Type Description

The randomly selected element of array.

Source code in hciw/auxiliary.py
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
def random_choice(array, probs=None):
    """Select a random element from an array using optional weights.

    Args:
        array (list): The values to choose from.
        probs (list, optional): Probability weights corresponding to each
            value. If omitted, a uniform choice is made.

    Returns:
        The randomly selected element of ``array``.
    """
    # If no pdf provided, assume uniform dist:
    if probs is None:
        index = int(random.random() * len(array))
        return array[index]

    # A common case, guaranteed to reach the Exit node;
    # No need to sample for this:
    if (set(probs[:-1]) == {0.0}) and (probs[-1] == 1.0):
        return array[-1]

    # Sample a random value from using pdf
    rdm_num = random.random()
    i, p = 0, probs[0]
    while rdm_num > p:
        i += 1
        p += probs[i]
    return array[i]

truncated_normal

truncated_normal(mean, sd)

Sample from a Normal distribution truncated at zero.

Values less than or equal to zero are resampled until a positive value is obtained.

Parameters:

Name Type Description Default
mean float

The mean of the Normal distribution.

required
sd float

The standard deviation of the Normal distribution.

required

Returns:

Type Description
float

A positive sample from the truncated Normal distribution.

Source code in hciw/auxiliary.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def truncated_normal(mean, sd):
    """Sample from a Normal distribution truncated at zero.

    Values less than or equal to zero are resampled until a positive
    value is obtained.

    Args:
        mean (float): The mean of the Normal distribution.
        sd (float): The standard deviation of the Normal distribution.

    Returns:
        float: A positive sample from the truncated Normal distribution.
    """
    sample = random.normalvariate(mean, sd)
    while sample <= 0.0:
        sample = random.normalvariate(mean, sd)
    return sample

flatten_list

flatten_list(list_of_lists)

Flatten a list of lists into a single list.

Parameters:

Name Type Description Default
list_of_lists list

A list whose elements are themselves lists.

required

Returns:

Type Description
list

A single list containing all elements of the inner lists.

Source code in hciw/auxiliary.py
76
77
78
79
80
81
82
83
84
85
86
87
88
def flatten_list(list_of_lists):
    """Flatten a list of lists into a single list.

    Args:
        list_of_lists (list): A list whose elements are themselves lists.

    Returns:
        list: A single list containing all elements of the inner lists.
    """
    flat = []
    for a_list in list_of_lists:
        flat += a_list
    return flat

no_routing

no_routing(ind)

Send a customer straight to the exit node.

This process-based routing function is a placeholder used when NoArrivals is used.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
list

An empty list of destinations.

Source code in hciw/auxiliary.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def no_routing(ind):
    """Send a customer straight to the exit node.

    This process-based routing function is a placeholder used when
    ``NoArrivals`` is used.

    Args:
        ind (Individual): The individual being routed.

    Returns:
        list: An empty list of destinations.
    """
    return []

Warm starts

waitlist

Warm-start a simulation with an initial backlog of individuals.

A warm start seeds a simulation with individuals who are already present at time zero, rather than waiting for them to arrive via the arrival process. This is useful when simulating a system that is already busy, for example a hospital whose current patients are known from historical data.

Each backlog entry supplies the individual's identifier, customer class, historical arrival date, and the node they are waiting at. Arrival dates are expected to be at or before the simulation start time, so that the time an individual spent waiting before the simulation began is captured in their waiting-time statistics.

accept

accept(
    individual_id: int,
    individual_class: str,
    arrival_date: float,
    node: Node,
    simulation: Simulation,
) -> None

Place an individual already present at time zero into a node.

The individual is accepted by the node using the usual acceptance logic, so that service begins immediately if a server is free. Their recorded arrival_date is then overwritten with the supplied historical date so that waiting-time statistics include the time spent in the system before the simulation started.

Parameters:

Name Type Description Default
individual_id int

A unique identifier for the individual.

required
individual_class str

The customer class the individual belongs to.

required
arrival_date float

The historical date at which the individual arrived. This should be at or before the simulation start time.

required
node Node

The node the individual is waiting at.

required
simulation Simulation

The simulation the individual belongs to.

required
Source code in hciw/waitlist.py
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
def accept(
    individual_id: int,
    individual_class: str,
    arrival_date: float,
    node: hciw.Node,
    simulation: hciw.Simulation,
) -> None:
    """Place an individual already present at time zero into a node.

    The individual is accepted by the node using the usual acceptance logic,
    so that service begins immediately if a server is free. Their recorded
    ``arrival_date`` is then overwritten with the supplied historical date so
    that waiting-time statistics include the time spent in the system before
    the simulation started.

    Args:
        individual_id: A unique identifier for the individual.
        individual_class: The customer class the individual belongs to.
        arrival_date: The historical date at which the individual arrived.
            This should be at or before the simulation start time.
        node: The node the individual is waiting at.
        simulation: The simulation the individual belongs to.
    """
    simulation.current_time = 0
    priority_class = simulation.network.priority_class_mapping[individual_class]
    next_individual = simulation.IndividualType(
        individual_id,
        individual_class,
        priority_class,
        simulation=simulation,
    )
    next_individual.starting_node = node.id_number
    simulation.routers[individual_class].initialise_individual(next_individual)
    node.accept(next_individual)
    next_individual.arrival_date = arrival_date
    node.update_next_event_date()
    _update_arrival_node(simulation, individual_id, individual_class)

_update_arrival_node

_update_arrival_node(
    simulation: Simulation,
    individual_id: int,
    individual_class: str,
) -> None

Advance the arrival node's counters to account for a seeded individual.

The arrival node's number_of_individuals is also the source of new individual identifiers, so it is advanced past the seeded identifier to prevent later arrivals from reusing it.

Parameters:

Name Type Description Default
simulation Simulation

The simulation being seeded.

required
individual_id int

The identifier of the seeded individual.

required
individual_class str

The customer class of the seeded individual.

required
Source code in hciw/waitlist.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def _update_arrival_node(
    simulation: hciw.Simulation, individual_id: int, individual_class: str
) -> None:
    """Advance the arrival node's counters to account for a seeded individual.

    The arrival node's ``number_of_individuals`` is also the source of new
    individual identifiers, so it is advanced past the seeded identifier to
    prevent later arrivals from reusing it.

    Args:
        simulation: The simulation being seeded.
        individual_id: The identifier of the seeded individual.
        individual_class: The customer class of the seeded individual.
    """
    arrival_node = simulation.nodes[0]
    arrival_node.number_of_individuals = max(
        arrival_node.number_of_individuals + 1, individual_id
    )
    arrival_node.number_of_individuals_per_class[individual_class] += 1
    arrival_node.number_accepted_individuals += 1
    arrival_node.number_accepted_individuals_per_class[individual_class] += 1

create_existing_customers_from_list

create_existing_customers_from_list(
    backlog: list[list], simulation: Simulation
) -> None

Seed a simulation with an initial backlog of individuals.

Each row of the backlog describes one individual already present at time zero. Rows are supplied in the order the individuals should be placed into their queues.

Parameters:

Name Type Description Default
backlog list[list]

A list of rows, each of the form [individual_id, customer_class, arrival_date, node_index], where node_index is the index of the node in simulation.nodes.

required
simulation Simulation

The simulation to seed.

required
Source code in hciw/waitlist.py
 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
def create_existing_customers_from_list(
    backlog: list[list],
    simulation: hciw.Simulation,
) -> None:
    """Seed a simulation with an initial backlog of individuals.

    Each row of the backlog describes one individual already present at time
    zero. Rows are supplied in the order the individuals should be placed into
    their queues.

    Args:
        backlog: A list of rows, each of the form
            ``[individual_id, customer_class, arrival_date, node_index]``,
            where ``node_index`` is the index of the node in
            ``simulation.nodes``.
        simulation: The simulation to seed.
    """
    for individual_id, customer_class, arrival_date, node_index in backlog:
        accept(
            individual_id,
            customer_class,
            arrival_date,
            simulation.nodes[node_index],
            simulation,
        )
    if backlog:
        _record_initial_state(simulation)

_record_initial_state

_record_initial_state(simulation: Simulation) -> None

Record the state tracker's initial state after seeding.

The state tracker records its state when Simulation initialises it, before any individual has been seeded. Seeding changes that state at time zero, so the tracker's first history entry is refreshed to match, ensuring state probabilities cover the whole simulation correctly.

Parameters:

Name Type Description Default
simulation Simulation

The simulation that was seeded.

required
Source code in hciw/waitlist.py
114
115
116
117
118
119
120
121
122
123
124
125
126
def _record_initial_state(simulation: hciw.Simulation) -> None:
    """Record the state tracker's initial state after seeding.

    The state tracker records its state when ``Simulation`` initialises it,
    before any individual has been seeded. Seeding changes that state at time
    zero, so the tracker's first history entry is refreshed to match, ensuring
    state probabilities cover the whole simulation correctly.

    Args:
        simulation: The simulation that was seeded.
    """
    history = simulation.statetracker.history
    history[0] = [history[0][0], simulation.statetracker.hash_state()]

Distribution classes

distributions

Distributions available in HCiw.

Individual

Individual(
    id_number,
    customer_class="Customer",
    priority_class=0,
    simulation=False,
)

Represent an individual agent in a queueing network simulation.

An Individual models the flow of an agent through a queueing network, tracking its arrival time, service time, priority and other relevant data.

Attributes:

Name Type Description
arrival_date bool or float

Timestamp of the individual's arrival.

service_start_date bool or float

Timestamp the current service started.

service_time bool or float

Duration of the current service.

service_end_date bool or float

Timestamp the current service ends.

exit_date bool or float

Timestamp the individual exits the system.

id_number int

Unique identifier for the individual.

data_records list

The data records attached to the individual.

customer_class str

The customer class of the individual.

previous_class str

The previous customer class of the individual.

priority_class int

The priority class of the individual.

prev_priority_class int

The previous priority class of the individual.

original_class str

The original customer class of the individual.

is_blocked bool

Whether the individual is blocked.

server bool or Server

The server assigned to the individual, if any.

queue_size_at_arrival bool or int

Queue size when the individual arrived.

queue_size_at_departure bool or int

Queue size when the individual departed.

destination bool or int

The destination node in the network.

interrupted bool

Whether the individual's service was interrupted.

node bool or int

The node where the individual is located.

simulation bool or Simulation

The simulation the individual belongs to.

Example

.. code-block:: python

individual = Individual(
    id_number=1, customer_class='Gold', priority_class=1,
    simulation=True,
)

Initialise an Individual.

Parameters:

Name Type Description Default
id_number int

A unique identifier for the individual.

required
customer_class str

The customer class the individual belongs to. Defaults to 'Customer'.

'Customer'
priority_class int

The priority class of the individual. Defaults to 0.

0
simulation bool or Simulation

The simulation the individual is part of. Defaults to False.

False
Source code in hciw/individual.py
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
def __init__(
    self, id_number, customer_class="Customer", priority_class=0, simulation=False
):
    """Initialise an ``Individual``.

    Args:
        id_number (int): A unique identifier for the individual.
        customer_class (str, optional): The customer class the individual
            belongs to. Defaults to ``'Customer'``.
        priority_class (int, optional): The priority class of the
            individual. Defaults to ``0``.
        simulation (bool or Simulation, optional): The simulation the
            individual is part of. Defaults to ``False``.
    """
    self.arrival_date = False
    self.service_start_date = False
    self.service_time: float | bool | str = False
    self.service_end_date = False
    self.exit_date = False
    self.id_number = id_number
    self.data_records = []
    self.customer_class = customer_class
    self.previous_class = customer_class
    self.priority_class = priority_class
    self.prev_priority_class = priority_class
    self.original_class = customer_class
    self.is_blocked = False
    self.server: Server | bool = False
    self.queue_size_at_arrival: int | bool = False
    self.queue_size_at_departure: int | bool = False
    self.destination: int | bool = False
    self.interrupted = False
    self.node = False
    self.simulation = simulation

__repr__

__repr__() -> str

Return a string representation of the individual.

Returns:

Type Description
str

The individual represented as 'Individual <id_number>'.

Source code in hciw/individual.py
 98
 99
100
101
102
103
104
def __repr__(self) -> str:
    """Return a string representation of the individual.

    Returns:
        str: The individual represented as ``'Individual <id_number>'``.
    """
    return f"Individual {self.id_number}"

Distribution

A general distribution from which all other distributions inherit.

mean property

mean

Return the default mean of a distribution.

Returns:

Type Description
float

The mean, NaN by default.

variance property

variance

Return the default variance of a distribution.

Returns:

Type Description
float

The variance, NaN by default.

sd property

sd

Return the default standard deviation of a distribution.

Returns:

Type Description
float

The square root of the variance.

median property

median

Return the default median of a distribution.

Returns:

Type Description
float

The median, NaN by default.

upper_limit property

upper_limit

Return the default upper limit of a distribution.

Returns:

Type Description
float

The upper limit, NaN by default.

lower_limit property

lower_limit

Return the default lower limit of a distribution.

Returns:

Type Description
float

The lower limit, NaN by default.

__repr__

__repr__() -> str

Return a string representation of the distribution.

Source code in hciw/dists/distributions.py
28
29
30
def __repr__(self) -> str:
    """Return a string representation of the distribution."""
    return "Distribution"

sample

sample(
    t: float | None = None, ind: Individual | None = None
) -> float

Sample a value from the distribution.

The base class implements no sampling; subclasses must override this method.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
float

A sampled value. The base class returns None so that

float

meth:_sample rejects it.

Source code in hciw/dists/distributions.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def sample(self, t: float | None = None, ind: Individual | None = None) -> float:
    """Sample a value from the distribution.

    The base class implements no sampling; subclasses must override this
    method.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        float: A sampled value. The base class returns ``None`` so that
        :meth:`_sample` rejects it.
    """
    return None  # type: ignore[return-value]

_sample

_sample(t=None, ind=None)

Perform validity checks before sampling.

Source code in hciw/dists/distributions.py
48
49
50
51
52
53
def _sample(self, t=None, ind=None):
    """Perform validity checks before sampling."""
    s = self.sample(t=t, ind=ind)
    if (isinstance(s, (float, int))) and s >= 0:
        return s
    raise ValueError("Invalid time sampled.")

__add__

__add__(dist)

Add two distributions such that sampling is the sum of the samples.

Parameters:

Name Type Description Default
dist Distribution

The distribution to add.

required

Returns:

Type Description
CombinedDistribution

A distribution sampling the sum.

Source code in hciw/dists/distributions.py
55
56
57
58
59
60
61
62
63
64
def __add__(self, dist):
    """Add two distributions such that sampling is the sum of the samples.

    Args:
        dist (Distribution): The distribution to add.

    Returns:
        CombinedDistribution: A distribution sampling the sum.
    """
    return CombinedDistribution(self, dist, add)

__sub__

__sub__(dist)

Subtract two distributions such that sampling is the difference.

Parameters:

Name Type Description Default
dist Distribution

The distribution to subtract.

required

Returns:

Type Description
CombinedDistribution

A distribution sampling the difference.

Source code in hciw/dists/distributions.py
66
67
68
69
70
71
72
73
74
75
def __sub__(self, dist):
    """Subtract two distributions such that sampling is the difference.

    Args:
        dist (Distribution): The distribution to subtract.

    Returns:
        CombinedDistribution: A distribution sampling the difference.
    """
    return CombinedDistribution(self, dist, sub)

__mul__

__mul__(dist)

Multiply two distributions such that sampling is the product.

Parameters:

Name Type Description Default
dist Distribution

The distribution to multiply by.

required

Returns:

Type Description
CombinedDistribution

A distribution sampling the product.

Source code in hciw/dists/distributions.py
77
78
79
80
81
82
83
84
85
86
def __mul__(self, dist):
    """Multiply two distributions such that sampling is the product.

    Args:
        dist (Distribution): The distribution to multiply by.

    Returns:
        CombinedDistribution: A distribution sampling the product.
    """
    return CombinedDistribution(self, dist, mul)

__truediv__

__truediv__(dist)

Divide two distributions such that sampling is the ratio.

Parameters:

Name Type Description Default
dist Distribution

The distribution to divide by.

required

Returns:

Type Description
CombinedDistribution

A distribution sampling the ratio.

Source code in hciw/dists/distributions.py
88
89
90
91
92
93
94
95
96
97
def __truediv__(self, dist):
    """Divide two distributions such that sampling is the ratio.

    Args:
        dist (Distribution): The distribution to divide by.

    Returns:
        CombinedDistribution: A distribution sampling the ratio.
    """
    return CombinedDistribution(self, dist, truediv)

CombinedDistribution

CombinedDistribution(dist1, dist2, operator)

Bases: Distribution

A distribution combining the samples of two other distributions.

The samples of dist1 and dist2 are combined using operator.

Attributes:

Name Type Description
d1 Distribution

A deep copy of the first distribution.

d2 Distribution

A deep copy of the second distribution.

operator callable

The binary operator used to combine samples.

Initialize the combined distribution.

Parameters:

Name Type Description Default
dist1 Distribution

The first distribution.

required
dist2 Distribution

The second distribution.

required
operator callable

The binary operator used to combine samples.

required
Source code in hciw/dists/distributions.py
165
166
167
168
169
170
171
172
173
174
175
def __init__(self, dist1, dist2, operator):
    """Initialize the combined distribution.

    Args:
        dist1 (Distribution): The first distribution.
        dist2 (Distribution): The second distribution.
        operator (callable): The binary operator used to combine samples.
    """
    self.d1 = copy.deepcopy(dist1)
    self.d2 = copy.deepcopy(dist2)
    self.operator = operator

mean property

mean: float

Return the combined mean of the two distributions.

Returns:

Type Description
float

The mean implied by the operator.

variance property

variance: float

Return the combined variance of the two distributions.

Returns:

Type Description
float

The variance implied by the operator.

upper_limit property

upper_limit

Return the combined upper limit.

Returns:

Type Description
float

The sum of the upper limits for addition, NaN otherwise.

lower_limit property

lower_limit

Return the combined lower limit.

Returns:

Type Description
float

The sum of the lower limits for addition, NaN otherwise.

__repr__

__repr__()

Return a string representation of the combined distribution.

Source code in hciw/dists/distributions.py
177
178
179
def __repr__(self):
    """Return a string representation of the combined distribution."""
    return "CombinedDistribution"

sample

sample(t=None, ind=None)

Sample from each distribution and combine the samples.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
float

The combined sample.

Source code in hciw/dists/distributions.py
181
182
183
184
185
186
187
188
189
190
191
192
193
def sample(self, t=None, ind=None):
    """Sample from each distribution and combine the samples.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        float: The combined sample.
    """
    s1 = self.d1.sample(t, ind)
    s2 = self.d2.sample(t, ind)
    return self.operator(s1, s2)

Uniform

Uniform(lower, upper)

Bases: Distribution

The Uniform distribution.

Attributes:

Name Type Description
lower float

The lower bound.

upper float

The upper bound.

Initialize the Uniform distribution.

Parameters:

Name Type Description Default
lower float

The lower bound.

required
upper float

The upper bound.

required

Raises:

Type Description
ValueError

If a bound is negative or upper is less than lower.

Source code in hciw/dists/distributions.py
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
def __init__(self, lower, upper):
    """Initialize the Uniform distribution.

    Args:
        lower (float): The lower bound.
        upper (float): The upper bound.

    Raises:
        ValueError: If a bound is negative or ``upper`` is less than ``lower``.
    """
    if lower < 0.0 or upper < 0.0:
        raise ValueError("Uniform distribution must sample positive numbers only.")
    if upper < lower:
        raise ValueError(
            "Uniform distirbution upper bound should be >= lower bound."
        )
    self.lower = lower
    self.upper = upper

mean property

mean

Return the mean of the Uniform distribution.

Returns:

Type Description
float

The mean.

variance property

variance

Return the variance of the Uniform distribution.

Returns:

Type Description
float

The variance.

median property

median

Return the median of the Uniform distribution.

Returns:

Type Description
float

The median.

upper_limit property

upper_limit

Return the upper limit of the Uniform distribution.

Returns:

Type Description
float

The upper bound.

lower_limit property

lower_limit

Return the lower limit of the Uniform distribution.

Returns:

Type Description
float

The lower bound.

__repr__

__repr__()

Return a string representation of the Uniform distribution.

Source code in hciw/dists/distributions.py
292
293
294
def __repr__(self):
    """Return a string representation of the Uniform distribution."""
    return f"Uniform(lower={self.lower}, upper={self.upper})"

sample

sample(t=None, ind=None)

Sample from the Uniform distribution.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
float

A sample between lower and upper.

Source code in hciw/dists/distributions.py
296
297
298
299
300
301
302
303
304
305
306
def sample(self, t=None, ind=None):
    """Sample from the Uniform distribution.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        float: A sample between ``lower`` and ``upper``.
    """
    return uniform(self.lower, self.upper)

Deterministic

Deterministic(value)

Bases: Distribution

The Deterministic distribution.

Attributes:

Name Type Description
value float

The value always returned by the distribution.

Initialize the Deterministic distribution.

Parameters:

Name Type Description Default
value float

The value to return.

required

Raises:

Type Description
ValueError

If value is negative.

Source code in hciw/dists/distributions.py
361
362
363
364
365
366
367
368
369
370
371
372
373
374
def __init__(self, value):
    """Initialize the Deterministic distribution.

    Args:
        value (float): The value to return.

    Raises:
        ValueError: If ``value`` is negative.
    """
    if value < 0.0:
        raise ValueError(
            "Deterministic distribution must sample positive numbers only."
        )
    self.value = value

mean property

mean

Return the mean of the Deterministic distribution.

Returns:

Type Description
float

The deterministic value.

variance property

variance

Return the variance of the Deterministic distribution.

Returns:

Type Description
float

Always 0.0.

median property

median

Return the median of the Deterministic distribution.

Returns:

Type Description
float

The deterministic value.

upper_limit property

upper_limit

Return the upper limit of the Deterministic distribution.

Returns:

Type Description
float

The deterministic value.

lower_limit property

lower_limit

Return the lower limit of the Deterministic distribution.

Returns:

Type Description
float

The deterministic value.

__repr__

__repr__()

Return a string representation of the Deterministic distribution.

Source code in hciw/dists/distributions.py
376
377
378
def __repr__(self):
    """Return a string representation of the Deterministic distribution."""
    return f"Deterministic(value={self.value})"

sample

sample(t=None, ind=None)

Sample from the Deterministic distribution.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
float

The deterministic value.

Source code in hciw/dists/distributions.py
380
381
382
383
384
385
386
387
388
389
390
def sample(self, t=None, ind=None):
    """Sample from the Deterministic distribution.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        float: The deterministic value.
    """
    return self.value

Triangular

Triangular(lower, mode, upper)

Bases: Distribution

The Triangular distribution.

Attributes:

Name Type Description
lower float

The lower bound.

mode float

The modal value.

upper float

The upper bound.

Initialize the Triangular distribution.

Parameters:

Name Type Description Default
lower float

The lower bound.

required
mode float

The modal value.

required
upper float

The upper bound.

required

Raises:

Type Description
ValueError

If any parameter is negative.

ValueError

If the bounds do not satisfy lower <= mode <= upper.

Source code in hciw/dists/distributions.py
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
def __init__(self, lower, mode, upper):
    """Initialize the Triangular distribution.

    Args:
        lower (float): The lower bound.
        mode (float): The modal value.
        upper (float): The upper bound.

    Raises:
        ValueError: If any parameter is negative.
        ValueError: If the bounds do not satisfy ``lower <= mode <= upper``.
    """
    if lower < 0.0 or upper < 0.0 or mode < 0.0:
        raise ValueError(
            "Triangular distribution must sample positive numbers only."
        )
    if not lower <= mode <= upper:
        raise ValueError(
            "Triangular distribution lower bound must be <= mode must be "
            "<= upper bound."
        )
    self.lower = lower
    self.mode = mode
    self.upper = upper

mean property

mean

Return the mean of the Triangular distribution.

Returns:

Type Description
float

The mean.

variance property

variance

Return the variance of the Triangular distribution.

Returns:

Type Description
float

The variance.

median property

median

Return the median of the Triangular distribution.

Returns:

Type Description
float

The median.

upper_limit property

upper_limit

Return the upper limit of the Triangular distribution.

Returns:

Type Description
float

The upper bound.

lower_limit property

lower_limit

Return the lower limit of the Triangular distribution.

Returns:

Type Description
float

The lower bound.

__repr__

__repr__()

Return a string representation of the Triangular distribution.

Source code in hciw/dists/distributions.py
472
473
474
def __repr__(self):
    """Return a string representation of the Triangular distribution."""
    return f"Triangular(lower={self.lower}, mode={self.mode}, upper={self.upper})"

sample

sample(t=None, ind=None)

Sample from the Triangular distribution.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
float

A sample between lower and upper.

Source code in hciw/dists/distributions.py
476
477
478
479
480
481
482
483
484
485
486
def sample(self, t=None, ind=None):
    """Sample from the Triangular distribution.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        float: A sample between ``lower`` and ``upper``.
    """
    return triangular(self.lower, self.upper, self.mode)

Exponential

Exponential(rate)

Bases: Distribution

The Exponential distribution.

Attributes:

Name Type Description
rate float

The rate parameter, lambda.

Initialize the Exponential distribution.

Parameters:

Name Type Description Default
rate float

The rate parameter, lambda.

required

Raises:

Type Description
ValueError

If rate is not positive.

Source code in hciw/dists/distributions.py
547
548
549
550
551
552
553
554
555
556
557
558
559
560
def __init__(self, rate):
    """Initialize the Exponential distribution.

    Args:
        rate (float): The rate parameter, lambda.

    Raises:
        ValueError: If ``rate`` is not positive.
    """
    if rate <= 0.0:
        raise ValueError(
            "Exponential distribution must sample positive numbers only."
        )
    self.rate = rate

mean property

mean

Return the mean of the Exponential distribution.

Returns:

Type Description
float

The mean.

variance property

variance

Return the variance of the Exponential distribution.

Returns:

Type Description
float

The variance.

median property

median

Return the median of the Exponential distribution.

Returns:

Type Description
float

The median.

upper_limit property

upper_limit

Return the upper limit of the Exponential distribution.

Returns:

Type Description
float

Infinity.

lower_limit property

lower_limit

Return the lower limit of the Exponential distribution.

Returns:

Type Description
float

Zero.

__repr__

__repr__()

Return a string representation of the Exponential distribution.

Source code in hciw/dists/distributions.py
562
563
564
def __repr__(self):
    """Return a string representation of the Exponential distribution."""
    return f"Exponential(rate={self.rate})"

sample

sample(t=None, ind=None)

Sample from the Exponential distribution.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
float

A sample from the distribution.

Source code in hciw/dists/distributions.py
566
567
568
569
570
571
572
573
574
575
576
def sample(self, t=None, ind=None):
    """Sample from the Exponential distribution.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        float: A sample from the distribution.
    """
    return expovariate(self.rate)

Gamma

Gamma(shape, scale)

Bases: Distribution

The Gamma distribution.

Attributes:

Name Type Description
shape float

The shape parameter, alpha.

scale float

The scale parameter, beta.

Initialize the Gamma distribution.

Parameters:

Name Type Description Default
shape float

The shape parameter, alpha.

required
scale float

The scale parameter, beta.

required
Source code in hciw/dists/distributions.py
632
633
634
635
636
637
638
639
640
def __init__(self, shape, scale):
    """Initialize the Gamma distribution.

    Args:
        shape (float): The shape parameter, alpha.
        scale (float): The scale parameter, beta.
    """
    self.shape = shape
    self.scale = scale

mean property

mean

Return the mean of the Gamma distribution.

Returns:

Type Description
float

The mean.

variance property

variance

Return the variance of the Gamma distribution.

Returns:

Type Description
float

The variance.

upper_limit property

upper_limit

Return the upper limit of the Gamma distribution.

Returns:

Type Description
float

Infinity.

lower_limit property

lower_limit

Return the lower limit of the Gamma distribution.

Returns:

Type Description
float

Zero.

__repr__

__repr__()

Return a string representation of the Gamma distribution.

Source code in hciw/dists/distributions.py
642
643
644
def __repr__(self):
    """Return a string representation of the Gamma distribution."""
    return f"Gamma(shape={self.shape}, scale={self.scale})"

sample

sample(t=None, ind=None)

Sample from the Gamma distribution.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
float

A sample from the distribution.

Source code in hciw/dists/distributions.py
646
647
648
649
650
651
652
653
654
655
656
def sample(self, t=None, ind=None):
    """Sample from the Gamma distribution.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        float: A sample from the distribution.
    """
    return gammavariate(self.shape, self.scale)

Normal

Normal(mean, sd)

Bases: Distribution

Truncated Normal distribution, truncated below at 0.

Initialize the truncated Normal distribution.

Parameters:

Name Type Description Default
mean float

Mean of the underlying normal distribution.

required
sd float

Standard deviation of the underlying normal distribution.

required
Source code in hciw/dists/distributions.py
698
699
700
701
702
703
704
705
706
def __init__(self, mean, sd):
    """Initialize the truncated Normal distribution.

    Args:
        mean (float): Mean of the underlying normal distribution.
        sd (float): Standard deviation of the underlying normal distribution.
    """
    self._mean = mean
    self._sd = sd

mean property

mean

Return the mean of the truncated Normal distribution.

Returns:

Type Description
float

The mean of the truncated distribution.

variance property

variance

Return the variance of the truncated Normal distribution.

Returns:

Type Description
float

The variance of the truncated distribution.

median property

median

Return the median of the truncated Normal distribution.

Returns:

Type Description
float

The median of the truncated distribution.

upper_limit property

upper_limit

Return the upper limit of the truncated Normal distribution.

Returns:

Type Description
float

Infinity.

lower_limit property

lower_limit

Return the lower limit of the truncated Normal distribution.

Returns:

Type Description
float

Zero.

__repr__

__repr__()

Return a string representation of the Normal distribution.

Source code in hciw/dists/distributions.py
708
709
710
def __repr__(self):
    """Return a string representation of the Normal distribution."""
    return f"Normal(mean={self._mean}, sd={self._sd})"

sample

sample(t=None, ind=None)

Sample from the truncated Normal distribution.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
float

A sample truncated below at 0.

Source code in hciw/dists/distributions.py
712
713
714
715
716
717
718
719
720
721
722
def sample(self, t=None, ind=None):
    """Sample from the truncated Normal distribution.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        float: A sample truncated below at 0.
    """
    return truncated_normal(self._mean, self._sd)

Lognormal

Lognormal(mean, sd)

Bases: Distribution

The Lognormal distribution.

Initialize the Lognormal distribution.

Parameters:

Name Type Description Default
mean float

The mean of the underlying normal, mu.

required
sd float

The standard deviation of the underlying normal, sigma.

required
Source code in hciw/dists/distributions.py
784
785
786
787
788
789
790
791
792
def __init__(self, mean, sd):
    """Initialize the Lognormal distribution.

    Args:
        mean (float): The mean of the underlying normal, mu.
        sd (float): The standard deviation of the underlying normal, sigma.
    """
    self._mean = mean
    self._sd = sd

mean property

mean

Return the mean of the Lognormal distribution.

Returns:

Type Description
float

The mean.

variance property

variance

Return the variance of the Lognormal distribution.

Returns:

Type Description
float

The variance.

median property

median

Return the median of the Lognormal distribution.

Returns:

Type Description
float

The median.

upper_limit property

upper_limit

Return the upper limit of the Lognormal distribution.

Returns:

Type Description
float

Infinity.

lower_limit property

lower_limit

Return the lower limit of the Lognormal distribution.

Returns:

Type Description
float

Zero.

__repr__

__repr__()

Return a string representation of the Lognormal distribution.

Source code in hciw/dists/distributions.py
794
795
796
def __repr__(self):
    """Return a string representation of the Lognormal distribution."""
    return f"Lognormal(mean={self._mean}, sd={self._sd})"

sample

sample(t=None, ind=None)

Sample from the Lognormal distribution.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
float

A sample from the distribution.

Source code in hciw/dists/distributions.py
798
799
800
801
802
803
804
805
806
807
808
def sample(self, t=None, ind=None):
    """Sample from the Lognormal distribution.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        float: A sample from the distribution.
    """
    return lognormvariate(self._mean, self._sd)

Weibull

Weibull(scale, shape)

Bases: Distribution

The Weibull distribution.

Attributes:

Name Type Description
scale float

The scale parameter, alpha.

shape float

The shape parameter, beta.

Initialize the Weibull distribution.

Parameters:

Name Type Description Default
scale float

The scale parameter, alpha.

required
shape float

The shape parameter, beta.

required
Source code in hciw/dists/distributions.py
864
865
866
867
868
869
870
871
872
def __init__(self, scale, shape):
    """Initialize the Weibull distribution.

    Args:
        scale (float): The scale parameter, alpha.
        shape (float): The shape parameter, beta.
    """
    self.scale = scale
    self.shape = shape

mean property

mean

Return the mean of the Weibull distribution.

Returns:

Type Description
float

The mean.

variance property

variance

Return the variance of the Weibull distribution.

Returns:

Type Description
float

The variance.

median property

median

Return the median of the Weibull distribution.

Returns:

Type Description
float

The median.

upper_limit property

upper_limit

Return the upper limit of the Weibull distribution.

Returns:

Type Description
float

Infinity.

lower_limit property

lower_limit

Return the lower limit of the Weibull distribution.

Returns:

Type Description
float

Zero.

__repr__

__repr__()

Return a string representation of the Weibull distribution.

Source code in hciw/dists/distributions.py
874
875
876
def __repr__(self):
    """Return a string representation of the Weibull distribution."""
    return f"Weibull(shape={self.shape}, scale={self.scale})"

sample

sample(t=None, ind=None)

Sample from the Weibull distribution.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
float

A sample from the distribution.

Source code in hciw/dists/distributions.py
878
879
880
881
882
883
884
885
886
887
888
def sample(self, t=None, ind=None):
    """Sample from the Weibull distribution.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        float: A sample from the distribution.
    """
    return weibullvariate(self.scale, self.shape)

Empirical

Empirical(observations)

Bases: Distribution

The Empirical distribution.

Attributes:

Name Type Description
observations list

The observations from which to sample.

Initialize the Empirical distribution.

Parameters:

Name Type Description Default
observations list

The observations from which to sample.

required

Raises:

Type Description
ValueError

If any observation is negative.

Source code in hciw/dists/distributions.py
945
946
947
948
949
950
951
952
953
954
955
956
957
958
def __init__(self, observations):
    """Initialize the Empirical distribution.

    Args:
        observations (list): The observations from which to sample.

    Raises:
        ValueError: If any observation is negative.
    """
    if any(o < 0 for o in observations):
        raise ValueError(
            "Empirical distribution must sample positive numbers only."
        )
    self.observations = observations

mean property

mean

Return the mean of the Empirical distribution.

Returns:

Type Description
float

The mean of the observations.

variance property

variance

Return the variance of the Empirical distribution.

Returns:

Type Description
float

The population variance of the observations.

median property

median

Return the median of the Empirical distribution.

Returns:

Type Description
float

The sample median of the observations.

upper_limit property

upper_limit

Return the upper limit of the Empirical distribution.

Returns:

Type Description
float

The largest observation.

lower_limit property

lower_limit

Return the lower limit of the Empirical distribution.

Returns:

Type Description
float

The smallest observation.

__repr__

__repr__()

Return a string representation of the Empirical distribution.

Source code in hciw/dists/distributions.py
960
961
962
def __repr__(self):
    """Return a string representation of the Empirical distribution."""
    return "Empirical"

sample

sample(t=None, ind=None)

Sample from the Empirical distribution.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
float

A value chosen uniformly from observations.

Source code in hciw/dists/distributions.py
964
965
966
967
968
969
970
971
972
973
974
def sample(self, t=None, ind=None):
    """Sample from the Empirical distribution.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        float: A value chosen uniformly from ``observations``.
    """
    return random_choice(self.observations)

Sequential

Sequential(sequence)

Bases: Distribution

The Sequential distribution.

Attributes:

Name Type Description
sequence list

The sequence of values to cycle through.

generator cycle

The cycle iterator over sequence.

Initialize the Sequential distribution.

Parameters:

Name Type Description Default
sequence list

The sequence of values to cycle through.

required

Raises:

Type Description
ValueError

If any value in sequence is negative.

Source code in hciw/dists/distributions.py
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
def __init__(self, sequence):
    """Initialize the Sequential distribution.

    Args:
        sequence (list): The sequence of values to cycle through.

    Raises:
        ValueError: If any value in ``sequence`` is negative.
    """
    if any(o < 0 for o in sequence):
        raise ValueError(
            "Sequential distribution must sample positive numbers only."
        )
    self.sequence = sequence
    self.generator = cycle(self.sequence)

mean property

mean

Return the mean of the Sequential distribution.

Returns:

Type Description
float

The mean of the sequence.

variance property

variance

Return the variance of the Sequential distribution.

Returns:

Type Description
float

The population variance of the sequence.

median property

median

Return the median of the Sequential distribution.

Returns:

Type Description
float

The sample median of the sequence.

upper_limit property

upper_limit

Return the upper limit of the Sequential distribution.

Returns:

Type Description
float

The largest value in the sequence.

lower_limit property

lower_limit

Return the lower limit of the Sequential distribution.

Returns:

Type Description
float

The smallest value in the sequence.

__repr__

__repr__()

Return a string representation of the Sequential distribution.

Source code in hciw/dists/distributions.py
1055
1056
1057
1058
1059
def __repr__(self):
    """Return a string representation of the Sequential distribution."""
    if len(self.sequence) <= 3:  # noqa: PLR2004 - display cutoff, not a domain value
        return f"Sequential({self.sequence})"
    return f"Sequential([{self.sequence[0]}, ..., {self.sequence[-1]}])"

sample

sample(t=None, ind=None)

Sample the next value from the sequence.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
float

The next value in the sequence.

Source code in hciw/dists/distributions.py
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
def sample(self, t=None, ind=None):
    """Sample the next value from the sequence.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        float: The next value in the sequence.
    """
    return next(self.generator)

Pmf

Pmf(values, probs)

Bases: Distribution

A distribution defined by a probability mass function (pmf).

Attributes:

Name Type Description
values list

The values that can be sampled.

probs list

The probability associated with each value.

Initialize the Pmf distribution.

Parameters:

Name Type Description Default
values list

The values to sample.

required
probs list

The associated probabilities, which must sum to 1.

required

Raises:

Type Description
ValueError

If any value is negative.

ValueError

If any probability is outside [0, 1].

ValueError

If the probabilities do not sum to 1.0.

Source code in hciw/dists/distributions.py
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
def __init__(self, values, probs):
    """Initialize the Pmf distribution.

    Args:
        values (list): The values to sample.
        probs (list): The associated probabilities, which must sum to 1.

    Raises:
        ValueError: If any value is negative.
        ValueError: If any probability is outside [0, 1].
        ValueError: If the probabilities do not sum to 1.0.
    """
    if any(o < 0 for o in values):
        raise ValueError("Pmf must sample positive numbers only.")
    if any(p < 0 or p > 1.0 for p in probs):
        raise ValueError("Pmf must have valid probabilities.")
    if not np.isclose(sum(probs), 1.0):
        raise ValueError("Pmf probabilities must sum to 1.0.")
    self.values = values
    self.probs = probs

mean property

mean

Return the mean of the Pmf distribution.

Returns:

Type Description
float

The mean.

variance property

variance

Return the variance of the Pmf distribution.

Returns:

Type Description
float

The variance.

median property

median

Return the median of the Pmf distribution.

Returns:

Type Description
float

The value at which the cumulative probability reaches 0.5.

upper_limit property

upper_limit

Return the upper limit of the Pmf distribution.

Returns:

Type Description
float

The largest value.

lower_limit property

lower_limit

Return the lower limit of the Pmf distribution.

Returns:

Type Description
float

The smallest value.

__repr__

__repr__()

Return a string representation of the Pmf distribution.

Source code in hciw/dists/distributions.py
1157
1158
1159
def __repr__(self):
    """Return a string representation of the Pmf distribution."""
    return f"Pmf(values={self.values}, probs={self.probs})"

sample

sample(t=None, ind=None)

Sample from the Pmf distribution.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
float

A value chosen according to probs.

Source code in hciw/dists/distributions.py
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
def sample(self, t=None, ind=None):
    """Sample from the Pmf distribution.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        float: A value chosen according to ``probs``.
    """
    return random_choice(self.values, self.probs)

PhaseType

PhaseType(initial_state, absorbing_matrix)

Bases: Distribution

A distribution defined by an initial vector and an absorbing Markov chain.

The final state of the chain is the absorbing state.

Attributes:

Name Type Description
initial_state list

The initial probability of being in each state.

states tuple

The indices of the states.

absorbing_matrix list

The rates of the absorbing Markov chain.

Initialize the PhaseType distribution.

Parameters:

Name Type Description Default
initial_state list

The initial probabilities of being in each state.

required
absorbing_matrix list

The matrix representation of the absorbing Markov chain, with the final state the absorbing state.

required

Raises:

Type Description
ValueError

If the initial state contains negative probabilities.

ValueError

If the initial state contains probabilities above 1.0.

ValueError

If the initial state probabilities do not sum to 1.0.

ValueError

If the absorbing matrix is not square.

ValueError

If the initial state and matrix sizes differ.

ValueError

If any transition rate is negative.

ValueError

If the matrix rows do not sum to 0.

ValueError

If the final state is not the absorbing state.

ValueError

If the absorbing state cannot be reached.

Source code in hciw/dists/distributions.py
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
def __init__(self, initial_state, absorbing_matrix):
    """Initialize the PhaseType distribution.

    Args:
        initial_state (list): The initial probabilities of being in each state.
        absorbing_matrix (list): The matrix representation of the absorbing
            Markov chain, with the final state the absorbing state.

    Raises:
        ValueError: If the initial state contains negative probabilities.
        ValueError: If the initial state contains probabilities above 1.0.
        ValueError: If the initial state probabilities do not sum to 1.0.
        ValueError: If the absorbing matrix is not square.
        ValueError: If the initial state and matrix sizes differ.
        ValueError: If any transition rate is negative.
        ValueError: If the matrix rows do not sum to 0.
        ValueError: If the final state is not the absorbing state.
        ValueError: If the absorbing state cannot be reached.
    """
    if any(p < 0 for p in initial_state):
        raise ValueError("Initial state vector must have positive probabilities.")
    if any(p > 1.0 for p in initial_state):
        raise ValueError(
            "Initial state vector probabilities must be no more than 1.0."
        )
    if sum(initial_state) > 1.0 + 10 ** (-10) or sum(initial_state) < 1.0 - 10 ** (
        -10
    ):
        raise ValueError("Initial state vector probabilities must sum to 1.0.")
    if any(len(absorbing_matrix) != len(row) for row in absorbing_matrix):
        raise ValueError("Matrix of the absorbing Markov chain must be square.")
    if len(initial_state) != len(absorbing_matrix):
        raise ValueError(
            "Initial state vector must have same number of states as absorbing "
            "Markov chain matrix."
        )
    if any(
        row[j] < 0
        for i, row in enumerate(absorbing_matrix)
        for j in range(len(absorbing_matrix))
        if i != j
    ):
        raise ValueError("Transition rates must be positive.")
    if not all(
        -(10 ** (-10)) < sum(row) < 10 ** (-10)
        for i, row in enumerate(absorbing_matrix)
    ):
        raise ValueError("Matrix rows must sum to 0.")
    if not all(r == 0 for r in absorbing_matrix[-1]):
        raise ValueError("Final state must be the absorbing state.")
    if not any(row[-1] > 0 for row in absorbing_matrix):
        raise ValueError("Must be possible to reach the absorbing state.")
    self.initial_state = initial_state
    self.states = tuple(range(len(initial_state)))
    self.absorbing_matrix = absorbing_matrix

mean property

mean

Return the mean of the PhaseType distribution.

Returns:

Type Description
float

The expected time to absorption.

variance property

variance

Return the variance of the PhaseType distribution.

Returns:

Type Description
float

The variance of the time to absorption.

upper_limit property

upper_limit

Return the upper limit of the PhaseType distribution.

Returns:

Type Description
float

Infinity.

lower_limit property

lower_limit

Return the lower limit of the PhaseType distribution.

Returns:

Type Description
float

Zero.

__repr__

__repr__() -> str

Return a string representation of the PhaseType distribution.

Source code in hciw/dists/distributions.py
1295
1296
1297
def __repr__(self) -> str:
    """Return a string representation of the PhaseType distribution."""
    return "PhaseType"

sample_transition

sample_transition(rate)

Sample the time to a single transition.

Parameters:

Name Type Description Default
rate float

The rate of the transition.

required

Returns:

Type Description
float

The sampled transition time, or infinity if rate is not positive.

Source code in hciw/dists/distributions.py
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
def sample_transition(self, rate):
    """Sample the time to a single transition.

    Args:
        rate (float): The rate of the transition.

    Returns:
        float: The sampled transition time, or infinity if ``rate`` is not positive.
    """
    if rate <= 0.0:
        return float("Inf")
    return expovariate(rate)

sample

sample(t=None, ind=None)

Sample from the PhaseType distribution.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
float

The total time until absorption.

Source code in hciw/dists/distributions.py
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
def sample(self, t=None, ind=None):
    """Sample from the PhaseType distribution.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        float: The total time until absorption.
    """
    cumulative_time = 0
    current_state = random_choice(self.states, probs=self.initial_state)
    while current_state != self.states[-1]:
        potential_transitions = [
            self.sample_transition(r) for r in self.absorbing_matrix[current_state]
        ]
        time, idx = min(
            (time, idx) for (idx, time) in enumerate(potential_transitions)
        )
        cumulative_time += time
        current_state = idx
    return cumulative_time

Erlang

Erlang(rate, num_phases)

Bases: PhaseType

A shortcut for the Erlang distribution, using the PhaseType distribution.

Attributes:

Name Type Description
rate float

The rate spent in each phase.

num_phases int

The number of phases in series.

Initialize the Erlang distribution.

Parameters:

Name Type Description Default
rate float

The rate spent in each phase.

required
num_phases int

The number of phases in series.

required

Raises:

Type Description
ValueError

If rate is not positive.

ValueError

If num_phases is less than 1.

Source code in hciw/dists/distributions.py
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
def __init__(self, rate, num_phases):
    """Initialize the Erlang distribution.

    Args:
        rate (float): The rate spent in each phase.
        num_phases (int): The number of phases in series.

    Raises:
        ValueError: If ``rate`` is not positive.
        ValueError: If ``num_phases`` is less than 1.
    """
    if rate <= 0.0:
        raise ValueError("Rate must be positive.")
    if num_phases < 1:
        raise ValueError("At least one phase is required.")
    self.rate = rate
    self.num_phases = num_phases
    initial_state = [1] + [0] * num_phases
    absorbing_matrix = [[0] * (num_phases + 1) for _ in range(num_phases + 1)]
    for phase in range(num_phases):
        absorbing_matrix[phase][phase] = -self.rate
        absorbing_matrix[phase][phase + 1] = self.rate
    super().__init__(initial_state, absorbing_matrix)

mean property

mean

Return the mean of the Erlang distribution.

Returns:

Type Description
float

The mean.

variance property

variance

Return the variance of the Erlang distribution.

Returns:

Type Description
float

The variance.

__repr__

__repr__()

Return a string representation of the Erlang distribution.

Source code in hciw/dists/distributions.py
1417
1418
1419
def __repr__(self):
    """Return a string representation of the Erlang distribution."""
    return f"Erlang(rate={self.rate}, k={self.num_phases})"

HyperExponential

HyperExponential(rates, probs)

Bases: PhaseType

A shortcut for the HyperExponential distribution using PhaseType.

Attributes:

Name Type Description
rates list

The rate of each phase.

probs list

The probability of starting in each phase.

Initialize the HyperExponential distribution.

Parameters:

Name Type Description Default
rates list

A vector of rates for each phase.

required
probs list

A probability vector for starting in each phase.

required

Raises:

Type Description
ValueError

If any rate is not positive.

Source code in hciw/dists/distributions.py
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
def __init__(self, rates, probs):
    """Initialize the HyperExponential distribution.

    Args:
        rates (list): A vector of rates for each phase.
        probs (list): A probability vector for starting in each phase.

    Raises:
        ValueError: If any rate is not positive.
    """
    if any(r <= 0.0 for r in rates):
        raise ValueError("Rates must be positive.")
    self.rates = rates
    self.probs = probs
    initial_state = [*probs, 0]
    num_phases = len(probs)
    absorbing_matrix = [[0] * (num_phases + 1) for _ in range(num_phases + 1)]
    for phase in range(num_phases):
        absorbing_matrix[phase][phase] = -rates[phase]
        absorbing_matrix[phase][num_phases] = rates[phase]
    super().__init__(initial_state, absorbing_matrix)

mean property

mean

Return the mean of the HyperExponential distribution.

Returns:

Type Description
float

The mean.

variance property

variance

Return the variance of the HyperExponential distribution.

Returns:

Type Description
float

The variance.

__repr__

__repr__()

Return a string representation of the HyperExponential distribution.

Source code in hciw/dists/distributions.py
1470
1471
1472
def __repr__(self):
    """Return a string representation of the HyperExponential distribution."""
    return "HyperExponential"

HyperErlang

HyperErlang(rates, probs, phase_lengths)

Bases: PhaseType

A shortcut for the HyperErlang distribution, using the PhaseType distribution.

Attributes:

Name Type Description
rates list

The rate of each phase.

probs list

The probability of starting in each phase.

phase_lengths list

The number of sub-phases in each phase.

Initialize the HyperErlang distribution.

Parameters:

Name Type Description Default
rates list

A vector of rates for each phase.

required
probs list

A probability vector for starting in each phase.

required
phase_lengths list

The number of sub-phases in each phase.

required

Raises:

Type Description
ValueError

If any rate is not positive.

ValueError

If any phase length is less than 1.

Source code in hciw/dists/distributions.py
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
def __init__(self, rates, probs, phase_lengths):
    """Initialize the HyperErlang distribution.

    Args:
        rates (list): A vector of rates for each phase.
        probs (list): A probability vector for starting in each phase.
        phase_lengths (list): The number of sub-phases in each phase.

    Raises:
        ValueError: If any rate is not positive.
        ValueError: If any phase length is less than 1.
    """
    if any(r <= 0.0 for r in rates):
        raise ValueError("Rates must be positive.")
    if any(n < 1 for n in phase_lengths):
        raise ValueError("At least one phase is required for each sub-phase.")
    initial_state = []
    for p, n in zip(probs, phase_lengths, strict=False):
        initial_state += [p]
        initial_state += [0] * (n - 1)
    initial_state += [0]

    num_phases = sum(phase_lengths)
    absorbing_matrix = [[0] * (num_phases + 1) for _ in range(num_phases + 1)]
    for i, r in enumerate(rates):
        for subphase in range(phase_lengths[i]):
            offset = sum(phase_lengths[:i])
            absorbing_matrix[offset + subphase][offset + subphase] = -r
            if subphase < phase_lengths[i] - 1:
                absorbing_matrix[offset + subphase][offset + subphase + 1] = r
            else:
                absorbing_matrix[offset + subphase][-1] = r
    self.rates = rates
    self.probs = probs
    self.phase_lengths = phase_lengths

    super().__init__(initial_state, absorbing_matrix)

mean property

mean

Return the mean of the HyperErlang distribution.

Returns:

Type Description
float

The mean.

variance property

variance

Return the variance of the HyperErlang distribution.

Returns:

Type Description
float

The variance.

__repr__

__repr__()

Return a string representation of the HyperErlang distribution.

Source code in hciw/dists/distributions.py
1543
1544
1545
def __repr__(self):
    """Return a string representation of the HyperErlang distribution."""
    return "HyperErlang"

Coxian

Coxian(rates, probs)

Bases: PhaseType

A shortcut for the Coxian distribution, using the PhaseType distribution.

Initialize the Coxian distribution.

Parameters:

Name Type Description Default
rates list

A vector of rates for each phase.

required
probs list

A vector of the probability of absorption at each phase.

required

Raises:

Type Description
ValueError

If any rate is not positive.

ValueError

If any probability is outside [0, 1].

ValueError

If the final probability is not exactly 1.0.

Source code in hciw/dists/distributions.py
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
def __init__(self, rates, probs):
    """Initialize the Coxian distribution.

    Args:
        rates (list): A vector of rates for each phase.
        probs (list): A vector of the probability of absorption at each phase.

    Raises:
        ValueError: If any rate is not positive.
        ValueError: If any probability is outside [0, 1].
        ValueError: If the final probability is not exactly 1.0.
    """
    if any(r <= 0.0 for r in rates):
        raise ValueError("Rates must be positive.")
    if any(p < 0 or p > 1.0 for p in probs):
        raise ValueError("Probability vector must have valid probabilities.")
    if probs[-1] != 1.0:
        raise ValueError(
            "The probability of going to the absorbing state from the final "
            "phase must be 1.0."
        )
    num_phases = len(rates)
    initial_state = [1] + [0] * num_phases
    absorbing_matrix = [[0] * (num_phases + 1) for _ in range(num_phases + 1)]
    for i, (p, r) in enumerate(zip(probs, rates, strict=False)):
        absorbing_matrix[i][i] = -r
        absorbing_matrix[i][i + 1] = (1 - p) * r
        absorbing_matrix[i][-1] = p * r
    super().__init__(initial_state, absorbing_matrix)

__repr__

__repr__()

Return a string representation of the Coxian distribution.

Source code in hciw/dists/distributions.py
1608
1609
1610
def __repr__(self):
    """Return a string representation of the Coxian distribution."""
    return "Coxian"

PoissonIntervals

PoissonIntervals(rates, endpoints, max_sample_date)

Bases: Sequential

A time-dependent Poisson distribution for arrivals.

The number of arrivals in each time interval is sampled from a Poisson distribution, the arrival dates are distributed uniformly within each interval, and the consecutive differences of these dates are used as the inter-arrival times.

Attributes:

Name Type Description
rates list

The Poisson rate for each time interval.

endpoints list

The endpoints of each time interval.

max_sample_date float

The point after which no arrivals occur.

Initialize the PoissonIntervals distribution.

Parameters:

Name Type Description Default
rates list

A list of Poisson rates for each time interval.

required
endpoints list

The endpoints of each time interval.

required
max_sample_date float

The point where no more arrivals can occur.

required

Raises:

Type Description
ValueError

If any rate is negative.

ValueError

If any endpoint is not positive.

ValueError

If max_sample_date is not positive.

ValueError

If the endpoints are not strictly increasing.

ValueError

If the numbers of rates and endpoints differ.

Source code in hciw/dists/distributions.py
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
def __init__(self, rates, endpoints, max_sample_date):
    """Initialize the PoissonIntervals distribution.

    Args:
        rates (list): A list of Poisson rates for each time interval.
        endpoints (list): The endpoints of each time interval.
        max_sample_date (float): The point where no more arrivals can occur.

    Raises:
        ValueError: If any rate is negative.
        ValueError: If any endpoint is not positive.
        ValueError: If ``max_sample_date`` is not positive.
        ValueError: If the endpoints are not strictly increasing.
        ValueError: If the numbers of rates and endpoints differ.
    """
    if any(r < 0.0 for r in rates):
        raise ValueError("All rates must be positive.")
    if any(e <= 0.0 for e in endpoints):
        raise ValueError("All interval endpoints must be positive.")
    if max_sample_date <= 0.0:
        raise ValueError("Maximum sample date must be positive.")
    diffs = [s - t for s, t in zip(endpoints[1:], endpoints[:-1], strict=False)]
    if any(d <= 0.0 for d in diffs):
        raise ValueError("Interval endpoints must be strictly increasing.")
    if len(rates) != len(endpoints):
        raise ValueError(
            "Consistent number of intervals (rates and endpoints) must be used."
        )

    self.rates = rates
    self.endpoints = endpoints
    self.max_sample_date = max_sample_date
    self.get_intervals()
    self.get_dates()
    self.inter_arrivals = [
        t - s for s, t in zip(self.dates, self.dates[1:], strict=False)
    ] + [float("inf")]
    super().__init__(self.inter_arrivals)

overall_rate property

overall_rate

Return the overall rate across all intervals.

Returns:

Type Description
float

The time-averaged Poisson rate.

mean property

mean

Return the mean inter-arrival time.

Returns:

Type Description
float

The reciprocal of the overall rate, or infinity.

variance property

variance

Return the variance of the inter-arrival times.

Returns:

Type Description
float

The variance, which is NaN.

median property

median

Return the median inter-arrival time.

Returns:

Type Description
float

The median, which is NaN.

upper_limit property

upper_limit

Return the upper limit of the inter-arrival times.

Returns:

Type Description
float

Infinity.

lower_limit property

lower_limit

Return the lower limit of the inter-arrival times.

Returns:

Type Description
float

Zero.

__repr__

__repr__()

Return a string representation of the PoissonIntervals distribution.

Source code in hciw/dists/distributions.py
1666
1667
1668
def __repr__(self):
    """Return a string representation of the PoissonIntervals distribution."""
    return "PoissonIntervals"

get_intervals

get_intervals()

Build the list of time intervals up to max_sample_date.

Source code in hciw/dists/distributions.py
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
def get_intervals(self):
    """Build the list of time intervals up to ``max_sample_date``."""
    self.num_intervals = len(self.endpoints)
    self.intervals = [(0, self.endpoints[0])]
    i, t1, t2 = 0, 0, 0
    while self.intervals[-1][-1] < self.max_sample_date:
        if i % self.num_intervals == self.num_intervals - 1:
            t2 = t1 + self.endpoints[-1]
        self.intervals.append(
            (
                t1 + self.endpoints[i % self.num_intervals],
                min(
                    t2 + self.endpoints[(i + 1) % self.num_intervals],
                    self.max_sample_date,
                ),
            )
        )
        i += 1
        t1 = t2

get_dates

get_dates()

Sample the arrival dates within each time interval.

Source code in hciw/dists/distributions.py
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
def get_dates(self):
    """Sample the arrival dates within each time interval."""
    self.dates = [0.0]
    for i, interval in enumerate(self.intervals):
        n = hciw.rng.poisson(
            self.rates[i % self.num_intervals] * (interval[1] - interval[0])
        )
        interval_dates = [
            random.uniform(interval[0], interval[1]) for _ in range(n)
        ]
        interval_dates = sorted(interval_dates)
        self.dates += interval_dates

Poisson

Poisson(rate)

Bases: Distribution

The Poisson distribution.

Note that this is a discrete integer distribution, for use with batching.

Attributes:

Name Type Description
rate float

The rate parameter, lambda.

Initialize the Poisson distribution.

Parameters:

Name Type Description Default
rate float

The rate parameter, lambda.

required

Raises:

Type Description
ValueError

If rate is not positive.

Source code in hciw/dists/distributions.py
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
def __init__(self, rate):
    """Initialize the Poisson distribution.

    Args:
        rate (float): The rate parameter, lambda.

    Raises:
        ValueError: If ``rate`` is not positive.
    """
    if rate <= 0.0:
        raise ValueError("Poisson distribution must sample positive numbers only.")
    self.rate = rate

mean property

mean

Return the mean of the Poisson distribution.

Returns:

Type Description
float

The mean.

variance property

variance

Return the variance of the Poisson distribution.

Returns:

Type Description
float

The variance.

median property

median

Return the median of the Poisson distribution.

Returns:

Type Description
float

An approximation of the median.

upper_limit property

upper_limit

Return the upper limit of the Poisson distribution.

Returns:

Type Description
float

Infinity.

lower_limit property

lower_limit

Return the lower limit of the Poisson distribution.

Returns:

Type Description
float

Zero.

sample

sample(t=None, ind=None)

Sample from the Poisson distribution.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
int

A sample from the distribution.

Source code in hciw/dists/distributions.py
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
def sample(self, t=None, ind=None):
    """Sample from the Poisson distribution.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        int: A sample from the distribution.
    """
    return hciw.rng.poisson(lam=self.rate)

__repr__

__repr__()

Return a string representation of the Poisson distribution.

Source code in hciw/dists/distributions.py
1799
1800
1801
def __repr__(self):
    """Return a string representation of the Poisson distribution."""
    return f"Poisson(rate={self.rate})"

Geometric

Geometric(prob)

Bases: Distribution

The Geometric distribution.

Note that this is a discrete integer distribution, for use with batching.

Attributes:

Name Type Description
prob float

The probability parameter.

Initialize the Geometric distribution.

Parameters:

Name Type Description Default
prob float

The probability parameter.

required

Raises:

Type Description
ValueError

If prob is not strictly between 0 and 1.

Source code in hciw/dists/distributions.py
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
def __init__(self, prob):
    """Initialize the Geometric distribution.

    Args:
        prob (float): The probability parameter.

    Raises:
        ValueError: If ``prob`` is not strictly between 0 and 1.
    """
    if prob <= 0.0 or prob >= 1:
        raise ValueError(
            "Geometric distribution must have parameter between 0 and 1."
        )
    self.prob = prob

mean property

mean

Return the mean of the Geometric distribution.

Returns:

Type Description
float

The mean.

variance property

variance

Return the variance of the Geometric distribution.

Returns:

Type Description
float

The variance.

median property

median

Return the median of the Geometric distribution.

Returns:

Type Description
float

The median.

upper_limit property

upper_limit

Return the upper limit of the Geometric distribution.

Returns:

Type Description
float

Infinity.

lower_limit property

lower_limit

Return the lower limit of the Geometric distribution.

Returns:

Type Description
float

Zero.

sample

sample(t=None, ind=None)

Sample from the Geometric distribution.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
int

A sample from the distribution.

Source code in hciw/dists/distributions.py
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
def sample(self, t=None, ind=None):
    """Sample from the Geometric distribution.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        int: A sample from the distribution.
    """
    return hciw.rng.geometric(p=self.prob)

__repr__

__repr__()

Return a string representation of the Geometric distribution.

Source code in hciw/dists/distributions.py
1885
1886
1887
def __repr__(self):
    """Return a string representation of the Geometric distribution."""
    return f"Geometric(prob={self.prob})"

Binomial

Binomial(n, prob)

Bases: Distribution

The Binomial distribution.

Note that this is a discrete integer distribution, for use with batching.

Attributes:

Name Type Description
n int

The total number of experiments.

prob float

The probability parameter.

Initialize the Binomial distribution.

Parameters:

Name Type Description Default
n int

The total number of experiments.

required
prob float

The probability parameter.

required

Raises:

Type Description
ValueError

If prob is not strictly between 0 and 1.

ValueError

If n is not a positive integer.

Source code in hciw/dists/distributions.py
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
def __init__(self, n, prob):
    """Initialize the Binomial distribution.

    Args:
        n (int): The total number of experiments.
        prob (float): The probability parameter.

    Raises:
        ValueError: If ``prob`` is not strictly between 0 and 1.
        ValueError: If ``n`` is not a positive integer.
    """
    if prob <= 0.0 or prob >= 1:
        raise ValueError(
            "Binomial distribution have probability parameter between 0 and 1."
        )
    if not isinstance(n, int) or n <= 0:
        raise ValueError(
            "The number of trials of the Binomial distirbution must be a "
            "positive integer."
        )
    self.n = n
    self.prob = prob

mean property

mean

Return the mean of the Binomial distribution.

Returns:

Type Description
float

The mean.

variance property

variance

Return the variance of the Binomial distribution.

Returns:

Type Description
float

The variance.

median property

median

Return the median of the Binomial distribution.

Returns:

Type Description
float

The rounded mean.

upper_limit property

upper_limit

Return the upper limit of the Binomial distribution.

Returns:

Type Description
float

Infinity.

lower_limit property

lower_limit

Return the lower limit of the Binomial distribution.

Returns:

Type Description
float

Zero.

sample

sample(t=None, ind=None)

Sample from the Binomial distribution.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
int

A sample from the distribution.

Source code in hciw/dists/distributions.py
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
def sample(self, t=None, ind=None):
    """Sample from the Binomial distribution.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        int: A sample from the distribution.
    """
    return hciw.rng.binomial(n=self.n, p=self.prob)

__repr__

__repr__()

Return a string representation of the Binomial distribution.

Source code in hciw/dists/distributions.py
1980
1981
1982
def __repr__(self):
    """Return a string representation of the Binomial distribution."""
    return f"Binomial(n={self.n}, prob={self.prob})"

MixtureDistribution

MixtureDistribution(
    dists: list[Distribution], probs: list[float]
)

Bases: Distribution

A mixture distribution combining multiple probability distributions.

Attributes:

Name Type Description
probs List[float]

The weights of each distribution, summing to 1.

dists List[Distribution]

The distributions in the mixture.

Initialize the MixtureDistribution.

Parameters:

Name Type Description Default
dists List[Distribution]

The distributions to combine in the mixture.

required
probs List[float]

The weights of each distribution, summing to 1.

required
Source code in hciw/dists/distributions.py
2038
2039
2040
2041
2042
2043
2044
2045
2046
def __init__(self, dists: list[Distribution], probs: list[float]) -> None:
    """Initialize the MixtureDistribution.

    Args:
        dists (List[Distribution]): The distributions to combine in the mixture.
        probs (List[float]): The weights of each distribution, summing to 1.
    """
    self.probs = probs
    self.dists = dists

mean property

mean

Return the mean of the mixture distribution.

Returns:

Type Description
float

The weighted mean of the component distributions.

variance property

variance

Return the variance of the mixture distribution.

Returns:

Type Description
float

The weighted variance of the component distributions.

upper_limit property

upper_limit

Return the upper limit of the mixture distribution.

Returns:

Type Description
float

The largest component upper limit, or NaN if any is NaN.

lower_limit property

lower_limit

Return the lower limit of the mixture distribution.

Returns:

Type Description
float

The smallest component lower limit, or NaN if any is NaN.

sample

sample(
    t: float | None = None, ind: Individual | None = None
) -> float

Generate a random sample from the mixture distribution.

Parameters:

Name Type Description Default
t float

The current time. Defaults to None.

None
ind Individual

The individual being sampled. Defaults to None.

None

Returns:

Type Description
float

A sample from one of the distributions, chosen by weight.

Source code in hciw/dists/distributions.py
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
def sample(self, t: float | None = None, ind: Individual | None = None) -> float:
    """Generate a random sample from the mixture distribution.

    Args:
        t (float, optional): The current time. Defaults to None.
        ind (Individual, optional): The individual being sampled. Defaults to None.

    Returns:
        float: A sample from one of the distributions, chosen by weight.
    """
    chosen_dist = random.choices(population=self.dists, weights=self.probs, k=1)[0]

    return chosen_dist.sample(t, ind)

__repr__

__repr__()

Return a string representation of the mixture distribution.

Source code in hciw/dists/distributions.py
2062
2063
2064
def __repr__(self):
    """Return a string representation of the mixture distribution."""
    return "MixtureDistribution"

seed

seed(z)

Set all seeds used to generate random number streams.

Seeds both the standard library random module and the NumPy random generator used by hciw.

Parameters:

Name Type Description Default
z int

The seed value to use.

required
Source code in hciw/auxiliary.py
14
15
16
17
18
19
20
21
22
23
24
def seed(z):
    """Set all seeds used to generate random number streams.

    Seeds both the standard library ``random`` module and the NumPy
    random generator used by ``hciw``.

    Args:
        z (int): The seed value to use.
    """
    random.seed(z)
    hciw.rng = np.random.default_rng(seed=z)

random_choice

random_choice(array, probs=None)

Select a random element from an array using optional weights.

Parameters:

Name Type Description Default
array list

The values to choose from.

required
probs list

Probability weights corresponding to each value. If omitted, a uniform choice is made.

None

Returns:

Type Description

The randomly selected element of array.

Source code in hciw/auxiliary.py
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
def random_choice(array, probs=None):
    """Select a random element from an array using optional weights.

    Args:
        array (list): The values to choose from.
        probs (list, optional): Probability weights corresponding to each
            value. If omitted, a uniform choice is made.

    Returns:
        The randomly selected element of ``array``.
    """
    # If no pdf provided, assume uniform dist:
    if probs is None:
        index = int(random.random() * len(array))
        return array[index]

    # A common case, guaranteed to reach the Exit node;
    # No need to sample for this:
    if (set(probs[:-1]) == {0.0}) and (probs[-1] == 1.0):
        return array[-1]

    # Sample a random value from using pdf
    rdm_num = random.random()
    i, p = 0, probs[0]
    while rdm_num > p:
        i += 1
        p += probs[i]
    return array[i]

truncated_normal

truncated_normal(mean, sd)

Sample from a Normal distribution truncated at zero.

Values less than or equal to zero are resampled until a positive value is obtained.

Parameters:

Name Type Description Default
mean float

The mean of the Normal distribution.

required
sd float

The standard deviation of the Normal distribution.

required

Returns:

Type Description
float

A positive sample from the truncated Normal distribution.

Source code in hciw/auxiliary.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def truncated_normal(mean, sd):
    """Sample from a Normal distribution truncated at zero.

    Values less than or equal to zero are resampled until a positive
    value is obtained.

    Args:
        mean (float): The mean of the Normal distribution.
        sd (float): The standard deviation of the Normal distribution.

    Returns:
        float: A positive sample from the truncated Normal distribution.
    """
    sample = random.normalvariate(mean, sd)
    while sample <= 0.0:
        sample = random.normalvariate(mean, sd)
    return sample

flatten_list

flatten_list(list_of_lists)

Flatten a list of lists into a single list.

Parameters:

Name Type Description Default
list_of_lists list

A list whose elements are themselves lists.

required

Returns:

Type Description
list

A single list containing all elements of the inner lists.

Source code in hciw/auxiliary.py
76
77
78
79
80
81
82
83
84
85
86
87
88
def flatten_list(list_of_lists):
    """Flatten a list of lists into a single list.

    Args:
        list_of_lists (list): A list whose elements are themselves lists.

    Returns:
        list: A single list containing all elements of the inner lists.
    """
    flat = []
    for a_list in list_of_lists:
        flat += a_list
    return flat

no_routing

no_routing(ind)

Send a customer straight to the exit node.

This process-based routing function is a placeholder used when NoArrivals is used.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
list

An empty list of destinations.

Source code in hciw/auxiliary.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def no_routing(ind):
    """Send a customer straight to the exit node.

    This process-based routing function is a placeholder used when
    ``NoArrivals`` is used.

    Args:
        ind (Individual): The individual being routed.

    Returns:
        list: An empty list of destinations.
    """
    return []

Disciplines

disciplines

Service disciplines for selecting the next individual from a queue.

Individual

Individual(
    id_number,
    customer_class="Customer",
    priority_class=0,
    simulation=False,
)

Represent an individual agent in a queueing network simulation.

An Individual models the flow of an agent through a queueing network, tracking its arrival time, service time, priority and other relevant data.

Attributes:

Name Type Description
arrival_date bool or float

Timestamp of the individual's arrival.

service_start_date bool or float

Timestamp the current service started.

service_time bool or float

Duration of the current service.

service_end_date bool or float

Timestamp the current service ends.

exit_date bool or float

Timestamp the individual exits the system.

id_number int

Unique identifier for the individual.

data_records list

The data records attached to the individual.

customer_class str

The customer class of the individual.

previous_class str

The previous customer class of the individual.

priority_class int

The priority class of the individual.

prev_priority_class int

The previous priority class of the individual.

original_class str

The original customer class of the individual.

is_blocked bool

Whether the individual is blocked.

server bool or Server

The server assigned to the individual, if any.

queue_size_at_arrival bool or int

Queue size when the individual arrived.

queue_size_at_departure bool or int

Queue size when the individual departed.

destination bool or int

The destination node in the network.

interrupted bool

Whether the individual's service was interrupted.

node bool or int

The node where the individual is located.

simulation bool or Simulation

The simulation the individual belongs to.

Example

.. code-block:: python

individual = Individual(
    id_number=1, customer_class='Gold', priority_class=1,
    simulation=True,
)

Initialise an Individual.

Parameters:

Name Type Description Default
id_number int

A unique identifier for the individual.

required
customer_class str

The customer class the individual belongs to. Defaults to 'Customer'.

'Customer'
priority_class int

The priority class of the individual. Defaults to 0.

0
simulation bool or Simulation

The simulation the individual is part of. Defaults to False.

False
Source code in hciw/individual.py
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
def __init__(
    self, id_number, customer_class="Customer", priority_class=0, simulation=False
):
    """Initialise an ``Individual``.

    Args:
        id_number (int): A unique identifier for the individual.
        customer_class (str, optional): The customer class the individual
            belongs to. Defaults to ``'Customer'``.
        priority_class (int, optional): The priority class of the
            individual. Defaults to ``0``.
        simulation (bool or Simulation, optional): The simulation the
            individual is part of. Defaults to ``False``.
    """
    self.arrival_date = False
    self.service_start_date = False
    self.service_time: float | bool | str = False
    self.service_end_date = False
    self.exit_date = False
    self.id_number = id_number
    self.data_records = []
    self.customer_class = customer_class
    self.previous_class = customer_class
    self.priority_class = priority_class
    self.prev_priority_class = priority_class
    self.original_class = customer_class
    self.is_blocked = False
    self.server: Server | bool = False
    self.queue_size_at_arrival: int | bool = False
    self.queue_size_at_departure: int | bool = False
    self.destination: int | bool = False
    self.interrupted = False
    self.node = False
    self.simulation = simulation

Simulation

Simulation(
    network,
    exact=False,
    name="Simulation",
    tracker=None,
    deadlock_detector=None,
    node_class=None,
    arrival_node_class=None,
    exit_node_class=None,
    individual_class=None,
    server_class=None,
    initial_individuals=None,
)

The engine that runs a queueing network simulation.

A Simulation drives customers through the network's nodes over simulated time, collecting data records and tracking state.

Attributes:

Name Type Description
current_time float

The current simulated time.

network Network

The network being simulated.

name str

The name of the simulation.

NodeTypes list

The node classes used at each service centre.

ArrivalNodeType type

The class used for the arrival node.

ExitNodeType type

The class used for the exit node.

IndividualType type

The class used for individuals.

ServerType type

The class used for servers.

deadlock_detector object

The detector used to identify deadlocks.

inter_arrival_times dict

Inter-arrival distributions by node and class.

service_times dict

Service distributions by node and class.

batch_sizes dict

Batch size distributions by node and class.

number_of_priority_classes int

The number of priority classes.

transitive_nodes list

The service centre nodes.

nodes list

The arrival, transitive and exit nodes.

active_nodes list

The nodes that are not the exit node.

routers dict

The routing objects by customer class.

statetracker StateTracker

The state tracker in use.

times_dictionary dict

Maps hashed states to the time first seen.

times_to_deadlock dict

The time from each state to deadlock.

unchecked_blockage bool

Whether a blockage needs checking.

Initialise a simulation instance.

Parameters:

Name Type Description Default
network Network

The network to simulate.

required
exact int

If truthy, use exact Decimal arithmetic with this many digits of precision. Defaults to False.

False
name str

The name of the simulation. Defaults to "Simulation".

'Simulation'
tracker StateTracker

The state tracker to use. Defaults to a plain StateTracker.

None
deadlock_detector object

The deadlock detector to use. Defaults to deadlock.NoDetection().

None
node_class type or list

The node class or classes to use. Defaults to Node.

None
arrival_node_class type

The arrival node class to use. Defaults to ArrivalNode.

None
exit_node_class type

The exit node class to use. Defaults to ExitNode.

None
individual_class type

The individual class to use. Defaults to Individual.

None
server_class type

The server class to use. Defaults to Server.

None
initial_individuals list[list]

A list of backlogged individuals to seed the simulation with, in the form [[id, customer_class, arrival_date, node_index], ...] where arrival_date is the individual's historical arrival date (typically negative, meaning it arrived before the simulation began). If provided, these individuals are placed at their specified nodes before the simulation begins. Defaults to None.

None
Source code in hciw/simulation.py
 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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def __init__(  # noqa: PLR0913, PLR0917 - public configuration API
    self,
    network,
    exact=False,
    name="Simulation",
    tracker=None,
    deadlock_detector=None,
    node_class=None,
    arrival_node_class=None,
    exit_node_class=None,
    individual_class=None,
    server_class=None,
    initial_individuals=None,
):
    """Initialise a simulation instance.

    Args:
        network (Network): The network to simulate.
        exact (int, optional): If truthy, use exact ``Decimal`` arithmetic
            with this many digits of precision. Defaults to ``False``.
        name (str, optional): The name of the simulation. Defaults to
            ``"Simulation"``.
        tracker (StateTracker, optional): The state tracker to use.
            Defaults to a plain ``StateTracker``.
        deadlock_detector (object, optional): The deadlock detector to
            use. Defaults to ``deadlock.NoDetection()``.
        node_class (type or list, optional): The node class or classes to
            use. Defaults to ``Node``.
        arrival_node_class (type, optional): The arrival node class to use.
            Defaults to ``ArrivalNode``.
        exit_node_class (type, optional): The exit node class to use.
            Defaults to ``ExitNode``.
        individual_class (type, optional): The individual class to use.
            Defaults to ``Individual``.
        server_class (type, optional): The server class to use. Defaults
            to ``Server``.
        initial_individuals (list[list], optional): A list of backlogged
            individuals to seed the simulation with, in the form
            ``[[id, customer_class, arrival_date, node_index], ...]`` where
            ``arrival_date`` is the individual's historical arrival date
            (typically negative, meaning it arrived before the simulation
            began). If provided, these individuals are placed at their
            specified nodes before the simulation begins. Defaults to
            ``None``.
    """
    self.current_time = 0.0
    self.network = network
    self.set_classes(
        node_class,
        arrival_node_class,
        exit_node_class,
        individual_class,
        server_class,
    )
    if exact:
        self.NodeTypes = [ExactNode for _ in range(network.number_of_nodes)]
        self.ArrivalNodeType = ExactArrivalNode
        getcontext().prec = exact

    self.name = name
    self.deadlock_detector = (
        deadlock.NoDetection() if deadlock_detector is None else deadlock_detector
    )
    self.inter_arrival_times = self.find_arrival_dists()
    self.service_times = self.find_service_dists()
    self.batch_sizes = self.find_batching_dists()
    self.show_simulation_to_distributions()
    self.number_of_priority_classes = self.network.number_of_priority_classes
    self.transitive_nodes = [
        node_type(i + 1, self) for i, node_type in enumerate(self.NodeTypes)
    ]
    self.exit_node = self.ExitNodeType()
    self.nodes = [
        self.ArrivalNodeType(self),
        *self.transitive_nodes,
        self.exit_node,
    ]
    self.active_nodes = self.nodes[:-1]
    self.routers = self.find_and_initialise_routers()
    self.nodes[0].initialise()
    if tracker is None:
        self.statetracker = trackers.StateTracker()
    else:
        self.statetracker = tracker
    self.statetracker.initialise(self)
    if initial_individuals is not None:
        waitlist.create_existing_customers_from_list(initial_individuals, self)
    self.times_dictionary = {self.statetracker.hash_state(): 0.0}
    self.times_to_deadlock = {}
    self.unchecked_blockage = False

number_of_individuals property

number_of_individuals

Return the number of individuals currently in the system.

Returns:

Type Description
int

The number of individuals currently in the system.

find_arrival_dists

find_arrival_dists()

Create the arrival time distribution objects for each node and class.

Returns:

Type Description
dict

Arrival time distributions keyed by node and customer class.

Source code in hciw/simulation.py
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def find_arrival_dists(self):
    """Create the arrival time distribution objects for each node and class.

    Returns:
        dict: Arrival time distributions keyed by node and customer class.
    """
    return {
        node + 1: {
            clss: copy.deepcopy(
                self.network.customer_classes[clss].arrival_distributions[node]
            )
            for clss in self.network.customer_class_names
        }
        for node in range(self.network.number_of_nodes)
    }

find_service_dists

find_service_dists()

Create the service time distribution objects for each node and class.

Returns:

Type Description
dict

Service time distributions keyed by node and customer class.

Source code in hciw/simulation.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def find_service_dists(self):
    """Create the service time distribution objects for each node and class.

    Returns:
        dict: Service time distributions keyed by node and customer class.
    """
    return {
        node + 1: {
            clss: copy.deepcopy(
                self.network.customer_classes[clss].service_distributions[node]
            )
            for clss in self.network.customer_class_names
        }
        for node in range(self.network.number_of_nodes)
    }

find_batching_dists

find_batching_dists()

Create the batch size distribution objects for each node and class.

Returns:

Type Description
dict

Batch size distributions keyed by node and customer class.

Source code in hciw/simulation.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def find_batching_dists(self):
    """Create the batch size distribution objects for each node and class.

    Returns:
        dict: Batch size distributions keyed by node and customer class.
    """
    return {
        node + 1: {
            clss: copy.deepcopy(
                self.network.customer_classes[clss].batching_distributions[node]
            )
            for clss in self.network.customer_class_names
        }
        for node in range(self.network.number_of_nodes)
    }

show_simulation_to_distributions

show_simulation_to_distributions()

Add the simulation as an attribute of the distribution objects.

Source code in hciw/simulation.py
208
209
210
211
212
213
214
215
def show_simulation_to_distributions(self):
    """Add the simulation as an attribute of the distribution objects."""
    for clss in self.network.customer_class_names:
        for nd in range(self.network.number_of_nodes):
            if self.inter_arrival_times[nd + 1][clss] is not None:
                self.inter_arrival_times[nd + 1][clss].simulation = self
                self.service_times[nd + 1][clss].simulation = self
                self.batch_sizes[nd + 1][clss].simulation = self

find_and_initialise_routers

find_and_initialise_routers()

Initialise and return the routing objects.

Returns:

Type Description
dict

Routing objects keyed by customer class.

Source code in hciw/simulation.py
217
218
219
220
221
222
223
224
225
226
227
def find_and_initialise_routers(self):
    """Initialise and return the routing objects.

    Returns:
        dict: Routing objects keyed by customer class.
    """
    routers_dict = {}
    for clss in self.network.customer_class_names:
        routers_dict[clss] = self.network.customer_classes[clss].routing
        routers_dict[clss].initialise(self)
    return routers_dict

find_next_active_node

find_next_active_node()

Return the active node with the earliest next event date.

If several nodes share the earliest date, one is chosen at random.

Returns:

Type Description
Node

The next active node.

Source code in hciw/simulation.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
def find_next_active_node(self):
    """Return the active node with the earliest next event date.

    If several nodes share the earliest date, one is chosen at random.

    Returns:
        Node: The next active node.
    """
    mindate = float("Inf")
    next_active_nodes = []
    for nd in self.active_nodes:
        if nd.next_event_date < mindate:
            mindate = nd.next_event_date
            next_active_nodes = [nd]
        elif nd.next_event_date == mindate:
            next_active_nodes.append(nd)
    if len(next_active_nodes) > 1:
        return random_choice(next_active_nodes)
    return next_active_nodes[0]

get_all_individuals

get_all_individuals()

Return all individuals with at least one data record.

Returns:

Type Description
list

The individuals with at least one data record.

Source code in hciw/simulation.py
249
250
251
252
253
254
255
256
257
258
259
def get_all_individuals(self):
    """Return all individuals with at least one data record.

    Returns:
        list: The individuals with at least one data record.
    """
    return [
        individual
        for node in [*self.transitive_nodes, self.exit_node]
        for individual in node.all_individuals
    ]

get_all_records

get_all_records(only=None, include_incomplete=False)

Get data records from all individuals.

Parameters:

Name Type Description Default
only list

The record types to include. Defaults to service, baulk, rejection, renege and interrupted service records.

None
include_incomplete bool

Whether to include records for individuals still in the system. Defaults to False.

False

Returns:

Type Description
list

The collected data records.

Source code in hciw/simulation.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
def get_all_records(
    self,
    only=None,
    include_incomplete=False,
):
    """Get data records from all individuals.

    Args:
        only (list, optional): The record types to include. Defaults to
            service, baulk, rejection, renege and interrupted service
            records.
        include_incomplete (bool, optional): Whether to include records
            for individuals still in the system. Defaults to ``False``.

    Returns:
        list: The collected data records.
    """
    if only is None:
        only = ["service", "baulk", "rejection", "renege", "interrupted service"]
    records = []
    for individual in self.get_all_individuals():
        records.extend(
            record
            for record in individual.data_records
            if record.record_type in only
        )
        if include_incomplete and individual.node != -1:
            incomplete_record = self.nodes[individual.node].write_incomplete_record(
                individual
            )
            records.append(incomplete_record)
    self.all_records = records
    return records

set_classes

set_classes(
    node_class,
    arrival_node_class,
    exit_node_class,
    individual_class,
    server_class,
)

Set the node, individual and server classes used by the simulation.

Parameters:

Name Type Description Default
node_class type or list

The node class or classes to use. If a list, it must have one entry per node.

required
arrival_node_class type

The arrival node class to use, or None for the default.

required
exit_node_class type

The exit node class to use, or None for the default.

required
individual_class type

The individual class to use, or None for the default.

required
server_class type

The server class to use, or None for the default.

required

Raises:

Type Description
ValueError

If node_class is a list whose length does not match the number of nodes.

Source code in hciw/simulation.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
def set_classes(  # noqa: PLR0912 - per-class selection branch chain
    self,
    node_class,
    arrival_node_class,
    exit_node_class,
    individual_class,
    server_class,
):
    """Set the node, individual and server classes used by the simulation.

    Args:
        node_class (type or list): The node class or classes to use. If a
            list, it must have one entry per node.
        arrival_node_class (type): The arrival node class to use, or
            ``None`` for the default.
        exit_node_class (type): The exit node class to use, or ``None``
            for the default.
        individual_class (type): The individual class to use, or ``None``
            for the default.
        server_class (type): The server class to use, or ``None`` for the
            default.

    Raises:
        ValueError: If ``node_class`` is a list whose length does not match
            the number of nodes.
    """
    if arrival_node_class is not None:
        self.ArrivalNodeType = arrival_node_class
    else:
        self.ArrivalNodeType = ArrivalNode

    if exit_node_class is not None:
        self.ExitNodeType = exit_node_class
    else:
        self.ExitNodeType = ExitNode

    if node_class is not None:
        if not isinstance(node_class, list):
            self.NodeTypes = [
                node_class for _ in range(self.network.number_of_nodes)
            ]
        else:
            if len(node_class) != self.network.number_of_nodes:
                raise ValueError(
                    "Ensure consistant number of nodes is used throughout."
                )
            self.NodeTypes = node_class
    else:
        self.NodeTypes = [Node for _ in range(self.network.number_of_nodes)]

    if individual_class is not None:
        self.IndividualType = individual_class
    else:
        self.IndividualType = Individual

    if server_class is not None:
        self.ServerType = server_class
    else:
        self.ServerType = Server

event_and_return_nextnode

event_and_return_nextnode(next_active_node)

Carry out an event and return the next active node.

Parameters:

Name Type Description Default
next_active_node Node

The node whose event to carry out.

required

Returns:

Type Description
Node

The next active node.

Source code in hciw/simulation.py
355
356
357
358
359
360
361
362
363
364
365
366
367
def event_and_return_nextnode(self, next_active_node):
    """Carry out an event and return the next active node.

    Args:
        next_active_node (Node): The node whose event to carry out.

    Returns:
        Node: The next active node.
    """
    next_active_node.have_event()
    for node in self.transitive_nodes:
        node.update_next_event_date()
    return self.find_next_active_node()

simulate_until_deadlock

simulate_until_deadlock()

Run the simulation until deadlock is reached.

Source code in hciw/simulation.py
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
def simulate_until_deadlock(self):
    """Run the simulation until deadlock is reached."""
    deadlocked = False
    next_active_node = self.find_next_active_node()
    self.current_time = next_active_node.next_event_date
    time_of_deadlock = self.current_time
    while not deadlocked:
        next_active_node = self.event_and_return_nextnode(next_active_node)
        current_state = self.statetracker.hash_state()
        if current_state not in self.times_dictionary:
            self.times_dictionary[current_state] = self.current_time
        if self.unchecked_blockage:
            deadlocked = self.deadlock_detector.detect_deadlock()
            self.unchecked_blockage = False
        if deadlocked:
            time_of_deadlock = self.current_time
        self.current_time = next_active_node.next_event_date

    self.wrap_up_servers(time_of_deadlock)
    self.times_to_deadlock = {
        state: time_of_deadlock - self.times_dictionary[state]
        for state in self.times_dictionary
    }

simulate_until_max_time

simulate_until_max_time(
    max_simulation_time, progress_bar=False
)

Run the simulation until a maximum simulated time is reached.

Parameters:

Name Type Description Default
max_simulation_time float

The time to stop the simulation at.

required
progress_bar bool

Whether to display a progress bar. Defaults to False.

False
Source code in hciw/simulation.py
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
def simulate_until_max_time(self, max_simulation_time, progress_bar=False):
    """Run the simulation until a maximum simulated time is reached.

    Args:
        max_simulation_time (float): The time to stop the simulation at.
        progress_bar (bool, optional): Whether to display a progress bar.
            Defaults to ``False``.
    """
    next_active_node = self.find_next_active_node()
    self.current_time = next_active_node.next_event_date

    if progress_bar:
        self.progress_bar = tqdm.tqdm(total=max_simulation_time)

    while self.current_time < max_simulation_time:
        next_active_node = self.event_and_return_nextnode(next_active_node)
        self.statetracker.timestamp()

        if progress_bar:
            remaining_time = max_simulation_time - self.progress_bar.n
            time_increment = next_active_node.next_event_date - self.current_time
            self.progress_bar.update(min(time_increment, remaining_time))

        self.current_time = next_active_node.next_event_date
    self.current_time = max_simulation_time

    self.wrap_up_servers(max_simulation_time)
    if progress_bar:
        remaining_time = max(max_simulation_time - self.progress_bar.n, 0)
        self.progress_bar.update(remaining_time)
        self.progress_bar.close()

simulate_until_max_customers

simulate_until_max_customers(
    max_customers, progress_bar=False, method="Complete"
)

Run the simulation until a number of customers is reached.

Parameters:

Name Type Description Default
max_customers int

The number of customers to stop at.

required
progress_bar bool

Whether to display a progress bar. Defaults to False.

False
method str

The measure used to count customers. One of:

  • 'Complete': count customers that reach the Exit Node after completing their journey.
  • 'Finish': count customers that reach the Exit Node, whether or not they completed their journey (including baulkers and renegers).
  • 'Arrive': count customers spawned at the Arrival Node.
  • 'Accept': count customers spawned and accepted (not rejected) at the Arrival Node.

Defaults to 'Complete'.

'Complete'

Raises:

Type Description
ValueError

If method is not one of the valid options.

Source code in hciw/simulation.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
def simulate_until_max_customers(  # noqa: C901 - inherent event-loop branching
    self, max_customers, progress_bar=False, method="Complete"
):
    """Run the simulation until a number of customers is reached.

    Args:
        max_customers (int): The number of customers to stop at.
        progress_bar (bool, optional): Whether to display a progress bar.
            Defaults to ``False``.
        method (str, optional): The measure used to count customers. One
            of:

            - ``'Complete'``: count customers that reach the Exit Node
              after completing their journey.
            - ``'Finish'``: count customers that reach the Exit Node,
              whether or not they completed their journey (including
              baulkers and renegers).
            - ``'Arrive'``: count customers spawned at the Arrival Node.
            - ``'Accept'``: count customers spawned and accepted (not
              rejected) at the Arrival Node.

            Defaults to ``'Complete'``.

    Raises:
        ValueError: If ``method`` is not one of the valid options.
    """
    next_active_node = self.find_next_active_node()
    self.current_time = next_active_node.next_event_date

    if progress_bar:
        self.progress_bar = tqdm.tqdm(total=max_customers)

    if method == "Complete":

        def check():
            return self.exit_node.number_of_completed_individuals
    elif method == "Finish":

        def check():
            return self.exit_node.number_of_individuals
    elif method == "Arrive":

        def check():
            return self.nodes[0].number_of_individuals
    elif method == "Accept":

        def check():
            return self.nodes[0].number_accepted_individuals
    else:
        raise ValueError("Invalid 'method' for 'simulate_until_max_customers'.")

    previous_time = self.current_time
    while check() < max_customers:
        old_check = check()
        next_active_node = self.event_and_return_nextnode(next_active_node)
        self.statetracker.timestamp()

        if progress_bar:
            remaining_time = max_customers - self.progress_bar.n
            time_increment = check() - old_check
            self.progress_bar.update(min(time_increment, remaining_time))

        previous_time = self.current_time
        self.current_time = next_active_node.next_event_date
    self.current_time = previous_time

    self.wrap_up_servers(self.current_time)

    if progress_bar:
        remaining_time = max(max_customers - self.progress_bar.n, 0)
        self.progress_bar.update(remaining_time)
        self.progress_bar.close()

wrap_up_servers

wrap_up_servers(current_time)

Update server times and find the overall server utilisation.

Updates each server's total time and busy time at the end of the simulation run, and finds the overall server utilisation for each node.

Parameters:

Name Type Description Default
current_time float

The time at which the run finished.

required
Source code in hciw/simulation.py
498
499
500
501
502
503
504
505
506
507
508
509
def wrap_up_servers(self, current_time):
    """Update server times and find the overall server utilisation.

    Updates each server's total time and busy time at the end of the
    simulation run, and finds the overall server utilisation for each node.

    Args:
        current_time (float): The time at which the run finished.
    """
    for nd in self.transitive_nodes:
        nd.wrap_up_servers(current_time)
        nd.find_server_utilisation()

random_choice

random_choice(array, probs=None)

Select a random element from an array using optional weights.

Parameters:

Name Type Description Default
array list

The values to choose from.

required
probs list

Probability weights corresponding to each value. If omitted, a uniform choice is made.

None

Returns:

Type Description

The randomly selected element of array.

Source code in hciw/auxiliary.py
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
def random_choice(array, probs=None):
    """Select a random element from an array using optional weights.

    Args:
        array (list): The values to choose from.
        probs (list, optional): Probability weights corresponding to each
            value. If omitted, a uniform choice is made.

    Returns:
        The randomly selected element of ``array``.
    """
    # If no pdf provided, assume uniform dist:
    if probs is None:
        index = int(random.random() * len(array))
        return array[index]

    # A common case, guaranteed to reach the Exit node;
    # No need to sample for this:
    if (set(probs[:-1]) == {0.0}) and (probs[-1] == 1.0):
        return array[-1]

    # Sample a random value from using pdf
    rdm_num = random.random()
    i, p = 0, probs[0]
    while rdm_num > p:
        i += 1
        p += probs[i]
    return array[i]

FIFO

FIFO(individuals: list[Individual], t: float) -> Individual

Select individuals first in, first out.

Parameters:

Name Type Description Default
individuals List[Individual]

The individuals in the queue.

required
t float

The current simulation time.

required

Returns:

Type Description
Individual

The individual at the head of the queue.

Source code in hciw/disciplines.py
13
14
15
16
17
18
19
20
21
22
23
def FIFO(individuals: list[Individual], t: float) -> Individual:
    """Select individuals first in, first out.

    Args:
        individuals (List[Individual]): The individuals in the queue.
        t (float): The current simulation time.

    Returns:
        Individual: The individual at the head of the queue.
    """
    return individuals[0]

SIRO

SIRO(individuals: list[Individual], t: float) -> Individual

Select individuals in random order.

Parameters:

Name Type Description Default
individuals List[Individual]

The individuals in the queue.

required
t float

The current simulation time.

required

Returns:

Type Description
Individual

A randomly selected individual from the queue.

Source code in hciw/disciplines.py
26
27
28
29
30
31
32
33
34
35
36
def SIRO(individuals: list[Individual], t: float) -> Individual:
    """Select individuals in random order.

    Args:
        individuals (List[Individual]): The individuals in the queue.
        t (float): The current simulation time.

    Returns:
        Individual: A randomly selected individual from the queue.
    """
    return random_choice(individuals)

LIFO

LIFO(individuals: list[Individual], t: float) -> Individual

Select individuals last in, first out.

Parameters:

Name Type Description Default
individuals List[Individual]

The individuals in the queue.

required
t float

The current simulation time.

required

Returns:

Type Description
Individual

The individual who joined the queue most recently.

Source code in hciw/disciplines.py
39
40
41
42
43
44
45
46
47
48
49
def LIFO(individuals: list[Individual], t: float) -> Individual:
    """Select individuals last in, first out.

    Args:
        individuals (List[Individual]): The individuals in the queue.
        t (float): The current simulation time.

    Returns:
        Individual: The individual who joined the queue most recently.
    """
    return individuals[-1]

wait_time_over_benchmark

wait_time_over_benchmark(
    individuals: list[Individual], t: float | Decimal
) -> Individual

Select the individual furthest past their customer class's benchmark.

Each customer class may declare a target waiting time, set with create_network(customer_class_benchmarks={...}). The individual chosen is the one for whom t - arrival_date - benchmark is greatest: that is, the individual who has waited furthest beyond their target. This is the discipline HCiw uses to express a service target such as "no one should wait more than X time units", by treating any wait beyond X as a first claim on the next free server.

Parameters:

Name Type Description Default
individuals list[Individual]

The individuals in the queue.

required
t float or Decimal

The current simulation time.

required

Returns:

Type Description
Individual

The individual furthest past their benchmark. Ties are broken by queue order, so the earliest of the tied individuals is served next.

Raises:

Type Description
ValueError

If any individual's customer class has no benchmark set.

Source code in hciw/disciplines.py
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
def wait_time_over_benchmark(
    individuals: list[Individual], t: float | Decimal
) -> Individual:
    """Select the individual furthest past their customer class's benchmark.

    Each customer class may declare a target waiting time, set with
    ``create_network(customer_class_benchmarks={...})``. The individual chosen
    is the one for whom ``t - arrival_date - benchmark`` is greatest: that is,
    the individual who has waited furthest beyond their target. This is the
    discipline HCiw uses to express a service target such as "no one should
    wait more than X time units", by treating any wait beyond X as a first
    claim on the next free server.

    Args:
        individuals (list[Individual]): The individuals in the queue.
        t (float or Decimal): The current simulation time.

    Returns:
        Individual: The individual furthest past their benchmark. Ties are
            broken by queue order, so the earliest of the tied individuals is
            served next.

    Raises:
        ValueError: If any individual's customer class has no benchmark set.
    """
    simulation = cast("Simulation", individuals[0].simulation)
    customer_classes = simulation.network.customer_classes
    over_benchmarks = []
    for ind in individuals:
        benchmark = customer_classes[ind.customer_class].benchmark
        if benchmark is None:
            raise ValueError(
                f"Customer class '{ind.customer_class}' has no benchmark set; "
                "pass customer_class_benchmarks to hciw.create_network."
            )
        if isinstance(t, Decimal):
            if not isinstance(benchmark, Decimal):
                benchmark = Decimal(str(benchmark))
            over = t - Decimal(str(ind.arrival_date)) - benchmark
        else:
            over = t - ind.arrival_date - float(benchmark)
        over_benchmarks.append(over)
    return individuals[over_benchmarks.index(max(over_benchmarks))]

Schedules

schedules

Server schedules for varying the number of servers over time.

Schedule

Schedule(
    numbers_of_servers: list[int],
    shift_end_dates: list[float],
    preemption: bool | str = False,
    offset: float = 0.0,
)

Store server schedule information and generate the next shifts.

Attributes:

Name Type Description
schedule_type str

The type of the schedule.

shift_end_dates List[float]

The end dates of each shift.

numbers_of_servers List[int]

The number of servers for each shift.

preemption Union[bool, str]

The pre-emption option.

cyclelength float

The length of the schedule cycle.

offset float

The offset applied to the schedule.

Initialise a Schedule instance.

Parameters:

Name Type Description Default
numbers_of_servers List[int]

The number of servers working at each shift.

required
shift_end_dates List[float]

The end dates of each shift.

required
preemption Union[bool, str]

The pre-emption option, one of 'resume', 'restart', 'resample', 'reroute' or False. Defaults to False.

False
offset float

A non-negative offset applied to the schedule. Defaults to 0.0.

0.0

Raises:

Type Description
ValueError

If preemption or offset is invalid.

Source code in hciw/schedules.py
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
def __init__(
    self,
    numbers_of_servers: list[int],
    shift_end_dates: list[float],
    preemption: bool | str = False,
    offset: float = 0.0,
) -> None:
    """Initialise a ``Schedule`` instance.

    Args:
        numbers_of_servers (List[int]): The number of servers working at
            each shift.
        shift_end_dates (List[float]): The end dates of each shift.
        preemption (Union[bool, str], optional): The pre-emption option,
            one of ``'resume'``, ``'restart'``, ``'resample'``,
            ``'reroute'`` or ``False``. Defaults to ``False``.
        offset (float, optional): A non-negative offset applied to the
            schedule. Defaults to ``0.0``.

    Raises:
        ValueError: If ``preemption`` or ``offset`` is invalid.
    """
    if preemption not in [False, "resume", "restart", "resample", "reroute"]:
        raise ValueError(
            "Preemption must be False, 'resume', 'restart', "
            "'resample', 'reroute', or False."
        )
    if not isinstance(offset, float):
        raise ValueError(  # noqa: TRY004 - public API raises ValueError
            "Offset should be a positive float."
        )
    if offset < 0.0:
        raise ValueError("Offset should be a positive float.")
    self.schedule_type = "schedule"
    self.shift_end_dates = shift_end_dates
    self.numbers_of_servers = numbers_of_servers
    self.preemption = preemption
    self.cyclelength = self.shift_end_dates[-1]
    self.offset = offset

initialise

initialise() -> None

Initialise the generator at the beginning of a simulation.

Source code in hciw/schedules.py
58
59
60
61
62
63
64
65
def initialise(self) -> None:
    """Initialise the generator at the beginning of a simulation."""
    self.c = 0
    self.next_shift_change_date = self.offset
    self.next_c = self.numbers_of_servers[0]
    self.schedule_generator = self.get_schedule_generator(
        self.shift_end_dates, self.numbers_of_servers, self.offset
    )

get_schedule_generator

get_schedule_generator(
    boundaries: list[float],
    values: list[int],
    offset: float,
) -> Generator[tuple[float, int], None, None]

Yield the next shift date and server count.

Parameters:

Name Type Description Default
boundaries List[float]

The shift boundaries.

required
values List[int]

The number of servers corresponding to each boundary.

required
offset float

The offset applied to the schedule.

required

Yields:

Type Description
Tuple[float, int]

The next shift date and the number of servers.

Source code in hciw/schedules.py
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
def get_schedule_generator(
    self, boundaries: list[float], values: list[int], offset: float
) -> Generator[tuple[float, int], None, None]:
    """Yield the next shift date and server count.

    Args:
        boundaries (List[float]): The shift boundaries.
        values (List[int]): The number of servers corresponding to each
            boundary.
        offset (float): The offset applied to the schedule.

    Yields:
        Tuple[float, int]: The next shift date and the number of servers.
    """
    num_boundaries = len(boundaries)
    index = 0
    date = 0
    while True:
        date = (
            offset
            + boundaries[index % num_boundaries]
            + ((index) // num_boundaries * self.cyclelength)
        )
        index += 1
        yield date, values[index % num_boundaries]

get_next_shift

get_next_shift() -> None

Update the next shift from the generator.

Source code in hciw/schedules.py
93
94
95
96
97
98
def get_next_shift(self) -> None:
    """Update the next shift from the generator."""
    self.c = self.next_c
    date, c = next(self.schedule_generator)
    self.next_shift_change_date = date
    self.next_c = c

Slotted

Slotted(
    slots,
    slot_sizes,
    capacitated=False,
    preemption=False,
    offset=0.0,
)

Bases: Schedule

A schedule divided into slots with a number of servers per slot.

Attributes:

Name Type Description
schedule_type str

The type of the schedule.

offset float

The offset applied to the schedule.

slots list

The slot boundaries.

slot_sizes list

The number of servers in each slot.

next_slot_sizes list

The slot sizes of the next cycle.

capacitated bool

Whether the slots are capacitated.

preemption Union[bool, str]

The pre-emption option.

cyclelength float

The length of the schedule cycle.

c int

The current number of servers.

Initialise a Slotted schedule instance.

Parameters:

Name Type Description Default
slots list

The slot boundaries.

required
slot_sizes list

The number of servers in each slot.

required
capacitated bool

Whether the slots are capacitated. Defaults to False.

False
preemption Union[bool, str]

The pre-emption option, one of 'resume', 'restart', 'resample' or False. Defaults to False.

False
offset float

A non-negative offset applied to the schedule. Defaults to 0.0.

0.0

Raises:

Type Description
ValueError

If preemption or offset is invalid.

Source code in hciw/schedules.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def __init__(
    self, slots, slot_sizes, capacitated=False, preemption=False, offset=0.0
):
    """Initialise a ``Slotted`` schedule instance.

    Args:
        slots (list): The slot boundaries.
        slot_sizes (list): The number of servers in each slot.
        capacitated (bool, optional): Whether the slots are capacitated.
            Defaults to ``False``.
        preemption (Union[bool, str], optional): The pre-emption option,
            one of ``'resume'``, ``'restart'``, ``'resample'`` or
            ``False``. Defaults to ``False``.
        offset (float, optional): A non-negative offset applied to the
            schedule. Defaults to ``0.0``.

    Raises:
        ValueError: If ``preemption`` or ``offset`` is invalid.
    """
    if not capacitated and preemption is not False:
        raise ValueError(
            "Pre-emption options not availale for non-capacitated slots."
        )
    if preemption not in [False, "resume", "restart", "resample"]:
        raise ValueError(
            "Pre-emption options should be either 'resume', 'restart', "
            "'resample', or False."
        )
    if not isinstance(offset, float):
        raise ValueError(  # noqa: TRY004 - public API raises ValueError
            "Offset should be a positive float."
        )
    if offset < 0.0:
        raise ValueError("Offset should be a positive float.")
    self.schedule_type = "slotted"
    self.offset = offset
    self.slots = slots
    self.slot_sizes = slot_sizes
    self.next_slot_sizes = [self.slot_sizes[-1], *self.slot_sizes[:-1]]
    self.capacitated = capacitated
    self.preemption = preemption
    self.cyclelength = self.slots[-1]
    self.c = 0

initialise

initialise()

Initialise the generator at the beginning of a simulation.

Source code in hciw/schedules.py
160
161
162
163
164
165
def initialise(self):
    """Initialise the generator at the beginning of a simulation."""
    self.schedule_generator = self.get_schedule_generator(
        self.slots, self.next_slot_sizes, self.offset
    )
    self.get_next_slot()

get_next_slot

get_next_slot()

Update the next slot date and size from the generator.

Source code in hciw/schedules.py
167
168
169
170
171
def get_next_slot(self):
    """Update the next slot date and size from the generator."""
    date, size = next(self.schedule_generator)
    self.next_slot_date = date
    self.slot_size = size

Routing

routing

Routing classes that determine the next node an individual visits.

NetworkRouting

NetworkRouting(routers)

Hold a number of routing objects, one for each node.

Set up the router objects for each node.

Parameters:

Name Type Description Default
routers list

The routing objects, one for each node.

required
Source code in hciw/routing/routing.py
11
12
13
14
15
16
17
def __init__(self, routers):
    """Set up the router objects for each node.

    Args:
        routers (list): The routing objects, one for each node.
    """
    self.routers = routers

initialise

initialise(simulation)

Give the simulation and node attributes to the routing objects.

Parameters:

Name Type Description Default
simulation Simulation

The simulation the routers belong to.

required
Source code in hciw/routing/routing.py
19
20
21
22
23
24
25
26
27
28
29
30
def initialise(self, simulation):
    """Give the simulation and node attributes to the routing objects.

    Args:
        simulation (hciw.Simulation): The simulation the routers belong
            to.
    """
    self.simulation = simulation
    for router, node in zip(
        self.routers, self.simulation.transitive_nodes, strict=False
    ):
        router.initialise(self.simulation, node)

initialise_individual

initialise_individual(ind)

Run at the arrival node when the individual is spawned.

Parameters:

Name Type Description Default
ind Individual

The individual being spawned.

required
Source code in hciw/routing/routing.py
32
33
34
35
36
37
def initialise_individual(self, ind):
    """Run at the arrival node when the individual is spawned.

    Args:
        ind (hciw.Individual): The individual being spawned.
    """

next_node

next_node(ind, node_id)

Choose the next node.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required
node_id int

The id number of the node the individual is at.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
39
40
41
42
43
44
45
46
47
48
49
def next_node(self, ind, node_id):
    """Choose the next node.

    Args:
        ind (hciw.Individual): The individual being routed.
        node_id (int): The id number of the node the individual is at.

    Returns:
        hciw.Node: The next node.
    """
    return self.routers[node_id - 1].next_node(ind)

next_node_for_rerouting

next_node_for_rerouting(ind, node_id)

Choose the next node when rerouting a preempted customer.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required
node_id int

The id number of the node the individual is at.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
51
52
53
54
55
56
57
58
59
60
61
def next_node_for_rerouting(self, ind, node_id):
    """Choose the next node when rerouting a preempted customer.

    Args:
        ind (hciw.Individual): The individual being routed.
        node_id (int): The id number of the node the individual is at.

    Returns:
        hciw.Node: The next node.
    """
    return self.routers[node_id - 1].next_node_for_rerouting(ind)

next_node_for_jockeying

next_node_for_jockeying(ind, node_id)

Choose the next node when jockeying to another node after reneging.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required
node_id int

The id number of the node the individual is at.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
63
64
65
66
67
68
69
70
71
72
73
def next_node_for_jockeying(self, ind, node_id):
    """Choose the next node when jockeying to another node after reneging.

    Args:
        ind (hciw.Individual): The individual being routed.
        node_id (int): The id number of the node the individual is at.

    Returns:
        hciw.Node: The next node.
    """
    return self.routers[node_id - 1].next_node_for_jockeying(ind)

TransitionMatrix

TransitionMatrix(transition_matrix)

Bases: NetworkRouting

Hold a number of probabilistic routing objects.

Set up the relevant probabilistic router objects for each node.

Parameters:

Name Type Description Default
transition_matrix list

A square matrix where the entry in row i and column j is the probability of moving from node i + 1 to node j + 1.

required
Source code in hciw/routing/routing.py
79
80
81
82
83
84
85
86
87
88
89
90
91
def __init__(self, transition_matrix):
    """Set up the relevant probabilistic router objects for each node.

    Args:
        transition_matrix (list): A square matrix where the entry in row
            ``i`` and column ``j`` is the probability of moving from node
            ``i + 1`` to node ``j + 1``.
    """
    n_destinations = len(transition_matrix)
    self.routers = [
        Probabilistic(destinations=list(range(1, n_destinations + 1)), probs=row)
        for row in transition_matrix
    ]

initialise

initialise(simulation)

Give the simulation to the routing objects.

Parameters:

Name Type Description Default
simulation Simulation

The simulation the routers belong to.

required

Raises:

Type Description
ValueError

If the number of rows in the transition matrix does not equal the number of nodes in the network.

Source code in hciw/routing/routing.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def initialise(self, simulation):
    """Give the simulation to the routing objects.

    Args:
        simulation (hciw.Simulation): The simulation the routers belong
            to.

    Raises:
        ValueError: If the number of rows in the transition matrix does
            not equal the number of nodes in the network.
    """
    super().initialise(simulation)
    if len(self.routers) != simulation.network.number_of_nodes:
        raise ValueError(
            "Ensure a transition matrix is given, and that the number of rows "
            "is equal to the number of nodes in the network."
        )

ProcessBased

ProcessBased(route_function)

Bases: NetworkRouting

Route an individual based on a pre-defined process.

Initialise the routing object.

Parameters:

Name Type Description Default
route_function callable

A function that returns a pre-defined route for an individual.

required
Source code in hciw/routing/routing.py
115
116
117
118
119
120
121
122
def __init__(self, route_function):
    """Initialise the routing object.

    Args:
        route_function (callable): A function that returns a pre-defined
            route for an individual.
    """
    self.route_function = route_function

initialise

initialise(simulation)

Give the simulation and node attributes to the routing objects.

Parameters:

Name Type Description Default
simulation Simulation

The simulation the routers belong to.

required
Source code in hciw/routing/routing.py
124
125
126
127
128
129
130
131
def initialise(self, simulation):
    """Give the simulation and node attributes to the routing objects.

    Args:
        simulation (hciw.Simulation): The simulation the routers belong
            to.
    """
    self.simulation = simulation

initialise_individual

initialise_individual(ind)

Set the route of the individual when they are spawned.

Parameters:

Name Type Description Default
ind Individual

The individual being spawned.

required
Source code in hciw/routing/routing.py
133
134
135
136
137
138
139
def initialise_individual(self, ind):
    """Set the route of the individual when they are spawned.

    Args:
        ind (hciw.Individual): The individual being spawned.
    """
    ind.route = self.route_function(ind, self.simulation)

next_node

next_node(ind, node_id)

Choose the next node from the process-based pre-defined route.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required
node_id int

The id number of the node the individual is at.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
141
142
143
144
145
146
147
148
149
150
151
152
def next_node(self, ind, node_id):
    """Choose the next node from the process-based pre-defined route.

    Args:
        ind (hciw.Individual): The individual being routed.
        node_id (int): The id number of the node the individual is at.

    Returns:
        hciw.Node: The next node.
    """
    node_index = -1 if len(ind.route) == 0 else ind.route.pop(0)
    return self.simulation.nodes[node_index]

next_node_for_rerouting

next_node_for_rerouting(ind, node_id)

Choose the next node when rerouting a preempted customer.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required
node_id int

The id number of the node the individual is at.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
154
155
156
157
158
159
160
161
162
163
164
def next_node_for_rerouting(self, ind, node_id):
    """Choose the next node when rerouting a preempted customer.

    Args:
        ind (hciw.Individual): The individual being routed.
        node_id (int): The id number of the node the individual is at.

    Returns:
        hciw.Node: The next node.
    """
    return self.next_node(ind, node_id)

next_node_for_jockeying

next_node_for_jockeying(ind, node_id)

Choose the next node when jockeying to another node after reneging.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required
node_id int

The id number of the node the individual is at.

required

Returns:

Type Description
Node

The exit node.

Source code in hciw/routing/routing.py
166
167
168
169
170
171
172
173
174
175
176
def next_node_for_jockeying(self, ind, node_id):
    """Choose the next node when jockeying to another node after reneging.

    Args:
        ind (hciw.Individual): The individual being routed.
        node_id (int): The id number of the node the individual is at.

    Returns:
        hciw.Node: The exit node.
    """
    return self.simulation.nodes[-1]

FlexibleProcessBased

FlexibleProcessBased(route_function, rule, choice)

Bases: ProcessBased

Route an individual based on a pre-defined process.

The route is flexible: each step may be a subset of nodes from which the next node is chosen according to a rule and a choice method.

Initialise the routing object.

Parameters:

Name Type Description Default
route_function callable

A function that returns a pre-defined route for an individual.

required
rule str

Either 'any' or 'all'.

required
choice str

Either 'random', 'jsq' or 'lb'.

required

Raises:

Type Description
ValueError

If rule is not 'any' or 'all'.

ValueError

If choice is not 'random', 'jsq' or 'lb'.

Source code in hciw/routing/routing.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def __init__(self, route_function, rule, choice):
    """Initialise the routing object.

    Args:
        route_function (callable): A function that returns a pre-defined
            route for an individual.
        rule (str): Either ``'any'`` or ``'all'``.
        choice (str): Either ``'random'``, ``'jsq'`` or ``'lb'``.

    Raises:
        ValueError: If ``rule`` is not ``'any'`` or ``'all'``.
        ValueError: If ``choice`` is not ``'random'``, ``'jsq'`` or
            ``'lb'``.
    """
    self.route_function = route_function
    if rule not in ["any", "all"]:
        raise ValueError("Flexible routing rules must be one of 'any' or 'all'.")
    if choice not in ["random", "jsq", "lb"]:
        raise ValueError(
            "Flexible routing choices must be one of 'random', 'jsq', or 'lb'."
        )
    self.rule = rule
    self.choice = choice

find_next_node_from_subset

find_next_node_from_subset(subset, ind)

Find the next node within a subset according to choice.

Parameters:

Name Type Description Default
subset list

The subset of node indices to choose from.

required
ind Individual

The individual being routed.

required

Returns:

Type Description
int

The id number of the chosen node.

Source code in hciw/routing/routing.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
def find_next_node_from_subset(self, subset, ind):
    """Find the next node within a subset according to ``choice``.

    Args:
        subset (list): The subset of node indices to choose from.
        ind (hciw.Individual): The individual being routed.

    Returns:
        int: The id number of the chosen node.
    """
    if self.choice == "random":
        return hciw.random_choice(subset)
    if self.choice == "jsq":
        temp_router = JoinShortestQueue(destinations=subset)
        temp_router.initialise(self.simulation, None)
        nd = temp_router.next_node(ind)
        assert nd is not None
        return nd.id_number
    if self.choice == "lb":
        temp_router = LoadBalancing(destinations=subset)
        temp_router.initialise(self.simulation, None)
        nd = temp_router.next_node(ind)
        assert nd is not None
        return nd.id_number
    return None

update_individual_route

update_individual_route(ind, next_node_id)

Remove chosen nodes from the individual's route according to rule.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required
next_node_id int

The id number of the chosen next node.

required
Source code in hciw/routing/routing.py
236
237
238
239
240
241
242
243
244
245
246
247
248
def update_individual_route(self, ind, next_node_id):
    """Remove chosen nodes from the individual's route according to ``rule``.

    Args:
        ind (hciw.Individual): The individual being routed.
        next_node_id (int): The id number of the chosen next node.
    """
    if self.rule == "any":
        ind.route = ind.route[1:]
    if self.rule == "all":
        ind.route[0].remove(next_node_id)
        if len(ind.route[0]) == 0:
            ind.route = ind.route[1:]

next_node

next_node(ind, node_id)

Choose the next node from the process-based pre-defined route.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required
node_id int

The id number of the node the individual is at.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
def next_node(self, ind, node_id):
    """Choose the next node from the process-based pre-defined route.

    Args:
        ind (hciw.Individual): The individual being routed.
        node_id (int): The id number of the node the individual is at.

    Returns:
        hciw.Node: The next node.
    """
    if len(ind.route) == 0:
        node_index = -1
    else:
        node_index = self.find_next_node_from_subset(ind.route[0], ind)
        self.update_individual_route(ind, node_index)
    return self.simulation.nodes[node_index]

NodeRouting

A generic routing class to determine next sampled node.

initialise

initialise(simulation, node)

Give the simulation and node attributes to the routing object.

Parameters:

Name Type Description Default
simulation Simulation

The simulation the router belongs to.

required
node Node

The node the router belongs to.

required
Source code in hciw/routing/routing.py
271
272
273
274
275
276
277
278
279
280
281
def initialise(self, simulation, node):
    """Give the simulation and node attributes to the routing object.

    Args:
        simulation (hciw.Simulation): The simulation the router belongs
            to.
        node (hciw.Node): The node the router belongs to.
    """
    self.simulation = simulation
    self.node = node
    self.error_check_at_initialise()

error_check_at_initialise

error_check_at_initialise()

Check the routing object is valid when it is initialised.

Source code in hciw/routing/routing.py
283
284
def error_check_at_initialise(self):
    """Check the routing object is valid when it is initialised."""

next_node

next_node(ind)

Choose the next node for an individual.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
286
287
288
289
290
291
292
293
294
295
def next_node(self, ind):
    """Choose the next node for an individual.

    Args:
        ind (hciw.Individual): The individual being routed.

    Returns:
        hciw.Node: The next node.
    """
    raise NotImplementedError

next_node_for_rerouting

next_node_for_rerouting(ind)

Choose the next node when rerouting a preempted customer.

By default, the next node for rerouting uses the same method as :meth:next_node.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
297
298
299
300
301
302
303
304
305
306
307
308
309
def next_node_for_rerouting(self, ind):
    """Choose the next node when rerouting a preempted customer.

    By default, the next node for rerouting uses the same method as
    :meth:`next_node`.

    Args:
        ind (hciw.Individual): The individual being routed.

    Returns:
        hciw.Node: The next node.
    """
    return self.next_node(ind)

next_node_for_jockeying

next_node_for_jockeying(ind)

Choose the next node when jockeying to another node after reneging.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The exit node.

Source code in hciw/routing/routing.py
311
312
313
314
315
316
317
318
319
320
def next_node_for_jockeying(self, ind):
    """Choose the next node when jockeying to another node after reneging.

    Args:
        ind (hciw.Individual): The individual being routed.

    Returns:
        hciw.Node: The exit node.
    """
    return self.simulation.nodes[-1]

Probabilistic

Probabilistic(destinations, probs)

Bases: NodeRouting

A router that probabilistically chooses the next node.

Initialise the routing object.

Parameters:

Name Type Description Default
destinations list

A list of node indices.

required
probs list

A list of probabilities associated with each destination.

required

Raises:

Type Description
ValueError

If any probability is not a float.

ValueError

If any probability is less than 0 or greater than 1.

ValueError

If the probabilities sum to more than 1.

Source code in hciw/routing/routing.py
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
def __init__(self, destinations, probs):
    """Initialise the routing object.

    Args:
        destinations (list): A list of node indices.
        probs (list): A list of probabilities associated with each
            destination.

    Raises:
        ValueError: If any probability is not a float.
        ValueError: If any probability is less than 0 or greater than 1.
        ValueError: If the probabilities sum to more than 1.
    """
    for p in probs:
        if not isinstance(p, float):
            raise ValueError(  # noqa: TRY004 - public API raises ValueError
                "Routing probabilities must be a float."
            )
        if p < 0 or p > 1:
            raise ValueError("Routing probabilities must be between 0 and 1.")
    if sum(probs) > 1.0:
        raise ValueError("Routing probabilities must sum to 1 or less.")
    self.destinations = [*destinations, -1]
    self.probs = [*probs, 1 - sum(probs)]

error_check_at_initialise

error_check_at_initialise()

Check the probabilities and destinations are valid.

Raises:

Type Description
ValueError

If the probabilities and destinations differ in length.

ValueError

If the destinations are not a subset of the nodes in the network.

Source code in hciw/routing/routing.py
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
def error_check_at_initialise(self):
    """Check the probabilities and destinations are valid.

    Raises:
        ValueError: If the probabilities and destinations differ in
            length.
        ValueError: If the destinations are not a subset of the nodes in
            the network.
    """
    if len(self.probs) != len(self.destinations):
        raise ValueError(
            "Routing probabilities should correspond to destinations, and so "
            "should be lists of the same length."
        )
    if not set(self.destinations).issubset(
        {nd.id_number for nd in self.simulation.nodes[1:]}
    ):
        raise ValueError(
            "Routing destinations should be a subset of the nodes in the network."
        )

next_node

next_node(ind)

Probabilistically choose the next node from the destinations.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
372
373
374
375
376
377
378
379
380
381
382
def next_node(self, ind):
    """Probabilistically choose the next node from the destinations.

    Args:
        ind (hciw.Individual): The individual being routed.

    Returns:
        hciw.Node: The next node.
    """
    node_index = hciw.random_choice(self.destinations, self.probs)
    return self.simulation.nodes[node_index]

Direct

Direct(to)

Bases: NodeRouting

A router that sends the individual directly to another node.

Initialise the routing object.

Parameters:

Name Type Description Default
to int

The node index to send to.

required
Source code in hciw/routing/routing.py
388
389
390
391
392
393
394
def __init__(self, to):
    """Initialise the routing object.

    Args:
        to (int): The node index to send to.
    """
    self.to = to

next_node

next_node(ind)

Choose the node to with probability 1.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The destination node.

Source code in hciw/routing/routing.py
396
397
398
399
400
401
402
403
404
405
def next_node(self, ind):
    """Choose the node ``to`` with probability 1.

    Args:
        ind (hciw.Individual): The individual being routed.

    Returns:
        hciw.Node: The destination node.
    """
    return self.simulation.nodes[self.to]

Leave

Bases: NodeRouting

A router that sends the individual directly to the exit node.

next_node

next_node(ind)

Choose the exit node with probability 1.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The exit node.

Source code in hciw/routing/routing.py
411
412
413
414
415
416
417
418
419
420
def next_node(self, ind):
    """Choose the exit node with probability 1.

    Args:
        ind (hciw.Individual): The individual being routed.

    Returns:
        hciw.Node: The exit node.
    """
    return self.simulation.nodes[-1]

JoinShortestQueue

JoinShortestQueue(destinations, tie_break='random')

Bases: NodeRouting

A router that sends the individual to the node with the shortest queue.

The node is chosen from a list of destinations.

Initialise the routing object.

Parameters:

Name Type Description Default
destinations list

A list of node indices.

required
tie_break str

The method to deal with ties. One of 'random' (randomly choose between ties) or 'order' (prioritise nodes in the order given by destinations). Defaults to 'random'.

'random'
Source code in hciw/routing/routing.py
429
430
431
432
433
434
435
436
437
438
439
440
def __init__(self, destinations, tie_break="random"):
    """Initialise the routing object.

    Args:
        destinations (list): A list of node indices.
        tie_break (str, optional): The method to deal with ties. One of
            ``'random'`` (randomly choose between ties) or ``'order'``
            (prioritise nodes in the order given by ``destinations``).
            Defaults to ``'random'``.
    """
    self.destinations = destinations
    self.tie_break = tie_break

error_check_at_initialise

error_check_at_initialise()

Check the destinations are a subset of the network's nodes.

Raises:

Type Description
ValueError

If the destinations are not a subset of the nodes in the network.

Source code in hciw/routing/routing.py
442
443
444
445
446
447
448
449
450
451
452
453
454
def error_check_at_initialise(self):
    """Check the destinations are a subset of the network's nodes.

    Raises:
        ValueError: If the destinations are not a subset of the nodes in
            the network.
    """
    if not set(self.destinations).issubset(
        {nd.id_number for nd in self.simulation.nodes[1:]}
    ):
        raise ValueError(
            "Routing destinations should be a subset of the nodes in the network."
        )

get_queue_size

get_queue_size(node_index)

Get the size of the queue at node_index.

Parameters:

Name Type Description Default
node_index int

The index of the node.

required

Returns:

Type Description
int

The number of waiting individuals.

Source code in hciw/routing/routing.py
456
457
458
459
460
461
462
463
464
465
466
467
468
def get_queue_size(self, node_index):
    """Get the size of the queue at ``node_index``.

    Args:
        node_index (int): The index of the node.

    Returns:
        int: The number of waiting individuals.
    """
    return (
        self.simulation.nodes[node_index].number_of_individuals
        - self.simulation.nodes[node_index].number_in_service
    )

next_node

next_node(ind)

Choose the destination node with the shortest queue.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
def next_node(self, ind):
    """Choose the destination node with the shortest queue.

    Args:
        ind (hciw.Individual): The individual being routed.

    Returns:
        hciw.Node: The next node.
    """
    shortest_queues = []
    shortest_queue_size = float("inf")
    for node_index in self.destinations:
        queue_size = self.get_queue_size(node_index)
        if queue_size == shortest_queue_size:
            shortest_queues.append(node_index)
        if queue_size < shortest_queue_size:
            shortest_queues = [node_index]
            shortest_queue_size = queue_size
    if self.tie_break == "random":
        next_node_index = hciw.random_choice(shortest_queues)
        return self.simulation.nodes[next_node_index]
    if self.tie_break == "order":
        next_node_index = shortest_queues[0]
        return self.simulation.nodes[next_node_index]
    return None

LoadBalancing

LoadBalancing(destinations, tie_break='random')

Bases: JoinShortestQueue

A version of JoinShortestQueue that also counts customers in service.

Source code in hciw/routing/routing.py
429
430
431
432
433
434
435
436
437
438
439
440
def __init__(self, destinations, tie_break="random"):
    """Initialise the routing object.

    Args:
        destinations (list): A list of node indices.
        tie_break (str, optional): The method to deal with ties. One of
            ``'random'`` (randomly choose between ties) or ``'order'``
            (prioritise nodes in the order given by ``destinations``).
            Defaults to ``'random'``.
    """
    self.destinations = destinations
    self.tie_break = tie_break

get_queue_size

get_queue_size(node_index)

Get the size of the queue at node_index.

Parameters:

Name Type Description Default
node_index int

The index of the node.

required

Returns:

Type Description
int

The number of individuals at the node.

Source code in hciw/routing/routing.py
500
501
502
503
504
505
506
507
508
509
def get_queue_size(self, node_index):
    """Get the size of the queue at ``node_index``.

    Args:
        node_index (int): The index of the node.

    Returns:
        int: The number of individuals at the node.
    """
    return self.simulation.nodes[node_index].number_of_individuals

Cycle

Cycle(cycle)

Bases: NodeRouting

A router that cycles through destinations, repeating the cycle once ended.

Initialise the routing object.

Parameters:

Name Type Description Default
cycle list

An ordered sequence of node indices.

required
Source code in hciw/routing/routing.py
515
516
517
518
519
520
521
522
def __init__(self, cycle):
    """Initialise the routing object.

    Args:
        cycle (list): An ordered sequence of node indices.
    """
    self.cycle = cycle
    self.generator = itertools.cycle(self.cycle)

error_check_at_initialise

error_check_at_initialise()

Check the cycle is a subset of the network's nodes.

Raises:

Type Description
ValueError

If the cycle is not a subset of the nodes in the network.

Source code in hciw/routing/routing.py
524
525
526
527
528
529
530
531
532
533
534
535
536
def error_check_at_initialise(self):
    """Check the cycle is a subset of the network's nodes.

    Raises:
        ValueError: If the cycle is not a subset of the nodes in the
            network.
    """
    if not set(self.cycle).issubset(
        {nd.id_number for nd in self.simulation.nodes[1:]}
    ):
        raise ValueError(
            "Routing destinations should be a subset of the nodes in the network."
        )

next_node

next_node(ind)

Choose the next node in the cycle.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/routing/routing.py
538
539
540
541
542
543
544
545
546
547
548
def next_node(self, ind):
    """Choose the next node in the cycle.

    Args:
        ind (hciw.Individual): The individual being routed.

    Returns:
        hciw.Node: The next node.
    """
    next_node_index = next(self.generator)
    return self.simulation.nodes[next_node_index]

State trackers

state_tracker

State tracker classes that record the state of a simulation over time.

StateTracker

A generic class to record a system's state.

initialise

initialise(simulation)

Initialise the state tracker class.

Parameters:

Name Type Description Default
simulation Simulation

The simulation being tracked.

required
Source code in hciw/trackers/state_tracker.py
 7
 8
 9
10
11
12
13
14
15
def initialise(self, simulation):
    """Initialise the state tracker class.

    Args:
        simulation (hciw.Simulation): The simulation being tracked.
    """
    self.simulation = simulation
    self.state = None
    self.history = [[self.simulation.current_time, self.hash_state()]]

change_state_accept

change_state_accept(node, ind)

Change the state of the system when a customer is accepted.

Parameters:

Name Type Description Default
node Node

The node at which the individual is accepted.

required
ind Individual

The individual being accepted.

required
Source code in hciw/trackers/state_tracker.py
17
18
19
20
21
22
23
def change_state_accept(self, node, ind):
    """Change the state of the system when a customer is accepted.

    Args:
        node (hciw.Node): The node at which the individual is accepted.
        ind (hciw.Individual): The individual being accepted.
    """

change_state_block

change_state_block(node, destination, ind)

Change the state of the system when a customer gets blocked.

Parameters:

Name Type Description Default
node Node

The node at which the individual is blocked.

required
destination Node

The node the individual is blocked from entering.

required
ind Individual

The individual being blocked.

required
Source code in hciw/trackers/state_tracker.py
25
26
27
28
29
30
31
32
33
def change_state_block(self, node, destination, ind):
    """Change the state of the system when a customer gets blocked.

    Args:
        node (hciw.Node): The node at which the individual is blocked.
        destination (hciw.Node): The node the individual is blocked from
            entering.
        ind (hciw.Individual): The individual being blocked.
    """

change_state_release

change_state_release(node, destination, ind, blocked)

Change the state of the system when a customer is released.

Parameters:

Name Type Description Default
node Node

The node at which the individual is released.

required
destination Node

The node the individual was blocked from entering.

required
ind Individual

The individual being released.

required
blocked bool

Whether the individual was blocked.

required
Source code in hciw/trackers/state_tracker.py
35
36
37
38
39
40
41
42
43
44
def change_state_release(self, node, destination, ind, blocked):
    """Change the state of the system when a customer is released.

    Args:
        node (hciw.Node): The node at which the individual is released.
        destination (hciw.Node): The node the individual was blocked from
            entering.
        ind (hciw.Individual): The individual being released.
        blocked (bool): Whether the individual was blocked.
    """

change_state_renege

change_state_renege(node, destination, ind, blocked)

Change the state of the system when a customer reneges.

Parameters:

Name Type Description Default
node Node

The node at which the individual reneges.

required
destination Node

The node the individual was blocked from entering.

required
ind Individual

The individual reneging.

required
blocked bool

Whether the individual was blocked.

required
Source code in hciw/trackers/state_tracker.py
46
47
48
49
50
51
52
53
54
55
56
def change_state_renege(self, node, destination, ind, blocked):
    """Change the state of the system when a customer reneges.

    Args:
        node (hciw.Node): The node at which the individual reneges.
        destination (hciw.Node): The node the individual was blocked from
            entering.
        ind (hciw.Individual): The individual reneging.
        blocked (bool): Whether the individual was blocked.
    """
    self.change_state_release(node, destination, ind, blocked)

change_state_classchange

change_state_classchange(node, ind)

Change the state of the system when a customer changes class.

Parameters:

Name Type Description Default
node Node

The node at which the individual changes class.

required
ind Individual

The individual changing class.

required
Source code in hciw/trackers/state_tracker.py
58
59
60
61
62
63
64
def change_state_classchange(self, node, ind):
    """Change the state of the system when a customer changes class.

    Args:
        node (hciw.Node): The node at which the individual changes class.
        ind (hciw.Individual): The individual changing class.
    """

hash_state

hash_state() -> object

Return a hashable state.

Returns:

Type Description
Hashable

A hashable representation of the state.

Source code in hciw/trackers/state_tracker.py
66
67
68
69
70
71
72
def hash_state(self) -> object:
    """Return a hashable state.

    Returns:
        Hashable: A hashable representation of the state.
    """
    return None

timestamp

timestamp()

Record the current state in the history if it has changed.

Source code in hciw/trackers/state_tracker.py
74
75
76
77
78
def timestamp(self):
    """Record the current state in the history if it has changed."""
    current_hash_state = self.hash_state()
    if current_hash_state != self.history[-1][1]:
        self.history.append([self.simulation.current_time, current_hash_state])

state_probabilities

state_probabilities(observation_period=(0, float('Inf')))

Get the state probabilities from the history.

Parameters:

Name Type Description Default
observation_period tuple

A (start, end) tuple identifying the observation period over which the probabilities are computed. Defaults to (0, float('Inf')).

(0, float('Inf'))

Returns:

Type Description
dict

A dictionary of states as keys and probabilities as values.

Raises:

Type Description
ValueError

If the observation period is not a positive interval above zero.

Source code in hciw/trackers/state_tracker.py
 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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
def state_probabilities(self, observation_period=(0, float("Inf"))):
    """Get the state probabilities from the history.

    Args:
        observation_period (tuple, optional): A ``(start, end)`` tuple
            identifying the observation period over which the
            probabilities are computed. Defaults to ``(0, float('Inf'))``.

    Returns:
        dict: A dictionary of states as keys and probabilities as values.

    Raises:
        ValueError: If the observation period is not a positive interval
            above zero.
    """
    start = observation_period[0]
    end = observation_period[1]
    steady_state_dictionary = {}
    prev_date = self.history[0][0]
    prev_state = self.history[0][1]
    date_diff = 0.0

    if start < 0 or end <= start:
        raise ValueError(
            "Observation period need to be a positive interval above zero"
        )

    for event in self.history:
        date = event[0]
        state = event[1]
        if date > end:
            break
        date_diff = self.simulation.nodes[1].increment_time(
            date, -max(prev_date, start)
        )
        if start < date < end:
            if prev_state not in steady_state_dictionary:
                steady_state_dictionary[prev_state] = date_diff
            else:
                steady_state_dictionary[prev_state] += date_diff
        prev_date = date
        prev_state = state

    if end != float("Inf"):
        date_diff = self.simulation.nodes[1].increment_time(end, -prev_date)
    if prev_state not in steady_state_dictionary:
        steady_state_dictionary[prev_state] = date_diff
    else:
        steady_state_dictionary[prev_state] += date_diff

    tot = sum(steady_state_dictionary.values())
    for state in steady_state_dictionary:
        steady_state_dictionary[state] /= tot

    return steady_state_dictionary

SystemPopulation

Bases: StateTracker

Record the number of customers in the system, regardless of node.

Example

3 This denotes 3 customers in the whole system.

initialise

initialise(simulation)

Initialise the state tracker class.

Parameters:

Name Type Description Default
simulation Simulation

The simulation being tracked.

required
Source code in hciw/trackers/state_tracker.py
145
146
147
148
149
150
151
152
153
def initialise(self, simulation):
    """Initialise the state tracker class.

    Args:
        simulation (hciw.Simulation): The simulation being tracked.
    """
    self.simulation = simulation
    self.state = 0
    self.history = [[self.simulation.current_time, self.hash_state()]]

change_state_accept

change_state_accept(node, ind)

Change the state of the system when a customer is accepted.

Parameters:

Name Type Description Default
node Node

The node at which the individual is accepted.

required
ind Individual

The individual being accepted.

required
Source code in hciw/trackers/state_tracker.py
155
156
157
158
159
160
161
162
def change_state_accept(self, node, ind):
    """Change the state of the system when a customer is accepted.

    Args:
        node (hciw.Node): The node at which the individual is accepted.
        ind (hciw.Individual): The individual being accepted.
    """
    self.state += 1

change_state_block

change_state_block(node, destination, ind)

Change the state of the system when a customer gets blocked.

Parameters:

Name Type Description Default
node Node

The node at which the individual is blocked.

required
destination Node

The node the individual is blocked from entering.

required
ind Individual

The individual being blocked.

required
Source code in hciw/trackers/state_tracker.py
164
165
166
167
168
169
170
171
172
def change_state_block(self, node, destination, ind):
    """Change the state of the system when a customer gets blocked.

    Args:
        node (hciw.Node): The node at which the individual is blocked.
        destination (hciw.Node): The node the individual is blocked from
            entering.
        ind (hciw.Individual): The individual being blocked.
    """

change_state_release

change_state_release(node, destination, ind, blocked)

Change the state of the system when a customer is released.

Parameters:

Name Type Description Default
node Node

The node at which the individual is released.

required
destination Node

The node the individual was blocked from entering.

required
ind Individual

The individual being released.

required
blocked bool

Whether the individual was blocked.

required
Source code in hciw/trackers/state_tracker.py
174
175
176
177
178
179
180
181
182
183
184
def change_state_release(self, node, destination, ind, blocked):
    """Change the state of the system when a customer is released.

    Args:
        node (hciw.Node): The node at which the individual is released.
        destination (hciw.Node): The node the individual was blocked from
            entering.
        ind (hciw.Individual): The individual being released.
        blocked (bool): Whether the individual was blocked.
    """
    self.state -= 1

hash_state

hash_state()

Return a hashable state.

Returns:

Type Description
int

The number of customers in the system.

Source code in hciw/trackers/state_tracker.py
186
187
188
189
190
191
192
def hash_state(self):
    """Return a hashable state.

    Returns:
        int: The number of customers in the system.
    """
    return self.state

NodePopulation

Bases: StateTracker

Record the number of customers at each node.

Example

(3, 1) This denotes 3 customers at the first node, and 1 customer at the second node.

initialise

initialise(simulation)

Initialise the state tracker class.

Parameters:

Name Type Description Default
simulation Simulation

The simulation being tracked.

required
Source code in hciw/trackers/state_tracker.py
204
205
206
207
208
209
210
211
212
def initialise(self, simulation):
    """Initialise the state tracker class.

    Args:
        simulation (hciw.Simulation): The simulation being tracked.
    """
    self.simulation = simulation
    self.state = [0 for i in range(self.simulation.network.number_of_nodes)]
    self.history = [[self.simulation.current_time, self.hash_state()]]

change_state_accept

change_state_accept(node, ind)

Change the state of the system when a customer is accepted.

Parameters:

Name Type Description Default
node Node

The node at which the individual is accepted.

required
ind Individual

The individual being accepted.

required
Source code in hciw/trackers/state_tracker.py
214
215
216
217
218
219
220
221
def change_state_accept(self, node, ind):
    """Change the state of the system when a customer is accepted.

    Args:
        node (hciw.Node): The node at which the individual is accepted.
        ind (hciw.Individual): The individual being accepted.
    """
    self.state[node.id_number - 1] += 1

change_state_block

change_state_block(node, destination, ind)

Change the state of the system when a customer gets blocked.

Parameters:

Name Type Description Default
node Node

The node at which the individual is blocked.

required
destination Node

The node the individual is blocked from entering.

required
ind Individual

The individual being blocked.

required
Source code in hciw/trackers/state_tracker.py
223
224
225
226
227
228
229
230
231
def change_state_block(self, node, destination, ind):
    """Change the state of the system when a customer gets blocked.

    Args:
        node (hciw.Node): The node at which the individual is blocked.
        destination (hciw.Node): The node the individual is blocked from
            entering.
        ind (hciw.Individual): The individual being blocked.
    """

change_state_release

change_state_release(node, destination, ind, blocked)

Change the state of the system when a customer is released.

Parameters:

Name Type Description Default
node Node

The node at which the individual is released.

required
destination Node

The node the individual was blocked from entering.

required
ind Individual

The individual being released.

required
blocked bool

Whether the individual was blocked.

required
Source code in hciw/trackers/state_tracker.py
233
234
235
236
237
238
239
240
241
242
243
def change_state_release(self, node, destination, ind, blocked):
    """Change the state of the system when a customer is released.

    Args:
        node (hciw.Node): The node at which the individual is released.
        destination (hciw.Node): The node the individual was blocked from
            entering.
        ind (hciw.Individual): The individual being released.
        blocked (bool): Whether the individual was blocked.
    """
    self.state[node.id_number - 1] -= 1

hash_state

hash_state()

Return a hashable state.

Returns:

Type Description
tuple

The number of customers at each node.

Source code in hciw/trackers/state_tracker.py
245
246
247
248
249
250
251
def hash_state(self):
    """Return a hashable state.

    Returns:
        tuple: The number of customers at each node.
    """
    return tuple(self.state)

NodePopulationSubset

NodePopulationSubset(observed_nodes)

Bases: StateTracker

Record the number of customers at each node in a given set.

Example

(3, 1) This denotes 3 customers at the first node, and 1 customer at the second node.

Pre-initialise the object with the observed nodes.

Parameters:

Name Type Description Default
observed_nodes list

The indices of the nodes to observe.

required
Source code in hciw/trackers/state_tracker.py
263
264
265
266
267
268
269
def __init__(self, observed_nodes):
    """Pre-initialise the object with the observed nodes.

    Args:
        observed_nodes (list): The indices of the nodes to observe.
    """
    self.observed_nodes = observed_nodes

initialise

initialise(simulation)

Initialise the state tracker class.

Parameters:

Name Type Description Default
simulation Simulation

The simulation being tracked.

required
Source code in hciw/trackers/state_tracker.py
271
272
273
274
275
276
277
278
279
def initialise(self, simulation):
    """Initialise the state tracker class.

    Args:
        simulation (hciw.Simulation): The simulation being tracked.
    """
    self.simulation = simulation
    self.state = [0 for i in self.observed_nodes]
    self.history = [[self.simulation.current_time, self.hash_state()]]

change_state_accept

change_state_accept(node, ind)

Change the state of the system when a customer is accepted.

Parameters:

Name Type Description Default
node Node

The node at which the individual is accepted.

required
ind Individual

The individual being accepted.

required
Source code in hciw/trackers/state_tracker.py
281
282
283
284
285
286
287
288
289
290
def change_state_accept(self, node, ind):
    """Change the state of the system when a customer is accepted.

    Args:
        node (hciw.Node): The node at which the individual is accepted.
        ind (hciw.Individual): The individual being accepted.
    """
    if node.id_number - 1 in self.observed_nodes:
        state_index = self.observed_nodes.index(node.id_number - 1)
        self.state[state_index] += 1

change_state_block

change_state_block(node, destination, ind)

Change the state of the system when a customer gets blocked.

Parameters:

Name Type Description Default
node Node

The node at which the individual is blocked.

required
destination Node

The node the individual is blocked from entering.

required
ind Individual

The individual being blocked.

required
Source code in hciw/trackers/state_tracker.py
292
293
294
295
296
297
298
299
300
def change_state_block(self, node, destination, ind):
    """Change the state of the system when a customer gets blocked.

    Args:
        node (hciw.Node): The node at which the individual is blocked.
        destination (hciw.Node): The node the individual is blocked from
            entering.
        ind (hciw.Individual): The individual being blocked.
    """

change_state_release

change_state_release(node, destination, ind, blocked)

Change the state of the system when a customer is released.

Parameters:

Name Type Description Default
node Node

The node at which the individual is released.

required
destination Node

The node the individual was blocked from entering.

required
ind Individual

The individual being released.

required
blocked bool

Whether the individual was blocked.

required
Source code in hciw/trackers/state_tracker.py
302
303
304
305
306
307
308
309
310
311
312
313
314
def change_state_release(self, node, destination, ind, blocked):
    """Change the state of the system when a customer is released.

    Args:
        node (hciw.Node): The node at which the individual is released.
        destination (hciw.Node): The node the individual was blocked from
            entering.
        ind (hciw.Individual): The individual being released.
        blocked (bool): Whether the individual was blocked.
    """
    if node.id_number - 1 in self.observed_nodes:
        state_index = self.observed_nodes.index(node.id_number - 1)
        self.state[state_index] -= 1

hash_state

hash_state()

Return a hashable state.

Returns:

Type Description
tuple

The number of customers at each observed node.

Source code in hciw/trackers/state_tracker.py
316
317
318
319
320
321
322
def hash_state(self):
    """Return a hashable state.

    Returns:
        tuple: The number of customers at each observed node.
    """
    return tuple(self.state)

GroupedNodePopulation

GroupedNodePopulation(groups)

Bases: StateTracker

Record the number of customers at each group of nodes.

Node groups are defined by the user.

Example

(3, 1) This denotes 3 customers at the first node group, and 1 customer at the second node group.

Pre-initialise the object with the observed node groups.

Parameters:

Name Type Description Default
groups list

A list of node groups, each a list of node indices.

required
Source code in hciw/trackers/state_tracker.py
336
337
338
339
340
341
342
343
def __init__(self, groups):
    """Pre-initialise the object with the observed node groups.

    Args:
        groups (list): A list of node groups, each a list of node indices.
    """
    self.groups = groups
    self.observed_nodes = [nd for group in groups for nd in group]

initialise

initialise(simulation)

Initialise the state tracker class.

Parameters:

Name Type Description Default
simulation Simulation

The simulation being tracked.

required
Source code in hciw/trackers/state_tracker.py
345
346
347
348
349
350
351
352
353
def initialise(self, simulation):
    """Initialise the state tracker class.

    Args:
        simulation (hciw.Simulation): The simulation being tracked.
    """
    self.simulation = simulation
    self.state = [0 for i in self.groups]
    self.history = [[self.simulation.current_time, self.hash_state()]]

change_state_accept

change_state_accept(node, ind)

Change the state of the system when a customer is accepted.

Parameters:

Name Type Description Default
node Node

The node at which the individual is accepted.

required
ind Individual

The individual being accepted.

required
Source code in hciw/trackers/state_tracker.py
355
356
357
358
359
360
361
362
363
364
365
def change_state_accept(self, node, ind):
    """Change the state of the system when a customer is accepted.

    Args:
        node (hciw.Node): The node at which the individual is accepted.
        ind (hciw.Individual): The individual being accepted.
    """
    if node.id_number - 1 in self.observed_nodes:
        node_in_group = [(node.id_number - 1) in group for group in self.groups]
        group_index = node_in_group.index(True)
        self.state[group_index] += 1

change_state_block

change_state_block(node, destination, ind)

Change the state of the system when a customer gets blocked.

Parameters:

Name Type Description Default
node Node

The node at which the individual is blocked.

required
destination Node

The node the individual is blocked from entering.

required
ind Individual

The individual being blocked.

required
Source code in hciw/trackers/state_tracker.py
367
368
369
370
371
372
373
374
375
def change_state_block(self, node, destination, ind):
    """Change the state of the system when a customer gets blocked.

    Args:
        node (hciw.Node): The node at which the individual is blocked.
        destination (hciw.Node): The node the individual is blocked from
            entering.
        ind (hciw.Individual): The individual being blocked.
    """

change_state_release

change_state_release(node, destination, ind, blocked)

Change the state of the system when a customer is released.

Parameters:

Name Type Description Default
node Node

The node at which the individual is released.

required
destination Node

The node the individual was blocked from entering.

required
ind Individual

The individual being released.

required
blocked bool

Whether the individual was blocked.

required
Source code in hciw/trackers/state_tracker.py
377
378
379
380
381
382
383
384
385
386
387
388
389
390
def change_state_release(self, node, destination, ind, blocked):
    """Change the state of the system when a customer is released.

    Args:
        node (hciw.Node): The node at which the individual is released.
        destination (hciw.Node): The node the individual was blocked from
            entering.
        ind (hciw.Individual): The individual being released.
        blocked (bool): Whether the individual was blocked.
    """
    if node.id_number - 1 in self.observed_nodes:
        node_in_group = [(node.id_number - 1) in group for group in self.groups]
        group_index = node_in_group.index(True)
        self.state[group_index] -= 1

hash_state

hash_state()

Return a hashable state.

Returns:

Type Description
tuple

The number of customers at each node group.

Source code in hciw/trackers/state_tracker.py
392
393
394
395
396
397
398
def hash_state(self):
    """Return a hashable state.

    Returns:
        tuple: The number of customers at each node group.
    """
    return tuple(self.state)

NodeClassMatrix

NodeClassMatrix(class_ordering=None)

Bases: StateTracker

Record the number of customers of each class at each node.

Example

::

((3, 1),
 (0, 1))
This denotes 4 customers at the first node (3 of Class 0, 1 of
Class 0), and 1 customer at the second node (0 of Class 0, 1 of
Class 1).

Pre-initialise the object with the class ordering.

Parameters:

Name Type Description Default
class_ordering list

The order of the customer classes. Defaults to None.

None
Source code in hciw/trackers/state_tracker.py
414
415
416
417
418
419
420
421
422
def __init__(self, class_ordering=None):
    """Pre-initialise the object with the class ordering.

    Args:
        class_ordering (list, optional): The order of the customer
            classes. Defaults to ``None``.
    """
    self.class_ordering: dict = {}
    self._class_ordering_input = class_ordering

initialise

initialise(simulation)

Initialise the state tracker class.

Parameters:

Name Type Description Default
simulation Simulation

The simulation being tracked.

required
Source code in hciw/trackers/state_tracker.py
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
def initialise(self, simulation):
    """Initialise the state tracker class.

    Args:
        simulation (hciw.Simulation): The simulation being tracked.
    """
    self.simulation = simulation

    if self._class_ordering_input is not None:
        self.class_ordering = {
            clss: i for i, clss in enumerate(self._class_ordering_input)
        }
    else:
        self.class_ordering = {
            clss: i
            for i, clss in enumerate(self.simulation.network.customer_class_names)
        }

    self.state = [
        [0 for cls in range(self.simulation.network.number_of_classes)]
        for i in range(self.simulation.network.number_of_nodes)
    ]
    self.history = [[self.simulation.current_time, self.hash_state()]]

change_state_accept

change_state_accept(node, ind)

Change the state of the system when a customer is accepted.

Parameters:

Name Type Description Default
node Node

The node at which the individual is accepted.

required
ind Individual

The individual being accepted.

required
Source code in hciw/trackers/state_tracker.py
448
449
450
451
452
453
454
455
def change_state_accept(self, node, ind):
    """Change the state of the system when a customer is accepted.

    Args:
        node (hciw.Node): The node at which the individual is accepted.
        ind (hciw.Individual): The individual being accepted.
    """
    self.state[node.id_number - 1][self.class_ordering[ind.customer_class]] += 1

change_state_block

change_state_block(node, destination, ind)

Change the state of the system when a customer gets blocked.

Parameters:

Name Type Description Default
node Node

The node at which the individual is blocked.

required
destination Node

The node the individual is blocked from entering.

required
ind Individual

The individual being blocked.

required
Source code in hciw/trackers/state_tracker.py
457
458
459
460
461
462
463
464
465
def change_state_block(self, node, destination, ind):
    """Change the state of the system when a customer gets blocked.

    Args:
        node (hciw.Node): The node at which the individual is blocked.
        destination (hciw.Node): The node the individual is blocked from
            entering.
        ind (hciw.Individual): The individual being blocked.
    """

change_state_release

change_state_release(node, destination, ind, blocked)

Change the state of the system when a customer is released.

Parameters:

Name Type Description Default
node Node

The node at which the individual is released.

required
destination Node

The node the individual was blocked from entering.

required
ind Individual

The individual being released.

required
blocked bool

Whether the individual was blocked.

required
Source code in hciw/trackers/state_tracker.py
467
468
469
470
471
472
473
474
475
476
477
def change_state_release(self, node, destination, ind, blocked):
    """Change the state of the system when a customer is released.

    Args:
        node (hciw.Node): The node at which the individual is released.
        destination (hciw.Node): The node the individual was blocked from
            entering.
        ind (hciw.Individual): The individual being released.
        blocked (bool): Whether the individual was blocked.
    """
    self.state[node.id_number - 1][self.class_ordering[ind.customer_class]] -= 1

change_state_classchange

change_state_classchange(node, ind)

Change the state of the system when a customer changes class.

Parameters:

Name Type Description Default
node Node

The node at which the individual changes class.

required
ind Individual

The individual changing class.

required
Source code in hciw/trackers/state_tracker.py
479
480
481
482
483
484
485
486
487
def change_state_classchange(self, node, ind):
    """Change the state of the system when a customer changes class.

    Args:
        node (hciw.Node): The node at which the individual changes class.
        ind (hciw.Individual): The individual changing class.
    """
    self.state[node.id_number - 1][self.class_ordering[ind.previous_class]] -= 1
    self.state[node.id_number - 1][self.class_ordering[ind.customer_class]] += 1

hash_state

hash_state()

Return a hashable state.

Returns:

Type Description
tuple

The number of customers of each class at each node.

Source code in hciw/trackers/state_tracker.py
489
490
491
492
493
494
495
def hash_state(self):
    """Return a hashable state.

    Returns:
        tuple: The number of customers of each class at each node.
    """
    return tuple(tuple(obs) for obs in self.state)

NaiveBlocking

Bases: StateTracker

Record the number of customers and blocked customers at each node.

Example

((3, 0), (1, 4)) This denotes 3 customers at the first node, 0 of which are blocked, 5 customers at the second node, 4 of which are blocked.

initialise

initialise(simulation)

Initialise the naive blocking tracker class.

Parameters:

Name Type Description Default
simulation Simulation

The simulation being tracked.

required
Source code in hciw/trackers/state_tracker.py
508
509
510
511
512
513
514
515
516
def initialise(self, simulation):
    """Initialise the naive blocking tracker class.

    Args:
        simulation (hciw.Simulation): The simulation being tracked.
    """
    self.simulation = simulation
    self.state = [[0, 0] for i in range(self.simulation.network.number_of_nodes)]
    self.history = [[self.simulation.current_time, self.hash_state()]]

change_state_accept

change_state_accept(node, ind)

Change the state of the system when a customer is accepted.

Parameters:

Name Type Description Default
node Node

The node at which the individual is accepted.

required
ind Individual

The individual being accepted.

required
Source code in hciw/trackers/state_tracker.py
518
519
520
521
522
523
524
525
def change_state_accept(self, node, ind):
    """Change the state of the system when a customer is accepted.

    Args:
        node (hciw.Node): The node at which the individual is accepted.
        ind (hciw.Individual): The individual being accepted.
    """
    self.state[node.id_number - 1][0] += 1

change_state_block

change_state_block(node, destination, ind)

Change the state of the system when a customer gets blocked.

Parameters:

Name Type Description Default
node Node

The node at which the individual is blocked.

required
destination Node

The node the individual is blocked from entering.

required
ind Individual

The individual being blocked.

required
Source code in hciw/trackers/state_tracker.py
527
528
529
530
531
532
533
534
535
536
537
def change_state_block(self, node, destination, ind):
    """Change the state of the system when a customer gets blocked.

    Args:
        node (hciw.Node): The node at which the individual is blocked.
        destination (hciw.Node): The node the individual is blocked from
            entering.
        ind (hciw.Individual): The individual being blocked.
    """
    self.state[node.id_number - 1][1] += 1
    self.state[node.id_number - 1][0] -= 1

change_state_release

change_state_release(node, destination, ind, blocked)

Change the state of the system when a customer is released.

Parameters:

Name Type Description Default
node Node

The node at which the individual is released.

required
destination Node

The node the individual was blocked from entering.

required
ind Individual

The individual being released.

required
blocked bool

Whether the individual was blocked.

required
Source code in hciw/trackers/state_tracker.py
539
540
541
542
543
544
545
546
547
548
549
550
551
552
def change_state_release(self, node, destination, ind, blocked):
    """Change the state of the system when a customer is released.

    Args:
        node (hciw.Node): The node at which the individual is released.
        destination (hciw.Node): The node the individual was blocked from
            entering.
        ind (hciw.Individual): The individual being released.
        blocked (bool): Whether the individual was blocked.
    """
    if blocked:
        self.state[node.id_number - 1][1] -= 1
    else:
        self.state[node.id_number - 1][0] -= 1

hash_state

hash_state()

Return a hashable state.

Returns:

Type Description
tuple

The number of customers and blocked customers at each node.

Source code in hciw/trackers/state_tracker.py
554
555
556
557
558
559
560
def hash_state(self):
    """Return a hashable state.

    Returns:
        tuple: The number of customers and blocked customers at each node.
    """
    return tuple(tuple(obs) for obs in self.state)

MatrixBlocking

Bases: StateTracker

Record the order and destination of blockages, and node populations.

The blockages are recorded in the form of a matrix, alongside the number of customers at each node.

Example

::

((((1, 4), (2)),
   (3),    ())),
   (5, 8))
This denotes 5 customers at the first node, 8 customers at the
second; 2 customers blocked from the first node to the first,
one from the first node to the second, and one from the second
node to the first. The numbers denote the order at which they
became blocked.

initialise

initialise(simulation)

Initialise the matrix blocking tracker class.

Parameters:

Name Type Description Default
simulation Simulation

The simulation being tracked.

required
Source code in hciw/trackers/state_tracker.py
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
def initialise(self, simulation):
    """Initialise the matrix blocking tracker class.

    Args:
        simulation (hciw.Simulation): The simulation being tracked.
    """
    self.simulation = simulation
    self.state = [
        [
            [[] for i in range(self.simulation.network.number_of_nodes)]
            for i in range(self.simulation.network.number_of_nodes)
        ],
        [0 for i in range(self.simulation.network.number_of_nodes)],
    ]
    self.increment = 1
    self.history = [[self.simulation.current_time, self.hash_state()]]

change_state_accept

change_state_accept(node, ind)

Change the state of the system when a customer is accepted.

Parameters:

Name Type Description Default
node Node

The node at which the individual is accepted.

required
ind Individual

The individual being accepted.

required
Source code in hciw/trackers/state_tracker.py
599
600
601
602
603
604
605
606
def change_state_accept(self, node, ind):
    """Change the state of the system when a customer is accepted.

    Args:
        node (hciw.Node): The node at which the individual is accepted.
        ind (hciw.Individual): The individual being accepted.
    """
    self.state[-1][node.id_number - 1] += 1

change_state_block

change_state_block(node, destination, ind)

Change the state of the system when a customer gets blocked.

Parameters:

Name Type Description Default
node Node

The node at which the individual is blocked.

required
destination Node

The node the individual is blocked from entering.

required
ind Individual

The individual being blocked.

required
Source code in hciw/trackers/state_tracker.py
608
609
610
611
612
613
614
615
616
617
618
619
620
def change_state_block(self, node, destination, ind):
    """Change the state of the system when a customer gets blocked.

    Args:
        node (hciw.Node): The node at which the individual is blocked.
        destination (hciw.Node): The node the individual is blocked from
            entering.
        ind (hciw.Individual): The individual being blocked.
    """
    self.state[0][node.id_number - 1][destination.id_number - 1].append(
        self.increment
    )
    self.increment += 1

change_state_release

change_state_release(node, destination, ind, blocked)

Change the state of the system when a customer is released.

Parameters:

Name Type Description Default
node Node

The node at which the individual is released.

required
destination Node

The node the individual was blocked from entering.

required
ind Individual

The individual being released.

required
blocked bool

Whether the individual was blocked.

required
Source code in hciw/trackers/state_tracker.py
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
def change_state_release(self, node, destination, ind, blocked):
    """Change the state of the system when a customer is released.

    Args:
        node (hciw.Node): The node at which the individual is released.
        destination (hciw.Node): The node the individual was blocked from
            entering.
        ind (hciw.Individual): The individual being released.
        blocked (bool): Whether the individual was blocked.
    """
    if blocked:
        self.state[-1][node.id_number - 1] -= 1
        self.increment -= 1
        position = self.find_blocked_position_and_pop(node, destination)
        self.adjust_positions(position)
    else:
        self.state[-1][node.id_number - 1] -= 1

find_blocked_position_and_pop

find_blocked_position_and_pop(node, destination)

Find the position of the next customer to unblock.

Parameters:

Name Type Description Default
node Node

The node the individual is blocked at.

required
destination Node

The node the individual is blocked from entering.

required

Returns:

Type Description
int

The order position of the blocked customer.

Source code in hciw/trackers/state_tracker.py
640
641
642
643
644
645
646
647
648
649
650
651
def find_blocked_position_and_pop(self, node, destination):
    """Find the position of the next customer to unblock.

    Args:
        node (hciw.Node): The node the individual is blocked at.
        destination (hciw.Node): The node the individual is blocked from
            entering.

    Returns:
        int: The order position of the blocked customer.
    """
    return self.state[0][node.id_number - 1][destination.id_number - 1].pop(0)

adjust_positions

adjust_positions(position)

Reduce any positions greater than position by one.

Loops through the whole matrix.

Parameters:

Name Type Description Default
position int

The order position that was removed.

required
Source code in hciw/trackers/state_tracker.py
653
654
655
656
657
658
659
660
661
662
663
664
665
def adjust_positions(self, position):
    """Reduce any positions greater than ``position`` by one.

    Loops through the whole matrix.

    Args:
        position (int): The order position that was removed.
    """
    for r in range(len(self.state[0])):
        for c in range(len(self.state[0][r])):
            for o in range(len(self.state[0][r][c])):
                if self.state[0][r][c][o] > position:
                    self.state[0][r][c][o] -= 1

hash_state

hash_state()

Return a hashable state.

Returns:

Type Description
tuple

The blockage matrix and the number of customers at each node.

Source code in hciw/trackers/state_tracker.py
667
668
669
670
671
672
673
674
675
676
677
678
def hash_state(self):
    """Return a hashable state.

    Returns:
        tuple: The blockage matrix and the number of customers at each
            node.
    """
    naive = tuple(self.state[-1])
    matrix = tuple(
        tuple(tuple(obs for obs in col) for col in row) for row in self.state[0]
    )
    return (matrix, naive)

Deadlock detection

deadlock_detector

Deadlock detection mechanisms for queueing networks.

NoDetection

NoDetection()

A generic class for all deadlock detector classes to inherit from.

Using this class is equivalent to having no deadlock detection capabilities.

Initialise the detection mechanism class.

Source code in hciw/deadlock/deadlock_detector.py
13
14
def __init__(self):
    """Initialise the detection mechanism class."""

initialise_at_node

initialise_at_node(node)

Initialise the detection mechanism when the node is created.

Parameters:

Name Type Description Default
node Node

The node being created.

required
Source code in hciw/deadlock/deadlock_detector.py
16
17
18
19
20
21
def initialise_at_node(self, node):
    """Initialise the detection mechanism when the node is created.

    Args:
        node (hciw.Node): The node being created.
    """

detect_deadlock

detect_deadlock() -> bool

Return whether deadlock has been reached.

Returns:

Type Description
bool

True if deadlock is reached, otherwise False.

Source code in hciw/deadlock/deadlock_detector.py
23
24
25
26
27
28
29
def detect_deadlock(self) -> bool:
    """Return whether deadlock has been reached.

    Returns:
        bool: ``True`` if deadlock is reached, otherwise ``False``.
    """
    return False

action_at_attach_server

action_at_attach_server(node, server, individual)

Take action when the node's attach_server method is called.

Parameters:

Name Type Description Default
node Node

The node the server is attached to.

required
server Server

The server being attached.

required
individual Individual

The individual being served.

required
Source code in hciw/deadlock/deadlock_detector.py
31
32
33
34
35
36
37
38
def action_at_attach_server(self, node, server, individual):
    """Take action when the node's ``attach_server`` method is called.

    Args:
        node (hciw.Node): The node the server is attached to.
        server (hciw.Server): The server being attached.
        individual (hciw.Individual): The individual being served.
    """

action_at_blockage

action_at_blockage(individual, next_node)

Take action when the node's block_individual method is called.

Parameters:

Name Type Description Default
individual Individual

The individual being blocked.

required
next_node Node

The node the individual is blocked from entering.

required
Source code in hciw/deadlock/deadlock_detector.py
40
41
42
43
44
45
46
47
def action_at_blockage(self, individual, next_node):
    """Take action when the node's ``block_individual`` method is called.

    Args:
        individual (hciw.Individual): The individual being blocked.
        next_node (hciw.Node): The node the individual is blocked from
            entering.
    """

action_at_detatch_server

action_at_detatch_server(server)

Take action when the node's detatch_server method is called.

Parameters:

Name Type Description Default
server Server

The server being detached.

required
Source code in hciw/deadlock/deadlock_detector.py
49
50
51
52
53
54
def action_at_detatch_server(self, server):
    """Take action when the node's ``detatch_server`` method is called.

    Args:
        server (hciw.Server): The server being detached.
    """

StateDigraph

StateDigraph()

Bases: NoDetection

Keep track of a directed graph of the simulation state.

Vertices represent servers. Edges represent blockage relationships, such that there is a directed edge from vertex j to vertex k if and only if the customer at server j is blocked from entering the node that contains k. Deadlock is equivalent to a knot in the directed graph.

Initialise the state digraph detection mechanism class.

Source code in hciw/deadlock/deadlock_detector.py
67
68
69
def __init__(self):
    """Initialise the state digraph detection mechanism class."""
    self.statedigraph = nx.DiGraph()

initialise_at_node

initialise_at_node(node)

Initialise the state digraph when the node is created.

Adds the servers of that node if c < Inf.

Parameters:

Name Type Description Default
node Node

The node being created.

required
Source code in hciw/deadlock/deadlock_detector.py
71
72
73
74
75
76
77
78
79
80
def initialise_at_node(self, node):
    """Initialise the state digraph when the node is created.

    Adds the servers of that node if ``c < Inf``.

    Args:
        node (hciw.Node): The node being created.
    """
    if node.c < float("Inf"):
        self.statedigraph.add_nodes_from([str(s) for s in node.servers])

detect_deadlock

detect_deadlock()

Detect whether the system is in a deadlocked state.

That is, whether there is a knot. Note that this code is taken and adapted from the NetworkX Developer Zone Ticket #663 knot.py (09/06/2015).

Returns:

Type Description
bool

True if a knot exists, otherwise False.

Source code in hciw/deadlock/deadlock_detector.py
 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
def detect_deadlock(self):
    """Detect whether the system is in a deadlocked state.

    That is, whether there is a knot. Note that this code is taken and
    adapted from the NetworkX Developer Zone Ticket #663 knot.py
    (09/06/2015).

    Returns:
        bool: ``True`` if a knot exists, otherwise ``False``.
    """
    knots = []
    for c in nx.strongly_connected_components(self.statedigraph):
        subgraph = self.statedigraph.subgraph(c)
        nodes = set(subgraph.nodes())
        if len(nodes) == 1:
            n = nodes.pop()
            nodes.add(n)
            if set(self.statedigraph.successors(n)) == nodes:
                knots.append(subgraph)
        else:
            for n in nodes:
                successors = nx.descendants(self.statedigraph, n)
                if successors <= nodes:
                    knots.append(subgraph)
                    break
    return len(knots) > 0

action_at_attach_server

action_at_attach_server(node, server, individual)

Take action when the node's attach_server method is called.

If a new customer joins a server, and their server is still blocking a customer, then that edge needs to remain. However it was removed in :meth:action_at_detatch_server, so it needs to be added back in.

Parameters:

Name Type Description Default
node Node

The node the server is attached to.

required
server Server

The server being attached.

required
individual Individual

The individual being served.

required
Source code in hciw/deadlock/deadlock_detector.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def action_at_attach_server(self, node, server, individual):
    """Take action when the node's ``attach_server`` method is called.

    If a new customer joins a server, and their server is still blocking
    a customer, then that edge needs to remain. However it was removed
    in :meth:`action_at_detatch_server`, so it needs to be added back in.

    Args:
        node (hciw.Node): The node the server is attached to.
        server (hciw.Server): The server being attached.
        individual (hciw.Individual): The individual being served.
    """
    for blq in node.blocked_queue:
        inds = [
            ind
            for ind in node.simulation.nodes[blq[0]].all_individuals
            if ind.id_number == blq[1]
        ]
        ind = inds[0]
        if ind != individual:
            self.statedigraph.add_edge(str(ind.server), str(server))

action_at_blockage

action_at_blockage(individual, next_node)

Take action when the node's block_individual method is called.

Adds edges between the blocked server and the servers of the next node.

Parameters:

Name Type Description Default
individual Individual

The individual being blocked.

required
next_node Node

The node the individual is blocked from entering.

required
Source code in hciw/deadlock/deadlock_detector.py
131
132
133
134
135
136
137
138
139
140
141
142
143
def action_at_blockage(self, individual, next_node):
    """Take action when the node's ``block_individual`` method is called.

    Adds edges between the blocked server and the servers of the next
    node.

    Args:
        individual (hciw.Individual): The individual being blocked.
        next_node (hciw.Node): The node the individual is blocked from
            entering.
    """
    for svr in next_node.servers:
        self.statedigraph.add_edge(str(individual.server), str(svr))

action_at_detatch_server

action_at_detatch_server(server)

Take action when the node's detatch_server method is called.

Removes any edges of servers that have been detached.

Parameters:

Name Type Description Default
server Server

The server being detached.

required
Source code in hciw/deadlock/deadlock_detector.py
145
146
147
148
149
150
151
152
153
154
155
156
def action_at_detatch_server(self, server):
    """Take action when the node's ``detatch_server`` method is called.

    Removes any edges of servers that have been detached.

    Args:
        server (hciw.Server): The server being detached.
    """
    self.statedigraph.remove_edges_from(
        list(self.statedigraph.in_edges(str(server)))
        + list(self.statedigraph.out_edges(str(server)))
    )

Simulation internals

The classes below model the internal workings of a simulation. They are documented for reference and for advanced use, but most users will interact with HCiw only through hciw.create_network and hciw.Simulation.

node

Transient node of the queueing network.

DataRecord module-attribute

DataRecord = namedtuple(
    "Record",
    [
        "id_number",
        "customer_class",
        "original_customer_class",
        "node",
        "arrival_date",
        "waiting_time",
        "service_start_date",
        "service_time",
        "service_end_date",
        "time_blocked",
        "exit_date",
        "destination",
        "queue_size_at_arrival",
        "queue_size_at_departure",
        "server_id",
        "record_type",
    ],
)

Individual

Individual(
    id_number,
    customer_class="Customer",
    priority_class=0,
    simulation=False,
)

Represent an individual agent in a queueing network simulation.

An Individual models the flow of an agent through a queueing network, tracking its arrival time, service time, priority and other relevant data.

Attributes:

Name Type Description
arrival_date bool or float

Timestamp of the individual's arrival.

service_start_date bool or float

Timestamp the current service started.

service_time bool or float

Duration of the current service.

service_end_date bool or float

Timestamp the current service ends.

exit_date bool or float

Timestamp the individual exits the system.

id_number int

Unique identifier for the individual.

data_records list

The data records attached to the individual.

customer_class str

The customer class of the individual.

previous_class str

The previous customer class of the individual.

priority_class int

The priority class of the individual.

prev_priority_class int

The previous priority class of the individual.

original_class str

The original customer class of the individual.

is_blocked bool

Whether the individual is blocked.

server bool or Server

The server assigned to the individual, if any.

queue_size_at_arrival bool or int

Queue size when the individual arrived.

queue_size_at_departure bool or int

Queue size when the individual departed.

destination bool or int

The destination node in the network.

interrupted bool

Whether the individual's service was interrupted.

node bool or int

The node where the individual is located.

simulation bool or Simulation

The simulation the individual belongs to.

Example

.. code-block:: python

individual = Individual(
    id_number=1, customer_class='Gold', priority_class=1,
    simulation=True,
)

Initialise an Individual.

Parameters:

Name Type Description Default
id_number int

A unique identifier for the individual.

required
customer_class str

The customer class the individual belongs to. Defaults to 'Customer'.

'Customer'
priority_class int

The priority class of the individual. Defaults to 0.

0
simulation bool or Simulation

The simulation the individual is part of. Defaults to False.

False
Source code in hciw/individual.py
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
def __init__(
    self, id_number, customer_class="Customer", priority_class=0, simulation=False
):
    """Initialise an ``Individual``.

    Args:
        id_number (int): A unique identifier for the individual.
        customer_class (str, optional): The customer class the individual
            belongs to. Defaults to ``'Customer'``.
        priority_class (int, optional): The priority class of the
            individual. Defaults to ``0``.
        simulation (bool or Simulation, optional): The simulation the
            individual is part of. Defaults to ``False``.
    """
    self.arrival_date = False
    self.service_start_date = False
    self.service_time: float | bool | str = False
    self.service_end_date = False
    self.exit_date = False
    self.id_number = id_number
    self.data_records = []
    self.customer_class = customer_class
    self.previous_class = customer_class
    self.priority_class = priority_class
    self.prev_priority_class = priority_class
    self.original_class = customer_class
    self.is_blocked = False
    self.server: Server | bool = False
    self.queue_size_at_arrival: int | bool = False
    self.queue_size_at_departure: int | bool = False
    self.destination: int | bool = False
    self.interrupted = False
    self.node = False
    self.simulation = simulation

Schedule

Schedule(
    numbers_of_servers: list[int],
    shift_end_dates: list[float],
    preemption: bool | str = False,
    offset: float = 0.0,
)

Store server schedule information and generate the next shifts.

Attributes:

Name Type Description
schedule_type str

The type of the schedule.

shift_end_dates List[float]

The end dates of each shift.

numbers_of_servers List[int]

The number of servers for each shift.

preemption Union[bool, str]

The pre-emption option.

cyclelength float

The length of the schedule cycle.

offset float

The offset applied to the schedule.

Initialise a Schedule instance.

Parameters:

Name Type Description Default
numbers_of_servers List[int]

The number of servers working at each shift.

required
shift_end_dates List[float]

The end dates of each shift.

required
preemption Union[bool, str]

The pre-emption option, one of 'resume', 'restart', 'resample', 'reroute' or False. Defaults to False.

False
offset float

A non-negative offset applied to the schedule. Defaults to 0.0.

0.0

Raises:

Type Description
ValueError

If preemption or offset is invalid.

Source code in hciw/schedules.py
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
def __init__(
    self,
    numbers_of_servers: list[int],
    shift_end_dates: list[float],
    preemption: bool | str = False,
    offset: float = 0.0,
) -> None:
    """Initialise a ``Schedule`` instance.

    Args:
        numbers_of_servers (List[int]): The number of servers working at
            each shift.
        shift_end_dates (List[float]): The end dates of each shift.
        preemption (Union[bool, str], optional): The pre-emption option,
            one of ``'resume'``, ``'restart'``, ``'resample'``,
            ``'reroute'`` or ``False``. Defaults to ``False``.
        offset (float, optional): A non-negative offset applied to the
            schedule. Defaults to ``0.0``.

    Raises:
        ValueError: If ``preemption`` or ``offset`` is invalid.
    """
    if preemption not in [False, "resume", "restart", "resample", "reroute"]:
        raise ValueError(
            "Preemption must be False, 'resume', 'restart', "
            "'resample', 'reroute', or False."
        )
    if not isinstance(offset, float):
        raise ValueError(  # noqa: TRY004 - public API raises ValueError
            "Offset should be a positive float."
        )
    if offset < 0.0:
        raise ValueError("Offset should be a positive float.")
    self.schedule_type = "schedule"
    self.shift_end_dates = shift_end_dates
    self.numbers_of_servers = numbers_of_servers
    self.preemption = preemption
    self.cyclelength = self.shift_end_dates[-1]
    self.offset = offset

initialise

initialise() -> None

Initialise the generator at the beginning of a simulation.

Source code in hciw/schedules.py
58
59
60
61
62
63
64
65
def initialise(self) -> None:
    """Initialise the generator at the beginning of a simulation."""
    self.c = 0
    self.next_shift_change_date = self.offset
    self.next_c = self.numbers_of_servers[0]
    self.schedule_generator = self.get_schedule_generator(
        self.shift_end_dates, self.numbers_of_servers, self.offset
    )

get_schedule_generator

get_schedule_generator(
    boundaries: list[float],
    values: list[int],
    offset: float,
) -> Generator[tuple[float, int], None, None]

Yield the next shift date and server count.

Parameters:

Name Type Description Default
boundaries List[float]

The shift boundaries.

required
values List[int]

The number of servers corresponding to each boundary.

required
offset float

The offset applied to the schedule.

required

Yields:

Type Description
Tuple[float, int]

The next shift date and the number of servers.

Source code in hciw/schedules.py
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
def get_schedule_generator(
    self, boundaries: list[float], values: list[int], offset: float
) -> Generator[tuple[float, int], None, None]:
    """Yield the next shift date and server count.

    Args:
        boundaries (List[float]): The shift boundaries.
        values (List[int]): The number of servers corresponding to each
            boundary.
        offset (float): The offset applied to the schedule.

    Yields:
        Tuple[float, int]: The next shift date and the number of servers.
    """
    num_boundaries = len(boundaries)
    index = 0
    date = 0
    while True:
        date = (
            offset
            + boundaries[index % num_boundaries]
            + ((index) // num_boundaries * self.cyclelength)
        )
        index += 1
        yield date, values[index % num_boundaries]

get_next_shift

get_next_shift() -> None

Update the next shift from the generator.

Source code in hciw/schedules.py
93
94
95
96
97
98
def get_next_shift(self) -> None:
    """Update the next shift from the generator."""
    self.c = self.next_c
    date, c = next(self.schedule_generator)
    self.next_shift_change_date = date
    self.next_c = c

Slotted

Slotted(
    slots,
    slot_sizes,
    capacitated=False,
    preemption=False,
    offset=0.0,
)

Bases: Schedule

A schedule divided into slots with a number of servers per slot.

Attributes:

Name Type Description
schedule_type str

The type of the schedule.

offset float

The offset applied to the schedule.

slots list

The slot boundaries.

slot_sizes list

The number of servers in each slot.

next_slot_sizes list

The slot sizes of the next cycle.

capacitated bool

Whether the slots are capacitated.

preemption Union[bool, str]

The pre-emption option.

cyclelength float

The length of the schedule cycle.

c int

The current number of servers.

Initialise a Slotted schedule instance.

Parameters:

Name Type Description Default
slots list

The slot boundaries.

required
slot_sizes list

The number of servers in each slot.

required
capacitated bool

Whether the slots are capacitated. Defaults to False.

False
preemption Union[bool, str]

The pre-emption option, one of 'resume', 'restart', 'resample' or False. Defaults to False.

False
offset float

A non-negative offset applied to the schedule. Defaults to 0.0.

0.0

Raises:

Type Description
ValueError

If preemption or offset is invalid.

Source code in hciw/schedules.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def __init__(
    self, slots, slot_sizes, capacitated=False, preemption=False, offset=0.0
):
    """Initialise a ``Slotted`` schedule instance.

    Args:
        slots (list): The slot boundaries.
        slot_sizes (list): The number of servers in each slot.
        capacitated (bool, optional): Whether the slots are capacitated.
            Defaults to ``False``.
        preemption (Union[bool, str], optional): The pre-emption option,
            one of ``'resume'``, ``'restart'``, ``'resample'`` or
            ``False``. Defaults to ``False``.
        offset (float, optional): A non-negative offset applied to the
            schedule. Defaults to ``0.0``.

    Raises:
        ValueError: If ``preemption`` or ``offset`` is invalid.
    """
    if not capacitated and preemption is not False:
        raise ValueError(
            "Pre-emption options not availale for non-capacitated slots."
        )
    if preemption not in [False, "resume", "restart", "resample"]:
        raise ValueError(
            "Pre-emption options should be either 'resume', 'restart', "
            "'resample', or False."
        )
    if not isinstance(offset, float):
        raise ValueError(  # noqa: TRY004 - public API raises ValueError
            "Offset should be a positive float."
        )
    if offset < 0.0:
        raise ValueError("Offset should be a positive float.")
    self.schedule_type = "slotted"
    self.offset = offset
    self.slots = slots
    self.slot_sizes = slot_sizes
    self.next_slot_sizes = [self.slot_sizes[-1], *self.slot_sizes[:-1]]
    self.capacitated = capacitated
    self.preemption = preemption
    self.cyclelength = self.slots[-1]
    self.c = 0

initialise

initialise()

Initialise the generator at the beginning of a simulation.

Source code in hciw/schedules.py
160
161
162
163
164
165
def initialise(self):
    """Initialise the generator at the beginning of a simulation."""
    self.schedule_generator = self.get_schedule_generator(
        self.slots, self.next_slot_sizes, self.offset
    )
    self.get_next_slot()

get_next_slot

get_next_slot()

Update the next slot date and size from the generator.

Source code in hciw/schedules.py
167
168
169
170
171
def get_next_slot(self):
    """Update the next slot date and size from the generator."""
    date, size = next(self.schedule_generator)
    self.next_slot_date = date
    self.slot_size = size

Server

Server(node, id_number, start_date: float | Decimal = 0.0)

Contain information about a server.

Initialise the server.

Parameters:

Name Type Description Default
node Node

The node the server belongs to.

required
id_number int

Identifier for the server.

required
start_date float | Decimal

The time at which the server started, and whose type sets the precision of the server's accumulated times.

0.0
Source code in hciw/server.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def __init__(self, node, id_number, start_date: float | Decimal = 0.0):
    """Initialise the server.

    Args:
        node (Node): The node the server belongs to.
        id_number (int): Identifier for the server.
        start_date (float | Decimal): The time at which the server started,
            and whose type sets the precision of the server's accumulated
            times.
    """
    self.node = node
    self.id_number = id_number
    self.cust = False
    self.busy = False
    self.offduty = False
    self.all_time = False
    self.start_date = start_date
    zero = type(start_date)()
    self.busy_time = zero
    self.total_time = zero
    self.shift_end = False
    self.next_end_service_date = float("Inf")

utilisation property

utilisation

Get the server utilisation.

Returns:

Type Description
float | Decimal

The proportion of total time the server was busy.

Node

Node(id_, simulation)

Represent a node in the queueing network.

The node holds a queue of individuals and, where applicable, the servers that process them.

Initialise a node.

Parameters:

Name Type Description Default
id_ int

Identifier for the node.

required
simulation object

The simulation to which this node belongs.

required
Source code in hciw/node.py
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
def __init__(self, id_, simulation):
    """Initialise a node.

    Args:
        id_ (int): Identifier for the node.
        simulation (object): The simulation to which this node belongs.
    """
    self.simulation = simulation
    node = self.simulation.network.service_centres[id_ - 1]
    self.server_priority_function = node.server_priority_function
    self.service_discipline = node.service_discipline
    self.next_event_type: str | None = None
    self.schedule: Schedule | Slotted | None
    if isinstance(node.number_of_servers, Schedule):
        self.schedule = node.number_of_servers
        self.schedule.initialise()
        self.c: int | float = self.schedule.c
        if isinstance(self.schedule, Slotted):
            self.slotted: bool = True
            self.next_event_date = self.schedule.next_slot_date
            self.next_event_type = "slotted_service"
        else:
            self.slotted = False
            self.next_event_date = self.schedule.next_shift_change_date
            self.next_shift_change = self.schedule.next_shift_change_date
            self.next_event_type = "shift_change"
    else:
        self.c = node.number_of_servers
        self.schedule = None
        self.slotted = False
        self.next_event_date = float("Inf")
        self.next_shift_change = float("Inf")
    self.node_capacity = node.queueing_capacity + self.c
    self.class_change = node.class_change_matrix
    self.individuals = [[] for _ in range(simulation.number_of_priority_classes)]
    self.number_of_individuals = 0
    self.number_in_service = 0
    self.id_number = id_
    self.baulking_functions = {
        clss: self.simulation.network.customer_classes[clss].baulking_functions[
            id_ - 1
        ]
        for clss in self.simulation.network.customer_class_names
    }
    self.overtime = []
    self.blocked_queue = []
    self.len_blocked_queue = 0
    if not isinf(self.c):
        self.servers = self.create_starting_servers()
    self.highest_id = self.c
    self.simulation.deadlock_detector.initialise_at_node(self)
    self.priority_preempt = node.priority_preempt
    self.interrupted_individuals = []
    self.number_interrupted_individuals = 0
    self.all_servers_total = []
    self.all_servers_busy = []
    self.reneging = node.reneging
    self.dynamic_classes = node.class_change_time
    self.next_class_change_date = float("Inf")
    self.next_individual: list[Individual] | Individual | None = None

now property

now

Get the current simulation time.

Returns:

Type Description
float

The current simulation time.

all_individuals property

all_individuals

Get all individuals currently at the node.

Returns:

Type Description
list

Every individual at the node, across all priority classes.

accept

accept(next_individual, completed=False)

Accept a new individual into the queue.

Removes any previous exit date and blockage status, begins service if possible, records arrival information, and updates the state tracker.

Parameters:

Name Type Description Default
next_individual Individual

The individual to accept.

required
completed bool

Whether the individual has completed service.

False
Source code in hciw/node.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def accept(self, next_individual, completed=False):
    """Accept a new individual into the queue.

    Removes any previous exit date and blockage status, begins service
    if possible, records arrival information, and updates the state
    tracker.

    Args:
        next_individual (Individual): The individual to accept.
        completed (bool): Whether the individual has completed service.
    """
    next_individual.node = self.id_number
    next_individual.exit_date = False
    next_individual.is_blocked = False
    next_individual.original_class = next_individual.customer_class
    next_individual.queue_size_at_arrival = self.number_of_individuals
    self.individuals[next_individual.priority_class].append(next_individual)
    self.number_of_individuals += 1
    self.begin_service_if_possible_accept(next_individual)
    self.simulation.statetracker.change_state_accept(self, next_individual)

add_new_servers

add_new_servers(num_servers)

Add the appropriate number of servers for the given shift.

Parameters:

Name Type Description Default
num_servers int

The number of servers to add.

required
Source code in hciw/node.py
129
130
131
132
133
134
135
136
137
138
139
def add_new_servers(self, num_servers):
    """Add the appropriate number of servers for the given shift.

    Args:
        num_servers (int): The number of servers to add.
    """
    for _i in range(num_servers):
        self.highest_id += 1
        self.servers.append(
            self.simulation.ServerType(self, self.highest_id, self.now)
        )

attach_server

attach_server(server, individual)

Attach a server to an individual, and vice versa.

Parameters:

Name Type Description Default
server Server

The server to attach.

required
individual Individual

The individual to attach.

required
Source code in hciw/node.py
141
142
143
144
145
146
147
148
149
150
151
152
153
def attach_server(self, server, individual):
    """Attach a server to an individual, and vice versa.

    Args:
        server (Server): The server to attach.
        individual (Individual): The individual to attach.
    """
    server.cust = individual
    server.busy = True
    individual.server = server
    self.simulation.deadlock_detector.action_at_attach_server(
        self, server, individual
    )

begin_service_if_possible_accept

begin_service_if_possible_accept(next_individual)

Begin service for an individual at the acceptance point.

Sets the arrival date to the current time, and if a server is free or there are infinitely many servers, attaches the server, gets the service start time, service time and service end time, and updates the server's end date.

Parameters:

Name Type Description Default
next_individual Individual

The individual to serve.

required
Source code in hciw/node.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def begin_service_if_possible_accept(self, next_individual):
    """Begin service for an individual at the acceptance point.

    Sets the arrival date to the current time, and if a server is free
    or there are infinitely many servers, attaches the server, gets the
    service start time, service time and service end time, and updates
    the server's end date.

    Args:
        next_individual (Individual): The individual to serve.
    """
    next_individual.arrival_date = self.now
    if self.reneging is True:
        next_individual.reneging_date = self.get_reneging_date(next_individual)
    self.decide_class_change(next_individual)

    ind = next_individual if isinf(self.c) else self.choose_next_customer()

    if ind is not None:
        free_server = self.find_free_server(ind)
        if free_server is None and isinf(self.c) is False and self.c > 0:
            self.decide_preempt(ind)
        if free_server is not None or isinf(self.c):
            if isinf(self.c) is False:
                self.attach_server(free_server, ind)
            ind.service_start_date = self.now
            ind.service_time = self.get_service_time(ind)
            ind.service_end_date = self.now + ind.service_time
            self.number_in_service += 1
            self.reset_class_change(ind)
            if not isinf(self.c):
                assert isinstance(free_server, Server)
                free_server.next_end_service_date = ind.service_end_date

begin_interrupted_individuals_service

begin_interrupted_individuals_service(srvr)

Restart an interrupted individual's service.

The individual's service time is resampled.

Parameters:

Name Type Description Default
srvr Server

The server to give to the individual.

required
Source code in hciw/node.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
def begin_interrupted_individuals_service(self, srvr):
    """Restart an interrupted individual's service.

    The individual's service time is resampled.

    Args:
        srvr (Server): The server to give to the individual.
    """
    ind = next(iter(self.interrupted_individuals))
    if ind.is_blocked:
        node_blocked_to = self.simulation.nodes[ind.destination]
        ind.destination = False
        node_blocked_to.blocked_queue.remove((self.id_number, ind.id_number))
        node_blocked_to.len_blocked_queue -= 1
        ind.is_blocked = False
    self.attach_server(srvr, ind)
    self.give_service_time_after_preemption(ind)
    ind.service_start_date = self.now
    ind.service_end_date = self.now + ind.service_time
    ind.interrupted = False
    self.number_in_service += 1
    srvr.next_end_service_date = ind.service_end_date
    self.interrupted_individuals.remove(ind)
    self.number_interrupted_individuals -= 1

begin_service_if_possible_change_shift

begin_service_if_possible_change_shift()

Begin service for customers after a shift change.

If there are free servers, restarts interrupted customers' services and begins service for any waiting customers by giving a start date and end date and attaching servers to individuals.

Source code in hciw/node.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def begin_service_if_possible_change_shift(self):
    """Begin service for customers after a shift change.

    If there are free servers, restarts interrupted customers' services
    and begins service for any waiting customers by giving a start date
    and end date and attaching servers to individuals.
    """
    free_servers = [s for s in self.servers if not s.busy]
    for srvr in free_servers:
        if self.number_interrupted_individuals > 0:
            self.begin_interrupted_individuals_service(srvr)
        else:
            ind = self.choose_next_customer()
            if ind is not None:
                self.attach_server(srvr, ind)
                ind.service_start_date = self.now
                self.give_individual_a_service_time(ind)
                ind.service_end_date = self.increment_time(
                    ind.service_start_date, ind.service_time
                )
                self.number_in_service += 1
                self.reset_class_change(ind)
                srvr.next_end_service_date = ind.service_end_date

begin_service_if_possible_release

begin_service_if_possible_release(
    next_individual, newly_free_server
)

Begin service for an individual after a release.

Checks for interrupted individuals left to restart service, then gives an arrival date and service time, gives a start date and end date, and attaches the server to the individual.

Parameters:

Name Type Description Default
next_individual Individual

The individual being released.

required
newly_free_server Server

The server freed by the release.

required
Source code in hciw/node.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
def begin_service_if_possible_release(self, next_individual, newly_free_server):
    """Begin service for an individual after a release.

    Checks for interrupted individuals left to restart service, then
    gives an arrival date and service time, gives a start date and end
    date, and attaches the server to the individual.

    Args:
        next_individual (Individual): The individual being released.
        newly_free_server (Server): The server freed by the release.
    """
    if newly_free_server is not None and newly_free_server in self.servers:
        if self.number_interrupted_individuals > 0:
            self.begin_interrupted_individuals_service(newly_free_server)
        else:
            ind = self.choose_next_customer()
            if ind is not None:
                self.attach_server(newly_free_server, ind)
                ind.service_start_date = self.now
                self.give_individual_a_service_time(ind)
                ind.service_end_date = self.increment_time(
                    ind.service_start_date, ind.service_time
                )
                self.number_in_service += 1
                self.reset_class_change(ind)
                newly_free_server.next_end_service_date = ind.service_end_date

block_individual

block_individual(individual, next_node)

Block an individual from entering the next node.

Changes the blocked status, updates the state tracker, adds information to the next node's blocked queue, and updates the deadlock detector, informing the simulation that there are unchecked blockages for deadlock detection.

Parameters:

Name Type Description Default
individual Individual

The individual being blocked.

required
next_node Node

The node the individual is blocked from entering.

required
Source code in hciw/node.py
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
def block_individual(self, individual, next_node):
    """Block an individual from entering the next node.

    Changes the blocked status, updates the state tracker, adds
    information to the next node's blocked queue, and updates the
    deadlock detector, informing the simulation that there are unchecked
    blockages for deadlock detection.

    Args:
        individual (Individual): The individual being blocked.
        next_node (Node): The node the individual is blocked from
            entering.
    """
    individual.is_blocked = True
    self.simulation.statetracker.change_state_block(self, next_node, individual)
    next_node.blocked_queue.append((self.id_number, individual.id_number))
    next_node.len_blocked_queue += 1
    self.simulation.deadlock_detector.action_at_blockage(individual, next_node)
    self.simulation.unchecked_blockage = True

change_customer_class

change_customer_class(individual)

Change an individual's customer class.

The new class is chosen according to a probability distribution.

Parameters:

Name Type Description Default
individual Individual

The individual whose class changes.

required
Source code in hciw/node.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
def change_customer_class(self, individual):
    """Change an individual's customer class.

    The new class is chosen according to a probability distribution.

    Args:
        individual (Individual): The individual whose class changes.
    """
    if self.class_change:
        individual.previous_class = individual.customer_class
        individual.customer_class = random_choice(
            self.simulation.network.customer_class_names,
            [
                self.class_change[individual.previous_class][clss_name]
                for clss_name in self.simulation.network.customer_class_names
            ],
        )
        individual.prev_priority_class = individual.priority_class
        individual.priority_class = self.simulation.network.priority_class_mapping[
            individual.customer_class
        ]

change_customer_class_while_waiting

change_customer_class_while_waiting()

Change the next queued individual's class while waiting.

The individual is moved between priority queues and preemption is decided if the priority class changes.

Source code in hciw/node.py
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def change_customer_class_while_waiting(self):
    """Change the next queued individual's class while waiting.

    The individual is moved between priority queues and preemption is
    decided if the priority class changes.
    """
    changing_individual = self.next_individual
    assert isinstance(changing_individual, Individual)
    changing_individual.customer_class = changing_individual.next_class
    changing_individual.priority_class = (
        self.simulation.network.priority_class_mapping[
            changing_individual.next_class
        ]
    )
    if (
        changing_individual.priority_class
        != changing_individual.prev_priority_class
    ):
        self.change_priority_queue(changing_individual)
        self.decide_preempt(changing_individual)
    self.simulation.statetracker.change_state_classchange(self, changing_individual)
    changing_individual.previous_class = changing_individual.next_class
    changing_individual.prev_priority_class = changing_individual.priority_class
    self.decide_class_change(changing_individual)

change_priority_queue

change_priority_queue(individual)

Move an individual to their new priority queue.

Parameters:

Name Type Description Default
individual Individual

The individual to move.

required
Source code in hciw/node.py
332
333
334
335
336
337
338
339
def change_priority_queue(self, individual):
    """Move an individual to their new priority queue.

    Args:
        individual (Individual): The individual to move.
    """
    self.individuals[individual.prev_priority_class].remove(individual)
    self.individuals[individual.priority_class].append(individual)

change_shift

change_shift()

Implement a server shift change.

Adds or deletes servers, or indicates which servers should go off duty, and begins any new services if there are free servers.

Source code in hciw/node.py
341
342
343
344
345
346
347
348
349
350
351
352
353
def change_shift(self):
    """Implement a server shift change.

    Adds or deletes servers, or indicates which servers should go off
    duty, and begins any new services if there are free servers.
    """
    assert self.schedule is not None
    self.schedule.get_next_shift()
    self.next_shift_change = self.schedule.next_shift_change_date
    self.c = self.schedule.c
    self.take_servers_off_duty(preemption=self.schedule.preemption)
    self.add_new_servers(self.schedule.c)
    self.begin_service_if_possible_change_shift()

find_number_of_slotted_services

find_number_of_slotted_services()

Find the number of slotted services to start in this slot.

Returns:

Type Description
int

The number of services to start.

Source code in hciw/node.py
355
356
357
358
359
360
361
362
363
364
365
366
367
def find_number_of_slotted_services(self):
    """Find the number of slotted services to start in this slot.

    Returns:
        int: The number of services to start.
    """
    assert isinstance(self.schedule, Slotted)
    if self.schedule.capacitated:
        return min(
            max(self.schedule.slot_size - self.number_in_service, 0),
            self.number_of_individuals,
        )
    return min(self.schedule.slot_size, self.number_of_individuals)

interrupt_slotted_services

interrupt_slotted_services()

Interrupt services when there is not enough slot capacity.

Finds the customers whose slotted services need to be interrupted because of insufficient capacity at the current slot, and interrupts their services.

Source code in hciw/node.py
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
def interrupt_slotted_services(self):
    """Interrupt services when there is not enough slot capacity.

    Finds the customers whose slotted services need to be interrupted
    because of insufficient capacity at the current slot, and interrupts
    their services.
    """
    assert isinstance(self.schedule, Slotted)
    if self.schedule.capacitated and self.schedule.preemption is not False:
        number_to_interrupt = self.number_in_service - self.schedule.slot_size
        if number_to_interrupt > 0:
            inds_to_interrupt = sorted(
                [
                    ind
                    for ind in self.all_individuals
                    if ind.service_start_date is not False
                ],
                key=lambda x: (x.priority_class, x.arrival_date),
                reverse=True,
            )[:number_to_interrupt]
            for ind in inds_to_interrupt:
                self.interrupt_service(ind)

slotted_service

slotted_service()

Serve a set number of customers at the current time.

Only the number of customers allowed by the current slot may have service at the exact time the method is called.

Source code in hciw/node.py
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
def slotted_service(self):
    """Serve a set number of customers at the current time.

    Only the number of customers allowed by the current slot may have
    service at the exact time the method is called.
    """
    assert isinstance(self.schedule, Slotted)
    number_of_slotted_services = self.find_number_of_slotted_services()
    self.interrupt_slotted_services()
    for _i in range(number_of_slotted_services):
        if self.number_interrupted_individuals > 0:
            ind = self.interrupted_individuals[0]
            self.interrupted_individuals.remove(ind)
            self.number_interrupted_individuals -= 1
        else:
            ind = self.choose_next_customer()
        if ind is not None:
            ind.service_start_date = self.now
            self.give_individual_a_service_time(ind)
            ind.service_end_date = self.now + ind.service_time
            ind.server = True
            self.number_in_service += 1
            self.reset_class_change(ind)
    self.schedule.get_next_slot()

choose_next_customer

choose_next_customer()

Choose which customer will be next to be served.

Returns:

Type Description
Individual

The customer chosen by the service discipline, or None if no customer is waiting.

Source code in hciw/node.py
417
418
419
420
421
422
423
424
425
426
427
428
429
430
def choose_next_customer(self):
    """Choose which customer will be next to be served.

    Returns:
        Individual: The customer chosen by the service discipline, or
            None if no customer is waiting.
    """
    for priority_individuals in self.individuals:
        waiting_individuals = [
            ind for ind in priority_individuals if not ind.server
        ]
        if len(waiting_individuals) > 0:
            return self.service_discipline(waiting_individuals, self.now)
    return None

create_starting_servers

create_starting_servers()

Initialise the servers.

Returns:

Type Description
list

The newly created starting servers.

Source code in hciw/node.py
432
433
434
435
436
437
438
439
440
def create_starting_servers(self):
    """Initialise the servers.

    Returns:
        list: The newly created starting servers.
    """
    return [
        self.simulation.ServerType(self, i + 1, 0.0) for i in range(int(self.c))
    ]

decide_class_change

decide_class_change(next_individual)

Decide the next class and class change date for an individual.

Parameters:

Name Type Description Default
next_individual Individual

The individual to update.

required
Source code in hciw/node.py
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
def decide_class_change(self, next_individual):
    """Decide the next class and class change date for an individual.

    Args:
        next_individual (Individual): The individual to update.
    """
    if self.dynamic_classes is True:
        next_time = float("inf")
        next_class = next_individual.customer_class
        for clss, dist in self.simulation.network.customer_classes[
            next_individual.customer_class
        ].class_change_time_distributions.items():
            if dist is not None:
                t = dist.sample()
                if t < next_time:
                    next_time = t
                    next_class = clss
        next_individual.next_class = next_class
        next_individual.class_change_date = self.increment_time(self.now, next_time)
        self.find_next_class_change()

decide_preempt

decide_preempt(individual)

Decide whether priority preemption is needed.

If needed, finds the individual to preempt and preempts them.

Parameters:

Name Type Description Default
individual Individual

The individual seeking service.

required
Source code in hciw/node.py
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def decide_preempt(self, individual):
    """Decide whether priority preemption is needed.

    If needed, finds the individual to preempt and preempts them.

    Args:
        individual (Individual): The individual seeking service.
    """
    if self.priority_preempt:
        least_priority = max(s.cust.priority_class for s in self.servers)
        if individual.priority_class < least_priority:
            least_prioritised_individuals = [
                s.cust
                for s in self.servers
                if s.cust.priority_class == least_priority
            ]
            individual_to_preempt = max(
                list(least_prioritised_individuals),
                key=lambda cust: cust.service_start_date,
            )
            self.preempt(individual_to_preempt, individual)

detatch_server

detatch_server(server, individual)

Detach a server from an individual, and vice versa.

Parameters:

Name Type Description Default
server Server

The server to detach.

required
individual Individual

The individual to detach.

required
Source code in hciw/node.py
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
def detatch_server(self, server, individual):
    """Detach a server from an individual, and vice versa.

    Args:
        server (Server): The server to detach.
        individual (Individual): The individual to detach.
    """
    self.simulation.deadlock_detector.action_at_detatch_server(server)
    server.cust = False
    server.busy = False
    individual.server = False
    server.busy_time = self.increment_time(
        server.busy_time, individual.exit_date - individual.service_start_date
    )
    server.total_time = self.now - server.start_date
    if server.offduty:
        self.kill_server(server)

find_free_server

find_free_server(ind)

Find a free server.

Parameters:

Name Type Description Default
ind Individual

The individual needing a server.

required

Returns:

Type Description
Server

A free server, or None if none is available.

Source code in hciw/node.py
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def find_free_server(self, ind):
    """Find a free server.

    Args:
        ind (Individual): The individual needing a server.

    Returns:
        Server: A free server, or None if none is available.
    """
    if isinf(self.c):
        return None

    if self.server_priority_function is None:
        all_servers = self.servers
    else:
        all_servers = sorted(
            self.servers, key=lambda srv: self.server_priority_function(srv, ind)
        )

    for svr in all_servers:
        if not svr.busy:
            return svr
    return None

decide_between_simultaneous_individuals

decide_between_simultaneous_individuals()

Find the next individual that should now finish service.

Returns:

Type Description
Individual

A randomly chosen individual when several finish at the same time.

Source code in hciw/node.py
527
528
529
530
531
532
533
534
535
536
537
538
539
def decide_between_simultaneous_individuals(self):
    """Find the next individual that should now finish service.

    Returns:
        Individual: A randomly chosen individual when several finish at
            the same time.
    """
    assert isinstance(self.next_individual, list)
    if len(self.next_individual) > 1:
        next_individual = random_choice(self.next_individual)
    else:
        next_individual = self.next_individual[0]
    return next_individual

find_next_class_change

find_next_class_change()

Update the next class change date and individual.

Source code in hciw/node.py
541
542
543
544
545
546
547
548
def find_next_class_change(self):
    """Update the next class change date and individual."""
    self.next_class_change_date = float("inf")
    self.next_class_change_ind = None
    for ind in self.all_individuals:
        if (ind.class_change_date < self.next_class_change_date) and not ind.server:
            self.next_class_change_ind = ind
            self.next_class_change_date = ind.class_change_date

find_server_utilisation

find_server_utilisation()

Find the overall server utilisation for the node.

Source code in hciw/node.py
550
551
552
553
554
555
556
557
558
559
560
def find_server_utilisation(self):
    """Find the overall server utilisation for the node."""
    if isinf(self.c) or self.c == 0:
        self.server_utilisation = None
    else:
        for server in self.servers:
            self.all_servers_total.append(server.total_time)
            self.all_servers_busy.append(server.busy_time)
        self.server_utilisation = sum(self.all_servers_busy) / sum(
            self.all_servers_total
        )

finish_service

finish_service()

Finish the service of the next individual.

Finds the individual to finish service, checks whether they need to change class, finds their next node, and releases the individual if there is capacity at the destination, otherwise causes blockage.

Source code in hciw/node.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
def finish_service(self):
    """Finish the service of the next individual.

    Finds the individual to finish service, checks whether they need to
    change class, finds their next node, and releases the individual if
    there is capacity at the destination, otherwise causes blockage.
    """
    next_individual = self.decide_between_simultaneous_individuals()
    self.change_customer_class(next_individual)
    next_node = self.next_node(next_individual)
    next_individual.destination = next_node.id_number
    if not isinf(self.c) and self.c > 0:
        assert isinstance(next_individual.server, Server)
        next_individual.server.next_end_service_date = float("Inf")
    if next_node.number_of_individuals < next_node.node_capacity:
        self.release(next_individual, next_node)
    else:
        self.block_individual(next_individual, next_node)

give_individual_a_service_time

give_individual_a_service_time(individual)

Give a service time to an individual.

Either a new service time, or a service time after pre-emption.

Parameters:

Name Type Description Default
individual Individual

The individual to serve.

required
Source code in hciw/node.py
581
582
583
584
585
586
587
588
589
590
591
592
def give_individual_a_service_time(self, individual):
    """Give a service time to an individual.

    Either a new service time, or a service time after pre-emption.

    Args:
        individual (Individual): The individual to serve.
    """
    if individual.service_time is False:
        individual.service_time = self.get_service_time(individual)
    else:
        self.give_service_time_after_preemption(individual)

give_service_time_after_preemption

give_service_time_after_preemption(individual)

Give a service time after preemption.

Either resamples, restarts, or resumes service time from where it was left off.

Parameters:

Name Type Description Default
individual Individual

The individual to serve.

required
Source code in hciw/node.py
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
def give_service_time_after_preemption(self, individual):
    """Give a service time after preemption.

    Either resamples, restarts, or resumes service time from where it was
    left off.

    Args:
        individual (Individual): The individual to serve.
    """
    if individual.service_time == "resample":
        individual.service_time = self.get_service_time(individual)
    if individual.service_time == "restart":
        individual.service_time = individual.original_service_time
    if individual.service_time == "resume":
        individual.service_time = individual.time_left

have_event

have_event()

Trigger the node's next event.

Source code in hciw/node.py
610
611
612
613
614
615
616
617
618
619
620
621
def have_event(self):
    """Trigger the node's next event."""
    if self.next_event_type == "end_service":
        self.finish_service()
    elif self.next_event_type == "shift_change":
        self.change_shift()
    elif self.next_event_type == "renege":
        self.renege()
    elif self.next_event_type == "class_change":
        self.change_customer_class_while_waiting()
    elif self.next_event_type == "slotted_service":
        self.slotted_service()

increment_time

increment_time(original, increment)

Increment the original time by the increment.

Parameters:

Name Type Description Default
original float

The original time.

required
increment float

The amount to add.

required

Returns:

Type Description

The incremented time.

Source code in hciw/node.py
623
624
625
626
627
628
629
630
631
632
633
def increment_time(self, original, increment):
    """Increment the original time by the increment.

    Args:
        original (float): The original time.
        increment (float): The amount to add.

    Returns:
        The incremented time.
    """
    return original + increment

kill_server

kill_server(srvr)

Kill a server when it goes off duty.

Parameters:

Name Type Description Default
srvr Server

The server to remove.

required
Source code in hciw/node.py
635
636
637
638
639
640
641
642
643
644
645
646
def kill_server(self, srvr):
    """Kill a server when it goes off duty.

    Args:
        srvr (Server): The server to remove.
    """
    srvr.total_time = self.increment_time(self.next_event_date, -srvr.start_date)
    self.overtime.append(self.increment_time(self.next_event_date, -srvr.shift_end))
    self.all_servers_busy.append(srvr.busy_time)
    self.all_servers_total.append(srvr.total_time)
    indx = self.servers.index(srvr)
    del self.servers[indx]

next_node

next_node(ind)

Find the next node according to the routing method.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/node.py
648
649
650
651
652
653
654
655
656
657
658
659
def next_node(self, ind):
    """Find the next node according to the routing method.

    Args:
        ind (Individual): The individual being routed.

    Returns:
        Node: The next node.
    """
    return self.simulation.routers[ind.customer_class].next_node(
        ind, self.id_number
    )

next_node_for_rerouting

next_node_for_rerouting(ind)

Find the next node for rerouting according to the routing method.

Parameters:

Name Type Description Default
ind Individual

The individual being rerouted.

required

Returns:

Type Description
Node

The next node for rerouting.

Source code in hciw/node.py
661
662
663
664
665
666
667
668
669
670
671
672
def next_node_for_rerouting(self, ind):
    """Find the next node for rerouting according to the routing method.

    Args:
        ind (Individual): The individual being rerouted.

    Returns:
        Node: The next node for rerouting.
    """
    return self.simulation.routers[ind.customer_class].next_node_for_rerouting(
        ind, self.id_number
    )

next_node_for_jockeying

next_node_for_jockeying(ind)

Find the next node for jockeying according to the routing method.

Parameters:

Name Type Description Default
ind Individual

The jockeying individual.

required

Returns:

Type Description
Node

The next node for jockeying.

Source code in hciw/node.py
674
675
676
677
678
679
680
681
682
683
684
685
def next_node_for_jockeying(self, ind):
    """Find the next node for jockeying according to the routing method.

    Args:
        ind (Individual): The jockeying individual.

    Returns:
        Node: The next node for jockeying.
    """
    return self.simulation.routers[ind.customer_class].next_node_for_jockeying(
        ind, self.id_number
    )

preempt

preempt(individual_to_preempt, next_individual)

Preempt an individual and replace them in service.

Parameters:

Name Type Description Default
individual_to_preempt Individual

The individual removed from service.

required
next_individual Individual

The individual taking their place.

required
Source code in hciw/node.py
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
def preempt(self, individual_to_preempt, next_individual):
    """Preempt an individual and replace them in service.

    Args:
        individual_to_preempt (Individual): The individual removed from
            service.
        next_individual (Individual): The individual taking their place.
    """
    server = individual_to_preempt.server
    individual_to_preempt.original_service_time = individual_to_preempt.service_time
    if self.priority_preempt == "reroute":
        self.reroute(individual_to_preempt)
    else:
        self.write_interruption_record(individual_to_preempt)
        individual_to_preempt.service_start_date = False
        individual_to_preempt.time_left = (
            individual_to_preempt.service_end_date - self.now
        )
        individual_to_preempt.service_time = self.priority_preempt
        individual_to_preempt.service_end_date = False
        self.detatch_server(server, individual_to_preempt)
        self.decide_class_change(individual_to_preempt)
    self.attach_server(server, next_individual)
    next_individual.service_start_date = self.now
    next_individual.service_time = self.get_service_time(next_individual)
    next_individual.service_end_date = self.now + next_individual.service_time
    self.reset_class_change(next_individual)
    server.next_end_service_date = next_individual.service_end_date

release

release(next_individual, next_node, reroute=False)

Release an individual from the node.

Removes the individual from the queue, records relevant information, detaches the server, updates the state tracker, begins service of any waiting customers, sends the individual to their next destination, and releases any individuals blocked by this node.

Parameters:

Name Type Description Default
next_individual Individual

The individual to release.

required
next_node Node

The node receiving the individual.

required
reroute bool

Whether the release is due to rerouting.

False
Source code in hciw/node.py
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
def release(self, next_individual, next_node, reroute=False):
    """Release an individual from the node.

    Removes the individual from the queue, records relevant information,
    detaches the server, updates the state tracker, begins service of any
    waiting customers, sends the individual to their next destination, and
    releases any individuals blocked by this node.

    Args:
        next_individual (Individual): The individual to release.
        next_node (Node): The node receiving the individual.
        reroute (bool): Whether the release is due to rerouting.
    """
    self.individuals[next_individual.prev_priority_class].remove(next_individual)
    self.number_of_individuals -= 1
    self.number_in_service -= 1
    next_individual.queue_size_at_departure = self.number_of_individuals
    next_individual.exit_date = self.now
    if not reroute:
        self.write_individual_record(next_individual)
    newly_free_server = None
    if not isinf(self.c) and not self.slotted:
        newly_free_server = next_individual.server
        self.detatch_server(newly_free_server, next_individual)
    if self.slotted:
        next_individual.server = False
    self.reset_individual_attributes(next_individual)
    self.simulation.statetracker.change_state_release(
        self, next_node, next_individual, next_individual.is_blocked
    )
    if not reroute:
        self.begin_service_if_possible_release(next_individual, newly_free_server)
    next_node.accept(next_individual)
    if not reroute:
        self.release_blocked_individual()

release_blocked_individual

release_blocked_individual()

Release an individual blocked by this node.

Finds the individual who has been blocked the longest, removes them from the blocked queue, restores their service if it was interrupted, and releases them from their node.

Source code in hciw/node.py
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
def release_blocked_individual(self):
    """Release an individual blocked by this node.

    Finds the individual who has been blocked the longest, removes them
    from the blocked queue, restores their service if it was interrupted,
    and releases them from their node.
    """
    if (self.len_blocked_queue > 0) and (
        self.number_of_individuals < self.node_capacity
    ):
        node_to_receive_from = self.simulation.nodes[self.blocked_queue[0][0]]
        individual_to_receive_index = [
            ind.id_number for ind in node_to_receive_from.all_individuals
        ].index(self.blocked_queue[0][1])
        individual_to_receive = node_to_receive_from.all_individuals[
            individual_to_receive_index
        ]
        self.blocked_queue.pop(0)
        self.len_blocked_queue -= 1
        if individual_to_receive.interrupted:
            individual_to_receive.interrupted = False
            individual_to_receive.service_start_date = (
                individual_to_receive.original_service_start_date
            )
            individual_to_receive.service_end_date = (
                individual_to_receive.service_start_date
                + individual_to_receive.original_service_time
            )
            node_to_receive_from.interrupted_individuals.remove(
                individual_to_receive
            )
            node_to_receive_from.number_interrupted_individuals -= 1
        node_to_receive_from.release(individual_to_receive, self)

reset_class_change

reset_class_change(individual)

Reset an individual's class change date when service begins.

Parameters:

Name Type Description Default
individual Individual

The individual beginning service.

required
Source code in hciw/node.py
786
787
788
789
790
791
792
793
794
795
def reset_class_change(self, individual):
    """Reset an individual's class change date when service begins.

    Args:
        individual (Individual): The individual beginning service.
    """
    if self.dynamic_classes:
        individual.class_change_date = float("inf")
        if individual == self.next_class_change_ind:
            self.find_next_class_change()

renege

renege()

Remove an individual from the queue due to reneging.

Resets the individual's reneging date and sends the individual to their reneging destination.

Source code in hciw/node.py
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
def renege(self):
    """Remove an individual from the queue due to reneging.

    Resets the individual's reneging date and sends the individual to
    their reneging destination.
    """
    reneging_individual = self.decide_between_simultaneous_individuals()
    reneging_individual.reneging_date = float("Inf")
    next_node = self.next_node_for_jockeying(reneging_individual)
    self.individuals[reneging_individual.prev_priority_class].remove(
        reneging_individual
    )
    self.number_of_individuals -= 1
    reneging_individual.queue_size_at_departure = self.number_of_individuals
    reneging_individual.exit_date = self.now
    self.write_reneging_record(reneging_individual)
    self.reset_individual_attributes(reneging_individual)
    self.simulation.statetracker.change_state_renege(
        self, next_node, reneging_individual, blocked=False
    )
    next_node.accept(reneging_individual, completed=False)
    self.release_blocked_individual()

get_reneging_date

get_reneging_date(ind)

Return the reneging date for a given individual.

Parameters:

Name Type Description Default
ind Individual

The individual.

required

Returns:

Type Description
float

The reneging date.

Source code in hciw/node.py
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
def get_reneging_date(self, ind):
    """Return the reneging date for a given individual.

    Args:
        ind (Individual): The individual.

    Returns:
        float: The reneging date.
    """
    dist = self.simulation.network.customer_classes[
        ind.customer_class
    ].reneging_time_distributions[self.id_number - 1]
    if dist is None:
        return float("inf")
    return self.now + dist.sample(t=self.now, ind=ind)

get_service_time

get_service_time(ind)

Return a service time for the given customer class.

Parameters:

Name Type Description Default
ind Individual

The individual being served.

required

Returns:

Type Description
float

The sampled service time.

Source code in hciw/node.py
836
837
838
839
840
841
842
843
844
845
846
847
def get_service_time(self, ind):
    """Return a service time for the given customer class.

    Args:
        ind (Individual): The individual being served.

    Returns:
        float: The sampled service time.
    """
    return self.simulation.service_times[self.id_number][ind.customer_class].sample(
        t=self.now, ind=ind
    )

take_servers_off_duty

take_servers_off_duty(preemption: bool | str = False)

Gather the servers that should be deleted.

Parameters:

Name Type Description Default
preemption bool

Whether preemption is active for this shift change.

False
Source code in hciw/node.py
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
def take_servers_off_duty(self, preemption: bool | str = False):
    """Gather the servers that should be deleted.

    Args:
        preemption (bool): Whether preemption is active for this shift
            change.
    """
    if not preemption:
        to_delete = []
        for srvr in self.servers:
            srvr.shift_end = self.next_event_date
            if srvr.busy:
                srvr.offduty = True
            else:
                to_delete.append(srvr)
    else:
        to_delete = self.servers[::1]  # copy
        for s in self.servers:
            s.shift_end = self.next_event_date
            if s.cust is not False:
                self.interrupt_service(s.cust)
        self.sort_interrupted_individuals()
    for obs in to_delete:
        self.kill_server(obs)

interrupt_service

interrupt_service(individual)

Interrupt an individual's service.

Places the individual in the interrupted queue and writes an interruption record for them.

Parameters:

Name Type Description Default
individual Individual

The individual being interrupted.

required
Source code in hciw/node.py
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
def interrupt_service(self, individual):
    """Interrupt an individual's service.

    Places the individual in the interrupted queue and writes an
    interruption record for them.

    Args:
        individual (Individual): The individual being interrupted.
    """
    individual.original_service_time = individual.service_time
    assert self.schedule is not None
    if self.schedule.preemption == "reroute":
        self.reroute(individual)
    else:
        self.interrupted_individuals.append(individual)
        individual.interrupted = True
        self.number_interrupted_individuals += 1
        self.write_interruption_record(individual)
        individual.original_service_start_date = individual.service_start_date
        individual.service_start_date = False
        individual.time_left = individual.service_end_date - self.now
        individual.service_time = self.schedule.preemption
        individual.service_end_date = False
        self.number_in_service -= 1

sort_interrupted_individuals

sort_interrupted_individuals()

Sort interrupted individuals by priority class and arrival date.

Source code in hciw/node.py
899
900
901
902
903
def sort_interrupted_individuals(self):
    """Sort interrupted individuals by priority class and arrival date."""
    self.interrupted_individuals.sort(
        key=lambda x: (x.priority_class, x.arrival_date)
    )

reroute

reroute(individual)

Reroute a preempted individual.

Parameters:

Name Type Description Default
individual Individual

The individual being rerouted.

required
Source code in hciw/node.py
905
906
907
908
909
910
911
912
913
def reroute(self, individual):
    """Reroute a preempted individual.

    Args:
        individual (Individual): The individual being rerouted.
    """
    next_node = self.next_node_for_rerouting(individual)
    self.write_interruption_record(individual, destination=next_node.id_number)
    self.release(individual, next_node, reroute=True)

update_next_end_service_without_server

update_next_end_service_without_server()

Update the next end of a slotted service in possible_next_events.

Source code in hciw/node.py
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
def update_next_end_service_without_server(self):
    """Update the next end of a slotted service in `possible_next_events`."""
    if self.slotted or isinf(self.c):
        next_end_service_date = float("Inf")
        for ind in self.all_individuals:
            if not ind.is_blocked and ind.service_end_date >= self.now:
                if ind.service_end_date < next_end_service_date:
                    self.possible_next_events["end_service"] = (
                        [ind],
                        ind.service_end_date,
                    )
                    next_end_service_date = ind.service_end_date
                elif (ind.service_end_date == next_end_service_date) and (
                    not isinf(next_end_service_date)
                ):
                    self.possible_next_events["end_service"][0].append(ind)

update_next_end_service_with_server

update_next_end_service_with_server()

Update the next end service with a server in possible_next_events.

Source code in hciw/node.py
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
def update_next_end_service_with_server(self):
    """Update the next end service with a server in `possible_next_events`."""
    if not self.slotted and not isinf(self.c):
        next_end_service_date = float("Inf")
        for s in self.servers:
            if s.next_end_service_date < next_end_service_date:
                self.possible_next_events["end_service"] = (
                    [s.cust],
                    s.next_end_service_date,
                )
                next_end_service_date = s.next_end_service_date
            elif (s.next_end_service_date == next_end_service_date) and (
                not isinf(next_end_service_date)
            ):
                self.possible_next_events["end_service"][0].append(s.cust)

update_next_renege_time

update_next_renege_time()

Update the next renege time in possible_next_events.

Source code in hciw/node.py
948
949
950
951
952
953
954
955
956
957
958
959
960
961
def update_next_renege_time(self):
    """Update the next renege time in `possible_next_events`."""
    if not isinf(self.c) and self.reneging is True:
        next_renege_date = float("Inf")
        for ind in self.all_individuals:
            if (ind.reneging_date < next_renege_date) and not ind.server:
                self.possible_next_events["renege"] = ([ind], ind.reneging_date)
                next_renege_date = ind.reneging_date
            elif (
                (ind.reneging_date == next_renege_date)
                and (not ind.server)
                and (not isinf(next_renege_date))
            ):
                self.possible_next_events["renege"][0].append(ind)

update_next_class_change_while_waiting

update_next_class_change_while_waiting()

Update the next class change while waiting in possible_next_events.

Source code in hciw/node.py
963
964
965
966
967
968
969
def update_next_class_change_while_waiting(self):
    """Update the next class change while waiting in `possible_next_events`."""
    if self.dynamic_classes is True and not isinf(self.c):
        self.possible_next_events["class_change"] = (
            self.next_class_change_ind,
            self.next_class_change_date,
        )

update_next_shift_change_or_slot_time

update_next_shift_change_or_slot_time()

Update possible_next_events with the next shift or slot time.

Source code in hciw/node.py
971
972
973
974
975
976
977
978
979
980
981
982
983
def update_next_shift_change_or_slot_time(self):
    """Update `possible_next_events` with the next shift or slot time."""
    if self.schedule is not None:
        if self.schedule.schedule_type == "schedule":
            self.possible_next_events["shift_change"] = (
                None,
                self.next_shift_change,
            )
        if isinstance(self.schedule, Slotted):
            self.possible_next_events["slotted_service"] = (
                None,
                self.schedule.next_slot_date,
            )

update_next_event_date

update_next_event_date()

Find the time of the next event at this node.

With infinite servers this is the time of the next individual to end service, or infinity. Otherwise it is the minimum of the next shift change and the time for the next individual (who is not blocked) to end service, or infinity.

Source code in hciw/node.py
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
def update_next_event_date(self):
    """Find the time of the next event at this node.

    With infinite servers this is the time of the next individual to end
    service, or infinity. Otherwise it is the minimum of the next shift
    change and the time for the next individual (who is not blocked) to
    end service, or infinity.
    """
    self.possible_next_events = {}
    self.update_next_end_service_without_server()
    self.update_next_end_service_with_server()
    self.update_next_renege_time()
    self.update_next_class_change_while_waiting()
    self.update_next_shift_change_or_slot_time()
    if self.reneging or self.dynamic_classes or self.schedule:
        next_event, self.next_event_type = self.decide_next_event()
        self.next_event_date = next_event[1]
        self.next_individual = next_event[0]
    else:
        next_end_service = self.possible_next_events.get(
            "end_service", (None, float("inf"))
        )
        self.next_event_date = next_end_service[1]
        self.next_individual = next_end_service[0]
        self.next_event_type = "end_service"

decide_next_event

decide_next_event()

Decide the next event.

Chooses the next service end, renege, shift change, or class change. In the case of a tie, events are prioritised in the following order:

  1. slotted service
  2. shift change
  3. end service
  4. class change
  5. renege

Returns:

Type Description
tuple

The next event and its event type.

Source code in hciw/node.py
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
def decide_next_event(self):
    """Decide the next event.

    Chooses the next service end, renege, shift change, or class change.
    In the case of a tie, events are prioritised in the following order:

    1. slotted service
    2. shift change
    3. end service
    4. class change
    5. renege

    Returns:
        tuple: The next event and its event type.
    """
    next_date = float("inf")
    next_event = (None, float("inf"))
    next_event_type = None
    for event_type in [
        "slotted_service",
        "shift_change",
        "end_service",
        "class_change",
        "renege",
    ]:
        possible_next_event = self.possible_next_events.get(
            event_type, (None, float("inf"))
        )
        if possible_next_event[1] < next_date:
            next_event = possible_next_event
            next_event_type = event_type
            next_date = next_event[1]
    return next_event, next_event_type

wrap_up_servers

wrap_up_servers(current_time)

Update server total and busy times at the end of the run.

Parameters:

Name Type Description Default
current_time float

The simulation end time.

required
Source code in hciw/node.py
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
def wrap_up_servers(self, current_time):
    """Update server total and busy times at the end of the run.

    Args:
        current_time (float): The simulation end time.
    """
    if not isinf(self.c):
        for srvr in self.servers:
            srvr.total_time = self.increment_time(current_time, -srvr.start_date)
            if srvr.busy:
                srvr.busy_time += self.increment_time(
                    current_time, -srvr.cust.service_start_date
                )

write_individual_record

write_individual_record(individual)

Write a data record for an individual leaving a node.

Parameters:

Name Type Description Default
individual Individual

The individual leaving the node.

required
Source code in hciw/node.py
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
def write_individual_record(self, individual):
    """Write a data record for an individual leaving a node.

    Args:
        individual (Individual): The individual leaving the node.
    """
    if isinf(self.c) or self.slotted:
        server_id = False
    else:
        server_id = individual.server.id_number

    record = DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=individual.arrival_date,
        waiting_time=individual.service_start_date - individual.arrival_date,
        service_start_date=individual.service_start_date,
        service_time=individual.service_end_date - individual.service_start_date,
        service_end_date=individual.service_end_date,
        time_blocked=individual.exit_date - individual.service_end_date,
        exit_date=individual.exit_date,
        destination=individual.destination,
        queue_size_at_arrival=individual.queue_size_at_arrival,
        queue_size_at_departure=individual.queue_size_at_departure,
        server_id=server_id,
        record_type="service",
    )
    individual.data_records.append(record)

write_incomplete_record

write_incomplete_record(individual)

Write an incomplete data record at the end of the run.

Parameters:

Name Type Description Default
individual Individual

The individual still at the node.

required

Returns:

Type Description
DataRecord

The incomplete data record.

Source code in hciw/node.py
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
def write_incomplete_record(self, individual):
    """Write an incomplete data record at the end of the run.

    Args:
        individual (Individual): The individual still at the node.

    Returns:
        DataRecord: The incomplete data record.
    """
    if not individual.service_time:  # Still in queue
        service_start_date = None
        waiting_time = None
        service_time = None
        service_end_date = None
    else:
        service_start_date = individual.service_start_date
        waiting_time = individual.service_start_date - individual.arrival_date
        if individual.service_end_date > self.now:  # Still in service
            service_time = None
            service_end_date = None
        else:  # Still blocked
            service_time = individual.service_time
            service_end_date = individual.service_end_date

    return DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=individual.arrival_date,
        waiting_time=waiting_time,
        service_start_date=service_start_date,
        service_time=service_time,
        service_end_date=service_end_date,
        time_blocked=None,
        exit_date=None,
        destination=None,
        queue_size_at_arrival=individual.queue_size_at_arrival,
        queue_size_at_departure=None,
        server_id=False,
        record_type="incomplete",
    )

write_interruption_record

write_interruption_record(individual, destination=nan)

Write a data record for an individual being interrupted.

Parameters:

Name Type Description Default
individual Individual

The interrupted individual.

required
destination int

The destination node number.

nan
Source code in hciw/node.py
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
def write_interruption_record(self, individual, destination=nan):
    """Write a data record for an individual being interrupted.

    Args:
        individual (Individual): The interrupted individual.
        destination (int): The destination node number.
    """
    server_id = False if self.slotted else individual.server.id_number

    record = DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=individual.arrival_date,
        waiting_time=individual.service_start_date - individual.arrival_date,
        service_start_date=individual.service_start_date,
        service_time=individual.original_service_time,
        service_end_date=nan,
        time_blocked=nan,
        exit_date=self.now,
        destination=destination,
        queue_size_at_arrival=individual.queue_size_at_arrival,
        queue_size_at_departure=individual.queue_size_at_departure,
        server_id=server_id,
        record_type="interrupted service",
    )
    individual.data_records.append(record)

write_reneging_record

write_reneging_record(individual)

Write a data record for an individual when reneging.

Parameters:

Name Type Description Default
individual Individual

The reneging individual.

required
Source code in hciw/node.py
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
def write_reneging_record(self, individual):
    """Write a data record for an individual when reneging.

    Args:
        individual (Individual): The reneging individual.
    """
    record = DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=individual.arrival_date,
        waiting_time=individual.exit_date - individual.arrival_date,
        service_start_date=nan,
        service_time=nan,
        service_end_date=nan,
        time_blocked=nan,
        exit_date=individual.exit_date,
        destination=individual.destination,
        queue_size_at_arrival=individual.queue_size_at_arrival,
        queue_size_at_departure=individual.queue_size_at_departure,
        server_id=nan,
        record_type="renege",
    )
    individual.data_records.append(record)

write_baulking_or_rejection_record

write_baulking_or_rejection_record(individual, record_type)

Write a data record for an individual who baulks or is rejected.

Parameters:

Name Type Description Default
individual Individual

The individual.

required
record_type str

The type of record to write.

required
Source code in hciw/node.py
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
def write_baulking_or_rejection_record(self, individual, record_type):
    """Write a data record for an individual who baulks or is rejected.

    Args:
        individual (Individual): The individual.
        record_type (str): The type of record to write.
    """
    record = DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=self.now,
        waiting_time=nan,
        service_start_date=nan,
        service_time=nan,
        service_end_date=nan,
        time_blocked=nan,
        exit_date=self.now,
        destination=nan,
        queue_size_at_arrival=self.number_of_individuals,
        queue_size_at_departure=nan,
        server_id=nan,
        record_type=record_type,
    )
    individual.data_records.append(record)

reset_individual_attributes

reset_individual_attributes(individual)

Reset the attributes of an individual.

Parameters:

Name Type Description Default
individual Individual

The individual to reset.

required
Source code in hciw/node.py
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
def reset_individual_attributes(self, individual):
    """Reset the attributes of an individual.

    Args:
        individual (Individual): The individual to reset.
    """
    individual.arrival_date = False
    individual.service_time = False
    individual.service_start_date = False
    individual.service_end_date = False
    individual.exit_date = False
    individual.queue_size_at_arrival = False
    individual.queue_size_at_departure = False
    individual.destination = False

flatten_list

flatten_list(list_of_lists)

Flatten a list of lists into a single list.

Parameters:

Name Type Description Default
list_of_lists list

A list whose elements are themselves lists.

required

Returns:

Type Description
list

A single list containing all elements of the inner lists.

Source code in hciw/auxiliary.py
76
77
78
79
80
81
82
83
84
85
86
87
88
def flatten_list(list_of_lists):
    """Flatten a list of lists into a single list.

    Args:
        list_of_lists (list): A list whose elements are themselves lists.

    Returns:
        list: A single list containing all elements of the inner lists.
    """
    flat = []
    for a_list in list_of_lists:
        flat += a_list
    return flat

random_choice

random_choice(array, probs=None)

Select a random element from an array using optional weights.

Parameters:

Name Type Description Default
array list

The values to choose from.

required
probs list

Probability weights corresponding to each value. If omitted, a uniform choice is made.

None

Returns:

Type Description

The randomly selected element of array.

Source code in hciw/auxiliary.py
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
def random_choice(array, probs=None):
    """Select a random element from an array using optional weights.

    Args:
        array (list): The values to choose from.
        probs (list, optional): Probability weights corresponding to each
            value. If omitted, a uniform choice is made.

    Returns:
        The randomly selected element of ``array``.
    """
    # If no pdf provided, assume uniform dist:
    if probs is None:
        index = int(random.random() * len(array))
        return array[index]

    # A common case, guaranteed to reach the Exit node;
    # No need to sample for this:
    if (set(probs[:-1]) == {0.0}) and (probs[-1] == 1.0):
        return array[-1]

    # Sample a random value from using pdf
    rdm_num = random.random()
    i, p = 0, probs[0]
    while rdm_num > p:
        i += 1
        p += probs[i]
    return array[i]

arrival_node

Arrival node of the queueing network.

ArrivalNode

ArrivalNode(simulation)

Represent the arrival node of the network.

This class handles arrivals, baulking, and rejections of individuals. See hciw.Simulation for the main simulation class.

Initialise the arrival node.

Parameters:

Name Type Description Default
simulation object

The simulation to which this arrival node belongs.

required
Source code in hciw/arrival_node.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def __init__(self, simulation):
    """Initialise the arrival node.

    Args:
        simulation (object): The simulation to which this arrival node
            belongs.
    """
    self.simulation = simulation
    self.number_of_individuals = 0
    self.all_individuals = []
    self.number_of_individuals_per_class = dict.fromkeys(
        self.simulation.network.customer_class_names, 0
    )
    self.number_accepted_individuals = 0
    self.number_accepted_individuals_per_class = dict.fromkeys(
        self.simulation.network.customer_class_names, 0
    )
    self.system_capacity = self.simulation.network.system_capacity
    self.event_dates_dict: dict[int, dict[str, float | bool]] = {
        nd + 1: dict.fromkeys(self.simulation.network.customer_class_names, False)
        for nd in range(self.simulation.network.number_of_nodes)
    }

initialise

initialise()

Initialise the event dates dictionary and next event date.

Source code in hciw/arrival_node.py
36
37
38
39
def initialise(self):
    """Initialise the event dates dictionary and next event date."""
    self.initialise_event_dates_dict()
    self.find_next_event_date()

decide_baulk

decide_baulk(next_node, next_individual)

Either make an individual baulk or send them to the next node.

Parameters:

Name Type Description Default
next_node Node

The next node.

required
next_individual Individual

The individual being considered.

required
Source code in hciw/arrival_node.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def decide_baulk(self, next_node, next_individual):
    """Either make an individual baulk or send them to the next node.

    Args:
        next_node (Node): The next node.
        next_individual (Individual): The individual being considered.
    """
    if next_node.baulking_functions[self.next_class] is None:
        self.send_individual(next_node, next_individual)
    else:
        rnd_num = random()
        if rnd_num < next_node.baulking_functions[self.next_class](
            next_node.number_of_individuals,
            Q=self.simulation,
            next_ind=next_individual,
            next_node=next_node,
        ):
            self.record_baulk(next_node, next_individual)
            self.simulation.nodes[-1].accept(next_individual, completed=False)
        else:
            self.send_individual(next_node, next_individual)

find_next_event_date

find_next_event_date()

Find the time of the next arrival.

Source code in hciw/arrival_node.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def find_next_event_date(self):
    """Find the time of the next arrival."""
    minnd = None
    minclss = None
    mindate = float("Inf")
    for nd in self.event_dates_dict:
        for clss in self.event_dates_dict[nd]:
            if self.event_dates_dict[nd][clss] < mindate:
                minnd = nd
                minclss = clss
                mindate = self.event_dates_dict[nd][clss]
    self.next_node = minnd
    self.next_class = minclss
    self.next_event_date = mindate

have_event

have_event()

Create and send a batch of arriving individuals.

Finds a batch size, creates that many individuals, sends them to the relevant node, then updates the event dates dictionary.

Source code in hciw/arrival_node.py
 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
116
117
118
def have_event(self):
    """Create and send a batch of arriving individuals.

    Finds a batch size, creates that many individuals, sends them to the
    relevant node, then updates the event dates dictionary.
    """
    batch = self.batch_size(self.next_node, self.next_class)
    assert self.next_node is not None
    assert self.next_class is not None
    for _ in range(batch):
        self.number_of_individuals += 1
        self.number_of_individuals_per_class[self.next_class] += 1
        priority_class = self.simulation.network.priority_class_mapping[
            self.next_class
        ]
        next_individual = self.simulation.IndividualType(
            self.number_of_individuals,
            self.next_class,
            priority_class,
            simulation=self.simulation,
        )
        next_node = self.simulation.transitive_nodes[self.next_node - 1]
        next_individual.starting_node = next_node.id_number
        self.simulation.routers[
            next_individual.customer_class
        ].initialise_individual(next_individual)
        self.release_individual(next_node, next_individual)

    self.event_dates_dict[self.next_node][self.next_class] = self.increment_time(
        self.event_dates_dict[self.next_node][self.next_class],
        self.inter_arrival(self.next_node, self.next_class),
    )
    self.find_next_event_date()

increment_time

increment_time(original, increment)

Increment the original time by the increment.

Parameters:

Name Type Description Default
original float

The original time.

required
increment float

The amount to add.

required

Returns:

Type Description

The incremented time.

Source code in hciw/arrival_node.py
120
121
122
123
124
125
126
127
128
129
130
def increment_time(self, original, increment):
    """Increment the original time by the increment.

    Args:
        original (float): The original time.
        increment (float): The amount to add.

    Returns:
        The incremented time.
    """
    return original + increment

initialise_event_dates_dict

initialise_event_dates_dict()

Initialise the event dates dictionary.

Random times are sampled for each node and class.

Source code in hciw/arrival_node.py
132
133
134
135
136
137
138
139
140
141
142
def initialise_event_dates_dict(self):
    """Initialise the event dates dictionary.

    Random times are sampled for each node and class.
    """
    for nd in self.event_dates_dict:
        for clss in self.event_dates_dict[nd]:
            if self.simulation.inter_arrival_times[nd][clss] is not None:
                self.event_dates_dict[nd][clss] = self.inter_arrival(nd, clss)
            else:
                self.event_dates_dict[nd][clss] = float("inf")

inter_arrival

inter_arrival(nd, clss)

Sample the inter-arrival time for the next class and node.

Parameters:

Name Type Description Default
nd int

The destination node.

required
clss str

The customer class.

required

Returns:

Type Description

The sampled inter-arrival time.

Source code in hciw/arrival_node.py
144
145
146
147
148
149
150
151
152
153
154
155
156
def inter_arrival(self, nd, clss):
    """Sample the inter-arrival time for the next class and node.

    Args:
        nd (int): The destination node.
        clss (str): The customer class.

    Returns:
        The sampled inter-arrival time.
    """
    return self.simulation.inter_arrival_times[nd][clss]._sample(
        t=self.simulation.current_time
    )

batch_size

batch_size(nd, clss)

Sample the batch size for the next class and node.

Parameters:

Name Type Description Default
nd int

The destination node.

required
clss str

The customer class.

required

Returns:

Type Description
int

The sampled batch size.

Raises:

Type Description
ValueError

If the sampled batch size is not a non-negative integer.

Source code in hciw/arrival_node.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def batch_size(self, nd, clss):
    """Sample the batch size for the next class and node.

    Args:
        nd (int): The destination node.
        clss (str): The customer class.

    Returns:
        int: The sampled batch size.

    Raises:
        ValueError: If the sampled batch size is not a non-negative
            integer.
    """
    batch = self.simulation.batch_sizes[nd][clss]._sample(
        t=self.simulation.current_time
    )
    if isinstance(batch, int) and batch >= 0:
        return batch
    raise ValueError("Batch sizes must be positive integers.")

record_baulk

record_baulk(next_node, individual)

Write a baulking record for an individual.

Parameters:

Name Type Description Default
next_node Node

The node the individual would have joined.

required
individual Individual

The baulking individual.

required
Source code in hciw/arrival_node.py
179
180
181
182
183
184
185
186
def record_baulk(self, next_node, individual):
    """Write a baulking record for an individual.

    Args:
        next_node (Node): The node the individual would have joined.
        individual (Individual): The baulking individual.
    """
    next_node.write_baulking_or_rejection_record(individual, record_type="baulk")

record_rejection

record_rejection(next_node, individual)

Write a rejection record for an individual.

Parameters:

Name Type Description Default
next_node Node

The node the individual was rejected from.

required
individual Individual

The rejected individual.

required
Source code in hciw/arrival_node.py
188
189
190
191
192
193
194
195
196
197
def record_rejection(self, next_node, individual):
    """Write a rejection record for an individual.

    Args:
        next_node (Node): The node the individual was rejected from.
        individual (Individual): The rejected individual.
    """
    next_node.write_baulking_or_rejection_record(
        individual, record_type="rejection"
    )

release_individual

release_individual(next_node, next_individual)

Release an individual to the next node.

Either rejects the individual due to lack of capacity, or sends that individual to baulk or not.

Parameters:

Name Type Description Default
next_node Node

The next node.

required
next_individual Individual

The individual being released.

required
Source code in hciw/arrival_node.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
def release_individual(self, next_node, next_individual):
    """Release an individual to the next node.

    Either rejects the individual due to lack of capacity, or sends that
    individual to baulk or not.

    Args:
        next_node (Node): The next node.
        next_individual (Individual): The individual being released.
    """
    if (next_node.number_of_individuals >= next_node.node_capacity) or (
        self.simulation.number_of_individuals >= self.system_capacity
    ):
        self.record_rejection(next_node, next_individual)
        self.simulation.nodes[-1].accept(next_individual, completed=False)
    else:
        self.decide_baulk(next_node, next_individual)

send_individual

send_individual(next_node, next_individual)

Send the next individual to the next node.

Parameters:

Name Type Description Default
next_node Node

The next node.

required
next_individual Individual

The individual being sent.

required
Source code in hciw/arrival_node.py
217
218
219
220
221
222
223
224
225
226
def send_individual(self, next_node, next_individual):
    """Send the next individual to the next node.

    Args:
        next_node (Node): The next node.
        next_individual (Individual): The individual being sent.
    """
    self.number_accepted_individuals += 1
    self.number_accepted_individuals_per_class[next_individual.customer_class] += 1
    next_node.accept(next_individual)

update_next_event_date

update_next_event_date()

Do nothing, as the next event update happens at the event time.

Source code in hciw/arrival_node.py
228
229
def update_next_event_date(self):
    """Do nothing, as the next event update happens at the event time."""

exit_node

Exit node of the queueing network.

ExitNode

ExitNode()

Represent the exit node of the network.

Initialise the exit node.

Source code in hciw/exit_node.py
 7
 8
 9
10
11
12
13
14
def __init__(self):
    """Initialise the exit node."""
    self.all_individuals = []
    self.number_of_individuals = 0
    self.number_of_completed_individuals = 0
    self.id_number = -1
    self.next_event_date = float("Inf")
    self.node_capacity = float("Inf")

accept

accept(next_individual, completed=True)

Add an individual to the list of completed individuals.

Parameters:

Name Type Description Default
next_individual Individual

The individual to accept.

required
completed bool

Whether the individual completed service.

True
Source code in hciw/exit_node.py
24
25
26
27
28
29
30
31
32
33
34
35
def accept(self, next_individual, completed=True):
    """Add an individual to the list of completed individuals.

    Args:
        next_individual (Individual): The individual to accept.
        completed (bool): Whether the individual completed service.
    """
    self.all_individuals.append(next_individual)
    next_individual.node = -1
    self.number_of_individuals += 1
    if completed:
        self.number_of_completed_individuals += 1

update_next_event_date

update_next_event_date()

Do nothing, as the next event date is always the max time.

Source code in hciw/exit_node.py
37
38
def update_next_event_date(self):
    """Do nothing, as the next event date is always the max time."""

server

Server that processes individuals at a node.

Server

Server(node, id_number, start_date: float | Decimal = 0.0)

Contain information about a server.

Initialise the server.

Parameters:

Name Type Description Default
node Node

The node the server belongs to.

required
id_number int

Identifier for the server.

required
start_date float | Decimal

The time at which the server started, and whose type sets the precision of the server's accumulated times.

0.0
Source code in hciw/server.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def __init__(self, node, id_number, start_date: float | Decimal = 0.0):
    """Initialise the server.

    Args:
        node (Node): The node the server belongs to.
        id_number (int): Identifier for the server.
        start_date (float | Decimal): The time at which the server started,
            and whose type sets the precision of the server's accumulated
            times.
    """
    self.node = node
    self.id_number = id_number
    self.cust = False
    self.busy = False
    self.offduty = False
    self.all_time = False
    self.start_date = start_date
    zero = type(start_date)()
    self.busy_time = zero
    self.total_time = zero
    self.shift_end = False
    self.next_end_service_date = float("Inf")

utilisation property

utilisation

Get the server utilisation.

Returns:

Type Description
float | Decimal

The proportion of total time the server was busy.

individual

The individual agent that flows through a queueing network.

Server

Server(node, id_number, start_date: float | Decimal = 0.0)

Contain information about a server.

Initialise the server.

Parameters:

Name Type Description Default
node Node

The node the server belongs to.

required
id_number int

Identifier for the server.

required
start_date float | Decimal

The time at which the server started, and whose type sets the precision of the server's accumulated times.

0.0
Source code in hciw/server.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def __init__(self, node, id_number, start_date: float | Decimal = 0.0):
    """Initialise the server.

    Args:
        node (Node): The node the server belongs to.
        id_number (int): Identifier for the server.
        start_date (float | Decimal): The time at which the server started,
            and whose type sets the precision of the server's accumulated
            times.
    """
    self.node = node
    self.id_number = id_number
    self.cust = False
    self.busy = False
    self.offduty = False
    self.all_time = False
    self.start_date = start_date
    zero = type(start_date)()
    self.busy_time = zero
    self.total_time = zero
    self.shift_end = False
    self.next_end_service_date = float("Inf")

utilisation property

utilisation

Get the server utilisation.

Returns:

Type Description
float | Decimal

The proportion of total time the server was busy.

Individual

Individual(
    id_number,
    customer_class="Customer",
    priority_class=0,
    simulation=False,
)

Represent an individual agent in a queueing network simulation.

An Individual models the flow of an agent through a queueing network, tracking its arrival time, service time, priority and other relevant data.

Attributes:

Name Type Description
arrival_date bool or float

Timestamp of the individual's arrival.

service_start_date bool or float

Timestamp the current service started.

service_time bool or float

Duration of the current service.

service_end_date bool or float

Timestamp the current service ends.

exit_date bool or float

Timestamp the individual exits the system.

id_number int

Unique identifier for the individual.

data_records list

The data records attached to the individual.

customer_class str

The customer class of the individual.

previous_class str

The previous customer class of the individual.

priority_class int

The priority class of the individual.

prev_priority_class int

The previous priority class of the individual.

original_class str

The original customer class of the individual.

is_blocked bool

Whether the individual is blocked.

server bool or Server

The server assigned to the individual, if any.

queue_size_at_arrival bool or int

Queue size when the individual arrived.

queue_size_at_departure bool or int

Queue size when the individual departed.

destination bool or int

The destination node in the network.

interrupted bool

Whether the individual's service was interrupted.

node bool or int

The node where the individual is located.

simulation bool or Simulation

The simulation the individual belongs to.

Example

.. code-block:: python

individual = Individual(
    id_number=1, customer_class='Gold', priority_class=1,
    simulation=True,
)

Initialise an Individual.

Parameters:

Name Type Description Default
id_number int

A unique identifier for the individual.

required
customer_class str

The customer class the individual belongs to. Defaults to 'Customer'.

'Customer'
priority_class int

The priority class of the individual. Defaults to 0.

0
simulation bool or Simulation

The simulation the individual is part of. Defaults to False.

False
Source code in hciw/individual.py
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
def __init__(
    self, id_number, customer_class="Customer", priority_class=0, simulation=False
):
    """Initialise an ``Individual``.

    Args:
        id_number (int): A unique identifier for the individual.
        customer_class (str, optional): The customer class the individual
            belongs to. Defaults to ``'Customer'``.
        priority_class (int, optional): The priority class of the
            individual. Defaults to ``0``.
        simulation (bool or Simulation, optional): The simulation the
            individual is part of. Defaults to ``False``.
    """
    self.arrival_date = False
    self.service_start_date = False
    self.service_time: float | bool | str = False
    self.service_end_date = False
    self.exit_date = False
    self.id_number = id_number
    self.data_records = []
    self.customer_class = customer_class
    self.previous_class = customer_class
    self.priority_class = priority_class
    self.prev_priority_class = priority_class
    self.original_class = customer_class
    self.is_blocked = False
    self.server: Server | bool = False
    self.queue_size_at_arrival: int | bool = False
    self.queue_size_at_departure: int | bool = False
    self.destination: int | bool = False
    self.interrupted = False
    self.node = False
    self.simulation = simulation

data_record

The data record stored for each visit of an individual to a node.

Attributes:

Name Type Description
id_number int

Unique identifier of the individual.

customer_class str

The customer class of the individual.

original_customer_class str

The original customer class of the individual.

node int

The node the record refers to.

arrival_date float

The date the individual arrived at the node.

waiting_time float

The time the individual waited before service.

service_start_date float

The date service started.

service_time float

The duration of service.

service_end_date float

The date service ended.

time_blocked float

The time the individual spent blocked.

exit_date float

The date the individual left the node.

destination int

The destination node.

queue_size_at_arrival int

The queue size when the individual arrived.

queue_size_at_departure int

The queue size when the individual departed.

server_id int

The identifier of the server that served the individual.

record_type str

The type of record.

DataRecord module-attribute

DataRecord = namedtuple(
    "Record",
    [
        "id_number",
        "customer_class",
        "original_customer_class",
        "node",
        "arrival_date",
        "waiting_time",
        "service_start_date",
        "service_time",
        "service_end_date",
        "time_blocked",
        "exit_date",
        "destination",
        "queue_size_at_arrival",
        "queue_size_at_departure",
        "server_id",
        "record_type",
    ],
)

processor_sharing

Limited processor-sharing node.

Node

Node(id_, simulation)

Represent a node in the queueing network.

The node holds a queue of individuals and, where applicable, the servers that process them.

Initialise a node.

Parameters:

Name Type Description Default
id_ int

Identifier for the node.

required
simulation object

The simulation to which this node belongs.

required
Source code in hciw/node.py
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
def __init__(self, id_, simulation):
    """Initialise a node.

    Args:
        id_ (int): Identifier for the node.
        simulation (object): The simulation to which this node belongs.
    """
    self.simulation = simulation
    node = self.simulation.network.service_centres[id_ - 1]
    self.server_priority_function = node.server_priority_function
    self.service_discipline = node.service_discipline
    self.next_event_type: str | None = None
    self.schedule: Schedule | Slotted | None
    if isinstance(node.number_of_servers, Schedule):
        self.schedule = node.number_of_servers
        self.schedule.initialise()
        self.c: int | float = self.schedule.c
        if isinstance(self.schedule, Slotted):
            self.slotted: bool = True
            self.next_event_date = self.schedule.next_slot_date
            self.next_event_type = "slotted_service"
        else:
            self.slotted = False
            self.next_event_date = self.schedule.next_shift_change_date
            self.next_shift_change = self.schedule.next_shift_change_date
            self.next_event_type = "shift_change"
    else:
        self.c = node.number_of_servers
        self.schedule = None
        self.slotted = False
        self.next_event_date = float("Inf")
        self.next_shift_change = float("Inf")
    self.node_capacity = node.queueing_capacity + self.c
    self.class_change = node.class_change_matrix
    self.individuals = [[] for _ in range(simulation.number_of_priority_classes)]
    self.number_of_individuals = 0
    self.number_in_service = 0
    self.id_number = id_
    self.baulking_functions = {
        clss: self.simulation.network.customer_classes[clss].baulking_functions[
            id_ - 1
        ]
        for clss in self.simulation.network.customer_class_names
    }
    self.overtime = []
    self.blocked_queue = []
    self.len_blocked_queue = 0
    if not isinf(self.c):
        self.servers = self.create_starting_servers()
    self.highest_id = self.c
    self.simulation.deadlock_detector.initialise_at_node(self)
    self.priority_preempt = node.priority_preempt
    self.interrupted_individuals = []
    self.number_interrupted_individuals = 0
    self.all_servers_total = []
    self.all_servers_busy = []
    self.reneging = node.reneging
    self.dynamic_classes = node.class_change_time
    self.next_class_change_date = float("Inf")
    self.next_individual: list[Individual] | Individual | None = None

now property

now

Get the current simulation time.

Returns:

Type Description
float

The current simulation time.

all_individuals property

all_individuals

Get all individuals currently at the node.

Returns:

Type Description
list

Every individual at the node, across all priority classes.

accept

accept(next_individual, completed=False)

Accept a new individual into the queue.

Removes any previous exit date and blockage status, begins service if possible, records arrival information, and updates the state tracker.

Parameters:

Name Type Description Default
next_individual Individual

The individual to accept.

required
completed bool

Whether the individual has completed service.

False
Source code in hciw/node.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def accept(self, next_individual, completed=False):
    """Accept a new individual into the queue.

    Removes any previous exit date and blockage status, begins service
    if possible, records arrival information, and updates the state
    tracker.

    Args:
        next_individual (Individual): The individual to accept.
        completed (bool): Whether the individual has completed service.
    """
    next_individual.node = self.id_number
    next_individual.exit_date = False
    next_individual.is_blocked = False
    next_individual.original_class = next_individual.customer_class
    next_individual.queue_size_at_arrival = self.number_of_individuals
    self.individuals[next_individual.priority_class].append(next_individual)
    self.number_of_individuals += 1
    self.begin_service_if_possible_accept(next_individual)
    self.simulation.statetracker.change_state_accept(self, next_individual)

add_new_servers

add_new_servers(num_servers)

Add the appropriate number of servers for the given shift.

Parameters:

Name Type Description Default
num_servers int

The number of servers to add.

required
Source code in hciw/node.py
129
130
131
132
133
134
135
136
137
138
139
def add_new_servers(self, num_servers):
    """Add the appropriate number of servers for the given shift.

    Args:
        num_servers (int): The number of servers to add.
    """
    for _i in range(num_servers):
        self.highest_id += 1
        self.servers.append(
            self.simulation.ServerType(self, self.highest_id, self.now)
        )

attach_server

attach_server(server, individual)

Attach a server to an individual, and vice versa.

Parameters:

Name Type Description Default
server Server

The server to attach.

required
individual Individual

The individual to attach.

required
Source code in hciw/node.py
141
142
143
144
145
146
147
148
149
150
151
152
153
def attach_server(self, server, individual):
    """Attach a server to an individual, and vice versa.

    Args:
        server (Server): The server to attach.
        individual (Individual): The individual to attach.
    """
    server.cust = individual
    server.busy = True
    individual.server = server
    self.simulation.deadlock_detector.action_at_attach_server(
        self, server, individual
    )

begin_service_if_possible_accept

begin_service_if_possible_accept(next_individual)

Begin service for an individual at the acceptance point.

Sets the arrival date to the current time, and if a server is free or there are infinitely many servers, attaches the server, gets the service start time, service time and service end time, and updates the server's end date.

Parameters:

Name Type Description Default
next_individual Individual

The individual to serve.

required
Source code in hciw/node.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def begin_service_if_possible_accept(self, next_individual):
    """Begin service for an individual at the acceptance point.

    Sets the arrival date to the current time, and if a server is free
    or there are infinitely many servers, attaches the server, gets the
    service start time, service time and service end time, and updates
    the server's end date.

    Args:
        next_individual (Individual): The individual to serve.
    """
    next_individual.arrival_date = self.now
    if self.reneging is True:
        next_individual.reneging_date = self.get_reneging_date(next_individual)
    self.decide_class_change(next_individual)

    ind = next_individual if isinf(self.c) else self.choose_next_customer()

    if ind is not None:
        free_server = self.find_free_server(ind)
        if free_server is None and isinf(self.c) is False and self.c > 0:
            self.decide_preempt(ind)
        if free_server is not None or isinf(self.c):
            if isinf(self.c) is False:
                self.attach_server(free_server, ind)
            ind.service_start_date = self.now
            ind.service_time = self.get_service_time(ind)
            ind.service_end_date = self.now + ind.service_time
            self.number_in_service += 1
            self.reset_class_change(ind)
            if not isinf(self.c):
                assert isinstance(free_server, Server)
                free_server.next_end_service_date = ind.service_end_date

begin_interrupted_individuals_service

begin_interrupted_individuals_service(srvr)

Restart an interrupted individual's service.

The individual's service time is resampled.

Parameters:

Name Type Description Default
srvr Server

The server to give to the individual.

required
Source code in hciw/node.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
def begin_interrupted_individuals_service(self, srvr):
    """Restart an interrupted individual's service.

    The individual's service time is resampled.

    Args:
        srvr (Server): The server to give to the individual.
    """
    ind = next(iter(self.interrupted_individuals))
    if ind.is_blocked:
        node_blocked_to = self.simulation.nodes[ind.destination]
        ind.destination = False
        node_blocked_to.blocked_queue.remove((self.id_number, ind.id_number))
        node_blocked_to.len_blocked_queue -= 1
        ind.is_blocked = False
    self.attach_server(srvr, ind)
    self.give_service_time_after_preemption(ind)
    ind.service_start_date = self.now
    ind.service_end_date = self.now + ind.service_time
    ind.interrupted = False
    self.number_in_service += 1
    srvr.next_end_service_date = ind.service_end_date
    self.interrupted_individuals.remove(ind)
    self.number_interrupted_individuals -= 1

begin_service_if_possible_change_shift

begin_service_if_possible_change_shift()

Begin service for customers after a shift change.

If there are free servers, restarts interrupted customers' services and begins service for any waiting customers by giving a start date and end date and attaching servers to individuals.

Source code in hciw/node.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def begin_service_if_possible_change_shift(self):
    """Begin service for customers after a shift change.

    If there are free servers, restarts interrupted customers' services
    and begins service for any waiting customers by giving a start date
    and end date and attaching servers to individuals.
    """
    free_servers = [s for s in self.servers if not s.busy]
    for srvr in free_servers:
        if self.number_interrupted_individuals > 0:
            self.begin_interrupted_individuals_service(srvr)
        else:
            ind = self.choose_next_customer()
            if ind is not None:
                self.attach_server(srvr, ind)
                ind.service_start_date = self.now
                self.give_individual_a_service_time(ind)
                ind.service_end_date = self.increment_time(
                    ind.service_start_date, ind.service_time
                )
                self.number_in_service += 1
                self.reset_class_change(ind)
                srvr.next_end_service_date = ind.service_end_date

begin_service_if_possible_release

begin_service_if_possible_release(
    next_individual, newly_free_server
)

Begin service for an individual after a release.

Checks for interrupted individuals left to restart service, then gives an arrival date and service time, gives a start date and end date, and attaches the server to the individual.

Parameters:

Name Type Description Default
next_individual Individual

The individual being released.

required
newly_free_server Server

The server freed by the release.

required
Source code in hciw/node.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
def begin_service_if_possible_release(self, next_individual, newly_free_server):
    """Begin service for an individual after a release.

    Checks for interrupted individuals left to restart service, then
    gives an arrival date and service time, gives a start date and end
    date, and attaches the server to the individual.

    Args:
        next_individual (Individual): The individual being released.
        newly_free_server (Server): The server freed by the release.
    """
    if newly_free_server is not None and newly_free_server in self.servers:
        if self.number_interrupted_individuals > 0:
            self.begin_interrupted_individuals_service(newly_free_server)
        else:
            ind = self.choose_next_customer()
            if ind is not None:
                self.attach_server(newly_free_server, ind)
                ind.service_start_date = self.now
                self.give_individual_a_service_time(ind)
                ind.service_end_date = self.increment_time(
                    ind.service_start_date, ind.service_time
                )
                self.number_in_service += 1
                self.reset_class_change(ind)
                newly_free_server.next_end_service_date = ind.service_end_date

block_individual

block_individual(individual, next_node)

Block an individual from entering the next node.

Changes the blocked status, updates the state tracker, adds information to the next node's blocked queue, and updates the deadlock detector, informing the simulation that there are unchecked blockages for deadlock detection.

Parameters:

Name Type Description Default
individual Individual

The individual being blocked.

required
next_node Node

The node the individual is blocked from entering.

required
Source code in hciw/node.py
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
def block_individual(self, individual, next_node):
    """Block an individual from entering the next node.

    Changes the blocked status, updates the state tracker, adds
    information to the next node's blocked queue, and updates the
    deadlock detector, informing the simulation that there are unchecked
    blockages for deadlock detection.

    Args:
        individual (Individual): The individual being blocked.
        next_node (Node): The node the individual is blocked from
            entering.
    """
    individual.is_blocked = True
    self.simulation.statetracker.change_state_block(self, next_node, individual)
    next_node.blocked_queue.append((self.id_number, individual.id_number))
    next_node.len_blocked_queue += 1
    self.simulation.deadlock_detector.action_at_blockage(individual, next_node)
    self.simulation.unchecked_blockage = True

change_customer_class

change_customer_class(individual)

Change an individual's customer class.

The new class is chosen according to a probability distribution.

Parameters:

Name Type Description Default
individual Individual

The individual whose class changes.

required
Source code in hciw/node.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
def change_customer_class(self, individual):
    """Change an individual's customer class.

    The new class is chosen according to a probability distribution.

    Args:
        individual (Individual): The individual whose class changes.
    """
    if self.class_change:
        individual.previous_class = individual.customer_class
        individual.customer_class = random_choice(
            self.simulation.network.customer_class_names,
            [
                self.class_change[individual.previous_class][clss_name]
                for clss_name in self.simulation.network.customer_class_names
            ],
        )
        individual.prev_priority_class = individual.priority_class
        individual.priority_class = self.simulation.network.priority_class_mapping[
            individual.customer_class
        ]

change_customer_class_while_waiting

change_customer_class_while_waiting()

Change the next queued individual's class while waiting.

The individual is moved between priority queues and preemption is decided if the priority class changes.

Source code in hciw/node.py
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def change_customer_class_while_waiting(self):
    """Change the next queued individual's class while waiting.

    The individual is moved between priority queues and preemption is
    decided if the priority class changes.
    """
    changing_individual = self.next_individual
    assert isinstance(changing_individual, Individual)
    changing_individual.customer_class = changing_individual.next_class
    changing_individual.priority_class = (
        self.simulation.network.priority_class_mapping[
            changing_individual.next_class
        ]
    )
    if (
        changing_individual.priority_class
        != changing_individual.prev_priority_class
    ):
        self.change_priority_queue(changing_individual)
        self.decide_preempt(changing_individual)
    self.simulation.statetracker.change_state_classchange(self, changing_individual)
    changing_individual.previous_class = changing_individual.next_class
    changing_individual.prev_priority_class = changing_individual.priority_class
    self.decide_class_change(changing_individual)

change_priority_queue

change_priority_queue(individual)

Move an individual to their new priority queue.

Parameters:

Name Type Description Default
individual Individual

The individual to move.

required
Source code in hciw/node.py
332
333
334
335
336
337
338
339
def change_priority_queue(self, individual):
    """Move an individual to their new priority queue.

    Args:
        individual (Individual): The individual to move.
    """
    self.individuals[individual.prev_priority_class].remove(individual)
    self.individuals[individual.priority_class].append(individual)

change_shift

change_shift()

Implement a server shift change.

Adds or deletes servers, or indicates which servers should go off duty, and begins any new services if there are free servers.

Source code in hciw/node.py
341
342
343
344
345
346
347
348
349
350
351
352
353
def change_shift(self):
    """Implement a server shift change.

    Adds or deletes servers, or indicates which servers should go off
    duty, and begins any new services if there are free servers.
    """
    assert self.schedule is not None
    self.schedule.get_next_shift()
    self.next_shift_change = self.schedule.next_shift_change_date
    self.c = self.schedule.c
    self.take_servers_off_duty(preemption=self.schedule.preemption)
    self.add_new_servers(self.schedule.c)
    self.begin_service_if_possible_change_shift()

find_number_of_slotted_services

find_number_of_slotted_services()

Find the number of slotted services to start in this slot.

Returns:

Type Description
int

The number of services to start.

Source code in hciw/node.py
355
356
357
358
359
360
361
362
363
364
365
366
367
def find_number_of_slotted_services(self):
    """Find the number of slotted services to start in this slot.

    Returns:
        int: The number of services to start.
    """
    assert isinstance(self.schedule, Slotted)
    if self.schedule.capacitated:
        return min(
            max(self.schedule.slot_size - self.number_in_service, 0),
            self.number_of_individuals,
        )
    return min(self.schedule.slot_size, self.number_of_individuals)

interrupt_slotted_services

interrupt_slotted_services()

Interrupt services when there is not enough slot capacity.

Finds the customers whose slotted services need to be interrupted because of insufficient capacity at the current slot, and interrupts their services.

Source code in hciw/node.py
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
def interrupt_slotted_services(self):
    """Interrupt services when there is not enough slot capacity.

    Finds the customers whose slotted services need to be interrupted
    because of insufficient capacity at the current slot, and interrupts
    their services.
    """
    assert isinstance(self.schedule, Slotted)
    if self.schedule.capacitated and self.schedule.preemption is not False:
        number_to_interrupt = self.number_in_service - self.schedule.slot_size
        if number_to_interrupt > 0:
            inds_to_interrupt = sorted(
                [
                    ind
                    for ind in self.all_individuals
                    if ind.service_start_date is not False
                ],
                key=lambda x: (x.priority_class, x.arrival_date),
                reverse=True,
            )[:number_to_interrupt]
            for ind in inds_to_interrupt:
                self.interrupt_service(ind)

slotted_service

slotted_service()

Serve a set number of customers at the current time.

Only the number of customers allowed by the current slot may have service at the exact time the method is called.

Source code in hciw/node.py
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
def slotted_service(self):
    """Serve a set number of customers at the current time.

    Only the number of customers allowed by the current slot may have
    service at the exact time the method is called.
    """
    assert isinstance(self.schedule, Slotted)
    number_of_slotted_services = self.find_number_of_slotted_services()
    self.interrupt_slotted_services()
    for _i in range(number_of_slotted_services):
        if self.number_interrupted_individuals > 0:
            ind = self.interrupted_individuals[0]
            self.interrupted_individuals.remove(ind)
            self.number_interrupted_individuals -= 1
        else:
            ind = self.choose_next_customer()
        if ind is not None:
            ind.service_start_date = self.now
            self.give_individual_a_service_time(ind)
            ind.service_end_date = self.now + ind.service_time
            ind.server = True
            self.number_in_service += 1
            self.reset_class_change(ind)
    self.schedule.get_next_slot()

choose_next_customer

choose_next_customer()

Choose which customer will be next to be served.

Returns:

Type Description
Individual

The customer chosen by the service discipline, or None if no customer is waiting.

Source code in hciw/node.py
417
418
419
420
421
422
423
424
425
426
427
428
429
430
def choose_next_customer(self):
    """Choose which customer will be next to be served.

    Returns:
        Individual: The customer chosen by the service discipline, or
            None if no customer is waiting.
    """
    for priority_individuals in self.individuals:
        waiting_individuals = [
            ind for ind in priority_individuals if not ind.server
        ]
        if len(waiting_individuals) > 0:
            return self.service_discipline(waiting_individuals, self.now)
    return None

create_starting_servers

create_starting_servers()

Initialise the servers.

Returns:

Type Description
list

The newly created starting servers.

Source code in hciw/node.py
432
433
434
435
436
437
438
439
440
def create_starting_servers(self):
    """Initialise the servers.

    Returns:
        list: The newly created starting servers.
    """
    return [
        self.simulation.ServerType(self, i + 1, 0.0) for i in range(int(self.c))
    ]

decide_class_change

decide_class_change(next_individual)

Decide the next class and class change date for an individual.

Parameters:

Name Type Description Default
next_individual Individual

The individual to update.

required
Source code in hciw/node.py
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
def decide_class_change(self, next_individual):
    """Decide the next class and class change date for an individual.

    Args:
        next_individual (Individual): The individual to update.
    """
    if self.dynamic_classes is True:
        next_time = float("inf")
        next_class = next_individual.customer_class
        for clss, dist in self.simulation.network.customer_classes[
            next_individual.customer_class
        ].class_change_time_distributions.items():
            if dist is not None:
                t = dist.sample()
                if t < next_time:
                    next_time = t
                    next_class = clss
        next_individual.next_class = next_class
        next_individual.class_change_date = self.increment_time(self.now, next_time)
        self.find_next_class_change()

decide_preempt

decide_preempt(individual)

Decide whether priority preemption is needed.

If needed, finds the individual to preempt and preempts them.

Parameters:

Name Type Description Default
individual Individual

The individual seeking service.

required
Source code in hciw/node.py
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def decide_preempt(self, individual):
    """Decide whether priority preemption is needed.

    If needed, finds the individual to preempt and preempts them.

    Args:
        individual (Individual): The individual seeking service.
    """
    if self.priority_preempt:
        least_priority = max(s.cust.priority_class for s in self.servers)
        if individual.priority_class < least_priority:
            least_prioritised_individuals = [
                s.cust
                for s in self.servers
                if s.cust.priority_class == least_priority
            ]
            individual_to_preempt = max(
                list(least_prioritised_individuals),
                key=lambda cust: cust.service_start_date,
            )
            self.preempt(individual_to_preempt, individual)

detatch_server

detatch_server(server, individual)

Detach a server from an individual, and vice versa.

Parameters:

Name Type Description Default
server Server

The server to detach.

required
individual Individual

The individual to detach.

required
Source code in hciw/node.py
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
def detatch_server(self, server, individual):
    """Detach a server from an individual, and vice versa.

    Args:
        server (Server): The server to detach.
        individual (Individual): The individual to detach.
    """
    self.simulation.deadlock_detector.action_at_detatch_server(server)
    server.cust = False
    server.busy = False
    individual.server = False
    server.busy_time = self.increment_time(
        server.busy_time, individual.exit_date - individual.service_start_date
    )
    server.total_time = self.now - server.start_date
    if server.offduty:
        self.kill_server(server)

find_free_server

find_free_server(ind)

Find a free server.

Parameters:

Name Type Description Default
ind Individual

The individual needing a server.

required

Returns:

Type Description
Server

A free server, or None if none is available.

Source code in hciw/node.py
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def find_free_server(self, ind):
    """Find a free server.

    Args:
        ind (Individual): The individual needing a server.

    Returns:
        Server: A free server, or None if none is available.
    """
    if isinf(self.c):
        return None

    if self.server_priority_function is None:
        all_servers = self.servers
    else:
        all_servers = sorted(
            self.servers, key=lambda srv: self.server_priority_function(srv, ind)
        )

    for svr in all_servers:
        if not svr.busy:
            return svr
    return None

decide_between_simultaneous_individuals

decide_between_simultaneous_individuals()

Find the next individual that should now finish service.

Returns:

Type Description
Individual

A randomly chosen individual when several finish at the same time.

Source code in hciw/node.py
527
528
529
530
531
532
533
534
535
536
537
538
539
def decide_between_simultaneous_individuals(self):
    """Find the next individual that should now finish service.

    Returns:
        Individual: A randomly chosen individual when several finish at
            the same time.
    """
    assert isinstance(self.next_individual, list)
    if len(self.next_individual) > 1:
        next_individual = random_choice(self.next_individual)
    else:
        next_individual = self.next_individual[0]
    return next_individual

find_next_class_change

find_next_class_change()

Update the next class change date and individual.

Source code in hciw/node.py
541
542
543
544
545
546
547
548
def find_next_class_change(self):
    """Update the next class change date and individual."""
    self.next_class_change_date = float("inf")
    self.next_class_change_ind = None
    for ind in self.all_individuals:
        if (ind.class_change_date < self.next_class_change_date) and not ind.server:
            self.next_class_change_ind = ind
            self.next_class_change_date = ind.class_change_date

find_server_utilisation

find_server_utilisation()

Find the overall server utilisation for the node.

Source code in hciw/node.py
550
551
552
553
554
555
556
557
558
559
560
def find_server_utilisation(self):
    """Find the overall server utilisation for the node."""
    if isinf(self.c) or self.c == 0:
        self.server_utilisation = None
    else:
        for server in self.servers:
            self.all_servers_total.append(server.total_time)
            self.all_servers_busy.append(server.busy_time)
        self.server_utilisation = sum(self.all_servers_busy) / sum(
            self.all_servers_total
        )

finish_service

finish_service()

Finish the service of the next individual.

Finds the individual to finish service, checks whether they need to change class, finds their next node, and releases the individual if there is capacity at the destination, otherwise causes blockage.

Source code in hciw/node.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
def finish_service(self):
    """Finish the service of the next individual.

    Finds the individual to finish service, checks whether they need to
    change class, finds their next node, and releases the individual if
    there is capacity at the destination, otherwise causes blockage.
    """
    next_individual = self.decide_between_simultaneous_individuals()
    self.change_customer_class(next_individual)
    next_node = self.next_node(next_individual)
    next_individual.destination = next_node.id_number
    if not isinf(self.c) and self.c > 0:
        assert isinstance(next_individual.server, Server)
        next_individual.server.next_end_service_date = float("Inf")
    if next_node.number_of_individuals < next_node.node_capacity:
        self.release(next_individual, next_node)
    else:
        self.block_individual(next_individual, next_node)

give_individual_a_service_time

give_individual_a_service_time(individual)

Give a service time to an individual.

Either a new service time, or a service time after pre-emption.

Parameters:

Name Type Description Default
individual Individual

The individual to serve.

required
Source code in hciw/node.py
581
582
583
584
585
586
587
588
589
590
591
592
def give_individual_a_service_time(self, individual):
    """Give a service time to an individual.

    Either a new service time, or a service time after pre-emption.

    Args:
        individual (Individual): The individual to serve.
    """
    if individual.service_time is False:
        individual.service_time = self.get_service_time(individual)
    else:
        self.give_service_time_after_preemption(individual)

give_service_time_after_preemption

give_service_time_after_preemption(individual)

Give a service time after preemption.

Either resamples, restarts, or resumes service time from where it was left off.

Parameters:

Name Type Description Default
individual Individual

The individual to serve.

required
Source code in hciw/node.py
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
def give_service_time_after_preemption(self, individual):
    """Give a service time after preemption.

    Either resamples, restarts, or resumes service time from where it was
    left off.

    Args:
        individual (Individual): The individual to serve.
    """
    if individual.service_time == "resample":
        individual.service_time = self.get_service_time(individual)
    if individual.service_time == "restart":
        individual.service_time = individual.original_service_time
    if individual.service_time == "resume":
        individual.service_time = individual.time_left

have_event

have_event()

Trigger the node's next event.

Source code in hciw/node.py
610
611
612
613
614
615
616
617
618
619
620
621
def have_event(self):
    """Trigger the node's next event."""
    if self.next_event_type == "end_service":
        self.finish_service()
    elif self.next_event_type == "shift_change":
        self.change_shift()
    elif self.next_event_type == "renege":
        self.renege()
    elif self.next_event_type == "class_change":
        self.change_customer_class_while_waiting()
    elif self.next_event_type == "slotted_service":
        self.slotted_service()

increment_time

increment_time(original, increment)

Increment the original time by the increment.

Parameters:

Name Type Description Default
original float

The original time.

required
increment float

The amount to add.

required

Returns:

Type Description

The incremented time.

Source code in hciw/node.py
623
624
625
626
627
628
629
630
631
632
633
def increment_time(self, original, increment):
    """Increment the original time by the increment.

    Args:
        original (float): The original time.
        increment (float): The amount to add.

    Returns:
        The incremented time.
    """
    return original + increment

kill_server

kill_server(srvr)

Kill a server when it goes off duty.

Parameters:

Name Type Description Default
srvr Server

The server to remove.

required
Source code in hciw/node.py
635
636
637
638
639
640
641
642
643
644
645
646
def kill_server(self, srvr):
    """Kill a server when it goes off duty.

    Args:
        srvr (Server): The server to remove.
    """
    srvr.total_time = self.increment_time(self.next_event_date, -srvr.start_date)
    self.overtime.append(self.increment_time(self.next_event_date, -srvr.shift_end))
    self.all_servers_busy.append(srvr.busy_time)
    self.all_servers_total.append(srvr.total_time)
    indx = self.servers.index(srvr)
    del self.servers[indx]

next_node

next_node(ind)

Find the next node according to the routing method.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/node.py
648
649
650
651
652
653
654
655
656
657
658
659
def next_node(self, ind):
    """Find the next node according to the routing method.

    Args:
        ind (Individual): The individual being routed.

    Returns:
        Node: The next node.
    """
    return self.simulation.routers[ind.customer_class].next_node(
        ind, self.id_number
    )

next_node_for_rerouting

next_node_for_rerouting(ind)

Find the next node for rerouting according to the routing method.

Parameters:

Name Type Description Default
ind Individual

The individual being rerouted.

required

Returns:

Type Description
Node

The next node for rerouting.

Source code in hciw/node.py
661
662
663
664
665
666
667
668
669
670
671
672
def next_node_for_rerouting(self, ind):
    """Find the next node for rerouting according to the routing method.

    Args:
        ind (Individual): The individual being rerouted.

    Returns:
        Node: The next node for rerouting.
    """
    return self.simulation.routers[ind.customer_class].next_node_for_rerouting(
        ind, self.id_number
    )

next_node_for_jockeying

next_node_for_jockeying(ind)

Find the next node for jockeying according to the routing method.

Parameters:

Name Type Description Default
ind Individual

The jockeying individual.

required

Returns:

Type Description
Node

The next node for jockeying.

Source code in hciw/node.py
674
675
676
677
678
679
680
681
682
683
684
685
def next_node_for_jockeying(self, ind):
    """Find the next node for jockeying according to the routing method.

    Args:
        ind (Individual): The jockeying individual.

    Returns:
        Node: The next node for jockeying.
    """
    return self.simulation.routers[ind.customer_class].next_node_for_jockeying(
        ind, self.id_number
    )

preempt

preempt(individual_to_preempt, next_individual)

Preempt an individual and replace them in service.

Parameters:

Name Type Description Default
individual_to_preempt Individual

The individual removed from service.

required
next_individual Individual

The individual taking their place.

required
Source code in hciw/node.py
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
def preempt(self, individual_to_preempt, next_individual):
    """Preempt an individual and replace them in service.

    Args:
        individual_to_preempt (Individual): The individual removed from
            service.
        next_individual (Individual): The individual taking their place.
    """
    server = individual_to_preempt.server
    individual_to_preempt.original_service_time = individual_to_preempt.service_time
    if self.priority_preempt == "reroute":
        self.reroute(individual_to_preempt)
    else:
        self.write_interruption_record(individual_to_preempt)
        individual_to_preempt.service_start_date = False
        individual_to_preempt.time_left = (
            individual_to_preempt.service_end_date - self.now
        )
        individual_to_preempt.service_time = self.priority_preempt
        individual_to_preempt.service_end_date = False
        self.detatch_server(server, individual_to_preempt)
        self.decide_class_change(individual_to_preempt)
    self.attach_server(server, next_individual)
    next_individual.service_start_date = self.now
    next_individual.service_time = self.get_service_time(next_individual)
    next_individual.service_end_date = self.now + next_individual.service_time
    self.reset_class_change(next_individual)
    server.next_end_service_date = next_individual.service_end_date

release

release(next_individual, next_node, reroute=False)

Release an individual from the node.

Removes the individual from the queue, records relevant information, detaches the server, updates the state tracker, begins service of any waiting customers, sends the individual to their next destination, and releases any individuals blocked by this node.

Parameters:

Name Type Description Default
next_individual Individual

The individual to release.

required
next_node Node

The node receiving the individual.

required
reroute bool

Whether the release is due to rerouting.

False
Source code in hciw/node.py
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
def release(self, next_individual, next_node, reroute=False):
    """Release an individual from the node.

    Removes the individual from the queue, records relevant information,
    detaches the server, updates the state tracker, begins service of any
    waiting customers, sends the individual to their next destination, and
    releases any individuals blocked by this node.

    Args:
        next_individual (Individual): The individual to release.
        next_node (Node): The node receiving the individual.
        reroute (bool): Whether the release is due to rerouting.
    """
    self.individuals[next_individual.prev_priority_class].remove(next_individual)
    self.number_of_individuals -= 1
    self.number_in_service -= 1
    next_individual.queue_size_at_departure = self.number_of_individuals
    next_individual.exit_date = self.now
    if not reroute:
        self.write_individual_record(next_individual)
    newly_free_server = None
    if not isinf(self.c) and not self.slotted:
        newly_free_server = next_individual.server
        self.detatch_server(newly_free_server, next_individual)
    if self.slotted:
        next_individual.server = False
    self.reset_individual_attributes(next_individual)
    self.simulation.statetracker.change_state_release(
        self, next_node, next_individual, next_individual.is_blocked
    )
    if not reroute:
        self.begin_service_if_possible_release(next_individual, newly_free_server)
    next_node.accept(next_individual)
    if not reroute:
        self.release_blocked_individual()

release_blocked_individual

release_blocked_individual()

Release an individual blocked by this node.

Finds the individual who has been blocked the longest, removes them from the blocked queue, restores their service if it was interrupted, and releases them from their node.

Source code in hciw/node.py
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
def release_blocked_individual(self):
    """Release an individual blocked by this node.

    Finds the individual who has been blocked the longest, removes them
    from the blocked queue, restores their service if it was interrupted,
    and releases them from their node.
    """
    if (self.len_blocked_queue > 0) and (
        self.number_of_individuals < self.node_capacity
    ):
        node_to_receive_from = self.simulation.nodes[self.blocked_queue[0][0]]
        individual_to_receive_index = [
            ind.id_number for ind in node_to_receive_from.all_individuals
        ].index(self.blocked_queue[0][1])
        individual_to_receive = node_to_receive_from.all_individuals[
            individual_to_receive_index
        ]
        self.blocked_queue.pop(0)
        self.len_blocked_queue -= 1
        if individual_to_receive.interrupted:
            individual_to_receive.interrupted = False
            individual_to_receive.service_start_date = (
                individual_to_receive.original_service_start_date
            )
            individual_to_receive.service_end_date = (
                individual_to_receive.service_start_date
                + individual_to_receive.original_service_time
            )
            node_to_receive_from.interrupted_individuals.remove(
                individual_to_receive
            )
            node_to_receive_from.number_interrupted_individuals -= 1
        node_to_receive_from.release(individual_to_receive, self)

reset_class_change

reset_class_change(individual)

Reset an individual's class change date when service begins.

Parameters:

Name Type Description Default
individual Individual

The individual beginning service.

required
Source code in hciw/node.py
786
787
788
789
790
791
792
793
794
795
def reset_class_change(self, individual):
    """Reset an individual's class change date when service begins.

    Args:
        individual (Individual): The individual beginning service.
    """
    if self.dynamic_classes:
        individual.class_change_date = float("inf")
        if individual == self.next_class_change_ind:
            self.find_next_class_change()

renege

renege()

Remove an individual from the queue due to reneging.

Resets the individual's reneging date and sends the individual to their reneging destination.

Source code in hciw/node.py
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
def renege(self):
    """Remove an individual from the queue due to reneging.

    Resets the individual's reneging date and sends the individual to
    their reneging destination.
    """
    reneging_individual = self.decide_between_simultaneous_individuals()
    reneging_individual.reneging_date = float("Inf")
    next_node = self.next_node_for_jockeying(reneging_individual)
    self.individuals[reneging_individual.prev_priority_class].remove(
        reneging_individual
    )
    self.number_of_individuals -= 1
    reneging_individual.queue_size_at_departure = self.number_of_individuals
    reneging_individual.exit_date = self.now
    self.write_reneging_record(reneging_individual)
    self.reset_individual_attributes(reneging_individual)
    self.simulation.statetracker.change_state_renege(
        self, next_node, reneging_individual, blocked=False
    )
    next_node.accept(reneging_individual, completed=False)
    self.release_blocked_individual()

get_reneging_date

get_reneging_date(ind)

Return the reneging date for a given individual.

Parameters:

Name Type Description Default
ind Individual

The individual.

required

Returns:

Type Description
float

The reneging date.

Source code in hciw/node.py
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
def get_reneging_date(self, ind):
    """Return the reneging date for a given individual.

    Args:
        ind (Individual): The individual.

    Returns:
        float: The reneging date.
    """
    dist = self.simulation.network.customer_classes[
        ind.customer_class
    ].reneging_time_distributions[self.id_number - 1]
    if dist is None:
        return float("inf")
    return self.now + dist.sample(t=self.now, ind=ind)

get_service_time

get_service_time(ind)

Return a service time for the given customer class.

Parameters:

Name Type Description Default
ind Individual

The individual being served.

required

Returns:

Type Description
float

The sampled service time.

Source code in hciw/node.py
836
837
838
839
840
841
842
843
844
845
846
847
def get_service_time(self, ind):
    """Return a service time for the given customer class.

    Args:
        ind (Individual): The individual being served.

    Returns:
        float: The sampled service time.
    """
    return self.simulation.service_times[self.id_number][ind.customer_class].sample(
        t=self.now, ind=ind
    )

take_servers_off_duty

take_servers_off_duty(preemption: bool | str = False)

Gather the servers that should be deleted.

Parameters:

Name Type Description Default
preemption bool

Whether preemption is active for this shift change.

False
Source code in hciw/node.py
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
def take_servers_off_duty(self, preemption: bool | str = False):
    """Gather the servers that should be deleted.

    Args:
        preemption (bool): Whether preemption is active for this shift
            change.
    """
    if not preemption:
        to_delete = []
        for srvr in self.servers:
            srvr.shift_end = self.next_event_date
            if srvr.busy:
                srvr.offduty = True
            else:
                to_delete.append(srvr)
    else:
        to_delete = self.servers[::1]  # copy
        for s in self.servers:
            s.shift_end = self.next_event_date
            if s.cust is not False:
                self.interrupt_service(s.cust)
        self.sort_interrupted_individuals()
    for obs in to_delete:
        self.kill_server(obs)

interrupt_service

interrupt_service(individual)

Interrupt an individual's service.

Places the individual in the interrupted queue and writes an interruption record for them.

Parameters:

Name Type Description Default
individual Individual

The individual being interrupted.

required
Source code in hciw/node.py
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
def interrupt_service(self, individual):
    """Interrupt an individual's service.

    Places the individual in the interrupted queue and writes an
    interruption record for them.

    Args:
        individual (Individual): The individual being interrupted.
    """
    individual.original_service_time = individual.service_time
    assert self.schedule is not None
    if self.schedule.preemption == "reroute":
        self.reroute(individual)
    else:
        self.interrupted_individuals.append(individual)
        individual.interrupted = True
        self.number_interrupted_individuals += 1
        self.write_interruption_record(individual)
        individual.original_service_start_date = individual.service_start_date
        individual.service_start_date = False
        individual.time_left = individual.service_end_date - self.now
        individual.service_time = self.schedule.preemption
        individual.service_end_date = False
        self.number_in_service -= 1

sort_interrupted_individuals

sort_interrupted_individuals()

Sort interrupted individuals by priority class and arrival date.

Source code in hciw/node.py
899
900
901
902
903
def sort_interrupted_individuals(self):
    """Sort interrupted individuals by priority class and arrival date."""
    self.interrupted_individuals.sort(
        key=lambda x: (x.priority_class, x.arrival_date)
    )

reroute

reroute(individual)

Reroute a preempted individual.

Parameters:

Name Type Description Default
individual Individual

The individual being rerouted.

required
Source code in hciw/node.py
905
906
907
908
909
910
911
912
913
def reroute(self, individual):
    """Reroute a preempted individual.

    Args:
        individual (Individual): The individual being rerouted.
    """
    next_node = self.next_node_for_rerouting(individual)
    self.write_interruption_record(individual, destination=next_node.id_number)
    self.release(individual, next_node, reroute=True)

update_next_end_service_without_server

update_next_end_service_without_server()

Update the next end of a slotted service in possible_next_events.

Source code in hciw/node.py
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
def update_next_end_service_without_server(self):
    """Update the next end of a slotted service in `possible_next_events`."""
    if self.slotted or isinf(self.c):
        next_end_service_date = float("Inf")
        for ind in self.all_individuals:
            if not ind.is_blocked and ind.service_end_date >= self.now:
                if ind.service_end_date < next_end_service_date:
                    self.possible_next_events["end_service"] = (
                        [ind],
                        ind.service_end_date,
                    )
                    next_end_service_date = ind.service_end_date
                elif (ind.service_end_date == next_end_service_date) and (
                    not isinf(next_end_service_date)
                ):
                    self.possible_next_events["end_service"][0].append(ind)

update_next_end_service_with_server

update_next_end_service_with_server()

Update the next end service with a server in possible_next_events.

Source code in hciw/node.py
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
def update_next_end_service_with_server(self):
    """Update the next end service with a server in `possible_next_events`."""
    if not self.slotted and not isinf(self.c):
        next_end_service_date = float("Inf")
        for s in self.servers:
            if s.next_end_service_date < next_end_service_date:
                self.possible_next_events["end_service"] = (
                    [s.cust],
                    s.next_end_service_date,
                )
                next_end_service_date = s.next_end_service_date
            elif (s.next_end_service_date == next_end_service_date) and (
                not isinf(next_end_service_date)
            ):
                self.possible_next_events["end_service"][0].append(s.cust)

update_next_renege_time

update_next_renege_time()

Update the next renege time in possible_next_events.

Source code in hciw/node.py
948
949
950
951
952
953
954
955
956
957
958
959
960
961
def update_next_renege_time(self):
    """Update the next renege time in `possible_next_events`."""
    if not isinf(self.c) and self.reneging is True:
        next_renege_date = float("Inf")
        for ind in self.all_individuals:
            if (ind.reneging_date < next_renege_date) and not ind.server:
                self.possible_next_events["renege"] = ([ind], ind.reneging_date)
                next_renege_date = ind.reneging_date
            elif (
                (ind.reneging_date == next_renege_date)
                and (not ind.server)
                and (not isinf(next_renege_date))
            ):
                self.possible_next_events["renege"][0].append(ind)

update_next_class_change_while_waiting

update_next_class_change_while_waiting()

Update the next class change while waiting in possible_next_events.

Source code in hciw/node.py
963
964
965
966
967
968
969
def update_next_class_change_while_waiting(self):
    """Update the next class change while waiting in `possible_next_events`."""
    if self.dynamic_classes is True and not isinf(self.c):
        self.possible_next_events["class_change"] = (
            self.next_class_change_ind,
            self.next_class_change_date,
        )

update_next_shift_change_or_slot_time

update_next_shift_change_or_slot_time()

Update possible_next_events with the next shift or slot time.

Source code in hciw/node.py
971
972
973
974
975
976
977
978
979
980
981
982
983
def update_next_shift_change_or_slot_time(self):
    """Update `possible_next_events` with the next shift or slot time."""
    if self.schedule is not None:
        if self.schedule.schedule_type == "schedule":
            self.possible_next_events["shift_change"] = (
                None,
                self.next_shift_change,
            )
        if isinstance(self.schedule, Slotted):
            self.possible_next_events["slotted_service"] = (
                None,
                self.schedule.next_slot_date,
            )

update_next_event_date

update_next_event_date()

Find the time of the next event at this node.

With infinite servers this is the time of the next individual to end service, or infinity. Otherwise it is the minimum of the next shift change and the time for the next individual (who is not blocked) to end service, or infinity.

Source code in hciw/node.py
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
def update_next_event_date(self):
    """Find the time of the next event at this node.

    With infinite servers this is the time of the next individual to end
    service, or infinity. Otherwise it is the minimum of the next shift
    change and the time for the next individual (who is not blocked) to
    end service, or infinity.
    """
    self.possible_next_events = {}
    self.update_next_end_service_without_server()
    self.update_next_end_service_with_server()
    self.update_next_renege_time()
    self.update_next_class_change_while_waiting()
    self.update_next_shift_change_or_slot_time()
    if self.reneging or self.dynamic_classes or self.schedule:
        next_event, self.next_event_type = self.decide_next_event()
        self.next_event_date = next_event[1]
        self.next_individual = next_event[0]
    else:
        next_end_service = self.possible_next_events.get(
            "end_service", (None, float("inf"))
        )
        self.next_event_date = next_end_service[1]
        self.next_individual = next_end_service[0]
        self.next_event_type = "end_service"

decide_next_event

decide_next_event()

Decide the next event.

Chooses the next service end, renege, shift change, or class change. In the case of a tie, events are prioritised in the following order:

  1. slotted service
  2. shift change
  3. end service
  4. class change
  5. renege

Returns:

Type Description
tuple

The next event and its event type.

Source code in hciw/node.py
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
def decide_next_event(self):
    """Decide the next event.

    Chooses the next service end, renege, shift change, or class change.
    In the case of a tie, events are prioritised in the following order:

    1. slotted service
    2. shift change
    3. end service
    4. class change
    5. renege

    Returns:
        tuple: The next event and its event type.
    """
    next_date = float("inf")
    next_event = (None, float("inf"))
    next_event_type = None
    for event_type in [
        "slotted_service",
        "shift_change",
        "end_service",
        "class_change",
        "renege",
    ]:
        possible_next_event = self.possible_next_events.get(
            event_type, (None, float("inf"))
        )
        if possible_next_event[1] < next_date:
            next_event = possible_next_event
            next_event_type = event_type
            next_date = next_event[1]
    return next_event, next_event_type

wrap_up_servers

wrap_up_servers(current_time)

Update server total and busy times at the end of the run.

Parameters:

Name Type Description Default
current_time float

The simulation end time.

required
Source code in hciw/node.py
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
def wrap_up_servers(self, current_time):
    """Update server total and busy times at the end of the run.

    Args:
        current_time (float): The simulation end time.
    """
    if not isinf(self.c):
        for srvr in self.servers:
            srvr.total_time = self.increment_time(current_time, -srvr.start_date)
            if srvr.busy:
                srvr.busy_time += self.increment_time(
                    current_time, -srvr.cust.service_start_date
                )

write_individual_record

write_individual_record(individual)

Write a data record for an individual leaving a node.

Parameters:

Name Type Description Default
individual Individual

The individual leaving the node.

required
Source code in hciw/node.py
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
def write_individual_record(self, individual):
    """Write a data record for an individual leaving a node.

    Args:
        individual (Individual): The individual leaving the node.
    """
    if isinf(self.c) or self.slotted:
        server_id = False
    else:
        server_id = individual.server.id_number

    record = DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=individual.arrival_date,
        waiting_time=individual.service_start_date - individual.arrival_date,
        service_start_date=individual.service_start_date,
        service_time=individual.service_end_date - individual.service_start_date,
        service_end_date=individual.service_end_date,
        time_blocked=individual.exit_date - individual.service_end_date,
        exit_date=individual.exit_date,
        destination=individual.destination,
        queue_size_at_arrival=individual.queue_size_at_arrival,
        queue_size_at_departure=individual.queue_size_at_departure,
        server_id=server_id,
        record_type="service",
    )
    individual.data_records.append(record)

write_incomplete_record

write_incomplete_record(individual)

Write an incomplete data record at the end of the run.

Parameters:

Name Type Description Default
individual Individual

The individual still at the node.

required

Returns:

Type Description
DataRecord

The incomplete data record.

Source code in hciw/node.py
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
def write_incomplete_record(self, individual):
    """Write an incomplete data record at the end of the run.

    Args:
        individual (Individual): The individual still at the node.

    Returns:
        DataRecord: The incomplete data record.
    """
    if not individual.service_time:  # Still in queue
        service_start_date = None
        waiting_time = None
        service_time = None
        service_end_date = None
    else:
        service_start_date = individual.service_start_date
        waiting_time = individual.service_start_date - individual.arrival_date
        if individual.service_end_date > self.now:  # Still in service
            service_time = None
            service_end_date = None
        else:  # Still blocked
            service_time = individual.service_time
            service_end_date = individual.service_end_date

    return DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=individual.arrival_date,
        waiting_time=waiting_time,
        service_start_date=service_start_date,
        service_time=service_time,
        service_end_date=service_end_date,
        time_blocked=None,
        exit_date=None,
        destination=None,
        queue_size_at_arrival=individual.queue_size_at_arrival,
        queue_size_at_departure=None,
        server_id=False,
        record_type="incomplete",
    )

write_interruption_record

write_interruption_record(individual, destination=nan)

Write a data record for an individual being interrupted.

Parameters:

Name Type Description Default
individual Individual

The interrupted individual.

required
destination int

The destination node number.

nan
Source code in hciw/node.py
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
def write_interruption_record(self, individual, destination=nan):
    """Write a data record for an individual being interrupted.

    Args:
        individual (Individual): The interrupted individual.
        destination (int): The destination node number.
    """
    server_id = False if self.slotted else individual.server.id_number

    record = DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=individual.arrival_date,
        waiting_time=individual.service_start_date - individual.arrival_date,
        service_start_date=individual.service_start_date,
        service_time=individual.original_service_time,
        service_end_date=nan,
        time_blocked=nan,
        exit_date=self.now,
        destination=destination,
        queue_size_at_arrival=individual.queue_size_at_arrival,
        queue_size_at_departure=individual.queue_size_at_departure,
        server_id=server_id,
        record_type="interrupted service",
    )
    individual.data_records.append(record)

write_reneging_record

write_reneging_record(individual)

Write a data record for an individual when reneging.

Parameters:

Name Type Description Default
individual Individual

The reneging individual.

required
Source code in hciw/node.py
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
def write_reneging_record(self, individual):
    """Write a data record for an individual when reneging.

    Args:
        individual (Individual): The reneging individual.
    """
    record = DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=individual.arrival_date,
        waiting_time=individual.exit_date - individual.arrival_date,
        service_start_date=nan,
        service_time=nan,
        service_end_date=nan,
        time_blocked=nan,
        exit_date=individual.exit_date,
        destination=individual.destination,
        queue_size_at_arrival=individual.queue_size_at_arrival,
        queue_size_at_departure=individual.queue_size_at_departure,
        server_id=nan,
        record_type="renege",
    )
    individual.data_records.append(record)

write_baulking_or_rejection_record

write_baulking_or_rejection_record(individual, record_type)

Write a data record for an individual who baulks or is rejected.

Parameters:

Name Type Description Default
individual Individual

The individual.

required
record_type str

The type of record to write.

required
Source code in hciw/node.py
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
def write_baulking_or_rejection_record(self, individual, record_type):
    """Write a data record for an individual who baulks or is rejected.

    Args:
        individual (Individual): The individual.
        record_type (str): The type of record to write.
    """
    record = DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=self.now,
        waiting_time=nan,
        service_start_date=nan,
        service_time=nan,
        service_end_date=nan,
        time_blocked=nan,
        exit_date=self.now,
        destination=nan,
        queue_size_at_arrival=self.number_of_individuals,
        queue_size_at_departure=nan,
        server_id=nan,
        record_type=record_type,
    )
    individual.data_records.append(record)

reset_individual_attributes

reset_individual_attributes(individual)

Reset the attributes of an individual.

Parameters:

Name Type Description Default
individual Individual

The individual to reset.

required
Source code in hciw/node.py
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
def reset_individual_attributes(self, individual):
    """Reset the attributes of an individual.

    Args:
        individual (Individual): The individual to reset.
    """
    individual.arrival_date = False
    individual.service_time = False
    individual.service_start_date = False
    individual.service_end_date = False
    individual.exit_date = False
    individual.queue_size_at_arrival = False
    individual.queue_size_at_departure = False
    individual.destination = False

PSNode

PSNode(id_, simulation)

Bases: Node

Represent a limited processor-sharing node.

In processor-sharing systems the service load is shared equally between all customers at the node. While there are x customers present, the service slows down x-fold, so the service time increases x-fold.

number_of_servers represents the node capacity, the maximum number of customers allowed to share the service load at a time. Customers arriving when the node is overcapacity wait in line.

Initialise the node.

Differs from Node by setting last_occupancy to 0, required to calculate service times for the current period, setting ps_capacity to the node capacity, and setting c to infinity, as within capacity the code logic behaves more like an infinite server queue.

Parameters:

Name Type Description Default
id_ int

Identifier for the node.

required
simulation object

The simulation to which this node belongs.

required
Source code in hciw/processor_sharing.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def __init__(self, id_, simulation):
    """Initialise the node.

    Differs from `Node` by setting ``last_occupancy`` to 0, required to
    calculate service times for the current period, setting
    ``ps_capacity`` to the node capacity, and setting ``c`` to infinity,
    as within capacity the code logic behaves more like an infinite
    server queue.

    Args:
        id_ (int): Identifier for the node.
        simulation (object): The simulation to which this node belongs.
    """
    self.last_occupancy = 0
    super().__init__(id_, simulation)
    self.ps_threshold = self.simulation.network.service_centres[
        id_ - 1
    ].ps_threshold
    self.ps_capacity: int | float = self.c
    self.c: float = float("inf")

update_all_service_end_dates

update_all_service_end_dates()

Update projected service end dates for customers in service.

For each individual receiving service, calculates the projected end service dates if the system state remains constant.

Source code in hciw/processor_sharing.py
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
def update_all_service_end_dates(self):
    """Update projected service end dates for customers in service.

    For each individual receiving service, calculates the projected end
    service dates if the system state remains constant.
    """
    next_occupancy = min(self.number_of_individuals, self.ps_capacity)
    inds_in_service = [ind for ind in self.all_individuals if ind.with_server]
    for ind in inds_in_service:
        current_period = self.simulation.current_time - ind.date_last_update
        if self.last_occupancy > 0:
            share_completed = (self.ps_threshold * current_period) / max(
                self.last_occupancy, self.ps_threshold
            )
        else:
            share_completed = 0
        ind.time_left -= share_completed
        ind.service_end_date = self.simulation.current_time + (
            (ind.time_left * max(next_occupancy, self.ps_threshold))
            / self.ps_threshold
        )
        ind.date_last_update = self.simulation.current_time

    self.last_occupancy = next_occupancy
    self.date_last_update = self.simulation.current_time

begin_service_if_possible_accept

begin_service_if_possible_accept(next_individual)

Begin service for an individual at the acceptance point.

Gives an arrival date and service time, gives a start date and a service time to complete if there is free capacity, marks the individual as 'with server', and updates all customers' service times.

Parameters:

Name Type Description Default
next_individual Individual

The individual to serve.

required
Source code in hciw/processor_sharing.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def begin_service_if_possible_accept(self, next_individual):
    """Begin service for an individual at the acceptance point.

    Gives an arrival date and service time, gives a start date and a
    service time to complete if there is free capacity, marks the
    individual as 'with server', and updates all customers' service
    times.

    Args:
        next_individual (Individual): The individual to serve.
    """
    next_individual.arrival_date = self.now
    next_individual.with_server = False
    if self.number_of_individuals <= self.ps_capacity:
        next_individual.service_start_date = self.now
        next_individual.date_last_update = self.now
        next_individual.service_time = self.get_service_time(next_individual)
        next_individual.time_left = next_individual.service_time
        next_individual.with_server = True
        self.update_all_service_end_dates()

begin_service_if_possible_release

begin_service_if_possible_release(
    next_individual=None, newly_free_server=None
)

Begin service for an individual at a release point.

Checks for individuals waiting for capacity, gives a start date and 'last update' date, samples a service time and uses it as the time left, and updates all customers' service times.

Parameters:

Name Type Description Default
next_individual Individual

The individual being released.

None
newly_free_server Server

The server freed by the release.

None
Source code in hciw/processor_sharing.py
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def begin_service_if_possible_release(
    self, next_individual=None, newly_free_server=None
):
    """Begin service for an individual at a release point.

    Checks for individuals waiting for capacity, gives a start date and
    'last update' date, samples a service time and uses it as the time
    left, and updates all customers' service times.

    Args:
        next_individual (Individual): The individual being released.
        newly_free_server (Server): The server freed by the release.
    """
    if self.number_of_individuals >= self.ps_capacity:
        next_individual = self.all_individuals[int(self.ps_capacity) - 1]
        next_individual.service_start_date = self.now
        next_individual.date_last_update = self.now
        next_individual.service_time = self.get_service_time(next_individual)
        next_individual.time_left = next_individual.service_time
        next_individual.with_server = True
    self.update_all_service_end_dates()

exactnode

Node classes with exact numerical precision.

ArrivalNode

ArrivalNode(simulation)

Represent the arrival node of the network.

This class handles arrivals, baulking, and rejections of individuals. See hciw.Simulation for the main simulation class.

Initialise the arrival node.

Parameters:

Name Type Description Default
simulation object

The simulation to which this arrival node belongs.

required
Source code in hciw/arrival_node.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def __init__(self, simulation):
    """Initialise the arrival node.

    Args:
        simulation (object): The simulation to which this arrival node
            belongs.
    """
    self.simulation = simulation
    self.number_of_individuals = 0
    self.all_individuals = []
    self.number_of_individuals_per_class = dict.fromkeys(
        self.simulation.network.customer_class_names, 0
    )
    self.number_accepted_individuals = 0
    self.number_accepted_individuals_per_class = dict.fromkeys(
        self.simulation.network.customer_class_names, 0
    )
    self.system_capacity = self.simulation.network.system_capacity
    self.event_dates_dict: dict[int, dict[str, float | bool]] = {
        nd + 1: dict.fromkeys(self.simulation.network.customer_class_names, False)
        for nd in range(self.simulation.network.number_of_nodes)
    }

initialise

initialise()

Initialise the event dates dictionary and next event date.

Source code in hciw/arrival_node.py
36
37
38
39
def initialise(self):
    """Initialise the event dates dictionary and next event date."""
    self.initialise_event_dates_dict()
    self.find_next_event_date()

decide_baulk

decide_baulk(next_node, next_individual)

Either make an individual baulk or send them to the next node.

Parameters:

Name Type Description Default
next_node Node

The next node.

required
next_individual Individual

The individual being considered.

required
Source code in hciw/arrival_node.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def decide_baulk(self, next_node, next_individual):
    """Either make an individual baulk or send them to the next node.

    Args:
        next_node (Node): The next node.
        next_individual (Individual): The individual being considered.
    """
    if next_node.baulking_functions[self.next_class] is None:
        self.send_individual(next_node, next_individual)
    else:
        rnd_num = random()
        if rnd_num < next_node.baulking_functions[self.next_class](
            next_node.number_of_individuals,
            Q=self.simulation,
            next_ind=next_individual,
            next_node=next_node,
        ):
            self.record_baulk(next_node, next_individual)
            self.simulation.nodes[-1].accept(next_individual, completed=False)
        else:
            self.send_individual(next_node, next_individual)

find_next_event_date

find_next_event_date()

Find the time of the next arrival.

Source code in hciw/arrival_node.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def find_next_event_date(self):
    """Find the time of the next arrival."""
    minnd = None
    minclss = None
    mindate = float("Inf")
    for nd in self.event_dates_dict:
        for clss in self.event_dates_dict[nd]:
            if self.event_dates_dict[nd][clss] < mindate:
                minnd = nd
                minclss = clss
                mindate = self.event_dates_dict[nd][clss]
    self.next_node = minnd
    self.next_class = minclss
    self.next_event_date = mindate

have_event

have_event()

Create and send a batch of arriving individuals.

Finds a batch size, creates that many individuals, sends them to the relevant node, then updates the event dates dictionary.

Source code in hciw/arrival_node.py
 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
116
117
118
def have_event(self):
    """Create and send a batch of arriving individuals.

    Finds a batch size, creates that many individuals, sends them to the
    relevant node, then updates the event dates dictionary.
    """
    batch = self.batch_size(self.next_node, self.next_class)
    assert self.next_node is not None
    assert self.next_class is not None
    for _ in range(batch):
        self.number_of_individuals += 1
        self.number_of_individuals_per_class[self.next_class] += 1
        priority_class = self.simulation.network.priority_class_mapping[
            self.next_class
        ]
        next_individual = self.simulation.IndividualType(
            self.number_of_individuals,
            self.next_class,
            priority_class,
            simulation=self.simulation,
        )
        next_node = self.simulation.transitive_nodes[self.next_node - 1]
        next_individual.starting_node = next_node.id_number
        self.simulation.routers[
            next_individual.customer_class
        ].initialise_individual(next_individual)
        self.release_individual(next_node, next_individual)

    self.event_dates_dict[self.next_node][self.next_class] = self.increment_time(
        self.event_dates_dict[self.next_node][self.next_class],
        self.inter_arrival(self.next_node, self.next_class),
    )
    self.find_next_event_date()

increment_time

increment_time(original, increment)

Increment the original time by the increment.

Parameters:

Name Type Description Default
original float

The original time.

required
increment float

The amount to add.

required

Returns:

Type Description

The incremented time.

Source code in hciw/arrival_node.py
120
121
122
123
124
125
126
127
128
129
130
def increment_time(self, original, increment):
    """Increment the original time by the increment.

    Args:
        original (float): The original time.
        increment (float): The amount to add.

    Returns:
        The incremented time.
    """
    return original + increment

initialise_event_dates_dict

initialise_event_dates_dict()

Initialise the event dates dictionary.

Random times are sampled for each node and class.

Source code in hciw/arrival_node.py
132
133
134
135
136
137
138
139
140
141
142
def initialise_event_dates_dict(self):
    """Initialise the event dates dictionary.

    Random times are sampled for each node and class.
    """
    for nd in self.event_dates_dict:
        for clss in self.event_dates_dict[nd]:
            if self.simulation.inter_arrival_times[nd][clss] is not None:
                self.event_dates_dict[nd][clss] = self.inter_arrival(nd, clss)
            else:
                self.event_dates_dict[nd][clss] = float("inf")

inter_arrival

inter_arrival(nd, clss)

Sample the inter-arrival time for the next class and node.

Parameters:

Name Type Description Default
nd int

The destination node.

required
clss str

The customer class.

required

Returns:

Type Description

The sampled inter-arrival time.

Source code in hciw/arrival_node.py
144
145
146
147
148
149
150
151
152
153
154
155
156
def inter_arrival(self, nd, clss):
    """Sample the inter-arrival time for the next class and node.

    Args:
        nd (int): The destination node.
        clss (str): The customer class.

    Returns:
        The sampled inter-arrival time.
    """
    return self.simulation.inter_arrival_times[nd][clss]._sample(
        t=self.simulation.current_time
    )

batch_size

batch_size(nd, clss)

Sample the batch size for the next class and node.

Parameters:

Name Type Description Default
nd int

The destination node.

required
clss str

The customer class.

required

Returns:

Type Description
int

The sampled batch size.

Raises:

Type Description
ValueError

If the sampled batch size is not a non-negative integer.

Source code in hciw/arrival_node.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def batch_size(self, nd, clss):
    """Sample the batch size for the next class and node.

    Args:
        nd (int): The destination node.
        clss (str): The customer class.

    Returns:
        int: The sampled batch size.

    Raises:
        ValueError: If the sampled batch size is not a non-negative
            integer.
    """
    batch = self.simulation.batch_sizes[nd][clss]._sample(
        t=self.simulation.current_time
    )
    if isinstance(batch, int) and batch >= 0:
        return batch
    raise ValueError("Batch sizes must be positive integers.")

record_baulk

record_baulk(next_node, individual)

Write a baulking record for an individual.

Parameters:

Name Type Description Default
next_node Node

The node the individual would have joined.

required
individual Individual

The baulking individual.

required
Source code in hciw/arrival_node.py
179
180
181
182
183
184
185
186
def record_baulk(self, next_node, individual):
    """Write a baulking record for an individual.

    Args:
        next_node (Node): The node the individual would have joined.
        individual (Individual): The baulking individual.
    """
    next_node.write_baulking_or_rejection_record(individual, record_type="baulk")

record_rejection

record_rejection(next_node, individual)

Write a rejection record for an individual.

Parameters:

Name Type Description Default
next_node Node

The node the individual was rejected from.

required
individual Individual

The rejected individual.

required
Source code in hciw/arrival_node.py
188
189
190
191
192
193
194
195
196
197
def record_rejection(self, next_node, individual):
    """Write a rejection record for an individual.

    Args:
        next_node (Node): The node the individual was rejected from.
        individual (Individual): The rejected individual.
    """
    next_node.write_baulking_or_rejection_record(
        individual, record_type="rejection"
    )

release_individual

release_individual(next_node, next_individual)

Release an individual to the next node.

Either rejects the individual due to lack of capacity, or sends that individual to baulk or not.

Parameters:

Name Type Description Default
next_node Node

The next node.

required
next_individual Individual

The individual being released.

required
Source code in hciw/arrival_node.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
def release_individual(self, next_node, next_individual):
    """Release an individual to the next node.

    Either rejects the individual due to lack of capacity, or sends that
    individual to baulk or not.

    Args:
        next_node (Node): The next node.
        next_individual (Individual): The individual being released.
    """
    if (next_node.number_of_individuals >= next_node.node_capacity) or (
        self.simulation.number_of_individuals >= self.system_capacity
    ):
        self.record_rejection(next_node, next_individual)
        self.simulation.nodes[-1].accept(next_individual, completed=False)
    else:
        self.decide_baulk(next_node, next_individual)

send_individual

send_individual(next_node, next_individual)

Send the next individual to the next node.

Parameters:

Name Type Description Default
next_node Node

The next node.

required
next_individual Individual

The individual being sent.

required
Source code in hciw/arrival_node.py
217
218
219
220
221
222
223
224
225
226
def send_individual(self, next_node, next_individual):
    """Send the next individual to the next node.

    Args:
        next_node (Node): The next node.
        next_individual (Individual): The individual being sent.
    """
    self.number_accepted_individuals += 1
    self.number_accepted_individuals_per_class[next_individual.customer_class] += 1
    next_node.accept(next_individual)

update_next_event_date

update_next_event_date()

Do nothing, as the next event update happens at the event time.

Source code in hciw/arrival_node.py
228
229
def update_next_event_date(self):
    """Do nothing, as the next event update happens at the event time."""

Node

Node(id_, simulation)

Represent a node in the queueing network.

The node holds a queue of individuals and, where applicable, the servers that process them.

Initialise a node.

Parameters:

Name Type Description Default
id_ int

Identifier for the node.

required
simulation object

The simulation to which this node belongs.

required
Source code in hciw/node.py
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
def __init__(self, id_, simulation):
    """Initialise a node.

    Args:
        id_ (int): Identifier for the node.
        simulation (object): The simulation to which this node belongs.
    """
    self.simulation = simulation
    node = self.simulation.network.service_centres[id_ - 1]
    self.server_priority_function = node.server_priority_function
    self.service_discipline = node.service_discipline
    self.next_event_type: str | None = None
    self.schedule: Schedule | Slotted | None
    if isinstance(node.number_of_servers, Schedule):
        self.schedule = node.number_of_servers
        self.schedule.initialise()
        self.c: int | float = self.schedule.c
        if isinstance(self.schedule, Slotted):
            self.slotted: bool = True
            self.next_event_date = self.schedule.next_slot_date
            self.next_event_type = "slotted_service"
        else:
            self.slotted = False
            self.next_event_date = self.schedule.next_shift_change_date
            self.next_shift_change = self.schedule.next_shift_change_date
            self.next_event_type = "shift_change"
    else:
        self.c = node.number_of_servers
        self.schedule = None
        self.slotted = False
        self.next_event_date = float("Inf")
        self.next_shift_change = float("Inf")
    self.node_capacity = node.queueing_capacity + self.c
    self.class_change = node.class_change_matrix
    self.individuals = [[] for _ in range(simulation.number_of_priority_classes)]
    self.number_of_individuals = 0
    self.number_in_service = 0
    self.id_number = id_
    self.baulking_functions = {
        clss: self.simulation.network.customer_classes[clss].baulking_functions[
            id_ - 1
        ]
        for clss in self.simulation.network.customer_class_names
    }
    self.overtime = []
    self.blocked_queue = []
    self.len_blocked_queue = 0
    if not isinf(self.c):
        self.servers = self.create_starting_servers()
    self.highest_id = self.c
    self.simulation.deadlock_detector.initialise_at_node(self)
    self.priority_preempt = node.priority_preempt
    self.interrupted_individuals = []
    self.number_interrupted_individuals = 0
    self.all_servers_total = []
    self.all_servers_busy = []
    self.reneging = node.reneging
    self.dynamic_classes = node.class_change_time
    self.next_class_change_date = float("Inf")
    self.next_individual: list[Individual] | Individual | None = None

now property

now

Get the current simulation time.

Returns:

Type Description
float

The current simulation time.

all_individuals property

all_individuals

Get all individuals currently at the node.

Returns:

Type Description
list

Every individual at the node, across all priority classes.

accept

accept(next_individual, completed=False)

Accept a new individual into the queue.

Removes any previous exit date and blockage status, begins service if possible, records arrival information, and updates the state tracker.

Parameters:

Name Type Description Default
next_individual Individual

The individual to accept.

required
completed bool

Whether the individual has completed service.

False
Source code in hciw/node.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def accept(self, next_individual, completed=False):
    """Accept a new individual into the queue.

    Removes any previous exit date and blockage status, begins service
    if possible, records arrival information, and updates the state
    tracker.

    Args:
        next_individual (Individual): The individual to accept.
        completed (bool): Whether the individual has completed service.
    """
    next_individual.node = self.id_number
    next_individual.exit_date = False
    next_individual.is_blocked = False
    next_individual.original_class = next_individual.customer_class
    next_individual.queue_size_at_arrival = self.number_of_individuals
    self.individuals[next_individual.priority_class].append(next_individual)
    self.number_of_individuals += 1
    self.begin_service_if_possible_accept(next_individual)
    self.simulation.statetracker.change_state_accept(self, next_individual)

add_new_servers

add_new_servers(num_servers)

Add the appropriate number of servers for the given shift.

Parameters:

Name Type Description Default
num_servers int

The number of servers to add.

required
Source code in hciw/node.py
129
130
131
132
133
134
135
136
137
138
139
def add_new_servers(self, num_servers):
    """Add the appropriate number of servers for the given shift.

    Args:
        num_servers (int): The number of servers to add.
    """
    for _i in range(num_servers):
        self.highest_id += 1
        self.servers.append(
            self.simulation.ServerType(self, self.highest_id, self.now)
        )

attach_server

attach_server(server, individual)

Attach a server to an individual, and vice versa.

Parameters:

Name Type Description Default
server Server

The server to attach.

required
individual Individual

The individual to attach.

required
Source code in hciw/node.py
141
142
143
144
145
146
147
148
149
150
151
152
153
def attach_server(self, server, individual):
    """Attach a server to an individual, and vice versa.

    Args:
        server (Server): The server to attach.
        individual (Individual): The individual to attach.
    """
    server.cust = individual
    server.busy = True
    individual.server = server
    self.simulation.deadlock_detector.action_at_attach_server(
        self, server, individual
    )

begin_service_if_possible_accept

begin_service_if_possible_accept(next_individual)

Begin service for an individual at the acceptance point.

Sets the arrival date to the current time, and if a server is free or there are infinitely many servers, attaches the server, gets the service start time, service time and service end time, and updates the server's end date.

Parameters:

Name Type Description Default
next_individual Individual

The individual to serve.

required
Source code in hciw/node.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def begin_service_if_possible_accept(self, next_individual):
    """Begin service for an individual at the acceptance point.

    Sets the arrival date to the current time, and if a server is free
    or there are infinitely many servers, attaches the server, gets the
    service start time, service time and service end time, and updates
    the server's end date.

    Args:
        next_individual (Individual): The individual to serve.
    """
    next_individual.arrival_date = self.now
    if self.reneging is True:
        next_individual.reneging_date = self.get_reneging_date(next_individual)
    self.decide_class_change(next_individual)

    ind = next_individual if isinf(self.c) else self.choose_next_customer()

    if ind is not None:
        free_server = self.find_free_server(ind)
        if free_server is None and isinf(self.c) is False and self.c > 0:
            self.decide_preempt(ind)
        if free_server is not None or isinf(self.c):
            if isinf(self.c) is False:
                self.attach_server(free_server, ind)
            ind.service_start_date = self.now
            ind.service_time = self.get_service_time(ind)
            ind.service_end_date = self.now + ind.service_time
            self.number_in_service += 1
            self.reset_class_change(ind)
            if not isinf(self.c):
                assert isinstance(free_server, Server)
                free_server.next_end_service_date = ind.service_end_date

begin_interrupted_individuals_service

begin_interrupted_individuals_service(srvr)

Restart an interrupted individual's service.

The individual's service time is resampled.

Parameters:

Name Type Description Default
srvr Server

The server to give to the individual.

required
Source code in hciw/node.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
def begin_interrupted_individuals_service(self, srvr):
    """Restart an interrupted individual's service.

    The individual's service time is resampled.

    Args:
        srvr (Server): The server to give to the individual.
    """
    ind = next(iter(self.interrupted_individuals))
    if ind.is_blocked:
        node_blocked_to = self.simulation.nodes[ind.destination]
        ind.destination = False
        node_blocked_to.blocked_queue.remove((self.id_number, ind.id_number))
        node_blocked_to.len_blocked_queue -= 1
        ind.is_blocked = False
    self.attach_server(srvr, ind)
    self.give_service_time_after_preemption(ind)
    ind.service_start_date = self.now
    ind.service_end_date = self.now + ind.service_time
    ind.interrupted = False
    self.number_in_service += 1
    srvr.next_end_service_date = ind.service_end_date
    self.interrupted_individuals.remove(ind)
    self.number_interrupted_individuals -= 1

begin_service_if_possible_change_shift

begin_service_if_possible_change_shift()

Begin service for customers after a shift change.

If there are free servers, restarts interrupted customers' services and begins service for any waiting customers by giving a start date and end date and attaching servers to individuals.

Source code in hciw/node.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def begin_service_if_possible_change_shift(self):
    """Begin service for customers after a shift change.

    If there are free servers, restarts interrupted customers' services
    and begins service for any waiting customers by giving a start date
    and end date and attaching servers to individuals.
    """
    free_servers = [s for s in self.servers if not s.busy]
    for srvr in free_servers:
        if self.number_interrupted_individuals > 0:
            self.begin_interrupted_individuals_service(srvr)
        else:
            ind = self.choose_next_customer()
            if ind is not None:
                self.attach_server(srvr, ind)
                ind.service_start_date = self.now
                self.give_individual_a_service_time(ind)
                ind.service_end_date = self.increment_time(
                    ind.service_start_date, ind.service_time
                )
                self.number_in_service += 1
                self.reset_class_change(ind)
                srvr.next_end_service_date = ind.service_end_date

begin_service_if_possible_release

begin_service_if_possible_release(
    next_individual, newly_free_server
)

Begin service for an individual after a release.

Checks for interrupted individuals left to restart service, then gives an arrival date and service time, gives a start date and end date, and attaches the server to the individual.

Parameters:

Name Type Description Default
next_individual Individual

The individual being released.

required
newly_free_server Server

The server freed by the release.

required
Source code in hciw/node.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
def begin_service_if_possible_release(self, next_individual, newly_free_server):
    """Begin service for an individual after a release.

    Checks for interrupted individuals left to restart service, then
    gives an arrival date and service time, gives a start date and end
    date, and attaches the server to the individual.

    Args:
        next_individual (Individual): The individual being released.
        newly_free_server (Server): The server freed by the release.
    """
    if newly_free_server is not None and newly_free_server in self.servers:
        if self.number_interrupted_individuals > 0:
            self.begin_interrupted_individuals_service(newly_free_server)
        else:
            ind = self.choose_next_customer()
            if ind is not None:
                self.attach_server(newly_free_server, ind)
                ind.service_start_date = self.now
                self.give_individual_a_service_time(ind)
                ind.service_end_date = self.increment_time(
                    ind.service_start_date, ind.service_time
                )
                self.number_in_service += 1
                self.reset_class_change(ind)
                newly_free_server.next_end_service_date = ind.service_end_date

block_individual

block_individual(individual, next_node)

Block an individual from entering the next node.

Changes the blocked status, updates the state tracker, adds information to the next node's blocked queue, and updates the deadlock detector, informing the simulation that there are unchecked blockages for deadlock detection.

Parameters:

Name Type Description Default
individual Individual

The individual being blocked.

required
next_node Node

The node the individual is blocked from entering.

required
Source code in hciw/node.py
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
def block_individual(self, individual, next_node):
    """Block an individual from entering the next node.

    Changes the blocked status, updates the state tracker, adds
    information to the next node's blocked queue, and updates the
    deadlock detector, informing the simulation that there are unchecked
    blockages for deadlock detection.

    Args:
        individual (Individual): The individual being blocked.
        next_node (Node): The node the individual is blocked from
            entering.
    """
    individual.is_blocked = True
    self.simulation.statetracker.change_state_block(self, next_node, individual)
    next_node.blocked_queue.append((self.id_number, individual.id_number))
    next_node.len_blocked_queue += 1
    self.simulation.deadlock_detector.action_at_blockage(individual, next_node)
    self.simulation.unchecked_blockage = True

change_customer_class

change_customer_class(individual)

Change an individual's customer class.

The new class is chosen according to a probability distribution.

Parameters:

Name Type Description Default
individual Individual

The individual whose class changes.

required
Source code in hciw/node.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
def change_customer_class(self, individual):
    """Change an individual's customer class.

    The new class is chosen according to a probability distribution.

    Args:
        individual (Individual): The individual whose class changes.
    """
    if self.class_change:
        individual.previous_class = individual.customer_class
        individual.customer_class = random_choice(
            self.simulation.network.customer_class_names,
            [
                self.class_change[individual.previous_class][clss_name]
                for clss_name in self.simulation.network.customer_class_names
            ],
        )
        individual.prev_priority_class = individual.priority_class
        individual.priority_class = self.simulation.network.priority_class_mapping[
            individual.customer_class
        ]

change_customer_class_while_waiting

change_customer_class_while_waiting()

Change the next queued individual's class while waiting.

The individual is moved between priority queues and preemption is decided if the priority class changes.

Source code in hciw/node.py
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def change_customer_class_while_waiting(self):
    """Change the next queued individual's class while waiting.

    The individual is moved between priority queues and preemption is
    decided if the priority class changes.
    """
    changing_individual = self.next_individual
    assert isinstance(changing_individual, Individual)
    changing_individual.customer_class = changing_individual.next_class
    changing_individual.priority_class = (
        self.simulation.network.priority_class_mapping[
            changing_individual.next_class
        ]
    )
    if (
        changing_individual.priority_class
        != changing_individual.prev_priority_class
    ):
        self.change_priority_queue(changing_individual)
        self.decide_preempt(changing_individual)
    self.simulation.statetracker.change_state_classchange(self, changing_individual)
    changing_individual.previous_class = changing_individual.next_class
    changing_individual.prev_priority_class = changing_individual.priority_class
    self.decide_class_change(changing_individual)

change_priority_queue

change_priority_queue(individual)

Move an individual to their new priority queue.

Parameters:

Name Type Description Default
individual Individual

The individual to move.

required
Source code in hciw/node.py
332
333
334
335
336
337
338
339
def change_priority_queue(self, individual):
    """Move an individual to their new priority queue.

    Args:
        individual (Individual): The individual to move.
    """
    self.individuals[individual.prev_priority_class].remove(individual)
    self.individuals[individual.priority_class].append(individual)

change_shift

change_shift()

Implement a server shift change.

Adds or deletes servers, or indicates which servers should go off duty, and begins any new services if there are free servers.

Source code in hciw/node.py
341
342
343
344
345
346
347
348
349
350
351
352
353
def change_shift(self):
    """Implement a server shift change.

    Adds or deletes servers, or indicates which servers should go off
    duty, and begins any new services if there are free servers.
    """
    assert self.schedule is not None
    self.schedule.get_next_shift()
    self.next_shift_change = self.schedule.next_shift_change_date
    self.c = self.schedule.c
    self.take_servers_off_duty(preemption=self.schedule.preemption)
    self.add_new_servers(self.schedule.c)
    self.begin_service_if_possible_change_shift()

find_number_of_slotted_services

find_number_of_slotted_services()

Find the number of slotted services to start in this slot.

Returns:

Type Description
int

The number of services to start.

Source code in hciw/node.py
355
356
357
358
359
360
361
362
363
364
365
366
367
def find_number_of_slotted_services(self):
    """Find the number of slotted services to start in this slot.

    Returns:
        int: The number of services to start.
    """
    assert isinstance(self.schedule, Slotted)
    if self.schedule.capacitated:
        return min(
            max(self.schedule.slot_size - self.number_in_service, 0),
            self.number_of_individuals,
        )
    return min(self.schedule.slot_size, self.number_of_individuals)

interrupt_slotted_services

interrupt_slotted_services()

Interrupt services when there is not enough slot capacity.

Finds the customers whose slotted services need to be interrupted because of insufficient capacity at the current slot, and interrupts their services.

Source code in hciw/node.py
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
def interrupt_slotted_services(self):
    """Interrupt services when there is not enough slot capacity.

    Finds the customers whose slotted services need to be interrupted
    because of insufficient capacity at the current slot, and interrupts
    their services.
    """
    assert isinstance(self.schedule, Slotted)
    if self.schedule.capacitated and self.schedule.preemption is not False:
        number_to_interrupt = self.number_in_service - self.schedule.slot_size
        if number_to_interrupt > 0:
            inds_to_interrupt = sorted(
                [
                    ind
                    for ind in self.all_individuals
                    if ind.service_start_date is not False
                ],
                key=lambda x: (x.priority_class, x.arrival_date),
                reverse=True,
            )[:number_to_interrupt]
            for ind in inds_to_interrupt:
                self.interrupt_service(ind)

slotted_service

slotted_service()

Serve a set number of customers at the current time.

Only the number of customers allowed by the current slot may have service at the exact time the method is called.

Source code in hciw/node.py
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
def slotted_service(self):
    """Serve a set number of customers at the current time.

    Only the number of customers allowed by the current slot may have
    service at the exact time the method is called.
    """
    assert isinstance(self.schedule, Slotted)
    number_of_slotted_services = self.find_number_of_slotted_services()
    self.interrupt_slotted_services()
    for _i in range(number_of_slotted_services):
        if self.number_interrupted_individuals > 0:
            ind = self.interrupted_individuals[0]
            self.interrupted_individuals.remove(ind)
            self.number_interrupted_individuals -= 1
        else:
            ind = self.choose_next_customer()
        if ind is not None:
            ind.service_start_date = self.now
            self.give_individual_a_service_time(ind)
            ind.service_end_date = self.now + ind.service_time
            ind.server = True
            self.number_in_service += 1
            self.reset_class_change(ind)
    self.schedule.get_next_slot()

choose_next_customer

choose_next_customer()

Choose which customer will be next to be served.

Returns:

Type Description
Individual

The customer chosen by the service discipline, or None if no customer is waiting.

Source code in hciw/node.py
417
418
419
420
421
422
423
424
425
426
427
428
429
430
def choose_next_customer(self):
    """Choose which customer will be next to be served.

    Returns:
        Individual: The customer chosen by the service discipline, or
            None if no customer is waiting.
    """
    for priority_individuals in self.individuals:
        waiting_individuals = [
            ind for ind in priority_individuals if not ind.server
        ]
        if len(waiting_individuals) > 0:
            return self.service_discipline(waiting_individuals, self.now)
    return None

create_starting_servers

create_starting_servers()

Initialise the servers.

Returns:

Type Description
list

The newly created starting servers.

Source code in hciw/node.py
432
433
434
435
436
437
438
439
440
def create_starting_servers(self):
    """Initialise the servers.

    Returns:
        list: The newly created starting servers.
    """
    return [
        self.simulation.ServerType(self, i + 1, 0.0) for i in range(int(self.c))
    ]

decide_class_change

decide_class_change(next_individual)

Decide the next class and class change date for an individual.

Parameters:

Name Type Description Default
next_individual Individual

The individual to update.

required
Source code in hciw/node.py
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
def decide_class_change(self, next_individual):
    """Decide the next class and class change date for an individual.

    Args:
        next_individual (Individual): The individual to update.
    """
    if self.dynamic_classes is True:
        next_time = float("inf")
        next_class = next_individual.customer_class
        for clss, dist in self.simulation.network.customer_classes[
            next_individual.customer_class
        ].class_change_time_distributions.items():
            if dist is not None:
                t = dist.sample()
                if t < next_time:
                    next_time = t
                    next_class = clss
        next_individual.next_class = next_class
        next_individual.class_change_date = self.increment_time(self.now, next_time)
        self.find_next_class_change()

decide_preempt

decide_preempt(individual)

Decide whether priority preemption is needed.

If needed, finds the individual to preempt and preempts them.

Parameters:

Name Type Description Default
individual Individual

The individual seeking service.

required
Source code in hciw/node.py
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
def decide_preempt(self, individual):
    """Decide whether priority preemption is needed.

    If needed, finds the individual to preempt and preempts them.

    Args:
        individual (Individual): The individual seeking service.
    """
    if self.priority_preempt:
        least_priority = max(s.cust.priority_class for s in self.servers)
        if individual.priority_class < least_priority:
            least_prioritised_individuals = [
                s.cust
                for s in self.servers
                if s.cust.priority_class == least_priority
            ]
            individual_to_preempt = max(
                list(least_prioritised_individuals),
                key=lambda cust: cust.service_start_date,
            )
            self.preempt(individual_to_preempt, individual)

detatch_server

detatch_server(server, individual)

Detach a server from an individual, and vice versa.

Parameters:

Name Type Description Default
server Server

The server to detach.

required
individual Individual

The individual to detach.

required
Source code in hciw/node.py
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
def detatch_server(self, server, individual):
    """Detach a server from an individual, and vice versa.

    Args:
        server (Server): The server to detach.
        individual (Individual): The individual to detach.
    """
    self.simulation.deadlock_detector.action_at_detatch_server(server)
    server.cust = False
    server.busy = False
    individual.server = False
    server.busy_time = self.increment_time(
        server.busy_time, individual.exit_date - individual.service_start_date
    )
    server.total_time = self.now - server.start_date
    if server.offduty:
        self.kill_server(server)

find_free_server

find_free_server(ind)

Find a free server.

Parameters:

Name Type Description Default
ind Individual

The individual needing a server.

required

Returns:

Type Description
Server

A free server, or None if none is available.

Source code in hciw/node.py
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def find_free_server(self, ind):
    """Find a free server.

    Args:
        ind (Individual): The individual needing a server.

    Returns:
        Server: A free server, or None if none is available.
    """
    if isinf(self.c):
        return None

    if self.server_priority_function is None:
        all_servers = self.servers
    else:
        all_servers = sorted(
            self.servers, key=lambda srv: self.server_priority_function(srv, ind)
        )

    for svr in all_servers:
        if not svr.busy:
            return svr
    return None

decide_between_simultaneous_individuals

decide_between_simultaneous_individuals()

Find the next individual that should now finish service.

Returns:

Type Description
Individual

A randomly chosen individual when several finish at the same time.

Source code in hciw/node.py
527
528
529
530
531
532
533
534
535
536
537
538
539
def decide_between_simultaneous_individuals(self):
    """Find the next individual that should now finish service.

    Returns:
        Individual: A randomly chosen individual when several finish at
            the same time.
    """
    assert isinstance(self.next_individual, list)
    if len(self.next_individual) > 1:
        next_individual = random_choice(self.next_individual)
    else:
        next_individual = self.next_individual[0]
    return next_individual

find_next_class_change

find_next_class_change()

Update the next class change date and individual.

Source code in hciw/node.py
541
542
543
544
545
546
547
548
def find_next_class_change(self):
    """Update the next class change date and individual."""
    self.next_class_change_date = float("inf")
    self.next_class_change_ind = None
    for ind in self.all_individuals:
        if (ind.class_change_date < self.next_class_change_date) and not ind.server:
            self.next_class_change_ind = ind
            self.next_class_change_date = ind.class_change_date

find_server_utilisation

find_server_utilisation()

Find the overall server utilisation for the node.

Source code in hciw/node.py
550
551
552
553
554
555
556
557
558
559
560
def find_server_utilisation(self):
    """Find the overall server utilisation for the node."""
    if isinf(self.c) or self.c == 0:
        self.server_utilisation = None
    else:
        for server in self.servers:
            self.all_servers_total.append(server.total_time)
            self.all_servers_busy.append(server.busy_time)
        self.server_utilisation = sum(self.all_servers_busy) / sum(
            self.all_servers_total
        )

finish_service

finish_service()

Finish the service of the next individual.

Finds the individual to finish service, checks whether they need to change class, finds their next node, and releases the individual if there is capacity at the destination, otherwise causes blockage.

Source code in hciw/node.py
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
def finish_service(self):
    """Finish the service of the next individual.

    Finds the individual to finish service, checks whether they need to
    change class, finds their next node, and releases the individual if
    there is capacity at the destination, otherwise causes blockage.
    """
    next_individual = self.decide_between_simultaneous_individuals()
    self.change_customer_class(next_individual)
    next_node = self.next_node(next_individual)
    next_individual.destination = next_node.id_number
    if not isinf(self.c) and self.c > 0:
        assert isinstance(next_individual.server, Server)
        next_individual.server.next_end_service_date = float("Inf")
    if next_node.number_of_individuals < next_node.node_capacity:
        self.release(next_individual, next_node)
    else:
        self.block_individual(next_individual, next_node)

give_individual_a_service_time

give_individual_a_service_time(individual)

Give a service time to an individual.

Either a new service time, or a service time after pre-emption.

Parameters:

Name Type Description Default
individual Individual

The individual to serve.

required
Source code in hciw/node.py
581
582
583
584
585
586
587
588
589
590
591
592
def give_individual_a_service_time(self, individual):
    """Give a service time to an individual.

    Either a new service time, or a service time after pre-emption.

    Args:
        individual (Individual): The individual to serve.
    """
    if individual.service_time is False:
        individual.service_time = self.get_service_time(individual)
    else:
        self.give_service_time_after_preemption(individual)

give_service_time_after_preemption

give_service_time_after_preemption(individual)

Give a service time after preemption.

Either resamples, restarts, or resumes service time from where it was left off.

Parameters:

Name Type Description Default
individual Individual

The individual to serve.

required
Source code in hciw/node.py
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
def give_service_time_after_preemption(self, individual):
    """Give a service time after preemption.

    Either resamples, restarts, or resumes service time from where it was
    left off.

    Args:
        individual (Individual): The individual to serve.
    """
    if individual.service_time == "resample":
        individual.service_time = self.get_service_time(individual)
    if individual.service_time == "restart":
        individual.service_time = individual.original_service_time
    if individual.service_time == "resume":
        individual.service_time = individual.time_left

have_event

have_event()

Trigger the node's next event.

Source code in hciw/node.py
610
611
612
613
614
615
616
617
618
619
620
621
def have_event(self):
    """Trigger the node's next event."""
    if self.next_event_type == "end_service":
        self.finish_service()
    elif self.next_event_type == "shift_change":
        self.change_shift()
    elif self.next_event_type == "renege":
        self.renege()
    elif self.next_event_type == "class_change":
        self.change_customer_class_while_waiting()
    elif self.next_event_type == "slotted_service":
        self.slotted_service()

increment_time

increment_time(original, increment)

Increment the original time by the increment.

Parameters:

Name Type Description Default
original float

The original time.

required
increment float

The amount to add.

required

Returns:

Type Description

The incremented time.

Source code in hciw/node.py
623
624
625
626
627
628
629
630
631
632
633
def increment_time(self, original, increment):
    """Increment the original time by the increment.

    Args:
        original (float): The original time.
        increment (float): The amount to add.

    Returns:
        The incremented time.
    """
    return original + increment

kill_server

kill_server(srvr)

Kill a server when it goes off duty.

Parameters:

Name Type Description Default
srvr Server

The server to remove.

required
Source code in hciw/node.py
635
636
637
638
639
640
641
642
643
644
645
646
def kill_server(self, srvr):
    """Kill a server when it goes off duty.

    Args:
        srvr (Server): The server to remove.
    """
    srvr.total_time = self.increment_time(self.next_event_date, -srvr.start_date)
    self.overtime.append(self.increment_time(self.next_event_date, -srvr.shift_end))
    self.all_servers_busy.append(srvr.busy_time)
    self.all_servers_total.append(srvr.total_time)
    indx = self.servers.index(srvr)
    del self.servers[indx]

next_node

next_node(ind)

Find the next node according to the routing method.

Parameters:

Name Type Description Default
ind Individual

The individual being routed.

required

Returns:

Type Description
Node

The next node.

Source code in hciw/node.py
648
649
650
651
652
653
654
655
656
657
658
659
def next_node(self, ind):
    """Find the next node according to the routing method.

    Args:
        ind (Individual): The individual being routed.

    Returns:
        Node: The next node.
    """
    return self.simulation.routers[ind.customer_class].next_node(
        ind, self.id_number
    )

next_node_for_rerouting

next_node_for_rerouting(ind)

Find the next node for rerouting according to the routing method.

Parameters:

Name Type Description Default
ind Individual

The individual being rerouted.

required

Returns:

Type Description
Node

The next node for rerouting.

Source code in hciw/node.py
661
662
663
664
665
666
667
668
669
670
671
672
def next_node_for_rerouting(self, ind):
    """Find the next node for rerouting according to the routing method.

    Args:
        ind (Individual): The individual being rerouted.

    Returns:
        Node: The next node for rerouting.
    """
    return self.simulation.routers[ind.customer_class].next_node_for_rerouting(
        ind, self.id_number
    )

next_node_for_jockeying

next_node_for_jockeying(ind)

Find the next node for jockeying according to the routing method.

Parameters:

Name Type Description Default
ind Individual

The jockeying individual.

required

Returns:

Type Description
Node

The next node for jockeying.

Source code in hciw/node.py
674
675
676
677
678
679
680
681
682
683
684
685
def next_node_for_jockeying(self, ind):
    """Find the next node for jockeying according to the routing method.

    Args:
        ind (Individual): The jockeying individual.

    Returns:
        Node: The next node for jockeying.
    """
    return self.simulation.routers[ind.customer_class].next_node_for_jockeying(
        ind, self.id_number
    )

preempt

preempt(individual_to_preempt, next_individual)

Preempt an individual and replace them in service.

Parameters:

Name Type Description Default
individual_to_preempt Individual

The individual removed from service.

required
next_individual Individual

The individual taking their place.

required
Source code in hciw/node.py
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
def preempt(self, individual_to_preempt, next_individual):
    """Preempt an individual and replace them in service.

    Args:
        individual_to_preempt (Individual): The individual removed from
            service.
        next_individual (Individual): The individual taking their place.
    """
    server = individual_to_preempt.server
    individual_to_preempt.original_service_time = individual_to_preempt.service_time
    if self.priority_preempt == "reroute":
        self.reroute(individual_to_preempt)
    else:
        self.write_interruption_record(individual_to_preempt)
        individual_to_preempt.service_start_date = False
        individual_to_preempt.time_left = (
            individual_to_preempt.service_end_date - self.now
        )
        individual_to_preempt.service_time = self.priority_preempt
        individual_to_preempt.service_end_date = False
        self.detatch_server(server, individual_to_preempt)
        self.decide_class_change(individual_to_preempt)
    self.attach_server(server, next_individual)
    next_individual.service_start_date = self.now
    next_individual.service_time = self.get_service_time(next_individual)
    next_individual.service_end_date = self.now + next_individual.service_time
    self.reset_class_change(next_individual)
    server.next_end_service_date = next_individual.service_end_date

release

release(next_individual, next_node, reroute=False)

Release an individual from the node.

Removes the individual from the queue, records relevant information, detaches the server, updates the state tracker, begins service of any waiting customers, sends the individual to their next destination, and releases any individuals blocked by this node.

Parameters:

Name Type Description Default
next_individual Individual

The individual to release.

required
next_node Node

The node receiving the individual.

required
reroute bool

Whether the release is due to rerouting.

False
Source code in hciw/node.py
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
def release(self, next_individual, next_node, reroute=False):
    """Release an individual from the node.

    Removes the individual from the queue, records relevant information,
    detaches the server, updates the state tracker, begins service of any
    waiting customers, sends the individual to their next destination, and
    releases any individuals blocked by this node.

    Args:
        next_individual (Individual): The individual to release.
        next_node (Node): The node receiving the individual.
        reroute (bool): Whether the release is due to rerouting.
    """
    self.individuals[next_individual.prev_priority_class].remove(next_individual)
    self.number_of_individuals -= 1
    self.number_in_service -= 1
    next_individual.queue_size_at_departure = self.number_of_individuals
    next_individual.exit_date = self.now
    if not reroute:
        self.write_individual_record(next_individual)
    newly_free_server = None
    if not isinf(self.c) and not self.slotted:
        newly_free_server = next_individual.server
        self.detatch_server(newly_free_server, next_individual)
    if self.slotted:
        next_individual.server = False
    self.reset_individual_attributes(next_individual)
    self.simulation.statetracker.change_state_release(
        self, next_node, next_individual, next_individual.is_blocked
    )
    if not reroute:
        self.begin_service_if_possible_release(next_individual, newly_free_server)
    next_node.accept(next_individual)
    if not reroute:
        self.release_blocked_individual()

release_blocked_individual

release_blocked_individual()

Release an individual blocked by this node.

Finds the individual who has been blocked the longest, removes them from the blocked queue, restores their service if it was interrupted, and releases them from their node.

Source code in hciw/node.py
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
def release_blocked_individual(self):
    """Release an individual blocked by this node.

    Finds the individual who has been blocked the longest, removes them
    from the blocked queue, restores their service if it was interrupted,
    and releases them from their node.
    """
    if (self.len_blocked_queue > 0) and (
        self.number_of_individuals < self.node_capacity
    ):
        node_to_receive_from = self.simulation.nodes[self.blocked_queue[0][0]]
        individual_to_receive_index = [
            ind.id_number for ind in node_to_receive_from.all_individuals
        ].index(self.blocked_queue[0][1])
        individual_to_receive = node_to_receive_from.all_individuals[
            individual_to_receive_index
        ]
        self.blocked_queue.pop(0)
        self.len_blocked_queue -= 1
        if individual_to_receive.interrupted:
            individual_to_receive.interrupted = False
            individual_to_receive.service_start_date = (
                individual_to_receive.original_service_start_date
            )
            individual_to_receive.service_end_date = (
                individual_to_receive.service_start_date
                + individual_to_receive.original_service_time
            )
            node_to_receive_from.interrupted_individuals.remove(
                individual_to_receive
            )
            node_to_receive_from.number_interrupted_individuals -= 1
        node_to_receive_from.release(individual_to_receive, self)

reset_class_change

reset_class_change(individual)

Reset an individual's class change date when service begins.

Parameters:

Name Type Description Default
individual Individual

The individual beginning service.

required
Source code in hciw/node.py
786
787
788
789
790
791
792
793
794
795
def reset_class_change(self, individual):
    """Reset an individual's class change date when service begins.

    Args:
        individual (Individual): The individual beginning service.
    """
    if self.dynamic_classes:
        individual.class_change_date = float("inf")
        if individual == self.next_class_change_ind:
            self.find_next_class_change()

renege

renege()

Remove an individual from the queue due to reneging.

Resets the individual's reneging date and sends the individual to their reneging destination.

Source code in hciw/node.py
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
def renege(self):
    """Remove an individual from the queue due to reneging.

    Resets the individual's reneging date and sends the individual to
    their reneging destination.
    """
    reneging_individual = self.decide_between_simultaneous_individuals()
    reneging_individual.reneging_date = float("Inf")
    next_node = self.next_node_for_jockeying(reneging_individual)
    self.individuals[reneging_individual.prev_priority_class].remove(
        reneging_individual
    )
    self.number_of_individuals -= 1
    reneging_individual.queue_size_at_departure = self.number_of_individuals
    reneging_individual.exit_date = self.now
    self.write_reneging_record(reneging_individual)
    self.reset_individual_attributes(reneging_individual)
    self.simulation.statetracker.change_state_renege(
        self, next_node, reneging_individual, blocked=False
    )
    next_node.accept(reneging_individual, completed=False)
    self.release_blocked_individual()

get_reneging_date

get_reneging_date(ind)

Return the reneging date for a given individual.

Parameters:

Name Type Description Default
ind Individual

The individual.

required

Returns:

Type Description
float

The reneging date.

Source code in hciw/node.py
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
def get_reneging_date(self, ind):
    """Return the reneging date for a given individual.

    Args:
        ind (Individual): The individual.

    Returns:
        float: The reneging date.
    """
    dist = self.simulation.network.customer_classes[
        ind.customer_class
    ].reneging_time_distributions[self.id_number - 1]
    if dist is None:
        return float("inf")
    return self.now + dist.sample(t=self.now, ind=ind)

get_service_time

get_service_time(ind)

Return a service time for the given customer class.

Parameters:

Name Type Description Default
ind Individual

The individual being served.

required

Returns:

Type Description
float

The sampled service time.

Source code in hciw/node.py
836
837
838
839
840
841
842
843
844
845
846
847
def get_service_time(self, ind):
    """Return a service time for the given customer class.

    Args:
        ind (Individual): The individual being served.

    Returns:
        float: The sampled service time.
    """
    return self.simulation.service_times[self.id_number][ind.customer_class].sample(
        t=self.now, ind=ind
    )

take_servers_off_duty

take_servers_off_duty(preemption: bool | str = False)

Gather the servers that should be deleted.

Parameters:

Name Type Description Default
preemption bool

Whether preemption is active for this shift change.

False
Source code in hciw/node.py
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
def take_servers_off_duty(self, preemption: bool | str = False):
    """Gather the servers that should be deleted.

    Args:
        preemption (bool): Whether preemption is active for this shift
            change.
    """
    if not preemption:
        to_delete = []
        for srvr in self.servers:
            srvr.shift_end = self.next_event_date
            if srvr.busy:
                srvr.offduty = True
            else:
                to_delete.append(srvr)
    else:
        to_delete = self.servers[::1]  # copy
        for s in self.servers:
            s.shift_end = self.next_event_date
            if s.cust is not False:
                self.interrupt_service(s.cust)
        self.sort_interrupted_individuals()
    for obs in to_delete:
        self.kill_server(obs)

interrupt_service

interrupt_service(individual)

Interrupt an individual's service.

Places the individual in the interrupted queue and writes an interruption record for them.

Parameters:

Name Type Description Default
individual Individual

The individual being interrupted.

required
Source code in hciw/node.py
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
def interrupt_service(self, individual):
    """Interrupt an individual's service.

    Places the individual in the interrupted queue and writes an
    interruption record for them.

    Args:
        individual (Individual): The individual being interrupted.
    """
    individual.original_service_time = individual.service_time
    assert self.schedule is not None
    if self.schedule.preemption == "reroute":
        self.reroute(individual)
    else:
        self.interrupted_individuals.append(individual)
        individual.interrupted = True
        self.number_interrupted_individuals += 1
        self.write_interruption_record(individual)
        individual.original_service_start_date = individual.service_start_date
        individual.service_start_date = False
        individual.time_left = individual.service_end_date - self.now
        individual.service_time = self.schedule.preemption
        individual.service_end_date = False
        self.number_in_service -= 1

sort_interrupted_individuals

sort_interrupted_individuals()

Sort interrupted individuals by priority class and arrival date.

Source code in hciw/node.py
899
900
901
902
903
def sort_interrupted_individuals(self):
    """Sort interrupted individuals by priority class and arrival date."""
    self.interrupted_individuals.sort(
        key=lambda x: (x.priority_class, x.arrival_date)
    )

reroute

reroute(individual)

Reroute a preempted individual.

Parameters:

Name Type Description Default
individual Individual

The individual being rerouted.

required
Source code in hciw/node.py
905
906
907
908
909
910
911
912
913
def reroute(self, individual):
    """Reroute a preempted individual.

    Args:
        individual (Individual): The individual being rerouted.
    """
    next_node = self.next_node_for_rerouting(individual)
    self.write_interruption_record(individual, destination=next_node.id_number)
    self.release(individual, next_node, reroute=True)

update_next_end_service_without_server

update_next_end_service_without_server()

Update the next end of a slotted service in possible_next_events.

Source code in hciw/node.py
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
def update_next_end_service_without_server(self):
    """Update the next end of a slotted service in `possible_next_events`."""
    if self.slotted or isinf(self.c):
        next_end_service_date = float("Inf")
        for ind in self.all_individuals:
            if not ind.is_blocked and ind.service_end_date >= self.now:
                if ind.service_end_date < next_end_service_date:
                    self.possible_next_events["end_service"] = (
                        [ind],
                        ind.service_end_date,
                    )
                    next_end_service_date = ind.service_end_date
                elif (ind.service_end_date == next_end_service_date) and (
                    not isinf(next_end_service_date)
                ):
                    self.possible_next_events["end_service"][0].append(ind)

update_next_end_service_with_server

update_next_end_service_with_server()

Update the next end service with a server in possible_next_events.

Source code in hciw/node.py
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
def update_next_end_service_with_server(self):
    """Update the next end service with a server in `possible_next_events`."""
    if not self.slotted and not isinf(self.c):
        next_end_service_date = float("Inf")
        for s in self.servers:
            if s.next_end_service_date < next_end_service_date:
                self.possible_next_events["end_service"] = (
                    [s.cust],
                    s.next_end_service_date,
                )
                next_end_service_date = s.next_end_service_date
            elif (s.next_end_service_date == next_end_service_date) and (
                not isinf(next_end_service_date)
            ):
                self.possible_next_events["end_service"][0].append(s.cust)

update_next_renege_time

update_next_renege_time()

Update the next renege time in possible_next_events.

Source code in hciw/node.py
948
949
950
951
952
953
954
955
956
957
958
959
960
961
def update_next_renege_time(self):
    """Update the next renege time in `possible_next_events`."""
    if not isinf(self.c) and self.reneging is True:
        next_renege_date = float("Inf")
        for ind in self.all_individuals:
            if (ind.reneging_date < next_renege_date) and not ind.server:
                self.possible_next_events["renege"] = ([ind], ind.reneging_date)
                next_renege_date = ind.reneging_date
            elif (
                (ind.reneging_date == next_renege_date)
                and (not ind.server)
                and (not isinf(next_renege_date))
            ):
                self.possible_next_events["renege"][0].append(ind)

update_next_class_change_while_waiting

update_next_class_change_while_waiting()

Update the next class change while waiting in possible_next_events.

Source code in hciw/node.py
963
964
965
966
967
968
969
def update_next_class_change_while_waiting(self):
    """Update the next class change while waiting in `possible_next_events`."""
    if self.dynamic_classes is True and not isinf(self.c):
        self.possible_next_events["class_change"] = (
            self.next_class_change_ind,
            self.next_class_change_date,
        )

update_next_shift_change_or_slot_time

update_next_shift_change_or_slot_time()

Update possible_next_events with the next shift or slot time.

Source code in hciw/node.py
971
972
973
974
975
976
977
978
979
980
981
982
983
def update_next_shift_change_or_slot_time(self):
    """Update `possible_next_events` with the next shift or slot time."""
    if self.schedule is not None:
        if self.schedule.schedule_type == "schedule":
            self.possible_next_events["shift_change"] = (
                None,
                self.next_shift_change,
            )
        if isinstance(self.schedule, Slotted):
            self.possible_next_events["slotted_service"] = (
                None,
                self.schedule.next_slot_date,
            )

update_next_event_date

update_next_event_date()

Find the time of the next event at this node.

With infinite servers this is the time of the next individual to end service, or infinity. Otherwise it is the minimum of the next shift change and the time for the next individual (who is not blocked) to end service, or infinity.

Source code in hciw/node.py
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
def update_next_event_date(self):
    """Find the time of the next event at this node.

    With infinite servers this is the time of the next individual to end
    service, or infinity. Otherwise it is the minimum of the next shift
    change and the time for the next individual (who is not blocked) to
    end service, or infinity.
    """
    self.possible_next_events = {}
    self.update_next_end_service_without_server()
    self.update_next_end_service_with_server()
    self.update_next_renege_time()
    self.update_next_class_change_while_waiting()
    self.update_next_shift_change_or_slot_time()
    if self.reneging or self.dynamic_classes or self.schedule:
        next_event, self.next_event_type = self.decide_next_event()
        self.next_event_date = next_event[1]
        self.next_individual = next_event[0]
    else:
        next_end_service = self.possible_next_events.get(
            "end_service", (None, float("inf"))
        )
        self.next_event_date = next_end_service[1]
        self.next_individual = next_end_service[0]
        self.next_event_type = "end_service"

decide_next_event

decide_next_event()

Decide the next event.

Chooses the next service end, renege, shift change, or class change. In the case of a tie, events are prioritised in the following order:

  1. slotted service
  2. shift change
  3. end service
  4. class change
  5. renege

Returns:

Type Description
tuple

The next event and its event type.

Source code in hciw/node.py
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
def decide_next_event(self):
    """Decide the next event.

    Chooses the next service end, renege, shift change, or class change.
    In the case of a tie, events are prioritised in the following order:

    1. slotted service
    2. shift change
    3. end service
    4. class change
    5. renege

    Returns:
        tuple: The next event and its event type.
    """
    next_date = float("inf")
    next_event = (None, float("inf"))
    next_event_type = None
    for event_type in [
        "slotted_service",
        "shift_change",
        "end_service",
        "class_change",
        "renege",
    ]:
        possible_next_event = self.possible_next_events.get(
            event_type, (None, float("inf"))
        )
        if possible_next_event[1] < next_date:
            next_event = possible_next_event
            next_event_type = event_type
            next_date = next_event[1]
    return next_event, next_event_type

wrap_up_servers

wrap_up_servers(current_time)

Update server total and busy times at the end of the run.

Parameters:

Name Type Description Default
current_time float

The simulation end time.

required
Source code in hciw/node.py
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
def wrap_up_servers(self, current_time):
    """Update server total and busy times at the end of the run.

    Args:
        current_time (float): The simulation end time.
    """
    if not isinf(self.c):
        for srvr in self.servers:
            srvr.total_time = self.increment_time(current_time, -srvr.start_date)
            if srvr.busy:
                srvr.busy_time += self.increment_time(
                    current_time, -srvr.cust.service_start_date
                )

write_individual_record

write_individual_record(individual)

Write a data record for an individual leaving a node.

Parameters:

Name Type Description Default
individual Individual

The individual leaving the node.

required
Source code in hciw/node.py
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
def write_individual_record(self, individual):
    """Write a data record for an individual leaving a node.

    Args:
        individual (Individual): The individual leaving the node.
    """
    if isinf(self.c) or self.slotted:
        server_id = False
    else:
        server_id = individual.server.id_number

    record = DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=individual.arrival_date,
        waiting_time=individual.service_start_date - individual.arrival_date,
        service_start_date=individual.service_start_date,
        service_time=individual.service_end_date - individual.service_start_date,
        service_end_date=individual.service_end_date,
        time_blocked=individual.exit_date - individual.service_end_date,
        exit_date=individual.exit_date,
        destination=individual.destination,
        queue_size_at_arrival=individual.queue_size_at_arrival,
        queue_size_at_departure=individual.queue_size_at_departure,
        server_id=server_id,
        record_type="service",
    )
    individual.data_records.append(record)

write_incomplete_record

write_incomplete_record(individual)

Write an incomplete data record at the end of the run.

Parameters:

Name Type Description Default
individual Individual

The individual still at the node.

required

Returns:

Type Description
DataRecord

The incomplete data record.

Source code in hciw/node.py
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
def write_incomplete_record(self, individual):
    """Write an incomplete data record at the end of the run.

    Args:
        individual (Individual): The individual still at the node.

    Returns:
        DataRecord: The incomplete data record.
    """
    if not individual.service_time:  # Still in queue
        service_start_date = None
        waiting_time = None
        service_time = None
        service_end_date = None
    else:
        service_start_date = individual.service_start_date
        waiting_time = individual.service_start_date - individual.arrival_date
        if individual.service_end_date > self.now:  # Still in service
            service_time = None
            service_end_date = None
        else:  # Still blocked
            service_time = individual.service_time
            service_end_date = individual.service_end_date

    return DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=individual.arrival_date,
        waiting_time=waiting_time,
        service_start_date=service_start_date,
        service_time=service_time,
        service_end_date=service_end_date,
        time_blocked=None,
        exit_date=None,
        destination=None,
        queue_size_at_arrival=individual.queue_size_at_arrival,
        queue_size_at_departure=None,
        server_id=False,
        record_type="incomplete",
    )

write_interruption_record

write_interruption_record(individual, destination=nan)

Write a data record for an individual being interrupted.

Parameters:

Name Type Description Default
individual Individual

The interrupted individual.

required
destination int

The destination node number.

nan
Source code in hciw/node.py
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
def write_interruption_record(self, individual, destination=nan):
    """Write a data record for an individual being interrupted.

    Args:
        individual (Individual): The interrupted individual.
        destination (int): The destination node number.
    """
    server_id = False if self.slotted else individual.server.id_number

    record = DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=individual.arrival_date,
        waiting_time=individual.service_start_date - individual.arrival_date,
        service_start_date=individual.service_start_date,
        service_time=individual.original_service_time,
        service_end_date=nan,
        time_blocked=nan,
        exit_date=self.now,
        destination=destination,
        queue_size_at_arrival=individual.queue_size_at_arrival,
        queue_size_at_departure=individual.queue_size_at_departure,
        server_id=server_id,
        record_type="interrupted service",
    )
    individual.data_records.append(record)

write_reneging_record

write_reneging_record(individual)

Write a data record for an individual when reneging.

Parameters:

Name Type Description Default
individual Individual

The reneging individual.

required
Source code in hciw/node.py
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
def write_reneging_record(self, individual):
    """Write a data record for an individual when reneging.

    Args:
        individual (Individual): The reneging individual.
    """
    record = DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=individual.arrival_date,
        waiting_time=individual.exit_date - individual.arrival_date,
        service_start_date=nan,
        service_time=nan,
        service_end_date=nan,
        time_blocked=nan,
        exit_date=individual.exit_date,
        destination=individual.destination,
        queue_size_at_arrival=individual.queue_size_at_arrival,
        queue_size_at_departure=individual.queue_size_at_departure,
        server_id=nan,
        record_type="renege",
    )
    individual.data_records.append(record)

write_baulking_or_rejection_record

write_baulking_or_rejection_record(individual, record_type)

Write a data record for an individual who baulks or is rejected.

Parameters:

Name Type Description Default
individual Individual

The individual.

required
record_type str

The type of record to write.

required
Source code in hciw/node.py
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
def write_baulking_or_rejection_record(self, individual, record_type):
    """Write a data record for an individual who baulks or is rejected.

    Args:
        individual (Individual): The individual.
        record_type (str): The type of record to write.
    """
    record = DataRecord(
        id_number=individual.id_number,
        customer_class=individual.previous_class,
        original_customer_class=individual.original_class,
        node=self.id_number,
        arrival_date=self.now,
        waiting_time=nan,
        service_start_date=nan,
        service_time=nan,
        service_end_date=nan,
        time_blocked=nan,
        exit_date=self.now,
        destination=nan,
        queue_size_at_arrival=self.number_of_individuals,
        queue_size_at_departure=nan,
        server_id=nan,
        record_type=record_type,
    )
    individual.data_records.append(record)

reset_individual_attributes

reset_individual_attributes(individual)

Reset the attributes of an individual.

Parameters:

Name Type Description Default
individual Individual

The individual to reset.

required
Source code in hciw/node.py
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
def reset_individual_attributes(self, individual):
    """Reset the attributes of an individual.

    Args:
        individual (Individual): The individual to reset.
    """
    individual.arrival_date = False
    individual.service_time = False
    individual.service_start_date = False
    individual.service_end_date = False
    individual.exit_date = False
    individual.queue_size_at_arrival = False
    individual.queue_size_at_departure = False
    individual.destination = False

Server

Server(node, id_number, start_date: float | Decimal = 0.0)

Contain information about a server.

Initialise the server.

Parameters:

Name Type Description Default
node Node

The node the server belongs to.

required
id_number int

Identifier for the server.

required
start_date float | Decimal

The time at which the server started, and whose type sets the precision of the server's accumulated times.

0.0
Source code in hciw/server.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def __init__(self, node, id_number, start_date: float | Decimal = 0.0):
    """Initialise the server.

    Args:
        node (Node): The node the server belongs to.
        id_number (int): Identifier for the server.
        start_date (float | Decimal): The time at which the server started,
            and whose type sets the precision of the server's accumulated
            times.
    """
    self.node = node
    self.id_number = id_number
    self.cust = False
    self.busy = False
    self.offduty = False
    self.all_time = False
    self.start_date = start_date
    zero = type(start_date)()
    self.busy_time = zero
    self.total_time = zero
    self.shift_end = False
    self.next_end_service_date = float("Inf")

utilisation property

utilisation

Get the server utilisation.

Returns:

Type Description
float | Decimal

The proportion of total time the server was busy.

ExactNode

ExactNode(id_, simulation)

Bases: Node

Represent a node with exact numerical precision.

This class inherits from Node and implements a more precise version of addition to fix discrepancies with floating point numbers.

Source code in hciw/node.py
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
def __init__(self, id_, simulation):
    """Initialise a node.

    Args:
        id_ (int): Identifier for the node.
        simulation (object): The simulation to which this node belongs.
    """
    self.simulation = simulation
    node = self.simulation.network.service_centres[id_ - 1]
    self.server_priority_function = node.server_priority_function
    self.service_discipline = node.service_discipline
    self.next_event_type: str | None = None
    self.schedule: Schedule | Slotted | None
    if isinstance(node.number_of_servers, Schedule):
        self.schedule = node.number_of_servers
        self.schedule.initialise()
        self.c: int | float = self.schedule.c
        if isinstance(self.schedule, Slotted):
            self.slotted: bool = True
            self.next_event_date = self.schedule.next_slot_date
            self.next_event_type = "slotted_service"
        else:
            self.slotted = False
            self.next_event_date = self.schedule.next_shift_change_date
            self.next_shift_change = self.schedule.next_shift_change_date
            self.next_event_type = "shift_change"
    else:
        self.c = node.number_of_servers
        self.schedule = None
        self.slotted = False
        self.next_event_date = float("Inf")
        self.next_shift_change = float("Inf")
    self.node_capacity = node.queueing_capacity + self.c
    self.class_change = node.class_change_matrix
    self.individuals = [[] for _ in range(simulation.number_of_priority_classes)]
    self.number_of_individuals = 0
    self.number_in_service = 0
    self.id_number = id_
    self.baulking_functions = {
        clss: self.simulation.network.customer_classes[clss].baulking_functions[
            id_ - 1
        ]
        for clss in self.simulation.network.customer_class_names
    }
    self.overtime = []
    self.blocked_queue = []
    self.len_blocked_queue = 0
    if not isinf(self.c):
        self.servers = self.create_starting_servers()
    self.highest_id = self.c
    self.simulation.deadlock_detector.initialise_at_node(self)
    self.priority_preempt = node.priority_preempt
    self.interrupted_individuals = []
    self.number_interrupted_individuals = 0
    self.all_servers_total = []
    self.all_servers_busy = []
    self.reneging = node.reneging
    self.dynamic_classes = node.class_change_time
    self.next_class_change_date = float("Inf")
    self.next_individual: list[Individual] | Individual | None = None

now property

now: Decimal

Get the current simulation time.

Returns:

Type Description
Decimal

The current simulation time.

create_starting_servers

create_starting_servers() -> list[Server]

Initialise the servers at this node.

Returns:

Type Description
List[Server]

The newly created starting servers.

Source code in hciw/exactnode.py
26
27
28
29
30
31
32
def create_starting_servers(self) -> list[Server]:
    """Initialise the servers at this node.

    Returns:
        List[Server]: The newly created starting servers.
    """
    return [Server(self, i + 1, Decimal("0.0")) for i in range(int(self.c))]

increment_time

increment_time(original, increment) -> Decimal

Increment the original time by the increment.

Parameters:

Name Type Description Default
original float

The original time.

required
increment float

The amount to add.

required

Returns:

Type Description
Decimal

The incremented time.

Source code in hciw/exactnode.py
34
35
36
37
38
39
40
41
42
43
44
def increment_time(self, original, increment) -> Decimal:
    """Increment the original time by the increment.

    Args:
        original (float): The original time.
        increment (float): The amount to add.

    Returns:
        Decimal: The incremented time.
    """
    return Decimal(str(original)) + Decimal(str(increment))

get_service_time

get_service_time(ind) -> Decimal

Return a service time for the given customer class.

Parameters:

Name Type Description Default
ind Individual

The individual being served.

required

Returns:

Type Description
Decimal

The sampled service time.

Source code in hciw/exactnode.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def get_service_time(self, ind) -> Decimal:
    """Return a service time for the given customer class.

    Args:
        ind (Individual): The individual being served.

    Returns:
        Decimal: The sampled service time.
    """
    return Decimal(
        str(
            self.simulation.service_times[self.id_number][
                ind.customer_class
            ]._sample(self.simulation.current_time, ind=ind)
        )
    )

ExactArrivalNode

ExactArrivalNode(simulation)

Bases: ArrivalNode

Represent an arrival node with exact numerical precision.

Inherits from ArrivalNode and implements a more precise version of addition to fix discrepancies with floating point numbers.

Source code in hciw/arrival_node.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def __init__(self, simulation):
    """Initialise the arrival node.

    Args:
        simulation (object): The simulation to which this arrival node
            belongs.
    """
    self.simulation = simulation
    self.number_of_individuals = 0
    self.all_individuals = []
    self.number_of_individuals_per_class = dict.fromkeys(
        self.simulation.network.customer_class_names, 0
    )
    self.number_accepted_individuals = 0
    self.number_accepted_individuals_per_class = dict.fromkeys(
        self.simulation.network.customer_class_names, 0
    )
    self.system_capacity = self.simulation.network.system_capacity
    self.event_dates_dict: dict[int, dict[str, float | bool]] = {
        nd + 1: dict.fromkeys(self.simulation.network.customer_class_names, False)
        for nd in range(self.simulation.network.number_of_nodes)
    }

increment_time

increment_time(original, increment) -> Decimal

Increment the original time by the increment.

Parameters:

Name Type Description Default
original float

The original time.

required
increment float

The amount to add.

required

Returns:

Type Description
Decimal

The incremented time.

Source code in hciw/exactnode.py
71
72
73
74
75
76
77
78
79
80
81
def increment_time(self, original, increment) -> Decimal:
    """Increment the original time by the increment.

    Args:
        original (float): The original time.
        increment (float): The amount to add.

    Returns:
        Decimal: The incremented time.
    """
    return Decimal(str(original)) + Decimal(str(increment))

inter_arrival

inter_arrival(nd, clss) -> Decimal

Sample the inter-arrival time for the next class and node.

Parameters:

Name Type Description Default
nd Node

The next node.

required
clss Individual

The individual class to select next.

required

Returns:

Type Description
Decimal

The sampled inter-arrival time.

Source code in hciw/exactnode.py
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def inter_arrival(self, nd, clss) -> Decimal:
    """Sample the inter-arrival time for the next class and node.

    Args:
        nd (Node): The next node.
        clss (Individual): The individual class to select next.

    Returns:
        Decimal: The sampled inter-arrival time.
    """
    return Decimal(
        str(
            self.simulation.inter_arrival_times[nd][clss]._sample(
                self.simulation.current_time
            )
        )
    )