tensor-optix 2.0.0__tar.gz → 2.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/PKG-INFO +815 -815
  2. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/README.md +746 -746
  3. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/pyproject.toml +127 -127
  4. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/setup.cfg +4 -4
  5. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/__init__.py +145 -145
  6. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/jax/__init__.py +4 -4
  7. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/jax/flax_agent.py +116 -116
  8. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/jax/flax_evaluator.py +27 -27
  9. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/pytorch/__init__.py +6 -6
  10. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/pytorch/torch_agent.py +170 -154
  11. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/pytorch/torch_evaluator.py +73 -73
  12. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/tensorflow/__init__.py +6 -6
  13. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/tensorflow/tf_agent.py +163 -163
  14. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/tensorflow/tf_evaluator.py +70 -70
  15. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/__init__.py +17 -17
  16. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/flax_ppo.py +333 -333
  17. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/tf_dqn.py +229 -229
  18. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/tf_ppo.py +315 -315
  19. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/tf_ppo_continuous.py +333 -333
  20. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/tf_sac.py +369 -369
  21. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/tf_td3.py +335 -335
  22. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/torch_dqn.py +302 -302
  23. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/torch_ppo.py +357 -338
  24. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/torch_ppo_continuous.py +364 -364
  25. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/torch_rainbow_dqn.py +505 -505
  26. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/torch_recurrent_ppo.py +498 -498
  27. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/torch_sac.py +435 -398
  28. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/algorithms/torch_td3.py +465 -465
  29. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/callbacks/__init__.py +6 -6
  30. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/callbacks/rich_dashboard.py +350 -350
  31. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/callbacks/tensorboard_callback.py +176 -176
  32. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/callbacks/wandb_callback.py +203 -203
  33. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/cli.py +143 -143
  34. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/config.py +360 -360
  35. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/__init__.py +12 -12
  36. tensor_optix-2.0.1/tensor_optix/core/async_callback.py +131 -0
  37. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/backoff_scheduler.py +315 -315
  38. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/base_agent.py +197 -146
  39. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/base_evaluator.py +191 -191
  40. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/base_optimizer.py +58 -58
  41. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/base_pipeline.py +42 -42
  42. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/checkpoint_confirmation.py +125 -125
  43. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/checkpoint_registry.py +215 -205
  44. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/device.py +51 -51
  45. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/diagnostic_controller.py +207 -207
  46. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/ensemble_agent.py +78 -78
  47. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/her_buffer.py +238 -238
  48. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/loop_controller.py +684 -684
  49. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/meta_controller.py +170 -170
  50. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/noisy_linear.py +138 -138
  51. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/normalizers.py +123 -123
  52. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/policy_manager.py +810 -810
  53. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/regime_detector.py +102 -102
  54. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/replay_buffer.py +253 -253
  55. tensor_optix-2.0.1/tensor_optix/core/resume_state.py +72 -0
  56. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/reward_shape.py +135 -135
  57. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/trajectory_buffer.py +82 -82
  58. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/core/types.py +136 -130
  59. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/distributed/__init__.py +4 -4
  60. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/distributed/async_learner.py +400 -400
  61. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/distributed/vtrace.py +101 -101
  62. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/exploration/__init__.py +3 -3
  63. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/exploration/rnd.py +199 -199
  64. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/factory.py +798 -798
  65. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ml/__init__.py +11 -11
  66. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ml/dataset_pipeline.py +185 -185
  67. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ml/loss_registry.py +289 -289
  68. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ml/ml_agent.py +229 -229
  69. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/__init__.py +26 -26
  70. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/agent/__init__.py +3 -3
  71. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/agent/graph_agent.py +610 -610
  72. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/brain_network.py +518 -518
  73. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/controller/__init__.py +3 -3
  74. tensor_optix-2.0.1/tensor_optix/neuroevo/controller/topology_callback.py +66 -0
  75. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/controller/topology_controller.py +676 -647
  76. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/graph/__init__.py +23 -23
  77. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/graph/neuron.py +547 -547
  78. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/graph/neuron_graph.py +850 -850
  79. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/graph/topology_ops.py +328 -328
  80. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/hebbian.py +192 -192
  81. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/neuromodulator.py +275 -275
  82. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/optimizer/__init__.py +3 -3
  83. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/neuroevo/optimizer/topology_aware_adam.py +104 -104
  84. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/optimizer.py +384 -384
  85. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/optimizers/__init__.py +7 -7
  86. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/optimizers/adaptive_optimizer.py +294 -294
  87. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/optimizers/backoff_optimizer.py +167 -167
  88. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/optimizers/momentum_optimizer.py +210 -210
  89. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/optimizers/pbt_optimizer.py +163 -163
  90. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/optimizers/spsa_optimizer.py +230 -230
  91. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/orchestrator.py +469 -469
  92. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/pipeline/__init__.py +4 -4
  93. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/pipeline/batch_pipeline.py +138 -138
  94. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/pipeline/live_pipeline.py +161 -161
  95. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/pipeline/vector_pipeline.py +167 -167
  96. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/simple.py +285 -285
  97. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/strategy/promotion.py +14 -0
  98. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/utils/__init__.py +3 -3
  99. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/utils/window_size.py +149 -149
  100. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix.egg-info/PKG-INFO +815 -815
  101. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix.egg-info/SOURCES.txt +3 -0
  102. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tests/test_ml.py +478 -478
  103. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/adapters/__init__.py +0 -0
  104. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ledger/__init__.py +0 -0
  105. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ledger/callback.py +0 -0
  106. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ledger/provenance.py +0 -0
  107. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ledger/records.py +0 -0
  108. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ledger/schema.py +0 -0
  109. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/ledger/store.py +0 -0
  110. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/nanonets/__init__.py +0 -0
  111. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/nanonets/abstention.py +0 -0
  112. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/nanonets/features.py +0 -0
  113. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/nanonets/pooling.py +0 -0
  114. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/nanonets/selector.py +0 -0
  115. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/nanonets/smalln.py +0 -0
  116. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/runtime/__init__.py +0 -0
  117. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/runtime/baseline.py +0 -0
  118. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/runtime/experiment_stage.py +0 -0
  119. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/runtime/live_runtime.py +0 -0
  120. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/runtime/observer.py +0 -0
  121. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/runtime/promote.py +0 -0
  122. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/scheduler/__init__.py +0 -0
  123. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/scheduler/_mp_common.py +0 -0
  124. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/scheduler/budget.py +0 -0
  125. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/scheduler/dedup.py +0 -0
  126. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/scheduler/isolation.py +0 -0
  127. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/scheduler/pool.py +0 -0
  128. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/scheduler/retry.py +0 -0
  129. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/scheduler/worker.py +0 -0
  130. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/selection/__init__.py +0 -0
  131. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/selection/backfill.py +0 -0
  132. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/selection/selector.py +0 -0
  133. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/selection/uncertainty.py +0 -0
  134. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/strategy/__init__.py +0 -0
  135. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/strategy/candidate.py +0 -0
  136. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/strategy/comparator.py +0 -0
  137. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/strategy/grammar.py +0 -0
  138. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/strategy/runner.py +0 -0
  139. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix/strategy/worker.py +0 -0
  140. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix.egg-info/dependency_links.txt +0 -0
  141. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix.egg-info/entry_points.txt +0 -0
  142. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix.egg-info/requires.txt +0 -0
  143. {tensor_optix-2.0.0 → tensor_optix-2.0.1}/tensor_optix.egg-info/top_level.txt +0 -0
