diff --git a/brainpy/boost_misc_test.py b/brainpy/boost_misc_test.py index 49cb636f5..910ed213f 100644 --- a/brainpy/boost_misc_test.py +++ b/brainpy/boost_misc_test.py @@ -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 @@ -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(): @@ -158,10 +156,12 @@ 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 @@ -169,8 +169,9 @@ def test_offline_logistic_regression_known_indexerror(): 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(): diff --git a/brainpy/dnn/dnn_toolbox_fixes_test.py b/brainpy/dnn/dnn_toolbox_fixes_test.py index fa9cfafbc..e36348e02 100644 --- a/brainpy/dnn/dnn_toolbox_fixes_test.py +++ b/brainpy/dnn/dnn_toolbox_fixes_test.py @@ -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(): diff --git a/brainpy/dyn/rates/nvar_coverage_test.py b/brainpy/dyn/rates/nvar_coverage_test.py index 15e057493..18aa1d8b6 100644 --- a/brainpy/dyn/rates/nvar_coverage_test.py +++ b/brainpy/dyn/rates/nvar_coverage_test.py @@ -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): diff --git a/brainpy/integrators/runner.py b/brainpy/integrators/runner.py index b429e2647..f50f72a8b 100644 --- a/brainpy/integrators/runner.py +++ b/brainpy/integrators/runner.py @@ -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 diff --git a/brainpy/train_analysis_glue_fixes_test.py b/brainpy/train_analysis_glue_fixes_test.py index 07d216a4e..6ad66b422 100644 --- a/brainpy/train_analysis_glue_fixes_test.py +++ b/brainpy/train_analysis_glue_fixes_test.py @@ -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():