conceptual_model.md

← Back to submission · View raw on GitHub

# Conceptual model — synthetic mine throughput

This document describes the discrete-event simulation built for benchmark
`001_synthetic_mine_throughput`. The implementation is in `src/` (model,
simulation, experiment, analysis) and is driven by `run.py`. The model
follows the data exactly where the data is unambiguous, and documents every
assumption it has had to introduce.

## 1. System boundary

**Included:**

- One 8-hour ore haulage shift (480 minutes simulated time per replication).
- Movement of ore from `LOAD_N` and `LOAD_S` to the primary crusher `CRUSH`.
- Truck cycles: empty travel from current node to a chosen loader, loading,
  loaded travel to the crusher, dumping, then dispatch to the next loader.
- Capacity-constrained roads (E03 ramp segments, E05 crusher approach,
  E07 north-pit access, E09 south-pit access) modelled as resources.
- Loaders (`L_N`, `L_S`) and the crusher dump point (`D_CRUSH`) as resources.
- Stochastic loading time, dumping time, and per-edge travel time.

**Excluded:**

- Waste and maintenance haulage. Baseline `production.dump_destination` is
  `CRUSH` — only ore movement is counted. `WASTE` and `MAINT` are present in
  the topology but no truck routes through them in any required scenario.
- Truck breakdowns, fuelling, operator breaks, shift handovers.
- Time-of-day effects (weather, lighting), grade-aware truck dynamics, and
  driver behaviour.
- Multi-shift continuity. Each replication starts with an empty system and
  trucks at PARK.

## 2. Entities

**Trucks (T01 … T0N)** are the only active entities. Each truck is a
SimPy process carrying:

- current node,
- loaded / empty status,
- payload (100 t when loaded, 0 t empty),
- per-truck statistics (cycles completed, busy time, queueing time,
  cycle-start timestamps).

Ore payloads are carried by trucks rather than modelled as independent
entities — the simulation only needs to track tonnes delivered, which is
incremented at every `dump_end` event.

## 3. Resources

| Resource          | SimPy capacity | Notes                                           |
| ----------------- | -------------- | ----------------------------------------------- |
| `L_N` loader      | 1              | North pit, mean load 6.5 min (slower face)      |
| `L_S` loader      | 1              | South pit, mean load 4.5 min (faster face)      |
| `D_CRUSH` crusher | 1              | Mean dump 3.5 min (7.0 min in `crusher_slowdown`) |
| `E03_UP`          | 1              | Narrow uphill ramp, designated bottleneck       |
| `E03_DOWN`        | 1              | Narrow downhill ramp                            |
| `E05_TO_CRUSH`    | 1              | Crusher approach (inbound)                      |
| `E05_FROM_CRUSH`  | 1              | Crusher approach (outbound)                     |
| `E07_TO_LOAD_N`   | 1              | Single-lane north-pit face access (in)          |
| `E07_FROM_LOAD_N` | 1              | Single-lane north-pit face access (out)         |
| `E09_TO_LOAD_S`   | 1              | Single-lane south-pit face access (in)          |
| `E09_FROM_LOAD_S` | 1              | Single-lane south-pit face access (out)         |

All other roads have capacity 999 (declared in `edges.csv`). They are
*not* modelled as SimPy resources — that would add bookkeeping overhead with
no queueing realism. They are simple `env.timeout(travel_min)` segments.

## 4. Events

Logged event types (written to `event_log.csv`):

- `truck_dispatched` — process started, truck released from PARK.
- `edge_queue_join` — truck arrives at a constrained edge resource.
- `edge_entered` — truck obtains the edge resource and begins travelling.
- `edge_exited` — truck has finished traversing the edge.
- `edge_traversed_unconstrained` — emitted once per traversal of an
  unconstrained edge; carries `from`, `to`, `resource_id` (the edge ID).
- `arrived_at_loader` — truck reached its dispatched loader node.
- `load_start` / `load_end` — bracket the loading service.
- `arrived_at_crusher` — truck reached the dump node loaded.
- `dump_start` / `dump_end` — bracket the dump service. Tonnes are credited
  at `dump_end` only.
- `shift_end_truncated` — truck process exits because the shift ended.
- `routing_error` — emitted only if the topology cannot route the truck
  (intentional fail-loud).

## 5. State variables

- Per-truck: location, loaded flag, payload, cycles completed, busy time,
  queue time, cycle-start timestamps.
- Per-resource: cumulative busy time, cumulative queue-wait time,
  queue-event count, queue-length samples (timestamped).
- System-wide: total tonnes delivered, total dump events, simulation clock.

## 6. Assumptions

### 6.1 Data-derived assumptions

- **The directed graph is authoritative.** Each `edges.csv` row is a single
  directed segment with its own capacity. `E03_UP` and `E03_DOWN` are
  modelled as independent capacity-1 resources, exactly as the data designer
  encoded them. The dataset metadata describes the up/down split as a
  "simplification" of the real shared physical channel.
