diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ab896a..db97827 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/scripts/ssn/train_bm.py b/scripts/ssn/train_bm.py index c7d536d..c2d5320 100644 --- a/scripts/ssn/train_bm.py +++ b/scripts/ssn/train_bm.py @@ -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}") diff --git a/scripts/symm_net/sweep.py b/scripts/symm_net/sweep.py index 4f87db3..1610771 100644 --- a/scripts/symm_net/sweep.py +++ b/scripts/symm_net/sweep.py @@ -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: diff --git a/spiking_microcircuits/src/microcircuits/experiment.py b/spiking_microcircuits/src/microcircuits/experiment.py index a605abc..8925dac 100644 --- a/spiking_microcircuits/src/microcircuits/experiment.py +++ b/spiking_microcircuits/src/microcircuits/experiment.py @@ -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: diff --git a/symmnet/src/symmnet/conv_net.py b/symmnet/src/symmnet/conv_net.py index 82562d8..2155861 100755 --- a/symmnet/src/symmnet/conv_net.py +++ b/symmnet/src/symmnet/conv_net.py @@ -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 = [] diff --git a/symmnet/src/symmnet/rdd_net.py b/symmnet/src/symmnet/rdd_net.py index 0d7f267..461f3fd 100755 --- a/symmnet/src/symmnet/rdd_net.py +++ b/symmnet/src/symmnet/rdd_net.py @@ -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) @@ -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 @@ -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: """ @@ -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() diff --git a/symmnet/src/symmnet/sal_net.py b/symmnet/src/symmnet/sal_net.py index b850e33..998cb07 100644 --- a/symmnet/src/symmnet/sal_net.py +++ b/symmnet/src/symmnet/sal_net.py @@ -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): """ @@ -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) ) @@ -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: @@ -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: @@ -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: @@ -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)