Skip to content

Watts threshold cascades

The Watts threshold model of global cascades started by a few random seeds.

  • Name: watts_cascades
  • Field: social/spreading
  • Formalism: agent-based model
  • References: Watts 2002; Pastor-Satorras et al. 2015

Factory

@register_model(
    field='social/spreading',
    tags=('cascades', 'agents', 'network', 'threshold'),
    references=('Watts 2002', 'Pastor-Satorras et al. 2015'),
    defaults={'until': 30},
)
def watts_cascades(
    graph: object = None,
    *,
    threshold: float = 0.18,
    seeds: int = 1,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the Watts threshold model of global cascades started by a few random seeds.

    Args:
        graph:
            The NetworkX graph, or `None` for a random graph of 200 nodes with mean degree 4.

        threshold:
            The share of active neighbours at which a node becomes active.

        seeds:
            The number of nodes active at the start.

        random_state:
            The seed or generator of the default graph and of the seeds.

    Returns:
        system:
            The model, with the group `nodes`.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.models.social.spreading import watts_cascades
        >>> watts_cascades(nx.cycle_graph(10), random_state=0).elements[0].check().prepare().size_
        10
    """
    rng = np.random.default_rng(random_state)
    graph = nx.gnp_random_graph(200, 4 / 199, seed=int(rng.integers(2**31))) if graph is None else graph
    size = nx.number_of_nodes(graph)
    active = np.zeros(size)
    active[rng.choice(size, seeds, replace=False)] = 1.0
    nodes = GroupElement(
        'nodes',
        element=ThresholdNodeElement,
        size=size,
        values={'active': active, 'threshold': threshold},
    )
    return System(
        elements=[nodes],
        interactions=[ThresholdActivationInteraction('activation', target='nodes')],
        space=NetworkSpace(graph),
    )

Components

class ThresholdNodeElement(Element):
    """Node of the Watts cascade model, which the activation turns active once enough of its neighbours are active.

    Args:
        label:
            The label of the agent or group.

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

        active:
            Whether the node starts active.

        threshold:
            The share of active neighbours at which the node becomes active.

    Examples:
        >>> from skcomplex.models.social.spreading import ThresholdNodeElement
        >>> group = GroupElement('nodes', element=ThresholdNodeElement, size=10, values={'threshold': 0.18})
        >>> group.label, group.element.__name__, group.size
        ('nodes', 'ThresholdNodeElement', 10)
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'active', 'color_range': (0.0, 1.0)}),
        *,
        active: Annotated[float, StateSpec(domain=FiniteEnumeratedSet([0, 1]))] = 0.0,
        threshold: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.18,
    ) -> None:
        super().__init__(label, visual=visual)
        self.active = active
        self.threshold = threshold

    @classmethod
    def _get_view(cls) -> type[ThresholdNodeView]:
        """Return the class of the views of the agents, whose attributes are the states and parameters."""
        return ThresholdNodeView

class ThresholdActivationInteraction(MapInteraction):
    """Activation of the Watts cascade model, where a node becomes active once enough of its neighbours are active.

    A node becomes active for good once the share of active nodes among its neighbouring nodes reaches its threshold.
    The nodes update one after the other in a new random order every step, each reading the activations the nodes
    before it set, so a cascade can cross several nodes in one step.

    Args:
        label:
            The label of the interaction.

        target:
            The path of the group of nodes.

    Examples:
        >>> from skcomplex.models.social.spreading import ThresholdActivationInteraction
        >>> ThresholdActivationInteraction('activation', target='nodes').check().state
        'active'
    """

    def __init__(self, label: str, *, target: Annotated[str, RoleSpec()]) -> None:
        super().__init__(
            label,
            target=target,
            state='active',
            function=self._get_active,
            schedule='random-sequential',
            form='agent',
        )

    def _get_active(self, agent: ThresholdNodeView, model: ModelViewProtocol) -> float:
        """Return 1 for a node that is or becomes active, and 0 for one that stays inactive."""
        del model
        neighbors = [item for item in agent.neighbors() if isinstance(item, ThresholdNodeView)]
        if agent.active or not neighbors:
            return float(agent.active)
        return float(sum(neighbor.active for neighbor in neighbors) / len(neighbors) >= agent.threshold)