Skip to content

ADR-0009: Simulation as a Separate Package Above digital_twin

Status Accepted
Date 2026-07-05
Deciders Chief Software Architect, MineProductivity
Governs mineproductivity.simulation
Related documents docs/architecture/09_Simulation_Design_Specification.md; docs/design/09_Simulation_Implementation_Checklist.md; docs/architecture/08_Digital_Twin_Design_Specification.md §14, §34; ADR-0008-Digital-Twin.md; docs/architecture/07_Decision_Intelligence_Design_Specification.md §19, §36; ADR-0007-Decision-Intelligence.md; docs/architecture/06_Analytics_Engine_Design_Specification.md §33 (not yet merged to main - see feature/analytics-engine)

Context

MineProductivity's Foundation Layer and its first three consumer packages, analytics, decision, and digital_twin, are complete, tested, and locked as of digital_twin v1.0.0. Two of those packages' own documents already named what comes next, from two different directions. Decision Intelligence spec 07 §19 designed WhatIfEngine around events.AsOf's already-reserved scenario field specifically "so a future scenario/what-if-forking capability (Digital Twin) can extend this type's usage without a breaking change," and spec 07's own §36 (Future Roadmap) named digital_twin as WhatIfEngine's most direct anticipated consumer. Digital Twin spec 08 then defined TwinSimulationModel (§14) as an interface-only extension point - deliberately shipping no concrete implementation, since choosing a simulation algorithm is out of scope for a package whose charter is representing current state, not projecting future state - and its own §34 (Future Roadmap) went one step further than spec 07 had: it named simulation (not digital_twin itself) as TwinSimulationModel's most direct anticipated consumer, and explicitly stated that simulation "is also positioned to become the first concrete implementer of decision.WhatIfEngine (spec 07 §19) by composing the two." This ADR formalizes and designs simulation, the package both decision's and digital_twin's own documents already assumed would exist, and resolves the two-hop chain those documents left open: digital_twin never ships a concrete TwinSimulationModel; simulation is the package that does, and by doing so becomes WhatIfEngine's bridge implementer as well.

The immediate question this ADR answers: given a scenario - a hypothetical or historical starting condition and a set of parameters - who owns projecting a mining system's state forward under it, comparing that projection across scenarios, and calibrating the projection against what actually happened, without becoming a fifth computation engine, a second stateful-representation layer, a decision-maker, or an optimizer? This ADR decides where that projection layer lives, why it is shaped as an orchestration layer over pluggable, interface-only methodologies rather than a shipped algorithm, and why SimulationRun - alone among this package's own abstractions - breaks from the stateless pattern every "as-object" abstraction below digital_twin already established.

Decision

Create mineproductivity.simulation as a new, independent package sitting directly above digital_twin and directly below the not-yet-designed optimization/agents/visualization packages:

core → ontology → events → kpis → analytics → decision → digital_twin → simulation

simulation is permitted to import core, ontology, events, registry, plugins, connectors, kpis, analytics, decision, and digital_twin; it introduces its own public API (SimulationModel and SimulationContext; Scenario/ScenarioStatus as a governed configuration artifact; SimulationRun - a core.BaseEntity[str] subclass - and RunStatus; SimulationClock/TimeProgressionMode; seed_from_replay(); four interface-only ABCs - MonteCarloModel, DiscreteEventModel, SystemDynamicsModel, CalibrationModel; Experiment/ExperimentRunner; ScenarioComparator/SensitivityAnalyzer; SimulationRunRepository as a direct type alias over core.BaseRepository[SimulationRun, str]; and SimulationStateCache) without requiring any change to any Foundation Layer file, or to analytics/decision/digital_twin, or to any of their public APIs or dependency rules. Its own registry (simulation.REGISTRY) is a direct specialization of registry.Registry, exactly as digital_twin.REGISTRY, decision.REGISTRY, analytics.REGISTRY, and kpis.REGISTRY already are. Full design detail is in the governing specification linked above; this ADR records why the package exists and why it is shaped the way it is.

