tensor-optix 2.0.2__tar.gz → 2.0.3__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 (166) hide show
  1. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/PKG-INFO +52 -5
  2. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/README.md +49 -4
  3. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/pyproject.toml +5 -1
  4. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/tf_dqn.py +10 -6
  5. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/tf_ppo.py +9 -6
  6. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/tf_ppo_continuous.py +6 -5
  7. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/tf_sac.py +43 -12
  8. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/tf_td3.py +40 -9
  9. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/torch_dqn.py +14 -6
  10. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/torch_ppo.py +12 -5
  11. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/torch_ppo_continuous.py +8 -5
  12. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/torch_rainbow_dqn.py +11 -7
  13. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/torch_sac.py +41 -8
  14. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/torch_td3.py +29 -8
  15. tensor_optix-2.0.3/tensor_optix/core/obs_utils.py +120 -0
  16. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/replay_buffer.py +31 -6
  17. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/trajectory_buffer.py +31 -5
  18. tensor_optix-2.0.3/tensor_optix/encoders/__init__.py +15 -0
  19. tensor_optix-2.0.3/tensor_optix/encoders/obs_encoder.py +229 -0
  20. tensor_optix-2.0.3/tensor_optix/encoders/obs_encoder_tf.py +80 -0
  21. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/factory.py +263 -75
  22. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ml/ml_agent.py +12 -0
  23. tensor_optix-2.0.3/tensor_optix/ml_core/__init__.py +53 -0
  24. tensor_optix-2.0.3/tensor_optix/ml_core/adapters/__init__.py +0 -0
  25. tensor_optix-2.0.3/tensor_optix/ml_core/adapters/flax_model.py +230 -0
  26. tensor_optix-2.0.3/tensor_optix/ml_core/adapters/tf_model.py +208 -0
  27. tensor_optix-2.0.3/tensor_optix/ml_core/adapters/torch_model.py +241 -0
  28. tensor_optix-2.0.3/tensor_optix/ml_core/base_model.py +150 -0
  29. tensor_optix-2.0.3/tensor_optix/ml_core/lr_schedules.py +67 -0
  30. tensor_optix-2.0.3/tensor_optix/ml_core/metrics.py +188 -0
  31. tensor_optix-2.0.3/tensor_optix/ml_core/metrics_impl/__init__.py +0 -0
  32. tensor_optix-2.0.3/tensor_optix/ml_core/metrics_impl/classification.py +101 -0
  33. tensor_optix-2.0.3/tensor_optix/ml_core/metrics_impl/regression.py +46 -0
  34. tensor_optix-2.0.3/tensor_optix/ml_core/trainer.py +207 -0
  35. tensor_optix-2.0.3/tensor_optix/ml_core/transfer.py +152 -0
  36. tensor_optix-2.0.3/tensor_optix/multi_agent/__init__.py +34 -0
  37. tensor_optix-2.0.3/tensor_optix/multi_agent/aec_pipeline.py +173 -0
  38. tensor_optix-2.0.3/tensor_optix/multi_agent/multi_agent_episode.py +18 -0
  39. tensor_optix-2.0.3/tensor_optix/multi_agent/multi_agent_runtime.py +53 -0
  40. tensor_optix-2.0.3/tensor_optix/multi_agent/parallel_pipeline.py +145 -0
  41. tensor_optix-2.0.3/tensor_optix/multi_agent/seat_env.py +81 -0
  42. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/simple.py +6 -2
  43. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix.egg-info/PKG-INFO +52 -5
  44. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix.egg-info/SOURCES.txt +23 -0
  45. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix.egg-info/requires.txt +3 -0
  46. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/setup.cfg +0 -0
  47. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/__init__.py +0 -0
  48. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/__init__.py +0 -0
  49. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/jax/__init__.py +0 -0
  50. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/jax/flax_agent.py +0 -0
  51. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/jax/flax_evaluator.py +0 -0
  52. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/pytorch/__init__.py +0 -0
  53. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/pytorch/torch_agent.py +0 -0
  54. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/pytorch/torch_evaluator.py +0 -0
  55. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/tensorflow/__init__.py +0 -0
  56. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/tensorflow/tf_agent.py +0 -0
  57. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/adapters/tensorflow/tf_evaluator.py +0 -0
  58. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/__init__.py +0 -0
  59. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/flax_ppo.py +0 -0
  60. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/algorithms/torch_recurrent_ppo.py +0 -0
  61. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/callbacks/__init__.py +0 -0
  62. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/callbacks/rich_dashboard.py +0 -0
  63. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/callbacks/tensorboard_callback.py +0 -0
  64. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/callbacks/wandb_callback.py +0 -0
  65. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/cli.py +0 -0
  66. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/config.py +0 -0
  67. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/__init__.py +0 -0
  68. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/async_callback.py +0 -0
  69. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/backoff_scheduler.py +0 -0
  70. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/base_agent.py +0 -0
  71. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/base_evaluator.py +0 -0
  72. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/base_optimizer.py +0 -0
  73. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/base_pipeline.py +0 -0
  74. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/checkpoint_confirmation.py +0 -0
  75. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/checkpoint_registry.py +0 -0
  76. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/device.py +0 -0
  77. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/diagnostic_controller.py +0 -0
  78. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/ensemble_agent.py +0 -0
  79. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/her_buffer.py +0 -0
  80. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/loop_controller.py +0 -0
  81. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/meta_controller.py +0 -0
  82. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/noisy_linear.py +0 -0
  83. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/normalizers.py +0 -0
  84. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/policy_manager.py +0 -0
  85. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/regime_detector.py +0 -0
  86. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/resume_state.py +0 -0
  87. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/reward_shape.py +0 -0
  88. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/core/types.py +0 -0
  89. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/distributed/__init__.py +0 -0
  90. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/distributed/async_learner.py +0 -0
  91. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/distributed/vtrace.py +0 -0
  92. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/exploration/__init__.py +0 -0
  93. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/exploration/rnd.py +0 -0
  94. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ledger/__init__.py +0 -0
  95. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ledger/callback.py +0 -0
  96. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ledger/provenance.py +0 -0
  97. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ledger/records.py +0 -0
  98. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ledger/schema.py +0 -0
  99. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ledger/store.py +0 -0
  100. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ml/__init__.py +0 -0
  101. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ml/dataset_pipeline.py +0 -0
  102. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/ml/loss_registry.py +0 -0
  103. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/nanonets/__init__.py +0 -0
  104. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/nanonets/abstention.py +0 -0
  105. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/nanonets/features.py +0 -0
  106. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/nanonets/pooling.py +0 -0
  107. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/nanonets/selector.py +0 -0
  108. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/nanonets/smalln.py +0 -0
  109. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/__init__.py +0 -0
  110. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/agent/__init__.py +0 -0
  111. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/agent/graph_agent.py +0 -0
  112. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/brain_network.py +0 -0
  113. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/controller/__init__.py +0 -0
  114. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/controller/topology_callback.py +0 -0
  115. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/controller/topology_controller.py +0 -0
  116. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/graph/__init__.py +0 -0
  117. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/graph/neuron.py +0 -0
  118. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/graph/neuron_graph.py +0 -0
  119. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/graph/topology_ops.py +0 -0
  120. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/hebbian.py +0 -0
  121. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/neuromodulator.py +0 -0
  122. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/optimizer/__init__.py +0 -0
  123. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/neuroevo/optimizer/topology_aware_adam.py +0 -0
  124. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/optimizer.py +0 -0
  125. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/optimizers/__init__.py +0 -0
  126. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/optimizers/adaptive_optimizer.py +0 -0
  127. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/optimizers/backoff_optimizer.py +0 -0
  128. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/optimizers/momentum_optimizer.py +0 -0
  129. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/optimizers/pbt_optimizer.py +0 -0
  130. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/optimizers/spsa_optimizer.py +0 -0
  131. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/orchestrator.py +0 -0
  132. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/pipeline/__init__.py +0 -0
  133. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/pipeline/batch_pipeline.py +0 -0
  134. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/pipeline/live_pipeline.py +0 -0
  135. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/pipeline/vector_pipeline.py +0 -0
  136. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/runtime/__init__.py +0 -0
  137. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/runtime/baseline.py +0 -0
  138. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/runtime/experiment_stage.py +0 -0
  139. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/runtime/live_runtime.py +0 -0
  140. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/runtime/observer.py +0 -0
  141. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/runtime/promote.py +0 -0
  142. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/scheduler/__init__.py +0 -0
  143. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/scheduler/_mp_common.py +0 -0
  144. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/scheduler/budget.py +0 -0
  145. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/scheduler/dedup.py +0 -0
  146. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/scheduler/isolation.py +0 -0
  147. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/scheduler/pool.py +0 -0
  148. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/scheduler/retry.py +0 -0
  149. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/scheduler/worker.py +0 -0
  150. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/selection/__init__.py +0 -0
  151. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/selection/backfill.py +0 -0
  152. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/selection/selector.py +0 -0
  153. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/selection/uncertainty.py +0 -0
  154. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/strategy/__init__.py +0 -0
  155. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/strategy/candidate.py +0 -0
  156. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/strategy/comparator.py +0 -0
  157. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/strategy/grammar.py +0 -0
  158. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/strategy/promotion.py +0 -0
  159. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/strategy/runner.py +0 -0
  160. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/strategy/worker.py +0 -0
  161. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/utils/__init__.py +0 -0
  162. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix/utils/window_size.py +0 -0
  163. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix.egg-info/dependency_links.txt +0 -0
  164. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix.egg-info/entry_points.txt +0 -0
  165. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tensor_optix.egg-info/top_level.txt +0 -0
  166. {tensor_optix-2.0.2 → tensor_optix-2.0.3}/tests/test_ml.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tensor-optix
3
- Version: 2.0.2
3
+ Version: 2.0.3
4
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
5
  Author: sup3rus3r
6
6
  License-Expression: MIT
@@ -35,6 +35,8 @@ Requires-Dist: gymnasium[atari]>=1.0.0; extra == "atari"
35
35
  Requires-Dist: ale-py>=0.8; extra == "atari"
36
36
  Provides-Extra: mujoco
37
37
  Requires-Dist: gymnasium[mujoco]>=1.0.0; extra == "mujoco"
38
+ Provides-Extra: pettingzoo
39
+ Requires-Dist: pettingzoo>=1.24.0; extra == "pettingzoo"
38
40
  Provides-Extra: all
39
41
  Requires-Dist: tensorflow>=2.18.0; extra == "all"
40
42
  Requires-Dist: torch>=2.11.0; extra == "all"
@@ -69,13 +71,22 @@ Requires-Dist: torch>=2.11.0; extra == "dev"
69
71
 
70
72
  # tensor-optix
71
73
 
72
- tensor-optix is a training loop framework with statistical convergence control, online hyperparameter optimisation, and an optional neuroevolution subsystem for dynamic topology.
74
+ tensor-optix is a training framework for reinforcement learning and for supervised/unsupervised machine learning. Both have statistical convergence control, online hyperparameter optimisation, and real checkpointing.
73
75
 
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.
76
+ It has two parallel cores:
75
77
 
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.
78
+ - **RL core**. An episode-driven loop (`LoopController`/`RLOptimizer`) built around `BaseAgent`'s `act()`/`learn()` contract. Statistical convergence control, SPSA-based online hyperparameter tuning, and an optional neuroevolution subsystem for dynamic topology.
79
+ - **ML core** (`tensor_optix.ml_core`). An epoch/batch-driven loop (`Trainer`, wrapped by `BaseMLModel.fit()`) built around a sklearn-style `fit()`/`predict()`/`evaluate()` contract. Real classification metrics (accuracy, precision/recall/F1, confusion matrix) and regression metrics (MAE, RMSE, R²), validation-based early stopping, and checkpointing on the best metric.
77
80
 
