README.md

← Back to submission · View raw on GitHub

# Synthetic Mine Throughput — SimPy Discrete-Event Simulation

A reproducible SimPy DES that estimates ore throughput to the primary crusher
over an 8-hour shift, identifies bottlenecks, and answers the operator's
decision questions through scenario analysis.

- **Model:** `mine_sim.py` (data loading, scenario resolution, routing, SimPy model)
- **Experiment harness:** `run_experiment.py` (scenarios × replications, aggregation, output files)
- **Conceptual model:** `conceptual_model.md`
- **Optional plot:** `plot_topology.py``topology.png`

---

## 1. Install dependencies

Python 3.11+ is required. From this folder:

```bash
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
```

The model itself needs only `simpy`, `numpy`, `scipy`, `pyyaml`, `networkx`.
`matplotlib` is needed only for the optional topology plot.

## 2. Run the simulation

```bash
python3 run_experiment.py
```

This runs all six required scenarios plus one optional agent-proposed scenario,
30 replications each (≈1–2 s total), and (re)writes the deliverables into this
folder:

| File | Contents |
|---|---|
| `results.csv` | one row per (scenario, replication) with all metrics |
| `summary.json` | per-scenario means, 95% CIs, bottleneck ranking, assumptions |
| `event_log.csv` | full event trace of replication 0 of each scenario |
| `run_metrics.json` | self-timed wall-clock runtime / return code |

Useful flags:

```bash
python3 run_experiment.py --scenarios baseline,ramp_closed   # subset
python3 run_experiment.py --replications 100                 # more reps
python3 run_experiment.py --log-replication 5                # log a different rep
python3 run_experiment.py --help
```

## 3. Reproduce the required scenario results

Results are deterministic. Replication `r` of every scenario uses
`seed = base_random_seed (12345) + r`, so the same seed index is reused across
scenarios (common random numbers) for paired comparison. Re-running
`python3 run_experiment.py` reproduces `results.csv` / `summary.json` exactly on
the same NumPy version. The optional topology figure is produced with:

```bash
python3 plot_topology.py        # writes topology.png
```

---

## 4. Conceptual model (summary)

Full detail is in `conceptual_model.md`. In brief: trucks are active SimPy
processes that cycle **load → haul → dump → return**. Loaders, the crusher and
the capacity-1 road segments (ramp, crusher approach, pit roads) are SimPy
resources. Trucks start empty at `PARK`, are dispatched to a loader, then cycle
between the pits and the crusher until the 480-minute shift clock stops the run.
Tonnage is counted only on **completed dump events** at the crusher.

## 5. Main assumptions

The full list (split into *data-derived* and *introduced*) is in
`conceptual_model.md §6` and `summary.json → key_assumptions`. The decision-
critical ones:

- Ore goes only to `CRUSH`; waste and maintenance are out of boundary; all
  equipment availability is 1.0 (no breakdowns).
- Loading/dumping times are truncated-normal (data means/SDs); per-edge travel
  time carries log-normal noise (mean 1, CV 0.10) on top of
  distance ÷ (max speed × truck speed factor), with loaded trucks at 0.85×.
- Capacity-1 roads are single-lane resources; the two directions of a physical
  road are independent one-way resources (as the data represents them).
- **Topology fact:** the narrow ramp `E03` is *not* on the loaded-haul cycle —
  loaded hauls run `pit → J3/J4 → CRUSH` and never cross it. The ramp connects
  the parking area to the production area, so it is only used for start-of-shift
  positioning (and only by South-bound trucks; the North pit is reached faster
  via the bypass even in the baseline).

## 6. Routing and dispatching logic

- **Routing — shortest time.** The road graph is a NetworkX `DiGraph` with edge
  weight = distance ÷ max speed. Routes are the shortest-time paths between the
  parking area, the two loaders and the crusher. A uniform truck speed factor
  does not change which path is shortest, so one graph serves both loaded and
  empty legs. If a required origin/destination pair has **no** path (e.g. a
  closure with no detour), the model raises `RouteError` and stops rather than
  silently under-reporting. Closing the ramp (`ramp_closed`) automatically
  reroutes traffic onto the bypass `J2 → J7 → J8 → …`.
- **Dispatching — nearest available loader.** When a truck needs an assignment,
  each loader is scored by the estimated time until *this* truck starts loading
  = travel time to the loader + (committed trucks already heading there × mean
  load time). The lowest score wins; ties and the configured secondary
  objective are broken by shortest expected full cycle time. A committed-
  assignment counter stops the whole fleet herding onto the nominally nearest
  loader and naturally balances trucks between the faster South face and the
  slower-but-closer North face.

---

## 7. Key results

30 replications per scenario, 8-hour shift. Tonnes are mean ± 95% CI.

| Scenario | Tonnes (95% CI) | t/h | Cycle (min) | Truck util | Crusher util | Crusher queue (min) |
|---|---|---:|---:|---:|---:|---:|
| **baseline** (8 trucks) | **12,953** [12,872–13,035] | 1,619 | 28.1 | 0.81 | 0.94 | 4.3 |
| trucks_4 | 7,753 [7,728–7,779] | 969 | 23.8 | 0.96 | 0.56 | 0.6 |
| trucks_12 | 13,033 [12,924–13,143] | 1,629 | 40.8 | 0.56 | 0.95 | 16.5 |
| ramp_upgrade | 12,983 [12,911–13,056] | 1,623 | 28.1 | 0.81 | 0.95 | 4.3 |
| crusher_slowdown | 6,513 [6,441–6,586] | 814 | 54.1 | 0.49 | 0.95 | 27.5 |
| ramp_closed | 12,803 [12,726–12,880] | 1,600 | 28.3 | 0.80 | 0.94 | 4.6 |
| _crusher_debottleneck_ (proposed) | 14,853 [14,805–14,902] | 1,857 | 24.7 | 0.91 | 0.54 | 0.05 |

