Skip to content

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, from Element
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