Skip to content

Commit a5e75b2

Browse files
authored
fix(docs,inputs,rates): repair docs notebooks + Hz-input & RNNCell.reset(batch_size) bugs (#857)
fix(docs,inputs,rates): repair docs notebooks + Hz-input and RNNCell.reset(batch_size) bugs Re-ran every notebook under docs/ in a memory-capped, sequential sandbox. Most failures were an artifact of executing against a stale *installed* brainpy build; the repo source already fixes them. The remainder were genuine notebook API-drift and two real brainpy bugs. Notebooks (re-executed green against the repo source): - tutorial_math/array, Dedicated_Operators: wrap computed / random arrays in bm.Array so the documented in-place assignment works (bm.random.* and Array arithmetic now return immutable jax arrays). - tutorial_math/control_flows: while_loop body returns a tuple matching operands. - tutorial_math/variables: RandomState.split_keys -> split_key. - tutorial_FAQs/gotchas_of_brainpy_transforms: tag the intentional "State as a transform argument" gotcha raises-exception; the "this works" example now passes a plain array instead of a State. - tutorial_training/esn_introduction: bp.layers.Reservoir -> bp.dyn.Reservoir. - tutorial_training/build_training_models: bp.layers.{LSTMCell,NVAR} -> bp.dyn.*, fix the DeepRNN.update layer chain, and rewrite the SNN example to the bp.dyn projection API (HalfProjAlignPost / Expon / CUBA / LifRef / Leaky). - tutorial_advanced/interoperation: wrap b in bm.Array so b.value works. brainpy fixes (with reproducing tests): - inputs.sinusoidal_input / square_input: attach Hz (frequency) and ms (time) units before delegating to braintools, which now rejects bare numbers. - dyn.rates RNNCell/GRUCell/LSTMCell/ConvLSTM reset_state: accept batch_size as an alias for batch_or_mode, matching the canonical model.reset(batch_size=...) convention used by brainpy.dyn neurons.
1 parent 01aa0c8 commit a5e75b2

13 files changed

Lines changed: 13500 additions & 1558 deletions

File tree

brainpy/dyn/rates/rnncells.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ def __init__(
124124
self.state[:] = self.state2train
125125

126126
def reset_state(self, batch_or_mode=None, **kwargs):
127+
# Accept ``batch_size`` as an alias for ``batch_or_mode`` so the canonical
128+
# ``model.reset(batch_size=...)`` convention (used by ``brainpy.dyn``
129+
# neurons and the training tutorials) works on recurrent cells too.
130+
if batch_or_mode is None:
131+
batch_or_mode = kwargs.get('batch_size', None)
127132
self.state.value = variable(self._state_initializer, batch_or_mode, self.num_out)
128133
if self.train_state:
129134
self.state2train.value = parameter(self._state_initializer, self.num_out, allow_none=False)
@@ -236,6 +241,11 @@ def __init__(
236241
self.state[:] = self.state2train
237242

238243
def reset_state(self, batch_or_mode=None, **kwargs):
244+
# Accept ``batch_size`` as an alias for ``batch_or_mode`` so the canonical
245+
# ``model.reset(batch_size=...)`` convention (used by ``brainpy.dyn``
246+
# neurons and the training tutorials) works on recurrent cells too.
247+
if batch_or_mode is None:
248+
batch_or_mode = kwargs.get('batch_size', None)
239249
self.state.value = variable(self._state_initializer, batch_or_mode, self.num_out)
240250
if self.train_state:
241251
self.state2train.value = parameter(self._state_initializer, self.num_out, allow_none=False)
@@ -371,6 +381,8 @@ def __init__(
371381
self.state[:] = self.state2train
372382

373383
def reset_state(self, batch_or_mode=None, **kwargs):
384+
if batch_or_mode is None:
385+
batch_or_mode = kwargs.get('batch_size', None)
374386
self.state.value = variable(self._state_initializer, batch_or_mode, self.num_out * 2)
375387
if self.train_state:
376388
self.state2train.value = parameter(self._state_initializer, self.num_out * 2, allow_none=False)
@@ -522,6 +534,8 @@ def __init__(
522534
self.reset_state()
523535

524536
def reset_state(self, batch_or_mode: int = 1, **kwargs):
537+
if 'batch_size' in kwargs and kwargs['batch_size'] is not None:
538+
batch_or_mode = kwargs['batch_size']
525539
if self.mode.is_a(bm.NonBatchingMode):
526540
shape = self.input_shape + (self.out_channels,)
527541
self.h = variable_(self._state_initializer, shape)

brainpy/dyn/rates/rnncells_test.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,28 @@ def test_rnn_cells_reset_state_int_batch(self, cls):
202202
expected = (2, 8) if cls == 'LSTMCell' else (2, 4)
203203
self.assertTupleEqual(tuple(cell.state.shape), expected)
204204

205+
@parameterized.product(cls=['RNNCell', 'GRUCell', 'LSTMCell'])
206+
def test_rnn_cells_reset_batch_size_kwarg(self, cls):
207+
# Regression (build_training_models tutorial): ``reset(batch_size=...)``
208+
# — the canonical convention shared with ``brainpy.dyn`` neurons — must
209+
# add a leading batch axis on recurrent cells. Previously the cells only
210+
# understood ``batch_or_mode`` and silently dropped ``batch_size``,
211+
# resetting the state to an unbatched shape and raising a MathError.
212+
bm.random.seed()
213+
cell = getattr(bp.dyn, cls)(num_in=3, num_out=4, mode=bm.batching_mode)
214+
cell.reset(batch_size=5)
215+
expected = (5, 8) if cls == 'LSTMCell' else (5, 4)
216+
self.assertTupleEqual(tuple(cell.state.shape), expected)
217+
218+
@parameterized.product(cls=['RNNCell', 'GRUCell', 'LSTMCell'])
219+
def test_rnn_cells_reset_batch_or_mode_kwarg_still_works(self, cls):
220+
# Back-compat: the original ``batch_or_mode`` keyword must keep working.
221+
bm.random.seed()
222+
cell = getattr(bp.dyn, cls)(num_in=3, num_out=4, mode=bm.batching_mode)
223+
cell.reset_state(batch_or_mode=3)
224+
expected = (3, 8) if cls == 'LSTMCell' else (3, 4)
225+
self.assertTupleEqual(tuple(cell.state.shape), expected)
226+
205227

206228
if __name__ == '__main__':
207229
absltest.main()

brainpy/inputs/currents.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import brainstate
1919
import braintools
20+
import brainunit as u
2021

2122
import brainpy.math
2223

@@ -35,6 +36,31 @@
3536
]
3637

3738

39+
def _as_hz(frequency):
40+
"""Attach ``Hz`` units to a bare numeric frequency.
41+
42+
``braintools.input.sinusoidal`` / ``braintools.input.square`` require the
43+
frequency to carry frequency (``Hz``) units; a plain number is treated as
44+
dimensionless and rejected. A value that is already a
45+
:class:`brainunit.Quantity` is returned unchanged.
46+
"""
47+
return frequency if isinstance(frequency, u.Quantity) else frequency * u.Hz
48+
49+
50+
def _as_ms(value):
51+
"""Attach ``ms`` units to a bare numeric time value.
52+
53+
Once ``dt`` carries time units the waveform helpers convert the oscillation
54+
frequency against that time unit, so the remaining time arguments
55+
(``dt``/``duration``/``t_start``/``t_end``) must be unit-carrying as well.
56+
``None`` passes through and existing :class:`brainunit.Quantity` values are
57+
left untouched.
58+
"""
59+
if value is None:
60+
return None
61+
return value if isinstance(value, u.Quantity) else value * u.ms
62+
63+
3864
def section_input(values, durations, dt=None, return_length=False):
3965
"""Format an input current with different sections.
4066
@@ -279,8 +305,10 @@ def sinusoidal_input(amplitude, frequency, duration, dt=None, t_start=0., t_end=
279305
Whether the sinusoid oscillates around 0 (False), or
280306
has a positive DC bias, thus non-negative (True).
281307
"""
282-
with brainstate.environ.context(dt=brainpy.math.get_dt() if dt is None else dt):
283-
return braintools.input.sinusoidal(amplitude, frequency, duration, t_start=t_start, t_end=t_end, bias=bias)
308+
dt = brainpy.math.get_dt() if dt is None else dt
309+
with brainstate.environ.context(dt=_as_ms(dt)):
310+
return braintools.input.sinusoidal(amplitude, _as_hz(frequency), _as_ms(duration),
311+
t_start=_as_ms(t_start), t_end=_as_ms(t_end), bias=bias)
284312

285313

286314
def square_input(amplitude, frequency, duration, dt=None, bias=False, t_start=0., t_end=None):
@@ -305,6 +333,8 @@ def square_input(amplitude, frequency, duration, dt=None, bias=False, t_start=0.
305333
Whether the sinusoid oscillates around 0 (False), or
306334
has a positive DC bias, thus non-negative (True).
307335
"""
308-
with brainstate.environ.context(dt=brainpy.math.get_dt() if dt is None else dt):
309-
return braintools.input.square(amplitude, frequency, duration, t_start=t_start, t_end=t_end, duty_cycle=0.5,
336+
dt = brainpy.math.get_dt() if dt is None else dt
337+
with brainstate.environ.context(dt=_as_ms(dt)):
338+
return braintools.input.square(amplitude, _as_hz(frequency), _as_ms(duration),
339+
t_start=_as_ms(t_start), t_end=_as_ms(t_end), duty_cycle=0.5,
310340
bias=bias)

brainpy/inputs/currents_test.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# ==============================================================================
1616
from unittest import TestCase
1717

18+
import brainunit as u
1819
import numpy as np
1920

2021
import brainpy as bp
@@ -81,17 +82,42 @@ def test_ou_process(self):
8182
current7 = bp.inputs.ou_process(mean=1., sigma=0.1, tau=10., duration=duration, n=2, t_start=10., t_end=180.)
8283
show(current7, duration, 'Ornstein-Uhlenbeck Process')
8384

84-
# def test_sinusoidal_input(self):
85-
# duration = 2000 * u.ms
86-
# current8 = bp.inputs.sinusoidal_input(amplitude=1., frequency=2.0 * u.Hz,
87-
# duration=duration, t_start=100. * u.ms, dt=0.1 * u.ms)
88-
# show(current8, duration, 'Sinusoidal Input')
89-
#
90-
# def test_square_input(self):
91-
# duration = 2000 * u.ms
92-
# current9 = bp.inputs.square_input(amplitude=1., frequency=2.0 * u.Hz,
93-
# duration=duration, t_start=100 * u.ms, dt=0.1 * u.ms)
94-
# show(current9, duration, 'Square Input')
85+
def test_sinusoidal_input_bare_frequency(self):
86+
# Regression: a bare numeric ``frequency`` (in Hz) must be accepted.
87+
# ``braintools`` started requiring frequency/time arguments to carry
88+
# units; the wrapper now attaches ``Hz``/``ms`` so the documented plain
89+
# ``frequency=2.0`` call keeps working instead of raising
90+
# ``AssertionError: Frequency must be in Hz``.
91+
duration = 2000
92+
current8 = bp.inputs.sinusoidal_input(amplitude=1., frequency=2.0,
93+
duration=duration, t_start=100., dt=0.1)
94+
current8 = np.asarray(current8)
95+
self.assertEqual(current8.shape[0], int(duration / 0.1))
96+
# amplitude 1 -> values bounded in [-1, 1]; current is zero before t_start
97+
self.assertLessEqual(float(np.max(np.abs(current8))), 1.0 + 1e-5)
98+
self.assertTrue(np.allclose(current8[:int(100 / 0.1)], 0.))
99+
show(current8, duration, 'Sinusoidal Input')
100+
101+
def test_square_input_bare_frequency(self):
102+
# Regression: same contract for ``square_input``.
103+
duration = 2000
104+
current9 = bp.inputs.square_input(amplitude=1., frequency=2.0,
105+
duration=duration, t_start=100., dt=0.1)
106+
current9 = np.asarray(current9)
107+
self.assertEqual(current9.shape[0], int(duration / 0.1))
108+
self.assertLessEqual(float(np.max(np.abs(current9))), 1.0 + 1e-5)
109+
show(current9, duration, 'Square Input')
110+
111+
def test_sinusoidal_input_quantity_frequency(self):
112+
# The unit-carrying form (``frequency=2 * u.Hz``, ``duration=... * u.ms``)
113+
# must produce the same waveform as the bare-number form.
114+
bare = np.asarray(bp.inputs.sinusoidal_input(amplitude=1., frequency=2.0,
115+
duration=2000, t_start=100., dt=0.1))
116+
quant = np.asarray(bp.inputs.sinusoidal_input(amplitude=1., frequency=2.0 * u.Hz,
117+
duration=2000 * u.ms, t_start=100. * u.ms,
118+
dt=0.1 * u.ms))
119+
self.assertEqual(bare.shape, quant.shape)
120+
self.assertTrue(np.allclose(bare, quant))
95121

96122
def test_general1(self):
97123
I1 = bp.inputs.section_input(values=[0, 1, 2], durations=[10, 20, 30], dt=0.1)

0 commit comments

Comments
 (0)