Lesson 05 - Ontology¶
Objective¶
Model equipment, locations, and production time using the platform's shared vocabulary - and understand why two sites that define "haul truck" differently can never compare their tonnes per hour, no matter how good their arithmetic is.
Prerequisites¶
- Lesson 04 - Events (you now have facts; this lesson is about what those facts are about)
Concepts covered¶
| Concept | Why it exists |
|---|---|
Typed equipment leaves (RigidHaulTruck, HydraulicShovel, …) |
A truck is a type with declared capabilities - not a string in a column. |
| Shared base behaviour | Every leaf shares one four-state operational state machine. That is what makes fleet-wide utilisation a coherent question. |
meta.supported_kpis |
Metadata declares which KPIs a type can even support. |
Mine → Pit → Bench |
Location is modelled, not implied by a naming convention. |
Relationship + RelationshipKind |
Explicit typed edges between ids - never nested object pointers. |
Shift |
Production time is a UTC half-open interval that knows .contains(t). |
to_schema() |
Every type exports JSON Schema for dashboards, integrations, and agents. |
Complete runnable example¶
examples/fundamentals/05_ontology/ontology.py
Edges, not nesting - this is the design decision worth understanding:
from mineproductivity.ontology import Mine, Pit, Bench, Relationship, RelationshipKind
mine = Mine(id="bingham-west", commodity_codes=("copper",), method="open_pit")
pit = Pit(id="pit-west", mine_id=mine.id, commodity="copper")
bench = Bench(id="bench-7", pit_id=pit.id, elevation_m=1820.0)
# The Bench does NOT hold a pointer to its Pit. The edge is a separate object.
edges = [
Relationship(source_id=pit.id, kind=RelationshipKind.BELONGS_TO, target_id=mine.id),
Relationship(source_id=bench.id, kind=RelationshipKind.BELONGS_TO, target_id=pit.id),
]
Expected output¶
--- 1. Equipment is a typed leaf, not a string in a spreadsheet ---
HT-214: CAT 793F, rated 226.0t, diesel
SH-1: EX8000, rated 42.0t
--- 2. Shared behaviour lives on the base; specifics live on the leaf ---
operational states (identical for both): ['operating', 'standby', 'down', 'maintenance']
(a truck and a shovel share one state machine -- that is why fleet-wide
utilisation is even a meaningful question)
--- 3. Metadata declares which KPIs a type can support ---
RigidHaulTruck supports : ('PROD.TruckCycleTime', 'PROD.Payload', 'UTIL.PA', 'UTIL.UA', 'HAUL.TKPH', 'HAUL.MatchFactor')
HydraulicShovel supports: ('UTIL.PA', 'UTIL.UA', 'LOAD.Rate', 'HAUL.MatchFactor')
--- 4. Location and organisation are modelled, not implied ---
bingham-west (open_pit) -> pit-west -> bench-7 @ 1820.0m
fleet FL-NORTH groups equipment_type_code='RIGID_HAUL_TRUCK'
--- 5. Relationships are explicit, typed edges between ids ---
pit-west --belongs_to--> bingham-west
bench-7 --belongs_to--> pit-west
HT-214 --belongs_to--> FL-NORTH
(edges, not nested objects: a Bench does not hold a pointer to its Pit,
so each entity stays independently serializable -- design spec AD-ON-02)
--- 6. Production time is a modelled interval, not a naive timestamp ---
shift A-2026-06-25 (12.0h) contains 2026-06-25T14:30:00+00:00: True
--- 7. Every type exports its shape as JSON Schema ---
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "RIGID_HAUL_TRUCK",
"type": "object"
}
(dashboards, agents, and integrations read this instead of guessing)
Explanation¶
Why an ontology at all? Because "tonnes per hour" is not a number - it is an agreement. If the north pit counts a truck as available while it queues at the shovel and the south pit does not, their availability figures are incomparable. Both are arithmetically flawless. Both are useless side by side.
The ontology is where that agreement is written down in a form a machine can read. Every package above it models the mine using these concepts, which is why a KPI computed at one pit means the same thing at another.
The inheritance split. Notice section 2: RigidHaulTruck.operational_states == HydraulicShovel.operational_states. Common behaviour - the operational state machine, rated capacity - lives on the base EquipmentType. Specifics - fuel_type on the truck, throughput on the shovel - live on the leaves. This is not tidiness for its own sake: it is precisely what makes "fleet-wide utilisation" a well-defined question across mixed equipment.
Why supported_kpis on the metadata? Section 3 shows a truck supports HAUL.TKPH and a shovel supports LOAD.Rate. Asking a shovel for its tonne-kilometres-per-hour is a category error. The metadata makes that checkable before someone builds a dashboard that quietly reports zero.
Edges, not nesting (design spec AD-ON-02). This is the subtle one. A Bench does not hold a reference to its Pit. Instead a Relationship names the two ids and the kind of link. Why? Because nesting would make every entity drag its whole ancestry along - you could not serialize a bench without serializing the mine. With explicit edges, each entity is independently serializable, independently versionable, and the graph is a first-class thing you can query, project, and validate.
Shift is an interval, not a label. shift.contains(probe) answers "did this happen on A-shift?" without anyone re-deriving a half-open UTC comparison at nineteen call sites, each subtly wrong at the boundary.
Best practices¶
- Model with the ontology's types. If you find yourself putting
"haul truck"in a string column, stop - there is a type for it. - Check
supported_kpisbefore wiring an equipment type to a metric. - Declare relationships explicitly. Do not encode hierarchy in an id naming convention (
"north-4820"meaning bench 4820 of the north pit) - that is a convention only humans can read. - Everything UTC.
Shift.start_utc/end_utcare UTC; keep it that way. - Export
to_schema()for integrations rather than hand-writing a JSON contract that will drift.
Common mistakes¶
| Mistake | What happens | Do this instead |
|---|---|---|
Mine(commodity_code=..., country=...) |
TypeError - these fields do not exist |
Mine(id=, commodity_codes=("copper",), method="open_pit") - note the plural commodity_codes |
RelationshipKind.PART_OF |
AttributeError - this member does not exist |
RelationshipKind.BELONGS_TO |
Nesting entities (bench.pit.mine) |
Entities stop being independently serializable | Declare a Relationship edge |
| Encoding hierarchy in ids | Only humans can parse it; machines guess | Explicit edges |
| Assuming a shovel supports truck KPIs | Silent zeros on a dashboard | Check meta.supported_kpis |
These two signatures were wrong in this lesson's first draft
Mine(commodity_code=...) and RelationshipKind.PART_OF were both invented from memory and both failed at runtime. The real API is commodity_codes (plural) and BELONGS_TO. This is why the Learning Suite's rule is to read the implementation before writing - see examples/ontology/02_structural_modelling.py.
Exercises¶
- Add a loader. Construct a
WheelLoaderand compare itsmeta.supported_kpisto the shovel's. Why do they differ? - Extend the graph. Add a second pit and a second fleet, then write the
Relationshipedges. Print every edge whosetarget_id == mine.id. - Test the boundary. A shift runs 06:00–18:00 UTC. Does
contains(18:00:00)returnTrue? What does that tell you about half-open intervals, and why does it matter for a cycle recorded at exactly 18:00:00? - Read the schema. Print the full
truck.to_schema(). Which fields are required? How would a dashboard use this instead of hard-coding a form? - Explore. List all 56 ontology exports (
mineproductivity.ontology.__all__). Find the types for a stope, a conveyor, and a delay category.
Suggested next lesson¶
Lesson 06 - KPIs - you have facts (04) about modelled things (05). Now measure them properly, and meet the guardrail that stops the most expensive mistake in mining reporting.
See also: ontology API Reference · ontology package guide · Ontology Framework design specification