Skip to content

Food relief system

A food relief system, a pipeline that ships food to a warehouse and distributes it to people in need.

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

Factory

@register_model(
    field='engineering/processes',
    tags=('humanitarian logistics', 'supply chain', 'stocks and flows', 'system dynamics', 'ode'),
    references=('Bala et al. 2017',),
    defaults={'until': 120.0, 'dt': 0.5},
    observables=lambda system: [
        ObservableSpec('need', ReliefPipelineElement._need, element='relief'),
        ObservableSpec('shipments', ReliefPipelineElement._shipments, element='relief'),
    ],
)
def food_relief(
    affected: float = 1e5,
    *,
    ration: float = 5e-4,
    onset: float = 10.0,
    shipping_delay: float = 14.0,
    cover: float = 7.0,
    stock_time: float = 7.0,
) -> System:
    """Return a food relief system, a pipeline that ships food to a warehouse and distributes it to people in need.

    A disaster at `onset` leaves `affected` people who need a daily ration. The relief agency ships food at the need
    plus corrections that close the gaps between the warehouse stock and its desired cover of the need, and between
    the food in transit and the need over the shipping delay. Shipped food enters the pipeline and arrives at the
    warehouse through a third-order material delay. The warehouse distributes the need while its stock lasts. Food is
    conserved, so almost nothing reaches the people in the first days after the disaster, and the gap closes as the
    pipeline fills. The structure follows the relief food example of Bala, Arshad and Noh (2017, Section 4.11), with
    illustrative parameters in tonnes and days.

    Args:
        affected:
            The number of people in need after the disaster.

        ration:
            The daily ration in tonnes per person.

        onset:
            The day of the disaster.

        shipping_delay:
            The average time from shipping to arrival, in days.

        cover:
            The desired warehouse stock in days of need.

        stock_time:
            The time to correct the warehouse stock, in days.

    Returns:
        system:
            The model, with the food stocks `relief.in_transit` and `relief.warehouse` in tonnes, and the flows
            `relief.shipping`, `relief.arrivals`, and `relief.distribution` in tonnes per day, over days. The
            observables of the model are `relief.need` and `relief.shipments`.

    Examples:
        >>> from skcomplex.models.engineering.processes import food_relief
        >>> food_relief(shipping_delay=7.0).elements[0].shipping_delay
        7.0
    """
    relief = ReliefPipelineElement(
        'relief',
        affected=affected,
        ration=ration,
        onset=onset,
        shipping_delay=shipping_delay,
        cover=cover,
        stock_time=stock_time,
    )
    return System(elements=[relief])

Components

class ReliefPipelineElement(Element):
    """Relief supply line that ships food through a delayed pipeline to a warehouse and distributes it."""

    def __init__(
        self,
        label: str,
        visual: VisualSpec | None = None,
        *,
        in_transit: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo), quantity='food')] = 0.0,
        warehouse: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo), quantity='food')] = 0.0,
        affected: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 100000.0,
        ration: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.0005,
        onset: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 10.0,
        shipping_delay: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 14.0,
        cover: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 7.0,
        stock_time: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 7.0,
        pipeline_time: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 14.0,
        distribution_time: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
    ) -> None:
        super().__init__(label, visual=visual)
        self.in_transit = in_transit
        self.warehouse = warehouse
        self.affected = affected
        self.ration = ration
        self.onset = onset
        self.shipping_delay = shipping_delay
        self.cover = cover
        self.stock_time = stock_time
        self.pipeline_time = pipeline_time
        self.distribution_time = distribution_time

    def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
        """Return the flows of food through the supply line."""
        return ReliefPipelineElement._relief_laws(s)

    @staticmethod
    def _need(s: SimpleNamespace) -> Expression:
        """Return the need of relief food, which starts at the onset of the disaster."""
        return s.ration * s.affected * step(s.t, s.onset)

    @staticmethod
    def _shipments(s: SimpleNamespace) -> Expression:
        """Return the shipments that meet the need and close the gaps of the stock and of the pipeline."""
        need = ReliefPipelineElement._need(s)
        stock_gap = (s.cover * need - s.warehouse) / s.stock_time
        pipeline_gap = (s.shipping_delay * need - s.in_transit) / s.pipeline_time
        return clip(need + stock_gap + pipeline_gap, 0)

    @staticmethod
    def _relief_laws(s: SimpleNamespace) -> list[BaseLaw]:
        """Return the shipments, arrivals, and distribution of relief food after a disaster."""
        shipments = ReliefPipelineElement._shipments(s)
        return [
            TransferLaw(Source('donors'), s.in_transit, shipments, label='shipping'),
            TransferLaw(s.in_transit, s.warehouse, delay3(shipments, s.shipping_delay), label='arrivals'),
            TransferLaw(
                s.warehouse,
                Sink('beneficiaries'),
                minimum(ReliefPipelineElement._need(s), s.warehouse / s.distribution_time),
                label='distribution',
            ),
        ]

Equations

relief.in_transit: diff(in_transit_relief(t), t) == -3*_material_delay3_0_relief_2/shipping_delay_relief + where(affected_relief*ration_relief*where(t >= onset_relief, 1.0, 0) + (affected_relief*ration_relief*shipping_delay_relief*where(t >= onset_relief, 1.0, 0) - in_transit_relief(t))/pipeline_time_relief + (affected_relief*cover_relief*ration_relief*where(t >= onset_relief, 1.0, 0) - warehouse_relief(t))/stock_time_relief < 0, 0, affected_relief*ration_relief*where(t >= onset_relief, 1.0, 0) + (affected_relief*ration_relief*shipping_delay_relief*where(t >= onset_relief, 1.0, 0) - in_transit_relief(t))/pipeline_time_relief + (affected_relief*cover_relief*ration_relief*where(t >= onset_relief, 1.0, 0) - warehouse_relief(t))/stock_time_relief)
relief.warehouse: diff(warehouse_relief(t), t) == 3*_material_delay3_0_relief_2/shipping_delay_relief - where(warehouse_relief(t)/distribution_time_relief < affected_relief*ration_relief*where(t >= onset_relief, 1.0, 0), warehouse_relief(t)/distribution_time_relief, affected_relief*ration_relief*where(t >= onset_relief, 1.0, 0))