From a965554eae853abfa00482196026a61bb6f61a85 Mon Sep 17 00:00:00 2001 From: "Christian Y. Brenninkmeijer" Date: Thu, 12 Feb 2026 17:10:40 +0000 Subject: [PATCH 1/7] get_parameters method --- .../connectors/abstract_connector.py | 52 ++- .../connectors/all_but_me_connector.py | 9 +- .../connectors/all_to_all_connector.py | 8 +- .../connectors/array_connector.py | 8 +- .../connectors/convolution_connector.py | 28 +- .../connectors/csa_connector.py | 8 +- ...istance_dependent_probability_connector.py | 24 +- .../connectors/fixed_number_post_connector.py | 11 +- .../connectors/fixed_number_pre_connector.py | 11 +- .../connectors/fixed_probability_connector.py | 10 +- .../connectors/from_list_connector.py | 30 +- .../index_based_probability_connector.py | 10 +- .../connectors/kernel_connector.py | 22 +- .../connectors/multapse_connector.py | 11 +- .../connectors/one_to_one_connector.py | 7 +- .../connectors/one_to_one_offset_connector.py | 10 +- .../connectors/pool_dense_connector.py | 12 +- .../connectors/small_world_connector.py | 11 +- .../test_various_connectors.py | 318 ++++++++++++++++++ 19 files changed, 561 insertions(+), 39 deletions(-) create mode 100644 unittests/connector_tests/test_various_connectors.py diff --git a/spynnaker/pyNN/models/neural_projections/connectors/abstract_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/abstract_connector.py index 988746a6ff..e31b99f0e1 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/abstract_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/abstract_connector.py @@ -15,13 +15,14 @@ import logging import math import re -from typing import Dict, Optional, Sequence, Tuple, Union, TYPE_CHECKING +from typing import Any, Dict, Optional, Sequence, Tuple, Union, TYPE_CHECKING from typing_extensions import Never import numpy from numpy import float64, uint32, uint16, uint8 from numpy.typing import NDArray +from pyNN import descriptions from pyNN.random import NumpyRNG, RandomDistribution from pyNN.space import Space @@ -679,3 +680,52 @@ def validate_connection( "Using a projection where the source or target is a " "PopulationView on a multi-dimensional Population is not " "supported") + + def get_parameters(self) -> Dict[str, Any]: + """ + A list of parameters that would recreate this connector. + + May not be exactly the same as the ones used to construct this. + + Will also not include any information or changes added by later calls. + + :return: A map of the init parameters to the values passed in. + """ + # The default is error + # This to avoid missing parameters in user Connnectors + raise NotImplementedError( + f"{type(self)} does not implement " + f"Standard pyNN get_parameters method") + + def _get_parameters(self) -> Dict[str, Any]: + """ + :return: A map of the init parameters to the values passed in. + """ + return { + "safe": self.safe, + "verbose": self.verbose, + "callback": None + } + + def clone(self) -> "AbstractConnector": + """ + Create a clone of the Connector at init point + """ + theType = type(self) + params = self.get_parameters() + return theType(**params) + + def describe(self, template: Optional[str] = None, + engine: str = 'default') -> str: + """ + Returns a human-readable description of the connection method. + + The output may be customized by specifying a different template + togther with an associated template engine (see pyNN.descriptions). + + If template is None, + then a dictionary containing the template context will be returned. + """ + context = {"Type": self.__class__.__name__} + context.update(self.get_parameters()) + return descriptions.render(engine, template, context) diff --git a/spynnaker/pyNN/models/neural_projections/connectors/all_but_me_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/all_but_me_connector.py index 4dfed89e25..25b64a25eb 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/all_but_me_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/all_but_me_connector.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations -from typing import Sequence, Optional, TYPE_CHECKING +from typing import Any, Dict, Sequence, Optional, TYPE_CHECKING import numpy from numpy import uint32 @@ -92,6 +92,13 @@ def __init__(self, n_neurons_per_group: Optional[int] = None, self.__weights = weights self.__check_weights(weights, n_neurons_per_group) + @overrides(AbstractGenerateConnectorOnMachine.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + parameters = self._get_parameters() + parameters["n_neurons_per_group"] = self.__n_neurons_per_group + parameters["weights"] = self.__weights + return parameters + def __check_weights(self, weights: Optional[NDArray[numpy.float64]], n_neurons_per_group: Optional[int]) -> None: if weights is not None and n_neurons_per_group is not None: diff --git a/spynnaker/pyNN/models/neural_projections/connectors/all_to_all_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/all_to_all_connector.py index a6ca81993f..2d1e977662 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/all_to_all_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/all_to_all_connector.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations -from typing import Sequence, Optional, TYPE_CHECKING +from typing import Any, Dict, Optional, Sequence, TYPE_CHECKING import numpy from numpy import uint32 @@ -65,6 +65,12 @@ def __init__(self, allow_self_connections: bool = True, safe: bool = True, super().__init__(safe, callback, verbose) self.__allow_self_connections = allow_self_connections + @overrides(AbstractGenerateConnectorOnMachine.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + parameters = self._get_parameters() + parameters["allow_self_connections"] = self.allow_self_connections + return parameters + @overrides(AbstractConnector.get_delay_maximum) def get_delay_maximum(self, synapse_info: SynapseInformation) -> float: return self._get_delay_maximum( diff --git a/spynnaker/pyNN/models/neural_projections/connectors/array_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/array_connector.py index dd3f99d754..198377a76a 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/array_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/array_connector.py @@ -13,7 +13,7 @@ # limitations under the License. from __future__ import annotations -from typing import Sequence, Optional, TYPE_CHECKING +from typing import Any, Dict, Optional, Sequence, TYPE_CHECKING import numpy from numpy import uint8 @@ -76,6 +76,12 @@ def __init__(self, array: NDArray[uint8], safe: bool = True, self.__n_total_connections = n_total_connections self.__array_dims = dims + @overrides(AbstractConnector.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + parameters = self._get_parameters() + parameters["array"] = self.__array + return parameters + @overrides(AbstractConnector.get_delay_maximum) def get_delay_maximum(self, synapse_info: SynapseInformation) -> float: return self._get_delay_maximum( diff --git a/spynnaker/pyNN/models/neural_projections/connectors/convolution_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/convolution_connector.py index 873ce8d1fb..5af2808ca5 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/convolution_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/convolution_connector.py @@ -17,7 +17,7 @@ from __future__ import annotations from collections.abc import (Iterable, Sequence) from typing import ( - List, Optional, Sequence as TSequence, Tuple, Union, + Any, Dict, List, Optional, Sequence as TSequence, Tuple, Union, cast, overload, TYPE_CHECKING) import numpy @@ -169,10 +169,34 @@ def __init__(self, kernel_weights: _Weights, self.__pool_stride = self.__pool_shape if self.__pool_shape is not None: self.__kernel_weights /= numpy.prod(self.__pool_shape) - self.__positive_receptor_type = positive_receptor_type self.__negative_receptor_type = negative_receptor_type + @overrides(AbstractConnector.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + parameters = self._get_parameters() + if self.__pool_shape is None: + parameters["kernel_weights"] = self.__kernel_weights + else: + parameters["kernel_weights"] = ( + self.__kernel_weights * numpy.prod(self.__pool_shape)) + # Now included in weights + parameters["kernel_shape"] = None + parameters["strides"] = tuple(self.__strides) + parameters["padding"] = tuple(self.__padding_shape) + if self.__pool_shape is None: + parameters["pool_shape"] = None + else: + parameters["pool_shape"] = tuple(self.__pool_shape) + if self.__pool_stride is None: + parameters["pool_stride"] = None + else: + parameters["pool_stride"] = tuple(self.__pool_stride) + parameters["positive_receptor_type"] = self.__positive_receptor_type + parameters["negative_receptor_type"] = self.__negative_receptor_type + parameters["filter_edges"] = self.__filter_edges + return parameters + @property def positive_receptor_type(self) -> str: """ diff --git a/spynnaker/pyNN/models/neural_projections/connectors/csa_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/csa_connector.py index aa3d6ddeaf..2df2eecead 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/csa_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/csa_connector.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations -from typing import List, Optional, Tuple, TYPE_CHECKING, Sequence +from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING, Sequence import numpy from numpy.typing import NDArray @@ -81,6 +81,12 @@ def __init__(self, cset: CSet, safe: bool = True, callback: None = None, self.__full_connection_set: Optional[List[CSet]] = None self.__full_cset: Optional[List[CSet]] = None + @overrides(AbstractConnector.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + parameters = self._get_parameters() + parameters["cset"] = self.__cset + return parameters + @overrides(AbstractConnector.get_delay_maximum) def get_delay_maximum(self, synapse_info: SynapseInformation) -> float: n_conns_max = synapse_info.n_pre_neurons * synapse_info.n_post_neurons diff --git a/spynnaker/pyNN/models/neural_projections/connectors/distance_dependent_probability_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/distance_dependent_probability_connector.py index 20c2f10846..2d2f9abfab 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/distance_dependent_probability_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/distance_dependent_probability_connector.py @@ -14,7 +14,7 @@ from __future__ import annotations import math -from typing import Optional, Sequence, TYPE_CHECKING +from typing import Any, Dict, Optional, Sequence, TYPE_CHECKING import numpy from numpy import ( @@ -102,6 +102,14 @@ def __init__( "n_connections is not implemented for" " DistanceDependentProbabilityConnector on this platform") + @overrides(AbstractConnector.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + parameters = self._get_parameters() + parameters["d_expression"] = self.d_expression + parameters["allow_self_connections"] = self.__allow_self_connections + parameters["rng"] = self.__rng + return parameters + @overrides(AbstractConnector.set_projection_information) def set_projection_information( self, synapse_info: SynapseInformation) -> None: @@ -131,7 +139,6 @@ def _set_probabilities(self, synapse_info: SynapseInformation) -> None: self.__probs = _d_expr_context.eval(self.__d_expression, d=d) - @property def _probs(self) -> NDArray[floating]: if self.__probs is None: raise ValueError("no projection information set") @@ -144,7 +151,7 @@ def get_delay_maximum(self, synapse_info: SynapseInformation) -> float: get_probable_maximum_selected( synapse_info.n_pre_neurons * synapse_info.n_post_neurons, synapse_info.n_pre_neurons * synapse_info.n_post_neurons, - numpy.amax(self._probs)), + numpy.amax(self._probs())), synapse_info) @overrides(AbstractConnector.get_delay_minimum) @@ -154,7 +161,7 @@ def get_delay_minimum(self, synapse_info: SynapseInformation) -> float: get_probable_minimum_selected( synapse_info.n_pre_neurons * synapse_info.n_post_neurons, synapse_info.n_pre_neurons * synapse_info.n_post_neurons, - numpy.amax(self._probs)), + numpy.amax(self._probs())), synapse_info) @overrides(AbstractConnector.get_n_connections_from_pre_vertex_maximum) @@ -162,7 +169,7 @@ def get_n_connections_from_pre_vertex_maximum( self, n_post_atoms: int, synapse_info: SynapseInformation, min_delay: Optional[float] = None, max_delay: Optional[float] = None) -> int: - max_prob = numpy.amax(self._probs) + max_prob = numpy.amax(self._probs()) n_connections = get_probable_maximum_selected( synapse_info.n_pre_neurons * synapse_info.n_post_neurons, n_post_atoms, max_prob) @@ -181,7 +188,7 @@ def get_n_connections_to_post_vertex_maximum( return get_probable_maximum_selected( synapse_info.n_pre_neurons * synapse_info.n_post_neurons, synapse_info.n_post_neurons, - numpy.amax(self._probs)) + numpy.amax(self._probs())) @overrides(AbstractConnector.get_weight_maximum) def get_weight_maximum(self, synapse_info: SynapseInformation) -> float: @@ -190,14 +197,15 @@ def get_weight_maximum(self, synapse_info: SynapseInformation) -> float: get_probable_maximum_selected( synapse_info.n_pre_neurons * synapse_info.n_post_neurons, synapse_info.n_pre_neurons * synapse_info.n_post_neurons, - numpy.amax(self._probs)), + numpy.amax(self._probs())), synapse_info) @overrides(AbstractGenerateConnectorOnHost.create_synaptic_block) def create_synaptic_block( self, post_slices: Sequence[Slice], post_vertex_slice: Slice, synapse_type: int, synapse_info: SynapseInformation) -> NDArray: - probs = self._probs[:, post_vertex_slice.get_raster_ids()].reshape(-1) + probs = self._probs()[ + :, post_vertex_slice.get_raster_ids()].reshape(-1) n_items = synapse_info.n_pre_neurons * post_vertex_slice.n_atoms items = self.__rng.next(n_items) diff --git a/spynnaker/pyNN/models/neural_projections/connectors/fixed_number_post_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/fixed_number_post_connector.py index bae1dc0bca..fc904faac8 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/fixed_number_post_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/fixed_number_post_connector.py @@ -13,7 +13,7 @@ # limitations under the License. from __future__ import annotations import math -from typing import List, Optional, Sequence, TYPE_CHECKING +from typing import Any, Dict, List, Optional, Sequence, TYPE_CHECKING import numpy from numpy import integer, uint32 @@ -99,6 +99,15 @@ def __init__( self.__post_neurons_set = False self.__rng = rng + @overrides(AbstractGenerateConnectorOnMachine.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + parameters = self._get_parameters() + parameters["n"] = self.__n_post + parameters["allow_self_connections"] = self.allow_self_connections + parameters["with_replacement"] = self.__with_replacement + parameters["rng"] = self.__rng + return parameters + def set_projection_information( self, synapse_info: SynapseInformation) -> None: super().set_projection_information(synapse_info) diff --git a/spynnaker/pyNN/models/neural_projections/connectors/fixed_number_pre_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/fixed_number_pre_connector.py index 91438db8aa..974c1297c8 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/fixed_number_pre_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/fixed_number_pre_connector.py @@ -13,7 +13,7 @@ # limitations under the License. from __future__ import annotations import math -from typing import List, Optional, Sequence, TYPE_CHECKING +from typing import Any, Dict, List, Optional, Sequence, TYPE_CHECKING import numpy from numpy import integer, uint32 @@ -96,6 +96,15 @@ def __init__( self.__pre_neurons: List[NDArray[integer]] = [] self.__rng = rng + @overrides(AbstractGenerateConnectorOnMachine.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + parameters = self._get_parameters() + parameters["n"] = self.__n_pre + parameters["allow_self_connections"] = self.allow_self_connections + parameters["with_replacement"] = self.__with_replacement + parameters["rng"] = self.__rng + return parameters + def set_projection_information( self, synapse_info: SynapseInformation) -> None: super().set_projection_information(synapse_info) diff --git a/spynnaker/pyNN/models/neural_projections/connectors/fixed_probability_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/fixed_probability_connector.py index 39c24f1f59..ad4188c0f4 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/fixed_probability_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/fixed_probability_connector.py @@ -14,7 +14,7 @@ from __future__ import annotations import logging import math -from typing import Optional, Sequence, TYPE_CHECKING +from typing import Any, Dict, Optional, Sequence, TYPE_CHECKING import numpy from numpy.typing import NDArray @@ -100,6 +100,14 @@ def __init__( self.__allow_self_connections = allow_self_connections self.__rng = rng + @overrides(AbstractGenerateConnectorOnMachine.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + parameters = self._get_parameters() + parameters["p_connect"] = self.p_connect + parameters["allow_self_connections"] = self.__allow_self_connections + parameters["rng"] = self.__rng + return parameters + @overrides(AbstractConnector.get_delay_maximum) def get_delay_maximum(self, synapse_info: SynapseInformation) -> float: n_connections = get_probable_maximum_selected( diff --git a/spynnaker/pyNN/models/neural_projections/connectors/from_list_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/from_list_connector.py index d433fc0ad5..791d6740c3 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/from_list_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/from_list_connector.py @@ -16,7 +16,7 @@ from dataclasses import dataclass import logging from typing import ( - Dict, List, Optional, Sequence, Tuple, Union, cast, TYPE_CHECKING) + Any, Dict, List, Optional, Sequence, Tuple, Union, TYPE_CHECKING) import numpy from numpy import floating, integer, int64, uint32 @@ -81,7 +81,7 @@ class FromListConnector(AbstractConnector, AbstractGenerateConnectorOnHost): "__split_conn_list", "__split_post_slices") - def __init__(self, conn_list: Union[None, NDArray, List[Tuple[int, ...]]], + def __init__(self, conn_list: Union[NDArray, List[Tuple[int, ...]]], column_names: Optional[Sequence[str]] = None, *, safe: bool = True, verbose: bool = False, callback: None = None): @@ -119,16 +119,25 @@ def __init__(self, conn_list: Union[None, NDArray, List[Tuple[int, ...]]], self.__split_conn_list: Dict[int, NDArray[integer]] = {} self.__split_post_slices: Optional[List[Slice]] = None - # These are set by the conn_list setter - self.__conn_list: NDArray + # These are set by __setup_using_conn_list self.__sources: NDArray[uint32] self.__targets: NDArray[uint32] self.__delays: Optional[NDArray[floating]] self.__weights: Optional[NDArray[floating]] self.__extra_params: Optional[_ExtraParams] - # Call the conn_list setter, as this sets the internal values - self.conn_list = cast(NDArray, conn_list) # Bug: python/mypy#3004 + if conn_list is None or len(conn_list) == 0: + self.__conn_list = numpy.zeros((0, 2), dtype=uint32) + else: + self.__conn_list = numpy.array(conn_list) + self.__setup_using_conn_list() + + @overrides(AbstractConnector.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + parameters = self._get_parameters() + parameters["conn_list"] = self.conn_list + parameters["column_names"] = self.column_names + return parameters @overrides(AbstractConnector.get_delay_maximum) def get_delay_maximum(self, synapse_info: SynapseInformation) -> float: @@ -373,14 +382,7 @@ def conn_list(self) -> NDArray: """ return self.__conn_list - @conn_list.setter - def conn_list(self, conn_list: Union[ - None, NDArray, List[Tuple[int, ...]]]) -> None: - if conn_list is None or len(conn_list) == 0: - self.__conn_list = numpy.zeros((0, 2), dtype=uint32) - else: - self.__conn_list = numpy.array(conn_list) - + def __setup_using_conn_list(self) -> None: # If the shape of the conn_list is 2D, numpy has been able to create # a 2D array which means every entry has the same number of values. # If this was not possible, raise an exception! diff --git a/spynnaker/pyNN/models/neural_projections/connectors/index_based_probability_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/index_based_probability_connector.py index 091b027b0e..ea3159432f 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/index_based_probability_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/index_based_probability_connector.py @@ -14,7 +14,7 @@ from __future__ import annotations import math -from typing import Optional, Sequence, TYPE_CHECKING +from typing import Any, Dict, Optional, Sequence, TYPE_CHECKING import numpy from numpy import ( @@ -97,6 +97,14 @@ def __init__( self.__allow_self_connections = allow_self_connections self.__probs: Optional[NDArray] = None + @overrides(AbstractConnector.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + parameters = self._get_parameters() + parameters["index_expression"] = self.index_expression + parameters["allow_self_connections"] = self.__allow_self_connections + parameters["rng"] = self.__rng + return parameters + def _update_probs_from_index_expression( self, synapse_info: SynapseInformation) -> NDArray: # note: this only needs to be done once diff --git a/spynnaker/pyNN/models/neural_projections/connectors/kernel_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/kernel_connector.py index c2cb55f5de..0e5203affe 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/kernel_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/kernel_connector.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations -from typing import (Dict, List, Final, Optional, Sequence, Tuple, Union, +from typing import (Any, Dict, List, Final, Optional, Sequence, Tuple, Union, TYPE_CHECKING) import numpy @@ -208,6 +208,26 @@ def __init__( self._post_as_pre: Dict[ Slice, Tuple[NDArray[integer], NDArray[integer]]] = {} + @overrides(AbstractGenerateConnectorOnMachine.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + parameters = self._get_parameters() + parameters["shape_pre"] = [self._pre_h, self._pre_w] + parameters["shape_post"] = [self._post_h, self._post_w] + parameters["shape_kernel"] = (self._kernel_h, self._kernel_w) + parameters["weight_kernel"] = self._krn_weights + parameters["delay_kernel"] = self._krn_delays + parameters["shape_common"] = self._shape_common + parameters["pre_sample_steps_in_post"] = ( + self._pre_step_h, self._pre_step_w) + parameters["pre_start_coords_in_post"] = ( + self._pre_start_h, self._pre_start_w) + parameters["post_sample_steps_in_pre"] = ( + self._post_step_h, self._post_step_w) + parameters["post_start_coords_in_pre"] = ( + self._post_start_h, self._post_start_w) + parameters["space"] = None + return parameters + def __to_post_coords( self, post_vertex_slice: Slice) -> Tuple[ NDArray[integer], NDArray[integer]]: diff --git a/spynnaker/pyNN/models/neural_projections/connectors/multapse_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/multapse_connector.py index 3b37054726..049712b389 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/multapse_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/multapse_connector.py @@ -15,7 +15,7 @@ from __future__ import annotations import math -from typing import Optional, Sequence, TYPE_CHECKING +from typing import Any, Dict, Optional, Sequence, TYPE_CHECKING from numpy import uint32, integer from numpy.typing import NDArray @@ -95,6 +95,15 @@ def __init__(self, n: int, allow_self_connections: bool = True, self.__synapses_per_edge: Optional[NDArray[integer]] = None self.__rng = rng + @overrides(AbstractGenerateConnectorOnMachine.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + parameters = self._get_parameters() + parameters["n"] = self.__num_synapses + parameters["allow_self_connections"] = self.__allow_self_connections + parameters["with_replacement"] = self.__with_replacement + parameters["rng"] = self.__rng + return parameters + def set_projection_information( self, synapse_info: SynapseInformation) -> None: super().set_projection_information(synapse_info) diff --git a/spynnaker/pyNN/models/neural_projections/connectors/one_to_one_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/one_to_one_connector.py index f1414a7797..73acc27dd5 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/one_to_one_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/one_to_one_connector.py @@ -13,7 +13,7 @@ # limitations under the License. from __future__ import annotations import math -from typing import Optional, Sequence, Tuple, TYPE_CHECKING +from typing import Any, Dict, Optional, Sequence, Tuple, TYPE_CHECKING import numpy from numpy import integer, floating, uint32 @@ -56,6 +56,11 @@ class OneToOneConnector(AbstractGenerateConnectorOnMachine, """ __slots__ = () + + @overrides(AbstractGenerateConnectorOnMachine.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + return self._get_parameters() + @overrides(AbstractConnector.get_delay_maximum) def get_delay_maximum(self, synapse_info: SynapseInformation) -> float: return self._get_delay_maximum( diff --git a/spynnaker/pyNN/models/neural_projections/connectors/one_to_one_offset_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/one_to_one_offset_connector.py index 4dd2e9758d..90444b764a 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/one_to_one_offset_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/one_to_one_offset_connector.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. from __future__ import annotations -from typing import Sequence, Optional, TYPE_CHECKING +from typing import Any, Dict, Sequence, Optional, TYPE_CHECKING import numpy from numpy import int32, uint32 @@ -85,6 +85,14 @@ def __init__(self, offset: int, wrap: bool, self.__offset = offset self.__wrap = wrap + @overrides(AbstractGenerateConnectorOnMachine.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + parameters = self._get_parameters() + parameters["n_neurons_per_group"] = self.__n_neurons_per_group + parameters["offset"] = self.__offset + parameters["wrap"] = self.__wrap + return parameters + def __n_connections(self, synapse_info: SynapseInformation) -> int: if self.__wrap: # If there is a wrap, there will always be a next connection diff --git a/spynnaker/pyNN/models/neural_projections/connectors/pool_dense_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/pool_dense_connector.py index cd8dcb5111..8f684de0e4 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/pool_dense_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/pool_dense_connector.py @@ -17,7 +17,7 @@ from __future__ import annotations from collections.abc import Iterable, Sized from typing import ( - Optional, Tuple, Union, cast, TYPE_CHECKING) + Any, Dict, Optional, Tuple, Union, cast, TYPE_CHECKING) import numpy from numpy import integer, floating, float64, uint16, uint32 @@ -114,6 +114,16 @@ def __init__(self, weights: ArrayLike, self.__positive_receptor_type = positive_receptor_type self.__negative_receptor_type = negative_receptor_type + @overrides(AbstractConnector.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + parameters = self._get_parameters() + parameters["weights"] = self.weights + parameters["pool_shape"] = self.__pool_shape + parameters["pool_stride"] = self.__pool_stride + parameters["positive_receptor_type"] = self.__positive_receptor_type + parameters["negative_receptor_type"] = self.__negative_receptor_type + return parameters + @property def positive_receptor_type(self) -> str: """ diff --git a/spynnaker/pyNN/models/neural_projections/connectors/small_world_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/small_world_connector.py index 6064871bd6..b6b34cc17a 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/small_world_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/small_world_connector.py @@ -13,7 +13,7 @@ # limitations under the License. from __future__ import annotations import math -from typing import Optional, Sequence, TYPE_CHECKING +from typing import Any, Dict, Optional, Sequence, TYPE_CHECKING import numpy from numpy.typing import NDArray @@ -97,6 +97,15 @@ def __init__( "n_connections is not implemented for" " SmallWorldConnector on this platform") + @overrides(AbstractConnector.get_parameters) + def get_parameters(self) -> Dict[str, Any]: + parameters = self._get_parameters() + parameters["rewiring"] = self.__rewiring + parameters["degree"] = self.__degree + parameters["n_connections"] = None + parameters["rng"] = self.__rng + return parameters + @overrides(AbstractConnector.set_projection_information) def set_projection_information( self, synapse_info: SynapseInformation) -> None: diff --git a/unittests/connector_tests/test_various_connectors.py b/unittests/connector_tests/test_various_connectors.py new file mode 100644 index 0000000000..4be9c9eea4 --- /dev/null +++ b/unittests/connector_tests/test_various_connectors.py @@ -0,0 +1,318 @@ +# Copyright (c) 2027 The University of Manchester +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import unittest + +import csa +import numpy +from pyNN.random import NumpyRNG + +from spynnaker.pyNN.models.neural_projections.connectors import ( + AllButMeConnector, AllToAllConnector, ArrayConnector, + ConvolutionConnector, CSAConnector, DistanceDependentProbabilityConnector, + FixedNumberPostConnector, FixedNumberPreConnector, + FixedProbabilityConnector, FromListConnector, + IndexBasedProbabilityConnector, KernelConnector, MultapseConnector, + OneToOneConnector, OneToOneOffsetConnector, PoolDenseConnector, + SmallWorldConnector) + + +class TestConnectors(unittest.TestCase): + + def compare_values(self, key, value1, value2): + if isinstance(value1, numpy.ndarray): + numpy.testing.assert_array_equal(value1, value2) + else: + self.assertEqual(value1, value2, key) + + def compare_connectors(self, connector, connector2): + """ + ultra pythonic way of comparing two connectors + + Just enough to pass these tests + """ + members = inspect.getmembers(connector) + members2 = inspect.getmembers(connector2) + for (key1, value1), (key2, value2) in zip(members, members2): + if key1 != key2: + raise AssertionError(f"{key1=}, {key2=}") + if key1.startswith("__") and key2.endswith("__"): + pass + elif inspect.ismethod(value1): + pass + elif inspect.isfunction(value1): + pass + else: + self.compare_values(key1, value1, value2) + + def compare_parameters(self, params, params2): + assert len(params) == len(params2) + for key in params: + self.compare_values(key, params[key], params2[key]) + + def testOneToOneConnector_defaults(self): + connector = OneToOneConnector() + params = connector.get_parameters() + connector2 = OneToOneConnector(**params) + assert connector2.get_parameters() == params + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + connector.describe() + + def testOneToOneConnector_not_defaults(self): + connector = OneToOneConnector(safe=False, verbose=True) + params = connector.get_parameters() + connector2 = OneToOneConnector(**params) + assert connector2.get_parameters() == params + assert connector2.safe == False + assert connector2.verbose == True + # Callback is not interesting + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + + def testAllButMeConnector(self): + weights = numpy.ones((2, 2)) + connector = AllButMeConnector(n_neurons_per_group = 2, weights=weights) + params = connector.get_parameters() + connector2 = AllButMeConnector(**params) + assert connector2.get_parameters() == params + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + + def testAllToAllConnector(self): + connector = AllToAllConnector(allow_self_connections=False) + params = connector.get_parameters() + connector2 = AllToAllConnector(**params) + assert connector2.get_parameters() == params + assert connector2.allow_self_connections == False + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + + def testArrayConnector(self): + array = numpy.array([[1, 2, 3], [4, 5, 6]]) + connector = ArrayConnector(array) + params = connector.get_parameters() + connector2 = ArrayConnector(**params) + assert connector2.get_parameters() == params + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + + def testFixedNumberPostConnector(self): + rng = NumpyRNG(seed=42) + connector = FixedNumberPostConnector( + 5, allow_self_connections = False, with_replacement = True, + rng = rng) + params = connector.get_parameters() + connector2 = FixedNumberPostConnector(**params) + assert connector2.get_parameters() == params + assert connector2.allow_self_connections == False + connector2.allow_self_connections = False + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + + def testFixedNumberPreConnector(self): + rng = NumpyRNG(seed=14) + connector = FixedNumberPreConnector( + 7, allow_self_connections = False, with_replacement = True, + rng = rng) + params = connector.get_parameters() + connector2 = FixedNumberPreConnector(**params) + assert connector2.get_parameters() == params + assert connector2.allow_self_connections == False + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + + def testFixedProbabilityConnectorr(self): + rng = NumpyRNG(seed=14) + connector = FixedProbabilityConnector( + 0.5, allow_self_connections = False, rng = rng) + params = connector.get_parameters() + connector2 = FixedProbabilityConnector(**params) + assert connector2.get_parameters() == params + assert connector2.p_connect == 0.5 + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + + def testKernelConnector(self): + (psh, psw, ksh, ksw) = (32, 16, 3, 3) + shape_pre = [psh, psw] + shape_post = [psh // 2, psw // 2] + shape_kernel = [ksh, ksw] + weight_list = [[7.0 if ((a + b) % 2 == 0) else 5.0 + for a in range(ksw)] for b in range(ksh)] + delay_list = [[20.0 if ((a + b) % 2 == 1) else 10.0 + for a in range(ksw)] for b in range(ksh)] + weight_kernel = numpy.asarray(weight_list) + pre_step = (1, 1) + post_step = (1, 1) + pre_start = (0, 0) + post_start = (0, 0) + connector = KernelConnector( + shape_pre, shape_post, shape_kernel, + weight_kernel=weight_kernel, delay_kernel=delay_list, + pre_sample_steps_in_post=pre_step, + post_sample_steps_in_pre=post_step, + pre_start_coords_in_post=pre_start, + post_start_coords_in_pre=post_start) + params = connector.get_parameters() + connector2 = KernelConnector(**params) + params2 = connector2.get_parameters() + self.compare_parameters(params, params2) + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + connector.describe() + + def testMultapseConnector(self): + rng = NumpyRNG(seed=37) + connector = MultapseConnector( + 4, allow_self_connections = False, with_replacement = False, + rng = rng) + params = connector.get_parameters() + connector2 = MultapseConnector(**params) + assert connector2.get_parameters() == params + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + + def testOneToOneOffsetConnector(self): + connector = OneToOneOffsetConnector(offset=5, wrap=True, n_neurons_per_group=4) + params = connector.get_parameters() + connector2 = OneToOneOffsetConnector(**params) + assert connector2.get_parameters() == params + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + + def testConvolutionConnectorDefaults(self): + k_shape = numpy.array([5, 5], dtype='int32') + kernel = (numpy.arange(numpy.prod( + k_shape)) - (numpy.prod(k_shape) / 2)).reshape(k_shape) * 0.1 + connector = ConvolutionConnector(kernel) + params = connector.get_parameters() + connector2 = ConvolutionConnector(**params) + self.compare_parameters(params, connector2.get_parameters()) + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + + def testConvolutionConnectorWeird(self): + # WARNING the values used here are NOT good use case examples + # They are purely random values to pass in and out + k_shape = numpy.array([5, 5], dtype='int32') + kernel = (numpy.arange(numpy.prod( + k_shape)) - (numpy.prod(k_shape) / 2)).reshape(k_shape) * 0.1 + strides = (3, 3) + connector = ConvolutionConnector( + kernel_weights=kernel, kernel_shape=k_shape, strides=strides, + padding=(2, 3), pool_shape=(4, 5), pool_stride=(4, 3), + positive_receptor_type="gigo1", negative_receptor_type="gigo2", + filter_edges=False) + params = connector.get_parameters() + connector2 = ConvolutionConnector(**params) + self.compare_parameters(params, connector2.get_parameters()) + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + # Remember values used here NOT examples + + def testCSAConnector(self): + connector = CSAConnector(csa.oneToOne) + params = connector.get_parameters() + connector2 = CSAConnector(**params) + self.compare_parameters(params, connector2.get_parameters()) + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + + def testDistanceDependentProbabilityConnector(self): + rng = NumpyRNG(seed=14) + connector = DistanceDependentProbabilityConnector( + d_expression="gigo", allow_self_connections=False, + n_connections=None, rng = rng) + params = connector.get_parameters() + connector2 = DistanceDependentProbabilityConnector(**params) + assert connector2.get_parameters() == params + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + + def testFromListConnectorrSimple(self): + from_list = [[1, 2], [3, 4], [5, 6]] + connector = FromListConnector(from_list) + params = connector.get_parameters() + connector2 = FromListConnector(**params) + self.compare_parameters(params, connector2.get_parameters()) + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + + def testFromListConnectorrNamed(self): + from_list = [[1, 2, 3], [4, 5, 6]] + connector = FromListConnector(from_list, ["weight"]) + params = connector.get_parameters() + connector2 = FromListConnector(**params) + self.compare_parameters(params, connector2.get_parameters()) + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + + def testIndexBasedProbabilityConnector(self): + rng = NumpyRNG(seed=14) + connector = IndexBasedProbabilityConnector( + index_expression="gigo", allow_self_connections=False, + rng=rng) + params = connector.get_parameters() + connector2 = IndexBasedProbabilityConnector(**params) + assert connector2.get_parameters() == params + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + + def testPoolDenseConnector(self): + # WARNING the values used here are NOT good use case examples + # They are purely random values to pass in and out + shape = numpy.array([5, 5]) + stride = numpy.array([1, 2, 3]) + connector = PoolDenseConnector( + [0, 200, 0], shape, stride, "gigo1", "gigo2") + params = connector.get_parameters() + connector2 = PoolDenseConnector(**params) + self.compare_parameters(params, connector2.get_parameters()) + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + # Remember values used here NOT examples + + def testSmallWorldConnector(self): + # WARNING the values used here are NOT good use case examples + # They are purely random values to pass in and out + rng = NumpyRNG(seed=13) + connector = SmallWorldConnector(1.2, 3.4, True, None, rng) + params = connector.get_parameters() + connector2 = SmallWorldConnector(**params) + self.compare_parameters(params, connector2.get_parameters()) + self.compare_connectors(connector, connector2) + connector2 = connector.clone() + self.compare_connectors(connector, connector2) + # Remember values used here NOT examples From 16e856e9fa7e9e7e0238ea9a2e180a5b01ab1e0a Mon Sep 17 00:00:00 2001 From: "Christian Y. Brenninkmeijer" Date: Mon, 16 Feb 2026 12:08:01 +0000 Subject: [PATCH 2/7] mpyp fixes --- pyproject.toml | 2 +- .../connectors/from_list_connector.py | 1 + .../test_various_connectors.py | 144 +++++++++--------- 3 files changed, 75 insertions(+), 72 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 347e9ace24..bc2e57566d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,5 +17,5 @@ requires = ["setuptools"] build-backend = "setuptools.build_meta" [[tool.mypy.overrides]] -module = ["matplotlib", "matplotlib.*", "pyNN.*", "quantities", "neo", "neo.*", "scipy", "scipy.*", "lazyarray"] +module = ["csa", "docstring_parser", "matplotlib", "matplotlib.*", "pyNN.*", "quantities", "neo", "neo.*", "scipy", "scipy.*", "lazyarray"] ignore_missing_imports = true diff --git a/spynnaker/pyNN/models/neural_projections/connectors/from_list_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/from_list_connector.py index 791d6740c3..af1ad9fa4f 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/from_list_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/from_list_connector.py @@ -119,6 +119,7 @@ def __init__(self, conn_list: Union[NDArray, List[Tuple[int, ...]]], self.__split_conn_list: Dict[int, NDArray[integer]] = {} self.__split_post_slices: Optional[List[Slice]] = None + self.__conn_list: NDArray # These are set by __setup_using_conn_list self.__sources: NDArray[uint32] self.__targets: NDArray[uint32] diff --git a/unittests/connector_tests/test_various_connectors.py b/unittests/connector_tests/test_various_connectors.py index 4be9c9eea4..cf7d8fcee5 100644 --- a/unittests/connector_tests/test_various_connectors.py +++ b/unittests/connector_tests/test_various_connectors.py @@ -13,6 +13,7 @@ # limitations under the License. import inspect +from typing import Any, Dict, List, Tuple import unittest import csa @@ -20,7 +21,7 @@ from pyNN.random import NumpyRNG from spynnaker.pyNN.models.neural_projections.connectors import ( - AllButMeConnector, AllToAllConnector, ArrayConnector, + AbstractConnector, AllButMeConnector, AllToAllConnector, ArrayConnector, ConvolutionConnector, CSAConnector, DistanceDependentProbabilityConnector, FixedNumberPostConnector, FixedNumberPreConnector, FixedProbabilityConnector, FromListConnector, @@ -31,13 +32,14 @@ class TestConnectors(unittest.TestCase): - def compare_values(self, key, value1, value2): + def compare_values(self, key: str, value1: Any, value2: Any) -> None: if isinstance(value1, numpy.ndarray): numpy.testing.assert_array_equal(value1, value2) else: self.assertEqual(value1, value2, key) - def compare_connectors(self, connector, connector2): + def compare_connectors(self, connector: AbstractConnector, + connector2: AbstractConnector) -> None: """ ultra pythonic way of comparing two connectors @@ -57,22 +59,23 @@ def compare_connectors(self, connector, connector2): else: self.compare_values(key1, value1, value2) - def compare_parameters(self, params, params2): + def compare_parameters( + self, params: Dict[str, Any], params2: Dict[str, Any]) -> None: assert len(params) == len(params2) for key in params: self.compare_values(key, params[key], params2[key]) - def testOneToOneConnector_defaults(self): + def testOneToOneConnector_defaults(self) -> None: connector = OneToOneConnector() params = connector.get_parameters() connector2 = OneToOneConnector(**params) assert connector2.get_parameters() == params self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) connector.describe() - def testOneToOneConnector_not_defaults(self): + def testOneToOneConnector_not_defaults(self) -> None: connector = OneToOneConnector(safe=False, verbose=True) params = connector.get_parameters() connector2 = OneToOneConnector(**params) @@ -81,40 +84,40 @@ def testOneToOneConnector_not_defaults(self): assert connector2.verbose == True # Callback is not interesting self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) - def testAllButMeConnector(self): + def testAllButMeConnector(self) -> None: weights = numpy.ones((2, 2)) connector = AllButMeConnector(n_neurons_per_group = 2, weights=weights) params = connector.get_parameters() connector2 = AllButMeConnector(**params) assert connector2.get_parameters() == params self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) - def testAllToAllConnector(self): + def testAllToAllConnector(self) -> None: connector = AllToAllConnector(allow_self_connections=False) params = connector.get_parameters() connector2 = AllToAllConnector(**params) assert connector2.get_parameters() == params assert connector2.allow_self_connections == False self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) - def testArrayConnector(self): + def testArrayConnector(self) -> None: array = numpy.array([[1, 2, 3], [4, 5, 6]]) connector = ArrayConnector(array) params = connector.get_parameters() connector2 = ArrayConnector(**params) assert connector2.get_parameters() == params self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) - def testFixedNumberPostConnector(self): + def testFixedNumberPostConnector(self) -> None: rng = NumpyRNG(seed=42) connector = FixedNumberPostConnector( 5, allow_self_connections = False, with_replacement = True, @@ -125,10 +128,10 @@ def testFixedNumberPostConnector(self): assert connector2.allow_self_connections == False connector2.allow_self_connections = False self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) - def testFixedNumberPreConnector(self): + def testFixedNumberPreConnector(self) -> None: rng = NumpyRNG(seed=14) connector = FixedNumberPreConnector( 7, allow_self_connections = False, with_replacement = True, @@ -138,10 +141,10 @@ def testFixedNumberPreConnector(self): assert connector2.get_parameters() == params assert connector2.allow_self_connections == False self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) - def testFixedProbabilityConnectorr(self): + def testFixedProbabilityConnectorr(self) -> None: rng = NumpyRNG(seed=14) connector = FixedProbabilityConnector( 0.5, allow_self_connections = False, rng = rng) @@ -150,10 +153,10 @@ def testFixedProbabilityConnectorr(self): assert connector2.get_parameters() == params assert connector2.p_connect == 0.5 self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) - def testKernelConnector(self): + def testKernelConnector(self) -> None: (psh, psw, ksh, ksw) = (32, 16, 3, 3) shape_pre = [psh, psw] shape_post = [psh // 2, psw // 2] @@ -179,11 +182,10 @@ def testKernelConnector(self): params2 = connector2.get_parameters() self.compare_parameters(params, params2) self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) - connector.describe() + connector3 = connector.clone() + self.compare_connectors(connector, connector3) - def testMultapseConnector(self): + def testMultapseConnector(self) -> None: rng = NumpyRNG(seed=37) connector = MultapseConnector( 4, allow_self_connections = False, with_replacement = False, @@ -192,19 +194,19 @@ def testMultapseConnector(self): connector2 = MultapseConnector(**params) assert connector2.get_parameters() == params self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) - def testOneToOneOffsetConnector(self): + def testOneToOneOffsetConnector(self) -> None: connector = OneToOneOffsetConnector(offset=5, wrap=True, n_neurons_per_group=4) params = connector.get_parameters() connector2 = OneToOneOffsetConnector(**params) assert connector2.get_parameters() == params self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) - def testConvolutionConnectorDefaults(self): + def testConvolutionConnectorDefaults(self) -> None: k_shape = numpy.array([5, 5], dtype='int32') kernel = (numpy.arange(numpy.prod( k_shape)) - (numpy.prod(k_shape) / 2)).reshape(k_shape) * 0.1 @@ -213,18 +215,18 @@ def testConvolutionConnectorDefaults(self): connector2 = ConvolutionConnector(**params) self.compare_parameters(params, connector2.get_parameters()) self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) - def testConvolutionConnectorWeird(self): + def testConvolutionConnectorWeird(self) -> None: # WARNING the values used here are NOT good use case examples # They are purely random values to pass in and out - k_shape = numpy.array([5, 5], dtype='int32') + k_shape = (5, 5) kernel = (numpy.arange(numpy.prod( k_shape)) - (numpy.prod(k_shape) / 2)).reshape(k_shape) * 0.1 strides = (3, 3) connector = ConvolutionConnector( - kernel_weights=kernel, kernel_shape=k_shape, strides=strides, + kernel_weights=kernel, kernel_shape=(5, 5), strides=strides, padding=(2, 3), pool_shape=(4, 5), pool_stride=(4, 3), positive_receptor_type="gigo1", negative_receptor_type="gigo2", filter_edges=False) @@ -232,20 +234,20 @@ def testConvolutionConnectorWeird(self): connector2 = ConvolutionConnector(**params) self.compare_parameters(params, connector2.get_parameters()) self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) # Remember values used here NOT examples - def testCSAConnector(self): + def testCSAConnector(self) -> None: connector = CSAConnector(csa.oneToOne) params = connector.get_parameters() connector2 = CSAConnector(**params) self.compare_parameters(params, connector2.get_parameters()) self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) - def testDistanceDependentProbabilityConnector(self): + def testDistanceDependentProbabilityConnector(self) -> None: rng = NumpyRNG(seed=14) connector = DistanceDependentProbabilityConnector( d_expression="gigo", allow_self_connections=False, @@ -254,30 +256,30 @@ def testDistanceDependentProbabilityConnector(self): connector2 = DistanceDependentProbabilityConnector(**params) assert connector2.get_parameters() == params self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) - def testFromListConnectorrSimple(self): - from_list = [[1, 2], [3, 4], [5, 6]] + def testFromListConnectorrSimple(self) -> None: + from_list: List[Tuple[int, ...]] = [(1, 2), (3, 4), (5, 6)] connector = FromListConnector(from_list) params = connector.get_parameters() connector2 = FromListConnector(**params) self.compare_parameters(params, connector2.get_parameters()) self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) - def testFromListConnectorrNamed(self): - from_list = [[1, 2, 3], [4, 5, 6]] + def testFromListConnectorrNamed(self) -> None: + from_list: List[Tuple[int, ...]] = [(1, 2, 3), (4, 5, 6)] connector = FromListConnector(from_list, ["weight"]) params = connector.get_parameters() connector2 = FromListConnector(**params) self.compare_parameters(params, connector2.get_parameters()) self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) - def testIndexBasedProbabilityConnector(self): + def testIndexBasedProbabilityConnector(self) -> None: rng = NumpyRNG(seed=14) connector = IndexBasedProbabilityConnector( index_expression="gigo", allow_self_connections=False, @@ -286,25 +288,25 @@ def testIndexBasedProbabilityConnector(self): connector2 = IndexBasedProbabilityConnector(**params) assert connector2.get_parameters() == params self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) - def testPoolDenseConnector(self): + def testPoolDenseConnector(self) -> None: # WARNING the values used here are NOT good use case examples # They are purely random values to pass in and out - shape = numpy.array([5, 5]) - stride = numpy.array([1, 2, 3]) + shape = 1 + stride = 2 connector = PoolDenseConnector( [0, 200, 0], shape, stride, "gigo1", "gigo2") params = connector.get_parameters() connector2 = PoolDenseConnector(**params) self.compare_parameters(params, connector2.get_parameters()) self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) # Remember values used here NOT examples - def testSmallWorldConnector(self): + def testSmallWorldConnector(self) -> None: # WARNING the values used here are NOT good use case examples # They are purely random values to pass in and out rng = NumpyRNG(seed=13) @@ -313,6 +315,6 @@ def testSmallWorldConnector(self): connector2 = SmallWorldConnector(**params) self.compare_parameters(params, connector2.get_parameters()) self.compare_connectors(connector, connector2) - connector2 = connector.clone() - self.compare_connectors(connector, connector2) + connector3 = connector.clone() + self.compare_connectors(connector, connector3) # Remember values used here NOT examples From 7efa13bcf501c2b2db0d70c22defa65482900c5d Mon Sep 17 00:00:00 2001 From: "Christian Y. Brenninkmeijer" Date: Mon, 16 Feb 2026 12:18:06 +0000 Subject: [PATCH 3/7] remove None and empty tests --- unittests/connector_tests/test_from_list_connector.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/unittests/connector_tests/test_from_list_connector.py b/unittests/connector_tests/test_from_list_connector.py index 0a8114db4a..0020d63a81 100644 --- a/unittests/connector_tests/test_from_list_connector.py +++ b/unittests/connector_tests/test_from_list_connector.py @@ -38,8 +38,6 @@ "clist, column_names, weights, delays, expected_clist, expected_weights, " "expected_delays, expected_extra_parameters, " "expected_extra_parameter_names", [ - (None, None, 0, 0, numpy.zeros((0, 2)), [], [], None, None), - ([], None, 0, 0, numpy.zeros((0, 2)), [], [], None, None), (numpy.array([(0, 0, 0, 0), (1, 1, 1, 1), (2, 2, 2, 2)]), None, 5, 1, None, [0, 1, 2], [0, 1, 2], None, None), (numpy.array([(0, 0), (1, 1), (2, 2)]), None, 5, 1, @@ -51,8 +49,6 @@ (numpy.array([(0, 0, 0), (1, 1, 0), (2, 2, 0)]), ["extra"], 5, 1, None, [5, 5, 5], [1, 1, 1], numpy.array([[0], [0], [0]]), ["extra"]), ], ids=[ - "None Connections", - "Empty Connections", "4-elements", "2-elements", "3-elements-weight", @@ -60,7 +56,7 @@ "3-elements-extra" ]) def test_connector( - clist: Optional[NDArray], column_names: Optional[List[str]], + clist: NDArray, column_names: Optional[List[str]], weights: int, delays: int, expected_clist: Optional[NDArray], expected_weights: List[int], expected_delays: List[int], expected_extra_parameters: Optional[NDArray], From 4c613a5e4c46d0f0d53018d89d35e16281989383 Mon Sep 17 00:00:00 2001 From: "Christian Y. Brenninkmeijer" Date: Mon, 16 Feb 2026 14:03:32 +0000 Subject: [PATCH 4/7] flake8 --- .../test_various_connectors.py | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/unittests/connector_tests/test_various_connectors.py b/unittests/connector_tests/test_various_connectors.py index cf7d8fcee5..bc334ffcb7 100644 --- a/unittests/connector_tests/test_various_connectors.py +++ b/unittests/connector_tests/test_various_connectors.py @@ -80,8 +80,8 @@ def testOneToOneConnector_not_defaults(self) -> None: params = connector.get_parameters() connector2 = OneToOneConnector(**params) assert connector2.get_parameters() == params - assert connector2.safe == False - assert connector2.verbose == True + assert connector2.safe is False + assert connector2.verbose is True # Callback is not interesting self.compare_connectors(connector, connector2) connector3 = connector.clone() @@ -89,7 +89,7 @@ def testOneToOneConnector_not_defaults(self) -> None: def testAllButMeConnector(self) -> None: weights = numpy.ones((2, 2)) - connector = AllButMeConnector(n_neurons_per_group = 2, weights=weights) + connector = AllButMeConnector(n_neurons_per_group=2, weights=weights) params = connector.get_parameters() connector2 = AllButMeConnector(**params) assert connector2.get_parameters() == params @@ -100,9 +100,9 @@ def testAllButMeConnector(self) -> None: def testAllToAllConnector(self) -> None: connector = AllToAllConnector(allow_self_connections=False) params = connector.get_parameters() - connector2 = AllToAllConnector(**params) + connector2 = AllToAllConnector(**params) assert connector2.get_parameters() == params - assert connector2.allow_self_connections == False + assert connector2.allow_self_connections is False self.compare_connectors(connector, connector2) connector3 = connector.clone() self.compare_connectors(connector, connector3) @@ -120,12 +120,12 @@ def testArrayConnector(self) -> None: def testFixedNumberPostConnector(self) -> None: rng = NumpyRNG(seed=42) connector = FixedNumberPostConnector( - 5, allow_self_connections = False, with_replacement = True, - rng = rng) + 5, allow_self_connections=False, with_replacement=True, + rng=rng) params = connector.get_parameters() - connector2 = FixedNumberPostConnector(**params) + connector2 = FixedNumberPostConnector(**params) assert connector2.get_parameters() == params - assert connector2.allow_self_connections == False + assert connector2.allow_self_connections is False connector2.allow_self_connections = False self.compare_connectors(connector, connector2) connector3 = connector.clone() @@ -134,12 +134,12 @@ def testFixedNumberPostConnector(self) -> None: def testFixedNumberPreConnector(self) -> None: rng = NumpyRNG(seed=14) connector = FixedNumberPreConnector( - 7, allow_self_connections = False, with_replacement = True, - rng = rng) + 7, allow_self_connections=False, with_replacement=True, + rng=rng) params = connector.get_parameters() connector2 = FixedNumberPreConnector(**params) assert connector2.get_parameters() == params - assert connector2.allow_self_connections == False + assert connector2.allow_self_connections is False self.compare_connectors(connector, connector2) connector3 = connector.clone() self.compare_connectors(connector, connector3) @@ -147,7 +147,7 @@ def testFixedNumberPreConnector(self) -> None: def testFixedProbabilityConnectorr(self) -> None: rng = NumpyRNG(seed=14) connector = FixedProbabilityConnector( - 0.5, allow_self_connections = False, rng = rng) + 0.5, allow_self_connections=False, rng=rng) params = connector.get_parameters() connector2 = FixedProbabilityConnector(**params) assert connector2.get_parameters() == params @@ -188,8 +188,8 @@ def testKernelConnector(self) -> None: def testMultapseConnector(self) -> None: rng = NumpyRNG(seed=37) connector = MultapseConnector( - 4, allow_self_connections = False, with_replacement = False, - rng = rng) + 4, allow_self_connections=False, with_replacement=False, + rng=rng) params = connector.get_parameters() connector2 = MultapseConnector(**params) assert connector2.get_parameters() == params @@ -198,7 +198,8 @@ def testMultapseConnector(self) -> None: self.compare_connectors(connector, connector3) def testOneToOneOffsetConnector(self) -> None: - connector = OneToOneOffsetConnector(offset=5, wrap=True, n_neurons_per_group=4) + connector = OneToOneOffsetConnector( + offset=5, wrap=True, n_neurons_per_group=4) params = connector.get_parameters() connector2 = OneToOneOffsetConnector(**params) assert connector2.get_parameters() == params @@ -212,7 +213,7 @@ def testConvolutionConnectorDefaults(self) -> None: k_shape)) - (numpy.prod(k_shape) / 2)).reshape(k_shape) * 0.1 connector = ConvolutionConnector(kernel) params = connector.get_parameters() - connector2 = ConvolutionConnector(**params) + connector2 = ConvolutionConnector(**params) self.compare_parameters(params, connector2.get_parameters()) self.compare_connectors(connector, connector2) connector3 = connector.clone() @@ -251,7 +252,7 @@ def testDistanceDependentProbabilityConnector(self) -> None: rng = NumpyRNG(seed=14) connector = DistanceDependentProbabilityConnector( d_expression="gigo", allow_self_connections=False, - n_connections=None, rng = rng) + n_connections=None, rng=rng) params = connector.get_parameters() connector2 = DistanceDependentProbabilityConnector(**params) assert connector2.get_parameters() == params From f89ba0e184d9530b6adc85238f68fa00ef84b0de Mon Sep 17 00:00:00 2001 From: "Christian Y. Brenninkmeijer" Date: Mon, 16 Feb 2026 14:23:09 +0000 Subject: [PATCH 5/7] remove blank line --- .../models/neural_projections/connectors/one_to_one_connector.py | 1 - 1 file changed, 1 deletion(-) diff --git a/spynnaker/pyNN/models/neural_projections/connectors/one_to_one_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/one_to_one_connector.py index 73acc27dd5..1f3a13c500 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/one_to_one_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/one_to_one_connector.py @@ -56,7 +56,6 @@ class OneToOneConnector(AbstractGenerateConnectorOnMachine, """ __slots__ = () - @overrides(AbstractGenerateConnectorOnMachine.get_parameters) def get_parameters(self) -> Dict[str, Any]: return self._get_parameters() From c915d7dd519b695ef91caaed65a0a6af158fa1ec Mon Sep 17 00:00:00 2001 From: "Christian Y. Brenninkmeijer" Date: Mon, 16 Feb 2026 14:28:17 +0000 Subject: [PATCH 6/7] spelling --- .../neural_projections/connectors/abstract_connector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spynnaker/pyNN/models/neural_projections/connectors/abstract_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/abstract_connector.py index e31b99f0e1..e81b4dc7b0 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/abstract_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/abstract_connector.py @@ -692,7 +692,7 @@ def get_parameters(self) -> Dict[str, Any]: :return: A map of the init parameters to the values passed in. """ # The default is error - # This to avoid missing parameters in user Connnectors + # This to avoid missing parameters in user Connectors raise NotImplementedError( f"{type(self)} does not implement " f"Standard pyNN get_parameters method") @@ -721,7 +721,7 @@ def describe(self, template: Optional[str] = None, Returns a human-readable description of the connection method. The output may be customized by specifying a different template - togther with an associated template engine (see pyNN.descriptions). + together with an associated template engine (see pyNN.descriptions). If template is None, then a dictionary containing the template context will be returned. From 940984110e7198cdc010ce786c85147cc06fec20 Mon Sep 17 00:00:00 2001 From: "Christian Y. Brenninkmeijer" Date: Mon, 16 Feb 2026 14:41:41 +0000 Subject: [PATCH 7/7] docs and log warning on clone --- .../neural_projections/connectors/abstract_connector.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spynnaker/pyNN/models/neural_projections/connectors/abstract_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/abstract_connector.py index e81b4dc7b0..44ac4dd7de 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/abstract_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/abstract_connector.py @@ -710,9 +710,15 @@ def _get_parameters(self) -> Dict[str, Any]: def clone(self) -> "AbstractConnector": """ Create a clone of the Connector at init point + + This is a best effort attempt and its use is not recommended + + :return: A clone of the Connector. Ignoring any SynapseInformation """ theType = type(self) params = self.get_parameters() + logger.warning(f"Cloning type{self} is not recommended " + f"and may lead to incorrect results.") return theType(**params) def describe(self, template: Optional[str] = None, @@ -725,6 +731,8 @@ def describe(self, template: Optional[str] = None, If template is None, then a dictionary containing the template context will be returned. + + :return: A human-readable description of the connector. """ context = {"Type": self.__class__.__name__} context.update(self.get_parameters())