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.
- {braintools-0.2.0 → braintools-0.3.0}/PKG-INFO +1 -1
- {braintools-0.2.0 → braintools-0.3.0}/braintools/_version.py +1 -1
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/__init__.py +6 -4
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/context.py +14 -8
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/feature.py +1 -1
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/phase.py +20 -12
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/phase_test.py +60 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/task.py +27 -12
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/task_test.py +33 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/working_memory.py +7 -2
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/working_memory_test.py +14 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_base.py +29 -9
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_base_test.py +97 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_basic.py +21 -15
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_basic_test.py +58 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_orthogonal.py +15 -2
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_orthogonal_test.py +26 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_variance_scaling.py +10 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/__init__.py +2 -0
- braintools-0.3.0/braintools/metric/_audit_20260619_test.py +255 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_classification.py +15 -7
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_correlation.py +29 -28
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_correlation_test.py +3 -3
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_fenchel_young.py +10 -7
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_firings.py +45 -33
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_lfp.py +42 -3
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_lfp_test.py +8 -4
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_pairwise.py +22 -15
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_ranking.py +5 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_regression.py +19 -10
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_smoothing.py +6 -2
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_smoothing_test.py +3 -1
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_util.py +3 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_optax_lr_scheduler.py +45 -20
- {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_optax_optimizer.py +115 -15
- braintools-0.3.0/braintools/optim/_reaudit_fixes_test.py +269 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_state_uniquifier.py +84 -60
- {braintools-0.2.0 → braintools-0.3.0}/braintools/surrogate/_base.py +1 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/surrogate/_correctness_test.py +37 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/surrogate/_impl.py +44 -18
- {braintools-0.2.0 → braintools-0.3.0}/braintools/surrogate/_impl_extra_test.py +2 -2
- braintools-0.3.0/braintools/trainer/_audit_20260619_test.py +275 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_callbacks.py +31 -9
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_dataloader.py +7 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_distributed.py +38 -28
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_trainer.py +37 -4
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/__init__.py +2 -2
- braintools-0.3.0/braintools/visualize/_audit_20260619_test.py +356 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_interactive.py +9 -3
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_neural.py +35 -3
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_plots.py +63 -41
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_statistical.py +42 -14
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_three_d.py +20 -4
- {braintools-0.2.0 → braintools-0.3.0}/braintools.egg-info/PKG-INFO +1 -1
- {braintools-0.2.0 → braintools-0.3.0}/braintools.egg-info/SOURCES.txt +4 -0
- {braintools-0.2.0 → braintools-0.3.0}/LICENSE +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/README.md +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/__init__.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/_misc.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/_spike_encoder.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/_spike_encoder_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/_spike_operation.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/_spike_operation_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/_typing_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/_typing.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/_version.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/conditional.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/conditional_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/conftest.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/context_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/encoder.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/encoder_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/feature_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/label.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/label_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/task_variable_length_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/__init__.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/decision_making.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/decision_making_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/motor.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/motor_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/reasoning.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/tasks/reasoning_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/utils.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/cogtask/utils_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/__init__.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_base.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_base_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_biological.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_biological_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_compartment.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_compartment_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_kernel.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_kernel_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_random.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_random_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_regular.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_regular_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_spatial.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_spatial_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_topological.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/conn/_topological_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/file/__init__.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/file/_file_comprehensive_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/file/_matfile.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/file/_matfile_extra_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/file/_msg_checkpoint.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/file/_msg_checkpoint_async_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/file/_msg_checkpoint_extra_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/file/_msg_checkpoint_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/__init__.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_distance_base.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_distance_base_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_distance_impl.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_distance_impl_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_composite.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_composite_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_variance_scaling_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_with_distance.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/init/_init_with_distance_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/__init__.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_base.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_base_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_basic.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_basic_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_pulses.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_pulses_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_stochastic.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_stochastic_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_waveforms.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_composable_waveforms_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_deprecation.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_functional_basic.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_functional_basic_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_functional_pulses.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_functional_pulses_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_functional_stochastic.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_functional_stochastic_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_functional_waveforms.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/input/_functional_waveforms_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_classification_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_fenchel_young_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_firings_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_pairwise_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_ranking_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/metric/_regression_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/__init__.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_audit_fixes_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_base.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_nevergrad_optimizer.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_nevergrad_optimizer_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_optax_lr_scheduler_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_optax_optimizer_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_scipy_optimizer.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_scipy_optimizer_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_sofo_optimizer.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_sofo_optimizer_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/optim/_state_uniquifier_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/py.typed +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/__init__.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_dde_integrator.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_dde_integrator_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_imex_integrator.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_imex_integrator_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_ode_integrator.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_ode_integrator_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_quad_coverage_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_quad_regression_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_sde_integrator.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/quad/_sde_integrator_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/surrogate/__init__.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/surrogate/_base_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/surrogate/_impl_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/__init__.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_callbacks_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_checkpoint.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_checkpoint_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_dataloader_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_distributed_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_loggers.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_loggers_backends_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_loggers_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_module.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_module_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_progress.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_progress_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_trainer_audit_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_trainer_extra_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/trainer/_trainer_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/tree.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/tree_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_animation.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_animation_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_audit_fixes_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_colormaps.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_colormaps_extra_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_colormaps_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_figures.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_figures_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_interactive_extra_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_interactive_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_neural_extra_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_neural_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_plots_extra_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_plots_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_statistical_extra_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_statistical_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_three_d_extra_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_three_d_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools/visualize/_visualize_test.py +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools.egg-info/dependency_links.txt +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools.egg-info/requires.txt +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/braintools.egg-info/top_level.txt +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/pyproject.toml +0 -0
- {braintools-0.2.0 → braintools-0.3.0}/setup.cfg +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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,
|
|
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,
|
|
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 [
|
|
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
|
-
|
|
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
|
|
475
|
-
|
|
476
|
-
|
|
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,
|
|
93
|
-
... >> Response(100 * u.ms,
|
|
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*
|
|
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,
|
|
111
|
-
... stim = Feature(self.num_stimuli,
|
|
112
|
-
... return fix + stim, fix + Feature(2,
|
|
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
|
|
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=
|
|
636
|
-
...
|
|
637
|
-
...
|
|
638
|
-
...
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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(
|