mineproductivity.kpis¶
Purpose¶
mineproductivity.kpis is the metric backbone of MineProductivity — the platform's most important package. It makes every performance indicator a discoverable, versioned, self-describing object ("KPI-as-object") rather than a formula buried in a script or a spreadsheet cell, guaranteeing the platform's central promise: two engineers, two sites, or two AI agents each computing "availability" get the same number from the same events.
This package implements the KPI Engine Design Specification exactly. Where this README and that specification disagree, the specification governs.
Scope¶
What belongs here:
KPIMetadata— the complete, governed 29-field mandatory schema (Developer & Cookbook Guide Part III, KPI Standard Library) every registered KPI carries.BaseKPI,CompositeKPI— the two KPI base classes: a leaf reads raw event rows (_compute), a composite reads other KPIs' already-computed results (_combine). Never conflated.- Nine category base classes (
ProductionKPI,UtilizationKPI,MaintenanceKPI,HaulageKPI,DelayKPI,EnergyKPI,QualityKPI,CostKPI,SafetyKPI), one per controlled namespace family, contributing only a namespace-conformance check. KPIEngine— orchestration only: resolves a KPI's dependency graph, assembles exactly the rows each node needs, executes leaves before composites, caches, and returns a traceableKPIResult. Holds zero metric-specific logic (AD-KP-01).DependencyGraph,aggregation.combine_results,Window/RollingWindow/CumulativeWindow,ResultCache.- Four pluggable
ExecutionBackends (PandasBackenddefault,NumPyBackend,PolarsBackend,DuckDBBackend) — the same_computeruns unchanged regardless of which one is active (backend parity). - The 12-KPI Standard Library reference implementation, one flagship per category plus the
UTIL.OEEcomposite worked example. parse_identifier/KPIIdentifier(theNAMESPACE.Name[.Specialization]naming standard),specialize()(KPI inheritance, e.g.PROD.TPH.Ore),KPIValidator,CertificationFixture/run_certification_fixture.
What must never belong here:
- Event definitions — owned by
events;kpisreads canonical events via an injectedEventStore, never redefines their shape. - Ontology entity definitions — owned by
ontology;kpisreadsShiftfor window resolution, never defines domain entities. - Any dependency on
connectors,analytics,optimization,simulation,decision,digital_twin, oragents— the single most load-bearing rule in the governing specification (§7). - Metric-specific branches inside
engine.py— a new KPI is added entirely instandard_library/, never by editing the engine.
Architecture¶
kpis sits after events, ontology, and registry in the dependency stack — it is intelligence built on top of the durable event log, resolving ontology reference data (shifts) for window bounds and using registry as its own KPI catalogue's specialization mechanism.
The engine's own internal flow, end to end:
KPIEngine.execute(code, window, scope)
|
v
DependencyGraph.topological_order(code) -- dependencies before dependents (raises
| KPICircularDependencyError if cyclic)
v
for each step in order:
|
+-- CompositeKPI? -> combine(already-computed dependency KPIResults)
|
+-- leaf KPI? -> rows_for(kpi, window, scope)
| EventQuery scoped to kpi.meta.required_events + window/scope
| EventStore.query() -> envelopes -> flatten -> ExecutionBackend
| .assemble() -> .to_rows() (mechanical backend-parity proof)
v
ResultCache.get_or_compute(code, window, scope, fingerprint, ...)
|
v
BaseKPI.compute(rows) -- missing-column precheck, then _compute()
|
v
KPIResult (value | None, unit, n, warnings, scope)
KPIEngine.summary() is the batched equivalent: it computes the transitive closure of every requested code's dependencies once, then scans the event store once per distinct required_events set shared across them — not once per KPI.
See the design specification's §10 for the full object model and canonical semantics.
Package Structure¶
kpis/
├── __init__.py # public API surface (__all__); registers the 12-KPI standard library
├── _registry.py # REGISTRY, register() (internal, avoids a circular import)
├── metadata.py # KPIMetadata, Aggregation, Direction, DigitalMaturity
├── naming.py # KPIIdentifier, parse_identifier, CONTROLLED_NAMESPACES
├── lifecycle.py # KPIStatus (Proposed -> Active -> Deprecated -> Retired)
├── base_kpi.py # BaseKPI (compute() orchestration, _compute() abstract)
├── composite.py # CompositeKPI (combine(), _combine() abstract)
├── inheritance.py # specialize() (PROD.TPH.Ore-style specialization)
├── result.py # KPIResult (to_frame/plot/pareto)
├── engine.py # KPIEngine (orchestration only, AD-KP-01)
├── dependency_graph.py # DependencyGraph (topological_order, detect_cycle)
├── aggregation.py # combine_results (RATIO-never-averaged rule)
├── windowing.py # Window, RollingWindow, CumulativeWindow
├── caching.py # ResultCache
├── validation.py # KPIValidator (canonical time-model check)
├── certification.py # CertificationFixture, run_certification_fixture
├── exceptions.py # the kpis exception hierarchy
├── categories/ # nine namespace-family base classes
│ ├── _common.py # enforce_namespace (shared)
│ └── production_kpi.py, utilization_kpi.py, maintenance_kpi.py, haulage_kpi.py,
│ delay_kpi.py, energy_kpi.py, quality_kpi.py, cost_kpi.py, safety_kpi.py
├── backends/ # pluggable ExecutionBackend strategies
│ ├── base_backend.py # ExecutionBackend ABC
│ ├── pandas_backend.py # PandasBackend (default)
│ ├── numpy_backend.py, polars_backend.py, duckdb_backend.py
├── standard_library/ # the 12-KPI reference implementation
│ └── production.py, utilization.py, maintenance.py, haulage.py, delay.py,
│ energy.py, quality.py, cost.py, safety.py
└── README.md # this file
Dependency Rules¶
kpisdepends on:core,ontology,events,registry.kpisis depended on by:analytics,decision,digital_twin,simulation,optimization,agents, andvisualization, per their own locked design specifications (none is implemented yet) — never the reverse.- Forbidden (the single most load-bearing rule in this document):
kpisMUST NOT importconnectors,analytics,optimization,simulation,decision,digital_twin,agents, orvisualization. This is mechanically checked bytests/unit/kpis/test_public_api.py::TestNoForbiddenDependencies, including a dedicatedtest_never_imports_connectors_specificallycheck.
Public API¶
from mineproductivity.kpis import (
REGISTRY, register,
BaseKPI, CompositeKPI,
ProductionKPI, UtilizationKPI, MaintenanceKPI, HaulageKPI, DelayKPI,
EnergyKPI, QualityKPI, CostKPI, SafetyKPI,
KPIMetadata, Aggregation, Direction, DigitalMaturity, KPIStatus,
KPIIdentifier, parse_identifier,
KPIResult,
KPIEngine, DependencyGraph,
ResultCache,
Window, RollingWindow, CumulativeWindow,
KPIValidationError, KPINotFoundError, KPICircularDependencyError,
KPIAggregationError, KPIVersionConflictError,
)
Every flagship in the 12-KPI Standard Library (TonnesPerHour, PhysicalAvailability, OverallEquipmentEffectiveness, ...) is registered into REGISTRY automatically at import time ("PROD.TPH" in REGISTRY is True with no setup) — but, matching the design specification's own §8 list, is deliberately not re-exported at the top level. Look them up by code via REGISTRY.get("PROD.TPH"), or import the concrete class directly from mineproductivity.kpis.standard_library.production. ExecutionBackend and its four implementations live under mineproductivity.kpis.backends; KPIValidator/CertificationFixture under mineproductivity.kpis.validation/.certification (design spec §9 — internal-API-documented, not top-level-exported, symbols).
Extension Guide¶
Adding a new KPI. Subclass the matching category base, declare meta, implement _compute, decorate with @register:
from typing import ClassVar
from mineproductivity.kpis import register
from mineproductivity.kpis.categories.maintenance_kpi import MaintenanceKPI
from mineproductivity.kpis.metadata import Aggregation, Direction, DigitalMaturity, KPIMetadata
@register
class MeanTimeBetweenFailures(MaintenanceKPI):
meta: ClassVar[KPIMetadata] = KPIMetadata(
code="MAINT.MTBF", name="Mean Time Between Failures", official_name="Mean Time Between Failures",
business_purpose="...", operational_question="...", business_meaning="...",
formula="sum(operating_h) / count(failures)", unit="h",
dimensions=("Shift", "Equipment"), required_events=("MAINTENANCE",),
aggregation=Aggregation.AVERAGE, direction=Direction.HIGHER_IS_BETTER,
min_maturity=DigitalMaturity.L2_FMS, leading_or_lagging="lagging",
operational_or_strategic="operational",
)
def _required_columns(self) -> tuple[str, ...]:
return ("duration_h",)
def _compute(self, rows):
...
A malformed code, an already-registered code, or a completed dependency cycle each raise immediately at class-definition/registration time — never deferred to first KPIEngine.execute() call.
Adding a KPI specialization (e.g. PROD.TPH.Ore), reusing a parent's _compute with a material filter:
from mineproductivity.kpis.inheritance import specialize
from mineproductivity.kpis.standard_library.production import TonnesPerHour
ore_tph = specialize(TonnesPerHour, code="PROD.TPH.Ore", material_filter="ore")
Switching the active ExecutionBackend (process-level, e.g. to DuckDBBackend for a fleet-scale batch job):
from mineproductivity.kpis.backends import DuckDBBackend, set_active_backend
set_active_backend(DuckDBBackend())
Examples¶
Runnable, narrated scripts live in examples/kpis/; a beginner-tier walkthrough lives in notebooks/beginner/01_first_kpi_lookup.ipynb.
| Script | Demonstrates |
|---|---|
01_simple_execution.py |
PROD.TPH end to end: resolve, scan, compute, inspect provenance, export to a DataFrame. |
02_composite_oee.py |
UTIL.OEE's dependency graph resolved automatically; None propagation when a dependency has no data. |
03_batch_summary.py |
Nine KPIs across every category, computed in a single summary() call. |
04_discovery.py |
REGISTRY introspection: list, filter by namespace, filter composite-vs-leaf, fully describe a KPI. |
Design Rationale¶
- Why is
KPIMetadata.codea required positional field, not defaulted? A KPI with no identifier is a specification gap, not a valid partial object — mirrorscore.BaseMetadata.name's own required-positional-field contract. - Why does
enforce_namespaceaccept more than one namespace? The design specification itself describes some categories as spanning several controlled namespaces (EnergyKPIcoversENERGY/CARBON/WATER;QualityKPIcoversQUAL/GRADE;SafetyKPIcoversSAFE/AUTO); a single-namespace check would have made those categories unimplementable as specified. - Why does
DelayKPIuse only theDISPnamespace, notDELAY? Design spec §10.4's prose describes "DELAY/DISP," but §20's controlled namespace list — the oneparse_identifier/enforce_namespaceactually validate against — contains onlyDISP. The controlled list governs identifier validation; the prose description does not. Seecategories/delay_kpi.py's module docstring for the full resolution. - Why is
ExecutionBackend.assemble()(rows, columns) -> tableinstead of the design spec's illustrative(query, columns) -> table? Store-querying is anevents-specific concern already owned byKPIEngine(which holds theEventStore); duplicating query execution once per backend implementation would violate DRY and blur backend responsibility. Backends stay focused on tabular manipulation — assembly, grouping, pandas export — their own specialty. - Why does
KPIValidator's canonical time-model check skipCompositeKPIsubclasses? A composite's own formula composes other KPI codes' results (e.g.UTIL.OEE'sUTIL.PA * UTIL.UA * UTIL.Performance), not raw hour fields directly — each dependency is independently validated when it is itself registered. Checking a composite's formula string forscheduled_h/available_h/operating_h/calendar_hsubstrings would be a false negative for every legitimate composite,UTIL.OEEincluded. - Why does
UTIL.OEEmultiply only three factors (PA × UA × Performance), not the classic four-factor Availability × Performance × Quality? No dedicated grade/reject-rate event stream is part of this milestone's canonicaleventscatalogue yet; this is documented inOverallEquipmentEffectiveness's own docstring as a valid three-factor formulation, withQUAL.OreProportionoffered as a standalone, grade-adjacent ratio computed from what is available today. - Why does
COST.FuelPerTonnedeclaredependencies=("PROD.TPH",)yet never readPROD.TPH's computed value? A non-composite KPI receives only raw rows fromBaseKPI.compute— never a dependency's already-computedKPIResult(onlyCompositeKPI.combinedoes). The declared dependency documents the Standard Library's own thematic/DAG grouping (ensuringPROD.TPHis computed and cache-warmed alongside it), while_computestill re-derives its own ratio directly from the union ofCONSUMPTION/CYCLErows. - Why does
KPIEngine.rows_forround-trip every row through the activeExecutionBackend'sassemble()/to_rows()even for a single-KPI request? This is the mechanical proof of backend parity (design spec §29): swapping the active backend never changes the row data a_computeimplementation sees, only how it was vectorized in transit. - Why is
ResultCache's key(code, window, scope, fingerprint)rather than a time-based TTL?fingerprint(the matching envelope count) is a cheap, correct proxy for "has anything relevant changed" — the cache key naturally changes the moment a new matching event lands, giving the design spec §22 "invalidated automatically, never silently stale" guarantee without a new versioning API on the lockedEventStorecontract.
Anti-Patterns¶
- ❌ Averaging per-shift
RATIO-aggregation results instead of re-deriving from the union of raw rows (design spec §10.8, §19, §29) — the single most-cited mistake across the entire Standard Library.aggregation.combine_resultsraisesKPIAggregationErrorstructurally rather than merely discouraging this by convention. - ❌ A
UtilizationKPIdenominator outside the canonical ladder (calendar_h ⊇ scheduled_h ⊇ available_h ⊇ operating_h).KPIValidatorrejects this at registration time for every leafUtilizationKPI. - ❌ A composite KPI fabricating a zero when a dependency has no value.
CompositeKPI.combine()propagatesNonewith a warning the moment any declared dependency's value isNone, before_combine()is ever called. - ❌ Editing
engine.pyto add a metric-specific branch. A new KPI is always added entirely instandard_library/(or a plugin package); the engine's own zero-metric-logic invariant is mechanically checked bytests/unit/kpis/test_public_api.py::TestEngineHoldsNoMetricLogic. - ❌ Re-registering an existing
Activecode with different semantics.register()raisesKPIVersionConflictError— a genuine new meaning requires a MAJOR version bump under review, never silent re-registration. - ❌ Catching
Exceptioninstead of thekpisexception hierarchy when handling a KPI failure. Every exception this package raises derives fromcore.MineProductivityError,core.ValidationError,core.NotFoundError, orregistry.RegistrationErrorspecifically so callers do not need a broadexcept Exception.
Testing & Quality¶
tests/unit/kpis/mirrorssrc/mineproductivity/kpis/1:1 — 100% line coverage.- Backend parity tests (
tests/unit/kpis/backends/test_backend_parity.py): identicalKPIResult.valueacross all fourExecutionBackends for every implemented leaf KPI, and identicalgroup_and_aggregateoutput. - The exact Cookbook Part I Ch. 6 ratio-correctness worked numbers (A-shift 1,300 t/h over 12h + B-shift 1,100 t/h over 6h → combined 1,233.3 t/h, not the naive 1,200 average) are proven at both the row level (
test_engine.py::TestRatioNeverAveraged) and the aggregation-refusal level (test_aggregation.py). tests/integration/test_kpi_pipeline.py— the full CSV → canonical events →EventStore→KPIEnginepipeline, no stage bypassed, against the golden dataset intests/fixtures/kpis/.mypy --strictandruffare clean onsrc/mineproductivity/kpis/andexamples/kpis/.
Contents¶
See Package Structure above for the full file layout.
Dependencies¶
Depends on: core, ontology, events, registry. Optionally, numpy/pandas/polars/duckdb (the analytics extra) for the non-default ExecutionBackends — pandas alone is required for the default PandasBackend and KPIResult.to_frame().
Depended on by: analytics, decision, digital_twin, and simulation, per their own locked design specifications (none of the four is implemented yet, so no code currently imports kpis).
Future Work¶
- A fourth, Quality-factor OEE variant once a grade/reject-rate event stream joins the canonical
eventscatalogue. KPIEngineConfiguration/ResultCacheConfigurationascore.BaseConfigurationsubclasses, once the cross-cuttingconfigpackage exists.- Database-backed
EventStoreimplementations exercised against the sameKPIEngine.rows_forcontract, onceconnectorsgrows a database/historian adapter.
References¶
- Master Architecture Handbook v1.0
- Reference Implementation Blueprint v1.0
docs/architecture/05_KPI_Engine_Design_Specification.mddocs/design/05_KPI_Implementation_Checklist.md- Developer & Cookbook Guide Part I, Chapter 4 ("The Query Layer") and Chapter 6 ("Your First KPI")
- Developer & Cookbook Guide Part III, the KPI Standard Library (29-field template, Canonical Semantics, KPI Naming Standard)