Skip to content

EpiSims city of people and locations

A synthetic city in the spirit of EpiSims, where people infect those they meet at home and at work.

  • Name: episims_city
  • Field: biology/epidemiology
  • Formalism: agent-based system
  • References: Eubank et al. 2004; Raman 2021

Select this system

from skcomplex.systems import create_simulation, create_system, describe

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

Factory

@register_system(
    field='biology/epidemiology',
    tags=('agents', 'contact network', 'bipartite', 'commuting', 'episims'),
    references=('Eubank et al. 2004', 'Raman 2021'),
    defaults={'until': 200},
)
def episims_city(
    people: int = 1000,
    *,
    household: float = 3.0,
    workplace: float = 20.0,
    p: float = 0.05,
    gamma: float = 0.1,
    infected: int = 5,
    closed: bool = False,
    seed: int | None = 0,
) -> System:
    """Return a synthetic city in the spirit of EpiSims, where people infect those they meet at home and at work.

    EpiSims models a city as a bipartite graph of people and locations whose edges are visits labelled with their
    times, and spreads disease between people at the same location. Here every person belongs to a household of mean
    size `household` and a workplace or school of mean size `workplace`, the people sleep at home and work by day,
    and the disease spreads by SIR contacts at every location. The locations form a network whose edges join a home to
    the workplaces of its members, the projection of the bipartite graph onto the locations. Households spread the
    disease within families and workplaces between them, so closing the workplaces, one of the interventions EpiSims
    was built to compare, confines outbreaks to households.

    Args:
        people:
            The number of people.

        household:
            The mean size of a household.

        workplace:
            The mean size of a workplace or school.

        p:
            The probability of transmission per infected contact and half day.

        gamma:
            The probability of recovery per half day.

        infected:
            The initial number of infected people.

        closed:
            Whether the workplaces are closed.

        seed:
            The seed of the households, the workplaces, and the initial infections.

    Returns:
        system:
            The system, with the homes, workplaces, and health states of the agent group `people` on the locations.

    Examples:
        >>> from skcomplex.systems.biology.epidemiology import episims_city
        >>> episims_city(100).elements[0].size
        100
    """
    rng = np.random.default_rng(seed)
    homes = rng.integers(0, max(1, round(people / household)), people)
    n_homes = int(homes.max()) + 1
    works = n_homes + rng.integers(0, max(1, round(people / workplace)), people)
    graph = nx.Graph()
    graph.add_nodes_from(range(int(works.max()) + 1))
    graph.add_edges_from(zip(homes.tolist(), works.tolist(), strict=True))
    health = np.zeros(people)
    health[rng.choice(people, size=infected, replace=False)] = 1.0
    group = GroupElement(
        'people',
        element=CommuterElement,
        size=people,
        values={'home': homes.astype(float), 'work': works.astype(float), 'health': health},
        location=homes.tolist(),
    )
    contacts = ColocationInteraction('contacts', people='people', p=p, gamma=gamma, closed=closed)
    return System(elements=[group], interactions=[contacts], space=NetworkSpace(graph), time=DiscreteTime())

Components

Elements:

  • CommuterElement, from Element

Interactions:

  • ColocationInteraction, from Interaction
class CommuterElement(Element):
    """Person of a synthetic city who sleeps at home and spends the day at a workplace, with an SIR health state.

    The home and the workplace are the locations the person visits, the edges of the person in the bipartite graph of
    people and locations. Health is 0 susceptible, 1 infected, or 2 recovered.

    Args:
        label:
            The label of the person or of the group of people.

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

        home:
            The location of the home.

        work:
            The location of the workplace or school.

        health:
            The health state.

    Examples:
        >>> from skcomplex.systems.biology.epidemiology import CommuterElement
        >>> CommuterElement('person', home=3.0, work=12.0).work
        12.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'health', 'color_range': (0.0, 2.0)}),
        *,
        home: Annotated[float, StateSpec(domain=NN)] = 0.0,
        work: Annotated[float, StateSpec(domain=NN)] = 0.0,
        health: Annotated[float, StateSpec(domain=NN)] = 0.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.home = home
        self.work = work
        self.health = health

    @classmethod
    def _get_view(cls) -> type[CommuterView]:
        """Return the view of a commuter, `CommuterView`."""
        return CommuterView

class ColocationInteraction(Interaction):
    """Daily movement of people between their homes and workplaces, and infection between people at one location.

    A step is half a day: in even steps every person is at home and in odd steps at the workplace, unless workplaces
    are closed, the time-labelled visits of the bipartite graph of people and locations of EpiSims. At every step a
    susceptible person is infected with the probability `1 - (1 - p)^k` by the `k` infected people at the same location,
    and an infected person recovers with the probability `gamma`.

    Args:
        label:
            The label of the interaction.

        people:
            The path of the agent group of people.

        p:
            The probability of transmission per infected contact and step.

        gamma:
            The probability of recovery per step.

        closed:
            Whether the workplaces are closed, so people stay at home.

    Examples:
        >>> from skcomplex.systems.biology.epidemiology import ColocationInteraction
        >>> ColocationInteraction('contacts', people='people', p=0.02).p
        0.02
    """

    def __init__(
        self,
        label: str,
        *,
        people: Annotated[str, RoleSpec()],
        p: float = 0.05,
        gamma: float = 0.1,
        closed: bool = False,
    ) -> None:
        super().__init__(label)
        self.people = people
        self.p = p
        self.gamma = gamma
        self.closed = closed

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the rule of a half day.

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

        Returns:
            laws:
                The laws of the component.
        """
        return [RuleLaw(self.visit)]

    def visit(self, system: SystemView) -> None:
        """Move every person to the location of the half day, then infect and recover.

        Args:
            system:
                The system.
        """
        people = system.agents(self.people, CommuterView)
        at_work = round(system.time) % 2 == 1 and not self.closed
        places = np.array([int(person.work if at_work else person.home) for person in people])
        health = np.array([person.health for person in people])
        infected = np.bincount(places[health == INFECTED], minlength=places.max() + 1)[places]
        draws = system.rng.random(len(people))
        infection = (health == SUSCEPTIBLE) & (draws < 1 - (1 - self.p) ** infected)
        recovery = (health == INFECTED) & (draws < self.gamma)
        health = np.where(infection, INFECTED, np.where(recovery, RECOVERED, health))
        for person, place, state in zip(people, places, health, strict=True):
            person.move_to(int(place))
            person.health = float(state)