Skip to content
Closed
3 changes: 3 additions & 0 deletions spynnaker/pyNN/models/common/parameter_holder.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ def __init__(
def _safe_read_values(self, parameter: str) -> Union[List[float], float]:
values = self.__get_call(parameter, self.__selector)

if not values:
return values

# The values must be a single item, a list or a random distribution;
# if a random distribution we must not have generated yet!
if isinstance(values, RandomDistribution):
Expand Down
7 changes: 2 additions & 5 deletions spynnaker/pyNN/models/common/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from numpy.typing import NDArray
from typing_extensions import TypeAlias

from spinn_utilities.ranged import RangedList
from spynnaker.pyNN.random_distribution import RandomDistribution

#: Type of names of parameters and state variables.
Expand All @@ -27,8 +28,4 @@
float, Sequence[float], NDArray[numpy.floating], RandomDistribution]

#: Type of spikes in spike sources.
Spikes: TypeAlias = Union[
# Can be floating point values (will round)
Values,
# Can be integer values, or lists of such
int, Sequence[int], Sequence[Sequence[int]], NDArray[numpy.integer]]
Spikes: TypeAlias = Values | RangedList[float] | int | Sequence[int] | Sequence[Sequence[int]] | NDArray[numpy.integer]
99 changes: 41 additions & 58 deletions spynnaker/pyNN/models/spike_source/spike_source_array_vertex.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from spinn_utilities.log import FormatAdapter
from spinn_utilities.overrides import overrides
from spinn_utilities.config_holder import get_config_int
from spinn_utilities.ranged import RangedList
from spinn_utilities.ranged.abstract_sized import Selector

from pacman.model.graphs.common import Slice
Expand All @@ -40,7 +41,6 @@
ParameterHolder, PopulationApplicationVertex)
from spynnaker.pyNN.models.common.types import (Names, Spikes)
from spynnaker.pyNN.utilities.buffer_data_type import BufferDataType
from spynnaker.pyNN.utilities.ranged import SpynnakerRangedList

from .spike_source_array_machine_vertex import SpikeSourceArrayMachineVertex

Expand Down Expand Up @@ -104,7 +104,6 @@ class SpikeSourceArrayVertex(
"__model_name",
"__model",
"__structure",
"_spike_times",
"__n_colour_bits")