- **`dump_points.csv` is authoritative for crusher service time.** The
  `crusher_slowdown` scenario also overrides `nodes.CRUSH.service_time_*`,
  which we apply for consistency, but the simulation reads from
  `dump_points`.
- **`production.dump_destination = CRUSH`** in baseline.yaml is honoured —
  the simulation routes all ore to `CRUSH` and ignores `WASTE`.
- **Loader and crusher capacities are 1** (one truck served at a time),
  consistent with `loaders.csv` and `dump_points.csv`.
- **Truck count and start node** come from `fleet.truck_count` and
  `trucks.csv` (`start_node = PARK`). The first N trucks of `trucks.csv`
  are used.

### 6.2 Introduced assumptions

- **Capacity threshold for SimPy resources = 100.** Any edge with
  declared capacity ≥ 100 is treated as effectively unlimited and is not
  modelled as a SimPy resource. This avoids spurious overhead on the
  capacity-999 edges while still honouring `road_capacity_enabled = true`
  for the genuinely narrow segments.
- **Truncated normal load and dump times.** Sampled from `N(mean, sd)` and
  rejected (resampled) below `0.1 * mean`; the floor prevents non-physical
  near-zero samples. With `sd << mean` rejection is rare (≤ 1 % of
  samples).
- **Lognormal travel-noise multiplier with unit mean.** Each edge
  traversal multiplies `travel_min` by `lognormal(μ, σ)` with
  `σ² = ln(1 + CV²)` and `μ = -σ²/2`, so `E[multiplier] = 1`. CV is read
  from `stochasticity.travel_time_noise_cv` (0.10 in baseline). This avoids
  the bias of a naive `lognormal(0, σ)` whose mean is `exp(σ²/2) > 1`.
- **Random initial dispatch stagger** uniformly over [0, 60] s per truck.
  Without this, all 8–12 trucks at PARK would request the same edge at
  `t = 0` and SimPy's deterministic insertion-order tie-break would create
  artefacts. The stagger is small relative to a multi-minute cycle and does
  not violate `warmup_minutes = 0`, which refers to statistical warm-up
  rather than initial conditions. Documented in `key_assumptions`.
- **Routing = Dijkstra shortest travel time.** Edge weights are
  `distance_m / (max_speed_kph * 1000 / 60)`. Routes are recomputed from
  scratch at every dispatch (so closures and per-scenario speed overrides
  apply). Trucks already en route do not re-plan.
- **Dispatching = `nearest_available_loader`.** Score
  `= travel_time_to_loader + queue_size * mean_load_time_loader`.
  Tie-breaker: shorter expected return cycle (loader → crusher).
- **End-of-shift policy.** No new loader requests start after
  `shift_end_min = 480`. Trucks already loaded complete their travel and
  dump (otherwise tonnes physically held in trucks would be discarded).
  Trucks empty when the shift ends finish their current edge then exit.
  Tonnes are counted only at `dump_end`.
- **Utilisation accounting clipped at the shift boundary.** Resource and
  truck busy-time accumulators record only the portion of each operation
  that falls within `[0, 480]` min, so utilisation values lie in `[0, 1]`
  even when tail dumping continues past 480 min.

### 6.3 Limitations

- E03_UP / E03_DOWN as two independent capacity-1 resources slightly
  understates contention versus a single shared bidirectional channel. A
  more conservative variant could be modelled as a single resource with
  direction switching, but the data does not provide switching parameters.
- Uniform speed factors per edge — gradient and curvature are not modelled
  separately beyond what is encoded in `max_speed_kph`.
- Travel-time noise is i.i.d. per edge traversal, with no temporal
  autocorrelation (no weather front, no shift fatigue effects).
- Loaders and the crusher are 100 % available within the shift
  (no breakdowns, no operator switches).
- Trucks already en route do not reroute when a downstream queue grows;
  routing decisions are made only at dispatch time.

## 7. Performance measures

Reported per scenario (mean across 30 replications, ± 95 % CI by Student's
t-distribution with df = 29):

- `total_tonnes_delivered` — sum of payloads at completed `dump_end`.
- `tonnes_per_hour``total_tonnes_delivered / 8`.
- `average_truck_cycle_time_min` — mean inter-cycle-start gap, averaged
  across all trucks and all cycles.
- `average_truck_utilisation` — fraction of shift each truck spent
  travelling, loading or dumping (excludes queueing).
- `crusher_utilisation` — fraction of shift the crusher was serving a
  truck, clipped at the shift boundary.
- `loader_utilisation` — same, per loader (`L_N`, `L_S`).
- `average_loader_queue_time_min` — mean queue-wait at loaders.
- `average_crusher_queue_time_min` — mean queue-wait at the crusher.
- `top_bottlenecks` — resources ranked by mean queue-wait time, used to
  identify the binding constraint in each scenario.