@@ -1,815 +1,815 @@
1
- Metadata-Version: 2.4
2
- Name: tensor-optix
3
- Version: 2.0.0
4
- Summary: Autonomous training loop for any sequential learning model — PPO, DQN, SAC, TD3, Rainbow DQN, Recurrent PPO for TensorFlow, PyTorch, and JAX/Flax; distributed async actor-learner (IMPALA + V-trace)
5
- Author: sup3rus3r
6
- License-Expression: MIT
7
- Requires-Python: >=3.11
8
- Description-Content-Type: text/markdown
9
- Requires-Dist: gymnasium[box2d]>=1.0.0
10
- Requires-Dist: numpy>=1.24.0
11
- Requires-Dist: matplotlib>=3.7.0
12
- Requires-Dist: optuna>=3.0.0
13
- Requires-Dist: swig>=4.4.1
14
- Requires-Dist: pyyaml>=6.0
15
- Requires-Dist: torch>=2.11.0
16
- Provides-Extra: tensorflow
17
- Requires-Dist: tensorflow>=2.18.0; extra == "tensorflow"
18
- Provides-Extra: tensorflow-gpu
19
- Requires-Dist: tensorflow[and-cuda]>=2.18.0; sys_platform == "linux" and extra == "tensorflow-gpu"
20
- Provides-Extra: torch
21
- Requires-Dist: torch>=2.11.0; extra == "torch"
22
- Requires-Dist: torchvision; extra == "torch"
23
- Requires-Dist: torchaudio; extra == "torch"
24
- Provides-Extra: jax
25
- Requires-Dist: flax>=0.12.6; extra == "jax"
26
- Requires-Dist: jax>=0.10.0; extra == "jax"
27
- Requires-Dist: optax>=0.2.8; extra == "jax"
28
- Provides-Extra: cuda
29
- Requires-Dist: nvidia-cuda-nvcc-cu12; sys_platform == "linux" and extra == "cuda"
30
- Requires-Dist: tensorflow[and-cuda]>=2.18.0; sys_platform == "linux" and extra == "cuda"
31
- Provides-Extra: box2d
32
- Requires-Dist: gymnasium[box2d]>=1.0.0; extra == "box2d"
33
- Provides-Extra: atari
34
- Requires-Dist: gymnasium[atari]>=1.0.0; extra == "atari"
35
- Requires-Dist: ale-py>=0.8; extra == "atari"
36
- Provides-Extra: mujoco
37
- Requires-Dist: gymnasium[mujoco]>=1.0.0; extra == "mujoco"
38
- Provides-Extra: all
39
- Requires-Dist: tensorflow>=2.18.0; extra == "all"
40
- Requires-Dist: torch>=2.11.0; extra == "all"
41
- Requires-Dist: torchvision; extra == "all"
42
- Requires-Dist: torchaudio; extra == "all"
43
- Requires-Dist: gymnasium[atari]>=1.0.0; extra == "all"
44
- Requires-Dist: gymnasium[mujoco]>=1.0.0; extra == "all"
45
- Requires-Dist: ale-py>=0.8; extra == "all"
46
- Requires-Dist: jax>=0.10.0; extra == "all"
47
- Requires-Dist: flax>=0.12.6; extra == "all"
48
- Requires-Dist: optax>=0.2.8; extra == "all"
49
- Requires-Dist: torch>=2.11.0; extra == "all"
50
- Provides-Extra: neuroevo
51
- Requires-Dist: torch>=2.11.0; extra == "neuroevo"
52
- Provides-Extra: onnx
53
- Requires-Dist: onnx>=1.14; extra == "onnx"
54
- Requires-Dist: onnxruntime>=1.16; extra == "onnx"
55
- Requires-Dist: onnxscript>=0.1; extra == "onnx"
56
- Provides-Extra: wandb
57
- Requires-Dist: wandb>=0.16; extra == "wandb"
58
- Provides-Extra: tensorboard
59
- Requires-Dist: torch>=2.11.0; extra == "tensorboard"
60
- Requires-Dist: tensorboard>=2.14; extra == "tensorboard"
61
- Provides-Extra: dev
62
- Requires-Dist: pytest>=7.0; extra == "dev"
63
- Requires-Dist: pytest-cov; extra == "dev"
64
- Requires-Dist: black; extra == "dev"
65
- Requires-Dist: ruff; extra == "dev"
66
- Requires-Dist: mypy; extra == "dev"
67
- Requires-Dist: twine>=5.0; extra == "dev"
68
- Requires-Dist: torch>=2.11.0; extra == "dev"
69
-
70
- # tensor-optix
71
-
72
- tensor-optix is a training loop framework with statistical convergence control, online hyperparameter optimisation, and an optional neuroevolution subsystem for dynamic topology.
73
-
74
- The core loop runs your agent against a pipeline, maintains a separate validation signal, and manages four states: ACTIVE, COOLING, DORMANT, and watchdog shutdown. Convergence is detected using a corrected t-test on the smoothed score slope plus lag-1 autocorrelation, not a fixed patience counter. Hyperparameters are updated every episode via SPSA gradient estimates, with automatic routing to momentum-based or sign-only updates depending on the autocorrelation structure of the score landscape. Checkpointing and rollback are driven by the validation signal only, never training score. On DORMANT, a MetaController evaluates the generalization gap and its slope to decide between spawning a policy variant, pruning the ensemble, or stopping.
75
-
76
- The neuroevo subsystem (`pip install tensor-optix[neuroevo]`) represents the policy as a `NeuronGraph`: a mutable directed graph of heterogeneous scalar neurons (point, GRU, LSTM, trainable-GRU, trainable-LSTM) with variable-delay edges. Weights for excitatory and inhibitory neurons follow softplus Dale's Law: raw parameter θ maps to `softplus(θ)` (excitatory) or `-softplus(θ)` (inhibitory), eliminating gradient dead zones at weight boundaries. `TopologyController` runs as a loop callback and evaluates three independent signals per episode: improvement slope significance, residual autocorrelation structure, and gradient utilization across hidden neurons. All three must cross their thresholds before a grow operation fires. Pruning is by importance score (incident edge weight magnitude times mean absolute activation). Merging is by Pearson correlation of per-episode activation histories. After every structural mutation the controller calls `graph.invalidate_compile()` to reset for the new topology. `TopologyAwareAdam` resets momentum state for parameters affected by any structural change. `BrainNetwork` composes multiple named `NeuronGraph` regions with sparse learnable inter-region edges — when a neuron is pruned from a region, all inter-region edges referencing it are automatically cleaned up. `HebbianHook` accumulates co-activation products across each episode and applies an Oja-style weight update after the PPO gradient step. `NeuromodulatorSignal` maps a `RegimeDetector` output (trending / ranging / volatile) to simultaneous changes in Hebbian learning rate, entropy coefficient, and topology grow/prune thresholds.
77
-
78
- The entire system, including neuroevo, is accessed through a six-method `BaseAgent` interface:
79
-
80
- ```python
81
- class BaseAgent(ABC):
82
- def act(self, observation) -> any: ...
83
- def learn(self, episode_data: EpisodeData) -> dict: ...
84
- def get_hyperparams(self) -> HyperparamSet: ...
85
- def set_hyperparams(self, hyperparams: HyperparamSet) -> None: ...
86
- def save_weights(self, path: str) -> None: ...
87
- def load_weights(self, path: str) -> None: ...
88
- ```
89
-
90
- Fifteen agents are included across PyTorch, TensorFlow, and JAX/Flax. Bring your own by implementing the interface above.
91
-
92
- ---
93
-
94
- ## Install
95
-
96
- ```bash
97
- # Core loop only, no algorithm implementations
98
- pip install tensor-optix
99
-
100
- # PyTorch algorithms
101
- pip install tensor-optix[torch]
102
-
103
- # TensorFlow algorithms
104
- pip install tensor-optix[tensorflow]
105
-
106
- # JAX/Flax
107
- pip install tensor-optix[jax]
108
-
109
- # Neuroevo (requires torch)
110
- pip install tensor-optix[neuroevo]
111
-
112
- # GPU (Linux/WSL2, CUDA 12.8)
113
- pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128
114
- pip install tensor-optix[torch]
115
-
116
- # All frameworks and environment extras
117
- pip install tensor-optix[all]
118
- ```
119
-
120
- Environment extras: `[box2d]`, `[atari]`, `[mujoco]`
121
- Logging extras: `[wandb]`, `[tensorboard]`
122
- Export: `[onnx]`
123
-
124
- ---
125
-
126
- ## Algorithms
127
-
128
- All agents implement `BaseAgent` and are interchangeable with `RLOptimizer`.
129
-
130
- ### PyTorch
131
-
132
- | Agent | Algorithm | Action Space |
133
- |---|---|---|
134
- | `TorchPPOAgent` | PPO + GAE-λ | Discrete |
135
- | `TorchGaussianPPOAgent` | PPO | Continuous |
136
- | `TorchRecurrentPPOAgent` | PPO + GRU/LSTM hidden state | Discrete |
137
- | `TorchDQNAgent` | DQN + PER + n-step returns | Discrete |
138
- | `TorchRainbowDQNAgent` | Rainbow DQN (NoisyNet, distributional, PER, n-step, dueling, double) | Discrete |
139
- | `TorchSACAgent` | SAC, twin Q-critics, automatic entropy tuning | Continuous |
140
- | `TorchTD3Agent` | TD3 | Continuous |
141
-
142
- ### TensorFlow
143
-
144
- `TFPPOAgent`, `TFGaussianPPOAgent`, `TFDQNAgent`, `TFSACAgent`, `TFTDDAgent`
145
-
146
- ### JAX/Flax
147
-
148
- `FlaxPPOAgent`
149
-
150
- ### Auto-selection
151
-
152
- `make_agent` inspects the environment action space and returns a fully constructed agent. Pass an algorithm name as the first argument, or let it be inferred.
153
-
154
- ```python
155
- from tensor_optix import make_agent
156
- import gymnasium as gym
157
-
158
- env = gym.make("LunarLanderContinuous-v3")
159
- agent = make_agent(env) # -> TorchSACAgent (continuous)
160
- agent = make_agent("SAC", env) # same, explicit
161
- agent = make_agent(env, framework="tf") # -> TFSACAgent
162
- agent = make_agent(env, deterministic=True) # -> TorchTD3Agent
163
-
164
- # Discrete action spaces
165
- env = gym.make("CartPole-v1")
166
- agent = make_agent(env) # -> TorchPPOAgent (default discrete)
167
- agent = make_agent(env, algorithm="DQN") # -> TorchDQNAgent
168
- agent = make_agent(env, algorithm="RAINBOW") # -> TorchRainbowDQNAgent
169
-
170
- # Neuroevo path: NeuronGraph + GraphAgent with Hebbian + TopologyController
171
- agent = make_agent(env, neuroevo=True)
172
- agent = make_agent("PPO", env, neuroevo=True, graph_hidden=16, hebbian_lr=1e-3)
173
-
174
- # Feature-extractor mode: NeuronGraph -> features concatenated to obs, fed into SAC
175
- agent = make_agent("SAC", env, neuroevo=True, neuroevo_mode="feature_extractor")
176
- ```
177
-
178
- Neuroevo options: `graph_in` (input neurons, default `min(obs_dim, 16)`), `graph_hidden` (GRU neurons, default 8), `graph_out` (output neurons, default `act_dim + 1`), `hebbian_lr`, `hebbian_decay`, `grow_cooldown`.
179
-
180
- `neuroevo_mode` controls how the graph is used:
181
- - `"policy"` (default) — graph IS the policy, wrapped as `GraphAgent` (PPO-based)
182
- - `"feature_extractor"` — graph runs in parallel as a feature extractor; its `graph_out`-dim output is concatenated with the raw observation before SAC actor/critic networks. The base SAC layer handles exploration and replay; the graph adds adaptive temporal features via GRU + LSTM neurons with Hebbian learning.
183
-
184
- ---
185
-
186
- ## One-line training with Optimizer
187
-
188
- `Optimizer` wraps `RLOptimizer` with sensible defaults. It auto-computes `window_size`, wires neuroevo callbacks, and activates SPSA when the agent has `default_param_bounds`.
189
-
190
- ```python
191
- from tensor_optix import make_agent, Optimizer
192
- import gymnasium as gym
193
-
194
- env = gym.make("HumanoidStandup-v5")
195
- agent = make_agent("SAC", env, neuroevo=True)
196
- opt = Optimizer(agent, env)
197
- opt.run()
198
-
199
- # Vectorized: 8 parallel envs
200
- opt = Optimizer(agent, lambda: gym.make("CartPole-v1"), n_envs=8)
201
- opt.run()
202
- ```
203
-
204
- `optimal_window_size(env, algorithm)` computes the window size formula used internally: `clip(k * mean_episode_steps, 512, 8192)` where k=4.0 for on-policy (PPO) and k=1.0 for off-policy (SAC/TD3).
205
-
206
- ```python
207
- from tensor_optix import optimal_window_size
208
- window = optimal_window_size(env, "PPO") # e.g. 2000 for CartPole
209
- ```
210
-
211
- ---
212
-
213
- ## Pipelines
214
-
215
- A pipeline steps an environment (or data source), collects `EpisodeData`, and yields it to the agent. Three implementations are provided.
216
-
217
- ```python
218
- from tensor_optix import BatchPipeline, LivePipeline, VectorBatchPipeline
219
-
220
- # Gymnasium env: steps continuously, no reset between windows
221
- pipeline = BatchPipeline(env=gym.make("CartPole-v1"), agent=agent, window_size=200)
222
-
223
- # External data stream: background thread with bounded queue, configurable episode boundaries
224
- pipeline = LivePipeline(
225
- data_source=MyFeed(),
226
- agent=agent,
227
- episode_boundary_fn=LivePipeline.every_n_seconds(300),
228
- )
229
-
230
- # N parallel envs via gymnasium.vector, sync or async subprocess
231
- pipeline = VectorBatchPipeline(
232
- env_fns=[lambda: gym.make("CartPole-v1")] * 8,
233
- agent=agent,
234
- window_size=200,
235
- )
236
- ```
237
-
238
- ---
239
-
240
- ## The loop
241
-
242
- ```python
243
- from tensor_optix import RLOptimizer
244
-
245
- opt = RLOptimizer(
246
- agent=agent,
247
- pipeline=pipeline,
248
-
249
- # Separate validation pipeline. All checkpoint and rollback decisions use val score only.
250
- val_pipeline=val_pipeline,
251
- rollback_on_degradation=True,
252
-
253
- # Optional external scorer run at checkpoint evaluation (e.g. held-out backtest)
254
- checkpoint_score_fn=lambda a: evaluate(a, held_out_env),
255
-
256
- # Convergence parameters
257
- dormant_threshold=10, # consecutive episodes without improvement -> DORMANT
258
- min_episodes_before_dormant=50, # statistical warmup before convergence detection activates
259
- )
260
-
261
- opt.run()
262
- opt.best_snapshot # -> PolicySnapshot: best weights + EvalMetrics + HyperparamSet
263
- ```
264
-
265
- Loop state transitions: `ACTIVE` -> `COOLING` -> `DORMANT` -> watchdog shutdown or policy spawn.
266
-
267
- On shutdown the loop restores best-known weights, not the final checkpoint.
268
-
269
- ---
270
-
271
- ## Hyperparameter optimisation
272
-
273
- All optimisers operate in normalised [0, 1] parameter space and update every episode. No restarts required.
274
-
275
- ```python
276
- from tensor_optix.optimizers import SPSAOptimizer, AdaptiveOptimizer
277
-
278
- # SPSA: Rademacher perturbation vector, two-episode gradient estimate
279
- optimizer = SPSAOptimizer(
280
- param_bounds={"learning_rate": (1e-4, 3e-3), "clip_ratio": (0.1, 0.3)},
281
- log_params={"learning_rate"}, # log-space normalisation for params spanning orders of magnitude
282
- )
283
-
284
- # AdaptiveOptimizer: routes between SPSA, Momentum, Backoff, and PBT
285
- # based on lag-1 autocorrelation of the score stream and relative performance gap
286
- optimizer = AdaptiveOptimizer(param_bounds={...})
287
-
288
- opt = RLOptimizer(agent=agent, pipeline=pipeline, optimizer=optimizer)
289
- ```
290
-
291
- | Optimizer | Routing condition |
292
- |---|---|
293
- | `SPSAOptimizer` | i.i.d. score noise, no autocorrelation structure |
294
- | `MomentumOptimizer` | Positive lag-1 autocorrelation (smooth landscape) |
295
- | `BackoffOptimizer` | Negative lag-1 autocorrelation (oscillating landscape, sign-only updates) |
296
- | `PBTOptimizer` | Score below 20th percentile of history (exploit checkpoint population) |
297
- | `AdaptiveOptimizer` | Routes automatically based on the two signals above |
298
-
299
- ### Trial-level search
300
-
301
- `TrialOrchestrator` runs N independent short trials via Optuna TPE before the main run, then warm-starts from the best trial's weights and config.
302
-
303
- ```python
304
- from tensor_optix import TrialOrchestrator
305
-
306
- orch = TrialOrchestrator(
307
- agent_factory=make_agent,
308
- pipeline_factory=make_pipeline,
309
- param_space={
310
- "learning_rate": ("log_float", 1e-4, 3e-3),
311
- "clip_ratio": ("float", 0.1, 0.3),
312
- "batch_size": ("int", 32, 512),
313
- },
314
- n_trials=20,
315
- trial_episodes=50,
316
- )
317
- best_config = orch.run()
318
- ```
319
-
320
- ---
321
-
322
- ## Ensemble and policy evolution
323
-
324
- `PolicyManager` runs as a loop callback. On each DORMANT event, `MetaController` evaluates the generalization gap (train minus val, normalised) and its slope, and the validation improvement rate, then issues one of: SPAWN, PRUNE, or STOP. Spawned variants are cloned from the best checkpoint with perturbed hyperparameters. `EnsembleAgent` wraps all active variants behind the `BaseAgent` interface, with weighted action averaging.
325
-
326
- ```python
327
- from tensor_optix import PolicyManager
328
-
329
- pm = PolicyManager(registry, max_spawns=4)
330
- cb = pm.as_callback(agent, agent_factory=make_agent)
331
- cb.set_stop_fn(opt.stop)
332
- opt.add_callback(cb)
333
- opt.run()
334
- ```
335
-
336
- ---
337
-
338
- ## Callbacks
339
-
340
- ```python
341
- from tensor_optix.callbacks import RichDashboardCallback, WandbCallback, TensorBoardCallback
342
-
343
- opt.add_callback(RichDashboardCallback()) # Rich live terminal panel
344
- opt.add_callback(WandbCallback(project="run"))
345
- opt.add_callback(TensorBoardCallback(log_dir="./tb"))
346
- ```
347
-
348
- Custom callbacks subclass `LoopCallback` and override any of:
349
-
350
- ```python
351
- class LoopCallback:
352
- def on_loop_start(self) -> None: ...
353
- def on_loop_stop(self) -> None: ...
354
- def on_episode_end(self, episode_id: int, eval_metrics) -> None: ...
355
- def on_improvement(self, snapshot) -> None: ...
356
- def on_plateau(self, episode_id: int, state) -> None: ...
357
- def on_dormant(self, episode_id: int) -> None: ...
358
- def on_degradation(self, episode_id: int, eval_metrics) -> None: ...
359
- def on_hyperparam_update(self, old: dict, new: dict) -> None: ...
360
- ```
361
-
362
- `HebbianHook` and `NeuromodulatorSignal` are both `LoopCallback` subclasses and can be passed directly to `Optimizer` or `RLOptimizer` via `callbacks=`.
363
-
364
- ---
365
-
366
- ## Distributed training (IMPALA + V-trace)
367
-
368
- `AsyncActorLearner` implements IMPALA-style async actor-learner. N actor subprocesses read weights from shared memory (lock-free), collect trajectories, and push them to a queue. The learner dequeues trajectories, applies V-trace importance-sampling correction, and writes updated weights back to shared memory.
369
-
370
- ```python
371
- from tensor_optix.distributed import AsyncActorLearner
372
-
373
- learner = AsyncActorLearner(
374
- actor=actor,
375
- critic=critic,
376
- optimizer=optimizer,
377
- env_factory=lambda: gym.make("ALE/Pong-v5"),
378
- n_actors=8,
379
- trajectory_len=64,
380
- )
381
- stats = learner.run(max_steps=10_000_000)
382
- # stats["steps_per_second"] -> ~4x single-process throughput on CPU
383
- ```
384
-
385
- ---
386
-
387
- ## Neuroevo
388
-
389
- `NeuronGraph` is a mutable directed graph of scalar neurons with variable-delay edges. `GraphAgent` wraps it as a `BaseAgent` with PPO-style weight learning. `TopologyController` mutates the graph live during the training loop.
390
-
391
- ```bash
392
- pip install tensor-optix[neuroevo]
393
- ```
394
-
395
- ### Graph construction
396
-
397
- ```python
398
- from tensor_optix.neuroevo import NeuronGraph, GraphAgent, GRUNeuron, LSTMNeuron
399
-
400
- graph = NeuronGraph()
401
-
402
- for _ in range(4):
403
- graph.add_neuron(role="input", activation="linear")
404
- for _ in range(8):
405
- graph.add_neuron(role="hidden", activation="tanh")
406
- # or: graph.add_neuron(role="hidden", neuron=GRUNeuron())
407
- # or: graph.add_neuron(role="hidden", neuron=LSTMNeuron())
408
- graph.add_neuron(role="output", activation="linear") # last output neuron is the value head
409
-
410
- graph.add_edge(src_id, dst_id, weight=0.0, delay=0) # feedforward (d=0)
411
- graph.add_edge(src_id, dst_id, weight=0.0, delay=1) # recurrent (d>=1, reads from history buffer)
412
-
413
- agent = GraphAgent(graph, obs_dim=4, n_actions=2)
414
- ```
415
-
416
- All edges initialise at weight=0.0, which is function-preserving at insertion time.
417
-
418
- ### Neuron types
419
-
420
- | Type | Hidden state | Gradient through state |
421
- |---|---|---|
422
- | `Neuron` | None (point neuron) | N/A |
423
- | `GRUNeuron` | Scalar h, detached | No |
424
- | `LSTMNeuron` | Scalar h and c, detached | No |
425
- | `TrainableGRUNeuron` | Scalar h, not detached | Yes, up to chunk_len steps |
426
- | `TrainableLSTMNeuron` | Scalar h and c, not detached | Yes, up to chunk_len steps |
427
-
428
- All types implement the same protocol: `step()`, `importance()`, `can_merge_with()`, `make_relay()`, `split_copy()`. `NeuronGraph` and `TopologyController` are type-blind.
429
-
430
- ### Trainable recurrent neurons
431
-
432
- `TrainableGRUNeuron` and `TrainableLSTMNeuron` set `is_recurrent = True`. `RecurrentGraphAgent` detects this flag and switches from shuffled-minibatch PPO to sequential chunk training with truncated BPTT.
433
-
434
- ```python
435
- from tensor_optix.neuroevo import TrainableGRUNeuron, TrainableLSTMNeuron, RecurrentGraphAgent
436
-
437
- graph = NeuronGraph()
438
- # ... input and output neurons ...
439
- graph.add_neuron(role="hidden", neuron=TrainableGRUNeuron())
440
- graph.add_neuron(role="hidden", neuron=TrainableLSTMNeuron())
441
-
442
- agent = RecurrentGraphAgent(
443
- graph, obs_dim=4, n_actions=2,
444
- hyperparams=HyperparamSet(params={"chunk_len": 64}),
445
- )
446
- # Falls back to standard shuffled-minibatch PPO if no recurrent neurons are present
447
- ```
448
-
449
- ### Topology controller
450
-
451
- ```python
452
- from tensor_optix.neuroevo import TopologyController
453
-
454
- controller = TopologyController.for_graph(
455
- graph=graph,
456
- scheduler=opt._scheduler,
457
- grow_grad_threshold=0.7, # fraction of hidden neurons with |grad| > eps required to grow
458
- prune_neuron_threshold=1e-4, # importance score below this -> prune candidate
459
- prune_edge_threshold=1e-3, # |weight| below this for prune_edge_patience episodes -> prune
460
- merge_similarity_threshold=0.95, # Pearson correlation threshold for merge
461
- )
462
- opt.add_callback(controller)
463
- opt.run()
464
- ```
465
-
466
- Grow fires only when all three signals agree:
467
- 1. Improvement slope t-test is not significant (gradient updates are not making progress)
468
- 2. Score residuals have significant autocorrelation (capacity is underutilised)
469
- 3. Gradient utilization exceeds `grow_grad_threshold` (existing neurons are saturated)
470
-
471
- For multi-region graphs, use `TopologyController.for_brain(brain, scheduler=...)`. Each region gets independent signal buffers and cooldown timers. When a neuron is pruned from a region, all inter-region `BrainNetwork` edges referencing it are automatically removed.
472
-
473
- ### Save and load
474
-
475
- Topology-aware checkpointing works identically to non-evo agents. The topology (neuron types, roles, edges) is saved alongside weights so the graph is fully reconstructed on load — no manual topology reconstruction required.
476
-
477
- ```python
478
- # Save
479
- agent.save_weights("checkpoint.pt")
480
-
481
- # Load into any agent instance — topology is reconstructed automatically
482
- agent2 = make_agent(env, neuroevo=True)
483
- agent2.load_weights("checkpoint.pt")
484
-
485
- # Or skip constructing an agent entirely
486
- agent3 = GraphAgent.from_checkpoint("checkpoint.pt")
487
- agent3.act(obs)
488
- ```
489
-
490
- Rollback on degradation uses the same path internally — when the loop restores a best checkpoint, the topology at the time of that checkpoint is fully restored, even if the graph has grown or been pruned since.
491
-
492
- ### BrainNetwork
493
-
494
- ```python
495
- from tensor_optix.neuroevo import BrainNetwork, TopologyController
496
-
497
- brain = BrainNetwork()
498
- brain.add_region("sensory", sensory_graph)
499
- brain.add_region("memory", memory_graph)
500
- brain.add_region("executive", executive_graph)
501
-
502
- brain.add_pathway("sensory", "memory", n_connections=8, delay=1)
503
- brain.add_pathway("memory", "executive", n_connections=8, delay=0)
504
-
505
- controller = TopologyController.for_brain(brain, scheduler=opt._scheduler)
506
- ```
507
-
508
- Inter-region edges are learnable parameters. Regions are executed in topological order each forward pass.
509
-
510
- ### Hebbian learning
511
-
512
- `HebbianHook` applies an Oja-style local weight update after each episode. The rule is: `dw = eta * mean_t(h_pre * h_post) - lambda * w`. Call `record()` after each `act()` to accumulate co-activation products, then `apply()` after the PPO gradient step.
513
-
514
- ```python
515
- from tensor_optix.neuroevo import HebbianHook
516
-
517
- hook = HebbianHook(graph, hebbian_lr=1e-3, weight_decay=1e-4)
518
-
519
- for step in episode:
520
- action = agent.act(obs)
521
- hook.record()
522
- obs, reward, done, _ = env.step(action)
523
-
524
- agent.learn(episode_data)
525
- hook.apply()
526
- hook.reset()
527
- ```
528
-
529
- `HebbianHook` is a `LoopCallback` — pass it directly to `Optimizer` or `RLOptimizer` via `callbacks=` and it wires itself automatically. Use `HebbianHook.from_brain(brain, ...)` for `BrainNetwork` graphs.
530
-
531
- ### Neuromodulation
532
-
533
- `NeuromodulatorSignal` takes a `RegimeDetector` classification and applies coordinated parameter changes across `HebbianHook`, `GraphAgent`, and `TopologyController` simultaneously. It is a `LoopCallback` and can be passed directly to `Optimizer`.
534
-
535
- ```python
536
- from tensor_optix.neuroevo import NeuromodulatorSignal
537
- from tensor_optix.core import RegimeDetector
538
-
539
- signal = NeuromodulatorSignal(
540
- detector=RegimeDetector(),
541
- hebbian_hook=hook, # optional
542
- agent=agent, # optional — modulates entropy_coef
543
- topology_controller=tc, # optional — modulates grow/prune thresholds
544
- )
545
-
546
- # In your training loop, after each episode:
547
- signal.step(metrics_history)
548
- # trending -> lower entropy_coef, lower hebbian_lr (consolidate)
549
- # ranging -> raise hebbian_lr, lower grow threshold (explore structure)
550
- # volatile -> lower hebbian_lr, raise entropy_coef (cautious plasticity)
551
-
552
- # Or wire as a callback — called automatically each episode
553
- opt = Optimizer(agent, env, callbacks=[hook, signal])
554
- opt.run()
555
- ```
556
-
557
- ### Dale's Law
558
-
559
- ```python
560
- # clamp mode (default): outgoing weights clamped post-step
561
- graph = NeuronGraph(dale_mode="clamp")
562
- graph.add_neuron(role="hidden", activation="relu", cell_type="excitatory") # weights >= 0
563
- graph.add_neuron(role="hidden", activation="tanh", cell_type="inhibitory") # weights <= 0
564
-
565
- # softplus mode: raw parameter theta, effective weight = softplus(theta) * sign
566
- # gradient-safe, no dead zone at the clamp boundary
567
- # enforce_dale() is a no-op in this mode
568
- graph = NeuronGraph(dale_mode="softplus")
569
- w = graph.effective_weight(edge_id) # reads post-softplus value
570
- ```
571
-
572
- ### TopologyAwareAdam
573
-
574
- Drop-in Adam replacement that resets (m, v) momentum state for parameters touched by a grow, prune, or merge operation. Stale momentum estimates from before a structural change would otherwise corrupt the first update on modified parameters.
575
-
576
- ```python
577
- from tensor_optix.neuroevo import TopologyAwareAdam
578
-
579
- optimizer = TopologyAwareAdam(graph.parameters(), lr=3e-4)
580
- optimizer.notify_topology_change(new_params) # call after any topology mutation
581
- ```
582
-
583
- ### Compiled forward
584
-
585
- `NeuronGraph` runs in **eager mode by default**. Because `_raw_forward` mutates Python-side neuron state (`neuron._current`, `push_history`), `torch.compile` cannot safely trace it without replaying those side effects. The default eager path is safe for training, recurrent neurons, and dynamic topologies.
586
-
587
- If the topology is static and you manage neuron state externally, you can opt in to a compiled forward:
588
-
589
- ```python
590
- graph.compile_forward() # one-time call; re-call after any topology mutation
591
- ```
592
-
593
- The backend is selected automatically: `inductor` on Linux/macOS, `aot_eager` on Windows. Has no effect on PyTorch < 2.0.
594
-
595
- `TopologyController` calls `graph.invalidate_compile()` after every grow, prune, and merge. If you mutate the graph directly outside the controller, call it yourself:
596
-
597
- ```python
598
- graph.add_edge(src_id, dst_id, weight=0.0, delay=1)
599
- graph.invalidate_compile()
600
- ```
601
-
602
- `invalidate_compile()` rebuilds the matrix cache and resets to eager mode. If `compile_forward()` was previously called, it also evicts stale Dynamo kernels (`torch._dynamo.reset()`) and recompiles — this reset is process-global, so all `NeuronGraph` instances in the process retrace on their next forward call.
603
-
604
- ---
605
-
606
- ## ML support
607
-
608
- tensor-optix also supports general ML training via `Optimizer`. Pass any `nn.Module` and a PyTorch `Dataset` or `DataLoader` — the same SPSA hyperparameter tuning, rollback on degradation, convergence detection, and checkpointing apply automatically.
609
-
610
- ```python
611
- import tensor_optix as optix
612
- import torch.nn as nn
613
-
614
- # Supervised — loss auto-detected from dataset
615
- model = nn.Sequential(nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10))
616
- opt = optix.Optimizer(model, train_dataset)
617
- opt.run()
618
-
619
- # Explicit loss
620
- opt = optix.Optimizer(model, train_dataset, loss="cross_entropy")
621
- opt = optix.Optimizer(model, train_dataset, loss="mse")
622
-
623
- # Autoencoder
624
- opt = optix.Optimizer(autoencoder, train_dataset, loss="reconstruction")
625
-
626
- # VAE — model must return (reconstruction, mu, logvar)
627
- opt = optix.Optimizer(vae, train_dataset, loss="vae")
628
-
629
- # Contrastive (SimCLR) — dataset yields (view1, view2) pairs
630
- opt = optix.Optimizer(encoder, pairs_dataset, loss="contrastive")
631
-
632
- # DataLoader works too
633
- opt = optix.Optimizer(model, DataLoader(dataset, batch_size=64))
634
-
635
- # Custom loss
636
- opt = optix.Optimizer(model, dataset, loss=nn.HuberLoss())
637
- opt.run()
638
- ```
639
-
640
- Available `loss=` strings:
641
-
642
- | String | Criterion | Use case |
643
- |---|---|---|
644
- | `"auto"` | detected from data | default |
645
- | `"cross_entropy"` | `CrossEntropyLoss` | multi-class classification |
646
- | `"bce"` | `BCEWithLogitsLoss` | binary classification |
647
- | `"mse"` | `MSELoss` | regression |
648
- | `"mae"` | `L1Loss` | regression, outlier-robust |
649
- | `"huber"` | `HuberLoss` | regression, very outlier-robust |
650
- | `"reconstruction"` | MSE(output, input) | autoencoders |
651
- | `"vae"` | ELBO: recon + KL | variational autoencoders |
652
- | `"contrastive"` | NT-Xent | SimCLR-style contrastive learning |
653
-
654
- Any `nn.Module` or callable can be passed directly as `loss=`.
655
-
656
- Save and load work identically to RL agents:
657
-
658
- ```python
659
- agent = optix.Optimizer(model, dataset, loss="cross_entropy")
660
- # Checkpoints written automatically to checkpoint_dir
661
- # Load back:
662
- from tensor_optix.ml import MLAgent
663
- ml_agent.load_weights("checkpoint.pt")
664
- ```
665
-
666
- ---
667
-
668
- ## Core utilities
669
-
670
- ### Normalizers
671
-
672
- Online Welford mean/variance. `ObsNormalizer` normalises observations. `RewardNormalizer` divides rewards by return standard deviation (not reward std), preserving sign.
673
-
674
- ```python
675
- from tensor_optix.core.normalizers import ObsNormalizer, RewardNormalizer
676
-
677
- obs_norm = ObsNormalizer(shape=(obs_dim,))
678
- obs_norm.update(obs_batch)
679
- normalized = obs_norm.normalize(obs)
680
-
681
- rew_norm = RewardNormalizer()
682
- ```
683
-
684
- ### Hindsight Experience Replay
685
-
686
- Wraps `PrioritizedReplayBuffer` with episode-level goal relabeling. Supports `future` (default), `final`, and `episode` relabeling strategies.
687
-
688
- ```python
689
- from tensor_optix.core.her_buffer import HERBuffer
690
-
691
- her = HERBuffer(obs_dim=obs_dim, act_dim=act_dim, goal_dim=goal_dim, strategy="future", k=4)
692
- her.store_episode(obs_list, act_list, rew_list, next_obs_list, done_list,
693
- achieved_goals, compute_reward_fn)
694
- obs_b, act_b, rew_b, next_b, done_b, weights, idx, n = her.sample(batch_size)
695
- ```
696
-
697
- ### Checkpoint registry
698
-
699
- ```python
700
- from tensor_optix.core.checkpoint_registry import CheckpointRegistry
701
-
702
- registry = CheckpointRegistry(checkpoint_dir="./checkpoints", max_snapshots=10)
703
- registry.save(agent, eval_metrics, hyperparams)
704
- registry.load_best(agent)
705
- registry.load_ensemble(agent, top_k=3) # stochastic weight averaging over top-k snapshots
706
- ```
707
-
708
- ### Regime detection
709
-
710
- Classifies score history into one of three regimes using detrended coefficient of variation. Detrended CV measures noise around the trend, not raw score variance.
711
-
712
- ```python
713
- from tensor_optix.core import RegimeDetector
714
-
715
- detector = RegimeDetector()
716
- regime = detector.detect(metrics_history) # "trending" | "ranging" | "volatile"
717
- ```
718
-
719
- ---
720
-
721
- ## Learning-to-Learn Engine
722
-
723
- tensor-optix also includes an optional Learning-to-Learn Engine: a layer that makes experiment history actionable — recording every trial with enough structure to compare reliably, running experiments concurrently under explicit budgets, learning which training strategies help on which tasks, and (recommend-only, never automatic) proposing interventions for a human to review before anything is promoted to a deployed configuration.
724
-
725
- ### Experiment ledger
726
-
727
- Every experiment — a trial, a strategy candidate, a re-evaluation of a deployed agent — is recorded to a SQLite-backed ledger with full provenance (code revision, library versions, hostname) and a typed lifecycle.
728
-
729
- ```python
730
- from tensor_optix.ledger import ExperimentStore, ExperimentState
731
-
732
- store = ExperimentStore("./experiments.sqlite3")
733
- record = store.create_experiment(
734
- task_env="CartPole-v1", algorithm="PPO", framework="torch",
735
- seed=0, checkpoint_dir="./ckpt", solve_threshold=475.0,
736
- )
737
- store.start_experiment(record.experiment_id)
738
- store.record_progress(record.experiment_id, best_primary_score=480.0)
739
- store.finalize_experiment(record.experiment_id, state=ExperimentState.COMPLETED)
740
- ```
741
-
742
- Writes are idempotent on `experiment_id`, and every method opens a short, single-statement transaction — nothing here ever holds a lock for the duration of training.
743
-
744
- ### Concurrent trial scheduler
745
-
746
- `ConcurrentTrialScheduler` runs multiple Optuna trials as real OS processes, with the parent process as sole owner of the Optuna study and the ledger.
747
-
748
- ```python
749
- from tensor_optix.scheduler import ConcurrentTrialScheduler
750
-
751
- scheduler = ConcurrentTrialScheduler(
752
- agent_factory=make_agent, pipeline_factory=make_pipeline,
753
- param_space={"learning_rate": ("log_float", 1e-4, 3e-3)},
754
- n_trials=20, n_workers=4, ledger_store=store,
755
- task_env="CartPole-v1", algorithm="PPO", solve_threshold=475.0,
756
- )
757
- best_params, best_score = scheduler.run()
758
- ```
759
-
760
- ### Cross-task transfer (Nanonets)
761
-
762
- `NanonetsSelector` pools evidence from other tasks' ledger history when deciding how to allocate a new task's trial budget — but only when a conservative abstention gate confirms a source task's data is actually comparable (median performance at or above the target's own 95th percentile, both normalized against each task's own solve threshold so differently-scaled/signed scores are never compared on raw terms). When nothing passes the gate, it falls back to the non-learned baseline rather than guess.
763
-
764
- ```python
765
- from tensor_optix.nanonets import NanonetsSelector
766
-
767
- selector = NanonetsSelector(store, source_task_envs=["Acrobot-v1"])
768
- decision = selector.propose(task_env="CartPole-v1", methods=["TPESampler"], budget=10)
769
- print(decision.rationale) # states explicitly whether it pooled or fell back
770
- ```
771
-
772
- ### Strategy discovery
773
-
774
- A small, closed set of typed training-procedure operations (learning-rate trend adjustment, checkpoint rollback, exploration bounds, optimizer-method choice, budget allocation, schedule changes, policy-variant selection) — never arbitrary generated code. Each candidate is run paired against a no-strategy baseline, compared via a conservative confidence-interval gate, and checked against a 6-criterion promotion policy.
775
-
776
- ```python
777
- from tensor_optix.strategy import LearningRateTrendOp, StrategyCandidate, StrategyTrialRunner, PromotionPolicy
778
-
779
- candidate = StrategyCandidate.from_op(LearningRateTrendOp(window=5, increase_factor=1.2))
780
- runner = StrategyTrialRunner(
781
- agent_factory=make_agent, pipeline_factory=make_pipeline,
782
- trial_steps=50_000, ledger_store=store, task_env="CartPole-v1",
783
- algorithm="PPO", solve_threshold=475.0,
784
- )
785
- results = runner.run_candidate(candidate, seeds=[0, 1, 2])
786
-
787
- policy = PromotionPolicy(store, margin=5.0, min_seeds=3, approval_mode="human")
788
- verdict = policy.evaluate(candidate_id=candidate.candidate_id, train_task_envs=["CartPole-v1"], held_out_task_envs=[])
789
- ```
790
-
791
- `approval_mode="human"` (the default) means `verdict.criteria["human_reviewed"]` can only ever become `True` via a separate, explicit `store.approve_strategy(...)` call — `PromotionPolicy.evaluate()` never sets it itself.
792
-
793
- ### Live adaptive runtime
794
-
795
- `LiveAdaptiveRuntime` runs the full observe → detect → propose → experiment → evaluate → recommend loop against a long-running deployed agent, standing in for live inference feedback. It is **recommend-only**: it issues durable, auditable recommendation records to the ledger, but the loop itself never calls `approve_strategy` and never changes which agent is deployed. Promoting a candidate requires two separate, explicit human actions outside the loop (`store.approve_strategy(...)`, then `promote_candidate_to_baseline(...)`).
796
-
797
- ```python
798
- from tensor_optix.runtime import LiveAdaptiveRuntime, RuntimeConfig
799
-
800
- config = RuntimeConfig(task_env="CartPole-v1", algorithm="PPO", max_cycles=20)
801
- runtime = LiveAdaptiveRuntime(store, config, agent_factory=make_agent, pipeline_factory=make_pipeline)
802
- observations, recommendations = runtime.run()
803
- ```
804
-
805
- Full guide: [Learning-to-Learn Engine](https://sup3rus3r.github.io/tensor-optix/guides/learning-to-learn/).
806
-
807
- ---
808
-
809
- ## Requirements
810
-
811
- - Python >= 3.11
812
- - gymnasium >= 1.0
813
- - numpy >= 1.24
814
-
815
- The core loop, `PolicyManager`, and all ensemble and evolution logic have no framework dependency. Framework installs are opt-in via extras.
1
+ Metadata-Version: 2.4
2
+ Name: tensor-optix
3
+ Version: 2.0.1
4
+ Summary: Autonomous training loop for any sequential learning model — PPO, DQN, SAC, TD3, Rainbow DQN, Recurrent PPO for TensorFlow, PyTorch, and JAX/Flax; distributed async actor-learner (IMPALA + V-trace)
5
+ Author: sup3rus3r
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: gymnasium[box2d]>=1.0.0
10
+ Requires-Dist: numpy>=1.24.0
11
+ Requires-Dist: matplotlib>=3.7.0
12
+ Requires-Dist: optuna>=3.0.0
13
+ Requires-Dist: swig>=4.4.1
14
+ Requires-Dist: pyyaml>=6.0
15
+ Requires-Dist: torch>=2.11.0
16
+ Provides-Extra: tensorflow
17
+ Requires-Dist: tensorflow>=2.18.0; extra == "tensorflow"
18
+ Provides-Extra: tensorflow-gpu
19
+ Requires-Dist: tensorflow[and-cuda]>=2.18.0; sys_platform == "linux" and extra == "tensorflow-gpu"
20
+ Provides-Extra: torch
21
+ Requires-Dist: torch>=2.11.0; extra == "torch"
22
+ Requires-Dist: torchvision; extra == "torch"
23
+ Requires-Dist: torchaudio; extra == "torch"
24
+ Provides-Extra: jax
25
+ Requires-Dist: flax>=0.12.6; extra == "jax"
26
+ Requires-Dist: jax>=0.10.0; extra == "jax"
27
+ Requires-Dist: optax>=0.2.8; extra == "jax"
28
+ Provides-Extra: cuda
29
+ Requires-Dist: nvidia-cuda-nvcc-cu12; sys_platform == "linux" and extra == "cuda"
30
+ Requires-Dist: tensorflow[and-cuda]>=2.18.0; sys_platform == "linux" and extra == "cuda"
31
+ Provides-Extra: box2d
32
+ Requires-Dist: gymnasium[box2d]>=1.0.0; extra == "box2d"
33
+ Provides-Extra: atari
34
+ Requires-Dist: gymnasium[atari]>=1.0.0; extra == "atari"
35
+ Requires-Dist: ale-py>=0.8; extra == "atari"
36
+ Provides-Extra: mujoco
37
+ Requires-Dist: gymnasium[mujoco]>=1.0.0; extra == "mujoco"
38
+ Provides-Extra: all
39
+ Requires-Dist: tensorflow>=2.18.0; extra == "all"
40
+ Requires-Dist: torch>=2.11.0; extra == "all"
41
+ Requires-Dist: torchvision; extra == "all"
42
+ Requires-Dist: torchaudio; extra == "all"
43
+ Requires-Dist: gymnasium[atari]>=1.0.0; extra == "all"
44
+ Requires-Dist: gymnasium[mujoco]>=1.0.0; extra == "all"
45
+ Requires-Dist: ale-py>=0.8; extra == "all"
46
+ Requires-Dist: jax>=0.10.0; extra == "all"
47
+ Requires-Dist: flax>=0.12.6; extra == "all"
48
+ Requires-Dist: optax>=0.2.8; extra == "all"
49
+ Requires-Dist: torch>=2.11.0; extra == "all"
50
+ Provides-Extra: neuroevo
51
+ Requires-Dist: torch>=2.11.0; extra == "neuroevo"
52
+ Provides-Extra: onnx
53
+ Requires-Dist: onnx>=1.14; extra == "onnx"
54
+ Requires-Dist: onnxruntime>=1.16; extra == "onnx"
55
+ Requires-Dist: onnxscript>=0.1; extra == "onnx"
56
+ Provides-Extra: wandb
57
+ Requires-Dist: wandb>=0.16; extra == "wandb"
58
+ Provides-Extra: tensorboard
59
+ Requires-Dist: torch>=2.11.0; extra == "tensorboard"
60
+ Requires-Dist: tensorboard>=2.14; extra == "tensorboard"
61
+ Provides-Extra: dev
62
+ Requires-Dist: pytest>=7.0; extra == "dev"
63
+ Requires-Dist: pytest-cov; extra == "dev"
64
+ Requires-Dist: black; extra == "dev"
65
+ Requires-Dist: ruff; extra == "dev"
66
+ Requires-Dist: mypy; extra == "dev"
67
+ Requires-Dist: twine>=5.0; extra == "dev"
68
+ Requires-Dist: torch>=2.11.0; extra == "dev"
69
+
70
+ # tensor-optix
71
+
72
+ tensor-optix is a training loop framework with statistical convergence control, online hyperparameter optimisation, and an optional neuroevolution subsystem for dynamic topology.
73
+
74
+ The core loop runs your agent against a pipeline, maintains a separate validation signal, and manages four states: ACTIVE, COOLING, DORMANT, and watchdog shutdown. Convergence is detected using a corrected t-test on the smoothed score slope plus lag-1 autocorrelation, not a fixed patience counter. Hyperparameters are updated every episode via SPSA gradient estimates, with automatic routing to momentum-based or sign-only updates depending on the autocorrelation structure of the score landscape. Checkpointing and rollback are driven by the validation signal only, never training score. On DORMANT, a MetaController evaluates the generalization gap and its slope to decide between spawning a policy variant, pruning the ensemble, or stopping.
75
+
76
+ The neuroevo subsystem (`pip install tensor-optix[neuroevo]`) represents the policy as a `NeuronGraph`: a mutable directed graph of heterogeneous scalar neurons (point, GRU, LSTM, trainable-GRU, trainable-LSTM) with variable-delay edges. Weights for excitatory and inhibitory neurons follow softplus Dale's Law: raw parameter θ maps to `softplus(θ)` (excitatory) or `-softplus(θ)` (inhibitory), eliminating gradient dead zones at weight boundaries. `TopologyController` runs as a loop callback and evaluates three independent signals per episode: improvement slope significance, residual autocorrelation structure, and gradient utilization across hidden neurons. All three must cross their thresholds before a grow operation fires. Pruning is by importance score (incident edge weight magnitude times mean absolute activation). Merging is by Pearson correlation of per-episode activation histories. After every structural mutation the controller calls `graph.invalidate_compile()` to reset for the new topology. `TopologyAwareAdam` resets momentum state for parameters affected by any structural change. `BrainNetwork` composes multiple named `NeuronGraph` regions with sparse learnable inter-region edges — when a neuron is pruned from a region, all inter-region edges referencing it are automatically cleaned up. `HebbianHook` accumulates co-activation products across each episode and applies an Oja-style weight update after the PPO gradient step. `NeuromodulatorSignal` maps a `RegimeDetector` output (trending / ranging / volatile) to simultaneous changes in Hebbian learning rate, entropy coefficient, and topology grow/prune thresholds.
77
+
78
+ The entire system, including neuroevo, is accessed through a six-method `BaseAgent` interface:
79
+
80
+ ```python
81
+ class BaseAgent(ABC):
82
+ def act(self, observation) -> any: ...
83
+ def learn(self, episode_data: EpisodeData) -> dict: ...
84
+ def get_hyperparams(self) -> HyperparamSet: ...
85
+ def set_hyperparams(self, hyperparams: HyperparamSet) -> None: ...
86
+ def save_weights(self, path: str) -> None: ...
87
+ def load_weights(self, path: str) -> None: ...
88
+ ```
89
+
90
+ Fifteen agents are included across PyTorch, TensorFlow, and JAX/Flax. Bring your own by implementing the interface above.
91
+
92
+ ---
93
+
94
+ ## Install
95
+
96
+ ```bash
97
+ # Core loop only, no algorithm implementations
98
+ pip install tensor-optix
99
+
100
+ # PyTorch algorithms
101
+ pip install tensor-optix[torch]
102
+
103
+ # TensorFlow algorithms
104
+ pip install tensor-optix[tensorflow]
105
+
106
+ # JAX/Flax
107
+ pip install tensor-optix[jax]
108
+
109
+ # Neuroevo (requires torch)
110
+ pip install tensor-optix[neuroevo]
111
+
112
+ # GPU (Linux/WSL2, CUDA 12.8)
113
+ pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128
114
+ pip install tensor-optix[torch]
115
+
116
+ # All frameworks and environment extras
117
+ pip install tensor-optix[all]
118
+ ```
119
+
120
+ Environment extras: `[box2d]`, `[atari]`, `[mujoco]`
121
+ Logging extras: `[wandb]`, `[tensorboard]`
122
+ Export: `[onnx]`
123
+
124
+ ---
125
+
126
+ ## Algorithms
127
+
128
+ All agents implement `BaseAgent` and are interchangeable with `RLOptimizer`.
129
+
130
+ ### PyTorch
131
+
132
+ | Agent | Algorithm | Action Space |
133
+ |---|---|---|
134
+ | `TorchPPOAgent` | PPO + GAE-λ | Discrete |
135
+ | `TorchGaussianPPOAgent` | PPO | Continuous |
136
+ | `TorchRecurrentPPOAgent` | PPO + GRU/LSTM hidden state | Discrete |
137
+ | `TorchDQNAgent` | DQN + PER + n-step returns | Discrete |
138
+ | `TorchRainbowDQNAgent` | Rainbow DQN (NoisyNet, distributional, PER, n-step, dueling, double) | Discrete |
139
+ | `TorchSACAgent` | SAC, twin Q-critics, automatic entropy tuning | Continuous |
140
+ | `TorchTD3Agent` | TD3 | Continuous |
141
+
142
+ ### TensorFlow
143
+
144
+ `TFPPOAgent`, `TFGaussianPPOAgent`, `TFDQNAgent`, `TFSACAgent`, `TFTDDAgent`
145
+
146
+ ### JAX/Flax
147
+
148
+ `FlaxPPOAgent`
149
+
150
+ ### Auto-selection
151
+
152
+ `make_agent` inspects the environment action space and returns a fully constructed agent. Pass an algorithm name as the first argument, or let it be inferred.
153
+
154
+ ```python
155
+ from tensor_optix import make_agent
156
+ import gymnasium as gym
157
+
158
+ env = gym.make("LunarLanderContinuous-v3")
159
+ agent = make_agent(env) # -> TorchSACAgent (continuous)
160
+ agent = make_agent("SAC", env) # same, explicit
161
+ agent = make_agent(env, framework="tf") # -> TFSACAgent
162
+ agent = make_agent(env, deterministic=True) # -> TorchTD3Agent
163
+
164
+ # Discrete action spaces
165
+ env = gym.make("CartPole-v1")
166
+ agent = make_agent(env) # -> TorchPPOAgent (default discrete)
167
+ agent = make_agent(env, algorithm="DQN") # -> TorchDQNAgent
168
+ agent = make_agent(env, algorithm="RAINBOW") # -> TorchRainbowDQNAgent
169
+
170
+ # Neuroevo path: NeuronGraph + GraphAgent with Hebbian + TopologyController
171
+ agent = make_agent(env, neuroevo=True)
172
+ agent = make_agent("PPO", env, neuroevo=True, graph_hidden=16, hebbian_lr=1e-3)
173
+
174
+ # Feature-extractor mode: NeuronGraph -> features concatenated to obs, fed into SAC
175
+ agent = make_agent("SAC", env, neuroevo=True, neuroevo_mode="feature_extractor")
176
+ ```
177
+
178
+ Neuroevo options: `graph_in` (input neurons, default `min(obs_dim, 16)`), `graph_hidden` (GRU neurons, default 8), `graph_out` (output neurons, default `act_dim + 1`), `hebbian_lr`, `hebbian_decay`, `grow_cooldown`.
179
+
180
+ `neuroevo_mode` controls how the graph is used:
181
+ - `"policy"` (default) — graph IS the policy, wrapped as `GraphAgent` (PPO-based)
182
+ - `"feature_extractor"` — graph runs in parallel as a feature extractor; its `graph_out`-dim output is concatenated with the raw observation before SAC actor/critic networks. The base SAC layer handles exploration and replay; the graph adds adaptive temporal features via GRU + LSTM neurons with Hebbian learning.
183
+
184
+ ---
185
+
186
+ ## One-line training with Optimizer
187
+
188
+ `Optimizer` wraps `RLOptimizer` with sensible defaults. It auto-computes `window_size`, wires neuroevo callbacks, and activates SPSA when the agent has `default_param_bounds`.
189
+
190
+ ```python
191
+ from tensor_optix import make_agent, Optimizer
192
+ import gymnasium as gym
193
+
194
+ env = gym.make("HumanoidStandup-v5")
195
+ agent = make_agent("SAC", env, neuroevo=True)
196
+ opt = Optimizer(agent, env)
197
+ opt.run()
198
+
199
+ # Vectorized: 8 parallel envs
200
+ opt = Optimizer(agent, lambda: gym.make("CartPole-v1"), n_envs=8)
201
+ opt.run()
202
+ ```
203
+
204
+ `optimal_window_size(env, algorithm)` computes the window size formula used internally: `clip(k * mean_episode_steps, 512, 8192)` where k=4.0 for on-policy (PPO) and k=1.0 for off-policy (SAC/TD3).
205
+
206
+ ```python
207
+ from tensor_optix import optimal_window_size
208
+ window = optimal_window_size(env, "PPO") # e.g. 2000 for CartPole
209
+ ```
210
+
211
+ ---
212
+
213
+ ## Pipelines
214
+
215
+ A pipeline steps an environment (or data source), collects `EpisodeData`, and yields it to the agent. Three implementations are provided.
216
+
217
+ ```python
218
+ from tensor_optix import BatchPipeline, LivePipeline, VectorBatchPipeline
219
+
220
+ # Gymnasium env: steps continuously, no reset between windows
221
+ pipeline = BatchPipeline(env=gym.make("CartPole-v1"), agent=agent, window_size=200)
222
+
223
+ # External data stream: background thread with bounded queue, configurable episode boundaries
224
+ pipeline = LivePipeline(
225
+ data_source=MyFeed(),
226
+ agent=agent,
227
+ episode_boundary_fn=LivePipeline.every_n_seconds(300),
228
+ )
229
+
230
+ # N parallel envs via gymnasium.vector, sync or async subprocess
231
+ pipeline = VectorBatchPipeline(
232
+ env_fns=[lambda: gym.make("CartPole-v1")] * 8,
233
+ agent=agent,
234
+ window_size=200,
235
+ )
236
+ ```
237
+
238
+ ---
239
+
240
+ ## The loop
241
+
242
+ ```python
243
+ from tensor_optix import RLOptimizer
244
+
245
+ opt = RLOptimizer(
246
+ agent=agent,
247
+ pipeline=pipeline,
248
+
249
+ # Separate validation pipeline. All checkpoint and rollback decisions use val score only.
250
+ val_pipeline=val_pipeline,
251
+ rollback_on_degradation=True,
252
+
253
+ # Optional external scorer run at checkpoint evaluation (e.g. held-out backtest)
254
+ checkpoint_score_fn=lambda a: evaluate(a, held_out_env),
255
+
256
+ # Convergence parameters
257
+ dormant_threshold=10, # consecutive episodes without improvement -> DORMANT
258
+ min_episodes_before_dormant=50, # statistical warmup before convergence detection activates
259
+ )
260
+
261
+ opt.run()
262
+ opt.best_snapshot # -> PolicySnapshot: best weights + EvalMetrics + HyperparamSet
263
+ ```
264
+
265
+ Loop state transitions: `ACTIVE` -> `COOLING` -> `DORMANT` -> watchdog shutdown or policy spawn.
266
+
267
+ On shutdown the loop restores best-known weights, not the final checkpoint.
268
+
269
+ ---
270
+
271
+ ## Hyperparameter optimisation
272
+
273
+ All optimisers operate in normalised [0, 1] parameter space and update every episode. No restarts required.
274
+
275
+ ```python
276
+ from tensor_optix.optimizers import SPSAOptimizer, AdaptiveOptimizer
277
+
278
+ # SPSA: Rademacher perturbation vector, two-episode gradient estimate
279
+ optimizer = SPSAOptimizer(
280
+ param_bounds={"learning_rate": (1e-4, 3e-3), "clip_ratio": (0.1, 0.3)},
281
+ log_params={"learning_rate"}, # log-space normalisation for params spanning orders of magnitude
282
+ )
283
+
284
+ # AdaptiveOptimizer: routes between SPSA, Momentum, Backoff, and PBT
285
+ # based on lag-1 autocorrelation of the score stream and relative performance gap
286
+ optimizer = AdaptiveOptimizer(param_bounds={...})
287
+
288
+ opt = RLOptimizer(agent=agent, pipeline=pipeline, optimizer=optimizer)
289
+ ```
290
+
291
+ | Optimizer | Routing condition |
292
+ |---|---|
293
+ | `SPSAOptimizer` | i.i.d. score noise, no autocorrelation structure |
294
+ | `MomentumOptimizer` | Positive lag-1 autocorrelation (smooth landscape) |
295
+ | `BackoffOptimizer` | Negative lag-1 autocorrelation (oscillating landscape, sign-only updates) |
296
+ | `PBTOptimizer` | Score below 20th percentile of history (exploit checkpoint population) |
297
+ | `AdaptiveOptimizer` | Routes automatically based on the two signals above |
298
+
299
+ ### Trial-level search
300
+
301
+ `TrialOrchestrator` runs N independent short trials via Optuna TPE before the main run, then warm-starts from the best trial's weights and config.
302
+
303
+ ```python
304
+ from tensor_optix import TrialOrchestrator
305
+
306
+ orch = TrialOrchestrator(
307
+ agent_factory=make_agent,
308
+ pipeline_factory=make_pipeline,
309
+ param_space={
310
+ "learning_rate": ("log_float", 1e-4, 3e-3),
311
+ "clip_ratio": ("float", 0.1, 0.3),
312
+ "batch_size": ("int", 32, 512),
313
+ },
314
+ n_trials=20,
315
+ trial_episodes=50,
316
+ )
317
+ best_config = orch.run()
318
+ ```
319
+
320
+ ---
321
+
322
+ ## Ensemble and policy evolution
323
+
324
+ `PolicyManager` runs as a loop callback. On each DORMANT event, `MetaController` evaluates the generalization gap (train minus val, normalised) and its slope, and the validation improvement rate, then issues one of: SPAWN, PRUNE, or STOP. Spawned variants are cloned from the best checkpoint with perturbed hyperparameters. `EnsembleAgent` wraps all active variants behind the `BaseAgent` interface, with weighted action averaging.
325
+
326
+ ```python
327
+ from tensor_optix import PolicyManager
328
+
329
+ pm = PolicyManager(registry, max_spawns=4)
330
+ cb = pm.as_callback(agent, agent_factory=make_agent)
331
+ cb.set_stop_fn(opt.stop)
332
+ opt.add_callback(cb)
333
+ opt.run()
334
+ ```
335
+
336
+ ---
337
+
338
+ ## Callbacks
339
+
340
+ ```python
341
+ from tensor_optix.callbacks import RichDashboardCallback, WandbCallback, TensorBoardCallback
342
+
343
+ opt.add_callback(RichDashboardCallback()) # Rich live terminal panel
344
+ opt.add_callback(WandbCallback(project="run"))
345
+ opt.add_callback(TensorBoardCallback(log_dir="./tb"))
346
+ ```
347
+
348
+ Custom callbacks subclass `LoopCallback` and override any of:
349
+
350
+ ```python
351
+ class LoopCallback:
352
+ def on_loop_start(self) -> None: ...
353
+ def on_loop_stop(self) -> None: ...
354
+ def on_episode_end(self, episode_id: int, eval_metrics) -> None: ...
355
+ def on_improvement(self, snapshot) -> None: ...
356
+ def on_plateau(self, episode_id: int, state) -> None: ...
357
+ def on_dormant(self, episode_id: int) -> None: ...
358
+ def on_degradation(self, episode_id: int, eval_metrics) -> None: ...
359
+ def on_hyperparam_update(self, old: dict, new: dict) -> None: ...
360
+ ```
361
+
362
+ `HebbianHook` and `NeuromodulatorSignal` are both `LoopCallback` subclasses and can be passed directly to `Optimizer` or `RLOptimizer` via `callbacks=`.
363
+
364
+ ---
365
+
366
+ ## Distributed training (IMPALA + V-trace)
367
+
368
+ `AsyncActorLearner` implements IMPALA-style async actor-learner. N actor subprocesses read weights from shared memory (lock-free), collect trajectories, and push them to a queue. The learner dequeues trajectories, applies V-trace importance-sampling correction, and writes updated weights back to shared memory.
369
+
370
+ ```python
371
+ from tensor_optix.distributed import AsyncActorLearner
372
+
373
+ learner = AsyncActorLearner(
374
+ actor=actor,
375
+ critic=critic,
376
+ optimizer=optimizer,
377
+ env_factory=lambda: gym.make("ALE/Pong-v5"),
378
+ n_actors=8,
379
+ trajectory_len=64,
380
+ )
381
+ stats = learner.run(max_steps=10_000_000)
382
+ # stats["steps_per_second"] -> ~4x single-process throughput on CPU
383
+ ```
384
+
385
+ ---
386
+
387
+ ## Neuroevo
388
+
389
+ `NeuronGraph` is a mutable directed graph of scalar neurons with variable-delay edges. `GraphAgent` wraps it as a `BaseAgent` with PPO-style weight learning. `TopologyController` mutates the graph live during the training loop.
390
+
391
+ ```bash
392
+ pip install tensor-optix[neuroevo]
393
+ ```
394
+
395
+ ### Graph construction
396
+
397
+ ```python
398
+ from tensor_optix.neuroevo import NeuronGraph, GraphAgent, GRUNeuron, LSTMNeuron
399
+
400
+ graph = NeuronGraph()
401
+
402
+ for _ in range(4):
403
+ graph.add_neuron(role="input", activation="linear")
404
+ for _ in range(8):
405
+ graph.add_neuron(role="hidden", activation="tanh")
406
+ # or: graph.add_neuron(role="hidden", neuron=GRUNeuron())
407
+ # or: graph.add_neuron(role="hidden", neuron=LSTMNeuron())
408
+ graph.add_neuron(role="output", activation="linear") # last output neuron is the value head
409
+
410
+ graph.add_edge(src_id, dst_id, weight=0.0, delay=0) # feedforward (d=0)
411
+ graph.add_edge(src_id, dst_id, weight=0.0, delay=1) # recurrent (d>=1, reads from history buffer)
412
+
413
+ agent = GraphAgent(graph, obs_dim=4, n_actions=2)
414
+ ```
415
+
416
+ All edges initialise at weight=0.0, which is function-preserving at insertion time.
417
+
418
+ ### Neuron types
419
+
420
+ | Type | Hidden state | Gradient through state |
421
+ |---|---|---|
422
+ | `Neuron` | None (point neuron) | N/A |
423
+ | `GRUNeuron` | Scalar h, detached | No |
424
+ | `LSTMNeuron` | Scalar h and c, detached | No |
425
+ | `TrainableGRUNeuron` | Scalar h, not detached | Yes, up to chunk_len steps |
426
+ | `TrainableLSTMNeuron` | Scalar h and c, not detached | Yes, up to chunk_len steps |
427
+
428
+ All types implement the same protocol: `step()`, `importance()`, `can_merge_with()`, `make_relay()`, `split_copy()`. `NeuronGraph` and `TopologyController` are type-blind.
429
+
430
+ ### Trainable recurrent neurons
431
+
432
+ `TrainableGRUNeuron` and `TrainableLSTMNeuron` set `is_recurrent = True`. `RecurrentGraphAgent` detects this flag and switches from shuffled-minibatch PPO to sequential chunk training with truncated BPTT.
433
+
434
+ ```python
435
+ from tensor_optix.neuroevo import TrainableGRUNeuron, TrainableLSTMNeuron, RecurrentGraphAgent
436
+
437
+ graph = NeuronGraph()
438
+ # ... input and output neurons ...
439
+ graph.add_neuron(role="hidden", neuron=TrainableGRUNeuron())
440
+ graph.add_neuron(role="hidden", neuron=TrainableLSTMNeuron())
441
+
442
+ agent = RecurrentGraphAgent(
443
+ graph, obs_dim=4, n_actions=2,
444
+ hyperparams=HyperparamSet(params={"chunk_len": 64}),
445
+ )
446
+ # Falls back to standard shuffled-minibatch PPO if no recurrent neurons are present
447
+ ```
448
+
449
+ ### Topology controller
450
+
451
+ ```python
452
+ from tensor_optix.neuroevo import TopologyController
453
+
454
+ controller = TopologyController.for_graph(
455
+ graph=graph,
456
+ scheduler=opt._scheduler,
457
+ grow_grad_threshold=0.7, # fraction of hidden neurons with |grad| > eps required to grow
458
+ prune_neuron_threshold=1e-4, # importance score below this -> prune candidate
459
+ prune_edge_threshold=1e-3, # |weight| below this for prune_edge_patience episodes -> prune
460
+ merge_similarity_threshold=0.95, # Pearson correlation threshold for merge
461
+ )
462
+ opt.add_callback(controller)
463
+ opt.run()
464
+ ```
465
+
466
+ Grow fires only when all three signals agree:
467
+ 1. Improvement slope t-test is not significant (gradient updates are not making progress)
468
+ 2. Score residuals have significant autocorrelation (capacity is underutilised)
469
+ 3. Gradient utilization exceeds `grow_grad_threshold` (existing neurons are saturated)
470
+
471
+ For multi-region graphs, use `TopologyController.for_brain(brain, scheduler=...)`. Each region gets independent signal buffers and cooldown timers. When a neuron is pruned from a region, all inter-region `BrainNetwork` edges referencing it are automatically removed.
472
+
473
+ ### Save and load
474
+
475
+ Topology-aware checkpointing works identically to non-evo agents. The topology (neuron types, roles, edges) is saved alongside weights so the graph is fully reconstructed on load — no manual topology reconstruction required.
476
+
477
+ ```python
478
+ # Save
479
+ agent.save_weights("checkpoint.pt")
480
+
481
+ # Load into any agent instance — topology is reconstructed automatically
482
+ agent2 = make_agent(env, neuroevo=True)
483
+ agent2.load_weights("checkpoint.pt")
484
+
485
+ # Or skip constructing an agent entirely
486
+ agent3 = GraphAgent.from_checkpoint("checkpoint.pt")
487
+ agent3.act(obs)
488
+ ```
489
+
490
+ Rollback on degradation uses the same path internally — when the loop restores a best checkpoint, the topology at the time of that checkpoint is fully restored, even if the graph has grown or been pruned since.
491
+
492
+ ### BrainNetwork
493
+
494
+ ```python
495
+ from tensor_optix.neuroevo import BrainNetwork, TopologyController
496
+
497
+ brain = BrainNetwork()
498
+ brain.add_region("sensory", sensory_graph)
499
+ brain.add_region("memory", memory_graph)
500
+ brain.add_region("executive", executive_graph)
501
+
502
+ brain.add_pathway("sensory", "memory", n_connections=8, delay=1)
503
+ brain.add_pathway("memory", "executive", n_connections=8, delay=0)
504
+
505
+ controller = TopologyController.for_brain(brain, scheduler=opt._scheduler)
506
+ ```
507
+
508
+ Inter-region edges are learnable parameters. Regions are executed in topological order each forward pass.
509
+
510
+ ### Hebbian learning
511
+
512
+ `HebbianHook` applies an Oja-style local weight update after each episode. The rule is: `dw = eta * mean_t(h_pre * h_post) - lambda * w`. Call `record()` after each `act()` to accumulate co-activation products, then `apply()` after the PPO gradient step.
513
+
514
+ ```python
515
+ from tensor_optix.neuroevo import HebbianHook
516
+
517
+ hook = HebbianHook(graph, hebbian_lr=1e-3, weight_decay=1e-4)
518
+
519
+ for step in episode:
520
+ action = agent.act(obs)
521
+ hook.record()
522
+ obs, reward, done, _ = env.step(action)
523
+
524
+ agent.learn(episode_data)
525
+ hook.apply()
526
+ hook.reset()
527
+ ```
528
+
529
+ `HebbianHook` is a `LoopCallback` — pass it directly to `Optimizer` or `RLOptimizer` via `callbacks=` and it wires itself automatically. Use `HebbianHook.from_brain(brain, ...)` for `BrainNetwork` graphs.
530
+
531
+ ### Neuromodulation
532
+
533
+ `NeuromodulatorSignal` takes a `RegimeDetector` classification and applies coordinated parameter changes across `HebbianHook`, `GraphAgent`, and `TopologyController` simultaneously. It is a `LoopCallback` and can be passed directly to `Optimizer`.
534
+
535
+ ```python
536
+ from tensor_optix.neuroevo import NeuromodulatorSignal
537
+ from tensor_optix.core import RegimeDetector
538
+
539
+ signal = NeuromodulatorSignal(
540
+ detector=RegimeDetector(),
541
+ hebbian_hook=hook, # optional
542
+ agent=agent, # optional — modulates entropy_coef
543
+ topology_controller=tc, # optional — modulates grow/prune thresholds
544
+ )
545
+
546
+ # In your training loop, after each episode:
547
+ signal.step(metrics_history)
548
+ # trending -> lower entropy_coef, lower hebbian_lr (consolidate)
549
+ # ranging -> raise hebbian_lr, lower grow threshold (explore structure)
550
+ # volatile -> lower hebbian_lr, raise entropy_coef (cautious plasticity)
551
+
552
+ # Or wire as a callback — called automatically each episode
553
+ opt = Optimizer(agent, env, callbacks=[hook, signal])
554
+ opt.run()
555
+ ```
556
+
557
+ ### Dale's Law
558
+
559
+ ```python
560
+ # clamp mode (default): outgoing weights clamped post-step
561
+ graph = NeuronGraph(dale_mode="clamp")
562
+ graph.add_neuron(role="hidden", activation="relu", cell_type="excitatory") # weights >= 0
563
+ graph.add_neuron(role="hidden", activation="tanh", cell_type="inhibitory") # weights <= 0
564
+
565
+ # softplus mode: raw parameter theta, effective weight = softplus(theta) * sign
566
+ # gradient-safe, no dead zone at the clamp boundary
567
+ # enforce_dale() is a no-op in this mode
568
+ graph = NeuronGraph(dale_mode="softplus")
569
+ w = graph.effective_weight(edge_id) # reads post-softplus value
570
+ ```
571
+
572
+ ### TopologyAwareAdam
573
+
574
+ Drop-in Adam replacement that resets (m, v) momentum state for parameters touched by a grow, prune, or merge operation. Stale momentum estimates from before a structural change would otherwise corrupt the first update on modified parameters.
575
+
576
+ ```python
577
+ from tensor_optix.neuroevo import TopologyAwareAdam
578
+
579
+ optimizer = TopologyAwareAdam(graph.parameters(), lr=3e-4)
580
+ optimizer.notify_topology_change(new_params) # call after any topology mutation
581
+ ```
582
+
583
+ ### Compiled forward
584
+
585
+ `NeuronGraph` runs in **eager mode by default**. Because `_raw_forward` mutates Python-side neuron state (`neuron._current`, `push_history`), `torch.compile` cannot safely trace it without replaying those side effects. The default eager path is safe for training, recurrent neurons, and dynamic topologies.
586
+
587
+ If the topology is static and you manage neuron state externally, you can opt in to a compiled forward:
588
+
589
+ ```python
590
+ graph.compile_forward() # one-time call; re-call after any topology mutation
591
+ ```
592
+
593
+ The backend is selected automatically: `inductor` on Linux/macOS, `aot_eager` on Windows. Has no effect on PyTorch < 2.0.
594
+
595
+ `TopologyController` calls `graph.invalidate_compile()` after every grow, prune, and merge. If you mutate the graph directly outside the controller, call it yourself:
596
+
597
+ ```python
598
+ graph.add_edge(src_id, dst_id, weight=0.0, delay=1)
599
+ graph.invalidate_compile()
600
+ ```
601
+
602
+ `invalidate_compile()` rebuilds the matrix cache and resets to eager mode. If `compile_forward()` was previously called, it also evicts stale Dynamo kernels (`torch._dynamo.reset()`) and recompiles — this reset is process-global, so all `NeuronGraph` instances in the process retrace on their next forward call.
603
+
604
+ ---
605
+
606
+ ## ML support
607
+
608
+ tensor-optix also supports general ML training via `Optimizer`. Pass any `nn.Module` and a PyTorch `Dataset` or `DataLoader` — the same SPSA hyperparameter tuning, rollback on degradation, convergence detection, and checkpointing apply automatically.
609
+
610
+ ```python
611
+ import tensor_optix as optix
612
+ import torch.nn as nn
613
+
614
+ # Supervised — loss auto-detected from dataset
615
+ model = nn.Sequential(nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10))
616
+ opt = optix.Optimizer(model, train_dataset)
617
+ opt.run()
618
+
619
+ # Explicit loss
620
+ opt = optix.Optimizer(model, train_dataset, loss="cross_entropy")
621
+ opt = optix.Optimizer(model, train_dataset, loss="mse")
622
+
623
+ # Autoencoder
624
+ opt = optix.Optimizer(autoencoder, train_dataset, loss="reconstruction")
625
+
626
+ # VAE — model must return (reconstruction, mu, logvar)
627
+ opt = optix.Optimizer(vae, train_dataset, loss="vae")
628
+
629
+ # Contrastive (SimCLR) — dataset yields (view1, view2) pairs
630
+ opt = optix.Optimizer(encoder, pairs_dataset, loss="contrastive")
631
+
632
+ # DataLoader works too
633
+ opt = optix.Optimizer(model, DataLoader(dataset, batch_size=64))
634
+
635
+ # Custom loss
636
+ opt = optix.Optimizer(model, dataset, loss=nn.HuberLoss())
637
+ opt.run()
638
+ ```
639
+
640
+ Available `loss=` strings:
641
+
642
+ | String | Criterion | Use case |
643
+ |---|---|---|
644
+ | `"auto"` | detected from data | default |
645
+ | `"cross_entropy"` | `CrossEntropyLoss` | multi-class classification |
646
+ | `"bce"` | `BCEWithLogitsLoss` | binary classification |
647
+ | `"mse"` | `MSELoss` | regression |
648
+ | `"mae"` | `L1Loss` | regression, outlier-robust |
649
+ | `"huber"` | `HuberLoss` | regression, very outlier-robust |
650
+ | `"reconstruction"` | MSE(output, input) | autoencoders |
651
+ | `"vae"` | ELBO: recon + KL | variational autoencoders |
652
+ | `"contrastive"` | NT-Xent | SimCLR-style contrastive learning |
653
+
654
+ Any `nn.Module` or callable can be passed directly as `loss=`.
655
+
656
+ Save and load work identically to RL agents:
657
+
658
+ ```python
659
+ agent = optix.Optimizer(model, dataset, loss="cross_entropy")
660
+ # Checkpoints written automatically to checkpoint_dir
661
+ # Load back:
662
+ from tensor_optix.ml import MLAgent
663
+ ml_agent.load_weights("checkpoint.pt")
664
+ ```
665
+
666
+ ---
667
+
668
+ ## Core utilities
669
+
670
+ ### Normalizers
671
+
672
+ Online Welford mean/variance. `ObsNormalizer` normalises observations. `RewardNormalizer` divides rewards by return standard deviation (not reward std), preserving sign.
673
+
674
+ ```python
675
+ from tensor_optix.core.normalizers import ObsNormalizer, RewardNormalizer
676
+
677
+ obs_norm = ObsNormalizer(shape=(obs_dim,))
678
+ obs_norm.update(obs_batch)
679
+ normalized = obs_norm.normalize(obs)
680
+
681
+ rew_norm = RewardNormalizer()
682
+ ```
683
+
684
+ ### Hindsight Experience Replay
685
+
686
+ Wraps `PrioritizedReplayBuffer` with episode-level goal relabeling. Supports `future` (default), `final`, and `episode` relabeling strategies.
687
+
688
+ ```python
689
+ from tensor_optix.core.her_buffer import HERBuffer
690
+
691
+ her = HERBuffer(obs_dim=obs_dim, act_dim=act_dim, goal_dim=goal_dim, strategy="future", k=4)
692
+ her.store_episode(obs_list, act_list, rew_list, next_obs_list, done_list,
693
+ achieved_goals, compute_reward_fn)
694
+ obs_b, act_b, rew_b, next_b, done_b, weights, idx, n = her.sample(batch_size)
695
+ ```
696
+
697
+ ### Checkpoint registry
698
+
699
+ ```python
700
+ from tensor_optix.core.checkpoint_registry import CheckpointRegistry
701
+
702
+ registry = CheckpointRegistry(checkpoint_dir="./checkpoints", max_snapshots=10)
703
+ registry.save(agent, eval_metrics, hyperparams)
704
+ registry.load_best(agent)
705
+ registry.load_ensemble(agent, top_k=3) # stochastic weight averaging over top-k snapshots
706
+ ```
707
+
708
+ ### Regime detection
709
+
710
+ Classifies score history into one of three regimes using detrended coefficient of variation. Detrended CV measures noise around the trend, not raw score variance.
711
+
712
+ ```python
713
+ from tensor_optix.core import RegimeDetector
714
+
715
+ detector = RegimeDetector()
716
+ regime = detector.detect(metrics_history) # "trending" | "ranging" | "volatile"
717
+ ```
718
+
719
+ ---
720
+
721
+ ## Learning-to-Learn Engine
722
+
723
+ tensor-optix also includes an optional Learning-to-Learn Engine: a layer that makes experiment history actionable — recording every trial with enough structure to compare reliably, running experiments concurrently under explicit budgets, learning which training strategies help on which tasks, and (recommend-only, never automatic) proposing interventions for a human to review before anything is promoted to a deployed configuration.
724
+
725
+ ### Experiment ledger
726
+
727
+ Every experiment — a trial, a strategy candidate, a re-evaluation of a deployed agent — is recorded to a SQLite-backed ledger with full provenance (code revision, library versions, hostname) and a typed lifecycle.
728
+
729
+ ```python
730
+ from tensor_optix.ledger import ExperimentStore, ExperimentState
731
+
732
+ store = ExperimentStore("./experiments.sqlite3")
733
+ record = store.create_experiment(
734
+ task_env="CartPole-v1", algorithm="PPO", framework="torch",
735
+ seed=0, checkpoint_dir="./ckpt", solve_threshold=475.0,
736
+ )
737
+ store.start_experiment(record.experiment_id)
738
+ store.record_progress(record.experiment_id, best_primary_score=480.0)
739
+ store.finalize_experiment(record.experiment_id, state=ExperimentState.COMPLETED)
740
+ ```
741
+
742
+ Writes are idempotent on `experiment_id`, and every method opens a short, single-statement transaction — nothing here ever holds a lock for the duration of training.
743
+
744
+ ### Concurrent trial scheduler
745
+
746
+ `ConcurrentTrialScheduler` runs multiple Optuna trials as real OS processes, with the parent process as sole owner of the Optuna study and the ledger.
747
+
748
+ ```python
749
+ from tensor_optix.scheduler import ConcurrentTrialScheduler
750
+
751
+ scheduler = ConcurrentTrialScheduler(
752
+ agent_factory=make_agent, pipeline_factory=make_pipeline,
753
+ param_space={"learning_rate": ("log_float", 1e-4, 3e-3)},
754
+ n_trials=20, n_workers=4, ledger_store=store,
755
+ task_env="CartPole-v1", algorithm="PPO", solve_threshold=475.0,
756
+ )
757
+ best_params, best_score = scheduler.run()
758
+ ```
759
+
760
+ ### Cross-task transfer (Nanonets)
761
+
762
+ `NanonetsSelector` pools evidence from other tasks' ledger history when deciding how to allocate a new task's trial budget — but only when a conservative abstention gate confirms a source task's data is actually comparable (median performance at or above the target's own 95th percentile, both normalized against each task's own solve threshold so differently-scaled/signed scores are never compared on raw terms). When nothing passes the gate, it falls back to the non-learned baseline rather than guess.
763
+
764
+ ```python
765
+ from tensor_optix.nanonets import NanonetsSelector
766
+
767
+ selector = NanonetsSelector(store, source_task_envs=["Acrobot-v1"])
768
+ decision = selector.propose(task_env="CartPole-v1", methods=["TPESampler"], budget=10)
769
+ print(decision.rationale) # states explicitly whether it pooled or fell back
770
+ ```
771
+
772
+ ### Strategy discovery
773
+
774
+ A small, closed set of typed training-procedure operations (learning-rate trend adjustment, checkpoint rollback, exploration bounds, optimizer-method choice, budget allocation, schedule changes, policy-variant selection) — never arbitrary generated code. Each candidate is run paired against a no-strategy baseline, compared via a conservative confidence-interval gate, and checked against a 6-criterion promotion policy.
775
+
776
+ ```python
777
+ from tensor_optix.strategy import LearningRateTrendOp, StrategyCandidate, StrategyTrialRunner, PromotionPolicy
778
+
779
+ candidate = StrategyCandidate.from_op(LearningRateTrendOp(window=5, increase_factor=1.2))
780
+ runner = StrategyTrialRunner(
781
+ agent_factory=make_agent, pipeline_factory=make_pipeline,
782
+ trial_steps=50_000, ledger_store=store, task_env="CartPole-v1",
783
+ algorithm="PPO", solve_threshold=475.0,
784
+ )
785
+ results = runner.run_candidate(candidate, seeds=[0, 1, 2])
786
+
787
+ policy = PromotionPolicy(store, margin=5.0, min_seeds=3, approval_mode="human")
788
+ verdict = policy.evaluate(candidate_id=candidate.candidate_id, train_task_envs=["CartPole-v1"], held_out_task_envs=[])
789
+ ```
790
+
791
+ `approval_mode="human"` (the default) means `verdict.criteria["human_reviewed"]` can only ever become `True` via a separate, explicit `store.approve_strategy(...)` call — `PromotionPolicy.evaluate()` never sets it itself.
792
+
793
+ ### Live adaptive runtime
794
+
795
+ `LiveAdaptiveRuntime` runs the full observe → detect → propose → experiment → evaluate → recommend loop against a long-running deployed agent, standing in for live inference feedback. It is **recommend-only**: it issues durable, auditable recommendation records to the ledger, but the loop itself never calls `approve_strategy` and never changes which agent is deployed. Promoting a candidate requires two separate, explicit human actions outside the loop (`store.approve_strategy(...)`, then `promote_candidate_to_baseline(...)`).
796
+
797
+ ```python
798
+ from tensor_optix.runtime import LiveAdaptiveRuntime, RuntimeConfig
799
+
800
+ config = RuntimeConfig(task_env="CartPole-v1", algorithm="PPO", max_cycles=20)
801
+ runtime = LiveAdaptiveRuntime(store, config, agent_factory=make_agent, pipeline_factory=make_pipeline)
802
+ observations, recommendations = runtime.run()
803
+ ```
804
+
805
+ Full guide: [Learning-to-Learn Engine](https://sup3rus3r.github.io/tensor-optix/guides/learning-to-learn/).
806
+
807
+ ---
808
+
809
+ ## Requirements
810
+
811
+ - Python >= 3.11
812
+ - gymnasium >= 1.0
813
+ - numpy >= 1.24
814
+
815
+ The core loop, `PolicyManager`, and all ensemble and evolution logic have no framework dependency. Framework installs are opt-in via extras.