Skip to content

Innovation

Innovation: technological progress, combinatorial evolution, discovery, autocatalytic sets, and adoption.

AdjacentPossibleElement(label, visual=None, *, x=0.1, d=0.0, r=5.0)

Bases: Element

Combinatorial system of entities, with the share that exists and the share its new pairs can produce.

Parameters:

Name Type Description Default
label str

The label of the system.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
x Annotated[object, StateSpec(domain=closed_open(0, oo))]

The share of existing entities, the diversity.

0.1
d Annotated[object, StateSpec(domain=closed_open(0, oo))]

The share of new entities, the adjacent possible.

0.0
r Annotated[object, ParameterSpec()]

The rule density, the number of productive pairs per entity.

5.0

Examples:

>>> from skcomplex.systems.economics.innovation import AdjacentPossibleElement
>>> AdjacentPossibleElement('system', r=2.0).r
2.0
Source code in src/skcomplex/systems/economics/innovation/_diversity.py
53
54
55
56
57
58
59
60
61
62
63
64
65
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    x: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
    d: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
    r: Annotated[object, ParameterSpec()] = 5.0,
) -> None:
    super().__init__(label, visual=visual)
    self.x = x
    self.d = d
    self.r = r

BallElement(label, visual=None, *, color=0, count=1, drawn=0)

Bases: 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.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
color Annotated[float, StateSpec(domain=NN)]

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

0
count Annotated[float, StateSpec(domain=NN)]

The number of balls of the colour in the urn.

1
drawn Annotated[float, StateSpec(domain=NN)]

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

0

Examples:

>>> from skcomplex.systems.economics.innovation import BallElement
>>> BallElement('balls', color=3, count=2).count
2
Source code in src/skcomplex/systems/economics/innovation/_discovery.py
58
59
60
61
62
63
64
65
66
67
68
69
70
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

CombinatorialEntityElement(label, visual=VisualSpec('agents', {'color_by': 'exists'}), *, exists=0.0, fitness=0.0)

Bases: Element

Entity of a combinatorial evolution, a species, a good, or a technology, that exists or not.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'exists'})
exists Annotated[float, StateSpec(domain=closed(0, 1))]

1 when the entity exists, 0 otherwise.

0.0
fitness Annotated[float, StateSpec()]

The active productive minus the active destructive sets of the entity at its last update.

0.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.economics.innovation import CombinatorialEntityElement
>>> GroupElement('entities', element=CombinatorialEntityElement, size=3).size
3
Source code in src/skcomplex/systems/economics/innovation/_ccc.py
52
53
54
55
56
57
58
59
60
61
62
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'exists'}),
    *,
    exists: Annotated[float, StateSpec(domain=RealSet.closed(0, 1))] = 0.0,
    fitness: Annotated[float, StateSpec()] = 0.0,
) -> None:
    super().__init__(label, visual=visual)
    self.exists = exists
    self.fitness = fitness

CombinatorialUpdateInteraction(label, *, rule=None, phase=None, entities, production=(), destruction=(), p=0.0002)

Bases: FeedbackInteraction[SystemView]

Random sequential update of the CCC model by the sign of the fitness, with spontaneous flips.

Every step every entity i, in random order, computes its fitness f_i = sum_jk (M+_ijk - M-_ijk) sigma_j sigma_k from the current states, exists when it is positive, disappears when it is negative, and keeps its state when it is zero (Eq. 5.61 of Thurner et al. 2018). Then it switches its state with the probability p.

Parameters:

Name Type Description Default
label str

The label of the update.

required
rule None

Unused, since the update overrides act.

None
phase str | None

The phase the update acts in, or None for the default phase.

None
entities Annotated[str, RoleSpec()]

The path of the group of entities.

required
production tuple[tuple[int, int, int], ...]

The productive sets, triples (i, j, k) of the nonzero entries of M+.

()
destruction tuple[tuple[int, int, int], ...]

The destructive sets, triples (i, j, k) of the nonzero entries of M-.

()
p Annotated[float, ParameterSpec(domain=closed(0, 1))]

The probability per step that an entity switches its state spontaneously.

0.0002

Examples:

>>> from skcomplex.systems.economics.innovation import CombinatorialUpdateInteraction
>>> CombinatorialUpdateInteraction('update', entities='entities').reports
('diversity', 'changes')
Source code in src/skcomplex/systems/economics/innovation/_ccc.py
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def __init__(
    self,
    label: str,
    *,
    rule: None = None,
    phase: str | None = None,
    entities: Annotated[str, RoleSpec()],
    production: tuple[tuple[int, int, int], ...] = (),
    destruction: tuple[tuple[int, int, int], ...] = (),
    p: Annotated[float, ParameterSpec(domain=RealSet.closed(0, 1))] = 0.0002,
) -> None:
    super().__init__(label, rule=rule, phase=phase)
    self.entities = entities
    self.production = production
    self.destruction = destruction
    self.p = p

act(system)

Update every entity once in random order.

Parameters:

Name Type Description Default
system SystemView

The system view of the update.

required
Source code in src/skcomplex/systems/economics/innovation/_ccc.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def act(self, system: SystemView) -> None:
    """Update every entity once in random order.

    Args:
        system:
            The system view of the update.
    """
    entities = sorted(system.agents('entities', CombinatorialEntityView), key=lambda entity: entity.id)
    size, rng = len(entities), system.rng
    states = np.array([entity.exists for entity in entities])
    before = states.copy()
    plus, minus = self._sets(self.production, size), self._sets(self.destruction, size)
    fitness = np.array([entity.fitness for entity in entities])
    flips = rng.random(size) < system.parameter('p')
    for entity in rng.permutation(size):
        gain = states[plus[entity][:, 0]] @ states[plus[entity][:, 1]]
        loss = states[minus[entity][:, 0]] @ states[minus[entity][:, 1]]
        fitness[entity] = gain - loss
        if fitness[entity] != 0:
            states[entity] = float(fitness[entity] > 0)
        if flips[entity]:
            states[entity] = 1 - states[entity]
    for entity, state, value in zip(entities, states, fitness, strict=True):
        entity.exists, entity.fitness = float(state), float(value)
    system.report('diversity', float(states.sum()))
    system.report('changes', float(np.sum(states != before)))

HouseholdElement(label, visual=VisualSpec('agents', {'color_by': 'phone', 'color_range': (0.0, 1.0)}), *, income=1.0, phone=0, social_class=MIDDLE, growth=0.02, threshold=1.2)

Bases: Element

Household whose income grows every year and that gets a phone when it can afford one and a neighbour has one.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

VisualSpec('agents', {'color_by': 'phone', 'color_range': (0.0, 1.0)})
income Annotated[float, StateSpec(domain=closed_open(0, oo))]

The initial income, relative to the mean income at the start.

1.0
phone Annotated[float, StateSpec(domain=NN)]

1 for a household with a phone and 0 otherwise.

0
social_class Annotated[float, StateSpec(domain=NN)]

0 for the upper, 1 for the middle, and 2 for the lower class.

MIDDLE
growth Annotated[float, ParameterSpec()]

The yearly growth rate of the income.

0.02
threshold Annotated[float, ParameterSpec(domain=closed_open(0, oo))]

The income from which a household can afford a phone.

1.2

Examples:

>>> from skcomplex.systems.economics.innovation import HouseholdElement
>>> GroupElement('households', element=HouseholdElement, size=4, values={'threshold': 1.5}).size
4
Source code in src/skcomplex/systems/economics/innovation/_adoption.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'phone', 'color_range': (0.0, 1.0)}),
    *,
    income: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
    phone: Annotated[float, StateSpec(domain=NN)] = 0,
    social_class: Annotated[float, StateSpec(domain=NN)] = MIDDLE,
    growth: Annotated[float, ParameterSpec()] = 0.02,
    threshold: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.2,
) -> None:
    super().__init__(label, visual=visual)
    self.income = income
    self.phone = phone
    self.social_class = social_class
    self.growth = growth
    self.threshold = threshold

get_income(agent, system) staticmethod

Return the income grown by a year.

Parameters:

Name Type Description Default
agent HouseholdView

The household.

required
system SystemView

The system view of the growth.

required

Returns:

Name Type Description
income float

The income of the next year.

Source code in src/skcomplex/systems/economics/innovation/_adoption.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
@staticmethod
def get_income(agent: HouseholdView, system: SystemView) -> float:
    """Return the income grown by a year.

    Args:
        agent:
            The household.

        system:
            The system view of the growth.

    Returns:
        income:
            The income of the next year.
    """
    return agent.income * (1 + agent.growth)

get_phone(agent, system) staticmethod

Return 1 for a household that has a phone or gets one, affordable with a neighbour that has one, else 0.

Parameters:

Name Type Description Default
agent HouseholdView

The household.

required
system SystemView

The system view of the adoption.

required

Returns:

Name Type Description
phone float

1 for a household with a phone and 0 otherwise.

Source code in src/skcomplex/systems/economics/innovation/_adoption.py
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
@staticmethod
def get_phone(agent: HouseholdView, system: SystemView) -> float:
    """Return 1 for a household that has a phone or gets one, affordable with a neighbour that has one, else 0.

    Args:
        agent:
            The household.

        system:
            The system view of the adoption.

    Returns:
        phone:
            1 for a household with a phone and 0 otherwise.
    """
    if agent.phone or agent.income < agent.threshold:
        return float(agent.phone)
    return float(any(isinstance(neighbor, HouseholdView) and neighbor.phone for neighbor in agent.neighbors()))

InventionInteraction(label, *, technologies, max_components=12, max_inputs=3)

Bases: Interaction

Random wiring of technologies of the pool into a circuit, kept if it computes a wanted function more cheaply.

A circuit has between one and max_inputs inputs and wires between two and max_components technologies in a row, each pin to an input or to the output of an earlier technology. The output of the last one is the circuit's output. A circuit that computes a wanted function joins the pool, or replaces the technology of that function when it uses fewer NAND gates.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
technologies Annotated[str, RoleSpec()]

The path of the group of technologies.

required
max_components Annotated[float, ParameterSpec(domain=open(0, oo))]

The largest number of technologies in a circuit.

12
max_inputs Annotated[float, ParameterSpec(domain=open(0, oo))]

The largest number of inputs of a circuit.

3

Examples:

