Skip to content

Watts-May SEIR with lasting partnerships

The SEIR epidemic of Watts and May, where partnerships that last longer spread the infection faster.

  • Name: watts_may_concurrency
  • Field: biology/epidemiology
  • Formalism: ordinary differential equations
  • References: Watts and May 1992; Masuda, Miller, and Holme 2021

Select this system

from skcomplex.systems import create_simulation, create_system, describe

system = create_system('watts_may_concurrency')
entry = describe('watts_may_concurrency')
trajectory = create_simulation('watts_may_concurrency').prepare().run().trajectory_

Factory

@register_system(
    field='biology/epidemiology',
    tags=('concurrency', 'partnerships', 'seir', 'delay', 'hiv'),
    references=('Watts and May 1992', 'Masuda, Miller, and Holme 2021'),
    defaults={'until': 40.0, 'dt': 0.25},
)
def watts_may_concurrency(
    infected: float = 0.01,
    *,
    c: float = 2.0,
    beta: float = 0.2,
    tau: float = 1.0,
    T: float = 0.5,
    nu: float = 0.1,
) -> System:
    """Return the SEIR epidemic of Watts and May, where partnerships that last longer spread the infection faster.

    A susceptible person is infected at the rate `pi(t) = c beta [I / (S + E + I) + int exp(-(t - t') / tau) pi(t' - T)
    S(t' - T) / (S + E + I)(t') dt']`, by a new partner who is infectious or by a current partner who turned infectious
    during the partnership. The mean number of current partners is `c tau`, so a longer partnership raises the mean
    degree of the momentary network, and the infection rate grows with `tau` at fixed `c` and `beta`.

    Args:
        infected:
            The initial fraction of infectious people.

        c:
            The rate of acquiring new partners.

        beta:
            The probability of infection by an infectious partner over a partnership.

        tau:
            The mean duration of a partnership.

        T:
            The duration of the exposed period.

        nu:
            The rate at which the infectious develop the disease.

    Returns:
        system:
            The system, with the fractions `population.S`, `population.E`, `population.I`, `population.R`, and the
            infection memory `population.J`.

    Examples:
        >>> from skcomplex.systems.biology.epidemiology import watts_may_concurrency
        >>> watts_may_concurrency(tau=3.0).elements[0].tau
        3.0
    """
    population = PartnershipEpidemicElement(
        'population',
        S=1 - infected,
        I=infected,
        c=c,
        beta=beta,
        tau=tau,
        T=T,
        nu=nu,
    )
    return System(elements=[population])

Components

Elements:

  • PartnershipEpidemicElement, from Element
class PartnershipEpidemicElement(Element):
    """Population of Watts and May whose infections pass along partnerships that last, with a fixed exposed period.

    Susceptible people `S` acquire new partners at the rate `c`, each infecting with the probability `beta` over the
    partnership, and are infected at the rate `pi = c beta (I / A + J)` with the active population `A = S + E + I`. The
    first term is the infection by a new partner who is infectious, and the memory `J` is the infection by a current
    partner who turned infectious after the partnership formed, `dJ/dt = pi(t - T) S(t - T) / A - J / tau`, where
    partnerships last a mean time `tau`. The exposed `E` turn infectious after the fixed time `T`, and the infectious
    `I` develop the disease, which ends their sexual activity, at the rate `nu`, into `R`. The infection starts at time
    zero, so the infectious and the memory are zero before it.

    Args:
        label:
            The label of the population.

        visual:
            How the element is drawn, or `None` for `default_visual_`.

        S:
            The initial fraction of susceptible people.

        E:
            The initial fraction of exposed people.

        I:
            The initial fraction of infectious people.

        R:
            The initial fraction of people who developed the disease.

        J:
            The initial infection memory of current partnerships.

        c:
            The rate of acquiring new partners.

        beta:
            The probability of infection by an infectious partner over a partnership.

        tau:
            The mean duration of a partnership.

        T:
            The duration of the exposed period.

        nu:
            The rate at which the infectious develop the disease.

    Examples:
        >>> from skcomplex.systems.biology.epidemiology import PartnershipEpidemicElement
        >>> PartnershipEpidemicElement('population').tau
        1.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        S: Annotated[float, StateSpec(domain=FRACTION)] = 0.99,
        E: Annotated[float, StateSpec(domain=FRACTION)] = 0.0,
        I: Annotated[float, StateSpec(domain=FRACTION, history=0.0)] = 0.01,
        R: Annotated[float, StateSpec(domain=FRACTION)] = 0.0,
        J: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo), history=0.0)] = 0.0,
        c: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 2.0,
        beta: Annotated[float, ParameterSpec(domain=FRACTION)] = 0.2,
        tau: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        T: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.5,
        nu: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    ) -> None:
        super().__init__(label, visual=visual)
        self.S = S
        self.E = E
        self.I = I
        self.R = R
        self.J = J
        self.c = c
        self.beta = beta
        self.tau = tau
        self.T = T
        self.nu = nu

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the infection by new and current partners, the exposed period, and the progression.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The laws of the component.
        """
        active = s.S + s.E + s.I
        infection = s.c * s.beta * (s.I / active + s.J) * s.S
        past = [delayed(state, s.T) for state in (s.S, s.E, s.I, s.J)]
        past_active = past[0] + past[1] + past[2]
        matured = s.c * s.beta * (past[2] / past_active + past[3]) * past[0]
        return [
            RateLaw(s.S, -infection),
            RateLaw(s.E, infection - matured),
            RateLaw(s.I, matured - s.nu * s.I),
            RateLaw(s.R, s.nu * s.I),
            RateLaw(s.J, matured / active - s.J / s.tau),
        ]

Equations

population.S: diff(S_population(t), t) == -beta_population*c_population*(I_population(t)/(E_population(t) + I_population(t) + S_population(t)) + J_population(t))*S_population(t)
population.E: diff(E_population(t), t) == -beta_population*c_population*(I_population(-T_population + t)/(E_population(-T_population + t) + I_population(-T_population + t) + S_population(-T_population + t)) + J_population(-T_population + t))*S_population(-T_population + t) + beta_population*c_population*(I_population(t)/(E_population(t) + I_population(t) + S_population(t)) + J_population(t))*S_population(t)
population.I: diff(I_population(t), t) == beta_population*c_population*(I_population(-T_population + t)/(E_population(-T_population + t) + I_population(-T_population + t) + S_population(-T_population + t)) + J_population(-T_population + t))*S_population(-T_population + t) - nu_population*I_population(t)
population.R: diff(R_population(t), t) == nu_population*I_population(t)
population.J: diff(J_population(t), t) == beta_population*c_population*(I_population(-T_population + t)/(E_population(-T_population + t) + I_population(-T_population + t) + S_population(-T_population + t)) + J_population(-T_population + t))*S_population(-T_population + t)/(E_population(t) + I_population(t) + S_population(t)) - J_population(t)/tau_population