Skip to content

Three-sector purchasing power decoupled by feedback

Forrest's categorized purchasing power of money, three sectors that feedback can steer one by one.

  • Name: categorized_purchasing_power
  • Field: economics/macroeconomics
  • Formalism: ordinary differential equations
  • References: Forrest 2018

Select this system

from skcomplex.systems import create_simulation, create_system, describe

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

Factory

@register_system(
    field='economics/macroeconomics',
    tags=('purchasing power', 'monetary policy', 'fiscal policy', 'decoupling', 'feedback', 'linear system', 'ode'),
    references=('Forrest 2018',),
    defaults={'until': 15.0, 'dt': 0.05},
    observables=_observables,
)
def categorized_purchasing_power(
    policy: Sequence[float] = (0.0, 0.0, 1.0),
    policy_time: Sequence[float] = (1.0, 1.0, 1.0),
    *,
    feedback: bool = True,
    A: Sequence[Sequence[float]] = A_MATRIX,
    B: Sequence[Sequence[float]] = B_MATRIX,
    C: Sequence[Sequence[float]] = C_MATRIX,
    D: Sequence[Sequence[float]] = D_MATRIX,
) -> System:
    """Return Forrest's categorized purchasing power of money, three sectors that feedback can steer one by one.

    The economy is split into the sectors of the basic, the desired, and the luxurious living conditions, with the
    gaps `z_i = D_i - S_i` between the demand and the supply of money of each, which the categorized monetary and
    fiscal policies `x_i` move through `z' = A z + B x`, and the indices of the sectors `y = C z + D x`, from the zero
    state (Forrest 2018, Eqs. 22.6 and 22.7). The policies of one sector act on all of them. By Theorem 11.3 and Example
    11.2 the system can be decoupled by feedback into the systems `w' = A w + B_i x_i` and `y_i = C_i w + D_i x_i` of
    the sectors (Eqs. 22.9 to 22.12), whose market reactions keep each index on its own policy, so money poured into the
    luxury sector leaves the basic sector alone. The source gives no numbers, so the matrices are illustrative, with a
    stable `A` and a stable `A - B D^-1 C`, and the policies are steps.

    Args:
        policy:
            The size of the step policy of every sector.

        policy_time:
            The time of the step policy of every sector.

        feedback:
            Whether the feedback component decouples the sectors.

        A:
            The matrix of the state.

        B:
            The matrix of the policies in the state.

        C:
            The matrix of the state in the indices.

        D:
            The nonsingular matrix of the policies in the indices.

    Returns:
        system:
            The system, with the gaps `economy.z1` to `economy.z3` and, with feedback, the decoupled states
            `economy.w11` to `economy.w33`. The observables of the system are the indices `economy.y1` to
            `economy.y3` and the inputs of the economy `economy.u1` to `economy.u3`.

    Examples:
        >>> from skcomplex.systems.economics.macroeconomics import categorized_purchasing_power
        >>> categorized_purchasing_power(feedback=False).elements[0].feedback
        False
    """
    variables = {
        name: DeclaredVariableSpec(float(value), ParameterSpec())
        for i in range(SECTORS)
        for name, value in ((f'policy{i + 1}', policy[i]), (f'policy_time{i + 1}', policy_time[i]))
    }
    economy = PurchasingPowerElement('economy', variables=variables, A=A, B=B, C=C, D=D, feedback=feedback)
    return System(elements=[economy])

Components

Elements:

  • PurchasingPowerElement, from DeclaredElement
