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-corefirst;scpn-controlports 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-controlholds only the control adapter, classifier API, and feature-ordering contract. - When
scpn-controlneeds new mathematics, the solver-level result belongs upstream inscpn-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-orchestratoralone 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-orchestratordomain-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/andcore/as semantic surfaces that describe behavior. - Treat
scpn-control-rsas the execution boundary for hot-path timing constraints. - Treat
cli.pyand 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:
- Traceability first: map each change to a module family (
scpn,core,control, orphase) before touching timing paths. - Execution boundary check: choose whether code is expected to run in Python orchestration or Rust/PyO3 hot path.
- 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.mdbefore updating public narratives.