Source code for spynnaker.pyNN.models.neuron.abstract_population_vertex

# Copyright (c) 2017-2019 The University of Manchester
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import logging
import math
import numpy
from scipy import special  # @UnresolvedImport

from spinn_utilities.log import FormatAdapter
from spinn_utilities.overrides import overrides
from spinn_utilities.progress_bar import ProgressBar
from data_specification.enums.data_type import DataType
from pacman.model.constraints.key_allocator_constraints import (
    ContiguousKeyRangeContraint)
from spinn_utilities.config_holder import (
    get_config_int, get_config_float, get_config_bool)
from pacman.model.resources import MultiRegionSDRAM
from spinn_front_end_common.abstract_models import (
    AbstractChangableAfterRun, AbstractProvidesOutgoingPartitionConstraints,
    AbstractCanReset, AbstractRewritesDataSpecification)
from spinn_front_end_common.abstract_models.impl import (
    ProvidesKeyToAtomMappingImpl, TDMAAwareApplicationVertex)
from spinn_front_end_common.utilities.constants import (
    BYTES_PER_WORD, MICRO_TO_SECOND_CONVERSION, SYSTEM_BYTES_REQUIREMENT)
from spinn_front_end_common.utilities.exceptions import ConfigurationException
from spinn_front_end_common.interface.profiling.profile_utils import (
    get_profile_region_size)
from spinn_front_end_common.interface.buffer_management\
    .recording_utilities import (
       get_recording_header_size, get_recording_data_constant_size)
from spinn_front_end_common.interface.provenance import (
    ProvidesProvenanceDataFromMachineImpl)
from spinn_front_end_common.utilities.globals_variables import (
    machine_time_step)

from spynnaker.pyNN.models.common import (
    AbstractSpikeRecordable, AbstractNeuronRecordable, AbstractEventRecordable,
    NeuronRecorder)
from spynnaker.pyNN.models.abstract_models import (
    AbstractPopulationInitializable, AbstractAcceptsIncomingSynapses,
    AbstractPopulationSettable, AbstractContainsUnits, AbstractMaxSpikes,
    HasSynapses)
from spynnaker.pyNN.exceptions import InvalidParameterType
from spynnaker.pyNN.utilities.ranged import (
    SpynnakerRangeDictionary)
from spynnaker.pyNN.utilities.constants import POSSION_SIGMA_SUMMATION_LIMIT
from spynnaker.pyNN.utilities.running_stats import RunningStats
from spynnaker.pyNN.models.neuron.synapse_dynamics import (
    AbstractSynapseDynamics, AbstractSynapseDynamicsStructural)
from .synapse_io import get_max_row_info
from .master_pop_table import MasterPopTableAsBinarySearch
from .generator_data import GeneratorData
from .synaptic_matrices import SYNAPSES_BASE_GENERATOR_SDRAM_USAGE_IN_BYTES

logger = FormatAdapter(logging.getLogger(__name__))

# TODO: Make sure these values are correct (particularly CPU cycles)
_NEURON_BASE_DTCM_USAGE_IN_BYTES = 9 * BYTES_PER_WORD
_NEURON_BASE_N_CPU_CYCLES_PER_NEURON = 22
_NEURON_BASE_N_CPU_CYCLES = 10

# 1 for number of neurons
# 1 for number of synapse types
# 1 for number of neuron bits
# 1 for number of synapse type bits
# 1 for number of delay bits
# 1 for drop late packets,
# 1 for incoming spike buffer size
_SYNAPSES_BASE_SDRAM_USAGE_IN_BYTES = 7 * BYTES_PER_WORD


