Skip to content

Interacting urns with triggering on a network

One urn with triggering per node of a network, which draws from its own urn and those of its neighbours.

  • Name: interacting_urns_network
  • Field: economics/innovation
  • Formalism: agent-based system
  • References: Iacopini et al. 2020

Select this system

from skcomplex.systems import create_simulation, create_system, describe

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

Factory

@register_system(
    field='economics/innovation',
    tags=('innovation', 'adjacent possible', 'urn', 'network', 'agents', 'heaps law'),
    references=('Iacopini et al. 2020',),
    defaults={'until': 100},
)
def interacting_urns_network(
    graph: nx.Graph | None = None,
    *,
    rho: int = 4,
    nu: int = 1,
    m0: int | None = None,
) -> System:
    """Return one urn with triggering per node of a network, which draws from its own urn and those of its neighbours.

    The discovery process of Iacopini et al. (2020, Eqs. 1 and 2). Every node holds an urn of `m0` balls of colours
    that no other urn holds. Every step every node draws one ball from its social urn, its own urn and one ball of every
    colour of the urns of its neighbours, all from the urns at the start of the step. It then adds `rho` balls of the
    drawn colour to its own urn, and when it draws a colour for the first time, a novelty, `nu + 1` balls of colours
    that no urn held before. The number of novelties of a node grows as Heaps' law `D_i(t) ~ t^beta_i`, with
    `beta = nu / rho` for an isolated urn and higher exponents for nodes whose neighbours enlarge their social urns.

    The urn of a node is the set of ball agents of the variable group `balls` on its node, one agent per colour with
    its multiplicity, which the node reads with `agents_here` and its neighbours with `neighbors(group='balls')`.

    Args:
        graph:
            The NetworkX graph of the explorers, or `None` for Zachary's karate club.

        rho:
            The reinforcement, the balls of the drawn colour added after every draw.

        nu:
            The triggering, one less than the colours added after a novelty.

        m0:
            The initial number of balls of every urn, or `None` for `nu + 1`.

    Returns:
        system:
            The system, with the urns `urns` and their novelties `urns.novelties`, and the balls `balls`.

    Examples:
        >>> import networkx as nx
        >>> from skcomplex.systems.economics.innovation import interacting_urns_network
        >>> system = interacting_urns_network(nx.path_graph(3), rho=2, nu=1)
        >>> [element.prepare().size_ for element in system.elements]
        [3, 6]
    """
    graph = nx.karate_club_graph() if graph is None else graph
    nodes = list(graph.nodes)
    initial = nu + 1 if m0 is None else m0
    places = [node for node in nodes for _ in range(initial)]
    colors = list(range(len(places)))
    return System(
        elements=[
            GroupElement('urns', element=UrnElement, size=len(nodes), location=nodes, values={'rho': rho, 'nu': nu}),
            GroupElement(
                'balls',
                element=BallElement,
                size=len(places),
                variable=True,
                location=places,
                values={'color': colors},
            ),
        ],
        space=NetworkSpace(graph),
        time=DiscreteTime(
            schedule=PhasesScheduleSpec((PhaseSpec(DRAW, activation='simultaneous'), PhaseSpec(GROW))),
        ),
    )

Components

Elements:

  • UrnElement, from Element
  • BallElement, from Element
