diff --git a/neural_modelling/src/synapse_expander/connection_generator.c b/neural_modelling/src/synapse_expander/connection_generator.c index a5b8777552..706a3dad22 100644 --- a/neural_modelling/src/synapse_expander/connection_generator.c +++ b/neural_modelling/src/synapse_expander/connection_generator.c @@ -32,6 +32,7 @@ #include "connection_generators/connection_generator_fixed_post.h" #include "connection_generators/connection_generator_kernel.h" #include "connection_generators/connection_generator_all_but_me.h" +#include "connection_generators/connection_generator_one_to_one_offset.h" //! \brief Known "hashes" of connection generators //! @@ -45,6 +46,7 @@ enum { FIXED_POST, //!< Fixed post-size connection generator KERNEL, //!< Convolution kernel connection generator ALL_BUT_ME, //!< AllButMe connection generator + ONE_TO_ONE_OFFSET, //!< One-to-one offset connection generator N_CONNECTION_GENERATORS//!< The number of known generators }; @@ -102,7 +104,11 @@ static const connection_generator_info connection_generators[] = { {ALL_BUT_ME, connection_generator_all_but_me_initialise, connection_generator_all_but_me_generate, - connection_generator_all_but_me_free} + connection_generator_all_but_me_free}, + {ONE_TO_ONE_OFFSET, + connection_generator_one_to_one_offset_initialise, + connection_generator_one_to_one_offset_generate, + connection_generator_one_to_one_offset_free} }; connection_generator_t connection_generator_init( diff --git a/neural_modelling/src/synapse_expander/connection_generators/connection_generator_one_to_one_offset.h b/neural_modelling/src/synapse_expander/connection_generators/connection_generator_one_to_one_offset.h new file mode 100644 index 0000000000..e4941797ed --- /dev/null +++ b/neural_modelling/src/synapse_expander/connection_generators/connection_generator_one_to_one_offset.h @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2024 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. + */ + +/** + * \file + * \brief one_to_one_offset Connection generator implementation + */ + +#include + +//! \brief The parameters to be passed around for this connector +struct one_to_one_offset { + // Amount to add to the pre by to get the post + int32_t offset; + // Whether to wrap around the post values or just clip + uint32_t wrap; + // The group size to consider for the offset + uint32_t n_neurons_per_group; + +}; + + +/** + * \brief Initialise the one_to_one_offset connection generator + * \param[in,out] region: Region to read parameters from. Should be updated + * to position just after parameters after calling. + * \return A data item to be passed in to other functions later on + */ +static void *connection_generator_one_to_one_offset_initialise(UNUSED void **region) { + // Allocate the data structure for parameters + struct one_to_one_offset *params = spin1_malloc(sizeof(struct one_to_one_offset)); + struct one_to_one_offset *params_sdram = *region; + + // Copy the parameters into the data structure + *params = *params_sdram; + *region = ¶ms_sdram[1]; + + log_debug("one_to_one_offset connector, one_to_one_offset = %u, wrap = %u, " + "n_neurons_per_group = %u", + params->offset, params->wrap, params->n_neurons_per_group); + + return params; +} + +/** + * \brief Free the one_to_one_offset connection generator + * \param[in] generator: The generator to free + */ +static void connection_generator_one_to_one_offset_free(UNUSED void *generator) { + // Nothing to do +} + +/** + * \brief Generate connections with the one_to_one_offset connection generator + * \param[in] generator: The generator to use to generate connections + * \param[in] pre_slice_start: The start of the slice of the pre-population + * being generated + * \param[in] pre_slice_count: The number of neurons in the slice of the + * pre-population being generated + * \param[in] pre_neuron_index: The index of the neuron in the pre-population + * being generated + * \param[in] post_slice_start: The start of the slice of the post-population + * being generated + * \param[in] post_slice_count: The number of neurons in the slice of the + * post-population being generated + * \param[in] max_row_length: The maximum number of connections to generate + * \param[in,out] indices: An array into which the core-relative post-indices + * should be placed. This will be initialised to be + * \p max_row_length in size + * \return The number of connections generated + */ +static bool connection_generator_one_to_one_offset_generate( + void *generator, uint32_t pre_lo, uint32_t pre_hi, + uint32_t post_lo, uint32_t post_hi, UNUSED uint32_t post_index, + uint32_t post_slice_start, uint32_t post_slice_count, + unsigned long accum weight_scale, accum timestep_per_delay, + param_generator_t weight_generator, param_generator_t delay_generator, + matrix_generator_t matrix_generator) { + + struct one_to_one_offset *obj = generator; + + // Get the actual ranges to generate within + uint32_t post_start = max(post_slice_start, post_lo); + uint32_t post_end = min(post_slice_start + post_slice_count - 1, post_hi); + + // Work out where we are in the generation + // We need to connect each pre-neuron to each post-neuron in each group + // (but not to itself). We are currently generating a subset of the post + // neurons, so we need to work out which group we are in within that subset, + // and which is the first post-neuron in the group that we are generating + // for now. + uint32_t post_group; + uint32_t post_value; + div_mod(post_start, obj->n_neurons_per_group, &post_group, &post_value); + + // Work out where the pre-neurons start and end for the group that we are + // in at the start of the post-neurons. + uint32_t pre_start = pre_lo + post_group * obj->n_neurons_per_group; + uint32_t pre_end = min(pre_start + obj->n_neurons_per_group - 1, pre_hi); + + // Go through the post neurons in this slice + for (uint32_t post = post_start; post <= post_end; post++) { + uint32_t local_post = post - post_slice_start; + + // Find the pre that occurs after offset; as the offset is post from + // pre, we subtract it to get pre from post (note it might be negative already) + int32_t pre = post - obj->offset; + bool use = true; + if (pre < (int32_t) pre_start) { + if (obj->wrap) { + pre += obj->n_neurons_per_group; + } else { + use = false; + } + } else if (pre > (int32_t) pre_end) { + if (obj->wrap) { + pre -= obj->n_neurons_per_group; + } else { + use = false; + } + } + + if (use) { + accum weight = param_generator_generate(weight_generator); + uint16_t delay = rescale_delay( + param_generator_generate(delay_generator), timestep_per_delay); + if (!matrix_generator_write_synapse(matrix_generator, (uint32_t) pre, + local_post, weight, delay, weight_scale)) { + log_error("Matrix not sized correctly!"); + return false; + } + } + + // Work out next loop iteration. If we have reached the end of a group + // of values, we need to move onto the next group. + post_value += 1; + if (post_value == obj->n_neurons_per_group) { + post_value = 0; + pre_start += obj->n_neurons_per_group; + pre_end = min(pre_start + obj->n_neurons_per_group - 1, pre_hi); + if (pre_start > pre_hi) { + break; + } + } + } + + return true; +} diff --git a/spynnaker/pyNN/extra_models/__init__.py b/spynnaker/pyNN/extra_models/__init__.py index 8ad6fdf95a..8efe5c8b2b 100644 --- a/spynnaker/pyNN/extra_models/__init__.py +++ b/spynnaker/pyNN/extra_models/__init__.py @@ -34,7 +34,7 @@ IFCurrDeltaCa2Adaptive, StocExp, StocExpStable, StocSigma, IFTruncDelta, IFCurrDeltaFixedProb) from spynnaker.pyNN.models.neural_projections.connectors import ( - AllButMeConnector) + AllButMeConnector, OneToOneOffsetConnector) # Variable rate poisson from spynnaker.pyNN.models.spike_source import SpikeSourcePoissonVariable @@ -64,7 +64,7 @@ 'IFTruncDelta', # Connectors - 'AllButMeConnector', + 'AllButMeConnector', 'OneToOneOffsetConnector', # Weight changeable synapse dynamics 'WeightChangeable', 'WeightChanger' diff --git a/spynnaker/pyNN/models/neural_projections/connectors/__init__.py b/spynnaker/pyNN/models/neural_projections/connectors/__init__.py index f6eecc2b75..5ddacd77fb 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/__init__.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/__init__.py @@ -35,6 +35,7 @@ from .kernel_connector import KernelConnector from .convolution_connector import ConvolutionConnector from .pool_dense_connector import PoolDenseConnector +from .one_to_one_offset_connector import OneToOneOffsetConnector __all__ = ["AbstractConnector", "AbstractGenerateConnectorOnMachine", "AbstractGenerateConnectorOnHost", "AllButMeConnector", @@ -44,4 +45,5 @@ "FromFileConnector", "FromListConnector", "IndexBasedProbabilityConnector", "KernelConnector", "ConvolutionConnector", "PoolDenseConnector", - "MultapseConnector", "OneToOneConnector", "SmallWorldConnector"] + "MultapseConnector", "OneToOneConnector", "SmallWorldConnector", + "OneToOneOffsetConnector"] diff --git a/spynnaker/pyNN/models/neural_projections/connectors/abstract_generate_connector_on_machine.py b/spynnaker/pyNN/models/neural_projections/connectors/abstract_generate_connector_on_machine.py index f98369064f..03c103f749 100644 --- a/spynnaker/pyNN/models/neural_projections/connectors/abstract_generate_connector_on_machine.py +++ b/spynnaker/pyNN/models/neural_projections/connectors/abstract_generate_connector_on_machine.py @@ -54,6 +54,7 @@ class ConnectorIDs(Enum): FIXED_NUMBER_POST_CONNECTOR = 5 KERNEL_CONNECTOR = 6 WTA_CONNECTOR = 7 + ONE_TO_ONE_OFFSET_CONNECTOR = 8 class AbstractGenerateConnectorOnMachine( diff --git a/spynnaker/pyNN/models/neural_projections/connectors/one_to_one_offset_connector.py b/spynnaker/pyNN/models/neural_projections/connectors/one_to_one_offset_connector.py new file mode 100644 index 0000000000..628b57ab96 --- /dev/null +++ b/spynnaker/pyNN/models/neural_projections/connectors/one_to_one_offset_connector.py @@ -0,0 +1,239 @@ +# Copyright (c) 2024 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. +from __future__ import annotations +from typing import Sequence, Optional, TYPE_CHECKING + +import numpy +from numpy import uint32 +from numpy.typing import NDArray + +from spinn_utilities.overrides import overrides + +from pacman.model.graphs.common import Slice + +from spinn_front_end_common.utilities.constants import BYTES_PER_WORD + +from .abstract_connector import AbstractConnector +from .abstract_generate_connector_on_machine import ( + AbstractGenerateConnectorOnMachine, ConnectorIDs) +from .abstract_generate_connector_on_host import ( + AbstractGenerateConnectorOnHost) + +if TYPE_CHECKING: + from spynnaker.pyNN.models.neural_projections import SynapseInformation + from spynnaker.pyNN.models.neural_projections import ( + ProjectionApplicationEdge) + + +class OneToOneOffsetConnector( + AbstractGenerateConnectorOnMachine, + AbstractGenerateConnectorOnHost): + """ + A Connector that connects each pre-neuron to a post-neuron offset by a + specific amount, positive or negative. If this goes beyond the start or + end of the post neurons, it can optionally wrap around. Additional options + include a group size, where the offset and wrap is applied repeatedly to + subsets of neurons. + + In the current implementation it is assumed that the pre- and + post-populations have the same number of neurons, and that the number of + neurons is divisible by the group size if specified. The offset must also + be smaller than the group size or the number of neurons. + """ + + __slots__ = ("__n_neurons_per_group", "__offset", "__wrap") + + def __init__(self, offset: int, wrap: bool, + n_neurons_per_group: Optional[int] = None, + safe: bool = True, verbose: bool = False, + callback: None = None): + """ + :param offset: + The offset to apply to the pre-neuron index to get the post neuron + index. This can be positive or negative. + :param wrap: + Whether to wrap around the start or end of the post neurons if the + post neuron id is out of range. + :param n_neurons_per_group: + The number of neurons in each group. + Must be a positive integer divisor of source.size + :param safe: + If ``True``, check that weights and delays have valid values. + If ``False``, this check is skipped. + :param verbose: + Whether to output extra information about the connectivity to a + CSV file + :param callback: + if given, a callable that display a progress bar on the terminal. + + .. note:: + Not supported by sPyNNaker. + """ + super().__init__(safe, callback, verbose) + self.__n_neurons_per_group = n_neurons_per_group + self.__offset = offset + self.__wrap = wrap + + def __n_connections(self, synapse_info: SynapseInformation): + if self.__wrap: + # If there is a wrap, there will always be a next connection + return synapse_info.n_pre_neurons + + n_groups = 1 + if self.__n_neurons_per_group is not None: + n_groups = synapse_info.n_pre_neurons // self.__n_neurons_per_group + + # If there isn't a wrap, there are always offset less per group + return synapse_info.n_pre_neurons - (n_groups * abs(self.__offset)) + + @overrides(AbstractConnector.get_delay_maximum) + def get_delay_maximum(self, synapse_info: SynapseInformation) -> float: + return self._get_delay_maximum( + synapse_info.delays, self.__n_connections(synapse_info), + synapse_info) + + @overrides(AbstractConnector.get_delay_minimum) + def get_delay_minimum(self, synapse_info: SynapseInformation) -> float: + return self._get_delay_minimum( + synapse_info.delays, self.__n_connections(synapse_info), + synapse_info) + + @overrides(AbstractConnector.get_n_connections_from_pre_vertex_maximum) + def get_n_connections_from_pre_vertex_maximum( + self, n_post_atoms: int, synapse_info: SynapseInformation, + min_delay: Optional[float] = None, + max_delay: Optional[float] = None) -> int: + + # At most each pre-neuron will one post neuron + return 1 + + @overrides(AbstractConnector.get_n_connections_to_post_vertex_maximum) + def get_n_connections_to_post_vertex_maximum( + self, synapse_info: SynapseInformation) -> int: + # At most each post neuron will be targeted by one pre-neuron + return 1 + + @overrides(AbstractConnector.get_weight_maximum) + def get_weight_maximum(self, synapse_info: SynapseInformation) -> float: + return self._get_weight_maximum( + synapse_info.weights, self.__n_connections(synapse_info), + synapse_info) + + @overrides(AbstractGenerateConnectorOnHost.create_synaptic_block) + def create_synaptic_block( + self, post_slices: Sequence[Slice], post_vertex_slice: Slice, + synapse_type: int, synapse_info: SynapseInformation) -> NDArray: + group_size = self.__n_neurons_per_group + if group_size is None: + group_size = synapse_info.n_pre_neurons + + # pylint: disable=protected-access + post_lo, post_hi = synapse_info.pre_population._view_range + pre_lo, pre_hi = synapse_info.pre_population._view_range + post_start = max(post_vertex_slice.lo_atom, post_lo) + post_end = min(post_vertex_slice.hi_atom + 1, post_hi + 1) + post_group, post_value = divmod(post_start, group_size) + + pre_start = pre_lo + (post_group * group_size) + pre_end = min(pre_start + group_size, pre_hi) + + pres = list() + posts = list() + for post in range(post_start, post_end): + pre = post - self.__offset + if pre < pre_start: + if self.__wrap: + pre += group_size + else: + continue + if pre > pre_end: + if self.__wrap: + pre -= group_size + else: + continue + pres.append(pre) + posts.append(post) + + post_value += 1 + if post_value == group_size: + post_value = 0 + pre_start += group_size + pre_end = min(pre_start + group_size, pre_hi) + if pre_start >= pre_hi: + break + + block = numpy.zeros(len(pres), dtype=self.NUMPY_SYNAPSES_DTYPE) + block["source"] = pres + block["target"] = posts + block["weight"] = self._generate_weights( + block["source"], block["target"], len(pres), post_vertex_slice, + synapse_info) + block["delay"] = self._generate_delays( + block["source"], block["target"], len(pres), post_vertex_slice, + synapse_info) + block["synapse_type"] = synapse_type + return block + + def __repr__(self): + return (f"offsetConnector(offset={self.__offset}, wrap={self.__wrap}, " + f"n_neurons_per_group={self.__n_neurons_per_group})") + + @property + @overrides(AbstractGenerateConnectorOnMachine.gen_connector_id) + def gen_connector_id(self) -> int: + return ConnectorIDs.ONE_TO_ONE_OFFSET_CONNECTOR.value + + @overrides(AbstractGenerateConnectorOnMachine.gen_connector_params) + def gen_connector_params( + self, synapse_info: SynapseInformation) -> NDArray[uint32]: + n_values = self.__n_neurons_per_group + if n_values is None: + n_values = synapse_info.n_pre_neurons + return numpy.array([self.__offset, int(self.__wrap), n_values], + dtype=uint32) + + @property + @overrides( + AbstractGenerateConnectorOnMachine.gen_connector_params_size_in_bytes) + def gen_connector_params_size_in_bytes(self) -> int: + return BYTES_PER_WORD * 3 + + @overrides(AbstractConnector.validate_connection) + def validate_connection( + self, application_edge: ProjectionApplicationEdge, + synapse_info: SynapseInformation): + if (synapse_info.pre_population.size != + synapse_info.post_population.size): + raise NotImplementedError( + "OneToOneOffsetConnector is only designed to be used with " + "populations that are the same size as each other") + if self.__n_neurons_per_group is not None: + if self.__n_neurons_per_group > synapse_info.pre_population.size: + raise ValueError( + "OneToOneOffsetConnector cannot be used with a group size " + "larger than the population size") + if ((synapse_info.post_population.size / + self.__n_neurons_per_group) != + (synapse_info.post_population.size // + self.__n_neurons_per_group)): + raise NotImplementedError( + "The number of neurons in each population must be " + "divisible by the number of neurons per group") + + n_values = self.__n_neurons_per_group + if n_values is None: + n_values = synapse_info.n_pre_neurons + if n_values < abs(self.__offset): + raise ValueError( + "The offset must be smaller than the number of neurons") diff --git a/spynnaker_integration_tests/test_connectors/test_one_to_one_offset_connector.py b/spynnaker_integration_tests/test_connectors/test_one_to_one_offset_connector.py new file mode 100644 index 0000000000..f88de87661 --- /dev/null +++ b/spynnaker_integration_tests/test_connectors/test_one_to_one_offset_connector.py @@ -0,0 +1,156 @@ +# Copyright (c) 2024 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 +import pytest +import numpy + +from pacman.model.graphs.common.slice import Slice +from spinnaker_testbase import BaseTestCase + + +class TestOneToOneOffsetConnector(BaseTestCase): + + def check_offset(self): + timestep = 1.0 + sim.setup(timestep=timestep) + sim.set_number_of_neurons_per_core(sim.IF_curr_exp, 2) + pop = sim.Population(11, sim.IF_curr_exp()) + proj_no_wrap = sim.Projection( + pop, pop, sim.extra_models.OneToOneOffsetConnector(-2, wrap=False), + synapse_type=sim.StaticSynapse()) + proj_wrap = sim.Projection( + pop, pop, sim.extra_models.OneToOneOffsetConnector(3, wrap=True), + synapse_type=sim.StaticSynapse()) + sim.run(0) + conns_no_wrap = list(proj_no_wrap.get([], format="list")) + conns_wrap = list(proj_wrap.get([], format="list")) + sim.end() + print(conns_wrap) + print(conns_no_wrap) + assert len(conns_no_wrap) == 9 + assert len(conns_wrap) == 11 + for i, j in conns_no_wrap: + assert j == i - 2 + for i, j in conns_wrap: + assert j == (i + 3) % 11 + + def test_offset(self): + self.runsafe(self.check_offset) + + def check_offset_groups(self): + sim.setup(timestep=1) + sim.set_number_of_neurons_per_core(sim.IF_curr_exp, 5) + pop = sim.Population(12, sim.IF_curr_exp()) + proj_no_wrap = sim.Projection( + pop, pop, sim.extra_models.OneToOneOffsetConnector( + 4, wrap=False, n_neurons_per_group=6)) + proj_wrap = sim.Projection( + pop, pop, sim.extra_models.OneToOneOffsetConnector( + -1, wrap=True, n_neurons_per_group=3)) + sim.run(0) + conns_no_wrap = list(proj_no_wrap.get([], format="list")) + conns_wrap = list(proj_wrap.get([], format="list")) + sim.end() + + assert len(conns_no_wrap) == 4 + assert len(conns_wrap) == 12 + + for i, j in conns_no_wrap: + group_i = i // 6 + group_j = j // 6 + assert group_i == group_j + assert j == i + 4 + + for i, j in conns_wrap: + group_i = i // 3 + group_j = j // 3 + assert group_i == group_j + assert j - (group_j * 3) == (i - (group_i * 3) - 1) % 3 + + def test_offset_groups(self): + self.runsafe(self.check_offset_groups) + + def check_offset_offline(self): + sim.setup(timestep=1) + pop = sim.Population(11, sim.IF_curr_exp()) + conn_no_wrap = sim.extra_models.OneToOneOffsetConnector( + offset=-1, wrap=False) + conn_wrap = sim.extra_models.OneToOneOffsetConnector( + offset=3, wrap=True) + proj_wrap = sim.Projection(pop, pop, conn_wrap) + proj_no_wrap = sim.Projection(pop, pop, conn_no_wrap) + sim.run(0) + conns_wrap = list(proj_wrap.get([], format="list")) + conns_no_wrap = list(proj_no_wrap.get([], format="list")) + post_vertex_slice = Slice(0, 11) + post_slices = [post_vertex_slice] + synapse_type = 0 + synapse_info_no_wrap = proj_no_wrap._synapse_information + synapse_info_wrap = proj_wrap._synapse_information + block_no_wrap = conn_no_wrap.create_synaptic_block( + post_slices, post_vertex_slice, synapse_type, synapse_info_no_wrap) + block_wrap = conn_wrap.create_synaptic_block( + post_slices, post_vertex_slice, synapse_type, synapse_info_wrap) + offline_conns_no_wrap = sorted( + list([i, j] for (i, j, _w, _d, _typ) in block_no_wrap)) + offline_conns_wrap = sorted( + list([i, j] for (i, j, _w, _d, _typ) in block_wrap)) + sim.end() + assert numpy.array_equal(conns_no_wrap, offline_conns_no_wrap) + assert numpy.array_equal(conns_wrap, offline_conns_wrap) + + def test_offset_offline(self): + self.runsafe(self.check_offset_offline) + + def check_offset_wrong_number_of_neurons(self): + sim.setup(timestep=1) + sim.set_number_of_neurons_per_core(sim.IF_curr_exp, 3) + pre = sim.Population(11, sim.IF_curr_exp()) + post = sim.Population(11, sim.IF_curr_exp()) + with pytest.raises(NotImplementedError): + sim.Projection( + pre, post, sim.extra_models.OneToOneOffsetConnector( + 3, False, n_neurons_per_group=3)) + sim.end() + + def test_offset_wrong_number_of_neurons(self): + self.runsafe(self.check_offset_wrong_number_of_neurons) + + def check_offset_diff_number_of_neurons(self): + sim.setup(timestep=1) + sim.set_number_of_neurons_per_core(sim.IF_curr_exp, 3) + pre = sim.Population(12, sim.IF_curr_exp()) + post = sim.Population(9, sim.IF_curr_exp()) + with pytest.raises(NotImplementedError): + sim.Projection( + pre, post, sim.extra_models.OneToOneOffsetConnector( + 2, True, n_neurons_per_group=3)) + sim.end() + + def test_offset_diff_number_of_neurons(self): + self.runsafe(self.check_offset_diff_number_of_neurons) + + def check_offset_wrong_offset(self): + sim.setup(timestep=1) + sim.set_number_of_neurons_per_core(sim.IF_curr_exp, 3) + pre = sim.Population(12, sim.IF_curr_exp()) + post = sim.Population(12, sim.IF_curr_exp()) + with pytest.raises(ValueError): + sim.Projection( + pre, post, sim.extra_models.OneToOneOffsetConnector( + 12, True, n_neurons_per_group=3)) + sim.end() + + def test_offset_wrong_offset(self): + self.runsafe(self.check_offset_wrong_offset)