src/mine_sim/narrative.py

← Back to submission · View raw on GitHub

"""Narrative constants surfaced in ``summary.json`` (and the README).

These three lists are the qualitative section of the deliverables:
``key_assumptions``, ``model_limitations``, and
``additional_scenarios_proposed``. They live in their own module so
:mod:`mine_sim.io_writers` stays focused on serialisation and well under the
file-size budget. Keep these in lockstep with ``conceptual_model.md`` and
``README.md``.
"""

from __future__ import annotations

#: Benchmark identifier surfaced at the top of ``summary.json``.
DEFAULT_BENCHMARK_ID: str = "001_synthetic_mine_throughput"

DEFAULT_KEY_ASSUMPTIONS: tuple[str, ...] = (
    "Time horizon: a hard cut at t=480 min via env.run(until=480). Only "
    "end_dump events that close strictly before 480 credit tonnes; loads or "
    "dumps still in progress at the cut are discarded (operator-facing "
    "'tonnes closed at shift end').",
    "Routing: static shortest-time per scenario via Dijkstra on free-flow "
    "edge times (distance_m / (max_speed_kph*1000/60)). Recomputed only when "
    "a scenario closes or upgrades edges; a truck commits to its path at "
    "dispatch and does not re-plan mid-leg.",
    "Capacity-1 directed edges are modelled as independent SimPy Resources, "
    "mirroring edges.csv literally: the 8 single-lane segments are E03_UP/"
    "DOWN (ramp), E05_TO/FROM_CRUSH (crusher approach), E07_TO/FROM_LOAD_N "
    "and E09_TO/FROM_LOAD_S (pit-face roads).",
    "Each physical direction is a separate resource: E03_UP and E03_DOWN are "
    "two capacity-1 lanes, so opposing trucks do not contend for one shared "
    "lane (no head-on blocking is modelled).",
    "Travel-time noise: an independent per-edge-traversal lognormal "
    "multiplier with mean 1 and cv=0.10 (sigma^2 = ln(1+cv^2), mu = "
    "-sigma^2/2). Empty trucks use empty_speed_factor=1.00, loaded trucks "
    "loaded_speed_factor=0.85.",
    "Loading and dumping times are drawn from normal_truncated(mean, sd) "
    "floored at max(0.1, sample) so durations are strictly positive without "
    "biasing the mean. Parameters: L_N 6.5/1.2, L_S 4.5/1.0, D_CRUSH 3.5/0.8 "
    "min (crusher_slowdown raises D_CRUSH to 7.0/1.5).",
    "Dispatch: each empty truck is sent to argmin(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.",
    "Initial conditions: all trucks released simultaneously at t=0 from PARK; "
    "no warmup, staged ramp-up, or shift-handover modelling.",
    "Throughput accounting: tonnes are credited at end_dump (not arrive or "
    "start_dump). Every completed dump credits exactly the truck payload "
    "(homogeneous 100 t fleet).",
    "Scope: only ore haulage to the primary crusher is simulated "
    "(LOAD_N/LOAD_S -> CRUSH). WASTE and MAINT remain in the graph for "
    "completeness but carry no truck traffic and are excluded from routing "
    "and dispatch.",
    "Reproducibility: per-replication seed = base_random_seed (12345) + "
    "replication_index, with independent numpy SeedSequence streams per "
    "stochastic source, so each replication is independently reproducible and "
    "the full run is deterministic.",
    "Uncertainty: 95% confidence intervals are Student-t with n-1 = 29 "
    "degrees of freedom over 30 replications per scenario.",
)

DEFAULT_MODEL_LIMITATIONS: tuple[str, ...] = (
    "No truck breakdowns, refuelling, or maintenance windows: truck "
    "availability is held at 1.00 for the whole 480-min shift, so the model "
    "estimates an upper-bound, fully-available throughput.",
    "Separate ramp directions: E03_UP and E03_DOWN are two independent "
    "single-lane resources. If the real ramp is one shared physical lane, "
    "true congestion (head-on / passing constraints) would be worse than "
    "modelled.",
    "Static routing: trucks commit to their shortest-time path at dispatch "
    "and never divert, even if a capacity-1 edge develops a queue. A real "
    "dispatcher could re-route through the bypass, so modelled queueing on "
    "single-lane edges is an upper bound.",
    "Boundary under-count: productive time and tonnes accrue only on "
    "completed phases. A load/dump/leg straddling t=480 contributes nothing, "
    "slightly under-counting both utilisation and the final cycle.",
    "Crusher never blocks downstream: D_CRUSH has no stockpile back-pressure, "
    "full-bin signal, or scheduled maintenance. Real surge-pile limits are "
    "not represented.",
    "Single homogeneous payload: every dump is exactly 100 t. Heterogeneous "
    "trucks, ore blending, and grade-dependent processing are not modelled.",
    "Free-flow (capacity 999) edges have effectively unlimited capacity: "
    "trucks do not interact on multi-lane haul roads, so headway and "
    "following-distance effects are ignored.",
    "No warmup trimming: the shift starts empty (all trucks at PARK, all "
    "queues empty). The empty-system bias is small because the fleet reaches "
    "steady state within a few cycles, but it is not zero.",
    "Deterministic speeds aside from edge noise: max-speeds and per-truck "
    "speed factors are fixed. Only the per-edge lognormal multiplier "
    "(cv=0.10) introduces travel variability — weather, dust, and visibility "
    "are not modelled.",
    "Node coordinates from nodes.csv are used for visualisation only; "
    "haul-road bends and grade are not propagated into travel time beyond the "
    "given distance_m and max_speed_kph.",
)

DEFAULT_ADDITIONAL_SCENARIOS_PROPOSED: tuple[str, ...] = (
    "trucks_12_ramp_upgrade (included here as the 7th scenario): combines a "
    "12-truck fleet with the ramp upgrade to test whether the two "
    "investments are complementary, substitutive, or independent. Because "
    "the loaded legs to the crusher never use the ramp, the upgrade adds "
    "little on top of the fleet increase — the pair is close to "
    "non-interacting.",
    "Crusher reliability: inject random short outages on D_CRUSH (e.g. 5 min "
    "every 60 min) to size the surge-pile buffer and quantify the cost of "
    "unplanned crusher downtime, since the crusher is the binding constraint.",
    "Dynamic re-routing: re-plan when a queue at a capacity-1 edge exceeds a "
    "threshold (e.g. 2 waiting trucks). Quantifies the upside available from "
    "a smarter dispatcher over the static-routing baseline.",
    "Crusher service-time upgrade: cut mean dump time from 3.5 to 2.5 min and "
    "re-run trucks_12 to confirm the crusher is the true ceiling and value a "
    "faster tip.",
    "Heterogeneous fleet: replace some 100 t trucks with 150 t trucks to "
    "trade cycle count for per-cycle payload and find the mix that maximises "
    "tonnes given the crusher constraint.",
    "Mid-shift loader outage: take L_N or L_S offline for 30 min mid-shift to "
    "size the single-loader fall-back tonnes and inform redundancy planning.",
)


__all__ = [
    "DEFAULT_ADDITIONAL_SCENARIOS_PROPOSED",
    "DEFAULT_BENCHMARK_ID",
    "DEFAULT_KEY_ASSUMPTIONS",
    "DEFAULT_MODEL_LIMITATIONS",
]