conceptual_model.md

← Back to submission · View raw on GitHub

# Conceptual Model — Synthetic Mine Ore Haulage (Benchmark 001)

A discrete-event simulation (SimPy) of an 8-hour ore haulage shift in a
synthetic open-pit mine. It estimates ore throughput to the primary crusher
and quantifies the effect of fleet size, ramp capacity, and crusher service
time. This document is the design record; numeric results live in
`summary.json` / `results.csv` and are discussed in `README.md`.

---

## 1. System boundary

### Included

- The ore-haulage cycle from the truck park (`PARK`) to the two ore faces
  (`LOAD_N`, `LOAD_S`) and to the primary crusher (`CRUSH`).
- The directed road graph from `nodes.csv` / `edges.csv`, including the main
  ramp and the western/eastern bypass routes.
- The two loaders (`L_N`, `L_S`) and the crusher (`D_CRUSH`) as service
  resources.
- The eight single-lane (capacity-1) road segments as shared resources.
- Stochastic travel, loading, and dumping times.

### Excluded (out of scope)

- **Waste haulage and the waste dump (`WASTE` / `D_WASTE`).** The task is ore
  to the crusher; waste edges remain in the graph for completeness but carry
  no traffic.
- **Maintenance / refuelling (`MAINT`).** No truck ever visits the
  maintenance bay; trucks are available for the whole shift.
- **Truck breakdowns and availability < 1.0.** `trucks.csv` lists
  `availability = 1.00`; we honour that, so the estimate is an upper bound on
  a fully-available fleet.
- **Operator behaviour:** shift handover, breaks, and manual dispatcher
  overrides are not modelled.
- **Downstream of the crusher:** no stockpile back-pressure or full-bin
  blocking; the crusher is always ready to receive.

---

## 2. Entities

| Entity | Role | Source |
|---|---|---|
| **Truck** | The only active (moving) entity. One SimPy *process* per truck runs the haul cycle: choose loader → travel empty → load → travel loaded → dump → repeat. | `trucks.csv` (T01–T12; first *N* used per scenario) |
| **Ore payload** | Not a separate process. Carried implicitly by a loaded truck and credited as `payload_tonnes` (100 t) at each completed dump. | `trucks.csv` |

All trucks are homogeneous: 100 t payload, empty speed factor 1.00, loaded
speed factor 0.85, and they all start at `PARK`.

---

## 3. Resources (what constrains the system)

Every constrained resource is a SimPy `Resource` with capacity 1 (one truck
served at a time, others queue FIFO).

| Resource | Count | Service parameters |
|---|---|---|
| **Loaders** `L_N`, `L_S` | 2 | truncated-normal load time; `L_N` 6.5 / 1.2 min, `L_S` 4.5 / 1.0 min |
| **Crusher** `D_CRUSH` | 1 | truncated-normal dump time 3.5 / 0.8 min (7.0 / 1.5 in `crusher_slowdown`) |
| **Capacity-1 edges** | 8 | one resource per *directed* single-lane segment |

The eight capacity-1 edges (the only `capacity = 1` rows in `edges.csv`) are:

- `E03_UP`, `E03_DOWN` — the narrow main ramp (J2 ↔ J3), the "intended
  transport bottleneck".
- `E05_TO_CRUSH`, `E05_FROM_CRUSH` — the crusher approach road (J4 ↔ CRUSH).
- `E07_TO_LOAD_N`, `E07_FROM_LOAD_N` — the North pit-face road (J5 ↔ LOAD_N).
- `E09_TO_LOAD_S`, `E09_FROM_LOAD_S` — the South pit-face road (J6 ↔ LOAD_S).

Each physical direction is modelled as its **own** capacity-1 resource (e.g.
`E03_UP` and `E03_DOWN` are independent), so opposing trucks do not contend
for a single shared lane. This is a documented simplification (see §6).
All other edges have `capacity = 999` and are modelled as plain time delays
(no resource contention).

---

## 4. Events

The per-truck cycle generates these events (all recorded to `event_log.csv`):

1. `dispatch` — truck released at `PARK` at t = 0.
2. `edge_enter` / `edge_leave` — acquiring / releasing each capacity-1 edge
   along a route (brackets the time the truck holds the single lane).
3. `arrive_loader` — truck reaches the chosen loading face and joins its queue.
4. `start_load` / `end_load` — loader service begins / ends.
5. `depart_loader` — truck leaves loaded.
6. `arrive_crusher` — truck reaches the crusher and joins its queue.
7. `start_dump` / `end_dump` — crusher service begins / ends. **Tonnes are
   credited at `end_dump`** (and only if it closes before the shift cut).