>>> from skcomplex.systems.economics.innovation import InventionInteraction
>>> InventionInteraction('invention', technologies='technologies', max_components=6).max_components
6
Source code in src/skcomplex/systems/economics/innovation/_technology.py
139
140
141
142
143
144
145
146
147
148
149
150
def __init__(
    self,
    label: str,
    *,
    technologies: Annotated[str, RoleSpec()],
    max_components: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 12,
    max_inputs: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 3,
) -> None:
    super().__init__(label)
    self.technologies = technologies
    self.max_components = max_components
    self.max_inputs = max_inputs

invent(system)

Wire a random circuit and add it to the pool if it computes a wanted function better.

Parameters:

Name Type Description Default
system SystemView

The system.

required
Source code in src/skcomplex/systems/economics/innovation/_technology.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
def invent(self, system: SystemView) -> None:
    """Wire a random circuit and add it to the pool if it computes a wanted function better.

    Args:
        system:
            The system.
    """
    pool = system.agents('technologies', TechnologyView)
    arity, table, cost = self._wire(pool, system)
    if (arity, table) not in GOALS:
        return
    known = [technology for technology in pool if (technology.arity, technology.table) == (arity, table)]
    if not known:
        system.create('technologies', arity=arity, table=table, cost=cost)
    elif cost < known[0].cost:
        known[0].cost = cost

PartElement(label, visual=None, *, cost=1.0)

Bases: Element

PartElement of a technology, described by its unit cost.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
cost Annotated[float, StateSpec(domain=closed_open(0, oo))]

The initial unit cost.

1.0

Examples:

>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.economics.innovation import PartElement
>>> GroupElement('components', element=PartElement, size=5, values={'cost': 0.5}).size
5
Source code in src/skcomplex/systems/economics/innovation/_technology.py
59
60
61
62
63
64
65
66
67
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    cost: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
) -> None:
    super().__init__(label, visual=visual)
    self.cost = cost

RedesignInteraction(label, *, components, dependencies=())

Bases: Interaction

Attempt to cut the cost of a technology by redesigning a random component and the components that depend on it.

The redesigned components get new costs drawn uniformly from [0, 1], and the redesign is kept only if the total cost falls. dependencies[i][j] is 1 when a change of component j requires a change of component i.

Parameters:

Name Type Description Default
label str

The label of the interaction.

required
components Annotated[str, RoleSpec()]

The path of the group of components.

required
dependencies Sequence[Sequence[float]]

The design structure matrix.

()

Examples:

>>> from skcomplex.systems.economics.innovation import RedesignInteraction
>>> RedesignInteraction('redesign', components='components', dependencies=[[1.0]]).dependencies
[[1.0]]
Source code in src/skcomplex/systems/economics/innovation/_technology.py
233
234
235
236
237
238
239
240
241
242
def __init__(
    self,
    label: str,
    *,
    components: Annotated[str, RoleSpec()],
    dependencies: Sequence[Sequence[float]] = (),
) -> None:
    super().__init__(label)
    self.components = components
    self.dependencies = dependencies

redesign(system)

Propose new costs for a random component and its dependents and keep them if the total cost falls.

Parameters:

Name Type Description Default
system SystemView

The system.

required
Source code in src/skcomplex/systems/economics/innovation/_technology.py
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
def redesign(self, system: SystemView) -> None:
    """Propose new costs for a random component and its dependents and keep them if the total cost falls.

    Args:
        system:
            The system.
    """
    components = sorted(system.agents('components', PartView), key=lambda component: component.id)
    cost = np.array([component.cost for component in components])
    proposal, changed = RedesignInteraction._components(
        cost,
        np.asarray(self.dependencies, dtype=float),
        system.rng,
    )
    if proposal.sum() >= cost.sum():
        return
    for index in changed:
        components[index].cost = float(proposal[index])

TechnologyElement(label, visual=None, *, arity=2, table=NAND[1], cost=1)

Bases: Element

Logic circuit in the pool of technologies, described by the truth table of the function it computes.

Bit r of the table is the output on the inputs whose bits form r, with the first input as the lowest bit.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
arity Annotated[float, StateSpec(domain=NN)]

The number of inputs.

2
table Annotated[float, StateSpec(domain=NN)]

The truth table.

NAND[1]
cost Annotated[float, StateSpec(domain=NN)]

The number of NAND gates in the circuit.

1

Examples:

>>> from skcomplex.systems.economics.innovation import TechnologyElement
>>> GroupElement(
...     'technologies', element=TechnologyElement, size=3, values={'arity': [2, 0, 0], 'table': [7, 0, 1]}
... ).size
3
Source code in src/skcomplex/systems/economics/innovation/_technology.py
332
333
334
335
336
337
338
339
340
341
342
343
344
def __init__(
    self,
    label: str,
    visual: VisualSpec | None = None,
    *,
    arity: Annotated[float, StateSpec(domain=NN)] = 2,
    table: Annotated[float, StateSpec(domain=NN)] = NAND[1],
    cost: Annotated[float, StateSpec(domain=NN)] = 1,
) -> None:
    super().__init__(label, visual=visual)
    self.arity = arity
    self.table = table
    self.cost = cost

UrnElement(label, visual=None, *, novelties=0, color=-1, novel=0, rho=4, nu=1)

Bases: 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.

Parameters:

Name Type Description Default
label str

The label of the agent or group.

required
visual VisualSpec | None

How the element is drawn, or None for default_visual_.

None
novelties Annotated[float, StateSpec(domain=NN)]

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

