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