Skip to content

Commit ff664cb

Browse files
committed
fix: CI failures — ruff format, version sync, mypy type narrowing
- 44 files reformatted by ruff - CITATION.cff and .zenodo.json version synced to 0.19.0 - QProfile args cast to float64 for mypy NDArray compatibility Co-Authored-By: Arcane Sapience <protoscience@anulum.li>
1 parent b3749dc commit ff664cb

47 files changed

Lines changed: 1670 additions & 701 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.zenodo.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"title": "SCPN Control: Neuro-Symbolic Stochastic Petri Net Controller for Plasma Control",
3-
"version": "0.18.0",
3+
"version": "0.19.0",
44
"description": "Standalone neuro-symbolic control engine that compiles Stochastic Petri Nets into spiking neural network controllers with contract-based verification. Features full Grad-Shafranov equilibrium solver (fixed + free boundary, JAX-differentiable), three-path gyrokinetic transport (native linear eigenvalue solver + 5 external GK code interfaces + hybrid surrogate+GK validation with OOD detection and online learning), ballooning eigenvalue solver, sawtooth Kadomtsev crash model, NTM dynamics, current diffusion/drive, SOL two-point model, H-infinity/mu-synthesis/NMPC/gain-scheduled/sliding-mode/fault-tolerant controllers, safe RL (PPO 500K), scenario scheduling, real-time EFIT, shape control, integrated scenario simulator, QLKNN neural transport surrogate, adaptive Kuramoto coupling with GK-driven K_nm bridge, Lyapunov guard, and WebSocket streaming. 98 Python modules, 5 Rust crates, 3,015 tests (100% coverage), 20 CI jobs.",
55
"upload_type": "software",
66
"publication_date": "2026-03-13",

CITATION.cff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ authors:
1313
- family-names: "Sotek"
1414
given-names: "Miroslav"
1515
orcid: "https://orcid.org/0009-0009-3560-0851"
16-
version: "0.18.0"
16+
version: "0.19.0"
1717
date-released: "2026-03-14"
1818
license: "AGPL-3.0-or-later"
1919
repository-code: "https://github.com/anulum/scpn-control"

