From 7fe500c3e33b1646714972a92487b776f5441d2a Mon Sep 17 00:00:00 2001 From: Hananel Hazan Date: Sun, 8 Nov 2026 00:05:03 -0500 Subject: [PATCH] Clamp a given w to finite bounds in Conv1d/2d/3d and MeanField connections The given-w branch of these constructors clamped only when a bound was infinite (== instead of !=), so with finite bounds a given w was never clamped. Connection already used !=. Results change for code passing out-of-range weights with finite bounds; no built-in model or example does, and the golden tests are unchanged. Test: test_given_weights_are_clamped_to_finite_bounds fails on master for all four classes and passes with the fix. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 9 +++++++ bindsnet/network/topology.py | 8 +++--- test/network/test_connections.py | 45 ++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f49c077..23de1803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ see the [GitHub releases / tags](https://github.com/BindsNET/bindsnet/releases). ## [Unreleased] +### Results changes (read this before upgrading) +- `Conv1dConnection`, `Conv2dConnection`, `Conv3dConnection` and `MeanFieldConnection` + now clamp a `w` passed to the constructor into `[wmin, wmax]`, as `Connection` does. + Before, the check was reversed (`==` instead of `!=`): a given `w` was clamped only + when a bound was infinite, which does nothing, so with finite bounds it was never + clamped. Affects code that passes these classes weights outside finite bounds; their + starting weights are now clamped. No built-in model or example does this, and the + golden-output tests are unchanged. + ### Added - Contributor rules for AI-assisted work (`CONTRIBUTING.md`, "AI-assisted contributions"): AI tools are welcome, a human must have read and run every change, diff --git a/bindsnet/network/topology.py b/bindsnet/network/topology.py index 409c4953..2f07484d 100644 --- a/bindsnet/network/topology.py +++ b/bindsnet/network/topology.py @@ -880,7 +880,7 @@ def __init__( w += self.wmin w = w.to(dtype=w_dtype) else: - if (self.wmin == -inf).any() or (self.wmax == inf).any(): + if (self.wmin != -inf).any() or (self.wmax != inf).any(): w = torch.clamp(w, self.wmin, self.wmax) w = self.cast_dtype_if_needed(w, w_dtype) @@ -1041,7 +1041,7 @@ def __init__( w += self.wmin w = w.to(dtype=w_dtype) else: - if (self.wmin == -inf).any() or (self.wmax == inf).any(): + if (self.wmin != -inf).any() or (self.wmax != inf).any(): w = torch.clamp(w, self.wmin, self.wmax) w = self.cast_dtype_if_needed(w, w_dtype) @@ -1223,7 +1223,7 @@ def __init__( w += self.wmin w = w.to(dtype=w_dtype) else: - if (self.wmin == -inf).any() or (self.wmax == inf).any(): + if (self.wmin != -inf).any() or (self.wmax != inf).any(): w = torch.clamp(w, self.wmin, self.wmax) w = self.cast_dtype_if_needed(w, w_dtype) @@ -2262,7 +2262,7 @@ def __init__( w = self.wmin + ((torch.randn(1)[0] + 1) / 10) * (self.wmax - self.wmin) w = w.to(dtype=w_dtype) else: - if (self.wmin == -np.inf).any() or (self.wmax == np.inf).any(): + if (self.wmin != -np.inf).any() or (self.wmax != np.inf).any(): w = torch.clamp(w, self.wmin, self.wmax) w = self.cast_dtype_if_needed(w, w_dtype) self.w = Parameter(w, requires_grad=False) diff --git a/test/network/test_connections.py b/test/network/test_connections.py index fdb21c78..c4753cb8 100644 --- a/test/network/test_connections.py +++ b/test/network/test_connections.py @@ -977,3 +977,48 @@ def test_connection_from_gpu_weights_clamps_and_learns_on_gpu(wmin, wmax): net.run(inputs={"in": (torch.rand(20, 1, 4) < 0.5).cuda()}, time=20) assert conn.w.is_cuda assert ((conn.w >= 0) & (conn.w <= 1)).all() + + +def _given_w_out_of_bounds(): + """Constructors of the connection types whose given w is checked against finite bounds.""" + return { + "Conv1dConnection": lambda: Conv1dConnection( + Input(shape=(1, 6)), + LIFNodes(shape=(2, 4)), + kernel_size=3, + w=torch.linspace(-1.0, 2.0, 6).view(2, 1, 3), + wmin=0.0, + wmax=1.0, + ), + "Conv2dConnection": lambda: Conv2dConnection( + Input(shape=(1, 6, 6)), + LIFNodes(shape=(2, 4, 4)), + kernel_size=3, + w=torch.linspace(-1.0, 2.0, 18).view(2, 1, 3, 3), + wmin=0.0, + wmax=1.0, + ), + "Conv3dConnection": lambda: Conv3dConnection( + Input(shape=(1, 4, 4, 4)), + LIFNodes(shape=(2, 2, 2, 2)), + kernel_size=3, + w=torch.linspace(-1.0, 2.0, 54).view(2, 1, 3, 3, 3), + wmin=0.0, + wmax=1.0, + ), + "MeanFieldConnection": lambda: MeanFieldConnection( + Input(n=4), LIFNodes(n=3), w=torch.tensor(2.0), wmin=0.0, wmax=1.0 + ), + } + + +@pytest.mark.parametrize("name", list(_given_w_out_of_bounds())) +def test_given_weights_are_clamped_to_finite_bounds(name): + """A w passed to the constructor is clamped into [wmin, wmax], as Connection does. + + Caught: Conv1d/2d/3dConnection and MeanFieldConnection clamped a given w only when + a bound was infinite (== instead of !=), so with finite bounds it was never clamped. + """ + conn = _given_w_out_of_bounds()[name]() + assert conn.w.min() >= 0.0 + assert conn.w.max() <= 1.0