Skip to content

Architecture

Architecture decisions are recorded as focused ADRs under Architecture Decision Records. The ADR set captures the standing decisions behind module ownership, cross-repository boundaries, Python/Rust/PyO3 dispatch, solver selection, validation strategy, and public versus internal API surfaces.

SCPN ecosystem and cross-repository contracts

scpn-control is one repository in a four-repository reactor-semantics path. Each repository owns a distinct surface, and the boundaries between them are explicit contracts rather than copied code. Understanding the split explains why a given capability lives where it does, and where to look for the canonical version of a physics model, a quantum routine, or a control surface.

Repository Role Owns
scpn-fusion-core Physics-solver laboratory Broad physics kernels, high-fidelity numerical formulation, external-code validation campaigns, facility-data integration, Rust and GPU solver paths, 3D equilibrium / stellarator / VMEC / free-boundary internals, and neural physics training stacks.
scpn-phase-orchestrator Reactor-semantic owner Reactor configuration identity, phase and nonphase semantic carriers, regime meaning, U0 vocabulary and registry, and portable review-only handoffs.
scpn-control (this repository) Control-grade integration and facade Bounded public control APIs, NMPC and controller-loop integration, Petri-net and SNN runtime contracts, replay and campaign metadata, fail-closed adapters, HIL/CODAC/EPICS/WebSocket safety boundaries, control-side source and actuator gradients, and traceable claim surfaces.
scpn-quantum-control Quantum and phase-dynamics research Quantum disruption classifiers, Qiskit/PennyLane execution and provider integration, and quantum Kuramoto/UPDE variants. scpn-control consumes a bounded control adapter for the quantum disruption path rather than re-implementing it.

Why the split exists

Each surface has a single canonical owner, so the same work is not developed twice and each repository's public claims stay scoped to what it can validate:

  • A physics solver matures in scpn-fusion-core first; scpn-control ports or wraps the subset that has a clear control-loop contract, turning selected physics into auditable controller surfaces.
  • The quantum path is owned in scpn-quantum-control; scpn-control holds only the control adapter, classifier API, and feature-ordering contract.
  • When scpn-control needs new mathematics, the solver-level result belongs upstream in scpn-fusion-core, so that control stays a bounded facade and does not become a second physics laboratory with divergent copies of the same model.
  • scpn-phase-orchestrator alone assigns reactor identity and semantic meaning. CONTROL consumes its portable result and does not reproduce the U0 registry, phase vocabulary, or source-envelope decoder.

What crosses a boundary

Every cross-repository boundary is a contract, not a shared mutable file. When a capability is ported between repositories, the boundary fixes:

  • physical units, array shapes, and timestep semantics;
  • the failure mode (boundaries fail closed when an optional dependency, native backend, or evidence gate is unavailable);
  • replay metadata, provenance, and the bounded public-claim status of the result.

Reactor semantic review boundary

The reactor-semantic path is a one-way, non-actuating evidence exchange:

SCPN-FUSION-CORE canonical model evidence bytes
  -> SCPN-PHASE-ORCHESTRATOR U0/nonphase semantic handoff bytes
  -> SCPN-CONTROL sealed review admission bytes

SPO's public handoff_from_bytes decoder is the only portable ingress. It refuses duplicate keys, alternate JSON encodings, schema/U0/registry drift, embedded-source tamper, non-empty phase relations, phase relabelling, and action authority. CONTROL independently checks the expected byte and source digests, producer identity, caller-supplied reference clock, evidence and calibration age, declared calibration identities, observable usability, and provenance.

Transport quantities deliberately carry bounded-feature semantics with UNOBSERVABLE phase validity and UNKNOWN noncyclic quality. That states that no cyclic phase exists in the evidence; it is not an observable-data rejection. The bundle's UNKNOWN regime has the same bounded meaning. CONTROL emits only a review decision with review_only=true and actionable=false; no ControlAction crosses or is created at this boundary.

Equilibrium data boundary

core.imas_adapter.EquilibriumSnapshot is the single solver-facing equilibrium representation. It stores SI metadata, strictly increasing R/Z grids, and immutable (Z, R) flux and optional toroidal-current arrays in COCOS 1. Kernel, GEQDSK, IMAS-Python, and OMAS integrations converge on this object instead of passing backend-specific containers into control code.

