State-Altering Tuples

Processes

Throughout the project, processes will refer to tuples that mimick reaction equations. The tuples contain references to compartments involved in a reaction and a rate (or a placeholder for a rate).

There are five allowed processes

# transition process
( source_compartment, rate, target_compartment),

# transmission process
( coupling_compartment_0, coupling_compartment_1, rate, target_compartment_0, target_compartment_1),

# fission process
( source_compartment, rate, target_compartment_0, target_ccompartment_1),

# fusion process
( source_compartment_0, source_compartment_1, rate, target_compartment),

# death process
( source_compartment, rate, None),

# birth process
( None, rate, target_compartment),

epipack identifies the process based on the length of the tuple and the position of the rate (an entry not being a compartment). For SymbolicEpiModel, rates can be equal to compartments, but then processes have to be set with ignore_rate_position_checks. This might change in the future to be default behavior for SymbolicEpiModel.

Death and birth processes are identified by one of the compartments being None. An involved compartment can only be None in birth and death processes.

For a StochasticEpiModel, link transmission processes have a stricter format: (i) they must be processes where one compartment stays constant while the other compartment changes, and (ii) the compartment remaining constant has to be in the first position on each side of the reaction. For instance,

( "I", "S", rate, "I", "I" )

is a valid transition, but ("S", "I", rate, "I", "I") is not. The reasoning behind this restriction is to make sure that the user really thinks about which node is reacting and transmitting.

For StochasticEpiModel, only node transition and link transmission processes are allowed. Additionally, there is the possibility to set conditional link transmission processes that are executed immediately after a base process happened. Base reactions are encoded like this

(source_base, "->", target_base) # or
(transmitting, source, "->", transmitting, target)

Conditional transmission processes are always formulated as dictionaries with lists of processes.

{
    (source_base, "->", target_base) : [
        (target_base, source, "->", target_base, target)
    ]
}

This means that after a node transitions from source_base to target_base, its neighbors will be checked and every neighbor that is of compartment source will be transitioned to compartment target. Note the constancy of target_base in both the base process and the conditional process. This a necessary condition for conditional transmission processes because they can only happen for nodes that change their compartments. We might relax this condition in the future, should it be necessary for a transmitting node to be involved in a conditional process, too.

We can relax the every neighbor condition by introducing probabilities. For instance, in

{
    (source_base, "->", target_base) : [
        (target_base, source, p, target_base, target) # case A
    ]
}

the conditional transition happens for every source-neighbor with probability p. Otherwise, the source-neighbor will remain a source-neighbor.

One may introduce several events for source-neighbors, each with certain probabilities, like

{
    (source_base, "->", target_base) : [
        (target_base, source, p, target_base, target) # case A
        (target_base, source, q, target_base, target) # case B
    ]
}

In this case, epipack adds a "nothing happens"-process (target_base, source, 1-p-q, target_base, target) automatically such that any of the possible processes happens to the source-neighbor (with corresponding probability \(p\), \(q\), or \(1-p-q\)).

Events

Event tuples are used in the default implementations of EpiModels (EpiModel, StochasticEpiModel), because they're flexible enough that we can construct both mean-field ODEs as well as stochastic simulations. Events are defined in a way such that coupling of one or two compartments leads to a change in the overall state by applying a difference vector to the current state as

\[\Delta Y^{(e)} = ( +1, 0, 0, -1, ... ).\]

Hence, for event tuples we need to define

  1. Coupling compartments

  2. A rate value

  3. The state change vector.

We do that as follows:

events = [
    (
        (coupling_compartment_0, coupling_compartment_1,),
        rate_value,
        ( (affected_compartment_0, 1), (affected_compartment_1, -1), ... )
    )
]

For linear events, the first entry of an event tuple will just be a single-element-tuple. All un-mentioned compartments in the event tuple's last entry will be assumed to not change (a zero entry in the state change vector).

For instance, for an SEIR model, we would set

[
    (
        ('E',),
        1/incubation_time,
        ( ('E', -1), ('I', +1) )
    ),
    (
        ('I',),
        1/infectious_period,
        ( ('I', -1), ('R', +1) )
    )
]

For infection events, e.g. in an SEIR model, we would set

[
    (
        ('S','I'),
        infection_rate,
        ( ('S', -1), ('E', +1) )
    ),
]

Rates

Rate tuples are used for constant-rate EpiModels like MatrixEpiModel and SymbolicMatrixEpiModel. Only constant values can be set in MatrixEpiModel, because it makes use of scipy's sparse matrix API which is quite efficient for large systems.

Linear rates look like this:

( source_compartment, affected_compartment, rate_value ).

For instance, for an SEIR model, we would set

[
    ('E', 'E', -1/incubation_time),
    ('E', 'I', +1/incubation_time),
    ('I', 'I', -1/infectious_period),
    ('I', 'R', +1/infectious_period),
]

Quadratic rates look like this:

( coupling_compartment0, coupling_compartment_1, affected_compartment, rate_value ).

E.g. for a model where both asymptomatic infecteds A as well as symptomatic infecteds I could infect susceptibles, we would define

[
    ('I', 'S', 'S', -inf_to_inf_rate),
    ('I', 'S', 'I', +inf_to_inf_rate),
    ('I', 'S', 'S', -inf_to_asymp_rate),
    ('I', 'S', 'A', +inf_to_asymp_rate),
    ('A', 'S', 'S', -asymp_to_asymp_rate),
    ('A', 'S', 'A', +asymp_to_asymp_rate),
    ('A', 'S', 'S', -asymp_to_inf_rate),
    ('A', 'S', 'I', +asymp_to_inf_rate),
]

The reasoning here is that, sometimes, you just want to create a model by copying an existing ODE system. Then, it's easier to directly set the rates instead of converting them to reaction processes in your head.

Node-Based Events

For StochasticEpiModels, processes are converted to node-based events. Here, the algorithm needs to know which events a node can take part in leading the active (transmitting) role or a transitioning role.

Please check out the section on Stochastic Simulations for more info.