[docs]class AbstractPopulationVertex( TDMAAwareApplicationVertex, AbstractContainsUnits, AbstractSpikeRecordable, AbstractNeuronRecordable, AbstractEventRecordable, AbstractProvidesOutgoingPartitionConstraints, AbstractPopulationInitializable, AbstractPopulationSettable, AbstractChangableAfterRun, AbstractAcceptsIncomingSynapses, ProvidesKeyToAtomMappingImpl, AbstractCanReset): """ Underlying vertex model for Neural Populations.\ Not actually abstract. """ __slots__ = [ "__all_single_syn_sz", "__change_requires_mapping", "__change_requires_data_generation", "__incoming_spike_buffer_size", "__n_atoms", "__n_profile_samples", "__neuron_impl", "__neuron_recorder", "__synapse_recorder", "_parameters", # See AbstractPyNNModel "__pynn_model", "_state_variables", # See AbstractPyNNModel "__initial_state_variables", "__has_run", "__updated_state_variables", "__ring_buffer_sigma", "__spikes_per_second", "__drop_late_spikes", "__incoming_projections", "__synapse_dynamics", "__max_row_info", "__self_projection"] #: recording region IDs _SPIKE_RECORDING_REGION = 0 #: the size of the runtime SDP port data region _RUNTIME_SDP_PORT_SIZE = BYTES_PER_WORD #: The Buffer traffic type _TRAFFIC_IDENTIFIER = "BufferTraffic" _C_MAIN_BASE_N_CPU_CYCLES = 0 _NEURON_BASE_N_CPU_CYCLES_PER_NEURON = 22 _NEURON_BASE_N_CPU_CYCLES = 10 _SYNAPSE_BASE_N_CPU_CYCLES_PER_NEURON = 22 _SYNAPSE_BASE_N_CPU_CYCLES = 10 # 5 elements before the start of global parameters # 1. has key, 2. key, 3. n atoms, 4. n_atoms_peak 5. n_synapse_types BYTES_TILL_START_OF_GLOBAL_PARAMETERS = 5 * BYTES_PER_WORD def __init__( self, n_neurons, label, constraints, max_atoms_per_core, spikes_per_second, ring_buffer_sigma, incoming_spike_buffer_size, neuron_impl, pynn_model, drop_late_spikes, splitter): """ :param int n_neurons: The number of neurons in the population :param str label: The label on the population :param list(~pacman.model.constraints.AbstractConstraint) constraints: Constraints on where a population's vertices may be placed. :param int max_atoms_per_core: The maximum number of atoms (neurons) per SpiNNaker core. :param spikes_per_second: Expected spike rate :type spikes_per_second: float or None :param ring_buffer_sigma: How many SD above the mean to go for upper bound of ring buffer size; a good starting choice is 5.0. Given length of simulation we can set this for approximate number of saturation events. :type ring_buffer_sigma: float or None :param incoming_spike_buffer_size: :type incoming_spike_buffer_size: int or None :param bool drop_late_spikes: control flag for dropping late packets. :param AbstractNeuronImpl neuron_impl: The (Python side of the) implementation of the neurons themselves. :param AbstractPyNNNeuronModel pynn_model: The PyNN neuron model that this vertex is working on behalf of. :param splitter: splitter object :type splitter: None or ~pacman.model.partitioner_splitters.abstract_splitters.AbstractSplitterCommon """ # pylint: disable=too-many-arguments, too-many-locals super().__init__(label, constraints, max_atoms_per_core, splitter) self.__n_atoms = self.round_n_atoms(n_neurons, "n_neurons") # buffer data self.__incoming_spike_buffer_size = incoming_spike_buffer_size if incoming_spike_buffer_size is None: self.__incoming_spike_buffer_size = get_config_int( "Simulation", "incoming_spike_buffer_size") # Limit the DTCM used by one-to-one connections self.__all_single_syn_sz = get_config_int( "Simulation", "one_to_one_connection_dtcm_max_bytes") self.__ring_buffer_sigma = ring_buffer_sigma if self.__ring_buffer_sigma is None: self.__ring_buffer_sigma = get_config_float( "Simulation", "ring_buffer_sigma") self.__spikes_per_second = spikes_per_second if self.__spikes_per_second is None: self.__spikes_per_second = get_config_float( "Simulation", "spikes_per_second") self.__drop_late_spikes = drop_late_spikes if self.__drop_late_spikes is None: self.__drop_late_spikes = get_config_bool( "Simulation", "drop_late_spikes") self.__neuron_impl = neuron_impl self.__pynn_model = pynn_model self._parameters = SpynnakerRangeDictionary(n_neurons) self.__neuron_impl.add_parameters(self._parameters) self.__initial_state_variables = SpynnakerRangeDictionary(n_neurons) self.__neuron_impl.add_state_variables(self.__initial_state_variables) self._state_variables = self.__initial_state_variables.copy() # Set up for recording neuron_recordable_variables = list( self.__neuron_impl.get_recordable_variables()) record_data_types = dict( self.__neuron_impl.get_recordable_data_types()) self.__neuron_recorder = NeuronRecorder( neuron_recordable_variables, record_data_types, [NeuronRecorder.SPIKES], n_neurons, [], {}, [], {}) self.__synapse_recorder = NeuronRecorder( [], {}, [], n_neurons, [NeuronRecorder.PACKETS], {NeuronRecorder.PACKETS: NeuronRecorder.PACKETS_TYPE}, [NeuronRecorder.REWIRING], {NeuronRecorder.REWIRING: NeuronRecorder.REWIRING_TYPE}) # bool for if state has changed. self.__change_requires_mapping = True self.__change_requires_data_generation = False self.__has_run = False # Set up for profiling self.__n_profile_samples = get_config_int( "Reports", "n_profile_samples") # Set up for incoming self.__incoming_projections = list() self.__max_row_info = dict() self.__self_projection = None # Prepare for dealing with STDP - there can only be one (non-static) # synapse dynamics per vertex at present self.__synapse_dynamics = None @property def synapse_dynamics(self): """ The synapse dynamics used by the synapses e.g. plastic or static. Settable. :rtype: AbstractSynapseDynamics or None """ return self.__synapse_dynamics @synapse_dynamics.setter def synapse_dynamics(self, synapse_dynamics): """ Set the synapse dynamics. Note that after setting, the dynamics might not be the type set as it can be combined with the existing dynamics in exciting ways. :param AbstractSynapseDynamics synapse_dynamics: The synapse dynamics to set """ if self.__synapse_dynamics is None: self.__synapse_dynamics = synapse_dynamics else: self.__synapse_dynamics = self.__synapse_dynamics.merge( synapse_dynamics)
[docs] def add_incoming_projection(self, projection): """ Add a projection incoming to this vertex :param PyNNProjectionCommon projection: The new projection to add """ # Reset the ring buffer shifts as a projection has been added self.__change_requires_mapping = True self.__incoming_projections.append(projection) if projection._projection_edge.pre_vertex == self: self.__self_projection = projection
@property def self_projection(self): """ Get any projection from this vertex to itself :rtype: PyNNProjectionCommon or None """ return self.__self_projection @property @overrides(TDMAAwareApplicationVertex.n_atoms) def n_atoms(self): return self.__n_atoms
[docs] @overrides(TDMAAwareApplicationVertex.get_n_cores) def get_n_cores(self): return len(self._splitter.get_out_going_slices()[0])
@property def size(self): """ The number of neurons in the vertex :rtype: int """ return self.__n_atoms @property def all_single_syn_size(self): """ The maximum amount of DTCM to use for single synapses :rtype: int """ return self.__all_single_syn_sz @property def incoming_spike_buffer_size(self): """ The size of the incoming spike buffer to be used on the cores :rtype: int """ return self.__incoming_spike_buffer_size @property def parameters(self): """ The parameters of the neurons in the population :rtype: SpyNNakerRangeDictionary """ return self._parameters @property def state_variables(self): """ The state variables of the neuron in the population :rtype: SpyNNakerRangeDicationary """ return self._state_variables @property def neuron_impl(self): """ The neuron implementation :rtype: AbstractNeuronImpl """ return self.__neuron_impl @property def n_profile_samples(self): """ The maximum number of profile samples to report :rtype: int """ return self.__n_profile_samples @property def neuron_recorder(self): """ The recorder for neurons :rtype: NeuronRecorder """ return self.__neuron_recorder @property def synapse_recorder(self): """ The recorder for synapses :rtype: SynapseRecorder """ return self.__synapse_recorder @property def drop_late_spikes(self): """ Whether spikes should be dropped if not processed in a timestep :rtype: bool """ return self.__drop_late_spikes
[docs] def set_has_run(self): """ Set the flag has run so initialize only affects state variables :rtype: None """ self.__has_run = True
@property @overrides(AbstractChangableAfterRun.requires_mapping) def requires_mapping(self): return self.__change_requires_mapping @property @overrides(AbstractChangableAfterRun.requires_data_generation) def requires_data_generation(self): return self.__change_requires_data_generation
[docs] @overrides(AbstractChangableAfterRun.mark_no_changes) def mark_no_changes(self): self.__change_requires_mapping = False self.__change_requires_data_generation = False
[docs] def get_sdram_usage_for_neuron_params(self, vertex_slice): """ Calculate the SDRAM usage for just the neuron parameters region. :param ~pacman.model.graphs.common.Slice vertex_slice: the slice of atoms. :return: The SDRAM required for the neuron region """ return ( self.BYTES_TILL_START_OF_GLOBAL_PARAMETERS + (self.__neuron_impl.get_n_synapse_types() * BYTES_PER_WORD) + self.tdma_sdram_size_in_bytes + self.__neuron_impl.get_sdram_usage_in_bytes(vertex_slice.n_atoms))
[docs] @overrides(AbstractSpikeRecordable.is_recording_spikes) def is_recording_spikes(self): return self.__neuron_recorder.is_recording(NeuronRecorder.SPIKES)
[docs] @overrides(AbstractSpikeRecordable.set_recording_spikes) def set_recording_spikes( self, new_state=True, sampling_interval=None, indexes=None): self.set_recording( NeuronRecorder.SPIKES, new_state, sampling_interval, indexes)
[docs] @overrides(AbstractEventRecordable.is_recording_events) def is_recording_events(self, variable): return self.__synapse_recorder.is_recording(variable)
[docs] @overrides(AbstractEventRecordable.set_recording_events) def set_recording_events( self, variable, new_state=True, sampling_interval=None, indexes=None): self.set_recording( variable, new_state, sampling_interval, indexes)
[docs] @overrides(AbstractSpikeRecordable.get_spikes) def get_spikes(self, placements, buffer_manager): return self.__neuron_recorder.get_spikes( self.label, buffer_manager, placements, self, NeuronRecorder.SPIKES)
[docs] @overrides(AbstractEventRecordable.get_events) def get_events( self, variable, placements, buffer_manager): return self.__synapse_recorder.get_events( self.label, buffer_manager, placements, self, variable)
[docs] @overrides(AbstractNeuronRecordable.get_recordable_variables) def get_recordable_variables(self): variables = list() variables.extend(self.__neuron_recorder.get_recordable_variables()) variables.extend(self.__synapse_recorder.get_recordable_variables()) return variables
def __raise_var_not_supported(self, variable): """ Helper to indicate that recording a variable is not supported :param str variable: The variable to report as unsupported """ msg = ("Variable {} is not supported. Supported variables are" "{}".format(variable, self.get_recordable_variables())) raise ConfigurationException(msg)
[docs] @overrides(AbstractNeuronRecordable.is_recording) def is_recording(self, variable): if self.__neuron_recorder.is_recordable(variable): return self.__neuron_recorder.is_recording(variable) if self.__synapse_recorder.is_recordable(variable): return self.__synapse_recorder.is_recording(variable) self.__raise_var_not_supported(variable)
[docs] @overrides(AbstractNeuronRecordable.set_recording) def set_recording(self, variable, new_state=True, sampling_interval=None, indexes=None): if self.__neuron_recorder.is_recordable(variable): self.__neuron_recorder.set_recording( variable, new_state, sampling_interval, indexes) elif self.__synapse_recorder.is_recordable(variable): self.__synapse_recorder.set_recording( variable, new_state, sampling_interval, indexes) else: self.__raise_var_not_supported(variable) self.__change_requires_mapping = not self.is_recording(variable)
[docs] @overrides(AbstractNeuronRecordable.get_data) def get_data( self, variable, n_machine_time_steps, placements, buffer_manager): # pylint: disable=too-many-arguments if self.__neuron_recorder.is_recordable(variable): return self.__neuron_recorder.get_matrix_data( self.label, buffer_manager, placements, self, variable, n_machine_time_steps) elif self.__synapse_recorder.is_recordable(variable): return self.__synapse_recorder.get_matrix_data( self.label, buffer_manager, placements, self, variable, n_machine_time_steps) self.__raise_var_not_supported(variable)
[docs] @overrides(AbstractNeuronRecordable.get_neuron_sampling_interval) def get_neuron_sampling_interval(self, variable): if self.__neuron_recorder.is_recordable(variable): return self.__neuron_recorder.get_neuron_sampling_interval( variable) elif self.__synapse_recorder.is_recordable(variable): return self.__synapse_recorder.get_neuron_sampling_interval( variable) self.__raise_var_not_supported(variable)
[docs] @overrides(AbstractSpikeRecordable.get_spikes_sampling_interval) def get_spikes_sampling_interval(self): return self.__neuron_recorder.get_neuron_sampling_interval("spikes")
[docs] @overrides(AbstractEventRecordable.get_events_sampling_interval) def get_events_sampling_interval(self, variable): return self.__neuron_recorder.get_neuron_sampling_interval(variable)
[docs] @overrides(AbstractPopulationInitializable.initialize) def initialize(self, variable, value, selector=None): if variable not in self._state_variables: raise KeyError( "Vertex does not support initialisation of" " parameter {}".format(variable)) if self.__has_run: self._state_variables[variable].set_value_by_selector( selector, value) logger.warning( "initializing {} after run and before reset only changes the " "current state and will be lost after reset".format(variable)) else: # set the inital values self.__initial_state_variables[variable].set_value_by_selector( selector, value) # Update the sate variables in case asked for self._state_variables.copy_into(self.__initial_state_variables) for vertex in self.machine_vertices: if isinstance(vertex, AbstractRewritesDataSpecification): vertex.set_reload_required(True)
@property def initialize_parameters(self): """ The names of parameters that have default initial values. :rtype: iterable(str) """ return self.__pynn_model.default_initial_values.keys() def _get_parameter(self, variable): """ Get a neuron parameter value :param str variable: The variable to get the value of """ if variable.endswith("_init"): # method called with "V_init" key = variable[:-5] if variable in self._state_variables: # variable is v and parameter is v_init return variable elif key in self._state_variables: # Oops neuron defines v and not v_init return key else: # method called with "v" if variable + "_init" in self._state_variables: # variable is v and parameter is v_init return variable + "_init" if variable in self._state_variables: # Oops neuron defines v and not v_init return variable # parameter not found for this variable raise KeyError("No variable {} found in {}".format( variable, self.__neuron_impl.model_name))
[docs] @overrides(AbstractPopulationInitializable.get_initial_value) def get_initial_value(self, variable, selector=None): parameter = self._get_parameter(variable) ranged_list = self._state_variables[parameter] if selector is None: return ranged_list return ranged_list.get_values(selector)
@property def conductance_based(self): """ :rtype: bool """ return self.__neuron_impl.is_conductance_based
[docs] @overrides(AbstractPopulationSettable.get_value) def get_value(self, key): """ Get a property of the overall model. """ if key not in self._parameters: raise InvalidParameterType( "Population {} does not have parameter {}".format( self.__neuron_impl.model_name, key)) return self._parameters[key]
[docs] @overrides(AbstractPopulationSettable.set_value) def set_value(self, key, value): """ Set a property of the overall model. """ if key not in self._parameters: raise InvalidParameterType( "Population {} does not have parameter {}".format( self.__neuron_impl.model_name, key)) self._parameters.set_value(key, value) for vertex in self.machine_vertices: if isinstance(vertex, AbstractRewritesDataSpecification): vertex.set_reload_required(True)
@property def weight_scale(self): """ :rtype: float """ return self.__neuron_impl.get_global_weight_scale() @property def ring_buffer_sigma(self): return self.__ring_buffer_sigma @ring_buffer_sigma.setter def ring_buffer_sigma(self, ring_buffer_sigma): self.__ring_buffer_sigma = ring_buffer_sigma @property def spikes_per_second(self): return self.__spikes_per_second @spikes_per_second.setter def spikes_per_second(self, spikes_per_second): self.__spikes_per_second = spikes_per_second
[docs] def set_synapse_dynamics(self, synapse_dynamics): """ Set the synapse dynamics of this population :param AbstractSynapseDynamics synapse_dynamics: The synapse dynamics to set """ self.synapse_dynamics = synapse_dynamics
[docs] def clear_connection_cache(self): """ Flush the cache of connection information; needed for a second run """ for post_vertex in self.machine_vertices: if isinstance(post_vertex, HasSynapses): post_vertex.clear_connection_cache()
[docs] @overrides(AbstractProvidesOutgoingPartitionConstraints. get_outgoing_partition_constraints) def get_outgoing_partition_constraints(self, partition): """ Gets the constraints for partitions going out of this vertex. :param partition: the partition that leaves this vertex :return: list of constraints """ return [ContiguousKeyRangeContraint()]
[docs] @overrides(AbstractNeuronRecordable.clear_recording) def clear_recording(self, variable, buffer_manager, placements): if variable == NeuronRecorder.SPIKES: index = len(self.__neuron_impl.get_recordable_variables()) elif variable == NeuronRecorder.REWIRING: index = len(self.__neuron_impl.get_recordable_variables()) + 1 else: index = ( self.__neuron_impl.get_recordable_variable_index(variable)) self._clear_recording_region(buffer_manager, placements, index)
[docs] @overrides(AbstractSpikeRecordable.clear_spike_recording) def clear_spike_recording(self, buffer_manager, placements): self._clear_recording_region( buffer_manager, placements, len(self.__neuron_impl.get_recordable_variables()))
[docs] @overrides(AbstractEventRecordable.clear_event_recording) def clear_event_recording(self, buffer_manager, placements): self._clear_recording_region( buffer_manager, placements, len(self.__neuron_impl.get_recordable_variables()) + 1)
def _clear_recording_region( self, buffer_manager, placements, recording_region_id): """ Clear a recorded data region from the buffer manager. :param buffer_manager: the buffer manager object :param placements: the placements object :param recording_region_id: the recorded region ID for clearing :rtype: None """ for machine_vertex in self.machine_vertices: placement = placements.get_placement_of_vertex(machine_vertex) buffer_manager.clear_recorded_data( placement.x, placement.y, placement.p, recording_region_id)
[docs] @overrides(AbstractContainsUnits.get_units) def get_units(self, variable): if variable == NeuronRecorder.SPIKES: return NeuronRecorder.SPIKES if variable == NeuronRecorder.PACKETS: return "count" if self.__neuron_impl.is_recordable(variable): return self.__neuron_impl.get_recordable_units(variable) if variable not in self._parameters: raise Exception("Population {} does not have parameter {}".format( self.__neuron_impl.model_name, variable)) return self.__neuron_impl.get_units(variable)
[docs] def describe(self): """ Get a human-readable description of the cell or synapse type. The output may be customised by specifying a different template together with an associated template engine (see :py:mod:`pyNN.descriptions`). If template is None, then a dictionary containing the template context will be returned. :rtype: dict(str, ...) """ parameters = dict() for parameter_name in self.__pynn_model.default_parameters: parameters[parameter_name] = self.get_value(parameter_name) context = { "name": self.__neuron_impl.model_name, "default_parameters": self.__pynn_model.default_parameters, "default_initial_values": self.__pynn_model.default_parameters, "parameters": parameters, } return context
[docs] def get_synapse_id_by_target(self, target): """ Get the id of synapse using its target name :param str target: The synapse to get the id of """ return self.__neuron_impl.get_synapse_id_by_target(target)
def __str__(self): return "{} with {} atoms".format(self.label, self.n_atoms) def __repr__(self): return self.__str__()
[docs] @overrides(AbstractCanReset.reset_to_first_timestep) def reset_to_first_timestep(self): # Mark that reset has been done, and reload state variables self.__has_run = False self._state_variables.copy_into(self.__initial_state_variables) for vertex in self.machine_vertices: if isinstance(vertex, AbstractRewritesDataSpecification): vertex.set_reload_required(True) # If synapses change during the run, if (self.__synapse_dynamics is not None and self.__synapse_dynamics.changes_during_run): self.__change_requires_data_generation = True for vertex in self.machine_vertices: if isinstance(vertex, AbstractRewritesDataSpecification): vertex.set_reload_required(True)
@staticmethod def _ring_buffer_expected_upper_bound( weight_mean, weight_std_dev, spikes_per_second, n_synapses_in, sigma): """ Provides expected upper bound on accumulated values in a ring\ buffer element. Requires an assessment of maximum Poisson input rate. Assumes knowledge of mean and SD of weight distribution, fan-in\ and timestep. All arguments should be assumed real values except n_synapses_in\ which will be an integer. :param float weight_mean: Mean of weight distribution (in either nA or\ microSiemens as required) :param float weight_std_dev: SD of weight distribution :param float spikes_per_second: Maximum expected Poisson rate in Hz :param int machine_timestep: in us :param int n_synapses_in: No of connected synapses :param float sigma: How many SD above the mean to go for upper bound;\ a good starting choice is 5.0. Given length of simulation we can\ set this for approximate number of saturation events. :rtype: float """ # E[ number of spikes ] in a timestep steps_per_second = MICRO_TO_SECOND_CONVERSION / machine_time_step() average_spikes_per_timestep = ( float(n_synapses_in * spikes_per_second) / steps_per_second) # Exact variance contribution from inherent Poisson variation poisson_variance = average_spikes_per_timestep * (weight_mean ** 2) # Upper end of range for Poisson summation required below # upper_bound needs to be an integer upper_bound = int(round(average_spikes_per_timestep + POSSION_SIGMA_SUMMATION_LIMIT * math.sqrt(average_spikes_per_timestep))) # Closed-form exact solution for summation that gives the variance # contributed by weight distribution variation when modulated by # Poisson PDF. Requires scipy.special for gamma and incomplete gamma # functions. Beware: incomplete gamma doesn't work the same as # Mathematica because (1) it's regularised and needs a further # multiplication and (2) it's actually the complement that is needed # i.e. 'gammaincc'] weight_variance = 0.0 if weight_std_dev > 0: # pylint: disable=no-member lngamma = special.gammaln(1 + upper_bound) gammai = special.gammaincc( 1 + upper_bound, average_spikes_per_timestep) big_ratio = (math.log(average_spikes_per_timestep) * upper_bound - lngamma) if -701.0 < big_ratio < 701.0 and big_ratio != 0.0: log_weight_variance = ( -average_spikes_per_timestep + math.log(average_spikes_per_timestep) + 2.0 * math.log(weight_std_dev) + math.log(math.exp(average_spikes_per_timestep) * gammai - math.exp(big_ratio))) weight_variance = math.exp(log_weight_variance) # upper bound calculation -> mean + n * SD return ((average_spikes_per_timestep * weight_mean) + (sigma * math.sqrt(poisson_variance + weight_variance)))
[docs] def get_ring_buffer_shifts(self, incoming_projections): """ Get the shift of the ring buffers for transfer of values into the input buffers for this model. :param list(~spynnaker.pyNN.models.Projection) incoming_projections: The projections to consider in the calculations :rtype: list(int) """ weight_scale = self.__neuron_impl.get_global_weight_scale() weight_scale_squared = weight_scale * weight_scale # This only gets ring buffer shifts for neuron synapses n_synapse_types = self.__neuron_impl.get_n_synapse_types() running_totals = [RunningStats() for _ in range(n_synapse_types)] delay_running_totals = [RunningStats() for _ in range(n_synapse_types)] total_weights = numpy.zeros(n_synapse_types) biggest_weight = numpy.zeros(n_synapse_types) weights_signed = False rate_stats = [RunningStats() for _ in range(n_synapse_types)] steps_per_second = MICRO_TO_SECOND_CONVERSION / machine_time_step() for proj in incoming_projections: synapse_info = proj._synapse_information # Skip if this is a synapse dynamics synapse type if synapse_info.synapse_type_from_dynamics: continue synapse_type = synapse_info.synapse_type synapse_dynamics = synapse_info.synapse_dynamics connector = synapse_info.connector weight_mean = ( synapse_dynamics.get_weight_mean( connector, synapse_info) * weight_scale) n_connections = \ connector.get_n_connections_to_post_vertex_maximum( synapse_info) weight_variance = synapse_dynamics.get_weight_variance( connector, synapse_info.weights, synapse_info) * weight_scale_squared running_totals[synapse_type].add_items( weight_mean, weight_variance, n_connections) delay_variance = synapse_dynamics.get_delay_variance( connector, synapse_info.delays, synapse_info) delay_running_totals[synapse_type].add_items( 0.0, delay_variance, n_connections) weight_max = (synapse_dynamics.get_weight_maximum( connector, synapse_info) * weight_scale) biggest_weight[synapse_type] = max( biggest_weight[synapse_type], weight_max) spikes_per_tick = max( 1.0, self.__spikes_per_second / steps_per_second) spikes_per_second = self.__spikes_per_second pre_vertex = proj._projection_edge.pre_vertex if isinstance(pre_vertex, AbstractMaxSpikes): rate = pre_vertex.max_spikes_per_second() if rate != 0: spikes_per_second = rate spikes_per_tick = pre_vertex.max_spikes_per_ts() rate_stats[synapse_type].add_items( spikes_per_second, 0, n_connections) total_weights[synapse_type] += spikes_per_tick * ( weight_max * n_connections) if synapse_dynamics.are_weights_signed(): weights_signed = True max_weights = numpy.zeros(n_synapse_types) for synapse_type in range(n_synapse_types): if delay_running_totals[synapse_type].variance == 0.0: max_weights[synapse_type] = max(total_weights[synapse_type], biggest_weight[synapse_type]) else: stats = running_totals[synapse_type] rates = rate_stats[synapse_type] max_weights[synapse_type] = min( self._ring_buffer_expected_upper_bound( stats.mean, stats.standard_deviation, rates.mean, stats.n_items, self.__ring_buffer_sigma), total_weights[synapse_type]) max_weights[synapse_type] = max( max_weights[synapse_type], biggest_weight[synapse_type]) # Convert these to powers; we could use int.bit_length() for this if # they were integers, but they aren't... max_weight_powers = ( 0 if w <= 0 else int(math.ceil(max(0, math.log(w, 2)))) for w in max_weights) # If 2^max_weight_power equals the max weight, we have to add another # power, as range is 0 - (just under 2^max_weight_power)! max_weight_powers = ( w + 1 if (2 ** w) <= a else w for w, a in zip(max_weight_powers, max_weights)) # If we have synapse dynamics that uses signed weights, # Add another bit of shift to prevent overflows if weights_signed: max_weight_powers = (m + 1 for m in max_weight_powers) return list(max_weight_powers)
@staticmethod def __get_weight_scale(ring_buffer_to_input_left_shift): """ Return the amount to scale the weights by to convert them from \ floating point values to 16-bit fixed point numbers which can be \ shifted left by ring_buffer_to_input_left_shift to produce an\ s1615 fixed point number :param int ring_buffer_to_input_left_shift: :rtype: float """ return float(math.pow(2, 16 - (ring_buffer_to_input_left_shift + 1)))
[docs] def get_weight_scales(self, ring_buffer_shifts): """ Get the weight scaling to apply to weights in synapses :param list(int) ring_buffer_shifts: The shifts to convert to weight scales :rtype: list(int) """ weight_scale = self.__neuron_impl.get_global_weight_scale() return numpy.array([ self.__get_weight_scale(r) * weight_scale for r in ring_buffer_shifts])
[docs] @overrides(AbstractAcceptsIncomingSynapses.get_connections_from_machine) def get_connections_from_machine( self, transceiver, placements, app_edge, synapse_info): # Start with something in the list so that concatenate works connections = [numpy.zeros( 0, dtype=AbstractSynapseDynamics.NUMPY_CONNECTORS_DTYPE)] progress = ProgressBar( len(self.machine_vertices), "Getting synaptic data between {} and {}".format( app_edge.pre_vertex.label, app_edge.post_vertex.label)) for post_vertex in progress.over(self.machine_vertices): if isinstance(post_vertex, HasSynapses): placement = placements.get_placement_of_vertex(post_vertex) connections.extend(post_vertex.get_connections_from_machine( transceiver, placement, app_edge, synapse_info)) return numpy.concatenate(connections)
[docs] def get_synapse_params_size(self): """ Get the size of the synapse parameters in bytes :rtype: int """ # This will only hold ring buffer scaling for the neuron synapse # types return (_SYNAPSES_BASE_SDRAM_USAGE_IN_BYTES + (BYTES_PER_WORD * self.__neuron_impl.get_n_synapse_types()))
[docs] def get_synapse_dynamics_size(self, vertex_slice): """ Get the size of the synapse dynamics region :param ~pacman.model.graphs.common.Slice vertex_slice: The slice of the vertex to get the usage of :rtype: int """ if self.__synapse_dynamics is None: return 0 return self.__synapse_dynamics.get_parameters_sdram_usage_in_bytes( vertex_slice.n_atoms, self.__neuron_impl.get_n_synapse_types())
[docs] def get_structural_dynamics_size(self, vertex_slice, incoming_projections): """ Get the size of the structural dynamics region :param ~pacman.model.graphs.common.Slice vertex_slice: The slice of the vertex to get the usage of :param list(~spynnaker.pyNN.models.Projection) incoming_projections: The projections to consider in the calculations """ if self.__synapse_dynamics is None: return 0 if not isinstance( self.__synapse_dynamics, AbstractSynapseDynamicsStructural): return 0 return self.__synapse_dynamics\ .get_structural_parameters_sdram_usage_in_bytes( incoming_projections, vertex_slice.n_atoms)
[docs] def get_synapses_size(self, vertex_slice, incoming_projections): """ Get the maximum SDRAM usage for the synapses on a vertex slice :param ~pacman.model.graphs.common.Slice vertex_slice: The slice of the vertex to get the usage of :param list(~spynnaker.pyNN.models.Projection) incoming_projections: The projections to consider in the calculations """ addr = 2 * BYTES_PER_WORD for proj in incoming_projections: addr = self.__add_matrix_size(addr, proj, vertex_slice) return addr
def __add_matrix_size(self, addr, projection, vertex_slice): """ Add to the address the size of the matrices for the projection to the vertex slice :param int addr: The address to start from :param ~spynnaker.pyNN.models.Projection: The projection to add :param ~pacman.model.graphs.common.Slice vertex_slice: The slice projected to :rtype: int """ synapse_info = projection._synapse_information app_edge = projection._projection_edge max_row_info = self.get_max_row_info( synapse_info, vertex_slice, app_edge) vertex = app_edge.pre_vertex n_sub_atoms = int(min(vertex.get_max_atoms_per_core(), vertex.n_atoms)) n_sub_edges = int(math.ceil(vertex.n_atoms / n_sub_atoms)) if max_row_info.undelayed_max_n_synapses > 0: size = n_sub_atoms * max_row_info.undelayed_max_bytes for _ in range(n_sub_edges): addr = MasterPopTableAsBinarySearch.get_next_allowed_address( addr) addr += size if max_row_info.delayed_max_n_synapses > 0: size = (n_sub_atoms * max_row_info.delayed_max_bytes * app_edge.n_delay_stages) for _ in range(n_sub_edges): addr = MasterPopTableAsBinarySearch.get_next_allowed_address( addr) addr += size return addr
[docs] def get_max_row_info(self, synapse_info, vertex_slice, app_edge): """ Get maximum row length data :param SynapseInformation synapse_info: Information about synapses :param ~pacman.model.graphs.common.Slice vertex_slice: The slice projected to :param ProjectionApplicationEdge app_edge: The edge of the projection """ key = (app_edge, synapse_info, vertex_slice) if key in self.__max_row_info: return self.__max_row_info[key] max_row_info = get_max_row_info( synapse_info, vertex_slice, app_edge.n_delay_stages, app_edge) self.__max_row_info[key] = max_row_info return max_row_info
[docs] def get_synapse_expander_size(self, incoming_projections): """ Get the size of the synapse expander region in bytes :param list(~spynnaker.pyNN.models.Projection) incoming_projections: The projections to consider in the calculations :rtype: int """ size = 0 for proj in incoming_projections: synapse_info = proj._synapse_information app_edge = proj._projection_edge n_sub_edges = len( app_edge.pre_vertex.splitter.get_out_going_slices()[0]) if not n_sub_edges: vertex = app_edge.pre_vertex max_atoms = float(min(vertex.get_max_atoms_per_core(), vertex.n_atoms)) n_sub_edges = int(math.ceil(vertex.n_atoms / max_atoms)) size += self.__generator_info_size(synapse_info) * n_sub_edges # If anything generates data, also add some base information if size: size += SYNAPSES_BASE_GENERATOR_SDRAM_USAGE_IN_BYTES size += (self.__neuron_impl.get_n_synapse_types() * DataType.U3232.size) return size
@staticmethod def __generator_info_size(synapse_info): """ The number of bytes required by the generator information :param SynapseInformation synapse_info: The synapse information to use :rtype: int """ if not synapse_info.may_generate_on_machine(): return 0 connector = synapse_info.connector dynamics = synapse_info.synapse_dynamics gen_size = sum(( GeneratorData.BASE_SIZE, connector.gen_delay_params_size_in_bytes(synapse_info.delays), connector.gen_weight_params_size_in_bytes(synapse_info.weights), connector.gen_connector_params_size_in_bytes, dynamics.gen_matrix_params_size_in_bytes )) return gen_size @property def synapse_executable_suffix(self): """ The suffix of the executable name due to the type of synapses \ in use. :rtype: str """ if self.__synapse_dynamics is None: return "" return self.__synapse_dynamics.get_vertex_executable_suffix() @property def neuron_recordables(self): """ Get the names of variables that can be recorded by the neuron :rtype: list(str) """ return self.__neuron_recorder.get_recordable_variables() @property def synapse_recordables(self): """ Get the names of variables that can be recorded by the synapses :rtype: list(str) """ return self.__synapse_recorder.get_recordable_variables()
[docs] def get_common_constant_sdram( self, n_record, n_provenance, common_regions): """ Get the amount of SDRAM used by common parts :param int n_record: The number of recording regions :param int n_provenance: The number of provenance items :param CommonRegions common_regions: Region IDs :rtype: int """ sdram = MultiRegionSDRAM() sdram.add_cost(common_regions.system, SYSTEM_BYTES_REQUIREMENT) sdram.add_cost( common_regions.recording, get_recording_header_size(n_record) + get_recording_data_constant_size(n_record)) sdram.add_cost( common_regions.provenance, ProvidesProvenanceDataFromMachineImpl.get_provenance_data_size( n_provenance)) sdram.add_cost( common_regions.profile, get_profile_region_size(self.__n_profile_samples)) return sdram
[docs] def get_neuron_variable_sdram(self, vertex_slice): """ Get the amount of SDRAM per timestep used by neuron parts :param ~pacman.model.graphs.common.Slice vertex_slice: The slice of neurons to get the size of :rtype: int """ return self.__neuron_recorder.get_variable_sdram_usage(vertex_slice)
[docs] def get_synapse_variable_sdram(self, vertex_slice): """ Get the amount of SDRAM per timestep used by synapse parts :param ~pacman.model.graphs.common.Slice vertex_slice: The slice of neurons to get the size of :rtype: int """ if isinstance(self.__synapse_dynamics, AbstractSynapseDynamicsStructural): self.__synapse_recorder.set_max_rewires_per_ts( self.__synapse_dynamics.get_max_rewires_per_ts()) return self.__synapse_recorder.get_variable_sdram_usage(vertex_slice)
[docs] def get_neuron_constant_sdram(self, vertex_slice, neuron_regions): """ Get the amount of fixed SDRAM used by neuron parts :param ~pacman.model.graphs.common.Slice vertex_slice: The slice of neurons to get the size of :param NeuronRegions neuron_regions: Region IDs :rtype: int """ sdram = MultiRegionSDRAM() sdram.add_cost( neuron_regions.neuron_params, self.get_sdram_usage_for_neuron_params(vertex_slice)) sdram.add_cost( neuron_regions.neuron_recording, self.__neuron_recorder.get_metadata_sdram_usage_in_bytes( vertex_slice)) return sdram
[docs] def get_common_dtcm(self): """ Get the amount of DTCM used by common parts :rtype: int """ # TODO: Get some real numbers here return 0
[docs] def get_neuron_dtcm(self, vertex_slice): """ Get the amount of DTCM used by neuron parts :param ~pacman.model.graphs.common.Slice vertex_slice: The slice of neurons to get the size of :rtype: int """ return ( self.__neuron_impl.get_dtcm_usage_in_bytes(vertex_slice.n_atoms) + self.__neuron_recorder.get_dtcm_usage_in_bytes(vertex_slice) )
[docs] def get_synapse_dtcm(self, vertex_slice): """ Get the amount of DTCM used by synapse parts :param ~pacman.model.graphs.common.Slice vertex_slice: The slice of neurons to get the size of :rtype: int """ return self.__synapse_recorder.get_dtcm_usage_in_bytes(vertex_slice)
[docs] def get_common_cpu(self): """ Get the amount of CPU used by common parts :rtype: int """ return self._C_MAIN_BASE_N_CPU_CYCLES
[docs] def get_neuron_cpu(self, vertex_slice): """ Get the amount of CPU used by neuron parts :param ~pacman.model.graphs.common.Slice vertex_slice: The slice of neurons to get the size of :rtype: int """ return ( self._NEURON_BASE_N_CPU_CYCLES + (self._NEURON_BASE_N_CPU_CYCLES_PER_NEURON * vertex_slice.n_atoms) + self.__neuron_recorder.get_n_cpu_cycles(vertex_slice.n_atoms) + self.__neuron_impl.get_n_cpu_cycles(vertex_slice.n_atoms))
[docs] def get_synapse_cpu(self, vertex_slice): """ Get the amount of CPU used by synapse parts :param ~pacman.model.graphs.common.Slice vertex_slice: The slice of neurons to get the size of :rtype: int """ return ( self._SYNAPSE_BASE_N_CPU_CYCLES + (self._SYNAPSE_BASE_N_CPU_CYCLES_PER_NEURON * vertex_slice.n_atoms) + self.__synapse_recorder.get_n_cpu_cycles(vertex_slice.n_atoms))
@property def incoming_projections(self): """ The projections that target this population vertex :rtype: list(~spynnaker.pyNN.models.projection.Projection) """ return self.__incoming_projections