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 three-repository SCPN ecosystem. 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-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.

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.
graph LR
    FC["scpn-fusion-core<br/>physics-solver laboratory"]
    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
    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[mu_synthesis.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[shape_controller.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)
    ├── H-infinity (HInfinityController)
    ├── Mu-Synthesis (MuSynthesisController)
    ├── 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 CI, using the standard loopback-UDP transport at 127.0.0.1 with port base 55900. Treat this as a loopback handoff campaign, not fielded plant or PCS-cycle latency.

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) manage timing and synchronization artifacts that are hard to model as low-order linear systems.
  • 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[mu_synthesis]
        NMPC[nmpc_controller]
        GS_CTRL[gain_scheduled]
        SM[sliding_mode_vertical]
        FT[fault_tolerant]
        SC[shape_controller]
        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
    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.