Kinetic wealth and welfare dynamics
The kinetic theory of wealth classes, with a welfare policy when the tax is positive.
- Name:
kinetic_wealth_welfare - Field:
economics/econophysics - Formalism: ordinary differential equations
- References: Bellomo et al. 2017; Dolfin and Lachowicz 2014; Bertotti and Delitala 2008
Factory
@register_model(
field='economics/econophysics',
tags=('wealth', 'kinetic', 'distribution', 'welfare'),
references=WELFARE_REFERENCES,
defaults={'until': 100, 'dt': 1.0},
observables=lambda system: wealth_observables(_classes_of(system)),
)
def kinetic_wealth_welfare(
classes: int = 9,
*,
cooperation: float = 0.2,
competition: float = 0.3,
eta: float = 1.0,
tax: float = 0.0,
) -> System:
"""Return the kinetic theory of wealth classes, with a welfare policy when the tax is positive.
The population is a distribution over wealth classes, a site group on a lattice of the classes. Encounters
between classes move particles one class up or down, with cooperation narrowing the gaps and competition widening
them, so the total density stays constant. When competition wins, wealth polarises toward the poorest and the
richest classes, and the Gini coefficient grows. A welfare policy is an encounter with a group of fixed density,
which moves the richest third of the classes down and the poorest third up and lowers the stationary Gini
coefficient.
Args:
classes:
The number of wealth classes.
cooperation:
The probability of the move that narrows the gap between the classes of an encounter.
competition:
The probability of the move that widens it.
eta:
The encounter rate.
tax:
The rate of the welfare policy, 0 for none.
Returns:
system:
The model, with the distribution `wealth`, the encounters `exchange`, and with a tax the group `state` and
the policy `welfare`. Its standard observables are the density, the mean wealth, and the Gini coefficient
of `wealth`.
Examples:
>>> from skcomplex.models.economics.econophysics import kinetic_wealth_welfare
>>> [item.label for item in kinetic_wealth_welfare(tax=0.2).interactions]
['exchange', 'welfare']
"""
initial = np.exp(-((wealth_classes(classes) - 0.5) ** 2) / 0.05)
components: list[GroupElement] = [
GroupElement(
'wealth',
element=DensityElement,
size=classes,
on='sites',
values={'f': initial / initial.sum()},
),
]
interactions: list[BaseInteraction] = [
WealthExchangeInteraction(
'exchange',
candidate='wealth',
field='wealth',
classes=classes,
eta=eta,
cooperation=cooperation,
competition=competition,
),
]
if tax > 0:
components.append(GroupElement('state', element=DensityElement, size=1, values={'f': 1.0}))
interactions.append(
WelfarePolicyInteraction('welfare', candidate='wealth', field='state', classes=classes, tax=tax),
)
return System(
elements=components,
interactions=interactions,
space=LatticeSpace(classes, boundary='fixed'),
time=ContinuousTime(),
)
Components
class DensityElement(Element):
"""Value of a density at one class, a member of a distribution over the classes.
A distribution is a group of densities on the sites of a lattice of classes, one member per class, and encounter,
proliferation, and destruction laws change it. A density has no laws of its own.
Args:
label:
The label of the density, unique among the parts of its system.
visual:
How the element is drawn, or `None` for `default_visual_`.
f:
The initial density, a nonnegative number.
Attributes:
default_visual_:
The visual of the element when it sets none.
Examples:
>>> from skcomplex.core.elements import DensityElement, GroupElement
>>> from skcomplex.core.spaces import LatticeSpace
>>> classes = LatticeSpace(5, boundary='fixed')
>>> group = GroupElement('distribution', element=DensityElement, space=classes, on='sites', values={'f': 0.2})
>>> group.check().prepare().size_
5
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
f: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
super().__init__(label, visual=visual)
self.f = f
class WealthExchangeInteraction(EncounterInteraction):
"""Conservative encounters between wealth classes, with cooperation and competition.
A candidate of class `h` meets a field particle of class `k` at the rate `eta` and moves by the transition table
of `exchange_table`.
Args:
label:
The label of the interaction.
candidate:
The path of the distribution over wealth classes whose particles move.
field:
The path of the distribution over wealth classes they meet, the candidate again.
classes:
The number of wealth classes.
eta:
The encounter rate.
cooperation:
The probability of the move that narrows the gap between the classes of an encounter.
competition:
The probability of the move that widens it.
Examples:
>>> from skcomplex.models.economics.econophysics import WealthExchangeInteraction
>>> WealthExchangeInteraction('exchange', candidate='wealth', field='wealth').check().competition
0.3
"""
def __init__(
self,
label: str,
*,
candidate: str,
field: str,
classes: Annotated[int, ParameterSpec(domain=RealSet.open(0, oo))] = 9,
eta: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
cooperation: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.2,
competition: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.3,
) -> None:
super().__init__(label, candidate=candidate, field=field, rate=self._get_rate, kernel=self._get_kernel)
self.classes = classes
self.eta = eta
self.cooperation = cooperation
self.competition = competition
def _get_rate(self, s: SimpleNamespace) -> Expression:
"""Return the encounter rate `eta`."""
return s.eta
def _get_kernel(self, s: SimpleNamespace) -> npt.NDArray[np.object_]:
"""Return the transition table of the encounters, with the cooperation and competition parameters."""
return exchange_table(int(self.classes), s.cooperation, s.competition)
Equations
wealth.f[i]: diff(f_wealth(t), t) == 0