benches/bench_fusion_snn_hook.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
Run: pytest benches/bench_fusion_snn_hook.py -v
99
Requires: pytest-benchmark
1010
"""
11+
1112
from __future__ import annotations
1213

1314
import json
@@ -19,6 +20,7 @@
1920

2021
# ── Minimal LIF layer (mirrors SNN controller hot path) ──────────────
2122

23+
2224
class LIFLayer:
2325
"""Minimal LIF population for benchmarking."""
2426

@@ -50,23 +52,27 @@ def rate_to_psi(spikes: np.ndarray, nu_max: float = 100.0) -> float:
5052

5153
# ── Benchmarks ────────────────────────────────────────────────────────
5254

55+
5356
@pytest.fixture
5457
def rng():
5558
return np.random.default_rng(42)
5659

5760

5861
class TestFusionSNNHookBench:
59-
6062
@pytest.mark.benchmark(group="phase_sync_step")
6163
@pytest.mark.parametrize("N", [256, 1000, 4096])
6264
def test_phase_sync_step_only(self, benchmark, rng, N):
6365
theta = rng.uniform(-np.pi, np.pi, N)
6466
omega = rng.normal(0, 0.5, N)
6567
benchmark(
6668
kuramoto_sakaguchi_step,
67-
theta, omega,
68-
dt=1e-3, K=2.0, zeta=0.5,
69-
psi_driver=0.3, psi_mode="external",
69+
theta,
70+
omega,
71+
dt=1e-3,
72+
K=2.0,
73+
zeta=0.5,
74+
psi_driver=0.3,
75+
psi_mode="external",
7076
)
7177

7278
@pytest.mark.benchmark(group="snn_lif_step")
@@ -89,17 +95,20 @@ def closed_loop_tick():
8995
nonlocal theta, psi
9096
# Kuramoto step with current Ψ
9197
out = kuramoto_sakaguchi_step(
92-
theta, omega, dt=1e-3, K=2.0, zeta=0.5,
93-
psi_driver=psi, psi_mode="external",
98+
theta,
99+
omega,
100+
dt=1e-3,
101+
K=2.0,
102+
zeta=0.5,
103+
psi_driver=psi,
104+
psi_mode="external",
94105
)
95106
theta = out["theta1"]
96107
R = out["R"]
97108
psi_r = out["Psi_r"]
98109

99110
# Inject Kuramoto coherence into SNN synaptic current
100-
i_syn = 10.0 + 5.0 * R * np.cos(
101-
psi_r - np.linspace(0, 2 * np.pi, N_lif, endpoint=False)
102-
)
111+
i_syn = 10.0 + 5.0 * R * np.cos(psi_r - np.linspace(0, 2 * np.pi, N_lif, endpoint=False))
103112
spikes = lif.step(i_syn)
104113

105114
# Decode spike rate → Ψ for next tick
@@ -121,13 +130,16 @@ def run_100():
121130
psi = 0.0
122131
for _ in range(100):
123132
out = kuramoto_sakaguchi_step(
124-
theta, omega, dt=1e-4, K=2.0, zeta=0.5,
125-
psi_driver=psi, psi_mode="external",
133+
theta,
134+
omega,
135+
dt=1e-4,
136+
K=2.0,
137+
zeta=0.5,
138+
psi_driver=psi,
139+
psi_mode="external",
126140
)
127141
theta = out["theta1"]
128-
i_syn = 10.0 + 5.0 * out["R"] * np.cos(
129-
out["Psi_r"] - np.linspace(0, 2 * np.pi, N_lif, endpoint=False)
130-
)
142+
i_syn = 10.0 + 5.0 * out["R"] * np.cos(out["Psi_r"] - np.linspace(0, 2 * np.pi, N_lif, endpoint=False))
131143
spikes = lif.step(i_syn)
132144
psi = rate_to_psi(spikes)
133145
return out["R"]
@@ -149,6 +161,7 @@ def test_fusion_kernel_phase_sync(self, benchmark, rng, tmp_path):
149161
cfg_path = tmp_path / "bench.json"
150162
cfg_path.write_text(json.dumps(cfg))
151163
from scpn_control.core.fusion_kernel import FusionKernel
164+
152165
kernel = FusionKernel(str(cfg_path))
153166

154167
N = 1000

benchmarks/e2e_control_latency.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919
python benchmarks/e2e_control_latency.py --json
2020
python benchmarks/e2e_control_latency.py --iterations 5000
2121
"""
22+
2223
from __future__ import annotations
2324

2425
import argparse
2526
import json
26-
import sys
2727
import tempfile
2828
import time
2929
from pathlib import Path
@@ -90,9 +90,7 @@ def bench_e2e(transport, hinf, n_warmup: int, n_iter: int) -> np.ndarray:
9090
times_ns = np.empty(n_iter, dtype=np.int64)
9191
for i in range(n_iter):
9292
t0 = time.perf_counter_ns()
93-
u_prev = _e2e_iteration(
94-
transport, hinf, rng, source, dt_transport, dt_control, P_aux, u_max, slew_max, u_prev
95-
)
93+
u_prev = _e2e_iteration(transport, hinf, rng, source, dt_transport, dt_control, P_aux, u_max, slew_max, u_prev)
9694
times_ns[i] = time.perf_counter_ns() - t0
9795
return times_ns
9896

@@ -174,7 +172,7 @@ def main():
174172
print(f" Iterations: {args.iterations} Warmup: {args.warmup} Grid: 16x16")
175173
print()
176174
print(f" {'Stage':<30} {'P50 µs':>8} {'P95 µs':>8} {'P99 µs':>8}")
177-
print(f" {'-'*30} {'-'*8} {'-'*8} {'-'*8}")
175+
print(f" {'-' * 30} {'-' * 8} {'-' * 8} {'-' * 8}")
178176
for label, key in [("Kernel only (1 SOR step)", "kernel_only_us"), ("Full E2E control cycle", "e2e_us")]:
179177
d = results[key]
180178
print(f" {label:<30} {d['p50']:>8.1f} {d['p95']:>8.1f} {d['p99']:>8.1f}")

check_activated_features.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import numpy as np
33
import scpn_control_rs as rs
44

5+
56
def main():
67
print("SCPN-CONTROL: Activated Feature Verification")
78
print("-" * 40)
@@ -16,15 +17,15 @@ def main():
1617
t0 = time.perf_counter()
1718
x = rs.py_thomas_solve(a, b, c, d)
1819
t1 = time.perf_counter()
19-
print(f"Thomas Solver: {(t1-t0)*1e6:.2f} us (size 10)")
20+
print(f"Thomas Solver: {(t1 - t0) * 1e6:.2f} us (size 10)")
2021

