diff --git a/.github/workflows/python_actions.yml b/.github/workflows/python_actions.yml index d0ed016..e4fe0db 100644 --- a/.github/workflows/python_actions.yml +++ b/.github/workflows/python_actions.yml @@ -44,6 +44,8 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install pip, etc uses: ./support/actions/python-tools + - name: Install mypy + run: pip install mypy - name: Install Spinnaker Dependencies uses: ./support/actions/install-spinn-deps @@ -96,3 +98,6 @@ jobs: - name: Validate CITATION.cff if: matrix.python-version == 3.12 uses: dieghernan/cff-validator@main + + - name: Lint with mypy + run: mypy $CODE_PATHS diff --git a/pyproject.toml b/pyproject.toml index c7a3681..26a7f77 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,4 +14,8 @@ [build-system] requires = ["setuptools"] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" + +[[tool.mypy.overrides]] +module = ["pyNN.*", "quantities", "neo", "scipy", "scipy.*", "lazyarray", "matplotlib.*"] +ignore_missing_imports = true diff --git a/python_models8/connectors/my_connector.py b/python_models8/connectors/my_connector.py index 5396954..44f4d37 100644 --- a/python_models8/connectors/my_connector.py +++ b/python_models8/connectors/my_connector.py @@ -1,4 +1,9 @@ +from numpy.typing import NDArray +from typing import Optional, Sequence from spinn_utilities.overrides import overrides +from pacman.model.graphs.common import Slice +from spynnaker.pyNN.models.neural_projections import SynapseInformation +from spynnaker.pyNN.types import Weight_Types from spynnaker.pyNN.models.neural_projections.connectors import ( AbstractConnector, AbstractGenerateConnectorOnHost) @@ -27,40 +32,45 @@ def __init__(self, weights=0.0, delays=1, allow_self_connections=True # TODO: Store any additional parameters @overrides(AbstractConnector.get_delay_maximum) - def get_delay_maximum(self, synapse_info): + def get_delay_maximum( + self, synapse_info: SynapseInformation) -> Optional[float]: # TODO call self._get_delay_maximum if needed return 16 @overrides(AbstractConnector.get_delay_minimum) - def get_delay_minimum(self, synapse_info): + def get_delay_minimum( + self, synapse_info: SynapseInformation) -> Optional[float]: # TODO call self._get_delay_minimum if needed return 1 @overrides(AbstractGenerateConnectorOnHost.create_synaptic_block) def create_synaptic_block( - self, post_slices, post_vertex_slice, synapse_type, synapse_info): + self, post_slices: Sequence[Slice], post_vertex_slice: Slice, + synapse_type: int, synapse_info: SynapseInformation) -> NDArray: # TODO: update accordingly - pass + raise NotImplementedError @overrides(AbstractConnector.get_weight_variance) - def get_weight_variance( - self, weights, synapse_info): + def get_weight_variance(self, weights: Weight_Types, + synapse_info: SynapseInformation) -> float: # TODO: update accordingly - pass + raise NotImplementedError @overrides(AbstractConnector.get_weight_maximum) - def get_weight_maximum(self, synapse_info): + def get_weight_maximum(self, synapse_info: SynapseInformation) -> float: # TODO: update accordingly - pass + raise NotImplementedError @overrides(AbstractConnector.get_n_connections_from_pre_vertex_maximum) def get_n_connections_from_pre_vertex_maximum( - self, n_post_atoms, synapse_info, min_delay=None, - max_delay=None): + self, n_post_atoms: int, synapse_info: SynapseInformation, + min_delay: Optional[float] = None, + max_delay: Optional[float] = None) -> int: # TODO: update accordingly - pass + raise NotImplementedError @overrides(AbstractConnector.get_n_connections_to_post_vertex_maximum) - def get_n_connections_to_post_vertex_maximum(self, synapse_info): + def get_n_connections_to_post_vertex_maximum( + self, synapse_info: SynapseInformation) -> int: # TODO: update accordingly - pass + raise NotImplementedError diff --git a/python_models8/neuron/implementations/my_full_neuron_impl.py b/python_models8/neuron/implementations/my_full_neuron_impl.py index 673b6d8..ee2ac56 100644 --- a/python_models8/neuron/implementations/my_full_neuron_impl.py +++ b/python_models8/neuron/implementations/my_full_neuron_impl.py @@ -1,8 +1,10 @@ +from typing import List, Mapping, Optional from spinn_front_end_common.interface.ds import DataType from spynnaker.pyNN.utilities.struct import Struct from spynnaker.pyNN.models.neuron.implementations import ( AbstractNeuronImpl) from spinn_utilities.overrides import overrides +from spinn_utilities.ranged import RangeDictionary # TODO: Add names for parameters and state variables THRESHOLD = "threshold" @@ -41,34 +43,34 @@ def __init__(self, @property @overrides(AbstractNeuronImpl.structs) - def structs(self): + def structs(self) -> List[Struct]: return [self._struct] @property @overrides(AbstractNeuronImpl.model_name) - def model_name(self): + def model_name(self) -> str: # TODO: Update the name return "MyFullNeuronImpl" @property @overrides(AbstractNeuronImpl.binary_name) - def binary_name(self): + def binary_name(self) -> str: # TODO: Update the binary name return "my_full_neuron_impl.aplx" @overrides(AbstractNeuronImpl.get_global_weight_scale) - def get_global_weight_scale(self): + def get_global_weight_scale(self) -> float: # TODO: Update if a weight scale is required return 1.0 @overrides(AbstractNeuronImpl.get_n_synapse_types) - def get_n_synapse_types(self): + def get_n_synapse_types(self) -> int: # TODO: Update to the number of synapse types your model uses # (this is the inputs array in this model) return 2 @overrides(AbstractNeuronImpl.get_synapse_id_by_target) - def get_synapse_id_by_target(self, target): + def get_synapse_id_by_target(self, target: str) -> Optional[int]: # TODO: Update with the names that are allowed on a PyNN synapse # receptor_type and match up with indices if target == "excitatory": @@ -78,31 +80,31 @@ def get_synapse_id_by_target(self, target): raise ValueError("Unknown target {}".format(target)) @overrides(AbstractNeuronImpl.get_synapse_targets) - def get_synapse_targets(self): + def get_synapse_targets(self) -> List[str]: # TODO: Update with the names that are allowed on a PyNN synapse # receptor_type return ["excitatory", "inhibitory"] @overrides(AbstractNeuronImpl.get_recordable_variables) - def get_recordable_variables(self): + def get_recordable_variables(self) -> List[str]: # TODO: Update with the names of state variables that can be recorded return ["v"] @overrides(AbstractNeuronImpl.get_recordable_data_types) - def get_recordable_data_types(self): + def get_recordable_data_types(self) -> Mapping[str, DataType]: # TODO: Update with the names and recorded types of the state variables return {"v": DataType.S1615} @overrides(AbstractNeuronImpl.get_recordable_units) - def get_recordable_units(self, variable): + def get_recordable_units(self, variable: str) -> str: # TODO: Update with the appropriate units for variables if variable != "v": raise ValueError("Unknown variable {}".format(variable)) return "mV" @overrides(AbstractNeuronImpl.get_recordable_variable_index) - def get_recordable_variable_index(self, variable): + def get_recordable_variable_index(self, variable: str) -> int: # TODO: Update with the index in the recorded_variable_values array # that the given variable will be recorded in to if variable != "v": @@ -110,29 +112,29 @@ def get_recordable_variable_index(self, variable): return 0 @overrides(AbstractNeuronImpl.is_recordable) - def is_recordable(self, variable): + def is_recordable(self, variable: str) -> bool: # TODO: Update to identify variables that can be recorded return variable == "v" @overrides(AbstractNeuronImpl.add_parameters) - def add_parameters(self, parameters): + def add_parameters(self, parameters: RangeDictionary): # TODO: Write the parameter values parameters[THRESHOLD] = self._threshold @overrides(AbstractNeuronImpl.add_state_variables) - def add_state_variables(self, state_variables): + def add_state_variables(self, state_variables: RangeDictionary): # TODO: Write the state variable values state_variables[V] = self._v state_variables[EXC_INPUT] = self._exc_input state_variables[INH_INPUT] = self._inh_input @overrides(AbstractNeuronImpl.get_units) - def get_units(self, variable): + def get_units(self, variable: str) -> str: # This uses the UNITS dict so shouldn't need to be updated return UNITS[variable] @property @overrides(AbstractNeuronImpl.is_conductance_based) - def is_conductance_based(self): + def is_conductance_based(self) -> bool: # TODO: Update if uses conductance return False diff --git a/python_models8/neuron/input_types/my_input_type.py b/python_models8/neuron/input_types/my_input_type.py index a6bece4..a468247 100644 --- a/python_models8/neuron/input_types/my_input_type.py +++ b/python_models8/neuron/input_types/my_input_type.py @@ -33,14 +33,14 @@ def __init__( def my_multiplicator(self): return self._my_multiplicator - @property - def my_input_parameter(self): - return self._my_input_parameter - @my_multiplicator.setter def my_multiplicator(self, my_multiplicator): self._my_multiplicator = my_multiplicator + @property + def my_input_parameter(self): + return self._my_input_parameter + @my_input_parameter.setter def my_input_parameter(self, my_input_parameter): self._my_input_parameter = my_input_parameter diff --git a/python_models8/neuron/input_types/my_input_type_semd.py b/python_models8/neuron/input_types/my_input_type_semd.py index c09efbd..a753b15 100644 --- a/python_models8/neuron/input_types/my_input_type_semd.py +++ b/python_models8/neuron/input_types/my_input_type_semd.py @@ -1,4 +1,5 @@ from spinn_utilities.overrides import overrides +from spinn_utilities.ranged import RangeDictionary from spinn_front_end_common.interface.ds import DataType from spynnaker.pyNN.models.neuron.input_types import AbstractInputType from spynnaker.pyNN.utilities.struct import Struct @@ -24,11 +25,11 @@ def __init__(self, my_multiplicator, my_inh_input_previous): self.__my_inh_input_previous = my_inh_input_previous @overrides(AbstractInputType.add_parameters) - def add_parameters(self, parameters): + def add_parameters(self, parameters: RangeDictionary[float]): parameters[MY_MULTIPLICATOR] = self.__my_multiplicator @overrides(AbstractInputType.add_state_variables) - def add_state_variables(self, state_variables): + def add_state_variables(self, state_variables: RangeDictionary[float]): state_variables[MY_INH_INPUT_PREVIOUS] = self.__my_inh_input_previous @property diff --git a/python_models8/neuron/neuron_models/my_neuron_model.py b/python_models8/neuron/neuron_models/my_neuron_model.py index 80c7fb8..0b5f417 100644 --- a/python_models8/neuron/neuron_models/my_neuron_model.py +++ b/python_models8/neuron/neuron_models/my_neuron_model.py @@ -1,4 +1,5 @@ from spinn_utilities.overrides import overrides +from spinn_utilities.ranged import RangeDictionary from spinn_front_end_common.interface.ds import DataType from spynnaker.pyNN.models.neuron.implementations import ( AbstractStandardNeuronComponent) @@ -57,13 +58,13 @@ def v(self, v): self._v = v @overrides(AbstractStandardNeuronComponent.add_parameters) - def add_parameters(self, parameters): + def add_parameters(self, parameters: RangeDictionary[float]): # TODO: Add initial values of the parameters that the user can change parameters[I_OFFSET] = self._i_offset parameters[MY_NEURON_PARAMETER] = self._my_neuron_parameter @overrides(AbstractStandardNeuronComponent.add_state_variables) - def add_state_variables(self, state_variables): + def add_state_variables(self, state_variables: RangeDictionary[float]): # TODO: Add initial values of the state variables that the user can # change state_variables[V] = self._v diff --git a/python_models8/neuron/plasticity/stdp/timing_dependence/my_timing_dependence.py b/python_models8/neuron/plasticity/stdp/timing_dependence/my_timing_dependence.py index acbae24..dc1ea0b 100644 --- a/python_models8/neuron/plasticity/stdp/timing_dependence/my_timing_dependence.py +++ b/python_models8/neuron/plasticity/stdp/timing_dependence/my_timing_dependence.py @@ -1,5 +1,9 @@ +from numpy import floating +from numpy.typing import NDArray +from typing import List from spinn_utilities.overrides import overrides -from spinn_front_end_common.interface.ds import DataType +from spinn_front_end_common.interface.ds import ( + DataSpecificationBase, DataType) from spinn_front_end_common.utilities.constants import BYTES_PER_WORD from spynnaker.pyNN.models.neuron.plasticity.stdp.timing_dependence import ( AbstractTimingDependence) @@ -56,7 +60,7 @@ def my_depression_parameter(self, my_depression_parameter): self._my_depression_parameter = my_depression_parameter @overrides(AbstractTimingDependence.is_same_as) - def is_same_as(self, timing_dependence): + def is_same_as(self, timing_dependence: AbstractTimingDependence) -> bool: # TODO: Update with the correct class name if not isinstance(timing_dependence, MyTimingDependence): return False @@ -87,7 +91,7 @@ def pre_trace_n_bytes(self): return 0 @overrides(AbstractTimingDependence.get_parameters_sdram_usage_in_bytes) - def get_parameters_sdram_usage_in_bytes(self): + def get_parameters_sdram_usage_in_bytes(self) -> int: # TODO: update to match the number of bytes used by the parameters return self.NUM_PARAMETERS * BYTES_PER_WORD @@ -101,7 +105,8 @@ def n_weight_terms(self): @overrides(AbstractTimingDependence.write_parameters) def write_parameters( - self, spec, global_weight_scale, synapse_weight_scales): + self, spec: DataSpecificationBase, global_weight_scale: float, + synapse_weight_scales: NDArray[floating]): # TODO: update to write the parameters spec.write_value( self._my_potentiation_parameter, data_type=DataType.S1615) @@ -109,7 +114,7 @@ def write_parameters( self._my_depression_parameter, data_type=DataType.S1615) @overrides(AbstractTimingDependence.get_parameter_names) - def get_parameter_names(self): + def get_parameter_names(self) -> List[str]: return ['my_potentiation_parameter', 'my_depression_parameter'] @property diff --git a/python_models8/neuron/plasticity/stdp/weight_dependence/my_weight_dependence.py b/python_models8/neuron/plasticity/stdp/weight_dependence/my_weight_dependence.py index 723075d..fc642d0 100644 --- a/python_models8/neuron/plasticity/stdp/weight_dependence/my_weight_dependence.py +++ b/python_models8/neuron/plasticity/stdp/weight_dependence/my_weight_dependence.py @@ -1,5 +1,9 @@ +from numpy import floating +from numpy.typing import NDArray +from typing import List from spinn_utilities.overrides import overrides -from spinn_front_end_common.interface.ds import DataType +from spinn_front_end_common.interface.ds import ( + DataSpecificationBase, DataType) from spinn_front_end_common.utilities.constants import BYTES_PER_WORD from spynnaker.pyNN.models.neuron.plasticity.stdp.weight_dependence import ( AbstractWeightDependence, AbstractHasAPlusAMinus) @@ -53,7 +57,8 @@ def my_weight_parameter(self, my_weight_parameter): self._my_weight_parameter = my_weight_parameter @overrides(AbstractWeightDependence.is_same_as) - def is_same_as(self, weight_dependence): + def is_same_as( + self, weight_dependence: AbstractWeightDependence) -> bool: # TODO: Update with the correct class name if not isinstance(weight_dependence, MyWeightDependence): return False @@ -78,7 +83,7 @@ def vertex_executable_suffix(self): @overrides(AbstractWeightDependence.get_parameters_sdram_usage_in_bytes) def get_parameters_sdram_usage_in_bytes( - self, n_synapse_types, n_weight_terms): + self, n_synapse_types: int, n_weight_terms: int) -> int: # TODO: update to match the number of bytes used by the parameters if n_weight_terms != 1: raise NotImplementedError( @@ -88,8 +93,8 @@ def get_parameters_sdram_usage_in_bytes( @overrides(AbstractWeightDependence.write_parameters) def write_parameters( - self, spec, global_weight_scale, synapse_weight_scales, - n_weight_terms): + self, spec: DataSpecificationBase, global_weight_scale: float, + synapse_weight_scales: NDArray[floating], n_weight_terms: int): # TODO: update to write the parameters # Loop through each synapse type's weight scale for w in synapse_weight_scales: @@ -119,5 +124,5 @@ def weight_maximum(self): return self._w_max @overrides(AbstractWeightDependence.get_parameter_names) - def get_parameter_names(self): + def get_parameter_names(self) -> List[str]: return ['w_min', 'w_max', 'my_weight_parameter'] diff --git a/python_models8/neuron/synapse_types/my_synapse_type.py b/python_models8/neuron/synapse_types/my_synapse_type.py index cbc142e..90c4eb6 100644 --- a/python_models8/neuron/synapse_types/my_synapse_type.py +++ b/python_models8/neuron/synapse_types/my_synapse_type.py @@ -1,3 +1,4 @@ +from typing import Optional, Sequence from spinn_utilities.overrides import overrides from spinn_front_end_common.interface.ds import DataType from spynnaker.pyNN.models.neuron.synapse_types import AbstractSynapseType @@ -73,12 +74,12 @@ def my_inh_init(self, my_inh_init): self._my_inh_init = my_inh_init @overrides(AbstractSynapseType.get_n_synapse_types) - def get_n_synapse_types(self): + def get_n_synapse_types(self) -> int: # TODO: Update with the number of supported synapse types return 2 @overrides(AbstractSynapseType.get_synapse_id_by_target) - def get_synapse_id_by_target(self, target): + def get_synapse_id_by_target(self, target: str) -> Optional[int]: # TODO: update the mapping from name to ID if target == "excitatory": return 0 @@ -87,7 +88,7 @@ def get_synapse_id_by_target(self, target): return None @overrides(AbstractSynapseType.get_synapse_targets) - def get_synapse_targets(self): + def get_synapse_targets(self) -> Sequence[str]: # TODO: update to return the same names as above return "excitatory", "inhibitory" diff --git a/python_models8/py.typed b/python_models8/py.typed new file mode 100644 index 0000000..91eaa0c --- /dev/null +++ b/python_models8/py.typed @@ -0,0 +1,13 @@ +# Copyright (c) 2023 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.