braintrace 0.2.2__tar.gz → 0.2.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 (125) hide show
  1. {braintrace-0.2.2 → braintrace-0.2.3}/PKG-INFO +2 -1
  2. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/__init__.py +5 -3
  3. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/__init__.py +2 -0
  4. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/_common.py +20 -18
  5. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/base.py +8 -6
  6. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/d_rtrl.py +2 -0
  7. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/d_rtrl_test.py +103 -9
  8. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/e_prop.py +11 -8
  9. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/graph_executor.py +6 -4
  10. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/io_dim_vjp.py +16 -14
  11. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/ostl.py +5 -3
  12. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/osttp.py +10 -7
  13. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/otpe.py +18 -15
  14. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/ottt.py +16 -13
  15. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/param_dim_vjp.py +40 -132
  16. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/pp_prop.py +2 -0
  17. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/vjp_base.py +10 -8
  18. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/vjp_graph_executor.py +35 -33
  19. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compile.py +14 -12
  20. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/scenario_catalog_test.py +42 -0
  21. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_grad_exponential.py +4 -2
  22. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_input_data.py +10 -8
  23. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_legacy/_ops.py +1 -1
  24. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_legacy/_ops_test.py +2 -2
  25. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_misc.py +13 -11
  26. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_op/__init__.py +8 -0
  27. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_op/_primitive.py +34 -21
  28. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_op/_registries.py +71 -1
  29. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_op/_registries_test.py +12 -0
  30. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_op/conv.py +105 -26
  31. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_op/conv_test.py +45 -0
  32. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_op/dense.py +273 -42
  33. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_op/dense_test.py +207 -0
  34. braintrace-0.2.3/braintrace/_op/elemwise.py +421 -0
  35. braintrace-0.2.3/braintrace/_op/elemwise_test.py +394 -0
  36. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_op/lora.py +111 -25
  37. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_op/lora_test.py +35 -0
  38. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_op/op_rule_oracle_test.py +1 -1
  39. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_op/sparse.py +122 -32
  40. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_op/sparse_test.py +180 -16
  41. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_typing.py +7 -1
  42. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_version.py +1 -1
  43. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/nn/__init__.py +8 -4
  44. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/nn/_conv.py +6 -1
  45. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/nn/_linear.py +60 -19
  46. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/nn/_linear_test.py +225 -13
  47. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/nn/_readout.py +7 -5
  48. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/nn/_rnn.py +52 -50
  49. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace.egg-info/PKG-INFO +2 -1
  50. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace.egg-info/requires.txt +1 -0
  51. {braintrace-0.2.2 → braintrace-0.2.3}/pyproject.toml +43 -0
  52. braintrace-0.2.2/braintrace/_op/elemwise.py +0 -255
  53. braintrace-0.2.2/braintrace/_op/elemwise_test.py +0 -244
  54. {braintrace-0.2.2 → braintrace-0.2.3}/LICENSE +0 -0
  55. {braintrace-0.2.2 → braintrace-0.2.3}/README.md +0 -0
  56. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/__init___test.py +0 -0
  57. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/_common_test.py +0 -0
  58. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/base_test.py +0 -0
  59. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/e_prop_test.py +0 -0
  60. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/graph_executor_test.py +0 -0
  61. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/io_dim_vjp_test.py +0 -0
  62. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/oracle.py +0 -0
  63. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/oracle_models.py +0 -0
  64. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/oracle_test.py +0 -0
  65. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/ostl_test.py +0 -0
  66. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/osttp_test.py +0 -0
  67. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/otpe_test.py +0 -0
  68. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/ottt_test.py +0 -0
  69. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/param_dim_vjp_test.py +0 -0
  70. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/pp_prop_test.py +0 -0
  71. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/tests/approx_correctness_test.py +0 -0
  72. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/tests/conv_vmap_correctness_test.py +0 -0
  73. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/tests/cross_check_test.py +0 -0
  74. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/tests/diagnostic_exploration_test.py +0 -0
  75. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/tests/exact_correctness_test.py +0 -0
  76. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/tests/public_api_test.py +0 -0
  77. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/tests/scan_fusion_test.py +0 -0
  78. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/tests/transform_correctness_test.py +0 -0
  79. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/vjp_base_test.py +0 -0
  80. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_algorithm/vjp_graph_executor_test.py +0 -0
  81. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compatible_imports.py +0 -0
  82. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compatible_imports_test.py +0 -0
  83. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compile_test.py +0 -0
  84. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/__init__.py +0 -0
  85. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/base.py +0 -0
  86. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/base_test.py +0 -0
  87. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/diagnostics.py +0 -0
  88. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/diagnostics_test.py +0 -0
  89. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/graph.py +0 -0
  90. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/graph_test.py +0 -0
  91. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/hid_param_op.py +0 -0
  92. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/hid_param_op_test.py +0 -0
  93. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/hidden_group.py +0 -0
  94. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/hidden_group_test.py +0 -0
  95. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/hidden_pertubation.py +0 -0
  96. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/hidden_pertubation_test.py +0 -0
  97. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/model4test.py +0 -0
  98. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/module_info.py +0 -0
  99. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/module_info_test.py +0 -0
  100. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/report.py +0 -0
  101. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/report_test.py +0 -0
  102. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/scenario_catalog.py +0 -0
  103. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/tests/cell_relation_guardrail_test.py +0 -0
  104. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/tests/compiler_oracle_test.py +0 -0
  105. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_compiler/tests/compiler_property_test.py +0 -0
  106. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_docs_examples_test.py +0 -0
  107. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_etrace_model_test.py +0 -0
  108. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_grad_exponential_test.py +0 -0
  109. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_input_data_test.py +0 -0
  110. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_legacy/__init__.py +0 -0
  111. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_legacy/_params.py +0 -0
  112. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_legacy/_params_test.py +0 -0
  113. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_misc_test.py +0 -0
  114. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_op/_primitive_test.py +0 -0
  115. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_op/op_rule_oracle.py +0 -0
  116. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_state_managment.py +0 -0
  117. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/_state_managment_test.py +0 -0
  118. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/nn/_conv_test.py +0 -0
  119. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/nn/_readout_test.py +0 -0
  120. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/nn/_rnn_test.py +0 -0
  121. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace/py.typed +0 -0
  122. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace.egg-info/SOURCES.txt +0 -0
  123. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace.egg-info/dependency_links.txt +0 -0
  124. {braintrace-0.2.2 → braintrace-0.2.3}/braintrace.egg-info/top_level.txt +0 -0
  125. {braintrace-0.2.2 → braintrace-0.2.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: braintrace
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Enabling Scalable Online Learning for Brain Dynamics.
5
5
  Author-email: BrainTrace Developers <chao.brain@qq.com>
6
6
  License: Apache-2.0 license
@@ -28,6 +28,7 @@ Description-Content-Type: text/markdown
28
28
  License-File: LICENSE
29
29
  Requires-Dist: brainstate>=0.5.2
30
30
  Requires-Dist: brainunit
31
+ Requires-Dist: brainevent
31
32
  Requires-Dist: brainpy.state
32
33
  Requires-Dist: braintools
33
34
  Provides-Extra: cpu
@@ -66,7 +66,9 @@ Examples
66
66
  """
67
67
 
68
68
 
69
- from typing import TYPE_CHECKING
69
+ from __future__ import annotations
70
+
71
+ from typing import TYPE_CHECKING, Any
70
72
 
71
73
  from . import nn
72
74
  from ._compile import compile
@@ -244,7 +246,7 @@ _DEPRECATED_LEGACY = {
244
246
  }
245
247
 
246
248
 
247
- def __getattr__(name):
249
+ def __getattr__(name: str) -> Any:
248
250
  if name in _DEPRECATED_LEGACY:
249
251
  import warnings
250
252
  warnings.warn(
@@ -258,5 +260,5 @@ def __getattr__(name):
258
260
  raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
259
261
 
260
262
 
261
- def __dir__():
263
+ def __dir__() -> list[str]:
262
264
  return sorted(list(__all__) + list(_DEPRECATED_LEGACY))
@@ -21,6 +21,8 @@ and paper-faithful SNN algorithms (EProp, OSTLRecurrent, OSTLFeedforward, OTPE,
21
21
  OTTT, OSTTP).
22
22
  """
23
23
 
24
+ from __future__ import annotations
25
+
24
26
  from ._common import FixedRandomFeedback, KappaFilter, PresynapticTrace
25
27
  from .base import ETraceAlgorithm, EligibilityTrace
26
28
  from .d_rtrl import D_RTRL
@@ -15,6 +15,8 @@
15
15
 
16
16
  """Shared helpers for the SNN online-learning algorithms."""
17
17
 
18
+ from __future__ import annotations
19
+
18
20
  from functools import partial
19
21
  from typing import Any, Dict, Optional
20
22
 
@@ -23,7 +25,7 @@ import jax
23
25
  import jax.numpy as jnp
24
26
  import brainunit as u
25
27
 
26
- from braintrace._typing import PyTree
28
+ from braintrace._typing import ArrayLike, PyTree
27
29
 
28
30
  __all__ = [
29
31
  'PresynapticTrace',
@@ -42,12 +44,12 @@ class _ZeroResetState(brainstate.ShortTermState):
42
44
  for ``batch_size`` (or prepended for scalar-shaped states).
43
45
  """
44
46
 
45
- def __init__(self, init_value):
47
+ def __init__(self, init_value: Any) -> None:
46
48
  super().__init__(init_value)
47
49
  self._init_shape = jnp.shape(init_value)
48
50
  self._init_dtype = init_value.dtype
49
51
 
50
- def reset_state(self, batch_size: Optional[int] = None, **kwargs):
52
+ def reset_state(self, batch_size: Optional[int] = None, **kwargs: Any) -> None:
51
53
  """Re-zero the state at its original shape.
52
54
 
53
55
  Parameters
@@ -105,13 +107,13 @@ class PresynapticTrace(_ZeroResetState):
105
107
 
106
108
  __module__ = 'braintrace'
107
109
 
108
- def __init__(self, init_value, leak: float):
110
+ def __init__(self, init_value: Any, leak: float) -> None:
109
111
  super().__init__(init_value)
110
112
  if not (0.0 < leak < 1.0):
111
113
  raise ValueError(f'leak must be in (0, 1); got {leak}')
112
114
  self.leak = float(leak)
113
115
 
114
- def update(self, x):
116
+ def update(self, x: ArrayLike) -> Any:
115
117
  r"""Apply one accumulation step :math:`\hat{a} \leftarrow \lambda \cdot \hat{a} + x`.
116
118
 
117
119
  Parameters
@@ -163,13 +165,13 @@ class KappaFilter(_ZeroResetState):
163
165
 
164
166
  __module__ = 'braintrace'
165
167
 
166
- def __init__(self, init_value, kappa: float):
168
+ def __init__(self, init_value: Any, kappa: float) -> None:
167
169
  super().__init__(init_value)
168
170
  if not (0.0 <= kappa < 1.0):
169
171
  raise ValueError(f'kappa must be in [0, 1); got {kappa}')
170
172
  self.kappa = float(kappa)
171
173
 
172
- def update(self, x):
174
+ def update(self, x: ArrayLike) -> Any:
173
175
  r"""Apply one low-pass step :math:`x_{\mathrm{filt}} \leftarrow (1-\kappa) x + \kappa\, x_{\mathrm{filt}}`.
174
176
 
175
177
  Parameters
@@ -230,14 +232,14 @@ class FixedRandomFeedback:
230
232
 
231
233
  __module__ = 'braintrace'
232
234
 
233
- def __init__(self, n_target: int, n_layer: int, key, init_scale: float = 0.1):
235
+ def __init__(self, n_target: int, n_layer: int, key: Any, init_scale: float = 0.1) -> None:
234
236
  self.B = jax.lax.stop_gradient(
235
237
  init_scale * jax.random.normal(key, (n_target, n_layer))
236
238
  )
237
239
  self.n_target = int(n_target)
238
240
  self.n_layer = int(n_layer)
239
241
 
240
- def project(self, y_target):
242
+ def project(self, y_target: Any) -> Any:
241
243
  """Project the target onto the frozen feedback matrix.
242
244
 
243
245
  Parameters
@@ -267,7 +269,7 @@ def extract_y_target(args: tuple, *, index: int = -1) -> Optional[jax.Array]:
267
269
  def _reset_state_in_a_dict(
268
270
  state_dict: Dict[Any, brainstate.State],
269
271
  batch_size: Optional[int],
270
- ):
272
+ ) -> None:
271
273
  """
272
274
  Reset the values in a dictionary of states to zero.
273
275
 
@@ -293,7 +295,7 @@ def _reset_state_in_a_dict(
293
295
  def _zeros_like_batch_or_not(
294
296
  batch_size: Optional[int],
295
297
  x: jax.Array
296
- ):
298
+ ) -> Any:
297
299
  """
298
300
  Create a zeros array with the same shape and type as the input array,
299
301
  optionally including a batch dimension.
@@ -325,7 +327,7 @@ def _batched_zeros_like(
325
327
  batch_size: Optional[int],
326
328
  num_state: int, # the number of hidden states
327
329
  x: jax.Array # the input array
328
- ):
330
+ ) -> Any:
329
331
  """
330
332
  Create a batched zeros array with the same shape as the input array,
331
333
  extended by the number of hidden states.
@@ -354,7 +356,7 @@ def _batched_zeros_like(
354
356
  return u.math.zeros((batch_size, *x.shape, num_state), x.dtype)
355
357
 
356
358
 
357
- def _sum_dim(xs: jax.Array, axis: int = -1):
359
+ def _sum_dim(xs: jax.Array, axis: int = -1) -> Any:
358
360
  """
359
361
  Sums the elements along the last dimension of each array in a PyTree.
360
362
 
@@ -373,7 +375,7 @@ def _sum_dim(xs: jax.Array, axis: int = -1):
373
375
  return jax.tree.map(lambda x: u.math.sum(x, axis=axis), xs)
374
376
 
375
377
 
376
- def _unit_safe_add(a, b):
378
+ def _unit_safe_add(a: Any, b: Any) -> Any:
377
379
  """Add two leaves, stripping units only when one side has units and the other does not.
378
380
 
379
381
  Gradient contributions for the same weight may come from paths that
@@ -390,7 +392,7 @@ def _unit_safe_add(a, b):
390
392
  return u.math.add(a, b)
391
393
 
392
394
 
393
- def _extract_leaf(pytree_val: PyTree, leaf_idx: int):
395
+ def _extract_leaf(pytree_val: PyTree, leaf_idx: int) -> Any:
394
396
  """Return the leaf at ``leaf_idx`` in ``jax.tree.leaves(pytree_val)``.
395
397
 
396
398
  Bare arrays (treedef with a single leaf) return the array unchanged.
@@ -409,7 +411,7 @@ def _extract_leaf(pytree_val: PyTree, leaf_idx: int):
409
411
  def _wrap_leaves_as_pytree(
410
412
  reference_pytree: PyTree,
411
413
  leaf_grads: Dict[int, jax.Array],
412
- ):
414
+ ) -> Any:
413
415
  """Build a pytree matching ``reference_pytree`` with ``leaf_grads``
414
416
  inserted at the given leaf indices; any other leaf is zero-filled.
415
417
 
@@ -441,7 +443,7 @@ def _wrap_leaves_as_pytree(
441
443
 
442
444
 
443
445
  def _route_grads_by_path(
444
- relation,
446
+ relation: Any,
445
447
  per_key_grads: Dict[str, jax.Array],
446
448
  weight_vals: Dict[Any, PyTree],
447
449
  target_dict: Dict[Any, PyTree],
@@ -478,7 +480,7 @@ def _update_dict(
478
480
  key: Any,
479
481
  value: PyTree,
480
482
  error_when_no_key: Optional[bool] = False
481
- ):
483
+ ) -> None:
482
484
  """Update the dictionary.
483
485
 
484
486
  If the key exists, then add the value to the existing value.
@@ -19,6 +19,8 @@
19
19
 
20
20
  # -*- coding: utf-8 -*-
21
21
 
22
+ from __future__ import annotations
23
+
22
24
  from typing import Dict, Any, Optional
23
25
 
24
26
  import brainstate
@@ -89,7 +91,7 @@ class ETraceAlgorithm(brainstate.nn.Module):
89
91
  model: brainstate.nn.Module,
90
92
  graph_executor: ETraceGraphExecutor,
91
93
  name: Optional[str] = None,
92
- ):
94
+ ) -> None:
93
95
  super().__init__(name=name) # type: ignore[call-arg] # brainstate hides Module.__init__ from type checkers
94
96
 
95
97
  # the model
@@ -210,7 +212,7 @@ class ETraceAlgorithm(brainstate.nn.Module):
210
212
  assert self._other_states is not None
211
213
  return self._other_states
212
214
 
213
- def _split_state(self):
215
+ def _split_state(self) -> None:
214
216
  # --- the state separation --- #
215
217
  #
216
218
  # [NOTE]
@@ -228,7 +230,7 @@ class ETraceAlgorithm(brainstate.nn.Module):
228
230
  self._other_states
229
231
  ) = self.graph.module_info.retrieved_model_states.split(brainstate.ParamState, brainstate.HiddenState, ...)
230
232
 
231
- def compile_graph(self, *args) -> None:
233
+ def compile_graph(self, *args: Any) -> None:
232
234
  r"""
233
235
  Compile the eligibility trace graph of the relationship between etrace weights, states and operators.
234
236
 
@@ -318,7 +320,7 @@ class ETraceAlgorithm(brainstate.nn.Module):
318
320
  """
319
321
  return self.graph_executor.show_graph(verbose=verbose, return_msg=return_msg)
320
322
 
321
- def __call__(self, *args) -> Any:
323
+ def __call__(self, *args: Any) -> Any:
322
324
  """
323
325
  Update the model and the eligibility trace states.
324
326
 
@@ -334,7 +336,7 @@ class ETraceAlgorithm(brainstate.nn.Module):
334
336
  """
335
337
  return self.update(*args)
336
338
 
337
- def update(self, *args) -> Any:
339
+ def update(self, *args: Any) -> Any:
338
340
  """
339
341
  Update the model and the eligibility trace states.
340
342
 
@@ -355,7 +357,7 @@ class ETraceAlgorithm(brainstate.nn.Module):
355
357
  """
356
358
  raise NotImplementedError
357
359
 
358
- def init_etrace_state(self, *args, **kwargs) -> None:
360
+ def init_etrace_state(self, *args: Any, **kwargs: Any) -> None:
359
361
  """
360
362
  Initialize the eligibility trace states of the etrace algorithm.
361
363
 
@@ -13,6 +13,8 @@
13
13
  # limitations under the License.
14
14
  # ==============================================================================
15
15
 
16
+ from __future__ import annotations
17
+
16
18
  from .param_dim_vjp import ParamDimVjpAlgorithm
17
19
 
18
20
  __all__ = [
@@ -25,7 +25,6 @@ from braintrace._algorithm.d_rtrl import D_RTRL
25
25
  from braintrace._algorithm.param_dim_vjp import (
26
26
  ParamDimVjpAlgorithm,
27
27
  _remove_units,
28
- _fast_solve_contract,
29
28
  )
30
29
  from braintrace._etrace_model_test import (
31
30
  IF_Delta_Dense_Layer,
@@ -39,7 +38,7 @@ from braintrace._etrace_model_test import (
39
38
  ALIF_STDExpCu_Dense_Layer,
40
39
  ALIF_STPExpCu_Dense_Layer,
41
40
  )
42
- from braintrace._op import etp_mm_p
41
+ from braintrace._op import etp_mm_p, get_fast_path_rules
43
42
 
44
43
 
45
44
  # ---------------------------------------------------------------------------
@@ -657,7 +656,7 @@ class TestDRtrlDictTraceStorage:
657
656
  # Helpers for the trace-update specialization + dtype-knob tests
658
657
  # ---------------------------------------------------------------------------
659
658
 
660
- def _rnn_mm(seed=0, n=4, bias=False):
659
+ def _rnn_mm(seed=0, n=4, bias=False, weight_fn=None, bias_fn=None):
661
660
  """Batched single-recurrent-weight tanh RNN -> etp_mm_p, num_state==1."""
662
661
 
663
662
  class Net(brainstate.nn.Module):
@@ -670,7 +669,10 @@ def _rnn_mm(seed=0, n=4, bias=False):
670
669
 
671
670
  def update(self, x):
672
671
  b = self.b.value if self.b is not None else None
673
- self.h.value = jax.nn.tanh(braintrace.matmul(self.h.value + x, self.w.value, b))
672
+ self.h.value = jax.nn.tanh(braintrace.matmul(
673
+ self.h.value + x, self.w.value, b,
674
+ weight_fn=weight_fn, bias_fn=bias_fn,
675
+ ))
674
676
  return self.h.value
675
677
 
676
678
  net = Net()
@@ -678,7 +680,7 @@ def _rnn_mm(seed=0, n=4, bias=False):
678
680
  return net
679
681
 
680
682
 
681
- def _rnn_mv(seed=0, n=4):
683
+ def _rnn_mv(seed=0, n=4, weight_fn=None):
682
684
  """Unbatched single-recurrent-weight tanh RNN -> etp_mv_p, num_state==1."""
683
685
 
684
686
  class Net(brainstate.nn.Module):
@@ -690,7 +692,9 @@ def _rnn_mv(seed=0, n=4):
690
692
  self.h = brainstate.HiddenState(jnp.zeros((n,)))
691
693
 
692
694
  def update(self, x):
693
- self.h.value = jax.nn.tanh(braintrace.matmul(self.h.value + x, self.w.value))
695
+ self.h.value = jax.nn.tanh(braintrace.matmul(
696
+ self.h.value + x, self.w.value, weight_fn=weight_fn,
697
+ ))
694
698
  return self.h.value
695
699
 
696
700
  net = Net()
@@ -698,6 +702,27 @@ def _rnn_mv(seed=0, n=4):
698
702
  return net
699
703
 
700
704
 
705
+ def _rnn_elemwise(seed=0, n=4, weight_fn=None):
706
+ """Single element-wise-weight tanh RNN -> etp_elemwise_p, num_state==1."""
707
+
708
+ class Net(brainstate.nn.Module):
709
+ def __init__(self):
710
+ super().__init__()
711
+ self.alpha = brainstate.ParamState(
712
+ 0.5 * jax.random.normal(jax.random.PRNGKey(seed), (n,))
713
+ )
714
+ self.h = brainstate.HiddenState(jnp.zeros((1, n)))
715
+
716
+ def update(self, x):
717
+ a = braintrace.element_wise(self.alpha.value, weight_fn=weight_fn)
718
+ self.h.value = jax.nn.tanh(a * (self.h.value + x))
719
+ return self.h.value
720
+
721
+ net = Net()
722
+ brainstate.nn.init_all_states(net, batch_size=1)
723
+ return net
724
+
725
+
701
726
  def _run_updates(algo, xs):
702
727
  """Compile, init, run len(xs) eager updates; return etrace_bwg snapshot.
703
728
 
@@ -773,6 +798,73 @@ class TestS1RecurrentBroadcastEqualsLegacy:
773
798
  _assert_etrace_lists_equal(fast, legacy, exact=True)
774
799
 
775
800
 
801
+ # ---------------------------------------------------------------------------
802
+ # weight_fn/bias_fn — the fast closed-form trace must equal the rule (legacy)
803
+ # path when a transform hook is present.
804
+ #
805
+ # The fast kernels compute the instant term as the outer product x ⊗ df, which
806
+ # equals ∂h/∂V (the *transformed* weight). With a transform present the true
807
+ # eligibility trace is ∂h/∂W_raw = (x ⊗ df) ⊙ f'(W); the f' factor is supplied
808
+ # only by the rule path's ``xy_to_dw`` (via jax.vjp). So the fast path must fall
809
+ # back to the rules whenever a ``*_fn`` hook is set, otherwise the trace is the
810
+ # gradient w.r.t. the transformed weight and silently drops f'(W).
811
+ # ---------------------------------------------------------------------------
812
+
813
+ class TestTransformFastEqualsLegacy:
814
+ """With a weight_fn/bias_fn hook, the default (fast) trace must match the
815
+ rule path bit-for-bit — i.e. the fast path falls back to the rules so the
816
+ f'(W) factor from ``xy_to_dw`` is not dropped.
817
+
818
+ Run several steps so the recurrent term (non-zero from the 2nd update) is
819
+ exercised, not just the instant term."""
820
+
821
+ def _xs_mm(self, n=4, steps=4):
822
+ return brainstate.random.randn(steps, 1, n)
823
+
824
+ def _xs_mv(self, n=4, steps=4):
825
+ return brainstate.random.randn(steps, n)
826
+
827
+ def test_mm_weight_fn_fast_equals_legacy(self):
828
+ xs = self._xs_mm()
829
+ wfn = lambda w: w ** 2
830
+ fast = _run_updates(ParamDimVjpAlgorithm(_rnn_mm(weight_fn=wfn), fast_solve=True), xs)
831
+ legacy = _run_updates(ParamDimVjpAlgorithm(_rnn_mm(weight_fn=wfn), fast_solve=False), xs)
832
+ _assert_etrace_lists_equal(fast, legacy, exact=True)
833
+
834
+ def test_mm_weight_fn_and_bias_fn_fast_equals_legacy(self):
835
+ xs = self._xs_mm()
836
+ fast = _run_updates(
837
+ ParamDimVjpAlgorithm(
838
+ _rnn_mm(bias=True, weight_fn=jnp.tanh, bias_fn=lambda b: b * 2.0),
839
+ fast_solve=True), xs)
840
+ legacy = _run_updates(
841
+ ParamDimVjpAlgorithm(
842
+ _rnn_mm(bias=True, weight_fn=jnp.tanh, bias_fn=lambda b: b * 2.0),
843
+ fast_solve=False), xs)
844
+ _assert_etrace_lists_equal(fast, legacy, exact=True)
845
+
846
+ def test_mv_weight_fn_fast_equals_legacy(self):
847
+ xs = self._xs_mv()
848
+ fast = _run_updates(ParamDimVjpAlgorithm(_rnn_mv(weight_fn=jnp.tanh), fast_solve=True), xs)
849
+ legacy = _run_updates(ParamDimVjpAlgorithm(_rnn_mv(weight_fn=jnp.tanh), fast_solve=False), xs)
850
+ _assert_etrace_lists_equal(fast, legacy, exact=True)
851
+
852
+ def test_elemwise_weight_fn_fast_equals_legacy(self):
853
+ xs = self._xs_mm()
854
+ wfn = jnp.tanh
855
+ fast = _run_updates(ParamDimVjpAlgorithm(_rnn_elemwise(weight_fn=wfn), fast_solve=True), xs)
856
+ legacy = _run_updates(ParamDimVjpAlgorithm(_rnn_elemwise(weight_fn=wfn), fast_solve=False), xs)
857
+ _assert_etrace_lists_equal(fast, legacy, exact=True)
858
+
859
+ def test_no_transform_still_uses_fast_path(self):
860
+ """Guard: without a transform, fast and legacy remain bit-identical (the
861
+ gate must not disable the fast path when no ``*_fn`` hook is present)."""
862
+ xs = self._xs_mm()
863
+ fast = _run_updates(ParamDimVjpAlgorithm(_rnn_mm(), fast_solve=True), xs)
864
+ legacy = _run_updates(ParamDimVjpAlgorithm(_rnn_mm(), fast_solve=False), xs)
865
+ _assert_etrace_lists_equal(fast, legacy, exact=True)
866
+
867
+
776
868
  # ---------------------------------------------------------------------------
777
869
  # Change A — trace_dtype knob (store the eligibility trace in lower precision)
778
870
  # ---------------------------------------------------------------------------
@@ -838,7 +930,8 @@ class TestTraceDtypeKnob:
838
930
 
839
931
  def _alif(n_in=4, n_rec=5):
840
932
  """Adaptive-LIF dense layer -> coupled (V, adaptation) hidden group with
841
- num_state==2, exercising the S>1 einsum branch of _fast_recurrent_term."""
933
+ num_state==2, exercising the S>1 einsum branch of the dense fast-path
934
+ recurrent rule (``FastPathRules.recurrent``)."""
842
935
  model = ALIF_Delta_Dense_Layer(n_in, n_rec)
843
936
  brainstate.nn.init_all_states(model)
844
937
  return model
@@ -891,11 +984,12 @@ class TestSolveBatchFold:
891
984
  'bias': brainstate.random.randn(B, O, S),
892
985
  }
893
986
  # reference: current per-batch contraction, then explicit batch sum
894
- ref = _fast_solve_contract(etp_mm_p, diag_like, etrace)
987
+ solve = get_fast_path_rules(etp_mm_p).solve
988
+ ref = solve(diag_like, etrace, fold_batch=False)
895
989
  ref_w = ref['weight'].sum(axis=0) # (I, O)
896
990
  ref_b = ref['bias'].sum(axis=0) # (O,)
897
991
  # new: fold the batch axis inside the einsum
898
- folded = _fast_solve_contract(etp_mm_p, diag_like, etrace, fold_batch=True)
992
+ folded = solve(diag_like, etrace, fold_batch=True)
899
993
  assert folded['weight'].shape == (I, O)
900
994
  assert folded['bias'].shape == (O,)
901
995
  npt.assert_allclose(folded['weight'], ref_w, atol=1e-6)
@@ -29,7 +29,9 @@ that make the rule biologically plausible:
29
29
  See :class:`EProp` for the mathematical formulation, references, and an example.
30
30
  """
31
31
 
32
- from typing import Dict, Optional
32
+ from __future__ import annotations
33
+
34
+ from typing import Any, Dict, Optional
33
35
 
34
36
  import brainstate
35
37
  import jax
@@ -158,8 +160,8 @@ class EProp(ParamDimVjpAlgorithm):
158
160
  name: Optional[str] = None,
159
161
  vjp_method: str = 'single-step',
160
162
  fast_solve: bool = True,
161
- **kwargs,
162
- ):
163
+ **kwargs: Any,
164
+ ) -> None:
163
165
  if feedback not in ('symmetric', 'random'):
164
166
  raise ValueError(
165
167
  f"feedback must be 'symmetric' or 'random'; got {feedback!r}"
@@ -181,7 +183,7 @@ class EProp(ParamDimVjpAlgorithm):
181
183
  self._kappa_filters: Dict[int, KappaFilter] = {}
182
184
  self._random_feedback: Dict[int, FixedRandomFeedback] = {}
183
185
 
184
- def init_etrace_state(self, *args, **kwargs):
186
+ def init_etrace_state(self, *args: Any, **kwargs: Any) -> None:
185
187
  super().init_etrace_state(*args, **kwargs)
186
188
  self._kappa_filters = {}
187
189
  self._random_feedback = {}
@@ -197,6 +199,7 @@ class EProp(ParamDimVjpAlgorithm):
197
199
  )
198
200
  if self.feedback == 'random':
199
201
  key = self._random_feedback_key
202
+ assert key is not None # constructor enforces a key when feedback='random'
200
203
  for rel in self.graph.hidden_param_op_relations:
201
204
  for group in rel.hidden_groups:
202
205
  gid = group.index
@@ -209,17 +212,17 @@ class EProp(ParamDimVjpAlgorithm):
209
212
  n_target=n_layer, n_layer=n_layer, key=sub, init_scale=0.1
210
213
  )
211
214
 
212
- def reset_state(self, batch_size: Optional[int] = None, **kwargs):
215
+ def reset_state(self, batch_size: Optional[int] = None, **kwargs: Any) -> None:
213
216
  super().reset_state(batch_size=batch_size, **kwargs)
214
217
  for flt in self._kappa_filters.values():
215
218
  flt.reset_state(batch_size=batch_size)
216
219
 
217
- def _compute_learning_signal(self, dl_autodiff, args):
220
+ def _compute_learning_signal(self, dl_autodiff: Any, args: Any) -> Any:
218
221
  signals = list(dl_autodiff)
219
222
  if self.feedback == 'random' and self._random_feedback:
220
223
  # dl_autodiff[g].shape == (*varshape, num_state). Project over the
221
224
  # trailing n_layer axis (-2), preserving num_state on the tail.
222
- def _project(B, s):
225
+ def _project(B: Any, s: Any) -> Any:
223
226
  return jnp.einsum('...lj,lk->...kj', s, B)
224
227
 
225
228
  signals = [
@@ -231,7 +234,7 @@ class EProp(ParamDimVjpAlgorithm):
231
234
  # KappaFilter state carries varshape, but signal has an extra
232
235
  # trailing num_state axis. Collapse num_state for filter purposes;
233
236
  # broadcast the filtered value back.
234
- def _filter(flt, s):
237
+ def _filter(flt: Any, s: Any) -> Any:
235
238
  # collapse num_state tail: sum over last axis produces shape (*varshape,)
236
239
  collapsed = s.sum(axis=-1)
237
240
  filtered = flt.update(collapsed)
@@ -35,6 +35,8 @@
35
35
 
36
36
  # -*- coding: utf-8 -*-
37
37
 
38
+ from __future__ import annotations
39
+
38
40
  from typing import Dict, Any, Optional
39
41
 
40
42
  import brainstate
@@ -70,7 +72,7 @@ class ETraceGraphExecutor:
70
72
  self,
71
73
  model: brainstate.nn.Module,
72
74
  include_recurrent_mixing: bool = False,
73
- ):
75
+ ) -> None:
74
76
  # The original model
75
77
  if not isinstance(model, brainstate.nn.Module):
76
78
  raise TypeError(
@@ -152,7 +154,7 @@ class ETraceGraphExecutor:
152
154
  self._state_id_to_path = {id(state): path for path, state in self.states.items()}
153
155
  return self._state_id_to_path
154
156
 
155
- def compile_graph(self, *args) -> None:
157
+ def compile_graph(self, *args: Any) -> None:
156
158
  r"""
157
159
  Build the eligibility trace graph for the model based on the provided inputs.
158
160
 
@@ -217,7 +219,7 @@ class ETraceGraphExecutor:
217
219
 
218
220
  def solve_h2w_h2h_jacobian(
219
221
  self,
220
- *args,
222
+ *args: Any,
221
223
  ) -> Any:
222
224
  r"""
223
225
  Compute the hidden-to-weight and hidden-to-hidden Jacobian matrices.
@@ -262,7 +264,7 @@ class ETraceGraphExecutor:
262
264
  'implemented in the subclass.')
263
265
 
264
266
  def solve_h2w_h2h_l2h_jacobian(
265
- self, *args,
267
+ self, *args: Any,
266
268
  ) -> Any:
267
269
  r"""
268
270
  Compute the hidden-to-weight and hidden-to-hidden Jacobian matrices, along with the VJP transformed