78
- The entire system, including neuroevo, is accessed through a six-method `BaseAgent` interface:
81
+ Both cores reuse the same lower-level, algorithm-agnostic machinery: `CheckpointRegistry`, `BackoffScheduler`'s convergence/plateau detection, `SPSAOptimizer`. Each core has its own contract on top because an RL episode and a training epoch aren't the same shape. See [Concepts](https://sup3rus3r.github.io/tensor-optix/getting-started/concepts/).
82
+
83
+ ## RL core
84
+
85
+ The RL 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 via a linear trend fit over the recent score window compared against an adaptive, noise-scaled floor, not a fixed patience counter (see [Concepts](https://sup3rus3r.github.io/tensor-optix/getting-started/concepts/) for the exact math; `BackoffScheduler` is the implementation). 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 (`AdaptiveOptimizer`). 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.
86
+
87
+ 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.
88
+
89
+ The RL core (including neuroevo) is accessed through a six-method `BaseAgent` interface:
79
90
 
80
91
  ```python
81
92
  class BaseAgent(ABC):
@@ -89,6 +100,40 @@ class BaseAgent(ABC):
89
100
 
90
101
  Fifteen agents are included across PyTorch, TensorFlow, and JAX/Flax. Bring your own by implementing the interface above.
91
102
 
103
+ ## ML core
104
+
105
+ The ML loop trains your model for a number of epochs, evaluates on a held-out validation set each epoch, and stops early once the validation metric plateaus. It restores the best checkpoint automatically. It is accessed through a `BaseMLModel` interface, with `fit()` as the one-call entry point built on top:
106
+
107
+ ```python
108
+ class BaseMLModel(ABC):
109
+ def predict(self, X) -> Any: ...
110
+ def train_step(self, batch) -> float: ...
111
+ def evaluate(self, data) -> MLMetrics: ...
112
+ def iter_batches(self, data, batch_size: int): ...
113
+ def save_weights(self, path: str) -> None: ...
114
+ def load_weights(self, path: str) -> None: ...
115
+ # fit(train_data, val_data, epochs, early_stopping_patience, ...) is provided
116
+ ```
117
+
118
+ ```python
119
+ import torch.nn as nn
120
+ from tensor_optix.ml_core import TorchMLModel
121
+
122
+ model = TorchMLModel(
123
+ nn.Sequential(nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10)),
124
+ loss_fn=nn.CrossEntropyLoss(),
125
+ )
126
+ history = model.fit(
127
+ (X_train, y_train), (X_val, y_val),
128
+ epochs=50, early_stopping_patience=5, checkpoint_dir="./checkpoints",
129
+ )
130
+ print(history.val_metrics[-1].metrics) # {'accuracy': 0.94, 'f1_macro': 0.93, ...}
131
+ ```
132
+
133
+ Metrics are auto-detected from the target dtype. Integer/class-index targets get classification metrics (accuracy, per-class and macro precision/recall/F1, confusion matrix). Float targets get regression metrics (MAE, RMSE, R²). Pass `metrics="classification"`, `metrics="regression"`, or a list of custom `(y_true, y_pred) -> float` callables to override. `TorchMLModel`, `TFMLModel`, and `FlaxMLModel` are provided. The Torch and TF adapters accept a raw `(X, y)` array pair or a native `Dataset`/`DataLoader`/`tf.data.Dataset`; the Flax adapter accepts a raw `(X, y)` array pair only. `fit()` also takes `lr_schedule=` (see `tensor_optix.ml_core.lr_schedules`), `augment=` for per-batch data augmentation, and there are `freeze_layers()`/`unfreeze_layers()`/`load_pretrained_backbone()` helpers for transfer learning. See [Train an ML model](https://sup3rus3r.github.io/tensor-optix/guides/ml-core/) for the full guide.
134
+
135
+ There is also an older, RL-loop-based way to train an arbitrary `nn.Module`: `Optimizer(model, dataset)`, which runs the model through the RL loop as a fake episode. See [ML support](#ml-support) below. It predates `ml_core`, is kept for backward compatibility, and has no real classification/regression metrics and no validation-based early stopping. Use `ml_core` for new code.
136
+
92
137
  ---
93
138
 
94
139
  ## Install
@@ -605,6 +650,8 @@ graph.invalidate_compile()
605
650
 
606
651
  ## ML support
607
652
 
653
+ > **Use [`tensor_optix.ml_core`](#ml-core) for new code.** The `Optimizer(model, dataset)` path below is the original way to train an arbitrary `nn.Module`. It runs the model through the RL loop's `BaseAgent`/`RLOptimizer` machinery by treating each mini-batch as a fake RL episode (`reward = -loss`). It is deprecated but kept working for backward compatibility — `MLAgent` emits a `DeprecationWarning` pointing to `ml_core` on construction, and there is no removal planned. It has no real classification/regression metrics (checkpointing and rollback compare mean "episode return," which is just negative loss) and no validation-split-based early stopping. `ml_core`'s `BaseMLModel`/`Trainer` reuse the same convergence/checkpointing machinery through a real `fit()`/`evaluate()` contract with real metrics.
654
+
608
655
  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
656
 
610
657
  ```python
@@ -1,12 +1,21 @@
1
1
  # tensor-optix
2
2
 
3
- tensor-optix is a training loop framework with statistical convergence control, online hyperparameter optimisation, and an optional neuroevolution subsystem for dynamic topology.
3
+ tensor-optix is a training framework for reinforcement learning and for supervised/unsupervised machine learning. Both have statistical convergence control, online hyperparameter optimisation, and real checkpointing.
4
4
 
5
- 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.
5
+ It has two parallel cores:
6
6
 
7
- 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.
7
+ - **RL core**. An episode-driven loop (`LoopController`/`RLOptimizer`) built around `BaseAgent`'s `act()`/`learn()` contract. Statistical convergence control, SPSA-based online hyperparameter tuning, and an optional neuroevolution subsystem for dynamic topology.
8
+ - **ML core** (`tensor_optix.ml_core`). An epoch/batch-driven loop (`Trainer`, wrapped by `BaseMLModel.fit()`) built around a sklearn-style `fit()`/`predict()`/`evaluate()` contract. Real classification metrics (accuracy, precision/recall/F1, confusion matrix) and regression metrics (MAE, RMSE, R²), validation-based early stopping, and checkpointing on the best metric.
8
9
 
9
- The entire system, including neuroevo, is accessed through a six-method `BaseAgent` interface:
10
+ Both cores reuse the same lower-level, algorithm-agnostic machinery: `CheckpointRegistry`, `BackoffScheduler`'s convergence/plateau detection, `SPSAOptimizer`. Each core has its own contract on top because an RL episode and a training epoch aren't the same shape. See [Concepts](https://sup3rus3r.github.io/tensor-optix/getting-started/concepts/).
11
+
12
+ ## RL core
13
+
14
+ The RL 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 via a linear trend fit over the recent score window compared against an adaptive, noise-scaled floor, not a fixed patience counter (see [Concepts](https://sup3rus3r.github.io/tensor-optix/getting-started/concepts/) for the exact math; `BackoffScheduler` is the implementation). 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 (`AdaptiveOptimizer`). 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.
15
+
16
+ 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.
17
+
18
+ The RL core (including neuroevo) is accessed through a six-method `BaseAgent` interface:
10
19
 
11
20
  ```python
12
21
  class BaseAgent(ABC):
@@ -20,6 +29,40 @@ class BaseAgent(ABC):
20
29
 
21
30
  Fifteen agents are included across PyTorch, TensorFlow, and JAX/Flax. Bring your own by implementing the interface above.
22
31
 
32
+ ## ML core
33
+
34
+ The ML loop trains your model for a number of epochs, evaluates on a held-out validation set each epoch, and stops early once the validation metric plateaus. It restores the best checkpoint automatically. It is accessed through a `BaseMLModel` interface, with `fit()` as the one-call entry point built on top:
35
+
36
+ ```python
37
+ class BaseMLModel(ABC):
38
+ def predict(self, X) -> Any: ...
39
+ def train_step(self, batch) -> float: ...
40
+ def evaluate(self, data) -> MLMetrics: ...
41
+ def iter_batches(self, data, batch_size: int): ...
42
+ def save_weights(self, path: str) -> None: ...
43
+ def load_weights(self, path: str) -> None: ...
44
+ # fit(train_data, val_data, epochs, early_stopping_patience, ...) is provided
45
+ ```
46
+
47
+ ```python
48
+ import torch.nn as nn
49
+ from tensor_optix.ml_core import TorchMLModel
50
+
51
+ model = TorchMLModel(
52
+ nn.Sequential(nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10)),
53
+ loss_fn=nn.CrossEntropyLoss(),
54
+ )
55
+ history = model.fit(
56
+ (X_train, y_train), (X_val, y_val),
57
+ epochs=50, early_stopping_patience=5, checkpoint_dir="./checkpoints",
58
+ )
59
+ print(history.val_metrics[-1].metrics) # {'accuracy': 0.94, 'f1_macro': 0.93, ...}
60
+ ```
61
+
62
+ Metrics are auto-detected from the target dtype. Integer/class-index targets get classification metrics (accuracy, per-class and macro precision/recall/F1, confusion matrix). Float targets get regression metrics (MAE, RMSE, R²). Pass `metrics="classification"`, `metrics="regression"`, or a list of custom `(y_true, y_pred) -> float` callables to override. `TorchMLModel`, `TFMLModel`, and `FlaxMLModel` are provided. The Torch and TF adapters accept a raw `(X, y)` array pair or a native `Dataset`/`DataLoader`/`tf.data.Dataset`; the Flax adapter accepts a raw `(X, y)` array pair only. `fit()` also takes `lr_schedule=` (see `tensor_optix.ml_core.lr_schedules`), `augment=` for per-batch data augmentation, and there are `freeze_layers()`/`unfreeze_layers()`/`load_pretrained_backbone()` helpers for transfer learning. See [Train an ML model](https://sup3rus3r.github.io/tensor-optix/guides/ml-core/) for the full guide.
63
+
64
+ There is also an older, RL-loop-based way to train an arbitrary `nn.Module`: `Optimizer(model, dataset)`, which runs the model through the RL loop as a fake episode. See [ML support](#ml-support) below. It predates `ml_core`, is kept for backward compatibility, and has no real classification/regression metrics and no validation-based early stopping. Use `ml_core` for new code.
65
+
23
66
  ---
24
67
 
25
68
  ## Install
@@ -536,6 +579,8 @@ graph.invalidate_compile()
536
579
 
537
580
  ## ML support
538
581
 
582
+ > **Use [`tensor_optix.ml_core`](#ml-core) for new code.** The `Optimizer(model, dataset)` path below is the original way to train an arbitrary `nn.Module`. It runs the model through the RL loop's `BaseAgent`/`RLOptimizer` machinery by treating each mini-batch as a fake RL episode (`reward = -loss`). It is deprecated but kept working for backward compatibility — `MLAgent` emits a `DeprecationWarning` pointing to `ml_core` on construction, and there is no removal planned. It has no real classification/regression metrics (checkpointing and rollback compare mean "episode return," which is just negative loss) and no validation-split-based early stopping. `ml_core`'s `BaseMLModel`/`Trainer` reuse the same convergence/checkpointing machinery through a real `fit()`/`evaluate()` contract with real metrics.
583
+
539
584
  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.
540
585
 
541
586
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tensor-optix"
7
- version = "2.0.2"
7
+ version = "2.0.3"
8
8
  description = "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)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -60,6 +60,10 @@ atari = [
60
60
  mujoco = [
61
61
  "gymnasium[mujoco]>=1.0.0",
62
62
  ]
63
+ # Multi-agent environments (tensor_optix.multi_agent)
64
+ pettingzoo = [
65
+ "pettingzoo>=1.24.0",
66
+ ]
63
67
  # Convenience bundles
64
68
  all = [
65
69
  "tensorflow>=2.18.0",
@@ -7,6 +7,7 @@ import tensorflow as tf
7
7
  from tensor_optix.core.base_agent import BaseAgent
8
8
  from tensor_optix.core.types import EpisodeData, HyperparamSet
9
9
  from tensor_optix.core.replay_buffer import PrioritizedReplayBuffer
10
+ from tensor_optix.core.obs_utils import to_batch_numpy, to_tf_batch, already_batched_to_tf, batch_index
10
11
 
11
12
 
12
13
  class TFDQNAgent(BaseAgent):
@@ -97,7 +98,7 @@ class TFDQNAgent(BaseAgent):
97
98
  epsilon = float(self._hyperparams.params.get("epsilon", 0.1))
98
99
  if np.random.random() < epsilon:
99
100
  return np.random.randint(self._n_actions)
100
- obs = tf.cast(np.atleast_2d(observation), tf.float32)
101
+ obs = to_tf_batch(observation)
101
102
  q_values = self._q(obs, training=False)
102
103
  return int(tf.argmax(q_values, axis=-1).numpy()[0])
103
104
 
@@ -109,7 +110,10 @@ class TFDQNAgent(BaseAgent):
109
110
  epsilon_decay = float(hp.get("epsilon_decay", 0.995))
110
111
  epsilon_min = float(hp.get("epsilon_min", 0.05))
111
112
 
112
- obs_arr = np.array(episode_data.observations, dtype=np.float32)
113
+ # DQN pushes the RAW observation into the buffer (not pre-encoded)
114
+ # -- same single-network constraint as TorchDQNAgent, see
115
+ # torch_dqn.py's learn() comment for the full rationale.
116
+ obs_arr = to_batch_numpy(list(episode_data.observations))
113
117
  act_arr = episode_data.actions
114
118
  rew_arr = episode_data.rewards
115
119
  done_arr = episode_data.dones
@@ -117,8 +121,8 @@ class TFDQNAgent(BaseAgent):
117
121
  T = len(rew_arr)
118
122
  for t in range(T - 1):
119
123
  self._buffer.push(
120
- obs_arr[t], int(act_arr[t]), float(rew_arr[t]),
121
- obs_arr[t + 1], bool(done_arr[t]),
124
+ batch_index(obs_arr, t), int(act_arr[t]), float(rew_arr[t]),
125
+ batch_index(obs_arr, t + 1), bool(done_arr[t]),
122
126
  )
123
127
  self._buffer.flush_episode()
124
128
 
@@ -138,10 +142,10 @@ class TFDQNAgent(BaseAgent):
138
142
 
139
143
  for _ in range(n_updates):
140
144
  obs_b, act_b, rew_b, next_obs_b, done_b, weights, indices, n_steps = self._buffer.sample(batch_size)
141
- obs_b = tf.cast(obs_b, tf.float32)
145
+ obs_b = already_batched_to_tf(obs_b)
142
146
  act_b = tf.cast(act_b, tf.int32)
143
147
  rew_b = tf.cast(rew_b, tf.float32)
144
- next_obs_b = tf.cast(next_obs_b, tf.float32)
148
+ next_obs_b = already_batched_to_tf(next_obs_b)
145
149
  done_b = tf.cast(done_b, tf.float32)
146
150
  weights = tf.cast(weights, tf.float32)
147
151
  gammas_n = tf.cast(gamma ** n_steps.astype(np.float32), tf.float32)
@@ -4,6 +4,7 @@ import tensorflow as tf
4
4
  from tensor_optix.core.base_agent import BaseAgent
5
5
  from tensor_optix.core.types import EpisodeData, HyperparamSet
6
6
  from tensor_optix.core.trajectory_buffer import compute_gae, make_minibatches, resolve_gae_bootstrap
7
+ from tensor_optix.core.obs_utils import to_batch_numpy, to_tf_batch, already_batched_to_tf
7
8
 
8
9
 
9
10
  class TFPPOAgent(BaseAgent):
@@ -89,7 +90,7 @@ class TFPPOAgent(BaseAgent):
89
90
  Sample action from π(·|obs), cache log_prob and V(obs) for learn().
90
91
  Returns a Python int (action index).
91
92
  """
92
- obs = tf.cast(np.atleast_2d(observation), tf.float32)
93
+ obs = to_tf_batch(observation)
93
94
 
94
95
  logits = self._actor(obs, training=False) # [1, n_actions]
95
96
  log_probs_all = tf.nn.log_softmax(logits) # [1, n_actions]
@@ -98,7 +99,9 @@ class TFPPOAgent(BaseAgent):
98
99
 
99
100
  value = float(tf.squeeze(self._critic(obs, training=False)).numpy())
100
101
 
101
- self._cache_obs.append(np.squeeze(observation))
102
+ # Cache the raw observation -- learn() re-batches via
103
+ # to_batch_numpy(), which handles both array and dict observations.
104
+ self._cache_obs.append(np.squeeze(observation) if not isinstance(observation, dict) else observation)
102
105
  self._cache_log_probs.append(log_prob)
103
106
  self._cache_values.append(value)
104
107
 
@@ -134,7 +137,7 @@ class TFPPOAgent(BaseAgent):
134
137
  f"Cache underflow: expected >= {T} entries but got {len(self._cache_obs)}. "
135
138
  "Ensure act() is called exactly once per environment step."
136
139
  )
137
- obs_arr = np.array(self._cache_obs[:T], dtype=np.float32)
140
+ obs_arr = to_batch_numpy(self._cache_obs[:T])
138
141
  old_lp_arr = np.array(self._cache_log_probs[:T], dtype=np.float32)
139
142
  values_arr = np.array(self._cache_values[:T], dtype=np.float32)
140
143
  rewards = list(episode_data.rewards)
@@ -155,7 +158,7 @@ class TFPPOAgent(BaseAgent):
155
158
  # bootstrap value per env, and n_envs tells compute_gae how the
156
159
  # rewards/values array is interleaved across envs.
157
160
  def _critic_value(obs):
158
- obs_t = tf.cast(np.atleast_2d(obs), tf.float32)
161
+ obs_t = to_tf_batch(obs)
159
162
  return float(tf.squeeze(self._critic(obs_t, training=False)).numpy())
160
163
 
161
164
  last_value, n_envs = resolve_gae_bootstrap(episode_data, _critic_value)
@@ -183,7 +186,7 @@ class TFPPOAgent(BaseAgent):
183
186
 
184
187
  for _ in range(n_epochs):
185
188
  for batch in make_minibatches(rollout, mb_size):
186
- obs_b = tf.cast(batch["obs"], tf.float32)
189
+ obs_b = already_batched_to_tf(batch["obs"])
187
190
  act_b = tf.cast(batch["actions"], tf.int32)
188
191
  old_b = tf.cast(batch["old_lp"], tf.float32)
189
192
  adv_b = tf.cast(batch["advantages"], tf.float32)
@@ -248,7 +251,7 @@ class TFPPOAgent(BaseAgent):
248
251
  Used by PolicyManager.ensemble_action() to average distributions
249
252
  across ensemble members rather than averaging sampled actions.
250
253
  """
251
- obs = tf.cast(np.atleast_2d(observation), tf.float32)
254
+ obs = to_tf_batch(observation)
252
255
  logits = self._actor(obs, training=False)
253
256
  return tf.nn.softmax(logits)[0].numpy()
254
257
 
@@ -5,6 +5,7 @@ import tensorflow as tf
5
5
  from tensor_optix.core.base_agent import BaseAgent
6
6
  from tensor_optix.core.types import EpisodeData, HyperparamSet
7
7
  from tensor_optix.core.trajectory_buffer import compute_gae, make_minibatches, resolve_gae_bootstrap
8
+ from tensor_optix.core.obs_utils import to_batch_numpy, to_tf_batch, already_batched_to_tf
8
9
 
9
10
  LOG_STD_MIN = -5
10
11
  LOG_STD_MAX = 2
@@ -133,7 +134,7 @@ class TFGaussianPPOAgent(BaseAgent):
133
134
  Sample action ∈ (-1, 1)^action_dim; cache log_prob and V(obs) for learn().
134
135
  Returns a float numpy array of shape [action_dim].
135
136
  """
136
- obs = tf.cast(np.atleast_2d(observation), tf.float32)
137
+ obs = to_tf_batch(observation)
137
138
 
138
139
  out = self._actor(obs, training=False) # [1, 2*action_dim]
139
140
  mean = out[:, :self._action_dim]
@@ -147,7 +148,7 @@ class TFGaussianPPOAgent(BaseAgent):
147
148
  log_prob = self._gaussian_log_prob(pre_tanh, mean, log_std) # [1]
148
149
  value = float(tf.squeeze(self._critic(obs, training=False)).numpy())
149
150
 
150
- self._cache_obs.append(np.squeeze(observation))
151
+ self._cache_obs.append(np.squeeze(observation) if not isinstance(observation, dict) else observation)
151
152
  self._cache_log_probs.append(float(tf.squeeze(log_prob).numpy()))
152
153
  self._cache_values.append(value)
153
154
 
@@ -179,7 +180,7 @@ class TFGaussianPPOAgent(BaseAgent):
179
180
  "Ensure act() is called exactly once per environment step."
180
181
  )
181
182
 
182
- obs_arr = np.array(self._cache_obs[:T], dtype=np.float32)
183
+ obs_arr = to_batch_numpy(self._cache_obs[:T])
183
184
  old_lp_arr = np.array(self._cache_log_probs[:T], dtype=np.float32)
184
185
  val_arr = np.array(self._cache_values[:T], dtype=np.float32)
185
186
  act_arr = np.array(episode_data.actions, dtype=np.float32)
@@ -194,7 +195,7 @@ class TFGaussianPPOAgent(BaseAgent):
194
195
  rewards = list(self._reward_normalizer.normalize(np.array(rewards, dtype=np.float32)))
195
196
 
196
197
  def _critic_value(obs):
197
- obs_t = tf.cast(np.atleast_2d(obs), tf.float32)
198
+ obs_t = to_tf_batch(obs)
198
199
  return float(tf.squeeze(self._critic(obs_t, training=False)).numpy())
199
200
 
200
201
  last_value, n_envs = resolve_gae_bootstrap(episode_data, _critic_value)
@@ -220,7 +221,7 @@ class TFGaussianPPOAgent(BaseAgent):
220
221
 
221
222
  for _ in range(n_epochs):
222
223
  for batch in make_minibatches(rollout, mb_size):
223
- obs_b = tf.cast(batch["obs"], tf.float32)
224
+ obs_b = already_batched_to_tf(batch["obs"])
224
225
  act_b = tf.cast(batch["actions"], tf.float32)
225
226
  old_b = tf.cast(batch["old_lp"], tf.float32)
226
227
  adv_b = tf.cast(batch["advantages"], tf.float32)
@@ -7,6 +7,7 @@ import tensorflow as tf
7
7
  from tensor_optix.core.base_agent import BaseAgent
8
8
  from tensor_optix.core.types import EpisodeData, HyperparamSet
9
9
  from tensor_optix.core.replay_buffer import PrioritizedReplayBuffer
10
+ from tensor_optix.core.obs_utils import to_batch_numpy, to_tf_batch, already_batched_to_tf
10
11
 
11
12
 
12
13
  class TFSACAgent(BaseAgent):
@@ -132,14 +133,28 @@ class TFSACAgent(BaseAgent):
132
133
  def is_on_policy(self) -> bool:
133
134
  return False # replay buffer — rollback without buffer clear is harmful
134
135
 
136
+ def _encode(self, observation_batched):
137
+ """Run the actor's encoder alone, if it has one (for non-flat
138
+ observations); identity pass-through for the flat-Box case,
139
+ exactly matching today's behavior. Used to pre-encode observations
140
+ before they enter the replay buffer -- see tensor_optix.encoders'
141
+ module docstring for why SAC's critic only ever sees already-
142
+ encoded flat vectors, never raw observations. self._actor.encoder
143
+ is the standalone encoder Model, attached by the factory alongside
144
+ head_model (see _make_sac_tf), so this never has to split the
145
+ composed actor Model's graph apart at call time."""
146
+ encoder = getattr(self._actor, "encoder", None)
147
+ if encoder is None:
148
+ return observation_batched
149
+ return encoder(observation_batched, training=False)
150
+
135
151
  def act(self, observation) -> np.ndarray:
136
152
  """
137
153
  Sample action from the squashed Gaussian policy.
138
154
  Returns np.ndarray of shape [action_dim], values in (-1, 1).
139
155
  """
140
- obs = tf.cast(np.atleast_2d(observation), tf.float32)
156
+ obs = to_tf_batch(observation)
141
157
  action, _ = self._sample_action(obs, training=False)
142
- self._last_obs = np.array(observation, dtype=np.float32)
143
158
  return action.numpy()[0]
144
159
 
145
160
  def learn(self, episode_data: EpisodeData) -> dict:
@@ -153,7 +168,8 @@ class TFSACAgent(BaseAgent):
153
168
  batch_size = int(hp.get("batch_size", 256))
154
169
  updates_per_step = float(hp.get("updates_per_step", 1))
155
170
 
156
- obs_arr = np.array(episode_data.observations, dtype=np.float32)
171
+ obs_batched = to_tf_batch(list(episode_data.observations))
172
+ obs_arr = self._encode(obs_batched).numpy()
157
173
  act_arr = np.array(episode_data.actions, dtype=np.float32)
158
174
  rew_arr = episode_data.rewards
159
175
  done_arr = episode_data.dones
@@ -267,15 +283,25 @@ class TFSACAgent(BaseAgent):
267
283
  # Internal helpers
268
284
  # ------------------------------------------------------------------
269
285
 
270
- def _sample_action(self, obs_tf, training: bool):
286
+ def _sample_action(self, obs_tf, training: bool, pre_encoded: bool = False):
271
287
  """
272
288
  Reparameterized squashed Gaussian sample.
273
289
 
290
+ pre_encoded=True: obs_tf is already a flat, encoder-output vector
291
+ (buffer-sampled data) -- route through the actor's head_model only,
292
+ skipping its encoder a second time (see tensor_optix.encoders'
293
+ module docstring). pre_encoded=False (default): obs_tf is a raw
294
+ observation (from act()) -- route through the full composed actor
295
+ (encoder + head). For the flat-Box case (actor has no head_model
296
+ attribute, it IS the head) this falls back to calling it directly,
297
+ identical to today's behavior.
298
+
274
299
  Returns:
275
300
  action: tanh(mean + std * ε), shape [batch, action_dim]
276
301
  log_prob: log π(action|obs) with tanh correction, shape [batch]
277
302
  """
278
- out = self._actor(obs_tf, training=training) # [batch, 2*action_dim]
303
+ net = getattr(self._actor, "head_model", self._actor) if pre_encoded else self._actor
304
+ out = net(obs_tf, training=training) # [batch, 2*action_dim]
279
305
  mean, log_std = tf.split(out, 2, axis=-1)
280
306
  log_std = tf.clip_by_value(log_std, self.LOG_STD_MIN, self.LOG_STD_MAX)
281
307
  std = tf.exp(log_std)
@@ -306,18 +332,23 @@ class TFSACAgent(BaseAgent):
306
332
  def _update_step(self, batch_size: int, gamma: float, tau: float):
307
333
  """One full SAC gradient step (critic + actor + alpha + soft update)."""
308
334
  obs_b, act_b, rew_b, next_obs_b, done_b, weights, indices, n_steps = self._buffer.sample(batch_size)
309
- obs_b = tf.cast(obs_b, tf.float32)
335
+ obs_b = already_batched_to_tf(obs_b)
310
336
  act_b = tf.cast(act_b, tf.float32)
311
337
  rew_b = tf.cast(rew_b, tf.float32)
312
- next_obs_b = tf.cast(next_obs_b, tf.float32)
338
+ next_obs_b = already_batched_to_tf(next_obs_b)
313
339
  done_b = tf.cast(done_b, tf.float32)
314
340
  weights = tf.cast(weights, tf.float32)
315
341
  gammas_n = tf.cast(gamma ** n_steps.astype(np.float32), tf.float32)
316
342
  alpha = tf.exp(self._log_alpha)
317
343
 
344
+ # Buffer-sampled obs_b/next_obs_b are already flat, pre-encoded
345
+ # vectors -- actor updates below train only the head's variables,
346
+ # never the (frozen, collection-time-only) encoder's.
347
+ actor_trainable_vars = getattr(self._actor, "head_model", self._actor).trainable_variables
348
+
318
349
  # ---- Critic update ----
319
350
  with tf.GradientTape() as tape:
320
- next_a, next_lp = self._sample_action(next_obs_b, training=False)
351
+ next_a, next_lp = self._sample_action(next_obs_b, training=False, pre_encoded=True)
321
352
  ci_next = self._critic_input(next_obs_b, next_a)
322
353
  q1_tgt = tf.squeeze(self._c1_tgt(ci_next, training=False), -1)
323
354
  q2_tgt = tf.squeeze(self._c2_tgt(ci_next, training=False), -1)
@@ -339,19 +370,19 @@ class TFSACAgent(BaseAgent):
339
370
 
340
371
  # ---- Actor update ----
341
372
  with tf.GradientTape() as tape:
342
- new_a, new_lp = self._sample_action(obs_b, training=True)
373
+ new_a, new_lp = self._sample_action(obs_b, training=True, pre_encoded=True)
343
374
  ci_new = self._critic_input(obs_b, new_a)
344
375
  q1_new = tf.squeeze(self._c1(ci_new, training=False), -1)
345
376
  q2_new = tf.squeeze(self._c2(ci_new, training=False), -1)
346
377
  a_loss = tf.reduce_mean(alpha * new_lp - tf.minimum(q1_new, q2_new))
347
378
 
348
- a_grads = tape.gradient(a_loss, self._actor.trainable_variables)
379
+ a_grads = tape.gradient(a_loss, actor_trainable_vars)
349
380
  a_grads, _ = tf.clip_by_global_norm(a_grads, 10.0)
350
- self._actor_opt.apply_gradients(zip(a_grads, self._actor.trainable_variables))
381
+ self._actor_opt.apply_gradients(zip(a_grads, actor_trainable_vars))
351
382
 
352
383
  # ---- Alpha (entropy temperature) update ----
353
384
  with tf.GradientTape() as tape:
354
- _, new_lp2 = self._sample_action(obs_b, training=False)
385
+ _, new_lp2 = self._sample_action(obs_b, training=False, pre_encoded=True)
355
386
  alpha_loss = -tf.reduce_mean(
356
387
  self._log_alpha * tf.stop_gradient(new_lp2 + self._target_entropy)
357
388
  )
@@ -62,6 +62,7 @@ import tensorflow as tf
62
62
  from tensor_optix.core.base_agent import BaseAgent
63
63
  from tensor_optix.core.types import EpisodeData, HyperparamSet
64
64
  from tensor_optix.core.replay_buffer import PrioritizedReplayBuffer
65
+ from tensor_optix.core.obs_utils import to_tf_batch, already_batched_to_tf
65
66
 
66
67
 
67
68
  class TFTDDAgent(BaseAgent):
@@ -97,6 +98,20 @@ class TFTDDAgent(BaseAgent):
97
98
  self._c1_tgt.set_weights(critic1.get_weights())
98
99
  self._c2_tgt.set_weights(critic2.get_weights())
99
100
 
101
+ # clone_model() reconstructs the Model graph/config but does not
102
+ # carry over head_model/encoder (plain Python attributes the
103
+ # factory attaches post-construction, not part of the Keras
104
+ # graph/config clone_model reads from) -- re-derive them the same
105
+ # way so _update_step()'s getattr(self._actor_tgt, "head_model",
106
+ # ...) lookups actually find the real head-only submodel instead
107
+ # of silently falling back to the full (encoder+head) actor_tgt.
108
+ if hasattr(actor, "head_model"):
109
+ self._actor_tgt.head_model = tf.keras.models.clone_model(actor.head_model)
110
+ self._actor_tgt.head_model.set_weights(actor.head_model.get_weights())
111
+ if hasattr(actor, "encoder"):
112
+ self._actor_tgt.encoder = tf.keras.models.clone_model(actor.encoder)
113
+ self._actor_tgt.encoder.set_weights(actor.encoder.get_weights())
114
+
100
115
  self._action_dim = action_dim
101
116
  self._actor_opt = actor_optimizer
102
117
  self._critic_opt = critic_optimizer
@@ -120,9 +135,17 @@ class TFTDDAgent(BaseAgent):
120
135
  def is_on_policy(self) -> bool:
121
136
  return False
122
137
 
138
+ def _encode(self, observation_batched):
139
+ """Run the actor's encoder alone, if it has one -- see
140
+ tf_sac.py's _encode() docstring for the full rationale."""
141
+ encoder = getattr(self._actor, "encoder", None)
142
+ if encoder is None:
143
+ return observation_batched
144
+ return encoder(observation_batched, training=False)
145
+
123
146
  def act(self, observation) -> np.ndarray:
124
147
  """Deterministic action: a = π_θ(s) = tanh(actor(s))."""
125
- obs = tf.cast(np.atleast_2d(observation), tf.float32)
148
+ obs = to_tf_batch(observation)
126
149
  action = self._actor(obs, training=False)
127
150
  return action.numpy()[0]
128
151
 
@@ -134,7 +157,8 @@ class TFTDDAgent(BaseAgent):
134
157
  updates_per_step = float(hp.get("updates_per_step", 1))
135
158
  policy_delay = int(hp.get("policy_delay", 2))
136
159
 
137
- obs_arr = np.array(episode_data.observations, dtype=np.float32)
160
+ obs_batched = to_tf_batch(list(episode_data.observations))
161
+ obs_arr = self._encode(obs_batched).numpy()
138
162
  act_arr = np.array(episode_data.actions, dtype=np.float32)
139
163
  rew_arr = episode_data.rewards
140
164
  done_arr = episode_data.dones
@@ -265,14 +289,21 @@ class TFTDDAgent(BaseAgent):
265
289
  obs_b, act_b, rew_b, next_b, done_b, weights, indices, n_steps = \
266
290
  self._buffer.sample(batch_size)
267
291
 
268
- obs_b = tf.cast(obs_b, tf.float32)
292
+ obs_b = already_batched_to_tf(obs_b)
269
293
  act_b = tf.cast(act_b, tf.float32)
270
294
  rew_b = tf.cast(rew_b, tf.float32)
271
- next_b = tf.cast(next_b, tf.float32)
295
+ next_b = already_batched_to_tf(next_b)
272
296
  done_b = tf.cast(done_b, tf.float32)
273
297
  weights = tf.cast(weights, tf.float32)
274
298
  gammas_n = tf.cast(gamma ** n_steps.astype(np.float32), tf.float32)
275
299
 
300
+ # Buffer-sampled obs_b/next_b are already flat, pre-encoded vectors
301
+ # -- route through head_model only, skipping the encoder a second
302
+ # time (see tf_sac.py's _update_step comment for the full
303
+ # rationale). Falls back to the full actor/actor_tgt for flat-Box.
304
+ actor_head = getattr(self._actor, "head_model", self._actor)
305
+ actor_tgt_head = getattr(self._actor_tgt, "head_model", self._actor_tgt)
306
+
276
307
  # ---- Critic update ----
277
308
  with tf.GradientTape() as tape:
278
309
  # Fix 3: target policy smoothing
@@ -282,7 +313,7 @@ class TFTDDAgent(BaseAgent):
282
313
  target_noise_clip,
283
314
  )
284
315
  next_action = tf.clip_by_value(
285
- self._actor_tgt(next_b, training=False) + noise, -1.0, 1.0
316
+ actor_tgt_head(next_b, training=False) + noise, -1.0, 1.0
286
317
  )
287
318
 
288
319
  # Fix 1: twin critics
@@ -311,20 +342,20 @@ class TFTDDAgent(BaseAgent):
311
342
  a_loss = 0.0
312
343
  if update_actor:
313
344
  with tf.GradientTape() as tape:
314
- new_a = self._actor(obs_b, training=True)
345
+ new_a = actor_head(obs_b, training=True)
315
346
  ci_new = tf.concat([obs_b, new_a], axis=-1)
316
347
  # Deterministic policy gradient: maximise Q1(s, π(s))
317
348
  a_loss_tf = -tf.reduce_mean(
318
349
  tf.squeeze(self._c1(ci_new, training=False), -1)
319
350
  )
320
351
 
321
- a_grads = tape.gradient(a_loss_tf, self._actor.trainable_variables)
352
+ a_grads = tape.gradient(a_loss_tf, actor_head.trainable_variables)
322
353
  a_grads, _ = tf.clip_by_global_norm(a_grads, 10.0)
323
354
  self._actor_opt.apply_gradients(
324
- zip(a_grads, self._actor.trainable_variables)
355
+ zip(a_grads, actor_head.trainable_variables)
325
356
  )
326
357
  a_loss = float(a_loss_tf.numpy())
327
- self._soft_update(self._actor, self._actor_tgt, tau)
358
+ self._soft_update(actor_head, actor_tgt_head, tau)
328
359
 
329
360
  self._soft_update(self._c1, self._c1_tgt, tau)
330
361
  self._soft_update(self._c2, self._c2_tgt, tau)