Skip to content

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