diff --git a/brainpy/algorithms/offline.py b/brainpy/algorithms/offline.py index 476b52285..4f31093bb 100644 --- a/brainpy/algorithms/offline.py +++ b/brainpy/algorithms/offline.py @@ -385,8 +385,11 @@ def call(self, targets, inputs, outputs=None) -> ArrayType: raise ValueError(f'Target must be a scalar, but got multiple variables: {targets.shape}. ') targets = targets.flatten() - # initialize parameters - param = self.init_weights(inputs.shape[1], targets.shape[1]) + # Initialize a 1-D parameter vector to match the flattened 1-D + # ``targets`` used below. (``init_weights`` returns a 2-D ``(n_features, + # n_out)`` array; reading ``targets.shape[1]`` after the flatten raised + # IndexError, so request a single output and squeeze to 1-D.) + param = self.init_weights(inputs.shape[1], 1).flatten() def cond_fun(a): i, par_old, par_new = a @@ -538,8 +541,9 @@ def call(self, targets, inputs, outputs=None): # checking inputs = _check_data_2d_atls(bm.as_jax(inputs)) targets = _check_data_2d_atls(bm.as_jax(targets)) - # solving - inputs = normalize(polynomial_features(inputs, degree=self.degree)) + # solving. ``add_bias`` must match ``predict`` so the fitted weight + # length equals the feature width seen at prediction time. + inputs = normalize(polynomial_features(inputs, degree=self.degree, add_bias=self.add_bias)) return super(ElasticNetRegression, self).gradient_descent_solve(targets, inputs) def predict(self, W, X): diff --git a/brainpy/algorithms/offline_test.py b/brainpy/algorithms/offline_test.py index d4b3dcbf1..c983fcbcb 100644 --- a/brainpy/algorithms/offline_test.py +++ b/brainpy/algorithms/offline_test.py @@ -154,18 +154,24 @@ def test_fit_and_predict(self): class TestLogisticRegression: - def test_call_is_currently_broken(self): - # NOTE (defect): LogisticRegression.call flattens ``targets`` to 1-D - # (offline.py line ~386) and then immediately reads ``targets.shape[1]`` - # at line ~389, which raises IndexError. The closed-form (non gradient - # descent) branch is also unreachable because of this. The fit path is - # therefore broken for both gradient_descent=True and =False. + def test_call_runs_and_separates(self): + # P16-C2 (was test_call_is_currently_broken): ``LogisticRegression.call`` + # used to crash with IndexError because it flattened ``targets`` to 1-D + # and then read ``targets.shape[1]``. After the fix it must run and learn + # a usable separator on a trivially separable problem. rng = np.random.RandomState(1) x = rng.uniform(-1, 1, size=(30, 2)).astype(np.float32) y = (x[:, :1] > 0).astype(np.float32) - algo = offline.LogisticRegression(max_iter=50, learning_rate=0.1) - with pytest.raises(IndexError): - algo(bm.asarray(y), bm.asarray(x)) + algo = offline.LogisticRegression(max_iter=200, learning_rate=0.5) + w = algo(bm.asarray(y), bm.asarray(x)) + w = np.asarray(bm.as_jax(w)) + # one weight per input feature (1-D parameter vector) + assert w.reshape(-1).shape == (2,) + assert np.all(np.isfinite(w)) + # predictions should mostly agree with the (separable) labels + pred = np.asarray(bm.as_jax(algo.predict(bm.asarray(w), bm.asarray(x)))) + acc = np.mean((pred.reshape(-1) > 0.5) == y.reshape(-1)) + assert acc >= 0.8 def test_predict_applies_sigmoid(self): # ``predict`` itself works in isolation (it does not hit the broken call). @@ -209,19 +215,29 @@ def test_elastic_net_regression_fit(self): w = np.asarray(bm.as_jax(algo(y, x))) assert np.all(np.isfinite(w)) - def test_elastic_net_predict_bias_mismatch_is_broken(self): - # NOTE (defect): ElasticNetRegression.call builds features with - # ``polynomial_features(inputs, degree=self.degree)`` which defaults to - # add_bias=True, while ``predict`` calls it with add_bias=self.add_bias - # (default False). The resulting feature width differs from the fitted - # weight length, so predicting on freshly-built features raises a - # shape-mismatch TypeError from jnp.dot. + def test_elastic_net_train_predict_consistent(self): + # P16-H1 (was test_elastic_net_predict_bias_mismatch_is_broken): + # ``call`` used to build features with the default add_bias=True while + # ``predict`` used add_bias=self.add_bias (default False), giving a + # train/predict feature-count mismatch that crashed jnp.dot. After the + # fix, training and prediction must use identical feature construction. x, y = _xy(slope=2.0) algo = offline.ElasticNetRegression(alpha=0.01, degree=2, l1_ratio=0.5, max_iter=50, learning_rate=0.001) w = bm.asarray(np.asarray(bm.as_jax(algo(y, x)))) - with pytest.raises(TypeError): - algo.predict(w, x) + pred = np.asarray(bm.as_jax(algo.predict(w, x))) + assert pred.shape[0] == np.asarray(bm.as_jax(x)).shape[0] + assert np.all(np.isfinite(pred)) + + def test_elastic_net_add_bias_true_consistent(self): + # The fix must also hold when add_bias=True is requested explicitly. + x, y = _xy(slope=2.0) + algo = offline.ElasticNetRegression(alpha=0.01, degree=2, l1_ratio=0.5, + add_bias=True, max_iter=50, + learning_rate=0.001) + w = bm.asarray(np.asarray(bm.as_jax(algo(y, x)))) + pred = np.asarray(bm.as_jax(algo.predict(w, x))) + assert np.all(np.isfinite(pred)) class TestRegistry: diff --git a/brainpy/algorithms/utils.py b/brainpy/algorithms/utils.py index 352fb2550..13e4c873b 100644 --- a/brainpy/algorithms/utils.py +++ b/brainpy/algorithms/utils.py @@ -106,7 +106,11 @@ def polynomial_features(X, degree: int, add_bias: bool = True): return bm.insert(X, 0, 1, axis=1) if add_bias else X if add_bias: n_features += 1 - X_new = bm.zeros((n_samples, 1 + n_features + len(combinations))) + # ``n_features`` already accounts for the bias slot (when ``add_bias``); the + # design matrix is exactly the (bias +) original features plus the degree>=2 + # interaction terms. The previous extra leading ``1 +`` left a dead all-zero + # trailing column and over-counted the feature dimension by one. + X_new = bm.zeros((n_samples, n_features + len(combinations))) if add_bias: X_new[:, 0] = 1 X_new[:, 1:n_features] = X diff --git a/brainpy/algorithms/utils_test.py b/brainpy/algorithms/utils_test.py index 757754ba5..7ab59793f 100644 --- a/brainpy/algorithms/utils_test.py +++ b/brainpy/algorithms/utils_test.py @@ -97,25 +97,25 @@ def test_degree1_shortcircuit_without_bias(self): def test_degree2_with_bias(self): X = bm.asarray([[2.0, 3.0]]) out = np.asarray(bm.as_jax(utils.polynomial_features(X, degree=2, add_bias=True))) - # X_new has shape (n_samples, 1 + n_features + len(combinations)). - # With add_bias, n_features is bumped to 3 (2 + 1), combos == 3, so the - # allocated width is 1 + 3 + 3 == 7. NOTE: the leading "1 +" plus the - # bumped n_features leaves one all-zero trailing column unused, i.e. the - # output is wider than the mathematically expected 6 columns. - assert out.shape == (1, 7) + # P16-M2: width is exactly 1 bias + 2 linear + 3 interaction == 6 + # (previously a dead all-zero trailing column made it 7). + assert out.shape == (1, 6) assert out[0, 0] == 1.0 # the linear features should appear assert 2.0 in out[0] and 3.0 in out[0] # interactions: 4 (=2^2), 6 (=2*3), 9 (=3^2) for v in (4.0, 6.0, 9.0): assert np.any(np.isclose(out[0], v)) + # no dead all-zero column anymore + assert not np.any(np.all(out == 0, axis=0)) def test_degree2_without_bias(self): X = bm.asarray([[2.0, 3.0]]) out = np.asarray(bm.as_jax(utils.polynomial_features(X, degree=2, add_bias=False))) - # 1 + 2 linear + 3 interaction -> 6 cols (again one extra leading column; - # see NOTE in test_degree2_with_bias). - assert out.shape == (1, 6) + # P16-M2: 2 linear + 3 interaction -> 5 cols (previously 6 with a dead + # leading allocation slot). + assert out.shape == (1, 5) + assert not np.any(np.all(out == 0, axis=0)) class TestNormalize: diff --git a/brainpy/connect/base.py b/brainpy/connect/base.py index 8e7ad9260..f1a57b6e3 100644 --- a/brainpy/connect/base.py +++ b/brainpy/connect/base.py @@ -689,7 +689,9 @@ def coo2csr(coo, num_pre): post_ids = post_ids[sort_ids] indices = post_ids unique_pre_ids, pre_count = onp.unique(pre_ids, return_counts=True) - final_pre_count = onp.zeros(num_pre, dtype=jnp.uint32) + # Use the connection index dtype (not uint32) so the assignment below does + # not trigger an int->uint scatter dtype mismatch. + final_pre_count = onp.zeros(num_pre, dtype=get_idx_type()) final_pre_count[unique_pre_ids] = pre_count else: sort_ids = onp.argsort(bm.as_jax(pre_ids)) @@ -697,7 +699,10 @@ def coo2csr(coo, num_pre): post_ids = post_ids[sort_ids] indices = post_ids unique_pre_ids, pre_count = jnp.unique(pre_ids, return_counts=True) - final_pre_count = bm.zeros(num_pre, dtype=jnp.uint32) + # Use the connection index dtype (not uint32) so the in-place update below + # does not trigger an int->uint scatter dtype mismatch (FutureWarning that + # becomes an error in future JAX releases). + final_pre_count = bm.zeros(num_pre, dtype=get_idx_type()) final_pre_count[unique_pre_ids] = pre_count final_pre_count = bm.as_jax(final_pre_count) indptr = final_pre_count.cumsum() diff --git a/brainpy/connect/custom_conn.py b/brainpy/connect/custom_conn.py index 4d751fc43..1b6ec4418 100644 --- a/brainpy/connect/custom_conn.py +++ b/brainpy/connect/custom_conn.py @@ -100,7 +100,7 @@ def __init__(self, indices, inptr, **kwargs): self.max_post = self.indices.max() def build_csr(self): - if self.pre_num != self.pre_num: + if self.pre_num != self.inptr.size - 1: raise ConnectorError(f'(pre_size, post_size) is inconsistent with ' f'the shape of the sparse matrix.') if self.post_num <= self.max_post: diff --git a/brainpy/connect/custom_conn_test.py b/brainpy/connect/custom_conn_test.py index 248b8015b..9c5398b85 100644 --- a/brainpy/connect/custom_conn_test.py +++ b/brainpy/connect/custom_conn_test.py @@ -61,6 +61,45 @@ def test_MatConn2(self): conn(pre_size=5, post_size=1) +class TestCSRConn(TestCase): + def _csr(self): + # 3 pre-synaptic neurons (indptr has 4 entries), max post id == 2 + indices = np.array([0, 1, 2, 0, 1], dtype=np.int32) + indptr = np.array([0, 2, 3, 5], dtype=np.int32) + return indices, indptr + + def test_csrconn_consistent_ok(self): + # P16-H2: a CSRConn whose declared pre_size matches the indptr length + # must build without error. + indices, indptr = self._csr() + conn = bp.conn.CSRConn(indices, indptr) + ind, ip = conn.require(3, 3, 'csr') + assert np.array_equal(np.asarray(ind), indices) + assert np.array_equal(np.asarray(ip), indptr) + + def test_csrconn_inconsistent_pre_num_raises(self): + # P16-H2: previously the guard ``self.pre_num != self.pre_num`` was a + # tautology (always False), so an inconsistent pre_size silently produced + # a malformed CSR. It must now raise. + indices, indptr = self._csr() # indptr implies 3 pre + conn = bp.conn.CSRConn(indices, indptr) + with pytest.raises(bp.errors.ConnectorError): + conn.require(5, 3, 'csr') # pre=5 inconsistent with indptr (3) + + def test_coo2csr_no_dtype_warning(self): + # P16-M1: coo2csr must not emit the int32->uint32 scatter FutureWarning + # (which is slated to become an error in future JAX). + import warnings + import jax.numpy as jnp + from brainpy.connect.base import coo2csr + pre = jnp.array([0, 0, 1, 2, 2, 2]) + post = jnp.array([1, 2, 0, 0, 1, 2]) + with warnings.catch_warnings(): + warnings.simplefilter('error', FutureWarning) + ind, indptr = coo2csr((pre, post), 3) + assert np.array_equal(np.asarray(indptr), np.array([0, 2, 3, 6])) + + class TestSparseMatConn(TestCase): def test_sparseMatConn(self): conn_mat = np.random.randint(2, size=(5, 3), dtype=bp.math.bool_) diff --git a/brainpy/inputs/currents.py b/brainpy/inputs/currents.py index 2b40686f0..cf708f559 100644 --- a/brainpy/inputs/currents.py +++ b/brainpy/inputs/currents.py @@ -150,7 +150,7 @@ def spike_current(*args, **kwargs): warnings.warn('Please use "brainpy.inputs.spike_input()" instead. ' '"brainpy.inputs.spike_current()" is deprecated since version 2.1.13.', DeprecationWarning) - return constant_input(*args, **kwargs) + return spike_input(*args, **kwargs) def ramp_input(c_start, c_end, duration, t_start=0, t_end=None, dt=None): @@ -189,7 +189,7 @@ def ramp_current(*args, **kwargs): warnings.warn('Please use "brainpy.inputs.ramp_input()" instead. ' '"brainpy.inputs.ramp_current()" is deprecated since version 2.1.13.', DeprecationWarning) - return constant_input(*args, **kwargs) + return ramp_input(*args, **kwargs) def wiener_process(duration, dt=None, n=1, t_start=0., t_end=None, seed=None): diff --git a/brainpy/inputs/currents_coverage_test.py b/brainpy/inputs/currents_coverage_test.py index a93d4139e..49b0ad906 100644 --- a/brainpy/inputs/currents_coverage_test.py +++ b/brainpy/inputs/currents_coverage_test.py @@ -43,25 +43,27 @@ def test_constant_current_warns_and_delegates(self): self.assertEqual(duration, 200) def test_spike_current_warns(self): - # NOTE: ``spike_current`` is documented as a spike-input shim but its - # body actually delegates to ``constant_input`` (copy/paste defect in - # the deprecation wrapper), so it must be fed constant_input-style - # ``[(value, duration), ...]`` pairs rather than spike arguments. + # P16-C1: ``spike_current`` now correctly delegates to ``spike_input`` + # (it previously delegated to ``constant_input`` and crashed on spike + # arguments). It must warn AND accept spike-style arguments. + kwargs = dict(sp_times=[10, 20, 30], sp_lens=1., sp_sizes=0.5, duration=40.) with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') - out = bp.inputs.spike_current([(0, 50), (1, 50)]) + out = bp.inputs.spike_current(**kwargs) self.assertTrue(any(issubclass(x.category, DeprecationWarning) for x in w)) self.assertIsNotNone(out) + self.assertTrue(np.array_equal(np.asarray(out), np.asarray(bp.inputs.spike_input(**kwargs)))) def test_ramp_current_warns(self): - # NOTE: like ``spike_current``, ``ramp_current`` also delegates to - # ``constant_input`` rather than ``ramp_input`` (same wrapper defect), - # so it expects ``[(value, duration), ...]`` pairs. + # P16-C1: ``ramp_current`` now correctly delegates to ``ramp_input`` + # (it previously delegated to ``constant_input``). It must warn AND + # accept ramp-style arguments. with warnings.catch_warnings(record=True) as w: warnings.simplefilter('always') - out = bp.inputs.ramp_current([(0, 50), (1, 50)]) + out = bp.inputs.ramp_current(0., 1., 100.) self.assertTrue(any(issubclass(x.category, DeprecationWarning) for x in w)) self.assertIsNotNone(out) + self.assertTrue(np.array_equal(np.asarray(out), np.asarray(bp.inputs.ramp_input(0., 1., 100.)))) @unittest.skipUnless(HAS_BRAINUNIT, 'brainunit required for unit-aware inputs') diff --git a/brainpy/inputs/currents_test.py b/brainpy/inputs/currents_test.py index 329d88cbe..460ad591c 100644 --- a/brainpy/inputs/currents_test.py +++ b/brainpy/inputs/currents_test.py @@ -105,3 +105,24 @@ def test_general2(self): bp.math.random.random((3, 10))], durations=[100, 300, 100]) self.assertTrue(current.shape == (5000, 3, 10)) + + def test_spike_current_alias(self): + # P16-C1: the deprecated ``spike_current`` alias must forward to + # ``spike_input`` (it used to forward to ``constant_input`` and crash). + import warnings + kwargs = dict(sp_times=[10, 20, 30], sp_lens=1., sp_sizes=0.5, duration=40.) + with warnings.catch_warnings(): + warnings.simplefilter('ignore', DeprecationWarning) + aliased = bp.inputs.spike_current(**kwargs) + direct = bp.inputs.spike_input(**kwargs) + self.assertTrue(np.array_equal(np.asarray(aliased), np.asarray(direct))) + + def test_ramp_current_alias(self): + # P16-C1: the deprecated ``ramp_current`` alias must forward to + # ``ramp_input`` (it used to forward to ``constant_input`` and crash). + import warnings + with warnings.catch_warnings(): + warnings.simplefilter('ignore', DeprecationWarning) + aliased = bp.inputs.ramp_current(0., 1., 100.) + direct = bp.inputs.ramp_input(0., 1., 100.) + self.assertTrue(np.array_equal(np.asarray(aliased), np.asarray(direct))) diff --git a/docs/issues-found-20260619-small-modules.md b/docs/issues-found-20260619-small-modules.md new file mode 100644 index 000000000..6b2aa8709 --- /dev/null +++ b/docs/issues-found-20260619-small-modules.md @@ -0,0 +1,204 @@ +# P16 small-modules — Issues Found (2026-06-19) + +Scope: brainpy/connect, brainpy/initialize, brainpy/encoding, brainpy/inputs, +brainpy/algorithms, brainpy/tools. ID prefix `P16-`. + +--- + +### P16-C1 — `spike_current` / `ramp_current` deprecation wrappers dispatch to the wrong function [Critical] +- File: brainpy/inputs/currents.py:144-153, 183-192 +- Category: correctness / api-drift +- What: The deprecated public aliases `spike_current(*args, **kwargs)` and + `ramp_current(*args, **kwargs)` call `constant_input(*args, **kwargs)` instead + of `spike_input` / `ramp_input`. +- Why it's a bug: `constant_input` has a completely different signature + (`I_and_duration`), so any documented call such as + `spike_current(sp_times=..., sp_lens=..., sp_sizes=..., duration=...)` raises + `TypeError: constant_input() got an unexpected keyword argument 'sp_times'`. + These are public, exported, still-documented APIs. They are 100% broken. +- Repro: + ```python + import brainpy as bp + bp.inputs.spike_current(sp_times=[10, 20], sp_lens=1., sp_sizes=0.5, duration=40.) + # TypeError: constant_input() got an unexpected keyword argument 'sp_times' + ``` +- Fix: route `spike_current` -> `spike_input`, `ramp_current` -> `ramp_input`. +- Tests: currents_test.py::test_spike_current_alias, test_ramp_current_alias +- Status: fixed + +### P16-C2 — `LogisticRegression.call` crashes on every call (flatten then `.shape[1]`) [Critical] +- File: brainpy/algorithms/offline.py:386-389 +- Category: correctness +- What: `targets = targets.flatten()` makes `targets` 1-D, then the next line + calls `self.init_weights(inputs.shape[1], targets.shape[1])`, indexing + `shape[1]` of a 1-D array. +- Why it's a bug: `IndexError: tuple index out of range` on any invocation — + the algorithm can never run. +- Repro: + ```python + from brainpy.algorithms.offline import LogisticRegression + import brainpy.math as bm + X = bm.random.rand(20, 3); y = bm.asarray((bm.random.rand(20,1) > .5).astype(float)) + LogisticRegression(max_iter=10)(y, X) # IndexError + ``` +- Fix: initialise weights as `init_weights(inputs.shape[1], 1)` and flatten to a + 1-D parameter vector to match the 1-D `targets` used by the body function. +- Tests: offline_test.py::test_logistic_regression_runs +- Status: fixed + +### P16-H1 — `ElasticNetRegression` train/predict feature mismatch (`add_bias` ignored in training) [High] +- File: brainpy/algorithms/offline.py:542 +- Category: correctness +- What: `call()` builds features with `polynomial_features(inputs, degree=...)` + (so `add_bias` defaults to `True`), while `predict()` builds features with + `polynomial_features(X, degree=..., add_bias=self.add_bias)`. With the default + `add_bias=False`, training adds a bias column but prediction does not. +- Why it's a bug: weights are fit with `n+1` features but prediction supplies + `n` features -> `dot_general` shape error (or, if shapes happened to align, + silently wrong predictions). +- Repro: + ```python + from brainpy.algorithms.offline import ElasticNetRegression + import brainpy.math as bm + en = ElasticNetRegression(max_iter=20, add_bias=False) + W = en(bm.random.rand(15,1), bm.random.rand(15,2)) + en.predict(W, bm.random.rand(15,2)) # TypeError: contracting dims (6,) vs (7,) + ``` +- Fix: pass `add_bias=self.add_bias` in `call()` so training and prediction use + identical feature construction. +- Tests: offline_test.py::test_elasticnet_train_predict_consistent +- Status: fixed + +### P16-H2 — `CSRConn.build_csr` consistency check is dead (`self.pre_num != self.pre_num`) [High] +- File: brainpy/connect/custom_conn.py:103 +- Category: edge/error +- What: The guard `if self.pre_num != self.pre_num:` compares a value with + itself and is always `False`, so the "(pre_size, post_size) inconsistent with + the sparse matrix" error can never fire. +- Why it's a bug: A `CSRConn` whose declared `pre_size` disagrees with the + `indptr` length silently produces a malformed CSR (e.g. claims `pre_num=5` + but returns an `indptr` of length 4), instead of raising. The clear intent is + to compare the user-supplied `pre_num` against `self.inptr.size - 1`. +- Repro: + ```python + import numpy as np, brainpy as bp + c = bp.conn.CSRConn(np.array([0,1,2,0,1], np.int32), np.array([0,2,3,5], np.int32)) # 3 pre + c.require(5, 3, 'csr') # no error -> malformed CSR (indptr len 4 for pre_num 5) + ``` +- Fix: compare `self.pre_num != self.inptr.size - 1` and raise `ConnectorError`. +- Tests: custom_conn_test.py::test_csrconn_inconsistent_pre_num_raises, + test_csrconn_consistent_ok +- Status: fixed + +### P16-M1 — `coo2csr` scatters int counts into a `uint32` buffer (FutureWarning -> future error) [Medium] +- File: brainpy/connect/base.py:692, 700-702 +- Category: numerics / api-drift +- What: `final_pre_count = onp.zeros(num_pre, dtype=jnp.uint32)` (and the jax + branch `bm.zeros(num_pre, dtype=jnp.uint32)`) is filled with `pre_count` + values whose dtype is the platform int (int32/int64). On the jax branch this + triggers `FutureWarning: scatter inputs have incompatible types: cannot safely + cast value from dtype=int32 to dtype=uint32 ...` and is slated to become a hard + error in future JAX. +- Why it's a bug: `coo2csr` is on the default code path for building CSR/PRE2POST + structures from COO connectivity; a future JAX release will turn the cast into + an exception, breaking connectivity construction. +- Repro (static / warning): + ```python + import jax.numpy as jnp + from brainpy.connect.base import coo2csr + coo2csr((jnp.array([0,0,1,2,2,2]), jnp.array([1,2,0,0,1,2])), 3) # FutureWarning + ``` +- Fix: build the count buffer with the connection index dtype (`get_idx_type()`) + so the scatter dtype matches. +- Tests: base_coverage_test.py / random_conn — covered by no-warning assertion in + custom_conn_test.py::test_coo2csr_no_dtype_warning +- Status: fixed + +### P16-M2 — `polynomial_features` allocates one extra (always-zero) feature column [Medium] +- File: brainpy/algorithms/utils.py:107-117 +- Category: correctness / edge +- What: width is `1 + n_features + len(combinations)` after `n_features += 1` + (when `add_bias`), but only `1` bias + original features + `len(combinations)` + columns are written, leaving the final column permanently `0`. With + `add_bias=False` the same `1 +` adds a spurious leading allocation slot too. +- Why it's a bug: every transformed design matrix carries a dead all-zero + feature column, inflating the weight vector by one element (and any code that + reasons about feature counts, e.g. RidgeRegression's per-feature penalty + vector, sees the wrong dimensionality). Regression still "works" only because + pinv/least-squares assigns ~0 weight to the zero column. +- Repro: + ```python + import brainpy.math as bm + from brainpy.algorithms.utils import polynomial_features + polynomial_features(bm.arange(6.).reshape(2,3), degree=2, add_bias=True).shape # (2, 11), last col all 0 + ``` +- Fix: drop the spurious `1 +`; allocate exactly `n_features + len(combinations)` + columns (the bias slot is already accounted for by `n_features += 1`). +- Tests: utils_test.py::test_polynomial_features_no_dead_column +- Status: fixed + +--- + +## Recorded only (Low — not fixed) + +### P16-L1 — `LatencyEncoder` docstring example output shape is wrong [Low] +- File: brainpy/encoding/stateful_encoding.py:111-120 +- Category: style/docs +- What: The docstring shows `encoder.multi_steps(a, n_time=5)` producing a + `(5, 3)` array, but `n_time` is a duration divided by `bm.get_dt()` (0.1), so + the real output is `(50, 3)`. Misleading example. +- Status: recorded-only + +### P16-L2 — `coo2csr` jax branch returns a host (numpy) `indptr` via `onp.insert` [Low] +- File: brainpy/connect/base.py:704 +- Category: perf/consistency +- What: `indptr = onp.insert(indptr, 0, 0)` converts a jax array to numpy, so the + jax branch returns `indices` as a jax array but `indptr` as numpy — an + inconsistent (device/host) return and an implicit device->host transfer. Path + is already non-jittable (uses `jnp.unique` w/ return_counts), so no current + crash; recorded only. +- Status: recorded-only + +### P16-L3 — `_check_none` in initialize/generic.py is an empty no-op [Low] +- File: brainpy/initialize/generic.py:36-37 +- Category: dead code +- What: `_check_none(x, allow_none=False)` has a `pass` body and is never called. +- Status: recorded-only + +### P16-L4 — `numba_jit(_random_subset)` wraps a Python-`set` builder [Low] +- File: brainpy/connect/random_conn.py:706-713, 823-830, 969-976 +- Category: perf/correctness-risk +- What: When numba is installed, `_random_subset` (which builds a Python `set`) + is njit-compiled. Sets in nopython mode are limited; relies on numba fallback + / object mode. Works on the tested machine; recorded as fragility only. +- Status: recorded-only + +### P16-L5 — `RidgeRegression.__repr__` mislabels `alpha` as `beta` [Low] +- File: brainpy/algorithms/offline.py:287 +- Category: style +- What: `__repr__` prints `beta={self.regularizer.alpha}` though `beta` is the + deprecated alias. +- Status: recorded-only + +--- + +## Cross-check vs dev/issues-found-20260618.md (in-scope entries) + +The prior audit's Critical/High entries that fall in this slice were re-verified +against the current worktree code and found **already fixed** (no action needed): + +- C-23 (online.py RLS wrong for batch>1): now uses proper block RLS + `K = P Hᵀ (I_B + H P Hᵀ)⁻¹`; verified `dw` shape correct for B=4. +- C-24 (PoissonEncoder.single_step crash): `single_step` now draws a single + Bernoulli sample directly; verified it runs. +- H-46 (offline.py GD `.value` AttributeError on jax tracer): gradient-descent + path now uses `jax.lax.while_loop` with no `.value`; verified Ridge/Linear GD run. +- H-47 (ridge penalizes bias column): now skips the intercept penalty for + `add_bias` (PolynomialRidge); verified. +- M-30 (FixedProb floors `int(post_num*prob)` to 0; contradictory include_self + guard): now uses `int(round(...))` and the guard was removed; verified. + +Still-present prior entry (Low only): + +- L-11 (LatencyEncoder docstring example output shape ignores dt) — same as + P16-L1 above; recorded only.