Skip to content

Rice milling system

A rice milling system, a mill that buys paddy, mills it into rice and by-products, and sells them.

  • Name: rice_milling
  • Field: engineering/processes
  • Formalism: ordinary differential equations
  • References: Bala et al. 2017

Factory

@register_model(
    field='engineering/processes',
    tags=('agriculture', 'milling', 'stocks and flows', 'system dynamics', 'ode'),
    references=('Bala et al. 2017',),
    defaults={'until': 120.0, 'dt': 0.25},
    observables=lambda system: [
        ObservableSpec('procurement', RiceMillElement._procurement, element='mill'),
        ObservableSpec('indicated_milling', RiceMillElement._indicated_milling, element='mill'),
        ObservableSpec('milling', RiceMillElement._milling, element='mill'),
    ],
)
def rice_milling(
    *,
    harvest: float = 2000.0,
    seasonality: float = 0.8,
    procurement_delay: float = 1.0,
    outturn: float = 0.65,
    capacity: float = 2000.0,
    expansion_time: float = 12.0,
) -> System:
    """Return a rice milling system, a mill that buys paddy, mills it into rice and by-products, and sells them.

    Paddy arrives from the farms through a third-order procurement delay, at a rate that follows the harvest season
    around its mean. The mill processes its paddy stock in a month, as far as its milling capacity allows, and splits
    the milled paddy by mass into rice, the outturn, husk, bran, and milling losses. Rice, husk, and bran leave their
    inventories for the market at a fixed coverage. The capacity adjusts to the smoothed indicated milling rate, so in
    the harvest season the capacity lags and paddy accumulates, and in the lean season the capacity idles. Grain is
    conserved, every change of the paddy, rice, husk, and bran stocks is a flow, and over a year the milled paddy and
    the rice sales match the mean harvest and its outturn. The structure follows the rice milling case of Bala,
    Arshad and Noh (2017), with illustrative parameters in tonnes and months, since the case data are specific to
    their study.

    Args:
        harvest:
            The mean paddy procurement in tonnes per month.

        seasonality:
            The relative amplitude of the yearly harvest cycle.

        procurement_delay:
            The average time for purchased paddy to reach the mill, in months.

        outturn:
            The fraction of the milled paddy that becomes rice.

        capacity:
            The initial milling capacity in tonnes of paddy per month.

        expansion_time:
            The time for the capacity to adjust to the perceived milling rate, in months.

    Returns:
        system:
            The model, with the grain stocks `mill.paddy`, `mill.rice`, `mill.husk`, and `mill.bran` in tonnes, the
            capacity `mill.capacity`, and the flows `mill.paddy_purchases`, `mill.milled_rice`, `mill.husk_output`,
            `mill.bran_output`, `mill.milling_losses`, `mill.rice_sales`, `mill.husk_sales`, and `mill.bran_sales` in
            tonnes per month, over months. The observables of the model are `mill.procurement`,
            `mill.indicated_milling`, and `mill.milling`.

    Examples:
        >>> from skcomplex.models.engineering.processes import rice_milling
        >>> rice_milling(capacity=1500.0).elements[0].capacity
        1500.0
    """
    mill = RiceMillElement(
        'mill',
        paddy=harvest,
        rice=outturn * harvest,
        husk=HUSK_SHARE * harvest,
        bran=BRAN_SHARE * harvest,
        capacity=capacity,
        harvest=harvest,
        seasonality=seasonality,
        procurement_delay=procurement_delay,
        outturn=outturn,
        expansion_time=expansion_time,
    )
    return System(elements=[mill])

Components