#: ID of the recording region used for recording transmitted spikes.
Expand Down Expand Up @@ -132,9 +131,6 @@ def __init__(

if spike_times is None:
spike_times = []
self._spike_times = SpynnakerRangedList(
n_neurons, spike_times,
use_list_as_value=not _is_double_list(spike_times))

time_step = SpynnakerDataView.get_simulation_time_step_us()

Expand Down Expand Up @@ -199,6 +195,9 @@ def _check_density_double_list(self, spike_times: _DoubleList) -> None:
counter: Counter = Counter()
for neuron_id in range(0, self.n_atoms):
counter.update(spike_times[neuron_id])
if len(counter) == 0:
logger.warning("SpikeSourceArray all spike times lists are empty")
return
top = counter.most_common(1)
val, count = top[0]
if count > TOO_MANY_SPIKES:
Expand All @@ -219,65 +218,32 @@ def atoms_shape(self) -> Tuple[int, ...]:
return self.__structure.calculate_size(self.n_atoms)
return super().atoms_shape

def _to_early_spikes_single_list(self, spike_times: _SingleList) -> None:
"""
Checks if there is one or more spike_times before the current time.

Logs a warning for the first one found

:param spike_times:
"""
current_time = SpynnakerDataView.get_current_run_time_ms()
for spike_time in spike_times:
if spike_time < current_time:
logger.warning(
"SpikeSourceArray {} has spike_times that are lower than "
"the current time {} For example {} - "
"these will be ignored.",
self, current_time, float(spike_time))
return

def _check_spikes_double_list(self, spike_times: _DoubleList) -> None:
"""
Checks if there is one or more spike_times before the current time.

Logs a warning for the first one found

:param spike_times:
"""
current_time = SpynnakerDataView.get_current_run_time_ms()
for neuron_id in range(0, self.n_atoms):
id_times = spike_times[neuron_id]
for id_time in id_times:
if id_time < current_time:
logger.warning(
"SpikeSourceArray {} has spike_times that are lower "
"than the current time {} For example {} - "
"these will be ignored.",
self, current_time, float(id_time))
return

def __set_spike_buffer_times(self, spike_times: Spikes) -> None:
"""
Set the spike source array's buffer spike times.
"""
time_step = SpynnakerDataView.get_simulation_time_step_us()
# warn the user if they are asking for a spike time out of range
if _is_double_list(spike_times):
self._check_spikes_double_list(spike_times)
elif _is_single_list(spike_times):
self._to_early_spikes_single_list(spike_times)
elif _is_singleton(spike_times):
self._to_early_spikes_single_list([spike_times])
else:
# in case of empty list do not check
pass
self.send_buffer_times = _send_buffer_times(spike_times, time_step)
self.set_send_buffer_times(_send_buffer_times(spike_times, time_step))
self._check_spike_density(spike_times)

def __read_parameter(self, name: str, selector: Selector) -> Sequence:
_ = name
return self._spike_times.get_values(selector)
time_step = SpynnakerDataView.get_simulation_time_step_us()
send_buffer_times = self.send_buffer_times
if send_buffer_times is None:
return []
numpy_times = send_buffer_times * time_step / 1000
double_list = _is_double_list(numpy_times)
if selector or double_list:
# Let RangeList do the heavy lifting using 2D spikes
spike_times = [times.tolist() for times in numpy_times]
range_list: RangedList[float] = (
RangedList(self.n_atoms, spike_times,
use_list_as_value=not double_list))
return range_list.get_values(selector)

# A single list is fine
return numpy_times.tolist()

@overrides(PopulationApplicationVertex.get_parameter_values)
def get_parameter_values(
Expand All @@ -289,9 +255,26 @@ def get_parameter_values(
def set_parameter_values(
self, name: str, value: Spikes, selector: Selector = None) -> None:
self._check_parameters(name, {"spike_times"})
self.__set_spike_buffer_times(value)
self._spike_times.set_value_by_selector(
selector, value, use_list_as_value=not _is_double_list(value))
SpynnakerDataView.set_requires_mapping()
if value is None:
value = []
if selector is None:
self.__set_spike_buffer_times(value)
else:
# get the existing spiketimes in micro seconds
time_step = SpynnakerDataView.get_simulation_time_step_us()
send_buffer_times = self.send_buffer_times
if send_buffer_times is None:
self.__set_spike_buffer_times([])
return
numpy_times = send_buffer_times * time_step / 1000
# Use range list to set based on selector
spike_times: RangedList[float] = RangedList(
self.n_atoms, numpy_times,
use_list_as_value=not _is_double_list(numpy_times))
spike_times.set_value_by_selector(
selector, value, use_list_as_value=not _is_double_list(value))
self.__set_spike_buffer_times(spike_times)

@overrides(PopulationApplicationVertex.get_parameters)
def get_parameters(self) -> List[str]:
Expand Down
78 changes: 78 additions & 0 deletions spynnaker_integration_tests/test_spike_source/test_set_spikes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Copyright (c) 2017 The University of Manchester
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pyNN.spiNNaker as sim
from spinnaker_testbase import BaseTestCase


class TestSetSpikes(BaseTestCase):

def do_run(self) -> None:
n_neurons = 3

sim.setup(timestep=1.0)

pop_1 = sim.Population(n_neurons, sim.IF_curr_exp(), label="pop_1")
input_pop = sim.Population(
n_neurons, sim.SpikeSourceArray(), label="input")
sim.Projection(input_pop, pop_1, sim.OneToOneConnector(),
synapse_type=sim.StaticSynapse(weight=5, delay=1))
pop_1.record(["spikes"])
input_pop.record("spikes")
input_pop.describe()

sim.run(100)
in_spikes = input_pop.spinnaker_get_data("spikes").tolist()
out_spikes = pop_1.spinnaker_get_data("spikes").tolist()
self.assertEqual(0, len(in_spikes))
self.assertEqual(0, len(out_spikes))

input_pop.set(spike_times=[10, 20, 30])
sim.reset()
sim.run(100)
in_spikes = input_pop.spinnaker_get_data("spikes").tolist()
out_spikes = pop_1.spinnaker_get_data("spikes").tolist()
self.assertSequenceEqual([
[0, 10], [0, 20], [0, 30],
[1, 10], [1, 20], [1, 30],
[2, 10], [2, 20], [2, 30]], in_spikes)
self.assertSequenceEqual([
[0, 17], [0, 24], [0, 33],
[1, 17], [1, 24], [1, 33],
[2, 17], [2, 24], [2, 33]], out_spikes)

# ... but set spike times here
input_pop.set(spike_times=[])
spike_times = [[12, 40], [], [23]]
assert len(spike_times) == n_neurons
for idx in range(n_neurons):
input_pop[idx].set(spike_times=spike_times[idx])
sim.reset()
sim.run(100)

in_spikes = input_pop.spinnaker_get_data("spikes").tolist()
out_spikes = pop_1.spinnaker_get_data("spikes").tolist()
self.assertSequenceEqual([[0, 12], [0, 40], [2, 23]], in_spikes)
self.assertSequenceEqual([[0, 19], [0, 45], [2, 30]], out_spikes)

input_pop.set(spike_times=[])
with self.assertRaises(NotImplementedError):
# requires mapping so hard reset required.
sim.run(100)

sim.end()


def test_run(self) -> None:
self.runsafe(self.do_run)
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,63 @@ def test_no_spikes(self) -> None:
if "no spike" in str(record.msg):
found = True
self.assertTrue(found)

# None spike_times is converted to an empty list
self.assertSequenceEqual(
[], list(v.get_parameter_values("spike_times")))

v.set_parameter_values("spike_times", [4, 5, 6])
self.assertSequenceEqual(
[4, 5, 6], list(v.get_parameter_values("spike_times")))
self.assertSequenceEqual(
[4, 5, 6],
list(v.get_parameter_values("spike_times", selector=3)))
self.assertSequenceEqual(
[[4, 5, 6], [4, 5, 6]],
list(v.get_parameter_values("spike_times", selector=[2, 4])))

# None is not PyNN but lets check it works anyway
v.set_parameter_values("spike_times", None) # type: ignore[arg-type]
self.assertSequenceEqual(
[], list(v.get_parameter_values("spike_times")))

v.set_parameter_values("spike_times", 7)
# Single value converted to a list with 1 value
self.assertSequenceEqual(
[7], list(v.get_parameter_values("spike_times")))
self.assertSequenceEqual(
[7],
list(v.get_parameter_values("spike_times", selector=3)))
self.assertSequenceEqual(
[[7], [7]],
list(v.get_parameter_values("spike_times", selector=[2, 4])))

v.set_parameter_values("spike_times", [])
self.assertSequenceEqual(
[], list(v.get_parameter_values("spike_times")))

v.set_parameter_values("spike_times", [1, 2, 3], [1, 3])
self.assertSequenceEqual(
[[], [1, 2, 3], [], [1, 2, 3], []],
list(v.get_parameter_values("spike_times")))
self.assertSequenceEqual(
[[1, 2, 3], []],
list(v.get_parameter_values("spike_times", selector=[1, 4])))
self.assertSequenceEqual(
[1, 2, 3],
list(v.get_parameter_values("spike_times", selector=3)))

def test_double_no_spikes(self) -> None:
with LogCapture() as lc:
SpikeSourceArrayVertex(
n_neurons=5, spike_times=[[], [], [], [], []], label="test",
max_atoms_per_core=10, model=SpikeSourceArray(),
splitter=None, n_colour_bits=None)
found = False
for record in lc.records:
if "lists are empty" in str(record.msg):
found = True
self.assertTrue(found)

def test_singleton_list(self) -> None:
v = SpikeSourceArrayVertex(
Expand Down
Loading