The adapters own every convention change. IMAS Data Dictionary v3 uses COCOS 11 and j_tor; v4 uses COCOS 17 and j_phi; their rectangular IDS arrays are ordered (R, Z). The IMAS path therefore applies the version-specific +2π/-2π poloidal-flux transformation and transpose at the boundary. OMAS 0.x uses its real COCOS conversion environment and remains restricted to its bundled v3 schema. Time arrays, vacuum field, reference radius, plasma current, schema version, and provenance travel with each snapshot and are checked before solver admission.

The boundary is intentionally fail-closed: absent 2-D current density remains absent, incomplete kernel metadata is rejected, and DBEntry endpoints and credentials remain caller-owned. A successful data round trip proves the software conversion contract; it does not imply access to or validation by an ITER or other facility deployment.

graph LR
    FC["scpn-fusion-core<br/>physics-solver laboratory"]
    SPO["scpn-phase-orchestrator<br/>reactor semantic owner"]
    QC["scpn-quantum-control<br/>quantum and phase research"]
    CC["scpn-control<br/>control-grade facade"]
    FC -- "port / wrap solver subset<br/>(control-loop contract)" --> CC
    FC -- "canonical physics evidence bytes" --> SPO
    SPO -- "review-only semantic handoff bytes" --> CC
    QC -- "control adapter<br/>(classifier + feature contract)" --> CC
    CC -. "upstream reusable maths" .-> FC

The remainder of this page describes the internal architecture of scpn-control itself. The relationship to scpn-fusion-core is also summarised in the project README.

Module Map

This diagram is an illustrative subset for review and subsystem navigation. It does not enumerate every file or every generated capability. The authoritative live inventory is docs/_generated/capability_manifest.json, generated by tools/capability_manifest.py; run python tools/capability_manifest.py --check before publishing inventory or release-count claims.

graph TD
    CLI[cli.py] --> SCPN[scpn/]
    CLI --> CORE[core/]
    CLI --> CTRL[control/]
    CLI --> PHASE[phase/]

    subgraph "scpn/ — Petri Net Compiler"
        SPN[structure.py] --> COMP[compiler.py]
        COMP --> CNET[CompiledNet]
        CNET --> NSC[controller.py]
        CON[contracts.py] --> NSC
    end

    subgraph "core/ — Physics Solvers"
        FK[fusion_kernel.py] --> TC[tokamak_config.py]
        ITS[integrated_transport_solver.py]
        NEQ[neural_equilibrium.py]
        GT2[gyrokinetic_transport.py]
        BS2[ballooning_solver.py]
        ST2[sawtooth.py]
        NTM2[ntm_dynamics.py]
        CD2[current_diffusion.py]
        SOL2[sol_model.py]
        ISS2[integrated_scenario.py]
    end

    subgraph "control/ — Controllers"
        HINF2[h_infinity_controller.py]
        MU2[static_mu_analysis.py]
        NMPC2[nmpc_controller.py]
        GS_C[gain_scheduled_controller.py]
        CLS[closed_loop_scenario.py]
        SM2[sliding_mode_vertical.py]
        FT2[fault_tolerant_control.py]
        SC2[free_boundary_tracking.py]
        DP[disruption_predictor.py]
        GYM[gym_tokamak_env.py]
    end

    subgraph "phase/ — Paper 27 Dynamics"
        KUR[kuramoto.py] --> UPDE[upde.py]
        KNM[knm.py] --> UPDE
        UPDE --> LG[lyapunov_guard.py]
        UPDE --> RM[realtime_monitor.py]
        RM --> WS[ws_phase_stream.py]
    end

Logical Data Flow

The following diagram illustrates the signal path from equilibrium input to real-time actuation through the optional Rust acceleration layer.

GEQDSK/IMAS Input
FusionKernel (GS solver, 65×65)
    ↓                    ↓
