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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Versions correspond to arXiv submissions of the paper.

---

## [v0.3.0-arxiv_v3] - TODO: DATE!
## [v0.3.0-arxiv_v3] - 2026-05-11
### Added
- Test suite to test main simulation scripts in `tests/`
- `scripts/symm_net/scatter_stdwi_rdd_sal.ipynb` to reproduce paper figure no. 7
Expand Down
1 change: 0 additions & 1 deletion scripts/ssn/train_bm.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
if not params_file.exists():
with open(params_file, "w") as f:
yaml.dump(params, f)
# FIXME: could this lead to race conditions, if executed at the same time??

print(f"sweep: {sweep_id} seed: {seed_id}")
print(f"output dir: {outdir}")
Expand Down
3 changes: 1 addition & 2 deletions scripts/symm_net/sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@

ALL_DATASETS = ["cifar10", "fmnist", "svhn"]
ALL_ALGOS = ["bp", "fa", "bp_w_fa", "akrout", "scfa", "sal", "rdd"]
# PARAM_FILE = "exp_settings.yaml"
PARAM_FILE = "fast_exp.yaml"
PARAM_FILE = "exp_settings.yaml"


def parse_args() -> argparse.Namespace:
Expand Down
1 change: 0 additions & 1 deletion spiking_microcircuits/src/microcircuits/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ def __dir__(self) -> list[str]:
class PropertiesDescriptor(Descriptor):
"""Parameter dict with automatic sanity checks on required attributes."""

# TODO fill all attributes!
_REQUIRED_ATTR = {}

def __init__(self, **kwargs: Any) -> None:
Expand Down
1 change: 0 additions & 1 deletion symmnet/src/symmnet/conv_net.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ def __init__(
self.feature_layers.append(nn.MaxPool2d(kernel_size=2, stride=2))

# Flatten features for fully connected layers
# TODO: I changed this here! Does it still work??
self.feature_layers.append(nn.Flatten(start_dim=1))

self.classification_layers = []
Expand Down
92 changes: 20 additions & 72 deletions symmnet/src/symmnet/rdd_net.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@


class RDDNetBase:
"""This is new!"""
"""Base class for RDD-based spiking networks of arbitrary depth.

Builds a chain of SpikingFA layers and implements the shared forward pass,
reset, and feedback-weight update logic. Subclasses add weight I/O with
external PyTorch models (see RDDNet).
"""

def __init__(self, layer_dims: list[int]) -> None:
self.n_layers = len(layer_dims)
Expand All @@ -23,8 +28,14 @@ def __init__(self, layer_dims: list[int]) -> None:
)
self.classification_layers.append(SpikingFA(layer_dims[-1], layer_dims[-2]))

def out(self, *args: npt.NDArray) -> None:
"""args must be the driving_spike_hist"""
def out(self, *args: npt.NDArray | None) -> None:
"""Update all layers for one timestep given external driving inputs.

Args:
*args: One driving spike history array per non-output layer, i.e.
``len(args) == len(classification_layers) - 1``. Pass ``None``
for layers that receive no external drive at this timestep.
"""
assert len(args) == len(self.classification_layers) - 1

# Layer 0: receives external drive and feedback from layer 1
Expand Down Expand Up @@ -60,35 +71,22 @@ def update_fb_weights(self) -> None:
layer.update_fb_weights()


class RDDNet:
class RDDNet(RDDNetBase):
"""
Network composed of multiple SpikingFA layers, each optionally using
Regression Discontinuity Design (RDD) logic for causal inference of feedback
weigts.
weights.

This class provides methods to copy weights between PyTorch layers and
the SpikingFA layers, perform sequential updates, and manage feedback weights.
Extends RDDNetBase with methods to copy weights between PyTorch layers and
the SpikingFA layers.

Attributes:
classification_layers (list): List of SpikingFA layers representing the network.

TODO: make this inherit from RDDNetBase!
"""

def __init__(self, layer_dims: list[int]) -> None:
"""
Initializes the RDDNet with a fixed architecture of SpikingFA layers.
"""
self.classification_layers = []

self.classification_layers.append(SpikingFA(layer_dims[0], None, layer_dims[1]))
self.classification_layers.append(
SpikingFA(layer_dims[1], layer_dims[0], layer_dims[2])
)
self.classification_layers.append(
SpikingFA(layer_dims[2], layer_dims[1], layer_dims[3])
)
self.classification_layers.append(SpikingFA(layer_dims[3], layer_dims[2]))
"""Initializes the RDDNet with SpikingFA layers."""
super().__init__(layer_dims)