class PurchasingPowerElement(DeclaredElement):
    """National economy of three spending sectors, `z' = A z + B u` and `y = C z + D u`, with sector policies.

    The state `z_i` is the gap between the demand and the supply of money of sector `i`, the basic, the desired, and
    the luxurious living conditions, the output `y_i` is the index of the sector, and the policy `x_i` of a sector is a
    step of size `policy_i` at the time `policy_time_i`. Without feedback the economy takes the policies as its inputs,
    `u = x`. With feedback, the market reactions of Forrest's feedback component set `u` so that every sector responds
    to its own policy only, as the decoupled systems `w_i' = A w_i + B_i x_i` and `y_i = C_i w_i + D_i x_i`, whose
    states `w_ik` the element holds. The feedback component holds a copy of `z` that equals `z` from the common zero
    start, so `u = D^-1 (y - C z)` with the decoupled outputs `y`.

    Args:
        label:
            The label of the economy.

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

        variables:
            Declared states and parameters that replace the derived ones, by name.

        A:
            The matrix of the state.

        B:
            The matrix of the inputs in the state.

        C:
            The matrix of the state in the outputs.

        D:
            The nonsingular matrix of the inputs in the outputs.

        feedback:
            Whether the feedback component decouples the sectors.

    Examples:
        >>> from skcomplex.systems.economics.macroeconomics import PurchasingPowerElement
        >>> PurchasingPowerElement('economy', feedback=True).get_value('z1')
        0.0
    """

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        variables: Mapping[str, DeclaredVariableSpec] | None = None,
        A: Sequence[Sequence[float]] = A_MATRIX,
        B: Sequence[Sequence[float]] = B_MATRIX,
        C: Sequence[Sequence[float]] = C_MATRIX,
        D: Sequence[Sequence[float]] = D_MATRIX,
        feedback: bool = False,
    ) -> None:
        super().__init__(label, visual=visual, variables=variables)
        self.A = A
        self.B = B
        self.C = C
        self.D = D
        self.feedback = feedback

    def _declare_variables(self) -> Mapping[str, DeclaredVariableSpec]:
        """Return the states `z_i`, with feedback `w_ik`, starting at zero, and the sizes and times of the policies."""
        states = [f'z{i}' for i in range(1, SECTORS + 1)]
        if self.feedback:
            states += [f'w{i}{k}' for i in range(1, SECTORS + 1) for k in range(1, SECTORS + 1)]
        declared = {name: DeclaredVariableSpec(0.0, StateSpec()) for name in states}
        for i in range(1, SECTORS + 1):
            declared[f'policy{i}'] = DeclaredVariableSpec(0.0, ParameterSpec())
            declared[f'policy_time{i}'] = DeclaredVariableSpec(1.0, ParameterSpec())
        return {**declared, **(self.variables or {})}

    @staticmethod
    def _policies(s: SimpleNamespace) -> list[Expression]:
        """Return the policies of the sectors, steps of their sizes at their times."""
        return [step(s.t, getattr(s, f'policy_time{i}'), getattr(s, f'policy{i}')) for i in range(1, SECTORS + 1)]

    def get_outputs(self, s: SimpleNamespace) -> list[Expression]:
        """Return the indices `y` of the sectors.

        Args:
            s:
                The namespace of the economy.

        Returns:
            outputs:
                One index per sector.
        """
        x, C, D = self._policies(s), np.asarray(self.C), np.asarray(self.D)
        if not self.feedback:
            z = [getattr(s, f'z{i}') for i in range(1, SECTORS + 1)]
            return [sum(C[i, k] * z[k] + D[i, k] * x[k] for k in range(SECTORS)) for i in range(SECTORS)]
        return [
            sum(C[i, k] * getattr(s, f'w{i + 1}{k + 1}') for k in range(SECTORS)) + D[i, i] * x[i]
            for i in range(SECTORS)
        ]

    def get_inputs(self, s: SimpleNamespace) -> list[Expression]:
        """Return the inputs `u` of the economy, the policies, or `D^-1 (y - C z)` with feedback.

        Args:
            s:
                The namespace of the economy.

        Returns:
            inputs:
                One input per sector.
        """
        if not self.feedback:
            return self._policies(s)
        C, inverse = np.asarray(self.C), np.linalg.inv(np.asarray(self.D, dtype=float))
        z, y = [getattr(s, f'z{i}') for i in range(1, SECTORS + 1)], self.get_outputs(s)
        gaps = [y[i] - sum(C[i, k] * z[k] for k in range(SECTORS)) for i in range(SECTORS)]
        return [sum(inverse[i, k] * gaps[k] for k in range(SECTORS)) for i in range(SECTORS)]

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the rates of the economy and, with feedback, of the decoupled systems.

        Args:
            s:
                The namespace of the symbols the laws can use.

        Returns:
            laws:
                The laws of the element.
        """
        A, B = np.asarray(self.A), np.asarray(self.B)
        z, u = [getattr(s, f'z{i}') for i in range(1, SECTORS + 1)], self.get_inputs(s)
        laws: list[BaseLaw] = [
            RateLaw(z[i], sum(A[i, k] * z[k] + B[i, k] * u[k] for k in range(SECTORS))) for i in range(SECTORS)
        ]
        if self.feedback:
            x = self._policies(s)
            for i in range(SECTORS):
                w = [getattr(s, f'w{i + 1}{k + 1}') for k in range(SECTORS)]
                laws += [
                    RateLaw(w[j], sum(A[j, k] * w[k] for k in range(SECTORS)) + B[j, i] * x[i]) for j in range(SECTORS)
                ]
        return laws

Equations

economy.z1: diff(z1_economy(t), t) == 0.9900398406374502*w11_economy(t) + 0.29701195219123505*w12_economy(t) + 0.0019920318725099606*w22_economy(t) + 0.0005976095617529881*w23_economy(t) + 0.019920318725099605*w31_economy(t) + 0.09960159362549802*w33_economy(t) + 0.0019920318725099606*where(t >= policy_time2_economy, policy2_economy, 0) + 0.9900398406374502*where(t >= policy_time1_economy, policy1_economy, 0) + 0.09960159362549802*where(t >= policy_time3_economy, policy3_economy, 0) - 2.00996015936255*z1_economy(t) + 0.000996015936254952*z2_economy(t) + 0.099800796812749*z3_economy(t)
economy.z2: diff(z2_economy(t), t) == 0.099601593625498*w11_economy(t) + 0.029880478087649404*w12_economy(t) + 0.9800796812749004*w22_economy(t) + 0.2940239043824701*w23_economy(t) + 0.0007968127490039847*w31_economy(t) + 0.003984063745019917*w33_economy(t) + 0.9800796812749004*where(t >= policy_time2_economy, policy2_economy, 0) + 0.099601593625498*where(t >= policy_time1_economy, policy1_economy, 0) + 0.003984063745019917*where(t >= policy_time3_economy, policy3_economy, 0) + 0.099601593625498*z1_economy(t) - 1.8099601593625498*z2_economy(t) + 0.0019920318725099306*z3_economy(t)
economy.z3: diff(z3_economy(t), t) == 0.10159362549800798*w11_economy(t) + 0.030478087649402394*w12_economy(t) + 0.07968127490039842*w22_economy(t) + 0.02390438247011952*w23_economy(t) + 0.19681274900398407*w31_economy(t) + 0.9840637450199203*w33_economy(t) + 0.07968127490039842*where(t >= policy_time2_economy, policy2_economy, 0) + 0.10159362549800798*where(t >= policy_time1_economy, policy1_economy, 0) + 0.9840637450199203*where(t >= policy_time3_economy, policy3_economy, 0) - 0.19840637450199206*z1_economy(t) + 0.2898406374501992*z2_economy(t) - 1.60796812749004*z3_economy(t)
economy.w11: diff(w11_economy(t), t) == -1.0*w11_economy(t) + 0.3*w12_economy(t) + 0.2*w13_economy(t) + 1.0*where(t >= policy_time1_economy, policy1_economy, 0)
economy.w12: diff(w12_economy(t), t) == 0.2*w11_economy(t) - 0.8*w12_economy(t) + 0.3*w13_economy(t) + 0.1*where(t >= policy_time1_economy, policy1_economy, 0)
economy.w13: diff(w13_economy(t), t) == 0.1*w11_economy(t) + 0.4*w12_economy(t) - 0.6*w13_economy(t) + 0.2*where(t >= policy_time1_economy, policy1_economy, 0)
economy.w21: diff(w21_economy(t), t) == -1.0*w21_economy(t) + 0.3*w22_economy(t) + 0.2*w23_economy(t) + 0.2*where(t >= policy_time2_economy, policy2_economy, 0)
economy.w22: diff(w22_economy(t), t) == 0.2*w21_economy(t) - 0.8*w22_economy(t) + 0.3*w23_economy(t) + 1.0*where(t >= policy_time2_economy, policy2_economy, 0)
economy.w23: diff(w23_economy(t), t) == 0.1*w21_economy(t) + 0.4*w22_economy(t) - 0.6*w23_economy(t) + 0.1*where(t >= policy_time2_economy, policy2_economy, 0)
economy.w31: diff(w31_economy(t), t) == -1.0*w31_economy(t) + 0.3*w32_economy(t) + 0.2*w33_economy(t) + 0.1*where(t >= policy_time3_economy, policy3_economy, 0)
economy.w32: diff(w32_economy(t), t) == 0.2*w31_economy(t) - 0.8*w32_economy(t) + 0.3*w33_economy(t) + 0.2*where(t >= policy_time3_economy, policy3_economy, 0)
economy.w33: diff(w33_economy(t), t) == 0.1*w31_economy(t) + 0.4*w32_economy(t) - 0.6*w33_economy(t) + 1.0*where(t >= policy_time3_economy, policy3_economy, 0)