class RiceMillElement(Element):
    """Rice mill that buys seasonal paddy, mills it into rice, husk, and bran, and adjusts its capacity."""

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        paddy: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo), quantity='grain')] = 2000.0,
        rice: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo), quantity='grain')] = 1300.0,
        husk: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo), quantity='grain')] = 400.0,
        bran: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo), quantity='grain')] = 160.0,
        capacity: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo))] = 2000.0,
        harvest: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 2000.0,
        seasonality: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.8,
        procurement_delay: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        milling_time: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        outturn: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.65,
        husk_share: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = HUSK_SHARE,
        bran_share: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = BRAN_SHARE,
        dispatch_time: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        by_product_time: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
        perception_time: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 6.0,
        expansion_time: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 12.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.paddy = paddy
        self.rice = rice
        self.husk = husk
        self.bran = bran
        self.capacity = capacity
        self.harvest = harvest
        self.seasonality = seasonality
        self.procurement_delay = procurement_delay
        self.milling_time = milling_time
        self.outturn = outturn
        self.husk_share = husk_share
        self.bran_share = bran_share
        self.dispatch_time = dispatch_time
        self.by_product_time = by_product_time
        self.perception_time = perception_time
        self.expansion_time = expansion_time

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the flows of grain through the mill and the adjustment of its capacity."""
        return RiceMillElement._rice_mill_laws(s)

    @staticmethod
    def _season(mean: Expression, amplitude: Expression, t: Expression, period: float) -> Expression:
        """Return a seasonal value that oscillates about its mean with a relative amplitude and a period."""
        return mean * (1 + amplitude * sin(2 * math.pi * t / period))

    @staticmethod
    def _procurement(s: SimpleNamespace) -> Expression:
        """Return the procurement of paddy, the seasonal harvest after the procurement delay."""
        return delay3(RiceMillElement._season(s.harvest, s.seasonality, s.t, MONTHS_PER_YEAR), s.procurement_delay)

    @staticmethod
    def _indicated_milling(s: SimpleNamespace) -> Expression:
        """Return the indicated milling, the paddy milled over the milling time."""
        return s.paddy / s.milling_time

    @staticmethod
    def _milling(s: SimpleNamespace) -> Expression:
        """Return the milling, the indicated milling limited by the capacity."""
        return minimum(RiceMillElement._indicated_milling(s), s.capacity)

    @staticmethod
    def _rice_mill_laws(s: SimpleNamespace) -> list[BaseLaw]:
        """Return the procurement, milling, and sales of a rice mill, and the adjustment of its milling capacity."""
        milling = RiceMillElement._milling(s)
        losses = 1 - s.outturn - s.husk_share - s.bran_share
        return [
            TransferLaw(Source('farms'), s.paddy, RiceMillElement._procurement(s), label='paddy_purchases'),
            TransferLaw(s.paddy, s.rice, s.outturn * milling, label='milled_rice'),
            TransferLaw(s.paddy, s.husk, s.husk_share * milling, label='husk_output'),
            TransferLaw(s.paddy, s.bran, s.bran_share * milling, label='bran_output'),
            TransferLaw(s.paddy, Sink('losses'), losses * milling, label='milling_losses'),
            TransferLaw(s.rice, Sink('market'), s.rice / s.dispatch_time, label='rice_sales'),
            TransferLaw(s.husk, Sink('fuel'), s.husk / s.by_product_time, label='husk_sales'),
            TransferLaw(s.bran, Sink('feed'), s.bran / s.by_product_time, label='bran_sales'),
            RateLaw(
                s.capacity,
                (smooth(RiceMillElement._indicated_milling(s), s.perception_time) - s.capacity) / s.expansion_time,
            ),
        ]

Equations

mill.paddy: diff(paddy_mill(t), t) == (bran_share_mill + husk_share_mill + outturn_mill - 1)*where(capacity_mill(t) < paddy_mill(t)/milling_time_mill, capacity_mill(t), paddy_mill(t)/milling_time_mill) - bran_share_mill*where(capacity_mill(t) < paddy_mill(t)/milling_time_mill, capacity_mill(t), paddy_mill(t)/milling_time_mill) - husk_share_mill*where(capacity_mill(t) < paddy_mill(t)/milling_time_mill, capacity_mill(t), paddy_mill(t)/milling_time_mill) - outturn_mill*where(capacity_mill(t) < paddy_mill(t)/milling_time_mill, capacity_mill(t), paddy_mill(t)/milling_time_mill) + 3*_material_delay3_0_mill_2/procurement_delay_mill
mill.rice: diff(rice_mill(t), t) == outturn_mill*where(capacity_mill(t) < paddy_mill(t)/milling_time_mill, capacity_mill(t), paddy_mill(t)/milling_time_mill) - rice_mill(t)/dispatch_time_mill
mill.husk: diff(husk_mill(t), t) == husk_share_mill*where(capacity_mill(t) < paddy_mill(t)/milling_time_mill, capacity_mill(t), paddy_mill(t)/milling_time_mill) - husk_mill(t)/by_product_time_mill
mill.bran: diff(bran_mill(t), t) == bran_share_mill*where(capacity_mill(t) < paddy_mill(t)/milling_time_mill, capacity_mill(t), paddy_mill(t)/milling_time_mill) - bran_mill(t)/by_product_time_mill
mill.capacity: diff(capacity_mill(t), t) == (_smoothing_delay1_1_mill_0 - capacity_mill(t))/expansion_time_mill