class UrnElement(Element):
    """Urn with triggering on a node of a network, which draws from its own balls and those of its neighbours.

    The urn of the interacting discovery processes of Iacopini et al. (2020). The urn of a node is the set of ball
    agents on its node. Every step the node draws one ball from its social urn, its own balls with their multiplicities
    and one ball of every colour in the urns of its neighbours, in the phase `'draw'`. In the phase `'grow'` it adds
    `rho` balls of the drawn colour to its own urn, and when it draws the colour for the first time, a novelty, it adds
    one ball of each of `nu + 1` colours that no urn held before, the adjacent possible.

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

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

        novelties:
            The number of distinct colours the node has drawn, `D_i(t)`.

        color:
            The colour of the last draw.

        novel:
            1 when the last draw was a novelty, and 0 when it was not.

        rho:
            The reinforcement, the balls of the drawn colour added after every draw.

        nu:
            The triggering, one less than the colours added after a novelty.

    Examples:
        >>> from skcomplex.systems.economics.innovation import UrnElement
        >>> UrnElement('urns', rho=4, nu=2).nu
        2
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        novelties: Annotated[float, StateSpec(domain=NN)] = 0,
        color: Annotated[float, StateSpec()] = -1,
        novel: Annotated[float, StateSpec(domain=NN)] = 0,
        rho: Annotated[float, ParameterSpec(domain=NN)] = 4,
        nu: Annotated[float, ParameterSpec(domain=NN)] = 1,
    ) -> None:
        super().__init__(label, visual=visual)
        self.novelties = novelties
        self.color = color
        self.novel = novel
        self.rho = rho
        self.nu = nu

    @classmethod
    def _get_view(cls) -> type[UrnView]:
        """Return the view of the agents of the element."""
        return UrnView

    def _get_laws(self, namespace: SimpleNamespace) -> list[BaseLaw]:
        """Return the draw from the social urn and the growth of the own urn, in their phases.

        Args:
            namespace:
                The namespace of the symbols of the element.

        Returns:
            laws:
                The rules of the two phases.
        """
        return [RuleLaw(self.draw, phase=DRAW), RuleLaw(self.grow, phase=GROW)]

    @staticmethod
    def draw(agent: UrnView, system: SystemView) -> None:
        """Draw a ball from the social urn of the node, and record its colour and whether it is a novelty.

        Args:
            agent:
                The view of the urn.

            system:
                The view of the system.
        """
        own = [ball for ball in agent.agents_here(BALLS) if isinstance(ball, BallView)]
        social = [ball for ball in agent.neighbors(group=BALLS) if isinstance(ball, BallView)]
        colors = [ball.color for ball in own] + [ball.color for ball in social]
        weights = np.asarray([ball.count for ball in own] + [1.0] * len(social), dtype=float)
        if not colors:
            return
        chosen = colors[int(system.rng.choice(len(colors), p=weights / weights.sum()))]
        agent.color = chosen
        agent.novel = float(not any(ball.color == chosen and ball.drawn for ball in own))

    @staticmethod
    def grow(agent: UrnView, system: SystemView) -> None:
        """Reinforce the drawn colour in the own urn and, after a novelty, add the colours of the adjacent possible.

        Args:
            agent:
                The view of the urn.

            system:
                The view of the system.
        """
        if agent.color < 0:
            return
        rho, nu = system.parameter('rho'), system.parameter('nu')
        own = [ball for ball in agent.agents_here(BALLS) if isinstance(ball, BallView)]
        ball = next((ball for ball in own if ball.color == agent.color), None)
        if ball is None:
            agent.create(BALLS, color=agent.color, count=rho, drawn=1)
        else:
            ball.count += rho
            ball.drawn = 1
        if not agent.novel:
            return
        agent.novelties += 1
        stored = system.storage.get('colors')
        first = (
            stored if isinstance(stored, int) else int(max(item.color for item in system.agents(BALLS, BallView))) + 1
        )
        for color in range(first, first + int(nu) + 1):
            agent.create(BALLS, color=color, count=1, drawn=0)
        system.storage['colors'] = first + int(nu) + 1

class BallElement(Element):
    """Balls of one colour in the urn of a node, with how many there are and whether the node has drawn the colour.

    A ball agent stands for every ball of its colour in the urn of the node it sits on, so the set of ball agents on a
    node is the set of colours of its urn, and their multiplicities make the urn a multiset.

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

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

        color:
            The colour, a number shared by the balls of that colour in every urn.

        count:
            The number of balls of the colour in the urn.

        drawn:
            1 when the node has drawn the colour, and 0 when it has not.

    Examples:
        >>> from skcomplex.systems.economics.innovation import BallElement
        >>> BallElement('balls', color=3, count=2).count
        2
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        color: Annotated[float, StateSpec(domain=NN)] = 0,
        count: Annotated[float, StateSpec(domain=NN)] = 1,
        drawn: Annotated[float, StateSpec(domain=NN)] = 0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.color = color
        self.count = count
        self.drawn = drawn

    @classmethod
    def _get_view(cls) -> type[BallView]:
        """Return the view of the agents of the element."""
        return BallView