# Conceptual Model — Asia–Europe Container Shipping Throughput
This document defines the system being modelled, the entities and resources,
the events and state, and — kept deliberately separate — which assumptions are
**derived from the supplied data** and which are **introduced** modelling
choices. It closes with the model's limitations.
The implementation is a genuine SimPy discrete-event simulation
(`container_sim/simulation.py`); nothing here is a spreadsheet average.
---
## 1. System boundary
**In scope.** The liner service that lifts laden containers from two Asian
origin ports (Shanghai `CNSHA`, Singapore `SGSIN`), sails them along a directed
maritime network to the **primary** European import port (Rotterdam `NLRTM`),
discharges them, and returns the vessels empty (ballast) to reload. The horizon
is a 180-day planning window.
**Objective (the thing we measure).** Total TEU discharged at Rotterdam over the
horizon, and the rate at which it is delivered.
**Out of scope / boundary conditions.**
- **Hamburg (`DEHAM`)** is a reachable secondary port but *not* the delivery
objective; the service never calls there. It is a distractor sink and is
excluded from routing.
- **Cargo demand** is treated as unlimited: a vessel always finds a full load at
its origin (the operator's objective is to *maximise* TEU to Rotterdam, so the
binding limits are ships and ports, not cargo availability).
- **Backhaul** (Europe→Asia cargo) is out of scope; the return leg is ballast.
---
## 2. Entities
| Entity | Count | Description |
|---|---|---|
| **Vessel** | scenario `fleet.vessel_count` (8 / 12 / 20) | A neo-panamax of 10 000 TEU, service speed 19 kn, with a home port. Moves through a repeating round-trip *process*. |
| **Voyage (round trip)** | emergent | load → sail laden → wait+discharge → sail ballast → (maintenance). One delivery of 10 000 TEU per voyage. |
The fleet for a scenario is the **first `vessel_count` rows** of `vessels.csv`
(file order alternates SGSIN/CNSHA home ports, so any prefix is a balanced
split: 4/4, 6/6, 10/10).
---
## 3. Resources (the contended capacity)
| Resource | SimPy object | Capacity | Source |
|---|---|---|---|
| **Rotterdam discharge berth** | `Resource` | `berth_count` = **1** | `berths.csv` `B_RTM` |
| Shanghai / Singapore load berths | `Resource` | `berth_count` = 3 each | `berths.csv` `B_SHA`, `B_SIN` |
| **Suez Canal** (NB and SB) | `Resource` per directed leg | `capacity` = **3** (12 in `canal_upgrade`) | `sea_legs.csv` `L06_*` |
| Open-water / strait / coastal legs | none (pure delay) | `capacity` = 999 ⇒ unconstrained | `sea_legs.csv` |
A leg is treated as a contended chokepoint only if its capacity is below a
threshold (100); in this network **only the canal qualifies**. Everything coded
999 is open water and modelled as a transit delay with no queue.
**Handling rate.** A port advertises `berth_count` and `crane_count`. Cranes are
modelled as evenly pre-assigned to berths, so one berthed vessel is worked at
`(crane_count / berth_count) × moves_per_hour_per_crane` TEU/h and up to
`berth_count` vessels are served in parallel. The terminal's **aggregate**
discharge capacity is therefore exactly `crane_count × moves_per_hour_per_crane`,
and cranes are never double-counted. At Rotterdam this is `4 × 28 = 112` TEU/h on
the single berth (≈ 89 h to discharge 10 000 TEU).
---
## 4. Events
Per voyage, the model emits (and logs) these events:
`LOAD_START` → `DEPART_ORIGIN` → [`CANAL_ENTER` → `CANAL_EXIT`]\* →
`ARRIVE_DEST` → `DISCHARGE_START` → **`DELIVER`** → [canal on return]\* →
`ARRIVE_HOME`.
`DELIVER` carries the TEU discharged; summing `DELIVER.teu` over the event log
reconstructs delivered throughput exactly (verified for every scenario).
---
## 5. State
- **Per vessel:** position in its life-cycle, laden/ballast, current TEU on board,
cycle start time.
- **Per resource:** number of units in use / queued (SimPy-managed).
- **Per replication (accumulated):** list of deliveries (time, TEU), anchorage
waits, origin waits, completed cycle times, and busy-hours per port and per
canal leg (used to derive utilisation).
---
## 6. Routing and dispatching logic
- **Routing = shortest *time* on the directed graph.** Each leg's free-flow time
is `distance_nm / min(service_speed × speed_factor, leg_max_speed)`. Dijkstra
finds the least-time path and is **recomputed per scenario**, so a closed leg
simply disappears. At baseline the Suez path beats the Cape path; when the canal
is closed the Cape path (`IOX→CAPE→WAFR→NLRTM`, +3 200 nm) becomes shortest.
- **Fail-clear.** If no origin→Rotterdam (or return) path exists — e.g. the canal
is closed *and* the Cape reroute is forbidden — the model raises `RouteError`
and the scenario is recorded as **failed** with a reason, never as a low number.
- **Dispatching = return to home port** (`baseline.yaml`). Each vessel repeatedly
serves its own origin; the tie-breaker (`shortest_expected_cycle_time`) is moot
because each vessel has a single home port.
---
## 7. Stochasticity, seeds, and the transient
- **Leg times:** multiplicative lognormal noise with unit mean and cv = 0.10
(`leg_time_noise_cv`), applied to every leg traversal.
- **Handling times:** truncated normal about the mean `TEU / rate`, cv = 0.10,
floored at 0.1 h (truncation essentially never binds at this cv).
- **Seeds / reproducibility:** each vessel draws from its own stream seeded by
`(base_random_seed, replication, vessel_index)`. Re-running a replication is
bit-identical (`python -m container_sim verify`). Because all scenarios share
the baseline seed, the *same* vessel sees the *same* noise across scenarios —
**common random numbers**, which reduces the variance of scenario differences.
- **Transient handling.** All vessels start empty at their home port at *t* = 0
(a cold start). The pipeline is empty until the first vessels transit (~day 28),
so we report **(a)** total TEU over the full horizon (what the operator
receives) and **(b)** a post-warmup rate `teu_per_day` (deliveries after day 14,
the scenario `warmup_days`) and a fully-ramped **second-half** rate
`teu_per_day_second_half`. The second-half rate is the cleanest steady-state
estimate.
---
## 8. Assumptions — data-derived vs introduced
### 8a. Derived directly from the supplied data
- Network topology, leg distances, per-leg speed limits, leg capacities and
`closed` flags (`sea_legs.csv` + scenario `leg_overrides`).
- Vessel capacity (10 000 TEU), service speed (19 kn), laden/ballast factors
(1.00), per-vessel availability, and home ports (`vessels.csv`).
- Berth counts, crane counts, moves/h/crane (`berths.csv` + `berth_overrides`).
- Horizon (180 d), replications (30), base seed (20260603), warmup (14 d),
load ports, delivery port, routing flags, and noise cv (scenario YAML).
### 8b. Introduced modelling choices (with justification)
1. **Vessels always sail full** (10 000 TEU loaded, 10 000 discharged). The brief's
objective is to *maximise* TEU to Rotterdam with unlimited export demand, so
the constraint is capacity, not cargo.
2. **One move ≈ one TEU** (stated in the `B_RTM` metadata) — discharge time =
`TEU / (cranes × moves/h)`.
3. **Even crane-to-berth split** (Section 3). Makes Rotterdam exact (112 TEU/h);
at origins it splits 5 cranes over 3 berths. Aggregate port capacity is exact.
4. **Capacity-constrained leg ⇔ capacity < 100** ⇒ only the canal is a queueing
resource; open water is a pure delay.
5. **Discharge-only destination**; the return leg is ballast (factors are 1.0, so
ballast speed equals laden speed here).
6. **Availability → inter-voyage maintenance downtime** calibrated so long-run
availability equals the stated value: `downtime = cycle_time × (1−A)/A`
(zero for A = 1).
7. **Cold start at home ports** at *t* = 0 (Section 7).
8. **Draft is ignored** — no leg or port in the data carries a depth limit, so the
`max_draft_m` column is non-binding (a deliberate distractor; documented, not used).
9. **The two canal directions are modelled as two independent directed resources**
(mirroring the two `L06` rows) rather than one shared channel; at ~3 %
utilisation the distinction is immaterial.
---
## 9. Limitations
- **Cold-start synchronisation.** Starting all vessels together produces a
visible "staircase" in the cumulative-delivery curve (clustered arrivals) and a
pipeline fill (~28 d) longer than the 14-day warmup. We mitigate by reporting
the second-half steady-state rate; a staggered/random initial phase would smooth
this but is not specified by the data.
- **Low throughput variance.** Delivered TEU is quantised in whole 10 000-TEU
loads; at the specified cv = 0.10 the *count* of completed voyages is robust, so
the `total_teu` confidence intervals are tight while cycle-time and anchorage-wait
intervals are wider. This is a finding (throughput here is constraint-limited,
not noise-limited), not a missing source of randomness.
- **Origin dwell is conservative.** The even crane-split gives a slow per-ship
origin load (~7.8 d); origins nonetheless stay at 26–29 % utilisation, far from
binding, so this does not affect any conclusion. Rotterdam — the binding
resource — is exact under any crane-allocation convention because it has one berth.
- **Unmodelled real-world stochastics** (weather closures, demand seasonality,
unplanned breakdowns beyond the stated availability) are out of scope; the model
reflects only the variability the brief specifies.
conceptual_model.md
← Back to submission · View raw on GitHub