def copy_weights_from(self, layers: list[nn.Module]) -> None:
"""
Expand Down Expand Up @@ -139,53 +137,3 @@ def copy_weights_to(
layers[4].fb_weight.data = torch.from_numpy(
self.classification_layers[2].fb_weight.astype(np.float32).T
).to(device)

def out(
self,
driving_spike_hist_1: npt.NDArray | None,
driving_spike_hist_2: npt.NDArray | None,
driving_spike_hist_3: npt.NDArray | None,
) -> None:
"""
Sequentially updates each SpikingFA layer given the driving spike histories
and the spike histories of adjacent layers.

Args:
driving_spike_hist_1 (np.ndarray): External driving input for the first layer.
driving_spike_hist_2 (np.ndarray): External driving input for the second layer.
driving_spike_hist_3 (np.ndarray): External driving input for the third layer.
"""
# Layer 0: receives external drive and feedback from layer 1
self.classification_layers[0].update(
None,
self.classification_layers[1].spike_hist,
driving_input=driving_spike_hist_1,
)
# Layer 1: receives feedforward from layer 0, feedback from layer 2, and external drive
self.classification_layers[1].update(
self.classification_layers[0].spike_hist,
self.classification_layers[2].spike_hist,
driving_input=driving_spike_hist_2,
)
# Layer 2: receives feedforward from layer 1, feedback from layer 3, and external drive
self.classification_layers[2].update(
self.classification_layers[1].spike_hist,
self.classification_layers[3].spike_hist,
driving_input=driving_spike_hist_3,
)
# Layer 3: receives feedforward from layer 2, no feedback, no external drive
self.classification_layers[3].update(self.classification_layers[2].spike_hist)

def reset(self) -> None:
"""
Resets the state of all SpikingFA layers (membrane potentials, spike history, etc.).
"""
for layer in self.classification_layers:
layer.reset()

def update_fb_weights(self) -> None:
"""
Updates the feedback weights in all but the last SpikingFA layer.
"""
for layer in self.classification_layers[:-1]:
layer.update_fb_weights()
15 changes: 5 additions & 10 deletions symmnet/src/symmnet/sal_net.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@

from .utils import batched_outer

# TODO: add a non-rolling buffer and an all-at-once stdp rule
# TODO: maybe register all relevant model params (time constants etc.) as
# buffers with self.register_buffer?? --> makes it easier to log an entire model
# for reporducibility


class BaseSpikeBuffer(ABC, nn.Module):
"""
Expand Down Expand Up @@ -148,7 +143,7 @@ def __init__(
self.register_buffer("mem_pot", torch.zeros(batch_size, n_out))
self.spikes = SimpleSpikeBuffer(
n_out, buffer_length * t_ref, batch_size=batch_size
) # TODO: change buffer length!
)
self.register_buffer(
"last_spike_counter", torch.full_like(self.mem_pot, torch.inf)
)
Expand All @@ -165,7 +160,7 @@ def init_params(self) -> None:
# initialize params (although they're actually copied from the corresponding ANN)
nn.init.kaiming_uniform_(self.weight, a=5**0.5)
nn.init.kaiming_uniform_(self.fb_weight, a=5**0.5)
bound = self.n_out**-0.5 # TODO check if this is useful
bound = self.n_out**-0.5
nn.init.uniform_(self.bias, -bound, bound)

def update_mempot(self, bottom_up_spikes: Tensor, top_down_spikes: Tensor) -> None:
Expand Down Expand Up @@ -239,7 +234,7 @@ def __init__(
def init_params(self) -> None:
# initialize params (although they're actually copied from the corresponding ANN)
nn.init.kaiming_uniform_(self.fb_weight, a=5**0.5)
bound = self.n_out**-0.5 # TODO check if this is useful
bound = self.n_out**-0.5
nn.init.uniform_(self.bias, -bound, bound)

def update_mempot(self, top_down_spikes: Tensor) -> None:
Expand Down Expand Up @@ -276,7 +271,7 @@ def __init__(
def init_params(self) -> None:
# initialize params (although they're actually copied from the corresponding ANN)
nn.init.kaiming_uniform_(self.weight, a=5**0.5)
bound = self.n_out**-0.5 # TODO check if this is useful
bound = self.n_out**-0.5
nn.init.uniform_(self.bias, -bound, bound)

def update_mempot(self, bottom_up_spikes: Tensor) -> None:
Expand Down Expand Up @@ -372,7 +367,7 @@ def fb_stdp_online(self) -> None:
self.layers[i].fb_stdp_online(self.layers[i + 1].spikes.get())

def apply_fb_weight_update(self) -> list[Tensor]:
dws: list[Tensor] = [] # TODO: for debugging
dws: list[Tensor] = []
for layer in self.layers[:-1]:
dw = layer.apply_fb_weight_update()
dws.append(dw)
Expand Down
Loading