0
color Annotated[float, StateSpec()]

The colour of the last draw.

-1
novel Annotated[float, StateSpec(domain=NN)]

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

0
rho Annotated[float, ParameterSpec(domain=NN)]

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

4
nu Annotated[float, ParameterSpec(domain=NN)]

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

1

Examples:

>>> from skcomplex.systems.economics.innovation import UrnElement
>>> UrnElement('urns', rho=4, nu=2).nu
2
Source code in src/skcomplex/systems/economics/innovation/_discovery.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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

draw(agent, system) staticmethod

Draw a ball from the social urn of the node, and record its colour and whether it is a novelty.

Parameters:

Name Type Description Default
agent UrnView

The view of the urn.

required
system SystemView

The view of the system.

required
Source code in src/skcomplex/systems/economics/innovation/_discovery.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
@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))

grow(agent, system) staticmethod

Reinforce the drawn colour in the own urn and, after a novelty, add the colours of the adjacent possible.

Parameters:

Name Type Description Default
agent UrnView

The view of the urn.

required
system SystemView

The view of the system.

required
Source code in src/skcomplex/systems/economics/innovation/_discovery.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
@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

arthur_polak(*, max_components=12, max_inputs=3)

Return Arthur and Polak's evolution of technology, which builds logic functions from NAND gates.

The pool starts with the NAND gate and the constants 0 and 1. Every step wires a random circuit from the pool and keeps it when it computes a wanted function, negation, two-input and, or, exclusive or, nor, equivalence, and implication, or three-input and, or, parity, and majority, so simple inventions become the building blocks of complex ones.

Parameters:

Name Type Description Default
max_components int

The largest number of technologies in a circuit.

12
max_inputs int

The largest number of inputs of a circuit.

3

Returns:

Name Type Description
system System

The system, with the pool technologies, whose size is the reporter technologies.count.

Examples:

>>> from skcomplex.systems.economics.innovation import arthur_polak
>>> arthur_polak().elements[0].prepare().size_
3
Source code in src/skcomplex/systems/economics/innovation/_technology.py
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
@register_system(
    field='economics/innovation',
    tags=('innovation', 'technology', 'combinatorial evolution', 'agents'),
    references=('Thurner et al. 2018', 'Arthur and Polak 2006'),
    defaults={'until': 200},
)
def arthur_polak(*, max_components: int = 12, max_inputs: int = 3) -> System:
    """Return Arthur and Polak's evolution of technology, which builds logic functions from NAND gates.

    The pool starts with the NAND gate and the constants 0 and 1. Every step wires a random circuit from the pool and
    keeps it when it computes a wanted function, negation, two-input and, or, exclusive or, nor, equivalence, and
    implication, or three-input and, or, parity, and majority, so simple inventions become the building blocks of
    complex ones.

    Args:
        max_components:
            The largest number of technologies in a circuit.

        max_inputs:
            The largest number of inputs of a circuit.

    Returns:
        system:
            The system, with the pool `technologies`, whose size is the reporter `technologies.count`.

    Examples:
        >>> from skcomplex.systems.economics.innovation import arthur_polak
        >>> arthur_polak().elements[0].prepare().size_
        3
    """
    primitives = (NAND, FALSE, TRUE)
    technologies = GroupElement(
        'technologies',
        element=TechnologyElement,
        size=len(primitives),
        values={
            'arity': [float(arity) for arity, _ in primitives],
            'table': [float(table) for _, table in primitives],
            'cost': [1.0, 0.0, 0.0],
        },
        variable=True,
    )
    invention = InventionInteraction(
        'invention',
        technologies='technologies',
        max_components=float(max_components),
        max_inputs=float(max_inputs),
    )
    return System(elements=[technologies], interactions=[invention])

bass_diffusion(market=1000.0, *, adopters=0.0, p=0.03, q=0.38)

Return the Bass diffusion system, people who adopt a new product through advertising and word of mouth.

A market of N people splits into potential adopters P and adopters A. Potential adopters adopt at the rate (p + q A / N) P, where the coefficient of innovation p captures adoption from external influence, such as advertising, and the coefficient of imitation q adoption from contact with adopters. People are conserved, so the adoption is a flow from P to A. With q > p the adoption rate rises to a peak at the time log(q / p) / (p + q) and the adopters follow an S-shaped curve. Starting without adopters, the closed form is A(t) = N (1 - exp(-(p + q) t)) / (1 + (q / p) exp(-(p + q) t)). The defaults are the mean coefficients that Sultan, Farley and Lehmann (1990) estimated over many products, with time in years.

Parameters:

Name Type Description Default
market float

The market potential N, the number of people who eventually adopt.

1000.0
adopters float

The initial adopters, a part of the market.

0.0
p float

The coefficient of innovation, per year.

0.03
q float

The coefficient of imitation, per year.

0.38

Returns:

Name Type Description
system System

The system, with the potential adopters market.P, the adopters market.A, and the adoption flow market.adoption in people per year, over years.

Examples:

>>> from skcomplex.systems.economics.innovation import bass_diffusion
>>> bass_diffusion(market=500.0, adopters=10.0).elements[0].P
490.0
Source code in src/skcomplex/systems/economics/innovation/_bass.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
@register_system(
    field='economics/innovation',
    tags=('diffusion', 'marketing', 'stocks and flows', 'system dynamics', 'ode'),
    references=('Bass 1969',),
    defaults={'until': 20.0, 'dt': 0.25},
)
def bass_diffusion(market: float = 1000.0, *, adopters: float = 0.0, p: float = 0.03, q: float = 0.38) -> System:
    """Return the Bass diffusion system, people who adopt a new product through advertising and word of mouth.

    A market of `N` people splits into potential adopters `P` and adopters `A`. Potential adopters adopt at the rate
    `(p + q A / N) P`, where the coefficient of innovation `p` captures adoption from external influence, such as
    advertising, and the coefficient of imitation `q` adoption from contact with adopters. People are conserved, so
    the adoption is a flow from `P` to `A`. With `q > p` the adoption rate rises to a peak at the time
    `log(q / p) / (p + q)` and the adopters follow an S-shaped curve. Starting without adopters, the closed form is
    `A(t) = N (1 - exp(-(p + q) t)) / (1 + (q / p) exp(-(p + q) t))`. The defaults are the mean coefficients that
    Sultan, Farley and Lehmann (1990) estimated over many products, with time in years.

    Args:
        market:
            The market potential `N`, the number of people who eventually adopt.

        adopters:
            The initial adopters, a part of the market.

        p:
            The coefficient of innovation, per year.

        q:
            The coefficient of imitation, per year.

    Returns:
        system:
            The system, with the potential adopters `market.P`, the adopters `market.A`, and the adoption flow
            `market.adoption` in people per year, over years.

    Examples:
        >>> from skcomplex.systems.economics.innovation import bass_diffusion
        >>> bass_diffusion(market=500.0, adopters=10.0).elements[0].P
        490.0
    """
    adoption = TransferInteraction(
        'adoption',
        source='market',
        target='market',
        state='P',
        into='A',
        amount=_adoption,
        flow='adoption',
    )
    return System(
        elements=[BassMarketElement('market', P=market - adopters, A=adopters, p=p, q=q)],
        interactions=[adoption],
    )

ccc_model(n_entities=100, *, r_plus=5.0, r_minus=10.0, p=0.0002, initial=0.5, random_state=0)

Return the co-evolving combinatorial critical model, whose diversity shows punctuated equilibria.

Every entity exists or not. Pairs of existing entities produce other entities through the production tensor M+ and destroy them through the destruction tensor M-, random tensors with on average r+ productive and r- destructive sets per entity, fixed during the run. Every step, in random sequential order, an entity exists when its active productive sets outnumber its active destructive ones, disappears in the opposite case, and keeps its state on a tie, and then switches spontaneously with a small probability p (Thurner et al. 2018, Section 5.6.4). The diversity stays nearly constant for long quasi-equilibria, whose lifetimes and the sizes of the changes between them have approximate power laws, and restructures in bursts. Mostly productive tensors fill the system, mostly destructive ones keep it nearly empty, and the punctuated regime lies in between without fine-tuning. The source leaves the initial state open, so every entity exists at the start with the probability initial. With the tensors of this implementation the setting r+ = 10 and r- = 15 of Fig. 5.14 restructures almost every step, so the defaults take r+ = 5 and r- = 10, which is punctuated.

Parameters:

Name Type Description Default
n_entities int

The number of entities.

100
r_plus float

The mean number of productive sets per entity.

5.0
r_minus float

The mean number of destructive sets per entity.

10.0
p float

The probability per step that an entity switches its state spontaneously.

0.0002
initial float

The probability that an entity exists at the start.

0.5
random_state int | Generator | None

The seed or generator of the tensors and the initial states.

0

Returns:

Name Type Description
system System

The system, with the group entities and the update update, whose reporters are the number of existing entities update.diversity and the entities that changed state in the step update.changes.

Examples:

>>> from skcomplex.systems.economics.innovation import ccc_model
>>> ccc_model(10).elements[0].prepare().size_
10
Source code in src/skcomplex/systems/economics/innovation/_ccc.py
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
@register_system(
    field='economics/innovation',
    tags=('innovation', 'combinatorial evolution', 'punctuated equilibria', 'self-organized criticality', 'agents'),
    references=('Thurner et al. 2018',),
    defaults={'until': 1000},
)
def ccc_model(
    n_entities: int = 100,
    *,
    r_plus: float = 5.0,
    r_minus: float = 10.0,
    p: float = 0.0002,
    initial: float = 0.5,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return the co-evolving combinatorial critical model, whose diversity shows punctuated equilibria.

    Every entity exists or not. Pairs of existing entities produce other entities through the production tensor `M+`
    and destroy them through the destruction tensor `M-`, random tensors with on average `r+` productive and `r-`
    destructive sets per entity, fixed during the run. Every step, in random sequential order, an entity exists when
    its active productive sets outnumber its active destructive ones, disappears in the opposite case, and keeps its
    state on a tie, and then switches spontaneously with a small probability `p` (Thurner et al. 2018, Section 5.6.4).
    The diversity stays nearly constant for long quasi-equilibria, whose lifetimes and the sizes of the changes between
    them have approximate power laws, and restructures in bursts. Mostly productive tensors fill the system, mostly
    destructive ones keep it nearly empty, and the punctuated regime lies in between without fine-tuning. The source
    leaves the initial state open, so every entity exists at the start with the probability `initial`. With the
    tensors of this implementation the setting `r+ = 10` and `r- = 15` of Fig. 5.14 restructures almost every step,
    so the defaults take `r+ = 5` and `r- = 10`, which is punctuated.

    Args:
        n_entities:
            The number of entities.

        r_plus:
            The mean number of productive sets per entity.

        r_minus:
            The mean number of destructive sets per entity.

        p:
            The probability per step that an entity switches its state spontaneously.

        initial:
            The probability that an entity exists at the start.

        random_state:
            The seed or generator of the tensors and the initial states.

    Returns:
        system:
            The system, with the group `entities` and the update `update`, whose reporters are the number of existing
            entities `update.diversity` and the entities that changed state in the step `update.changes`.

    Examples:
        >>> from skcomplex.systems.economics.innovation import ccc_model
        >>> ccc_model(10).elements[0].prepare().size_
        10
    """
    rng = np.random.default_rng(random_state)
    entities = GroupElement(
        'entities',
        element=CombinatorialEntityElement,
        size=n_entities,
        values={'exists': (rng.random(n_entities) < initial).astype(float)},
    )
    update = CombinatorialUpdateInteraction(
        'update',
        entities='entities',
        production=_random_sets(n_entities, r_plus, rng),
        destruction=_random_sets(n_entities, r_minus, rng),
        p=p,
    )
    return System(elements=[entities], interactions=[update])

creative_phase_transition(x=0.1, *, r=5.0)

Return the diversity of a combinatorial system, which jumps from sparse to full above a critical rule density.

The share of existing entities x grows by the share d of new entities that pairs of existing entities can produce, x_{t+1} = x_t + d_t and d_{t+1} = r (1 - x_{t+1}) (x_{t+1}^2 - x_t^2), with the rule density r. The first step produces d_0 = r x_0^2 (1 - x_0), and no step produces more than the entities that do not exist yet. The final diversity changes discontinuously with r and x_0.

Parameters:

Name Type Description Default
x float

The initial share of existing entities.

0.1
r float

The rule density, the number of productive pairs per entity.

5.0

Returns:

Name Type Description
system System

The system, with the diversity system.x and the adjacent possible system.d.

Examples:

>>> from skcomplex.systems.economics.innovation import creative_phase_transition
>>> creative_phase_transition(r=2.0).elements[0].r
2.0
Source code in src/skcomplex/systems/economics/innovation/_diversity.py
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
@register_system(
    field='economics/innovation',
    tags=('innovation', 'combinatorial evolution', 'phase transition', 'discrete map'),
    references=('Thurner et al. 2018', 'Hanel et al. 2005'),
    defaults={'until': 50, 'dt': 1},
)
def creative_phase_transition(x: float = 0.1, *, r: float = 5.0) -> System:
    """Return the diversity of a combinatorial system, which jumps from sparse to full above a critical rule density.

    The share of existing entities `x` grows by the share `d` of new entities that pairs of existing entities can
    produce, `x_{t+1} = x_t + d_t` and `d_{t+1} = r (1 - x_{t+1}) (x_{t+1}^2 - x_t^2)`, with the rule density `r`.
    The first step produces `d_0 = r x_0^2 (1 - x_0)`, and no step produces more than the entities that do not exist
    yet. The final diversity changes discontinuously with `r` and `x_0`.

    Args:
        x:
            The initial share of existing entities.

        r:
            The rule density, the number of productive pairs per entity.

    Returns:
        system:
            The system, with the diversity `system.x` and the adjacent possible `system.d`.

    Examples:
        >>> from skcomplex.systems.economics.innovation import creative_phase_transition
        >>> creative_phase_transition(r=2.0).elements[0].r
        2.0
    """
    return System(
        elements=[AdjacentPossibleElement('system', x=x, d=r * x**2 * (1 - x), r=r)],
        interactions=[
            MapInteraction('combination', target='system', state='x', function=lambda s: s.target.d, mode='increment'),
            MapInteraction('adjacent_possible', target='system', state='d', function=_new_entities),
        ],
    )

interacting_urns_network(graph=None, *, rho=4, nu=1, m0=None)

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').

Parameters:

Name Type Description Default
graph Graph | None

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

None
rho int

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

4
nu int

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

1
m0 int | None

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

None

Returns:

Name Type Description
system 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]
Source code in src/skcomplex/systems/economics/innovation/_discovery.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
@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))),
        ),
    )

nk_technology(n_components=10, d=2, *, random_state=0)

Return McNerney's system of technological improvement, whose cost falls as a power of the cumulative production.

Every step is one unit of production and one attempt to redesign a random component together with the components that depend on it. Redesigns are kept only when they cut the total cost, which falls like Wright's law, more slowly for designs whose components depend on more others.

Parameters:

Name Type Description Default
n_components int

The number of components.

10
d int

The number of components that a redesign of one component changes, itself included.

2
random_state int | Generator | None

The seed or generator of the design structure matrix and the initial costs.

0

Returns:

Name Type Description
system System

The system, with the group components and the total cost total_cost.

Examples:

>>> from skcomplex.systems.economics.innovation import nk_technology
>>> nk_technology(5, 1).interactions[0].dependencies[0]
[1.0, 0.0, 0.0, 0.0, 0.0]
Source code in src/skcomplex/systems/economics/innovation/_technology.py
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
@register_system(
    field='economics/innovation',
    tags=('innovation', 'technology', 'learning curve', 'agents'),
    references=('Thurner et al. 2018', 'McNerney et al. 2011'),
    defaults={'until': 200},
    observables=lambda system: [ObservableSpec('total_cost', group='components', reduce='sum', variable='cost')],
)
def nk_technology(
    n_components: int = 10,
    d: int = 2,
    *,
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return McNerney's system of technological improvement, whose cost falls as a power of the cumulative production.

    Every step is one unit of production and one attempt to redesign a random component together with the components
    that depend on it. Redesigns are kept only when they cut the total cost, which falls like Wright's law, more slowly
    for designs whose components depend on more others.

    Args:
        n_components:
            The number of components.

        d:
            The number of components that a redesign of one component changes, itself included.

        random_state:
            The seed or generator of the design structure matrix and the initial costs.

    Returns:
        system:
            The system, with the group `components` and the total cost `total_cost`.

    Examples:
        >>> from skcomplex.systems.economics.innovation import nk_technology
        >>> nk_technology(5, 1).interactions[0].dependencies[0]
        [1.0, 0.0, 0.0, 0.0, 0.0]
    """
    rng = np.random.default_rng(random_state)
    dependencies = _design_structure(n_components, d, rng)
    components = GroupElement(
        'components',
        element=PartElement,
        size=n_components,
        values={'cost': rng.random(n_components)},
    )
    return System(
        elements=[components],
        interactions=[RedesignInteraction('redesign', components='components', dependencies=dependencies.tolist())],
    )

phone_adoption(shape=(30, 30), *, growth=0.02, threshold=1.2, initial_share=0.1, class_incomes=(1.6, 1.0, 0.6), random_state=0)

Return Hamill and Gilbert's adoption of fixed-line phones by British households from 1951.

One household lives in every cell of a torus. The upper class lives in the top left quadrant, the lower class in the bottom left, and the middle class elsewhere, so neighbours tend to be alike. Incomes are lognormal, scaled by class and normalised to a mean of 1, and grow every year. A household gets a phone once its income reaches the threshold and one of its eight neighbours has a phone. Households decide one after the other in a new random order every year, so phones spread along chains of affording neighbours within a year. The first owners are a cluster of affording households grown from the richest upper-class household.

Parameters:

Name Type Description Default
shape Sequence[int]

The number of columns and rows.

(30, 30)
growth float

The yearly growth rate of incomes.

0.02
threshold float

The income from which a household can afford a phone.

1.2
initial_share float

The share of households with a phone at the start.

0.1
class_incomes Sequence[float]

The income scale of the upper, middle, and lower classes.

(1.6, 1.0, 0.6)
random_state int | Generator | None

The seed or generator of the incomes.

0

Returns:

Name Type Description
system System

The system, with the group households and the share of households with a phone share.

Examples:

>>> from skcomplex.systems.economics.innovation import phone_adoption
>>> phone_adoption((10, 10)).elements[0].prepare().size_
100
Source code in src/skcomplex/systems/economics/innovation/_adoption.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
@register_system(
    field='economics/innovation',
    tags=('diffusion', 'adoption', 'network effect', 'agents'),
    references=('Hamill and Gilbert 2016',),
    defaults={'until': 50},
    observables=lambda system: [ObservableSpec('share', group='households', variable='phone')],
)
def phone_adoption(
    shape: Sequence[int] = (30, 30),
    *,
    growth: float = 0.02,
    threshold: float = 1.2,
    initial_share: float = 0.1,
    class_incomes: Sequence[float] = (1.6, 1.0, 0.6),
    random_state: int | np.random.Generator | None = 0,
) -> System:
    """Return Hamill and Gilbert's adoption of fixed-line phones by British households from 1951.

    One household lives in every cell of a torus. The upper class lives in the top left quadrant, the lower class in
    the bottom left, and the middle class elsewhere, so neighbours tend to be alike. Incomes are lognormal, scaled by
    class and normalised to a mean of 1, and grow every year. A household gets a phone once its income reaches the
    threshold and one of its eight neighbours has a phone. Households decide one after the other in a new random order
    every year, so phones spread along chains of affording neighbours within a year. The first owners are a cluster of
    affording households grown from the richest upper-class household.

    Args:
        shape:
            The number of columns and rows.

        growth:
            The yearly growth rate of incomes.

        threshold:
            The income from which a household can afford a phone.

        initial_share:
            The share of households with a phone at the start.

        class_incomes:
            The income scale of the upper, middle, and lower classes.

        random_state:
            The seed or generator of the incomes.

    Returns:
        system:
            The system, with the group `households` and the share of households with a phone `share`.

    Examples:
        >>> from skcomplex.systems.economics.innovation import phone_adoption
        >>> phone_adoption((10, 10)).elements[0].prepare().size_
        100
    """
    width, height = (int(side) for side in shape)
    rng = np.random.default_rng(random_state)
    classes = _social_classes(width, height)
    scales = np.asarray(class_incomes, dtype=float)[classes.astype(int)]
    incomes = scales * rng.lognormal(0.0, INCOME_SIGMA, len(classes))
    incomes /= incomes.mean()
    owners = _seed_network(incomes, classes, (width, height), threshold, initial_share)
    locations = [(column, row) for column in range(width) for row in range(height)]
    households = GroupElement(
        'households',
        element=HouseholdElement,
        size=len(locations),
        values={'income': incomes, 'phone': owners, 'social_class': classes, 'growth': growth, 'threshold': threshold},
        location=locations,
    )
    growth = MapInteraction(
        'growth',
        target='households',
        state='income',
        function=HouseholdElement.get_income,
        form='agent',
    )
    adoption = MapInteraction(
        'adoption',
        target='households',
        state='phone',
        function=HouseholdElement.get_phone,
        schedule='random-sequential',
        form='agent',
    )
    return System(
        elements=[households],
        interactions=[growth, adoption],
        space=LatticeSpace((width, height), boundary='periodic'),
        time=DiscreteTime(),
    )

tap_catalysis(m0=10, *, parents=4, alpha=0.01, goods=1250, pc=0.0044)

Return the TAP growth of goods from combinations of goods, with random catalysis of their production functions.

The TAP model with catalysis of Hordijk and Kauffman (2022, Algorithm 1), in continuous time. From m0 goods, new goods made from i parents drawn uniformly among the M goods appear at the rate alpha^i C(M, i), for every i up to parents, until there are goods goods. The production function of a new good is a hyperedge of its parents and the good, its last member. A new good catalyses the production of every other good, and every other good catalyses its production, each with the probability pc, as directed edges from the catalyst to the good. Algorithm 1 draws a Poisson number of goods of every order per step from the rates at its start, and the process here draws them one at a time from the rates at that time. The initial goods are the food set, and AutocatalyticSetAnalysis finds the largest autocatalytic set of the production functions, which exists in about half the runs at pc = 0.0044 and then tends to hold most of the network.

Parameters:

Name Type Description Default
m0 int

The number of initial goods, the food set.

10
parents int

The largest number of parents of a good, K.

4
alpha float

The base of the probabilities alpha^i that a combination of i goods makes a new one.

0.01
goods int

The number of goods at which the growth stops, M.

1250
pc float

The catalysis probability.

0.0044

Returns:

Name Type Description
system System

The system, with the goods goods, the catalysis catalysis, and the production functions functions.

Examples:

>>> from skcomplex.systems.economics.innovation import tap_catalysis
>>> [element.label for element in tap_catalysis(goods=50).elements]
['goods', 'catalysis', 'functions']
Source code in src/skcomplex/systems/economics/innovation/_catalysis.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
@register_system(
    field='economics/innovation',
    tags=('innovation', 'adjacent possible', 'autocatalytic sets', 'hypergraph', 'jump process'),
    references=('Hordijk and Kauffman 2022',),
    defaults={'until': 400},
)
def tap_catalysis(
    m0: int = 10,
    *,
    parents: int = 4,
    alpha: float = 0.01,
    goods: int = 1250,
    pc: float = 0.0044,
) -> System:
    """Return the TAP growth of goods from combinations of goods, with random catalysis of their production functions.

    The TAP model with catalysis of Hordijk and Kauffman (2022, Algorithm 1), in continuous time. From `m0` goods,
    new goods made from `i` parents drawn uniformly among the `M` goods appear at the rate `alpha^i C(M, i)`, for
    every `i` up to `parents`, until there are `goods` goods. The production function of a new good is a hyperedge of
    its parents and the good, its last member. A new good catalyses the production of every other good, and every
    other good catalyses its production, each with the probability `pc`, as directed edges from the catalyst to the
    good. Algorithm 1 draws a Poisson number of goods of every order per step from the rates at its start, and the
    process here draws them one at a time from the rates at that time. The initial goods are the food set, and
    `AutocatalyticSetAnalysis` finds the largest autocatalytic set of the production functions, which exists in about
    half the runs at `pc = 0.0044` and then tends to hold most of the network.

    Args:
        m0:
            The number of initial goods, the food set.

        parents:
            The largest number of parents of a good, `K`.

        alpha:
            The base of the probabilities `alpha^i` that a combination of `i` goods makes a new one.

        goods:
            The number of goods at which the growth stops, `M`.

        pc:
            The catalysis probability.

    Returns:
        system:
            The system, with the goods `goods`, the catalysis `catalysis`, and the production functions `functions`.

    Examples:
        >>> from skcomplex.systems.economics.innovation import tap_catalysis
        >>> [element.label for element in tap_catalysis(goods=50).elements]
        ['goods', 'catalysis', 'functions']
    """
    growth = [
        TopologyInteraction(
            f'order_{order}',
            target='goods',
            action='add-node',
            rate=_combination_rate(order, alpha, goods),
            hyperedge=order + 1,
            independent=True,
            direction='both',
            toward=lambda node: pc,
        )
        for order in range(1, parents + 1)
    ]
    links = int(3 * pc * goods * goods) + goods
    return System(
        elements=[
            GroupElement('goods', element=Element, on='sites', variable=True, capacity=goods),
            GroupElement('catalysis', element=Element, on='edges', variable=True, capacity=links),
            GroupElement(
                'functions',
                element=Element,
                on='hyperedges',
                variable=True,
                size=0,
                capacity=goods,
                rank=parents + 1,
            ),
        ],
        interactions=growth,
        space=NetworkSpace(nx.empty_graph(m0, create_using=nx.DiGraph)),
    )