Skip to content

Innovation

Innovation: technological progress, combinatorial evolution, and the adoption of new products.

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.models.economics.innovation import AdjacentPossibleElement
>>> AdjacentPossibleElement('system', r=2.0).r
2.0
Source code in src/skcomplex/models/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

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.models.economics.innovation import HouseholdElement
>>> GroupElement('households', element=HouseholdElement, size=4, values={'threshold': 1.5}).size
4
Source code in src/skcomplex/models/economics/innovation/_adoption.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
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, model) staticmethod

Return the income grown by a year.

Parameters:

Name Type Description Default
agent HouseholdView

The household.

required
model ModelView

The model view of the growth.

required

Returns:

Name Type Description
income float

The income of the next year.

Source code in src/skcomplex/models/economics/innovation/_adoption.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
@staticmethod
def get_income(agent: HouseholdView, model: ModelView) -> float:
    """Return the income grown by a year.

    Args:
        agent:
            The household.

        model:
            The model view of the growth.

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

get_phone(agent, model) 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
model ModelView

The model 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/models/economics/innovation/_adoption.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
@staticmethod
def get_phone(agent: HouseholdView, model: ModelView) -> 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.

        model:
            The model 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.models.economics.innovation import InventionInteraction
>>> InventionInteraction('invention', technologies='technologies', max_components=6).max_components
6
Source code in src/skcomplex/models/economics/innovation/_invention.py
71
72
73
74
75
76
77
78
79
80
81
82
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(model)

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

Parameters:

Name Type Description Default
model ModelView

The model.

required
Source code in src/skcomplex/models/economics/innovation/_invention.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def invent(self, model: ModelView) -> None:
    """Wire a random circuit and add it to the pool if it computes a wanted function better.

    Args:
        model:
            The model.
    """
    pool = model.agents('technologies', TechnologyView)
    arity, table, cost = self._wire(pool, model)
    if (arity, table) not in GOALS:
        return
    known = [technology for technology in pool if (technology.arity, technology.table) == (arity, table)]
    if not known:
        model.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.elements import GroupElement
>>> from skcomplex.models.economics.innovation import PartElement
>>> GroupElement('components', element=PartElement, size=5, values={'cost': 0.5}).size
5
Source code in src/skcomplex/models/economics/innovation/_part.py
38
39
40
41
42
43
44
45
46
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.models.economics.innovation import RedesignInteraction
>>> RedesignInteraction('redesign', components='components', dependencies=[[1.0]]).dependencies
[[1.0]]
Source code in src/skcomplex/models/economics/innovation/_redesign.py
43
44
45
46
47
48
49
50
51
52
def __init__(
    self,
    label: str,
    *,
    components: Annotated[str, RoleSpec()],
    dependencies: Sequence[Sequence[float]] = (),
) -> None:
    super().__init__(label)
    self.components = components
    self.dependencies = dependencies

redesign(model)

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

Parameters:

Name Type Description Default
model ModelView

The model.

required
Source code in src/skcomplex/models/economics/innovation/_redesign.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def redesign(self, model: ModelView) -> None:
    """Propose new costs for a random component and its dependents and keep them if the total cost falls.

    Args:
        model:
            The model.
    """
    components = sorted(model.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), model.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.models.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/models/economics/innovation/_technology.py
70
71
72
73
74
75
76
77
78
79
80
81
82
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

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 model, with the pool technologies, whose size is the reporter technologies.count.

Examples:

>>> from skcomplex.models.economics.innovation import arthur_polak
>>> arthur_polak().elements[0].check().prepare().size_
3
Source code in src/skcomplex/models/economics/innovation/_technology.py
155
156
157
158
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
@register_model(
    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 model, with the pool `technologies`, whose size is the reporter `technologies.count`.

    Examples:
        >>> from skcomplex.models.economics.innovation import arthur_polak
        >>> arthur_polak().elements[0].check().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 model, 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 model, 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.models.economics.innovation import bass_diffusion
>>> bass_diffusion(market=500.0, adopters=10.0).elements[0].P
490.0
Source code in src/skcomplex/models/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_model(
    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 model, 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 model, 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.models.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],
    )

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 model, with the diversity system.x and the adjacent possible system.d.

Examples:

>>> from skcomplex.models.economics.innovation import creative_phase_transition
>>> creative_phase_transition(r=2.0).elements[0].r
2.0
Source code in src/skcomplex/models/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_model(
    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 model, with the diversity `system.x` and the adjacent possible `system.d`.

    Examples:
        >>> from skcomplex.models.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),
        ],
    )

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

Return McNerney's model 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 model, with the group components and the total cost total_cost.

Examples:

>>> from skcomplex.models.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/models/economics/innovation/_technology.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
@register_model(
    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 model 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 model, with the group `components` and the total cost `total_cost`.

    Examples:
        >>> from skcomplex.models.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 model, with the group households and the share of households with a phone share.

Examples:

>>> from skcomplex.models.economics.innovation import phone_adoption
>>> phone_adoption((10, 10)).elements[0].check().prepare().size_
100
Source code in src/skcomplex/models/economics/innovation/_adoption.py
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
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
@register_model(
    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 model, with the group `households` and the share of households with a phone `share`.

    Examples:
        >>> from skcomplex.models.economics.innovation import phone_adoption
        >>> phone_adoption((10, 10)).elements[0].check().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(),
    )