Produce market
Hamill and Gilbert's produce market, where shoppers with lists of produce visit stalls at random.
- Name:
produce_market - Field:
economics/markets - Formalism: agent-based model
- References: Hamill and Gilbert 2016
Factory
@register_model(
field='economics/markets',
tags=('market', 'agents', 'heterogeneity'),
references=('Hamill and Gilbert 2016',),
defaults={'until': 10},
observables=lambda system: [
ObservableSpec('items_left', group='shoppers', variable='wanted', reduce=_items_left),
ObservableSpec('spent', group='shoppers', variable='spent'),
],
)
def produce_market(
n_shoppers: int = 50,
n_stalls: int = 9,
*,
n_items_stocked: int = 6,
random_state: int | np.random.Generator | None = 0,
) -> System:
"""Return Hamill and Gilbert's produce market, where shoppers with lists of produce visit stalls at random.
There are twelve kinds of fruit and vegetables. Every stall sells a random selection of them at its own markup of 1
to 30 percent on the wholesale prices, and every shopper wants between one and eight of them.
Args:
n_shoppers:
The number of shoppers.
n_stalls:
The number of stalls.
n_items_stocked:
The number of kinds of produce that every stall sells.
random_state:
The seed or generator of the stocks, the markups, and the shopping lists.
Returns:
system:
The model, with the groups `stalls` and `shoppers`, the mean number of items left on the lists
`items_left`, and the mean money spent `spent`.
Examples:
>>> from skcomplex.models.economics.markets import produce_market
>>> [group.size for group in produce_market(10, 3).elements]
[3, 10]
"""
rng = np.random.default_rng(random_state)
stocks = _random_masks(np.full(n_stalls, n_items_stocked), rng)
markups = rng.integers(1, MAX_MARKUP + 1, n_stalls) / 100
lists = _random_masks(rng.integers(1, MAX_LIST_LENGTH + 1, n_shoppers), rng)
stalls = GroupElement(
'stalls',
element=StallElement,
size=n_stalls,
values={'stall': np.arange(n_stalls, dtype=float), 'stock': stocks, 'markup': markups},
)
shoppers = GroupElement('shoppers', element=ShopperElement, size=n_shoppers, values={'wanted': lists})
return System(
elements=[stalls, shoppers],
)
Components
class StallElement(Element):
"""Market stall that sells some kinds of produce at the wholesale price plus its markup.
Args:
label:
The label of the agent or group.
visual:
How the element is drawn, or `None` for `default_visual_`.
stall:
The index of the stall.
stock:
The bit mask of the kinds of produce that the stall sells.
markup:
The relative markup on the wholesale prices.
Examples:
>>> from skcomplex.core.elements import GroupElement
>>> from skcomplex.models.economics.markets import StallElement
>>> GroupElement('stalls', element=StallElement, size=3, values={'stall': [0, 1, 2]}).size
3
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
stall: Annotated[float, StateSpec(domain=NN)] = 0,
stock: Annotated[float, StateSpec(domain=NN)] = 0,
markup: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.1,
) -> None:
super().__init__(label, visual=visual)
self.stall = stall
self.stock = stock
self.markup = markup
@classmethod
def _get_view(cls) -> type[StallView]:
"""Return the view of a stall, with its index, stock, and markup."""
return StallView
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return no laws, since stalls only wait for shoppers.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
return []
class ShopperElement(Element):
"""Shopper who visits one stall a step that it has not visited yet and buys what it wants there.
A shopper gives up the items that it has not found once it has visited every stall.
Args:
label:
The label of the agent or group.
visual:
How the element is drawn, or `None` for `default_visual_`.
wanted:
The bit mask of the items on the shopping list.
visited:
The bit mask of the stalls already visited.
missed:
The bit mask of the items given up.
spent:
The money spent.
Examples:
>>> from skcomplex.core.elements import GroupElement
>>> from skcomplex.models.economics.markets import ShopperElement
>>> GroupElement('shoppers', element=ShopperElement, size=5, values={'wanted': 3}).size
5
"""
stalls: ClassVar[str] = 'stalls'
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
wanted: Annotated[float, StateSpec(domain=NN)] = 0,
visited: Annotated[float, StateSpec(domain=NN)] = 0,
missed: Annotated[float, StateSpec(domain=NN)] = 0,
spent: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
) -> None:
super().__init__(label, visual=visual)
self.wanted = wanted
self.visited = visited
self.missed = missed
self.spent = spent
@classmethod
def _get_view(cls) -> type[ShopperView]:
"""Return the view of a shopper, with its list, visits, misses, and spending."""
return ShopperView
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the shopping rule.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
return [RuleLaw(self.shop)]
def shop(self, agent: ShopperView, model: ModelView) -> None:
"""Visit a random stall not visited yet and buy every wanted item that it sells.
Args:
agent:
The shopper.
model:
The model.
"""
wanted, visited = int(agent.wanted), int(agent.visited)
if not wanted:
return
unvisited = [stall for stall in model.agents(self.stalls, StallView) if not visited >> int(stall.stall) & 1]
if not unvisited:
agent.missed, agent.wanted = wanted, 0
return
stall = unvisited[int(model.rng.integers(len(unvisited)))]
bought = wanted & int(stall.stock)
agent.visited = visited | 1 << int(stall.stall)
agent.spent += (1 + stall.markup) * sum(WHOLESALE_PRICES[item] for item in ShopperElement._items_of(bought))
agent.wanted = wanted & ~bought
@staticmethod
def _items_of(mask: int) -> list[int]:
"""Return the indices of the items in a bit mask."""
return [item for item in range(len(WHOLESALE_PRICES)) if mask >> item & 1]