ThomasHeisig commited on
Commit
07fb183
·
verified ·
1 Parent(s): 98ef2d2

Sync GitHub main af692537c27175c8410bf57a747be0caf23718e2 (part 7)

Browse files
Files changed (44) hide show
  1. scripts/build_frozen_environment.py +136 -0
  2. scripts/check_canonical_imports.py +48 -0
  3. scripts/check_cuda_kernel_freeze.py +56 -0
  4. scripts/check_cuda_parity_contract.py +109 -0
  5. scripts/check_cuda_semantic_boundary.py +86 -0
  6. scripts/run_cuda_hardware_acceptance.py +291 -0
  7. scripts/run_fe_acceptance.py +121 -0
  8. scripts/run_playground_cuda_smoke.py +119 -0
  9. scripts/validate_playground_ptx.py +44 -0
  10. src/acceleration/__init__.py +1 -0
  11. src/acceleration/cuda/__init__.py +34 -0
  12. src/acceleration/cuda/backend.py +176 -0
  13. src/acceleration/cuda/driver.py +478 -0
  14. src/acceleration/cuda/errors.py +9 -0
  15. src/acceleration/cuda/memory.py +13 -0
  16. src/acceleration/cuda/nvrtc.py +116 -0
  17. src/acceleration/cuda/plasticity/__init__.py +18 -0
  18. src/acceleration/cuda/plasticity/backend.py +268 -0
  19. src/acceleration/cuda/plasticity/contracts.py +28 -0
  20. src/acceleration/cuda/plasticity/kernel.cu +62 -0
  21. src/acceleration/cuda/plasticity/reference.py +161 -0
  22. src/acceleration/cuda/preflight.py +78 -0
  23. src/acceleration/cuda/ptxas.py +119 -0
  24. src/acceleration/cuda/recurrent/__init__.py +27 -0
  25. src/acceleration/cuda/recurrent/backend.py +375 -0
  26. src/acceleration/cuda/recurrent/config.py +116 -0
  27. src/acceleration/cuda/recurrent/kernel.cu +113 -0
  28. src/acceleration/cuda/recurrent/reference.py +153 -0
  29. src/acceleration/cuda/recurrent/state.py +223 -0
  30. src/controller/runtime.py +1 -1
  31. src/dashboard/__init__.py +6 -5
  32. src/dashboard/experiment_evaluation.py +166 -0
  33. src/dashboard/gate_status.py +1 -1
  34. src/dashboard/integration_status.py +201 -0
  35. src/dashboard/network_settings.py +69 -0
  36. src/dashboard/playground_api.py +375 -6
  37. src/dashboard/server.py +75 -2
  38. src/dashboard/static/experiment-workflow-base.js +128 -1
  39. src/dashboard/static/frontend/index.js +2 -0
  40. src/dashboard/static/frontend/modules/acceleration-integration.js +204 -0
  41. src/dashboard/static/frontend/modules/neuron-model-science.js +16 -16
  42. src/dashboard/static/frontend/modules/playground-workspace.js +859 -0
  43. src/dashboard/static/frontend/modules/playground.js +308 -37
  44. src/dashboard/static/frontend/modules/recurrent-snn-stage.js +17 -20
scripts/build_frozen_environment.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Build a deterministic Frozen-Environment engineering manifest artifact."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+
9
+ from src.embodiment.deterministic import DeterministicTargetEnvironment
10
+ from src.embodiment.msba import SymbolFrame
11
+ from src.embodiment.neural_io_contracts import BoundaryFrame
12
+ from src.experience.frozen_environment import (
13
+ FreezeMode,
14
+ FrozenBoundaryFrame,
15
+ FrozenEnvironmentManifest,
16
+ RNGContractMode,
17
+ WorldRNGContract,
18
+ )
19
+ from src.verification.frozen_environment import write_manifest_artifact
20
+
21
+
22
+ def _boundary_frame(tick: int) -> FrozenBoundaryFrame:
23
+ symbol = SymbolFrame(
24
+ payload=json.dumps({"tick": tick}, sort_keys=True).encode("utf-8"),
25
+ codec="json",
26
+ sequence=tick,
27
+ provenance="fe-builder",
28
+ )
29
+ return FrozenBoundaryFrame.from_frame(
30
+ BoundaryFrame(
31
+ schema_version=1,
32
+ frame_id=f"fe-frame-{tick}",
33
+ stream_id="fe-sensor",
34
+ sequence=tick,
35
+ direction="INBOUND",
36
+ kind="DIGITAL",
37
+ content_type="application/json",
38
+ schema_id="fe-builder-v1",
39
+ symbol_frame=symbol,
40
+ correlation_id=f"fe-{tick}",
41
+ priority=0,
42
+ source_id="fe-builder",
43
+ provenance="engineering-verification",
44
+ arrival_time_ns=None,
45
+ admitted_tick=tick,
46
+ )
47
+ )
48
+
49
+
50
+ def _build(
51
+ mode: FreezeMode, *, target: int, ticks: int, seed: int
52
+ ) -> FrozenEnvironmentManifest:
53
+ if ticks < 1:
54
+ raise ValueError("ticks must be >= 1")
55
+ schedule = tuple(range(ticks))
56
+ if mode is FreezeMode.FE1_BOUNDARY_REPLAY:
57
+ return FrozenEnvironmentManifest(
58
+ mode=mode,
59
+ environment_id="boundary-replay-v1",
60
+ environment_version="1",
61
+ environment_config={"builder": "deterministic-fixture"},
62
+ rng=WorldRNGContract(
63
+ algorithm="none",
64
+ version="1",
65
+ mode=RNGContractMode.NONE,
66
+ initial_fingerprint="none",
67
+ ),
68
+ sensor_schedule=schedule,
69
+ action_schema={"kind": "none"},
70
+ reward_contract={"kind": "none"},
71
+ episode_policy={"reset": "explicit"},
72
+ boundary_frames=tuple(_boundary_frame(tick) for tick in schedule),
73
+ )
74
+
75
+ env = DeterministicTargetEnvironment(target=target)
76
+ env.reset(seed=seed)
77
+ return FrozenEnvironmentManifest(
78
+ mode=mode,
79
+ environment_id=env.environment_id,
80
+ environment_version="1",
81
+ environment_config={"target": target},
82
+ rng=WorldRNGContract(
83
+ algorithm="none",
84
+ version="1",
85
+ mode=RNGContractMode.NONE,
86
+ initial_fingerprint=env.rng_fingerprint(),
87
+ ),
88
+ sensor_schedule=schedule,
89
+ action_schema={
90
+ "actuator_id": "target-actuator",
91
+ "actions": ["left", "right"],
92
+ },
93
+ reward_contract={"type": "target-hit", "hit_reward": 1.0},
94
+ episode_policy={
95
+ "reset": "explicit",
96
+ "pending_rewards_cross_episode": False,
97
+ },
98
+ initial_world_state=env.snapshot_state(),
99
+ )
100
+
101
+
102
+ def main() -> int:
103
+ parser = argparse.ArgumentParser()
104
+ parser.add_argument(
105
+ "--mode", choices=[mode.value for mode in FreezeMode], required=True
106
+ )
107
+ parser.add_argument("--output", required=True)
108
+ parser.add_argument("--target", type=int, default=2)
109
+ parser.add_argument("--ticks", type=int, default=2)
110
+ parser.add_argument("--seed", type=int, default=0)
111
+ args = parser.parse_args()
112
+
113
+ manifest = _build(
114
+ FreezeMode(args.mode),
115
+ target=args.target,
116
+ ticks=args.ticks,
117
+ seed=args.seed,
118
+ )
119
+ path = write_manifest_artifact(args.output, manifest)
120
+ print(
121
+ json.dumps(
122
+ {
123
+ "classification": "FROZEN_ENVIRONMENT_ENGINEERING_ARTIFACT",
124
+ "scientific_evidence": False,
125
+ "path": str(path),
126
+ "mode": manifest.mode.value,
127
+ "manifest_sha256": manifest.manifest_sha256,
128
+ },
129
+ sort_keys=True,
130
+ )
131
+ )
132
+ return 0
133
+
134
+
135
+ if __name__ == "__main__":
136
+ raise SystemExit(main())
scripts/check_canonical_imports.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fail-closed canonical import boundary gate for Wave 4."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ast
6
+ from pathlib import Path
7
+
8
+ ROOT = Path(__file__).resolve().parents[1]
9
+ TARGETS = (
10
+ ROOT / "src" / "acceleration",
11
+ ROOT / "src" / "verification",
12
+ )
13
+
14
+
15
+ def forbidden_imports() -> list[str]:
16
+ failures: list[str] = []
17
+ for root in TARGETS:
18
+ for path in root.rglob("*.py"):
19
+ tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
20
+ for node in ast.walk(tree):
21
+ if isinstance(node, ast.Import):
22
+ for alias in node.names:
23
+ if alias.name.startswith("src.playground"):
24
+ failures.append(
25
+ f"{path.relative_to(ROOT)}: import {alias.name}"
26
+ )
27
+ elif isinstance(node, ast.ImportFrom):
28
+ module = node.module or ""
29
+ if module.startswith("src.playground"):
30
+ failures.append(
31
+ f"{path.relative_to(ROOT)}: from {module} import ..."
32
+ )
33
+ return failures
34
+
35
+
36
+ def main() -> int:
37
+ failures = forbidden_imports()
38
+ if failures:
39
+ raise SystemExit(
40
+ "canonical acceleration/verification layers import Playground:\n"
41
+ + "\n".join(failures)
42
+ )
43
+ print("canonical import boundary: PASS")
44
+ return 0
45
+
46
+
47
+ if __name__ == "__main__":
48
+ raise SystemExit(main())
scripts/check_cuda_kernel_freeze.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Verify Wave-4 CUDA kernel extraction preserved kernel bytes exactly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ from pathlib import Path
7
+
8
+ ROOT = Path(__file__).resolve().parents[1]
9
+ PAIRS = (
10
+ (
11
+ ROOT / "src" / "playground" / "cuda" / "recurrent.cu",
12
+ ROOT / "src" / "acceleration" / "cuda" / "recurrent" / "kernel.cu",
13
+ ),
14
+ (
15
+ ROOT / "src" / "playground" / "cuda" / "builder_synapses.cu",
16
+ ROOT / "src" / "acceleration" / "cuda" / "plasticity" / "kernel.cu",
17
+ ),
18
+ )
19
+
20
+
21
+ def sha256(path: Path) -> str:
22
+ return hashlib.sha256(path.read_bytes()).hexdigest()
23
+
24
+
25
+ def kernel_mismatches() -> list[str]:
26
+ mismatches: list[str] = []
27
+ for legacy, canonical in PAIRS:
28
+ if not legacy.is_file() or not canonical.is_file():
29
+ mismatches.append(
30
+ f"missing kernel pair: {legacy.relative_to(ROOT)} / "
31
+ f"{canonical.relative_to(ROOT)}"
32
+ )
33
+ continue
34
+ if legacy.read_bytes() != canonical.read_bytes():
35
+ mismatches.append(
36
+ f"kernel changed during extraction: "
37
+ f"{legacy.relative_to(ROOT)}={sha256(legacy)} "
38
+ f"{canonical.relative_to(ROOT)}={sha256(canonical)}"
39
+ )
40
+ return mismatches
41
+
42
+
43
+ def main() -> int:
44
+ mismatches = kernel_mismatches()
45
+ if mismatches:
46
+ raise SystemExit("\n".join(mismatches))
47
+ for legacy, canonical in PAIRS:
48
+ print(
49
+ f"kernel freeze PASS: {canonical.relative_to(ROOT)} "
50
+ f"sha256={sha256(canonical)}"
51
+ )
52
+ return 0
53
+
54
+
55
+ if __name__ == "__main__":
56
+ raise SystemExit(main())
scripts/check_cuda_parity_contract.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fail-closed Wave-4 canonical cross-backend parity contract gate.
2
+
3
+ Hosted CI does not have physical CUDA. This gate verifies that CPU and CUDA
4
+ conform to the canonical ExecutionBackend surface, share one canonical
5
+ configuration identity, and that the named physical acceptance test is wired
6
+ through canonical parity functions rather than Playground helpers.
7
+
8
+ Physical D1/D2 execution is performed by scripts/run_cuda_hardware_acceptance.py.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import ast
14
+ from collections.abc import Mapping
15
+ from pathlib import Path
16
+
17
+ from src.acceleration.cuda import CUDABackend
18
+ from src.acceleration.cuda.recurrent import (
19
+ CPUReferenceBackend,
20
+ recurrent_fixture,
21
+ recurrent_inputs_to_mapping,
22
+ )
23
+ from src.runtime.backend import ExecutionBackend
24
+ from src.verification.parity import config_fingerprint
25
+
26
+ ROOT = Path(__file__).resolve().parents[1]
27
+ TEST_FILE = ROOT / "tests" / "test_cuda_wave4_backend.py"
28
+ HARDWARE_TEST = "test_physical_cpu_cuda_cross_backend_d1_d2_parity"
29
+
30
+
31
+ def _hardware_test_source() -> str:
32
+ source = TEST_FILE.read_text(encoding="utf-8")
33
+ tree = ast.parse(source, filename=str(TEST_FILE))
34
+ for node in tree.body:
35
+ if (
36
+ isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
37
+ and node.name == HARDWARE_TEST
38
+ ):
39
+ segment = ast.get_source_segment(source, node)
40
+ if segment is None:
41
+ raise RuntimeError("could not recover hardware acceptance test source")
42
+ return segment
43
+ raise RuntimeError(f"missing named hardware acceptance test: {HARDWARE_TEST}")
44
+
45
+
46
+ def failures() -> list[str]:
47
+ problems: list[str] = []
48
+
49
+ cpu: ExecutionBackend = CPUReferenceBackend()
50
+ cuda: ExecutionBackend = CUDABackend()
51
+ config = recurrent_inputs_to_mapping(
52
+ recurrent_fixture(n_neurons=8, ticks=4, model="lif")
53
+ )
54
+ seed = 12345
55
+ cpu.initialize(config, seed)
56
+ cuda.initialize(config, seed)
57
+
58
+ cpu_config = cpu.snapshot().payload.get("config")
59
+ cuda_config = cuda.snapshot().payload.get("config")
60
+ if not isinstance(cpu_config, Mapping) or not isinstance(cuda_config, Mapping):
61
+ problems.append("backend snapshots must expose canonical mapping configs")
62
+ elif config_fingerprint(cpu_config) != config_fingerprint(cuda_config):
63
+ problems.append("CPU/CUDA canonical configuration fingerprints differ")
64
+
65
+ try:
66
+ test_source = _hardware_test_source()
67
+ except RuntimeError as exc:
68
+ problems.append(str(exc))
69
+ test_source = ""
70
+
71
+ required_tokens = (
72
+ "CPUReferenceBackend",
73
+ "CUDABackend",
74
+ "exact_spike_parity",
75
+ "state_vector_parity",
76
+ "tolerance=1.0e-4",
77
+ )
78
+ for token in required_tokens:
79
+ if token not in test_source:
80
+ problems.append(
81
+ "hardware parity acceptance test is missing canonical token: " + token
82
+ )
83
+
84
+ full_test_source = TEST_FILE.read_text(encoding="utf-8")
85
+ if "MHRN_TEST_CUDA_HARDWARE" not in full_test_source:
86
+ problems.append("hardware parity acceptance test must remain opt-in")
87
+
88
+ if "playground" in test_source.lower():
89
+ problems.append(
90
+ "hardware parity acceptance test must not use Playground parity helpers"
91
+ )
92
+
93
+ return problems
94
+
95
+
96
+ def main() -> int:
97
+ problems = failures()
98
+ if problems:
99
+ raise SystemExit(
100
+ "Wave-4 canonical parity contract failed:\n" + "\n".join(problems)
101
+ )
102
+ print("Wave-4 canonical parity contract: PASS")
103
+ print("hardware acceptance test: PRESENT / OPT-IN")
104
+ print("D3 status: NOT_CLAIMED_IN_WAVE4")
105
+ return 0
106
+
107
+
108
+ if __name__ == "__main__":
109
+ raise SystemExit(main())
scripts/check_cuda_semantic_boundary.py ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fail-closed Wave-4 CUDA capability/learning-semantics gate."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from src.acceleration.cuda import CUDABackend
8
+ from src.acceleration.cuda.plasticity.contracts import (
9
+ LEARNING_CONTRACT_ID,
10
+ LEARNING_CONTRACT_STATUS,
11
+ PLASTICITY_SEMANTICS,
12
+ )
13
+
14
+ ROOT = Path(__file__).resolve().parents[1]
15
+ LEARNING_CONTRACT = (
16
+ ROOT / "docs" / "02-architecture" / "MHRN_LEARNING_SYNAPSE_CONTRACT.md"
17
+ )
18
+
19
+
20
+ def failures() -> list[str]:
21
+ problems: list[str] = []
22
+ caps = CUDABackend().capabilities()
23
+
24
+ expected = {
25
+ "supports_recurrent": True,
26
+ "supports_plasticity": True,
27
+ "supports_pan_hyperstate": False,
28
+ "supports_structural_plasticity": False,
29
+ "max_neurons": 4096,
30
+ "max_ticks": 2000,
31
+ "max_edges": 65536,
32
+ "deterministic": True,
33
+ "plasticity_semantics": "NON_CANONICAL_DRAFT",
34
+ "execution_mode": "BOUNDED_REPLAY_REFERENCE",
35
+ }
36
+ actual = caps.to_mapping()
37
+ for key, value in expected.items():
38
+ if actual.get(key) != value:
39
+ problems.append(
40
+ f"CUDA capability mismatch: {key}={actual.get(key)!r}, expected {value!r}"
41
+ )
42
+
43
+ if PLASTICITY_SEMANTICS != "NON_CANONICAL_DRAFT":
44
+ problems.append(
45
+ "plasticity semantics must remain NON_CANONICAL_DRAFT until contract alignment"
46
+ )
47
+ if LEARNING_CONTRACT_ID != "mhrn-learning-synapse-v1":
48
+ problems.append(f"unexpected learning contract id: {LEARNING_CONTRACT_ID}")
49
+ if LEARNING_CONTRACT_STATUS != "ALIGNMENT_PENDING":
50
+ problems.append(
51
+ "learning contract status must remain ALIGNMENT_PENDING in Wave 4"
52
+ )
53
+
54
+ if not LEARNING_CONTRACT.is_file():
55
+ problems.append("canonical learning/synapse contract document is missing")
56
+ else:
57
+ text = LEARNING_CONTRACT.read_text(encoding="utf-8")
58
+ required = (
59
+ "canonical design contract; backend implementation alignment pending",
60
+ "learning_contract_id = mhrn-learning-synapse-v1",
61
+ "CPU and CUDA must not implement two merely similar learning rules",
62
+ )
63
+ for phrase in required:
64
+ if phrase not in text:
65
+ problems.append(
66
+ "learning contract document no longer contains required boundary: "
67
+ + phrase
68
+ )
69
+
70
+ return problems
71
+
72
+
73
+ def main() -> int:
74
+ problems = failures()
75
+ if problems:
76
+ raise SystemExit(
77
+ "Wave-4 CUDA semantic boundary failed:\n" + "\n".join(problems)
78
+ )
79
+ print("Wave-4 CUDA semantic boundary: PASS")
80
+ print("plasticity_semantics=NON_CANONICAL_DRAFT")
81
+ print("learning_contract_status=ALIGNMENT_PENDING")
82
+ return 0
83
+
84
+
85
+ if __name__ == "__main__":
86
+ raise SystemExit(main())
scripts/run_cuda_hardware_acceptance.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run canonical CUDA hardware acceptance on a physical NVIDIA GPU.
2
+
3
+ The runner always performs Wave-4 D1/D2. Optional FE-3 adds both the historical
4
+ Builder D3c bridge and the canonical FrozenEnvironment CPU-vs-CUDA live-loop
5
+ acceptance. All outputs are engineering verification only.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import os
13
+ import shutil
14
+ import subprocess
15
+ import sys
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+
19
+ ROOT = Path(__file__).resolve().parents[1]
20
+ DEFAULT_FE3_MANIFEST = (
21
+ ROOT
22
+ / "research"
23
+ / "verification"
24
+ / "frozen_environment"
25
+ / "FE3_DETERMINISTIC_TARGET_V1.json"
26
+ )
27
+
28
+
29
+ def _gpu_identity() -> str:
30
+ executable = shutil.which("nvidia-smi")
31
+ if executable is None:
32
+ raise SystemExit(
33
+ "CUDA hardware acceptance requires nvidia-smi on a physical NVIDIA host"
34
+ )
35
+ completed = subprocess.run(
36
+ [
37
+ executable,
38
+ "--query-gpu=name,driver_version",
39
+ "--format=csv,noheader",
40
+ ],
41
+ check=True,
42
+ capture_output=True,
43
+ text=True,
44
+ )
45
+ identity = completed.stdout.strip()
46
+ if not identity:
47
+ raise SystemExit("nvidia-smi returned no GPU identity")
48
+ return identity
49
+
50
+
51
+ def _last_json_object(stdout: str) -> dict[str, object] | None:
52
+ for line in reversed(stdout.splitlines()):
53
+ line = line.strip()
54
+ if not line.startswith("{"):
55
+ continue
56
+ try:
57
+ value = json.loads(line)
58
+ except json.JSONDecodeError:
59
+ continue
60
+ if isinstance(value, dict):
61
+ return value
62
+ return None
63
+
64
+
65
+ def _run_command_case(
66
+ *,
67
+ label: str,
68
+ command: list[str],
69
+ env: dict[str, str],
70
+ parse_report: bool = False,
71
+ ) -> dict[str, object]:
72
+ print(f"[{label}] Running:", " ".join(command))
73
+ completed = subprocess.run(
74
+ command,
75
+ cwd=ROOT,
76
+ env=env,
77
+ check=False,
78
+ capture_output=True,
79
+ text=True,
80
+ )
81
+ if completed.stdout:
82
+ print(completed.stdout, end="")
83
+ if completed.stderr:
84
+ print(completed.stderr, end="", file=sys.stderr)
85
+ result: dict[str, object] = {
86
+ "label": label,
87
+ "passed": completed.returncode == 0,
88
+ "returncode": completed.returncode,
89
+ "command": command,
90
+ }
91
+ if parse_report:
92
+ result["report"] = _last_json_object(completed.stdout)
93
+ return result
94
+
95
+
96
+ def _run_pytest_case(
97
+ *,
98
+ label: str,
99
+ selectors: list[str],
100
+ env: dict[str, str],
101
+ ) -> dict[str, object]:
102
+ return _run_command_case(
103
+ label=label,
104
+ command=[sys.executable, "-m", "pytest", "-v", "--tb=short", *selectors],
105
+ env=env,
106
+ )
107
+
108
+
109
+ def _write_artifacts(
110
+ report: dict[str, object],
111
+ output_dir: Path,
112
+ ) -> tuple[Path, Path]:
113
+ output_dir.mkdir(parents=True, exist_ok=True)
114
+ day = datetime.now(timezone.utc).date().isoformat()
115
+ json_path = output_dir / f"HARDWARE_ACCEPTANCE_{day}.json"
116
+ markdown_path = output_dir / f"HARDWARE_ACCEPTANCE_{day}.md"
117
+ json_path.write_text(
118
+ json.dumps(report, sort_keys=True, indent=2, ensure_ascii=True) + "\n",
119
+ encoding="utf-8",
120
+ )
121
+ markdown_path.write_text(
122
+ "\n".join(
123
+ [
124
+ f"# Hardware Acceptance {day}",
125
+ "",
126
+ "**Classification:** Engineering Verification; not DATA/EVID.",
127
+ "",
128
+ f"- GPU: {report['gpu_identity']}",
129
+ f"- Wave-4 D1/D2: {report['wave4_physical']}",
130
+ f"- Builder D3c bridge: {report['builder_d3c_bridge']}",
131
+ f"- Canonical Frozen-Environment FE-3: {report['frozen_environment_fe3']}",
132
+ f"- Full FE-3 accepted: {report['full_fe3_accepted']}",
133
+ "",
134
+ "## Interpretation boundary",
135
+ "",
136
+ "- No scientific EVID or CLAIM is created.",
137
+ "- No speedup claim is created.",
138
+ "- PAN hyperstate is not in scope.",
139
+ "- Canonical learning semantics are not in scope.",
140
+ "- CPU/CUDA execution fingerprints are expected to differ because backend identity is provenance.",
141
+ "- Exact FE-3 acceptance requires the same manifest hash, live-input fingerprint and D3c trajectory hash.",
142
+ "",
143
+ ]
144
+ ),
145
+ encoding="utf-8",
146
+ )
147
+ return json_path, markdown_path
148
+
149
+
150
+ def main() -> int:
151
+ parser = argparse.ArgumentParser()
152
+ parser.add_argument(
153
+ "--full",
154
+ action="store_true",
155
+ help="rerun all recurrent/plasticity physical acceptance cases too",
156
+ )
157
+ parser.add_argument(
158
+ "--include-fe3",
159
+ action="store_true",
160
+ help=(
161
+ "add canonical Frozen-Environment FE-3 CPU-vs-CUDA D3c acceptance "
162
+ "and the historical Builder D3c bridge; Wave-4 checks still run"
163
+ ),
164
+ )
165
+ parser.add_argument(
166
+ "--manifest",
167
+ default=str(DEFAULT_FE3_MANIFEST),
168
+ help="versioned FE-3 manifest artifact used by --include-fe3",
169
+ )
170
+ parser.add_argument(
171
+ "--output-dir",
172
+ default=str(ROOT / "docs" / "canonical"),
173
+ help="write dated JSON and Markdown engineering-verification artifacts here",
174
+ )
175
+ parser.add_argument(
176
+ "--require-gpu",
177
+ default="",
178
+ help="fail unless the reported GPU identity contains this string",
179
+ )
180
+ args = parser.parse_args()
181
+
182
+ identity = _gpu_identity()
183
+ print(f"CUDA hardware identity: {identity}")
184
+ if args.require_gpu and args.require_gpu.lower() not in identity.lower():
185
+ raise SystemExit(
186
+ f"required GPU substring {args.require_gpu!r} not present in {identity!r}"
187
+ )
188
+
189
+ env = dict(os.environ)
190
+ env["MHRN_TEST_CUDA_HARDWARE"] = "1"
191
+
192
+ results: list[dict[str, object]] = []
193
+ results.append(
194
+ _run_pytest_case(
195
+ label="WAVE4_D1_D2",
196
+ selectors=[
197
+ "tests/test_cuda_wave4_backend.py::"
198
+ "test_physical_cpu_cuda_cross_backend_d1_d2_parity"
199
+ ],
200
+ env=env,
201
+ )
202
+ )
203
+
204
+ if args.full:
205
+ results.append(
206
+ _run_pytest_case(
207
+ label="WAVE4_RECURRENT_PLASTICITY_FULL",
208
+ selectors=[
209
+ "tests/test_playground_cuda_recurrent.py",
210
+ "tests/test_playground_cuda_plasticity.py",
211
+ "-k",
212
+ "physical",
213
+ ],
214
+ env=env,
215
+ )
216
+ )
217
+
218
+ bridge_status = "NOT_REQUESTED"
219
+ fe3_status = "NOT_REQUESTED"
220
+ full_fe3_accepted = False
221
+ if args.include_fe3:
222
+ bridge = _run_pytest_case(
223
+ label="BUILDER_D3C_BRIDGE",
224
+ selectors=[
225
+ "tests/test_playground_cuda_builder.py::"
226
+ "test_full_pan_builder_hardware_d3"
227
+ ],
228
+ env=env,
229
+ )
230
+ results.append(bridge)
231
+ bridge_status = "PASS" if bool(bridge["passed"]) else "FAIL"
232
+
233
+ fe3 = _run_command_case(
234
+ label="FROZEN_ENVIRONMENT_FE3_CPU_CUDA_D3C",
235
+ command=[
236
+ sys.executable,
237
+ "scripts/run_fe_acceptance.py",
238
+ "--manifest",
239
+ str(Path(args.manifest)),
240
+ "--require-cuda",
241
+ ],
242
+ env=env,
243
+ parse_report=True,
244
+ )
245
+ results.append(fe3)
246
+ fe3_status = "PASS" if bool(fe3["passed"]) else "FAIL"
247
+ full_fe3_accepted = bool(fe3["passed"])
248
+
249
+ passed = all(bool(item["passed"]) for item in results)
250
+ report: dict[str, object] = {
251
+ "classification": "CUDA_HARDWARE_ACCEPTANCE_REPORT",
252
+ "scientific_evidence": False,
253
+ "gpu_identity": identity,
254
+ "wave4_physical": "PASS" if bool(results[0]["passed"]) else "FAIL",
255
+ "builder_d3c_bridge": bridge_status,
256
+ "frozen_environment_fe3": fe3_status,
257
+ "full_fe3_accepted": full_fe3_accepted,
258
+ "fe3_manifest": str(Path(args.manifest)),
259
+ "kernel_semantics_changed": False,
260
+ "execution_fingerprint_rule": (
261
+ "BACKEND_IDENTITY_IS_PROVENANCE; CPU/CUDA FINGERPRINTS DIFFER"
262
+ ),
263
+ "limitations": {
264
+ "speedup_claim": False,
265
+ "pan_hyperstate": False,
266
+ "canonical_learning_contract": False,
267
+ "scientific_data": False,
268
+ "evidence": False,
269
+ },
270
+ "results": results,
271
+ "passed": passed,
272
+ }
273
+ json_path, markdown_path = _write_artifacts(report, Path(args.output_dir))
274
+ report["artifact_json"] = str(json_path)
275
+ report["artifact_markdown"] = str(markdown_path)
276
+ print(json.dumps(report, sort_keys=True))
277
+
278
+ if not passed:
279
+ raise SystemExit(1)
280
+
281
+ print("CUDA hardware acceptance selections: PASS")
282
+ if args.include_fe3:
283
+ print(
284
+ "Canonical Frozen-Environment FE-3 CPU/CUDA D3c: PASS; "
285
+ "engineering verification only"
286
+ )
287
+ return 0
288
+
289
+
290
+ if __name__ == "__main__":
291
+ raise SystemExit(main())
scripts/run_fe_acceptance.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Run FE-1/FE-2/FE-3 engineering acceptance for one frozen manifest."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ from collections.abc import Callable
10
+
11
+ from src.acceleration.cuda import CUDABackend
12
+ from src.acceleration.cuda.recurrent import CPUReferenceBackend
13
+ from src.embodiment.deterministic import DeterministicTargetEnvironment
14
+ from src.embodiment.models import ActionCommand, EnvironmentObservation
15
+ from src.experience.frozen_environment import FreezeMode, FrozenWorldAdapter
16
+ from src.verification.frozen_environment import (
17
+ load_manifest_artifact,
18
+ run_fe1_integrity,
19
+ run_fe2_replay_determinism,
20
+ run_fe3_backend_parity,
21
+ run_fe3_cpu_self_control,
22
+ )
23
+
24
+
25
+ def _target_world_factory(target: int) -> Callable[[], FrozenWorldAdapter]:
26
+ def factory() -> FrozenWorldAdapter:
27
+ return DeterministicTargetEnvironment(target=target)
28
+
29
+ return factory
30
+
31
+
32
+ def _right_policy(
33
+ tick: int,
34
+ _previous: EnvironmentObservation | None,
35
+ ) -> ActionCommand:
36
+ return ActionCommand("target-actuator", tick, "right")
37
+
38
+
39
+ def main() -> int:
40
+ parser = argparse.ArgumentParser()
41
+ parser.add_argument("--manifest", required=True)
42
+ parser.add_argument("--repeats", type=int, default=10)
43
+ parser.add_argument(
44
+ "--require-cuda",
45
+ action="store_true",
46
+ help=(
47
+ "Fail unless canonical FrozenEnvironment FE-3 has a live backend adapter; "
48
+ "the Builder D3c hardware bridge does not satisfy this gate."
49
+ ),
50
+ )
51
+ args = parser.parse_args()
52
+
53
+ manifest = load_manifest_artifact(args.manifest)
54
+ results: list[dict[str, object]] = []
55
+
56
+ if manifest.mode is FreezeMode.FE1_BOUNDARY_REPLAY:
57
+ results.append(run_fe1_integrity(manifest).to_mapping())
58
+ else:
59
+ target = manifest.environment_config.get("target")
60
+ if type(target) is not int:
61
+ raise ValueError(
62
+ "acceptance CLI currently supports deterministic-target manifests only"
63
+ )
64
+ factory = _target_world_factory(target)
65
+ results.append(
66
+ run_fe2_replay_determinism(
67
+ manifest,
68
+ factory,
69
+ _right_policy,
70
+ repeats=args.repeats,
71
+ ).to_mapping()
72
+ )
73
+ if manifest.mode is FreezeMode.FE3_FULL_DETERMINISTIC_LIVE_LOOP:
74
+ results.append(
75
+ run_fe3_cpu_self_control(
76
+ manifest,
77
+ factory,
78
+ _right_policy,
79
+ ).to_mapping()
80
+ )
81
+
82
+ cuda_status = "NOT_REQUESTED"
83
+ passed = all(bool(item["passed"]) for item in results)
84
+ if args.require_cuda:
85
+ if manifest.mode is not FreezeMode.FE3_FULL_DETERMINISTIC_LIVE_LOOP:
86
+ cuda_status = "REQUIRES_FE3_MANIFEST"
87
+ passed = False
88
+ elif os.environ.get("MHRN_TEST_CUDA_HARDWARE") != "1":
89
+ cuda_status = "REQUIRES_MHRN_TEST_CUDA_HARDWARE"
90
+ passed = False
91
+ else:
92
+ target = manifest.environment_config.get("target")
93
+ if type(target) is not int:
94
+ raise ValueError(
95
+ "CUDA FE-3 acceptance supports deterministic-target manifests only"
96
+ )
97
+ hardware = run_fe3_backend_parity(
98
+ manifest,
99
+ _target_world_factory(target),
100
+ CPUReferenceBackend,
101
+ CUDABackend,
102
+ )
103
+ results.append(hardware.to_mapping())
104
+ cuda_status = "PASS" if hardware.passed else "FAIL"
105
+ passed = passed and hardware.passed
106
+
107
+ report = {
108
+ "classification": "FROZEN_ENVIRONMENT_ACCEPTANCE_REPORT",
109
+ "scientific_evidence": False,
110
+ "manifest_sha256": manifest.manifest_sha256,
111
+ "mode": manifest.mode.value,
112
+ "results": results,
113
+ "cuda_d3": cuda_status,
114
+ "passed": passed,
115
+ }
116
+ print(json.dumps(report, sort_keys=True))
117
+ return 0 if passed else 1
118
+
119
+
120
+ if __name__ == "__main__":
121
+ raise SystemExit(main())
scripts/run_playground_cuda_smoke.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run a real single-tick CUDA gate-kernel smoke/parity check.
2
+
3
+ This script requires:
4
+ - NVIDIA driver visible to the process;
5
+ - ptxas on PATH (CUDA toolkit);
6
+ - a supported NVIDIA GPU.
7
+
8
+ It is intentionally not part of ordinary hosted CI because GitHub-hosted Linux
9
+ runners do not expose an NVIDIA device.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+
16
+ from src.playground.cuda import (
17
+ GateLaunchInputs,
18
+ compile_mapping,
19
+ cpu_gate_reference,
20
+ execute_gate_bundle,
21
+ gate_execution_parity_summary,
22
+ )
23
+
24
+
25
+ def _inputs(bundle: object) -> GateLaunchInputs:
26
+ manifest = getattr(bundle, "manifest")
27
+ abi = manifest["kernel_abi"]
28
+ pan_dimensions = int(abi["pan_dimensions"])
29
+ return GateLaunchInputs.from_sequences(
30
+ input_current=[1.0, 2.0, 0.5, 1.5],
31
+ channel_masks=[0xFF] * 4,
32
+ amplitudes=[1.0] * 8,
33
+ reward_ring=[0.5],
34
+ action_map=[
35
+ 0.0,
36
+ 0.0,
37
+ 0.0,
38
+ 0.0,
39
+ 0.0,
40
+ 0.0,
41
+ 0.0,
42
+ 0.0,
43
+ 3.0,
44
+ 4.0,
45
+ 5.0,
46
+ 6.0,
47
+ 0.0,
48
+ 0.0,
49
+ 0.0,
50
+ 0.0,
51
+ 0.0,
52
+ 0.0,
53
+ 0.0,
54
+ 0.0,
55
+ 0.0,
56
+ 0.0,
57
+ 0.0,
58
+ 0.0,
59
+ 0.0,
60
+ 0.0,
61
+ 0.0,
62
+ 0.0,
63
+ 0.0,
64
+ 0.0,
65
+ 0.0,
66
+ 0.0,
67
+ ],
68
+ feedback_matrix=[0.0] * (4 * pan_dimensions),
69
+ population=[0.0] * pan_dimensions,
70
+ logits=[
71
+ 0.1,
72
+ 0.2,
73
+ 0.3,
74
+ 0.9,
75
+ 0.1,
76
+ 0.8,
77
+ 0.2,
78
+ 0.3,
79
+ 0.7,
80
+ 0.2,
81
+ 0.1,
82
+ 0.0,
83
+ 0.1,
84
+ 0.2,
85
+ 0.9,
86
+ 0.3,
87
+ ],
88
+ tick=0,
89
+ target_index=2,
90
+ previous_action=1,
91
+ seed=12345,
92
+ epsilon=0.0,
93
+ )
94
+
95
+
96
+ def main() -> int:
97
+ bundle = compile_mapping(
98
+ {
99
+ "closed_loop_preset": "minimal_closed_loop",
100
+ "pan_feedback_gain": 0.0,
101
+ }
102
+ )
103
+ inputs = _inputs(bundle)
104
+ reference = cpu_gate_reference(bundle, inputs)
105
+ cuda = execute_gate_bundle(bundle, inputs, block_size=64)
106
+ parity = gate_execution_parity_summary(reference, cuda)
107
+ result = {
108
+ "classification": "PLAYGROUND_CUDA1_3_HARDWARE_SMOKE",
109
+ "scientific_evidence": False,
110
+ "reference": reference,
111
+ "cuda": cuda,
112
+ "parity": parity,
113
+ }
114
+ print(json.dumps(result, indent=2, sort_keys=True))
115
+ return 0 if parity["passed"] else 1
116
+
117
+
118
+ if __name__ == "__main__":
119
+ raise SystemExit(main())
scripts/validate_playground_ptx.py CHANGED
@@ -6,6 +6,8 @@ import json
6
  from pathlib import Path
7
 
8
  from src.playground.cuda import assemble_bundle, compile_mapping
 
 
9
 
10
 
11
  def main() -> int:
@@ -25,6 +27,48 @@ def main() -> int:
25
  "spill_store_bytes": report.spill_store_bytes,
26
  "spill_load_bytes": report.spill_load_bytes,
27
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  print(json.dumps(results, indent=2, sort_keys=True))
29
  return 0
30
 
 
6
  from pathlib import Path
7
 
8
  from src.playground.cuda import assemble_bundle, compile_mapping
9
+ from src.playground.cuda.nvrtc import compile_cuda_source
10
+ from src.playground.cuda.runtime import assemble_ptx
11
 
12
 
13
  def main() -> int:
 
27
  "spill_store_bytes": report.spill_store_bytes,
28
  "spill_load_bytes": report.spill_load_bytes,
29
  }
30
+ source = Path("src/playground/cuda/recurrent.cu").read_text()
31
+ recurrent_dir = root / "recurrent"
32
+ recurrent_dir.mkdir(parents=True, exist_ok=True)
33
+ ptx_path = recurrent_dir / "recurrent.ptx"
34
+ ptx_path.write_text(compile_cuda_source(source), encoding="utf-8")
35
+ recurrent_report = assemble_ptx(
36
+ ptx_path.read_text(), output_dir=recurrent_dir, target_sm="sm_86"
37
+ )
38
+ results["recurrent"] = recurrent_report.to_mapping()
39
+ plastic_report = assemble_ptx(
40
+ compile_cuda_source("#define PAN_PLASTIC\n" + source),
41
+ output_dir=root / "plasticity",
42
+ target_sm="sm_86",
43
+ )
44
+ results["plasticity"] = plastic_report.to_mapping()
45
+ results["builder_membrane"] = assemble_ptx(
46
+ compile_cuda_source(Path("src/playground/cuda/membrane.cu").read_text()),
47
+ output_dir=root / "builder-membrane",
48
+ target_sm="sm_86",
49
+ ).to_mapping()
50
+ results["builder_pan_state"] = assemble_ptx(
51
+ compile_cuda_source(Path("src/playground/cuda/pan_state.cu").read_text()),
52
+ output_dir=root / "builder-pan-state",
53
+ target_sm="sm_86",
54
+ ).to_mapping()
55
+ results["builder_synaptic_emission"] = assemble_ptx(
56
+ compile_cuda_source(
57
+ Path("src/playground/cuda/builder_synapses.cu").read_text()
58
+ ),
59
+ output_dir=root / "builder-synaptic-emission",
60
+ target_sm="sm_86",
61
+ ).to_mapping()
62
+ results["resident_delay_queue"] = assemble_ptx(
63
+ compile_cuda_source(Path("src/playground/cuda/delay_queue.cu").read_text()),
64
+ output_dir=root / "resident-delay-queue",
65
+ target_sm="sm_86",
66
+ ).to_mapping()
67
+ results["resident_neuron_traces"] = assemble_ptx(
68
+ compile_cuda_source(Path("src/playground/cuda/neuron_traces.cu").read_text()),
69
+ output_dir=root / "resident-neuron-traces",
70
+ target_sm="sm_86",
71
+ ).to_mapping()
72
  print(json.dumps(results, indent=2, sort_keys=True))
73
  return 0
74
 
src/acceleration/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Canonical hardware acceleration interfaces."""
src/acceleration/cuda/__init__.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Canonical CUDA acceleration layer.
2
+
3
+ No module in this package may import :mod:`src.playground`.
4
+ """
5
+
6
+ from .backend import CUDABackend, execution_backend_contract_check
7
+ from .driver import CudaDriver, DriverModule
8
+ from .errors import CudaDriverError, CudaRuntimeUnavailable
9
+ from .memory import DeviceAllocation
10
+ from .nvrtc import compile_cuda_source, toolkit_root
11
+ from .preflight import (
12
+ CooperativePreflight,
13
+ cooperative_capacity,
14
+ validate_cuda_block_size,
15
+ )
16
+ from .ptxas import PtxasReport, assemble_ptx, parse_ptxas_verbose
17
+
18
+ __all__ = [
19
+ "CUDABackend",
20
+ "CooperativePreflight",
21
+ "CudaDriver",
22
+ "CudaDriverError",
23
+ "CudaRuntimeUnavailable",
24
+ "DeviceAllocation",
25
+ "DriverModule",
26
+ "PtxasReport",
27
+ "assemble_ptx",
28
+ "compile_cuda_source",
29
+ "cooperative_capacity",
30
+ "parse_ptxas_verbose",
31
+ "toolkit_root",
32
+ "execution_backend_contract_check",
33
+ "validate_cuda_block_size",
34
+ ]
src/acceleration/cuda/backend.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ExecutionBackend facade for the bounded Wave-4 CUDA reference."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import Any
7
+
8
+ from src.runtime.backend import (
9
+ BackendCapabilities,
10
+ BackendState,
11
+ ExecutionBackend,
12
+ RunResult,
13
+ StepResult,
14
+ )
15
+ from src.verification.parity import config_fingerprint, execution_fingerprint
16
+
17
+ from .plasticity.contracts import PLASTICITY_SEMANTICS
18
+ from .recurrent.backend import execute_recurrent
19
+ from .recurrent.state import (
20
+ canonical_int,
21
+ prefix_inputs,
22
+ recurrent_inputs_from_mapping,
23
+ recurrent_inputs_to_mapping,
24
+ set_external_tick_config,
25
+ stable_digest,
26
+ step_payload,
27
+ )
28
+
29
+
30
+ class CUDABackend:
31
+ """Bounded replay adapter over the hardware-verified recurrent kernel."""
32
+
33
+ backend_name = "cuda"
34
+ backend_version = "wave4-bounded-replay-v1"
35
+
36
+ def __init__(self) -> None:
37
+ self._config: dict[str, object] | None = None
38
+ self._seed = 0
39
+ self._tick = 0
40
+
41
+ def initialize(self, config: Mapping[str, object], seed: int) -> None:
42
+ inputs = recurrent_inputs_from_mapping(config, seed=seed)
43
+ self._config = recurrent_inputs_to_mapping(inputs)
44
+ self._seed = seed
45
+ self._tick = 0
46
+
47
+ def _require_config(self) -> dict[str, object]:
48
+ if self._config is None:
49
+ raise RuntimeError("backend is not initialized")
50
+ return self._config
51
+
52
+ def _execute_prefix(self, total_ticks: int) -> Mapping[str, object]:
53
+ config = self._require_config()
54
+ inputs = recurrent_inputs_from_mapping(config, seed=self._seed)
55
+ result = execute_recurrent(prefix_inputs(inputs, total_ticks))
56
+ outputs = result.get("outputs")
57
+ if not isinstance(outputs, Mapping):
58
+ raise RuntimeError("CUDA recurrent execution returned no outputs")
59
+ return outputs
60
+
61
+ def _step_from_outputs(
62
+ self, outputs: Mapping[str, object], tick: int
63
+ ) -> StepResult:
64
+ config = self._require_config()
65
+ payload = step_payload(
66
+ outputs,
67
+ tick=tick,
68
+ n_neurons=canonical_int(config["n_neurons"], field="n_neurons"),
69
+ )
70
+ spikes_raw = payload["spikes"]
71
+ if not isinstance(spikes_raw, list):
72
+ raise ValueError("spike payload must be a list")
73
+ spikes = tuple(
74
+ index for index, value in enumerate(spikes_raw) if int(value) == 1
75
+ )
76
+ return StepResult(
77
+ tick=tick + 1,
78
+ spikes=spikes,
79
+ state_digest=stable_digest(payload),
80
+ metrics={
81
+ "voltage": payload["voltage"],
82
+ "adaptation": payload["adaptation"],
83
+ "spike_count": len(spikes),
84
+ },
85
+ )
86
+
87
+ def set_external_tick(self, tick: int, currents: Sequence[float]) -> None:
88
+ """Inject one live input row without changing CUDA kernel semantics."""
89
+
90
+ if tick != self._tick:
91
+ raise ValueError("live external input tick must equal continuation cursor")
92
+ set_external_tick_config(self._require_config(), tick=tick, currents=currents)
93
+
94
+ def step(self, tick: int) -> StepResult:
95
+ if tick != self._tick:
96
+ raise ValueError("tick must equal the backend continuation cursor")
97
+ config = self._require_config()
98
+ if tick >= canonical_int(config["ticks"], field="ticks"):
99
+ raise ValueError("configured tick limit reached")
100
+ outputs = self._execute_prefix(tick + 1)
101
+ self._tick = tick + 1
102
+ return self._step_from_outputs(outputs, tick)
103
+
104
+ def run(self, ticks: int) -> RunResult:
105
+ if type(ticks) is not int or ticks < 1:
106
+ raise ValueError("ticks must be a positive int")
107
+ config = self._require_config()
108
+ total = self._tick + ticks
109
+ if total > canonical_int(config["ticks"], field="ticks"):
110
+ raise ValueError("requested run exceeds configured tick limit")
111
+ start = self._tick
112
+ outputs = self._execute_prefix(total)
113
+ steps = tuple(
114
+ self._step_from_outputs(outputs, tick) for tick in range(start, total)
115
+ )
116
+ self._tick = total
117
+ return RunResult(
118
+ ticks_requested=ticks,
119
+ steps=steps,
120
+ final_state=self.snapshot(),
121
+ execution_fingerprint=execution_fingerprint(
122
+ seed=self._seed,
123
+ config_hash=config_fingerprint(config),
124
+ backend_name=self.backend_name,
125
+ backend_version=self.backend_version,
126
+ ticks=total,
127
+ ),
128
+ )
129
+
130
+ def snapshot(self) -> BackendState:
131
+ config = self._require_config()
132
+ payload: dict[str, Any] = {
133
+ "backend": self.backend_version,
134
+ "seed": self._seed,
135
+ "tick": self._tick,
136
+ "config": config,
137
+ "replay_required": True,
138
+ }
139
+ return BackendState(
140
+ tick=self._tick,
141
+ payload=payload,
142
+ state_digest=stable_digest(payload),
143
+ )
144
+
145
+ def restore(self, state: BackendState) -> None:
146
+ payload = dict(state.payload)
147
+ if payload.get("backend") != self.backend_version:
148
+ raise ValueError("checkpoint backend version mismatch")
149
+ raw_config = payload.get("config")
150
+ raw_seed = payload.get("seed")
151
+ if not isinstance(raw_config, Mapping) or type(raw_seed) is not int:
152
+ raise ValueError("checkpoint is missing canonical config/seed")
153
+ self.initialize(raw_config, raw_seed)
154
+ if state.tick > canonical_int(self._require_config()["ticks"], field="ticks"):
155
+ raise ValueError("checkpoint tick exceeds configured limit")
156
+ self._tick = state.tick
157
+
158
+ def capabilities(self) -> BackendCapabilities:
159
+ return BackendCapabilities(
160
+ supports_recurrent=True,
161
+ supports_plasticity=True,
162
+ supports_pan_hyperstate=False,
163
+ supports_structural_plasticity=False,
164
+ max_neurons=4096,
165
+ max_ticks=2000,
166
+ max_edges=65536,
167
+ deterministic=True,
168
+ plasticity_semantics=PLASTICITY_SEMANTICS,
169
+ execution_mode="BOUNDED_REPLAY_REFERENCE",
170
+ supports_live_external_input=True,
171
+ )
172
+
173
+
174
+ def execution_backend_contract_check() -> bool:
175
+ backend: ExecutionBackend = CUDABackend()
176
+ return isinstance(backend, ExecutionBackend)
src/acceleration/cuda/driver.py ADDED
@@ -0,0 +1,478 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Canonical CUDA Driver API wrapper.
2
+
3
+ This module owns device/context/module/memory/launch mechanics only. It has no
4
+ Playground, PAN, Gate-IR or scientific semantics.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ctypes
10
+ import math
11
+ import os
12
+ from collections.abc import Sequence
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from .errors import CudaDriverError, CudaRuntimeUnavailable
18
+ from .memory import DeviceAllocation
19
+ from .preflight import CooperativePreflight, validate_cuda_block_size
20
+
21
+ # CUDA Driver API attribute IDs from cuda.h.
22
+ _CU_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT = 16
23
+ _CU_DEVICE_ATTRIBUTE_COOPERATIVE_LAUNCH = 95
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class DriverModule:
28
+ """Opaque handles returned by CUDA Driver API loading."""
29
+
30
+ context: ctypes.c_void_p
31
+ module: ctypes.c_void_p
32
+ function: ctypes.c_void_p
33
+ device_ordinal: int
34
+
35
+
36
+ class CudaDriver:
37
+ """Minimal ctypes wrapper for CUDA-1 loading, memory and kernel launch."""
38
+
39
+ def __init__(self, library: str | None = None) -> None:
40
+ candidates = (
41
+ [library]
42
+ if library
43
+ else (["nvcuda.dll"] if os.name == "nt" else ["libcuda.so.1", "libcuda.so"])
44
+ )
45
+ loader = (
46
+ getattr(ctypes, "WinDLL", ctypes.CDLL) if os.name == "nt" else ctypes.CDLL
47
+ )
48
+ last_error: OSError | None = None
49
+ for candidate in candidates:
50
+ try:
51
+ self._lib = loader(candidate)
52
+ break
53
+ except OSError as exc:
54
+ last_error = exc
55
+ else:
56
+ names = ", ".join(candidates)
57
+ raise CudaRuntimeUnavailable(
58
+ f"CUDA driver library not available; tried: {names}"
59
+ ) from last_error
60
+ self._bind()
61
+
62
+ def _bind(self) -> None:
63
+ lib = self._lib
64
+ lib.cuInit.argtypes = [ctypes.c_uint]
65
+ lib.cuInit.restype = ctypes.c_int
66
+ lib.cuDeviceGet.argtypes = [ctypes.POINTER(ctypes.c_int), ctypes.c_int]
67
+ lib.cuDeviceGet.restype = ctypes.c_int
68
+ lib.cuDeviceGetAttribute.argtypes = [
69
+ ctypes.POINTER(ctypes.c_int),
70
+ ctypes.c_int,
71
+ ctypes.c_int,
72
+ ]
73
+ lib.cuDeviceGetAttribute.restype = ctypes.c_int
74
+ lib.cuCtxCreate_v2.argtypes = [
75
+ ctypes.POINTER(ctypes.c_void_p),
76
+ ctypes.c_uint,
77
+ ctypes.c_int,
78
+ ]
79
+ lib.cuCtxCreate_v2.restype = ctypes.c_int
80
+ lib.cuCtxDestroy_v2.argtypes = [ctypes.c_void_p]
81
+ lib.cuCtxDestroy_v2.restype = ctypes.c_int
82
+ lib.cuModuleLoad.argtypes = [
83
+ ctypes.POINTER(ctypes.c_void_p),
84
+ ctypes.c_char_p,
85
+ ]
86
+ lib.cuModuleLoad.restype = ctypes.c_int
87
+ lib.cuModuleUnload.argtypes = [ctypes.c_void_p]
88
+ lib.cuModuleUnload.restype = ctypes.c_int
89
+ lib.cuModuleGetFunction.argtypes = [
90
+ ctypes.POINTER(ctypes.c_void_p),
91
+ ctypes.c_void_p,
92
+ ctypes.c_char_p,
93
+ ]
94
+ lib.cuModuleGetFunction.restype = ctypes.c_int
95
+ lib.cuOccupancyMaxActiveBlocksPerMultiprocessor.argtypes = [
96
+ ctypes.POINTER(ctypes.c_int),
97
+ ctypes.c_void_p,
98
+ ctypes.c_int,
99
+ ctypes.c_size_t,
100
+ ]
101
+ lib.cuOccupancyMaxActiveBlocksPerMultiprocessor.restype = ctypes.c_int
102
+ lib.cuMemAlloc_v2.argtypes = [
103
+ ctypes.POINTER(ctypes.c_uint64),
104
+ ctypes.c_size_t,
105
+ ]
106
+ lib.cuMemAlloc_v2.restype = ctypes.c_int
107
+ lib.cuMemFree_v2.argtypes = [ctypes.c_uint64]
108
+ lib.cuMemFree_v2.restype = ctypes.c_int
109
+ lib.cuMemGetInfo_v2.argtypes = [
110
+ ctypes.POINTER(ctypes.c_size_t),
111
+ ctypes.POINTER(ctypes.c_size_t),
112
+ ]
113
+ lib.cuMemGetInfo_v2.restype = ctypes.c_int
114
+ lib.cuMemcpyHtoD_v2.argtypes = [
115
+ ctypes.c_uint64,
116
+ ctypes.c_void_p,
117
+ ctypes.c_size_t,
118
+ ]
119
+ lib.cuMemcpyHtoD_v2.restype = ctypes.c_int
120
+ lib.cuMemcpyDtoH_v2.argtypes = [
121
+ ctypes.c_void_p,
122
+ ctypes.c_uint64,
123
+ ctypes.c_size_t,
124
+ ]
125
+ lib.cuMemcpyDtoH_v2.restype = ctypes.c_int
126
+ lib.cuLaunchKernel.argtypes = [
127
+ ctypes.c_void_p,
128
+ ctypes.c_uint,
129
+ ctypes.c_uint,
130
+ ctypes.c_uint,
131
+ ctypes.c_uint,
132
+ ctypes.c_uint,
133
+ ctypes.c_uint,
134
+ ctypes.c_uint,
135
+ ctypes.c_void_p,
136
+ ctypes.POINTER(ctypes.c_void_p),
137
+ ctypes.POINTER(ctypes.c_void_p),
138
+ ]
139
+ lib.cuLaunchKernel.restype = ctypes.c_int
140
+ lib.cuCtxSynchronize.argtypes = []
141
+ lib.cuCtxSynchronize.restype = ctypes.c_int
142
+
143
+ @staticmethod
144
+ def _check(code: int, call: str) -> None:
145
+ if code != 0:
146
+ raise CudaDriverError(f"{call} failed with CUDA error code {code}")
147
+
148
+ def initialize(self) -> None:
149
+ self._check(self._lib.cuInit(0), "cuInit")
150
+
151
+ def _device(self, ordinal: int) -> int:
152
+ device = ctypes.c_int()
153
+ self._check(
154
+ self._lib.cuDeviceGet(ctypes.byref(device), ordinal),
155
+ "cuDeviceGet",
156
+ )
157
+ return int(device.value)
158
+
159
+ def device_attribute(self, ordinal: int, attribute: int) -> int:
160
+ device = self._device(ordinal)
161
+ value = ctypes.c_int()
162
+ self._check(
163
+ self._lib.cuDeviceGetAttribute(
164
+ ctypes.byref(value),
165
+ attribute,
166
+ device,
167
+ ),
168
+ "cuDeviceGetAttribute",
169
+ )
170
+ return int(value.value)
171
+
172
+ def alloc_device(self, size_bytes: int) -> DeviceAllocation:
173
+ """Allocate device memory owned by the current CUDA context."""
174
+
175
+ if size_bytes <= 0:
176
+ raise ValueError("size_bytes must be positive")
177
+ ptr = ctypes.c_uint64()
178
+ self._check(
179
+ self._lib.cuMemAlloc_v2(ctypes.byref(ptr), size_bytes),
180
+ "cuMemAlloc_v2",
181
+ )
182
+ return DeviceAllocation(ptr=int(ptr.value), size_bytes=size_bytes)
183
+
184
+ def free_device(self, allocation: DeviceAllocation) -> None:
185
+ """Release one device allocation."""
186
+
187
+ if allocation.ptr:
188
+ self._check(
189
+ self._lib.cuMemFree_v2(ctypes.c_uint64(allocation.ptr)),
190
+ "cuMemFree_v2",
191
+ )
192
+
193
+ def memory_info(self) -> tuple[int, int]:
194
+ """Return free and total bytes for the current CUDA context."""
195
+
196
+ free_bytes = ctypes.c_size_t()
197
+ total_bytes = ctypes.c_size_t()
198
+ self._check(
199
+ self._lib.cuMemGetInfo_v2(
200
+ ctypes.byref(free_bytes),
201
+ ctypes.byref(total_bytes),
202
+ ),
203
+ "cuMemGetInfo_v2",
204
+ )
205
+ return int(free_bytes.value), int(total_bytes.value)
206
+
207
+ def copy_host_to_device(
208
+ self,
209
+ allocation: DeviceAllocation,
210
+ source: ctypes.Array[Any],
211
+ *,
212
+ size_bytes: int | None = None,
213
+ ) -> None:
214
+ """Copy a ctypes-backed host buffer to device memory."""
215
+
216
+ count = allocation.size_bytes if size_bytes is None else size_bytes
217
+ host_bytes = ctypes.sizeof(source)
218
+ if count < 0 or count > allocation.size_bytes:
219
+ raise ValueError("host-to-device copy exceeds device allocation")
220
+ if count > host_bytes:
221
+ raise ValueError(
222
+ f"host-to-device copy exceeds host buffer: {count} > {host_bytes}"
223
+ )
224
+ self._check(
225
+ self._lib.cuMemcpyHtoD_v2(
226
+ ctypes.c_uint64(allocation.ptr),
227
+ ctypes.cast(source, ctypes.c_void_p),
228
+ count,
229
+ ),
230
+ "cuMemcpyHtoD_v2",
231
+ )
232
+
233
+ def copy_device_to_host(
234
+ self,
235
+ destination: ctypes.Array[Any],
236
+ allocation: DeviceAllocation,
237
+ *,
238
+ size_bytes: int | None = None,
239
+ ) -> None:
240
+ """Copy device memory into a ctypes-backed host buffer."""
241
+
242
+ count = allocation.size_bytes if size_bytes is None else size_bytes
243
+ host_bytes = ctypes.sizeof(destination)
244
+ if count < 0 or count > allocation.size_bytes:
245
+ raise ValueError("device-to-host copy exceeds device allocation")
246
+ if count > host_bytes:
247
+ raise ValueError(
248
+ f"device-to-host copy exceeds host buffer: {count} > {host_bytes}"
249
+ )
250
+ self._check(
251
+ self._lib.cuMemcpyDtoH_v2(
252
+ ctypes.cast(destination, ctypes.c_void_p),
253
+ ctypes.c_uint64(allocation.ptr),
254
+ count,
255
+ ),
256
+ "cuMemcpyDtoH_v2",
257
+ )
258
+
259
+ def launch_kernel(
260
+ self,
261
+ loaded: DriverModule,
262
+ *,
263
+ grid: tuple[int, int, int],
264
+ block: tuple[int, int, int],
265
+ arguments: Sequence[object],
266
+ dynamic_shared_bytes: int = 0,
267
+ ) -> None:
268
+ """Launch a loaded kernel with CUDA Driver API argument packing."""
269
+
270
+ if min(*grid, *block) <= 0:
271
+ raise ValueError("grid and block dimensions must be positive")
272
+ storage = list(arguments)
273
+ kernel_params = (ctypes.c_void_p * len(storage))()
274
+ for index, argument in enumerate(storage):
275
+ kernel_params[index] = ctypes.cast(
276
+ ctypes.byref(argument), ctypes.c_void_p # type: ignore[arg-type]
277
+ )
278
+ self._check(
279
+ self._lib.cuLaunchKernel(
280
+ loaded.function,
281
+ grid[0],
282
+ grid[1],
283
+ grid[2],
284
+ block[0],
285
+ block[1],
286
+ block[2],
287
+ dynamic_shared_bytes,
288
+ None,
289
+ kernel_params,
290
+ None,
291
+ ),
292
+ "cuLaunchKernel",
293
+ )
294
+
295
+ def synchronize(self) -> None:
296
+ """Synchronize the current CUDA context."""
297
+
298
+ self._check(self._lib.cuCtxSynchronize(), "cuCtxSynchronize")
299
+
300
+ def launch_cooperative(
301
+ self,
302
+ loaded: DriverModule,
303
+ *,
304
+ n_neurons: int,
305
+ block_size: int,
306
+ arguments: Sequence[object],
307
+ ) -> CooperativePreflight:
308
+ """Launch a resident 1D grid only after checking actual kernel occupancy."""
309
+ preflight = self.cooperative_preflight(
310
+ loaded, n_neurons=n_neurons, block_size=block_size
311
+ )
312
+ if n_neurons < 1 or not preflight.launch_fits:
313
+ raise ValueError(
314
+ "cooperative grid exceeds the device/kernel resident capacity"
315
+ )
316
+ launch = self._lib.cuLaunchCooperativeKernel
317
+ launch.argtypes = [
318
+ ctypes.c_void_p,
319
+ ctypes.c_uint,
320
+ ctypes.c_uint,
321
+ ctypes.c_uint,
322
+ ctypes.c_uint,
323
+ ctypes.c_uint,
324
+ ctypes.c_uint,
325
+ ctypes.c_uint,
326
+ ctypes.c_void_p,
327
+ ctypes.POINTER(ctypes.c_void_p),
328
+ ]
329
+ launch.restype = ctypes.c_int
330
+ storage = list(arguments)
331
+ parameters = (ctypes.c_void_p * len(storage))()
332
+ for index, argument in enumerate(storage):
333
+ parameters[index] = ctypes.cast(
334
+ ctypes.byref(argument), ctypes.c_void_p # type: ignore[arg-type]
335
+ )
336
+ self._check(
337
+ launch(
338
+ loaded.function,
339
+ preflight.required_blocks,
340
+ 1,
341
+ 1,
342
+ block_size,
343
+ 1,
344
+ 1,
345
+ 0,
346
+ None,
347
+ parameters,
348
+ ),
349
+ "cuLaunchCooperativeKernel",
350
+ )
351
+ return preflight
352
+
353
+ def load_cubin(
354
+ self,
355
+ cubin_path: Path,
356
+ *,
357
+ kernel_name: str = "pan_gate_kernel",
358
+ device_ordinal: int = 0,
359
+ ) -> DriverModule:
360
+ """Load a ptxas-generated cubin and resolve one kernel symbol."""
361
+
362
+ self.initialize()
363
+ device = self._device(device_ordinal)
364
+ context = ctypes.c_void_p()
365
+ self._check(
366
+ self._lib.cuCtxCreate_v2(ctypes.byref(context), 0, device),
367
+ "cuCtxCreate_v2",
368
+ )
369
+ module = ctypes.c_void_p()
370
+ try:
371
+ self._check(
372
+ self._lib.cuModuleLoad(
373
+ ctypes.byref(module),
374
+ str(cubin_path).encode("utf-8"),
375
+ ),
376
+ "cuModuleLoad",
377
+ )
378
+ function = ctypes.c_void_p()
379
+ self._check(
380
+ self._lib.cuModuleGetFunction(
381
+ ctypes.byref(function),
382
+ module,
383
+ kernel_name.encode("ascii"),
384
+ ),
385
+ "cuModuleGetFunction",
386
+ )
387
+ except Exception:
388
+ if module.value:
389
+ self._lib.cuModuleUnload(module)
390
+ self._lib.cuCtxDestroy_v2(context)
391
+ raise
392
+ return DriverModule(
393
+ context=context,
394
+ module=module,
395
+ function=function,
396
+ device_ordinal=device_ordinal,
397
+ )
398
+
399
+ def kernel(self, loaded: DriverModule, name: str) -> DriverModule:
400
+ """Borrow another function in the same owned module/context; do not unload it separately."""
401
+ if not name or not name.isascii() or not name.replace("_", "").isalnum():
402
+ raise ValueError("invalid CUDA kernel symbol")
403
+ function = ctypes.c_void_p()
404
+ self._check(
405
+ self._lib.cuModuleGetFunction(
406
+ ctypes.byref(function), loaded.module, name.encode("ascii")
407
+ ),
408
+ "cuModuleGetFunction",
409
+ )
410
+ return DriverModule(
411
+ loaded.context, loaded.module, function, loaded.device_ordinal
412
+ )
413
+
414
+ def unload(self, loaded: DriverModule) -> None:
415
+ module_error: CudaDriverError | None = None
416
+ if loaded.module.value:
417
+ try:
418
+ self._check(
419
+ self._lib.cuModuleUnload(loaded.module),
420
+ "cuModuleUnload",
421
+ )
422
+ except CudaDriverError as exc:
423
+ module_error = exc
424
+ if loaded.context.value:
425
+ try:
426
+ self._check(
427
+ self._lib.cuCtxDestroy_v2(loaded.context),
428
+ "cuCtxDestroy_v2",
429
+ )
430
+ except CudaDriverError:
431
+ if module_error is None:
432
+ raise
433
+ if module_error is not None:
434
+ raise module_error
435
+
436
+ def cooperative_preflight(
437
+ self,
438
+ loaded: DriverModule,
439
+ *,
440
+ n_neurons: int,
441
+ block_size: int = 128,
442
+ dynamic_shared_bytes: int = 0,
443
+ ) -> CooperativePreflight:
444
+ """Calculate whether the full cooperative grid can be resident."""
445
+
446
+ validate_cuda_block_size(block_size)
447
+ cooperative = bool(
448
+ self.device_attribute(
449
+ loaded.device_ordinal,
450
+ _CU_DEVICE_ATTRIBUTE_COOPERATIVE_LAUNCH,
451
+ )
452
+ )
453
+ sms = self.device_attribute(
454
+ loaded.device_ordinal,
455
+ _CU_DEVICE_ATTRIBUTE_MULTIPROCESSOR_COUNT,
456
+ )
457
+ active = ctypes.c_int()
458
+ self._check(
459
+ self._lib.cuOccupancyMaxActiveBlocksPerMultiprocessor(
460
+ ctypes.byref(active),
461
+ loaded.function,
462
+ block_size,
463
+ dynamic_shared_bytes,
464
+ ),
465
+ "cuOccupancyMaxActiveBlocksPerMultiprocessor",
466
+ )
467
+ required = math.ceil(n_neurons / block_size)
468
+ capacity = int(active.value) * sms
469
+ return CooperativePreflight(
470
+ cooperative_launch=cooperative,
471
+ multiprocessor_count=sms,
472
+ active_blocks_per_sm=int(active.value),
473
+ block_size=block_size,
474
+ required_blocks=required,
475
+ resident_block_capacity=capacity,
476
+ launch_fits=cooperative and required <= capacity,
477
+ n_neurons=n_neurons,
478
+ )
src/acceleration/cuda/errors.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """Canonical CUDA acceleration errors."""
2
+
3
+
4
+ class CudaRuntimeUnavailable(RuntimeError):
5
+ """Raised when an optional CUDA-1 dependency is not available."""
6
+
7
+
8
+ class CudaDriverError(RuntimeError):
9
+ """Raised when a CUDA Driver API call fails."""
src/acceleration/cuda/memory.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Canonical CUDA memory ownership contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class DeviceAllocation:
10
+ """One owned CUDA device allocation."""
11
+
12
+ ptr: int
13
+ size_bytes: int
src/acceleration/cuda/nvrtc.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Optional CUDA source compilation without a host C++ compiler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes
6
+ import os
7
+ import re
8
+ import shutil
9
+ from contextlib import nullcontext
10
+ from pathlib import Path
11
+
12
+ from .errors import CudaRuntimeUnavailable
13
+
14
+
15
+ def toolkit_root() -> Path:
16
+ candidates = [os.environ.get("CUDA_PATH", ""), os.environ.get("CUDA_HOME", "")]
17
+ compiler = shutil.which("nvcc")
18
+ if compiler:
19
+ candidates.append(str(Path(compiler).resolve().parent.parent))
20
+ candidates.append("/usr/local/cuda")
21
+ if os.name == "nt":
22
+ base = Path(os.environ.get("ProgramFiles", "C:/Program Files"))
23
+ candidates.extend(
24
+ str(p)
25
+ for p in sorted(
26
+ (base / "NVIDIA GPU Computing Toolkit/CUDA").glob("v*"), reverse=True
27
+ )
28
+ )
29
+ for candidate in candidates:
30
+ if candidate and (Path(candidate) / "include/cooperative_groups.h").is_file():
31
+ return Path(candidate)
32
+ raise CudaRuntimeUnavailable("CUDA toolkit headers/NVRTC are required")
33
+
34
+
35
+ def compile_cuda_source(source: str, *, target_sm: str = "sm_86") -> str:
36
+ """Compile a bounded trusted source string to PTX; no GPU is required."""
37
+ if not re.fullmatch(r"sm_[0-9]{2,3}", target_sm):
38
+ raise ValueError("invalid CUDA architecture")
39
+ root = toolkit_root()
40
+ patterns = (
41
+ ["bin/nvrtc64_*.dll", "bin/x64/nvrtc64_*.dll"]
42
+ if os.name == "nt"
43
+ else ["lib64/libnvrtc.so*", "targets/x86_64-linux/lib/libnvrtc.so*"]
44
+ )
45
+ libraries = [
46
+ p for pattern in patterns for p in root.glob(pattern) if ".alt." not in p.name
47
+ ]
48
+ if not libraries:
49
+ raise CudaRuntimeUnavailable("NVRTC library is not installed")
50
+ library = libraries[0]
51
+ directory = (
52
+ getattr(os, "add_dll_directory")(str(library.parent))
53
+ if os.name == "nt"
54
+ else nullcontext()
55
+ )
56
+ with directory:
57
+ lib = ctypes.CDLL(str(library))
58
+ lib.nvrtcCreateProgram.argtypes = [
59
+ ctypes.POINTER(ctypes.c_void_p),
60
+ ctypes.c_char_p,
61
+ ctypes.c_char_p,
62
+ ctypes.c_int,
63
+ ctypes.c_void_p,
64
+ ctypes.c_void_p,
65
+ ]
66
+ lib.nvrtcCompileProgram.argtypes = [
67
+ ctypes.c_void_p,
68
+ ctypes.c_int,
69
+ ctypes.POINTER(ctypes.c_char_p),
70
+ ]
71
+ lib.nvrtcGetProgramLogSize.argtypes = [
72
+ ctypes.c_void_p,
73
+ ctypes.POINTER(ctypes.c_size_t),
74
+ ]
75
+ lib.nvrtcGetProgramLog.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
76
+ lib.nvrtcGetPTXSize.argtypes = [
77
+ ctypes.c_void_p,
78
+ ctypes.POINTER(ctypes.c_size_t),
79
+ ]
80
+ lib.nvrtcGetPTX.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
81
+ lib.nvrtcDestroyProgram.argtypes = [ctypes.POINTER(ctypes.c_void_p)]
82
+ program = ctypes.c_void_p()
83
+ code = lib.nvrtcCreateProgram(
84
+ ctypes.byref(program), source.encode(), b"recurrent.cu", 0, None, None
85
+ )
86
+ if code:
87
+ raise CudaRuntimeUnavailable(f"nvrtcCreateProgram failed: {code}")
88
+ try:
89
+ options = [
90
+ f"--gpu-architecture=compute_{target_sm[3:]}",
91
+ f"--include-path={root / 'include'}",
92
+ "--std=c++17",
93
+ "--fmad=false",
94
+ ]
95
+ if (root / "include/cccl").is_dir():
96
+ options.append(f"--include-path={root / 'include/cccl'}")
97
+ encoded = (ctypes.c_char_p * len(options))(
98
+ *(item.encode() for item in options)
99
+ )
100
+ code = lib.nvrtcCompileProgram(program, len(options), encoded)
101
+ size = ctypes.c_size_t()
102
+ lib.nvrtcGetProgramLogSize(program, ctypes.byref(size))
103
+ log = ctypes.create_string_buffer(max(1, size.value))
104
+ lib.nvrtcGetProgramLog(program, log)
105
+ if code:
106
+ raise CudaRuntimeUnavailable(
107
+ f"NVRTC compilation failed ({code}): {log.value.decode(errors='replace')}"
108
+ )
109
+ if lib.nvrtcGetPTXSize(program, ctypes.byref(size)):
110
+ raise CudaRuntimeUnavailable("NVRTC PTX size unavailable")
111
+ ptx = ctypes.create_string_buffer(size.value)
112
+ if lib.nvrtcGetPTX(program, ptx):
113
+ raise CudaRuntimeUnavailable("NVRTC PTX retrieval failed")
114
+ return ptx.value.decode()
115
+ finally:
116
+ lib.nvrtcDestroyProgram(ctypes.byref(program))
src/acceleration/cuda/plasticity/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Canonical CUDA plasticity engineering package."""
2
+
3
+ from .backend import BuilderSynapseStepper
4
+ from .contracts import (
5
+ LEARNING_CONTRACT_ID,
6
+ LEARNING_CONTRACT_STATUS,
7
+ PLASTICITY_SEMANTICS,
8
+ )
9
+ from .reference import SynapseConfig, SynapseState
10
+
11
+ __all__ = [
12
+ "BuilderSynapseStepper",
13
+ "LEARNING_CONTRACT_ID",
14
+ "LEARNING_CONTRACT_STATUS",
15
+ "PLASTICITY_SEMANTICS",
16
+ "SynapseConfig",
17
+ "SynapseState",
18
+ ]
src/acceleration/cuda/plasticity/backend.py ADDED
@@ -0,0 +1,268 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CUDA synaptic kernels extracted from the Playground engineering path.
2
+
3
+ Semantics remain NON_CANONICAL_DRAFT until the canonical learning contract is
4
+ implemented by both CPU and CUDA paths.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ctypes
10
+ import math
11
+ from collections.abc import Sequence
12
+ from typing import Any
13
+
14
+ from ..driver import CudaDriver, DriverModule
15
+ from ..errors import CudaDriverError
16
+ from ..memory import DeviceAllocation
17
+
18
+
19
+ class BuilderSynapseStepper:
20
+ """Reuse bounded buffers for emission and per-tick recovery/decay."""
21
+
22
+ def __init__(self, driver: CudaDriver, loaded: DriverModule, capacity: int) -> None:
23
+ if type(capacity) is not int or not 1 <= capacity <= 20000:
24
+ raise ValueError("invalid CUDA synaptic capacity")
25
+ self.driver, self.capacity = driver, capacity
26
+ self.emit_kernel = driver.kernel(loaded, "pan_synaptic_emit")
27
+ self.recover_kernel = driver.kernel(loaded, "pan_synaptic_recover")
28
+ self.reward_kernel = driver.kernel(loaded, "pan_synaptic_reward")
29
+ self.plasticity_kernel = driver.kernel(loaded, "pan_synaptic_plasticity")
30
+ self.scale_kernel = driver.kernel(loaded, "pan_synaptic_scale")
31
+ self.device: dict[str, DeviceAllocation] = {}
32
+ self.host: dict[str, Any] = {
33
+ "input": (ctypes.c_double * (12 * capacity))(),
34
+ "output": (ctypes.c_double * (3 * capacity))(),
35
+ }
36
+ self.emitted_events = 0
37
+ self.recovery_calls = 0
38
+ self.reward_calls = 0
39
+ self.plasticity_calls = 0
40
+ try:
41
+ for name in self.host:
42
+ self.device[name] = driver.alloc_device(ctypes.sizeof(self.host[name]))
43
+ except BaseException:
44
+ self.close()
45
+ raise
46
+
47
+ def close(self) -> None:
48
+ for allocation in reversed(list(self.device.values())):
49
+ try:
50
+ self.driver.free_device(allocation)
51
+ except CudaDriverError:
52
+ pass
53
+ self.device.clear()
54
+
55
+ def _execute(
56
+ self,
57
+ rows: list[tuple[float, float, float, float, float]],
58
+ *,
59
+ stp: bool,
60
+ first: float,
61
+ second: float,
62
+ kernel: DriverModule,
63
+ ) -> list[tuple[float, float]]:
64
+ count = len(rows)
65
+ if (
66
+ count > self.capacity
67
+ or any(
68
+ len(row) != 5 or any(not math.isfinite(x) for x in row) for row in rows
69
+ )
70
+ or not all(math.isfinite(x) for x in (first, second))
71
+ ):
72
+ raise ValueError("invalid CUDA synaptic shape/non-finite input")
73
+ if not count:
74
+ return []
75
+ self.host["input"][: 5 * count] = [x for row in rows for x in row]
76
+ self.driver.copy_host_to_device(
77
+ self.device["input"], self.host["input"], size_bytes=5 * count * 8
78
+ )
79
+ args: list[object] = [
80
+ ctypes.c_uint32(count),
81
+ ctypes.c_uint32(bool(stp)),
82
+ ctypes.c_double(first),
83
+ ctypes.c_double(second),
84
+ ctypes.c_uint64(self.device["input"].ptr),
85
+ ctypes.c_uint64(self.device["output"].ptr),
86
+ ]
87
+ self.driver.launch_kernel(
88
+ kernel, grid=((count + 127) // 128, 1, 1), block=(128, 1, 1), arguments=args
89
+ )
90
+ self.driver.synchronize()
91
+ self.driver.copy_device_to_host(
92
+ self.host["output"], self.device["output"], size_bytes=2 * count * 8
93
+ )
94
+ values = list(self.host["output"][: 2 * count])
95
+ if any(not math.isfinite(x) for x in values):
96
+ raise ValueError("non-finite CUDA synaptic output")
97
+ return [(values[2 * i], values[2 * i + 1]) for i in range(count)]
98
+
99
+ def emit(
100
+ self,
101
+ rows: list[tuple[float, float, float, float, float]],
102
+ *,
103
+ stp: bool,
104
+ gaba: float,
105
+ ratio: float,
106
+ ) -> list[tuple[float, float]]:
107
+ if (
108
+ not math.isfinite(gaba)
109
+ or gaba < 0
110
+ or not math.isfinite(ratio)
111
+ or ratio <= 0
112
+ ):
113
+ raise ValueError("invalid CUDA inhibitory scaling")
114
+ for _weight, available, amplitude, draw, inhibitory in rows:
115
+ if (
116
+ not 0 <= available <= 1
117
+ or not 0 <= amplitude <= 1
118
+ or not 0 <= draw < 1
119
+ or inhibitory not in (0, 1)
120
+ ):
121
+ raise ValueError("invalid CUDA STP/amplitude/RNG descriptor")
122
+ result = self._execute(
123
+ rows, stp=stp, first=gaba, second=ratio, kernel=self.emit_kernel
124
+ )
125
+ self.emitted_events += len(rows)
126
+ return result
127
+
128
+ def recover(
129
+ self,
130
+ weights: list[float],
131
+ resources: list[float],
132
+ *,
133
+ stp: bool,
134
+ decay: float,
135
+ maximum: float,
136
+ ) -> list[tuple[float, float]]:
137
+ if (
138
+ len(weights) != len(resources)
139
+ or not 0 <= decay <= 1
140
+ or not 0 < maximum <= 100
141
+ or any(not 0 <= x <= 1 for x in resources)
142
+ ):
143
+ raise ValueError("invalid CUDA recovery/decay parameters")
144
+ result = self._execute(
145
+ [(w, r, 0.0, 0.0, 0.0) for w, r in zip(weights, resources)],
146
+ stp=stp,
147
+ first=decay,
148
+ second=maximum,
149
+ kernel=self.recover_kernel,
150
+ )
151
+ if result:
152
+ self.recovery_calls += 1
153
+ return result
154
+
155
+ def reward(
156
+ self,
157
+ weights: list[float],
158
+ eligibility: list[float],
159
+ ages: list[int],
160
+ *,
161
+ scale: float,
162
+ reward: float,
163
+ maximum: float,
164
+ window: int,
165
+ respect_window: bool,
166
+ ) -> list[float]:
167
+ if (
168
+ len(weights) != len(eligibility)
169
+ or len(weights) != len(ages)
170
+ or not 0 < maximum <= 100
171
+ or type(window) is not int
172
+ or window < 0
173
+ or any(type(age) is not int for age in ages)
174
+ ):
175
+ raise ValueError("invalid CUDA live reward shape/window")
176
+ rows = [
177
+ (w, e, float(age), float(window), maximum)
178
+ for w, e, age in zip(weights, eligibility, ages)
179
+ ]
180
+ values = self._execute(
181
+ rows,
182
+ stp=respect_window,
183
+ first=scale,
184
+ second=reward,
185
+ kernel=self.reward_kernel,
186
+ )
187
+ if values:
188
+ self.reward_calls += 1
189
+ return [w for w, _ in values]
190
+
191
+ def plasticity(
192
+ self,
193
+ rows: Sequence[tuple[float, ...]],
194
+ *,
195
+ pair: bool,
196
+ triplet: bool,
197
+ eligibility_active: bool,
198
+ decay: float,
199
+ learning: float,
200
+ ) -> list[tuple[float, float, int]]:
201
+ count = len(rows)
202
+ if (
203
+ count > self.capacity
204
+ or not math.isfinite(decay)
205
+ or not 0 <= decay <= 1
206
+ or not math.isfinite(learning)
207
+ or learning < 0
208
+ or any(
209
+ len(row) != 12 or any(not math.isfinite(x) for x in row) for row in rows
210
+ )
211
+ ):
212
+ raise ValueError("invalid CUDA plasticity input")
213
+ for row in rows:
214
+ if (
215
+ row[7] not in (0, 1)
216
+ or row[8] not in (0, 1)
217
+ or row[9] == row[10]
218
+ or any(x != int(x) for x in (row[2], row[9], row[10], row[11]))
219
+ ):
220
+ raise ValueError("invalid CUDA plasticity event/index")
221
+ if not count:
222
+ return []
223
+ self.host["input"][: 12 * count] = [x for row in rows for x in row]
224
+ self.driver.copy_host_to_device(
225
+ self.device["input"], self.host["input"], size_bytes=12 * count * 8
226
+ )
227
+ flags = int(pair) | int(triplet) * 2 | int(eligibility_active) * 4
228
+ args: list[object] = [
229
+ ctypes.c_uint32(count),
230
+ ctypes.c_uint32(flags),
231
+ ctypes.c_double(decay),
232
+ ctypes.c_double(learning),
233
+ ctypes.c_uint64(self.device["input"].ptr),
234
+ ctypes.c_uint64(self.device["output"].ptr),
235
+ ]
236
+ self.driver.launch_kernel(
237
+ self.plasticity_kernel,
238
+ grid=((count + 127) // 128, 1, 1),
239
+ block=(128, 1, 1),
240
+ arguments=args,
241
+ )
242
+ self.driver.synchronize()
243
+ self.driver.copy_device_to_host(
244
+ self.host["output"], self.device["output"], size_bytes=3 * count * 8
245
+ )
246
+ values = list(self.host["output"][: 3 * count])
247
+ if any(not math.isfinite(x) for x in values) or any(
248
+ x != int(x) for x in values[2::3]
249
+ ):
250
+ raise ValueError("invalid CUDA plasticity output")
251
+ self.plasticity_calls += 1
252
+ return [
253
+ (values[3 * i], values[3 * i + 1], int(values[3 * i + 2]))
254
+ for i in range(count)
255
+ ]
256
+
257
+ def scale_weights(
258
+ self, weights: list[float], *, factor: float, maximum: float
259
+ ) -> list[float]:
260
+ if not math.isfinite(factor) or factor < 0 or not 0 < maximum <= 100:
261
+ raise ValueError("invalid CUDA weight scaling")
262
+ rows = [(w, 0.0, 0.0, 0.0, 0.0) for w in weights]
263
+ return [
264
+ w
265
+ for w, _ in self._execute(
266
+ rows, stp=False, first=factor, second=maximum, kernel=self.scale_kernel
267
+ )
268
+ ]
src/acceleration/cuda/plasticity/contracts.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Wave-4 plasticity capability markers with Wave-7 draft linkage."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Final
6
+
7
+ from src.learning.contract import (
8
+ LEARNING_CONTRACT_ID as LEARNING_DRAFT_CONTRACT_ID,
9
+ )
10
+ from src.learning.contract import (
11
+ LEARNING_CONTRACT_STATUS as LEARNING_DRAFT_CONTRACT_STATUS,
12
+ )
13
+
14
+ PLASTICITY_SEMANTICS: Final[str] = "NON_CANONICAL_DRAFT"
15
+
16
+ # Frozen Wave-4 compatibility boundary. These values are intentionally retained
17
+ # so the extracted CUDA backend does not retroactively change its acceptance
18
+ # contract while Wave 7 is still drafting the common learning semantics.
19
+ LEARNING_CONTRACT_ID: Final[str] = "mhrn-learning-synapse-v1"
20
+ LEARNING_CONTRACT_STATUS: Final[str] = "ALIGNMENT_PENDING"
21
+
22
+ __all__ = [
23
+ "LEARNING_CONTRACT_ID",
24
+ "LEARNING_CONTRACT_STATUS",
25
+ "LEARNING_DRAFT_CONTRACT_ID",
26
+ "LEARNING_DRAFT_CONTRACT_STATUS",
27
+ "PLASTICITY_SEMANTICS",
28
+ ]
src/acceleration/cuda/plasticity/kernel.cu ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Builder emission algebra; host supplies traversal-ordered random draws.
2
+ extern "C" __global__ void pan_synaptic_emit(unsigned count, unsigned stp,
3
+ double gaba, double ratio, const double* input, double* output) {
4
+ unsigned i=blockIdx.x*blockDim.x+threadIdx.x; if(i>=count) return;
5
+ const double* x=input+5*i;
6
+ double amplitude=x[0], available=x[1];
7
+ if(x[4]!=0.0) amplitude=-fabs(amplitude)*gaba/ratio;
8
+ amplitude*=x[2];
9
+ if(stp) {
10
+ amplitude*=x[3]<fmin(0.95,0.25+0.7*available)?1.0:0.0;
11
+ available=fmax(0.1,available*0.72);
12
+ }
13
+ output[2*i]=amplitude; output[2*i+1]=available;
14
+ }
15
+ extern "C" __global__ void pan_synaptic_recover(unsigned count, unsigned stp,
16
+ double decay, double maximum, const double* input, double* output) {
17
+ unsigned i=blockIdx.x*blockDim.x+threadIdx.x; if(i>=count) return;
18
+ double weight=input[5*i], available=input[5*i+1];
19
+ if(stp) available=fmin(1.0,available+0.025);
20
+ if(decay!=0.0) weight*=fmax(0.0,1.0-decay);
21
+ output[2*i]=fmin(maximum,fmax(0.0,weight)); output[2*i+1]=available;
22
+ }
23
+
24
+ // Live target/posture rewards keep the Builder's eligibility-window contract.
25
+ extern "C" __global__ void pan_synaptic_reward(unsigned count, unsigned filter_age,
26
+ double scale, double reward, const double* input, double* output) {
27
+ unsigned i=blockIdx.x*blockDim.x+threadIdx.x; if(i>=count) return;
28
+ const double* x=input+5*i;
29
+ double weight=x[0];
30
+ if(!filter_age || x[2]<=x[3]) weight=fmin(x[4],fmax(0.0,weight+scale*x[1]*reward));
31
+ output[2*i]=weight; output[2*i+1]=x[1];
32
+ }
33
+
34
+ // Independent edges reproduce the original ascending-neuron event order.
35
+ extern "C" __global__ void pan_synaptic_plasticity(unsigned count, unsigned flags,
36
+ double decay, double learning, const double* input, double* output) {
37
+ unsigned i=blockIdx.x*blockDim.x+threadIdx.x; if(i>=count) return;
38
+ // weight, eligibility, eligibility time, pre/post deltas, decayed traces,
39
+ // spike flags, source/target IDs, current tick.
40
+ const double* x=input+12*i;
41
+ double weight=x[0], eligibility=x[1]*decay, last=x[2];
42
+ bool pre=x[7]!=0.0, post=x[8]!=0.0, pre_first=x[9]<x[10];
43
+ for(unsigned event=0;event<2;++event) {
44
+ bool source_event=(event==0)==pre_first;
45
+ if(source_event && pre) {
46
+ if((flags&1) && x[4]>0.0 && x[4]<=20.0) weight=fmax(0.0,weight-0.08*learning);
47
+ if(flags&2) weight=fmax(0.0,weight-0.04*(x[6]+((!pre_first && post)?1.0:0.0)));
48
+ if(flags&4) {eligibility-=0.5;last=x[11];}
49
+ } else if(!source_event && post) {
50
+ if((flags&1) && x[3]>0.0 && x[3]<=20.0) weight=fmin(100.0,weight+0.1*learning);
51
+ if(flags&2) weight=fmin(100.0,fmax(0.0,weight+0.06*(x[5]+((pre_first && pre)?1.0:0.0))+0.025*x[6]));
52
+ if(flags&4) {eligibility+=1.0;last=x[11];}
53
+ }
54
+ }
55
+ output[3*i]=weight;output[3*i+1]=eligibility;output[3*i+2]=last;
56
+ }
57
+
58
+ extern "C" __global__ void pan_synaptic_scale(unsigned count, unsigned unused,
59
+ double factor, double maximum, const double* input, double* output) {
60
+ unsigned i=blockIdx.x*blockDim.x+threadIdx.x;if(i>=count)return;
61
+ output[2*i]=fmax(0.0,fmin(maximum,input[5*i]*factor));output[2*i+1]=0.0;
62
+ }
src/acceleration/cuda/plasticity/reference.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Ordered scalar synapse oracle for the canonical bounded CUDA reference."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING
8
+
9
+ from src.runtime.determinism import (
10
+ release_uniform as release_uniform,
11
+ )
12
+ from src.runtime.determinism import (
13
+ ring_slot as ring_slot,
14
+ )
15
+
16
+ if TYPE_CHECKING:
17
+ from ..recurrent.backend import RecurrentInputs
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class SynapseConfig:
22
+ stdp: bool = True
23
+ stp: bool = True
24
+ reward_modulated: bool = True
25
+ seed: int = 12345
26
+ learning_rate: float = 0.2
27
+ weight_decay: float = 0.001
28
+ weight_max: float = 20.0
29
+ eligibility_tau: float = 100.0
30
+ credit_window: int = 64
31
+ td_lambda: float = 0.9
32
+ gamma: float = 0.99
33
+
34
+ def parameters(self) -> list[float]:
35
+ return [
36
+ float(self.stdp),
37
+ float(self.stp),
38
+ float(self.reward_modulated),
39
+ float(self.seed),
40
+ self.learning_rate,
41
+ self.weight_decay,
42
+ self.weight_max,
43
+ self.eligibility_tau,
44
+ float(self.credit_window),
45
+ self.td_lambda,
46
+ self.gamma,
47
+ ]
48
+
49
+ def validate(self) -> None:
50
+ if any(
51
+ type(value) is not bool
52
+ for value in (self.stdp, self.stp, self.reward_modulated)
53
+ ):
54
+ raise ValueError("synapse mode flags must be boolean")
55
+ if type(self.seed) is not int or not 0 <= self.seed <= 0xFFFFFFFF:
56
+ raise ValueError("synapse seed must be uint32")
57
+ if type(self.credit_window) is not int or not 0 <= self.credit_window <= 2000:
58
+ raise ValueError("credit_window must be in [0,2000]")
59
+ for name, value, low, high in (
60
+ ("learning_rate", self.learning_rate, 0.0, 1.0),
61
+ ("weight_decay", self.weight_decay, 0.0, 1.0),
62
+ ("weight_max", self.weight_max, 0.001, 100.0),
63
+ ("eligibility_tau", self.eligibility_tau, 0.001, 10000.0),
64
+ ("td_lambda", self.td_lambda, 0.0, 1.0),
65
+ ("gamma", self.gamma, 0.0, 1.0),
66
+ ):
67
+ if (
68
+ isinstance(value, bool)
69
+ or not math.isfinite(value)
70
+ or not low <= value <= high
71
+ ):
72
+ raise ValueError(f"invalid synapse {name}")
73
+
74
+
75
+ class SynapseState:
76
+ def __init__(self, inputs: RecurrentInputs, config: SynapseConfig) -> None:
77
+ self.inputs = inputs
78
+ self.config = config
79
+ self.weights = list(inputs.weights)
80
+ self.eligibility = [0.0] * len(inputs.sources)
81
+ self.available = [1.0] * len(inputs.sources)
82
+ self.last_event = [-10_000_000] * len(inputs.sources)
83
+ self.last_spike = [-10_000_000] * inputs.n_neurons
84
+ self.ring_size = max(inputs.delays, default=1) + 1
85
+ self.emitted = [[0.0] * len(inputs.sources) for _ in range(self.ring_size)]
86
+
87
+ def incoming(self, tick: int, neuron: int) -> float:
88
+ # Kept for external callers; the membrane oracle sums in edge order.
89
+ return sum(
90
+ self.edge_current(tick, edge)
91
+ for edge in range(
92
+ self.inputs.offsets[neuron], self.inputs.offsets[neuron + 1]
93
+ )
94
+ )
95
+
96
+ def edge_current(self, tick: int, edge: int) -> float:
97
+ previous = tick - self.inputs.delays[edge]
98
+ return (
99
+ self.emitted[ring_slot(previous, self.ring_size)][edge]
100
+ if previous >= 0
101
+ else 0.0
102
+ )
103
+
104
+ def update(self, tick: int, spikes: list[int], reward: float) -> None:
105
+ c, inputs = self.config, self.inputs
106
+ for target in range(inputs.n_neurons):
107
+ for edge in range(inputs.offsets[target], inputs.offsets[target + 1]):
108
+ source = inputs.sources[edge]
109
+ pre, post = spikes[source], spikes[target]
110
+ weight = self.weights[edge]
111
+ eligibility = self.eligibility[edge] * math.exp(
112
+ -inputs.dt_ms / c.eligibility_tau
113
+ )
114
+ # Match ascending-neuron pair-rule ordering, including self edges.
115
+ operations = ("pre", "post") if source <= target else ("post", "pre")
116
+ for operation in operations:
117
+ if operation == "pre" and pre:
118
+ if c.stdp and 0 < tick - self.last_spike[target] <= 20:
119
+ weight = max(0.0, weight - 0.08)
120
+ eligibility -= 0.5
121
+ if operation == "post" and post:
122
+ if c.stdp and 0 < tick - self.last_spike[source] <= 20:
123
+ weight = min(100.0, weight + 0.1)
124
+ eligibility += 1.0
125
+ if pre or post:
126
+ self.last_event[edge] = tick
127
+ if (
128
+ c.reward_modulated
129
+ and tick - self.last_event[edge] <= c.credit_window
130
+ ):
131
+ weight = min(
132
+ 100.0,
133
+ max(
134
+ 0.0,
135
+ weight
136
+ + c.learning_rate
137
+ * c.td_lambda
138
+ * c.gamma
139
+ * eligibility
140
+ * reward,
141
+ ),
142
+ )
143
+ amplitude = weight if pre else 0.0
144
+ available = self.available[edge]
145
+ if c.stp:
146
+ if pre:
147
+ amplitude *= float(
148
+ release_uniform(c.seed, tick, edge)
149
+ < min(0.95, 0.25 + 0.7 * available)
150
+ )
151
+ available = max(0.1, available * 0.72)
152
+ available = min(1.0, available + 0.025)
153
+ self.available[edge] = available
154
+ self.emitted[ring_slot(tick, self.ring_size)][edge] = amplitude
155
+ self.weights[edge] = min(
156
+ c.weight_max, max(0.0, weight * (1.0 - c.weight_decay))
157
+ )
158
+ self.eligibility[edge] = eligibility
159
+ for neuron, spike in enumerate(spikes):
160
+ if spike:
161
+ self.last_spike[neuron] = tick
src/acceleration/cuda/preflight.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Backend-neutral CUDA cooperative launch preflight contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from dataclasses import dataclass
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class CooperativePreflight:
11
+ """Host/device limits required for a cooperative persistent launch."""
12
+
13
+ cooperative_launch: bool
14
+ multiprocessor_count: int
15
+ active_blocks_per_sm: int
16
+ block_size: int
17
+ required_blocks: int
18
+ resident_block_capacity: int
19
+ launch_fits: bool
20
+ n_neurons: int
21
+
22
+ def to_mapping(self) -> dict[str, object]:
23
+ return {
24
+ "cooperative_launch": self.cooperative_launch,
25
+ "multiprocessor_count": self.multiprocessor_count,
26
+ "active_blocks_per_sm": self.active_blocks_per_sm,
27
+ "block_size": self.block_size,
28
+ "required_blocks": self.required_blocks,
29
+ "resident_block_capacity": self.resident_block_capacity,
30
+ "launch_fits": self.launch_fits,
31
+ "n_neurons": self.n_neurons,
32
+ }
33
+
34
+
35
+ def validate_cuda_block_size(block_size: int) -> int:
36
+ """Validate a legal one-dimensional CUDA block size."""
37
+
38
+ if isinstance(block_size, bool) or not isinstance(block_size, int):
39
+ raise ValueError("block_size must be an integer")
40
+ if not 1 <= block_size <= 1024:
41
+ raise ValueError("block_size must be in [1, 1024]")
42
+ if block_size % 32 != 0:
43
+ raise ValueError("block_size must be a multiple of 32")
44
+ return block_size
45
+
46
+
47
+ def cooperative_capacity(
48
+ *,
49
+ n_neurons: int,
50
+ block_size: int,
51
+ multiprocessor_count: int,
52
+ active_blocks_per_sm: int,
53
+ cooperative_launch: bool = True,
54
+ ) -> CooperativePreflight:
55
+ """Pure helper used by CI to verify occupancy-bound grid logic."""
56
+
57
+ if (
58
+ min(
59
+ n_neurons,
60
+ block_size,
61
+ multiprocessor_count,
62
+ active_blocks_per_sm,
63
+ )
64
+ <= 0
65
+ ):
66
+ raise ValueError("cooperative capacity inputs must be positive")
67
+ required = math.ceil(n_neurons / block_size)
68
+ capacity = multiprocessor_count * active_blocks_per_sm
69
+ return CooperativePreflight(
70
+ cooperative_launch=cooperative_launch,
71
+ multiprocessor_count=multiprocessor_count,
72
+ active_blocks_per_sm=active_blocks_per_sm,
73
+ block_size=block_size,
74
+ required_blocks=required,
75
+ resident_block_capacity=capacity,
76
+ launch_fits=cooperative_launch and required <= capacity,
77
+ n_neurons=n_neurons,
78
+ )
src/acceleration/cuda/ptxas.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Canonical PTX assembly and resource-report helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import shutil
7
+ import subprocess
8
+ import tempfile
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ from .errors import CudaDriverError, CudaRuntimeUnavailable
13
+
14
+
15
+ @dataclass(frozen=True, slots=True)
16
+ class PtxasReport:
17
+ """Measured resource report emitted by ptxas."""
18
+
19
+ target_sm: str
20
+ registers: int | None
21
+ shared_bytes: int | None
22
+ constant_bytes: int | None
23
+ stack_bytes: int | None
24
+ spill_store_bytes: int | None
25
+ spill_load_bytes: int | None
26
+ cubin_path: Path
27
+ verbose_output: str
28
+
29
+ def to_mapping(self) -> dict[str, object]:
30
+ return {
31
+ "target_sm": self.target_sm,
32
+ "registers": self.registers,
33
+ "shared_bytes": self.shared_bytes,
34
+ "constant_bytes": self.constant_bytes,
35
+ "stack_bytes": self.stack_bytes,
36
+ "spill_store_bytes": self.spill_store_bytes,
37
+ "spill_load_bytes": self.spill_load_bytes,
38
+ "cubin_path": str(self.cubin_path),
39
+ "verbose_output": self.verbose_output,
40
+ }
41
+
42
+
43
+ _REGISTER_RE = re.compile(r"Used\s+(\d+)\s+registers")
44
+ _SMEM_RE = re.compile(r"(\d+)\s+bytes smem")
45
+ _CMEM_RE = re.compile(r"(\d+)\s+bytes cmem\[\d+\]")
46
+ _STACK_RE = re.compile(r"(\d+)\s+bytes stack frame")
47
+ _SPILL_STORE_RE = re.compile(r"(\d+)\s+bytes spill stores")
48
+ _SPILL_LOAD_RE = re.compile(r"(\d+)\s+bytes spill loads")
49
+
50
+
51
+ def _first_int(pattern: re.Pattern[str], text: str) -> int | None:
52
+ match = pattern.search(text)
53
+ return int(match.group(1)) if match else None
54
+
55
+
56
+ def parse_ptxas_verbose(
57
+ verbose_output: str,
58
+ *,
59
+ target_sm: str,
60
+ cubin_path: Path,
61
+ ) -> PtxasReport:
62
+ """Parse stable resource fields from ptxas verbose diagnostics."""
63
+
64
+ constant_values = [int(value) for value in _CMEM_RE.findall(verbose_output)]
65
+ return PtxasReport(
66
+ target_sm=target_sm,
67
+ registers=_first_int(_REGISTER_RE, verbose_output),
68
+ shared_bytes=_first_int(_SMEM_RE, verbose_output),
69
+ constant_bytes=sum(constant_values) if constant_values else None,
70
+ stack_bytes=_first_int(_STACK_RE, verbose_output),
71
+ spill_store_bytes=_first_int(_SPILL_STORE_RE, verbose_output),
72
+ spill_load_bytes=_first_int(_SPILL_LOAD_RE, verbose_output),
73
+ cubin_path=cubin_path,
74
+ verbose_output=verbose_output,
75
+ )
76
+
77
+
78
+ def assemble_ptx(
79
+ ptx_source: str,
80
+ *,
81
+ target_sm: str = "sm_86",
82
+ output_dir: Path | None = None,
83
+ ptxas: str = "ptxas",
84
+ ) -> PtxasReport:
85
+ """Assemble generated PTX and return measured ptxas resources."""
86
+
87
+ executable = shutil.which(ptxas)
88
+ if executable is None:
89
+ raise CudaRuntimeUnavailable(f"{ptxas} is not available on PATH")
90
+
91
+ root = output_dir or Path(tempfile.mkdtemp(prefix="mhrn-ptxas-"))
92
+ root.mkdir(parents=True, exist_ok=True)
93
+ ptx_path = root / "pan_gate_kernel.ptx"
94
+ cubin_path = root / "pan_gate_kernel.cubin"
95
+ ptx_path.write_text(ptx_source, encoding="utf-8")
96
+
97
+ process = subprocess.run(
98
+ [
99
+ executable,
100
+ "--verbose",
101
+ f"--gpu-name={target_sm}",
102
+ str(ptx_path),
103
+ "--output-file",
104
+ str(cubin_path),
105
+ ],
106
+ check=False,
107
+ capture_output=True,
108
+ text=True,
109
+ )
110
+ verbose = "\n".join(part for part in (process.stdout, process.stderr) if part)
111
+ if process.returncode != 0:
112
+ raise CudaDriverError(
113
+ "ptxas rejected generated PTX " f"(exit={process.returncode}):\n{verbose}"
114
+ )
115
+ return parse_ptxas_verbose(
116
+ verbose,
117
+ target_sm=target_sm,
118
+ cubin_path=cubin_path,
119
+ )
src/acceleration/cuda/recurrent/__init__.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Canonical bounded recurrent CUDA package."""
2
+
3
+ from .backend import (
4
+ RecurrentInputs,
5
+ cpu_recurrent_reference,
6
+ execute_recurrent,
7
+ recurrent_fixture,
8
+ recurrent_parity,
9
+ validate_recurrent_inputs,
10
+ )
11
+ from .config import SUPPORTED_MODELS, CudaReferenceModelProfile
12
+ from .reference import CPUReferenceBackend
13
+ from .state import recurrent_inputs_from_mapping, recurrent_inputs_to_mapping
14
+
15
+ __all__ = [
16
+ "CPUReferenceBackend",
17
+ "CudaReferenceModelProfile",
18
+ "RecurrentInputs",
19
+ "SUPPORTED_MODELS",
20
+ "cpu_recurrent_reference",
21
+ "execute_recurrent",
22
+ "recurrent_fixture",
23
+ "recurrent_inputs_from_mapping",
24
+ "recurrent_inputs_to_mapping",
25
+ "recurrent_parity",
26
+ "validate_recurrent_inputs",
27
+ ]
src/acceleration/cuda/recurrent/backend.py ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Canonical bounded CUDA recurrent reference with explicit FP64 state.
2
+
3
+ The CUDA kernel is byte-identical to the previously hardware-verified
4
+ Playground kernel. This module changes ownership and interfaces, not kernel
5
+ semantics. PAN hyperstate, structural plasticity and live environment state are
6
+ outside this Wave-4 backend.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ctypes
12
+ import math
13
+ import tempfile
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from src.verification.parity import max_abs_error
19
+
20
+ from ..driver import CudaDriver
21
+ from ..errors import CudaDriverError
22
+ from ..memory import DeviceAllocation
23
+ from ..nvrtc import compile_cuda_source
24
+ from ..plasticity.reference import SynapseConfig, SynapseState
25
+ from ..preflight import validate_cuda_block_size
26
+ from .config import SUPPORTED_MODELS, cuda_reference_model_profile, step_reference
27
+
28
+ PARAMETER_NAMES = (
29
+ "v_rest",
30
+ "v_t",
31
+ "delta_t",
32
+ "tau_m_ms",
33
+ "tau_w_ms",
34
+ "a",
35
+ "b",
36
+ "threshold",
37
+ "reset",
38
+ "resistance",
39
+ )
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class RecurrentInputs:
44
+ n_neurons: int
45
+ ticks: int
46
+ offsets: tuple[int, ...]
47
+ sources: tuple[int, ...]
48
+ delays: tuple[int, ...]
49
+ weights: tuple[float, ...]
50
+ external: tuple[float, ...]
51
+ voltage: tuple[float, ...]
52
+ adaptation: tuple[float, ...]
53
+ model: str = "lif"
54
+ dt_ms: float = 1.0
55
+ synapses: SynapseConfig | None = None
56
+ rewards: tuple[float, ...] = ()
57
+
58
+
59
+ def _bounded_integer(value: object, lower: int, upper: int) -> bool:
60
+ return (
61
+ not isinstance(value, bool)
62
+ and isinstance(value, int)
63
+ and lower <= value <= upper
64
+ )
65
+
66
+
67
+ def _bounded_number(value: object) -> bool:
68
+ return (
69
+ not isinstance(value, bool)
70
+ and isinstance(value, (int, float))
71
+ and math.isfinite(value)
72
+ and abs(value) <= 1e6
73
+ )
74
+
75
+
76
+ def validate_recurrent_inputs(inputs: RecurrentInputs) -> None:
77
+ for name, value, upper in (
78
+ ("n_neurons", inputs.n_neurons, 4096),
79
+ ("ticks", inputs.ticks, 2000),
80
+ ):
81
+ if not _bounded_integer(value, 1, upper):
82
+ raise ValueError(f"{name} must be an integer in [1,{upper}]")
83
+ n, ticks = inputs.n_neurons, inputs.ticks
84
+ if inputs.synapses is not None:
85
+ inputs.synapses.validate()
86
+ if len(inputs.rewards) != ticks or any(
87
+ not _bounded_number(x) for x in inputs.rewards
88
+ ):
89
+ raise ValueError("plastic execution requires finite reward per tick")
90
+ if not inputs.sources:
91
+ raise ValueError("plastic execution requires at least one synapse")
92
+ if any(weight < 0 for weight in inputs.weights):
93
+ raise ValueError("plastic weights must be nonnegative")
94
+ if n * ticks > 4_000_000:
95
+ raise ValueError("state history exceeds bounded reference capacity")
96
+ if inputs.model not in SUPPORTED_MODELS:
97
+ raise ValueError("unsupported recurrent membrane model")
98
+ if (
99
+ isinstance(inputs.dt_ms, bool)
100
+ or not math.isfinite(inputs.dt_ms)
101
+ or not 0 < inputs.dt_ms <= 1
102
+ ):
103
+ raise ValueError("dt_ms must be finite in (0,1]")
104
+ edges = len(inputs.sources)
105
+ if edges > 65536 or len(inputs.offsets) != n + 1:
106
+ raise ValueError("invalid incoming CSR shape")
107
+ for values, upper in (
108
+ (inputs.offsets, edges),
109
+ (inputs.sources, n - 1),
110
+ (inputs.delays, 64),
111
+ ):
112
+ if any(not _bounded_integer(x, 0, upper) for x in values):
113
+ raise ValueError("invalid CSR integer/index/delay")
114
+ if (
115
+ inputs.offsets[0] != 0
116
+ or inputs.offsets[-1] != edges
117
+ or any(a > b for a, b in zip(inputs.offsets, inputs.offsets[1:]))
118
+ ):
119
+ raise ValueError("incoming CSR offsets must be monotone and span all edges")
120
+ if len(inputs.delays) != edges or any(x < 1 for x in inputs.delays):
121
+ raise ValueError("each edge requires a delay in [1,64]")
122
+ for name, numbers, length in (
123
+ ("weights", inputs.weights, edges),
124
+ ("external", inputs.external, n * ticks),
125
+ ("voltage", inputs.voltage, n),
126
+ ("adaptation", inputs.adaptation, n),
127
+ ):
128
+ if len(numbers) != length or any(not _bounded_number(x) for x in numbers):
129
+ raise ValueError(
130
+ f"{name} has an invalid shape or non-finite/out-of-range values"
131
+ )
132
+
133
+
134
+ def cpu_recurrent_reference(inputs: RecurrentInputs) -> dict[str, object]:
135
+ """Use the existing Python neuron model as an independent scalar oracle."""
136
+ validate_recurrent_inputs(inputs)
137
+ profile = cuda_reference_model_profile(inputs.model)
138
+ states = [{"v": v, "w": w} for v, w in zip(inputs.voltage, inputs.adaptation)]
139
+ n = inputs.n_neurons
140
+ spikes: list[int] = []
141
+ voltage: list[float] = []
142
+ adaptation: list[float] = []
143
+ synapses = (
144
+ SynapseState(inputs, inputs.synapses) if inputs.synapses is not None else None
145
+ )
146
+ for tick in range(inputs.ticks):
147
+ for neuron in range(n):
148
+ current = inputs.external[tick * n + neuron]
149
+ for edge in range(inputs.offsets[neuron], inputs.offsets[neuron + 1]):
150
+ previous = tick - inputs.delays[edge]
151
+ if synapses is not None:
152
+ current += synapses.edge_current(tick, edge)
153
+ elif previous >= 0:
154
+ current += (
155
+ inputs.weights[edge]
156
+ * spikes[previous * n + inputs.sources[edge]]
157
+ )
158
+ spike = step_reference(profile, states[neuron], current, inputs.dt_ms)
159
+ spikes.append(int(spike))
160
+ voltage.append(float(states[neuron]["v"]))
161
+ adaptation.append(float(states[neuron]["w"]))
162
+ if synapses is not None:
163
+ synapses.update(
164
+ tick, spikes[tick * n : (tick + 1) * n], inputs.rewards[tick]
165
+ )
166
+ result: dict[str, object] = {
167
+ "voltage": voltage,
168
+ "adaptation": adaptation,
169
+ "spikes": spikes,
170
+ }
171
+ if synapses is not None:
172
+ result.update(
173
+ weights=synapses.weights,
174
+ eligibility=synapses.eligibility,
175
+ available=synapses.available,
176
+ )
177
+ return result
178
+
179
+
180
+ def execute_recurrent(
181
+ inputs: RecurrentInputs,
182
+ *,
183
+ block_size: int = 64,
184
+ target_sm: str = "sm_86",
185
+ output_dir: Path | None = None,
186
+ ) -> dict[str, object]:
187
+ validate_recurrent_inputs(inputs)
188
+ validate_cuda_block_size(block_size)
189
+ if output_dir is None:
190
+ with tempfile.TemporaryDirectory(prefix="mhrn-recurrent-") as directory:
191
+ return execute_recurrent(
192
+ inputs,
193
+ block_size=block_size,
194
+ target_sm=target_sm,
195
+ output_dir=Path(directory),
196
+ )
197
+ output_dir.mkdir(parents=True, exist_ok=True)
198
+ source = Path(__file__).with_name("kernel.cu").read_text()
199
+ if inputs.synapses is not None:
200
+ source = "#define PAN_PLASTIC\n" + source
201
+ ptx = compile_cuda_source(source, target_sm=target_sm)
202
+ artifact = output_dir / "recurrent.ptx"
203
+ artifact.write_text(ptx, encoding="utf-8")
204
+ driver = CudaDriver()
205
+ loaded = driver.load_cubin(artifact, kernel_name="pan_recurrent_kernel")
206
+ allocations: list[DeviceAllocation] = []
207
+ try:
208
+ n, count = inputs.n_neurons, inputs.n_neurons * inputs.ticks
209
+ ring_size = max(inputs.delays, default=1) + 1
210
+ params = cuda_reference_model_profile(inputs.model).parameters
211
+ parameter_row = [
212
+ params.get(
213
+ name, 1.0 if name in {"resistance", "tau_w_ms", "delta_t"} else 0.0
214
+ )
215
+ for name in PARAMETER_NAMES
216
+ ]
217
+ uint, double = ctypes.c_uint32, ctypes.c_double
218
+ host: dict[str, Any] = {}
219
+ for name, kind, values in (
220
+ ("offsets", uint, inputs.offsets),
221
+ ("sources", uint, inputs.sources),
222
+ ("delays", uint, inputs.delays),
223
+ ("weights", double, inputs.weights),
224
+ ("external", double, inputs.external),
225
+ ("parameters", double, parameter_row * n),
226
+ ("voltage", double, inputs.voltage),
227
+ ("adaptation", double, inputs.adaptation),
228
+ ("ring", uint, [0] * (ring_size * n)),
229
+ ("voltage_history", double, [0.0] * count),
230
+ ("adaptation_history", double, [0.0] * count),
231
+ ("spikes", uint, [0] * count),
232
+ ):
233
+ # CUDA does not accept a zero-byte allocation for an empty graph.
234
+ array_type: Any = kind * max(1, len(values))
235
+ host[name] = array_type(*values)
236
+ if inputs.synapses is not None:
237
+ edges = len(inputs.sources)
238
+ host.update(
239
+ synapse_config=(double * 11)(*inputs.synapses.parameters()),
240
+ rewards=(double * inputs.ticks)(*inputs.rewards),
241
+ eligibility=(double * edges)(),
242
+ available=(double * edges)(*([1.0] * edges)),
243
+ last_event=(ctypes.c_int32 * edges)(*([-10_000_000] * edges)),
244
+ last_spike=(ctypes.c_int32 * n)(*([-10_000_000] * n)),
245
+ emitted=(double * (ring_size * edges))(),
246
+ )
247
+ device: dict[str, DeviceAllocation] = {}
248
+ for name, buffer in host.items():
249
+ allocation = driver.alloc_device(ctypes.sizeof(buffer))
250
+ allocations.append(allocation)
251
+ device[name] = allocation
252
+ driver.copy_host_to_device(allocation, buffer)
253
+ arguments: list[object] = [
254
+ uint(n),
255
+ uint(inputs.ticks),
256
+ uint(ring_size),
257
+ uint(inputs.model != "lif"),
258
+ double(inputs.dt_ms),
259
+ ]
260
+ arguments.extend(ctypes.c_uint64(device[name].ptr) for name in host)
261
+ preflight = driver.launch_cooperative(
262
+ loaded, n_neurons=n, block_size=block_size, arguments=arguments
263
+ )
264
+ driver.synchronize()
265
+ outputs: dict[str, object] = {}
266
+ for name, key in (
267
+ ("voltage_history", "voltage"),
268
+ ("adaptation_history", "adaptation"),
269
+ ("spikes", "spikes"),
270
+ ):
271
+ driver.copy_device_to_host(host[name], device[name])
272
+ outputs[key] = list(host[name])
273
+ if inputs.synapses is not None:
274
+ for name in ("weights", "eligibility", "available"):
275
+ driver.copy_device_to_host(host[name], device[name])
276
+ outputs[name] = list(host[name])
277
+ return {
278
+ "classification": (
279
+ "MHRN_CUDA15_PLASTIC_REFERENCE"
280
+ if inputs.synapses
281
+ else "MHRN_CUDA14_RECURRENT_REFERENCE"
282
+ ),
283
+ "scientific_evidence": False,
284
+ "execution_status": "GPU_KERNEL_EXECUTED",
285
+ "state_dtype": "float64",
286
+ "comparison_scope": (
287
+ "MEMBRANE_AND_FROZEN_REWARD_PLASTICITY"
288
+ if inputs.synapses
289
+ else "STATIC_SYNAPSES_AND_MEMBRANE_ONLY"
290
+ ),
291
+ "full_pan_backend": False,
292
+ "preflight": preflight.to_mapping(),
293
+ "ticks": inputs.ticks,
294
+ "outputs": outputs,
295
+ }
296
+ finally:
297
+ try:
298
+ for allocation in reversed(allocations):
299
+ try:
300
+ driver.free_device(allocation)
301
+ except CudaDriverError:
302
+ pass
303
+ finally:
304
+ driver.unload(loaded)
305
+
306
+
307
+ def recurrent_parity(
308
+ reference: dict[str, Any], candidate: dict[str, Any], *, tolerance: float = 1e-4
309
+ ) -> dict[str, object]:
310
+ if isinstance(tolerance, bool) or not math.isfinite(tolerance) or tolerance < 0:
311
+ raise ValueError("tolerance must be finite and non-negative")
312
+ try:
313
+ names = ["voltage", "adaptation"]
314
+ if any(
315
+ name in reference or name in candidate
316
+ for name in ("weights", "eligibility", "available")
317
+ ):
318
+ names.extend(("weights", "eligibility", "available"))
319
+ errors = {
320
+ name: max_abs_error(reference.get(name, []), candidate.get(name, []))
321
+ for name in names
322
+ }
323
+ except (ValueError, TypeError, OverflowError) as exc:
324
+ return {
325
+ "passed": False,
326
+ "failure_reason": str(exc),
327
+ "scientific_evidence": False,
328
+ }
329
+ spikes = reference.get("spikes", [])
330
+ other = candidate.get("spikes", [])
331
+ exact = (
332
+ bool(spikes)
333
+ and len(spikes)
334
+ == len(other)
335
+ == len(reference["voltage"])
336
+ == len(reference["adaptation"])
337
+ and all(a in (0, 1) and b in (0, 1) and a == b for a, b in zip(spikes, other))
338
+ )
339
+ return {
340
+ "passed": exact
341
+ and all(math.isfinite(x) and x <= tolerance for x in errors.values()),
342
+ "D1_spikes_exact": exact,
343
+ "D2_max_abs_error": errors,
344
+ "scientific_evidence": False,
345
+ "scope": (
346
+ "MEMBRANE_AND_FROZEN_REWARD_PLASTICITY"
347
+ if "weights" in names
348
+ else "MEMBRANE_AND_STATIC_SYNAPSES"
349
+ ),
350
+ }
351
+
352
+
353
+ def recurrent_fixture(
354
+ *, n_neurons: int = 129, ticks: int = 100, model: str = "pan_adex_5d"
355
+ ) -> RecurrentInputs:
356
+ """Nontrivial cross-block ring graph used by CLI, HTTP and hardware checks."""
357
+ if not _bounded_integer(n_neurons, 1, 4096):
358
+ raise ValueError("n_neurons must be in [1,4096]")
359
+ if not _bounded_integer(ticks, 1, 2000) or n_neurons * ticks > 4_000_000:
360
+ raise ValueError("ticks or history exceeds diagnostic capacity")
361
+ n = n_neurons
362
+ inputs = RecurrentInputs(
363
+ n,
364
+ ticks,
365
+ tuple(range(n + 1)),
366
+ tuple((i - 65) % n for i in range(n)),
367
+ tuple(1 + i % 7 for i in range(n)),
368
+ (2.0,) * n,
369
+ tuple(40.0 + (i % 5) * 2 for _ in range(ticks) for i in range(n)),
370
+ (-65.0,) * n,
371
+ (0.0,) * n,
372
+ model=model,
373
+ )
374
+ validate_recurrent_inputs(inputs)
375
+ return inputs
src/acceleration/cuda/recurrent/config.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Frozen model profiles for the bounded CUDA recurrent reference.
2
+
3
+ These profiles preserve the hardware-tested CUDA-1.4/1.5 membrane semantics
4
+ without importing the exploratory Playground registry. They are engineering
5
+ compatibility profiles, not a new scientific neuron-model registry.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ from collections.abc import Mapping
12
+ from dataclasses import dataclass
13
+ from types import MappingProxyType
14
+ from typing import Any, Final
15
+
16
+
17
+ @dataclass(frozen=True, slots=True)
18
+ class CudaReferenceModelProfile:
19
+ name: str
20
+ parameters: Mapping[str, float]
21
+ family: str
22
+
23
+
24
+ _PROFILES: Final[dict[str, CudaReferenceModelProfile]] = {
25
+ "lif": CudaReferenceModelProfile(
26
+ "lif",
27
+ MappingProxyType(
28
+ {
29
+ "v_rest": -65.0,
30
+ "threshold": -50.0,
31
+ "reset": -65.0,
32
+ "tau_m_ms": 20.0,
33
+ "resistance": 1.0,
34
+ }
35
+ ),
36
+ "bounded-lif-reference",
37
+ ),
38
+ "adex": CudaReferenceModelProfile(
39
+ "adex",
40
+ MappingProxyType(
41
+ {
42
+ "v_rest": -65.0,
43
+ "v_t": -50.0,
44
+ "delta_t": 2.0,
45
+ "tau_m_ms": 20.0,
46
+ "tau_w_ms": 120.0,
47
+ "a": 0.02,
48
+ "b": 0.5,
49
+ "threshold": 20.0,
50
+ "reset": -58.0,
51
+ }
52
+ ),
53
+ "bounded-adex-reference",
54
+ ),
55
+ "pan_adex_5d": CudaReferenceModelProfile(
56
+ "pan_adex_5d",
57
+ MappingProxyType(
58
+ {
59
+ "v_rest": -65.0,
60
+ "v_t": -55.0,
61
+ "delta_t": 2.0,
62
+ "tau_m_ms": 20.0,
63
+ "tau_w_ms": 120.0,
64
+ "a": 0.02,
65
+ "b": 0.5,
66
+ "threshold": -20.0,
67
+ "reset": -60.0,
68
+ }
69
+ ),
70
+ "bounded-pan-adex-membrane-reference",
71
+ ),
72
+ }
73
+
74
+ SUPPORTED_MODELS: Final[frozenset[str]] = frozenset(_PROFILES)
75
+
76
+
77
+ def cuda_reference_model_profile(name: str) -> CudaReferenceModelProfile:
78
+ try:
79
+ return _PROFILES[name]
80
+ except KeyError as exc:
81
+ raise ValueError(f"unsupported recurrent membrane model: {name}") from exc
82
+
83
+
84
+ def step_reference(
85
+ profile: CudaReferenceModelProfile,
86
+ state: dict[str, Any],
87
+ current: float,
88
+ dt_ms: float,
89
+ ) -> bool:
90
+ params = profile.parameters
91
+ v = float(state["v"])
92
+ if profile.name == "lif":
93
+ tau = max(params["tau_m_ms"], 1e-6)
94
+ v += dt_ms * (
95
+ (params["v_rest"] - v + params.get("resistance", 1.0) * current) / tau
96
+ )
97
+ spiked = v >= params["threshold"]
98
+ state["v"] = params["reset"] if spiked else v
99
+ state["w"] = float(state.get("w", 0.0))
100
+ return spiked
101
+
102
+ w = float(state.get("w", 0.0))
103
+ exponent = min(20.0, (v - params["v_t"]) / params["delta_t"])
104
+ dv = (
105
+ -(v - params["v_rest"]) + params["delta_t"] * math.exp(exponent) - w + current
106
+ ) / params["tau_m_ms"]
107
+ dw = (params["a"] * (v - params["v_rest"]) - w) / params["tau_w_ms"]
108
+ v += dt_ms * dv
109
+ w += dt_ms * dw
110
+ spiked = v >= params["threshold"]
111
+ if spiked:
112
+ v = params["reset"]
113
+ w += params["b"]
114
+ state["v"] = v
115
+ state["w"] = w
116
+ return spiked
src/acceleration/cuda/recurrent/kernel.cu ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include <cooperative_groups.h>
2
+
3
+ #ifdef PAN_PLASTIC
4
+ __device__ double release_uniform(unsigned seed, unsigned tick, unsigned edge) {
5
+ unsigned bits = seed ^ (tick * 0x9E3779B9u) ^ (edge * 0x85EBCA6Bu);
6
+ bits ^= bits >> 16; bits *= 0x7FEB352Du;
7
+ bits ^= bits >> 15; bits *= 0x846CA68Bu; bits ^= bits >> 16;
8
+ return (bits >> 8) / 16777216.0;
9
+ }
10
+ #endif
11
+
12
+ // Deterministic incoming-edge gather: no floating point atomic accumulation.
13
+ // All blocks participate in the barrier, including partial-block inactive lanes.
14
+ extern "C" __global__ void pan_recurrent_kernel(
15
+ const unsigned n, const unsigned ticks, const unsigned ring_size,
16
+ const unsigned model, const double dt,
17
+ const unsigned* offsets, const unsigned* sources,
18
+ const unsigned* delays, double* weights,
19
+ const double* external, const double* parameters,
20
+ double* voltage, double* adaptation, unsigned* ring,
21
+ double* voltage_history, double* adaptation_history, unsigned* spikes
22
+ #ifdef PAN_PLASTIC
23
+ , const double* config, const double* rewards, double* eligibility,
24
+ double* available, int* last_event, int* last_spike, double* emitted
25
+ #endif
26
+ ) {
27
+ auto grid = cooperative_groups::this_grid();
28
+ const unsigned neuron = blockIdx.x * blockDim.x + threadIdx.x;
29
+ for (unsigned tick = 0; tick < ticks; ++tick) {
30
+ if (neuron < n) {
31
+ double current = external[tick * n + neuron];
32
+ for (unsigned edge = offsets[neuron]; edge < offsets[neuron + 1]; ++edge) {
33
+ if (tick >= delays[edge]) {
34
+ const unsigned slot = (tick - delays[edge]) % ring_size;
35
+ #ifdef PAN_PLASTIC
36
+ current += emitted[slot * offsets[n] + edge];
37
+ #else
38
+ current += weights[edge] * ring[slot * n + sources[edge]];
39
+ #endif
40
+ }
41
+ }
42
+ const double* p = parameters + neuron * 10;
43
+ double v = voltage[neuron];
44
+ double w = adaptation[neuron];
45
+ if (model == 0) {
46
+ v += dt * ((p[0] - v + p[9] * current) / p[3]);
47
+ } else {
48
+ const double exponent = fmin(20.0, (v - p[1]) / p[2]);
49
+ const double dv = (-(v - p[0]) + p[2] * exp(exponent) - w + current) / p[3];
50
+ const double dw = (p[5] * (v - p[0]) - w) / p[4];
51
+ v += dt * dv;
52
+ w += dt * dw;
53
+ }
54
+ const unsigned spike = v >= p[7];
55
+ if (spike) {
56
+ v = p[8];
57
+ if (model != 0) w += p[6];
58
+ }
59
+ voltage[neuron] = v;
60
+ adaptation[neuron] = w;
61
+ voltage_history[tick * n + neuron] = v;
62
+ adaptation_history[tick * n + neuron] = w;
63
+ spikes[tick * n + neuron] = spike;
64
+ ring[(tick % ring_size) * n + neuron] = spike;
65
+ }
66
+ grid.sync();
67
+ #ifdef PAN_PLASTIC
68
+ if (neuron < n) {
69
+ const unsigned post = spikes[tick * n + neuron];
70
+ for (unsigned edge = offsets[neuron]; edge < offsets[neuron + 1]; ++edge) {
71
+ const unsigned source = sources[edge];
72
+ const unsigned pre = spikes[tick * n + source];
73
+ double weight = weights[edge];
74
+ double trace = eligibility[edge] * exp(-dt / config[7]);
75
+ const int since_pre = (int)tick - last_spike[source];
76
+ const int since_post = (int)tick - last_spike[neuron];
77
+ for (unsigned phase = 0; phase < 2; ++phase) {
78
+ const bool pre_phase = source <= neuron ? phase == 0 : phase == 1;
79
+ if (pre_phase && pre) {
80
+ if (config[0] && since_post > 0 && since_post <= 20)
81
+ weight = fmax(0.0, weight - 0.08);
82
+ trace -= 0.5;
83
+ }
84
+ if (!pre_phase && post) {
85
+ if (config[0] && since_pre > 0 && since_pre <= 20)
86
+ weight = fmin(100.0, weight + 0.1);
87
+ trace += 1.0;
88
+ }
89
+ }
90
+ if (pre || post) last_event[edge] = tick;
91
+ if (config[2] && (int)tick - last_event[edge] <= config[8])
92
+ weight = fmin(100.0, fmax(0.0, weight + config[4] * config[9] * config[10] * trace * rewards[tick]));
93
+ double amplitude = pre ? weight : 0.0;
94
+ double resource = available[edge];
95
+ if (config[1]) {
96
+ if (pre) {
97
+ amplitude *= release_uniform((unsigned)config[3], tick, edge) < fmin(0.95, 0.25 + 0.7 * resource);
98
+ resource = fmax(0.1, resource * 0.72);
99
+ }
100
+ resource = fmin(1.0, resource + 0.025);
101
+ }
102
+ available[edge] = resource;
103
+ emitted[(tick % ring_size) * offsets[n] + edge] = amplitude;
104
+ weights[edge] = fmin(config[6], fmax(0.0, weight * (1.0 - config[5])));
105
+ eligibility[edge] = trace;
106
+ }
107
+ }
108
+ grid.sync();
109
+ if (neuron < n && spikes[tick * n + neuron]) last_spike[neuron] = tick;
110
+ grid.sync();
111
+ #endif
112
+ }
113
+ }
src/acceleration/cuda/recurrent/reference.py ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """CPU scalar reference backend for canonical Wave-4 parity."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from typing import Any
7
+
8
+ from src.runtime.backend import BackendCapabilities, BackendState, RunResult, StepResult
9
+ from src.verification.parity import config_fingerprint, execution_fingerprint
10
+
11
+ from .backend import cpu_recurrent_reference
12
+ from .state import (
13
+ canonical_int,
14
+ prefix_inputs,
15
+ recurrent_inputs_from_mapping,
16
+ recurrent_inputs_to_mapping,
17
+ set_external_tick_config,
18
+ stable_digest,
19
+ step_payload,
20
+ )
21
+
22
+
23
+ class CPUReferenceBackend:
24
+ backend_name = "cpu-reference"
25
+ backend_version = "wave4-scalar-reference-v1"
26
+
27
+ def __init__(self) -> None:
28
+ self._config: dict[str, object] | None = None
29
+ self._seed = 0
30
+ self._tick = 0
31
+
32
+ def initialize(self, config: Mapping[str, object], seed: int) -> None:
33
+ inputs = recurrent_inputs_from_mapping(config, seed=seed)
34
+ self._config = recurrent_inputs_to_mapping(inputs)
35
+ self._seed = seed
36
+ self._tick = 0
37
+
38
+ def _require_config(self) -> dict[str, object]:
39
+ if self._config is None:
40
+ raise RuntimeError("backend is not initialized")
41
+ return self._config
42
+
43
+ def _execute_prefix(self, total_ticks: int) -> Mapping[str, object]:
44
+ inputs = recurrent_inputs_from_mapping(self._require_config(), seed=self._seed)
45
+ return cpu_recurrent_reference(prefix_inputs(inputs, total_ticks))
46
+
47
+ def _step_from_outputs(
48
+ self, outputs: Mapping[str, object], tick: int
49
+ ) -> StepResult:
50
+ config = self._require_config()
51
+ payload = step_payload(
52
+ outputs,
53
+ tick=tick,
54
+ n_neurons=canonical_int(config["n_neurons"], field="n_neurons"),
55
+ )
56
+ spikes_raw = payload["spikes"]
57
+ if not isinstance(spikes_raw, list):
58
+ raise ValueError("spike payload must be a list")
59
+ spikes = tuple(
60
+ index for index, value in enumerate(spikes_raw) if int(value) == 1
61
+ )
62
+ return StepResult(
63
+ tick=tick + 1,
64
+ spikes=spikes,
65
+ state_digest=stable_digest(payload),
66
+ metrics={
67
+ "voltage": payload["voltage"],
68
+ "adaptation": payload["adaptation"],
69
+ "spike_count": len(spikes),
70
+ },
71
+ )
72
+
73
+ def set_external_tick(self, tick: int, currents: Sequence[float]) -> None:
74
+ if tick != self._tick:
75
+ raise ValueError("live external input tick must equal continuation cursor")
76
+ set_external_tick_config(self._require_config(), tick=tick, currents=currents)
77
+
78
+ def step(self, tick: int) -> StepResult:
79
+ if tick != self._tick:
80
+ raise ValueError("tick must equal the backend continuation cursor")
81
+ config = self._require_config()
82
+ if tick >= canonical_int(config["ticks"], field="ticks"):
83
+ raise ValueError("configured tick limit reached")
84
+ outputs = self._execute_prefix(tick + 1)
85
+ self._tick = tick + 1
86
+ return self._step_from_outputs(outputs, tick)
87
+
88
+ def run(self, ticks: int) -> RunResult:
89
+ if type(ticks) is not int or ticks < 1:
90
+ raise ValueError("ticks must be a positive int")
91
+ config = self._require_config()
92
+ total = self._tick + ticks
93
+ if total > canonical_int(config["ticks"], field="ticks"):
94
+ raise ValueError("requested run exceeds configured tick limit")
95
+ start = self._tick
96
+ outputs = self._execute_prefix(total)
97
+ steps = tuple(
98
+ self._step_from_outputs(outputs, tick) for tick in range(start, total)
99
+ )
100
+ self._tick = total
101
+ return RunResult(
102
+ ticks_requested=ticks,
103
+ steps=steps,
104
+ final_state=self.snapshot(),
105
+ execution_fingerprint=execution_fingerprint(
106
+ seed=self._seed,
107
+ config_hash=config_fingerprint(config),
108
+ backend_name=self.backend_name,
109
+ backend_version=self.backend_version,
110
+ ticks=total,
111
+ ),
112
+ )
113
+
114
+ def snapshot(self) -> BackendState:
115
+ config = self._require_config()
116
+ payload: dict[str, Any] = {
117
+ "backend": self.backend_version,
118
+ "seed": self._seed,
119
+ "tick": self._tick,
120
+ "config": config,
121
+ "replay_required": True,
122
+ }
123
+ return BackendState(
124
+ tick=self._tick,
125
+ payload=payload,
126
+ state_digest=stable_digest(payload),
127
+ )
128
+
129
+ def restore(self, state: BackendState) -> None:
130
+ payload = dict(state.payload)
131
+ if payload.get("backend") != self.backend_version:
132
+ raise ValueError("checkpoint backend version mismatch")
133
+ raw_config = payload.get("config")
134
+ raw_seed = payload.get("seed")
135
+ if not isinstance(raw_config, Mapping) or type(raw_seed) is not int:
136
+ raise ValueError("checkpoint is missing canonical config/seed")
137
+ self.initialize(raw_config, raw_seed)
138
+ self._tick = state.tick
139
+
140
+ def capabilities(self) -> BackendCapabilities:
141
+ return BackendCapabilities(
142
+ supports_recurrent=True,
143
+ supports_plasticity=True,
144
+ supports_pan_hyperstate=False,
145
+ supports_structural_plasticity=False,
146
+ max_neurons=4096,
147
+ max_ticks=2000,
148
+ max_edges=65536,
149
+ deterministic=True,
150
+ plasticity_semantics="REFERENCE_MATCH_TARGET_DRAFT",
151
+ execution_mode="BOUNDED_REPLAY_REFERENCE",
152
+ supports_live_external_input=True,
153
+ )
src/acceleration/cuda/recurrent/state.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Serialization helpers for the bounded recurrent backend."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from collections.abc import Mapping, Sequence
8
+ from dataclasses import asdict, replace
9
+
10
+ from ..plasticity.reference import SynapseConfig
11
+ from .backend import RecurrentInputs, validate_recurrent_inputs
12
+
13
+
14
+ def stable_digest(value: object) -> str:
15
+ encoded = json.dumps(
16
+ value,
17
+ sort_keys=True,
18
+ separators=(",", ":"),
19
+ ensure_ascii=False,
20
+ allow_nan=False,
21
+ ).encode("utf-8")
22
+ return hashlib.sha256(encoded).hexdigest()
23
+
24
+
25
+ def canonical_int(value: object, *, field: str) -> int:
26
+ if isinstance(value, bool) or not isinstance(value, int):
27
+ raise ValueError(f"{field} must be an integer")
28
+ return value
29
+
30
+
31
+ def canonical_float(value: object, *, field: str) -> float:
32
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
33
+ raise ValueError(f"{field} must be numeric")
34
+ return float(value)
35
+
36
+
37
+ def recurrent_inputs_to_mapping(inputs: RecurrentInputs) -> dict[str, object]:
38
+ result: dict[str, object] = {
39
+ "n_neurons": inputs.n_neurons,
40
+ "ticks": inputs.ticks,
41
+ "offsets": list(inputs.offsets),
42
+ "sources": list(inputs.sources),
43
+ "delays": list(inputs.delays),
44
+ "weights": list(inputs.weights),
45
+ "external": list(inputs.external),
46
+ "voltage": list(inputs.voltage),
47
+ "adaptation": list(inputs.adaptation),
48
+ "model": inputs.model,
49
+ "dt_ms": inputs.dt_ms,
50
+ "rewards": list(inputs.rewards),
51
+ }
52
+ result["synapses"] = (
53
+ asdict(inputs.synapses) if inputs.synapses is not None else None
54
+ )
55
+ return result
56
+
57
+
58
+ def _tuple_int(values: object, *, field: str) -> tuple[int, ...]:
59
+ if not isinstance(values, (list, tuple)):
60
+ raise ValueError(f"{field} must be a list/tuple")
61
+ return tuple(
62
+ canonical_int(value, field=f"{field}[{index}]")
63
+ for index, value in enumerate(values)
64
+ )
65
+
66
+
67
+ def _tuple_float(values: object, *, field: str) -> tuple[float, ...]:
68
+ if not isinstance(values, (list, tuple)):
69
+ raise ValueError(f"{field} must be a list/tuple")
70
+ return tuple(
71
+ canonical_float(value, field=f"{field}[{index}]")
72
+ for index, value in enumerate(values)
73
+ )
74
+
75
+
76
+ def recurrent_inputs_from_mapping(
77
+ config: Mapping[str, object],
78
+ *,
79
+ seed: int,
80
+ ) -> RecurrentInputs:
81
+ if type(seed) is not int or not 0 <= seed <= 0xFFFFFFFF:
82
+ raise ValueError("seed must be uint32")
83
+ raw_synapses = config.get("synapses")
84
+ synapses: SynapseConfig | None = None
85
+ if raw_synapses is not None:
86
+ if not isinstance(raw_synapses, Mapping):
87
+ raise ValueError("synapses must be a mapping or null")
88
+ supplied_seed = canonical_int(
89
+ raw_synapses.get("seed", seed), field="synapses.seed"
90
+ )
91
+ if supplied_seed != seed:
92
+ raise ValueError("backend seed must match synapse seed")
93
+ synapses = SynapseConfig(
94
+ stdp=bool(raw_synapses.get("stdp", True)),
95
+ stp=bool(raw_synapses.get("stp", True)),
96
+ reward_modulated=bool(raw_synapses.get("reward_modulated", True)),
97
+ seed=seed,
98
+ learning_rate=canonical_float(
99
+ raw_synapses.get("learning_rate", 0.2),
100
+ field="synapses.learning_rate",
101
+ ),
102
+ weight_decay=canonical_float(
103
+ raw_synapses.get("weight_decay", 0.001),
104
+ field="synapses.weight_decay",
105
+ ),
106
+ weight_max=canonical_float(
107
+ raw_synapses.get("weight_max", 20.0),
108
+ field="synapses.weight_max",
109
+ ),
110
+ eligibility_tau=canonical_float(
111
+ raw_synapses.get("eligibility_tau", 100.0),
112
+ field="synapses.eligibility_tau",
113
+ ),
114
+ credit_window=canonical_int(
115
+ raw_synapses.get("credit_window", 64),
116
+ field="synapses.credit_window",
117
+ ),
118
+ td_lambda=canonical_float(
119
+ raw_synapses.get("td_lambda", 0.9),
120
+ field="synapses.td_lambda",
121
+ ),
122
+ gamma=canonical_float(
123
+ raw_synapses.get("gamma", 0.99),
124
+ field="synapses.gamma",
125
+ ),
126
+ )
127
+ try:
128
+ inputs = RecurrentInputs(
129
+ n_neurons=canonical_int(config["n_neurons"], field="n_neurons"),
130
+ ticks=canonical_int(config["ticks"], field="ticks"),
131
+ offsets=_tuple_int(config["offsets"], field="offsets"),
132
+ sources=_tuple_int(config["sources"], field="sources"),
133
+ delays=_tuple_int(config["delays"], field="delays"),
134
+ weights=_tuple_float(config["weights"], field="weights"),
135
+ external=_tuple_float(config["external"], field="external"),
136
+ voltage=_tuple_float(config["voltage"], field="voltage"),
137
+ adaptation=_tuple_float(config["adaptation"], field="adaptation"),
138
+ model=str(config.get("model", "lif")),
139
+ dt_ms=canonical_float(config.get("dt_ms", 1.0), field="dt_ms"),
140
+ synapses=synapses,
141
+ rewards=_tuple_float(config.get("rewards", ()), field="rewards"),
142
+ )
143
+ except KeyError as exc:
144
+ raise ValueError(f"missing recurrent config field: {exc.args[0]}") from exc
145
+ validate_recurrent_inputs(inputs)
146
+ return inputs
147
+
148
+
149
+ def set_external_tick_config(
150
+ config: dict[str, object],
151
+ *,
152
+ tick: int,
153
+ currents: Sequence[float],
154
+ ) -> None:
155
+ """Replace exactly one external-current row in a canonical replay config."""
156
+
157
+ n = canonical_int(config.get("n_neurons"), field="n_neurons")
158
+ ticks = canonical_int(config.get("ticks"), field="ticks")
159
+ if type(tick) is not int or not 0 <= tick < ticks:
160
+ raise ValueError("tick is outside configured replay range")
161
+ if len(currents) != n:
162
+ raise ValueError("external-current row must match n_neurons")
163
+ raw_external = config.get("external")
164
+ if not isinstance(raw_external, (list, tuple)):
165
+ raise ValueError("external must be a list/tuple")
166
+ if len(raw_external) != n * ticks:
167
+ raise ValueError("external has invalid configured shape")
168
+ external = [
169
+ canonical_float(value, field=f"external[{index}]")
170
+ for index, value in enumerate(raw_external)
171
+ ]
172
+ row = [
173
+ canonical_float(value, field=f"currents[{index}]")
174
+ for index, value in enumerate(currents)
175
+ ]
176
+ start = tick * n
177
+ external[start : start + n] = row
178
+ config["external"] = external
179
+
180
+
181
+ def prefix_inputs(inputs: RecurrentInputs, ticks: int) -> RecurrentInputs:
182
+ if not 1 <= ticks <= inputs.ticks:
183
+ raise ValueError("prefix ticks outside configured range")
184
+ n = inputs.n_neurons
185
+ return replace(
186
+ inputs,
187
+ ticks=ticks,
188
+ external=inputs.external[: ticks * n],
189
+ rewards=inputs.rewards[:ticks] if inputs.synapses is not None else (),
190
+ )
191
+
192
+
193
+ def step_payload(
194
+ outputs: Mapping[str, object],
195
+ *,
196
+ tick: int,
197
+ n_neurons: int,
198
+ ) -> dict[str, object]:
199
+ start, stop = tick * n_neurons, (tick + 1) * n_neurons
200
+ voltage_raw = outputs.get("voltage")
201
+ adaptation_raw = outputs.get("adaptation")
202
+ spikes_raw = outputs.get("spikes")
203
+ if not isinstance(voltage_raw, list):
204
+ raise ValueError("voltage output must be a list")
205
+ if not isinstance(adaptation_raw, list):
206
+ raise ValueError("adaptation output must be a list")
207
+ if not isinstance(spikes_raw, list):
208
+ raise ValueError("spikes output must be a list")
209
+ return {
210
+ "tick": tick,
211
+ "voltage": [
212
+ canonical_float(value, field="voltage output")
213
+ for value in voltage_raw[start:stop]
214
+ ],
215
+ "adaptation": [
216
+ canonical_float(value, field="adaptation output")
217
+ for value in adaptation_raw[start:stop]
218
+ ],
219
+ "spikes": [
220
+ canonical_int(value, field="spike output")
221
+ for value in spikes_raw[start:stop]
222
+ ],
223
+ }
src/controller/runtime.py CHANGED
@@ -23,7 +23,7 @@ Example:
23
  Integration with dashboard:
24
  >>> from src.dashboard.operator_bridge import OperatorBridge
25
  >>> bridge = OperatorBridge(controller=controller)
26
- >>> serve_dashboard(host="127.0.0.1", port=8765, structural_bridge=bridge)
27
  """
28
 
29
  from __future__ import annotations
 
23
  Integration with dashboard:
24
  >>> from src.dashboard.operator_bridge import OperatorBridge
25
  >>> bridge = OperatorBridge(controller=controller)
26
+ >>> serve_dashboard(host="127.0.0.1", port=8767, structural_bridge=bridge)
27
  """
28
 
29
  from __future__ import annotations
src/dashboard/__init__.py CHANGED
@@ -15,10 +15,10 @@ and accessed via a web browser.
15
 
16
  Example:
17
  >>> from src.dashboard import serve_dashboard
18
- >>> serve_dashboard(host="127.0.0.1", port=8765)
19
 
20
  Or via command line:
21
- >>> python -m src.dashboard --host 0.0.0.0 --port 8765
22
  """
23
 
24
  from __future__ import annotations
@@ -45,6 +45,7 @@ from .models import (
45
  StructuralMetrics,
46
  SystemMetrics,
47
  )
 
48
  from .operator_bridge import OperatorBridge
49
  from .server import serve_dashboard
50
  from .state import (
@@ -67,8 +68,8 @@ logger = logging.getLogger(__name__)
67
 
68
 
69
  def create_default_dashboard(
70
- host: str = "127.0.0.1",
71
- port: int = 8765,
72
  snapshot_path: str | None = None,
73
  docs_root: str | None = None,
74
  state_store: DashboardStateStore | None = None,
@@ -81,7 +82,7 @@ def create_default_dashboard(
81
 
82
  Args:
83
  host: Host address to bind to (default: 127.0.0.1).
84
- port: Port to bind to (default: 8765).
85
  snapshot_path: Optional path to the default B5D snapshot.
86
  docs_root: Optional path to the documentation root.
87
  state_store: Optional custom state store.
 
15
 
16
  Example:
17
  >>> from src.dashboard import serve_dashboard
18
+ >>> serve_dashboard(host="127.0.0.1", port=8767)
19
 
20
  Or via command line:
21
+ >>> python -m src.dashboard --host 0.0.0.0 --port 8767
22
  """
23
 
24
  from __future__ import annotations
 
45
  StructuralMetrics,
46
  SystemMetrics,
47
  )
48
+ from .network_settings import DASHBOARD_HOST, DASHBOARD_PORT
49
  from .operator_bridge import OperatorBridge
50
  from .server import serve_dashboard
51
  from .state import (
 
68
 
69
 
70
  def create_default_dashboard(
71
+ host: str = DASHBOARD_HOST,
72
+ port: int = DASHBOARD_PORT,
73
  snapshot_path: str | None = None,
74
  docs_root: str | None = None,
75
  state_store: DashboardStateStore | None = None,
 
82
 
83
  Args:
84
  host: Host address to bind to (default: 127.0.0.1).
85
+ port: Port to bind to (default: 8767).
86
  snapshot_path: Optional path to the default B5D snapshot.
87
  docs_root: Optional path to the documentation root.
88
  state_store: Optional custom state store.
src/dashboard/experiment_evaluation.py ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Controlled post-hoc scientific interpretation artifacts for experiments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+ from typing import Any, cast
10
+
11
+
12
+ class ExperimentEvaluationError(ValueError):
13
+ """Raised when a post-hoc evaluation cannot be safely recorded."""
14
+
15
+
16
+ _EXPERIMENT_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
17
+ _ANSWERS = frozenset({"supported", "refuted", "mixed", "inconclusive", "not_assessed"})
18
+ _TEXT_LIMITS = {
19
+ "evaluator": 200,
20
+ "evaluation": 30_000,
21
+ "observations": 20_000,
22
+ "limitations": 20_000,
23
+ "follow_up": 20_000,
24
+ }
25
+
26
+
27
+ def _text(payload: dict[str, object], key: str, *, required: bool = False) -> str:
28
+ value = payload.get(key, "")
29
+ if not isinstance(value, str):
30
+ raise ExperimentEvaluationError(f"{key} must be text")
31
+ result = value.strip()
32
+ if required and not result:
33
+ raise ExperimentEvaluationError(f"{key} is required")
34
+ limit = _TEXT_LIMITS.get(key)
35
+ if limit is not None and len(result) > limit:
36
+ raise ExperimentEvaluationError(f"{key} exceeds {limit} characters")
37
+ return result
38
+
39
+
40
+ def _experiment_dir(research_root: Path, experiment_id: str) -> Path:
41
+ if not _EXPERIMENT_ID.fullmatch(experiment_id):
42
+ raise ExperimentEvaluationError("invalid experiment_id")
43
+ directory = research_root / "experiments" / experiment_id
44
+ manifest = directory / "manifest.json"
45
+ if not manifest.is_file():
46
+ raise ExperimentEvaluationError(f"experiment not found: {experiment_id}")
47
+ return directory
48
+
49
+
50
+ def read_experiment_evaluation(
51
+ research_root: Path, experiment_id: str
52
+ ) -> dict[str, object]:
53
+ """Return the saved evaluation without exposing arbitrary filesystem paths."""
54
+
55
+ directory = _experiment_dir(research_root, experiment_id)
56
+ path = directory / "posthoc" / "evaluation.json"
57
+ if not path.is_file():
58
+ return {"exists": False, "experiment_id": experiment_id}
59
+ try:
60
+ value: Any = json.loads(path.read_text(encoding="utf-8"))
61
+ except (OSError, json.JSONDecodeError) as exc:
62
+ raise ExperimentEvaluationError("saved evaluation is unreadable") from exc
63
+ if not isinstance(value, dict):
64
+ raise ExperimentEvaluationError("saved evaluation must be an object")
65
+ return {"exists": True, **value}
66
+
67
+
68
+ def write_experiment_evaluation(
69
+ research_root: Path, payload: dict[str, object]
70
+ ) -> dict[str, object]:
71
+ """Write a versioned interpretation artifact linked from the manifest."""
72
+
73
+ experiment_id = _text(payload, "experiment_id", required=True)
74
+ directory = _experiment_dir(research_root, experiment_id)
75
+ answer = _text(payload, "hypothesis_answer", required=True)
76
+ if answer not in _ANSWERS:
77
+ raise ExperimentEvaluationError("invalid hypothesis_answer")
78
+
79
+ evaluation: dict[str, object] = {
80
+ "schema": "MHRN_POSTHOC_EVALUATION_V1",
81
+ "experiment_id": experiment_id,
82
+ "research_question": _text(payload, "research_question"),
83
+ "hypothesis": _text(payload, "hypothesis"),
84
+ "hypothesis_answer": answer,
85
+ "evaluator": _text(payload, "evaluator", required=True),
86
+ "evaluation": _text(payload, "evaluation", required=True),
87
+ "observations": _text(payload, "observations"),
88
+ "limitations": _text(payload, "limitations"),
89
+ "follow_up": _text(payload, "follow_up"),
90
+ "created_at": _text(payload, "created_at")
91
+ or datetime.now(timezone.utc).isoformat(),
92
+ "scientific_evidence": False,
93
+ "epistemic_layer": "post_hoc_interpretation",
94
+ "human_review_status": "not_reviewed",
95
+ "automatic_evidence_promotion": False,
96
+ }
97
+
98
+ artifact_dir = directory / "posthoc"
99
+ artifact_dir.mkdir(exist_ok=True)
100
+ json_path = artifact_dir / "evaluation.json"
101
+ markdown_path = artifact_dir / "evaluation.md"
102
+ json_path.write_text(
103
+ json.dumps(evaluation, indent=2, ensure_ascii=True) + "\n", encoding="utf-8"
104
+ )
105
+ markdown_path.write_text(_markdown(evaluation), encoding="utf-8")
106
+
107
+ manifest_path = directory / "manifest.json"
108
+ manifest_value: Any = json.loads(manifest_path.read_text(encoding="utf-8"))
109
+ if not isinstance(manifest_value, dict):
110
+ raise ExperimentEvaluationError("experiment manifest must be an object")
111
+ manifest = cast(dict[str, object], manifest_value)
112
+ artifacts_value = manifest.get("artifacts")
113
+ artifacts = (
114
+ cast(dict[str, object], artifacts_value)
115
+ if isinstance(artifacts_value, dict)
116
+ else {}
117
+ )
118
+ artifacts["posthoc_evaluation"] = "posthoc/evaluation.md"
119
+ artifacts["posthoc_evaluation_data"] = "posthoc/evaluation.json"
120
+ manifest["artifacts"] = artifacts
121
+ manifest["posthoc_evaluation"] = {
122
+ "status": "recorded_interpretation_not_evidence",
123
+ "hypothesis_answer": answer,
124
+ "updated_at": evaluation["created_at"],
125
+ }
126
+ manifest_path.write_text(
127
+ json.dumps(manifest, indent=2, ensure_ascii=True) + "\n",
128
+ encoding="utf-8",
129
+ )
130
+
131
+ return {
132
+ "ok": True,
133
+ "experiment_id": experiment_id,
134
+ "markdown_path": f"experiments/{experiment_id}/posthoc/evaluation.md",
135
+ "json_path": f"experiments/{experiment_id}/posthoc/evaluation.json",
136
+ "scientific_evidence": False,
137
+ "epistemic_layer": "post_hoc_interpretation",
138
+ }
139
+
140
+
141
+ def _markdown(evaluation: dict[str, object]) -> str:
142
+ def value(key: str, fallback: str = "Nicht angegeben") -> str:
143
+ return str(evaluation.get(key) or fallback)
144
+
145
+ return (
146
+ "# Post-hoc wissenschaftliche Auswertung\n\n"
147
+ f"- Experiment: `{value('experiment_id')}`\n"
148
+ f"- Forschungsfrage: {value('research_question')}\n"
149
+ f"- Hypothese: {value('hypothesis')}\n"
150
+ f"- Hypothesenantwort: **{value('hypothesis_answer')}**\n"
151
+ f"- Auswertende Person: {value('evaluator')}\n"
152
+ f"- Zeitpunkt: {value('created_at')}\n\n"
153
+ "## Wissenschaftliche Auswertung\n\n"
154
+ f"{value('evaluation')}\n\n"
155
+ "## Beobachtungen und Auswertungskriterien\n\n"
156
+ f"{value('observations')}\n\n"
157
+ "## Limitationen und Alternativerklärungen\n\n"
158
+ f"{value('limitations')}\n\n"
159
+ "## Nächste Schritte\n\n"
160
+ f"{value('follow_up')}\n\n"
161
+ "## Evidenzgrenze\n\n"
162
+ "Dieses Dokument ist eine nachgelagerte Interpretation. Es ist keine "
163
+ "automatische EVID-Einstufung, verändert keine Hypothesenregistrierung "
164
+ "und benötigt einen separaten Human-Review-/EVID-Workflow, bevor es als "
165
+ "wissenschaftlich geprüfte Aussage in die Arbeit eingehen kann.\n"
166
+ )
src/dashboard/gate_status.py CHANGED
@@ -720,7 +720,7 @@ class GateStatusBuilder:
720
  gate=GATE_A,
721
  id="A-SINGLE-LISTENER",
722
  category="technical_integration",
723
- label="Exactly one TCP LISTEN socket on 127.0.0.1:8765 owned by MHRN PID",
724
  status=G_PASSED if listener_verified else G_PENDING,
725
  maturity=VERIFIED if listener_verified else IMPLEMENTED,
726
  source=(
 
720
  gate=GATE_A,
721
  id="A-SINGLE-LISTENER",
722
  category="technical_integration",
723
+ label="Exactly one TCP LISTEN socket on 127.0.0.1:8767 owned by MHRN PID",
724
  status=G_PASSED if listener_verified else G_PENDING,
725
  maturity=VERIFIED if listener_verified else IMPLEMENTED,
726
  source=(
src/dashboard/integration_status.py CHANGED
@@ -16,6 +16,7 @@ A component disabled by config is NEVER reported as "failed".
16
 
17
  from __future__ import annotations
18
 
 
19
  from pathlib import Path
20
  from typing import Any, cast
21
 
@@ -128,6 +129,206 @@ class IntegrationStatusBuilder:
128
  "total": len(items),
129
  "items": cast(JSONValue, items),
130
  "source": "live_backend",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  }
132
 
133
  # ------------------------------------------------------------------------
 
16
 
17
  from __future__ import annotations
18
 
19
+ import json
20
  from pathlib import Path
21
  from typing import Any, cast
22
 
 
129
  "total": len(items),
130
  "items": cast(JSONValue, items),
131
  "source": "live_backend",
132
+ "acceleration": cast(JSONValue, self._build_acceleration_status()),
133
+ }
134
+
135
+ def _build_acceleration_status(self) -> dict[str, JSONValue]:
136
+ """Project canonical CUDA/FE-3 integration state without executing GPU work."""
137
+
138
+ manifest_path = (
139
+ self.repo_root
140
+ / "research"
141
+ / "verification"
142
+ / "frozen_environment"
143
+ / "FE3_DETERMINISTIC_TARGET_V1.json"
144
+ )
145
+ manifest_status: dict[str, JSONValue] = {
146
+ "path": str(manifest_path.relative_to(self.repo_root)),
147
+ "status": "missing",
148
+ "valid": False,
149
+ }
150
+ try:
151
+ from src.verification.frozen_environment import load_manifest_artifact
152
+
153
+ manifest = load_manifest_artifact(manifest_path)
154
+ manifest_status = {
155
+ "path": str(manifest_path.relative_to(self.repo_root)),
156
+ "status": "verified",
157
+ "valid": True,
158
+ "mode": manifest.mode.value,
159
+ "contract_id": manifest.contract_id,
160
+ "manifest_sha256": manifest.manifest_sha256,
161
+ "environment_id": manifest.environment_id,
162
+ "sensor_ticks": len(manifest.sensor_schedule),
163
+ }
164
+ except (OSError, ValueError) as exc:
165
+ manifest_status["message"] = str(exc)
166
+
167
+ backend_status: dict[str, JSONValue]
168
+ try:
169
+ from src.acceleration.cuda import (
170
+ CUDABackend,
171
+ execution_backend_contract_check,
172
+ )
173
+
174
+ backend = CUDABackend()
175
+ capabilities = backend.capabilities().to_mapping()
176
+ backend_status = {
177
+ "status": "integrated",
178
+ "contract_ok": execution_backend_contract_check(),
179
+ "backend_name": backend.backend_name,
180
+ "backend_version": backend.backend_version,
181
+ "capabilities": cast(JSONValue, capabilities),
182
+ }
183
+ except Exception as exc: # fail closed in dashboard projection
184
+ backend_status = {
185
+ "status": "unavailable",
186
+ "contract_ok": False,
187
+ "message": str(exc),
188
+ }
189
+
190
+ reports = sorted(
191
+ (self.repo_root / "docs" / "canonical").glob("HARDWARE_ACCEPTANCE_*.json")
192
+ )
193
+ hardware: dict[str, JSONValue]
194
+ if not reports:
195
+ hardware = {
196
+ "status": "pending",
197
+ "accepted": False,
198
+ "artifact": None,
199
+ "message": "No reviewed physical hardware acceptance artifact is present.",
200
+ }
201
+ else:
202
+ latest = reports[-1]
203
+ try:
204
+ raw_object: object = json.loads(latest.read_text(encoding="utf-8"))
205
+ if not isinstance(raw_object, dict):
206
+ raise ValueError("hardware acceptance artifact must be an object")
207
+ raw = cast(dict[str, object], raw_object)
208
+ accepted = bool(raw.get("passed")) and bool(
209
+ raw.get("full_fe3_accepted")
210
+ )
211
+ hardware = {
212
+ "status": "passed" if accepted else "failed",
213
+ "accepted": accepted,
214
+ "artifact": str(latest.relative_to(self.repo_root)),
215
+ "gpu_identity": cast(JSONValue, raw.get("gpu_identity")),
216
+ "wave4_physical": cast(JSONValue, raw.get("wave4_physical")),
217
+ "builder_d3c_bridge": cast(
218
+ JSONValue, raw.get("builder_d3c_bridge")
219
+ ),
220
+ "frozen_environment_fe3": cast(
221
+ JSONValue, raw.get("frozen_environment_fe3")
222
+ ),
223
+ "full_fe3_accepted": bool(raw.get("full_fe3_accepted")),
224
+ "scientific_evidence": False,
225
+ }
226
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
227
+ hardware = {
228
+ "status": "failed",
229
+ "accepted": False,
230
+ "artifact": str(latest.relative_to(self.repo_root)),
231
+ "message": str(exc),
232
+ }
233
+
234
+ capabilities_object = backend_status.get("capabilities")
235
+ backend_live = False
236
+ if isinstance(capabilities_object, dict):
237
+ capabilities_mapping = capabilities_object
238
+ backend_live = (
239
+ capabilities_mapping.get("supports_live_external_input") is True
240
+ )
241
+ fe3_software_ready = bool(manifest_status["valid"]) and backend_live
242
+
243
+ try:
244
+ from src.homeostasis.pan_wave5b_preflight import (
245
+ evaluate_pan_wave5b_readiness,
246
+ )
247
+
248
+ pan_wave5b = evaluate_pan_wave5b_readiness(self.repo_root).to_mapping()
249
+ except Exception as exc: # fail closed in dashboard projection
250
+ pan_wave5b = {
251
+ "classification": "PAN_WAVE5B_PREFLIGHT",
252
+ "scientific_evidence": False,
253
+ "integration_claimed": False,
254
+ "preflight_ready": False,
255
+ "ready_for_execution": False,
256
+ "blockers": ["PAN_WAVE5B_PREFLIGHT_UNAVAILABLE"],
257
+ "next_gate": "REPAIR_PAN_WAVE5B_PREFLIGHT",
258
+ "message": str(exc),
259
+ }
260
+
261
+ pan_preflight_ready = pan_wave5b.get("preflight_ready") is True
262
+
263
+ waves: list[dict[str, JSONValue]] = [
264
+ {"id": "wave1", "label": "Neural I/O Contracts", "status": "integrated"},
265
+ {
266
+ "id": "wave2",
267
+ "label": "Codecs / Adapter / Gateway",
268
+ "status": "integrated",
269
+ },
270
+ {
271
+ "id": "wave3",
272
+ "label": "ExecutionBackend / Parity / Determinism",
273
+ "status": "integrated",
274
+ },
275
+ {"id": "wave4", "label": "Canonical CUDA Backend", "status": "integrated"},
276
+ {
277
+ "id": "fe3",
278
+ "label": "FE-3 Live Backend Bridge",
279
+ "status": "software_verified" if fe3_software_ready else "blocked",
280
+ },
281
+ {
282
+ "id": "hardware",
283
+ "label": "Physical RTX Hardware Acceptance",
284
+ "status": str(hardware["status"]),
285
+ },
286
+ {
287
+ "id": "wave5",
288
+ "label": "PAN Hyperstate / Wave 5B",
289
+ "status": (
290
+ "preflight_ready" if pan_preflight_ready else "contract_draft"
291
+ ),
292
+ },
293
+ {"id": "wave6", "label": "Structural Plasticity", "status": "blocked"},
294
+ {
295
+ "id": "wave7",
296
+ "label": "Canonical Learning Contract",
297
+ "status": "blocked",
298
+ },
299
+ ]
300
+
301
+ return {
302
+ "classification": "MHRN_ACCELERATION_INTEGRATION_STATUS",
303
+ "scientific_evidence": False,
304
+ "software_path_closed": fe3_software_ready,
305
+ "physical_hardware_accepted": bool(hardware["accepted"]),
306
+ "provenance_rule": (
307
+ "CPU/CUDA execution fingerprints differ by design; "
308
+ "semantic equality is manifest + live-input + D3c parity."
309
+ ),
310
+ "backend": cast(JSONValue, backend_status),
311
+ "fe3_manifest": cast(JSONValue, manifest_status),
312
+ "hardware_acceptance": cast(JSONValue, hardware),
313
+ "pan_wave5b_preflight": cast(JSONValue, pan_wave5b),
314
+ "waves": cast(JSONValue, waves),
315
+ "next_gate": str(
316
+ pan_wave5b.get(
317
+ "next_gate",
318
+ "REPAIR_PAN_WAVE5B_PREFLIGHT",
319
+ )
320
+ ),
321
+ "limitations": cast(
322
+ JSONValue,
323
+ {
324
+ "scientific_data": False,
325
+ "evidence": False,
326
+ "speedup_claim": False,
327
+ "pan_hyperstate": False,
328
+ "structural_plasticity": False,
329
+ "canonical_learning_contract": False,
330
+ },
331
+ ),
332
  }
333
 
334
  # ------------------------------------------------------------------------
src/dashboard/network_settings.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Central network settings for the MHRN dashboard.
2
+
3
+ All components (launcher, server, start scripts, Hugging Face config)
4
+ read their default host, port and LAN policy from this single module.
5
+ Change values here to reconfigure the entire stack.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ # ---------------------------------------------------------------------------
11
+ # Dashboard HTTP server
12
+ # ---------------------------------------------------------------------------
13
+
14
+ DASHBOARD_HOST: str = "127.0.0.1"
15
+ """Default bind address for direct Python dashboard startup."""
16
+
17
+ DASHBOARD_LAN_HOST: str = "0.0.0.0" # nosec B104 - trusted-LAN wrapper default
18
+ """Trusted-LAN bind address used by the Windows convenience wrappers.
19
+
20
+ The Windows launchers intentionally expose the dashboard to the host's private
21
+ network by default. Direct Python startup remains loopback-only unless the user
22
+ passes an explicit host.
23
+ """
24
+
25
+ DASHBOARD_PORT: int = 8767
26
+ """Default TCP port for the dashboard HTTP server."""
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # Public / Internet policy
30
+ # ---------------------------------------------------------------------------
31
+
32
+ ALLOW_PUBLIC_DEPLOYMENT: bool = False
33
+ """Whether the dashboard is allowed to bind to a public interface.
34
+
35
+ Set this to ``True`` only when a proper reverse proxy, TLS, authentication,
36
+ logging and privacy notice are in place. The dashboard exposes operator,
37
+ research and file-management endpoints without built-in access control.
38
+ """
39
+
40
+ PUBLIC_DEPLOYMENT_NOTE: str = (
41
+ "The dashboard exposes operator, research and file-management capabilities. "
42
+ "Do not expose it directly to the public Internet. For trusted-LAN use, "
43
+ f"restrict TCP port {DASHBOARD_PORT} to the intended private network."
44
+ )
45
+ """Safety note displayed in the README and referenced in deployment docs."""
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Hugging Face Space
49
+ # ---------------------------------------------------------------------------
50
+
51
+ HF_SPACE_PORT: int = DASHBOARD_PORT
52
+ """Port used inside the Hugging Face Docker Space (must match EXPOSE)."""
53
+
54
+ HF_SPACE_HOST: str = "0.0.0.0" # nosec B104 - required container bind for HF Spaces
55
+ """Host used inside the Hugging Face Space container."""
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Convenience accessors
59
+ # ---------------------------------------------------------------------------
60
+
61
+
62
+ def dashboard_local_url() -> str:
63
+ """Return the loopback URL of the dashboard."""
64
+ return f"http://127.0.0.1:{DASHBOARD_PORT}"
65
+
66
+
67
+ def dashboard_bind_spec() -> str:
68
+ """Return the ``host:port`` string used for bind / firewall docs."""
69
+ return f"{DASHBOARD_HOST}:{DASHBOARD_PORT}"
src/dashboard/playground_api.py CHANGED
@@ -2,17 +2,42 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  import threading
6
  import time
7
  from collections import deque
8
- from collections.abc import Mapping
 
9
  from typing import cast
10
 
11
  from src.playground import service
12
- from src.playground.cuda import compile_mapping
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  from src.playground.models import PlaygroundConfig
14
  from src.playground.night_run import NightRunManager
15
  from src.playground.pan import PANEmbodiedSandboxSession, PANSessionDaemon
 
 
16
 
17
  _MAX_CONCURRENT_RUNS = 2
18
  _RUNS_PER_MINUTE = 20
@@ -98,20 +123,29 @@ def _reserve_rate_slot() -> None:
98
  _RECENT_RUNS.append(now)
99
 
100
 
101
- def _bounded_run(path: str, payload: Mapping[str, object]) -> dict[str, object]:
 
 
102
  _reserve_rate_slot()
103
  if not _RUN_SEMAPHORE.acquire(blocking=False):
104
  raise PlaygroundBusyError(
105
  "Playground is busy; at most two simulation requests run concurrently."
106
  )
107
  try:
 
 
 
 
 
 
 
108
  if path == "/api/playground/run":
109
  return service.run(payload)
110
  if path == "/api/playground/robustness":
111
  return service.robustness(payload)
112
  raise ValueError(f"unknown Playground POST route: {path}")
113
- finally:
114
- _RUN_SEMAPHORE.release()
115
 
116
 
117
  def _live_parts(path: str) -> list[str]:
@@ -121,9 +155,314 @@ def _live_parts(path: str) -> list[str]:
121
  return [part for part in path[len(prefix) :].split("/") if part]
122
 
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  def get_playground(path: str) -> dict[str, object] | None:
125
  if path == "/api/playground/catalog":
126
  return service.catalog()
 
 
127
  if path == "/api/playground/sessions":
128
  return service.sessions()
129
  if path.startswith("/api/playground/sessions/"):
@@ -131,6 +470,8 @@ def get_playground(path: str) -> dict[str, object] | None:
131
  return service.replay(session_id)
132
  if path == "/api/playground/night":
133
  return _NIGHT_RUN.status()
 
 
134
  if path == "/api/playground/live":
135
  return {
136
  "class": "PLAYGROUND_LIVE_SESSIONS",
@@ -149,6 +490,15 @@ def post_playground(
149
  if path in {"/api/playground/run", "/api/playground/robustness"}:
150
  return _bounded_run(path, payload)
151
 
 
 
 
 
 
 
 
 
 
152
  if path == "/api/playground/determinism":
153
  return service.determinism(payload)
154
 
@@ -164,6 +514,21 @@ def post_playground(
164
  ptx_version=ptx_version,
165
  ).to_mapping()
166
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  if path == "/api/playground/night/start":
168
  return _NIGHT_RUN.start(
169
  hours=_payload_float(payload, "hours", 8.0),
@@ -205,7 +570,11 @@ def post_playground(
205
  session = _LIVE_DAEMON.get(session_id)
206
  if action == "step":
207
  ticks = _payload_int(payload, "ticks", 32)
208
- return {"session_id": session_id, **session.step(ticks)}
 
 
 
 
209
  if action == "input":
210
  values = _numeric_list(payload.get("values", []), "values")
211
  duration = _payload_int(payload, "duration_ticks", 16)
 
2
 
3
  from __future__ import annotations
4
 
5
+ import shutil
6
  import threading
7
  import time
8
  from collections import deque
9
+ from collections.abc import Callable, Mapping
10
+ from dataclasses import replace
11
  from typing import cast
12
 
13
  from src.playground import service
14
+ from src.playground.cuda import (
15
+ CompileBundle,
16
+ CudaDriver,
17
+ CudaDriverError,
18
+ CudaRuntimeUnavailable,
19
+ GateLaunchInputs,
20
+ compile_mapping,
21
+ cpu_gate_reference,
22
+ execute_gate_bundle,
23
+ gate_execution_parity_summary,
24
+ preflight_bundle,
25
+ validate_cuda_block_size,
26
+ )
27
+ from src.playground.cuda.builder_parity import run_builder_parity
28
+ from src.playground.cuda.recurrent import (
29
+ cpu_recurrent_reference,
30
+ execute_recurrent,
31
+ recurrent_fixture,
32
+ recurrent_parity,
33
+ )
34
+ from src.playground.cuda.synapses import SynapseConfig
35
+ from src.playground.integration import integration_catalog, transfer_element
36
  from src.playground.models import PlaygroundConfig
37
  from src.playground.night_run import NightRunManager
38
  from src.playground.pan import PANEmbodiedSandboxSession, PANSessionDaemon
39
+ from src.playground.pan.cue_controls import run_cue_controls
40
+ from src.playground.pan.transfer import run_synaptic_transfer
41
 
42
  _MAX_CONCURRENT_RUNS = 2
43
  _RUNS_PER_MINUTE = 20
 
123
  _RECENT_RUNS.append(now)
124
 
125
 
126
+ def _bounded_operation(
127
+ operation: Callable[[], dict[str, object]],
128
+ ) -> dict[str, object]:
129
  _reserve_rate_slot()
130
  if not _RUN_SEMAPHORE.acquire(blocking=False):
131
  raise PlaygroundBusyError(
132
  "Playground is busy; at most two simulation requests run concurrently."
133
  )
134
  try:
135
+ return operation()
136
+ finally:
137
+ _RUN_SEMAPHORE.release()
138
+
139
+
140
+ def _bounded_run(path: str, payload: Mapping[str, object]) -> dict[str, object]:
141
+ def operation() -> dict[str, object]:
142
  if path == "/api/playground/run":
143
  return service.run(payload)
144
  if path == "/api/playground/robustness":
145
  return service.robustness(payload)
146
  raise ValueError(f"unknown Playground POST route: {path}")
147
+
148
+ return _bounded_operation(operation)
149
 
150
 
151
  def _live_parts(path: str) -> list[str]:
 
155
  return [part for part in path[len(prefix) :].split("/") if part]
156
 
157
 
158
+ def _cuda_runtime_status() -> dict[str, object]:
159
+ ptxas_path = shutil.which("ptxas")
160
+ driver_available = False
161
+ driver_error: str | None = None
162
+ try:
163
+ driver = CudaDriver()
164
+ driver.initialize()
165
+ driver_available = True
166
+ except (CudaRuntimeUnavailable, CudaDriverError, OSError) as exc:
167
+ driver_error = str(exc)
168
+
169
+ return {
170
+ "classification": "PLAYGROUND_CUDA_DIAGNOSTICS_STATUS",
171
+ "scientific_evidence": False,
172
+ "canonical_cuda_backend": False,
173
+ "target_default": "sm_86",
174
+ "ptxas_available": ptxas_path is not None,
175
+ "ptxas_path": ptxas_path,
176
+ "cuda_driver_available": driver_available,
177
+ "cuda_driver_error": driver_error,
178
+ "stages": {
179
+ "CUDA-1.0": "PTXAS_LOAD_PREFLIGHT_IMPLEMENTED",
180
+ "CUDA-1.1": "DETERMINISM_FREEZE_PARITY_CONTRACT_IMPLEMENTED",
181
+ "CUDA-1.2": "LAUNCH_ABI_BUFFER_VALIDATION_IMPLEMENTED",
182
+ "CUDA-1.3": "CPU_GATE_REFERENCE_D2_IMPLEMENTED_HARDWARE_VERIFICATION_REQUIRED",
183
+ "CUDA-1.4": "BOUNDED_RECURRENT_FP64_REFERENCE_IMPLEMENTED",
184
+ "CUDA-1.5": "BOUNDED_FROZEN_REWARD_PLASTICITY_REFERENCE_IMPLEMENTED",
185
+ "CUDA-1.6": "HYBRID_BUILDER_MEMBRANE_AND_OPTIONAL_PAN_STATE_GPU_BODY_CPU_IMPLEMENTED",
186
+ },
187
+ "application_cpu": {
188
+ "sandbox_physics": True,
189
+ "sensorics": True,
190
+ "actuation": True,
191
+ "posture_analysis": True,
192
+ "reward_triggers": True,
193
+ "closed_loop": True,
194
+ },
195
+ "gpu_porting": {
196
+ "gate_single_tick": True,
197
+ "membrane_state_100_ticks": True,
198
+ "adaptation_state": True,
199
+ "pan_health_energy_hyperstate": "OPTIONAL_CUDA_PAN_BUILDER",
200
+ "pan_apoptosis_decision": "OPTIONAL_CUDA_PAN_BUILDER",
201
+ "pan_feedback_projection": "OPTIONAL_CUDA_PAN_BUILDER",
202
+ "refractory_state": False,
203
+ "synapses": "BUILDER_GPU_EMISSION_PLASTICITY_REWARD_RESIDENT_QUEUE_HOST_TRACE",
204
+ "builder_rng_semantics": "HOST_TRAVERSAL_STREAM_PRESERVED",
205
+ "builder_inhibitory_emission": True,
206
+ "delays": True,
207
+ "builder_delay_queue": "DEVICE_RESIDENT_RING_65",
208
+ "builder_neuron_traces": "DEVICE_RESIDENT_TWO_PHASE",
209
+ "builder_pan_population": "CUDA_ORDERED_AXIS_REDUCTION",
210
+ "plasticity": "BUILDER_PAIR_TRIPLET_ELIGIBILITY_MODULATION_AND_BOUNDED_REFERENCE",
211
+ "sandbox_physics": False,
212
+ },
213
+ "verification": {
214
+ "cpu_gate_reference": True,
215
+ "single_tick_d2_endpoint": True,
216
+ "rng_action_path_endpoint": True,
217
+ "raw_rng_value_parity": True,
218
+ "memory_leak_instrumentation": True,
219
+ },
220
+ }
221
+
222
+
223
+ def _diagnostic_inputs(
224
+ bundle: CompileBundle,
225
+ *,
226
+ n_neurons: int,
227
+ seed: int,
228
+ epsilon: float,
229
+ ) -> GateLaunchInputs:
230
+ manifest = bundle.manifest
231
+ raw_abi = manifest.get("kernel_abi")
232
+ if not isinstance(raw_abi, Mapping):
233
+ raise ValueError("compile bundle is missing kernel_abi")
234
+ abi = cast(Mapping[str, object], raw_abi)
235
+ input_channels = _payload_int(abi, "input_channels", 8)
236
+ action_count = _payload_int(abi, "action_space_size", 4)
237
+ pan_dimensions = _payload_int(abi, "pan_dimensions", 5)
238
+ full_mask = (1 << input_channels) - 1 if input_channels < 64 else 0xFFFFFFFFFFFFFFFF
239
+ logits: list[float] = []
240
+ for neuron in range(n_neurons):
241
+ preferred = neuron % action_count
242
+ for action in range(action_count):
243
+ logits.append(1.0 if action == preferred else 0.05 * (action + 1))
244
+ return GateLaunchInputs.from_sequences(
245
+ input_current=[1.0 + 0.125 * (index % 7) for index in range(n_neurons)],
246
+ channel_masks=[full_mask] * n_neurons,
247
+ amplitudes=[1.0 + 0.05 * channel for channel in range(input_channels)],
248
+ reward_ring=[0.5, -0.25, 0.75, 0.0],
249
+ action_map=[
250
+ 0.1 * (action + 1) * (channel + 1)
251
+ for action in range(action_count)
252
+ for channel in range(input_channels)
253
+ ],
254
+ feedback_matrix=[
255
+ 0.01 * (1 + ((neuron + dimension) % 5))
256
+ for neuron in range(n_neurons)
257
+ for dimension in range(pan_dimensions)
258
+ ],
259
+ population=[0.1 * (dimension + 1) for dimension in range(pan_dimensions)],
260
+ logits=logits,
261
+ tick=3,
262
+ target_index=min(2, action_count - 1),
263
+ previous_action=min(1, action_count - 1),
264
+ seed=seed,
265
+ epsilon=epsilon,
266
+ )
267
+
268
+
269
+ def _cuda_config_payload(payload: Mapping[str, object]) -> dict[str, object]:
270
+ excluded = {
271
+ "target_sm",
272
+ "ptx_version",
273
+ "n_neurons",
274
+ "block_size",
275
+ "device_ordinal",
276
+ "reference_commit",
277
+ "rng_samples",
278
+ }
279
+ return {key: value for key, value in payload.items() if key not in excluded}
280
+
281
+
282
+ def _cuda_preflight(payload: Mapping[str, object]) -> dict[str, object]:
283
+ target_sm = _payload_text(payload, "target_sm", "sm_86")
284
+ ptx_version = _payload_text(payload, "ptx_version", "7.1")
285
+ n_neurons = _payload_int(payload, "n_neurons", 256)
286
+ block_size = validate_cuda_block_size(_payload_int(payload, "block_size", 64))
287
+ device_ordinal = _payload_int(payload, "device_ordinal", 0)
288
+ if not 1 <= n_neurons <= 12_288:
289
+ raise ValueError("n_neurons must be in [1, 12288]")
290
+ bundle = compile_mapping(
291
+ _cuda_config_payload(payload),
292
+ target_sm=target_sm,
293
+ ptx_version=ptx_version,
294
+ )
295
+ return preflight_bundle(
296
+ bundle,
297
+ n_neurons=n_neurons,
298
+ block_size=block_size,
299
+ device_ordinal=device_ordinal,
300
+ )
301
+
302
+
303
+ def _cuda_smoke(payload: Mapping[str, object]) -> dict[str, object]:
304
+ target_sm = _payload_text(payload, "target_sm", "sm_86")
305
+ ptx_version = _payload_text(payload, "ptx_version", "7.1")
306
+ n_neurons = _payload_int(payload, "n_neurons", 64)
307
+ block_size = validate_cuda_block_size(_payload_int(payload, "block_size", 64))
308
+ device_ordinal = _payload_int(payload, "device_ordinal", 0)
309
+ reference_commit = _payload_text(payload, "reference_commit", "")
310
+ if not 1 <= n_neurons <= 4096:
311
+ raise ValueError("n_neurons must be in [1, 4096] for hardware smoke")
312
+ bundle = compile_mapping(
313
+ _cuda_config_payload(payload),
314
+ target_sm=target_sm,
315
+ ptx_version=ptx_version,
316
+ )
317
+ inputs = _diagnostic_inputs(
318
+ bundle,
319
+ n_neurons=n_neurons,
320
+ seed=12345,
321
+ epsilon=0.0,
322
+ )
323
+ reference = cpu_gate_reference(bundle, inputs)
324
+ first = execute_gate_bundle(
325
+ bundle,
326
+ inputs,
327
+ block_size=block_size,
328
+ device_ordinal=device_ordinal,
329
+ )
330
+ second = execute_gate_bundle(
331
+ bundle,
332
+ inputs,
333
+ block_size=block_size,
334
+ device_ordinal=device_ordinal,
335
+ )
336
+ first_parity = gate_execution_parity_summary(
337
+ reference,
338
+ first,
339
+ tolerance=1.0e-5,
340
+ reference_commit=reference_commit,
341
+ )
342
+ second_parity = gate_execution_parity_summary(
343
+ reference,
344
+ second,
345
+ tolerance=1.0e-5,
346
+ reference_commit=reference_commit,
347
+ )
348
+ deterministic = first.get("outputs") == second.get("outputs")
349
+ return {
350
+ "classification": "PLAYGROUND_CUDA1_3_HARDWARE_SMOKE",
351
+ "scientific_evidence": False,
352
+ "canonical_cuda_backend": False,
353
+ "reference": reference,
354
+ "first": first,
355
+ "second": second,
356
+ "parity": first_parity,
357
+ "second_parity": second_parity,
358
+ "gpu_repeat_exact": deterministic,
359
+ "passed": bool(first_parity["passed"])
360
+ and bool(second_parity["passed"])
361
+ and deterministic,
362
+ "cleanup_contract": {
363
+ "device_allocations_released_in_finally": True,
364
+ "driver_module_unloaded": True,
365
+ "memory_leak_instrumented": True,
366
+ "first_memory": first.get("memory"),
367
+ "second_memory": second.get("memory"),
368
+ },
369
+ }
370
+
371
+
372
+ def _cuda_rng_parity(payload: Mapping[str, object]) -> dict[str, object]:
373
+ target_sm = _payload_text(payload, "target_sm", "sm_86")
374
+ ptx_version = _payload_text(payload, "ptx_version", "7.1")
375
+ samples = _payload_int(payload, "rng_samples", 1000)
376
+ block_size = validate_cuda_block_size(_payload_int(payload, "block_size", 64))
377
+ device_ordinal = _payload_int(payload, "device_ordinal", 0)
378
+ if not 1 <= samples <= 4096:
379
+ raise ValueError("rng_samples must be in [1, 4096]")
380
+ bundle = compile_mapping(
381
+ _cuda_config_payload(payload),
382
+ target_sm=target_sm,
383
+ ptx_version=ptx_version,
384
+ )
385
+ inputs = _diagnostic_inputs(
386
+ bundle,
387
+ n_neurons=samples,
388
+ seed=12345,
389
+ epsilon=1.0,
390
+ )
391
+ reference = cpu_gate_reference(bundle, inputs)
392
+ candidate = execute_gate_bundle(
393
+ bundle,
394
+ inputs,
395
+ block_size=block_size,
396
+ device_ordinal=device_ordinal,
397
+ )
398
+ reference_outputs = cast(Mapping[str, object], reference["outputs"])
399
+ candidate_outputs = cast(Mapping[str, object], candidate["outputs"])
400
+ reference_actions = cast(list[int], reference_outputs["action"])
401
+ candidate_actions = cast(list[int], candidate_outputs["action"])
402
+ actions_exact = reference_actions == candidate_actions
403
+ return {
404
+ "classification": "PLAYGROUND_CUDA_RNG_ACTION_PATH_PARITY",
405
+ "scientific_evidence": False,
406
+ "samples": samples,
407
+ "seed": 12345,
408
+ "tick": inputs.tick,
409
+ "epsilon": 1.0,
410
+ "actions_exact": actions_exact,
411
+ "passed": actions_exact,
412
+ "first_reference_actions": reference_actions[:20],
413
+ "first_cuda_actions": candidate_actions[:20],
414
+ "scope": "HASH_EPSILON_GREEDY_ACTION_PATH_UINT24_TO_F32_HALF_OPEN",
415
+ }
416
+
417
+
418
+ def _cuda_recurrent_parity(payload: Mapping[str, object]) -> dict[str, object]:
419
+ block_size = validate_cuda_block_size(_payload_int(payload, "block_size", 64))
420
+ inputs = recurrent_fixture(
421
+ n_neurons=_payload_int(payload, "n_neurons", 129),
422
+ ticks=_payload_int(payload, "ticks", 100),
423
+ model=_payload_text(payload, "model", "pan_adex_5d"),
424
+ )
425
+ plasticity = payload.get("plasticity", False)
426
+ if not isinstance(plasticity, bool):
427
+ raise ValueError("plasticity must be boolean")
428
+ if plasticity:
429
+ inputs = replace(
430
+ inputs,
431
+ synapses=SynapseConfig(),
432
+ rewards=tuple(
433
+ 1.0 if tick % 32 == 0 else 0.0 for tick in range(inputs.ticks)
434
+ ),
435
+ )
436
+ reference = cpu_recurrent_reference(inputs)
437
+ candidate = execute_recurrent(
438
+ inputs,
439
+ block_size=block_size,
440
+ target_sm=_payload_text(payload, "target_sm", "sm_86"),
441
+ )
442
+ return {
443
+ "classification": (
444
+ "PLAYGROUND_CUDA15_PLASTICITY_PARITY"
445
+ if plasticity
446
+ else "PLAYGROUND_CUDA14_RECURRENT_PARITY"
447
+ ),
448
+ "scientific_evidence": False,
449
+ "model": inputs.model,
450
+ "ticks": inputs.ticks,
451
+ "state_dtype": candidate["state_dtype"],
452
+ "preflight": candidate["preflight"],
453
+ "parity": recurrent_parity(
454
+ reference, cast(dict[str, object], candidate["outputs"])
455
+ ),
456
+ "scope": candidate["comparison_scope"],
457
+ "full_pan_backend": False,
458
+ }
459
+
460
+
461
  def get_playground(path: str) -> dict[str, object] | None:
462
  if path == "/api/playground/catalog":
463
  return service.catalog()
464
+ if path == "/api/playground/integration":
465
+ return integration_catalog()
466
  if path == "/api/playground/sessions":
467
  return service.sessions()
468
  if path.startswith("/api/playground/sessions/"):
 
470
  return service.replay(session_id)
471
  if path == "/api/playground/night":
472
  return _NIGHT_RUN.status()
473
+ if path == "/api/playground/cuda/status":
474
+ return _cuda_runtime_status()
475
  if path == "/api/playground/live":
476
  return {
477
  "class": "PLAYGROUND_LIVE_SESSIONS",
 
490
  if path in {"/api/playground/run", "/api/playground/robustness"}:
491
  return _bounded_run(path, payload)
492
 
493
+ if path == "/api/playground/integration/transfer":
494
+ return transfer_element(payload)
495
+
496
+ if path == "/api/playground/research/synaptic-transfer":
497
+ return _bounded_operation(lambda: run_synaptic_transfer(payload))
498
+
499
+ if path == "/api/playground/research/cue-controls":
500
+ return _bounded_operation(lambda: run_cue_controls(payload))
501
+
502
  if path == "/api/playground/determinism":
503
  return service.determinism(payload)
504
 
 
514
  ptx_version=ptx_version,
515
  ).to_mapping()
516
 
517
+ if path == "/api/playground/cuda/preflight":
518
+ return _bounded_operation(lambda: _cuda_preflight(payload))
519
+
520
+ if path == "/api/playground/cuda/smoke":
521
+ return _bounded_operation(lambda: _cuda_smoke(payload))
522
+
523
+ if path == "/api/playground/cuda/rng-parity":
524
+ return _bounded_operation(lambda: _cuda_rng_parity(payload))
525
+
526
+ if path == "/api/playground/cuda/builder-parity":
527
+ return _bounded_operation(lambda: run_builder_parity(payload))
528
+
529
+ if path == "/api/playground/cuda/recurrent-parity":
530
+ return _bounded_operation(lambda: _cuda_recurrent_parity(payload))
531
+
532
  if path == "/api/playground/night/start":
533
  return _NIGHT_RUN.start(
534
  hours=_payload_float(payload, "hours", 8.0),
 
570
  session = _LIVE_DAEMON.get(session_id)
571
  if action == "step":
572
  ticks = _payload_int(payload, "ticks", 32)
573
+ include_topology = payload.get("include_topology", True) is not False
574
+ return {
575
+ "session_id": session_id,
576
+ **session.step(ticks, include_topology=include_topology),
577
+ }
578
  if action == "input":
579
  values = _numeric_list(payload.get("values", []), "values")
580
  duration = _payload_int(payload, "duration_ticks", 16)
src/dashboard/server.py CHANGED
@@ -93,6 +93,11 @@ from .development_timeline import build_development_timeline
93
  from .docs_source import DocumentationSource, create_docs_source
94
  from .embedding_jobs import EmbeddingJobError, list_embedding_jobs, run_embedding_job
95
  from .experiment_archive import ExperimentArchiveError, ExperimentArchiveService
 
 
 
 
 
96
  from .experiment_organizer import ExperimentOrganizerService
97
  from .experiment_workflow import (
98
  ExperimentWorkflowService,
@@ -206,6 +211,9 @@ class DashboardServer(ThreadingHTTPServer):
206
 
207
  allow_reuse_address = True
208
  daemon_threads = True
 
 
 
209
 
210
  def __init__(
211
  self,
@@ -683,6 +691,11 @@ class DashboardRequestHandler(BaseHTTPRequestHandler):
683
  if path == "/api/research/experiments/archive":
684
  self._serve_archived_experiments()
685
  return
 
 
 
 
 
686
  if path == "/api/research/experiment-series":
687
  self._serve_experiment_series()
688
  return
@@ -727,6 +740,10 @@ class DashboardRequestHandler(BaseHTTPRequestHandler):
727
  # Integration Status (real backend data, Phase 14)
728
  # ----------------------------------------------------------------
729
 
 
 
 
 
730
  if path == "/api/integration/status":
731
  self._serve_integration_status()
732
  return
@@ -1058,6 +1075,9 @@ class DashboardRequestHandler(BaseHTTPRequestHandler):
1058
  if path == "/api/research/experiments/archive":
1059
  self._archive_experiment(body)
1060
  return
 
 
 
1061
  if path == "/api/research/analysis-jobs":
1062
  self._run_analysis_job(body)
1063
  return
@@ -2165,6 +2185,33 @@ class DashboardRequestHandler(BaseHTTPRequestHandler):
2165
 
2166
  self._send_api_not_found(path)
2167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2168
  # ========================================================================
2169
  # Integration Status (real backend data, Phase 14)
2170
  # ========================================================================
@@ -4361,7 +4408,7 @@ class DashboardRequestHandler(BaseHTTPRequestHandler):
4361
  tenant = os.environ.get("BRAIN5D_MICROSOFT_TENANT", "common").strip()
4362
  redirect_uri = os.environ.get(
4363
  "BRAIN5D_MICROSOFT_REDIRECT_URI",
4364
- "http://127.0.0.1:8765/api/research/chat/oauth/callback",
4365
  ).strip()
4366
  if not client_id:
4367
  self._send_json(
@@ -4570,6 +4617,32 @@ class DashboardRequestHandler(BaseHTTPRequestHandler):
4570
  {"experiments": cast(list[JSONValue], source.list_experiments())}
4571
  )
4572
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4573
  def _write_artifact_review(self, body: dict[str, Any]) -> None:
4574
  source = self._require_research_source()
4575
  artifact_path = body.get("artifact_path")
@@ -6262,7 +6335,7 @@ def main() -> None:
6262
  parser.add_argument(
6263
  "--port",
6264
  type=int,
6265
- default=8765,
6266
  )
6267
 
6268
  parser.add_argument(
 
93
  from .docs_source import DocumentationSource, create_docs_source
94
  from .embedding_jobs import EmbeddingJobError, list_embedding_jobs, run_embedding_job
95
  from .experiment_archive import ExperimentArchiveError, ExperimentArchiveService
96
+ from .experiment_evaluation import (
97
+ ExperimentEvaluationError,
98
+ read_experiment_evaluation,
99
+ write_experiment_evaluation,
100
+ )
101
  from .experiment_organizer import ExperimentOrganizerService
102
  from .experiment_workflow import (
103
  ExperimentWorkflowService,
 
211
 
212
  allow_reuse_address = True
213
  daemon_threads = True
214
+ # A browser opens bursts of parallel ES-module and asset connections.
215
+ # The stdlib's older backlog of five rejects some on Windows.
216
+ request_queue_size = 64
217
 
218
  def __init__(
219
  self,
 
691
  if path == "/api/research/experiments/archive":
692
  self._serve_archived_experiments()
693
  return
694
+ if path.startswith("/api/research/experiments/") and path.endswith(
695
+ "/evaluation"
696
+ ):
697
+ self._serve_experiment_evaluation(path)
698
+ return
699
  if path == "/api/research/experiment-series":
700
  self._serve_experiment_series()
701
  return
 
740
  # Integration Status (real backend data, Phase 14)
741
  # ----------------------------------------------------------------
742
 
743
+ if path == "/api/settings/network":
744
+ self._serve_network_settings()
745
+ return
746
+
747
  if path == "/api/integration/status":
748
  self._serve_integration_status()
749
  return
 
1075
  if path == "/api/research/experiments/archive":
1076
  self._archive_experiment(body)
1077
  return
1078
+ if path == "/api/research/experiments/evaluation":
1079
+ self._write_experiment_evaluation(body)
1080
+ return
1081
  if path == "/api/research/analysis-jobs":
1082
  self._run_analysis_job(body)
1083
  return
 
2185
 
2186
  self._send_api_not_found(path)
2187
 
2188
+ # ========================================================================
2189
+ # Network Settings (central config)
2190
+ # ========================================================================
2191
+
2192
+ def _serve_network_settings(self) -> None:
2193
+ from .network_settings import (
2194
+ ALLOW_PUBLIC_DEPLOYMENT,
2195
+ DASHBOARD_HOST,
2196
+ DASHBOARD_PORT,
2197
+ HF_SPACE_HOST,
2198
+ HF_SPACE_PORT,
2199
+ PUBLIC_DEPLOYMENT_NOTE,
2200
+ )
2201
+
2202
+ self._send_json(
2203
+ {
2204
+ "dashboard_host": DASHBOARD_HOST,
2205
+ "dashboard_port": DASHBOARD_PORT,
2206
+ "local_url": f"http://127.0.0.1:{DASHBOARD_PORT}",
2207
+ "hf_space_host": HF_SPACE_HOST,
2208
+ "hf_space_port": HF_SPACE_PORT,
2209
+ "allow_public_deployment": ALLOW_PUBLIC_DEPLOYMENT,
2210
+ "public_deployment_note": PUBLIC_DEPLOYMENT_NOTE,
2211
+ "source": "src/dashboard/network_settings.py",
2212
+ }
2213
+ )
2214
+
2215
  # ========================================================================
2216
  # Integration Status (real backend data, Phase 14)
2217
  # ========================================================================
 
4408
  tenant = os.environ.get("BRAIN5D_MICROSOFT_TENANT", "common").strip()
4409
  redirect_uri = os.environ.get(
4410
  "BRAIN5D_MICROSOFT_REDIRECT_URI",
4411
+ "http://127.0.0.1:8767/api/research/chat/oauth/callback",
4412
  ).strip()
4413
  if not client_id:
4414
  self._send_json(
 
4617
  {"experiments": cast(list[JSONValue], source.list_experiments())}
4618
  )
4619
 
4620
+ def _serve_experiment_evaluation(self, path: str) -> None:
4621
+ prefix = "/api/research/experiments/"
4622
+ experiment_id = unquote(path[len(prefix) : -len("/evaluation")]).strip("/")
4623
+ try:
4624
+ source = self._require_research_source()
4625
+ self._send_json(
4626
+ cast(
4627
+ Mapping[str, JSONValue],
4628
+ read_experiment_evaluation(source.root(), experiment_id),
4629
+ )
4630
+ )
4631
+ except ExperimentEvaluationError as exc:
4632
+ self._send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
4633
+
4634
+ def _write_experiment_evaluation(self, body: dict[str, Any]) -> None:
4635
+ source = self._require_research_source()
4636
+ try:
4637
+ result = write_experiment_evaluation(source.root(), body)
4638
+ except ExperimentEvaluationError as exc:
4639
+ self._send_json({"error": str(exc)}, HTTPStatus.BAD_REQUEST)
4640
+ return
4641
+ self._send_json(
4642
+ cast(Mapping[str, JSONValue], result),
4643
+ HTTPStatus.CREATED,
4644
+ )
4645
+
4646
  def _write_artifact_review(self, body: dict[str, Any]) -> None:
4647
  source = self._require_research_source()
4648
  artifact_path = body.get("artifact_path")
 
6335
  parser.add_argument(
6336
  "--port",
6337
  type=int,
6338
+ default=8767,
6339
  )
6340
 
6341
  parser.add_argument(
src/dashboard/static/experiment-workflow-base.js CHANGED
@@ -613,7 +613,9 @@ export class ExperimentWorkflowPanel {
613
  if (artifacts.calibration) resultButtons.push(`<button type="button" class="btn-small exp-open-btn" data-experiment-open="calibration" data-experiment-id="${expId}" data-artifact-path="${escapeHtml(artifacts.calibration)}" title="Kalibrierungsdaten">⚙ Kalibrierung</button>`);
614
  if (artifacts.manifest) resultButtons.push(`<button type="button" class="btn-small exp-open-btn" data-experiment-open="manifest" data-experiment-id="${expId}" data-artifact-path="${escapeHtml(artifacts.manifest)}" title="Manifest und Provenienz">🧾 Manifest</button>`);
615
  if (artifacts.review) resultButtons.push(`<button type="button" class="btn-small exp-open-btn" data-experiment-open="review" data-experiment-id="${expId}" data-artifact-path="${escapeHtml(artifacts.review)}" title="Review und wissenschaftliche Weiterverarbeitung">✓ Review</button>`);
616
- const resultActions = resultButtons.length ? `<div class="experiment-library-results">${resultButtons.join("")}</div>` : "";
 
 
617
  const action = archived
618
  ? `<button type="button" class="btn-small" data-experiment-action="restore" data-experiment-id="${expId}">↶ Wiederherstellen</button>`
619
  : `<button type="button" class="btn-small" data-experiment-action="archive" data-experiment-id="${expId}">▣ Archivieren</button>`;
@@ -685,6 +687,124 @@ export class ExperimentWorkflowPanel {
685
  return `<details class="experiment-series-item ${item.archived ? "is-archived" : ""}"><summary><span><strong>${escapeHtml(item.series_id)}</strong><small>${escapeHtml(item.created_at || "")}</small></span><em class="series-status-${escapeHtml(item.status)}">${escapeHtml(item.status)} · ${escapeHtml(item.assessment_status)}${archiveLabel}</em></summary><div class="experiment-series-assessment"><div class="experiment-series-kpis"><span><small>Erfolgreich</small><strong>${Number(item.completed || 0)}</strong></span><span><small>Fehlgeschlagen</small><strong>${Number(item.failed || 0)}</strong></span><span><small>Ticks</small><strong>${escapeHtml(item.requested_ticks ?? "—")}</strong></span><span><small>Seeds</small><strong>${escapeHtml(item.seeds ?? "—")}</strong></span></div><p>${escapeHtml(item.assessment_boundary || "Technische Bewertung; Human Review erforderlich.")}</p><ul class="experiment-series-results">${resultRows}</ul><div class="experiment-series-actions">${reportButton}${archiveAction}</div></div></details>`;
686
  }
687
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
688
  async _handleExperimentLibraryAction(event) {
689
  const button = event.target.closest?.("[data-experiment-action], [data-series-action], [data-series-report], [data-experiment-open], [data-experiment-toggle], [data-experiment-use]");
690
  if (!button) return;
@@ -713,6 +833,12 @@ export class ExperimentWorkflowPanel {
713
  const seriesId = button.dataset.seriesId;
714
  const seriesReport = button.dataset.seriesReport;
715
  const openKind = button.dataset.experimentOpen;
 
 
 
 
 
 
716
  if (seriesReport) {
717
  await this._openArtifact(seriesReport);
718
  return;
@@ -1006,6 +1132,7 @@ export class ExperimentWorkflowPanel {
1006
  if (artifacts.summary) buttons.push({ kind: "summary", label: "Summary", path: artifacts.summary });
1007
  if (artifacts.statistics) buttons.push({ kind: "statistics", label: "Statistics", path: artifacts.statistics });
1008
  if (artifacts.data_index || artifacts.raw_run_index) buttons.push({ kind: "raw", label: "Raw Index", path: artifacts.data_index || artifacts.raw_run_index });
 
1009
  if (!buttons.length) return;
1010
  const group = document.createElement("span");
1011
  group.dataset.experimentPopupActions = "true";
 
613
  if (artifacts.calibration) resultButtons.push(`<button type="button" class="btn-small exp-open-btn" data-experiment-open="calibration" data-experiment-id="${expId}" data-artifact-path="${escapeHtml(artifacts.calibration)}" title="Kalibrierungsdaten">⚙ Kalibrierung</button>`);
614
  if (artifacts.manifest) resultButtons.push(`<button type="button" class="btn-small exp-open-btn" data-experiment-open="manifest" data-experiment-id="${expId}" data-artifact-path="${escapeHtml(artifacts.manifest)}" title="Manifest und Provenienz">🧾 Manifest</button>`);
615
  if (artifacts.review) resultButtons.push(`<button type="button" class="btn-small exp-open-btn" data-experiment-open="review" data-experiment-id="${expId}" data-artifact-path="${escapeHtml(artifacts.review)}" title="Review und wissenschaftliche Weiterverarbeitung">✓ Review</button>`);
616
+ if (artifacts.posthoc_evaluation) resultButtons.push(`<button type="button" class="btn-small exp-open-btn" data-experiment-open="posthoc" data-experiment-id="${expId}" data-artifact-path="${escapeHtml(artifacts.posthoc_evaluation)}" title="Gespeicherte wissenschaftliche Auswertung">📝 Auswertung</button>`);
617
+ const evaluationAction = `<button type="button" class="btn-small exp-evaluate-btn" data-experiment-evaluate="${expId}" title="Nachträgliche wissenschaftliche Auswertung erfassen">✎ Auswertung erfassen</button>`;
618
+ const resultActions = `<div class="experiment-library-results">${resultButtons.join("")}${evaluationAction}</div>`;
619
  const action = archived
620
  ? `<button type="button" class="btn-small" data-experiment-action="restore" data-experiment-id="${expId}">↶ Wiederherstellen</button>`
621
  : `<button type="button" class="btn-small" data-experiment-action="archive" data-experiment-id="${expId}">▣ Archivieren</button>`;
 
687
  return `<details class="experiment-series-item ${item.archived ? "is-archived" : ""}"><summary><span><strong>${escapeHtml(item.series_id)}</strong><small>${escapeHtml(item.created_at || "")}</small></span><em class="series-status-${escapeHtml(item.status)}">${escapeHtml(item.status)} · ${escapeHtml(item.assessment_status)}${archiveLabel}</em></summary><div class="experiment-series-assessment"><div class="experiment-series-kpis"><span><small>Erfolgreich</small><strong>${Number(item.completed || 0)}</strong></span><span><small>Fehlgeschlagen</small><strong>${Number(item.failed || 0)}</strong></span><span><small>Ticks</small><strong>${escapeHtml(item.requested_ticks ?? "—")}</strong></span><span><small>Seeds</small><strong>${escapeHtml(item.seeds ?? "—")}</strong></span></div><p>${escapeHtml(item.assessment_boundary || "Technische Bewertung; Human Review erforderlich.")}</p><ul class="experiment-series-results">${resultRows}</ul><div class="experiment-series-actions">${reportButton}${archiveAction}</div></div></details>`;
688
  }
689
 
690
+ _ensurePosthocEvaluationDialog() {
691
+ let dialog = byId("workflow-posthoc-evaluation-dialog");
692
+ if (dialog) return dialog;
693
+ dialog = document.createElement("dialog");
694
+ dialog.id = "workflow-posthoc-evaluation-dialog";
695
+ dialog.className = "posthoc-evaluation-dialog";
696
+ dialog.innerHTML = `
697
+ <header class="posthoc-evaluation-header"><div><span class="workspace-kicker">POST-HOC INTERPRETATION</span><h3>Wissenschaftliche Auswertung</h3><p id="posthoc-evaluation-context"></p></div><button type="button" id="posthoc-evaluation-close" class="icon-btn" aria-label="Auswertung schließen">×</button></header>
698
+ <div class="posthoc-evaluation-boundary"><strong>Nachgelagerte Auswertung</strong><span>Diese Antwort verändert weder den Lauf noch den EVID-Status. Sie wird als Interpretation gespeichert und benötigt für wissenschaftliche Verwendung einen separaten Human-Review-/EVID-Workflow.</span></div>
699
+ <div class="posthoc-evaluation-form">
700
+ <label>Auswertende Person<input id="posthoc-evaluation-evaluator" type="text" required placeholder="Name / Verantwortliche Person"></label>
701
+ <label>Antwort auf die Hypothese<select id="posthoc-evaluation-answer"><option value="supported">unterstützt</option><option value="refuted">widerlegt</option><option value="mixed">gemischt / teilweise</option><option value="inconclusive">nicht entscheidbar</option><option value="not_assessed">noch nicht bewertet</option></select></label>
702
+ <label class="posthoc-evaluation-wide">Wissenschaftliche Auswertung<textarea id="posthoc-evaluation-text" rows="8" required placeholder="Welche Auswertung beantwortet die Hypothese? Beziehe dich auf registrierte Outcomes, Kontrollen, Seeds und die beobachteten Daten."></textarea></label>
703
+ <label class="posthoc-evaluation-wide">Beobachtungen und Auswertungskriterien<textarea id="posthoc-evaluation-observations" rows="5" placeholder="Welche Messwerte, Vergleiche oder Artefakte tragen die Antwort?"></textarea></label>
704
+ <label class="posthoc-evaluation-wide">Limitationen und Alternativerklärungen<textarea id="posthoc-evaluation-limitations" rows="5" placeholder="Welche Unsicherheiten, fehlenden Kontrollen oder alternativen Erklärungen bleiben offen?"></textarea></label>
705
+ <label class="posthoc-evaluation-wide">Nächste Schritte für die wissenschaftliche Arbeit<textarea id="posthoc-evaluation-follow-up" rows="5" placeholder="Welche Replikation, Kontrolle, Abbildung oder Textpassage soll daraus entstehen?"></textarea></label>
706
+ </div>
707
+ <footer class="posthoc-evaluation-actions"><span id="posthoc-evaluation-status" role="status"></span><button type="button" id="posthoc-evaluation-open" class="btn-secondary" disabled>Im File Viewer öffnen</button><button type="button" id="posthoc-evaluation-save" class="btn-primary">Auswertung speichern</button></footer>`;
708
+ document.body.append(dialog);
709
+ if (!document.getElementById("posthoc-evaluation-style")) {
710
+ const style = document.createElement("style");
711
+ style.id = "posthoc-evaluation-style";
712
+ style.textContent = `.posthoc-evaluation-dialog{width:min(94vw,880px);max-height:92vh;padding:0;border:1px solid var(--rule-3);border-radius:var(--r-md);background:var(--paper);color:var(--ink);box-shadow:var(--shadow-float)}.posthoc-evaluation-dialog::backdrop{background:rgba(20,16,12,.58);backdrop-filter:blur(4px)}.posthoc-evaluation-header{display:flex;justify-content:space-between;gap:12px;padding:14px 16px;border-bottom:1px solid var(--rule);background:var(--paper-2)}.posthoc-evaluation-header h3{margin:3px 0;font-size:1.05rem}.posthoc-evaluation-header p{margin:0;color:var(--ink-3);font-size:.7rem}.posthoc-evaluation-boundary{margin:12px 16px;padding:9px 11px;border-left:3px solid var(--amber);background:var(--amber-wash);font-size:.7rem;line-height:1.45}.posthoc-evaluation-boundary strong{display:block}.posthoc-evaluation-form{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;padding:0 16px 12px;overflow:auto}.posthoc-evaluation-form label{display:grid;gap:5px;color:var(--ink-3);font-size:.68rem}.posthoc-evaluation-form input,.posthoc-evaluation-form select,.posthoc-evaluation-form textarea{width:100%;padding:7px;border:1px solid var(--rule-2);border-radius:var(--r-xs);background:var(--paper-2);color:var(--ink);font:inherit}.posthoc-evaluation-form textarea{resize:vertical;line-height:1.45}.posthoc-evaluation-wide{grid-column:1/-1}.posthoc-evaluation-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:10px 16px;border-top:1px solid var(--rule);background:var(--paper-2)}.posthoc-evaluation-actions span{margin-right:auto;color:var(--ink-3);font-size:.68rem}.posthoc-evaluation-actions button:disabled{opacity:.55}@media(max-width:700px){.posthoc-evaluation-form{grid-template-columns:1fr}.posthoc-evaluation-wide{grid-column:auto}.posthoc-evaluation-actions{flex-wrap:wrap}.posthoc-evaluation-actions span{flex-basis:100%}}`;
713
+ document.head.appendChild(style);
714
+ }
715
+ dialog.querySelector("#posthoc-evaluation-close")?.addEventListener("click", () => dialog.close());
716
+ dialog.querySelector("#posthoc-evaluation-save")?.addEventListener("click", () => this._savePosthocEvaluation());
717
+ dialog.querySelector("#posthoc-evaluation-open")?.addEventListener("click", () => {
718
+ const path = dialog.dataset.markdownPath;
719
+ if (path) this._openArtifact(path);
720
+ });
721
+ return dialog;
722
+ }
723
+
724
+ async _openPosthocEvaluation(item) {
725
+ const dialog = this._ensurePosthocEvaluationDialog();
726
+ const manifest = item.manifest && typeof item.manifest === "object" ? item.manifest : {};
727
+ const experimentId = String(item.experiment_id || item.id || "");
728
+ const questions = Array.isArray(manifest.research_questions) ? manifest.research_questions : [];
729
+ const hypotheses = Array.isArray(manifest.hypotheses) ? manifest.hypotheses : [];
730
+ const researchQuestion = questions.join(", ") || manifest.research_question || "";
731
+ const hypothesis = hypotheses.join(", ") || manifest.hypothesis || "";
732
+ dialog.dataset.experimentId = experimentId;
733
+ dialog.dataset.researchQuestion = String(researchQuestion);
734
+ dialog.dataset.hypothesis = String(hypothesis);
735
+ dialog.dataset.markdownPath = "";
736
+ const set = (id, value) => { const node = byId(id); if (node) node.value = String(value ?? ""); };
737
+ const context = byId("posthoc-evaluation-context");
738
+ if (context) context.textContent = `${experimentId} · ${researchQuestion || "Forschungsfrage nicht hinterlegt"} · ${hypothesis || "Hypothese nicht hinterlegt"}`;
739
+ set("posthoc-evaluation-evaluator", "");
740
+ set("posthoc-evaluation-answer", "not_assessed");
741
+ set("posthoc-evaluation-text", "");
742
+ set("posthoc-evaluation-observations", "");
743
+ set("posthoc-evaluation-limitations", "");
744
+ set("posthoc-evaluation-follow-up", "");
745
+ set("posthoc-evaluation-status", "Lade vorhandene Auswertung …");
746
+ const openButton = byId("posthoc-evaluation-open");
747
+ if (openButton) openButton.disabled = true;
748
+ if (!dialog.open) dialog.showModal();
749
+ try {
750
+ const saved = await fetchJson(`/api/research/experiments/${encodeURIComponent(experimentId)}/evaluation`);
751
+ if (saved.exists) {
752
+ set("posthoc-evaluation-evaluator", saved.evaluator);
753
+ set("posthoc-evaluation-answer", saved.hypothesis_answer || "not_assessed");
754
+ set("posthoc-evaluation-text", saved.evaluation);
755
+ set("posthoc-evaluation-observations", saved.observations);
756
+ set("posthoc-evaluation-limitations", saved.limitations);
757
+ set("posthoc-evaluation-follow-up", saved.follow_up);
758
+ set("posthoc-evaluation-status", "Vorhandene Auswertung geladen.");
759
+ dialog.dataset.markdownPath = `experiments/${experimentId}/posthoc/evaluation.md`;
760
+ if (openButton) openButton.disabled = false;
761
+ } else {
762
+ set("posthoc-evaluation-status", "Noch keine Auswertung erfasst.");
763
+ }
764
+ } catch (error) {
765
+ set("posthoc-evaluation-status", `Auswertung konnte nicht geladen werden: ${error.message || error}`);
766
+ }
767
+ byId("posthoc-evaluation-evaluator")?.focus();
768
+ }
769
+
770
+ async _savePosthocEvaluation() {
771
+ const dialog = byId("workflow-posthoc-evaluation-dialog");
772
+ const experimentId = dialog?.dataset.experimentId;
773
+ if (!dialog || !experimentId) return;
774
+ const value = (id) => byId(id)?.value?.trim() || "";
775
+ const status = byId("posthoc-evaluation-status");
776
+ const saveButton = byId("posthoc-evaluation-save");
777
+ const payload = {
778
+ experiment_id: experimentId,
779
+ research_question: dialog.dataset.researchQuestion || "",
780
+ hypothesis: dialog.dataset.hypothesis || "",
781
+ evaluator: value("posthoc-evaluation-evaluator"),
782
+ hypothesis_answer: value("posthoc-evaluation-answer"),
783
+ evaluation: value("posthoc-evaluation-text"),
784
+ observations: value("posthoc-evaluation-observations"),
785
+ limitations: value("posthoc-evaluation-limitations"),
786
+ follow_up: value("posthoc-evaluation-follow-up"),
787
+ };
788
+ if (!payload.evaluator || !payload.evaluation) {
789
+ if (status) status.textContent = "Auswertende Person und wissenschaftliche Auswertung sind Pflichtfelder.";
790
+ return;
791
+ }
792
+ if (saveButton) { saveButton.disabled = true; saveButton.textContent = "Speichere …"; }
793
+ try {
794
+ const result = await fetchJson("/api/research/experiments/evaluation", { method: "POST", body: JSON.stringify(payload) });
795
+ dialog.dataset.markdownPath = result.markdown_path;
796
+ if (status) status.textContent = "Gespeichert; EVID bleibt unverändert.";
797
+ if (byId("posthoc-evaluation-open")) byId("posthoc-evaluation-open").disabled = false;
798
+ await this._loadExperimentCollections();
799
+ await this._openArtifact(result.markdown_path);
800
+ dialog.close();
801
+ } catch (error) {
802
+ if (status) status.textContent = `Speichern fehlgeschlagen: ${error.message || error}`;
803
+ } finally {
804
+ if (saveButton) { saveButton.disabled = false; saveButton.textContent = "Auswertung speichern"; }
805
+ }
806
+ }
807
+
808
  async _handleExperimentLibraryAction(event) {
809
  const button = event.target.closest?.("[data-experiment-action], [data-series-action], [data-series-report], [data-experiment-open], [data-experiment-toggle], [data-experiment-use]");
810
  if (!button) return;
 
833
  const seriesId = button.dataset.seriesId;
834
  const seriesReport = button.dataset.seriesReport;
835
  const openKind = button.dataset.experimentOpen;
836
+ const evaluateId = button.dataset.experimentEvaluate;
837
+ if (evaluateId) {
838
+ const item = this._findExperimentDetail(evaluateId);
839
+ if (item) await this._openPosthocEvaluation(item);
840
+ return;
841
+ }
842
  if (seriesReport) {
843
  await this._openArtifact(seriesReport);
844
  return;
 
1132
  if (artifacts.summary) buttons.push({ kind: "summary", label: "Summary", path: artifacts.summary });
1133
  if (artifacts.statistics) buttons.push({ kind: "statistics", label: "Statistics", path: artifacts.statistics });
1134
  if (artifacts.data_index || artifacts.raw_run_index) buttons.push({ kind: "raw", label: "Raw Index", path: artifacts.data_index || artifacts.raw_run_index });
1135
+ if (artifacts.posthoc_evaluation) buttons.push({ kind: "posthoc", label: "Auswertung", path: artifacts.posthoc_evaluation });
1136
  if (!buttons.length) return;
1137
  const group = document.createElement("span");
1138
  group.dataset.experimentPopupActions = "true";
src/dashboard/static/frontend/index.js CHANGED
@@ -35,6 +35,7 @@ import { initSmallSNNStage } from "./modules/small-snn-stage.js";
35
  import { initRecurrentSNNStage } from "./modules/recurrent-snn-stage.js";
36
  import { initWorkspaceRouter } from "./workspace-router.js";
37
  import { initPlayground } from "./modules/playground.js";
 
38
  import { initExternalReview } from "../external-review.js";
39
 
40
  installPollingGovernor();
@@ -77,6 +78,7 @@ function init() {
77
  initRuntimeNeuron();
78
  initSmallSNNStage();
79
  initRecurrentSNNStage();
 
80
  }
81
 
82
  if (document.readyState === "loading") {
 
35
  import { initRecurrentSNNStage } from "./modules/recurrent-snn-stage.js";
36
  import { initWorkspaceRouter } from "./workspace-router.js";
37
  import { initPlayground } from "./modules/playground.js";
38
+ import { initAccelerationIntegrationStatus } from "./modules/acceleration-integration.js";
39
  import { initExternalReview } from "../external-review.js";
40
 
41
  installPollingGovernor();
 
78
  initRuntimeNeuron();
79
  initSmallSNNStage();
80
  initRecurrentSNNStage();
81
+ initAccelerationIntegrationStatus();
82
  }
83
 
84
  if (document.readyState === "loading") {
src/dashboard/static/frontend/modules/acceleration-integration.js ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use strict";
2
+
3
+ const STATUS_LABELS = {
4
+ integrated: "integriert",
5
+ software_verified: "Software verifiziert",
6
+ pending: "Hardware ausstehend",
7
+ passed: "physisch akzeptiert",
8
+ failed: "fehlgeschlagen",
9
+ blocked: "blockiert",
10
+ contract_draft: "Semantikvertrag in Arbeit",
11
+ preflight_ready: "Preflight bereit",
12
+ unavailable: "nicht verfügbar",
13
+ };
14
+
15
+ function escapeHtml(value) {
16
+ return String(value ?? "").replace(/[&<>"']/g, (character) => ({
17
+ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
18
+ }[character]));
19
+ }
20
+
21
+ async function readJson(url) {
22
+ const response = await fetch(url, { cache: "no-store" });
23
+ if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
24
+ return response.json();
25
+ }
26
+
27
+ function statusClass(status) {
28
+ if (status === "integrated" || status === "software_verified" || status === "passed") return "ok";
29
+ if (status === "failed" || status === "unavailable") return "bad";
30
+ return "pending";
31
+ }
32
+
33
+ function injectStyles() {
34
+ if (document.getElementById("mhrn-acceleration-integration-styles")) return;
35
+ const style = document.createElement("style");
36
+ style.id = "mhrn-acceleration-integration-styles";
37
+ style.textContent = `
38
+ .mhrn-accel-panel{margin:14px 0;padding:14px;border:1px solid var(--rule);border-radius:var(--r-md);background:var(--paper-2)}
39
+ .mhrn-accel-head{display:flex;justify-content:space-between;gap:16px;align-items:flex-start;margin-bottom:12px}
40
+ .mhrn-accel-head h3{margin:.2rem 0}.mhrn-accel-head p{margin:.2rem 0;max-width:88ch}
41
+ .mhrn-accel-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:9px}
42
+ .mhrn-accel-card{padding:10px;border:1px solid var(--rule);border-radius:8px;background:var(--paper)}
43
+ .mhrn-accel-card h4{margin:0 0 6px}.mhrn-accel-card p{margin:4px 0}
44
+ .mhrn-accel-state{display:inline-flex;padding:2px 7px;border-radius:999px;border:1px solid var(--rule);font-size:.78rem}
45
+ .mhrn-accel-state.ok{font-weight:700}.mhrn-accel-state.bad{font-weight:700}
46
+ .mhrn-accel-meta{display:grid;grid-template-columns:max-content 1fr;gap:4px 10px;margin-top:10px;font-size:.85rem}
47
+ .mhrn-accel-meta dt{font-weight:700}.mhrn-accel-meta dd{margin:0;overflow-wrap:anywhere}
48
+ .mhrn-accel-actions{display:flex;flex-wrap:wrap;gap:7px;margin-top:10px}
49
+ .mhrn-accel-boundary{padding:9px;border-left:3px solid var(--accent);background:var(--paper)}
50
+ `;
51
+ document.head.append(style);
52
+ }
53
+
54
+ function panel(id, parent, title, intro, routeTag = null) {
55
+ if (!parent) return null;
56
+ let node = document.getElementById(id);
57
+ if (node) return node;
58
+ node = document.createElement("section");
59
+ node.id = id;
60
+ node.className = "mhrn-accel-panel";
61
+ if (routeTag) node.dataset.mhrnRoute = routeTag;
62
+ node.innerHTML = `
63
+ <div class="mhrn-accel-head">
64
+ <div><span class="workspace-kicker">CUDA · FE-3</span><h3>${escapeHtml(title)}</h3><p>${escapeHtml(intro)}</p></div>
65
+ <button type="button" data-accel-refresh>Aktualisieren</button>
66
+ </div>
67
+ <div data-accel-body>Lade kanonischen Integrationsstatus …</div>
68
+ `;
69
+ parent.append(node);
70
+ return node;
71
+ }
72
+
73
+ function ensurePanels() {
74
+ const playgroundHost = document.getElementById("pg-mhrn-integration") || document.getElementById("tab-playground");
75
+ const releaseHost = document.querySelector('[data-release-view="development"]');
76
+ const scienceMetrics = document.getElementById("mhrn-scientific-metrics");
77
+ const scienceHost = scienceMetrics?.parentElement || document.getElementById("tab-research");
78
+ const oldHost = document.querySelector('#tab-old [data-area-overview="old"]') || document.getElementById("tab-old");
79
+ return {
80
+ playground: panel(
81
+ "mhrn-acceleration-playground",
82
+ playgroundHost,
83
+ "Playground → MHRN Integrationsbrücke",
84
+ "Welche Playground-Bausteine bereits kanonisch sind und welches Gate als Nächstes fehlt."
85
+ ),
86
+ release: panel(
87
+ "mhrn-acceleration-release",
88
+ releaseHost,
89
+ "CUDA / FE-3 Engineering Acceptance",
90
+ "Release-Sicht auf Backend-Vertrag, FE-3-Manifest und physische Hardware-Abnahme."
91
+ ),
92
+ science: panel(
93
+ "mhrn-acceleration-science",
94
+ scienceHost,
95
+ "Backend-Parität: Evidenzgrenze",
96
+ "Engineering Verification bleibt getrennt von wissenschaftlichen DATA/EVID.",
97
+ "science:observatory"
98
+ ),
99
+ old: panel(
100
+ "mhrn-acceleration-old",
101
+ oldHost,
102
+ "Archivierte Integrationsflächen",
103
+ "OLD bleibt funktionsfähig, ist aber kein automatischer Promotion-Pfad in den MHRN-Core."
104
+ ),
105
+ };
106
+ }
107
+
108
+ function waveMarkup(waves) {
109
+ return `<div class="mhrn-accel-grid">${(waves || []).map((wave) => `
110
+ <article class="mhrn-accel-card" data-accel-wave="${escapeHtml(wave.id)}">
111
+ <h4>${escapeHtml(wave.label)}</h4>
112
+ <span class="mhrn-accel-state ${statusClass(wave.status)}">${escapeHtml(STATUS_LABELS[wave.status] || wave.status)}</span>
113
+ </article>
114
+ `).join("")}</div>`;
115
+ }
116
+
117
+ function hardwareMarkup(acceleration) {
118
+ const hardware = acceleration.hardware_acceptance || {};
119
+ const manifest = acceleration.fe3_manifest || {};
120
+ const backend = acceleration.backend || {};
121
+ const caps = backend.capabilities || {};
122
+ const pan = acceleration.pan_wave5b_preflight || {};
123
+ return `
124
+ <div class="mhrn-accel-boundary"><strong>Interpretationsgrenze:</strong> ${escapeHtml(acceleration.provenance_rule)} Keine Anzeige in diesem Panel erzeugt DATA oder EVID.</div>
125
+ ${waveMarkup(acceleration.waves)}
126
+ <dl class="mhrn-accel-meta">
127
+ <dt>FE-3 Manifest</dt><dd>${escapeHtml(manifest.status)} · ${escapeHtml(manifest.manifest_sha256 || "—")}</dd>
128
+ <dt>CUDA Backend</dt><dd>${escapeHtml(backend.backend_version || backend.status || "—")}</dd>
129
+ <dt>Live Input</dt><dd>${caps.supports_live_external_input === true ? "ja" : "nein"}</dd>
130
+ <dt>Grenzen</dt><dd>${escapeHtml(caps.max_neurons || "—")} Neuronen · ${escapeHtml(caps.max_edges || "—")} Kanten · ${escapeHtml(caps.max_ticks || "—")} Ticks</dd>
131
+ <dt>Plasticity</dt><dd>${escapeHtml(caps.plasticity_semantics || "—")}</dd>
132
+ <dt>PAN Wave 5B</dt><dd>${pan.preflight_ready === true ? "Preflight bereit" : "Preflight unvollständig"} · ${pan.ready_for_execution === true ? "Ausführung freigegeben" : "Ausführung gesperrt"}</dd>
133
+ <dt>PAN Contract</dt><dd>${escapeHtml(pan.contract_status || "—")} · ${pan.contract_frozen === true ? "frozen" : "nicht frozen"}</dd>
134
+ <dt>PAN Blocker</dt><dd>${escapeHtml((pan.blockers || []).join(", ") || "keine")}</dd>
135
+ <dt>Hardware</dt><dd><span class="mhrn-accel-state ${statusClass(hardware.status)}">${escapeHtml(STATUS_LABELS[hardware.status] || hardware.status || "—")}</span> ${escapeHtml(hardware.gpu_identity || "")}</dd>
136
+ <dt>Artefakt</dt><dd>${escapeHtml(hardware.artifact || "noch kein HARDWARE_ACCEPTANCE_<date>.json")}</dd>
137
+ <dt>Nächstes Gate</dt><dd>${escapeHtml(acceleration.next_gate || "—")}</dd>
138
+ </dl>
139
+ `;
140
+ }
141
+
142
+ function scienceMarkup(acceleration) {
143
+ const hardware = acceleration.hardware_acceptance || {};
144
+ return `
145
+ <div class="mhrn-accel-boundary">
146
+ <strong>Kein Evidenzsprung.</strong> FE-3 und CUDA-Parität sind Engineering Verification. Selbst ein grünes physisches Artefakt wird nicht automatisch DATA/EVID.
147
+ </div>
148
+ <div class="mhrn-accel-grid">
149
+ <article class="mhrn-accel-card"><h4>Softwarepfad</h4><span class="mhrn-accel-state ${acceleration.software_path_closed ? "ok" : "pending"}">${acceleration.software_path_closed ? "geschlossen" : "offen"}</span></article>
150
+ <article class="mhrn-accel-card"><h4>Physische FE-3-Abnahme</h4><span class="mhrn-accel-state ${statusClass(hardware.status)}">${escapeHtml(STATUS_LABELS[hardware.status] || hardware.status)}</span></article>
151
+ <article class="mhrn-accel-card"><h4>Scientific status</h4><span class="mhrn-accel-state pending">keine DATA / keine EVID</span></article>
152
+ </div>
153
+ `;
154
+ }
155
+
156
+ function oldMarkup(catalog) {
157
+ const old = (catalog?.candidates || []).find((item) => item.element_id === "old_frontend_views");
158
+ const routes = old?.old_routes || [];
159
+ return `
160
+ <div class="mhrn-accel-boundary"><strong>OLD-Regel:</strong> Aufbewahren statt löschen. Reaktivierung erfolgt explizit; Archivansichten werden nicht stillschweigend kanonisch.</div>
161
+ <p><strong>${routes.length}</strong> Integrations-/Kompatibilitätsrouten sind im Promotion-Katalog ausdrücklich als OLD markiert.</p>
162
+ <p>${escapeHtml(routes.join(", ") || "Keine expliziten OLD-Routen gemeldet.")}</p>
163
+ `;
164
+ }
165
+
166
+ async function refresh(panels) {
167
+ const [integration, catalog] = await Promise.all([
168
+ readJson("/api/integration/status"),
169
+ readJson("/api/playground/integration"),
170
+ ]);
171
+ const acceleration = integration.acceleration;
172
+ if (!acceleration) throw new Error("/api/integration/status enthält keinen acceleration-Block");
173
+
174
+ if (panels.playground) panels.playground.querySelector("[data-accel-body]").innerHTML = hardwareMarkup(acceleration);
175
+ if (panels.release) panels.release.querySelector("[data-accel-body]").innerHTML = hardwareMarkup(acceleration);
176
+ if (panels.science) panels.science.querySelector("[data-accel-body]").innerHTML = scienceMarkup(acceleration);
177
+ if (panels.old) panels.old.querySelector("[data-accel-body]").innerHTML = oldMarkup(catalog);
178
+ }
179
+
180
+ export function initAccelerationIntegrationStatus() {
181
+ injectStyles();
182
+ const panels = ensurePanels();
183
+ const roots = Object.values(panels).filter(Boolean);
184
+ if (!roots.length) return;
185
+
186
+ document.addEventListener("click", (event) => {
187
+ const button = event.target.closest("[data-accel-refresh]");
188
+ if (button && roots.some((root) => root.contains(button))) {
189
+ refresh(panels).catch((error) => {
190
+ roots.forEach((root) => {
191
+ const body = root.querySelector("[data-accel-body]");
192
+ if (body) body.textContent = `Integrationsstatus nicht verfügbar: ${error.message || error}`;
193
+ });
194
+ });
195
+ }
196
+ });
197
+
198
+ refresh(panels).catch((error) => {
199
+ roots.forEach((root) => {
200
+ const body = root.querySelector("[data-accel-body]");
201
+ if (body) body.textContent = `Integrationsstatus nicht verfügbar: ${error.message || error}`;
202
+ });
203
+ });
204
+ }
src/dashboard/static/frontend/modules/neuron-model-science.js CHANGED
@@ -62,8 +62,8 @@ const FIELD_LABELS = Object.freeze({
62
  "neuron.enable_homeostasis": "Homeostatic regulation",
63
  });
64
 
65
- const ROUTE_ID = "cellmodel";
66
- const ROUTE = Object.freeze([ROUTE_ID, "Cell Model", "network", "view", ROUTE_ID]);
67
  const ROUTER_STORAGE_KEY = "mhrn-workspace-router-v1";
68
 
69
  const state = {
@@ -295,25 +295,25 @@ function addContextRouteButton(nav, architecture) {
295
  button.setAttribute("role", "tab");
296
  button.dataset.areaRoute = ROUTE_ID;
297
  button.title = "Stage 0 single-neuron model and settings";
298
- button.textContent = "Cell Model";
299
- const networkButton = nav.querySelector('[data-area-route="network"]');
300
  if (networkButton?.nextSibling) nav.insertBefore(button, networkButton.nextSibling);
301
  else nav.appendChild(button);
302
- button.addEventListener("click", () => architecture.selectRoute("science", ROUTE_ID));
303
  }
304
 
305
  function addOverviewRouteCard(architecture) {
306
- const grid = document.querySelector('[data-area-overview="science"] .mhrn-area-route-grid');
307
  if (!grid || grid.querySelector(`[data-route-card="${ROUTE_ID}"]`)) return;
308
  const button = document.createElement("button");
309
  button.type = "button";
310
  button.dataset.routeCard = ROUTE_ID;
311
- button.title = "Cell Model öffnen";
312
- button.innerHTML = '<span>04</span><strong>Cell Model</strong>';
313
- const networkCard = grid.querySelector('[data-route-card="network"]');
314
  if (networkCard?.nextSibling) grid.insertBefore(button, networkCard.nextSibling);
315
  else grid.appendChild(button);
316
- button.addEventListener("click", () => architecture.selectRoute("science", ROUTE_ID));
317
  }
318
 
319
  function ensureSettingsNavigationFallback(architecture) {
@@ -330,15 +330,15 @@ function ensureSettingsNavigationFallback(architecture) {
330
 
331
  function ensureCanonicalRoute() {
332
  const architecture = window.MHRNWorkspaceArchitecture;
333
- const routes = architecture?.areas?.science?.routes;
334
  if (!architecture?.selectRoute || !Array.isArray(routes)) return false;
335
 
336
  if (!routes.some(([id]) => id === ROUTE_ID)) {
337
- const networkIndex = routes.findIndex(([id]) => id === "network");
338
  routes.splice(networkIndex >= 0 ? networkIndex + 1 : routes.length, 0, [...ROUTE]);
339
  }
340
 
341
- document.querySelectorAll('.mhrn-context-nav[data-area="science"]').forEach((nav) => {
342
  addContextRouteButton(nav, architecture);
343
  });
344
  addOverviewRouteCard(architecture);
@@ -381,7 +381,7 @@ function ensureWorkbench() {
381
  </div>
382
  <div id="science-neuron-note" class="neuron-model-warning" role="note"></div>
383
  <div class="neuron-model-shortcuts" aria-label="Related settings">
384
- <button type="button" class="btn-secondary" data-route-jump="control:parameters">Alle Parameter</button>
385
  <button type="button" class="btn-secondary" data-route-jump="settings:overview">App Settings</button>
386
  </div>
387
  <div id="science-neuron-fields" class="neuron-model-fields" aria-live="polite"></div>
@@ -419,8 +419,8 @@ function bind() {
419
  function restoreCellModelRouteIfRequested() {
420
  try {
421
  const saved = JSON.parse(localStorage.getItem(ROUTER_STORAGE_KEY) || "null");
422
- if (saved?.area === "science" && saved?.route === ROUTE_ID) {
423
- window.MHRNWorkspaceArchitecture?.selectRoute?.("science", ROUTE_ID);
424
  }
425
  } catch (_) {}
426
  }
 
62
  "neuron.enable_homeostasis": "Homeostatic regulation",
63
  });
64
 
65
+ const ROUTE_ID = "science-cellmodel";
66
+ const ROUTE = Object.freeze([ROUTE_ID, "Wissenschaft · Cell Modell", "network", "focus", "#mhrn-neuron-model-science"]);
67
  const ROUTER_STORAGE_KEY = "mhrn-workspace-router-v1";
68
 
69
  const state = {
 
295
  button.setAttribute("role", "tab");
296
  button.dataset.areaRoute = ROUTE_ID;
297
  button.title = "Stage 0 single-neuron model and settings";
298
+ button.textContent = "Wissenschaft · Cell Modell";
299
+ const networkButton = nav.querySelector('[data-area-route="science-network"]');
300
  if (networkButton?.nextSibling) nav.insertBefore(button, networkButton.nextSibling);
301
  else nav.appendChild(button);
302
+ button.addEventListener("click", () => architecture.selectRoute("old", ROUTE_ID));
303
  }
304
 
305
  function addOverviewRouteCard(architecture) {
306
+ const grid = document.querySelector('[data-area-overview="old"] .mhrn-area-route-grid');
307
  if (!grid || grid.querySelector(`[data-route-card="${ROUTE_ID}"]`)) return;
308
  const button = document.createElement("button");
309
  button.type = "button";
310
  button.dataset.routeCard = ROUTE_ID;
311
+ button.title = "Cell Modell öffnen";
312
+ button.innerHTML = '<span>W0</span><strong>Wissenschaft · Cell Modell</strong>';
313
+ const networkCard = grid.querySelector('[data-route-card="science-network"]');
314
  if (networkCard?.nextSibling) grid.insertBefore(button, networkCard.nextSibling);
315
  else grid.appendChild(button);
316
+ button.addEventListener("click", () => architecture.selectRoute("old", ROUTE_ID));
317
  }
318
 
319
  function ensureSettingsNavigationFallback(architecture) {
 
330
 
331
  function ensureCanonicalRoute() {
332
  const architecture = window.MHRNWorkspaceArchitecture;
333
+ const routes = architecture?.areas?.old?.routes;
334
  if (!architecture?.selectRoute || !Array.isArray(routes)) return false;
335
 
336
  if (!routes.some(([id]) => id === ROUTE_ID)) {
337
+ const networkIndex = routes.findIndex(([id]) => id === "science-network");
338
  routes.splice(networkIndex >= 0 ? networkIndex + 1 : routes.length, 0, [...ROUTE]);
339
  }
340
 
341
+ document.querySelectorAll('.mhrn-context-nav[data-area="old"]').forEach((nav) => {
342
  addContextRouteButton(nav, architecture);
343
  });
344
  addOverviewRouteCard(architecture);
 
381
  </div>
382
  <div id="science-neuron-note" class="neuron-model-warning" role="note"></div>
383
  <div class="neuron-model-shortcuts" aria-label="Related settings">
384
+ <button type="button" class="btn-secondary" data-route-jump="settings:parameters">Alle Parameter</button>
385
  <button type="button" class="btn-secondary" data-route-jump="settings:overview">App Settings</button>
386
  </div>
387
  <div id="science-neuron-fields" class="neuron-model-fields" aria-live="polite"></div>
 
419
  function restoreCellModelRouteIfRequested() {
420
  try {
421
  const saved = JSON.parse(localStorage.getItem(ROUTER_STORAGE_KEY) || "null");
422
+ if (saved?.area === "old" && saved?.route === ROUTE_ID) {
423
+ window.MHRNWorkspaceArchitecture?.selectRoute?.("old", ROUTE_ID);
424
  }
425
  } catch (_) {}
426
  }
src/dashboard/static/frontend/modules/playground-workspace.js ADDED
@@ -0,0 +1,859 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Guided controls share the existing Playground form and API contract.
2
+ const $ = (id) => document.getElementById(id);
3
+ const escape = (value) =>
4
+ String(value ?? "").replace(
5
+ /[&<>"']/g,
6
+ (c) =>
7
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[
8
+ c
9
+ ],
10
+ );
11
+ const groups = [
12
+ [
13
+ "Modell & Netzwerk",
14
+ "Zellen, Verbindungen und zeitliche Dynamik",
15
+ [1, 2, 3, 15],
16
+ ],
17
+ ["Lauf & Stimulus", "Eingabe, Seed und Ausführung festlegen", [4, 7]],
18
+ ["PAN-Kern", "Hyperzustand, Geometrie und Feedback", [5, 6]],
19
+ [
20
+ "Lernen & Closed Loop",
21
+ "Kanäle, Aktionen, Ziele und Kredit-Zuweisung",
22
+ [8, 10, 11, 12, 13, 14],
23
+ ],
24
+ ["Anwendung", "Körperhaltung, Reward-Ereignisse und Episoden", [20, 21, 22]],
25
+ [
26
+ "Operationen",
27
+ "Neural I/O, Live-Monitor, CUDA und Nachtlauf",
28
+ [9, 17, 18, 19],
29
+ ],
30
+ ];
31
+ const channelFields = [
32
+ "pg-input-amplitudes",
33
+ "pg-input-frequencies",
34
+ "pg-input-phases",
35
+ ];
36
+ const limits = [
37
+ [0, 500, 0],
38
+ [1, 200, 20],
39
+ [0, Math.PI * 2, 0],
40
+ ];
41
+ const help = {
42
+ "pg-pan-feedback-threshold":
43
+ "Schwelle für das konfigurierte Feedback-Routing. Zusammen mit Quelle, Nichtlinearität und Sättigung betrachten.",
44
+ "pg-inhibitory-fraction":
45
+ "Anteil inhibitorischer Neuronen. Ohne GABA-Stärke entsteht daraus keine inhibitorische Wirkung.",
46
+ "pg-gaba-strength":
47
+ "Stärke der inhibitorischen Kopplung; 0 deaktiviert deren Wirkung.",
48
+ "pg-behavior-epsilon":
49
+ "Explorationswahrscheinlichkeit: 0 nutzt die bevorzugte Aktion, 1 exploriert immer.",
50
+ "pg-seed":
51
+ "Ein fester Seed macht gleiche CPU-Konfigurationen reproduzierbar. Modell- und Hardware-Parität separat prüfen.",
52
+ "pg-action-to-input-map":
53
+ "auto erzeugt die Zuordnung im Backend. Alternativ eine JSON-Matrix mit einer Zeile je Aktion eingeben.",
54
+ "pg-threshold-variance":
55
+ "Streuung der Neuronenschwellen. 0 lässt alle Schwellen identisch.",
56
+ "pg-tau-m-variance":
57
+ "Streuung der Membranzeitkonstanten. Mit identischem Seed gegen 0 vergleichen.",
58
+ };
59
+
60
+ export function initGuidedWorkspace({
61
+ payload,
62
+ presets,
63
+ applyPreset,
64
+ setValue,
65
+ lastResult,
66
+ }) {
67
+ const root = $("playground-builder");
68
+ if (!root || root.dataset.guided) return;
69
+ root.dataset.guided = "true";
70
+ const style = document.createElement("style");
71
+ style.textContent = `
72
+ #playground-run .playground-analysis-card{min-width:0}#playground-run .playground-analysis-card pre{max-width:100%;overflow:auto}
73
+ .pg-page-intro{padding:18px 0 12px;border-bottom:1px solid var(--rule);margin-bottom:16px}.pg-page-intro h2{font-size:1.35rem;margin:0 0 6px}.pg-page-intro p{color:var(--ink-3);max-width:75ch;line-height:1.5}
74
+ .pg-group{margin:12px 0;border:1px solid var(--rule-2);border-radius:8px;background:var(--paper-2);overflow:hidden}.pg-group>summary{padding:16px;cursor:pointer;font-size:1rem;font-weight:650}.pg-group>summary small{display:block;font-size:.75rem;font-weight:400;color:var(--ink-3);margin:5px 0 0 18px}.pg-group-body{padding:0 12px 12px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.pg-group-body>*{min-width:0}.pg-group-body>.playground-card{border-top:1px solid var(--rule)}.pg-group-body>.pg-wide{grid-column:1/-1}.pg-section-reset{float:right;font-size:.65rem;padding:3px 7px}
75
+ #tab-playground button{cursor:pointer}#tab-playground button:disabled{opacity:.5;cursor:not-allowed}#tab-playground :focus-visible{outline:2px solid var(--accent);outline-offset:3px}
76
+ .pg-tools{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0}.pg-tools button,.pg-page-intro button{min-height:36px;padding:7px 12px}.pg-preset-grid{max-height:360px;overflow:auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;margin:12px 0}.pg-preset-card{border:1px solid var(--rule);border-radius:6px;padding:14px;background:var(--paper-2);display:flex;flex-direction:column;gap:8px}.pg-preset-card h4,.pg-preset-card p{margin:0}.pg-preset-card p{font-size:.78rem;line-height:1.45}.pg-preset-card small{color:var(--ink-3)}.pg-preset-card button{margin-top:auto;align-self:start}.pg-preset-card[hidden]{display:none}
77
+ .pg-channel-wrap{grid-column:1/-1;overflow:auto}.pg-channel-table{width:100%;border-collapse:collapse;font-size:.8rem}.pg-channel-table th,.pg-channel-table td{padding:8px;border-bottom:1px solid var(--rule);text-align:left}.pg-channel-table input{width:100%;min-width:75px}.pg-channel-table td:nth-child(2) input{min-width:160px}.pg-validation{margin:12px 0;font-size:.8rem;line-height:1.6}.pg-validation a{color:var(--crimson)}.pg-warning{color:var(--amber);padding:4px 0}.pg-field-error{display:block;color:var(--crimson);font-size:.7rem;grid-column:1/-1}[aria-invalid=true]{outline:2px solid var(--crimson)!important}.pg-help{display:block;font-size:.68rem;line-height:1.4;color:var(--ink-3);grid-column:1/-1}
78
+ .pg-run-empty{padding:36px;border:1px dashed var(--rule-3);text-align:center;color:var(--ink-3)}.pg-config-preview{max-height:320px;overflow:auto;white-space:pre-wrap;font-size:.75rem}.pg-search{width:100%;max-width:460px;padding:10px;margin:10px 0}.pg-summary{padding:12px;border-left:3px solid var(--accent);background:var(--paper-2);line-height:1.6;font-size:.8rem}.pg-raw{margin:12px 0}.pg-raw>summary{cursor:pointer;padding:8px;font-size:.8rem}
79
+ @media(max-width:850px){.pg-group-body{grid-template-columns:1fr}.pg-preset-grid{grid-template-columns:1fr}.pg-group>summary{font-size:.9rem}.pg-tools button{min-height:44px}.pg-live-monitor-body{overflow:auto;max-height:75vh}.pg-live-monitor-params{overflow:auto}.playground-session{flex-wrap:wrap}}
80
+ `;
81
+ document.head.append(style);
82
+ const intro = (id, title, description) => {
83
+ const node = document.createElement("header");
84
+ node.className = "pg-page-intro";
85
+ node.innerHTML = `<h2>${title}</h2><p>${description}</p>`;
86
+ $(id).prepend(node);
87
+ return node;
88
+ };
89
+ intro(
90
+ "playground-builder",
91
+ "Experiment aufbauen",
92
+ "1 · Startpunkt wählen. 2 · Parameter prüfen. 3 · CPU, CUDA-Membran oder CUDA-PAN-Zustand wählen und den Lauf starten.",
93
+ );
94
+ intro(
95
+ "playground-run",
96
+ "Lauf & Auswertung",
97
+ "Aktivität, Netzwerk und Lernmechanismen gemeinsam betrachten. Die Diagramme zeigen den zuletzt ausgeführten oder geladenen Playground-Lauf.",
98
+ );
99
+ intro(
100
+ "playground-sessions",
101
+ "Gespeicherte Sessions",
102
+ "Persistierte Läufe vom lokalen Server öffnen und deren Ergebnisse vergleichen. Browser-Presets und Server-Sessions sind getrennte Speicher.",
103
+ );
104
+ intro(
105
+ "playground-catalog",
106
+ "Bausteine & Fähigkeiten",
107
+ "Modelle, Topologien, Lernregeln und Schnittstellen erkunden. Ein Eintrag öffnet die Erklärung und den Implementierungsstatus.",
108
+ );
109
+ const cards = new Map();
110
+ root.querySelectorAll("h3").forEach((h) => {
111
+ const n = Number(h.textContent.match(/^(\d+) ·/)?.[1]);
112
+ if (n) cards.set(n, h.parentElement);
113
+ });
114
+ const container = document.createElement("div");
115
+ container.id = "pg-groups";
116
+ root.querySelector(".playground-grid").before(container);
117
+ root.querySelectorAll("input[type=number][step]").forEach((e) => {
118
+ if (e.step.includes(".")) e.step = "any";
119
+ });
120
+ const defaults = new Map();
121
+ const controls = () =>
122
+ [...root.querySelectorAll("input[id],select[id],textarea[id]")].filter(
123
+ (e) =>
124
+ (e.id === "pg-user-preset-select" || !e.id.startsWith("pg-user-")) &&
125
+ !e.id.startsWith("pg-workspace-"),
126
+ );
127
+ const snapshot = () =>
128
+ Object.fromEntries(
129
+ controls().map((e) => [
130
+ e.id,
131
+ e.type === "checkbox" ? e.checked : e.value,
132
+ ]),
133
+ );
134
+ groups.forEach(([title, description, numbers], index) => {
135
+ const group = document.createElement("details");
136
+ group.className = "pg-group";
137
+ group.id = `pg-group-${index}`;
138
+ group.open = index === 0;
139
+ group.innerHTML = `<summary>${String.fromCharCode(65 + index)} · ${title}<small>${description}</small></summary><div class="pg-group-body"></div>`;
140
+ container.append(group);
141
+ numbers.forEach((n, position) => {
142
+ const card = cards.get(n);
143
+ if (!card) return;
144
+ const heading = card.querySelector("h3");
145
+ heading.textContent = heading.textContent.replace(
146
+ /^\d+ ·/,
147
+ `${String.fromCharCode(65 + index)}${position + 1} ·`,
148
+ );
149
+ if (n >= 17) card.classList.add("pg-wide");
150
+ const button = document.createElement("button");
151
+ button.type = "button";
152
+ button.className = "pg-section-reset";
153
+ button.textContent = "Zurücksetzen";
154
+ button.title = "Auf Werte des Startprofils zurücksetzen";
155
+ button.onclick = () => {
156
+ card
157
+ .querySelectorAll("input[id],select[id],textarea[id]")
158
+ .forEach((e) => {
159
+ if (defaults.has(e.id)) {
160
+ if (e.type === "checkbox") e.checked = defaults.get(e.id);
161
+ else e.value = defaults.get(e.id);
162
+ }
163
+ });
164
+ refresh();
165
+ remember();
166
+ };
167
+ heading.append(button);
168
+ group.lastElementChild.append(card);
169
+ });
170
+ });
171
+ root.querySelectorAll(".playground-grid").forEach((e) => {
172
+ if (!e.children.length) e.remove();
173
+ });
174
+ const tools = document.createElement("div");
175
+ tools.className = "pg-tools";
176
+ tools.innerHTML = `<button type="button" id="pg-workspace-expand">Alle öffnen</button><button type="button" id="pg-workspace-collapse">Alle schließen</button><button type="button" id="pg-workspace-undo" disabled>Änderung rückgängig</button><button type="button" id="pg-workspace-export">Konfiguration exportieren</button><button type="button" id="pg-workspace-import">JSON importieren</button><input type="file" id="pg-workspace-file" accept=".json,application/json" hidden>`;
177
+ container.before(tools);
178
+ const message = document.createElement("div");
179
+ message.id = "pg-workspace-message";
180
+ message.setAttribute("role", "status");
181
+ tools.after(message);
182
+ const validation = document.createElement("div");
183
+ validation.id = "pg-validation";
184
+ validation.className = "pg-validation";
185
+ validation.setAttribute("aria-live", "polite");
186
+ container.before(validation);
187
+ const summary = document.createElement("div");
188
+ summary.id = "pg-workspace-summary";
189
+ summary.className = "pg-summary";
190
+ container.before(summary);
191
+ $("pg-workspace-expand").onclick = () =>
192
+ container.querySelectorAll("details").forEach((e) => (e.open = true));
193
+ $("pg-workspace-collapse").onclick = () =>
194
+ container.querySelectorAll("details").forEach((e) => (e.open = false));
195
+ const history = [];
196
+ function remember() {
197
+ const current = snapshot();
198
+ if (JSON.stringify(current) !== JSON.stringify(history.at(-1))) {
199
+ history.push(current);
200
+ if (history.length > 30) history.shift();
201
+ }
202
+ $("pg-workspace-undo").disabled = history.length < 2;
203
+ }
204
+ $("pg-workspace-undo").onclick = () => {
205
+ history.pop();
206
+ Object.entries(history.at(-1) || {}).forEach(([id, v]) => {
207
+ if ($(id).type === "checkbox") $(id).checked = v;
208
+ else $(id).value = v;
209
+ });
210
+ refresh();
211
+ $("pg-workspace-undo").disabled = history.length < 2;
212
+ };
213
+ const download = (data, name) => {
214
+ const url = URL.createObjectURL(
215
+ new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }),
216
+ );
217
+ const a = document.createElement("a");
218
+ a.href = url;
219
+ a.download = name;
220
+ a.click();
221
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
222
+ };
223
+ $("pg-workspace-export").onclick = () => {
224
+ try {
225
+ if (!validate())
226
+ throw Error("Bitte zuerst die markierten Felder korrigieren.");
227
+ download(
228
+ {
229
+ schema: "mhrn-playground-config-v1",
230
+ settings: payload(),
231
+ controls: snapshot(),
232
+ },
233
+ "playground-config.json",
234
+ );
235
+ message.textContent =
236
+ "Konfiguration exportiert. Eigene Presets liegen nur im Browser-Speicher dieses Profils.";
237
+ } catch (e) {
238
+ message.textContent = e.message;
239
+ }
240
+ };
241
+ $("pg-workspace-import").onclick = () => $("pg-workspace-file").click();
242
+ $("pg-workspace-file").onchange = async (event) => {
243
+ try {
244
+ const file = event.target.files[0];
245
+ if (!file) return;
246
+ if (file.size > 1000000) throw Error("Datei ist größer als 1 MB.");
247
+ const data = JSON.parse(await file.text());
248
+ if (
249
+ data.schema !== "mhrn-playground-config-v1" ||
250
+ !data.settings ||
251
+ typeof data.settings !== "object" ||
252
+ Array.isArray(data.settings)
253
+ )
254
+ throw Error("Eine exportierte Playground-Konfiguration auswählen.");
255
+ const allowed = new Set(Object.keys(payload()));
256
+ const entries = Object.entries(data.settings);
257
+ if (
258
+ entries.some(
259
+ ([k, v]) =>
260
+ !allowed.has(k) ||
261
+ v === null ||
262
+ (!["number", "boolean", "string"].includes(typeof v) &&
263
+ !Array.isArray(v)),
264
+ )
265
+ )
266
+ throw Error(
267
+ "Unbekannte Felder oder ungültige Werte in der Konfiguration.",
268
+ );
269
+ if (
270
+ data.settings.closed_loop_preset &&
271
+ [...$("pg-user-preset-select").options].some(
272
+ (o) => o.value === data.settings.closed_loop_preset,
273
+ )
274
+ )
275
+ $("pg-user-preset-select").value = data.settings.closed_loop_preset;
276
+ if (data.controls) {
277
+ const known = new Map(controls().map((e) => [e.id, e]));
278
+ if (
279
+ Object.entries(data.controls).some(
280
+ ([id, v]) =>
281
+ !known.has(id) ||
282
+ typeof v !==
283
+ (known.get(id).type === "checkbox" ? "boolean" : "string"),
284
+ )
285
+ )
286
+ throw Error("Ungültige Formularfelder im Export.");
287
+ Object.entries(data.controls).forEach(([id, v]) => {
288
+ const e = known.get(id);
289
+ if (e.type === "checkbox") e.checked = v;
290
+ else e.value = v;
291
+ });
292
+ } else {
293
+ entries.forEach(([k, v]) => setValue(k, v));
294
+ }
295
+ refresh();
296
+ remember();
297
+ message.textContent =
298
+ "Konfiguration importiert. Hinweise und Validierung vor dem Start prüfen.";
299
+ } catch (e) {
300
+ message.textContent = `Import fehlgeschlagen: ${e.message}`;
301
+ } finally {
302
+ event.target.value = "";
303
+ }
304
+ };
305
+
306
+ const presetBox = document.createElement("details");
307
+ presetBox.open = true;
308
+ presetBox.className = "pg-raw";
309
+ presetBox.innerHTML =
310
+ '<summary>Preset-Katalog erkunden & vergleichen</summary><input class="pg-search" id="pg-workspace-preset-search" aria-label="Presets durchsuchen" placeholder="Preset suchen …"><div class="pg-preset-grid" id="pg-preset-grid"></div><pre class="pg-config-preview" id="pg-workspace-comparison" aria-live="polite"></pre>';
311
+ root.querySelector(".playground-preset-deck").after(presetBox);
312
+ const storageNote = document.createElement("p");
313
+ storageNote.className = "pg-help";
314
+ storageNote.textContent =
315
+ "Integrierte und Katalog-Presets kommen aus dieser Serverversion. Eigene Presets werden im localStorage dieses Browserprofils gespeichert. Exportierte JSON-Dateien können auf einem anderen Gerät importiert werden.";
316
+ presetBox.before(storageNote);
317
+ function renderPresets() {
318
+ const all = presets();
319
+ $("pg-preset-grid").innerHTML = Object.entries(all)
320
+ .map(
321
+ ([name, p]) =>
322
+ `<article class="pg-preset-card" data-name="${escape(name)}"><h4>${escape(p.label || name)}</h4><small>${p.source === "catalog" ? "Server-Katalog" : p.saved_at ? "Browser · eigenes Preset" : "Integriert"}</small><p>${escape(p.description)}</p>${p.expected_success != null ? `<small>Hypothese: ${escape(p.expected_success)} Erfolgsanteil · kein Messergebnis</small>` : ""}<button type="button" data-apply="${escape(name)}">Anwenden</button><button type="button" data-compare="${escape(name)}">Mit aktueller Konfiguration vergleichen</button></article>`,
323
+ )
324
+ .join("");
325
+ $("pg-preset-grid")
326
+ .querySelectorAll("[data-apply]")
327
+ .forEach(
328
+ (b) =>
329
+ (b.onclick = () => {
330
+ $("pg-user-preset-select").value = b.dataset.apply;
331
+ applyPreset();
332
+ refresh();
333
+ remember();
334
+ }),
335
+ );
336
+ $("pg-preset-grid")
337
+ .querySelectorAll("[data-compare]")
338
+ .forEach(
339
+ (b) =>
340
+ (b.onclick = () => {
341
+ try {
342
+ const current = payload(),
343
+ settings = all[b.dataset.compare].settings || {};
344
+ $("pg-workspace-comparison").textContent =
345
+ Object.entries(settings)
346
+ .filter(
347
+ ([k, v]) =>
348
+ JSON.stringify(v) !== JSON.stringify(current[k]),
349
+ )
350
+ .map(
351
+ ([k, v]) =>
352
+ `${k}\n Aktuell: ${JSON.stringify(current[k])}\n Preset: ${JSON.stringify(v)}`,
353
+ )
354
+ .join("\n\n") || "Keine abweichenden Einstellungen.";
355
+ } catch (e) {
356
+ message.textContent = e.message;
357
+ }
358
+ }),
359
+ );
360
+ }
361
+ $("pg-workspace-preset-search").oninput = (e) => {
362
+ $("pg-preset-grid")
363
+ .querySelectorAll("article")
364
+ .forEach(
365
+ (card) =>
366
+ (card.hidden = !card.textContent
367
+ .toLowerCase()
368
+ .includes(e.target.value.toLowerCase())),
369
+ );
370
+ };
371
+ const raw = document.createElement("details");
372
+ raw.className = "pg-raw";
373
+ raw.innerHTML = "<summary>Erweitert: Kanalwerte als JSON</summary>";
374
+ const channelCard = $("pg-input-amplitudes").closest("article");
375
+ channelCard.append(raw);
376
+ ["pg-input-channel-map", ...channelFields].forEach((id) =>
377
+ raw.append($(id).closest("label")),
378
+ );
379
+ const table = document.createElement("div");
380
+ table.className = "pg-channel-wrap";
381
+ table.innerHTML =
382
+ '<p class="pg-help">Automatische Neuronenzuordnung bleibt im Backend. Eigene IDs kommagetrennt eingeben. Leere Werte verwenden die Runtime-Defaults: 0 / 20 Hz / 0 rad; kurze Listen werden zyklisch wiederholt.</p><table class="pg-channel-table"><thead><tr><th>Kanal</th><th>Neuronen</th><th>Amplitude</th><th>Hz</th><th>Phase (rad)</th></tr></thead><tbody id="pg-channel-rows"></tbody></table><div class="pg-tools"><button type="button" id="pg-channel-frequency-up">Alle Frequenzen +2 Hz</button><button type="button" id="pg-channel-auto">Neuronenzuordnung automatisch</button></div>';
383
+ raw.before(table);
384
+ const arrays = () =>
385
+ ["pg-input-channel-map", ...channelFields].map((id) => {
386
+ const v = JSON.parse($(id).value);
387
+ if (!Array.isArray(v)) throw Error(`${id}: JSON-Liste erwartet.`);
388
+ return v;
389
+ });
390
+ function renderChannels() {
391
+ try {
392
+ const [map, ...vectors] = arrays();
393
+ const n = Number($("pg-input-channels").value);
394
+ if (!Number.isInteger(n) || n < 1 || n > 64) return;
395
+ $("pg-channel-rows").innerHTML = Array.from(
396
+ { length: n },
397
+ (_, i) =>
398
+ `<tr><th scope="row">${i}</th><td><input id="pg-workspace-channel-map-${i}" aria-label="Kanal ${i} Neuronen" data-channel="${i}" data-column="map" value="${escape(map[i]?.join(", ") || "")}" placeholder="Automatisch · ${escape($("pg-input-topology").value)}"></td>${vectors.map((v, j) => `<td><input id="pg-workspace-channel-${i}-${j}" type="number" required step="any" min="${limits[j][0]}" max="${limits[j][1]}" aria-label="Kanal ${i} ${["Amplitude", "Frequenz", "Phase"][j]}" data-channel="${i}" data-column="${j}" value="${escape(v.length ? v[i % v.length] : limits[j][2])}"></td>`).join("")}</tr>`,
399
+ ).join("");
400
+ } catch {
401
+ /* Raw JSON errors are displayed by validation. */
402
+ }
403
+ }
404
+ table.addEventListener("input", (e) => {
405
+ const input = e.target;
406
+ if (!input.dataset.channel) return;
407
+ const i = Number(input.dataset.channel),
408
+ col = input.dataset.column,
409
+ n = Number($("pg-input-channels").value);
410
+ try {
411
+ const [map, ...vectors] = arrays();
412
+ if (col === "map") {
413
+ if (
414
+ input.value.trim() &&
415
+ !/^\d+(\s*,\s*\d+)*$/.test(input.value.trim())
416
+ ) {
417
+ input.setCustomValidity(
418
+ "IDs als kommagetrennte ganze Zahlen eingeben.",
419
+ );
420
+ return;
421
+ }
422
+ input.setCustomValidity("");
423
+ while (map.length < n) map.push([]);
424
+ map[i] = input.value.trim() ? input.value.split(",").map(Number) : [];
425
+ $("pg-input-channel-map").value = JSON.stringify(map);
426
+ } else {
427
+ const j = Number(col),
428
+ old = vectors[j];
429
+ const values = Array.from({ length: n }, (_, k) =>
430
+ old.length ? old[k % old.length] : limits[j][2],
431
+ );
432
+ if (input.value !== "" && Number.isFinite(input.valueAsNumber)) {
433
+ values[i] = input.valueAsNumber;
434
+ $(channelFields[j]).value = JSON.stringify(values);
435
+ }
436
+ }
437
+ } catch (e) {
438
+ message.textContent = e.message;
439
+ }
440
+ });
441
+ $("pg-channel-frequency-up").onclick = () => {
442
+ try {
443
+ const v = arrays()[2],
444
+ n = Number($("pg-input-channels").value);
445
+ $("pg-input-frequencies").value = JSON.stringify(
446
+ Array.from({ length: n }, (_, i) =>
447
+ Math.min(200, (v.length ? v[i % v.length] : 20) + 2),
448
+ ),
449
+ );
450
+ refresh();
451
+ remember();
452
+ } catch (e) {
453
+ message.textContent = e.message;
454
+ }
455
+ };
456
+ $("pg-channel-auto").onclick = () => {
457
+ $("pg-input-channel-map").value = "[]";
458
+ refresh();
459
+ remember();
460
+ };
461
+ for (const [id, text] of Object.entries(help)) {
462
+ const field = $(id);
463
+ if (field) {
464
+ const note = document.createElement("small");
465
+ note.className = "pg-help";
466
+ note.id = `${id}-help`;
467
+ note.textContent = text;
468
+ field.closest("label").append(note);
469
+ field.setAttribute("aria-describedby", note.id);
470
+ }
471
+ }
472
+
473
+ function validate() {
474
+ const errors = [],
475
+ warnings = [];
476
+ root
477
+ .querySelectorAll("[aria-invalid]")
478
+ .forEach((e) => e.removeAttribute("aria-invalid"));
479
+ root.querySelectorAll(".pg-field-error").forEach((e) => e.remove());
480
+ root.querySelectorAll("input[type=number]").forEach((e) => {
481
+ e.setAttribute(
482
+ "aria-invalid",
483
+ String(
484
+ !e.validity.valid ||
485
+ e.value === "" ||
486
+ !Number.isFinite(e.valueAsNumber),
487
+ ),
488
+ );
489
+ if (e.getAttribute("aria-invalid") === "true")
490
+ errors.push([
491
+ e.id,
492
+ e.getAttribute("aria-label") ||
493
+ e.closest("label")?.firstChild?.textContent ||
494
+ "Kanalwert",
495
+ e.validationMessage || "Eine endliche Zahl eingeben.",
496
+ ]);
497
+ });
498
+ root.querySelectorAll("[data-column=map]").forEach((e) => {
499
+ if (!e.validity.valid)
500
+ errors.push([e.id, "Neuronenzuordnung", e.validationMessage]);
501
+ });
502
+ const value = (id) => Number($(id)?.value),
503
+ enabled = (id) => $(id)?.checked;
504
+ try {
505
+ const [map, ...vectors] = arrays(),
506
+ n = value("pg-input-channels");
507
+ vectors.forEach((v, j) => {
508
+ if (
509
+ v.length > n ||
510
+ v.some(
511
+ (x) =>
512
+ typeof x !== "number" ||
513
+ !Number.isFinite(x) ||
514
+ x < limits[j][0] ||
515
+ x > limits[j][1],
516
+ )
517
+ )
518
+ errors.push([
519
+ channelFields[j],
520
+ "Input-Kanäle",
521
+ `Maximal ${n} Werte im Bereich ${limits[j][0]} bis ${limits[j][1]}.`,
522
+ ]);
523
+ });
524
+ if (
525
+ map.length > n ||
526
+ map.some(
527
+ (row) =>
528
+ !Array.isArray(row) ||
529
+ row.some(
530
+ (x) => !Number.isInteger(x) || x < 0 || x >= value("pg-neurons"),
531
+ ),
532
+ )
533
+ )
534
+ errors.push([
535
+ "pg-input-channel-map",
536
+ "Neuronenzuordnung",
537
+ "Nur gültige Neuronen-IDs und maximal eine Gruppe je Kanal verwenden.",
538
+ ]);
539
+ if (map.length)
540
+ warnings.push(
541
+ "Eigene Neuronenzuordnung aktiv: leere Zeilen enthalten keine Neuronen. Mit ‚automatisch‘ zur Topologie-Zuordnung zurückkehren.",
542
+ );
543
+ } catch (e) {
544
+ errors.push(["pg-input-amplitudes", "Input-Kanäle", e.message]);
545
+ }
546
+ for (const id of [
547
+ "pg-target-cue-channel",
548
+ "pg-reward-cue-channel",
549
+ "pg-reward-channel",
550
+ "pg-action-feedback-channel",
551
+ "pg-posture-score-channel",
552
+ "pg-reward-event-channel",
553
+ ]) {
554
+ if (value(id) >= value("pg-input-channels"))
555
+ errors.push([
556
+ id,
557
+ "Kanalzuordnung",
558
+ `Kanal muss kleiner als ${value("pg-input-channels")} sein.`,
559
+ ]);
560
+ }
561
+ if (enabled("pg-posture-reward-enabled") && !enabled("pg-sandbox-enabled"))
562
+ warnings.push(
563
+ "Posture Reward ist aktiv, aber die Stick-Figure Sandbox ist nicht gekoppelt.",
564
+ );
565
+ if (value("pg-inhibitory-fraction") > 0 && value("pg-gaba-strength") === 0)
566
+ warnings.push(
567
+ "Inhibitorischer Anteil ohne GABA-Stärke: die inhibitorische Kopplung bleibt wirkungslos.",
568
+ );
569
+ if (
570
+ enabled("pg-action-loop-enabled") &&
571
+ !$("pg-action-to-input-map").value.trim()
572
+ )
573
+ errors.push([
574
+ "pg-action-to-input-map",
575
+ "Aktions-Loop",
576
+ "auto oder eine gültige Action → Input Map eingeben.",
577
+ ]);
578
+ if (
579
+ enabled("pg-reward-enabled") &&
580
+ $("pg-target-encoding").value !== "none" &&
581
+ value("pg-target-cue-channel") === value("pg-reward-channel")
582
+ )
583
+ warnings.push(
584
+ "Ziel und Reward verwenden denselben Kanal. Signale können sich überlagern.",
585
+ );
586
+ if (
587
+ enabled("pg-action-loop-enabled") &&
588
+ $("pg-target-encoding").value !== "none" &&
589
+ value("pg-action-feedback-channel") === value("pg-target-cue-channel")
590
+ )
591
+ warnings.push("Action-Feedback und Ziel-Cue verwenden denselben Kanal.");
592
+ if (value("pg-execution-low") >= value("pg-execution-high"))
593
+ errors.push([
594
+ "pg-execution-low",
595
+ "PAN Runtime",
596
+ "θ low muss kleiner als θ high sein.",
597
+ ]);
598
+ if (!errors.length) {
599
+ try {
600
+ payload();
601
+ } catch (e) {
602
+ errors.push(["pg-action-to-input-map", "Konfiguration", e.message]);
603
+ }
604
+ }
605
+ errors.forEach(([id, , text]) => {
606
+ const field = $(id);
607
+ if (field) {
608
+ field.setAttribute("aria-invalid", "true");
609
+ const note = document.createElement("small");
610
+ note.className = "pg-field-error";
611
+ note.textContent = text;
612
+ field.after(note);
613
+ }
614
+ });
615
+ validation.innerHTML =
616
+ errors
617
+ .map(
618
+ ([id, label, text]) =>
619
+ `<div><a href="#${id}">${escape(label)}: ${escape(text)}</a></div>`,
620
+ )
621
+ .join("") +
622
+ warnings
623
+ .map((text) => `<div class="pg-warning">Hinweis: ${escape(text)}</div>`)
624
+ .join("");
625
+ for (const id of ["pg-run", "pg-robustness", "pg-user-preset-save"]) {
626
+ if ($(id)) $(id).disabled = errors.length > 0;
627
+ }
628
+ summary.textContent = `${value("pg-neurons")} Neuronen · ${value("pg-edges")} Kanten · ${value("pg-ticks")} Ticks · Seed ${$("pg-seed").value} · ${$("pg-neuron-backend")?.value === "cuda_pan" ? "CUDA-Membran + PAN-Zustand" : $("pg-neuron-backend")?.value === "cuda_membrane" ? "CUDA-Membran / CPU-PAN" : "CPU-Referenz"}. ${errors.length ? `${errors.length} Eingabefehler.` : "Eingaben geprüft."} Laufzeit: noch keine belastbare Messung für diese Konfiguration. CUDA-PAN führt auch PAN-Zustand, Feedback und synaptische Regeln auf der GPU aus. Körper, Policy und übergeordnete Steuerung bleiben CPU; die Ergebnistabelle zeigt die tatsächlichen Komponenten.`;
629
+ return errors.length === 0;
630
+ }
631
+ validation.onclick = (e) => {
632
+ const a = e.target.closest("a");
633
+ if (a) {
634
+ e.preventDefault();
635
+ const target = $(a.hash.slice(1));
636
+ for (
637
+ let parent = target?.parentElement;
638
+ parent;
639
+ parent = parent.parentElement
640
+ )
641
+ if (parent.tagName === "DETAILS") parent.open = true;
642
+ target?.focus();
643
+ }
644
+ };
645
+ function refresh() {
646
+ renderChannels();
647
+ validate();
648
+ }
649
+ root.addEventListener("input", validate);
650
+ root.addEventListener(
651
+ "click",
652
+ (e) => {
653
+ if (
654
+ e.target.closest(
655
+ "#pg-run,#pg-robustness,#pg-live-open,#pg-night-start,#pg-cpu-determinism,#pg-cuda-compile,#pg-cuda-preflight,#pg-cuda-smoke,#pg-cuda-rng",
656
+ ) &&
657
+ !validate()
658
+ ) {
659
+ e.preventDefault();
660
+ e.stopImmediatePropagation();
661
+ validation.scrollIntoView({ block: "center" });
662
+ }
663
+ },
664
+ true,
665
+ );
666
+ root.addEventListener("change", (e) => {
667
+ if (
668
+ [
669
+ "pg-input-channels",
670
+ "pg-input-topology",
671
+ "pg-neurons",
672
+ ...channelFields,
673
+ "pg-input-channel-map",
674
+ ].includes(e.target.id)
675
+ ) {
676
+ if (e.target.id === "pg-input-channels") {
677
+ const n = Number(e.target.value);
678
+ if (Number.isInteger(n) && n >= 1 && n <= 64) {
679
+ try {
680
+ ["pg-input-channel-map", ...channelFields].forEach(
681
+ (id) =>
682
+ ($(id).value = JSON.stringify(
683
+ JSON.parse($(id).value).slice(0, n),
684
+ )),
685
+ );
686
+ } catch {}
687
+ }
688
+ }
689
+ refresh();
690
+ }
691
+ remember();
692
+ });
693
+ root.addEventListener("click", (e) => {
694
+ if (
695
+ e.target.closest(
696
+ "#pg-user-preset-apply,#pg-reset,#pg-user-preset-save,#pg-user-preset-delete",
697
+ )
698
+ ) {
699
+ refresh();
700
+ renderPresets();
701
+ remember();
702
+ }
703
+ });
704
+ $("pg-user-preset-select").addEventListener("change", () => {
705
+ refresh();
706
+ remember();
707
+ });
708
+ controls().forEach((e) =>
709
+ defaults.set(e.id, e.type === "checkbox" ? e.checked : e.value),
710
+ );
711
+ renderPresets();
712
+ refresh();
713
+ remember();
714
+
715
+ for (const id of [
716
+ "pg-run-json",
717
+ "pg-cuda-stage-state",
718
+ "pg-cuda-resource-state",
719
+ "pg-cuda-parity-state",
720
+ "pg-cuda-rng-state",
721
+ "pg-cuda-compiler-state",
722
+ ]) {
723
+ const node = $(id);
724
+ if (!node) continue;
725
+ const detail = document.createElement("details");
726
+ detail.className = "pg-raw";
727
+ detail.innerHTML = "<summary>Technische Details / JSON</summary>";
728
+ node.before(detail);
729
+ detail.append(node);
730
+ if (id.startsWith("pg-cuda-")) {
731
+ const status = document.createElement("p");
732
+ status.className = "pg-summary";
733
+ status.setAttribute("role", "status");
734
+ detail.before(status);
735
+ const update = () => {
736
+ try {
737
+ const data = JSON.parse(node.textContent);
738
+ if ("passed" in data)
739
+ status.textContent = `${data.passed ? "Bestanden" : "Nicht bestanden"}${data.parity ? ` · D2-Abweichung ${data.parity.current_max_abs_error ?? "nicht bestimmbar"} · Wiederholung ${data.gpu_repeat_exact ? "identisch" : "abweichend"}` : ""}${data.samples ? ` · ${data.samples} RNG-Aktionen` : ""}`;
740
+ else if ("ptxas_available" in data)
741
+ status.textContent = `ptxas ${data.ptxas_available ? "verfügbar" : "fehlt"} · Treiber ${data.cuda_driver_available ? "verfügbar" : "fehlt"}. Hardware-Nachweis nur durch einen aktuellen Smoke-Test.`;
742
+ else if (data.ptxas)
743
+ status.textContent = `Register ${data.ptxas.registers ?? "—"} · Spill Store/Load ${data.ptxas.spill_store_bytes ?? "—"}/${data.ptxas.spill_load_bytes ?? "—"} Byte${"ready_for_cooperative_launch" in data ? ` · Preflight ${data.ready_for_cooperative_launch ? "bereit" : "nicht bereit"}` : ""}`;
744
+ else if (data.manifest)
745
+ status.textContent =
746
+ "Gate-IR und PTX erzeugt. Nächster Schritt: ptxas + Occupancy.";
747
+ else
748
+ status.textContent =
749
+ "Diagnose verfügbar – technische Details öffnen.";
750
+ } catch {
751
+ status.textContent = node.textContent;
752
+ }
753
+ };
754
+ new MutationObserver(update).observe(node, { childList: true });
755
+ update();
756
+ }
757
+ }
758
+ const runSummary = document.createElement("div");
759
+ runSummary.id = "pg-result-summary";
760
+ runSummary.className = "pg-run-empty";
761
+ runSummary.textContent =
762
+ "Noch kein Lauf. Im Builder ein Preset wählen und starten oder eine gespeicherte Session öffnen.";
763
+ $("pg-metrics").before(runSummary);
764
+ const backendTable = document.createElement("table");
765
+ backendTable.id = "pg-backend-components";
766
+ backendTable.hidden = true;
767
+ runSummary.after(backendTable);
768
+ const resultPanels = [
769
+ ...$("playground-run").querySelectorAll(
770
+ ".playground-metrics,.playground-viz-grid,.playground-analysis-grid",
771
+ ),
772
+ ];
773
+ resultPanels.forEach((panel) => (panel.hidden = true));
774
+ new MutationObserver(() => {
775
+ const r = lastResult();
776
+ resultPanels.forEach((panel) => (panel.hidden = !r?.monitors));
777
+ if (r) {
778
+ backendTable.replaceChildren();
779
+ backendTable.hidden = !r.execution?.components;
780
+ if (r.execution?.components) {
781
+ const caption = document.createElement("caption");
782
+ caption.textContent =
783
+ "Tatsächliche Ausführung dieses Laufs · hybrider Playground, kein vollständiges CUDA-MHRN";
784
+ backendTable.append(caption);
785
+ for (const [component, backend] of Object.entries(
786
+ r.execution.components,
787
+ )) {
788
+ const row = document.createElement("tr"),
789
+ label = document.createElement("th"),
790
+ value = document.createElement("td");
791
+ label.textContent = component.replaceAll("_", " ");
792
+ value.textContent = backend === "cuda" ? "GPU (CUDA)" : "CPU";
793
+ row.append(label, value);
794
+ backendTable.append(row);
795
+ }
796
+ }
797
+ runSummary.className = "pg-summary";
798
+ runSummary.textContent = !r.session_id
799
+ ? "Robustheitskontrollen abgeschlossen. Ergebnisse stehen in den technischen Details und im Export bereit."
800
+ : `Session ${r.session_id || "—"} · Seed ${r.config?.seed ?? "—"} · ${r.execution?.neuron_backend === "cuda_pan" ? "CUDA-Membran + PAN-Zustand" : r.execution?.neuron_backend === "cuda_membrane" ? "CUDA-Membran / CPU-PAN" : "CPU-Referenz"} · ${r.metrics?.total_spikes ?? 0} Spikes · explorativer Lauf, keine wissenschaftliche Evidenz.`;
801
+ if (r.cue_decoding?.status === "DESCRIPTIVE_ONLY") {
802
+ runSummary.textContent += ` Aktivitaetsdecoder: ${(r.cue_decoding.accuracy * 100).toFixed(1)} % auf ${r.cue_decoding.test_episodes} Testepisoden; ${r.cue_decoding.input_cue_control || "aligned"}. Kein Nachweis eines neuronalen Lernvorteils.`;
803
+ }
804
+ if (r.behavioral_learning) {
805
+ const learning = r.behavioral_learning;
806
+ const contexts = Object.keys(learning.context_policies || {}).length;
807
+ runSummary.textContent += ` Lernen: ${contexts} Reiz-Kontexte · ${learning.policy_updates || 0} Updates · ${((learning.success_fraction || 0) * 100).toFixed(1)} % Erfolg bei bewerteten Aktionen.`;
808
+ if (r.closed_loop?.policy_context_source) {
809
+ runSummary.textContent +=
810
+ " Kontext aus ausgesendetem Zielreiz; kein Nachweis neuronaler Reizdekodierung.";
811
+ }
812
+ }
813
+ }
814
+ }).observe($("pg-run-json"), { childList: true });
815
+ const runTools = document.createElement("div");
816
+ runTools.className = "pg-tools";
817
+ runTools.innerHTML =
818
+ '<button type="button" id="pg-result-export">Letzten Lauf exportieren</button>';
819
+ runSummary.after(runTools);
820
+ $("pg-result-export").onclick = () => {
821
+ const r = lastResult();
822
+ if (r) download(r, "playground-result.json");
823
+ else
824
+ runSummary.textContent =
825
+ "Zuerst einen Lauf ausführen oder eine Session laden.";
826
+ };
827
+ for (const [id, target, selector, placeholder] of [
828
+ [
829
+ "pg-workspace-session-search",
830
+ "pg-session-list",
831
+ "article",
832
+ "Sessions durchsuchen …",
833
+ ],
834
+ [
835
+ "pg-workspace-catalog-search",
836
+ "pg-catalog-grid",
837
+ "article",
838
+ "Bausteine durchsuchen …",
839
+ ],
840
+ ]) {
841
+ const search = document.createElement("input");
842
+ search.id = id;
843
+ search.className = "pg-search";
844
+ search.placeholder = placeholder;
845
+ search.setAttribute("aria-label", placeholder);
846
+ $(target).before(search);
847
+ const filter = () =>
848
+ $(target)
849
+ .querySelectorAll(selector)
850
+ .forEach(
851
+ (e) =>
852
+ (e.hidden = !e.textContent
853
+ .toLowerCase()
854
+ .includes(search.value.toLowerCase())),
855
+ );
856
+ search.oninput = filter;
857
+ new MutationObserver(filter).observe($(target), { childList: true });
858
+ }
859
+ }
src/dashboard/static/frontend/modules/playground.js CHANGED
@@ -1,6 +1,7 @@
1
  "use strict";
2
 
3
  import { apiGet, apiPost, byId } from "../core/api.js";
 
4
  import { getLanguage } from "../core/i18n.js?v=i18n-fix-20260920b";
5
 
6
  let lastResult = null;
@@ -11,7 +12,7 @@ let nightPollTimer = null;
11
  let catalogInfoItems = new Map();
12
  let activeCatalogInfo = null;
13
  const PLAYGROUND_PRESETS_STORAGE = "mhrn.playground.presets.v1";
14
- const DEFAULT_PLAYGROUND_PRESET = "full_embodiment";
15
  const BUILTIN_PLAYGROUND_PRESETS = {
16
  izhikevich_reference: {
17
  label: "Izhikevich · Referenz",
@@ -123,6 +124,7 @@ function updateDefaultHints() {
123
  field.title = message;
124
  field.closest("label")?.classList.toggle("pg-non-default", changed);
125
  });
 
126
  }
127
 
128
  function injectStyles() {
@@ -142,7 +144,7 @@ function injectStyles() {
142
  .playground-status{padding:9px 11px;border:1px solid var(--rule);border-left:3px solid var(--rule-3);border-radius:var(--r-xs);background:var(--paper-2);color:var(--ink-2);font-size:.68rem;white-space:pre-wrap;overflow:auto}.playground-status[data-state="error"]{border-left-color:var(--crimson);color:var(--crimson)}.playground-status[data-state="ok"]{border-left-color:var(--moss);color:var(--moss)}
143
  .pg-live-monitor{width:min(94vw,1000px);max-height:84vh;padding:0;border:1px solid var(--rule-3);border-radius:var(--r-md);background:var(--paper);color:var(--ink);box-shadow:var(--shadow-float)}.pg-live-monitor::backdrop{background:rgba(20,16,12,.55);backdrop-filter:blur(4px)}.pg-live-monitor>header{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:9px 12px;border-bottom:1px solid var(--rule);background:var(--paper-2)}.pg-live-monitor>header h2{margin:0;font-size:.95rem}.pg-live-monitor>header p{margin:2px 0 0;color:var(--ink-3);font-size:.61rem}.pg-live-monitor-close{min-width:28px;padding:0}.pg-live-monitor-body{padding:9px;overflow:hidden}.pg-live-monitor-metrics{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:5px;margin-bottom:7px}.pg-live-monitor-metric{padding:6px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2)}.pg-live-monitor-metric span{display:block;color:var(--ink-4);font:700 .46rem/1.1 var(--font-mono);text-transform:uppercase}.pg-live-monitor-metric strong{display:block;margin-top:3px;font:600 .7rem/1 var(--font-mono)}.pg-live-monitor-grid{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(220px,1fr);gap:7px}.pg-live-monitor-panel{min-width:0;padding:7px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2)}.pg-live-monitor-panel h3{margin:0 0 5px;padding-bottom:4px;border-bottom:1px solid var(--rule);font-size:.68rem}.pg-live-monitor-panel canvas{display:block;width:100%;height:130px;background:var(--paper);border:1px solid var(--rule)}.pg-live-monitor-params{margin:0;max-height:118px;overflow:hidden;font:500 .55rem/1.25 var(--font-mono);white-space:pre-wrap}.pg-live-monitor-actions{display:flex;flex-wrap:wrap;gap:5px;margin-top:7px}.pg-live-monitor-actions button{min-height:27px;padding:0 8px;font-size:.6rem}.pg-live-monitor-actions .pg-monitor-primary{border-color:var(--accent);background:var(--accent);color:var(--paper-2)}
144
  .playground-metrics{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:5px;margin:10px 0}.playground-metric{padding:8px 9px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2)}.playground-metric span{display:block;color:var(--ink-4);font:700 .5rem/1.2 var(--font-mono);letter-spacing:.07em;text-transform:uppercase}.playground-metric strong{display:block;margin-top:4px;color:var(--ink);font:600 .82rem/1.1 var(--font-mono)}
145
- .pg-live-monitor-visuals{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:5px;margin-top:7px}.pg-live-monitor-tile{display:block;width:100%;padding:5px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2);color:var(--ink);text-align:left;cursor:pointer}.pg-live-monitor-tile:hover,.pg-live-monitor-tile:focus-visible{border-color:var(--accent);background:var(--accent-wash)}.pg-live-monitor-tile span{display:block;margin-bottom:3px;color:var(--ink-3);font:700 .47rem/1.1 var(--font-mono);text-transform:uppercase}.pg-live-monitor-tile canvas{width:100%;height:65px;background:var(--paper);border:1px solid var(--rule)}.pg-live-zoom{width:min(94vw,1100px);padding:0;border:1px solid var(--rule-3);border-radius:var(--r-md);background:var(--paper);box-shadow:var(--shadow-float)}.pg-live-zoom::backdrop{background:rgba(20,16,12,.58);backdrop-filter:blur(4px)}.pg-live-zoom header{display:flex;justify-content:space-between;align-items:center;padding:10px 13px;border-bottom:1px solid var(--rule);background:var(--paper-2)}.pg-live-zoom canvas{display:block;width:100%;height:min(70vh,600px);padding:12px;background:var(--paper)}
146
  .playground-viz-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.playground-viz{min-height:260px}.playground-viz canvas{width:100%;height:210px;display:block}
147
  .playground-analysis-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:10px}.playground-analysis-card pre{font-size:.62rem;max-height:260px;overflow:auto;white-space:pre-wrap}
148
  .playground-session-list{display:grid;gap:6px}.playground-session{display:flex;justify-content:space-between;gap:1rem;align-items:center;padding:9px 10px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2)}.playground-session small{display:block;color:var(--ink-4)}
@@ -156,10 +158,26 @@ function injectStyles() {
156
  function optionMarkup(items, labelKey = "label") {
157
  return (items || []).filter((item) => item.available !== false).map((item) => {
158
  const label = item[labelKey] || item.name;
159
- return `<option value="${item.name}" title="${item.note || ""}">${label}</option>`;
 
 
 
 
160
  }).join("");
161
  }
162
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  function ensurePermanentBoundary(root) {
164
  if (byId("pg-permanent-boundary")) return;
165
  const boundary = document.createElement("div");
@@ -177,7 +195,7 @@ function buildPanels(root) {
177
  <section data-generated-panel="builder" id="playground-builder">
178
  <div class="pg-neutral-note"><strong>Neutraler Baukasten:</strong> Keine Modell- oder Topologieauswahl ist eine Empfehlung. <code>MHRN 5D</code> ist der Architektur-Default, weil er den nativen MHRN-Vertrag <code>(x,y,z,d4,d5)</code> verwendet — nicht weil 5D als überlegen gilt.</div>
179
  <section class="playground-preset-deck" aria-labelledby="pg-preset-deck-title">
180
- <div class="playground-preset-copy"><div><h3 id="pg-preset-deck-title">Preset Lab</h3><p>Empfohlene Startpunkte für Izhikevich und PAN. Presets verändern nur den Playground und werden lokal auf diesem Gerät gespeichert.</p></div></div>
181
  <div class="playground-preset-controls"><label>Profil<select id="pg-user-preset-select"></select></label><label>Eigenes Preset speichern<input id="pg-user-preset-name" type="text" placeholder="z. B. PAN · Seed 2"></label><button type="button" id="pg-user-preset-apply">Anwenden</button><button type="button" id="pg-user-preset-save">Speichern</button><button type="button" id="pg-user-preset-delete">Eigenes löschen</button><p id="pg-user-preset-description" class="playground-preset-description">Preset auswählen oder eigene Einstellungen speichern.</p></div>
182
  </section>
183
  <div class="playground-grid">
@@ -214,7 +232,7 @@ function buildPanels(root) {
214
  <label>Seed-Ensemble<input id="pg-ensemble" type="number" min="1" max="8" value="1"></label>
215
  <label><span><input id="pg-persist" type="checkbox"> Session lokal speichern</span></label>
216
  </article>
217
- <article class="playground-card"><h3>05 · PAN-Hyperstate</h3>
218
  <label><span><input id="pg-pan-enabled" type="checkbox"> PAN explorativ aktivieren</span></label>
219
  <label>PAN-Dimensionen<input id="pg-pan-dimensions" type="number" min="5" max="32" value="5"></label>
220
  <label><span><input id="pg-pan-closed-loop" type="checkbox" checked> Closed Loop</span></label>
@@ -243,6 +261,7 @@ function buildPanels(root) {
243
  <label>Clock<select id="pg-clock-mode"><option value="continuous">Continuous</option><option value="dual">Dual · Event + Continuous</option></select></label>
244
  <label>Base Hz<input id="pg-clock-base-hz" type="number" min="1" max="10000" value="100"></label>
245
  <label>Event Batch ms<input id="pg-clock-event-batch" type="number" min="0.05" max="1000" step="0.05" value="10"></label>
 
246
  <label>Execution<select id="pg-execution-mode"><option value="HYBRID_AUTO">HYBRID_AUTO</option><option value="EVENT_ONLY">EVENT_ONLY</option><option value="TICK_ONLY">TICK_ONLY</option></select></label>
247
  <label>Initial Engine<select id="pg-execution-initial"><option value="EVENT_ONLY">EVENT_ONLY</option><option value="TICK_ONLY">TICK_ONLY</option></select></label>
248
  <label>θ high<input id="pg-execution-high" type="number" min="0" max="1" step="0.01" value="0.30"></label>
@@ -276,7 +295,7 @@ function buildPanels(root) {
276
  <label><span><input id="pg-cortical-plasticity" type="checkbox" checked> Layer-Gain plastisch</span></label>
277
  <label>Layer Lernrate<input id="pg-cortical-lr" type="number" min="0" max="1" step="0.005" value="0.01"></label>
278
  <label><span><input id="pg-behavior-enabled" type="checkbox"> Verhalten lernen</span></label>
279
- <label>Aktionen<input id="pg-behavior-actions" type="number" min="2" max="16" value="4"></label>
280
  <label>Zielaktion<input id="pg-behavior-target" type="number" min="0" max="15" value="0"></label>
281
  <label>Zielmodus<select id="pg-behavior-target-mode"><option value="cycle">cycle</option><option value="fixed">fixed</option></select></label>
282
  <label>Min. Aktivität<input id="pg-behavior-min-activity" type="number" min="0" max="1" step="0.01" value="0.01"></label>
@@ -318,7 +337,7 @@ function buildPanels(root) {
318
  <label><span><input id="pg-action-loop-enabled" type="checkbox"> Aktions-Loop aktiv</span></label>
319
  <label>Loop Delay<input id="pg-action-loop-delay" type="number" min="1" max="64" value="1"></label>
320
  <label>Persistenz<input id="pg-action-persistence" type="number" min="1" max="128" value="1"></label>
321
- <label>Aktionsraum<input id="pg-action-space-size" type="number" min="2" max="32" value="4"></label>
322
  <label>Action → Input Map<input id="pg-action-to-input-map" value="auto"></label>
323
  <label>Kopplungsstärke<input id="pg-action-coupling" type="number" min="0" max="10" step="0.1" value="0"></label>
324
  <label>Aktionsrauschen<input id="pg-action-noise" type="number" min="0" max="1" step="0.01" value="0"></label>
@@ -328,6 +347,7 @@ function buildPanels(root) {
328
  <label>Sandbox Sensor Noise<input id="pg-sandbox-sensor-noise" type="number" min="0" max="1" step="0.01" value="0.05"></label>
329
  </article>
330
  <article class="playground-card"><h3>12 · Ziel-Kodierung</h3>
 
331
  <label>Kodierung<select id="pg-target-encoding"><option value="none">none</option><option value="one_hot">one_hot</option><option value="rate">rate</option><option value="population_latency">population_latency</option></select></label>
332
  <label>Ziel sichtbar (Ticks)<input id="pg-target-persistence" type="number" min="1" max="256" value="1"></label>
333
  <label>Ziel-Cue Strom<input id="pg-target-cue-current" type="number" min="0" max="500" step="0.5" value="0"></label>
@@ -366,20 +386,41 @@ function buildPanels(root) {
366
  </article>
367
  </div>
368
  <div class="playground-actions"><button type="button" class="primary" id="pg-run">▶ Playground starten</button><button type="button" id="pg-robustness">Robustheitskontrollen</button><button type="button" id="pg-reset">Standardwerte</button></div>
369
- <article class="playground-card playground-live-launcher"><h3>17 · PAN Live Monitor</h3><p>Die laufende PAN-Session mit Start/Pause, Input, Sandbox-Männchen und allen Live-Grafiken im Monitor-Popup.</p><div class="playground-actions"><button type="button" class="primary" id="pg-live-open">Live Monitor öffnen</button><button type="button" id="pg-live-clear">Temporäre Sessions löschen</button></div><pre id="pg-live-state">Keine Live-Session geöffnet.</pre></article>
370
- <article class="playground-card"><h3>18 · CUDA & Parität</h3>
371
  <div class="playground-grid">
372
  <label>Target SM<input id="pg-cuda-target-sm" value="sm_86"></label>
373
  <label>PTX Version<input id="pg-cuda-ptx-version" value="7.1"></label>
 
 
 
 
374
  <label><span><input id="pg-freeze-actions" type="checkbox"> Actions einfrieren</span></label>
375
  <label><span><input id="pg-freeze-rewards" type="checkbox"> Rewards einfrieren</span></label>
376
  <label>Referenz-Commit<input id="pg-parity-reference-commit" placeholder="Git SHA"></label>
377
  </div>
378
  <div class="playground-actions">
379
- <button type="button" id="pg-cpu-determinism">CPU-Determinismus prüfen</button>
380
- <button type="button" id="pg-cuda-compile">Builder → Gate IR / PTX</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  </div>
382
- <small>D1 = exakte Spike-Ereignisse · D2 = numerische Zustandsparität · D3 = Verhaltens-/Metrikparität. CUDA-Codegen ist vorhanden, ein echter GPU-Kernel-Launch ist hier noch nicht nachgewiesen.</small>
383
  <pre id="pg-cuda-compiler-state">Noch kein CUDA-/Parity-Lauf.</pre>
384
  </article>
385
  <article class="playground-card"><h3>19 · Meta-Nachtlauf</h3>
@@ -431,6 +472,7 @@ function buildPanels(root) {
431
  <section data-generated-panel="run" id="playground-run">
432
  <div class="playground-metrics" id="pg-metrics"></div>
433
  <div class="playground-viz-grid">
 
434
  <article class="playground-viz"><h3>Spike Raster</h3><canvas id="pg-raster" width="800" height="300"></canvas></article>
435
  <article class="playground-viz"><h3>Population Rate</h3><canvas id="pg-rate-canvas" width="800" height="300"></canvas></article>
436
  <article class="playground-viz"><h3>Topologie · neutrale 2D-Projektion</h3><canvas id="pg-topology-canvas" width="800" height="300"></canvas></article>
@@ -522,6 +564,7 @@ function formPayload() {
522
  clock_mode: byId("pg-clock-mode").value,
523
  clock_base_hz: Number(byId("pg-clock-base-hz").value),
524
  clock_event_batch_ms: Number(byId("pg-clock-event-batch").value),
 
525
  execution_mode: byId("pg-execution-mode").value,
526
  execution_initial_mode: byId("pg-execution-initial").value,
527
  execution_theta_high: Number(byId("pg-execution-high").value),
@@ -644,6 +687,7 @@ function formPayload() {
644
  frozen_reward_sequence: lastResult?.closed_loop?.reward_history||[],
645
  parity_reference_source: "CPU_PYTHON_PLAYGROUND",
646
  parity_reference_commit: byId("pg-parity-reference-commit")?.value?.trim()||"",
 
647
  target_encoding: byId("pg-target-encoding").value,
648
  target_persistence: Number(byId("pg-target-persistence").value),
649
  target_cue_current: Number(byId("pg-target-cue-current").value),
@@ -720,14 +764,21 @@ function renderResult(result){
720
  byId("pg-analysis-spike").textContent=JSON.stringify(a.spike_time||{},null,2);
721
  byId("pg-analysis-network").textContent=JSON.stringify({network:net,dimensionality:dim,ensemble:result.ensemble||null,pan:result.pan||null,closed_loop:result.closed_loop||null,heterogeneity:result.heterogeneity||null,temporal_dynamics:result.temporal_dynamics||null,geometry:result.geometry||null,gates:result.gates||null,clock:result.clock||null,execution:result.execution||null,growth:result.growth||null,storage:result.storage||null},null,2);
722
  byId("pg-analysis-plasticity").textContent=JSON.stringify({plasticity:a.plasticity||{},performance:a.performance||{},pan:result.pan||null,growth:result.growth||null,behavioral_learning:result.behavioral_learning||null,credit_assignment:result.credit_assignment||null,closed_loop:result.closed_loop||null,thalamic_gating:result.thalamic_gating||null,cortical_organization:result.cortical_organization||null,storage:result.storage||null,hardware:result.hardware||null},null,2);
723
- byId("pg-analysis-io").textContent=JSON.stringify({neural_io:result.neural_io||{status:"disabled"},interfaces:result.interfaces||null},null,2);
724
  byId("pg-run-json").textContent=JSON.stringify({session_id:result.session_id,manifest:result.manifest,model:result.model,config:result.config,metrics:result.metrics,readout:result.readout},null,2);
 
 
 
 
 
 
 
725
  drawRaster(result);drawSeries("pg-rate-canvas",(result.monitors?.tick_spike_counts||[]).map(Number));drawTopology(result);drawState(result);drawSpectrum(result);drawDegree(result);
726
  }
727
 
728
 
729
- function drawLiveSandbox(world){
730
- const item=ctxFor("pg-live-sandbox-canvas");if(!item||!world?.joints)return;
731
  const{canvas,ctx}=item;
732
  const px=p=>[canvas.width/2+Number(p.x||0)*170,canvas.height-30-Number(p.y||0)*150];
733
  const links=[["head","neck"],["neck","hip"],["neck","shoulder_l"],["neck","shoulder_r"],["hip","knee_l"],["hip","knee_r"],["knee_l","foot_l"],["knee_r","foot_r"]];
@@ -738,12 +789,36 @@ function drawLiveSandbox(world){
738
  let liveMonitorHistory=[];
739
  let liveMonitorPaused=false;
740
  let liveMonitorWorld=null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
741
 
742
  function ensureLiveMonitor(){
743
  let dialog=byId("pg-live-monitor");if(dialog)return dialog;
744
  dialog=document.createElement("dialog");dialog.id="pg-live-monitor";dialog.className="pg-live-monitor";
745
  dialog.innerHTML=`<header><div><span class="workspace-kicker">PAN LIVE MONITOR</span><h2>Live-Session</h2><p id="pg-live-monitor-status">Bereit</p></div><button type="button" class="pg-live-monitor-close" aria-label="Monitor schließen">×</button></header><div class="pg-live-monitor-body"><div class="pg-live-monitor-metrics"><div class="pg-live-monitor-metric"><span>Tick</span><strong id="pg-live-monitor-tick">0</strong></div><div class="pg-live-monitor-metric"><span>Spikes</span><strong id="pg-live-monitor-spikes">0</strong></div><div class="pg-live-monitor-metric"><span>Aktion</span><strong id="pg-live-monitor-action">—</strong></div><div class="pg-live-monitor-metric"><span>Reward</span><strong id="pg-live-monitor-reward">0</strong></div><div class="pg-live-monitor-metric"><span>Engine</span><strong id="pg-live-monitor-engine">—</strong></div></div><div class="pg-live-monitor-visuals"><button class="pg-live-monitor-tile" data-pg-live-expand="reset"><span>Reset</span><canvas id="pg-live-tile-reset" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="figure"><span>PAN-Männchen</span><canvas id="pg-live-tile-figure" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="raster"><span>Spike Raster</span><canvas id="pg-live-tile-raster" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="rate"><span>Populationsrate</span><canvas id="pg-live-tile-rate" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="topology"><span>Topologie · live</span><canvas id="pg-live-tile-topology" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="input"><span>Input</span><canvas id="pg-live-tile-input" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="output"><span>Output</span><canvas id="pg-live-tile-output" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="membrane"><span>Membranpotential</span><canvas id="pg-live-tile-membrane" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="spectrum"><span>Spectrum</span><canvas id="pg-live-tile-spectrum" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="degree"><span>Degree</span><canvas id="pg-live-tile-degree" width="220" height="120"></canvas></button></div><div class="pg-live-monitor-grid"><section class="pg-live-monitor-panel"><h3>Aktivität / Spike-Verlauf</h3><canvas id="pg-live-monitor-chart" width="760" height="260"></canvas></section><section class="pg-live-monitor-panel"><h3>Parameter</h3><pre id="pg-live-monitor-params" class="pg-live-monitor-params">—</pre></section><section class="pg-live-monitor-panel"><h3>Session</h3><pre id="pg-live-monitor-state" class="pg-live-monitor-params">—</pre></section></div><div class="pg-live-monitor-actions"><button type="button" id="pg-live-monitor-start" class="pg-monitor-primary">Start</button><button type="button" id="pg-live-monitor-pause">Pause</button><button type="button" id="pg-live-monitor-step">+32 Ticks</button><button type="button" id="pg-live-monitor-input">Input</button><button type="button" id="pg-live-monitor-stop">Stop</button><button type="button" id="pg-live-monitor-reset">Reset Ansicht</button></div></div>`;
746
- document.body.append(dialog);
 
747
  dialog.querySelector(".pg-live-monitor-close").addEventListener("click",()=>dialog.close());
748
  dialog.querySelector("#pg-live-monitor-start").addEventListener("click",()=>{liveMonitorPaused=false;startLiveLoop().catch(error=>updateLiveMonitor({error:String(error.message||error)}));});
749
  dialog.querySelector("#pg-live-monitor-pause").addEventListener("click",()=>{liveMonitorPaused=true;stopLiveLoop();updateLiveMonitor({status:"paused"});});
@@ -765,10 +840,16 @@ function drawLiveMonitorChart(){
765
  const LIVE_TILE_TITLES={reset:"Live-Ansicht zurückgesetzt",figure:"PAN-Männchen",raster:"Spike Raster",rate:"Populationsrate",topology:"Topologie",input:"Input-Kanäle",output:"Output-Aktionen",membrane:"Membranpotential",spectrum:"Spectrum",degree:"Degree-Verteilung"};
766
  function ensureLiveZoom(){
767
  if(byId("pg-live-zoom"))return;
768
- const dialog=document.createElement("dialog");dialog.id="pg-live-zoom";dialog.className="pg-live-zoom";dialog.innerHTML=`<header><strong id="pg-live-zoom-title">Live-Grafik</strong><button type="button" id="pg-live-zoom-close" aria-label="Grafik schließen">×</button></header><canvas id="pg-live-zoom-canvas" width="1100" height="620"></canvas>`;document.body.append(dialog);dialog.querySelector("#pg-live-zoom-close").addEventListener("click",()=>dialog.close());
 
 
 
 
 
 
769
  }
770
  function openLiveZoom(kind){
771
- ensureLiveZoom();const dialog=byId("pg-live-zoom"),source=byId(`pg-live-tile-${kind}`),target=byId("pg-live-zoom-canvas");if(!dialog||!source||!target)return;byId("pg-live-zoom-title").textContent=LIVE_TILE_TITLES[kind]||kind;const ctx=target.getContext("2d");ctx.clearRect(0,0,target.width,target.height);ctx.drawImage(source,0,0,target.width,target.height);dialog.showModal();
772
  }
773
  function drawTileSeries(id,values,color){
774
  const canvas=byId(id),ctx=canvas?.getContext("2d");if(!canvas||!ctx)return;ctx.clearRect(0,0,canvas.width,canvas.height);ctx.strokeStyle=color||getComputedStyle(document.body).getPropertyValue("--accent")||"#b23a1a";ctx.lineWidth=2;ctx.beginPath();const data=values.length?values:[0];const max=Math.max(1,...data);data.forEach((value,index)=>{const x=5+index/Math.max(1,data.length-1)*(canvas.width-10);const y=canvas.height-5-(Number(value)||0)/max*(canvas.height-10);if(index===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);});ctx.stroke();
@@ -779,8 +860,8 @@ function drawLiveMonitorVisuals(payload={}){
779
  const raster=byId("pg-live-tile-raster"),rasterCtx=raster?.getContext("2d");if(raster&&rasterCtx){rasterCtx.clearRect(0,0,raster.width,raster.height);rasterCtx.fillStyle=getComputedStyle(document.body).getPropertyValue("--accent")||"#b23a1a";spikes.slice(-160).forEach(item=>{rasterCtx.fillRect((Number(item.tick)||0)%100/100*raster.width,(Number(item.neuron_id)||0)%128/128*raster.height,2,2);});}
780
  const membrane=byId("pg-live-tile-membrane"),mctx=membrane?.getContext("2d");if(membrane&&mctx){mctx.clearRect(0,0,membrane.width,membrane.height);mctx.strokeStyle=getComputedStyle(document.body).getPropertyValue("--moss")||"#2f5d47";mctx.lineWidth=2;const mean=Number(payload.mean_v??-65),min=Number(payload.min_v??-70),max=Number(payload.max_v??30);const y=membrane.height-5-(mean-min)/Math.max(1,max-min)*(membrane.height-10);mctx.beginPath();mctx.moveTo(5,y);mctx.lineTo(membrane.width-5,y);mctx.stroke();}
781
  const inputValues=[Number(payload.input_active_neurons||0),Number(payload.input_peak||0)];drawTileSeries("pg-live-tile-input",inputValues,getComputedStyle(document.body).getPropertyValue("--accent")||"#b23a1a");drawTileSeries("pg-live-tile-output",counts.length?counts:[0],getComputedStyle(document.body).getPropertyValue("--moss")||"#2f5d47");
782
- const topology=byId("pg-live-tile-topology"),tctx=topology?.getContext("2d"),topologyData=payload.topology; if(topology&&tctx){tctx.clearRect(0,0,topology.width,topology.height);const coords=topologyData?.coordinates||[],edges=topologyData?.edges||[];if(coords.length){const xs=coords.map(point=>Number(point[0]||0)),ys=coords.map(point=>Number(point[1]??point[2]??0)),minX=Math.min(...xs),maxX=Math.max(...xs),minY=Math.min(...ys),maxY=Math.max(...ys),scaleX=value=>(value-minX)/Math.max(1e-9,maxX-minX)*(topology.width-12)+6,scaleY=value=>(value-minY)/Math.max(1e-9,maxY-minY)*(topology.height-12)+6;tctx.strokeStyle=getComputedStyle(document.body).getPropertyValue("--rule-3")||"#777";tctx.globalAlpha=.25;edges.slice(0,600).forEach(edge=>{const source=coords[edge[0]],target=coords[edge[1]];if(!source||!target)return;tctx.beginPath();tctx.moveTo(scaleX(Number(source[0]||0)),scaleY(Number(source[1]??source[2]??0)));tctx.lineTo(scaleX(Number(target[0]||0)),scaleY(Number(target[1]??target[2]??0)));tctx.stroke();});tctx.globalAlpha=.9;tctx.fillStyle=getComputedStyle(document.body).getPropertyValue("--indigo")||"#2f4a7a";coords.forEach(point=>tctx.fillRect(scaleX(Number(point[0]||0))-1,scaleY(Number(point[1]??point[2]??0))-1,3,3));}}
783
- if(payload.world)liveMonitorWorld=payload.world;drawLiveMonitorFigure(liveMonitorWorld,"pg-live-tile-figure");
784
  }
785
 
786
  function drawLiveMonitorFigure(world,targetId="pg-live-tile-figure"){
@@ -792,13 +873,13 @@ function updateLiveMonitor(payload={}){
792
  const state=payload.state||payload;const tick=Number(state.tick??payload.tick??0);const spikes=Number(state.total_spikes??payload.total_spikes??0);const actions=payload.actions||state.actions||[];const rewards=payload.rewards||state.rewards||[];
793
  if(Number.isFinite(spikes)){liveMonitorHistory.push({tick,spikes});if(liveMonitorHistory.length>120)liveMonitorHistory.shift();}
794
  const set=(id,value)=>{const node=byId(id);if(node)node.textContent=String(value);};set("pg-live-monitor-tick",tick);set("pg-live-monitor-spikes",spikes);set("pg-live-monitor-action",actions.length?actions[actions.length-1]:"—");set("pg-live-monitor-reward",rewards.length?Number(rewards[rewards.length-1]).toFixed(3):"0");set("pg-live-monitor-engine",state.execution?.current_engine||payload.execution?.current_engine||"—");set("pg-live-monitor-status",payload.error||((liveMonitorPaused||!liveLoopTimer)?"Pausiert":"Läuft"));
795
- const params={session_id:liveSessionId,tick,posture_score:payload.posture_score??payload.world?.posture_score??null,reward:payload.reward??null,reward_events:payload.reward_events||[],terminal:payload.terminal||null,pan_bias_current:byId("pg-pan-bias-current")?.value,feedback_gain:byId("pg-pan-feedback-gain")?.value,learning_rate:byId("pg-behavior-lr")?.value,epsilon:byId("pg-behavior-epsilon")?.value,edge_budget:byId("pg-edges")?.value};const paramsNode=byId("pg-live-monitor-params");if(paramsNode)paramsNode.textContent=JSON.stringify(params,null,2);const stateNode=byId("pg-live-monitor-state");if(stateNode)stateNode.textContent=JSON.stringify({state_digest:state.state_digest||payload.state_digest||null,input_queue_depth:state.input_queue_depth??payload.input_queue_depth??0,learning:state.learning||payload.learning||null},null,2);drawLiveMonitorChart();drawLiveMonitorVisuals(payload);drawLiveMonitorFigure(payload.world||state.world||null);
796
  }
797
 
798
  async function createLiveSession(canRecover=true){
799
  const payload={...formPayload(),neuron_model:"pan_adex_5d",pan_enabled:true,thalamic_relay_threshold:0,pan_bias_current:Number(byId("pg-pan-bias-current").value),behavior_target_mode:byId("pg-behavior-target-mode").value};
800
  try{
801
- const result=await apiPost("/api/playground/live/create",payload);liveSessionId=result.session_id;liveMonitorHistory=[];liveMonitorWorld=null;const monitor=ensureLiveMonitor();if(!monitor.open)monitor.showModal();byId("pg-live-state").textContent=JSON.stringify(result,null,2);updateLiveMonitor(result.state||result);
802
  }catch(error){
803
  if(canRecover&&/maximum live Playground sessions reached/i.test(String(error.message||error))){await clearLiveSessions();return createLiveSession(false);}
804
  throw error;
@@ -817,16 +898,19 @@ async function clearLiveSessions(){
817
  await Promise.all(sessions.map(session=>apiPost(`/api/playground/live/${encodeURIComponent(session.session_id)}/stop`,{}).catch(()=>null)));
818
  result={classification:"PLAYGROUND_LIVE_SESSIONS_CLEARED",scientific_evidence:false,cleared:sessions.length,compatibility_cleanup:true};
819
  }
820
- liveSessionId=null;liveMonitorWorld=null;liveMonitorHistory=[];
821
  byId("pg-live-state").textContent=JSON.stringify(result,null,2);
822
  const monitor=byId("pg-live-monitor");if(monitor?.open)monitor.close();
823
  }
824
  async function stepLiveSession(){
825
  if(!liveSessionId)throw new Error("Zuerst Live-Session starten.");
826
- const result=await apiPost(`/api/playground/live/${encodeURIComponent(liveSessionId)}/step`,{ticks:32});
827
- try{const sandbox=await apiPost(`/api/playground/live/${encodeURIComponent(liveSessionId)}/sandbox`,{ticks:8});result.world=sandbox.world;liveMonitorWorld=sandbox.world;}catch{}
828
- try{Object.assign(result,await apiGet(`/api/playground/live/${encodeURIComponent(liveSessionId)}`));}catch{}
829
- byId("pg-live-state").textContent=JSON.stringify(result,null,2);updateLiveMonitor(result);
 
 
 
830
  }
831
  async function startLiveLoop(){
832
  if(!liveSessionId)throw new Error("Zuerst Live-Session starten.");
@@ -924,6 +1008,92 @@ async function checkCpuDeterminism(){
924
  return result;
925
  }
926
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
927
  async function compileCudaGates(){
928
  const node=byId("pg-cuda-compiler-state");
929
  if(node)node.textContent="Gate-IR/PTX wird erzeugt …";
@@ -949,7 +1119,7 @@ async function runSession(){
949
 
950
  async function runRobustness(){
951
  const status=byId("pg-status");status.dataset.state="running";status.textContent="Explorative Robustheitskontrollen laufen …";
952
- try{const result=await apiPost("/api/playground/robustness",{...formPayload(),persist:false,ensemble_runs:1});status.dataset.state="ok";status.textContent="Robustheitssuite abgeschlossen · nicht preregistriert · keine Evidenz";byId("pg-run-json").textContent=JSON.stringify(result,null,2);window.MHRNWorkspaceArchitecture?.selectRoute?.("playground","run");}
953
  catch(error){status.dataset.state="error";status.textContent=`Robustheitssuite fehlgeschlagen: ${error.message||error}`;}
954
  }
955
 
@@ -962,14 +1132,16 @@ function setBuilderValue(key,value){
962
  pan_feedback_gain:"pan-feedback-gain",pan_feedback_nonlinearity:"pan-feedback-nonlinearity",pan_feedback_saturation:"pan-feedback-saturation",
963
  action_loop_enabled:"action-loop-enabled",action_loop_delay:"action-loop-delay",action_persistence:"action-persistence",action_to_input_map:"action-to-input-map",action_space_size:"action-space-size",action_coupling_strength:"action-coupling",action_noise:"action-noise",
964
  reward_signal_enabled:"reward-enabled",reward_magnitude:"reward-magnitude",reward_delay_ticks:"reward-delay",reward_shaping:"reward-shaping",reward_baseline:"reward-baseline",reward_decay:"reward-decay",reward_channel:"reward-channel",
965
- target_encoding:"target-encoding",target_cue_channel:"target-cue-channel",target_cue_current:"target-cue-current",target_persistence:"target-persistence",target_shuffle:"target-shuffle",target_predictability:"target-predictability",
966
  credit_assignment:"credit-assignment",credit_window:"credit-window",eligibility_trace_tau:"eligibility-tau",td_lambda:"td-lambda",gamma_discount:"gamma-discount",
967
  neuron_threshold_variance:"threshold-variance",neuron_tau_m_variance:"tau-m-variance",inhibitory_fraction:"inhibitory-fraction",gaba_strength:"gaba-strength",e_i_ratio:"e-i-ratio",delay_distribution:"delay-distribution",delay_mean_ticks:"delay-mean",
968
  refractory_variance:"refractory-variance",adaptation_strength:"adaptation-strength",adaptation_tau:"adaptation-tau",oscillation_enabled:"oscillation-enabled",oscillation_frequency:"oscillation-frequency",
969
  input_topology:"input-topology",input_channels:"input-channels",input_noise_sigma:"input-noise",action_feedback_channel:"action-feedback-channel",reward_cue_channel:"reward-cue-channel",
970
  geometry_input_coupling:"geometry-input-coupling",geometry_input_sigma:"geometry-input-sigma",sandbox_enabled:"sandbox-enabled",sandbox_sensor_noise:"sandbox-sensor-noise",
971
- neuron_model:"neuron-model",synapse_model:"synapse-model",plasticity_rule:"plasticity",topology:"topology",readout:"readout",pan_enabled:"pan-enabled",behavior_learning_enabled:"behavior-enabled",stimulus:"stimulus"
 
972
  };
 
973
  const id="pg-"+(aliases[key]||key.replaceAll("_","-"));
974
  const el=byId(id);if(!el)return;
975
  if(el.type==="checkbox")el.checked=Boolean(value);
@@ -991,7 +1163,7 @@ function renderUserPresetOptions(){
991
  const select=byId("pg-user-preset-select");if(!select)return;
992
  const selected=select.value;
993
  const presets=allPlaygroundPresets();
994
- select.innerHTML=Object.entries(presets).map(([name,preset])=>`<option value="${name}">${preset.label||name}${BUILTIN_PLAYGROUND_PRESETS[name]?"":" · lokal"}</option>`).join("");
995
  if(selected&&presets[selected])select.value=selected;else if(!select.value)select.value=presets[DEFAULT_PLAYGROUND_PRESET]?DEFAULT_PLAYGROUND_PRESET:"pan_exploration";
996
  const preset=presets[select.value];
997
  if(byId("pg-user-preset-description"))byId("pg-user-preset-description").textContent=preset?.description||"Preset auswählen oder eigene Einstellungen speichern.";
@@ -1058,13 +1230,21 @@ function ensureCatalogInfoDialog(){
1058
  }
1059
 
1060
  function showCatalogInfo(item){
1061
- activeCatalogInfo=item;const language=catalogLanguage();const dialog=ensureCatalogInfoDialog();const category=catalogCategoryTitle(item.categoryKey||item.category||"repo",language);const description=describeCatalogItem(item,category,language);byId("pg-catalog-info-category").textContent=category;byId("pg-catalog-info-title").textContent=item.label||item.name||"—";byId("pg-catalog-info-body").innerHTML=`<p>${playgroundEscape(description)}</p>${item.source?`<p><strong>${language==="de"?"Quelle":"Source"}:</strong> <code>${playgroundEscape(item.source)}</code></p>`:""}<p><strong>${language==="de"?"Grenze":"Boundary"}:</strong> ${language==="de"?"Explorativ. Der Katalogeintrag ist keine Instanzierung und erzeugt keine wissenschaftliche Evidenz.":"Exploratory. This catalog entry is not an instantiation and does not create scientific evidence."}</p>`;dialog.querySelector("#pg-catalog-info-close").setAttribute("aria-label",language==="de"?"Bausteininfo schließen":"Close component information");dialog.showModal();
1062
  }
1063
 
1064
  function bindCatalogInfo(){
1065
  const grid=byId("pg-catalog-grid");if(!grid||grid.dataset.infoBound)return;grid.dataset.infoBound="true";grid.addEventListener("click",event=>{const button=event.target.closest("[data-pg-catalog-info]");if(!button)return;const item=catalogInfoItems.get(button.dataset.pgCatalogInfo);if(item)showCatalogInfo(item);});
1066
  }
1067
 
 
 
 
 
 
 
 
 
1068
  function renderCatalogCards(catalog){
1069
  const panCandidates=(catalog.pan?.research_candidates||[]).map(item=>({name:item.id,note:item.question}));
1070
  const panLiterature=(catalog.pan?.literature_context?.sources||[]).map(item=>({name:item.key,note:item.citation}));
@@ -1072,9 +1252,9 @@ function renderCatalogCards(catalog){
1072
  const neuralIOCodecs=(catalog.neural_io?.codecs||[]).map(item=>({name:item.id,note:`${item.input_kind||""} · ${item.reconstruction_class||""}`}));
1073
  const parityClasses=Object.entries(catalog.cuda_parity?.classes||{}).map(([name,item])=>({name,note:item.name||item.target_stage||""}));
1074
  const groups=[["models",catalog.models],["topologies",catalog.topologies],["stimuli",catalog.stimuli],["synapses",catalog.synapses],["plasticity",catalog.plasticity],["readouts",catalog.readouts],["panCandidates",panCandidates],["panLiterature",panLiterature],["geometryLiterature",geometryLiterature],["neuralIO",neuralIOCodecs],["analyses",(catalog.analyses||[]).map(name=>({name}))],["robustness",(catalog.robustness_controls||[]).map(name=>({name}))],["cudaCompiler",(catalog.cuda_gate_compiler?.gate_types||[]).map(name=>({name,note:catalog.cuda_gate_compiler?.status||""}))],["cudaParity",parityClasses]];
1075
- groups.push(["repo",PLAYGROUND_REPO_ELEMENTS.map(([name,category,description,source])=>({name,label:name,note:description,category,source}))]);
1076
  catalogInfoItems=new Map();let itemIndex=0;const language=catalogLanguage();
1077
- byId("pg-catalog-grid").innerHTML=groups.map(([categoryKey,items])=>{const title=catalogCategoryTitle(categoryKey,language);return `<article><h3>${playgroundEscape(title)}</h3>${(items||[]).map(item=>{const infoKey=String(itemIndex++);const info={...item,categoryKey};catalogInfoItems.set(infoKey,info);const description=describeCatalogItem(info,title,language);return `<button type="button" class="playground-chip" data-pg-catalog-info="${infoKey}" title="${playgroundEscape(description)}">${playgroundEscape(item.label||item.name)}</button>`;}).join("")}</article>`;}).join("");
1078
  }
1079
 
1080
  function renderCatalog(catalog){
@@ -1092,16 +1272,107 @@ function renderCatalog(catalog){
1092
  const status=byId("pg-status");status.dataset.state="ok";status.textContent=`Bereit · bis ${catalog.limits.n_neurons} Neuronen · ${catalog.limits.edges} Kanten · ${catalog.limits.dimensions}D · scientific_evidence=false`;
1093
  }
1094
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1095
  export async function initPlayground(){
1096
  const root=byId("tab-playground");if(!root)return;
1097
- injectStyles();ensurePermanentBoundary(root);buildPanels(root);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1098
  if(!root.dataset.languageBound){root.dataset.languageBound="true";document.addEventListener("mhrn:language-change",()=>{if(catalogState)renderCatalogCards(catalogState);const dialog=byId("pg-catalog-info-dialog");if(dialog?.open&&activeCatalogInfo)showCatalogInfo(activeCatalogInfo);});}
1099
- byId("pg-run")?.addEventListener("click",runSession);byId("pg-cpu-determinism")?.addEventListener("click",()=>checkCpuDeterminism().catch(error=>{const node=byId("pg-cuda-compiler-state");if(node)node.textContent=String(error.message||error);}));byId("pg-cuda-compile")?.addEventListener("click",()=>compileCudaGates().catch(error=>{const node=byId("pg-cuda-compiler-state");if(node)node.textContent=String(error.message||error);}));byId("pg-user-preset-apply")?.addEventListener("click",applyUserPreset);byId("pg-user-preset-save")?.addEventListener("click",saveUserPreset);byId("pg-user-preset-delete")?.addEventListener("click",deleteUserPreset);byId("pg-user-preset-select")?.addEventListener("change",()=>{renderUserPresetOptions();applyUserPreset();});byId("pg-robustness")?.addEventListener("click",runRobustness);byId("pg-reset")?.addEventListener("click",resetForm);byId("pg-live-open")?.addEventListener("click",()=>openLiveMonitor().catch(error=>byId("pg-live-state").textContent=String(error.message||error)));byId("pg-live-clear")?.addEventListener("click",()=>clearLiveSessions().catch(error=>byId("pg-live-state").textContent=String(error.message||error)));byId("pg-night-start")?.addEventListener("click",()=>startNightRun().catch(error=>byId("pg-night-state").textContent=String(error.message||error)));byId("pg-night-stop")?.addEventListener("click",()=>stopNightRun().catch(error=>byId("pg-night-state").textContent=String(error.message||error)));byId("pg-night-refresh")?.addEventListener("click",()=>refreshNightStatus().catch(error=>byId("pg-night-state").textContent=String(error.message||error)));
1100
  renderUserPresetOptions();
1101
  try{renderCatalog(await apiGet("/api/playground/catalog"));renderUserPresetOptions();resetForm();}catch(error){const status=byId("pg-status");status.dataset.state="error";status.textContent=`Katalog nicht verfügbar: ${error.message}`;}
 
1102
  byId("playground-builder")?.addEventListener("input", updateDefaultHints);
1103
  byId("playground-builder")?.addEventListener("change", updateDefaultHints);
 
1104
  await refreshSessions();
1105
  try{await refreshNightStatus();}catch{ /* night manager is optional during partial deployments */ }
1106
- window.MHRNPlayground={run:runSession,runRobustness,checkCpuDeterminism,compileCudaGates,refreshSessions,get catalog(){return catalogState;},get lastResult(){return lastResult;}};
 
1107
  }
 
1
  "use strict";
2
 
3
  import { apiGet, apiPost, byId } from "../core/api.js";
4
+ import { initGuidedWorkspace } from "./playground-workspace.js";
5
  import { getLanguage } from "../core/i18n.js?v=i18n-fix-20260920b";
6
 
7
  let lastResult = null;
 
12
  let catalogInfoItems = new Map();
13
  let activeCatalogInfo = null;
14
  const PLAYGROUND_PRESETS_STORAGE = "mhrn.playground.presets.v1";
15
+ const DEFAULT_PLAYGROUND_PRESET = "pan_full_balanced";
16
  const BUILTIN_PLAYGROUND_PRESETS = {
17
  izhikevich_reference: {
18
  label: "Izhikevich · Referenz",
 
124
  field.title = message;
125
  field.closest("label")?.classList.toggle("pg-non-default", changed);
126
  });
127
+ refreshCatalogOptionAvailability();
128
  }
129
 
130
  function injectStyles() {
 
144
  .playground-status{padding:9px 11px;border:1px solid var(--rule);border-left:3px solid var(--rule-3);border-radius:var(--r-xs);background:var(--paper-2);color:var(--ink-2);font-size:.68rem;white-space:pre-wrap;overflow:auto}.playground-status[data-state="error"]{border-left-color:var(--crimson);color:var(--crimson)}.playground-status[data-state="ok"]{border-left-color:var(--moss);color:var(--moss)}
145
  .pg-live-monitor{width:min(94vw,1000px);max-height:84vh;padding:0;border:1px solid var(--rule-3);border-radius:var(--r-md);background:var(--paper);color:var(--ink);box-shadow:var(--shadow-float)}.pg-live-monitor::backdrop{background:rgba(20,16,12,.55);backdrop-filter:blur(4px)}.pg-live-monitor>header{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:9px 12px;border-bottom:1px solid var(--rule);background:var(--paper-2)}.pg-live-monitor>header h2{margin:0;font-size:.95rem}.pg-live-monitor>header p{margin:2px 0 0;color:var(--ink-3);font-size:.61rem}.pg-live-monitor-close{min-width:28px;padding:0}.pg-live-monitor-body{padding:9px;overflow:hidden}.pg-live-monitor-metrics{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:5px;margin-bottom:7px}.pg-live-monitor-metric{padding:6px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2)}.pg-live-monitor-metric span{display:block;color:var(--ink-4);font:700 .46rem/1.1 var(--font-mono);text-transform:uppercase}.pg-live-monitor-metric strong{display:block;margin-top:3px;font:600 .7rem/1 var(--font-mono)}.pg-live-monitor-grid{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(220px,1fr);gap:7px}.pg-live-monitor-panel{min-width:0;padding:7px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2)}.pg-live-monitor-panel h3{margin:0 0 5px;padding-bottom:4px;border-bottom:1px solid var(--rule);font-size:.68rem}.pg-live-monitor-panel canvas{display:block;width:100%;height:130px;background:var(--paper);border:1px solid var(--rule)}.pg-live-monitor-params{margin:0;max-height:118px;overflow:hidden;font:500 .55rem/1.25 var(--font-mono);white-space:pre-wrap}.pg-live-monitor-actions{display:flex;flex-wrap:wrap;gap:5px;margin-top:7px}.pg-live-monitor-actions button{min-height:27px;padding:0 8px;font-size:.6rem}.pg-live-monitor-actions .pg-monitor-primary{border-color:var(--accent);background:var(--accent);color:var(--paper-2)}
146
  .playground-metrics{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:5px;margin:10px 0}.playground-metric{padding:8px 9px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2)}.playground-metric span{display:block;color:var(--ink-4);font:700 .5rem/1.2 var(--font-mono);letter-spacing:.07em;text-transform:uppercase}.playground-metric strong{display:block;margin-top:4px;color:var(--ink);font:600 .82rem/1.1 var(--font-mono)}
147
+ .pg-live-monitor-visuals{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:5px;margin-top:7px}.pg-live-monitor-tile{display:block;width:100%;padding:5px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2);color:var(--ink);text-align:left;cursor:pointer}.pg-live-monitor-tile:hover,.pg-live-monitor-tile:focus-visible{border-color:var(--accent);background:var(--accent-wash)}.pg-live-monitor-tile span{display:block;margin-bottom:3px;color:var(--ink-3);font:700 .47rem/1.1 var(--font-mono);text-transform:uppercase}.pg-live-monitor-tile canvas{display:block;width:100%;height:65px;background:var(--paper);border:1px solid var(--rule);image-rendering:auto}.pg-live-zoom{width:min(96vw,1400px);max-height:92vh;padding:0;border:1px solid var(--rule-3);border-radius:var(--r-md);background:var(--paper);box-shadow:var(--shadow-float)}.pg-live-zoom::backdrop{background:rgba(20,16,12,.66);backdrop-filter:blur(5px)}.pg-live-zoom header{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;padding:10px 13px;border-bottom:1px solid var(--rule);background:var(--paper-2)}.pg-live-zoom header strong{display:block;font-size:.96rem}.pg-live-zoom header p{margin:3px 0 0;color:var(--ink-3);font-size:.62rem}.pg-live-zoom-toolbar{display:flex;flex-wrap:wrap;gap:6px;padding:8px 13px;border-bottom:1px solid var(--rule);background:var(--paper-3)}.pg-live-zoom-toolbar button{min-height:30px;padding:0 9px;border:1px solid var(--rule-2);border-radius:var(--r-xs);background:var(--paper);color:var(--ink-2);font-size:.64rem}.pg-live-zoom-toolbar button:hover,.pg-live-zoom-toolbar button:focus-visible{border-color:var(--accent);background:var(--accent-wash)}.pg-live-zoom-stage{min-height:360px;padding:12px;overflow:auto;background:var(--paper)}.pg-live-zoom canvas{display:block;width:100%;height:min(72vh,760px);min-height:360px;background:var(--paper-2);border:1px solid var(--rule);image-rendering:auto}
148
  .playground-viz-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.playground-viz{min-height:260px}.playground-viz canvas{width:100%;height:210px;display:block}
149
  .playground-analysis-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:10px}.playground-analysis-card pre{font-size:.62rem;max-height:260px;overflow:auto;white-space:pre-wrap}
150
  .playground-session-list{display:grid;gap:6px}.playground-session{display:flex;justify-content:space-between;gap:1rem;align-items:center;padding:9px 10px;border:1px solid var(--rule);border-radius:var(--r-xs);background:var(--paper-2)}.playground-session small{display:block;color:var(--ink-4)}
 
158
  function optionMarkup(items, labelKey = "label") {
159
  return (items || []).filter((item) => item.available !== false).map((item) => {
160
  const label = item[labelKey] || item.name;
161
+ const executable = item.executable !== false;
162
+ const minimum = Number(item.min_dimensions || 0);
163
+ const status = executable ? "" : " · nur Katalog";
164
+ const note = item.note || (executable ? "Ausführbarer Playground-Referenzpfad." : "Nicht ausführbar; nur dokumentierter Katalogeintrag.");
165
+ return `<option value="${playgroundEscape(item.name)}" data-min-dimensions="${minimum}" data-executable="${String(executable)}" title="${playgroundEscape(note)}"${executable && !minimum ? "" : " disabled"}>${playgroundEscape(label)}${status}</option>`;
166
  }).join("");
167
  }
168
 
169
+ function refreshCatalogOptionAvailability() {
170
+ const dimensions = Number(byId("pg-dimensions")?.value || 5);
171
+ ["pg-neuron-model", "pg-synapse-model", "pg-plasticity", "pg-readout", "pg-topology", "pg-stimulus"].forEach((id) => {
172
+ byId(id)?.querySelectorAll("option").forEach((option) => {
173
+ const executable = option.dataset.executable !== "false";
174
+ const minimum = Number(option.dataset.minDimensions || 0);
175
+ option.disabled = !executable || (minimum > 0 && dimensions < minimum);
176
+ if (minimum > 0 && dimensions < minimum) option.title = `Ausführbar ab ${minimum}D; bei ${dimensions}D nur Katalogeintrag.`;
177
+ });
178
+ });
179
+ }
180
+
181
  function ensurePermanentBoundary(root) {
182
  if (byId("pg-permanent-boundary")) return;
183
  const boundary = document.createElement("div");
 
195
  <section data-generated-panel="builder" id="playground-builder">
196
  <div class="pg-neutral-note"><strong>Neutraler Baukasten:</strong> Keine Modell- oder Topologieauswahl ist eine Empfehlung. <code>MHRN 5D</code> ist der Architektur-Default, weil er den nativen MHRN-Vertrag <code>(x,y,z,d4,d5)</code> verwendet — nicht weil 5D als überlegen gilt.</div>
197
  <section class="playground-preset-deck" aria-labelledby="pg-preset-deck-title">
198
+ <div class="playground-preset-copy"><div><h3 id="pg-preset-deck-title">Preset Lab</h3><p>Empfohlene Startpunkte für Izhikevich und PAN. Presets verändern nur den Playground. Eigene Profile liegen im Browser; der Katalog kommt vom Server.</p></div></div>
199
  <div class="playground-preset-controls"><label>Profil<select id="pg-user-preset-select"></select></label><label>Eigenes Preset speichern<input id="pg-user-preset-name" type="text" placeholder="z. B. PAN · Seed 2"></label><button type="button" id="pg-user-preset-apply">Anwenden</button><button type="button" id="pg-user-preset-save">Speichern</button><button type="button" id="pg-user-preset-delete">Eigenes löschen</button><p id="pg-user-preset-description" class="playground-preset-description">Preset auswählen oder eigene Einstellungen speichern.</p></div>
200
  </section>
201
  <div class="playground-grid">
 
232
  <label>Seed-Ensemble<input id="pg-ensemble" type="number" min="1" max="8" value="1"></label>
233
  <label><span><input id="pg-persist" type="checkbox"> Session lokal speichern</span></label>
234
  </article>
235
+ <article class="playground-card" id="pg-pan-settings"><h3>05 · PAN-Hyperstate</h3>
236
  <label><span><input id="pg-pan-enabled" type="checkbox"> PAN explorativ aktivieren</span></label>
237
  <label>PAN-Dimensionen<input id="pg-pan-dimensions" type="number" min="5" max="32" value="5"></label>
238
  <label><span><input id="pg-pan-closed-loop" type="checkbox" checked> Closed Loop</span></label>
 
261
  <label>Clock<select id="pg-clock-mode"><option value="continuous">Continuous</option><option value="dual">Dual · Event + Continuous</option></select></label>
262
  <label>Base Hz<input id="pg-clock-base-hz" type="number" min="1" max="10000" value="100"></label>
263
  <label>Event Batch ms<input id="pg-clock-event-batch" type="number" min="0.05" max="1000" step="0.05" value="10"></label>
264
+ <label>Neuron-/PAN-Ausfuehrung<select id="pg-neuron-backend"><option value="cpu">CPU-Referenz</option><option value="cuda_membrane">CUDA FP64 · Membran (PAN/Koerper auf CPU)</option><option value="cuda_pan">CUDA FP64 · PAN + synaptische Regeln (Koerper/Policy/Steuerung CPU)</option></select></label>
265
  <label>Execution<select id="pg-execution-mode"><option value="HYBRID_AUTO">HYBRID_AUTO</option><option value="EVENT_ONLY">EVENT_ONLY</option><option value="TICK_ONLY">TICK_ONLY</option></select></label>
266
  <label>Initial Engine<select id="pg-execution-initial"><option value="EVENT_ONLY">EVENT_ONLY</option><option value="TICK_ONLY">TICK_ONLY</option></select></label>
267
  <label>θ high<input id="pg-execution-high" type="number" min="0" max="1" step="0.01" value="0.30"></label>
 
295
  <label><span><input id="pg-cortical-plasticity" type="checkbox" checked> Layer-Gain plastisch</span></label>
296
  <label>Layer Lernrate<input id="pg-cortical-lr" type="number" min="0" max="1" step="0.005" value="0.01"></label>
297
  <label><span><input id="pg-behavior-enabled" type="checkbox"> Verhalten lernen</span></label>
298
+ <label>Aktionen<input id="pg-behavior-actions" type="number" min="1" max="16" value="4"></label>
299
  <label>Zielaktion<input id="pg-behavior-target" type="number" min="0" max="15" value="0"></label>
300
  <label>Zielmodus<select id="pg-behavior-target-mode"><option value="cycle">cycle</option><option value="fixed">fixed</option></select></label>
301
  <label>Min. Aktivität<input id="pg-behavior-min-activity" type="number" min="0" max="1" step="0.01" value="0.01"></label>
 
337
  <label><span><input id="pg-action-loop-enabled" type="checkbox"> Aktions-Loop aktiv</span></label>
338
  <label>Loop Delay<input id="pg-action-loop-delay" type="number" min="1" max="64" value="1"></label>
339
  <label>Persistenz<input id="pg-action-persistence" type="number" min="1" max="128" value="1"></label>
340
+ <label>Aktionsraum<input id="pg-action-space-size" type="number" min="1" max="32" value="4"></label>
341
  <label>Action → Input Map<input id="pg-action-to-input-map" value="auto"></label>
342
  <label>Kopplungsstärke<input id="pg-action-coupling" type="number" min="0" max="10" step="0.1" value="0"></label>
343
  <label>Aktionsrauschen<input id="pg-action-noise" type="number" min="0" max="1" step="0.01" value="0"></label>
 
347
  <label>Sandbox Sensor Noise<input id="pg-sandbox-sensor-noise" type="number" min="0" max="1" step="0.01" value="0.05"></label>
348
  </article>
349
  <article class="playground-card"><h3>12 · Ziel-Kodierung</h3>
350
+ <label>Cue-Kontrolle<select id="pg-target-cue-control"><option value="aligned">Zielreiz passend</option><option value="randomized">Zielreiz unabhaengig randomisiert</option><option value="absent">Zielreiz entfernt</option></select></label>
351
  <label>Kodierung<select id="pg-target-encoding"><option value="none">none</option><option value="one_hot">one_hot</option><option value="rate">rate</option><option value="population_latency">population_latency</option></select></label>
352
  <label>Ziel sichtbar (Ticks)<input id="pg-target-persistence" type="number" min="1" max="256" value="1"></label>
353
  <label>Ziel-Cue Strom<input id="pg-target-cue-current" type="number" min="0" max="500" step="0.5" value="0"></label>
 
386
  </article>
387
  </div>
388
  <div class="playground-actions"><button type="button" class="primary" id="pg-run">▶ Playground starten</button><button type="button" id="pg-robustness">Robustheitskontrollen</button><button type="button" id="pg-reset">Standardwerte</button></div>
389
+ <article class="playground-card playground-live-launcher" id="pg-pan-live-monitor"><h3>17 · PAN Live Monitor</h3><p>Die laufende PAN-Session mit Start/Pause, Input, Sandbox-Männchen und allen Live-Grafiken im Monitor-Popup.</p><div class="playground-actions"><button type="button" class="primary" id="pg-live-open">Live Monitor öffnen</button><button type="button" id="pg-live-clear">Temporäre Sessions löschen</button></div><pre id="pg-live-state">Keine Live-Session geöffnet.</pre></article>
390
+ <article class="playground-card" id="pg-cuda-controls"><h3>18 · CUDA & Parität</h3>
391
  <div class="playground-grid">
392
  <label>Target SM<input id="pg-cuda-target-sm" value="sm_86"></label>
393
  <label>PTX Version<input id="pg-cuda-ptx-version" value="7.1"></label>
394
+ <label>GPU Neuronen<input id="pg-cuda-neurons" type="number" min="1" max="12288" value="256"></label>
395
+ <label>Blockgröße<select id="pg-cuda-block-size"><option>32</option><option selected>64</option><option>128</option><option>256</option><option>512</option></select></label>
396
+ <label>Device<input id="pg-cuda-device" type="number" min="0" value="0"></label>
397
+ <label>RNG Samples<input id="pg-cuda-rng-samples" type="number" min="1" max="4096" value="1000"></label>
398
  <label><span><input id="pg-freeze-actions" type="checkbox"> Actions einfrieren</span></label>
399
  <label><span><input id="pg-freeze-rewards" type="checkbox"> Rewards einfrieren</span></label>
400
  <label>Referenz-Commit<input id="pg-parity-reference-commit" placeholder="Git SHA"></label>
401
  </div>
402
  <div class="playground-actions">
403
+ <button type="button" id="pg-cuda-status">CUDA-Status</button>
404
+ <button type="button" id="pg-cpu-determinism">CPU-Determinismus</button>
405
+ <button type="button" id="pg-cuda-compile">Gate IR / PTX</button>
406
+ <button type="button" id="pg-cuda-preflight">ptxas + Occupancy</button>
407
+ <button type="button" class="primary" id="pg-cuda-smoke">RTX Hardware-Smoke</button>
408
+ <button type="button" id="pg-cuda-rng">RNG-Parität</button>
409
+ <button type="button" id="pg-cuda-recurrent">CUDA-1.4 · 100 Ticks / 3 Blöcke</button>
410
+ <button type="button" id="pg-synaptic-transfer">Synaptischer Transfer · Kontrollvergleich</button>
411
+ <button type="button" id="pg-cue-controls">Cue-Decoding · 6 Kontrollen</button>
412
+ <button type="button" id="pg-cuda-builder-parity">D3 · Builder CPU/CUDA vergleichen</button>
413
+ <button type="button" id="pg-cuda-plasticity">CUDA-1.5 · STP/STDP-Parität</button>
414
+ </div>
415
+ <small>D1 = exakte Spike-Ereignisse · D2 = numerische Zustandsparität · D3 = Verhaltens-/Metrikparität. CUDA-1.3 prüft den 17-Parameter-Gate-ABI gegen dieselbe CPU-Gate-Referenz. GPU-Portierung von Membran-/Adaptations-/Refractory-State, Synapsen, Delays, Plastizität und Sandbox folgt in CUDA-1.4 bis 1.6.</small>
416
+ <div class="playground-analysis-grid">
417
+ <article class="playground-analysis-card"><h3>CUDA Reifegrad</h3><pre id="pg-cuda-stage-state">Status noch nicht geladen.</pre></article>
418
+ <article class="playground-analysis-card"><h3>ptxas & Occupancy</h3><pre id="pg-cuda-resource-state">Noch kein Preflight.</pre></article>
419
+ <article class="playground-analysis-card"><h3>D2 Hardware-Parität</h3><pre id="pg-cuda-parity-state">Noch kein Hardware-Smoke.</pre></article>
420
+ <article class="playground-analysis-card"><h3>RNG ε-greedy</h3><pre id="pg-cuda-rng-state">Noch kein RNG-Paritätstest.</pre></article>
421
+ <article class="playground-analysis-card"><h3>Rekurrente Membran-Parität</h3><p>129 AdEx-Neuronen, statische Synapsen und Delays; FP64-Referenz. Noch kein vollständiger PAN-GPU-Lauf.</p><pre id="pg-cuda-recurrent-state">Noch kein Mehrtakt-Test.</pre></article>
422
+ <article class="playground-analysis-card"><h3>GPU-Plastizität</h3><p>STP, STDP, Eligibility und eingefrorene Reward-Folge; FP64-Referenz. Der Live-Regelkreis ist ein separater Schritt.</p><pre id="pg-cuda-plasticity-state">Noch kein Plastizitätstest.</pre><p>Builder-D3: echte Aktionen und Koerpertrajektorie; Membran auf GPU, Synapsen und Umwelt auf CPU.</p><pre id="pg-cuda-builder-parity-state">Noch kein Builder-Vergleich.</pre><p>Cue-Experiment: passend / randomisiert / entfernt, jeweils mit und ohne Pair-STDP. Ohne Policy-Strom und Koerperrueckkopplung; kein automatischer Lernnachweis.</p><pre id="pg-cue-controls-state">Noch kein Cue-Experiment.</pre><p id="pg-transfer-summary">Transfer: vortrainierte Gewichte gegen frische Initialisierung auf neuen Cue-Kanaelen. Alle anderen Zustaende werden zurueckgesetzt.</p><details><summary>Transfer-Protokoll und Decoder-Kurven</summary><pre id="pg-transfer-state" style="max-height:360px;overflow:auto">Noch kein Transfer-Vergleich.</pre></details></article>
423
  </div>
 
424
  <pre id="pg-cuda-compiler-state">Noch kein CUDA-/Parity-Lauf.</pre>
425
  </article>
426
  <article class="playground-card"><h3>19 · Meta-Nachtlauf</h3>
 
472
  <section data-generated-panel="run" id="playground-run">
473
  <div class="playground-metrics" id="pg-metrics"></div>
474
  <div class="playground-viz-grid">
475
+ <article class="playground-viz" id="pg-run-sandbox-panel" hidden><h3>PAN-Strichmann</h3><canvas id="pg-run-sandbox-canvas" width="800" height="360"></canvas><input id="pg-run-sandbox-frame" style="width:100%" type="range" min="0" max="0" value="0" aria-label="Strichmann-Zeitschritt"><p id="pg-run-sandbox-summary" aria-live="polite"></p><details><summary>Technische Frame-Daten</summary><pre id="pg-run-sandbox-state" style="max-height:240px;overflow:auto"></pre></details></article>
476
  <article class="playground-viz"><h3>Spike Raster</h3><canvas id="pg-raster" width="800" height="300"></canvas></article>
477
  <article class="playground-viz"><h3>Population Rate</h3><canvas id="pg-rate-canvas" width="800" height="300"></canvas></article>
478
  <article class="playground-viz"><h3>Topologie · neutrale 2D-Projektion</h3><canvas id="pg-topology-canvas" width="800" height="300"></canvas></article>
 
564
  clock_mode: byId("pg-clock-mode").value,
565
  clock_base_hz: Number(byId("pg-clock-base-hz").value),
566
  clock_event_batch_ms: Number(byId("pg-clock-event-batch").value),
567
+ neuron_backend: byId("pg-neuron-backend").value,
568
  execution_mode: byId("pg-execution-mode").value,
569
  execution_initial_mode: byId("pg-execution-initial").value,
570
  execution_theta_high: Number(byId("pg-execution-high").value),
 
687
  frozen_reward_sequence: lastResult?.closed_loop?.reward_history||[],
688
  parity_reference_source: "CPU_PYTHON_PLAYGROUND",
689
  parity_reference_commit: byId("pg-parity-reference-commit")?.value?.trim()||"",
690
+ target_cue_control: byId("pg-target-cue-control").value,
691
  target_encoding: byId("pg-target-encoding").value,
692
  target_persistence: Number(byId("pg-target-persistence").value),
693
  target_cue_current: Number(byId("pg-target-cue-current").value),
 
764
  byId("pg-analysis-spike").textContent=JSON.stringify(a.spike_time||{},null,2);
765
  byId("pg-analysis-network").textContent=JSON.stringify({network:net,dimensionality:dim,ensemble:result.ensemble||null,pan:result.pan||null,closed_loop:result.closed_loop||null,heterogeneity:result.heterogeneity||null,temporal_dynamics:result.temporal_dynamics||null,geometry:result.geometry||null,gates:result.gates||null,clock:result.clock||null,execution:result.execution||null,growth:result.growth||null,storage:result.storage||null},null,2);
766
  byId("pg-analysis-plasticity").textContent=JSON.stringify({plasticity:a.plasticity||{},performance:a.performance||{},pan:result.pan||null,growth:result.growth||null,behavioral_learning:result.behavioral_learning||null,credit_assignment:result.credit_assignment||null,closed_loop:result.closed_loop||null,thalamic_gating:result.thalamic_gating||null,cortical_organization:result.cortical_organization||null,storage:result.storage||null,hardware:result.hardware||null},null,2);
767
+ byId("pg-analysis-io").textContent=JSON.stringify({neural_io:result.neural_io||{status:"disabled"},cue_decoding:result.cue_decoding||null,interfaces:result.interfaces||null},null,2);
768
  byId("pg-run-json").textContent=JSON.stringify({session_id:result.session_id,manifest:result.manifest,model:result.model,config:result.config,metrics:result.metrics,readout:result.readout},null,2);
769
+ const sandboxPanel=byId("pg-run-sandbox-panel"),sandboxSlider=byId("pg-run-sandbox-frame");
770
+ sandboxPanel.hidden=!result.sandbox?.world;
771
+ const frames=result.sandbox?.frames||[];
772
+ sandboxSlider.max=String(Math.max(0,frames.length-1));sandboxSlider.value=sandboxSlider.max;
773
+ const renderSandboxFrame=()=>{const frame=frames[Number(sandboxSlider.value)]||result.sandbox?.world;drawLiveSandbox(frame,"pg-run-sandbox-canvas");byId("pg-run-sandbox-summary").textContent=`Schritt ${Math.max(1,(result.sandbox?.ticks||frames.length)-frames.length+Number(sandboxSlider.value)+1)} / ${result.sandbox?.ticks||frames.length} · Haltung ${(Number(frame?.posture_score||0)*100).toFixed(1)} % · Aktion ${frame?.pan_action??"keine"} · Reward ${Number(frame?.reward||0).toFixed(3)}${frame?.terminal?` · ${frame.terminal}`:""}`;byId("pg-run-sandbox-state").textContent=JSON.stringify({backend:result.sandbox?.backend,ticks:result.sandbox?.ticks,dt_seconds:result.sandbox?.dt_seconds,posture_credit_updates:result.sandbox?.posture_credit_updates,frame},null,2);};
774
+ sandboxSlider.oninput=renderSandboxFrame;
775
+ if(!sandboxPanel.hidden)renderSandboxFrame();
776
  drawRaster(result);drawSeries("pg-rate-canvas",(result.monitors?.tick_spike_counts||[]).map(Number));drawTopology(result);drawState(result);drawSpectrum(result);drawDegree(result);
777
  }
778
 
779
 
780
+ function drawLiveSandbox(world,canvasId="pg-live-sandbox-canvas"){
781
+ const item=ctxFor(canvasId);if(!item||!world?.joints)return;
782
  const{canvas,ctx}=item;
783
  const px=p=>[canvas.width/2+Number(p.x||0)*170,canvas.height-30-Number(p.y||0)*150];
784
  const links=[["head","neck"],["neck","hip"],["neck","shoulder_l"],["neck","shoulder_r"],["hip","knee_l"],["hip","knee_r"],["knee_l","foot_l"],["knee_r","foot_r"]];
 
789
  let liveMonitorHistory=[];
790
  let liveMonitorPaused=false;
791
  let liveMonitorWorld=null;
792
+ let liveZoomKind=null;
793
+ let liveUpdateFactor=1;
794
+ let liveStepBusy=false;
795
+ let liveVisualFrame=0;
796
+ let pendingLivePayload=null;
797
+ let liveMonitorTopology=null;
798
+ let liveTopologyRefreshCounter=0;
799
+
800
+ function prepareLiveTileCanvases(root){
801
+ root.querySelectorAll(".pg-live-monitor-tile canvas").forEach(canvas=>{canvas.width=880;canvas.height=480;canvas.dataset.pgLiveHiDpi="true";});
802
+ }
803
+
804
+ function resizeLiveZoomCanvas(){
805
+ const canvas=byId("pg-live-zoom-canvas");if(!canvas)return;
806
+ const bounds=canvas.getBoundingClientRect();const ratio=Math.min(2,window.devicePixelRatio||1);const width=Math.max(640,Math.round(bounds.width*ratio));const height=Math.max(360,Math.round(bounds.height*ratio));
807
+ if(canvas.width!==width||canvas.height!==height){canvas.width=width;canvas.height=height;}
808
+ }
809
+
810
+ function refreshLiveZoom(){
811
+ if(!liveZoomKind)return;
812
+ const dialog=byId("pg-live-zoom"),source=byId(`pg-live-tile-${liveZoomKind}`),target=byId("pg-live-zoom-canvas");if(!dialog?.open||!source||!target)return;
813
+ resizeLiveZoomCanvas();const ctx=target.getContext("2d");ctx.imageSmoothingEnabled=false;ctx.clearRect(0,0,target.width,target.height);ctx.drawImage(source,0,0,target.width,target.height);
814
+ }
815
 
816
  function ensureLiveMonitor(){
817
  let dialog=byId("pg-live-monitor");if(dialog)return dialog;
818
  dialog=document.createElement("dialog");dialog.id="pg-live-monitor";dialog.className="pg-live-monitor";
819
  dialog.innerHTML=`<header><div><span class="workspace-kicker">PAN LIVE MONITOR</span><h2>Live-Session</h2><p id="pg-live-monitor-status">Bereit</p></div><button type="button" class="pg-live-monitor-close" aria-label="Monitor schließen">×</button></header><div class="pg-live-monitor-body"><div class="pg-live-monitor-metrics"><div class="pg-live-monitor-metric"><span>Tick</span><strong id="pg-live-monitor-tick">0</strong></div><div class="pg-live-monitor-metric"><span>Spikes</span><strong id="pg-live-monitor-spikes">0</strong></div><div class="pg-live-monitor-metric"><span>Aktion</span><strong id="pg-live-monitor-action">—</strong></div><div class="pg-live-monitor-metric"><span>Reward</span><strong id="pg-live-monitor-reward">0</strong></div><div class="pg-live-monitor-metric"><span>Engine</span><strong id="pg-live-monitor-engine">—</strong></div></div><div class="pg-live-monitor-visuals"><button class="pg-live-monitor-tile" data-pg-live-expand="reset"><span>Reset</span><canvas id="pg-live-tile-reset" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="figure"><span>PAN-Männchen</span><canvas id="pg-live-tile-figure" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="raster"><span>Spike Raster</span><canvas id="pg-live-tile-raster" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="rate"><span>Populationsrate</span><canvas id="pg-live-tile-rate" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="topology"><span>Topologie · live</span><canvas id="pg-live-tile-topology" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="input"><span>Input</span><canvas id="pg-live-tile-input" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="output"><span>Output</span><canvas id="pg-live-tile-output" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="membrane"><span>Membranpotential</span><canvas id="pg-live-tile-membrane" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="spectrum"><span>Spectrum</span><canvas id="pg-live-tile-spectrum" width="220" height="120"></canvas></button><button class="pg-live-monitor-tile" data-pg-live-expand="degree"><span>Degree</span><canvas id="pg-live-tile-degree" width="220" height="120"></canvas></button></div><div class="pg-live-monitor-grid"><section class="pg-live-monitor-panel"><h3>Aktivität / Spike-Verlauf</h3><canvas id="pg-live-monitor-chart" width="760" height="260"></canvas></section><section class="pg-live-monitor-panel"><h3>Parameter</h3><pre id="pg-live-monitor-params" class="pg-live-monitor-params">—</pre></section><section class="pg-live-monitor-panel"><h3>Session</h3><pre id="pg-live-monitor-state" class="pg-live-monitor-params">—</pre></section></div><div class="pg-live-monitor-actions"><button type="button" id="pg-live-monitor-start" class="pg-monitor-primary">Start</button><button type="button" id="pg-live-monitor-pause">Pause</button><button type="button" id="pg-live-monitor-step">+32 Ticks</button><button type="button" id="pg-live-monitor-input">Input</button><button type="button" id="pg-live-monitor-stop">Stop</button><button type="button" id="pg-live-monitor-reset">Reset Ansicht</button></div></div>`;
820
+ document.body.append(dialog);prepareLiveTileCanvases(dialog);
821
+ const actions=dialog.querySelector(".pg-live-monitor-actions");if(actions&&!byId("pg-live-update-factor")){const scale=document.createElement("label");scale.className="pg-live-update-scale";scale.innerHTML='<span>UI-Update</span><select id="pg-live-update-factor" aria-label="Simulation steps per UI update"><option value="1">1x</option><option value="5">5x</option><option value="10">10x</option><option value="25">25x</option><option value="50">50x</option><option value="100">100x</option></select>';actions.prepend(scale);scale.querySelector("select").addEventListener("change",event=>{liveUpdateFactor=Math.max(1,Number(event.target.value)||1);updateLiveZoomState();});}
822
  dialog.querySelector(".pg-live-monitor-close").addEventListener("click",()=>dialog.close());
823
  dialog.querySelector("#pg-live-monitor-start").addEventListener("click",()=>{liveMonitorPaused=false;startLiveLoop().catch(error=>updateLiveMonitor({error:String(error.message||error)}));});
824
  dialog.querySelector("#pg-live-monitor-pause").addEventListener("click",()=>{liveMonitorPaused=true;stopLiveLoop();updateLiveMonitor({status:"paused"});});
 
840
  const LIVE_TILE_TITLES={reset:"Live-Ansicht zurückgesetzt",figure:"PAN-Männchen",raster:"Spike Raster",rate:"Populationsrate",topology:"Topologie",input:"Input-Kanäle",output:"Output-Aktionen",membrane:"Membranpotential",spectrum:"Spectrum",degree:"Degree-Verteilung"};
841
  function ensureLiveZoom(){
842
  if(byId("pg-live-zoom"))return;
843
+ const dialog=document.createElement("dialog");dialog.id="pg-live-zoom";dialog.className="pg-live-zoom";dialog.innerHTML=`<header><div><span class="workspace-kicker">LIVE DETAIL VIEW</span><strong id="pg-live-zoom-title">Live-Grafik</strong><p id="pg-live-zoom-state">Bereit</p></div><button type="button" id="pg-live-zoom-close" aria-label="Grafik schließen">×</button></header><div class="pg-live-zoom-toolbar"><button type="button" id="pg-live-zoom-start">Start</button><button type="button" id="pg-live-zoom-pause">Pause</button><button type="button" id="pg-live-zoom-step">Schritt</button><button type="button" id="pg-live-zoom-input">Input senden</button><button type="button" id="pg-live-zoom-reset">Reset</button><button type="button" id="pg-live-zoom-stop">Stop</button></div><div class="pg-live-zoom-stage"><canvas id="pg-live-zoom-canvas" width="1600" height="900"></canvas></div>`;document.body.append(dialog);dialog.querySelector("#pg-live-zoom-close").addEventListener("click",()=>{liveZoomKind=null;dialog.close();});dialog.querySelector("#pg-live-zoom-start").addEventListener("click",()=>startLiveLoop().catch(error=>updateLiveZoomState(error.message)));dialog.querySelector("#pg-live-zoom-pause").addEventListener("click",()=>{stopLiveLoop();updateLiveZoomState("Pausiert");});dialog.querySelector("#pg-live-zoom-step").addEventListener("click",()=>stepLiveSession().catch(error=>updateLiveZoomState(error.message)));dialog.querySelector("#pg-live-zoom-input").addEventListener("click",()=>injectLiveInput().catch(error=>updateLiveZoomState(error.message)));dialog.querySelector("#pg-live-zoom-reset").addEventListener("click",()=>{resetForm();liveMonitorHistory=[];drawLiveMonitorVisuals({});updateLiveZoomState("Ansicht zurückgesetzt");});dialog.querySelector("#pg-live-zoom-stop").addEventListener("click",()=>stopLiveSession().catch(error=>updateLiveZoomState(error.message)));dialog.addEventListener("close",()=>{liveZoomKind=null;});window.addEventListener("resize",refreshLiveZoom);
844
+ }
845
+ function currentLiveUpdateFactor(){return Math.max(1,Number(byId("pg-live-update-factor")?.value)||liveUpdateFactor);}
846
+ function updateLiveZoomState(message){const node=byId("pg-live-zoom-state");if(node)node.textContent=`${String(message||((liveMonitorPaused||!liveLoopTimer)?"Pausiert":"Läuft"))} · ${currentLiveUpdateFactor()}x UI-Update`;}
847
+ function ensureLiveZoomScaleControl(){
848
+ const toolbar=byId("pg-live-zoom")?.querySelector(".pg-live-zoom-toolbar");if(!toolbar||byId("pg-live-zoom-factor"))return;
849
+ const label=document.createElement("label");label.textContent="UI-Update";const select=document.createElement("select");select.id="pg-live-zoom-factor";select.setAttribute("aria-label","Simulation steps per UI update");select.innerHTML='<option value="1">1x</option><option value="5">5x</option><option value="10">10x</option><option value="25">25x</option><option value="50">50x</option><option value="100">100x</option>';select.value=String(liveUpdateFactor);select.addEventListener("change",event=>{liveUpdateFactor=Math.max(1,Number(event.target.value)||1);const monitorSelect=byId("pg-live-update-factor");if(monitorSelect)monitorSelect.value=String(liveUpdateFactor);updateLiveZoomState();});label.prepend(select);toolbar.prepend(label);
850
  }
851
  function openLiveZoom(kind){
852
+ ensureLiveZoom();ensureLiveZoomScaleControl();const dialog=byId("pg-live-zoom"),source=byId(`pg-live-tile-${kind}`);if(!dialog||!source)return;liveZoomKind=kind;byId("pg-live-zoom-title").textContent=LIVE_TILE_TITLES[kind]||kind;dialog.showModal();resizeLiveZoomCanvas();refreshLiveZoom();updateLiveZoomState();
853
  }
854
  function drawTileSeries(id,values,color){
855
  const canvas=byId(id),ctx=canvas?.getContext("2d");if(!canvas||!ctx)return;ctx.clearRect(0,0,canvas.width,canvas.height);ctx.strokeStyle=color||getComputedStyle(document.body).getPropertyValue("--accent")||"#b23a1a";ctx.lineWidth=2;ctx.beginPath();const data=values.length?values:[0];const max=Math.max(1,...data);data.forEach((value,index)=>{const x=5+index/Math.max(1,data.length-1)*(canvas.width-10);const y=canvas.height-5-(Number(value)||0)/max*(canvas.height-10);if(index===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);});ctx.stroke();
 
860
  const raster=byId("pg-live-tile-raster"),rasterCtx=raster?.getContext("2d");if(raster&&rasterCtx){rasterCtx.clearRect(0,0,raster.width,raster.height);rasterCtx.fillStyle=getComputedStyle(document.body).getPropertyValue("--accent")||"#b23a1a";spikes.slice(-160).forEach(item=>{rasterCtx.fillRect((Number(item.tick)||0)%100/100*raster.width,(Number(item.neuron_id)||0)%128/128*raster.height,2,2);});}
861
  const membrane=byId("pg-live-tile-membrane"),mctx=membrane?.getContext("2d");if(membrane&&mctx){mctx.clearRect(0,0,membrane.width,membrane.height);mctx.strokeStyle=getComputedStyle(document.body).getPropertyValue("--moss")||"#2f5d47";mctx.lineWidth=2;const mean=Number(payload.mean_v??-65),min=Number(payload.min_v??-70),max=Number(payload.max_v??30);const y=membrane.height-5-(mean-min)/Math.max(1,max-min)*(membrane.height-10);mctx.beginPath();mctx.moveTo(5,y);mctx.lineTo(membrane.width-5,y);mctx.stroke();}
862
  const inputValues=[Number(payload.input_active_neurons||0),Number(payload.input_peak||0)];drawTileSeries("pg-live-tile-input",inputValues,getComputedStyle(document.body).getPropertyValue("--accent")||"#b23a1a");drawTileSeries("pg-live-tile-output",counts.length?counts:[0],getComputedStyle(document.body).getPropertyValue("--moss")||"#2f5d47");
863
+ if(payload.topology)liveMonitorTopology=payload.topology;const topology=byId("pg-live-tile-topology"),tctx=topology?.getContext("2d"),topologyData=payload.topology||liveMonitorTopology; if(topology&&tctx){tctx.clearRect(0,0,topology.width,topology.height);const coords=topologyData?.coordinates||[],edges=topologyData?.edges||[];if(coords.length){const xs=coords.map(point=>Number(point[0]||0)),ys=coords.map(point=>Number(point[1]??point[2]??0)),minX=Math.min(...xs),maxX=Math.max(...xs),minY=Math.min(...ys),maxY=Math.max(...ys),scaleX=value=>(value-minX)/Math.max(1e-9,maxX-minX)*(topology.width-12)+6,scaleY=value=>(value-minY)/Math.max(1e-9,maxY-minY)*(topology.height-12)+6;tctx.strokeStyle=getComputedStyle(document.body).getPropertyValue("--rule-3")||"#777";tctx.globalAlpha=.25;edges.slice(0,600).forEach(edge=>{const source=coords[edge[0]],target=coords[edge[1]];if(!source||!target)return;tctx.beginPath();tctx.moveTo(scaleX(Number(source[0]||0)),scaleY(Number(source[1]??source[2]??0)));tctx.lineTo(scaleX(Number(target[0]||0)),scaleY(Number(target[1]??target[2]??0)));tctx.stroke();});tctx.globalAlpha=.9;tctx.fillStyle=getComputedStyle(document.body).getPropertyValue("--indigo")||"#2f4a7a";coords.forEach(point=>tctx.fillRect(scaleX(Number(point[0]||0))-1,scaleY(Number(point[1]??point[2]??0))-1,3,3));}}
864
+ if(payload.world)liveMonitorWorld=payload.world;drawLiveMonitorFigure(liveMonitorWorld,"pg-live-tile-figure");refreshLiveZoom();
865
  }
866
 
867
  function drawLiveMonitorFigure(world,targetId="pg-live-tile-figure"){
 
873
  const state=payload.state||payload;const tick=Number(state.tick??payload.tick??0);const spikes=Number(state.total_spikes??payload.total_spikes??0);const actions=payload.actions||state.actions||[];const rewards=payload.rewards||state.rewards||[];
874
  if(Number.isFinite(spikes)){liveMonitorHistory.push({tick,spikes});if(liveMonitorHistory.length>120)liveMonitorHistory.shift();}
875
  const set=(id,value)=>{const node=byId(id);if(node)node.textContent=String(value);};set("pg-live-monitor-tick",tick);set("pg-live-monitor-spikes",spikes);set("pg-live-monitor-action",actions.length?actions[actions.length-1]:"—");set("pg-live-monitor-reward",rewards.length?Number(rewards[rewards.length-1]).toFixed(3):"0");set("pg-live-monitor-engine",state.execution?.current_engine||payload.execution?.current_engine||"—");set("pg-live-monitor-status",payload.error||((liveMonitorPaused||!liveLoopTimer)?"Pausiert":"Läuft"));
876
+ const params={session_id:liveSessionId,tick,posture_score:payload.posture_score??payload.world?.posture_score??null,reward:payload.reward??null,reward_events:payload.reward_events||[],terminal:payload.terminal||null,pan_bias_current:byId("pg-pan-bias-current")?.value,feedback_gain:byId("pg-pan-feedback-gain")?.value,learning_rate:byId("pg-behavior-lr")?.value,epsilon:byId("pg-behavior-epsilon")?.value,edge_budget:byId("pg-edges")?.value};const paramsNode=byId("pg-live-monitor-params");if(paramsNode)paramsNode.textContent=JSON.stringify(params,null,2);const stateNode=byId("pg-live-monitor-state");if(stateNode)stateNode.textContent=JSON.stringify({state_digest:state.state_digest||payload.state_digest||null,input_queue_depth:state.input_queue_depth??payload.input_queue_depth??0,learning:state.learning||payload.learning||null},null,2);pendingLivePayload=payload;if(!liveVisualFrame){liveVisualFrame=requestAnimationFrame(()=>{liveVisualFrame=0;const nextPayload=pendingLivePayload||{};pendingLivePayload=null;drawLiveMonitorChart();drawLiveMonitorVisuals(nextPayload);drawLiveMonitorFigure(nextPayload.world||(nextPayload.state||{}).world||null);});}updateLiveZoomState(payload.error);
877
  }
878
 
879
  async function createLiveSession(canRecover=true){
880
  const payload={...formPayload(),neuron_model:"pan_adex_5d",pan_enabled:true,thalamic_relay_threshold:0,pan_bias_current:Number(byId("pg-pan-bias-current").value),behavior_target_mode:byId("pg-behavior-target-mode").value};
881
  try{
882
+ const result=await apiPost("/api/playground/live/create",payload);liveSessionId=result.session_id;liveMonitorHistory=[];liveMonitorWorld=null;liveMonitorTopology=null;liveTopologyRefreshCounter=0;const monitor=ensureLiveMonitor();if(!monitor.open)monitor.showModal();byId("pg-live-state").textContent=JSON.stringify(result,null,2);updateLiveMonitor(result.state||result);
883
  }catch(error){
884
  if(canRecover&&/maximum live Playground sessions reached/i.test(String(error.message||error))){await clearLiveSessions();return createLiveSession(false);}
885
  throw error;
 
898
  await Promise.all(sessions.map(session=>apiPost(`/api/playground/live/${encodeURIComponent(session.session_id)}/stop`,{}).catch(()=>null)));
899
  result={classification:"PLAYGROUND_LIVE_SESSIONS_CLEARED",scientific_evidence:false,cleared:sessions.length,compatibility_cleanup:true};
900
  }
901
+ liveSessionId=null;liveMonitorWorld=null;liveMonitorTopology=null;liveTopologyRefreshCounter=0;liveMonitorHistory=[];
902
  byId("pg-live-state").textContent=JSON.stringify(result,null,2);
903
  const monitor=byId("pg-live-monitor");if(monitor?.open)monitor.close();
904
  }
905
  async function stepLiveSession(){
906
  if(!liveSessionId)throw new Error("Zuerst Live-Session starten.");
907
+ if(liveStepBusy)return null;
908
+ liveStepBusy=true;
909
+ try{
910
+ const factor=currentLiveUpdateFactor();const includeTopology=liveMonitorTopology===null||liveTopologyRefreshCounter++%10===0;const result=await apiPost(`/api/playground/live/${encodeURIComponent(liveSessionId)}/step`,{ticks:Math.min(4096,32*factor),include_topology:includeTopology});
911
+ try{const sandbox=await apiPost(`/api/playground/live/${encodeURIComponent(liveSessionId)}/sandbox`,{ticks:Math.min(64,Math.max(8,8*factor))});result.world=sandbox.world;liveMonitorWorld=sandbox.world;}catch{}
912
+ byId("pg-live-state").textContent=JSON.stringify(result,null,2);updateLiveMonitor(result);return result;
913
+ } finally {liveStepBusy=false;}
914
  }
915
  async function startLiveLoop(){
916
  if(!liveSessionId)throw new Error("Zuerst Live-Session starten.");
 
1008
  return result;
1009
  }
1010
 
1011
+ function cudaRequestPayload(){
1012
+ return {
1013
+ ...formPayload(),
1014
+ target_sm:byId("pg-cuda-target-sm")?.value||"sm_86",
1015
+ ptx_version:byId("pg-cuda-ptx-version")?.value||"7.1",
1016
+ n_neurons:Number(byId("pg-cuda-neurons")?.value||256),
1017
+ block_size:Number(byId("pg-cuda-block-size")?.value||64),
1018
+ device_ordinal:Number(byId("pg-cuda-device")?.value||0),
1019
+ reference_commit:byId("pg-parity-reference-commit")?.value||"",
1020
+ rng_samples:Number(byId("pg-cuda-rng-samples")?.value||1000),
1021
+ };
1022
+ }
1023
+
1024
+ async function refreshCudaStatus(){
1025
+ const node=byId("pg-cuda-stage-state");
1026
+ if(node)node.textContent="CUDA-Laufzeit wird geprüft …";
1027
+ const result=await apiGet("/api/playground/cuda/status");
1028
+ if(node)node.textContent=JSON.stringify({
1029
+ target_default:result.target_default,
1030
+ ptxas_available:result.ptxas_available,
1031
+ cuda_driver_available:result.cuda_driver_available,
1032
+ cuda_driver_error:result.cuda_driver_error,
1033
+ stages:result.stages,
1034
+ application_cpu:result.application_cpu,
1035
+ gpu_porting:result.gpu_porting,
1036
+ verification:result.verification,
1037
+ },null,2);
1038
+ return result;
1039
+ }
1040
+
1041
+ async function runCudaPreflight(){
1042
+ const node=byId("pg-cuda-resource-state");
1043
+ if(node)node.textContent="ptxas + Driver-Load + Occupancy werden geprüft …";
1044
+ const result=await apiPost("/api/playground/cuda/preflight",cudaRequestPayload());
1045
+ if(node)node.textContent=JSON.stringify({
1046
+ execution_status:result.execution_status,
1047
+ ready_for_cooperative_launch:result.ready_for_cooperative_launch,
1048
+ ptxas:result.ptxas,
1049
+ cooperative:result.cooperative,
1050
+ },null,2);
1051
+ return result;
1052
+ }
1053
+
1054
+ async function runCudaHardwareSmoke(){
1055
+ const node=byId("pg-cuda-parity-state");
1056
+ if(node)node.textContent="GPU-Kernel wird zweimal ausgeführt und gegen CPU-Gate-ABI verglichen …";
1057
+ const payload={...cudaRequestPayload(),n_neurons:Math.min(Number(byId("pg-cuda-neurons")?.value||64),4096)};
1058
+ const result=await apiPost("/api/playground/cuda/smoke",payload);
1059
+ const first=result.first||{},ptxas=first.ptxas||{};
1060
+ if(node)node.textContent=JSON.stringify({
1061
+ passed:result.passed,
1062
+ gpu_repeat_exact:result.gpu_repeat_exact,
1063
+ parity:result.parity,
1064
+ launch:first.launch,
1065
+ registers:ptxas.registers,
1066
+ shared_bytes:ptxas.shared_bytes,
1067
+ spill_store_bytes:ptxas.spill_store_bytes,
1068
+ spill_load_bytes:ptxas.spill_load_bytes,
1069
+ cleanup_contract:result.cleanup_contract,
1070
+ },null,2);
1071
+ const resources=byId("pg-cuda-resource-state");
1072
+ if(resources)resources.textContent=JSON.stringify({
1073
+ ptxas:first.ptxas||null,
1074
+ launch:first.launch||null,
1075
+ },null,2);
1076
+ return result;
1077
+ }
1078
+
1079
+ async function runCudaRngParity(){
1080
+ const node=byId("pg-cuda-rng-state");
1081
+ if(node)node.textContent="ε-greedy-Hashpfad wird CPU ↔ CUDA verglichen …";
1082
+ const result=await apiPost("/api/playground/cuda/rng-parity",cudaRequestPayload());
1083
+ if(node)node.textContent=JSON.stringify(result,null,2);
1084
+ return result;
1085
+ }
1086
+
1087
+ async function runCudaRecurrentParity(plasticity=false){
1088
+ plasticity=plasticity===true;
1089
+ const node=byId(plasticity?"pg-cuda-plasticity-state":"pg-cuda-recurrent-state");
1090
+ node.textContent="100 Ticks mit kooperativem Grid und Delay-Ring werden geprüft …";
1091
+ try {
1092
+ const result=await apiPost("/api/playground/cuda/recurrent-parity",{n_neurons:129,ticks:100,block_size:64,plasticity,target_sm:byId("pg-cuda-target-sm")?.value||"sm_86"});
1093
+ node.textContent=JSON.stringify(result,null,2);
1094
+ } catch(error) { node.textContent=String(error.message||error); }
1095
+ }
1096
+
1097
  async function compileCudaGates(){
1098
  const node=byId("pg-cuda-compiler-state");
1099
  if(node)node.textContent="Gate-IR/PTX wird erzeugt …";
 
1119
 
1120
  async function runRobustness(){
1121
  const status=byId("pg-status");status.dataset.state="running";status.textContent="Explorative Robustheitskontrollen laufen …";
1122
+ try{const result=await apiPost("/api/playground/robustness",{...formPayload(),persist:false,ensemble_runs:1});status.dataset.state="ok";status.textContent="Robustheitssuite abgeschlossen · nicht preregistriert · keine Evidenz";lastResult=result;byId("pg-run-json").textContent=JSON.stringify(result,null,2);window.MHRNWorkspaceArchitecture?.selectRoute?.("playground","run");}
1123
  catch(error){status.dataset.state="error";status.textContent=`Robustheitssuite fehlgeschlagen: ${error.message||error}`;}
1124
  }
1125
 
 
1132
  pan_feedback_gain:"pan-feedback-gain",pan_feedback_nonlinearity:"pan-feedback-nonlinearity",pan_feedback_saturation:"pan-feedback-saturation",
1133
  action_loop_enabled:"action-loop-enabled",action_loop_delay:"action-loop-delay",action_persistence:"action-persistence",action_to_input_map:"action-to-input-map",action_space_size:"action-space-size",action_coupling_strength:"action-coupling",action_noise:"action-noise",
1134
  reward_signal_enabled:"reward-enabled",reward_magnitude:"reward-magnitude",reward_delay_ticks:"reward-delay",reward_shaping:"reward-shaping",reward_baseline:"reward-baseline",reward_decay:"reward-decay",reward_channel:"reward-channel",
1135
+ target_cue_control:"target-cue-control",target_encoding:"target-encoding",target_cue_channel:"target-cue-channel",target_cue_current:"target-cue-current",target_persistence:"target-persistence",target_shuffle:"target-shuffle",target_predictability:"target-predictability",
1136
  credit_assignment:"credit-assignment",credit_window:"credit-window",eligibility_trace_tau:"eligibility-tau",td_lambda:"td-lambda",gamma_discount:"gamma-discount",
1137
  neuron_threshold_variance:"threshold-variance",neuron_tau_m_variance:"tau-m-variance",inhibitory_fraction:"inhibitory-fraction",gaba_strength:"gaba-strength",e_i_ratio:"e-i-ratio",delay_distribution:"delay-distribution",delay_mean_ticks:"delay-mean",
1138
  refractory_variance:"refractory-variance",adaptation_strength:"adaptation-strength",adaptation_tau:"adaptation-tau",oscillation_enabled:"oscillation-enabled",oscillation_frequency:"oscillation-frequency",
1139
  input_topology:"input-topology",input_channels:"input-channels",input_noise_sigma:"input-noise",action_feedback_channel:"action-feedback-channel",reward_cue_channel:"reward-cue-channel",
1140
  geometry_input_coupling:"geometry-input-coupling",geometry_input_sigma:"geometry-input-sigma",sandbox_enabled:"sandbox-enabled",sandbox_sensor_noise:"sandbox-sensor-noise",
1141
+ neuron_model:"neuron-model",synapse_model:"synapse-model",plasticity_rule:"plasticity",topology:"topology",readout:"readout",pan_enabled:"pan-enabled",behavior_learning_enabled:"behavior-enabled",stimulus:"stimulus",
1142
+ weight_decay:"weight-decay",weight_max_clamp:"weight-max-clamp",persist:"persist",clock_mode:"clock-mode",clock_base_hz:"clock-base-hz",clock_event_batch_ms:"clock-event-batch",neuron_backend:"neuron-backend",execution_mode:"execution-mode",execution_initial_mode:"execution-initial",execution_theta_high:"execution-high",execution_theta_low:"execution-low",execution_hysteresis:"execution-hysteresis",execution_min_dwell:"execution-dwell",execution_activity_window:"execution-window",execution_transition_mode:"execution-transition",execution_sync_on_switch:"execution-sync",execution_log_transitions:"execution-log",growth_enabled:"growth-enabled",growth_activity_threshold:"growth-activity",growth_coactivation_threshold:"growth-coactivation",growth_information_threshold:"growth-info",growth_prune_threshold:"growth-prune",growth_max_synapses_per_neuron:"growth-max-synapses",growth_max_new_synapses_per_barrier:"growth-max-new",hardware_profile_name:"hardware-profile",cuda_budget_mb:"cuda-budget",offload_enabled:"offload-enabled",offload_snapshot_interval:"offload-snapshot",thalamic_gating_enabled:"thalamic-enabled",thalamic_relay_threshold:"thalamic-threshold",thalamic_attention_gain:"thalamic-attention",thalamic_inhibition_gain:"thalamic-inhibition",cortical_layers_enabled:"cortical-enabled",cortical_plasticity:"cortical-plasticity",cortical_learning_rate:"cortical-lr",behavior_target_mode:"behavior-target-mode",behavior_min_activity:"behavior-min-activity",behavior_learning_rate:"behavior-lr",behavior_epsilon:"behavior-epsilon",geometry_lambda_a:"geometry-lambda-a",geometry_lambda_b:"geometry-lambda-b",geometry_sigma:"geometry-sigma",geometry_p0:"geometry-p0",geometry_mode:"geometry-mode",geometry_delay_velocity:"geometry-delay-velocity",neural_io_enabled:"neural-io-enabled",neural_io_input_channels:"neural-io-input-channels",neural_io_output_channels:"neural-io-output-channels",neural_io_input_codec:"neural-io-codec",neural_io_output_decoder:"neural-io-decoder",neural_io_window_ticks:"neural-io-window",neural_io_input_current:"neural-io-current",neural_io_input_role:"neural-io-input-role",neural_io_output_role:"neural-io-output-role",neural_io_phase:"neural-io-phase",neural_io_modality:"neural-io-modality",neural_io_source_id:"neural-io-source",posture_score_channel:"posture-score-channel",reward_event_channel:"reward-event-channel",posture_current_scale:"posture-current-scale",reward_event_scale:"reward-event-scale",posture_reward_enabled:"posture-reward-enabled",posture_weight_upright:"posture-weight-upright",posture_weight_height:"posture-weight-height",posture_weight_stability:"posture-weight-stability",posture_weight_symmetry:"posture-weight-symmetry",posture_target_height:"posture-target-height",posture_tilt_max:"posture-tilt-max",posture_velocity_max:"posture-velocity-max",trigger_good_score:"trigger-good-score",trigger_good_duration:"trigger-good-duration",trigger_good_reward:"trigger-good-reward",trigger_warning_score:"trigger-warning-score",trigger_warning_reward:"trigger-warning-reward",trigger_falling_rate:"trigger-falling-rate",trigger_falling_reward:"trigger-falling-reward",trigger_collapse_score:"trigger-collapse-score",trigger_collapse_reward:"trigger-collapse-reward",trigger_recovery_bonus:"trigger-recovery-bonus",reward_continuous_alpha:"reward-continuous-alpha",episode_termination_enabled:"episode-termination-enabled",episode_max_ticks:"episode-max-ticks",episode_reset_on_collapse:"episode-reset-on-collapse",pan_feedback_delay:"pan-feedback-delay",pan_feedback_source:"pan-feedback-source",pan_feedback_target:"pan-feedback-target",pan_feedback_threshold:"pan-feedback-threshold",freeze_actions:"freeze-actions",freeze_rewards:"freeze-rewards",parity_reference_commit:"parity-reference-commit"
1143
  };
1144
+ if(key==="neural_io_input_payload"){const field=byId("pg-neural-io-payload");if(field)field.value=typeof value==="string"?value:JSON.stringify(value);return;}
1145
  const id="pg-"+(aliases[key]||key.replaceAll("_","-"));
1146
  const el=byId(id);if(!el)return;
1147
  if(el.type==="checkbox")el.checked=Boolean(value);
 
1163
  const select=byId("pg-user-preset-select");if(!select)return;
1164
  const selected=select.value;
1165
  const presets=allPlaygroundPresets();
1166
+ select.innerHTML=Object.entries(presets).map(([name,preset])=>`<option value="${playgroundEscape(name)}">${playgroundEscape(preset.label||name)} · ${preset.source==="catalog"?"Katalog":BUILTIN_PLAYGROUND_PRESETS[name]?"integriert":"Browser"}</option>`).join("");
1167
  if(selected&&presets[selected])select.value=selected;else if(!select.value)select.value=presets[DEFAULT_PLAYGROUND_PRESET]?DEFAULT_PLAYGROUND_PRESET:"pan_exploration";
1168
  const preset=presets[select.value];
1169
  if(byId("pg-user-preset-description"))byId("pg-user-preset-description").textContent=preset?.description||"Preset auswählen oder eigene Einstellungen speichern.";
 
1230
  }
1231
 
1232
  function showCatalogInfo(item){
1233
+ activeCatalogInfo=item;const language=catalogLanguage();const dialog=ensureCatalogInfoDialog();const categoryKey=item.categoryKey||item.category||"repo";const category=catalogCategoryTitle(categoryKey,language);const description=describeCatalogItem(item,category,language);const execution=catalogExecutionLabel(item,categoryKey,language);byId("pg-catalog-info-category").textContent=category;byId("pg-catalog-info-title").textContent=item.label||item.name||"—";byId("pg-catalog-info-body").innerHTML=`<p><strong>${language==="de"?"Ausführbarkeit":"Execution"}:</strong> ${playgroundEscape(execution)}</p><p>${playgroundEscape(description)}</p>${item.source?`<p><strong>${language==="de"?"Quelle":"Source"}:</strong> <code>${playgroundEscape(item.source)}</code></p>`:""}<p><strong>${language==="de"?"Grenze":"Boundary"}:</strong> ${language==="de"?"Explorativ. Der Katalogeintrag ist keine Instanzierung und erzeugt keine wissenschaftliche Evidenz.":"Exploratory. This catalog entry is not an instantiation and does not create scientific evidence."}</p>`;dialog.querySelector("#pg-catalog-info-close").setAttribute("aria-label",language==="de"?"Bausteininfo schließen":"Close component information");dialog.showModal();
1234
  }
1235
 
1236
  function bindCatalogInfo(){
1237
  const grid=byId("pg-catalog-grid");if(!grid||grid.dataset.infoBound)return;grid.dataset.infoBound="true";grid.addEventListener("click",event=>{const button=event.target.closest("[data-pg-catalog-info]");if(!button)return;const item=catalogInfoItems.get(button.dataset.pgCatalogInfo);if(item)showCatalogInfo(item);});
1238
  }
1239
 
1240
+ function catalogExecutionLabel(item, categoryKey, language=catalogLanguage()){
1241
+ const nonExecutableCategory=["panCandidates","panLiterature","geometryLiterature","neuralIO","analyses","robustness","cudaCompiler","cudaParity","repo"].includes(categoryKey);
1242
+ const minimum=Number(item.min_dimensions||0);const dimensions=Number(byId("pg-dimensions")?.value||5);
1243
+ if (item.executable===false||nonExecutableCategory) return language==="de"?"Nur Katalog / nicht direkt ausführbar":"Catalog-only / not directly executable";
1244
+ if (minimum>0&&dimensions<minimum) return language==="de"?`Nicht ausführbar bei ${dimensions}D; benötigt ${minimum}D`:`Not executable at ${dimensions}D; requires ${minimum}D`;
1245
+ return language==="de"?"Ausführbarer Referenzpfad":"Executable reference path";
1246
+ }
1247
+
1248
  function renderCatalogCards(catalog){
1249
  const panCandidates=(catalog.pan?.research_candidates||[]).map(item=>({name:item.id,note:item.question}));
1250
  const panLiterature=(catalog.pan?.literature_context?.sources||[]).map(item=>({name:item.key,note:item.citation}));
 
1252
  const neuralIOCodecs=(catalog.neural_io?.codecs||[]).map(item=>({name:item.id,note:`${item.input_kind||""} · ${item.reconstruction_class||""}`}));
1253
  const parityClasses=Object.entries(catalog.cuda_parity?.classes||{}).map(([name,item])=>({name,note:item.name||item.target_stage||""}));
1254
  const groups=[["models",catalog.models],["topologies",catalog.topologies],["stimuli",catalog.stimuli],["synapses",catalog.synapses],["plasticity",catalog.plasticity],["readouts",catalog.readouts],["panCandidates",panCandidates],["panLiterature",panLiterature],["geometryLiterature",geometryLiterature],["neuralIO",neuralIOCodecs],["analyses",(catalog.analyses||[]).map(name=>({name}))],["robustness",(catalog.robustness_controls||[]).map(name=>({name}))],["cudaCompiler",(catalog.cuda_gate_compiler?.gate_types||[]).map(name=>({name,note:catalog.cuda_gate_compiler?.status||""}))],["cudaParity",parityClasses]];
1255
+ groups.push(["repo",PLAYGROUND_REPO_ELEMENTS.map(([name,category,description,source])=>({name,label:name,note:description,category,source,executable:false}))]);
1256
  catalogInfoItems=new Map();let itemIndex=0;const language=catalogLanguage();
1257
+ byId("pg-catalog-grid").innerHTML=groups.map(([categoryKey,items])=>{const title=catalogCategoryTitle(categoryKey,language);return `<article><h3>${playgroundEscape(title)}</h3>${(items||[]).map(item=>{const infoKey=String(itemIndex++);const info={...item,categoryKey};catalogInfoItems.set(infoKey,info);const description=describeCatalogItem(info,title,language);const execution=catalogExecutionLabel(info,categoryKey,language);return `<button type="button" class="playground-chip" data-pg-catalog-info="${infoKey}" title="${playgroundEscape(`${execution}. ${description}`)}">${playgroundEscape(item.label||item.name)}</button>`;}).join("")}</article>`;}).join("");
1258
  }
1259
 
1260
  function renderCatalog(catalog){
 
1272
  const status=byId("pg-status");status.dataset.state="ok";status.textContent=`Bereit · bis ${catalog.limits.n_neurons} Neuronen · ${catalog.limits.edges} Kanten · ${catalog.limits.dimensions}D · scientific_evidence=false`;
1273
  }
1274
 
1275
+
1276
+ function integrationStatusLabel(status){
1277
+ return {
1278
+ INTEGRATED:"In MHRN integriert",
1279
+ READY_FOR_NEXT_WAVE:"Bereit für nächste Welle",
1280
+ READY_FOR_WAVE4:"Bereit für Wave 4",
1281
+ READY_AFTER_BACKEND_CONTRACT:"Backend-Vertrag zuerst",
1282
+ BLOCKED_CONTRACT_FREEZE:"Contract-Freeze erforderlich",
1283
+ BLOCKED_FROZEN_ENVIRONMENT:"Frozen-Environment erforderlich",
1284
+ FE_CONTRACT_EXECUTABLE_CUDA_D3_PENDING:"FE-Vertrag ausführbar · CUDA-D3 offen",
1285
+ FE_CONTRACT_EXECUTABLE_BUILDER_D3_BRIDGE_AVAILABLE_FE3_PENDING:"FE-Vertrag ausführbar · Builder-D3c Hardware-Bridge verfügbar · FE-3 Live-Adapter offen",
1286
+ BLOCKED_PAN_SEMANTICS:"PAN-Semantik erforderlich",
1287
+ SEMANTIC_CONTRACT_DRAFT:"Semantikvertrag in Arbeit",
1288
+ WAVE6_CONTRACT_DRAFT:"Wave 6 · Structural-Approval-Draft",
1289
+ WAVE7_CONTRACT_DRAFT:"Wave 7 · Learning-Contract-Draft",
1290
+ RETAINED_NOT_CORE:"Unter OLD behalten",
1291
+ FE3_LIVE_ADAPTER_IMPLEMENTED_HARDWARE_ACCEPTANCE_PENDING:"FE-3 Adapter integriert · Hardware-Abnahme offen",
1292
+ }[status]||status;
1293
+ }
1294
+
1295
+ function ensureMhrnIntegrationPanel(){
1296
+ const builder=byId("playground-builder");
1297
+ if(!builder||byId("pg-mhrn-integration"))return;
1298
+ const panel=document.createElement("section");
1299
+ panel.id="pg-mhrn-integration";
1300
+ panel.className="playground-card pg-wide";
1301
+ panel.innerHTML=`
1302
+ <h3>MHRN Integration</h3>
1303
+ <p>Playground-Bausteine kontrolliert in die kanonische MHRN-Struktur übernehmen. Der Transfer erzeugt keine DATA/EVID und verändert niemals Repository-Quellcode zur Laufzeit.</p>
1304
+ <div class="pg-summary"><strong>Richtung:</strong> Playground → MHRN. MHRN importiert niemals Playground.</div>
1305
+ <div id="pg-mhrn-integration-list" class="playground-analysis-grid"><p>Integrationsstatus wird geladen …</p></div>
1306
+ <pre id="pg-mhrn-integration-state" class="pg-config-preview">Noch keine Transferprüfung ausgeführt.</pre>
1307
+ `;
1308
+ const grid=builder.querySelector(".playground-grid");
1309
+ if(grid)grid.before(panel);else builder.append(panel);
1310
+ }
1311
+
1312
+ function renderIntegrationCatalog(catalog){
1313
+ const root=byId("pg-mhrn-integration-list");if(!root)return;
1314
+ const candidates=catalog?.candidates||[];
1315
+ root.innerHTML=candidates.map(item=>`
1316
+ <article class="playground-analysis-card" data-pg-integration-card="${playgroundEscape(item.element_id)}">
1317
+ <h4>${playgroundEscape(item.label)}</h4>
1318
+ <p><strong>${playgroundEscape(integrationStatusLabel(item.status))}</strong></p>
1319
+ <p>${playgroundEscape(item.notes||"")}</p>
1320
+ <small>Quelle: ${playgroundEscape((item.source_paths||[]).join(", ")||"—")}<br>Ziel: ${playgroundEscape((item.target_paths||[]).join(", ")||"OLD / kein Core-Ziel")}<br>Nächstes Gate: ${playgroundEscape(item.next_gate||"—")}</small>
1321
+ ${(item.old_routes||[]).length?`<p><small>OLD: ${playgroundEscape(item.old_routes.join(", "))}</small></p>`:""}
1322
+ <button type="button" data-pg-transfer="${playgroundEscape(item.element_id)}">${item.status==="INTEGRATED"?"Übertragung prüfen":"Übernahme vorbereiten"}</button>
1323
+ </article>
1324
+ `).join("");
1325
+ }
1326
+
1327
+ async function refreshIntegrationCatalog(){
1328
+ ensureMhrnIntegrationPanel();
1329
+ const catalog=await apiGet("/api/playground/integration");
1330
+ renderIntegrationCatalog(catalog);
1331
+ return catalog;
1332
+ }
1333
+
1334
+ async function transferIntegrationElement(elementId){
1335
+ const state=byId("pg-mhrn-integration-state");
1336
+ if(state)state.textContent=`${elementId}: Transfer-Gate wird geprüft …`;
1337
+ try{
1338
+ const result=await apiPost("/api/playground/integration/transfer",{element_id:elementId});
1339
+ if(state)state.textContent=JSON.stringify(result,null,2);
1340
+ await refreshIntegrationCatalog();
1341
+ return result;
1342
+ }catch(error){
1343
+ if(state)state.textContent=`Transferprüfung fehlgeschlagen: ${error.message||error}`;
1344
+ throw error;
1345
+ }
1346
+ }
1347
+
1348
  export async function initPlayground(){
1349
  const root=byId("tab-playground");if(!root)return;
1350
+ injectStyles();ensurePermanentBoundary(root);buildPanels(root);ensureMhrnIntegrationPanel();
1351
+ root.addEventListener("click",event=>{const button=event.target.closest("[data-pg-transfer]");if(button)transferIntegrationElement(button.dataset.pgTransfer).catch(()=>{});});
1352
+ byId("pg-synaptic-transfer")?.addEventListener("click",async()=>{
1353
+ const summary=byId("pg-transfer-summary"),node=byId("pg-transfer-state");summary.textContent="Vortraining und zwei gepaarte Transferlaeufe ...";
1354
+ try{const result=await apiPost("/api/playground/research/synaptic-transfer",formPayload());node.textContent=JSON.stringify(result,null,2);summary.textContent=`Vergleich abgeschlossen · ${result.changed_pretraining_weights} veraenderte Gewichte. Externer Aktivitaetsdecoder; kein automatischer Nachweis schnellerer neuronaler Aufgabenloesung.`;}catch(error){summary.textContent=`Transfer-Vergleich fehlgeschlagen: ${error.message}`;}
1355
+ });
1356
+ byId("pg-cue-controls")?.addEventListener("click",async()=>{
1357
+ const node=byId("pg-cue-controls-state");node.textContent="Sechs gepaarte Kontrolllaeufe ...";
1358
+ try{node.textContent=JSON.stringify(await apiPost("/api/playground/research/cue-controls",formPayload()),null,2);}catch(error){node.textContent=`Cue-Kontrollen fehlgeschlagen: ${error.message}`;}
1359
+ });
1360
+ byId("pg-cuda-builder-parity")?.addEventListener("click",async()=>{
1361
+ const node=byId("pg-cuda-builder-parity-state");node.textContent="CPU- und CUDA-Builder laufen ...";
1362
+ try{node.textContent=JSON.stringify(await apiPost("/api/playground/cuda/builder-parity",formPayload()),null,2);}catch(error){node.textContent=`Builder-Paritaet fehlgeschlagen: ${error.message}`;}
1363
+ });
1364
+ byId("pg-cuda-recurrent")?.addEventListener("click",runCudaRecurrentParity);
1365
+ byId("pg-cuda-plasticity")?.addEventListener("click",()=>runCudaRecurrentParity(true));
1366
  if(!root.dataset.languageBound){root.dataset.languageBound="true";document.addEventListener("mhrn:language-change",()=>{if(catalogState)renderCatalogCards(catalogState);const dialog=byId("pg-catalog-info-dialog");if(dialog?.open&&activeCatalogInfo)showCatalogInfo(activeCatalogInfo);});}
1367
+ byId("pg-run")?.addEventListener("click",runSession);byId("pg-cuda-status")?.addEventListener("click",()=>refreshCudaStatus().catch(error=>{const node=byId("pg-cuda-stage-state");if(node)node.textContent=String(error.message||error);}));byId("pg-cpu-determinism")?.addEventListener("click",()=>checkCpuDeterminism().catch(error=>{const node=byId("pg-cuda-compiler-state");if(node)node.textContent=String(error.message||error);}));byId("pg-cuda-compile")?.addEventListener("click",()=>compileCudaGates().catch(error=>{const node=byId("pg-cuda-compiler-state");if(node)node.textContent=String(error.message||error);}));byId("pg-cuda-preflight")?.addEventListener("click",()=>runCudaPreflight().catch(error=>{const node=byId("pg-cuda-resource-state");if(node)node.textContent=String(error.message||error);}));byId("pg-cuda-smoke")?.addEventListener("click",()=>runCudaHardwareSmoke().catch(error=>{const node=byId("pg-cuda-parity-state");if(node)node.textContent=String(error.message||error);}));byId("pg-cuda-rng")?.addEventListener("click",()=>runCudaRngParity().catch(error=>{const node=byId("pg-cuda-rng-state");if(node)node.textContent=String(error.message||error);}));byId("pg-user-preset-apply")?.addEventListener("click",applyUserPreset);byId("pg-user-preset-save")?.addEventListener("click",saveUserPreset);byId("pg-user-preset-delete")?.addEventListener("click",deleteUserPreset);byId("pg-user-preset-select")?.addEventListener("change",()=>{renderUserPresetOptions();applyUserPreset();});byId("pg-robustness")?.addEventListener("click",runRobustness);byId("pg-reset")?.addEventListener("click",resetForm);byId("pg-live-open")?.addEventListener("click",()=>openLiveMonitor().catch(error=>byId("pg-live-state").textContent=String(error.message||error)));byId("pg-live-clear")?.addEventListener("click",()=>clearLiveSessions().catch(error=>byId("pg-live-state").textContent=String(error.message||error)));byId("pg-night-start")?.addEventListener("click",()=>startNightRun().catch(error=>byId("pg-night-state").textContent=String(error.message||error)));byId("pg-night-stop")?.addEventListener("click",()=>stopNightRun().catch(error=>byId("pg-night-state").textContent=String(error.message||error)));byId("pg-night-refresh")?.addEventListener("click",()=>refreshNightStatus().catch(error=>byId("pg-night-state").textContent=String(error.message||error)));
1368
  renderUserPresetOptions();
1369
  try{renderCatalog(await apiGet("/api/playground/catalog"));renderUserPresetOptions();resetForm();}catch(error){const status=byId("pg-status");status.dataset.state="error";status.textContent=`Katalog nicht verfügbar: ${error.message}`;}
1370
+ try{await refreshIntegrationCatalog();}catch(error){const state=byId("pg-mhrn-integration-state");if(state)state.textContent=`Integrationsstatus nicht verfügbar: ${error.message}`;}
1371
  byId("playground-builder")?.addEventListener("input", updateDefaultHints);
1372
  byId("playground-builder")?.addEventListener("change", updateDefaultHints);
1373
+ initGuidedWorkspace({payload:formPayload,presets:()=>Object.fromEntries(Object.entries(allPlaygroundPresets()).map(([name,preset])=>[name,{...preset,settings:preset.source==="catalog"?resolvedPresetSettings(name):preset.settings}])),applyPreset:applyUserPreset,setValue:setBuilderValue,lastResult:()=>lastResult});
1374
  await refreshSessions();
1375
  try{await refreshNightStatus();}catch{ /* night manager is optional during partial deployments */ }
1376
+ try{await refreshCudaStatus();}catch{ /* CUDA toolkit/driver is optional */ }
1377
+ window.MHRNPlayground={run:runSession,runRobustness,checkCpuDeterminism,compileCudaGates,refreshCudaStatus,runCudaPreflight,runCudaHardwareSmoke,runCudaRngParity,refreshSessions,refreshIntegrationCatalog,transferIntegrationElement,get catalog(){return catalogState;},get lastResult(){return lastResult;}};
1378
  }
src/dashboard/static/frontend/modules/recurrent-snn-stage.js CHANGED
@@ -1,10 +1,7 @@
1
  "use strict";
2
 
3
- const ROUTES = Object.freeze({
4
- science: ["recurrent", "Rekurrentes SNN", "network", "view", "recurrent"],
5
- wesen: ["recurrent", "Rekurrenz", "wesen", "focus", "#mhrn-runtime-recurrent"],
6
- control: ["recurrent", "Rekurrenz-Parameter", "settings", "focus", "#mhrn-recurrent-control"],
7
- });
8
 
9
  const EDITABLE = Object.freeze([
10
  "network.initial_connections_per_neuron",
@@ -102,7 +99,7 @@ function ensureSciencePanel() {
102
  <div id="recurrent-science-summary" class="small-snn-summary"></div>
103
  <div class="small-snn-grid">
104
  <section><h3>Live-Netz</h3><div id="recurrent-science-graph" class="small-snn-graph"></div></section>
105
- <section><h3>Stage-2 Contract</h3><div class="small-snn-contract"><strong>A → B → C → D → A</strong><span>20.000 ticks reference run</span><span>bounded event queue</span><span>deterministic replay identity</span><span>restart/restore independently verified</span><span>RQ-SNN-001: 100.000 ticks/run research DATA</span></div><div class="small-snn-actions"><button type="button" data-route-jump="wesen:recurrent">Runtime ansehen</button><button type="button" data-route-jump="control:recurrent">Rekurrenz-Parameter</button><button type="button" data-route-jump="release:development">Release-Entwicklung</button></div></section>
106
  </div>
107
  <div id="recurrent-science-status" class="small-snn-status">lade …</div>`;
108
  root.appendChild(panel);
@@ -114,7 +111,7 @@ function ensureRuntimePanel() {
114
  const panel = document.createElement("section");
115
  panel.id = "mhrn-runtime-recurrent";
116
  panel.className = "card small-snn-panel runtime-snn-panel";
117
- panel.dataset.mhrnRoute = "wesen:recurrent";
118
  panel.hidden = true;
119
  panel.innerHTML = `
120
  <header class="small-snn-head"><div><span class="workspace-kicker">RUNTIME & WESEN · STAGE 2</span><h2>Rekurrenz Live</h2><p>Read-only Beobachtung des real laufenden Netzwerks mit Tick, Aktivität, Queue und rückgekoppelten Kanten im sichtbaren Ausschnitt.</p></div><button id="recurrent-runtime-refresh" type="button">↻ Aktualisieren</button></header>
@@ -132,12 +129,12 @@ function ensureControlPanel() {
132
  const panel = document.createElement("section");
133
  panel.id = "mhrn-recurrent-control";
134
  panel.className = "card small-snn-panel";
135
- panel.dataset.mhrnRoute = "control:recurrent";
136
  panel.hidden = true;
137
  panel.innerHTML = `
138
  <header class="small-snn-head"><div><span class="workspace-kicker">CONTROL · STAGE 2</span><h2>Rekurrenz-Parameter</h2><p>Relevante Konstruktionsparameter werden ausschließlich als Pending Changes vorbereitet.</p></div><span id="recurrent-pending" class="small-snn-stage-badge">0 pending</span></header>
139
  <div id="recurrent-control-fields" class="small-snn-control-fields"></div>
140
- <div class="small-snn-actions"><button id="recurrent-stage" type="button">Stage changes</button><button id="recurrent-apply" type="button" class="btn-primary">Apply</button><button type="button" data-route-jump="control:parameters">Alle Parameter</button><button type="button" data-route-jump="science:recurrent">Wissenschaft</button></div>
141
  <div id="recurrent-control-status" class="small-snn-status">lade …</div>
142
  <div class="small-snn-boundary">Topologie-, Radius- und Refraktäränderungen sind wissenschaftlich sensitiv. Ein neuer Run/Restart ist erforderlich.</div>`;
143
  root.appendChild(panel);
@@ -228,7 +225,7 @@ function summaryHtml(data) {
228
  }
229
 
230
  async function refreshScience() {
231
- if (document.body.dataset.currentArea !== "science" || document.body.dataset.currentRoute !== "recurrent") return;
232
  try {
233
  const data = await loadNetwork();
234
  $("recurrent-science-summary").innerHTML = summaryHtml(data);
@@ -241,8 +238,8 @@ async function refreshScience() {
241
 
242
  async function refreshRuntime() {
243
  if (
244
- document.body.dataset.currentArea !== "wesen" ||
245
- document.body.dataset.currentRoute !== "recurrent" ||
246
  state.inFlight
247
  ) return;
248
  state.inFlight = true;
@@ -284,7 +281,7 @@ function renderControl() {
284
  }
285
 
286
  async function refreshControl() {
287
- if (document.body.dataset.currentArea !== "control" || document.body.dataset.currentRoute !== "recurrent") return;
288
  try {
289
  const [parameters, pending] = await Promise.all([
290
  json("/api/parameters"),
@@ -342,17 +339,17 @@ async function applyControl() {
342
  function routeRefresh() {
343
  const area = document.body.dataset.currentArea;
344
  const route = document.body.dataset.currentRoute;
345
- if (route !== "recurrent") return;
346
- if (area === "science") refreshScience();
347
- if (area === "wesen") refreshRuntime();
348
- if (area === "control") refreshControl();
349
  }
350
 
351
  function syncPolling() {
352
  const area = document.body.dataset.currentArea;
353
  const route = document.body.dataset.currentRoute;
354
- const liveRoute = route === "recurrent" && (area === "science" || area === "wesen");
355
- if (route === "recurrent") routeRefresh();
356
  if (liveRoute && !state.timer) {
357
  state.timer = setInterval(routeRefresh, 1000);
358
  return;
@@ -374,7 +371,7 @@ export function initRecurrentSNNStage() {
374
  attributeFilter: ["data-current-area", "data-current-route"],
375
  });
376
  document.addEventListener("click", (event) => {
377
- if (event.target.closest('[data-area-route="recurrent"], [data-route-card="recurrent"]')) {
378
  setTimeout(syncPolling, 0);
379
  }
380
  });
 
1
  "use strict";
2
 
3
+ const ROUTES = Object.freeze({});
4
+
 
 
 
5
 
6
  const EDITABLE = Object.freeze([
7
  "network.initial_connections_per_neuron",
 
99
  <div id="recurrent-science-summary" class="small-snn-summary"></div>
100
  <div class="small-snn-grid">
101
  <section><h3>Live-Netz</h3><div id="recurrent-science-graph" class="small-snn-graph"></div></section>
102
+ <section><h3>Stage-2 Contract</h3><div class="small-snn-contract"><strong>A → B → C → D → A</strong><span>20.000 ticks reference run</span><span>bounded event queue</span><span>deterministic replay identity</span><span>restart/restore independently verified</span><span>RQ-SNN-001: 100.000 ticks/run research DATA</span></div><div class="small-snn-actions"><button type="button" data-route-jump="old:wesen-recurrent">Runtime ansehen</button><button type="button" data-route-jump="old:control-recurrent">Rekurrenz-Parameter</button><button type="button" data-route-jump="release:development">Release-Entwicklung</button></div></section>
103
  </div>
104
  <div id="recurrent-science-status" class="small-snn-status">lade …</div>`;
105
  root.appendChild(panel);
 
111
  const panel = document.createElement("section");
112
  panel.id = "mhrn-runtime-recurrent";
113
  panel.className = "card small-snn-panel runtime-snn-panel";
114
+ panel.dataset.mhrnRoute = "old:wesen-recurrent";
115
  panel.hidden = true;
116
  panel.innerHTML = `
117
  <header class="small-snn-head"><div><span class="workspace-kicker">RUNTIME & WESEN · STAGE 2</span><h2>Rekurrenz Live</h2><p>Read-only Beobachtung des real laufenden Netzwerks mit Tick, Aktivität, Queue und rückgekoppelten Kanten im sichtbaren Ausschnitt.</p></div><button id="recurrent-runtime-refresh" type="button">↻ Aktualisieren</button></header>
 
129
  const panel = document.createElement("section");
130
  panel.id = "mhrn-recurrent-control";
131
  panel.className = "card small-snn-panel";
132
+ panel.dataset.mhrnRoute = "old:control-recurrent";
133
  panel.hidden = true;
134
  panel.innerHTML = `
135
  <header class="small-snn-head"><div><span class="workspace-kicker">CONTROL · STAGE 2</span><h2>Rekurrenz-Parameter</h2><p>Relevante Konstruktionsparameter werden ausschließlich als Pending Changes vorbereitet.</p></div><span id="recurrent-pending" class="small-snn-stage-badge">0 pending</span></header>
136
  <div id="recurrent-control-fields" class="small-snn-control-fields"></div>
137
+ <div class="small-snn-actions"><button id="recurrent-stage" type="button">Stage changes</button><button id="recurrent-apply" type="button" class="btn-primary">Apply</button><button type="button" data-route-jump="settings:parameters">Alle Parameter</button><button type="button" data-route-jump="old:science-recurrent">Wissenschaft</button></div>
138
  <div id="recurrent-control-status" class="small-snn-status">lade …</div>
139
  <div class="small-snn-boundary">Topologie-, Radius- und Refraktäränderungen sind wissenschaftlich sensitiv. Ein neuer Run/Restart ist erforderlich.</div>`;
140
  root.appendChild(panel);
 
225
  }
226
 
227
  async function refreshScience() {
228
+ if (document.body.dataset.currentArea !== "old" || document.body.dataset.currentRoute !== "science-recurrent") return;
229
  try {
230
  const data = await loadNetwork();
231
  $("recurrent-science-summary").innerHTML = summaryHtml(data);
 
238
 
239
  async function refreshRuntime() {
240
  if (
241
+ document.body.dataset.currentArea !== "old" ||
242
+ document.body.dataset.currentRoute !== "wesen-recurrent" ||
243
  state.inFlight
244
  ) return;
245
  state.inFlight = true;
 
281
  }
282
 
283
  async function refreshControl() {
284
+ if (document.body.dataset.currentArea !== "old" || document.body.dataset.currentRoute !== "control-recurrent") return;
285
  try {
286
  const [parameters, pending] = await Promise.all([
287
  json("/api/parameters"),
 
339
  function routeRefresh() {
340
  const area = document.body.dataset.currentArea;
341
  const route = document.body.dataset.currentRoute;
342
+ if (area !== "old") return;
343
+ if (route === "science-recurrent") refreshScience();
344
+ if (route === "wesen-recurrent") refreshRuntime();
345
+ if (route === "control-recurrent") refreshControl();
346
  }
347
 
348
  function syncPolling() {
349
  const area = document.body.dataset.currentArea;
350
  const route = document.body.dataset.currentRoute;
351
+ const liveRoute = area === "old" && (route === "science-recurrent" || route === "wesen-recurrent");
352
+ if (area === "old" && route.endsWith("-recurrent")) routeRefresh();
353
  if (liveRoute && !state.timer) {
354
  state.timer = setInterval(routeRefresh, 1000);
355
  return;
 
371
  attributeFilter: ["data-current-area", "data-current-route"],
372
  });
373
  document.addEventListener("click", (event) => {
374
+ if (event.target.closest('[data-area-route="science-recurrent"], [data-area-route="wesen-recurrent"], [data-area-route="control-recurrent"], [data-route-card="science-recurrent"], [data-route-card="wesen-recurrent"], [data-route-card="control-recurrent"]')) {
375
  setTimeout(syncPolling, 0);
376
  }
377
  });