"""Event-log record schema for the mine throughput simulation.
``event_log.csv`` carries a fixed column set (prompt.md)::
time_min, replication, scenario_id, truck_id, event_type, from_node,
to_node, location, loaded, payload_tonnes, resource_id, queue_length
Defining the schema as a frozen dataclass means:
* The simulation cannot accidentally emit a malformed record โ every field
is named at construction time.
* The CSV header order is centralised here, derived from the dataclass, so
it cannot drift between the writer and any reader (e.g. the animation).
* Tests can construct synthetic events without importing SimPy.
Records are append-only and exist purely for traceability and animation.
KPI computation runs on a separate pure-Python accumulator
(:mod:`mine_sim.metrics`) so the two paths evolve independently.
"""
from __future__ import annotations
from dataclasses import dataclass, fields
from typing import Final
# ---------------------------------------------------------------------------
# Canonical event-type strings. The simulation must use these exact values so
# downstream tooling (event_log.csv, animation, tests) can pattern-match
# without copy-paste typos.
# ---------------------------------------------------------------------------
EVENT_DISPATCH: Final[str] = "dispatch"
EVENT_ARRIVE_LOADER: Final[str] = "arrive_loader"
EVENT_START_LOAD: Final[str] = "start_load"
EVENT_END_LOAD: Final[str] = "end_load"
EVENT_DEPART_LOADER: Final[str] = "depart_loader"
EVENT_ARRIVE_CRUSHER: Final[str] = "arrive_crusher"
EVENT_START_DUMP: Final[str] = "start_dump"
EVENT_END_DUMP: Final[str] = "end_dump"
EVENT_DEPART_CRUSHER: Final[str] = "depart_crusher"
EVENT_EDGE_ENTER: Final[str] = "edge_enter"
EVENT_EDGE_LEAVE: Final[str] = "edge_leave"
@dataclass(frozen=True)
class EventRecord:
"""One row of ``event_log.csv``.
Fields that do not apply to a particular event type carry ``None``
(rendered as the empty string / blank in CSV).
"""
time_min: float
replication: int
scenario_id: str
truck_id: str
event_type: str
from_node: str | None
to_node: str | None
location: str | None
loaded: bool | None
payload_tonnes: float | None
resource_id: str | None
queue_length: int | None
def to_csv_row(self) -> dict[str, object]:
"""Render the record as a flat dict suitable for ``csv.DictWriter``."""
return {
"time_min": round(self.time_min, 6),
"replication": self.replication,
"scenario_id": self.scenario_id,
"truck_id": self.truck_id,
"event_type": self.event_type,
"from_node": _blank_if_none(self.from_node),
"to_node": _blank_if_none(self.to_node),
"location": _blank_if_none(self.location),
"loaded": _bool_to_csv(self.loaded),
"payload_tonnes": (
round(self.payload_tonnes, 6)
if self.payload_tonnes is not None
else ""
),
"resource_id": _blank_if_none(self.resource_id),
"queue_length": (
int(self.queue_length) if self.queue_length is not None else ""
),
}
def _blank_if_none(value: str | None) -> str:
return "" if value is None else value
def _bool_to_csv(value: bool | None) -> str:
if value is None:
return ""
return "true" if value else "false"
#: CSV column order. Derived from the dataclass so writer and schema cannot drift.
EVENT_CSV_COLUMNS: Final[tuple[str, ...]] = tuple(f.name for f in fields(EventRecord))
__all__ = [
"EVENT_ARRIVE_CRUSHER",
"EVENT_ARRIVE_LOADER",
"EVENT_CSV_COLUMNS",
"EVENT_DEPART_CRUSHER",
"EVENT_DEPART_LOADER",
"EVENT_DISPATCH",
"EVENT_EDGE_ENTER",
"EVENT_EDGE_LEAVE",
"EVENT_END_DUMP",
"EVENT_END_LOAD",
"EVENT_START_DUMP",
"EVENT_START_LOAD",
"EventRecord",
]
src/mine_sim/events.py
โ Back to submission ยท View raw on GitHub