Skip to content

Suppliers

Suppliers, allocation, and delivery outcomes. Guides: Suppliers and open orders, Unreliable deliveries.

SupplyModel

SupplyModel(
    suppliers: Sequence[Supplier],
    *,
    allocation: Optional[SupplierAllocation] = None,
    random_seed: Optional[int] = None,
)

Suppliers, their allocation and the random seed for one simulation.

Parameters:

Name Type Description Default
suppliers Sequence[Supplier]

Non-empty sequence of Supplier objects with unique ids.

required
allocation Optional[SupplierAllocation]

SupplierAllocation deciding how each SKU order is split. Optional with one supplier (it then receives everything); required with several.

None
random_seed Optional[int]

Integer seed for random lead times. Required when any supplier has a random lead time; may be None otherwise.

None

Random lead times are drawn once per run for every (decision period, SKU, supplier) cell, from a stream per supplier. Runs of run_comparison therefore see the same lead-time realization for an order placed by the same SKU in the same period from the same supplier.

The state's pipeline must cover the longest delivery offset of every supplier (see max_delivery_offset): a state created without max_lead_time is sized to it by the engine; an explicit max_lead_time must be at least that long.

supplier_ids property

supplier_ids: tuple

The supplier ids, in the order given.

max_delivery_offset property

max_delivery_offset: int

Longest possible time from order to last delivery over all suppliers.

to_manifest

to_manifest() -> dict

Describe the suppliers, allocation, and seed for the run manifest.

Supplier

Supplier(
    supplier_id,
    lead_time,
    *,
    partial_deliveries=None,
    delivery=None,
)

One source of supply.

Parameters:

Name Type Description Default
supplier_id Hashable

Nonblank supplier identifier.

required
lead_time int | Mapping[int, float]

Periods from order placement to (first) delivery. Either a fixed integer >= 0, or a mapping {lead_time: probability} with integer lead times >= 0 and positive probabilities summing to 1. Random lead times are drawn with the SupplyModel seed.

required
partial_deliveries Sequence[tuple[int, float]] | None

Optional sequence of (delay, fraction) pairs. Each order line is delivered in parts, delay periods after the (drawn) lead time, with the given fraction of the line quantity. Delays are strictly increasing integers >= 0 and fractions are positive and sum to 1. Default: one delivery of the full quantity.

None
delivery DeliveryOutcome | None

Optional outcome that decides, at each due period, what actually arrives (see DeliveryOutcome). Default None: every delivery arrives in full when due.

None
Example
Supplier("importer", lead_time={4: 0.6, 5: 0.3, 7: 0.1},
         partial_deliveries=[(0, 0.7), (2, 0.3)])
# 70% arrives after 4, 5 or 7 periods; the rest two periods later.

is_random property

is_random: bool

Whether the lead time is drawn from a distribution.

max_delivery_offset property

max_delivery_offset: int

Longest possible time, in periods, from order to last delivery.

to_manifest

to_manifest() -> dict

Describe the supplier for the run manifest.

SupplierAllocation

Extension point: split each SKU's order quantity across suppliers.

Subclasses implement allocate(orders, context). orders has one row per SKU with a positive accepted order (context.sku_column and order_quantity). Return a DataFrame with columns context.sku_column, supplier_id and order_quantity: at most one row per SKU and supplier, known suppliers only, quantities >= 0, and per-SKU totals equal to the requested quantity (within floating-point tolerance). The per-SKU total in the event table is always the accepted order quantity. context.decision holds the policy's full accepted decision, so an allocation can follow a supplier the policy chose.

Allocations never change inventory. reset() is called before every run and get_config() must return JSON-serializable settings.

allocate

allocate(
    orders: DataFrame, context: AllocationContext
) -> DataFrame

Split each SKU's accepted order across suppliers.

Parameters:

Name Type Description Default
orders DataFrame

One row per SKU with a positive accepted order_quantity.

required
context AllocationContext

An AllocationContext: copies of the state and open orders, the period and date, the supplier ids, and the accepted decision.

required

Returns:

Type Description
DataFrame

One row per SKU and supplier with the SKU column, supplier_id and

DataFrame

order_quantity. Each SKU's rows must add up to its order.

validate

validate(supplier_ids: Sequence, skus: Sequence) -> None

Check, before a run, that the allocation fits the suppliers and SKUs.

reset

reset() -> None

Reset run-local state. Called before every run.

get_config

get_config() -> dict

Settings recorded in the run manifest (a JSON-serialisable dict).

SupplierShares

SupplierShares(
    shares: Mapping, *, by_sku: Optional[Mapping] = None
)

Bases: SupplierAllocation

Fixed fractional split of every order across suppliers.

Parameters:

Name Type Description Default
shares Mapping

Mapping {supplier_id: fraction}; fractions are >= 0 and sum to 1. Used for SKUs not listed in by_sku.

