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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ see the [GitHub releases / tags](https://github.com/BindsNET/bindsnet/releases).
`wmin` and `wmax` are now created on the device of the given `w`; before, they were
always on the CPU. Single-value and per-weight bounds both work. Results do not
change.
- `LocalConnection1D`, `LocalConnection2D` and `LocalConnection3D` accept a given `w`
and per-weight `wmin`/`wmax`, as the other connections do; their docstrings now say so.
Before, a given `w` raised `AttributeError` (the shape check used `out_channels`,
which these classes do not have, instead of `n_filters`), and per-weight bounds
raised "Boolean value of Tensor with more than one value is ambiguous" (the bound
check had no `.any()`). Both used to crash, so no working code changes results.

## [0.3.5] - 2026-10-29

Expand Down
30 changes: 18 additions & 12 deletions bindsnet/network/topology.py
Original file line number Diff line number Diff line change
Expand Up @@ -1803,8 +1803,10 @@ def __init__(
:param LearningRule update_rule: Modifies connection parameters according to some rule.
:param torch.Tensor w: Strengths of synapses.
:param torch.Tensor b: Target population bias.
:param float wmin: Minimum allowed value on the connection weights.
:param float wmax: Maximum allowed value on the connection weights.
:param Union[float, torch.Tensor] wmin: Minimum allowed value(s) on the connection weights. Single value, or
tensor of same size as w
:param Union[float, torch.Tensor] wmax: Maximum allowed value(s) on the connection weights. Single value, or
tensor of same size as w
:param float norm: Total weight per target neuron normalization constant.
"""

Expand Down Expand Up @@ -1836,12 +1838,12 @@ def __init__(
else:
assert w.shape == (
self.in_channels,
self.out_channels * self.conv_size,
self.n_filters * self.conv_size,
self.kernel_size,
), error
w = self.cast_dtype_if_needed(w, w_dtype)

if self.wmin != -np.inf or self.wmax != np.inf:
if (self.wmin != -np.inf).any() or (self.wmax != np.inf).any():
w = torch.clamp(w, self.wmin, self.wmax)

self.w = Parameter(w, requires_grad=False)
Expand Down Expand Up @@ -1945,8 +1947,10 @@ def __init__(
:param LearningRule update_rule: Modifies connection parameters according to some rule.
:param torch.Tensor w: Strengths of synapses.
:param torch.Tensor b: Target population bias.
:param float wmin: Minimum allowed value on the connection weights.
:param float wmax: Maximum allowed value on the connection weights.
:param Union[float, torch.Tensor] wmin: Minimum allowed value(s) on the connection weights. Single value, or
tensor of same size as w
:param Union[float, torch.Tensor] wmax: Maximum allowed value(s) on the connection weights. Single value, or
tensor of same size as w
:param float norm: Total weight per target neuron normalization constant.
"""

Expand Down Expand Up @@ -1988,12 +1992,12 @@ def __init__(
else:
assert w.shape == (
self.in_channels,
self.out_channels * self.conv_prod,
self.n_filters * self.conv_prod,
self.kernel_prod,
), error
w = self.cast_dtype_if_needed(w, w_dtype)

if self.wmin != -np.inf or self.wmax != np.inf:
if (self.wmin != -np.inf).any() or (self.wmax != np.inf).any():
w = torch.clamp(w, self.wmin, self.wmax)

self.w = Parameter(w, requires_grad=False)
Expand Down Expand Up @@ -2098,8 +2102,10 @@ def __init__(
:param LearningRule update_rule: Modifies connection parameters according to some rule.
:param torch.Tensor w: Strengths of synapses.
:param torch.Tensor b: Target population bias.
:param float wmin: Minimum allowed value on the connection weights.
:param float wmax: Maximum allowed value on the connection weights.
:param Union[float, torch.Tensor] wmin: Minimum allowed value(s) on the connection weights. Single value, or
tensor of same size as w
:param Union[float, torch.Tensor] wmax: Maximum allowed value(s) on the connection weights. Single value, or
tensor of same size as w
:param float norm: Total weight per target neuron normalization constant.
"""

Expand Down Expand Up @@ -2143,12 +2149,12 @@ def __init__(
else:
assert w.shape == (
self.in_channels,
self.out_channels * self.conv_prod,
self.n_filters * self.conv_prod,
self.kernel_prod,
), error
w = self.cast_dtype_if_needed(w, w_dtype)

if self.wmin != -np.inf or self.wmax != np.inf:
if (self.wmin != -np.inf).any() or (self.wmax != np.inf).any():
w = torch.clamp(w, self.wmin, self.wmax)

self.w = Parameter(w, requires_grad=False)
Expand Down
88 changes: 88 additions & 0 deletions test/network/test_connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -977,3 +977,91 @@ 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()


_LOCAL_CONNECTIONS_ND = {
"LocalConnection1D": (
LocalConnection1D,
(2, 12),
(2, 4),
{"kernel_size": 3, "stride": 3, "n_filters": 2},
),
"LocalConnection2D": (
LocalConnection2D,
(2, 8, 8),
(3, 3, 3),
{"kernel_size": 4, "stride": 2, "n_filters": 3},
),
"LocalConnection3D": (
LocalConnection3D,
(1, 6, 6, 6),
(2, 3, 3, 3),
{"kernel_size": 2, "stride": 2, "n_filters": 2},
),
}


@pytest.mark.parametrize("bounds", ["scalar-bounds", "per-weight-bounds"])
@pytest.mark.parametrize("name", list(_LOCAL_CONNECTIONS_ND))
def test_local_connection_nd_given_weights_and_per_weight_bounds(name, bounds):
"""LocalConnection1D/2D/3D accept a given w and per-weight bounds, clamp w into
them at construction, and keep it there while learning.

Caught: a given w raised AttributeError (the shape check used self.out_channels,
which these classes never set), and per-weight bounds raised "Boolean value of
Tensor with more than one value is ambiguous" (the bound check had no .any()).
"""
cls, src_shape, tgt_shape, kwargs = _LOCAL_CONNECTIONS_ND[name]
shape = cls(Input(shape=src_shape), LIFNodes(shape=tgt_shape), **kwargs).w.shape
n = int(torch.Size(shape).numel())
if bounds == "scalar-bounds":
wmin, wmax = 0.0, 1.0
lo, hi = torch.zeros(shape), torch.ones(shape)
else:
# a different upper bound for each weight
wmin, wmax = torch.zeros(shape), torch.linspace(0.2, 1.0, n).view(shape)
lo, hi = wmin, wmax
w = torch.linspace(-1.0, 2.0, n).view(shape) # values outside the bounds

source = Input(shape=src_shape, traces=True)
target = LIFNodes(shape=tgt_shape, traces=True)
conn = cls(
source,
target,
w=w,
wmin=wmin,
wmax=wmax,
update_rule=PostPre,
nu=(1.0, 1.0),
**kwargs,
)
assert ((conn.w >= lo) & (conn.w <= hi)).all()

net = Network(dt=1.0)
net.add_layer(source, "in")
net.add_layer(target, "out")
net.add_connection(conn, "in", "out")
torch.manual_seed(0)
net.run(inputs={"in": torch.rand(20, 1, *src_shape) < 0.5}, time=20)
assert ((conn.w >= lo) & (conn.w <= hi)).all()


@pytest.mark.parametrize("name", list(_LOCAL_CONNECTIONS_ND))
def test_local_connection_nd_random_weights_within_per_weight_bounds(name):
"""Without a given w, LocalConnection1D/2D/3D start inside per-weight bounds.

Caught: per-weight bounds raised "Boolean value of Tensor with more than one
value is ambiguous" (the bound check had no .any()).
"""
cls, src_shape, tgt_shape, kwargs = _LOCAL_CONNECTIONS_ND[name]
shape = cls(Input(shape=src_shape), LIFNodes(shape=tgt_shape), **kwargs).w.shape
wmax = torch.linspace(0.1, 0.5, int(torch.Size(shape).numel())).view(shape)
torch.manual_seed(0)
conn = cls(
Input(shape=src_shape),
LIFNodes(shape=tgt_shape),
wmin=torch.zeros(shape),
wmax=wmax,
**kwargs,
)
assert ((conn.w >= 0) & (conn.w <= wmax)).all()
Loading