NeuralEquilibrium   IntegratedTransportSolver
(synthetic pretraining)    (1.5D Crank-Nicolson)
    ↓                    ↓
    └──────┬─────────────┘
    Controller Selection
    ├── PID / Gain-Scheduled (GainScheduledController)
    ├── Normalized DGKF H-infinity (HInfinityController)
    ├── Riccati + static μ analysis (RiccatiStateFeedbackController)
    ├── NMPC (NMPCController, SQP 20-step)
    ├── MPC (ModelPredictiveController)
    ├── Sliding-Mode (SlidingModeVerticalController)
    ├── Fault-Tolerant (FaultTolerantController)
    ├── Shape (ShapeController)
    ├── RL/PPO (SafeRLController + SB3)
    └── SNN (LIF+NEF SNN Controller)
    DisruptionPredictor
    (LSTM + Greenwald + VDE)
    SPIMitigation → CoilSet actuation
    Native control cycle (PyO3, loopback UDP)

The native timing label is backed by validation/reports/native_handoff_comparison.json: the committed report records 5.619 µs P50 and 6.112 µs P95 native active-cycle latency over 7 repeats of 5,000 steps on an AMD EPYC 7763, using standard loopback-UDP transport at 127.0.0.1 with port base 55900. The report was generated on 2026-06-21 from source commit 5997eed1c135608dcd04720a8287ee9c10067265 in workflow run 27917648522. It records evidence class local_proxy, runtime admission fail, and production_claim_allowed=false. Treat this as a dated loopback handoff observation, not fielded plant or PCS-cycle latency, HIL evidence, or deterministic real-time admission.

The diagram is a module map and claim-boundary guide, not a statement that every listed controller is exercised in one runtime path. The currently wired bounded closed-loop E2E demo path is:

scpn-control demo --scenario combined
control.closed_loop_scenario.run_integrated_scenario_closed_loop
control.scenario_scheduler.FeedforwardController
core.integrated_scenario.IntegratedScenarioSimulator
core.integrated_scenario.audit_scenario_coupling

That path applies the scheduled-plus-feedback auxiliary-heating command before each integrated-scenario plant step, records the bounded actuator command, and emits a replay coupling audit. It is repository wiring evidence only; measured discharge validation and target-hardware admission remain separate gates.

Practical architecture framing

The stack is organized in layers so teams can replace one layer without rebuilding the whole system:

  • Plant models (Core) evolve state and produce physics-ready observables.
  • Controllers (Control) convert those observables into actions under explicit constraints.
  • Phase dynamics (Phase) evaluate example oscillator models and stream their state. They do not currently identify oscillators from reactor signals or close a plant-actuator feedback loop. Paper 27's abstract layers remain separate from the illustrative reduced-eight/refined-sixteen plasma-labelled hierarchy. A released scpn-phase-orchestrator domain-binding contract must identify any scenario-specific observation-to-oscillator mapping before a FUSION-owned plant and actuator simulation can validate feedback closure.
  • Execution boundary (Rust/Python) defines where hard-time and safety-critical work must stay deterministic and where Python orchestration can remain high-level.

The architecture separates Python orchestration from Rust/PyO3 hot paths instead of treating a language choice as a safety claim. Python surfaces carry module-level tests and claim-boundary metadata; native surfaces carry benchmark reports, runtime-admission records, and certificate evidence when those gates are available. The pure-Python and fused native paths share contracts, but they have different runtime envelopes and admissible claim levels.

Module Dependencies

Simplified internal dependency graph (arrows indicate "imports"):

graph LR
    subgraph "High Level"
        FS[tokamak_flight_sim]
        GYM[gym_tokamak_env]
    end

    subgraph "Controllers"
        MPC[fusion_neural_mpc]
        HINF[h_infinity_controller]
        MU[static_mu_analysis]
        NMPC[nmpc_controller]
        GS_CTRL[gain_scheduled]
        SM[sliding_mode_vertical]
        FT[fault_tolerant]
        SC[free_boundary_tracking]
        SNN[snn_controller]
        SRL[safe_rl_controller]
    end

    subgraph "Core Physics"
        ITS[integrated_transport_solver]
        FK[fusion_kernel]
        NEQ[neural_equilibrium]
        NT[neural_transport]
        GT[gyrokinetic_transport]
        BS[ballooning_solver]
        ST[sawtooth]
        NTM[ntm_dynamics]
        CD[current_diffusion]
        SOL[sol_model]
        ISS[integrated_scenario]
    end

    subgraph "Foundation"
        RC[_rust_compat]
        VAL[_validators]
    end

    FS --> MPC
    FS --> HINF
    GYM --> FK
    SC --> FK
    MPC --> NEQ
    ITS --> FK
    ITS --> NT
    FK --> RC
    NEQ --> RC
    RC --> VAL

