From d971e9011e26d73fb873acbce286eb290d0a900b Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Mon, 2 Mar 2026 15:58:45 +0000 Subject: [PATCH 01/23] Create and fill in a PyNN Report --- spynnaker/pyNN/data/spynnaker_data_view.py | 29 +++++++++++++++++-- .../connectors/multapse_connector.py | 2 +- .../abstract_pynn_neuron_model_standard.py | 3 ++ .../additional_input_ca2_adaptive.py | 8 +++++ .../abstract_standard_neuron_component.py | 7 +++++ .../implementations/neuron_impl_standard.py | 7 +++++ .../implementations/neuron_impl_stoc_exp.py | 5 ++++ .../neuron_impl_stoc_exp_stable.py | 7 +++++ .../implementations/neuron_impl_stoc_sigma.py | 6 ++++ .../input_types/input_type_conductance.py | 5 ++++ .../neuron/input_types/input_type_current.py | 7 +++++ .../neuron/input_types/input_type_delta.py | 6 ++++ .../neuron_models/neuron_model_if_trunc.py | 12 +++++++- .../neuron/neuron_models/neuron_model_izh.py | 8 +++++ .../neuron_model_leaky_integrate_and_fire.py | 13 ++++++++- .../synapse_dynamics_static.py | 4 +++ .../synapse_types/synapse_type_alpha.py | 12 +++++++- .../synapse_types/synapse_type_delta.py | 9 +++++- .../synapse_type_dual_exponential.py | 12 +++++++- .../synapse_types/synapse_type_exponential.py | 10 ++++++- .../neuron/synapse_types/synapse_type_semd.py | 15 +++++++++- .../threshold_type_fixed_prob.py | 6 +++- .../threshold_type_maass_stochastic.py | 8 +++++ .../threshold_types/threshold_type_static.py | 5 ++++ .../pyNN/models/populations/population.py | 11 +++++++ spynnaker/pyNN/models/projection.py | 5 ++++ .../models/spike_source/spike_source_array.py | 3 ++ .../spike_source/spike_source_poisson.py | 5 ++++ .../spike_source_poisson_variable.py | 6 ++++ .../spike_injector/spike_injector.py | 3 ++ spynnaker/pyNN/spynnaker.cfg | 4 +++ 31 files changed, 232 insertions(+), 11 deletions(-) diff --git a/spynnaker/pyNN/data/spynnaker_data_view.py b/spynnaker/pyNN/data/spynnaker_data_view.py index 261ab2ef8a4..2babc6efd21 100644 --- a/spynnaker/pyNN/data/spynnaker_data_view.py +++ b/spynnaker/pyNN/data/spynnaker_data_view.py @@ -13,13 +13,15 @@ # limitations under the License. from __future__ import annotations import logging -from typing import Iterator, Optional, Set, Tuple, TYPE_CHECKING +from typing import ( + Iterator, Optional, Set, Tuple, TYPE_CHECKING, List, Any, Dict) from spinn_utilities.log import FormatAdapter from spinn_front_end_common.data import FecDataView from spynnaker import _version +from spinn_utilities.config_holder import get_config_bool, get_report_path if TYPE_CHECKING: from spynnaker.pyNN.models.projection import Projection @@ -51,7 +53,8 @@ class _SpynnakerDataModel(object): "_id_counter", "_min_delay", "_populations", - "_projections") + "_projections", + "_pynn_report") def __new__(cls) -> '_SpynnakerDataModel': if cls.__singleton is not None: @@ -70,6 +73,7 @@ def _clear(self) -> None: # Using a dict to verify if later could be stored here only self._populations: Set[Population] = set() self._projections: Set[Projection] = set() + self._pynn_report = None def _hard_reset(self) -> None: """ @@ -233,3 +237,24 @@ def get_sim_name(cls) -> str: :returns: The name to be returned by `pyNN.spiNNaker.name`. """ return _version.NAME + + @classmethod + def write_pynn_report(cls, text: str, *args: List[Any], + **kwargs: Dict[str, Any]) -> None: + """ + Writes text to the PyNN report file, or does nothing if the report is + disabled. + + :param text: The text to write to the report file. + :param args: Any additional arguments to format into the text using + `str.format`. + :param kwargs: Any additional keyword arguments to format into the text + using `str.format`. + """ + if not get_config_bool("Reports", "write_pynn_report"): + return + if cls.__spy_data._pynn_report is None: + cls.__spy_data._pynn_report = get_report_path("path_pynn_report") + with open(cls.__spy_data._pynn_report, "a", encoding="utf-8") as f: + f.write(text.format(*args, **kwargs)) + f.write("\n") diff --git a/spynnaker/pyNN/models/neural_projections/connectors/multapse_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/multapse_connector.py index 3b370547263..a87196fa2c8 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/multapse_connector.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/multapse_connector.py @@ -276,7 +276,7 @@ def create_synaptic_block( return block def __repr__(self) -> str: - return f"MultapseConnector({self.__num_synapses})" + return f"FixedTotalNumberConnector({self.__num_synapses})" @property @overrides(AbstractGenerateConnectorOnMachine.gen_connector_id) diff --git a/spynnaker/pyNN/models/neuron/abstract_pynn_neuron_model_standard.py b/spynnaker/pyNN/models/neuron/abstract_pynn_neuron_model_standard.py index 0482cf7bbf4..dda573c9627 100644 --- a/spynnaker/pyNN/models/neuron/abstract_pynn_neuron_model_standard.py +++ b/spynnaker/pyNN/models/neuron/abstract_pynn_neuron_model_standard.py @@ -93,3 +93,6 @@ def create_vertex( splitter=splitter, seed=seed, n_colour_bits=n_colour_bits, neurons_per_core=neurons_per_core, n_synapse_cores=n_synapse_cores, allow_delay_extensions=allow_delay_extensions) + + def __str__(self) -> str: + return f"{self._model}" diff --git a/spynnaker/pyNN/models/neuron/additional_inputs/additional_input_ca2_adaptive.py b/spynnaker/pyNN/models/neuron/additional_inputs/additional_input_ca2_adaptive.py index cd83c5a8521..9ed75bfaede 100644 --- a/spynnaker/pyNN/models/neuron/additional_inputs/additional_input_ca2_adaptive.py +++ b/spynnaker/pyNN/models/neuron/additional_inputs/additional_input_ca2_adaptive.py @@ -22,6 +22,7 @@ from spynnaker.pyNN.data import SpynnakerDataView from .abstract_additional_input import AbstractAdditionalInput +from typing import Dict I_ALPHA = "i_alpha" I_CA2 = "i_ca2" @@ -56,6 +57,13 @@ def __init__(self, tau_ca2: ModelParameter, i_ca2: ModelParameter, self.__i_ca2 = i_ca2 self.__i_alpha = i_alpha + @overrides(AbstractAdditionalInput.get_param_values) + def get_param_values(self) -> Dict[str, ModelParameter]: + return { + TAU_CA2: self.__tau_ca2, + I_ALPHA: self.__i_alpha, + TIME_STEP: SpynnakerDataView.get_simulation_time_step_ms()} + @overrides(AbstractAdditionalInput.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: parameters[TAU_CA2] = self._convert(self.__tau_ca2) diff --git a/spynnaker/pyNN/models/neuron/implementations/abstract_standard_neuron_component.py b/spynnaker/pyNN/models/neuron/implementations/abstract_standard_neuron_component.py index 445c4395ce9..1b2d62e8264 100644 --- a/spynnaker/pyNN/models/neuron/implementations/abstract_standard_neuron_component.py +++ b/spynnaker/pyNN/models/neuron/implementations/abstract_standard_neuron_component.py @@ -109,3 +109,10 @@ def _convert(value: ModelParameter) -> \ return value # TODO: Is this correct? Without this, things will only handle floats return SpynnakerRangedList(None, value) + + @abstractmethod + def get_param_values(self) -> Dict[str, ModelParameter]: + """ + The parameters of the component and their values + """ + raise NotImplementedError diff --git a/spynnaker/pyNN/models/neuron/implementations/neuron_impl_standard.py b/spynnaker/pyNN/models/neuron/implementations/neuron_impl_standard.py index 7af2f11eca6..c0a668adf30 100644 --- a/spynnaker/pyNN/models/neuron/implementations/neuron_impl_standard.py +++ b/spynnaker/pyNN/models/neuron/implementations/neuron_impl_standard.py @@ -197,3 +197,10 @@ def __getitem__(self, key: str) -> Any: # ... or fail raise AttributeError( f"'{self.__class__.__name__}' object has no attribute {key}") + + def __str__(self) -> str: + param_values: str = ""; + for component in self.__components: + for param, value in component.get_param_values().items(): + param_values += f"{param}={value}, " + return f"{self.__class__.__name__}({param_values[:-2]})" diff --git a/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_exp.py b/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_exp.py index 5df9a7d92c5..4735f800237 100644 --- a/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_exp.py +++ b/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_exp.py @@ -163,3 +163,8 @@ def get_units(self, variable: str) -> str: @overrides(AbstractNeuronImpl.is_conductance_based) def is_conductance_based(self) -> bool: return False + + def __str__(self) -> str: + return ( + f"NeuronImplStocExp(tau={self._tau}, bias={self._bias}, " + f"refract_init={self._refract_init}, seed={self._random.seed})") diff --git a/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_exp_stable.py b/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_exp_stable.py index 1e788b4a893..f39b981d886 100644 --- a/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_exp_stable.py +++ b/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_exp_stable.py @@ -180,3 +180,10 @@ def get_units(self, variable: str) -> str: @overrides(AbstractNeuronImpl.is_conductance_based) def is_conductance_based(self) -> bool: return False + + def __str__(self) -> str: + return ( + f"{self.model_name}(v_init={self._v_init}, " + f"v_reset={self._v_reset}, tau={self._tau}, " + f"tau_refrac={self._tau_refrac}, bias={self._bias}, " + f"refract_init={self._refract_init}, seed={self._random.seed})") diff --git a/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_sigma.py b/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_sigma.py index 19951409285..67ca66a36e7 100644 --- a/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_sigma.py +++ b/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_sigma.py @@ -166,3 +166,9 @@ def get_units(self, variable: str) -> str: @overrides(AbstractNeuronImpl.is_conductance_based) def is_conductance_based(self) -> bool: return False + + def __str__(self) -> str: + return (f"NeuronImplStocSigma(tau_refrac={self._tau_refrac}, " + f"alpha={self._alpha}, bias={self._bias}, " + f"refract_init={self._refract_init}, " + f"seed={self._random.rng.seed})") diff --git a/spynnaker/pyNN/models/neuron/input_types/input_type_conductance.py b/spynnaker/pyNN/models/neuron/input_types/input_type_conductance.py index 705f76efffc..de0b753a8da 100644 --- a/spynnaker/pyNN/models/neuron/input_types/input_type_conductance.py +++ b/spynnaker/pyNN/models/neuron/input_types/input_type_conductance.py @@ -21,6 +21,7 @@ from spynnaker.pyNN.utilities.struct import Struct from .abstract_input_type import AbstractInputType +from typing import Dict E_REV_E = "e_rev_E" E_REV_I = "e_rev_I" @@ -49,6 +50,10 @@ def __init__(self, e_rev_E: ModelParameter, e_rev_I: ModelParameter): self.__e_rev_E = e_rev_E self.__e_rev_I = e_rev_I + @overrides(AbstractInputType.get_param_values) + def get_param_values(self) -> Dict[str, ModelParameter]: + return {E_REV_E: self.__e_rev_E, E_REV_I: self.__e_rev_I} + @overrides(AbstractInputType.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: parameters[E_REV_E] = self._convert(self.__e_rev_E) diff --git a/spynnaker/pyNN/models/neuron/input_types/input_type_current.py b/spynnaker/pyNN/models/neuron/input_types/input_type_current.py index 3708f2dd682..9545f226b2a 100644 --- a/spynnaker/pyNN/models/neuron/input_types/input_type_current.py +++ b/spynnaker/pyNN/models/neuron/input_types/input_type_current.py @@ -16,6 +16,9 @@ from spinn_utilities.ranged import RangeDictionary from spynnaker.pyNN.utilities.struct import Struct from .abstract_input_type import AbstractInputType +from typing import Dict +from spynnaker.pyNN.models.neuron.implementations\ + .abstract_standard_neuron_component import ModelParameter class InputTypeCurrent(AbstractInputType): @@ -27,6 +30,10 @@ class InputTypeCurrent(AbstractInputType): def __init__(self) -> None: super().__init__([Struct([])], dict()) + @overrides(AbstractInputType.get_param_values) + def get_param_values(self) -> Dict[str, ModelParameter]: + return dict() + @overrides(AbstractInputType.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: pass diff --git a/spynnaker/pyNN/models/neuron/input_types/input_type_delta.py b/spynnaker/pyNN/models/neuron/input_types/input_type_delta.py index 61dd8356eba..11f7372febc 100644 --- a/spynnaker/pyNN/models/neuron/input_types/input_type_delta.py +++ b/spynnaker/pyNN/models/neuron/input_types/input_type_delta.py @@ -21,6 +21,8 @@ from spynnaker.pyNN.data import SpynnakerDataView from .abstract_input_type import AbstractInputType +from typing import Dict +from spynnaker.pyNN.models.neuron.implementations.abstract_standard_neuron_component import ModelParameter TIME_STEP = "time_step" @@ -37,6 +39,10 @@ def __init__(self) -> None: [Struct([(DataType.S1615, TIME_STEP)])], dict()) + @overrides(AbstractInputType.get_param_values) + def get_param_values(self) -> Dict[str, ModelParameter]: + return dict() + @overrides(AbstractInputType.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: parameters[TIME_STEP] = SpynnakerDataView.get_simulation_time_step_ms() diff --git a/spynnaker/pyNN/models/neuron/neuron_models/neuron_model_if_trunc.py b/spynnaker/pyNN/models/neuron/neuron_models/neuron_model_if_trunc.py index e73ce3f6c64..8f40e7c5cc5 100644 --- a/spynnaker/pyNN/models/neuron/neuron_models/neuron_model_if_trunc.py +++ b/spynnaker/pyNN/models/neuron/neuron_models/neuron_model_if_trunc.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional +from typing import Optional, Dict from spinn_utilities.overrides import overrides from spinn_utilities.ranged import RangeDictionary from spinn_front_end_common.interface.ds import DataType @@ -80,6 +80,16 @@ def __init__( self.__v_reset = v_reset self.__tau_refrac = tau_refrac + @overrides(AbstractStandardNeuronComponent.get_param_values) + def get_param_values(self) -> Dict[str, ModelParameter]: + return { + "v_init": self.__v_init, + "tau_m": self.__tau_m, + "cm": self.__cm, + "i_offset": self.__i_offset, + "v_reset": self.__v_reset, + "tau_refrac": self.__tau_refrac} + @overrides(AbstractStandardNeuronComponent.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: parameters[TAU_M] = self._convert(self.__tau_m) diff --git a/spynnaker/pyNN/models/neuron/neuron_models/neuron_model_izh.py b/spynnaker/pyNN/models/neuron/neuron_models/neuron_model_izh.py index cb4329a7093..7d26cbe0ec7 100644 --- a/spynnaker/pyNN/models/neuron/neuron_models/neuron_model_izh.py +++ b/spynnaker/pyNN/models/neuron/neuron_models/neuron_model_izh.py @@ -19,6 +19,7 @@ from spynnaker.pyNN.utilities.struct import Struct from spynnaker.pyNN.data import SpynnakerDataView from .neuron_model import NeuronModel +from typing import Dict A = 'a' B = 'b' @@ -73,6 +74,13 @@ def __init__( self.__v_init = v_init self.__u_init = u_init + @overrides(AbstractStandardNeuronComponent.get_param_values) + def get_param_values(self) -> Dict[str, ModelParameter]: + return { + A: self.__a, B: self.__b, C: self.__c, D: self.__d, + I_OFFSET: self.__i_offset, "v_init": self.__v_init, + "u_init": self.__u_init} + @overrides(AbstractStandardNeuronComponent.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: parameters[A] = self._convert(self.__a) diff --git a/spynnaker/pyNN/models/neuron/neuron_models/neuron_model_leaky_integrate_and_fire.py b/spynnaker/pyNN/models/neuron/neuron_models/neuron_model_leaky_integrate_and_fire.py index 1013b3de54d..f30ba790981 100644 --- a/spynnaker/pyNN/models/neuron/neuron_models/neuron_model_leaky_integrate_and_fire.py +++ b/spynnaker/pyNN/models/neuron/neuron_models/neuron_model_leaky_integrate_and_fire.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional +from typing import Optional, Dict from spinn_utilities.overrides import overrides from spinn_utilities.ranged import RangeDictionary from spinn_front_end_common.interface.ds import DataType @@ -84,6 +84,17 @@ def __init__( self.__v_reset = v_reset self.__tau_refrac = tau_refrac + @overrides(AbstractStandardNeuronComponent.get_param_values) + def get_param_values(self) -> Dict[str, ModelParameter]: + return { + "v_init": self.__v_init, + V_REST: self.__v_rest, + TAU_M: self.__tau_m, + CM: self.__cm, + I_OFFSET: self.__i_offset, + V_RESET: self.__v_reset, + TAU_REFRAC: self.__tau_refrac} + @overrides(AbstractStandardNeuronComponent.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: parameters[V_REST] = self._convert(self.__v_rest) diff --git a/spynnaker/pyNN/models/neuron/synapse_dynamics/synapse_dynamics_static.py b/spynnaker/pyNN/models/neuron/synapse_dynamics/synapse_dynamics_static.py index 2ab58ecce48..3c02ed6f51b 100644 --- a/spynnaker/pyNN/models/neuron/synapse_dynamics/synapse_dynamics_static.py +++ b/spynnaker/pyNN/models/neuron/synapse_dynamics/synapse_dynamics_static.py @@ -252,3 +252,7 @@ def synapses_per_second(self) -> int: # From Synapse-Centric Mapping of Cortical Models to the SpiNNaker # Neuromorphic Architecture return 13000000 + + def __str__(self) -> str: + return ( + f"SynapseDynamicsStatic(weight={self.weight}, delay={self.delay})") diff --git a/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_alpha.py b/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_alpha.py index d964b1e485e..07d79de8f16 100644 --- a/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_alpha.py +++ b/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_alpha.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional, Tuple +from typing import Optional, Tuple, Dict from spinn_utilities.overrides import overrides from spinn_utilities.ranged import RangeDictionary @@ -85,6 +85,16 @@ def __init__( self.__inh_exp_response = inh_exp_response self.__tau_syn_I = tau_syn_I + @overrides(AbstractSynapseType.get_param_values) + def get_param_values(self) -> Dict[str, ModelParameter]: + return { + EXC_RESPONSE: self.__exc_response, + EXC_EXP_RESPONSE: self.__exc_exp_response, + TAU_SYN_E: self.__tau_syn_E, + INH_RESPONSE: self.__inh_response, + INH_EXP_RESPONSE: self.__inh_exp_response, + TAU_SYN_I: self.__tau_syn_I} + @overrides(AbstractSynapseType.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: parameters[TAU_SYN_E] = self._convert(self.__tau_syn_E) diff --git a/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_delta.py b/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_delta.py index 2e9ba68c70d..11e6aae9607 100644 --- a/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_delta.py +++ b/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_delta.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional, Tuple +from typing import Optional, Tuple, Dict from spinn_utilities.overrides import overrides from spinn_utilities.ranged import RangeDictionary @@ -48,6 +48,13 @@ def __init__(self, isyn_exc: ModelParameter, isyn_inh: ModelParameter): self.__isyn_exc = isyn_exc self.__isyn_inh = isyn_inh + @overrides(AbstractSynapseType.get_param_values) + def get_param_values(self) -> Dict[str, ModelParameter]: + return { + ISYN_EXC: self.__isyn_exc, + ISYN_INH: self.__isyn_inh + } + @overrides(AbstractSynapseType.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: pass diff --git a/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_dual_exponential.py b/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_dual_exponential.py index e8fcc680463..e61b65d7608 100644 --- a/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_dual_exponential.py +++ b/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_dual_exponential.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional, Tuple +from typing import Optional, Tuple, Dict from spinn_utilities.overrides import overrides from spinn_utilities.ranged import RangeDictionary @@ -77,6 +77,16 @@ def __init__( self.__isyn_exc2 = isyn_exc2 self.__isyn_inh = isyn_inh + @overrides(AbstractSynapseType.get_param_values) + def get_param_values(self) -> Dict[str, ModelParameter]: + return { + TAU_SYN_E: self.__tau_syn_E, + TAU_SYN_E2: self.__tau_syn_E2, + TAU_SYN_I: self.__tau_syn_I, + ISYN_EXC: self.__isyn_exc, + ISYN_EXC2: self.__isyn_exc2, + ISYN_INH: self.__isyn_inh} + @overrides(AbstractSynapseType.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: parameters[TAU_SYN_E] = self._convert(self.__tau_syn_E) diff --git a/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_exponential.py b/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_exponential.py index 926e8389b83..1e5a160b010 100644 --- a/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_exponential.py +++ b/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_exponential.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional, Tuple +from typing import Optional, Tuple, Dict from spinn_utilities.overrides import overrides from spinn_utilities.ranged import RangeDictionary @@ -64,6 +64,14 @@ def __init__(self, tau_syn_E: ModelParameter, tau_syn_I: ModelParameter, self.__isyn_exc = isyn_exc self.__isyn_inh = isyn_inh + @overrides(AbstractSynapseType.get_param_values) + def get_param_values(self) -> Dict[str, ModelParameter]: + return { + TAU_SYN_E: self.__tau_syn_E, + TAU_SYN_I: self.__tau_syn_I, + ISYN_EXC: self.__isyn_exc, + ISYN_INH: self.__isyn_inh} + @overrides(AbstractSynapseType.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: parameters[TAU_SYN_E] = self._convert(self.__tau_syn_E) diff --git a/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_semd.py b/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_semd.py index 9678d9a674e..6ab19d9a7f6 100644 --- a/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_semd.py +++ b/spynnaker/pyNN/models/neuron/synapse_types/synapse_type_semd.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional, Tuple +from typing import Optional, Tuple, Dict from spinn_utilities.overrides import overrides from spinn_utilities.ranged import RangeDictionary @@ -97,6 +97,19 @@ def __init__( self.__exc2_old = exc2_old self.__scaling_factor = scaling_factor + @overrides(AbstractSynapseType.get_param_values) + def get_param_values(self) -> Dict[str, ModelParameter]: + return { + TAU_SYN_E: self.__tau_syn_E, + TAU_SYN_E2: self.__tau_syn_E2, + TAU_SYN_I: self.__tau_syn_I, + ISYN_EXC: self.__isyn_exc, + ISYN_EXC2: self.__isyn_exc2, + ISYN_INH: self.__isyn_inh, + MULTIPLICATOR: self.__multiplicator, + EXC2_OLD: self.__exc2_old, + SCALING_FACTOR: self.__scaling_factor} + @overrides(AbstractSynapseType.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: parameters[TAU_SYN_E] = self._convert(self.__tau_syn_E) diff --git a/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_fixed_prob.py b/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_fixed_prob.py index dae0d414a8f..61894cbe59e 100644 --- a/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_fixed_prob.py +++ b/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_fixed_prob.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional +from typing import Optional, Dict from pyNN.random import NumpyRNG @@ -62,6 +62,10 @@ def __init__(self, v_thresh: ModelParameter, p_thresh: ModelParameter, self.__random = RandomDistribution( "uniform", low=0, high=0xFFFFFFFF, rng=NumpyRNG(seed)) + @overrides(AbstractThresholdType.get_param_values) + def get_param_values(self) -> Dict[str, ModelParameter]: + return {V_THRESH: self.__v_thresh, P_THRESH: self.__p_thresh} + @overrides(AbstractThresholdType.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: parameters[V_THRESH] = self._convert(self.__v_thresh) diff --git a/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_maass_stochastic.py b/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_maass_stochastic.py index fa6f8043f0a..43814ee4c14 100644 --- a/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_maass_stochastic.py +++ b/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_maass_stochastic.py @@ -22,6 +22,7 @@ from spynnaker.pyNN.data import SpynnakerDataView from .abstract_threshold_type import AbstractThresholdType +from typing import Dict DU_TH = "du_th" TAU_TH = "tau_th" @@ -61,6 +62,13 @@ def __init__(self, du_th: ModelParameter, tau_th: ModelParameter, self.__tau_th = tau_th self.__v_thresh = v_thresh + @overrides(AbstractThresholdType.get_param_values) + def get_param_values(self) -> Dict[str, ModelParameter]: + return { + DU_TH: self.__du_th, + TAU_TH: self.__tau_th, + V_THRESH: self.__v_thresh} + @overrides(AbstractThresholdType.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: parameters[DU_TH] = self._convert(self.__du_th) diff --git a/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_static.py b/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_static.py index 45056799741..8e05e6f0bb1 100644 --- a/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_static.py +++ b/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_static.py @@ -21,6 +21,7 @@ from spynnaker.pyNN.utilities.struct import Struct from .abstract_threshold_type import AbstractThresholdType +from typing import Dict V_THRESH = "v_thresh" @@ -40,6 +41,10 @@ def __init__(self, v_thresh: ModelParameter): {V_THRESH: "mV"}) self.__v_thresh = v_thresh + @overrides(AbstractThresholdType.get_param_values) + def get_param_values(self) -> Dict[str, ModelParameter]: + return {V_THRESH: self.__v_thresh} + @overrides(AbstractThresholdType.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: parameters[V_THRESH] = self._convert(self.__v_thresh) diff --git a/spynnaker/pyNN/models/populations/population.py b/spynnaker/pyNN/models/populations/population.py index 31bc036c540..ebca1dcd593 100644 --- a/spynnaker/pyNN/models/populations/population.py +++ b/spynnaker/pyNN/models/populations/population.py @@ -140,6 +140,10 @@ def __init__( for variable, value in initial_values.items(): self.__vertex.set_initial_state_values(variable, value) + SpynnakerDataView.write_pynn_report( + "{} = Population({}, {}, structure={}, initial_values={})", + label, size, model, structure, initial_values, label) + def __iter__(self) -> Iterator[PopulationView]: """ Iterate over local cells. @@ -188,6 +192,9 @@ def record(self, variables: Names, to_file: IoDest = None, sampling_interval: Optional[float] = None) -> None: self.__recorder.record( variables, to_file, sampling_interval, indexes=None) + SpynnakerDataView.write_pynn_report( + "{}.record(variables={}, to_file={}, sampling_interval={})", + self.label, variables, to_file, sampling_interval) def sample(self, n: int, rng: Optional[NumpyRNG] = None) -> PopulationView: """ @@ -370,6 +377,8 @@ def initialize(self, **kwargs: Values) -> None: " consider calling the non-PyNN function set_state instead.") for variable, value in kwargs.items(): self.__vertex.set_initial_state_values(variable, value) + SpynnakerDataView.write_pynn_report( + "{}.initialize({})", self.label, kwargs) @property def initial_values(self) -> ParameterHolder: @@ -590,6 +599,8 @@ def inject(self, current_source: NeuronCurrentSource) -> None: """ # Pass this into the vertex self.__vertex.inject(current_source, [n for n in range(self.__size)]) + SpynnakerDataView.write_pynn_report( + "{}.inject({})", self.label, current_source) def __len__(self) -> int: """ diff --git a/spynnaker/pyNN/models/projection.py b/spynnaker/pyNN/models/projection.py index c2e3929476d..28a58bcd559 100644 --- a/spynnaker/pyNN/models/projection.py +++ b/spynnaker/pyNN/models/projection.py @@ -208,6 +208,11 @@ def __init__( if isinstance(pre_vertex, SpikeSourcePoissonVertex): pre_vertex.add_outgoing_projection(self) + SpynnakerDataView.write_pynn_report( + "Projection(pre={}, post={}, connector={}, synapse_type={}, " + "source={}, receptor_type={}, space={}, label={})", + pre_synaptic_population.label, post_synaptic_population.label, + connector, synapse_type, source, receptor_type, space, label) @staticmethod def __check_population(param: _Pop) -> bool: """ diff --git a/spynnaker/pyNN/models/spike_source/spike_source_array.py b/spynnaker/pyNN/models/spike_source/spike_source_array.py index ff515e5e8da..cbf254d3bd2 100644 --- a/spynnaker/pyNN/models/spike_source/spike_source_array.py +++ b/spynnaker/pyNN/models/spike_source/spike_source_array.py @@ -55,3 +55,6 @@ def create_vertex( @property def _spike_times(self) -> Spikes: return self.__spike_times + + def __str__(self) -> str: + return f"SpikeSourceArray(spike_times={self.__spike_times})" diff --git a/spynnaker/pyNN/models/spike_source/spike_source_poisson.py b/spynnaker/pyNN/models/spike_source/spike_source_poisson.py index 433f1be9c49..2e06f5362f2 100644 --- a/spynnaker/pyNN/models/spike_source/spike_source_poisson.py +++ b/spynnaker/pyNN/models/spike_source/spike_source_poisson.py @@ -75,3 +75,8 @@ def create_vertex( n_neurons, label, seed, neurons_per_core, self, rate=self.__rate, start=self.__start, duration=self.__duration, max_rate=max_rate, splitter=splitter, n_colour_bits=n_colour_bits) + + + def __str__(self) -> str: + return (f"SpikeSourcePoisson(rate={self.__rate}, start={self.__start}, " + f"duration={self.__duration})") diff --git a/spynnaker/pyNN/models/spike_source/spike_source_poisson_variable.py b/spynnaker/pyNN/models/spike_source/spike_source_poisson_variable.py index a34195a1fc2..9c907614388 100644 --- a/spynnaker/pyNN/models/spike_source/spike_source_poisson_variable.py +++ b/spynnaker/pyNN/models/spike_source/spike_source_poisson_variable.py @@ -70,3 +70,9 @@ def create_vertex( return SpikeSourcePoissonVertex( n_neurons, label, seed, max_atoms, self, rates=self._rates, starts=self._starts, durations=self._durations, splitter=splitter) + + def __str__(self) -> str: + return f"SpikeSourcePoissonVariable(" \ + f"rates={self._rates}, " \ + f"starts={self._starts}, " \ + f"durations={self._durations})" diff --git a/spynnaker/pyNN/models/utility_models/spike_injector/spike_injector.py b/spynnaker/pyNN/models/utility_models/spike_injector/spike_injector.py index f7c78fc21cf..2d5aaebe891 100644 --- a/spynnaker/pyNN/models/utility_models/spike_injector/spike_injector.py +++ b/spynnaker/pyNN/models/utility_models/spike_injector/spike_injector.py @@ -51,3 +51,6 @@ def create_vertex( return SpikeInjectorVertex( n_neurons, label, port, virtual_key, reserve_reverse_ip_tag, splitter, max_atoms_per_core) + + def __str__(self) -> str: + return "SpikeInjector()" diff --git a/spynnaker/pyNN/spynnaker.cfg b/spynnaker/pyNN/spynnaker.cfg index 569ce8bede6..e62ef60f26f 100644 --- a/spynnaker/pyNN/spynnaker.cfg +++ b/spynnaker/pyNN/spynnaker.cfg @@ -34,6 +34,10 @@ write_redundant_packet_count_report = Info @write_redundant_packet_count_report = Writes a report showing how many redundant packets where recorded. path_redundant_packet_count_report = redundant_packet_count.rpt +write_pynn_report = Info +@write_pynn_report = Writes a report on the PyNN calls made. +path_pynn_report = pynn_report.txt + [Simulation] @ = The section covers settings which control how the models behave. From 4906a4f4b6d39b81fbce90d98ad5a3571aab3da8 Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Mon, 2 Mar 2026 16:29:17 +0000 Subject: [PATCH 02/23] Mypy fixes --- spynnaker/pyNN/data/spynnaker_data_view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spynnaker/pyNN/data/spynnaker_data_view.py b/spynnaker/pyNN/data/spynnaker_data_view.py index 2babc6efd21..059c5079fa4 100644 --- a/spynnaker/pyNN/data/spynnaker_data_view.py +++ b/spynnaker/pyNN/data/spynnaker_data_view.py @@ -73,7 +73,7 @@ def _clear(self) -> None: # Using a dict to verify if later could be stored here only self._populations: Set[Population] = set() self._projections: Set[Projection] = set() - self._pynn_report = None + self._pynn_report: Optional[str] = None def _hard_reset(self) -> None: """ @@ -239,8 +239,8 @@ def get_sim_name(cls) -> str: return _version.NAME @classmethod - def write_pynn_report(cls, text: str, *args: List[Any], - **kwargs: Dict[str, Any]) -> None: + def write_pynn_report(cls, text: str, *args: Any, + **kwargs: Any) -> None: """ Writes text to the PyNN report file, or does nothing if the report is disabled. From 72576640b41cd1628558099a1a1253ac0d5075d6 Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Tue, 3 Mar 2026 08:08:27 +0000 Subject: [PATCH 03/23] Also add the annotate method here as this should be available in PyNN --- spynnaker/pyNN/models/populations/population.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spynnaker/pyNN/models/populations/population.py b/spynnaker/pyNN/models/populations/population.py index ebca1dcd593..6cda4b01700 100644 --- a/spynnaker/pyNN/models/populations/population.py +++ b/spynnaker/pyNN/models/populations/population.py @@ -164,6 +164,16 @@ def all(self) -> Iterator[PopulationView]: for _id in range(self.__size): yield IDMixin(self, _id) + def annotate(self, **annotations: Any) -> None: + """ + Add annotations to this population. These are user-defined key-value + pairs that will be stored with the population and can be retrieved + later with the `annotations` property. + + :param annotations: The annotations to add to the population. + """ + self.__annotations.update(annotations) + @property def annotations(self) -> Dict[str, Any]: """ From b48ea1b0347092791c90dfa374b0baaf82344b65 Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Tue, 3 Mar 2026 08:18:45 +0000 Subject: [PATCH 04/23] Flake8 --- spynnaker/pyNN/data/spynnaker_data_view.py | 3 +-- .../additional_inputs/additional_input_ca2_adaptive.py | 3 ++- .../models/neuron/implementations/neuron_impl_standard.py | 2 +- .../pyNN/models/neuron/input_types/input_type_conductance.py | 3 ++- .../pyNN/models/neuron/input_types/input_type_current.py | 5 ++--- spynnaker/pyNN/models/neuron/input_types/input_type_delta.py | 5 +++-- spynnaker/pyNN/models/projection.py | 1 + spynnaker/pyNN/models/spike_source/spike_source_poisson.py | 5 ++--- 8 files changed, 14 insertions(+), 13 deletions(-) diff --git a/spynnaker/pyNN/data/spynnaker_data_view.py b/spynnaker/pyNN/data/spynnaker_data_view.py index 059c5079fa4..29c71497ddc 100644 --- a/spynnaker/pyNN/data/spynnaker_data_view.py +++ b/spynnaker/pyNN/data/spynnaker_data_view.py @@ -13,8 +13,7 @@ # limitations under the License. from __future__ import annotations import logging -from typing import ( - Iterator, Optional, Set, Tuple, TYPE_CHECKING, List, Any, Dict) +from typing import Iterator, Optional, Set, Tuple, TYPE_CHECKING, Any from spinn_utilities.log import FormatAdapter diff --git a/spynnaker/pyNN/models/neuron/additional_inputs/additional_input_ca2_adaptive.py b/spynnaker/pyNN/models/neuron/additional_inputs/additional_input_ca2_adaptive.py index 9ed75bfaede..b8e5513311c 100644 --- a/spynnaker/pyNN/models/neuron/additional_inputs/additional_input_ca2_adaptive.py +++ b/spynnaker/pyNN/models/neuron/additional_inputs/additional_input_ca2_adaptive.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict + from spinn_utilities.overrides import overrides from spinn_utilities.ranged import RangeDictionary @@ -22,7 +24,6 @@ from spynnaker.pyNN.data import SpynnakerDataView from .abstract_additional_input import AbstractAdditionalInput -from typing import Dict I_ALPHA = "i_alpha" I_CA2 = "i_ca2" diff --git a/spynnaker/pyNN/models/neuron/implementations/neuron_impl_standard.py b/spynnaker/pyNN/models/neuron/implementations/neuron_impl_standard.py index c0a668adf30..b893f43b58f 100644 --- a/spynnaker/pyNN/models/neuron/implementations/neuron_impl_standard.py +++ b/spynnaker/pyNN/models/neuron/implementations/neuron_impl_standard.py @@ -199,7 +199,7 @@ def __getitem__(self, key: str) -> Any: f"'{self.__class__.__name__}' object has no attribute {key}") def __str__(self) -> str: - param_values: str = ""; + param_values: str = "" for component in self.__components: for param, value in component.get_param_values().items(): param_values += f"{param}={value}, " diff --git a/spynnaker/pyNN/models/neuron/input_types/input_type_conductance.py b/spynnaker/pyNN/models/neuron/input_types/input_type_conductance.py index de0b753a8da..92e172cfb47 100644 --- a/spynnaker/pyNN/models/neuron/input_types/input_type_conductance.py +++ b/spynnaker/pyNN/models/neuron/input_types/input_type_conductance.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict + from spinn_utilities.overrides import overrides from spinn_utilities.ranged.range_dictionary import RangeDictionary @@ -21,7 +23,6 @@ from spynnaker.pyNN.utilities.struct import Struct from .abstract_input_type import AbstractInputType -from typing import Dict E_REV_E = "e_rev_E" E_REV_I = "e_rev_I" diff --git a/spynnaker/pyNN/models/neuron/input_types/input_type_current.py b/spynnaker/pyNN/models/neuron/input_types/input_type_current.py index 9545f226b2a..ed93775d8d9 100644 --- a/spynnaker/pyNN/models/neuron/input_types/input_type_current.py +++ b/spynnaker/pyNN/models/neuron/input_types/input_type_current.py @@ -12,13 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict from spinn_utilities.overrides import overrides from spinn_utilities.ranged import RangeDictionary from spynnaker.pyNN.utilities.struct import Struct +from spynnaker.pyNN.models.neuron.implementations import ModelParameter from .abstract_input_type import AbstractInputType -from typing import Dict -from spynnaker.pyNN.models.neuron.implementations\ - .abstract_standard_neuron_component import ModelParameter class InputTypeCurrent(AbstractInputType): diff --git a/spynnaker/pyNN/models/neuron/input_types/input_type_delta.py b/spynnaker/pyNN/models/neuron/input_types/input_type_delta.py index 11f7372febc..e34d2ce3339 100644 --- a/spynnaker/pyNN/models/neuron/input_types/input_type_delta.py +++ b/spynnaker/pyNN/models/neuron/input_types/input_type_delta.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict + from spinn_utilities.overrides import overrides from spinn_utilities.ranged import RangeDictionary @@ -19,10 +21,9 @@ from spynnaker.pyNN.utilities.struct import Struct from spynnaker.pyNN.data import SpynnakerDataView +from spynnaker.pyNN.models.neuron.implementations import ModelParameter from .abstract_input_type import AbstractInputType -from typing import Dict -from spynnaker.pyNN.models.neuron.implementations.abstract_standard_neuron_component import ModelParameter TIME_STEP = "time_step" diff --git a/spynnaker/pyNN/models/projection.py b/spynnaker/pyNN/models/projection.py index 28a58bcd559..88a46cfc48b 100644 --- a/spynnaker/pyNN/models/projection.py +++ b/spynnaker/pyNN/models/projection.py @@ -213,6 +213,7 @@ def __init__( "source={}, receptor_type={}, space={}, label={})", pre_synaptic_population.label, post_synaptic_population.label, connector, synapse_type, source, receptor_type, space, label) + @staticmethod def __check_population(param: _Pop) -> bool: """ diff --git a/spynnaker/pyNN/models/spike_source/spike_source_poisson.py b/spynnaker/pyNN/models/spike_source/spike_source_poisson.py index 2e06f5362f2..8ea815ad851 100644 --- a/spynnaker/pyNN/models/spike_source/spike_source_poisson.py +++ b/spynnaker/pyNN/models/spike_source/spike_source_poisson.py @@ -76,7 +76,6 @@ def create_vertex( rate=self.__rate, start=self.__start, duration=self.__duration, max_rate=max_rate, splitter=splitter, n_colour_bits=n_colour_bits) - def __str__(self) -> str: - return (f"SpikeSourcePoisson(rate={self.__rate}, start={self.__start}, " - f"duration={self.__duration})") + return (f"SpikeSourcePoisson(rate={self.__rate}, start={self.__start}," + f" duration={self.__duration})") From 4d3826a2b62d3099b44cca8372a0c258ad93d0af Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Tue, 3 Mar 2026 08:40:06 +0000 Subject: [PATCH 05/23] pylint --- spynnaker/pyNN/data/spynnaker_data_view.py | 2 +- .../threshold_type_multicast_device_control.py | 18 +++++++++++++++++- .../implementations/neuron_impl_stoc_exp.py | 3 ++- .../neuron_impl_stoc_exp_stable.py | 3 ++- .../neuron/neuron_models/neuron_model_izh.py | 2 +- .../threshold_type_maass_stochastic.py | 3 ++- .../threshold_types/threshold_type_static.py | 3 ++- 7 files changed, 27 insertions(+), 7 deletions(-) diff --git a/spynnaker/pyNN/data/spynnaker_data_view.py b/spynnaker/pyNN/data/spynnaker_data_view.py index 29c71497ddc..1d2f0038f2a 100644 --- a/spynnaker/pyNN/data/spynnaker_data_view.py +++ b/spynnaker/pyNN/data/spynnaker_data_view.py @@ -16,11 +16,11 @@ from typing import Iterator, Optional, Set, Tuple, TYPE_CHECKING, Any from spinn_utilities.log import FormatAdapter +from spinn_utilities.config_holder import get_config_bool, get_report_path from spinn_front_end_common.data import FecDataView from spynnaker import _version -from spinn_utilities.config_holder import get_config_bool, get_report_path if TYPE_CHECKING: from spynnaker.pyNN.models.projection import Projection diff --git a/spynnaker/pyNN/external_devices_models/threshold_type_multicast_device_control.py b/spynnaker/pyNN/external_devices_models/threshold_type_multicast_device_control.py index d0b0b52bba3..ca5ad6f0b42 100644 --- a/spynnaker/pyNN/external_devices_models/threshold_type_multicast_device_control.py +++ b/spynnaker/pyNN/external_devices_models/threshold_type_multicast_device_control.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Sequence +from typing import Sequence, Dict from spinn_utilities.overrides import overrides from spinn_utilities.ranged.range_dictionary import RangeDictionary @@ -23,6 +23,7 @@ from spynnaker.pyNN.utilities.struct import Struct from spynnaker.pyNN.external_devices_models import ( AbstractMulticastControllableDevice) +from spynnaker.pyNN.models.neuron.implementations import ModelParameter _DEVICE = "device" _KEY = "key" @@ -59,6 +60,21 @@ def __init__(self, devices: Sequence[AbstractMulticastControllableDevice]): _TYPE: ""}) self.__devices = devices + @overrides(AbstractThresholdType.get_param_values) + def get_param_values(self)->Dict[str, ModelParameter]: + return { + _KEY: [d.device_control_key for d in self.__devices], + _SCALE: [d.device_control_scaling_factor for d in self.__devices], + _MIN: [d.device_control_min_value for d in self.__devices], + _MAX: [d.device_control_max_value for d in self.__devices], + _TS_INTER_SEND: [ + d.device_control_timesteps_between_sending + for d in self.__devices], + _TS_NEXT_SEND: [ + d.device_control_first_send_timestep for d in self.__devices], + _TYPE: [d.device_control_send_type.value + for d in self.__devices]} + @overrides(AbstractThresholdType.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: parameters[_KEY] = self._convert([ diff --git a/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_exp.py b/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_exp.py index 4735f800237..df1eebc4d83 100644 --- a/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_exp.py +++ b/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_exp.py @@ -167,4 +167,5 @@ def is_conductance_based(self) -> bool: def __str__(self) -> str: return ( f"NeuronImplStocExp(tau={self._tau}, bias={self._bias}, " - f"refract_init={self._refract_init}, seed={self._random.seed})") + f"refract_init={self._refract_init}, " + f"seed={self._random.rng.seed})") diff --git a/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_exp_stable.py b/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_exp_stable.py index f39b981d886..9c62f9bc6c6 100644 --- a/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_exp_stable.py +++ b/spynnaker/pyNN/models/neuron/implementations/neuron_impl_stoc_exp_stable.py @@ -186,4 +186,5 @@ def __str__(self) -> str: f"{self.model_name}(v_init={self._v_init}, " f"v_reset={self._v_reset}, tau={self._tau}, " f"tau_refrac={self._tau_refrac}, bias={self._bias}, " - f"refract_init={self._refract_init}, seed={self._random.seed})") + f"refract_init={self._refract_init}, " + f"seed={self._random.rng.seed})") diff --git a/spynnaker/pyNN/models/neuron/neuron_models/neuron_model_izh.py b/spynnaker/pyNN/models/neuron/neuron_models/neuron_model_izh.py index 7d26cbe0ec7..c14c5e4c489 100644 --- a/spynnaker/pyNN/models/neuron/neuron_models/neuron_model_izh.py +++ b/spynnaker/pyNN/models/neuron/neuron_models/neuron_model_izh.py @@ -11,6 +11,7 @@ # 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. +from typing import Dict from spinn_utilities.overrides import overrides from spinn_utilities.ranged import RangeDictionary from spinn_front_end_common.interface.ds import DataType @@ -19,7 +20,6 @@ from spynnaker.pyNN.utilities.struct import Struct from spynnaker.pyNN.data import SpynnakerDataView from .neuron_model import NeuronModel -from typing import Dict A = 'a' B = 'b' diff --git a/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_maass_stochastic.py b/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_maass_stochastic.py index 43814ee4c14..694fbcb226a 100644 --- a/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_maass_stochastic.py +++ b/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_maass_stochastic.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict + from spinn_utilities.overrides import overrides from spinn_utilities.ranged import RangeDictionary @@ -22,7 +24,6 @@ from spynnaker.pyNN.data import SpynnakerDataView from .abstract_threshold_type import AbstractThresholdType -from typing import Dict DU_TH = "du_th" TAU_TH = "tau_th" diff --git a/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_static.py b/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_static.py index 8e05e6f0bb1..f979bae1ac4 100644 --- a/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_static.py +++ b/spynnaker/pyNN/models/neuron/threshold_types/threshold_type_static.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from typing import Dict + from spinn_utilities.overrides import overrides from spinn_utilities.ranged import RangeDictionary @@ -21,7 +23,6 @@ from spynnaker.pyNN.utilities.struct import Struct from .abstract_threshold_type import AbstractThresholdType -from typing import Dict V_THRESH = "v_thresh" From ee11b5761e0abba4faa1f0f8fc8a741da8923eb0 Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Tue, 3 Mar 2026 08:48:41 +0000 Subject: [PATCH 06/23] Reorganise to avoid issues in str --- .../synapse_dynamics/abstract_synapse_dynamics.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/spynnaker/pyNN/models/neuron/synapse_dynamics/abstract_synapse_dynamics.py b/spynnaker/pyNN/models/neuron/synapse_dynamics/abstract_synapse_dynamics.py index 09537b2a318..149489a3630 100644 --- a/spynnaker/pyNN/models/neuron/synapse_dynamics/abstract_synapse_dynamics.py +++ b/spynnaker/pyNN/models/neuron/synapse_dynamics/abstract_synapse_dynamics.py @@ -60,12 +60,18 @@ def __init__(self, delay: WeightsDelysIn, :param weight: The weights or way to generate the weights """ self.__check_in_type(delay, "delay") - self.__delay = self._round_delay(delay) - self.__check_out_delay(self.__delay, "delay") self.__check_in_type(weight, "weight") + + self.__delay = self._round_delay(delay) self.__weight = self._convert_weight(weight) + + self.__check_out_delay(self.__delay, "delay") self.__check_out_weight(self.__weight, "weight") + if not numpy.allclose(cast(float, delay), self.__delay): + logger.warning("Rounding up delay in f{} from {} to {}", + self, delay, self.__delay) + def __check_in_type(self, value: WeightsDelysIn, name: str) -> None: if value is None: return @@ -174,9 +180,6 @@ def _round_delay(self, delay: WeightsDelysIn) -> Delays: numpy.rint(numpy.array(cast(float, delay)) * SpynnakerDataView.get_simulation_time_step_per_ms()) * SpynnakerDataView.get_simulation_time_step_ms()) - if not numpy.allclose(cast(float, delay), new_delay): - logger.warning("Rounding up delay in f{} from {} to {}", - self, delay, new_delay) if isinstance(new_delay, numpy.float64): return float(new_delay) if isinstance(new_delay, numpy.ndarray): From 8fa96ae4fb84281b7fbf955a84c85f17dc7cc7ce Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Tue, 3 Mar 2026 08:49:35 +0000 Subject: [PATCH 07/23] Flake8 --- .../threshold_type_multicast_device_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spynnaker/pyNN/external_devices_models/threshold_type_multicast_device_control.py b/spynnaker/pyNN/external_devices_models/threshold_type_multicast_device_control.py index ca5ad6f0b42..69bc8797697 100644 --- a/spynnaker/pyNN/external_devices_models/threshold_type_multicast_device_control.py +++ b/spynnaker/pyNN/external_devices_models/threshold_type_multicast_device_control.py @@ -61,7 +61,7 @@ def __init__(self, devices: Sequence[AbstractMulticastControllableDevice]): self.__devices = devices @overrides(AbstractThresholdType.get_param_values) - def get_param_values(self)->Dict[str, ModelParameter]: + def get_param_values(self) -> Dict[str, ModelParameter]: return { _KEY: [d.device_control_key for d in self.__devices], _SCALE: [d.device_control_scaling_factor for d in self.__devices], From 7b99d2d241639bfaec5322d5cd025d0740da2146 Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Tue, 3 Mar 2026 08:55:43 +0000 Subject: [PATCH 08/23] Fix mock in test --- unittests/test_populations/test_vertex.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/unittests/test_populations/test_vertex.py b/unittests/test_populations/test_vertex.py index 6dd0a561810..7b205262080 100644 --- a/unittests/test_populations/test_vertex.py +++ b/unittests/test_populations/test_vertex.py @@ -13,6 +13,7 @@ # limitations under the License. import pytest +from typing import Dict import pyNN.spiNNaker as sim from spinn_utilities.overrides import overrides @@ -25,7 +26,7 @@ from spynnaker.pyNN.models.defaults import default_initial_values from spynnaker.pyNN.models.neuron.implementations import ( - AbstractStandardNeuronComponent) + AbstractStandardNeuronComponent, ModelParameter) from unittests.mocks import ( MockInputType, MockSynapseType, MockThresholdType) @@ -37,6 +38,10 @@ def __init__(self, foo: int, bar: int): self._foo = foo self._bar = bar + @overrides(AbstractStandardNeuronComponent.get_param_values) + def get_param_values(self)->Dict[str, ModelParameter]: + return {"foo": self._foo, "bar": self._bar} + @overrides(AbstractStandardNeuronComponent.add_parameters) def add_parameters(self, parameters: RangeDictionary[float]) -> None: pass From dd4753e61af4963575735e81c4f81fce8e4dd8d5 Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Tue, 3 Mar 2026 09:00:04 +0000 Subject: [PATCH 09/23] Add return --- .../implementations/abstract_standard_neuron_component.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spynnaker/pyNN/models/neuron/implementations/abstract_standard_neuron_component.py b/spynnaker/pyNN/models/neuron/implementations/abstract_standard_neuron_component.py index 1b2d62e8264..34f6a18e591 100644 --- a/spynnaker/pyNN/models/neuron/implementations/abstract_standard_neuron_component.py +++ b/spynnaker/pyNN/models/neuron/implementations/abstract_standard_neuron_component.py @@ -114,5 +114,7 @@ def _convert(value: ModelParameter) -> \ def get_param_values(self) -> Dict[str, ModelParameter]: """ The parameters of the component and their values + + :returns: A dictionary mapping parameter names to their values """ raise NotImplementedError From 4416aa86732c9bda9f8882a45b380084e8e9e3b8 Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Tue, 3 Mar 2026 09:51:50 +0000 Subject: [PATCH 10/23] Fix some tests by setting the value --- spynnaker/pyNN/config_setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spynnaker/pyNN/config_setup.py b/spynnaker/pyNN/config_setup.py index 3e1f1e39bbf..2f194242e61 100644 --- a/spynnaker/pyNN/config_setup.py +++ b/spynnaker/pyNN/config_setup.py @@ -15,11 +15,12 @@ import os from typing import Set -from spinn_utilities.config_holder import clear_cfg_files +from spinn_utilities.config_holder import clear_cfg_files, set_config from spinn_front_end_common.interface.config_setup import ( add_default_cfg, add_spinnaker_cfg) from spinn_front_end_common.interface.config_setup import ( fec_cfg_paths_skipped) +from spinn_front_end_common.interface.config_handler import ConfigHandler from spynnaker.pyNN.data.spynnaker_data_writer import SpynnakerDataWriter from spynnaker.pyNN.models.neuron import AbstractPyNNNeuronModel @@ -45,6 +46,7 @@ def unittest_setup() -> None: add_spynnaker_cfg() SpynnakerDataWriter.mock() AbstractPyNNNeuronModel.reset_all() + set_config("Reports", "write_pynn_report", "False") def add_spynnaker_cfg() -> None: From 30714696682fc655eef28a7ca25847fb5995139c Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Tue, 3 Mar 2026 14:18:50 +0000 Subject: [PATCH 11/23] Remove unused --- spynnaker/pyNN/config_setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/spynnaker/pyNN/config_setup.py b/spynnaker/pyNN/config_setup.py index 2f194242e61..1ed004d5518 100644 --- a/spynnaker/pyNN/config_setup.py +++ b/spynnaker/pyNN/config_setup.py @@ -20,7 +20,6 @@ add_default_cfg, add_spinnaker_cfg) from spinn_front_end_common.interface.config_setup import ( fec_cfg_paths_skipped) -from spinn_front_end_common.interface.config_handler import ConfigHandler from spynnaker.pyNN.data.spynnaker_data_writer import SpynnakerDataWriter from spynnaker.pyNN.models.neuron import AbstractPyNNNeuronModel From 9491a05ff8a6f59448bdaef2032168da47c0b42e Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Tue, 3 Mar 2026 14:20:57 +0000 Subject: [PATCH 12/23] Fix issue with None value --- .../neuron/synapse_dynamics/abstract_synapse_dynamics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spynnaker/pyNN/models/neuron/synapse_dynamics/abstract_synapse_dynamics.py b/spynnaker/pyNN/models/neuron/synapse_dynamics/abstract_synapse_dynamics.py index 149489a3630..1f75b3a529e 100644 --- a/spynnaker/pyNN/models/neuron/synapse_dynamics/abstract_synapse_dynamics.py +++ b/spynnaker/pyNN/models/neuron/synapse_dynamics/abstract_synapse_dynamics.py @@ -68,7 +68,8 @@ def __init__(self, delay: WeightsDelysIn, self.__check_out_delay(self.__delay, "delay") self.__check_out_weight(self.__weight, "weight") - if not numpy.allclose(cast(float, delay), self.__delay): + if delay is not None and not numpy.allclose( + cast(float, delay), self.__delay): logger.warning("Rounding up delay in f{} from {} to {}", self, delay, self.__delay) From 42f93aa1a75e489f0aeb9ff63ddd91344567189d Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Tue, 3 Mar 2026 14:31:12 +0000 Subject: [PATCH 13/23] Add missing method --- unittests/mocks.py | 8 ++++++-- unittests/test_populations/test_vertex.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/unittests/mocks.py b/unittests/mocks.py index 9cb30b210f5..ec140c59432 100644 --- a/unittests/mocks.py +++ b/unittests/mocks.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List, Mapping, Optional, Sequence, Tuple, Union +from typing import List, Mapping, Optional, Sequence, Tuple, Union, Dict from numpy.typing import NDArray from spinn_utilities.overrides import overrides @@ -30,7 +30,7 @@ from spynnaker.pyNN.models.neuron import PopulationVertex, \ AbstractPyNNNeuronModel from spynnaker.pyNN.models.neuron.implementations import ( - AbstractNeuronImpl, AbstractStandardNeuronComponent) + AbstractNeuronImpl, AbstractStandardNeuronComponent, ModelParameter) from spynnaker.pyNN.models.neuron.input_types import AbstractInputType from spynnaker.pyNN.models.neuron.synapse_dynamics import ( AbstractSynapseDynamics) @@ -268,6 +268,10 @@ def add_state_variables( self, state_variables: RangeDictionary[float]) -> None: pass + @overrides(AbstractStandardNeuronComponent.get_param_values) + def get_param_values(self) -> Dict[str, ModelParameter]: + return dict() + class MockInputType(MockNeuronComponent, AbstractInputType): diff --git a/unittests/test_populations/test_vertex.py b/unittests/test_populations/test_vertex.py index 7b205262080..de28c0a993c 100644 --- a/unittests/test_populations/test_vertex.py +++ b/unittests/test_populations/test_vertex.py @@ -39,7 +39,7 @@ def __init__(self, foo: int, bar: int): self._bar = bar @overrides(AbstractStandardNeuronComponent.get_param_values) - def get_param_values(self)->Dict[str, ModelParameter]: + def get_param_values(self) -> Dict[str, ModelParameter]: return {"foo": self._foo, "bar": self._bar} @overrides(AbstractStandardNeuronComponent.add_parameters) From 7559a13bb2f8cb72453810630a39f84f095896e2 Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Tue, 3 Mar 2026 14:47:04 +0000 Subject: [PATCH 14/23] Fix tests --- .../abstract_synapse_dynamics.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/spynnaker/pyNN/models/neuron/synapse_dynamics/abstract_synapse_dynamics.py b/spynnaker/pyNN/models/neuron/synapse_dynamics/abstract_synapse_dynamics.py index 1f75b3a529e..8fa6172c189 100644 --- a/spynnaker/pyNN/models/neuron/synapse_dynamics/abstract_synapse_dynamics.py +++ b/spynnaker/pyNN/models/neuron/synapse_dynamics/abstract_synapse_dynamics.py @@ -62,14 +62,15 @@ def __init__(self, delay: WeightsDelysIn, self.__check_in_type(delay, "delay") self.__check_in_type(weight, "weight") - self.__delay = self._round_delay(delay) + self.__delay, delay_rounded = self._round_delay(delay) self.__weight = self._convert_weight(weight) self.__check_out_delay(self.__delay, "delay") self.__check_out_weight(self.__weight, "weight") - if delay is not None and not numpy.allclose( - cast(float, delay), self.__delay): + # This check is done here to ensure the class has all values + # initialized before a warning is displayed. + if delay_rounded: logger.warning("Rounding up delay in f{} from {} to {}", self, delay, self.__delay) @@ -162,17 +163,17 @@ def weight(self) -> Weights: """ return self.__weight - def _round_delay(self, delay: WeightsDelysIn) -> Delays: + def _round_delay(self, delay: WeightsDelysIn) -> Tuple[Delays, bool]: """ Round the delays to multiples of full timesteps. (otherwise SDRAM estimation calculations can go wrong) :param delay: - :return: Rounded delay + :return: Rounded delay, and whether to display a warning """ if isinstance(delay, (RandomDistribution, str)): - return delay + return delay, False if delay is None: delay = SpynnakerDataView.get_min_delay() # Note the cast is just to say trust use the delay will work @@ -181,10 +182,11 @@ def _round_delay(self, delay: WeightsDelysIn) -> Delays: numpy.rint(numpy.array(cast(float, delay)) * SpynnakerDataView.get_simulation_time_step_per_ms()) * SpynnakerDataView.get_simulation_time_step_ms()) + delay_rounded = not numpy.allclose(cast(float, delay), new_delay) if isinstance(new_delay, numpy.float64): - return float(new_delay) + return float(new_delay), delay_rounded if isinstance(new_delay, numpy.ndarray): - return new_delay + return new_delay, delay_rounded raise TypeError(f"{type(delay)=}") def _convert_weight(self, weight: WeightsDelysIn) -> Weights: From 7f07a7f57d6d73b962718f7c265ce7b37f94a230 Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Tue, 3 Mar 2026 15:47:52 +0000 Subject: [PATCH 15/23] Remove pynn link for simple functions --- spynnaker/pyNN/__init__.py | 124 +++++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 55 deletions(-) diff --git a/spynnaker/pyNN/__init__.py b/spynnaker/pyNN/__init__.py index 27b8c01f47a..6a5fe5fc55a 100644 --- a/spynnaker/pyNN/__init__.py +++ b/spynnaker/pyNN/__init__.py @@ -32,7 +32,6 @@ from typing_extensions import Literal from numpy.typing import NDArray -from pyNN import common as pynn_common from pyNN.common import control as _pynn_control from pyNN.recording import get_io from pyNN.random import NumpyRNG @@ -225,9 +224,6 @@ class __PynnOperations(TypedDict, total=False): [Union[str, Sequence[str]], PopulationBase, str, Optional[float], Optional[Dict[str, Any]]], Block] - -# Dynamically-extracted operations from PyNN -__pynn: __PynnOperations = {} # Cache of the simulator created by setup __simulator: Optional[SpiNNaker] = None @@ -311,9 +307,6 @@ def setup(timestep: Optional[Union[float, Literal["auto"]]] = None, logger.warning( "max_delay is not supported by sPyNNaker so will be ignored") - # setup PyNN common stuff - pynn_common.setup(timestep, min_delay, **extra_params) - # create stuff simulator if SpynnakerDataView.is_setup(): logger.warning("Calling setup a second time causes the previous " @@ -339,8 +332,6 @@ def setup(timestep: Optional[Union[float, Literal["auto"]]] = None, logger.warning("Extra params {} have been applied to the setup " "command which we do not consider", extra_params) - # get overloaded functions from PyNN in relation of our simulator object - _create_overloaded_functions(__simulator) SpynnakerDataView.add_database_socket_addresses(database_socket_addresses) return rank() @@ -384,32 +375,6 @@ def Projection( partition_id=partition_id) -def _create_overloaded_functions(spinnaker_simulator: SpiNNaker) -> None: - """ - Creates functions that the main PyNN interface supports - (given from PyNN) - - :param spinnaker_simulator: the simulator object we use underneath - """ - # overload the failed ones with now valid ones, now that we're in setup - # phase. - __pynn["run"], __pynn["run_until"] = pynn_common.build_run( - spinnaker_simulator) - - __pynn["get_current_time"], __pynn["get_time_step"], \ - __pynn["get_min_delay"], __pynn["get_max_delay"], \ - __pynn["num_processes"], __pynn["rank"] = \ - pynn_common.build_state_queries(spinnaker_simulator) - - __pynn["reset"] = pynn_common.build_reset(spinnaker_simulator) - __pynn["create"] = pynn_common.build_create(Population) - - __pynn["connect"] = pynn_common.build_connect( - Projection, FixedProbabilityConnector, StaticSynapse) - - __pynn["record"] = pynn_common.build_record(spinnaker_simulator) - - def end(_: Any = True) -> None: """ Cleans up the SpiNNaker machine and software @@ -530,10 +495,20 @@ def connect(pre: Population, post: Population, weight: float = 0.0, :param delay: the delay of the connections :param receptor_type: excitatory / inhibitory :param p: probability - :param rng: random number generator + :param rng: random number generator (ignored) """ SpynnakerDataView.check_user_can_act() - __pynn["connect"](pre, post, weight, delay, receptor_type, p, rng) + if isinstance(pre, IDMixin): + pre = pre.as_view() + if isinstance(post, IDMixin): + post = post.as_view() + connector = FixedProbabilityConnector(p_connect=p) + synapse = StaticSynapse(weight=weight, delay=delay) + if rng is not None: + warn_once( + logger, "The rng argument to connect is ignored in sPyNNaker.") + return Projection(pre, post, connector, receptor_type=receptor_type, + synapse_type=synapse) def create( @@ -549,7 +524,7 @@ def create( :returns: A new Population """ SpynnakerDataView.check_user_can_act() - return __pynn["create"](cellclass, cellparams, n) + return Population(cellclass, cellparams, n) def NativeRNG(seed_value: Union[int, List[int], NDArray]) -> None: @@ -568,7 +543,7 @@ def get_current_time() -> float: :return: returns the current time """ SpynnakerDataView.check_user_can_act() - return __pynn["get_current_time"]() + return __simulator.t def get_min_delay() -> int: @@ -579,7 +554,7 @@ def get_min_delay() -> int: :return: returns the min delay of the simulation """ SpynnakerDataView.check_user_can_act() - return __pynn["get_min_delay"]() + return __simulator.dt def get_max_delay() -> int: @@ -604,7 +579,7 @@ def get_time_step() -> float: :return: get the time step of the simulation (in ms) """ SpynnakerDataView.check_user_can_act() - return float(__pynn["get_time_step"]()) + return __simulator.dt def initialize(cells: PopulationBase, **initial_values: Any) -> None: @@ -615,7 +590,8 @@ def initialize(cells: PopulationBase, **initial_values: Any) -> None: :param initial_values: the parameters and their values to change """ SpynnakerDataView.check_user_can_act() - pynn_common.initialize(cells, **initial_values) + assert isinstance(cells, (Population, Assembly)), type(cells) + cells.initialize(**initial_values) def num_processes() -> int: @@ -627,8 +603,7 @@ def num_processes() -> int: :return: the number of MPI processes """ - SpynnakerDataView.check_user_can_act() - return __pynn["num_processes"]() + return 1 def rank() -> int: @@ -640,8 +615,7 @@ def rank() -> int: :return: MPI rank """ - SpynnakerDataView.check_user_can_act() - return __pynn["rank"]() + return 0 def record(variables: Union[str, Sequence[str]], source: PopulationBase, @@ -661,8 +635,13 @@ def record(variables: Union[str, Sequence[str]], source: PopulationBase, :return: neo object """ SpynnakerDataView.check_user_can_act() - return __pynn["record"](variables, source, filename, sampling_interval, - annotations) + if not isinstance(source, (Population, Assembly)): + if isinstance(source, (IDMixin)): + source = source.as_view() + source.record(variables, to_file=filename, + sampling_interval=sampling_interval) + if annotations: + source.annotate(**annotations) def reset(annotations: Optional[Dict[str, Any]] = None) -> None: @@ -673,11 +652,46 @@ def reset(annotations: Optional[Dict[str, Any]] = None) -> None: """ if annotations is None: annotations = {} - SpynnakerDataView.check_user_can_act() - __pynn["reset"](annotations) - - -def run(simtime: float, callbacks: Optional[Callable] = None) -> float: + for recorder in __simulator.recorders: + recorder.store_to_cache(annotations) + __simulator.reset() + + +def _run_until(time_point: float, callbacks: Optional[List[Callable]] = None): + """ + Advance the simulation until a given time. + + :param time_point: the time to run until (in ms) + :param callbacks: an optional list of callables, each of which should + accept the current time as an argument, and return the next time it + wishes to be called. + """ + now = __simulator.t + # allow for floating point error + if time_point - now < -__simulator.dt / 2.0: + raise ValueError( + f"Time {time_point} is in the past (current time {now})") + if callbacks: + callback_events = [(callback(__simulator.t), callback) + for callback in callbacks] + while __simulator.t + 1e-9 < time_point: + callback_events.sort(key=lambda cbe: cbe[0], reverse=True) + nxt, callback = callback_events.pop() + # collapse multiple events that happen within the same timestep + active_callbacks = [callback] + while (len(callback_events) > 0 and + abs(nxt - callback_events[-1][0]) < __simulator.dt): + active_callbacks.append(callback_events.pop()[1]) + + nxt = min(nxt, time_point) + __simulator.run_until(nxt) + callback_events.extend((callback(__simulator.t), callback) + for callback in active_callbacks) + else: + __simulator.run_until(time_point) + return __simulator.t + +def run(simtime: float, callbacks: Optional[List[Callable]] = None) -> float: """ The run() function advances the simulation for a given number of milliseconds. @@ -687,7 +701,7 @@ def run(simtime: float, callbacks: Optional[Callable] = None) -> float: :return: the actual simulation time that the simulation stopped at """ SpynnakerDataView.check_user_can_act() - return __pynn["run"](simtime, callbacks) + return _run_until(__simulator.t + simtime, callbacks) # left here because needs to be done, and no better place to put it @@ -703,7 +717,7 @@ def run_until(tstop: float) -> float: :return: the actual simulation time that the simulation stopped at """ SpynnakerDataView.check_user_can_act() - return __pynn["run_until"](tstop, None) + return _run_until(tstop, None) def get_machine() -> Machine: From 282a22de9c5b67eeaca3e16f5be9e18584f6507b Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Tue, 3 Mar 2026 16:12:37 +0000 Subject: [PATCH 16/23] Fix Mypy errors --- spynnaker/pyNN/__init__.py | 15 +++++++++++---- spynnaker/pyNN/spinnaker.py | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/spynnaker/pyNN/__init__.py b/spynnaker/pyNN/__init__.py index 6a5fe5fc55a..ba2b275dc3e 100644 --- a/spynnaker/pyNN/__init__.py +++ b/spynnaker/pyNN/__init__.py @@ -484,8 +484,8 @@ def set_allow_delay_extensions( def connect(pre: Population, post: Population, weight: float = 0.0, - delay: Optional[float] = None, receptor_type: Optional[str] = None, - p: int = 1, rng: Optional[NumpyRNG] = None) -> None: + delay: Optional[float] = None, receptor_type: str = "excitatory", + p: int = 1, rng: Optional[NumpyRNG] = None) -> Projection: """ Builds a projection. @@ -496,6 +496,7 @@ def connect(pre: Population, post: Population, weight: float = 0.0, :param receptor_type: excitatory / inhibitory :param p: probability :param rng: random number generator (ignored) + :returns: a new Projection """ SpynnakerDataView.check_user_can_act() if isinstance(pre, IDMixin): @@ -524,7 +525,7 @@ def create( :returns: A new Population """ SpynnakerDataView.check_user_can_act() - return Population(cellclass, cellparams, n) + return Population(n, cellclass, cellparams) def NativeRNG(seed_value: Union[int, List[int], NDArray]) -> None: @@ -543,6 +544,7 @@ def get_current_time() -> float: :return: returns the current time """ SpynnakerDataView.check_user_can_act() + assert __simulator is not None return __simulator.t @@ -554,6 +556,7 @@ def get_min_delay() -> int: :return: returns the min delay of the simulation """ SpynnakerDataView.check_user_can_act() + assert __simulator is not None return __simulator.dt @@ -579,6 +582,7 @@ def get_time_step() -> float: :return: get the time step of the simulation (in ms) """ SpynnakerDataView.check_user_can_act() + assert __simulator is not None return __simulator.dt @@ -618,7 +622,7 @@ def rank() -> int: return 0 -def record(variables: Union[str, Sequence[str]], source: PopulationBase, +def record(variables: Union[str, Sequence[str]], source: Population, filename: str, sampling_interval: Optional[float] = None, annotations: Optional[Dict[str, Any]] = None) -> Block: """ @@ -652,6 +656,7 @@ def reset(annotations: Optional[Dict[str, Any]] = None) -> None: """ if annotations is None: annotations = {} + assert __simulator is not None for recorder in __simulator.recorders: recorder.store_to_cache(annotations) __simulator.reset() @@ -666,6 +671,7 @@ def _run_until(time_point: float, callbacks: Optional[List[Callable]] = None): accept the current time as an argument, and return the next time it wishes to be called. """ + assert __simulator is not None now = __simulator.t # allow for floating point error if time_point - now < -__simulator.dt / 2.0: @@ -701,6 +707,7 @@ def run(simtime: float, callbacks: Optional[List[Callable]] = None) -> float: :return: the actual simulation time that the simulation stopped at """ SpynnakerDataView.check_user_can_act() + assert __simulator is not None return _run_until(__simulator.t + simtime, callbacks) diff --git a/spynnaker/pyNN/spinnaker.py b/spynnaker/pyNN/spinnaker.py index 465d792e984..e94f625763b 100644 --- a/spynnaker/pyNN/spinnaker.py +++ b/spynnaker/pyNN/spinnaker.py @@ -192,7 +192,7 @@ def clear(self) -> None: """ Clear the current recordings and reset the simulation. """ - self.recorders = set() + self.__recorders = set() self.reset() # Stop any currently running SpiNNaker application From 1f3980df06ecc19ce6d4c14d728ee7ec2e226389 Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Tue, 3 Mar 2026 16:13:52 +0000 Subject: [PATCH 17/23] flake8 --- spynnaker/pyNN/__init__.py | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/spynnaker/pyNN/__init__.py b/spynnaker/pyNN/__init__.py index ba2b275dc3e..92bdd226b71 100644 --- a/spynnaker/pyNN/__init__.py +++ b/spynnaker/pyNN/__init__.py @@ -203,27 +203,6 @@ 'get_max_delay', 'initialize', 'list_standard_models', 'name', 'record', "get_machine"] - -class __PynnOperations(TypedDict, total=False): - run: Callable[[float, Any], float] - run_until: Callable[[float, Any], float] - get_current_time: Callable[[], float] - get_time_step: Callable[[], float] - get_max_delay: Callable[[], int] - get_min_delay: Callable[[], int] - num_processes: Callable[[], int] - rank: Callable[[], int] - reset: Callable[[Dict[str, Any]], None] - create: Callable[ - [Union[Type, AbstractPyNNModel], Optional[Dict[str, Any]], int], - Population] - connect: Callable[ - [Population, Population, float, Optional[float], Optional[str], int, - Optional[NumpyRNG]], None] - record: Callable[ - [Union[str, Sequence[str]], PopulationBase, str, Optional[float], - Optional[Dict[str, Any]]], Block] - # Cache of the simulator created by setup __simulator: Optional[SpiNNaker] = None @@ -500,7 +479,7 @@ def connect(pre: Population, post: Population, weight: float = 0.0, """ SpynnakerDataView.check_user_can_act() if isinstance(pre, IDMixin): - pre = pre.as_view() + pre = pre.as_view() if isinstance(post, IDMixin): post = post.as_view() connector = FixedProbabilityConnector(p_connect=p) @@ -697,6 +676,7 @@ def _run_until(time_point: float, callbacks: Optional[List[Callable]] = None): __simulator.run_until(time_point) return __simulator.t + def run(simtime: float, callbacks: Optional[List[Callable]] = None) -> float: """ The run() function advances the simulation for a given number of From 2c1c4a5da1808644b9c416c7c236eb791548a3f2 Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Tue, 3 Mar 2026 16:36:25 +0000 Subject: [PATCH 18/23] MyPy fixes --- spynnaker/pyNN/__init__.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/spynnaker/pyNN/__init__.py b/spynnaker/pyNN/__init__.py index 92bdd226b71..df31ad0413c 100644 --- a/spynnaker/pyNN/__init__.py +++ b/spynnaker/pyNN/__init__.py @@ -55,7 +55,7 @@ import spynnaker.pyNN as _sim # pylint: disable=import-self from spynnaker.pyNN.exceptions import SpynnakerException - +from spynnaker.pyNN.models.common.types import Names from spynnaker.pyNN.random_distribution import RandomDistribution from spynnaker.pyNN.data import SpynnakerDataView from spynnaker.pyNN.models.abstract_pynn_model import AbstractPyNNModel @@ -464,7 +464,7 @@ def set_allow_delay_extensions( def connect(pre: Population, post: Population, weight: float = 0.0, delay: Optional[float] = None, receptor_type: str = "excitatory", - p: int = 1, rng: Optional[NumpyRNG] = None) -> Projection: + p: int = 1, rng: Optional[NumpyRNG] = None) -> None: """ Builds a projection. @@ -487,8 +487,8 @@ def connect(pre: Population, post: Population, weight: float = 0.0, if rng is not None: warn_once( logger, "The rng argument to connect is ignored in sPyNNaker.") - return Projection(pre, post, connector, receptor_type=receptor_type, - synapse_type=synapse) + Projection(pre, post, connector, receptor_type=receptor_type, + synapse_type=synapse) def create( @@ -527,7 +527,7 @@ def get_current_time() -> float: return __simulator.t -def get_min_delay() -> int: +def get_min_delay() -> float: """ The minimum allowed synaptic delay; delays will be clamped to be at least this. @@ -539,7 +539,7 @@ def get_min_delay() -> int: return __simulator.dt -def get_max_delay() -> int: +def get_max_delay() -> float: """ Part of the PyNN API but does not make sense for sPyNNaker as different Projection, Vertex splitter combination could have different @@ -601,7 +601,7 @@ def rank() -> int: return 0 -def record(variables: Union[str, Sequence[str]], source: Population, +def record(variables: Names, source: Population, filename: str, sampling_interval: Optional[float] = None, annotations: Optional[Dict[str, Any]] = None) -> Block: """ @@ -636,8 +636,6 @@ def reset(annotations: Optional[Dict[str, Any]] = None) -> None: if annotations is None: annotations = {} assert __simulator is not None - for recorder in __simulator.recorders: - recorder.store_to_cache(annotations) __simulator.reset() From 99adaabe5eb58d81c4821e9e9e9be3aa58df11bf Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Wed, 4 Mar 2026 07:53:10 +0000 Subject: [PATCH 19/23] More types and flake8 --- spynnaker/pyNN/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spynnaker/pyNN/__init__.py b/spynnaker/pyNN/__init__.py index df31ad0413c..1678d20e756 100644 --- a/spynnaker/pyNN/__init__.py +++ b/spynnaker/pyNN/__init__.py @@ -25,8 +25,7 @@ import logging import os from typing import ( - Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Type, - TypedDict, Union, cast) + Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, Union, cast) import numpy as __numpy from typing_extensions import Literal @@ -639,7 +638,8 @@ def reset(annotations: Optional[Dict[str, Any]] = None) -> None: __simulator.reset() -def _run_until(time_point: float, callbacks: Optional[List[Callable]] = None): +def _run_until(time_point: float, + callbacks: Optional[List[Callable]] = None) -> float: """ Advance the simulation until a given time. @@ -647,6 +647,7 @@ def _run_until(time_point: float, callbacks: Optional[List[Callable]] = None): :param callbacks: an optional list of callables, each of which should accept the current time as an argument, and return the next time it wishes to be called. + :returns: the actual simulation time that the simulation stopped at """ assert __simulator is not None now = __simulator.t From 90a1613355553faf66eca88f7da522cc506fc8c1 Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Wed, 4 Mar 2026 08:57:45 +0000 Subject: [PATCH 20/23] Spelling issue --- spynnaker/pyNN/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spynnaker/pyNN/__init__.py b/spynnaker/pyNN/__init__.py index 1678d20e756..a4d7f17cfd4 100644 --- a/spynnaker/pyNN/__init__.py +++ b/spynnaker/pyNN/__init__.py @@ -644,8 +644,8 @@ def _run_until(time_point: float, Advance the simulation until a given time. :param time_point: the time to run until (in ms) - :param callbacks: an optional list of callables, each of which should - accept the current time as an argument, and return the next time it + :param callbacks: an optional list of callback functions, each of which + accepts the current time as an argument, and returns the next time it wishes to be called. :returns: the actual simulation time that the simulation stopped at """ From bc521fa26ebf9350acbebf93d08ba84f45b1fc37 Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Wed, 4 Mar 2026 15:28:07 +0000 Subject: [PATCH 21/23] Address some copilot suggestions --- spynnaker/pyNN/__init__.py | 6 ++--- .../abstract_synapse_dynamics.py | 2 +- .../pyNN/models/populations/population.py | 21 +---------------- .../models/populations/population_base.py | 23 ++++++++++++++++++- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/spynnaker/pyNN/__init__.py b/spynnaker/pyNN/__init__.py index 846bfe5fe82..b4270e0a9eb 100644 --- a/spynnaker/pyNN/__init__.py +++ b/spynnaker/pyNN/__init__.py @@ -475,7 +475,6 @@ def connect(pre: Population, post: Population, weight: float = 0.0, :param receptor_type: excitatory / inhibitory :param p: probability :param rng: random number generator (ignored) - :returns: a new Projection """ SpynnakerDataView.check_user_can_act() if isinstance(pre, IDMixin): @@ -601,9 +600,9 @@ def rank() -> int: return 0 -def record(variables: Names, source: Population, +def record(variables: Names, source: PopulationBase, filename: str, sampling_interval: Optional[float] = None, - annotations: Optional[Dict[str, Any]] = None) -> Block: + annotations: Optional[Dict[str, Any]] = None) -> None: """ Sets variables to be recorded. @@ -615,7 +614,6 @@ def record(variables: Names, source: Population, :param sampling_interval: how often to sample the recording, not ignored so far :param annotations: the annotations to data writers - :return: neo object """ SpynnakerDataView.check_user_can_act() if not isinstance(source, (Population, Assembly)): diff --git a/spynnaker/pyNN/models/neuron/synapse_dynamics/abstract_synapse_dynamics.py b/spynnaker/pyNN/models/neuron/synapse_dynamics/abstract_synapse_dynamics.py index 8fa6172c189..3087c28991d 100644 --- a/spynnaker/pyNN/models/neuron/synapse_dynamics/abstract_synapse_dynamics.py +++ b/spynnaker/pyNN/models/neuron/synapse_dynamics/abstract_synapse_dynamics.py @@ -71,7 +71,7 @@ def __init__(self, delay: WeightsDelysIn, # This check is done here to ensure the class has all values # initialized before a warning is displayed. if delay_rounded: - logger.warning("Rounding up delay in f{} from {} to {}", + logger.warning("Rounding up delay in {} from {} to {}", self, delay, self.__delay) def __check_in_type(self, value: WeightsDelysIn, name: str) -> None: diff --git a/spynnaker/pyNN/models/populations/population.py b/spynnaker/pyNN/models/populations/population.py index 6cda4b01700..4a1d21bced3 100644 --- a/spynnaker/pyNN/models/populations/population.py +++ b/spynnaker/pyNN/models/populations/population.py @@ -68,7 +68,6 @@ class Population(PopulationBase): """ # pylint: disable=redefined-builtin __slots__ = ( - "__annotations", "__celltype", "__first_id", "__last_id", @@ -129,7 +128,6 @@ def __init__( if realsize is None: realsize = self.__vertex.n_atoms self.__size = realsize - self.__annotations: Dict[str, Any] = dict() # things for pynn demands self.__first_id, self.__last_id = SpynnakerDataView.add_population( @@ -142,7 +140,7 @@ def __init__( SpynnakerDataView.write_pynn_report( "{} = Population({}, {}, structure={}, initial_values={})", - label, size, model, structure, initial_values, label) + label, size, model, structure, initial_values) def __iter__(self) -> Iterator[PopulationView]: """ @@ -164,23 +162,6 @@ def all(self) -> Iterator[PopulationView]: for _id in range(self.__size): yield IDMixin(self, _id) - def annotate(self, **annotations: Any) -> None: - """ - Add annotations to this population. These are user-defined key-value - pairs that will be stored with the population and can be retrieved - later with the `annotations` property. - - :param annotations: The annotations to add to the population. - """ - self.__annotations.update(annotations) - - @property - def annotations(self) -> Dict[str, Any]: - """ - The annotations given by the end user. - """ - return self.__annotations - @property def celltype(self) -> AbstractPyNNModel: """ diff --git a/spynnaker/pyNN/models/populations/population_base.py b/spynnaker/pyNN/models/populations/population_base.py index fe9a1fd84fe..fdfd98b35a7 100644 --- a/spynnaker/pyNN/models/populations/population_base.py +++ b/spynnaker/pyNN/models/populations/population_base.py @@ -47,7 +47,28 @@ class PopulationBase(object, metaclass=AbstractBase): Mainly pass through and not implemented. """ - __slots__ = () + __slots__ = ( + "__annotations",) + + def __init__(self): + self.__annotations: Dict[str, Any] = dict() + + def annotate(self, **annotations: Any) -> None: + """ + Add annotations to this population. These are user-defined key-value + pairs that will be stored with the population and can be retrieved + later with the `annotations` property. + + :param annotations: The annotations to add to the population. + """ + self.__annotations.update(annotations) + + @property + def annotations(self) -> Dict[str, Any]: + """ + The annotations given by the end user. + """ + return self.__annotations @property def local_cells(self) -> Sequence[IDMixin]: From c3a22c7db7ceeb70a13b506bb5850b870ca87175 Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Wed, 4 Mar 2026 15:34:58 +0000 Subject: [PATCH 22/23] Fixes --- spynnaker/pyNN/__init__.py | 1 - spynnaker/pyNN/models/populations/population.py | 2 ++ spynnaker/pyNN/models/populations/population_base.py | 2 +- spynnaker/pyNN/models/populations/population_view.py | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/spynnaker/pyNN/__init__.py b/spynnaker/pyNN/__init__.py index b4270e0a9eb..d2d86d32e5c 100644 --- a/spynnaker/pyNN/__init__.py +++ b/spynnaker/pyNN/__init__.py @@ -37,7 +37,6 @@ from pyNN.space import ( Space, Line, Grid2D, Grid3D, Cuboid, Sphere, RandomStructure) from pyNN.space import distance as _pynn_distance -from neo import Block from spinn_utilities.exceptions import SimulatorNotSetupException from spinn_utilities.log import FormatAdapter diff --git a/spynnaker/pyNN/models/populations/population.py b/spynnaker/pyNN/models/populations/population.py index 4a1d21bced3..56e262a00fd 100644 --- a/spynnaker/pyNN/models/populations/population.py +++ b/spynnaker/pyNN/models/populations/population.py @@ -99,6 +99,8 @@ def __init__( :param additional_kwargs: A nicer way of allowing additional things """ + super().__init__() + # Deal with the kwargs! additional: _ParamDict = dict() if additional_parameters is not None: diff --git a/spynnaker/pyNN/models/populations/population_base.py b/spynnaker/pyNN/models/populations/population_base.py index fdfd98b35a7..19fa6d67771 100644 --- a/spynnaker/pyNN/models/populations/population_base.py +++ b/spynnaker/pyNN/models/populations/population_base.py @@ -50,7 +50,7 @@ class PopulationBase(object, metaclass=AbstractBase): __slots__ = ( "__annotations",) - def __init__(self): + def __init__(self) -> None: self.__annotations: Dict[str, Any] = dict() def annotate(self, **annotations: Any) -> None: diff --git a/spynnaker/pyNN/models/populations/population_view.py b/spynnaker/pyNN/models/populations/population_view.py index cda8cfbe157..4a37425033c 100644 --- a/spynnaker/pyNN/models/populations/population_view.py +++ b/spynnaker/pyNN/models/populations/population_view.py @@ -99,6 +99,7 @@ def __init__( will all create the same view. :param label: A label for the view """ + super().__init__() self.__parent = parent sized = AbstractSized(parent.size) ids = sized.selector_to_ids(selector, warn=True) From faffd706fab43a8f5affd30547ebccaedad98b1f Mon Sep 17 00:00:00 2001 From: Andrew Rowley Date: Wed, 4 Mar 2026 15:56:15 +0000 Subject: [PATCH 23/23] Remove annotations from pop view --- spynnaker/pyNN/models/populations/population_view.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spynnaker/pyNN/models/populations/population_view.py b/spynnaker/pyNN/models/populations/population_view.py index 4a37425033c..fa15ff66fcf 100644 --- a/spynnaker/pyNN/models/populations/population_view.py +++ b/spynnaker/pyNN/models/populations/population_view.py @@ -68,7 +68,6 @@ class PopulationView(PopulationBase): Selector to Id is actually handled by :py:class:`~.AbstractSized`. """ __slots__ = ( - "__annotations", "__indexes", "__label", "__mask", @@ -77,7 +76,9 @@ class PopulationView(PopulationBase): "__vertex", "__recorder") - __realslots__ = frozenset("_PopulationView" + item for item in __slots__) + __realslots__ = frozenset( + ["_PopulationView" + item for item in __slots__] + + ["_PopulationBase__annotations"]) def __init__( self, parent: Union[Population, 'PopulationView'], @@ -114,7 +115,6 @@ def __init__( if label is None: label = f"{parent.label}:{selector}" self.__label = label - self.__annotations: Dict[str, Any] = dict() # Get these two objects to make access easier # pylint: disable=protected-access @@ -302,7 +302,7 @@ def describe(self, template: str = 'populationview_default.txt', "parent": self.parent.label, "mask": self.mask, "size": self.size} - context.update(self.__annotations) + context.update(self.annotations) return descriptions.render(engine, template, context) def find_units(self, variable: str) -> str: