Skip to content

Commit

Permalink
Merge pull request #1415 from SpiNNakerManchester/delay_type
Browse files Browse the repository at this point in the history
Typing and better errors for delay and weights
  • Loading branch information
Christian-B authored Nov 17, 2023
2 parents 5f8270d + 209e4c3 commit 9b814f5
Show file tree
Hide file tree
Showing 20 changed files with 546 additions and 165 deletions.
13 changes: 13 additions & 0 deletions spynnaker/py.typed
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import math
import re
import numpy
from numpy import float64
from numpy.typing import NDArray
from spinn_utilities.log import FormatAdapter
from pyNN.random import NumpyRNG, RandomDistribution

Expand All @@ -24,6 +26,8 @@
from spinn_utilities.abstract_base import AbstractBase, abstractmethod
from spinn_front_end_common.interface.provenance import ProvenanceWriter
from spynnaker.pyNN.data import SpynnakerDataView
from spynnaker.pyNN.types import (
Delay_Types, is_scalar, Weight_Delay_Types, Weight_Types)
from spynnaker.pyNN.utilities import utility_calls
from spynnaker.pyNN.exceptions import SpynnakerException
from spynnaker.pyNN.utilities.constants import SPIKE_PARTITION_ID
Expand Down Expand Up @@ -94,7 +98,9 @@ def set_projection_information(self, synapse_info):
"""
self.__min_delay = SpynnakerDataView.get_simulation_time_step_ms()

def _get_delay_minimum(self, delays, n_connections, synapse_info):
def _get_delay_minimum(
self, delays: Delay_Types, n_connections: int,
synapse_info) -> float:
"""
Get the minimum delay given a float or RandomDistribution.
Expand All @@ -114,12 +120,12 @@ def _get_delay_minimum(self, delays, n_connections, synapse_info):
elif isinstance(delays, str):
d = self._get_distances(delays, synapse_info)
return numpy.min(_expr_context.eval(delays, d=d))
elif numpy.isscalar(delays):
elif is_scalar(delays):
return delays
raise SpynnakerException(
f"Unrecognised delay format: {type(delays)}")
raise self.delay_type_exception(delays)

def _get_delay_maximum(self, delays, n_connections, synapse_info):
def _get_delay_maximum(
self, delays: Delay_Types, n_connections, synapse_info):
"""
Get the maximum delay given a float or RandomDistribution.
Expand All @@ -139,9 +145,9 @@ def _get_delay_maximum(self, delays, n_connections, synapse_info):
elif isinstance(delays, str):
d = self._get_distances(delays, synapse_info)
return numpy.max(_expr_context.eval(delays, d=d))
elif numpy.isscalar(delays):
elif is_scalar(delays):
return delays
raise SpynnakerException(f"Unrecognised delay format: {type(delays)}")
raise self.delay_type_exception(delays)

@abstractmethod
def get_delay_maximum(self, synapse_info):
Expand All @@ -163,7 +169,8 @@ def get_delay_minimum(self, synapse_info):
:rtype: int or None
"""