Why a Separate Package (Not Folded Into digital_twin or decision)

  1. Distinct responsibility, distinct question being answered. digital_twin answers "what is currently true about this asset." decision answers "what should happen, given already-known facts." simulation answers a third, different question neither package was ever chartered to answer: "given a hypothetical or historical starting condition, how does this system evolve, and how confident should we be in that projection?" A simulated projection is evidence a decision may later reason over, not a decision itself, and a projection's existence has nothing to do with whether any twin is currently synchronized to it.
  2. Both packages' own specs already drew this boundary, from two directions. Digital Twin spec 08 §14 defines TwinSimulationModel as an interface digital_twin itself never implements, and spec 08 §34 names simulation - not digital_twin - as that interface's anticipated concrete implementer. Decision spec 07 §19 defines WhatIfEngine the same way, and spec 08 §34 explicitly closes the loop by naming simulation as WhatIfEngine's eventual bridge implementer too, arrived at by composing a concrete TwinSimulationModel. Folding projection logic into either package would mean shipping the exact algorithmic commitment each package's own specification already declined to make.
  3. A fundamentally different position on the stateless/stateful spectrum than either neighbor, for a subtly different reason than digital_twin's. digital_twin.Twin is stateful because representing a persisting asset is inherently about accumulated history. simulation.SimulationRun is also stateful, but for a related, not identical, reason: a running or completed simulation is itself a piece of history the instant it starts executing, even though the SimulationModel producing its next state remains as stateless as kpis.BaseKPI/analytics.AnalyticsModel/decision.DecisionModel (spec 05 §24, spec 06 §8, spec 07 §8) - the statefulness lives entirely in the run, never in the model. This package is the second, not the first, to reach for core.BaseEntity's "produce a new instance via with_state()" idiom, and this ADR treats that as a now-precedented idiom this platform reuses, not a novel decision requiring fresh justification each time.
  4. Consistent with every other layer boundary already drawn in this platform, including the three analytics, decision, and digital_twin just established at the three layers below. A platform that has consistently drawn a package boundary at every "distinct responsibility, distinct rate of change, distinct statefulness/governance profile" seam would be inconsistent to stop doing so at the one seam (digital_twinsimulation) two of its own most recent specifications already named from both sides.

Dependency Rationale