8. `depart_crusher` — truck leaves empty; the loop repeats.

Free-flow (capacity-999) edges advance time with a plain `timeout` and emit no
edge events, keeping the log focused on the constrained segments.

---

## 5. State variables

**Tracked during the run:**

- Truck location and loaded/empty status (implicit in the process position).
- Queue length at each loader, the crusher, and each capacity-1 edge
  (`count + len(queue)`), sampled into the event log.
- Resource busy time (loaders, crusher, edges) for utilisation.
- Per-truck productive time (sum of completed wait + service + travel phases).
- Per-truck completed cycle count and total cycle time.
- Completed dumps and cumulative tonnes.

**Derived at end-of-shift** (see §7).

---

## 6. Assumptions

### Derived from the data

- Eight `capacity = 1` edges become single-lane resources; everything with
  `capacity = 999` is unconstrained. (From `edges.csv`.)
- Loader and crusher mean/sd service times. (From `loaders.csv`,
  `dump_points.csv`.)
- Homogeneous 100 t fleet, speed factors 1.00 / 0.85, all starting at `PARK`,
  `availability = 1.00`. (From `trucks.csv`.)
- Edge free-flow time = `distance_m / (max_speed_kph × 1000 / 60)`. (From
  `edges.csv` geometry.)
- Scenario overrides (fleet size, ramp capacity/speed, ramp closure, crusher
  slowdown) come straight from the scenario YAML inheritance chain.

### Introduced (modelling choices not dictated by the data)

- **Hard shift cut** at t = 480 min via `env.run(until=480)`. Only dumps that
  close strictly before 480 count — "tonnes closed at shift end".
- **Routing:** static shortest-*time* routing, computed once per scenario by
  Dijkstra on free-flow edge times and recomputed when a scenario closes or
  upgrades edges. A truck commits to its path at dispatch.
- **Dispatch (dynamic loader choice):** an empty truck picks the loader that
  minimises `travel_to_loader + queue_len × mean_load_time + own_mean_load`,
  where `queue_len` counts trucks in service plus waiting; ties break by lower
  `loader_id`. The route is static, the loader choice is dynamic.
- **Stochasticity:** per-edge-traversal lognormal travel multiplier (mean 1,
  cv = 0.10); truncated-normal load/dump times floored at `max(0.1, sample)`.
- **All trucks released simultaneously at t = 0**; no warm-up.
- **Reproducibility:** per-replication seed = `12345 + replication_index`,
  with independent RNG streams per stochastic source.

### Limitations

- **Separate ramp directions.** `E03_UP` and `E03_DOWN` are two independent
  single-lane resources. A genuinely shared single lane would congest worse.
- **Static routing.** Trucks do not re-route around a queue that builds on a
  capacity-1 edge, so single-lane queueing is an upper bound; a smarter
  dispatcher could divert via the bypass.
- **Boundary under-count.** Productive time and tonnes accrue only on
  *completed* phases, so a phase straddling t = 480 contributes nothing —
  utilisation and the final partial cycle are slightly under-counted.
- **Crusher never blocks downstream;** no stockpile/bin back-pressure.
- **Homogeneous payload;** no ore blending or grade-dependent processing.
- **Free-flow edges have unlimited capacity;** no headway/following effects on
  multi-lane haul roads.
- **No warm-up trimming;** the empty-system start is a small (non-zero) bias.
- Node coordinates are used for visualisation only; road grade/bends beyond
  `distance_m` and `max_speed_kph` are not modelled.

---

## 7. Performance measures

Per replication (see `metrics.py`), then aggregated across 30 replications
with a Student-t (n − 1 = 29) 95% confidence interval (see `aggregate.py`):

| Measure | Definition |
|---|---|
| `total_tonnes_delivered` | `payload ×` completed dumps before the cut |
| `tonnes_per_hour` | `total_tonnes_delivered / 8` |
| `average_truck_cycle_time_min` | mean completed-cycle duration (first cycle `dispatch → end_dump`, then `end_dump → end_dump`) |
| `average_truck_utilisation` | mean over trucks of productive time / 480 |
| `crusher_utilisation`, `loader_utilisation` | busy time / 480 |
| `average_loader_queue_time_min`, `average_crusher_queue_time_min` | mean wait per service |
| `top_bottlenecks` | loaders + crusher + capacity-1 edges ranked by the composite score **utilisation × mean queue wait** (top 5) |

The composite bottleneck score deliberately combines *how busy* a resource is
with *how long trucks wait for it*, so a resource that is occasionally used but
causes long waits (the ramp) is separated from one that is the true throughput
ceiling (the crusher). See `README.md` for the interpretation.