NK technology model
McNerney's model of technological improvement, whose cost falls as a power of the cumulative production.
- Name:
nk_technology - Field:
economics/innovation - Formalism: agent-based model
- References: Thurner et al. 2018; McNerney et al. 2011
Factory
@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())],
)
Components
class PartElement(Element):
"""PartElement of a technology, described by its unit cost.
Args:
label:
The label of the agent or group.
visual:
How the element is drawn, or `None` for `default_visual_`.
cost:
The initial unit cost.
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
"""
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
@classmethod
def _get_view(cls) -> type[PartView]:
"""Return the view of the agents of the element."""
return PartView
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return no laws, since redesigns change several components at once.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
return []
class RedesignInteraction(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`.
Args:
label:
The label of the interaction.
components:
The path of the group of components.
dependencies:
The design structure matrix.
Examples:
>>> from skcomplex.models.economics.innovation import RedesignInteraction
>>> RedesignInteraction('redesign', components='components', dependencies=[[1.0]]).dependencies
[[1.0]]
"""
def __init__(
self,
label: str,
*,
components: Annotated[str, RoleSpec()],
dependencies: Sequence[Sequence[float]] = (),
) -> None:
super().__init__(label)
self.components = components
self.dependencies = dependencies
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the redesign rule.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
return [RuleLaw(self.redesign)]
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])
@staticmethod
def _components(cost: Array, dependencies: Array, rng: np.random.Generator) -> tuple[Array, npt.NDArray[np.intp]]:
"""Return the proposed costs of a redesign of a random component and of the components that depend on it."""
changed = np.flatnonzero(dependencies[:, int(rng.integers(len(cost)))])
proposal = cost.copy()
proposal[changed] = rng.random(len(changed))
return proposal, changed