Grain storage system
A retail grain store, whose orders through a supply line make its stock oscillate.
- Name:
grain_storage - Field:
engineering/processes - Formalism: ordinary differential equations
- References: Bala et al. 2017
Select this system
from skcomplex.systems import create_simulation, create_system, describe
system = create_system('grain_storage')
entry = describe('grain_storage')
trajectory = create_simulation('grain_storage').prepare().run().trajectory_
Factory
@register_system(
field='engineering/processes',
tags=('agriculture', 'storage', 'stocks and flows', 'system dynamics', 'oscillation', 'ode'),
references=('Bala et al. 2017',),
defaults={'until': 100.0, 'dt': 0.25},
)
def grain_storage(
stock: float = 1000.0,
on_order: float = 10000.0,
*,
desired_stock: float = 6000.0,
release_fraction: float = 0.025,
receiving_delay: float = 10.0,
adjustment_delay: float = 5.0,
) -> System:
"""Return a retail grain store, whose orders through a supply line make its stock oscillate.
The grain stock gains the receiving rate `on_order / receiving_delay` and loses the release rate
`release_fraction stock`, and the grain on order gains the procurement rate
`(desired_stock - stock) / adjustment_delay` and loses the receiving rate. The orders ignore the grain already on
order, so the supply line overshoots and the stock, the receiving rate, and the procurement oscillate in weeks
before they settle, with the stock below the desired one at
`desired_stock / (1 + release_fraction adjustment_delay)`, 5333 tonnes for the defaults, where the release
balances the receiving. The defaults are those of the grain storage example of Bala, Arshad and Noh (2017), in
tonnes and weeks.
Args:
stock:
The initial grain stock.
on_order:
The initial grain on order.
desired_stock:
The desired grain stock.
release_fraction:
The fraction of the stock released per week.
receiving_delay:
The average time for grain on order to arrive, in weeks.
adjustment_delay:
The time to close the gap between the desired and the current stock, in weeks.
Returns:
system:
The system, with the stocks `store.stock` and `store.on_order` in tonnes, and the flows
`store.procurement`, `store.receiving`, and `store.release` in tonnes per week, over weeks.
Examples:
>>> from skcomplex.systems.engineering.processes import grain_storage
>>> grain_storage(receiving_delay=5.0).elements[0].receiving_delay
5.0
"""
store = GrainStoreElement(
'store',
stock=stock,
on_order=on_order,
desired_stock=desired_stock,
release_fraction=release_fraction,
receiving_delay=receiving_delay,
adjustment_delay=adjustment_delay,
)
return System(elements=[store])
Components
Elements:
GrainStoreElement, fromElement
class GrainStoreElement(Element):
"""Retail grain store, whose stock is restocked from grain on order through a receiving delay.
Grain on order arrives in the stock at the receiving rate `on_order / receiving_delay`, and the stock releases the
fraction `release_fraction` of itself per unit time to the consumers. The procurement rate
`(desired_stock - stock) / adjustment_delay` adds grain on order, and it is negative when the stock exceeds the
desired stock, which cancels orders.
Args:
label:
The label of the store.
visual:
How the store is drawn, or `None` for `default_visual_`.
stock:
The initial grain stock.
on_order:
The initial grain on order.
desired_stock:
The desired grain stock.
release_fraction:
The fraction of the stock released per unit time.
receiving_delay:
The average time for grain on order to arrive.
adjustment_delay:
The time to close the gap between the desired and the current stock.
Examples:
>>> from skcomplex.systems.engineering.processes import GrainStoreElement
>>> GrainStoreElement('store').desired_stock
6000.0
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
stock: Annotated[float, StateSpec(quantity='grain')] = 1000.0,
on_order: Annotated[float, StateSpec(quantity='grain')] = 10000.0,
desired_stock: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 6000.0,
release_fraction: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.025,
receiving_delay: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 10.0,
adjustment_delay: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 5.0,
) -> None:
super().__init__(label, visual=visual)
self.stock = stock
self.on_order = on_order
self.desired_stock = desired_stock
self.release_fraction = release_fraction
self.receiving_delay = receiving_delay
self.adjustment_delay = adjustment_delay
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the procurement, receiving, and release flows of grain.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
return [
TransferLaw(Source('suppliers'), s.on_order, GrainStoreElement._procurement(s), label='procurement'),
TransferLaw(s.on_order, s.stock, GrainStoreElement._receiving(s), label='receiving'),
TransferLaw(s.stock, Sink('consumers'), s.stock * s.release_fraction, label='release'),
]
@staticmethod
def _procurement(s: SimpleNamespace) -> Expression:
"""Return the procurement rate, the gap to the desired stock over the adjustment delay."""
procurement: Expression = (s.desired_stock - s.stock) / s.adjustment_delay
return procurement
@staticmethod
def _receiving(s: SimpleNamespace) -> Expression:
"""Return the receiving rate, the grain on order over the receiving delay."""
receiving: Expression = s.on_order / s.receiving_delay
return receiving
Equations
store.stock: diff(stock_store(t), t) == -release_fraction_store*stock_store(t) + on_order_store(t)/receiving_delay_store
store.on_order: diff(on_order_store(t), t) == (desired_stock_store - stock_store(t))/adjustment_delay_store - on_order_store(t)/receiving_delay_store