2122
# 2. X-Point search
2223
grid_r = np.linspace(1.0, 9.0, 129)
2324
grid_z = np.linspace(-5.0, 5.0, 129)
2425
psi = np.zeros((129, 129))
2526
# Gaussian at (5.0, -3.0)
2627
RR, ZZ = np.meshgrid(grid_r, grid_z)
27-
psi = ((RR - 5.0)**2 + (ZZ + 3.0)**2)
28+
psi = (RR - 5.0) ** 2 + (ZZ + 3.0) ** 2
2829

2930
t0 = time.perf_counter()
3031
fk_tmp = rs.PyFusionKernel("iter_config.json")
@@ -38,7 +39,7 @@ def main():
3839
t0 = time.perf_counter()
3940
br, bz = fk.compute_b_field()
4041
t1 = time.perf_counter()
41-
print(f"B-Field Compute (129x129): {(t1-t0)*1e6:.2f} us")
42+
print(f"B-Field Compute (129x129): {(t1 - t0) * 1e6:.2f} us")
4243

4344
# 4. AMR Solve (New!)
4445
amr = rs.PyAmrSolver(max_levels=2, coarse_iters=100)
@@ -48,7 +49,8 @@ def main():
4849
t0 = time.perf_counter()
4950
psi_amr = amr.solve_with_hierarchy(psi, grid_r, grid_z)
5051
t1 = time.perf_counter()
51-
print(f"AMR Equilibrium Solve: {(t1-t0):.4f} s")
52+
print(f"AMR Equilibrium Solve: {(t1 - t0):.4f} s")
53+
5254

5355
if __name__ == "__main__":
5456
main()

