Пример #1
0
class Projection(object):
    """
    Container for all the synapses of the same type between two populations.
    """

    def __init__(self, pre, post, target, synapse=None, name=None, copied=False):
        """
        *Parameters*:

        * **pre**: pre-synaptic population (either its name or a ``Population`` object).
        * **post**: post-synaptic population (either its name or a ``Population`` object).
        * **target**: type of the connection.
        * **synapse**: a ``Synapse`` instance.
        * **name**: unique name of the projection (optional, it defaults to ``proj0``, ``proj1``, etc).

        By default, the synapse only ensures linear synaptic transmission:

        * For rate-coded populations: ``psp = w * pre.r``
        * For spiking populations: ``g_target += w``

        """
        # Check if the network has already been compiled
        if Global._network[0]['compiled'] and not copied:
            Global._error('you cannot add a projection after the network has been compiled.')

        # Store the pre and post synaptic populations
        # the user provide either a string or a population object
        # in case of string, we need to search for the corresponding object
        if isinstance(pre, str):
            for pop in Global._network[0]['populations']:
                if pop.name == pre:
                    self.pre = pop
        else:
            self.pre = pre

        if isinstance(post, str):
            for pop in Global._network[0]['populations']:
                if pop.name == post:
                    self.post = pop
        else:
            self.post = post

        # Store the arguments
        if isinstance(target, list) and len(target) == 1:
            self.target = target[0]
        else:
            self.target = target

        # Add the target to the postsynaptic population
        self.post.targets.append(self.target)

        # check if a synapse description is attached
        if not synapse:
            # No synapse attached assume default synapse based on
            # presynaptic population.
            if self.pre.neuron_type.type == 'rate':
                from ANNarchy.models.Synapses import DefaultRateCodedSynapse
                self.synapse_type = DefaultRateCodedSynapse()
                self.synapse_type.type = 'rate'
            else:
                from ANNarchy.models.Synapses import DefaultSpikingSynapse
                self.synapse_type = DefaultSpikingSynapse()
                self.synapse_type.type = 'spike'

        elif inspect.isclass(synapse):
            self.synapse_type = synapse()
            self.synapse_type.type = self.pre.neuron_type.type
        else:
            self.synapse_type = copy.deepcopy(synapse)
            self.synapse_type.type = self.pre.neuron_type.type

        # Analyse the parameters and variables
        self.synapse_type._analyse()

        # Create a default name
        self.id = len(Global._network[0]['projections'])
        if name:
            self.name = name
        else:
            self.name = 'proj'+str(self.id)

        # Get a list of parameters and variables
        self.parameters = []
        self.init = {}
        for param in self.synapse_type.description['parameters']:
            self.parameters.append(param['name'])
            self.init[param['name']] = param['init']

        self.variables = []
        for var in self.synapse_type.description['variables']:
            self.variables.append(var['name'])
            self.init[var['name']] = var['init']

        self.attributes = self.parameters + self.variables

        # Get a list of user-defined functions
        self.functions = [func['name'] for func in self.synapse_type.description['functions']]

        # Add the population to the global network
        Global._network[0]['projections'].append(self)

        # Finalize initialization
        self.initialized = False

        # Cython instance
        self.cyInstance = None

        # Connectivity
        self._synapses = None
        self._connection_method = None
        self._connection_args = None
        self._connection_delay = None
        self._connector = None

        # List of post ranks is full by default, will be changed when the weights are created
        self.post_ranks = list(range(self.post.size))

        # Default configuration for connectivity
        self._storage_format = "lil"
        self._storage_order = "post_to_pre"

        # If a single weight value is used
        self._single_constant_weight = False

        # If a dense matrix should be used instead of LIL
        self._dense_matrix = False

        # Reporting
        self.connector_name = "Specific"
        self.connector_description = "Specific"

        # Overwritten by derived classes, to add
        # additional code
        self._specific_template = {}

        # Set to false  by derived classes to prevent saving of
        #  data, e. g. in case of weight-sharing projections
        self._saveable = True

        # To allow case-specific adjustment of parallelization
        # parameters, e. g. openMP schedule, we introduce a
        # dictionary read by the ProjectionGenerator.
        #
        # Will be overwritten either by inherited classes or
        # by an omp_config provided to the compile() method.
        self._omp_config = {
            #'psp_schedule': 'schedule(dynamic)'
        }

    # Add defined connectors
    connect_one_to_one = ConnectorMethods.connect_one_to_one
    connect_all_to_all = ConnectorMethods.connect_all_to_all
    connect_gaussian = ConnectorMethods.connect_gaussian
    connect_dog = ConnectorMethods.connect_dog
    connect_fixed_probability = ConnectorMethods.connect_fixed_probability
    connect_fixed_number_pre = ConnectorMethods.connect_fixed_number_pre
    connect_fixed_number_post = ConnectorMethods.connect_fixed_number_post
    connect_with_func = ConnectorMethods.connect_with_func
    connect_from_matrix = ConnectorMethods.connect_from_matrix
    _load_from_matrix = ConnectorMethods._load_from_matrix
    connect_from_sparse = ConnectorMethods.connect_from_sparse
    _load_from_sparse = ConnectorMethods._load_from_sparse
    connect_from_file = ConnectorMethods.connect_from_file
    _load_from_lil = ConnectorMethods._load_from_lil

    def _copy(self, pre, post):
        "Returns a copy of the projection when creating networks.  Internal use only."
        return Projection(pre=pre, post=post, target=self.target, synapse=self.synapse_type, name=self.name, copied=True)

    def _generate(self):
        "Overriden by specific projections to generate the code"
        pass

    def _instantiate(self, module):
        "Instantiates the projection after compilation."
        self._connect(module)
        self.initialized = True

    def _init_attributes(self):
        """
        Method used after compilation to initialize the attributes. Called by Generator._instantiate
        """
        for name, val in self.init.items():
            if not name in ['w']:
                self.__setattr__(name, val)

    def _connect(self, module):
        """
        Builds up dendrites either from list or dictionary. Called by instantiate().
        """
        if not self._connection_method:
            Global._error('The projection between ' + self.pre.name + ' and ' + self.post.name + ' is declared but not connected.')

        proj = getattr(module, 'proj'+str(self.id)+'_wrapper')
        self.cyInstance = proj(self._connection_method(*((self.pre, self.post,) + self._connection_args)))

        # Access the list of postsynaptic neurons
        self.post_ranks = self.cyInstance.post_rank()


    def _store_connectivity(self, method, args, delay, storage_format="lil", storage_order="post_to_pre"):
        """
        Store connectivity data. This function is called from cython_ext.Connectors module.
        """
        if self._connection_method != None:
            Global._warning("Projection ", self.proj.name, " was already connected ... data will be overwritten.")

        # Store connectivity pattern parameters
        self._connection_method = method
        self._connection_args = args
        self._connection_delay = delay
        self._storage_format = storage_format
        self._storage_order = storage_order

        # Analyse the delay
        if isinstance(delay, (int, float)): # Uniform delay
            self.max_delay = round(delay/Global.config['dt'])
            self.uniform_delay = round(delay/Global.config['dt'])
        elif isinstance(delay, RandomDistribution): # Non-uniform delay
            self.uniform_delay = -1
            # Ensure no negative delays are generated
            if delay.min is None or delay.min < Global.config['dt']:
                delay.min = Global.config['dt']
            # The user needs to provide a max in order to compute max_delay
            if delay.max is None:
                Global._error('Projection.connect_xxx(): if you use a non-bounded random distribution for the delays (e.g. Normal), you need to set the max argument to limit the maximal delay.')

            self.max_delay = round(delay.max/Global.config['dt'])
        elif isinstance(delay, (list, np.ndarray)): # connect_from_matrix/sparse
            if len(delay) > 0:
                self.uniform_delay = -1
                self.max_delay = round(max([max(l) for l in delay])/Global.config['dt'])
            else: # list is empty, no delay
                self.max_delay = -1
                self.uniform_delay = -1
        else:
            Global._error('Projection.connect_xxx(): delays are not valid!')

        # Transmit the max delay to the pre pop
        if isinstance(self.pre, PopulationView):
            self.pre.population.max_delay = max(self.max_delay, self.pre.population.max_delay)
        else:
            self.pre.max_delay = max(self.max_delay, self.pre.max_delay)

    def _has_single_weight(self):
        "If a single weight should be generated instead of a LIL"
        return self._single_constant_weight and not Global.config['structural_plasticity'] and not self.synapse_type.description['plasticity'] and Global.config['paradigm']=="openmp"

    def reset(self, attributes=-1, synapses=False):
        """
        Resets all parameters and variables of the projection to the value they had before the call to compile.

        *Parameters:*

        * **attributes**: list of attributes (parameter or variable) which should be reinitialized. Default: all attributes.

        .. note::

            Only parameters and variables are reinitialized, not the connectivity structure (including the weights and delays).

            The parameter ``synapses`` will be used in a future release to also reinitialize the connectivity structure.

        """
        if attributes == -1:
            attributes = self.attributes

        for var in attributes:
            # Skip w
            if var=='w':
                continue
            # check it exists
            if not var in self.attributes:
                Global._warning("Projection.reset():", var, "is not an attribute of the population, won't reset.")
                continue
            # Set the value
            try:
                self.__setattr__(var, self.init[var])
            except Exception as e:
                Global._print(e)
                Global._warning("Projection.reset(): something went wrong while resetting", var)
        #Global._warning('Projection.reset(): only parameters and variables are reinitialized, not the connectivity structure (including the weights)...')

    ################################
    ## Dendrite access
    ################################
    @property
    def size(self):
        "Number of post-synaptic neurons receiving synapses."
        if self.cyInstance:
            return len(self.post_ranks)
        else:
            return 0

    def __len__(self):
        " Number of postsynaptic neurons receiving synapses in this projection."
        return self.size

    @property
    def nb_synapses(self):
        "Total number of synapses in the projection."
        if self.cyInstance == None:
            Global._warning("Access 'nb_synapses' attribute of a Projection is only valid after compile()")
            return 0
        return sum([self.cyInstance.nb_synapses(n) for n in range(self.size)])


    @property
    def dendrites(self):
        """
        Iteratively returns the dendrites corresponding to this projection.
        """
        for idx, n in enumerate(self.post_ranks):
            yield Dendrite(self, n, idx)

    def dendrite(self, post):
        """
        Returns the dendrite of a postsynaptic neuron according to its rank.

        *Parameters*:

        * **post**: can be either the rank or the coordinates of the postsynaptic neuron
        """
        if not self.initialized:
            Global._error('dendrites can only be accessed after compilation.')

        if isinstance(post, int):
            rank = post
        else:
            rank = self.post.rank_from_coordinates(post)

        if rank in self.post_ranks:
            return Dendrite(self, rank, self.post_ranks.index(rank))
        else:
            Global._error(" The neuron of rank "+ str(rank) + " has no dendrite in this projection.", exit=True)


    def synapse(self, pre, post):
        """
        Returns the synapse between a pre- and a post-synaptic neuron if it exists, None otherwise.

        *Parameters*:

        * **pre**: rank of the pre-synaptic neuron.
        * **post**: rank of the post-synaptic neuron.
        """
        if not isinstance(pre, int) or not isinstance(post, int):
            Global._error('Projection.synapse() only accepts ranks for the pre and post neurons.')

        return self.dendrite(post).synapse(pre)


    # Iterators
    def __getitem__(self, *args, **kwds):
        """
        Returns dendrite of the given position in the postsynaptic population.

        If only one argument is given, it is a rank. If it is a tuple, it is coordinates.
        """
        if len(args) == 1:
            return self.dendrite(args[0])
        return self.dendrite(args)

    def __iter__(self):
        " Returns iteratively each dendrite in the population in ascending postsynaptic rank order."
        for idx, n in enumerate(self.post_ranks):
            yield Dendrite(self, n, idx)

    ################################
    ## Access to attributes
    ################################
    def get(self, name):
        """
        Returns a list of parameters/variables values for each dendrite in the projection.

        The list will have the same length as the number of actual dendrites (self.size), so it can be smaller than the size of the postsynaptic population. Use self.post_ranks to indice it.

        *Parameters*:

        * **name**: the name of the parameter or variable
        """
        return self.__getattr__(name)

    def set(self, value):
        """
        Sets the parameters/variables values for each dendrite in the projection.

        For parameters, you can provide:

        * a single value, which will be the same for all dendrites.

        * a list or 1D numpy array of the same length as the number of actual dendrites (self.size).

        For variables, you can provide:

        * a single value, which will be the same for all synapses of all dendrites.

        * a list or 1D numpy array of the same length as the number of actual dendrites (self.size). The synapses of each postsynaptic neuron will take the same value.

        .. warning::

            It not possible to set different values to each synapse using this method. One should iterate over the dendrites::

                for dendrite in proj.dendrites:
                    dendrite.w = np.ones(dendrite.size)

        *Parameter*:

        * **value**: a dictionary with the name of the parameter/variable as key.

        """

        for name, val in value.items():
            self.__setattr__(name, val)

    def __getattr__(self, name):
        " Method called when accessing an attribute."
        if name == 'initialized' or not hasattr(self, 'initialized'): # Before the end of the constructor
            return object.__getattribute__(self, name)
        elif hasattr(self, 'attributes'):
            if name in ['plasticity', 'transmission', 'update']:
                return self._get_flag(name)
            if name in ['delay']:
                return self._get_delay()
            if name in self.attributes:
                if not self.initialized:
                    return self.init[name]
                else:
                    return self._get_cython_attribute( name )
            elif name in self.functions:
                return self._function(name)
            else:
                return object.__getattribute__(self, name)
        return object.__getattribute__(self, name)

    def __setattr__(self, name, value):
        " Method called when setting an attribute."
        if name == 'initialized' or not hasattr(self, 'initialized'): # Before the end of the constructor
            object.__setattr__(self, name, value)
        elif hasattr(self, 'attributes'):
            if name in ['plasticity', 'transmission', 'update']:
                self._set_flag(name, bool(value))
                return
            if name in ['delay']:
                self._set_delay(value)
                return
            if name in self.attributes:
                if not self.initialized:
                    self.init[name] = value
                else:
                    self._set_cython_attribute(name, value)
            else:
                object.__setattr__(self, name, value)
        else:
            object.__setattr__(self, name, value)

    def _get_cython_attribute(self, attribute):
        """
        Returns the value of the given attribute for all neurons in the population,
        as a list of lists having the same geometry as the population if it is local.

        *Parameter:*

        * *attribute*: a string representing the variables's name.

        """
        return getattr(self.cyInstance, 'get_'+attribute)()

    def _set_cython_attribute(self, attribute, value):
        """
        Sets the value of the given attribute for all post-synaptic neurons in the projection,
        as a NumPy array having the same geometry as the population if it is local.

        *Parameter:*

        * **attribute**: a string representing the variables's name.
        * **value**: the value it should take.

        """
        # Convert np.arrays into lists for better iteration
        if isinstance(value, np.ndarray):
            value = list(value)
        # A list is given
        if isinstance(value, list):
            if len(value) == len(self.post_ranks):
                if attribute in self.synapse_type.description['local']:
                    for idx, n in enumerate(self.post_ranks):
                        if not len(value[idx]) == self.cyInstance.nb_synapses(idx):
                            Global._error('The postynaptic neuron ' + str(n) + ' receives '+ str(self.cyInstance.nb_synapses(idx))+ ' synapses.')
                        getattr(self.cyInstance, 'set_dendrite_'+attribute)(idx, value[idx])
                elif attribute in self.synapse_type.description['semiglobal']:
                    getattr(self.cyInstance, 'set_'+attribute)(value)
                else:
                    Global._error('The parameter', attribute, 'is global to the population, cannot assign a list.')
            else:
                Global._error('The projection has', self.size, 'post-synaptic neurons, the list must have the same size.')
        # A Random Distribution is given
        elif isinstance(value, RandomDistribution):
            if attribute in self.synapse_type.description['local']:
                for idx, n in enumerate(self.post_ranks):
                    getattr(self.cyInstance, 'set_dendrite_'+attribute)(idx, value.get_values(self.cyInstance.nb_synapses(idx)))
            elif attribute in self.synapse_type.description['semiglobal']:
                getattr(self.cyInstance, 'set_'+attribute)(value.get_values(len(self.post_ranks)))
            elif attribute in self.synapse_type.description['global']:
                getattr(self.cyInstance, 'set_'+attribute)(value.get_values(1))
        # A single value is given
        else:
            if attribute in self.synapse_type.description['local']:
                for idx, n in enumerate(self.post_ranks):
                    getattr(self.cyInstance, 'set_dendrite_'+attribute)(idx, value*np.ones(self.cyInstance.nb_synapses(idx)))
            elif attribute in self.synapse_type.description['semiglobal']:
                getattr(self.cyInstance, 'set_'+attribute)(value*np.ones(len(self.post_ranks)))
            else:
                getattr(self.cyInstance, 'set_'+attribute)(value)

    def _get_flag(self, attribute):
        "flags such as learning, transmission"
        return getattr(self.cyInstance, '_get_'+attribute)()

    def _set_flag(self, attribute, value):
        "flags such as learning, transmission"
        getattr(self.cyInstance, '_set_'+attribute)(value)



    ################################
    ## Access to delays
    ################################
    def _get_delay(self):
        if not hasattr(self.cyInstance, 'get_delay'):
            if self.max_delay <= 1 :
                return Global.config['dt']
        elif self.uniform_delay != -1:
                return self.uniform_delay * Global.config['dt']
        else:
            return [[pre * Global.config['dt'] for pre in post] for post in self.cyInstance.get_delay()]

    def _set_delay(self, value):

        if self.cyInstance: # After compile()
            if not hasattr(self.cyInstance, 'get_delay'):
                if self.max_delay <= 1 and value != Global.config['dt']:
                    Global._error("set_delay: the projection was instantiated without delays, it is too late to create them...")

            elif self.uniform_delay != -1:
                current_delay = self.uniform_delay
                if isinstance(value, (np.ndarray)):
                    if value.size > 1:
                        Global._error("set_delay: the projection was instantiated with uniform delays, it is too late to load non-uniform values...")
                    else:
                        value = max(1, round(value[0]/Global.config['dt']))
                elif isinstance(value, (float, int)):
                    value = max(1, round(float(value)/Global.config['dt']))
                else:
                    Global._error("set_delay: only float, int or np.array values are possible.")

                # The new max_delay is higher than before
                if value > self.max_delay:
                    self.max_delay = value
                    self.uniform_delay = value
                    self.cyInstance.set_delay(value)
                    if isinstance(self.pre, PopulationView):
                        self.pre.population.max_delay = max(self.max_delay, self.pre.population.max_delay)
                        self.pre.population.cyInstance.update_max_delay(self.pre.population.max_delay)
                    else:
                        self.pre.max_delay = max(self.max_delay, self.pre.max_delay)
                        self.pre.cyInstance.update_max_delay(self.pre.max_delay)
                    return
                else:
                    self.uniform_delay = value
                    self.cyInstance.set_delay(value)

            else: # variable delays
                if not isinstance(value, (np.ndarray, list)):
                    Global._error("set_delay with variable delays: you must provide a list of lists of exactly the same size as before.")

                # Check the number of delays
                nb_values = sum([len(s) for s in value])
                if nb_values != self.nb_synapses:
                    Global._error("set_delay with variable delays: the sizes do not match. You have to provide one value for each existing synapse.")
                if len(value) != len(self.post_ranks):
                    Global._error("set_delay with variable delays: the sizes do not match. You have to provide one value for each existing synapse.")

                # Convert to steps
                if isinstance(value, np.ndarray):
                    delays = [[max(1, round(value[i, j]/Global.config['dt'])) for j in range(value.shape[1])] for i in range(value.shape[0])]
                else:
                    delays = [[max(1, round(v/Global.config['dt'])) for v in c] for c in value]

                # Max delay
                max_delay = max([max(l) for l in delays])

                # Send the max delay to the pre population
                if max_delay > self.max_delay:
                    self.max_delay = max_delay
                    self.cyInstance.update_max_delay(self.max_delay)
                    if isinstance(self.pre, PopulationView):
                        self.pre.population.max_delay = max(self.max_delay, self.pre.population.max_delay)
                        self.pre.population.cyInstance.update_max_delay(self.pre.population.max_delay)
                    else:
                        self.pre.max_delay = max(self.max_delay, self.pre.max_delay)
                        self.pre.cyInstance.update_max_delay(self.pre.max_delay)

                # Send the new values to the projection
                self.cyInstance.set_delay(delays)

        else: # before compile()
            Global._error("set_delay before compile(): not implemented yet.")


    ################################
    ## Access to functions
    ################################
    def _function(self, func):
        "Access a user defined function"
        if not self.initialized:
            Global._error('the network is not compiled yet, cannot access the function ' + func)

        return getattr(self.cyInstance, func)

    ################################
    ## Learning flags
    ################################
    def enable_learning(self, period=None, offset=None):
        """
        Enables learning for all the synapses of this projection.

        *Parameters*:

        * **period**: determines how often the synaptic variables will be updated.
        * **offset**: determines the offset at which the synaptic variables will be updated relative to the current time.

        For example, providing the following parameters at time 10 ms::

            enable_learning(period=10., offset=5.)

        would call the updating methods at times 15, 25, 35, etc...

        The default behaviour is that the synaptic variables are updated at each time step. The parameters must be multiple of ``dt``
        """
        # Check arguments
        if not period is None and not offset is None:
            if offset >= period:
                Global._error('enable_learning(): the offset must be smaller than the period.')

        if period is None and not offset is None:
            Global._error('enable_learning(): if you define an offset, you have to define a period.')

        try:
            self.cyInstance._set_update(True)
            self.cyInstance._set_plasticity(True)
            if period != None:
                self.cyInstance._set_update_period(int(period/Global.config['dt']))
            else:
                self.cyInstance._set_update_period(int(1))
                period = Global.config['dt']
            if offset != None:
                relative_offset = Global.get_time() % period + offset
                self.cyInstance._set_update_offset(int(int(relative_offset%period)/Global.config['dt']))
            else:
                self.cyInstance._set_update_offset(int(0))
        except:
            Global._warning('Enable_learning() is only possible after compile()')

    def disable_learning(self, update=None):
        """
        Disables learning for all synapses of this projection.

        The effect depends on the rate-coded or spiking nature of the projection:

        * **Rate-coded**: the updating of all synaptic variables is disabled (including the weights ``w``). This is equivalent to ``proj.update = False``.

        * **Spiking**: the updating of the weights ``w`` is disabled, but all other variables are updated. This is equivalent to ``proj.plasticity = False``.

        This method is useful when performing some tests on a trained network without messing with the learned weights.
        """
        try:
            if self.synapse_type.type == 'rate':
                self.cyInstance._set_update(False)
            else:
                self.cyInstance._set_plasticity(False)
        except:
            Global._warning('disabling learning is only possible after compile().')


    ################################
    ## Methods on connectivity matrix
    ################################

    def save_connectivity(self, filename):
        """
        Saves the connectivity of the projection into a file.

        Only the connectivity matrix, the weights and delays are saved, not the other synaptic variables.

        The generated data can be used to create a projection in another network::

            proj.connect_from_file(filename)

        * If the file name is '.npz', the data will be saved and compressed using `np.savez_compressed` (recommended).

        * If the file name ends with '.gz', the data will be pickled into a binary file and compressed using gzip.

        * If the file name is '.mat', the data will be saved as a Matlab 7.2 file. Scipy must be installed.

        * Otherwise, the data will be pickled into a simple binary text file using pickle.

        *Parameters*:

        * **filename**: file name, may contain relative or absolute path.

        """
        # Check that the network is compiled
        if not self.initialized:
            Global._error('save_connectivity(): the network has not been compiled yet.')
            return

        # Check if the repertory exist
        (path, fname) = os.path.split(filename)

        if not path == '':
            if not os.path.isdir(path):
                Global._print('Creating folder', path)
                os.mkdir(path)

        extension = os.path.splitext(fname)[1]

        # Gathering the data
        data = {
                'name': self.name,
                'post_ranks': self.post_ranks,
                'pre_ranks': self.cyInstance.pre_rank_all(), # was: [self.cyInstance.pre_rank(n) for n in range(self.size)],
                'w': self.cyInstance.get_w(),
                'delay': self.cyInstance.get_delay() if hasattr(self.cyInstance, 'get_delay') else None,
                'max_delay': self.max_delay,
                'uniform_delay': self.uniform_delay,
                'size': self.size,
                'nb_synapses': sum([self.cyInstance.nb_synapses(n) for n in range(self.size)])
            }

        # Save the data
        try:
            import cPickle as pickle # Python2
        except:
            import pickle # Python3

        if extension == '.gz':
            Global._print("Saving connectivity in gunzipped binary format...")
            try:
                import gzip
            except:
                Global._error('gzip is not installed.')
                return
            with gzip.open(filename, mode = 'wb') as w_file:
                try:
                    pickle.dump(data, w_file, protocol=pickle.HIGHEST_PROTOCOL)
                except Exception as e:
                    Global._print('Error while saving in gzipped binary format.')
                    Global._print(e)
                    return

        elif extension == '.npz':
            Global._print("Saving connectivity in Numpy format...")
            np.savez_compressed(filename, **data )

        elif extension == '.mat':
            Global._print("Saving connectivity in Matlab format...")
            if data['delay'] is None:
                data['delay'] = 0
            try:
                import scipy.io as sio
                sio.savemat(filename, data)
            except Exception as e:
                Global._error('Error while saving in Matlab format.')
                Global._print(e)
                return

        else:
            Global._print("Saving connectivity in text format...")
            # save in Pythons pickle format
            with open(filename, mode = 'wb') as w_file:
                try:
                    pickle.dump(data, w_file, protocol=pickle.HIGHEST_PROTOCOL)
                except Exception as e:
                    Global._print('Error while saving in text format.')
                    Global._print(e)
                    return
            return


    def receptive_fields(self, variable = 'w', in_post_geometry = True):
        """
        Gathers all receptive fields within this projection.

        *Parameters*:

        * **variable**: name of the variable
        * **in_post_geometry**: if False, the data will be plotted as square grid. (default = True)
        """
        if in_post_geometry:
            x_size = self.post.geometry[1]
            y_size = self.post.geometry[0]
        else:
            x_size = int( math.floor(math.sqrt(self.post.size)) )
            y_size = int( math.ceil(math.sqrt(self.post.size)) )


        def get_rf(rank): # TODO: IMPROVE
            res = np.zeros( self.pre.size )
            for n in range(len(self.post_ranks)):
                if self.post_ranks[n] == n:
                    pre_ranks = self.cyInstance.pre_rank(n)
                    data = getattr(self.cyInstance, 'get_dendrite_'+variable)(rank)
                    for j in range(len(pre_ranks)):
                        res[pre_ranks[j]] = data[j]
            return res.reshape(self.pre.geometry)

        res = np.zeros((1, x_size*self.pre.geometry[1]))
        for y in range ( y_size ):
            row = np.concatenate(  [ get_rf(self.post.rank_from_coordinates( (y, x) ) ) for x in range ( x_size ) ], axis = 1)
            res = np.concatenate((res, row))

        return res

    def connectivity_matrix(self, fill=0.0):
        """
        Returns a dense connectivity matrix (2D Numpy array) representing the connections between the pre- and post-populations.

        The first index of the matrix represents post-synaptic neurons, the second the pre-synaptic ones.

        If PopulationViews were used for creating the projection, the matrix is expanded to the whole populations by default.

        *Parameters*:

        * **fill**: value to put in the matrix when there is no connection (default: 0.0).
        """
        if isinstance(self.pre, PopulationView):
            size_pre = self.pre.population.size
        else:
            size_pre = self.pre.size
        if isinstance(self.post, PopulationView):
            size_post = self.post.population.size
        else:
            size_post = self.post.size

        res = np.ones((size_post, size_pre)) * fill
        for rank in self.post_ranks:
            idx = self.post_ranks.index(rank)
            try:
                preranks = self.cyInstance.pre_rank(idx)
                w = self.cyInstance.get_dendrite_w(idx)
            except:
                Global._error('The connectivity matrix can only be accessed after compilation')
                return []
            res[rank, preranks] = w
        return res


    ################################
    ## Save/load methods
    ################################

    def _data(self):
        "Method gathering all info about the projection when calling save()"

        if not self.initialized:
            Global._error('save_connectivity(): the network has not been compiled yet.')

        desc = {}
        desc['name'] = self.name
        desc['pre'] = self.pre.name
        desc['post'] = self.post.name
        desc['target'] = self.target
        desc['post_ranks'] = self.post_ranks
        desc['attributes'] = self.attributes
        desc['parameters'] = self.parameters
        desc['variables'] = self.variables
        desc['pre_ranks'] = self.cyInstance.pre_rank_all()
        desc['delays'] = self._get_delay()


        # Attributes to save
        attributes = self.attributes
        if not 'w' in self.attributes:
            attributes.append('w')

        # Save all attributes
        for var in attributes:
            try:
                desc[var] = getattr(self.cyInstance, 'get_'+var)()
            except:
                Global._warning('Can not save the attribute ' + var + ' in the projection.')

        return desc

    def save(self, filename):
        """
        Saves all information about the projection (connectivity, current value of parameters and variables) into a file.

        * If the file name is '.npz', the data will be saved and compressed using `np.savez_compressed` (recommended).

        * If the file name ends with '.gz', the data will be pickled into a binary file and compressed using gzip.

        * If the file name is '.mat', the data will be saved as a Matlab 7.2 file. Scipy must be installed.

        * Otherwise, the data will be pickled into a simple binary text file using pickle.

        *Parameter*:

        * **filename**: file name, may contain relative or absolute path.

        .. warning::

            The '.mat' data will not be loadable by ANNarchy, it is only for external analysis purpose.

        *Example*::

            proj.save('proj1.npz')
            proj.save('proj1.txt')
            proj.save('proj1.txt.gz')
            proj.save('proj1.mat')

        """
        from ANNarchy.core.IO import _save_data
        _save_data(filename, self._data())


    def load(self, filename):
        """
        Loads the saved state of the projection by `Projection.save()`.

        Warning: Matlab data can not be loaded.

        *Parameters*:

        * **filename**: the file name with relative or absolute path.

        Example::

            proj.load('proj1.npz')
            proj.load('proj1.txt')
            proj.load('proj1.txt.gz')

        """
        from ANNarchy.core.IO import _load_data
        self._load_proj_data(_load_data(filename))


    def _load_proj_data(self, desc):
        """
        Updates the projection with the stored data set.
        """

        # Check deprecation
        if not 'attributes' in desc.keys():
            Global._error('The file was saved using a deprecated version of ANNarchy.')
            return
        if 'dendrites' in desc: # Saved before 4.5.3
            Global._error("The file was saved using a deprecated version of ANNarchy.")
            return

        # If the post ranks have changed, overwrite
        if 'post_ranks' in desc and not list(desc['post_ranks']) == self.post_ranks:
            getattr(self.cyInstance, 'set_post_rank')(desc['post_ranks'])
        # If the pre ranks have changed, overwrite
        if 'pre_ranks' in desc and not list(desc['pre_ranks']) == self.cyInstance.pre_rank_all():
            getattr(self.cyInstance, 'set_pre_rank')(desc['pre_ranks'])

        # Delays
        if 'delays' in desc:
            delays = desc['delays']
            if isinstance(delays, np.ndarray): # variable delays
                if delays.size == 1:
                    delays = float(delays)
                else:
                    delays = list(delays)
            self._set_delay(delays)

        # Other variables
        for var in desc['attributes']:
            try:
                getattr(self.cyInstance, 'set_' + var)(desc[var])
            except Exception as e:
                Global._print(e)
                Global._warning('load(): the variable', var, 'does not exist in the current version of the network, skipping it.')
                continue

    ################################
    ## Structural plasticity
    ################################
    def start_pruning(self, period=None):
        """
        Starts pruning the synapses in the projection if the synapse defines a 'pruning' argument.

        'structural_plasticity' must be set to True in setup().

        *Parameters*:

        * **period**: how often pruning should be evaluated (default: dt, i.e. each step)
        """
        if not period:
            period = Global.config['dt']
        if not self.cyInstance:
            Global._error('Can not start pruning if the network is not compiled.')

        if Global.config['structural_plasticity']:
            try:
                self.cyInstance.start_pruning(int(period/Global.config['dt']), Global.get_current_step())
            except :
                Global._error("The synapse does not define a 'pruning' argument.")

        else:
            Global._error("You must set 'structural_plasticity' to True in setup() to start pruning connections.")


    def stop_pruning(self):
        """
        Stops pruning the synapses in the projection if the synapse defines a 'pruning' argument.

        'structural_plasticity' must be set to True in setup().
        """
        if not self.cyInstance:
            Global._error('Can not stop pruning if the network is not compiled.')

        if Global.config['structural_plasticity']:
            try:
                self.cyInstance.stop_pruning()
            except:
                Global._error("The synapse does not define a 'pruning' argument.")

        else:
            Global._error("You must set 'structural_plasticity' to True in setup() to start pruning connections.")

    def start_creating(self, period=None):
        """
        Starts creating the synapses in the projection if the synapse defines a 'creating' argument.

        'structural_plasticity' must be set to True in setup().

        *Parameters*:

        * **period**: how often creating should be evaluated (default: dt, i.e. each step)
        """
        if not period:
            period = Global.config['dt']
        if not self.cyInstance:
            Global._error('Can not start creating if the network is not compiled.')

        if Global.config['structural_plasticity']:
            try:
                self.cyInstance.start_creating(int(period/Global.config['dt']), Global.get_current_step())
            except:
                Global._error("The synapse does not define a 'creating' argument.")

        else:
            Global._error("You must set 'structural_plasticity' to True in setup() to start creating connections.")

    def stop_creating(self):
        """
        Stops creating the synapses in the projection if the synapse defines a 'creating' argument.

        'structural_plasticity' must be set to True in setup().
        """
        if not self.cyInstance:
            Global._error('Can not stop creating if the network is not compiled.')

        if Global.config['structural_plasticity']:
            try:
                self.cyInstance.stop_creating()
            except:
                Global._error("The synapse does not define a 'creating' argument.")

        else:
            Global._error("You must set 'structural_plasticity' to True in setup() to start creating connections.")

    ################################
    ## Memory Management
    ################################
    def size_in_bytes(self):
        """
        Returns the size in bytes of the allocated memory on C++ side. Note that this does not reflect monitored data and that it only works after compile() was invoked.
        """
        if self.initialized:
            return self.cyInstance.size_in_bytes()
        else:
            return 0

    def _clear(self):
        """
        Deallocates the container within the C++ instance. The population object is not usable anymore after calling this function.

        Warning: should be only called by the net deconstructor (in the context of parallel_run).
        """
        if self.initialized:
            self.cyInstance.clear()