required
by_sku Optional[Mapping]

Optional mapping {sku: {supplier_id: fraction}} overriding shares for specific SKUs.

None

Suppliers with a zero share receive no order line. The last positive share receives the remainder, so line quantities add up to the order.

Example
SupplierShares({"local_roaster": 0.3, "importer": 0.7},
               by_sku={"decaf": {"local_roaster": 1.0}})

allocate

allocate(
    orders: DataFrame, context: AllocationContext
) -> DataFrame

Split each SKU's order by the configured shares.

get_config

get_config() -> dict

The shares, for the run manifest.

AllocationContext dataclass

AllocationContext(
    inventory: DataFrame,
    open_orders: DataFrame,
    sku_column: str,
    period: int,
    date: Timestamp,
    suppliers: tuple,
    decision: Optional[DataFrame] = None,
)

Defensive information handed to SupplierAllocation.allocate.

Attributes:

Name Type Description
inventory DataFrame

Copy of the state frame at the decision (before this order).

open_orders DataFrame

Copy of InventoryStateDataFrame.open_orders().

sku_column str

SKU identifier column of both frames.

period int

State period of the decision (the order period).

date Timestamp

Calendar date of the decision period.

suppliers tuple

Supplier identifiers of the SupplyModel, in order.

decision Optional[DataFrame]

Copy of the accepted order decision (after callbacks and constraints), one row per SKU, including any extra columns the policy wrote. A policy can, for example, add a supplier_id column and a custom allocation can follow it.

DeliveryOutcome

Extension point: what arrives when a supplier's deliveries fall due.

Attach an instance to a supplier with Supplier(..., delivery=...). At the start of every period in which deliveries of that supplier are due, before they are received, the engine calls resolve(due, context).

due has one row per due delivery, with columns order_id, the SKU column, supplier_id, source, order_period, scheduled_due_period (the due period set when the order was placed), due_period (now) and quantity.

Return None to let everything arrive, or a DataFrame with the same index as due and the column received_quantity, optionally with delayed_quantity and delay_periods. Other columns are ignored, so returning due with added columns works. Per row:

- ``received_quantity`` arrives now;
- ``delayed_quantity`` stays on order and is due again
  ``delay_periods`` periods later (an integer >= 1), when it is
  resolved again;
- the rest, ``quantity - received - delayed``, never arrives. It
  leaves stock on order and is recorded as
  ``supplier_shortfall_units`` in the event table.

Quantities are finite and >= 0, and received + delayed must not exceed quantity. A delayed delivery must still fall within inventory.max_lead_time periods of the current state.

The base class returns None: Supplier(id, lead_time, delivery= DeliveryOutcome()) gives the same deliveries as a supplier without an outcome. reset() is called before every run; get_config() must return JSON-serializable settings for the run manifest.

Example (10% of deliveries arrive one period late, the rest 90% filled):

class LateOrShort(DeliveryOutcome):
    def resolve(self, due, context):
        out = due.copy()
        late = context.rng.random(len(due)) < 0.1
        out["received_quantity"] = np.where(late, 0.0, 0.9 * due["quantity"])
        out["delayed_quantity"] = np.where(late, due["quantity"], 0.0)
        out["delay_periods"] = 1
        return out

resolve

resolve(
    due: DataFrame, context: DeliveryContext
) -> Optional[DataFrame]

Decide what arrives of the deliveries due now.

Parameters:

Name Type Description Default
due DataFrame

One row per due delivery of this supplier, with order_id, the SKU column, supplier_id, source, order_period, scheduled_due_period, due_period and quantity.

required
context DeliveryContext

A DeliveryContext with a copy of the state, the period, the date, the supplier id, and a seeded random generator rng.

required

Returns:

Type Description
Optional[DataFrame]

None to let everything arrive, or a DataFrame with the index of

Optional[DataFrame]

due and received_quantity, optionally delayed_quantity and

Optional[DataFrame]

delay_periods (>= 1). The rest never arrives.

reset

reset() -> None

Reset run-local state. Called before every run.

get_config

get_config() -> dict

Settings recorded in the run manifest (a JSON-serialisable dict).

DeliveryContext dataclass

DeliveryContext(
    inventory: DataFrame,
    sku_column: str,
    period: int,
    date: Timestamp,
    supplier_id: object,
    rng: Optional[Generator],
)

Defensive information handed to DeliveryOutcome.resolve.

Attributes:

Name Type Description
inventory DataFrame

Copy of the state frame before this period's receipts.

sku_column str

SKU identifier column of the frames.

period int

State period in which the deliveries are due.

date Timestamp

Calendar date of that period.

supplier_id object

The supplier whose deliveries are due.

rng Optional[Generator]

numpy.random.Generator for this supplier, seeded from SupplyModel.random_seed (one stream per supplier, created anew for every run). None when the model has no seed.