dashboard/scpn_studio.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
44
Runs Kuramoto-Sakaguchi phase dynamics via scpn-control (no inlined engine).
55
"""
6+
67
from __future__ import annotations
78

89
import threading
@@ -37,10 +38,7 @@ def _tick_loop(monitor: RealtimeMonitor, buf: deque, stop_ev: threading.Event):
3738

3839
# -- Header --
3940
st.title("SCPN Phase Sync")
40-
st.caption(
41-
"Kuramoto-Sakaguchi mean-field | Paper 27 Knm coupling | "
42-
"[GitHub](https://github.com/anulum/scpn-control)"
43-
)
41+
st.caption("Kuramoto-Sakaguchi mean-field | Paper 27 Knm coupling | [GitHub](https://github.com/anulum/scpn-control)")
4442

4543
# -- Sidebar --
4644
with st.sidebar:
@@ -62,8 +60,10 @@ def _tick_loop(monitor: RealtimeMonitor, buf: deque, stop_ev: threading.Event):
6260
st.session_state.stop.clear()
6361
st.session_state.buffer.clear()
6462
monitor = RealtimeMonitor.from_paper27(
65-
L=layers, N_per=n_per,
66-
zeta_uniform=zeta, psi_driver=psi,
63+
L=layers,
64+
N_per=n_per,
65+
zeta_uniform=zeta,
66+
psi_driver=psi,
6767
)
6868
st.session_state.thread = threading.Thread(
6969
target=_tick_loop,
@@ -78,13 +78,14 @@ def _tick_loop(monitor: RealtimeMonitor, buf: deque, stop_ev: threading.Event):
7878
st.session_state.running = False
7979

8080
# -- Auto-start on first visit with defaults --
81-
if not st.session_state.running and (
82-
st.session_state.thread is None or not st.session_state.thread.is_alive()
83-
):
81+
if not st.session_state.running and (st.session_state.thread is None or not st.session_state.thread.is_alive()):
8482
st.session_state.stop.clear()
8583
st.session_state.buffer.clear()
8684
monitor = RealtimeMonitor.from_paper27(
87-
L=16, N_per=50, zeta_uniform=0.5, psi_driver=0.0,
85+
L=16,
86+
N_per=50,
87+
zeta_uniform=0.5,
88+
psi_driver=0.0,
8889
)
8990
st.session_state.thread = threading.Thread(
9091
target=_tick_loop,
@@ -98,9 +99,7 @@ def _tick_loop(monitor: RealtimeMonitor, buf: deque, stop_ev: threading.Event):
9899
# -- Main --
99100
frames = list(st.session_state.buffer)
100101
is_live = (
101-
st.session_state.thread is not None
102-
and st.session_state.thread.is_alive()
103-
and not st.session_state.stop.is_set()
102+
st.session_state.thread is not None and st.session_state.thread.is_alive() and not st.session_state.stop.is_set()
104103
)
105104

106105
c1, c2 = st.columns(2)
@@ -128,8 +127,10 @@ def _tick_loop(monitor: RealtimeMonitor, buf: deque, stop_ev: threading.Event):
128127
# -- Charts --
129128
try:
130129
import matplotlib
130+
131131
matplotlib.use("Agg")
132132
import matplotlib.pyplot as plt
133+
133134
HAS_MPL = True
134135
except ImportError:
135136
HAS_MPL = False

examples/advanced_control_demo.ipynb

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,7 @@
125125
"outputs": [],
126126
"source": [
127127
"from scpn_control.control.gain_scheduled_controller import (\n",
128-
" GainScheduledController,\n",
129128
" RegimeDetector,\n",
130-
" RegimeController,\n",
131129
" OperatingRegime,\n",
132130
")\n",
133131
"\n",
@@ -227,9 +225,11 @@
227225
"\n",
228226
"# Check stabilization at beta_N = 3.5\n",
229227
"rwm_test = RWMPhysics(3.5, beta_nowall, beta_wall, tau_wall)\n",
230-
"print(f\"beta_N=3.5: unstable={rwm_test.is_unstable()}, \"\n",
231-
" f\"gamma_open={rwm_test.growth_rate():.1f} s^-1, \"\n",
232-
" f\"stabilized={ctrl_rwm.is_stabilized(rwm_test)}\")"
228+
"print(\n",
229+
" f\"beta_N=3.5: unstable={rwm_test.is_unstable()}, \"\n",
230+
" f\"gamma_open={rwm_test.growth_rate():.1f} s^-1, \"\n",
231+
" f\"stabilized={ctrl_rwm.is_stabilized(rwm_test)}\"\n",
232+
")"
233233
]
234234
},
235235
{
@@ -258,10 +258,12 @@
258258
")\n",
259259
"\n",
260260
"# Define uncertainty blocks\n",
261-
"uncertainty = StructuredUncertainty([\n",
262-
" UncertaintyBlock(\"plasma_current\", size=1, bound=0.1, block_type=\"real\"),\n",
263-
" UncertaintyBlock(\"wall_position\", size=1, bound=0.2, block_type=\"complex\"),\n",
264-
"])\n",
261+
"uncertainty = StructuredUncertainty(\n",
262+
" [\n",
263+
" UncertaintyBlock(\"plasma_current\", size=1, bound=0.1, block_type=\"real\"),\n",
264+
" UncertaintyBlock(\"wall_position\", size=1, bound=0.2, block_type=\"complex\"),\n",
265+
" ]\n",
266+
")\n",
265267
"\n",
266268
"# Compute mu upper bound for a sample transfer matrix\n",
267269
"n = uncertainty.total_size()\n",
@@ -273,10 +275,12 @@
273275
"for omega in freqs:\n",
274276
" # Frequency-dependent transfer matrix (simplified)\n",
275277
" s = 1j * omega\n",
276-
" M = np.array([\n",
277-
" [1.0 / (s + 10), 0.5 / (s + 20)],\n",
278-
" [0.3 / (s + 5), 1.0 / (s + 15)],\n",
279-
" ])\n",
278+
" M = np.array(\n",
279+
" [\n",
280+
" [1.0 / (s + 10), 0.5 / (s + 20)],\n",
281+
" [0.3 / (s + 5), 1.0 / (s + 15)],\n",
282+
" ]\n",
283+
" )\n",
280284
" delta_struct = uncertainty.build_Delta_structure()\n",
281285
" mu = compute_mu_upper_bound(M, delta_struct)\n",
282286
" mu_vals.append(mu)\n",
@@ -313,7 +317,7 @@
313317
"metadata": {},
314318
"outputs": [],
315319
"source": [
316-
"from scpn_control.control.fault_tolerant_control import FDIMonitor, FaultType\n",
320+
"from scpn_control.control.fault_tolerant_control import FDIMonitor\n",
317321
"\n",
318322
"fdi = FDIMonitor(n_sensors=4, n_actuators=3, threshold_sigma=3.0, n_alert=3)\n",
319323
"\n",
@@ -376,7 +380,43 @@
376380
"id": "g2",
377381
"metadata": {},
378382
"outputs": [],
379-
"source": "from scpn_control.control.shape_controller import (\n PlasmaShapeController,\n ShapeTarget,\n CoilSet,\n)\n\ntarget = ShapeTarget(\n isoflux_points=[(8.2, 0.0), (7.8, 1.5), (6.5, 3.0),\n (5.0, 2.5), (4.5, 0.0), (5.0, -2.5),\n (6.5, -3.0), (7.8, -1.5)],\n gap_points=[(8.5, 0.0, 1.0, 0.0), (4.2, 0.0, -1.0, 0.0)],\n gap_targets=[0.1, 0.08],\n)\n\ncoils = CoilSet(n_coils=6)\n# kernel=None works — ShapeJacobian uses a mock Jacobian for the demo\nshape_ctrl = PlasmaShapeController(target=target, coil_set=coils, kernel=None)\n\n# Simulate shape correction over several iterations\npsi = np.ones((33, 33)) * 0.5 # mock flux grid\ncoil_currents = np.zeros(6)\n\ncorrections = []\nfor i in range(10):\n delta_I = shape_ctrl.step(psi, coil_currents)\n coil_currents = np.clip(coil_currents + delta_I, -coils.max_currents, coils.max_currents)\n corrections.append(np.linalg.norm(delta_I))\n\nplt.figure(figsize=(8, 4))\nplt.semilogy(corrections, \"bo-\", lw=2)\nplt.xlabel(\"Iteration\")\nplt.ylabel(\"|delta_I| [A]\")\nplt.title(\"Shape controller convergence\")\nplt.grid(True, alpha=0.3)\nplt.show()\n\nprint(f\"Final coil currents: {np.round(coil_currents, 1)}\")"
383+
"source": [
384+
"from scpn_control.control.shape_controller import (\n",
385+
" PlasmaShapeController,\n",
386+
" ShapeTarget,\n",
387+
" CoilSet,\n",
388+
")\n",
389+
"\n",
390+
"target = ShapeTarget(\n",
391+
" isoflux_points=[(8.2, 0.0), (7.8, 1.5), (6.5, 3.0), (5.0, 2.5), (4.5, 0.0), (5.0, -2.5), (6.5, -3.0), (7.8, -1.5)],\n",
392+
" gap_points=[(8.5, 0.0, 1.0, 0.0), (4.2, 0.0, -1.0, 0.0)],\n",
393+
" gap_targets=[0.1, 0.08],\n",
394+
")\n",
395+
"\n",
396+
"coils = CoilSet(n_coils=6)\n",
397+
"# kernel=None works — ShapeJacobian uses a mock Jacobian for the demo\n",
398+
"shape_ctrl = PlasmaShapeController(target=target, coil_set=coils, kernel=None)\n",
399+
"\n",
400+
"# Simulate shape correction over several iterations\n",
401+
"psi = np.ones((33, 33)) * 0.5 # mock flux grid\n",
402+
"coil_currents = np.zeros(6)\n",
403+
"\n",
404+
"corrections = []\n",
405+
"for i in range(10):\n",
406+
" delta_I = shape_ctrl.step(psi, coil_currents)\n",
407+
" coil_currents = np.clip(coil_currents + delta_I, -coils.max_currents, coils.max_currents)\n",
408+
" corrections.append(np.linalg.norm(delta_I))\n",
409+
"\n",
410+
"plt.figure(figsize=(8, 4))\n",
411+
"plt.semilogy(corrections, \"bo-\", lw=2)\n",
412+
"plt.xlabel(\"Iteration\")\n",
413+
"plt.ylabel(\"|delta_I| [A]\")\n",
414+
"plt.title(\"Shape controller convergence\")\n",
415+
"plt.grid(True, alpha=0.3)\n",
416+
"plt.show()\n",
417+
"\n",
418+
"print(f\"Final coil currents: {np.round(coil_currents, 1)}\")"
419+
]
380420
},
381421
{
382422
"cell_type": "markdown",

0 commit comments

Comments
 (0)