braintools 0.2.0__tar.gz → 0.3.0__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 (215) hide show
  1. {braintools-0.2.0 → braintools-0.3.0}/PKG-INFO +1 -1
  2. {braintools-0.2.0 → braintools-0.3.0}/braintools/_version.py +1 -1
  3. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/__init__.py +6 -4
  4. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/context.py +14 -8
  5. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/feature.py +1 -1
  6. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/phase.py +20 -12
  7. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/phase_test.py +60 -0
  8. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/task.py +27 -12
  9. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/task_test.py +33 -0
  10. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/working_memory.py +7 -2
  11. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/working_memory_test.py +14 -0
  12. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_base.py +29 -9
  13. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_base_test.py +97 -0
  14. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_basic.py +21 -15
  15. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_basic_test.py +58 -0
  16. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_orthogonal.py +15 -2
  17. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_orthogonal_test.py +26 -0
  18. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_variance_scaling.py +10 -0
  19. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/__init__.py +2 -0
  20. braintools-0.3.0/braintools/metric/_audit_20260619_test.py +255 -0
  21. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_classification.py +15 -7
  22. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_correlation.py +29 -28
  23. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_correlation_test.py +3 -3
  24. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_fenchel_young.py +10 -7
  25. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_firings.py +45 -33
  26. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_lfp.py +42 -3
  27. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_lfp_test.py +8 -4
  28. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_pairwise.py +22 -15
  29. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_ranking.py +5 -0
  30. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_regression.py +19 -10
  31. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_smoothing.py +6 -2
  32. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_smoothing_test.py +3 -1
  33. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_util.py +3 -0
  34. {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_optax_lr_scheduler.py +45 -20
  35. {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_optax_optimizer.py +115 -15
  36. braintools-0.3.0/braintools/optim/_reaudit_fixes_test.py +269 -0
  37. {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_state_uniquifier.py +84 -60
  38. {braintools-0.2.0 → braintools-0.3.0}/braintools/surrogate/_base.py +1 -0
  39. {braintools-0.2.0 → braintools-0.3.0}/braintools/surrogate/_correctness_test.py +37 -0
  40. {braintools-0.2.0 → braintools-0.3.0}/braintools/surrogate/_impl.py +44 -18
  41. {braintools-0.2.0 → braintools-0.3.0}/braintools/surrogate/_impl_extra_test.py +2 -2
  42. braintools-0.3.0/braintools/trainer/_audit_20260619_test.py +275 -0
  43. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_callbacks.py +31 -9
  44. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_dataloader.py +7 -0
  45. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_distributed.py +38 -28
  46. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_trainer.py +37 -4
  47. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/__init__.py +2 -2
  48. braintools-0.3.0/braintools/visualize/_audit_20260619_test.py +356 -0
  49. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_interactive.py +9 -3
  50. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_neural.py +35 -3
  51. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_plots.py +63 -41
  52. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_statistical.py +42 -14
  53. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_three_d.py +20 -4
  54. {braintools-0.2.0 → braintools-0.3.0}/braintools.egg-info/PKG-INFO +1 -1
  55. {braintools-0.2.0 → braintools-0.3.0}/braintools.egg-info/SOURCES.txt +4 -0
  56. {braintools-0.2.0 → braintools-0.3.0}/LICENSE +0 -0
  57. {braintools-0.2.0 → braintools-0.3.0}/README.md +0 -0
  58. {braintools-0.2.0 → braintools-0.3.0}/braintools/__init__.py +0 -0
  59. {braintools-0.2.0 → braintools-0.3.0}/braintools/_misc.py +0 -0
  60. {braintools-0.2.0 → braintools-0.3.0}/braintools/_spike_encoder.py +0 -0
  61. {braintools-0.2.0 → braintools-0.3.0}/braintools/_spike_encoder_test.py +0 -0
  62. {braintools-0.2.0 → braintools-0.3.0}/braintools/_spike_operation.py +0 -0
  63. {braintools-0.2.0 → braintools-0.3.0}/braintools/_spike_operation_test.py +0 -0
  64. {braintools-0.2.0 → braintools-0.3.0}/braintools/_typing_test.py +0 -0
  65. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/_typing.py +0 -0
  66. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/_version.py +0 -0
  67. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/conditional.py +0 -0
  68. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/conditional_test.py +0 -0
  69. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/conftest.py +0 -0
  70. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/context_test.py +0 -0
  71. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/encoder.py +0 -0
  72. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/encoder_test.py +0 -0
  73. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/feature_test.py +0 -0
  74. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/label.py +0 -0
  75. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/label_test.py +0 -0
  76. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/task_variable_length_test.py +0 -0
  77. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/__init__.py +0 -0
  78. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/decision_making.py +0 -0
  79. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/decision_making_test.py +0 -0
  80. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/motor.py +0 -0
  81. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/motor_test.py +0 -0
  82. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/reasoning.py +0 -0
  83. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/reasoning_test.py +0 -0
  84. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/utils.py +0 -0
  85. {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/utils_test.py +0 -0
  86. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/__init__.py +0 -0
  87. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_base.py +0 -0
  88. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_base_test.py +0 -0
  89. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_biological.py +0 -0
  90. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_biological_test.py +0 -0
  91. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_compartment.py +0 -0
  92. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_compartment_test.py +0 -0
  93. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_kernel.py +0 -0
  94. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_kernel_test.py +0 -0
  95. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_random.py +0 -0
  96. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_random_test.py +0 -0
  97. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_regular.py +0 -0
  98. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_regular_test.py +0 -0
  99. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_spatial.py +0 -0
  100. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_spatial_test.py +0 -0
  101. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_topological.py +0 -0
  102. {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_topological_test.py +0 -0
  103. {braintools-0.2.0 → braintools-0.3.0}/braintools/file/__init__.py +0 -0
  104. {braintools-0.2.0 → braintools-0.3.0}/braintools/file/_file_comprehensive_test.py +0 -0
  105. {braintools-0.2.0 → braintools-0.3.0}/braintools/file/_matfile.py +0 -0
  106. {braintools-0.2.0 → braintools-0.3.0}/braintools/file/_matfile_extra_test.py +0 -0
  107. {braintools-0.2.0 → braintools-0.3.0}/braintools/file/_msg_checkpoint.py +0 -0
  108. {braintools-0.2.0 → braintools-0.3.0}/braintools/file/_msg_checkpoint_async_test.py +0 -0
  109. {braintools-0.2.0 → braintools-0.3.0}/braintools/file/_msg_checkpoint_extra_test.py +0 -0
  110. {braintools-0.2.0 → braintools-0.3.0}/braintools/file/_msg_checkpoint_test.py +0 -0
  111. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/__init__.py +0 -0
  112. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_distance_base.py +0 -0
  113. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_distance_base_test.py +0 -0
  114. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_distance_impl.py +0 -0
  115. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_distance_impl_test.py +0 -0
  116. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_composite.py +0 -0
  117. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_composite_test.py +0 -0
  118. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_variance_scaling_test.py +0 -0
  119. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_with_distance.py +0 -0
  120. {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_with_distance_test.py +0 -0
  121. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/__init__.py +0 -0
  122. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_base.py +0 -0
  123. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_base_test.py +0 -0
  124. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_basic.py +0 -0
  125. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_basic_test.py +0 -0
  126. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_pulses.py +0 -0
  127. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_pulses_test.py +0 -0
  128. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_stochastic.py +0 -0
  129. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_stochastic_test.py +0 -0
  130. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_waveforms.py +0 -0
  131. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_waveforms_test.py +0 -0
  132. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_deprecation.py +0 -0
  133. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_functional_basic.py +0 -0
  134. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_functional_basic_test.py +0 -0
  135. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_functional_pulses.py +0 -0
  136. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_functional_pulses_test.py +0 -0
  137. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_functional_stochastic.py +0 -0
  138. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_functional_stochastic_test.py +0 -0
  139. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_functional_waveforms.py +0 -0
  140. {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_functional_waveforms_test.py +0 -0
  141. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_classification_test.py +0 -0
  142. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_fenchel_young_test.py +0 -0
  143. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_firings_test.py +0 -0
  144. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_pairwise_test.py +0 -0
  145. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_ranking_test.py +0 -0
  146. {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_regression_test.py +0 -0
  147. {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/__init__.py +0 -0
  148. {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_audit_fixes_test.py +0 -0
  149. {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_base.py +0 -0
  150. {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_nevergrad_optimizer.py +0 -0
  151. {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_nevergrad_optimizer_test.py +0 -0
  152. {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_optax_lr_scheduler_test.py +0 -0
  153. {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_optax_optimizer_test.py +0 -0
  154. {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_scipy_optimizer.py +0 -0
  155. {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_scipy_optimizer_test.py +0 -0
  156. {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_sofo_optimizer.py +0 -0
  157. {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_sofo_optimizer_test.py +0 -0
  158. {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_state_uniquifier_test.py +0 -0
  159. {braintools-0.2.0 → braintools-0.3.0}/braintools/py.typed +0 -0
  160. {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/__init__.py +0 -0
  161. {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_dde_integrator.py +0 -0
  162. {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_dde_integrator_test.py +0 -0
  163. {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_imex_integrator.py +0 -0
  164. {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_imex_integrator_test.py +0 -0
  165. {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_ode_integrator.py +0 -0
  166. {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_ode_integrator_test.py +0 -0
  167. {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_quad_coverage_test.py +0 -0
  168. {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_quad_regression_test.py +0 -0
  169. {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_sde_integrator.py +0 -0
  170. {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_sde_integrator_test.py +0 -0
  171. {braintools-0.2.0 → braintools-0.3.0}/braintools/surrogate/__init__.py +0 -0
  172. {braintools-0.2.0 → braintools-0.3.0}/braintools/surrogate/_base_test.py +0 -0
  173. {braintools-0.2.0 → braintools-0.3.0}/braintools/surrogate/_impl_test.py +0 -0
  174. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/__init__.py +0 -0
  175. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_callbacks_test.py +0 -0
  176. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_checkpoint.py +0 -0
  177. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_checkpoint_test.py +0 -0
  178. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_dataloader_test.py +0 -0
  179. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_distributed_test.py +0 -0
  180. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_loggers.py +0 -0
  181. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_loggers_backends_test.py +0 -0
  182. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_loggers_test.py +0 -0
  183. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_module.py +0 -0
  184. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_module_test.py +0 -0
  185. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_progress.py +0 -0
  186. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_progress_test.py +0 -0
  187. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_trainer_audit_test.py +0 -0
  188. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_trainer_extra_test.py +0 -0
  189. {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_trainer_test.py +0 -0
  190. {braintools-0.2.0 → braintools-0.3.0}/braintools/tree.py +0 -0
  191. {braintools-0.2.0 → braintools-0.3.0}/braintools/tree_test.py +0 -0
  192. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_animation.py +0 -0
  193. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_animation_test.py +0 -0
  194. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_audit_fixes_test.py +0 -0
  195. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_colormaps.py +0 -0
  196. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_colormaps_extra_test.py +0 -0
  197. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_colormaps_test.py +0 -0
  198. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_figures.py +0 -0
  199. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_figures_test.py +0 -0
  200. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_interactive_extra_test.py +0 -0
  201. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_interactive_test.py +0 -0
  202. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_neural_extra_test.py +0 -0
  203. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_neural_test.py +0 -0
  204. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_plots_extra_test.py +0 -0
  205. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_plots_test.py +0 -0
  206. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_statistical_extra_test.py +0 -0
  207. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_statistical_test.py +0 -0
  208. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_three_d_extra_test.py +0 -0
  209. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_three_d_test.py +0 -0
  210. {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_visualize_test.py +0 -0
  211. {braintools-0.2.0 → braintools-0.3.0}/braintools.egg-info/dependency_links.txt +0 -0
  212. {braintools-0.2.0 → braintools-0.3.0}/braintools.egg-info/requires.txt +0 -0
  213. {braintools-0.2.0 → braintools-0.3.0}/braintools.egg-info/top_level.txt +0 -0
  214. {braintools-0.2.0 → braintools-0.3.0}/pyproject.toml +0 -0
  215. {braintools-0.2.0 → braintools-0.3.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: braintools
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Modeling tools for brain simulation.
5
5
  Author-email: BrainTools Developers <chao.brain@qq.com>
6
6
  License:
@@ -13,5 +13,5 @@
13
13
  # limitations under the License.
14
14
  # ==============================================================================
15
15
 
16
- __version__ = "0.2.0"
16
+ __version__ = "0.3.0"
17
17
  __version_info__ = tuple(map(int, __version__.split(".")))
@@ -35,7 +35,7 @@ Building custom tasks from phases:
35
35
  >>> from braintools.cogtask import (
36
36
  ... Task, Context, concat,
37
37
  ... Fixation, Stimulus, Delay, Response,
38
- ... Feature, circular, one_hot
38
+ ... Feature, circular, one_hot, label
39
39
  ... )
40
40
  >>> import brainunit as u
41
41
  >>>
@@ -44,13 +44,14 @@ Building custom tasks from phases:
44
44
  >>> stim = Feature(8, 'stimulus')
45
45
  >>> choice = Feature(2, 'choice')
46
46
  >>>
47
- >>> # Build task
47
+ >>> # Build task. A string label spec must be wrapped in ``label(...)`` so it
48
+ >>> # is read as a context key; a bare string is treated as a literal and fails.
48
49
  >>> task = Task(
49
50
  ... phases=concat([
50
51
  ... Fixation(100 * u.ms, inputs={'fixation': 1.0}),
51
52
  ... Stimulus(500 * u.ms, inputs={'stimulus': circular('direction')}),
52
53
  ... Delay(500 * u.ms, inputs={'fixation': 1.0}),
53
- ... Response(100 * u.ms, outputs={'label': 'ground_truth'})
54
+ ... Response(100 * u.ms, outputs={'label': label('ground_truth')})
54
55
  ... ]),
55
56
  ... input_features=fix + stim,
56
57
  ... output_features=fix + choice,
@@ -153,7 +154,7 @@ from .phase import (
153
154
  execute_phase_packed,
154
155
  phase_tree_is_variable,
155
156
  )
156
- from .task import Task
157
+ from .task import Task, create_task
157
158
  # Pre-built tasks
158
159
  from .tasks import (
159
160
  # Decision Making
@@ -215,6 +216,7 @@ __all__ = [
215
216
  'Switch',
216
217
  'While',
217
218
  'Task',
219
+ 'create_task',
218
220
  # Features
219
221
  'Feature',
220
222
  'FeatureSet',
@@ -36,19 +36,25 @@ class Context:
36
36
 
37
37
  Examples
38
38
  --------
39
- >>> ctx = Context(seed=42)
40
- >>> ctx['ground_truth'] = 1
41
- >>> ctx['stimulus_direction'] = 0.5 * np.pi
42
- >>> print(ctx['ground_truth'])
43
- 1
39
+ .. code-block:: python
44
40
 
45
- >>> # In a phase:
46
- >>> ctx.inputs[ctx.phase_start:ctx.phase_end, :] = stimulus_encoding
41
+ >>> import jax
42
+ >>> import jax.numpy as jnp
43
+ >>> ctx = Context(key=jax.random.PRNGKey(42))
44
+ >>> ctx['ground_truth'] = 1
45
+ >>> ctx['stimulus_direction'] = 0.5 * jnp.pi
46
+ >>> print(ctx['ground_truth'])
47
+ 1
48
+
49
+ >>> # In a phase, write into the buffer with a functional update
50
+ >>> # (JAX arrays are immutable; ``.at[...].set(...)`` returns a copy):
51
+ >>> # ctx.inputs = ctx.inputs.at[ctx.phase_start:ctx.phase_end, :].set(stimulus_encoding)
47
52
 
48
53
  Parameters
49
54
  ----------
50
55
  key : jax.Array, optional
51
- JAX PRNGKey for random number generation. If None, creates one with seed.
56
+ JAX PRNGKey for random number generation. If None, draws fresh
57
+ randomness from brainstate's default RNG.
52
58
 
53
59
  Note
54
60
  ----
@@ -300,7 +300,7 @@ class Feature(_FeatureBase):
300
300
  """
301
301
  Create multiple copies of this feature with indexed names.
302
302
 
303
- Example: Feature(1, 30.*u.Hz, 'choice') * 3
303
+ Example: Feature(1, 'choice') * 3
304
304
  Creates: choice_0, choice_1, choice_2
305
305
  """
306
306
  if not isinstance(count, int) or count < 1:
@@ -457,25 +457,33 @@ class Parallel(Phase):
457
457
  pass
458
458
 
459
459
  def execute(self, ctx: Context) -> None:
460
- """Encode each child within its own [phase_start, phase_start+dur)."""
460
+ """Encode each child within its own ``[parent_start, parent_start+dur)``.
461
+
462
+ Every child — leaf *or* compound — is dispatched through
463
+ ``execute_phase`` so that nested ``Sequence``/``Repeat``/conditional
464
+ children execute correctly. Routing compound children through
465
+ ``encode_inputs``/``encode_outputs`` directly would be a no-op (those
466
+ classes drive their sub-phases via ``execute``), silently dropping the
467
+ branch. This mirrors ``execute_packed`` and keeps the two paths
468
+ consistent. All children start at ``parent_start``; only the first child
469
+ contributes to the output buffer by convention.
470
+ """
461
471
  parent_start = ctx.phase_start
462
472
  parent_end = ctx.phase_end
463
473
  parent_name = ctx.current_phase
464
474
 
465
475
  for i, child in enumerate(self.phases):
466
- child_dur = child.get_duration(ctx)
467
- ctx.phase_start = parent_start
468
- ctx.phase_end = parent_start + child_dur
469
- ctx.current_phase = child.name
470
- child.on_enter(ctx)
471
- child.encode_inputs(ctx)
472
- # Only the first child contributes outputs by default.
476
+ ctx.current_step = parent_start
473
477
  if i == 0:
474
- child.encode_outputs(ctx)
475
- child.on_exit(ctx)
476
- ctx.phase_history.append((child.name, ctx.phase_start, ctx.phase_end))
478
+ execute_phase(child, ctx)
479
+ else:
480
+ # Non-first children must not contribute to the output buffer.
481
+ # Stash, encode (inputs + any state), then restore outputs.
482
+ saved_out = ctx.outputs
483
+ execute_phase(child, ctx)
484
+ ctx.outputs = saved_out
477
485
 
478
- # Restore parent scope.
486
+ # Restore parent scope and advance past the (longest) child.
479
487
  ctx.phase_start = parent_start
480
488
  ctx.phase_end = parent_end
481
489
  ctx.current_phase = parent_name
@@ -333,3 +333,63 @@ def test_describe_lists_inputs_outputs_and_noise():
333
333
  s = p.describe()
334
334
  assert "Inputs" in s and "Outputs" in s and "Noise" in s
335
335
  assert "fixation" in s
336
+
337
+
338
+ def test_parallel_executes_compound_child_eager():
339
+ # Regression: a compound child (Sequence) of a Parallel must execute its
340
+ # sub-phases. Previously Parallel.execute called encode_inputs directly,
341
+ # which is a no-op for compounds, so the whole branch was silently dropped.
342
+ m = ct.Feature(1, 'm')
343
+ n = ct.Feature(1, 'n')
344
+ fix = ct.Feature(1, 'fixation')
345
+ a = Stimulus(5 * u.ms, name='A', inputs={'m': 1.0}, outputs={'label': 0})
346
+ b = Stimulus(5 * u.ms, name='B', inputs={'m': 1.0}, outputs={'label': 0})
347
+ seq_ab = a >> b # compound child
348
+ c = Stimulus(10 * u.ms, name='C', inputs={'n': 1.0}, outputs={'label': 0})
349
+ par = seq_ab | c
350
+ task = ct.Task(
351
+ phases=par,
352
+ input_features=m + n + fix,
353
+ output_features=fix + ct.Feature(1, 'out'),
354
+ seed=0,
355
+ )
356
+ X, _, _ = task.sample_trial(0)
357
+ X = np.asarray(X)
358
+ assert X.shape[0] == 10
359
+ # A and B each ran for 5 ticks writing 1.0 into channel 'm' -> sum == 10.
360
+ np.testing.assert_allclose(X[:, 0].sum(), 10.0, atol=1e-6)
361
+ # C ran for all 10 ticks writing 1.0 into channel 'n'.
362
+ np.testing.assert_allclose(X[:, 1], 1.0, atol=1e-6)
363
+
364
+
365
+ def test_parallel_eager_and_packed_agree_on_compound_child():
366
+ # The fixed (eager) and variable (packed) execution paths must produce the
367
+ # same input channels for a Parallel whose first child is a compound, when
368
+ # all durations are fully realised.
369
+ m = ct.Feature(1, 'm')
370
+ n = ct.Feature(1, 'n')
371
+ fix = ct.Feature(1, 'fixation')
372
+
373
+ def build(extra_phase):
374
+ a = Stimulus(5 * u.ms, name='A', inputs={'m': 1.0}, outputs={'label': 0})
375
+ b = Stimulus(5 * u.ms, name='B', inputs={'m': 1.0}, outputs={'label': 0})
376
+ c = Stimulus(10 * u.ms, name='C', inputs={'n': 1.0}, outputs={'label': 0})
377
+ phases = (a >> b) | c
378
+ if extra_phase is not None:
379
+ phases = phases >> extra_phase
380
+ return ct.Task(phases=phases, input_features=m + n + fix,
381
+ output_features=fix + ct.Feature(1, 'out'), seed=0)
382
+
383
+ # Eager path.
384
+ X_eager, _, _ = build(None).sample_trial(0)
385
+ # Packed path: append a VariableDuration whose realised length is fixed via
386
+ # trial_init, forcing packed mode but the same total realised content.
387
+ vd = ct.VariableDuration(min_duration=2 * u.ms, max_duration=4 * u.ms,
388
+ ctx_key='d', inputs={'n': 1.0}, outputs={'label': 0},
389
+ name='vd')
390
+ packed_task = build(vd)
391
+ packed_task._trial_init_func = lambda ctx: ctx.update(d=4.0)
392
+ X_packed, _, info = packed_task.sample_trial(0)
393
+ # The Parallel block occupies the first 10 ticks in both; compare them.
394
+ np.testing.assert_allclose(np.asarray(X_eager)[:10, 0],
395
+ np.asarray(X_packed)[:10, 0], atol=1e-6)
@@ -86,17 +86,22 @@ class Task:
86
86
  --------
87
87
  Instance-based (traditional):
88
88
 
89
+ >>> import brainunit as u
90
+ >>> from braintools.cogtask import Feature, Fixation, Stimulus, Response, circular, label
91
+ >>> fix = Feature(1, 'fixation')
92
+ >>> stim = Feature(8, 'stimulus')
93
+ >>> choice = Feature(2, 'choice')
89
94
  >>> task = Task(
90
95
  ... phases=(
91
- ... Fixation(100 * u.ms)
92
- ... >> Stimulus(2000 * u.ms, feature=stim, encoder=circular_encoder())
93
- ... >> Response(100 * u.ms, output_feature=choice)
96
+ ... Fixation(100 * u.ms, inputs={'fixation': 1.0})
97
+ ... >> Stimulus(2000 * u.ms, inputs={'stimulus': circular('direction')})
98
+ ... >> Response(100 * u.ms, outputs={'label': label('ground_truth')})
94
99
  ... ),
95
100
  ... input_features=fix + stim,
96
101
  ... output_features=fix + choice,
97
102
  ... trial_init=lambda ctx: ctx.update(
98
103
  ... ground_truth=ctx.rng.choice(2),
99
- ... direction=ctx.rng.uniform(0, 2*np.pi)
104
+ ... direction=ctx.rng.uniform(0, 2 * 3.14159)
100
105
  ... )
101
106
  ... )
102
107
 
@@ -107,12 +112,13 @@ class Task:
107
112
  ... num_stimuli = 8
108
113
  ...
109
114
  ... def define_features(self):
110
- ... fix = Feature(1, 40*u.Hz, 'fixation')
111
- ... stim = Feature(self.num_stimuli, 40*u.Hz, 'stimulus')
112
- ... return fix + stim, fix + Feature(2, 40*u.Hz, 'response')
115
+ ... fix = Feature(1, 'fixation')
116
+ ... stim = Feature(self.num_stimuli, 'stimulus')
117
+ ... return fix + stim, fix + Feature(2, 'response')
113
118
  ...
114
119
  ... def define_phases(self):
115
- ... return FixationPhase(self.t_fixation) >> ResponsePhase()
120
+ ... return (Fixation(self.t_fixation, inputs={'fixation': 1.0})
121
+ ... >> Response(100 * u.ms, outputs={'label': label('ground_truth')}))
116
122
  ...
117
123
  ... def trial_init(self, ctx):
118
124
  ... ctx['ground_truth'] = ctx.rng.choice(2)
@@ -631,11 +637,20 @@ def create_task(
631
637
 
632
638
  Examples
633
639
  --------
640
+ >>> import brainunit as u
641
+ >>> from braintools.cogtask import Feature, Fixation, Stimulus, Response, circular, label
634
642
  >>> task = create_task(
635
- ... phases=Fixation(100*u.ms) >> Stimulus(500*u.ms) >> Response(100*u.ms),
636
- ... input_features=Feature('fix', 1) + Feature('stim', 2),
637
- ... output_features=Feature('fix', 1) + Feature('choice', 2),
638
- ... num_trial=1000
643
+ ... phases=(
644
+ ... Fixation(100 * u.ms, inputs={'fixation': 1.0})
645
+ ... >> Stimulus(500 * u.ms, inputs={'stimulus': circular('direction')})
646
+ ... >> Response(100 * u.ms, outputs={'label': label('ground_truth')})
647
+ ... ),
648
+ ... input_features=Feature(1, 'fixation') + Feature(2, 'stimulus'),
649
+ ... output_features=Feature(1, 'fixation') + Feature(2, 'choice'),
650
+ ... trial_init=lambda ctx: ctx.update(
651
+ ... ground_truth=ctx.rng.choice(2),
652
+ ... direction=ctx.rng.uniform(0, 2 * 3.14159)
653
+ ... ),
639
654
  ... )
640
655
  """
641
656
  return Task(phases, input_features, output_features, trial_init, name,
@@ -387,3 +387,36 @@ def test_pulse_decision_making_runs_through_repeat():
387
387
  gt = int(info['trial_state']['ground_truth'])
388
388
  last = int(np.asarray(Y)[-1])
389
389
  assert last == gt + 1
390
+
391
+
392
+ # ---------------------------------------------------------------------------
393
+ # create_task factory: must be exported from the package and produce a
394
+ # sampleable task. Also exercises the (previously broken) docstring pattern
395
+ # of a string label spec wrapped in label(...).
396
+ # ---------------------------------------------------------------------------
397
+
398
+ def test_create_task_is_exported_from_package():
399
+ assert hasattr(ct, 'create_task')
400
+ assert 'create_task' in ct.__all__
401
+
402
+
403
+ def test_create_task_builds_sampleable_task_with_string_label():
404
+ task = ct.create_task(
405
+ phases=(
406
+ ct.Fixation(10 * u.ms, inputs={'fixation': 1.0})
407
+ >> ct.Stimulus(20 * u.ms, inputs={'stimulus': ct.circular('direction')})
408
+ >> ct.Response(10 * u.ms, outputs={'label': ct.label('ground_truth')})
409
+ ),
410
+ input_features=ct.Feature(1, 'fixation') + ct.Feature(2, 'stimulus'),
411
+ output_features=ct.Feature(1, 'fixation') + ct.Feature(2, 'choice'),
412
+ trial_init=lambda ctx: ctx.update(
413
+ ground_truth=ctx.rng.choice(2),
414
+ direction=ctx.rng.uniform(0, 2 * 3.14159),
415
+ ),
416
+ seed=0,
417
+ )
418
+ X, Y, info = task.sample_trial(0)
419
+ assert X.shape[0] == Y.shape[0] == 40
420
+ gt = int(info['trial_state']['ground_truth'])
421
+ # Response phase is the last 10 ticks; label == ground_truth there.
422
+ assert (np.asarray(Y)[-10:] == gt).all()
@@ -62,6 +62,13 @@ def make_encoder(mode: str, key: str, *, feature_per_direction: int = 1, kappa=2
62
62
  """
63
63
  mode = mode.lower()
64
64
 
65
+ # ``scalar`` broadcasts a single value across every feature dimension, so it
66
+ # is agnostic to ``feature_per_direction`` — handle it up front rather than
67
+ # only in the K == 1 branch (otherwise scalar + K>1 fell through to the
68
+ # population branch and raised a misleading "Unknown mode=scalar").
69
+ if mode == "scalar":
70
+ return scalar(key)
71
+
65
72
  if feature_per_direction == 1:
66
73
  if mode == "von_mises":
67
74
  return von_mises(key, kappa=kappa, base_value=base_value, as_index=True, num_dirs=num_dirs)
@@ -69,8 +76,6 @@ def make_encoder(mode: str, key: str, *, feature_per_direction: int = 1, kappa=2
69
76
  return one_hot(key)
70
77
  elif mode == "circular":
71
78
  return circular(key, base_value=base_value, as_index=True, num_dirs=num_dirs)
72
- elif mode == "scalar":
73
- return scalar(key)
74
79
  else:
75
80
  raise ValueError(f"Unknown mode={mode}")
76
81
 
@@ -67,6 +67,20 @@ def test_make_encoder_population_dim_not_divisible_raises():
67
67
  enc(ctx, f)
68
68
 
69
69
 
70
+ @pytest.mark.parametrize("k", [1, 3, 5])
71
+ def test_make_encoder_scalar_is_feature_per_direction_agnostic(k):
72
+ # Regression: scalar was only handled in the K==1 branch, so
73
+ # feature_per_direction>1 raised "Unknown mode=scalar". scalar broadcasts a
74
+ # single value across all dims and must work for any K.
75
+ enc = make_encoder("scalar", "sample_value", feature_per_direction=k)
76
+ f = ct.Feature(6, 'stimulus')
77
+ ctx = ct.Context()
78
+ ctx['sample_value'] = 0.7
79
+ out = np.asarray(enc(ctx, f))
80
+ assert out.shape == (6,)
81
+ np.testing.assert_allclose(out, 0.7, atol=1e-6)
82
+
83
+
70
84
  def test_build_cues_returns_jax_arrays_with_right_shapes():
71
85
  non_resp, resp = build_cues(3)
72
86
  assert non_resp.shape == (3,)
@@ -87,9 +87,13 @@ class Initialization(ABC):
87
87
  >>>
88
88
  >>> delay_init = Uniform(1.0 * u.ms, 3.0 * u.ms).clip(0.5 * u.ms, 5.0 * u.ms)
89
89
  >>>
90
+ >>> # Each transform must be parenthesized: a bare ``lambda`` body would
91
+ >>> # greedily capture the rest of the expression. Bounds must share the
92
+ >>> # value's unit (``0.0 * u.nS``, not ``0``).
90
93
  >>> combined = (Normal(1.0 * u.nS, 0.2 * u.nS) |
91
- ... lambda x: x.clip(0, 2 * u.nS) |
92
- ... lambda x: x * 0.5)
94
+ ... (lambda x: u.math.maximum(x, 0.0 * u.nS)) |
95
+ ... (lambda x: x * 0.5))
96
+ >>> values = combined(1000, rng=np.random.default_rng(0))
93
97
  """
94
98
  __module__ = 'braintools.init'
95
99
 
@@ -344,9 +348,17 @@ def param(
344
348
  return init
345
349
 
346
350
  # Check if the parameter is a callable function
351
+ #
352
+ # For a callable, the batch axis is incorporated by prepending it to ``sizes``
353
+ # and letting the initializer fill the full batched shape (so a random
354
+ # initializer draws independently per batch element). The shared trailing
355
+ # batch-expansion below must therefore NOT run again for this path, otherwise
356
+ # the batch axis is applied twice (``(batch, batch, *sizes)``).
357
+ batch_applied_by_callable = False
347
358
  if callable(init):
348
359
  if batch_size is not None:
349
360
  sizes = (batch_size,) + sizes
361
+ batch_applied_by_callable = True
350
362
  init = init(sizes, **param_kwargs)
351
363
  else:
352
364
  # Check if the parameter is a scalar value
@@ -357,16 +369,20 @@ def param(
357
369
  else:
358
370
  raise TypeError(f'Unknown parameter type: {type(init)}')
359
371
 
372
+ # Resolve the underlying value first: a ``State`` is a mutable container and
373
+ # ``u.math.shape(state)`` reports ``()`` (it does not see into the wrapped
374
+ # value), which would silently bypass the shape check below.
375
+ param_value = init.value if isinstance(init, State) else init
376
+
360
377
  # Check if the shape of the parameter matches the given size
361
- if not _are_broadcastable_shapes(u.math.shape(init), sizes):
378
+ if not _are_broadcastable_shapes(u.math.shape(param_value), sizes):
362
379
  raise ValueError(
363
- f'The shape of the parameter {u.math.shape(init)} '
380
+ f'The shape of the parameter {u.math.shape(param_value)} '
364
381
  f'does not match with the given size {sizes}'
365
382
  )
366
383
 
367
384
  # Expand the parameter to match the given batch size
368
- param_value = init.value if isinstance(init, State) else init
369
- if batch_size is not None:
385
+ if batch_size is not None and not batch_applied_by_callable:
370
386
  if param_value.ndim <= len(sizes):
371
387
  # add a new axis to the params so that it matches the dimensionality of the given shape ``sizes``
372
388
  param_value = _expand_params_to_match_sizes(param_value, sizes)
@@ -375,15 +391,19 @@ def param(
375
391
  batch_size,
376
392
  axis=0
377
393
  )
378
- if isinstance(init, State):
379
- init.restore_value(param_value)
380
- param_value = init
381
394
  else:
382
395
  if param_value.shape[0] != batch_size:
383
396
  raise ValueError(
384
397
  f'The batch size of the parameter {param_value.shape[0]} '
385
398
  f'does not match with the given batch size {batch_size}'
386
399
  )
400
+ # A ``State`` input is a mutable container: update its value and return the
401
+ # container itself, consistent with the ``Param`` path above and the
402
+ # documented contract. Doing this only in the batch-expansion branch (the
403
+ # previous behaviour) made the return type depend on the batch shape.
404
+ if isinstance(init, State):
405
+ init.restore_value(param_value)
406
+ return init
387
407
  return param_value
388
408
 
389
409
 
@@ -709,6 +709,50 @@ class TestParamSupport:
709
709
  param(p, 5, batch_size=3)
710
710
 
711
711
 
712
+ class TestParamStateSupport:
713
+ """param() must return the State (value updated) consistently (bug B2).
714
+
715
+ Previously the no-batch and already-batched paths returned the bare
716
+ underlying value, while only the batch-expansion path returned the State.
717
+ """
718
+
719
+ def test_state_no_batch_returns_state(self):
720
+ from brainstate import State
721
+ s = State(np.ones(10) * u.nS)
722
+ result = param(s, 10)
723
+ assert isinstance(result, State)
724
+ assert result is s
725
+ assert u.math.shape(result.value) == (10,)
726
+
727
+ def test_state_batch_expand_returns_state(self):
728
+ from brainstate import State
729
+ s = State(np.ones(10) * u.nS)
730
+ result = param(s, 10, batch_size=4)
731
+ assert isinstance(result, State)
732
+ assert result is s
733
+ assert u.math.shape(result.value) == (4, 10)
734
+
735
+ def test_state_already_batched_returns_state(self):
736
+ from brainstate import State
737
+ s = State(np.ones((4, 10)) * u.nS)
738
+ result = param(s, 10, batch_size=4)
739
+ assert isinstance(result, State)
740
+ assert result is s
741
+ assert u.math.shape(result.value) == (4, 10)
742
+
743
+ def test_state_batch_mismatch_raises(self):
744
+ from brainstate import State
745
+ s = State(np.ones((3, 10)) * u.nS)
746
+ with pytest.raises(ValueError, match="batch size"):
747
+ param(s, 10, batch_size=4)
748
+
749
+ def test_state_shape_mismatch_raises(self):
750
+ from brainstate import State
751
+ s = State(np.ones(10) * u.nS)
752
+ with pytest.raises(ValueError, match="does not match"):
753
+ param(s, 5)
754
+
755
+
712
756
  class TestParamBatching:
713
757
  """Test param() batch_size expansion for plain arrays/quantities (bug C2)."""
714
758
 
@@ -741,6 +785,59 @@ class TestParamBatching:
741
785
  param(np.ones((4, 5)), 5, batch_size=3)
742
786
 
743
787
 
788
+ class TestParamCallableBatching:
789
+ """param() batch_size for callable / Initialization initializers.
790
+
791
+ Regression for the double-batching bug: a callable initializer with
792
+ ``batch_size`` must yield exactly ``(batch_size, *sizes)`` -- not
793
+ ``(batch_size, batch_size, *sizes)``.
794
+ """
795
+
796
+ def test_callable_with_batch_size_1d(self):
797
+ """A callable gains a single leading batch axis."""
798
+ result = param(SimpleInit(5.0), 3, batch_size=4)
799
+ assert u.math.shape(result) == (4, 3)
800
+ assert np.all(np.asarray(result) == 5.0)
801
+
802
+ def test_callable_with_batch_size_2d(self):
803
+ """A callable with 2-D sizes gains exactly one leading batch axis."""
804
+ result = param(SimpleInit(5.0), (3, 4), batch_size=2)
805
+ assert u.math.shape(result) == (2, 3, 4)
806
+ assert np.all(np.asarray(result) == 5.0)
807
+
808
+ def test_callable_quantity_with_batch_size(self):
809
+ """A callable returning a Quantity keeps its unit and batched shape."""
810
+ result = param(QuantityInit(2.0 * u.nS), 3, batch_size=4)
811
+ assert u.math.shape(result) == (4, 3)
812
+ assert u.get_unit(result) == u.nS
813
+ assert np.all(result.to_decimal(u.nS) == 2.0)
814
+
815
+ def test_callable_batch_size_none_unchanged(self):
816
+ """batch_size=None leaves the callable result un-batched."""
817
+ result = param(SimpleInit(5.0), 3, batch_size=None)
818
+ assert u.math.shape(result) == (3,)
819
+
820
+ def test_callable_random_independent_per_batch(self):
821
+ """A random initializer draws independently for each batch element."""
822
+ rng = np.random.default_rng(0)
823
+ result = param(RandomInit(0.0, 1.0), 3, batch_size=4, rng=rng)
824
+ assert u.math.shape(result) == (4, 3)
825
+ arr = np.asarray(result)
826
+ # Independent draws => batch rows must not all be identical.
827
+ assert not np.allclose(arr[0], arr[1])
828
+
829
+ def test_callable_forwards_kwargs_with_batch_size(self):
830
+ """**param_kwargs still reach the callable when batching."""
831
+
832
+ class KwargsInit(Initialization):
833
+ def __call__(self, size, fill=None, **kwargs):
834
+ return np.full(size, fill)
835
+
836
+ result = param(KwargsInit(), 3, batch_size=4, fill=7.0)
837
+ assert u.math.shape(result) == (4, 3)
838
+ assert np.all(np.asarray(result) == 7.0)
839
+
840
+
744
841
  class TestPipeInitRejectsInitialization:
745
842
  """PipeInit cannot pipe values into an Initialization right operand (bug H3)."""
746
843
 
@@ -482,21 +482,27 @@ class TruncatedNormal(Initialization):
482
482
  lo = None if self.low is None else u.Quantity(self.low).to(unit).mantissa
483
483
  hi = None if self.high is None else u.Quantity(self.high).to(unit).mantissa
484
484
 
485
- if std == 0:
486
- # Degenerate distribution: all mass at the (clamped) mean.
487
- samples = jnp.full(size, mean, dtype=float)
488
- else:
489
- a = -jnp.inf if lo is None else (lo - mean) / std
490
- b = jnp.inf if hi is None else (hi - mean) / std
491
- # Inverse-CDF sampling: draw u ~ U(0, 1), map into the truncated CDF
492
- # interval [F(a), F(b)], then invert through the normal quantile.
493
- cdf_a = 0.0 if lo is None else ndtr(a)
494
- cdf_b = 1.0 if hi is None else ndtr(b)
495
- u_samples = rng.uniform(0.0, 1.0, size)
496
- p = cdf_a + u_samples * (cdf_b - cdf_a)
497
- # Keep the quantile finite (avoids +-inf at the open ends).
498
- p = jnp.clip(p, 1e-7, 1.0 - 1e-7)
499
- samples = mean + std * ndtri(p)
485
+ # Vectorized inverse-CDF sampling that also handles a degenerate (std == 0)
486
+ # distribution element-wise. A scalar ``if std == 0`` would raise
487
+ # ("truth value ambiguous") for array-valued ``std`` (a per-element
488
+ # parametrization that the sibling distributions accept), so instead use a
489
+ # "safe" denominator: where ``std == 0`` the term ``std * ndtri(p)`` is 0,
490
+ # leaving ``samples == mean`` (then clamped into ``[low, high]`` below).
491
+ mean_arr = jnp.asarray(mean, dtype=float)
492
+ std_arr = jnp.asarray(std, dtype=float)
493
+ safe_std = jnp.where(std_arr == 0, 1.0, std_arr)
494
+
495
+ a = -jnp.inf if lo is None else (lo - mean_arr) / safe_std
496
+ b = jnp.inf if hi is None else (hi - mean_arr) / safe_std
497
+ # Inverse-CDF sampling: draw u ~ U(0, 1), map into the truncated CDF
498
+ # interval [F(a), F(b)], then invert through the normal quantile.
499
+ cdf_a = 0.0 if lo is None else ndtr(a)
500
+ cdf_b = 1.0 if hi is None else ndtr(b)
501
+ u_samples = rng.uniform(0.0, 1.0, size)
502
+ p = cdf_a + u_samples * (cdf_b - cdf_a)
503
+ # Keep the quantile finite (avoids +-inf at the open ends).
504
+ p = jnp.clip(p, 1e-7, 1.0 - 1e-7)
505
+ samples = mean_arr + std_arr * ndtri(p)
500
506
 
501
507
  if lo is not None or hi is not None:
502
508
  samples = jnp.clip(