Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 18 additions & 17 deletions brainpy/boost_misc_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,13 @@ def test_offline_lasso_regression_and_predict():
assert pred.shape[0] == x.shape[0]


def test_offline_elastic_net_regression_fit_and_broken_predict():
"""ElasticNet ``call`` (fit) runs through the gradient-descent solver.

NOTE: ``ElasticNetRegression.predict`` is inconsistent with ``call``: ``call``
builds features via ``polynomial_features(inputs, degree=...)`` (default
``add_bias=True`` -> bias column added), while ``predict`` passes
``add_bias=self.add_bias`` (default ``False``). The feature counts therefore
differ (7 vs 6) and ``predict`` raises a shape ``TypeError``. We exercise the
fit path and pin the broken predict.
def test_offline_elastic_net_regression_fit_and_predict():
"""ElasticNet ``call`` (fit) and ``predict`` are now consistent.

Fixed in audit 2026-06-19: ``call`` previously built features with the default
``add_bias=True`` while ``predict`` used ``add_bias=self.add_bias`` (default
``False``), so the feature counts differed and ``predict`` crashed. ``call``
now passes ``add_bias=self.add_bias``, so fit and predict agree.
"""
from brainpy.algorithms.offline import ElasticNetRegression

Expand All @@ -127,8 +125,8 @@ def test_offline_elastic_net_regression_fit_and_broken_predict():
model = ElasticNetRegression(alpha=0.05, degree=2, l1_ratio=0.5, max_iter=30)
w = model(y, x)
assert not bool(jnp.isnan(w).any())
with pytest.raises((TypeError, ValueError)):
model.predict(w, x)
pred = model.predict(w, x)
assert pred.shape[0] == x.shape[0]


def test_offline_polynomial_regression_and_predict():
Expand Down Expand Up @@ -158,19 +156,22 @@ def test_offline_polynomial_ridge_regression_and_predict():
assert pred.shape[0] == x.shape[0]


def test_offline_logistic_regression_known_indexerror():
"""NOTE: dead/broken path. ``LogisticRegression.call`` flattens ``targets``
to 1-D and then indexes ``targets.shape[1]``, raising ``IndexError``. Its
body (offline.py lines 391-415) is unreachable; we pin the broken behavior.
def test_offline_logistic_regression_fit():
"""``LogisticRegression.call`` now fits instead of crashing.

Fixed in audit 2026-06-19: ``call`` previously flattened ``targets`` to 1-D
and then indexed ``targets.shape[1]`` (``IndexError``); it now initialises a
1-D parameter vector and runs the gradient-descent solver.
"""
from brainpy.algorithms.offline import LogisticRegression

bm.random.seed(6)
rng = np.random.RandomState(6)
x = jnp.asarray(rng.randn(20, 2).astype('float32'))
y = jnp.asarray((np.asarray(x)[:, :1] > 0).astype('float32'))
with pytest.raises(IndexError):
LogisticRegression(max_iter=50)(y, x)
w = LogisticRegression(max_iter=50)(y, x)
assert w is not None
assert not bool(jnp.isnan(jnp.asarray(w)).any())


def test_offline_registry_helpers_and_base_repr():
Expand Down
12 changes: 6 additions & 6 deletions brainpy/dnn/dnn_toolbox_fixes_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,15 +406,15 @@ def test_adamw_invalid_hyperparams():
bp.optim.AdamW(lr=0.01, weight_decay=-1.0)


def test_adan_constructs_update_is_known_bug():
"""Adan constructs, but REMAINING BUG: ``Adan.update`` passes a tuple operand to
``lax.cond`` while the branch lambdas expect two args (optimizer.py:818),
raising TypeError. Pinned so the construct path is covered + the bug documented."""
def test_adan_constructs_and_updates():
"""Adan constructs AND ``update`` runs (fixed in audit 2026-06-19: the
``lax.cond`` first-step guard previously splatted its tuple operand, raising
``TypeError`` on every call). The update now applies and leaves the var finite."""
w = _train_var()
adan = bp.optim.Adan(lr=0.01, train_vars={'w': w})
repr(adan)
with pytest.raises(TypeError):
adan.update({'w': bm.ones((2, 3)) * 0.1})
adan.update({'w': bm.ones((2, 3)) * 0.1})
assert not bool(jnp.isnan(bm.as_jax(w)).any())


def test_adan_invalid_betas():
Expand Down
4 changes: 3 additions & 1 deletion brainpy/dyn/rates/nvar_coverage_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ def test_multi_order_stride_constant(self):

class TestNVARErrors(parameterized.TestCase):
def test_order_below_two_raises(self):
with self.assertRaises(AssertionError):
# ``NVAR.__init__`` validates ``order`` via ``check.is_integer(min_bound=2)``,
# which now (correctly) raises a ``ValueError`` for ``order < 2``.
with self.assertRaises(ValueError):
NVAR(num_in=3, delay=2, order=1)

def test_non_bool_constant_raises(self):
Expand Down
10 changes: 9 additions & 1 deletion brainpy/integrators/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,15 @@ def __init__(
monitors = {k: (self.variables[k], i) for k, i in monitors}
elif isinstance(monitors, dict):
monitors = self._format_dict_monitors(monitors)
monitors = {k: ((self.variables[i], i) if isinstance(i, str) else i) for k, i in monitors.items()}
# ``_format_dict_monitors`` yields ``{user_key: (var_name_or_Variable, index)}``.
# The integrator's state variables live in ``self.variables`` (they are NOT
# attributes of ``self.target``), so a string variable-name must be resolved
# here against ``self.variables``; the base runner then receives an already
# resolved ``(Variable, index)`` pair and passes it through unchanged.
monitors = {k: ((self.variables[v[0]], v[1])
if (isinstance(v, (tuple, list)) and isinstance(v[0], str))
else v)
for k, v in monitors.items()}
else:
raise ValueError

Expand Down
19 changes: 9 additions & 10 deletions brainpy/train_analysis_glue_fixes_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,22 +239,21 @@ def test_ridge_intercept_not_over_penalized():
assert abs(slope) < abs(intercept), f"slope not shrunk: slope={slope}"


def test_logistic_regression_remaining_bug():
"""Document a remaining (un-audited) bug in ``LogisticRegression.call``.

``call`` flattens ``targets`` to 1-D and then indexes ``targets.shape[1]``,
which raises ``IndexError: tuple index out of range``. This is independent
of the H-46/H-47 ridge fixes and is *not* in the assigned fix scope, so the
test pins the current broken behavior (and exercises the code path) rather
than asserting success. See summary notes.
def test_logistic_regression_fit_runs():
"""``LogisticRegression.call`` now fits instead of raising ``IndexError``.

Fixed in audit 2026-06-19: ``call`` previously flattened ``targets`` to 1-D
and then indexed ``targets.shape[1]``; it now initialises a 1-D parameter
vector and runs the gradient-descent solver to completion.
"""
from brainpy.algorithms.offline import LogisticRegression

rng = np.random.RandomState(3)
x = jnp.asarray(rng.randn(30, 2))
y = jnp.asarray((np.asarray(x)[:, :1] > 0).astype("float32"))
with pytest.raises(IndexError):
LogisticRegression(max_iter=100)(y, x)
w = LogisticRegression(max_iter=100)(y, x)
assert w is not None
assert not bool(jnp.isnan(jnp.asarray(w)).any())


def test_offline_least_square_and_polynomial():
Expand Down
Loading