## 8. Answers to the operational decision questions

**Q1 — Expected baseline throughput?**
**12,950 tonnes per 8-hour shift (1,619 t/h)**, 95% CI [12,872, 13,035],
about 129–130 truck loads. The crusher runs at 94% utilisation, so the shift is
close to the crusher's practical ceiling (~13,700 t at zero idle).

**Q2 — Likely bottlenecks?**
The **primary crusher `D_CRUSH`** is the binding constraint (94% utilisation,
4.3-minute average dump queue). Behind it sit the **loaders** (`L_S` 77%, `L_N`
70%) and then the **crusher approach `E05`** (44%). The narrow ramp is **not** a
steady-state bottleneck on this topology (≈3% utilisation) — see Q4/Q6.

**Q3 — Do more trucks help, or does the system saturate?**
It **saturates at the crusher**. Going 4→8 trucks adds +5,200 t (+67%); going
8→12 adds only **+80 t (+0.6%)**. At 12 trucks, truck utilisation collapses from
0.81 to 0.56 and the crusher queue rises to 16.5 min — the extra trucks simply
wait in line. **Eight trucks is already near the efficient fleet size;** adding
trucks does not buy crusher throughput.

**Q4 — Would improving the narrow ramp help?**
**Negligibly: +0.2%** (12,983 vs 12,953, CIs overlap). The loaded haul never
uses the ramp, so upgrading it only marginally speeds the South pit's
start-of-shift positioning. The ramp upgrade is **not** justified by throughput
(it may still be worth it for safety, cycle-time variance or redundancy).

**Q5 — Sensitivity to crusher service time?**
**Very high — this is the dominant lever.** Doubling the dump time (3.5 → 7.0
min) cuts throughput by **50%** to 6,513 t and pushes the crusher queue to
27.5 min and cycle time to 54 min. Throughput tracks the crusher service rate
almost one-for-one.

**Q6 — Operational impact of losing the main ramp?**
**Small for throughput: −1.2%** (12,803 vs 12,953). The model reroutes onto the
bypass automatically (all routes stay feasible). The real costs are
**start-of-shift delay** (South-bound trucks take the longer bypass) and **loss
of route redundancy**, not steady-state tonnage — because the ramp is off the
loaded-haul cycle.

**Optional proposed scenario — `crusher_debottleneck`.**
Adding a **second crusher dump bay and approach lane** lifts throughput
**+14.7% to 14,853 t** and moves the bottleneck to the South loader `L_S`
(utilisation 0.90). This is the **highest-leverage** intervention and confirms
the crusher is what caps the baseline.

## 9. Likely bottlenecks (ranked, baseline)

1. **`D_CRUSH` — primary crusher**, util 0.94, mean dump queue 4.3 min (binding).
2. **`L_S` — South loader**, util 0.77.
3. **`L_N` — North loader**, util 0.70.
4. **`E05_TO_CRUSH` — crusher approach**, util 0.44 (all trucks funnel through it).
5. South pit road `E09_*`, util ~0.40.

The ramp (`E03`) sits at ~0.03 utilisation — confirmed *not* a bottleneck here.

## 10. Limitations

See `summary.json → model_limitations` and `conceptual_model.md §6c`. Key points:
the ramp is off the loaded-haul cycle on this topology (so ramp scenarios move
throughput only a few percent); two-way single-lane roads are modelled as
independent one-way lanes (understates head-on contention); there is no
grade/rimpull haul-physics engine; loaders and the crusher are simple single
servers with no spotting time, breaks or breakdowns; truck utilisation excludes
queueing time. Results are conditional on these assumptions and should be read
as decision-support, not absolute predictions.

## 11. Suggested improvements and further scenarios

- **Debottleneck the crusher first** (proposed scenario): a second dump bay buys
  ~+15% throughput — far more than any fleet change.
- After that, **the South loader becomes binding** — evaluate a faster or second
  loader at the South face, or rebalancing dispatch toward the North face.
- **Combined ramp + crusher** scenario to confirm the ramp adds nothing even
  when the crusher is relieved.
- **Availability/breakdowns:** add MTBF/MTTR and refuelling at `MAINT` to test
  robustness (currently availability is 1.0).
- **Single-lane ramp realism:** model `E03` as one shared bidirectional lane to
  test head-on contention under heavier ramp use.
- **Payload / speed-factor sensitivity** to bound the throughput estimate.

---

## Run notes / interventions

Built and validated in a single autonomous session (plan → implement → smoke
test → 30-rep run → benchmark public tests + automated harness). Stochastic
behaviour, seed control and ≥30 replications are all in place; the six required
behavioural sanity checks pass. `token_usage.json` is left as `unknown` because
the harness used here does not expose exact token counts; `run_metrics.json` is
self-timed by `run_experiment.py`.