Rust / Python Boundary

flowchart LR
    subgraph Python
        P1[FusionKernel]
        P2[RealtimeMonitor]
        P3[SnnPool]
        P4[MpcController]
    end

    subgraph "PyO3 Bindings (control-python)"
        B1[PyFusionKernel]
        B2[PyRealtimeMonitor]
        B3[PySnnPool]
        B4[PyMpcController]
    end

    subgraph "Rust Workspace"
        R1[control-core]
        R2[control-math]
        R3[control-control]
        R4[control-types]
    end

    P1 -. "_rust_compat" .-> B1 --> R1
    P2 -. "_rust_compat" .-> B2 --> R2
    P3 -. "_rust_compat" .-> B3 --> R3
    P4 -. "_rust_compat" .-> B4 --> R3
    R1 --> R4
    R2 --> R4
    R3 --> R4

The _rust_compat.py module probes for the compiled scpn_control_rs extension at import time. If present, hot paths (GS solve, Kuramoto step, SNN tick, MPC solve) dispatch to Rust. Otherwise, pure-NumPy fallbacks execute identically.

Data Flow: Closed-Loop Control

sequenceDiagram
    participant Plant as TokamakDigitalTwin
    participant Obs as ControlObservation
    participant Ctrl as NeuroSymbolicController
    participant Act as ControlAction
    participant Guard as LyapunovGuard

    loop every dt
        Plant->>Obs: measure(Ip, q95, βN, li, Wmhd, ...)
        Obs->>Ctrl: step(observation)
        Ctrl->>Act: (Ip_cmd, shape_cmd, heating_cmd)
        Act->>Plant: actuate(action)
        Plant->>Guard: check(θ, Ψ)
        Guard-->>Ctrl: approved / halt
    end

Directory Layout

This directory layout is a navigational subset. Use the generated capability manifest for current module, test, Rust, validation, workflow, and public-doc counts.

scpn-control/
├── src/scpn_control/     # Python package; live counts in capability manifest
│   ├── scpn/             # SPN → SNN compiler
│   ├── core/             # Equilibrium, transport, scaling
│   ├── control/          # Controllers, optional deps guarded
│   └── phase/            # Kuramoto/UPDE engine
├── scpn-control-rs/      # Rust workspace (5 crates)
├── tests/                # Module-specific Python tests


├── examples/             # Notebooks and demo scripts
├── validation/           # DIII-D, JET, SPARC, ITER configs + reference data
├── docs/                 # MkDocs site
└── tools/                # CI gates, calibration, publishing

How to read this architecture page

The architecture page traces where data and control move, and where timing constraints are documented.

In practice:

  • Treat control/ and core/ as semantic surfaces that describe behavior.
  • Treat scpn-control-rs as the execution boundary for hot-path timing constraints.
  • Treat cli.py and orchestration scripts as control-plane selectors, not as the timing-critical loop itself.

Use this map to isolate review risk: logic updates belong to module-level testing; timing regressions belong to native-path benchmarks and deployment constraints.

Architecture review workflow

Use this map as a review checklist in three steps:

  1. Traceability first: map each change to a module family (scpn, core, control, or phase) before touching timing paths.
  2. Execution boundary check: choose whether code is expected to run in Python orchestration or Rust/PyO3 hot path.
  3. Claim boundary check: only promote the relevant result channel (local_regression, reference_validated, external_code_validated, etc.) when the claim level explicitly allows it.

The same graph is therefore used for two review modes:

  • Local iteration: choose module-level acceptance tests and keep claim levels local unless a validator admits promotion.
  • Deployment admission: pin execution boundaries, capture host metadata, and route claims through the strict validators before widening public language.

The graph links code review, benchmark scope, and claim promotion without implying deployment readiness by itself.

Practical use and scope

Use this document to identify the module boundaries before making engineering changes across control, physics, transport, and verification surfaces.

  • Start here when deciding whether to modify Rust/PyO3, Python orchestration, or deployment-admission code.
  • Use it as the first checkpoint before changing subsystem APIs or control-loop contracts.
  • Cross-check release claims against this map and docs/capability_manifest.md before updating public narratives.