Пример #2
0
class Projection(object):
    """
    Represents all synapses of the same type between two populations.
    """

    def __init__(self, pre, post, target, synapse=None, name=None):
        """
        *Parameters*:

            * **pre**: pre-synaptic population (either its name or a ``Population`` object).
            * **post**: post-synaptic population (either its name or a ``Population`` object).
            * **target**: type of the connection.
            * **synapse**: a ``Synapse`` instance.
            * **name**: unique name of the projection (optional).

        By default, the synapse only ensures linear synaptic transmission:

        * For rate-coded populations: ``psp = w * pre.r``
        * For spiking populations: ``g_target += w``

        """

        # Store the pre and post synaptic populations
        # the user provide either a string or a population object
        # in case of string, we need to search for the corresponding object
        if isinstance(pre, str):
            for pop in Global._network[0]['populations']:
                if pop.name == pre:
                    self.pre = pop
        else:
            self.pre = pre

        if isinstance(post, str):
            for pop in Global._network[0]['populations']:
                if pop.name == post:
                    self.post = pop
        else:
            self.post = post

        # Store the arguments
        self.target = target

        # Add the target to the postsynaptic population
        self.post.targets.append(self.target)

        # check if a synapse description is attached
        if not synapse:
            # No synapse attached assume default synapse based on
            # presynaptic population.
            if self.pre.neuron_type.type == 'rate':
                from ANNarchy.models.Synapses import DefaultRateCodedSynapse
                self.synapse_type = DefaultRateCodedSynapse()
                self.synapse_type.type = 'rate'
            else:
                from ANNarchy.models.Synapses import DefaultSpikingSynapse
                self.synapse_type = DefaultSpikingSynapse()
                self.synapse_type.type = 'spike'

        elif inspect.isclass(synapse):
            self.synapse_type = synapse()
            self.synapse_type.type = self.pre.neuron_type.type
        else:
            self.synapse_type = copy.deepcopy(synapse)
            self.synapse_type.type = self.pre.neuron_type.type

        # Analyse the parameters and variables
        self.synapse_type._analyse()

        # Create a default name
        self.id = len(Global._network[0]['projections'])
        if name:
            self.name = name
        else:
            self.name = 'proj'+str(self.id)

        # Get a list of parameters and variables
        self.parameters = []
        self.init = {}
        for param in self.synapse_type.description['parameters']:
            self.parameters.append(param['name'])
            self.init[param['name']] = param['init']

        self.variables = []
        for var in self.synapse_type.description['variables']:
            self.variables.append(var['name'])
            self.init[var['name']] = var['init']

        self.attributes = self.parameters + self.variables

        # Add the population to the global network
        Global._network[0]['projections'].append(self)

        # Finalize initialization
        self.initialized = False

        # Cython instance
        self.cyInstance = None

        # Connectivity
        self._synapses = None
        self._connection_method = None
        self._connection_args = None
        self._connection_delay = None
        self._connector = None

        # If a single weight value is used
        self._single_constant_weight = False

        # If a dense matrix should be used instead of LIL
        self._dense_matrix = False

        # Recorded variables
        self.recorded_variables = {}

        # Reporting
        self.connector_name = "Specific"
        self.connector_description = "Specific"

        # Overwritten by derived classes, to add
        # additional code
        self._specific_template = {}

        # To allow case-specific adjustment of parallelization
        # parameters, e. g. openMP schedule, we introduce a
        # dictionary read by the ProjectionGenerator.
        #
        # Will be overwritten either by inherited classes or
        # by an omp_config provided to the compile() method.
        self._omp_config = {
            #'psp_schedule': 'schedule(dynamic)'
        }

    # Add defined connectors
    connect_one_to_one = ConnectorMethods.connect_one_to_one
    connect_all_to_all = ConnectorMethods.connect_all_to_all
    connect_gaussian = ConnectorMethods.connect_gaussian
    connect_dog = ConnectorMethods.connect_dog
    connect_fixed_probability = ConnectorMethods.connect_fixed_probability
    connect_fixed_number_pre = ConnectorMethods.connect_fixed_number_pre
    connect_fixed_number_post = ConnectorMethods.connect_fixed_number_post
    connect_with_func = ConnectorMethods.connect_with_func
    connect_from_matrix = ConnectorMethods.connect_from_matrix
    _load_from_matrix = ConnectorMethods._load_from_matrix
    connect_from_sparse = ConnectorMethods.connect_from_sparse
    _load_from_sparse = ConnectorMethods._load_from_sparse
    connect_from_file = ConnectorMethods.connect_from_file
    _load_from_csr = ConnectorMethods._load_from_csr

    def _generate(self):
        "Overriden by specific projections to generate the code"
        pass

    def _instantiate(self, module):
        "Instantiates the projection after compilation."
        self._connect(module)
        self.initialized = True

    def _init_attributes(self):
        """
        Method used after compilation to initialize the attributes. Called by Generator._instantiate
        """
        for name, val in self.init.items():
            if not name in ['w']:
                self.__setattr__(name, val)

    def _connect(self, module):
        """
        Builds up dendrites either from list or dictionary. Called by instantiate().
        """
        if not self._connection_method:
            Global._error('The projection between ' + self.pre.name + ' and ' + self.post.name + ' is declared but not connected.')
            exit(0)

        proj = getattr(module, 'proj'+str(self.id)+'_wrapper')
        self.cyInstance = proj(self._connection_method(*((self.pre, self.post,) + self._connection_args)))

        # Access the list of postsynaptic neurons
        self.post_ranks = self.cyInstance.post_rank()

    def _store_connectivity(self, method, args, delay):

        self._connection_method = method
        self._connection_args = args
        self._connection_delay = delay

        # Analyse the delay
        if isinstance(delay, (int, float)): # Uniform delay
            self.max_delay = int(delay/Global.config['dt'])
            self.uniform_delay = int(delay/Global.config['dt'])
        elif isinstance(delay, RandomDistribution): # Non-uniform delay
            self.uniform_delay = -1
            # Ensure no negative delays are generated
            if delay.min == None:
                delay.min = Global.config['dt']
            # The user needs to provide a max in order to compute max_delay
            if delay.max == None:
                Global._error('Projection.connect_xxx(): if you use a non-bounded random distribution for the delays (e.g. Normal), you need to set the max argument to limit the maximal delay.')
                exit(0)
            self.max_delay = int(delay.max/Global.config['dt'])
        elif isinstance(delay, (list, np.ndarray)): # connect_from_matrix/sparse
            self.uniform_delay = -1
            self.max_delay = int(max([max(l) for l in delay])/Global.config['dt'])
        else:
            Global._error('Projection.connect_xxx(): delays are not valid!')
            exit(0)

        # Transmit the max delay to the pre pop
        if isinstance(self.pre, PopulationView):
            self.pre.population.max_delay = max(self.max_delay, self.pre.population.max_delay)
        else:
            self.pre.max_delay = max(self.max_delay, self.pre.max_delay)


    def _has_single_weight(self):
        "If a single weight should be generated instead of a LIL"
        return self._single_constant_weight and not Global.config['structural_plasticity'] and not self.synapse_type.description['plasticity'] and Global.config['paradigm']=="openmp"


    def reset(self, synapses=False):
        """
        Resets all parameters and variables to the value they had before the call to compile.

        *Parameters*:

        * **synapses**: if True, the connections will also be erased (default: False).

        .. note::

            Not implemented yet...
        """
        self._init_attributes()
        if synapses:
            Global._warning('Resetting synapses is not implemented yet...')

    ################################
    ## Dendrite access
    ################################
    @property
    def size(self):
        "Number of post-synaptic neurons receiving synapses."
        if self.cyInstance:
            return len(self.post_ranks)
        else:
            return 0

    def __len__(self):
        " Number of postsynaptic neurons receiving synapses in this projection."
        return self.size

    @property
    def nb_synapses(self):
        "Total number of synapses in the projection."
        return sum([self.cyInstance.nb_synapses(n) for n in range(self.size)])


    @property
    def dendrites(self):
        """
        Iteratively returns the dendrites corresponding to this projection.
        """
        for idx, n in enumerate(self.post_ranks):
            yield Dendrite(self, n, idx)

    def dendrite(self, post):
        """
        Returns the dendrite of a postsynaptic neuron according to its rank.

        *Parameters*:

            * **post**: can be either the rank or the coordinates of the postsynaptic neuron
        """
        if not self.initialized:
            Global._error('dendrites can only be accessed after compilation.')
            exit(0)
        if isinstance(post, int):
            rank = post
        else:
            rank = self.post.rank_from_coordinates(post)

        if rank in self.post_ranks:
            return Dendrite(self, rank, self.post_ranks.index(rank))
        else:
            Global._error(" The neuron of rank "+ str(rank) + " has no dendrite in this projection.")
            return None

    def synapse(self, pre, post):
        """
        Returns the synapse between a pre- and a post-synaptic neuron if it exists, None otherwise.

        *Parameters*:

            * **pre**: rank of the pre-synaptic neuron.
            * **post**: rank of the post-synaptic neuron.
        """
        if not isinstance(pre, int) or not isinstance(post, int):
            Global._error('Projection.synapse() only accepts ranks for the pre and post neurons.')
        return self.dendrite(post).synapse(pre)


    # Iterators
    def __getitem__(self, *args, **kwds):
        """ Returns dendrite of the given position in the postsynaptic population.

        If only one argument is given, it is a rank. If it is a tuple, it is coordinates.
        """
        if len(args) == 1:
            return self.dendrite(args[0])
        return self.dendrite(args)

    def __iter__(self):
        " Returns iteratively each dendrite in the population in ascending postsynaptic rank order."
        for idx, n in enumerate(self.post_ranks):
            yield Dendrite(self, n, idx)

    ################################
    ## Access to attributes
    ################################

    @property
    def delay(self):
        if not hasattr(self.cyInstance, 'get_delay'):
            if self.max_delay <= 1 :
                return Global.config['dt']
            elif self.uniform_delay != -1:
                return self.uniform_delay * Global.config['dt']
        else:
            return [[pre * Global.config['dt'] for pre in post] for post in self.cyInstance.get_delay()]


    def get(self, name):
        """
        Returns a list of parameters/variables values for each dendrite in the projection.

        The list will have the same length as the number of actual dendrites (self.size), so it can be smaller than the size of the postsynaptic population. Use self.post_ranks to indice it.

        *Parameters*:

        * **name**: the name of the parameter or variable
        """
        return self.__getattr__(name)

    def set(self, value):
        """ Sets the parameters/variables values for each dendrite in the projection.

        For parameters, you can provide:

            * a single value, which will be the same for all dendrites.

            * a list or 1D numpy array of the same length as the number of actual dendrites (self.size).

        For variables, you can provide:

            * a single value, which will be the same for all synapses of all dendrites.

            * a list or 1D numpy array of the same length as the number of actual dendrites (self.size). The synapses of each postsynaptic neuron will take the same value.

        .. warning::

            It not possible to set different values to each synapse using this method. One should iterate over the dendrites::

                for dendrite in proj.dendrites:
                    dendrite.w = np.ones(dendrite.size)

        *Parameters*:

        * **value**: a dictionary with the name of the parameter/variable as key.

        """

        for name, val in value:
            self.__setattr__(name, val)

    def __getattr__(self, name):
        " Method called when accessing an attribute."
        if name == 'initialized' or not hasattr(self, 'initialized'): # Before the end of the constructor
            return object.__getattribute__(self, name)
        elif hasattr(self, 'attributes'):
            if name in ['plasticity', 'transmission', 'update']:
                return self._get_flag(name)
            if name in self.attributes:
                if not self.initialized:
                    return self.init[name]
                else:
                    return self._get_cython_attribute( name )
            else:
                return object.__getattribute__(self, name)
        return object.__getattribute__(self, name)

    def __setattr__(self, name, value):
        " Method called when setting an attribute."
        if name == 'initialized' or not hasattr(self, 'initialized'): # Before the end of the constructor
            object.__setattr__(self, name, value)
        elif hasattr(self, 'attributes'):
            if name in ['plasticity', 'transmission', 'update']:
                self._set_flag(name, bool(value))
                return
            if name in self.attributes:
                if not self.initialized:
                    self.init[name] = value
                else:
                    self._set_cython_attribute(name, value)
            else:
                object.__setattr__(self, name, value)
        else:
            object.__setattr__(self, name, value)

    def _get_cython_attribute(self, attribute):
        """
        Returns the value of the given attribute for all neurons in the population,
        as a list of lists having the same geometry as the population if it is local.

        Parameter:

        * *attribute*: should be a string representing the variables's name.

        """
        return getattr(self.cyInstance, 'get_'+attribute)()

    def _set_cython_attribute(self, attribute, value):
        """
        Sets the value of the given attribute for all post-synaptic neurons in the projection,
        as a NumPy array having the same geometry as the population if it is local.

        Parameter:

        * *attribute*: should be a string representing the variables's name.

        """
        if isinstance(value, np.ndarray):
            value = list(value)
        if isinstance(value, list):
            if len(value) == len(self.post_ranks):
                for idx, n in enumerate(self.post_ranks):
                    if not len(value[idx]) == self.cyInstance.nb_synapses(idx):
                        Global._error('The postynaptic neuron ' + str(n) + ' receives '+ str(self.cyInstance.nb_synapses(idx))+ ' synapses.')
                        exit(0)
                    getattr(self.cyInstance, 'set_dendrite_'+attribute)(idx, value[idx])
            else:
                Global._error('The projection has ' + self.size + ' post-synaptic neurons.')
        elif isinstance(value, RandomDistribution):
            for idx, n in enumerate(self.post_ranks):
                getattr(self.cyInstance, 'set_dendrite_'+attribute)(idx, value.get_values(self.cyInstance.nb_synapses(idx)))
        else: # a single value
            if attribute in self.synapse_type.description['local']:
                for idx, n in enumerate(self.post_ranks):
                    getattr(self.cyInstance, 'set_dendrite_'+attribute)(idx, value*np.ones(self.cyInstance.nb_synapses(idx)))
            else:
                getattr(self.cyInstance, 'set_'+attribute)(value*np.ones(len(self.post_ranks)))

    def _get_flag(self, attribute):
        "flags such as learning, transmission"
        return getattr(self.cyInstance, '_get_'+attribute)()

    def _set_flag(self, attribute, value):
        "flags such as learning, transmission"
        getattr(self.cyInstance, '_set_'+attribute)(value)

    ################################
    ## Variable flags
    ################################
    def set_variable_flags(self, name, value):
        """ Sets the flags of a variable for the projection.

        If the variable ``r`` is defined in the Synapse description through:

            w = pre.r * post.r : max=1.0

        one can change its maximum value with:

            proj.set_variable_flags('w', {'max': 2.0})

        For valued flags (init, min, max), ``value`` must be a dictionary containing the flag as key ('init', 'min', 'max') and its value.

        For positional flags (postsynaptic, implicit), the value in the dictionary must be set to the empty string '':

            proj.set_variable_flags('w', {'implicit': ''})

        A None value in the dictionary deletes the corresponding flag:

            proj.set_variable_flags('w', {'max': None})


        *Parameters*:

        * **name**: the name of the variable.

        * **value**: a dictionary containing the flags.

        """
        rk_var = self._find_variable_index(name)
        if rk_var == -1:
            Global._error('The projection '+self.name+' has no variable called ' + name)
            return

        for key, val in value.items():
            if val == '': # a flag
                try:
                    self.synapse_type.description['variables'][rk_var]['flags'].index(key)
                except: # the flag does not exist yet, we can add it
                    self.synapse_type.description['variables'][rk_var]['flags'].append(key)
            elif val == None: # delete the flag
                try:
                    self.synapse_type.description['variables'][rk_var]['flags'].remove(key)
                except: # the flag did not exist, check if it is a bound
                    if has_key(self.synapse_type.description['variables'][rk_var]['bounds'], key):
                        self.synapse_type.description['variables'][rk_var]['bounds'].pop(key)
            else: # new value for init, min, max...
                if key == 'init':
                    self.synapse_type.description['variables'][rk_var]['init'] = val
                    self.init[name] = val
                else:
                    self.synapse_type.description['variables'][rk_var]['bounds'][key] = val



    def set_variable_equation(self, name, equation):
        """ Changes the equation of a variable for the projection.

        If the variable ``w`` is defined in the Synapse description through:

            eta * dw/dt = pre.r * post.r

        one can change the equation with:

            proj.set_variable_equation('w', 'eta * dw/dt = pre.r * (post.r - 0.1) ')

        Only the equation should be provided, the flags have to be changed with ``set_variable_flags()``.

        .. warning::

            This method should be used with great care, it is advised to define another Synapse object instead.

        *Parameters*:

        * **name**: the name of the variable.

        * **equation**: the new equation as string.
        """
        rk_var = self._find_variable_index(name)
        if rk_var == -1:
            Global._error('The projection '+self.name+' has no variable called ' + name)
            return
        self.synapse_type.description['variables'][rk_var]['eq'] = equation


    def _find_variable_index(self, name):
        " Returns the index of the variable name in self.synapse_type.description['variables']"
        for idx in range(len(self.synapse_type.description['variables'])):
            if self.synapse_type.description['variables'][idx]['name'] == name:
                return idx
        return -1

    ################################
    ## Learning flags
    ################################
    def enable_learning(self, period=None, offset=None):
        """
        Enables learning for all the synapses of this projection.

        *Parameters*:

        * **period** determines how often the synaptic variables will be updated.
        * **offset** determines the offset at which the synaptic variables will be updated relative to the current time.

        For example, providing the following parameters at time 10 ms::

            enable_learning(period=10., offset=5.)

        would call the updating methods at times 15, 25, 35, etc...

        The default behaviour is that the synaptic variables are updated at each time step. The parameters must be multiple of ``dt``
        """
        # Check arguments
        if period != None and offset!=None:
            if offset >= period:
                Global._error('enable_learning(): the offset must be smaller than the period.')
                exit(0)
        if period == None and offset!=None:
            Global._error('enable_learning(): if you define an offset, you have to define a period.')
            exit(0)
        try:
            self.cyInstance._set_update(True)
            self.cyInstance._set_plasticity(True)
            if period != None:
                self.cyInstance._set_update_period(int(period/Global.config['dt']))
            else:
                self.cyInstance._set_update_period(int(1))
                period = Global.config['dt']
            if offset != None:
                relative_offset = Global.get_time() % period + offset
                self.cyInstance._set_update_offset(int(relative_offset%period))
            else:
                self.cyInstance._set_update_offset(int(0))
        except:
            Global._warning('Enable_learning() is only possible after compile()')

    def disable_learning(self, update=None):
        """
        Disables learning for all synapses of this projection.

        The effect depends on the rate-coded or spiking nature of the projection:

        * **Rate-coded**: the updating of all synaptic variables is disabled (including the weights ``w``). This is equivalent to ``proj.update = False``.

        * **Spiking**: the updating of the weights ``w`` is disabled, but all other variables are updated. This is equivalent to ``proj.plasticity = False``.

        This method is useful when performing some tests on a trained network without messing with the learned weights.
        """
        try:
            if self.synapse_type.type == 'rate':
                self.cyInstance._set_update(False)
            else:
                self.cyInstance._set_plasticity(False)
        except Exception as e:
            Global._warning('disabling learning is only possible after compile().')


    ################################
    ## Methods on connectivity matrix
    ################################

    def save_connectivity(self, filename):
        """
        Saves the projection pattern in a file.

        Only the connectivity matrix, the weights and delays are saved, not the other synaptic variables.

        The generated data should be used to create a projection in another network::

            proj.connect_from_file(filename)

        *Parameters*:

        * **filename**: file where the data will be saved.
        """
        if not self.initialized:
            Global._error('save_connectivity(): the network has not been compiled yet.')
            return
        
        data = {
                'name': self.name,
                'post_ranks': self.post_ranks,
                'pre_ranks': self.cyInstance.pre_rank_all(), # was: [self.cyInstance.pre_rank(n) for n in range(self.size)],
                'w': self.cyInstance.get_w(),
                'delay': self.cyInstance.get_delay() if hasattr(self.cyInstance, 'get_delay') else None,
                'max_delay': self.max_delay,
                'uniform_delay': self.uniform_delay,
                'size': self.size,
                'nb_synapses': sum([self.cyInstance.nb_synapses(n) for n in range(self.size)])
            }
        try:
            import cPickle as pickle # Python2
        except:
            import pickle # Python3
        with open(filename, 'wb') as wfile:
            pickle.dump(data, wfile, protocol=pickle.HIGHEST_PROTOCOL)

    def _save_connectivity_as_csv(self):
        """
        Saves the projection pattern in the csv format.

        Please note, that only the pure connectivity data pre_rank, post_rank, w and delay are stored.
        """
        filename = self.pre.name + '_' + self.post.name + '_' + self.target+'.csv'

        with open(filename, mode='w') as w_file:

            for dendrite in self.dendrites:
                rank_iter = iter(dendrite.rank)
                w_iter = iter(dendrite.w)
                delay_iter = iter(dendrite.delay)
                post_rank = dendrite.post_rank

                for i in xrange(dendrite.size):
                    w_file.write(str(next(rank_iter))+', '+
                                 str(post_rank)+', '+
                                 str(next(w_iter))+', '+
                                 str(next(delay_iter))+'\n'
                                 )

    def _comp_dist(self, pre, post):
        """
        Compute euclidean distance between two coordinates.
        """
        res = 0.0

        for i in range(len(pre)):
            res = res + (pre[i]-post[i])*(pre[i]-post[i]);

        return res


    def receptive_fields(self, variable = 'w', in_post_geometry = True):
        """
        Gathers all receptive fields within this projection.

        *Parameters*:

        * **variable**: name of variable
        * **in_post_geometry**: if set to false, the data will be plotted as square grid. (default = True)
        """
        if in_post_geometry:
            x_size = self.post.geometry[1]
            y_size = self.post.geometry[0]
        else:
            x_size = int( math.floor(math.sqrt(self.post.size)) )
            y_size = int( math.ceil(math.sqrt(self.post.size)) )


        def get_rf(rank): # TODO: IMPROVE
            res = np.zeros( self.pre.size )
            for n in xrange(len(self.post_ranks)):
                if self.post_ranks[n] == n:
                    pre_ranks = self.cyInstance.pre_rank(n)
                    data = getattr(self.cyInstance, 'get_dendrite_'+variable)(rank)
                    for j in xrange(len(pre_ranks)):
                        res[pre_ranks[j]] = data[j]
            return res.reshape(self.pre.geometry)

        res = np.zeros((1, x_size*self.pre.geometry[1]))
        for y in xrange ( y_size ):
            row = np.concatenate(  [ get_rf(self.post.rank_from_coordinates( (y, x) ) ) for x in range ( x_size ) ], axis = 1)
            res = np.concatenate((res, row))

        return res

    def connectivity_matrix(self, fill=0.0):
        """
        Returns a dense connectivity matrix (2D Numpy array) representing the connections between the pre- and post-populations.

        The first index of the matrix represents post-synaptic neurons, the second the pre-synaptic ones.

        If PopulationViews were used for creating the projection, the matrix is expanded to the whole populations by default.

        *Parameters*:

        * **fill**: value to put in the matrix when there is no connection (default: 0.0).
        """
        if isinstance(self.pre, PopulationView):
            size_pre = self.pre.population.size
        else:
            size_pre = self.pre.size
        if isinstance(self.post, PopulationView):
            size_post = self.post.population.size
        else:
            size_post = self.post.size

        res = np.ones((size_post, size_pre)) * fill
        for rank in self.post_ranks:
            idx = self.post_ranks.index(rank)
            try:
                preranks = self.cyInstance.pre_rank(idx)
                w = self.cyInstance.get_dendrite_w(idx)
            except:
                Global._error('The connectivity matrix can only be accessed after compilation')
                return []
            res[rank, preranks] = w
        return res


    ################################
    ## Save/load methods
    ################################

    def _data(self):
        "Method gathering all info about the projection when calling save()"

        if not self.initialized:
            Global._error('save_connectivity(): the network has not been compiled yet.')
            return {}

        desc = {}
        desc['name'] = self.name
        desc['post_ranks'] = self.post_ranks
        desc['attributes'] = self.attributes
        desc['parameters'] = self.parameters
        desc['variables'] = self.variables
        desc['pre_ranks'] = self.cyInstance.pre_rank_all()

        # Attributes to save
        attributes = self.attributes
        if not 'w' in self.attributes:
            attributes.append('w')

        # Save all attributes
        for var in attributes:
            try:
                desc[var] = getattr(self.cyInstance, 'get_'+var)()
            except Exception as e:
                Global._error('Can not save the attribute ' + var + ' in the projection.')

        return desc


        # synapse_count = []
        # dendrites = []

        # for d in self.post_ranks:
        #     dendrite_desc = {}
        #     # Number of synapses in the dendrite
        #     synapse_count.append(self.dendrite(d).size)
        #     # Postsynaptic rank
        #     dendrite_desc['post_rank'] = d
        #     # Number of synapses
        #     dendrite_desc['size'] = self.cyInstance.nb_synapses(d)
        #     # Attributes
        #     attributes = self.attributes
        #     if not 'w' in self.attributes:
        #         attributes.append('w')
        #     # Save all attributes
        #     for var in attributes:
        #         try:
        #             dendrite_desc[var] = getattr(self.cyInstance, 'get_dendrite_'+var)(d)
        #         except Exception as e:
        #             Global._error('Can not save the attribute ' + var + ' in the projection.')
        #     # Add pre-synaptic ranks and delays
        #     dendrite_desc['rank'] = self.cyInstance.pre_rank(d)
        #     if hasattr(self.cyInstance, 'get_delay'):
        #         dendrite_desc['delay'] = self.cyInstance.get_delay()
        #     # Finish
        #     dendrites.append(dendrite_desc)

        # desc['dendrites'] = dendrites
        # desc['number_of_synapses'] = synapse_count

        # return desc

    def save(self, filename):
        """
        Saves all information about the projection (connectivity, current value of parameters and variables) into a file.

        * If the extension is '.mat', the data will be saved as a Matlab 7.2 file. Scipy must be installed.

        * If the extension ends with '.gz', the data will be pickled into a binary file and compressed using gzip.

        * Otherwise, the data will be pickled into a simple binary text file using pickle.

        *Parameter*:

        * **filename**: filename, may contain relative or absolute path.

            .. warning::

                The '.mat' data will not be loadable by ANNarchy, it is only for external analysis purpose.

        Example::

            proj.save('pop1.txt')

        """
        from ANNarchy.core.IO import _save_data
        _save_data(filename, self._data())


    def load(self, filename):
        """
        Loads the saved state of the projection.

        Warning: Matlab data can not be loaded.

        *Parameters*:

        * **filename**: the filename with relative or absolute path.

        Example::

            proj.load('pop1.txt')

        """
        from ANNarchy.core.IO import _load_data, _load_proj_data
        _load_proj_data(self, _load_data(filename))


    ################################
    ## Structural plasticity
    ################################
    def start_pruning(self, period=None):
        """
        Starts pruning the synapses in the projection if the synapse defines a 'pruning' argument.

        'structural_plasticity' must be set to True in setup().

        *Parameters*:

        * **period**: how often pruning should be evaluated (default: dt, i.e. each step)
        """
        if not period:
            period = Global.config['dt']
        if not self.cyInstance:
            Global._error('Can not start pruning if the network is not compiled.')
            exit(0)
        if Global.config['structural_plasticity']:
            try:
                self.cyInstance.start_pruning(int(period/Global.config['dt']), Global.get_current_step())
            except :
                Global._error("The synapse does not define a 'pruning' argument.")
                exit(0)
        else:
            Global._error("You must set 'structural_plasticity' to True in setup() to start pruning connections.")
            exit(0)

    def stop_pruning(self):
        """
        Stops pruning the synapses in the projection if the synapse defines a 'pruning' argument.

        'structural_plasticity' must be set to True in setup().
        """
        if not self.cyInstance:
            Global._error('Can not stop pruning if the network is not compiled.')
            exit(0)
        if Global.config['structural_plasticity']:
            try:
                self.cyInstance.stop_pruning()
            except:
                Global._error("The synapse does not define a 'pruning' argument.")
                exit(0)
        else:
            Global._error("You must set 'structural_plasticity' to True in setup() to start pruning connections.")
            exit(0)

    def start_creating(self, period=None):
        """
        Starts creating the synapses in the projection if the synapse defines a 'creating' argument.

        'structural_plasticity' must be set to True in setup().

        *Parameters*:

        * **period**: how often creating should be evaluated (default: dt, i.e. each step)
        """
        if not period:
            period = Global.config['dt']
        if not self.cyInstance:
            Global._error('Can not start creating if the network is not compiled.')
            exit(0)
        if Global.config['structural_plasticity']:
            try:
                self.cyInstance.start_creating(int(period/Global.config['dt']), Global.get_current_step())
            except:
                Global._error("The synapse does not define a 'creating' argument.")
                exit(0)
        else:
            Global._error("You must set 'structural_plasticity' to True in setup() to start creating connections.")
            exit(0)

    def stop_creating(self):
        """
        Stops creating the synapses in the projection if the synapse defines a 'creating' argument.

        'structural_plasticity' must be set to True in setup().
        """
        if not self.cyInstance:
            Global._error('Can not stop creating if the network is not compiled.')
            exit(0)
        if Global.config['structural_plasticity']:
            try:
                self.cyInstance.stop_creating()
            except:
                Global._error("The synapse does not define a 'creating' argument.")
                exit(0)
        else:
            Global._error("You must set 'structural_plasticity' to True in setup() to start creating connections.")
            exit(0)