def get_delay_variance(self, delays, synapse_info):
def get_delay_variance(
self, delays: Delay_Types, synapse_info):
"""
Get the variance of the delays.
Expand All @@ -176,13 +183,13 @@ def get_delay_variance(self, delays, synapse_info):
elif isinstance(delays, str):
d = self._get_distances(delays, synapse_info)
return numpy.var(_expr_context.eval(delays, d=d))
elif numpy.isscalar(delays):
elif is_scalar(delays):
return 0.0
raise SpynnakerException("Unrecognised delay format")
raise self.delay_type_exception(delays)

def _get_n_connections_from_pre_vertex_with_delay_maximum(
self, delays, n_total_connections, n_connections,
min_delay, max_delay, synapse_info):
self, delays: Delay_Types, n_total_connections,
n_connections, min_delay, max_delay, synapse_info):
"""
Get the expected number of delays that will fall within min_delay and
max_delay given given a float, RandomDistribution or list of delays.
Expand Down Expand Up @@ -219,11 +226,11 @@ def _get_n_connections_from_pre_vertex_with_delay_maximum(
prob_delayed = float(n_delayed) / float(n_total)
return int(math.ceil(utility_calls.get_probable_maximum_selected(
n_total_connections, n_connections, prob_delayed)))
elif numpy.isscalar(delays):
elif is_scalar(delays):
if min_delay <= delays <= max_delay:
return int(math.ceil(n_connections))
return 0
raise SpynnakerException("Unrecognised delay format")
raise self.delay_type_exception(delays)

@abstractmethod
def get_n_connections_from_pre_vertex_maximum(
Expand Down Expand Up @@ -255,7 +262,7 @@ def get_n_connections_to_post_vertex_maximum(self, synapse_info):
:rtype: int
"""

def get_weight_mean(self, weights, synapse_info):
def get_weight_mean(self, weights: Weight_Types, synapse_info):
"""
Get the mean of the weights.
Expand All @@ -268,11 +275,12 @@ def get_weight_mean(self, weights, synapse_info):
elif isinstance(weights, str):
d = self._get_distances(weights, synapse_info)
return numpy.mean(_expr_context.eval(weights, d=d))
elif numpy.isscalar(weights):
elif is_scalar(weights):
return abs(weights)
raise SpynnakerException("Unrecognised weight format")
raise self.weight_type_exception(synapse_info)

def _get_weight_maximum(self, weights, n_connections, synapse_info):
def _get_weight_maximum(self, weights: Weight_Types,
n_connections, synapse_info):
"""
Get the maximum of the weights.
Expand Down Expand Up @@ -300,9 +308,9 @@ def _get_weight_maximum(self, weights, n_connections, synapse_info):
elif isinstance(weights, str):
d = self._get_distances(weights, synapse_info)
return numpy.max(_expr_context.eval(weights, d=d))
elif numpy.isscalar(weights):
elif is_scalar(weights):
return abs(weights)
raise SpynnakerException("Unrecognised weight format")
raise self.weight_type_exception(weights)

@abstractmethod
def get_weight_maximum(self, synapse_info):
Expand All @@ -313,7 +321,7 @@ def get_weight_maximum(self, synapse_info):
:rtype: float
"""

def get_weight_variance(self, weights, synapse_info):
def get_weight_variance(self, weights: Weight_Types, synapse_info):
"""
Get the variance of the weights.
Expand All @@ -326,9 +334,9 @@ def get_weight_variance(self, weights, synapse_info):
elif isinstance(weights, str):
d = self._get_distances(weights, synapse_info)
return numpy.var(_expr_context.eval(weights, d=d))
elif numpy.isscalar(weights):
elif is_scalar(weights):
return 0.0
raise SpynnakerException("Unrecognised weight format")
raise self.weight_type_exception(weights)

def _expand_distances(self, d_expression):
"""
Expand All @@ -344,14 +352,9 @@ def _expand_distances(self, d_expression):
regexpr = re.compile(r'.*d\[\d*\].*')
return regexpr.match(d_expression)

def _get_distances(self, values, synapse_info):
def _get_distances(self, values: str, synapse_info):
if self.__space is None:
raise ValueError(
f"Weights or delays are distance-dependent "
f"but no space object was specified in projection "
f"{synapse_info.pre_population}-"
f"{synapse_info.post_population}")

raise self._no_space_exception(values, synapse_info)
expand_distances = self._expand_distances(values)

return self.__space.distances(
Expand Down Expand Up @@ -380,9 +383,69 @@ def _generate_random_values(
return numpy.array([copy_rd.next(1)], dtype="float64")
return copy_rd.next(n_connections)

def _no_space_exception(self, values: Weight_Delay_Types, synapse_info):
"""
Returns a SpynnakerException about there being no space defined
:param values:
:param synapse_info:
:rtype: SpynnakerException
"""
return SpynnakerException(
f"Str Weights or delays {values} are distance-dependent "
f"but no space object was specified in projection "
f"{synapse_info.pre_population}-"
f"{synapse_info.post_population}")

def weight_type_exception(self, weights: Weight_Types):
"""
Returns an Exception explaining incorrect weight or delay type
:param weights:
:raises: SpynnakerException
"""
if weights is None:
return SpynnakerException(
f"The Synapse used is not is not supported with a "
f"{(type(self))} as neither provided weights")
elif isinstance(weights, str):
return SpynnakerException(
f"Str Weights {weights} not supported by a {(type(self))}")
elif isinstance(weights, numpy.ndarray):
# The problem is that these methods are for a MachineVertex/ core
# while weight and delay are supplied at the application level
# The FromList is also the one designed to handle the 2D case
return SpynnakerException(
f"For efficiency reason {type(self)} does not supports "
f"list or arrays for weight."
f"Please use a FromListConnector instead")
else:
return SpynnakerException(f"Unrecognised weight {weights}")

def delay_type_exception(self, delays: Delay_Types):
"""
Returns an Exception explaining incorrect delay type
:param delays:
:raises: SpynnakerException
"""
if isinstance(delays, str):
return SpynnakerException(
f"Str delays {delays} not supported by {(type(self))}")
elif isinstance(delays, numpy.ndarray):
# The problem is that these methods are for a MachineVertex/ core
# while weight and delay are supplied at the application level
# The FromList is also the one designed to handle the 2D case
return SpynnakerException(
f"For efficiency reason {type(self)} does not supports "
f"list or arrays for weight or delay."
f"Please use a FromListConnector instead")
else:
return SpynnakerException(f"Unrecognised delay {delays}")

def _generate_values(
self, values, sources, targets, n_connections, post_slice,
synapse_info):
self, values: Weight_Delay_Types, sources, targets, n_connections,
post_slice, synapse_info, weights: bool) -> NDArray[float64]:
"""
:param values:
:type values: ~pyNN.random.RandomDistribution or int or float or str
Expand All @@ -397,10 +460,7 @@ def _generate_values(
values, n_connections, post_slice)
elif isinstance(values, str) or callable(values):
if self.__space is None:
raise SpynnakerException(
"No space object specified in projection "
f"{synapse_info.pre_population}-"
f"{synapse_info.post_population}")
raise self._no_space_exception(values, synapse_info)

expand_distances = True
if isinstance(values, str):
Expand All @@ -427,7 +487,10 @@ def _generate_values(
return values(d)
elif numpy.isscalar(values):
return numpy.repeat([values], n_connections).astype("float64")
raise SpynnakerException(f"Unrecognised values {values}")
if weights:
raise self.weight_type_exception(values)
else:
raise self.delay_type_exception(values)

def _generate_weights(
self, sources, targets, n_connections, post_slice, synapse_info):
Expand All @@ -441,7 +504,7 @@ def _generate_weights(
"""
weights = self._generate_values(
synapse_info.weights, sources, targets, n_connections, post_slice,
synapse_info)
synapse_info, weights=True)
if self.__safe:
if not weights.size:
warn_once(logger, "No connection in " + str(self))
Expand All @@ -452,7 +515,7 @@ def _generate_weights(
f"{synapse_info.post_population.label}")
return numpy.abs(weights)

def _clip_delays(self, delays):
def _clip_delays(self, delays: NDArray[float64]) -> NDArray[float64]:
"""
Clip delay values, keeping track of how many have been clipped.
Expand Down Expand Up @@ -483,7 +546,7 @@ def _generate_delays(
"""
delays = self._generate_values(
synapse_info.delays, sources, targets, n_connections, post_slice,
synapse_info)
synapse_info, weights=False)

return self._clip_delays(delays)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from spynnaker.pyNN.models.common.param_generator_data import (
param_generator_params, param_generator_params_size_in_bytes,
param_generator_id, is_param_generatable)
from spynnaker.pyNN.types import (Delay_Types, Weight_Types)
from .abstract_generate_connector_on_host import (
AbstractGenerateConnectorOnHost)
from pyNN.random import RandomDistribution
Expand Down Expand Up @@ -58,7 +59,8 @@ def validate_connection(self, application_edge, synapse_info):
" generated on the machine, but the connector cannot"
" be generated on host!")

def generate_on_machine(self, weights, delays):
def generate_on_machine(
self, weights: Weight_Types, delays: Delay_Types):
"""
Determine if this instance can generate on the machine.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from spinn_utilities.overrides import overrides
from spynnaker.pyNN.data import SpynnakerDataView
from spynnaker.pyNN.exceptions import InvalidParameterType
from spynnaker.pyNN.types import Weight_Types
from .abstract_connector import AbstractConnector
from .abstract_generate_connector_on_host import (
AbstractGenerateConnectorOnHost)
Expand Down Expand Up @@ -248,10 +249,10 @@ def get_weight_maximum(self, synapse_info):
return numpy.amax(numpy.abs(self.__weights))

@overrides(AbstractConnector.get_weight_variance)
def get_weight_variance(self, weights, synapse_info):
def get_weight_variance(self, weights: Weight_Types, synapse_info):
# pylint: disable=too-many-arguments
if self.__weights is None:
if hasattr(synapse_info.weights, "__len__"):
if isinstance(synapse_info.weights, numpy.ndarray):
return numpy.var(synapse_info.weights)
return AbstractConnector.get_weight_variance(
self, weights, synapse_info)
Expand Down
Loading

0 comments on commit 9b814f5

Please sign in to comment.