An simulation example using SimPy
Trucks -> stockpile -> reclaimer -> conveyor belt -> surge bin -> ship loader -> ship
The example shows the basic structure of material beining moved from a mining site to a ship. The purpose is for showing the baseic / simplified setup in SimPy.
Each entity (truck, reclaimer, convery, ship loader, conveyor process) is essentially running a While loop, and continously fetch from upstream, and put to downstream, at a given rate / batch size. All operations (put, get) and process time (timeout(x)) are generated by the python YIELD command, so that it can pause and resume. The YIELD creates a generater of the events (a process), and the SimPy env.processs() registers the process. Each process runs until it yields, then SimPy decides which process runs next based on simulation time and conditions.
Processes in SimPy
Each process is a generator function (a coroutine) that yields events like:
env.timeout(t) → wait t units of simulated time / Time-based event
container.get(n) or container.put(n) → wait until resource is available / Resource-based events
When a process yields, it pauses and tells the environment: “I will be ready at this event/time.”
Each event knows its condition for being triggered. At each simulation step:
SimPy checks the next event in simulated time.
For time-based events, the clock moves forward.
For resource events, it checks if the condition is satisfied:
If yes → trigger event → resume the process.
If no → keep the event pending until the condition becomes true.
In the example below, the trucking, reclaiming, conveying and ship loading are processes. the conveying is modeled as a stream (with delta time * rate), while others are modeled as delta size (rate). A converyor buffer (like a surge bin) is also created for between reclaimer and conveyor_stream. The stockpile, surge bin and ship are considered as Containers.
In reality, those processes and containers should handle more details such as availability, breakdown, movement, variable rate, etc. See after the script for some realistic options.
import simpy
import random
# ------------------------------
# Simulation parameters
# ------------------------------
NUM_TRUCKS = 3
STOCKPILE_CAPACITY = 1000
SURGE_BIN_CAPACITY = 500
SIM_TIME = 200 # total simulation time in minutes
# Processing rates (units per minute)
TRUCK_LOAD = 50 # units per truck load
RECLAIMER_RATE = 40 # units per reclaim cycle
CONVEYOR_RATE = 30 # units per minute (continuous flow)
SHIP_LOADER_RATE = 25 # units per ship loader cycle
# ------------------------------
# Truck Process
# ------------------------------
def truck(env, name, stockpile):
"""
Truck delivers ore to the stockpile in batches.
Travel time to stockpile is random between 10-30 minutes.
"""
while True:
travel_time = random.randint(10, 30) # simulate truck driving
yield env.timeout(travel_time)
print(f'{env.now:.1f}: {name} arrives at stockpile with {TRUCK_LOAD} units')
# Unload ore to stockpile (Container automatically handles capacity)
yield stockpile.put(TRUCK_LOAD)
print(f'{env.now:.1f}: {name} unloads at stockpile (Stockpile level: {stockpile.level})')
# ------------------------------
# Reclaimer Process
# ------------------------------
def reclaimer(env, stockpile, conveyor):
"""
Reclaimer moves ore from stockpile to conveyor in batches.
If stockpile has less than the batch size, it waits 1 minute.
"""
while True:
if stockpile.level >= RECLAIMER_RATE:
# Take ore from stockpile
yield stockpile.get(RECLAIMER_RATE)
# Time to reclaim (1 min per batch for simplicity)
yield env.timeout(1)
print(f'{env.now:.1f}: Reclaimer moves {RECLAIMER_RATE} units to conveyor')
# Put ore onto the conveyor (container simulates buffer before stream)
yield conveyor.put(RECLAIMER_RATE)
else:
# Not enough ore, wait and try again
yield env.timeout(1)
# ------------------------------
# Conveyor Process (Stream Approach)
# ------------------------------
def conveyor_stream(env, conveyor, surge_bin, dt=0.1):
"""
Conveyor moves ore continuously from conveyor buffer to surge bin.
Stream approach: small increments every dt minutes.
"""
while True:
# Determine amount to move in this small time step
transfer = min(CONVEYOR_RATE * dt, conveyor.level, surge_bin.capacity - surge_bin.level)
if transfer > 0:
# Remove from conveyor container
yield conveyor.get(transfer)
# Add to surge bin container
yield surge_bin.put(transfer)
# Print status
print(f'{env.now:.1f}: Conveyor moves {transfer:.1f} units to surge bin '
f'(Surge bin level: {surge_bin.level:.1f})')
# Advance simulation by small delta time
yield env.timeout(dt)
# ------------------------------
# Ship Loader Process
# ------------------------------
def ship_loader(env, surge_bin, ship):
"""
Ship loader moves ore from surge bin to ship in batches.
If surge bin has less than batch size, wait 1 minute.
"""
while True:
if surge_bin.level >= SHIP_LOADER_RATE:
yield surge_bin.get(SHIP_LOADER_RATE)
yield env.timeout(1)
print(f'{env.now:.1f}: Ship loader moves {SHIP_LOADER_RATE} units to ship')
yield ship.put(SHIP_LOADER_RATE)
else:
yield env.timeout(1)
# ------------------------------
# Main Simulation
# ------------------------------
def main():
# Create the simulation environment
env = simpy.Environment()
# ------------------------------
# Create Containers
# ------------------------------
stockpile = simpy.Container(env, capacity=STOCKPILE_CAPACITY, init=0)
conveyor = simpy.Container(env, capacity=STOCKPILE_CAPACITY, init=0) # buffer before stream
surge_bin = simpy.Container(env, capacity=SURGE_BIN_CAPACITY, init=0)
ship = simpy.Container(env, capacity=float('inf'), init=0) # infinite capacity for simplicity
# ------------------------------
# Start Truck Processes
# ------------------------------
for i in range(NUM_TRUCKS):
env.process(truck(env, f'Truck {i+1}', stockpile))
# ------------------------------
# Start Other Processes
# ------------------------------
env.process(reclaimer(env, stockpile, conveyor))
env.process(conveyor_stream(env, conveyor, surge_bin)) # conveyor modeled as stream
env.process(ship_loader(env, surge_bin, ship))
# ------------------------------
# Run Simulation
# ------------------------------
env.run(until=SIM_TIME)
print(f'\nTotal ore shipped: {ship.level:.1f} units')
# ------------------------------
# Entry Point
# ------------------------------
if __name__ == "__main__":
main()
result
...
194.0: Truck 3 arrives at stockpile with 50 units
194.0: Truck 3 unloads at stockpile (Stockpile level: 10)
195.0: Reclaimer moves 40 units to conveyor
195.1: Conveyor moves 3.0 units to surge bin (Surge bin level: 3.0)
195.2: Conveyor moves 3.0 units to surge bin (Surge bin level: 6.0)
195.3: Conveyor moves 3.0 units to surge bin (Surge bin level: 9.0)
195.4: Conveyor moves 3.0 units to surge bin (Surge bin level: 12.0)
195.5: Conveyor moves 3.0 units to surge bin (Surge bin level: 15.0)
195.6: Conveyor moves 3.0 units to surge bin (Surge bin level: 18.0)
195.7: Conveyor moves 3.0 units to surge bin (Surge bin level: 21.0)
195.8: Conveyor moves 3.0 units to surge bin (Surge bin level: 24.0)
195.9: Conveyor moves 3.0 units to surge bin (Surge bin level: 27.0)
196.0: Conveyor moves 3.0 units to surge bin (Surge bin level: 30.0)
196.1: Conveyor moves 3.0 units to surge bin (Surge bin level: 8.0)
196.2: Conveyor moves 3.0 units to surge bin (Surge bin level: 11.0)
196.3: Conveyor moves 3.0 units to surge bin (Surge bin level: 14.0)
196.4: Conveyor moves 1.0 units to surge bin (Surge bin level: 15.0)
197.0: Ship loader moves 25 units to ship
Total ore shipped: 2425.0 units
Reality
Several other components in the simulation can be modeled more realistically depending on the level of fidelity you want.
Those can be programmed into the python functions to simulate more realistic operations.
1. Trucks
Current: Trucks deliver fixed batches with random travel time.
More realistic options:
Include loading time at the mine and unloading time at the stockpile.
Model truck availability: breakdowns, maintenance, or queuing if the stockpile is full.
Variable capacity trucks (different trucks carry different loads).
Travel delays depending on congestion on haul roads.
2. Stockpile
Current: Modeled as a simple simpy.Container.
More realistic options:
Segregation & blending: Some mines track ore grade by source; stockpiles may mix material differently.
Stacker/reclaimer movement: Add stacker placement logic (where the ore is deposited) and reclaimer location logic (which stockpile region is reclaimed first).
Capacity limits by area: Instead of a single container, split stockpile into sections.
3. Reclaimer
Current: Moves fixed batches from stockpile to conveyor.
More realistic options:
Travel & positioning: Reclaimers are large machines that take time to move along the stockpile.
Variable rate: Reclaimer speed can depend on stack height, ore type, or congestion on conveyor.
Queuing for conveyor: If conveyor is full, reclaimer may stop.
4. Conveyor
Current: Either batch or stream.
More realistic options:
Capacity limits: Conveyor can jam if downstream is blocked.
Variable speed: Conveyors often slow down if approaching a surge bin or ship loader that’s full.
Breakdowns: Random downtime events.
5. Surge Bin
Current: simpy.Container with fixed capacity.
More realistic options:
Overflow and underflow: Real surge bins can overflow or starve the ship loader.
Variable draw rate: Ship loader may draw faster/slower depending on ship and environmental constraints.
6. Ship Loader
Current: Moves fixed batches to ship.
More realistic options:
Variable loading rate based on ship requirements or berth restrictions.
Downtime for maintenance or shift changes.
Queuing: Only one ship can be loaded at a berth at a time.
Reposition for different ships at the same port.
7. Ship
Current: Infinite capacity container.
More realistic options:
Finite ship capacity: Stop loading when full.
Berth availability & scheduling: Ship might not be ready for loading immediately.
Loading interruptions: Weather, tides, or mechanical issues.