Harvesting farmers
A socio-ecological model of farmer agents harvesting a logistically regrowing resource.
- Name:
harvesting - Field:
economics/resources - Formalism: co-simulation
- References: Hamill and Gilbert 2016 ch. 10
Factory
@register_model(
field='economics/resources',
tags=('socio-ecological', 'agents', 'ode', 'co-simulation'),
references=('Hamill and Gilbert 2016 ch. 10',),
defaults={'until': 50},
)
def harvesting(farmers: int = 10, *, effort: float = 0.05, r: float = 1.0, K: float = 1000.0) -> System:
"""Return a socio-ecological model of farmer agents harvesting a logistically regrowing resource.
The resource settles at `K * (1 - effort * farmers / r)` when `effort * farmers < r`, and collapses otherwise.
Args:
farmers:
The number of farmers.
effort:
The harvest rate per farmer and unit of stock.
r:
The growth rate of the resource.
K:
The carrying capacity of the resource.
Returns:
system:
The model, with the ODE subsystem `land` and the agent subsystem `village`.
Examples:
>>> from skcomplex.models.economics.resources import harvesting
>>> [subsystem.label for subsystem in harvesting().systems]
['land', 'village']
"""
land = System('land', elements=[StockElement('stock', n=K, r=r, K=K)])
village = System('village', elements=[GroupElement('farmers', element=FarmerElement, size=farmers)])
harvest = HarvestInteraction('harvest', target='land/stock', harvesters='village/farmers', effort=effort)
return System(interactions=[harvest], systems=[land, village])
Components
class HarvestInteraction(RateInteraction):
"""Harvest of a stock proportional to the stock and to the number of harvesters, the rate `-effort * N * n`.
A rate on the state `n` of the target whose coefficient counts the members of the group of harvesters, which may
live in another subsystem.
Args:
label:
The label of the interaction.
target:
The path of the harvested stock.
harvesters:
The path of the group of harvesters, which the harvest counts.
effort:
The harvest rate per harvester and unit of stock.
state:
The name of the state of the target that holds the stock.
Examples:
>>> from skcomplex.models.economics.resources import HarvestInteraction
>>> HarvestInteraction('harvest', target='land/stock', harvesters='village/farmers', effort=0.05).effort
0.05
"""
def __init__(
self,
label: str,
*,
target: Annotated[str, RoleSpec()],
harvesters: Annotated[str, RoleSpec()],
effort: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.05,
state: str = 'n',
) -> None:
super().__init__(label, target=target, state=state)
self.harvesters = harvesters
self.effort = effort
def _get_rate(self, namespace: SimpleNamespace) -> object:
"""Return the harvest `-effort * N * n`.
Args:
namespace:
The namespace of the symbols the rate can use, with the target and the harvesters.
Returns:
rate:
The harvest rate of the stock.
"""
return -namespace.effort * count(namespace.harvesters) * getattr(namespace.target, self.state)
class StockElement(Element):
"""Renewable stock that grows logistically, `n' = r n (1 - n / K)`.
Args:
label:
The label of the stock.
visual:
How the element is drawn, or `None` for `default_visual_`.
n:
The initial stock.
r:
The intrinsic growth rate.
K:
The carrying capacity.
Examples:
>>> from skcomplex.models.economics.resources import StockElement
>>> StockElement('stock', n=500.0).n
500.0
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
n: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo))] = 1000.0,
r: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
K: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 1000.0,
) -> None:
super().__init__(label, visual=visual)
self.n = n
self.r = r
self.K = K
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the logistic growth of the stock.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
return [RateLaw(s.n, s.r * s.n * (1 - s.n / s.K))]
class FarmerElement(Element):
"""Farmer who adds the catch of every step to its wealth, reading the resource from the other partition.
Args:
label:
The label of the agent or group.
visual:
How the element is drawn, or `None` for `default_visual_`.
wealth:
The initial wealth.
resource:
The dotted name of the resource stock.
effort:
The dotted name of the harvest effort per farmer.
Examples:
>>> from skcomplex.core.elements import GroupElement
>>> from skcomplex.models.economics.resources import FarmerElement
>>> GroupElement('farmers', element=FarmerElement, size=10).size
10
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
wealth: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
resource: str = 'land/stock.n',
effort: str = 'harvest.effort',
) -> None:
super().__init__(label, visual=visual)
self.wealth = wealth
self.resource = resource
self.effort = effort
@classmethod
def _get_view(cls) -> type[FarmerView]:
"""Return the view of the agents of the element."""
return FarmerView
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the catch rule.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
return [RuleLaw(self.catch)]
def catch(self, agent: FarmerView, model: ModelView) -> None:
"""Add the effort times the resource stock at the last synchronisation point to the wealth.
Args:
agent:
The farmer.
model:
The model.
"""
stock = model.inputs.get(self.resource, 0.0)
if not isinstance(stock, int | float | np.number):
error_msg = f'The input {self.resource!r} of the farmers is {stock!r}, not a number.'
raise ValidationError(error_msg)
agent.wealth += model.parameters[self.effort] * float(stock)