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 |
required |
allocation
|
Optional[SupplierAllocation]
|
|
None
|
random_seed
|
Optional[int]
|
Integer seed for random lead times. Required when any
supplier has a random lead time; may be |
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
¶
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
|
required |
partial_deliveries
|
Sequence[tuple[int, float]] | None
|
Optional
sequence of |
None
|
delivery
|
DeliveryOutcome | None
|
Optional outcome that decides, at
each due period, what actually arrives (see |
None
|
Example
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 |
required |
context
|
AllocationContext
|
An |
required |
Returns:
| Type | Description |
|---|---|
DataFrame
|
One row per SKU and supplier with the SKU column, |
DataFrame
|
|
validate
¶
Check, before a run, that the allocation fits the suppliers and SKUs.
SupplierShares
¶
Bases: SupplierAllocation
Fixed fractional split of every order across suppliers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
shares
|
Mapping
|
Mapping |
required |
by_sku
|
Optional[Mapping]
|
Optional mapping |
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
allocate
¶
allocate(
orders: DataFrame, context: AllocationContext
) -> DataFrame
Split each SKU's order by the configured shares.
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 |
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 |
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 |
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 |
required |
context
|
DeliveryContext
|
A |
required |
Returns:
| Type | Description |
|---|---|
Optional[DataFrame]
|
|
Optional[DataFrame]
|
|
Optional[DataFrame]
|
|
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]
|
|