simulation is permitted to import the entire Foundation Layer plus analytics, decision, and digital_twin, not merely digital_twin directly, for the same structural reason ADR-0007 and ADR-0008 already established one and two layers down: a SimulationContext (governing spec §8) legitimately needs events.EventStore/AsOf/ReplayHandle directly (for seeding a scenario from real history, §12), digital_twin.TwinSnapshot/TwinState (as a scenario's real-history starting condition, §9), and kpis.KPIResult, analytics.AnalyticsResult, and decision.DecisionResult (as evidence a scenario or calibration routine may incorporate). analytics is exercised especially heavily and directly - not merely as evidence but as the statistical engine ScenarioComparator/SensitivityAnalyzer delegate to entirely (governing spec §19, §20) - because Simulation's own charter explicitly excludes owning statistical computation of any kind. Restricting simulation to only digital_twin would have forced pass-through re-exports of types it does not otherwise need to expose, exactly the worse alternative ADR-0006, ADR-0007, and ADR-0008 already rejected at their own layers.

connectors is included in the permitted-import list for platform-wide layering consistency, but - exactly as in analytics, decision, and digital_twin before it - is not exercised: simulation operates on already-computed, already-event-sourced, already-synchronized facts, never on a vendor-specific wire format (governing spec §5, §35).

Architectural Trade-offs

  • Trade-off accepted: four interface-only ABCs (MonteCarloModel, DiscreteEventModel, SystemDynamicsModel, CalibrationModel) ship with zero concrete implementations, rather than at least one reference algorithm per methodology. This means the package is, on its own, unable to actually run a simulation until a plugin implements at least one of the four - a materially weaker out-of-the-box capability than shipping even a single toy Monte Carlo model would provide. This was accepted for the same three reasons ADR-0006, ADR-0007, and ADR-0008 already accepted the equivalent trade-off at their own layers (see "Alternatives Rejected"), now applied a fourth time; the governing specification treats this as confirmation that the discipline is a platform-wide convention rather than a one-off judgment call.
  • Trade-off accepted: SimulationRun subclasses core.BaseEntity[str] and is immutable, with state changes producing a new instance (with_state()), rather than being a plain mutable Python object, exactly mirroring the trade-off ADR-0008 already accepted for Twin. This is more expensive per step/trial (a small allocation instead of an in-place field write) but keeps simulation consistent with every other identity-bearing type in the platform, including its own immediate neighbor one layer down. The governing spec's own Performance Considerations (§36) judges this negligible next to the SimulationModel category call and repository round-trip a step or trial also makes.
  • Trade-off accepted: SimulationRunRepository is a literal type alias over core.BaseRepository[SimulationRun, str], not a new ABC, exactly mirroring TwinRepository's identical reuse one layer down (spec 08 §20). The cost is the same one ADR-0008 already accepted: no room for a simulation-run-specific repository method beyond add/get/find/remove/list without either extending core or introducing a new ABC later. Accepted because no such method was identified as necessary at design time, and because core.BaseRepository.list's BaseSpecification-based filtering already satisfies the discovery need (governing spec §22).
  • Trade-off accepted: SimulationStateCache does not reuse kpis.ResultCache or digital_twin.TwinStateCache, despite all three being "cache an expensive upstream result" concepts. This is the fourth occurrence of the same "shape looks similar, coupling doesn't fit" reasoning in this series (decision.ActionPlanner declining kpis.DependencyGraph, ADR-0007; digital_twin.TwinStateCache declining kpis.ResultCache, ADR-0008; now SimulationStateCache declining both predecessors at once) - recorded here as further confirmation of a recurring, deliberately applied pattern rather than inconsistency.
  • Trade-off accepted: ScenarioComparator and SensitivityAnalyzer own no statistical computation whatsoever, deferring every mean, percentile, confidence interval, and distributional judgment to analytics. This makes both classes thin orchestration layers whose entire value is in correctly assembling inputs for analytics to consume - a smaller, less immediately impressive surface than a package that computed its own statistics would present, but the only shape consistent with this package's own charter that Simulation does not own Analytics (governing spec §3.2, repeated at every relevant section).

Alternatives Considered

  1. Fold projection logic directly into digital_twin (e.g. a concrete TwinSimulationModel shipped from that package). Rejected - directly contradicts digital_twin's own already-locked scope (spec 08 §3.1, §14) and its own Future Roadmap's framing of simulation as the separate, downstream implementer of that interface (spec 08 §34).
  2. Fold projection logic directly into decision (e.g. a concrete WhatIfEngine shipped from that package). Rejected - for the identical reason: decision's own spec 07 §19 explicitly defers the concrete implementation, and spec 07 §36 already frames the eventual bridge as arriving from a package positioned to compose both digital_twin and decision concepts, not from decision alone.
  3. Defer simulation entirely until optimization/agents need it, treating scenario projection as those future packages' own internal detail. Rejected - for the same reason ADR-0006, ADR-0007, and ADR-0008 each rejected deferring their own packages: optimization, agents, and visualization (governing spec §37) are all independently expected to consume SimulationResult/Experiment; deferring this package would mean each reimplements its own, likely mutually inconsistent, notion of "what does a simulated projection look like."
  4. A single, larger "digital_twin + simulation" package, reasoning that projecting a twin forward is just an extension of representing it. Rejected - optimization, agents, and visualization (governing spec §37) are expected to consume simulation output without needing digital_twin's full synchronization/telemetry surface exposed alongside it, and vice versa; bundling the two would also obscure the subtly different reason each package's central abstraction is stateful (see "Why a Separate Package," item 3).
  5. Ship at least one concrete implementation per interface-only category now (a reference Monte Carlo model, at minimum), on the reasoning that a simulation package with no runnable simulation is of limited immediate use. Rejected - see "Alternatives Rejected" below; this was the most seriously debated alternative, mirroring the equivalent debate ADR-0006, ADR-0007, and ADR-0008 each recorded for their own interface-only capabilities.

Alternatives Rejected

Shipping at least one concrete implementation of MonteCarloModel, DiscreteEventModel, SystemDynamicsModel, or CalibrationModel is rejected for the same three compounding reasons ADR-0006, ADR-0007, and ADR-0008 already established for their own interface-only capabilities, restated at this layer:

  1. Scope creep into modeling/statistics/optimization. Choosing a random-sampling methodology, an event-scheduling policy, an integration method, or a parameter-fitting technique is exactly the kind of algorithmic decision this package's charter (governing spec §3.1, §3.5, §4) excludes. Shipping one "just as a starting point" would establish a precedent that quietly widens simulation's scope every time a plausible-looking heuristic seems worth adding.
  2. Premature commitment to an interface's shape via its first implementation. Defining all four ABCs now and deferring their first concrete implementations to independently-versioned future plugins keeps each contract general, rather than shaped by whichever algorithm happened to ship first.
  3. Two of the four interfaces already have a designated future owner. optimization is named (governing spec §37) as the most plausible first concrete implementer of CalibrationModel, since parameter-fitting against historical ground truth is itself a search problem; shipping a concrete implementation now, before that package exists, would preempt work it is better positioned to do once it does.

Modeling SimulationRun as a plain mutable object, for the same reasons ADR-0008 already rejected the equivalent alternative for Twin, was considered and rejected:

  1. It would make simulation the second package in the series (after digital_twin) to introduce a central abstraction violating the immutability discipline every value/entity type otherwise follows, without a compensating reason distinct from the one digital_twin already used to justify its own exception.
  2. core.BaseEntity already anticipates this exact pattern, and Twin.with_state() already exists as the precedent to mirror rather than re-derive.
  3. The performance cost is judged negligible (governing spec §36) relative to the SimulationModel category dispatch and repository round-trip already on the hot path of every step or trial.

Long-Term Evolution

This package is expected to grow along the same axes ADR-0006, ADR-0007, and ADR-0008 already established for analytics, decision, and digital_twin, without requiring a new ADR:

  1. New concrete models within existing categories, or new Scenarios - ordinary, checklist-governed extension (governing spec §30), not an architectural change.
  2. The first concrete implementations of MonteCarloModel/DiscreteEventModel/SystemDynamicsModel/CalibrationModel, whenever they arrive - expected to land as plugins against the already-stable ABCs this design defines, most plausibly the calibration case from a future optimization package (governing spec §37).
  3. The first concrete implementation of decision.WhatIfEngine, composed from this package's own TwinSimulationModel-conforming model and SimulationExecutor - the two-hop bridge Digital Twin spec 08 §34 and Decision spec 07 §19/§36 both anticipated before either digital_twin or simulation existed.
  4. A production-grade SimulationRunRepository backend (SQL, document store) implementing core.BaseRepository[SimulationRun, str] directly - no code change to this package required (governing spec §24).

A new ADR would be warranted if a future need arose to: change simulation's position in the dependency chain; permit simulation to be imported by a Foundation Layer package or by analytics/decision/digital_twin (which would indicate a design error, not a legitimate evolution); introduce a second extension mechanism alongside the Registry Framework specialization this design already commits to; or revisit the immutable-SimulationRun/with_state() trade-off recorded above in light of a measured performance problem this ADR did not anticipate.

Future Compatibility

Every public type this package introduces (governing spec §18, SimulationResult and its subclasses, plus SimulationState) is a plain, frozen core.BaseValueObject (or, for SimulationRun itself, a frozen core.BaseEntity), serializable via the platform's existing core.serialization mechanism, with no simulation-internal state leaking into any type a future package would need to consume - the identical compatibility guarantee ADR-0006, ADR-0007, and ADR-0008 each already established for analytics.AnalyticsResult, decision.DecisionResult, and digital_twin.TwinResult, extended one layer up. This is a deliberate guarantee for the packages named in the governing specification's Future Roadmap (§37) - optimization, agents, and visualization - none of which are designed by this ADR or by the governing specification, but all of which are expected to consume simulation's output types without requiring a breaking change to them. The four interface-only ABCs this package defines (governing spec §13-§16) exist specifically so that whichever future package first needs a concrete Monte Carlo, discrete-event, system-dynamics, or calibration capability can implement against a contract that already exists, and so that this package can plausibly close, at last, the WhatIfEngine bridge decision and digital_twin have each been waiting for since before simulation itself existed.


This ADR governs the existence and shape of mineproductivity.simulation as a package. See 09_Simulation_Design_Specification.md for the full object model and 09_Simulation_Implementation_Checklist.md for the locked implementation contract.