Metaphoria mortal hunter-gatherer and monetary economies
Gooding's Metaphoria, a mortal population of hunter-gatherers or of a monetary economy.
- Name:
metaphoria - Field:
economics/macroeconomics - Formalism: agent-based model
- References: Gooding 2019
Factory
@register_model(
field='economics/macroeconomics',
tags=('agents', 'mortality', 'evolution', 'money', 'hunter-gatherers', 'stock-flow consistent'),
references=('Gooding 2019',),
defaults={'until': 300},
observables=_metaphoria_observables,
)
def metaphoria(
n_people: int = 100,
*,
mode: str = 'hunter_gatherer',
shape: Sequence[int] = (20, 20),
max_food: float = 15.0,
max_resistance: float = 5.0,
regrowth: float = 1.0,
need: float = 2.0,
labour: float = 5.0,
interest: float = 0.003,
repayment: float = 0.004,
n_firms: int = 5,
random_state: int | np.random.Generator | None = 0,
) -> System:
"""Return Gooding's Metaphoria, a mortal population of hunter-gatherers or of a monetary economy.
Patches of a periodic lattice hold food that grows back to a random capacity and resist harvesting with a random
resistance. People need food every step, age, die of starvation or old age, and have children who inherit their
propensities to breed, explore, found firms, and quit jobs, with mutations, so the propensities evolve under the
test of survival. Hunter-gatherers pool the labour of the adults on a patch and share the harvest. In the monetary
economy firms harvest with paid workers, sell the food in shops, and borrow the wages from a bank that creates
money as loans, so the money of all holders sums to zero at every step. Gooding found the hunter-gatherers stable,
and the monetary population smaller and less healthy, with money concentrating to a Gini coefficient near 0.5. On
land where the hunter-gatherers thrive the monetary population dwindles towards extinction, and with abundant food
it lives on with persistent starvation.
Args:
n_people:
The initial number of adults.
mode:
`'hunter_gatherer'` or `'monetary'`.
shape:
The number of patches along each axis.
max_food:
The largest capacity of a patch, drawn uniformly from 0.
max_resistance:
The largest resistance of a patch, drawn uniformly from 0.
regrowth:
The share of the capacity that grows back in a step.
need:
The food a person eats in a step.
labour:
The labour of an adult in a step.
interest:
The interest rate of loans per step.
repayment:
The share of the principal repaid per step.
n_firms:
The number of firms of the monetary economy at the start, each without an owner until it has a worker.
random_state:
The seed or generator of the patches, the places, ages, and propensities of the people.
Returns:
system:
The model, with the group `patches`, one per cell, the variable group `people`, the reporters
`census.births`, `census.starvations`, and `census.old_age`, and the observables `population` and
`money_gini`. The monetary economy adds the variable group `businesses`, the group `bank`, and the reporter
`shops.gdp`.
Raises:
ValueError:
The mode is unknown.
Examples:
>>> from skcomplex.models.economics.macroeconomics import metaphoria
>>> [group.label for group in metaphoria(10, mode='monetary', shape=(5, 5)).elements]
['patches', 'people', 'businesses', 'bank']
"""
if mode not in MODES:
error_msg = f'Unknown mode {mode!r}. Use one of {MODES}.'
raise ValueError(error_msg)
rng = np.random.default_rng(random_state)
shape = tuple(int(item) for item in shape)
space = LatticeSpace(shape)
sites = list(np.ndindex(*shape))
capacity = rng.uniform(0.0, max_food, len(sites))
patches = GroupElement(
'patches',
element=PlotElement,
size=len(sites),
values={
'food': capacity,
'capacity': capacity,
'resistance': rng.uniform(0.0, max_resistance, len(sites)),
'regrowth': regrowth,
},
location=sites,
)
cells = rng.integers(0, shape, size=(n_people, len(shape)))
traits: dict[str, Array] = {name: rng.uniform(0.0, bound, n_people) for name, bound in PROPENSITIES.items()}
people = GroupElement(
'people',
element=SettlerElement,
size=n_people,
values={
'age': rng.integers(15, 40, n_people).astype(float),
'need': need,
'labour': labour,
'monetary': float(mode == 'monetary'),
'sight': SIGHT,
**traits,
},
variable=True,
location=[tuple(int(item) for item in cell) for cell in cells],
)
firms = rng.integers(0, shape, size=(n_firms, len(shape)))
businesses = GroupElement(
'businesses',
element=BusinessElement,
size=n_firms,
values={'interest': interest, 'repayment': repayment},
variable=True,
location=[tuple(int(item) for item in cell) for cell in firms],
)
bank = GroupElement('bank', element=CentralBankElement, size=1, location=(0,) * len(shape))
economy = [businesses, bank] if mode == 'monetary' else []
jobs = LabourMarketInteraction(
'jobs',
requesters='people',
providers='businesses',
rule=PartnerSpec(SIGHT, 'score', score=LabourMarketInteraction.score, clearing='provider'),
limit=('hireable', 'limit'),
quantity=('quantity', 'vacancies'),
phase='hiring',
on_match=LabourMarketInteraction.hire,
)
shops = FoodMarketInteraction(
'shops',
requesters='people',
providers='businesses',
rule=PartnerSpec(SIGHT, 'score', score=FoodMarketInteraction.score, clearing='provider'),
limit=('limit', 'price'),
quantity=('shortfall', 'stock'),
allowance='money',
transfers=(('money', -1),),
phase='market',
on_match=FoodMarketInteraction.sell,
)
dividends = TransferInteraction(
'dividends',
source='bank',
target='people',
state='money',
amount=CentralBankElement.get_income,
pairing='random',
fallback='businesses',
guard=lambda agent, holder, model: CentralBankElement.get_income(agent, holder, model) > 0,
form='agent',
phase='banking',
)
exchange: list[BaseInteraction] = (
[CommonsInteraction('commons', people='people')] if mode == 'hunter_gatherer' else [jobs, dividends, shops]
)
return System(
elements=[patches, people, *economy],
interactions=[
RegrowthInteraction(
'regrowth',
target='patches',
state='food',
growth='regrowth',
relative=True,
phase='growth',
),
*exchange,
EstateInteraction('census', people='people'),
],
space=space,
time=DiscreteTime(schedule=PhasesScheduleSpec([PhaseSpec(name) for name in PHASES])),
)
Components
class PlotElement(Element):
"""Patch of land whose food grows back by a share of its capacity every step, harvested against a resistance.
The regrowth is a `RegrowthInteraction` of the group of patches, relative to the capacity.
Args:
label:
The label of the patch or the site group.
visual:
How the element is drawn, or `None` for `default_visual_`.
food:
The food on the patch.
capacity:
The most food the patch holds.
resistance:
The labour lost to the resistance of the patch before any food is harvested.
regrowth:
The share of the capacity that grows back in a step.
Examples:
>>> from skcomplex.models.economics.macroeconomics import PlotElement
>>> PlotElement('patch').regrowth
1.0
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
food: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
capacity: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 10.0,
resistance: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
regrowth: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
) -> None:
super().__init__(label, visual=visual)
self.food = food
self.capacity = capacity
self.resistance = resistance
self.regrowth = regrowth
@classmethod
def _get_view(cls) -> type[PlotView]:
"""Return the view of a patch, with its food, capacity, resistance, and regrowth."""
return PlotView
class SettlerElement(Element):
"""Mortal person who eats, ages, raises children, moves, and, in the monetary economy, works and founds firms.
Every step a person eats `need` food and starves when its food runs out, and past `lifespan` it dies of old age
with probability 1/7. A child takes `need` food from its parent and follows it. A fed adult has a child with the
probability `fertility`, at most once every `spacing` steps, and gives it `endowment` food. The child inherits the
four propensities, each moved by a normal mutation and kept in [0, 1]. A person that harvested too little and holds
less than `reserve` steps of food moves, far with the probability `exploration`. In the monetary economy an
unemployed adult on a patch with food but no firm founds one with the probability `enterprise`, and a worker quits
with the probability `restlessness`.
Args:
label:
The label of the person or the group.
visual:
How the element is drawn, or `None` for `default_visual_`.
food:
The food the person holds.
money:
The money the person holds.
loans:
The loans the person holds, negative for a debt taken over from a firm.
age:
The age in steps.
parent:
The id of the parent, or -1.
employer:
The id of the firm that employs the person, or -1.
harvested:
The food the person harvested or earned in the step.
born:
The step of the person's last child.
fate:
0 while alive, 1 after starving, and 2 after dying of old age, until the census removes the person.
fertility:
The probability of a child in a step.
exploration:
The probability of moving far.
enterprise:
The probability of founding a firm.
restlessness:
The probability of quitting a job.
labour:
The labour of an adult in a step.
need:
The food a person eats in a step.
maturity:
The age of adulthood.
lifespan:
The age after which death by old age is possible.
spacing:
The fewest steps between two children.
endowment:
The food a parent gives a newborn child.
mutation:
The standard deviation of the mutation of a propensity.
sight:
The distance within which a person sees firms and explores.
reserve:
The steps of food a person buys up to, and below which a person that harvests too little moves.
monetary:
One in the monetary economy, zero among hunter-gatherers.
Examples:
>>> from skcomplex.core.elements import GroupElement
>>> from skcomplex.models.economics.macroeconomics import SettlerElement
>>> GroupElement('people', element=SettlerElement, size=3, variable=True).size
3
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
food: Annotated[float, StateSpec()] = 10.0,
money: Annotated[float, StateSpec(quantity='money')] = 0.0,
loans: Annotated[float, StateSpec(quantity='credit')] = 0.0,
age: Annotated[float, StateSpec(domain=NN)] = 20.0,
parent: Annotated[float, StateSpec(reference='people')] = -1.0,
employer: Annotated[float, StateSpec(reference='businesses')] = -1.0,
harvested: Annotated[float, StateSpec()] = 0.0,
born: Annotated[float, StateSpec()] = -100.0,
fate: Annotated[float, StateSpec()] = ALIVE,
fertility: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
exploration: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
enterprise: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
restlessness: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
labour: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 5.0,
need: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 2.0,
maturity: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 15.0,
lifespan: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 85.0,
spacing: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 2.0,
endowment: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 5.0,
mutation: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.05,
sight: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 5.0,
reserve: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 5.0,
monetary: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
super().__init__(label, visual=visual)
self.food = food
self.money = money
self.loans = loans
self.age = age
self.parent = parent
self.employer = employer
self.harvested = harvested
self.born = born
self.fate = fate
self.fertility = fertility
self.exploration = exploration
self.enterprise = enterprise
self.restlessness = restlessness
self.labour = labour
self.need = need
self.maturity = maturity
self.lifespan = lifespan
self.spacing = spacing
self.endowment = endowment
self.mutation = mutation
self.sight = sight
self.reserve = reserve
self.monetary = monetary
@classmethod
def _get_view(cls) -> type[SettlerView]:
"""Return the view of a person, with its states and parameters."""
return SettlerView
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the rule of living in the phase of living.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
return [RuleLaw(self.live, phase='living')]
def _feed_child(self, agent: SettlerView, model: ModelView) -> None:
"""Take food from the living parent and follow it."""
parent = agent.referenced('parent')
if not isinstance(parent, SettlerView):
return
given = min(agent.need, max(parent.food - parent.need, 0.0))
parent.food -= given
agent.food += given
agent.move_to(parent.location)
def _breed(self, agent: SettlerView, model: ModelView) -> None:
"""Have a child with mutated propensities at the probability of fertility."""
fed = agent.food >= agent.endowment + agent.need
spaced = model.time - agent.born >= agent.spacing
if not (fed and spaced and model.rng.random() < agent.fertility):
return
traits = {
name: float(np.clip(getattr(agent, name) + model.rng.normal(0.0, agent.mutation), 0.0, 1.0))
for name in PROPENSITIES
}
agent.food -= agent.endowment
agent.born = model.time
agent.create(None, None, food=agent.endowment, age=0.0, parent=float(agent.id), born=model.time, **traits)
def _work_or_found(self, agent: SettlerView, model: ModelView) -> bool:
"""Quit a job at the restlessness, or found a firm on a free patch with food, and return whether employed."""
if agent.employer >= 0:
if model.rng.random() < agent.restlessness:
agent.employer = -1.0
return agent.employer >= 0
if agent.agents_here('businesses') or SettlerElement._patch_here(agent).food <= 0:
return False
if model.rng.random() >= agent.enterprise:
return False
firm = agent.create('businesses', owner=float(agent.id))
agent.employer = float(firm.id)
return True
def live(self, agent: SettlerView, model: ModelView) -> None:
"""Age, eat, die, have children, and move or work.
Args:
agent:
The person.
model:
The model.
"""
agent.age += 1
adult = agent.age >= agent.maturity
if not adult:
self._feed_child(agent, model)
agent.food -= agent.need
if agent.food < 0:
agent.fate = STARVED
return
if agent.age > agent.lifespan and model.rng.random() < OLD_AGE_DEATH:
agent.fate = AGED
return
if not adult:
return
self._breed(agent, model)
employed = agent.monetary > 0 and self._work_or_found(agent, model)
if not employed and agent.harvested < agent.need and agent.food < agent.reserve * agent.need:
SettlerElement._wander(agent, model, agent.sight)
agent.harvested = 0.0
@staticmethod
def _patch_here(agent: AgentView) -> PlotView:
"""Return the patch of the cell of an agent."""
return next(patch for patch in agent.agents_here('patches') if isinstance(patch, PlotView))
@staticmethod
def _cell(agent: AgentView) -> tuple[int, ...]:
"""Return the cell of an agent of the lattice."""
location = agent.location
if not isinstance(location, tuple):
error_msg = f'The agent {agent.id} is not on a lattice cell.'
raise TypeError(error_msg)
return tuple(int(item) for item in location)
@staticmethod
def _wander(agent: SettlerView, model: ModelView, sight: float) -> None:
"""Move to a random cell within sight with the propensity to explore, and otherwise to a neighbouring cell."""
if model.rng.random() < agent.exploration:
width, height = model.dimensions or (1, 1)
reach = int(sight)
dx, dy = model.rng.integers(-reach, reach + 1, size=2)
x, y = SettlerElement._cell(agent)
agent.move_to(((x + int(dx)) % int(width), (y + int(dy)) % int(height)))
return
cells = agent.neighbor_cells()
agent.move_to(cells[int(model.rng.integers(len(cells)))])
class RegrowthInteraction(MapInteraction):
"""Linear regrowth of a resource held by the agents of a group, by a fixed amount a step up to their capacity.
Every step the resource `x` of an agent grows by the amount `g` its parameter `growth` names, `x = min(K, x + g)`,
or by that share of its capacity `K` when the regrowth is relative, `x = min(K, x + g K)`. It is a map of the agent
form, such as the sugar of a patch of Sugarscape or the food of a plot of land.
Args:
label:
The label of the interaction, unique among the parts of its system.
target:
The path of the group whose resource grows back.
state:
The name of the state that holds the resource.
capacity:
The name of the state or parameter of an agent that holds its capacity.
growth:
The name of the parameter of an agent that holds its regrowth in a step.
relative:
Whether the regrowth is a share of the capacity rather than an amount.
phase:
The phase of the step the regrowth runs in, or `None` for the default phase.
Examples:
>>> from skcomplex.models.economics.common.resources import RegrowthInteraction
>>> regrowth = RegrowthInteraction('regrowth', target='patches', state='sugar', growth='growback')
>>> regrowth.check().form
'agent'
"""
def __init__(
self,
label: str,
*,
target: Annotated[str, RoleSpec()],
state: str,
capacity: str = 'capacity',
growth: str = 'growth',
relative: bool = False,
phase: str | None = None,
) -> None:
super().__init__(label, target=target, state=state, function=self._get_regrown, form='agent', phase=phase)
self.capacity = capacity
self.growth = growth
self.relative = relative
def _get_regrown(self, agent: AgentViewProtocol, model: ModelViewProtocol) -> float:
"""Return the resource of an agent after it grows back, at most its capacity."""
del model
capacity = float(getattr(agent, self.capacity))
growth = float(getattr(agent, self.growth)) * (capacity if self.relative else 1.0)
return min(capacity, float(getattr(agent, self.state)) + growth)
class CommonsInteraction(Interaction):
"""Harvest of the hunter-gatherers, who pool the labour of the adults on a patch and share the food equally.
The adults of a patch harvest `min(food, labour - resistance)`, and everyone on the patch, child or adult, gets an
equal share.
Args:
label:
The label of the interaction.
people:
The path of the group of people.
Examples:
>>> from skcomplex.models.economics.macroeconomics import CommonsInteraction
>>> CommonsInteraction('commons', people='people').people
'people'
"""
def __init__(
self,
label: str,
*,
people: Annotated[str, RoleSpec()],
) -> None:
super().__init__(label)
self.people = people
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the harvest rule in the phase of work.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
return [RuleLaw(self.harvest, phase='work')]
def harvest(self, model: ModelView) -> None:
"""Harvest every occupied patch and share the food among the people on it.
Args:
model:
The model view of the interaction.
"""
cells: dict[tuple[int, ...], list[SettlerView]] = {}
for person in model.agents('people', SettlerView):
cells.setdefault(SettlerElement._cell(person), []).append(person)
for group in cells.values():
patch = SettlerElement._patch_here(group[0])
labour = sum(person.labour for person in group if person.age >= person.maturity)
harvest = min(patch.food, max(labour - patch.resistance, 0.0))
patch.food -= harvest
for person in group:
person.food += harvest / len(group)
person.harvested = harvest / len(group)
class EstateInteraction(Interaction):
"""Settlement of the estates of the dead, who leave their money and loans to an heir, with a census of the step.
The money and the loans of a dead person go to a random living person of its patch, or of anywhere. When nobody
lives to inherit them, the dead person stays as an estate that holds them. The reporters `births`, `starvations`,
and `old_age` count the events of a step.
Args:
label:
The label of the interaction.
people:
The path of the group of people.
Examples:
>>> from skcomplex.models.economics.macroeconomics import EstateInteraction
>>> EstateInteraction('census', people='people').reports
('births', 'starvations', 'old_age')
"""
reports: ClassVar[tuple[str, ...]] = ('births', 'starvations', 'old_age')
def __init__(
self,
label: str,
*,
people: Annotated[str, RoleSpec()],
) -> None:
super().__init__(label)
self.people = people
def _settle(self, dead: SettlerView, living: list[SettlerView], model: ModelView) -> None:
"""Leave the money and the loans of a dead person to an heir, and remove the person."""
near = [person for person in living if person.location == dead.location] or living
if near:
heir = near[int(model.rng.integers(len(near)))]
transfer(dead, heir, dead.money, 'money')
transfer(dead, heir, dead.loans, 'loans')
if dead.money == 0 and dead.loans == 0:
dead.remove()
def count(self, model: ModelView) -> None:
"""Count the births and deaths of the step and remove the dead.
Args:
model:
The model view of the interaction.
"""
people = model.agents('people', SettlerView)
living = [person for person in people if person.fate == ALIVE]
dead = [person for person in people if person.fate != ALIVE]
model.report('births', float(sum(person.age == 0 for person in living)))
model.report('starvations', float(sum(person.fate == STARVED for person in dead)))
model.report('old_age', float(sum(person.fate == AGED for person in dead)))
for person in dead:
self._settle(person, living, model)
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the counting rule in the phase of the census.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
return [RuleLaw(self.count, phase='census')]