Пример #3
0
class Projection(object):
    """
    Represents all synapses of the same type between two populations.
    """

    def __init__(self, pre, post, target, synapse=None, name=None):
        """
        *Parameters*:

            * **pre**: pre-synaptic population (either its name or a ``Population`` object).
            * **post**: post-synaptic population (either its name or a ``Population`` object).
            * **target**: type of the connection.
            * **synapse**: a ``Synapse`` instance.
            * **name**: unique name of the projection (optional).

        By default, the synapse only ensures linear synaptic transmission:

        * For rate-coded populations: ``psp = w * pre.r``
        * For spiking populations: ``g_target += w``

        """

        # Store the pre and post synaptic populations
        # the user provide either a string or a population object
        # in case of string, we need to search for the corresponding object
        if isinstance(pre, str):
            for pop in Global._network[0]['populations']:
                if pop.name == pre:
                    self.pre = pop
        else:
            self.pre = pre

        if isinstance(post, str):
            for pop in Global._network[0]['populations']:
                if pop.name == post:
                    self.post = pop
        else:
            self.post = post

        # Store the arguments
        self.target = target

        # Add the target to the postsynaptic population
        self.post.targets.append(self.target)

        # check if a synapse description is attached
        if not synapse:
            # No synapse attached assume default synapse based on
            # presynaptic population.
            if self.pre.neuron_type.type == 'rate':
                from ANNarchy.models.Synapses import DefaultRateCodedSynapse
                self.synapse_type = DefaultRateCodedSynapse()
                self.synapse_type.type = 'rate'
            else:
                from ANNarchy.models.Synapses import DefaultSpikingSynapse
                self.synapse_type = DefaultSpikingSynapse()
                self.synapse_type.type = 'spike'

        elif inspect.isclass(synapse):
            self.synapse_type = synapse()
            self.synapse_type.type = self.pre.neuron_type.type
        else:
            self.synapse_type = copy.deepcopy(synapse)
            self.synapse_type.type = self.pre.neuron_type.type

        # Analyse the parameters and variables
        self.synapse_type._analyse()

        # Create a default name
        self.id = len(Global._network[0]['projections'])
        if name:
            self.name = name
        else:
            self.name = 'proj'+str(self.id)

        # Get a list of parameters and variables
        self.parameters = []
        self.init = {}
        for param in self.synapse_type.description['parameters']:
            self.parameters.append(param['name'])
            self.init[param['name']] = param['init']

        self.variables = []
        for var in self.synapse_type.description['variables']:
            self.variables.append(var['name'])
            self.init[var['name']] = var['init']

        self.attributes = self.parameters + self.variables

        # Add the population to the global network
        Global._network[0]['projections'].append(self)

        # Finalize initialization
        self.initialized = False

        # Cython instance
        self.cyInstance = None

        # Connectivity
        self._synapses = None
        self._connection_method = None
        self._connection_args = None
        self._connection_delay = None
        self._connector = None

        # If a single weight value is used
        self._single_constant_weight = False

        # If a dense matrix should be used instead of LIL
        self._dense_matrix = False

        # Recorded variables
        self.recorded_variables = {}

        # Reporting
        self.connector_name = "Specific"
        self.connector_description = "Specific"

        # Overwritten by derived classes, to add
        # additional code
        self._specific_template = {}

        # To allow case-specific adjustment of parallelization
        # parameters, e. g. openMP schedule, we introduce a
        # dictionary read by the ProjectionGenerator.
        #
        # Will be overwritten either by inherited classes or
        # by an omp_config provided to the compile() method.
        self._omp_config = {
            #'psp_schedule': 'schedule(dynamic)'
        }

    # Add defined connectors
    connect_one_to_one = ConnectorMethods.connect_one_to_one
    connect_all_to_all = ConnectorMethods.connect_all_to_all
    connect_gaussian = ConnectorMethods.connect_gaussian
    connect_dog = ConnectorMethods.connect_dog
    connect_fixed_probability = ConnectorMethods.connect_fixed_probability
    connect_fixed_number_pre = ConnectorMethods.connect_fixed_number_pre
    connect_fixed_number_post = ConnectorMethods.connect_fixed_number_post
    connect_with_func = ConnectorMethods.connect_with_func
    connect_from_matrix = ConnectorMethods.connect_from_matrix
    _load_from_matrix = ConnectorMethods._load_from_matrix
    connect_from_sparse = ConnectorMethods.connect_from_sparse
    _load_from_sparse = ConnectorMethods._load_from_sparse
    connect_from_file = ConnectorMethods.connect_from_file
    _load_from_csr = ConnectorMethods._load_from_csr

    def _generate(self):
        "Overriden by specific projections to generate the code"
        pass

    def _instantiate(self, module):
        "Instantiates the projection after compilation."
        self._connect(module)
        self.initialized = True

    def _init_attributes(self):
        """
        Method used after compilation to initialize the attributes. Called by Generator._instantiate
        """
        for name, val in self.init.items():
            if not name in ['w']:
                self.__setattr__(name, val)

    def _connect(self, module):
        """
        Builds up dendrites either from list or dictionary. Called by instantiate().
        """
        if not self._connection_method:
            Global._error('The projection between ' + self.pre.name + ' and ' + self.post.name + ' is declared but not connected.')

        proj = getattr(module, 'proj'+str(self.id)+'_wrapper')
        self.cyInstance = proj(self._connection_method(*((self.pre, self.post,) + self._connection_args)))

        # Access the list of postsynaptic neurons
        self.post_ranks = self.cyInstance.post_rank()

    def _store_connectivity(self, method, args, delay):

        self._connection_method = method
        self._connection_args = args
        self._connection_delay = delay

        # Analyse the delay
        if isinstance(delay, (int, float)): # Uniform delay
            self.max_delay = round(delay/Global.config['dt'])
            self.uniform_delay = round(delay/Global.config['dt'])
        elif isinstance(delay, RandomDistribution): # Non-uniform delay
            self.uniform_delay = -1
            # Ensure no negative delays are generated
            if delay.min is None or delay.min < Global.config['dt']:
                delay.min = Global.config['dt']
            # The user needs to provide a max in order to compute max_delay
            if delay.max is None:
                Global._error('Projection.connect_xxx(): if you use a non-bounded random distribution for the delays (e.g. Normal), you need to set the max argument to limit the maximal delay.')

            self.max_delay = int(delay.max/Global.config['dt'])
        elif isinstance(delay, (list, np.ndarray)): # connect_from_matrix/sparse
            if len(delay) > 0:
                self.uniform_delay = -1
                self.max_delay = round(max([max(l) for l in delay])/Global.config['dt'])
            else: # list is empty, no delay
                self.max_delay = -1
                self.uniform_delay = -1
        else:
            Global._error('Projection.connect_xxx(): delays are not valid!')


        # Transmit the max delay to the pre pop
        if isinstance(self.pre, PopulationView):
            self.pre.population.max_delay = max(self.max_delay, self.pre.population.max_delay)
        else:
            self.pre.max_delay = max(self.max_delay, self.pre.max_delay)


    def _has_single_weight(self):
        "If a single weight should be generated instead of a LIL"
        return self._single_constant_weight and not Global.config['structural_plasticity'] and not self.synapse_type.description['plasticity'] and Global.config['paradigm']=="openmp"


    def reset(self, attributes=-1, synapses=False):
        """
        Resets all parameters and variables of the projection to the value they had before the call to compile.

        *Parameters:*

        * **attributes**: list of attributes (parameter or variable) which should be reinitialized. Default: all attributes.

        .. note::

            Only parameters and variables are reinitialized, not the connectivity structure (including the weights and delays).

            The parameter ``synapses`` will be used in a future release to also reinitialize the connectivity structure.

        """
        if attributes == -1:
            attributes = self.attributes

        for var in attributes:
            # Skip w
            if var=='w':
                continue
            # check it exists
            if not var in self.attributes:
                _warning("Projection.reset():", var, "is not an attribute of the population, won't reset.")
                continue
            # Set the value
            try:
                self.__setattr__(var, self.init[var])
            except Exception as e:
                _print(e)
                _warning("Projection.reset(): something went wrong while resetting", var)
        #Global._warning('Projection.reset(): only parameters and variables are reinitialized, not the connectivity structure (including the weights)...')

    ################################
    ## Dendrite access
    ################################
    @property
    def size(self):
        "Number of post-synaptic neurons receiving synapses."
        if self.cyInstance:
            return len(self.post_ranks)
        else:
            return 0

    def __len__(self):
        " Number of postsynaptic neurons receiving synapses in this projection."
        return self.size

    @property
    def nb_synapses(self):
        "Total number of synapses in the projection."
        return sum([self.cyInstance.nb_synapses(n) for n in range(self.size)])


    @property
    def dendrites(self):
        """
        Iteratively returns the dendrites corresponding to this projection.
        """
        for idx, n in enumerate(self.post_ranks):
            yield Dendrite(self, n, idx)

    def dendrite(self, post):
        """
        Returns the dendrite of a postsynaptic neuron according to its rank.

        *Parameters*:

            * **post**: can be either the rank or the coordinates of the postsynaptic neuron
        """
        if not self.initialized:
            Global._error('dendrites can only be accessed after compilation.')

        if isinstance(post, int):
            rank = post
        else:
            rank = self.post.rank_from_coordinates(post)

        if rank in self.post_ranks:
            return Dendrite(self, rank, self.post_ranks.index(rank))
        else:
            Global._error(" The neuron of rank "+ str(rank) + " has no dendrite in this projection.", exit=True)


    def synapse(self, pre, post):
        """
        Returns the synapse between a pre- and a post-synaptic neuron if it exists, None otherwise.

        *Parameters*:

            * **pre**: rank of the pre-synaptic neuron.
            * **post**: rank of the post-synaptic neuron.
        """
        if not isinstance(pre, int) or not isinstance(post, int):
            Global._error('Projection.synapse() only accepts ranks for the pre and post neurons.')

        return self.dendrite(post).synapse(pre)


    # Iterators
    def __getitem__(self, *args, **kwds):
        """ Returns dendrite of the given position in the postsynaptic population.

        If only one argument is given, it is a rank. If it is a tuple, it is coordinates.
        """
        if len(args) == 1:
            return self.dendrite(args[0])
        return self.dendrite(args)

    def __iter__(self):
        " Returns iteratively each dendrite in the population in ascending postsynaptic rank order."
        for idx, n in enumerate(self.post_ranks):
            yield Dendrite(self, n, idx)

    ################################
    ## Access to attributes
    ################################

    @property
    def delay(self):
        if not hasattr(self.cyInstance, 'get_delay'):
            if self.max_delay <= 1 :
                return Global.config['dt']
            elif self.uniform_delay != -1:
                return self.uniform_delay * Global.config['dt']
        else:
            return [[pre * Global.config['dt'] for pre in post] for post in self.cyInstance.get_delay()]


    def get(self, name):
        """
        Returns a list of parameters/variables values for each dendrite in the projection.

        The list will have the same length as the number of actual dendrites (self.size), so it can be smaller than the size of the postsynaptic population. Use self.post_ranks to indice it.

        *Parameters*:

        * **name**: the name of the parameter or variable
        """
        return self.__getattr__(name)

    def set(self, value):
        """ Sets the parameters/variables values for each dendrite in the projection.

        For parameters, you can provide:

            * a single value, which will be the same for all dendrites.

            * a list or 1D numpy array of the same length as the number of actual dendrites (self.size).

        For variables, you can provide:

            * a single value, which will be the same for all synapses of all dendrites.

            * a list or 1D numpy array of the same length as the number of actual dendrites (self.size). The synapses of each postsynaptic neuron will take the same value.

        .. warning::

            It not possible to set different values to each synapse using this method. One should iterate over the dendrites::

                for dendrite in proj.dendrites:
                    dendrite.w = np.ones(dendrite.size)

        *Parameters*:

        * **value**: a dictionary with the name of the parameter/variable as key.

        """

        for name, val in value:
            self.__setattr__(name, val)

    def __getattr__(self, name):
        " Method called when accessing an attribute."
        if name == 'initialized' or not hasattr(self, 'initialized'): # Before the end of the constructor
            return object.__getattribute__(self, name)
        elif hasattr(self, 'attributes'):
            if name in ['plasticity', 'transmission', 'update']:
                return self._get_flag(name)
            if name in self.attributes:
                if not self.initialized:
                    return self.init[name]
                else:
                    return self._get_cython_attribute( name )
            else:
                return object.__getattribute__(self, name)
        return object.__getattribute__(self, name)

    def __setattr__(self, name, value):
        " Method called when setting an attribute."
        if name == 'initialized' or not hasattr(self, 'initialized'): # Before the end of the constructor
            object.__setattr__(self, name, value)
        elif hasattr(self, 'attributes'):
            if name in ['plasticity', 'transmission', 'update']:
                self._set_flag(name, bool(value))
                return
            if name in self.attributes:
                if not self.initialized:
                    self.init[name] = value
                else:
                    self._set_cython_attribute(name, value)
            else:
                object.__setattr__(self, name, value)
        else:
            object.__setattr__(self, name, value)

    def _get_cython_attribute(self, attribute):
        """
        Returns the value of the given attribute for all neurons in the population,
        as a list of lists having the same geometry as the population if it is local.

        Parameter:

        * *attribute*: should be a string representing the variables's name.

        """
        return getattr(self.cyInstance, 'get_'+attribute)()

    def _set_cython_attribute(self, attribute, value):
        """
        Sets the value of the given attribute for all post-synaptic neurons in the projection,
        as a NumPy array having the same geometry as the population if it is local.

        Parameter:

        * *attribute*: should be a string representing the variables's name.

        """
        # Convert np.arrays into lists for better iteration
        if isinstance(value, np.ndarray):
            value = list(value)
        # A list is given
        if isinstance(value, list):
            if len(value) == len(self.post_ranks):
                if attribute in self.synapse_type.description['local']:
                    for idx, n in enumerate(self.post_ranks):
                        if not len(value[idx]) == self.cyInstance.nb_synapses(idx):
                            Global._error('The postynaptic neuron ' + str(n) + ' receives '+ str(self.cyInstance.nb_synapses(idx))+ ' synapses.')
                        getattr(self.cyInstance, 'set_dendrite_'+attribute)(idx, value[idx])
                else:
                    getattr(self.cyInstance, 'set_'+attribute)(value)
            else:
                Global._error('The projection has ' + self.size + ' post-synaptic neurons.')
        # A Random Distribution is given
        elif isinstance(value, RandomDistribution):
            if attribute in self.synapse_type.description['local']:
                for idx, n in enumerate(self.post_ranks):
                    getattr(self.cyInstance, 'set_dendrite_'+attribute)(idx, value.get_values(self.cyInstance.nb_synapses(idx)))
            else:
                getattr(self.cyInstance, 'set_'+attribute)(value.get_values(len(self.post_ranks)))
        # A single value is given
        else:
            if attribute in self.synapse_type.description['local']:
                for idx, n in enumerate(self.post_ranks):
                    getattr(self.cyInstance, 'set_dendrite_'+attribute)(idx, value*np.ones(self.cyInstance.nb_synapses(idx)))
            else:
                getattr(self.cyInstance, 'set_'+attribute)(value*np.ones(len(self.post_ranks)))

    def _get_flag(self, attribute):
        "flags such as learning, transmission"
        return getattr(self.cyInstance, '_get_'+attribute)()

    def _set_flag(self, attribute, value):
        "flags such as learning, transmission"
        getattr(self.cyInstance, '_set_'+attribute)(value)

    ################################
    ## Variable flags
    ################################
    def set_variable_flags(self, name, value):
        """ Sets the flags of a variable for the projection.

        If the variable ``r`` is defined in the Synapse description through:

            w = pre.r * post.r : max=1.0

        one can change its maximum value with:

            proj.set_variable_flags('w', {'max': 2.0})

        For valued flags (init, min, max), ``value`` must be a dictionary containing the flag as key ('init', 'min', 'max') and its value.

        For positional flags (postsynaptic, implicit), the value in the dictionary must be set to the empty string '':

            proj.set_variable_flags('w', {'implicit': ''})

        A None value in the dictionary deletes the corresponding flag:

            proj.set_variable_flags('w', {'max': None})


        *Parameters*:

        * **name**: the name of the variable.

        * **value**: a dictionary containing the flags.

        """
        rk_var = self._find_variable_index(name)
        if rk_var == -1:
            Global._error('The projection '+self.name+' has no variable called ' + name)
            return

        for key, val in value.items():
            if val == '': # a flag
                try:
                    self.synapse_type.description['variables'][rk_var]['flags'].index(key)
                except: # the flag does not exist yet, we can add it
                    self.synapse_type.description['variables'][rk_var]['flags'].append(key)
            elif val is None: # delete the flag
                try:
                    self.synapse_type.description['variables'][rk_var]['flags'].remove(key)
                except: # the flag did not exist, check if it is a bound
                    if has_key(self.synapse_type.description['variables'][rk_var]['bounds'], key):
                        self.synapse_type.description['variables'][rk_var]['bounds'].pop(key)
            else: # new value for init, min, max...
                if key == 'init':
                    self.synapse_type.description['variables'][rk_var]['init'] = val
                    self.init[name] = val
                else:
                    self.synapse_type.description['variables'][rk_var]['bounds'][key] = val



    def set_variable_equation(self, name, equation):
        """ Changes the equation of a variable for the projection.

        If the variable ``w`` is defined in the Synapse description through:

            eta * dw/dt = pre.r * post.r

        one can change the equation with:

            proj.set_variable_equation('w', 'eta * dw/dt = pre.r * (post.r - 0.1) ')

        Only the equation should be provided, the flags have to be changed with ``set_variable_flags()``.

        .. warning::

            This method should be used with great care, it is advised to define another Synapse object instead.

        *Parameters*:

        * **name**: the name of the variable.

        * **equation**: the new equation as string.
        """
        rk_var = self._find_variable_index(name)
        if rk_var == -1:
            Global._error('The projection '+self.name+' has no variable called ' + name)
            return

        self.synapse_type.description['variables'][rk_var]['eq'] = equation


    def _find_variable_index(self, name):
        " Returns the index of the variable name in self.synapse_type.description['variables']"
        for idx in range(len(self.synapse_type.description['variables'])):
            if self.synapse_type.description['variables'][idx]['name'] == name:
                return idx
        return -1

    ################################
    ## Learning flags
    ################################
    def enable_learning(self, period=None, offset=None):
        """
        Enables learning for all the synapses of this projection.

        *Parameters*:

        * **period** determines how often the synaptic variables will be updated.
        * **offset** determines the offset at which the synaptic variables will be updated relative to the current time.

        For example, providing the following parameters at time 10 ms::

            enable_learning(period=10., offset=5.)

        would call the updating methods at times 15, 25, 35, etc...

        The default behaviour is that the synaptic variables are updated at each time step. The parameters must be multiple of ``dt``
        """
        # Check arguments
        if not period is None and not offset is None:
            if offset >= period:
                Global._error('enable_learning(): the offset must be smaller than the period.')

        if period is None and not offset is None:
            Global._error('enable_learning(): if you define an offset, you have to define a period.')

        try:
            self.cyInstance._set_update(True)
            self.cyInstance._set_plasticity(True)
            if period != None:
                self.cyInstance._set_update_period(int(period/Global.config['dt']))
            else:
                self.cyInstance._set_update_period(int(1))
                period = Global.config['dt']
            if offset != None:
                relative_offset = Global.get_time() % period + offset
                self.cyInstance._set_update_offset(int(relative_offset%period))
            else:
                self.cyInstance._set_update_offset(int(0))
        except:
            Global._warning('Enable_learning() is only possible after compile()')

    def disable_learning(self, update=None):
        """
        Disables learning for all synapses of this projection.

        The effect depends on the rate-coded or spiking nature of the projection:

        * **Rate-coded**: the updating of all synaptic variables is disabled (including the weights ``w``). This is equivalent to ``proj.update = False``.

        * **Spiking**: the updating of the weights ``w`` is disabled, but all other variables are updated. This is equivalent to ``proj.plasticity = False``.

        This method is useful when performing some tests on a trained network without messing with the learned weights.
        """
        try:
            if self.synapse_type.type == 'rate':
                self.cyInstance._set_update(False)
            else:
                self.cyInstance._set_plasticity(False)
        except Exception as e:
            Global._warning('disabling learning is only possible after compile().')


    ################################
    ## Methods on connectivity matrix
    ################################

    def save_connectivity(self, filename):
        """
        Saves the projection pattern in a file.

        Only the connectivity matrix, the weights and delays are saved, not the other synaptic variables.

        The generated data should be used to create a projection in another network::

            proj.connect_from_file(filename)

        *Parameters*:

        * **filename**: file where the data will be saved.
        """
        if not self.initialized:
            Global._error('save_connectivity(): the network has not been compiled yet.')
            return

        data = {
                'name': self.name,
                'post_ranks': self.post_ranks,
                'pre_ranks': self.cyInstance.pre_rank_all(), # was: [self.cyInstance.pre_rank(n) for n in range(self.size)],
                'w': self.cyInstance.get_w(),
                'delay': self.cyInstance.get_delay() if hasattr(self.cyInstance, 'get_delay') else None,
                'max_delay': self.max_delay,
                'uniform_delay': self.uniform_delay,
                'size': self.size,
                'nb_synapses': sum([self.cyInstance.nb_synapses(n) for n in range(self.size)])
            }
        try:
            import cPickle as pickle # Python2
        except:
            import pickle # Python3
        with open(filename, 'wb') as wfile:
            pickle.dump(data, wfile, protocol=pickle.HIGHEST_PROTOCOL)

    def _save_connectivity_as_csv(self):
        """
        Saves the projection pattern in the csv format.

        Please note, that only the pure connectivity data pre_rank, post_rank, w and delay are stored.
        """
        filename = self.pre.name + '_' + self.post.name + '_' + self.target+'.csv'

        with open(filename, mode='w') as w_file:

            for dendrite in self.dendrites:
                rank_iter = iter(dendrite.rank)
                w_iter = iter(dendrite.w)
                delay_iter = iter(dendrite.delay)
                post_rank = dendrite.post_rank

                for i in xrange(dendrite.size):
                    w_file.write(str(next(rank_iter))+', '+
                                 str(post_rank)+', '+
                                 str(next(w_iter))+', '+
                                 str(next(delay_iter))+'\n'
                                 )

    def _comp_dist(self, pre, post):
        """
        Compute euclidean distance between two coordinates.
        """
        res = 0.0

        for i in range(len(pre)):
            res = res + (pre[i]-post[i])*(pre[i]-post[i]);

        return res


    def receptive_fields(self, variable = 'w', in_post_geometry = True):
        """
        Gathers all receptive fields within this projection.

        *Parameters*:

        * **variable**: name of variable
        * **in_post_geometry**: if set to false, the data will be plotted as square grid. (default = True)
        """
        if in_post_geometry:
            x_size = self.post.geometry[1]
            y_size = self.post.geometry[0]
        else:
            x_size = int( math.floor(math.sqrt(self.post.size)) )
            y_size = int( math.ceil(math.sqrt(self.post.size)) )


        def get_rf(rank): # TODO: IMPROVE
            res = np.zeros( self.pre.size )
            for n in xrange(len(self.post_ranks)):
                if self.post_ranks[n] == n:
                    pre_ranks = self.cyInstance.pre_rank(n)
                    data = getattr(self.cyInstance, 'get_dendrite_'+variable)(rank)
                    for j in xrange(len(pre_ranks)):
                        res[pre_ranks[j]] = data[j]
            return res.reshape(self.pre.geometry)

        res = np.zeros((1, x_size*self.pre.geometry[1]))
        for y in xrange ( y_size ):
            row = np.concatenate(  [ get_rf(self.post.rank_from_coordinates( (y, x) ) ) for x in range ( x_size ) ], axis = 1)
            res = np.concatenate((res, row))

        return res

    def connectivity_matrix(self, fill=0.0):
        """
        Returns a dense connectivity matrix (2D Numpy array) representing the connections between the pre- and post-populations.

        The first index of the matrix represents post-synaptic neurons, the second the pre-synaptic ones.

        If PopulationViews were used for creating the projection, the matrix is expanded to the whole populations by default.

        *Parameters*:

        * **fill**: value to put in the matrix when there is no connection (default: 0.0).
        """
        if isinstance(self.pre, PopulationView):
            size_pre = self.pre.population.size
        else:
            size_pre = self.pre.size
        if isinstance(self.post, PopulationView):
            size_post = self.post.population.size
        else:
            size_post = self.post.size

        res = np.ones((size_post, size_pre)) * fill
        for rank in self.post_ranks:
            idx = self.post_ranks.index(rank)
            try:
                preranks = self.cyInstance.pre_rank(idx)
                w = self.cyInstance.get_dendrite_w(idx)
            except:
                Global._error('The connectivity matrix can only be accessed after compilation')
                return []
            res[rank, preranks] = w
        return res


    ################################
    ## Save/load methods
    ################################

    def _data(self):
        "Method gathering all info about the projection when calling save()"

        if not self.initialized:
            Global._error('save_connectivity(): the network has not been compiled yet.')

        desc = {}
        desc['name'] = self.name
        desc['pre'] = self.pre.name
        desc['post'] = self.post.name
        desc['target'] = self.target
        desc['post_ranks'] = self.post_ranks
        desc['attributes'] = self.attributes
        desc['parameters'] = self.parameters
        desc['variables'] = self.variables
        desc['pre_ranks'] = self.cyInstance.pre_rank_all()

        # Attributes to save
        attributes = self.attributes
        if not 'w' in self.attributes:
            attributes.append('w')

        # Save all attributes
        for var in attributes:
            try:
                desc[var] = getattr(self.cyInstance, 'get_'+var)()
            except Exception as e:
                Global._warning('Can not save the attribute ' + var + ' in the projection.')

        return desc


        # synapse_count = []
        # dendrites = []

        # for d in self.post_ranks:
        #     dendrite_desc = {}
        #     # Number of synapses in the dendrite
        #     synapse_count.append(self.dendrite(d).size)
        #     # Postsynaptic rank
        #     dendrite_desc['post_rank'] = d
        #     # Number of synapses
        #     dendrite_desc['size'] = self.cyInstance.nb_synapses(d)
        #     # Attributes
        #     attributes = self.attributes
        #     if not 'w' in self.attributes:
        #         attributes.append('w')
        #     # Save all attributes
        #     for var in attributes:
        #         try:
        #             dendrite_desc[var] = getattr(self.cyInstance, 'get_dendrite_'+var)(d)
        #         except Exception as e:
        #             Global._error('Can not save the attribute ' + var + ' in the projection.')
        #     # Add pre-synaptic ranks and delays
        #     dendrite_desc['rank'] = self.cyInstance.pre_rank(d)
        #     if hasattr(self.cyInstance, 'get_delay'):
        #         dendrite_desc['delay'] = self.cyInstance.get_delay()
        #     # Finish
        #     dendrites.append(dendrite_desc)

        # desc['dendrites'] = dendrites
        # desc['number_of_synapses'] = synapse_count

        # return desc

    def save(self, filename):
        """
        Saves all information about the projection (connectivity, current value of parameters and variables) into a file.

        * If the extension is '.mat', the data will be saved as a Matlab 7.2 file. Scipy must be installed.

        * If the extension ends with '.gz', the data will be pickled into a binary file and compressed using gzip.

        * Otherwise, the data will be pickled into a simple binary text file using pickle.

        *Parameter*:

        * **filename**: filename, may contain relative or absolute path.

            .. warning::

                The '.mat' data will not be loadable by ANNarchy, it is only for external analysis purpose.

        Example::

            proj.save('proj1.txt')

        """
        from ANNarchy.core.IO import _save_data
        _save_data(filename, self._data())


    def load(self, filename):
        """
        Loads the saved state of the projection.

        Warning: Matlab data can not be loaded.

        *Parameters*:

        * **filename**: the filename with relative or absolute path.

        Example::

            proj.load('proj1.txt')

        """
        from ANNarchy.core.IO import _load_data, _load_proj_data
        _load_proj_data(self, _load_data(filename))


    ################################
    ## Structural plasticity
    ################################
    def start_pruning(self, period=None):
        """
        Starts pruning the synapses in the projection if the synapse defines a 'pruning' argument.

        'structural_plasticity' must be set to True in setup().

        *Parameters*:

        * **period**: how often pruning should be evaluated (default: dt, i.e. each step)
        """
        if not period:
            period = Global.config['dt']
        if not self.cyInstance:
            Global._error('Can not start pruning if the network is not compiled.')

        if Global.config['structural_plasticity']:
            try:
                self.cyInstance.start_pruning(int(period/Global.config['dt']), Global.get_current_step())
            except :
                Global._error("The synapse does not define a 'pruning' argument.")

        else:
            Global._error("You must set 'structural_plasticity' to True in setup() to start pruning connections.")


    def stop_pruning(self):
        """
        Stops pruning the synapses in the projection if the synapse defines a 'pruning' argument.

        'structural_plasticity' must be set to True in setup().
        """
        if not self.cyInstance:
            Global._error('Can not stop pruning if the network is not compiled.')

        if Global.config['structural_plasticity']:
            try:
                self.cyInstance.stop_pruning()
            except:
                Global._error("The synapse does not define a 'pruning' argument.")

        else:
            Global._error("You must set 'structural_plasticity' to True in setup() to start pruning connections.")


    def start_creating(self, period=None):
        """
        Starts creating the synapses in the projection if the synapse defines a 'creating' argument.

        'structural_plasticity' must be set to True in setup().

        *Parameters*:

        * **period**: how often creating should be evaluated (default: dt, i.e. each step)
        """
        if not period:
            period = Global.config['dt']
        if not self.cyInstance:
            Global._error('Can not start creating if the network is not compiled.')

        if Global.config['structural_plasticity']:
            try:
                self.cyInstance.start_creating(int(period/Global.config['dt']), Global.get_current_step())
            except:
                Global._error("The synapse does not define a 'creating' argument.")

        else:
            Global._error("You must set 'structural_plasticity' to True in setup() to start creating connections.")


    def stop_creating(self):
        """
        Stops creating the synapses in the projection if the synapse defines a 'creating' argument.

        'structural_plasticity' must be set to True in setup().
        """
        if not self.cyInstance:
            Global._error('Can not stop creating if the network is not compiled.')

        if Global.config['structural_plasticity']:
            try:
                self.cyInstance.stop_creating()
            except:
                Global._error("The synapse does not define a 'creating' argument.")

        else:
            Global._error("You must set 'structural_plasticity' to True in setup() to start creating connections.")