<commit_before>"""
Sheets for simulating a moving eye.

This module provides two classes, ShiftingGeneratorSheet, and
SaccadeController, that can be used to simulate a moving eye,
controlled by topographic neural activity from structures like the
superior colliculus.

ShiftingGeneratorSheet is a subclass of GeneratorSheet that accepts a
saccade command on the 'Saccade' port in the form of a tuple:
(amplitude,direction), specified in degrees.  It shifts its sheet
bounds in response to this command, keeping the centroid of the bounds
within a prespecified boundingregion.

SaccadeController is a subclass of CFSheet that accepts CF projections
and decodes its resulting activity into a saccade command suitable for
controlling a ShiftingGeneratorSheet.


$Id$
"""
__version__ = '$Revision$'

from numpy import dot,sin,cos,pi,array,asarray,argmax,zeros
from numpy import nonzero,take,random,alltrue,sometrue

from topo.base.cf import CFSheet
from topo.base.simulation import PeriodicEventSequence,FunctionEvent
from topo.sheets.generatorsheet import SequenceGeneratorSheet
from topo.base.parameterclasses import Number,Magnitude,CallableParameter,BooleanParameter
from topo.base.boundingregion import BoundingBox,BoundingRegionParameter
from topo.misc import utils


# JPALERT: The next three functions (activity_centroid,
# activity_sample, and activity_mode) could actually apply to any
# Sheet.  Maybe they should be moved to topo.base.sheet?

def activity_centroid(sheet,activity=None,threshold=0.0):
    """
    Return the sheet coords of the (weighted) centroid of sheet activity.

    If the activity argument is not None, then it is used instead
    of sheet.activity.  If the sheet activity is all zero, the
    centroid of the sheet bounds is returned.
    """

    if activity is None:
        activity = sheet.activity

    ys = sheet.sheet_rows()
    xs = sheet.sheet_cols()

    xy = array([(x,y) for y in reversed(ys) for x in xs])
    a = activity.flat

    ## Optimization to only compute centroid from
    ## active (non-zero) units. 
    idxs = nonzero(a > threshold)[0]
    if not len(idxs):
        return sheet.bounds.centroid()
    return utils.centroid(take(xy,idxs,axis=0),take(a,idxs))


def activity_sample(sheet,activity=None):
    """
    Sample from the sheet activity as if it were a probability distribution.

    Returns the sheet coordinates of the sampled unit.  If
    activity is not None, it is used instead of sheet.activity.
    """

    if activity is None:
        activity = sheet.activity
    idx = utils.weighted_sample_idx(activity.ravel())
    r,c = utils.idx2rowcol(idx,activity.shape)

    return sheet.matrix2sheet(r,c)


def activity_mode(sheet,activity=None):
    """
    Returns the sheet coordinates of the mode (highest value) of
    the sheet activity.   
    """

    # JPHACKALERT:  The mode is computed using numpy.argmax, and
    # thus for distributions with multiple equal-valued modes, the
    # result will have a systematic bias toward higher x and lower
    # y values. (in that order).  Function may still be useful for
    # unimodal activity distributions, or sheets without limiting/squashing
    # output functions.
    if activity is None:
        activity = sheet.activity
    idx = argmax(activity.flat)
    r,c = utils.idx2rowcol(idx,activity.shape)
    return sheet.matrix2sheet(r,c)
      


class SaccadeController(CFSheet):
    """
    Sheet that decodes activity on CFProjections into a saccade command.
    
    This class accepts CF-projected input and computes its activity
    like a normal CFSheet, then decodes that activity into a saccade
    amplitude and direction as would be specified by activity in the
    superior colliculi.  The X dimension of activity corresponds to
    amplitude, the Y dimension to direction.  The activity is decoded
    to a single (x,y) point according to the parameter decode_method.

    From this (x,y) point an (amplitude,direction) pair, specified in
    degrees, is computed using the parameters amplitude_scale and
    direction scale.  That pair is then sent out on the 'Saccade'
    output port.

    NOTE: Non-linear mappings for saccade commands, as in Ottes, et
    al (below), are assumed to be provided using the coord_mapperg
    parameter of the incoming CFProjection.

    References:
    Ottes, <NAME>, Egglermont. 1986.  Visuomotor fields of the
    superior colliculus: a quantitative model.  Vision Research;
    26(6): 857-73.
    """

    amplitude_scale = Number(default=120,doc="""
        Scale factor for saccade command amplitude, expressed in
        degrees per unit of sheet.  Indicates how large a
        saccade is represented by the x-component of the command
        input.""")
    
    direction_scale = Number(default=180,doc="""
        Scale factor for saccade command direction, expressed in
        degrees per unit of sheet.  Indicates what direction of saccade
        is represented by the y-component of the command input.""")


    decode_fn = CallableParameter(default=activity_centroid,
                                  instantiate=False,doc="""
        The function for extracting a single point from sheet activity.
        Should take a sheet as the first argument, and return (x,y).""")



    src_ports = ['Activity','Saccade']


    def activate(self):
        super(SaccadeController,self).activate()
            
        # get the input projection activity            
        # decode the input projection activity as a command
        xa,ya = self.decode_fn(self)
        self.verbose("Saccade command centroid = (%.2f,%.2f)"%(xa,ya))
        
        amplitude = xa * self.amplitude_scale
        direction = ya * self.direction_scale

        self.verbose("Saccade amplitute = %.2f."%amplitude)
        self.verbose("Saccade direction = %.2f."%direction)
        
        self.send_output(src_port='Saccade',data=(amplitude,direction))




        
class ShiftingGeneratorSheet(SequenceGeneratorSheet):
    """
    A GeneratorSheet that takes an extra input on port 'Saccade'
    that specifies a saccade command as a tuple (amplitude,direction),
    indicating the relative size and direction of the saccade in
    degrees.  The parameter visual_angle_scale defines the<commit_msg>Added SaccadeController.command_mapper parameter, that can take a CoordMapperFn to remap the saccade command after it is computed from the sheet activity.<commit_after>"""
Sheets for simulating a moving eye.

This module provides two classes, ShiftingGeneratorSheet, and
SaccadeController, that can be used to simulate a moving eye,
controlled by topographic neural activity from structures like the
superior colliculus.

ShiftingGeneratorSheet is a subclass of GeneratorSheet that accepts a
saccade command on the 'Saccade' port in the form of a tuple:
(amplitude,direction), specified in degrees.  It shifts its sheet
bounds in response to this command, keeping the centroid of the bounds
within a prespecified boundingregion.

SaccadeController is a subclass of CFSheet that accepts CF projections
and decodes its resulting activity into a saccade command suitable for
controlling a ShiftingGeneratorSheet.


$Id$
"""
__version__ = '$Revision$'

from numpy import dot,sin,cos,pi,array,asarray,argmax,zeros
from numpy import nonzero,take,random,alltrue,sometrue

from topo.base.cf import CFSheet
from topo.base.simulation import PeriodicEventSequence,FunctionEvent
from topo.sheets.generatorsheet import SequenceGeneratorSheet
from topo.base.parameterclasses import Number,Magnitude,CallableParameter,BooleanParameter
from topo.base.parameterclasses import ClassSelectorParameter
from topo.base.boundingregion import BoundingBox,BoundingRegionParameter
from topo.coordmapperfns.basic import  CoordinateMapperFn, IdentityMF

from topo.misc import utils


# JPALERT: The next three functions (activity_centroid,
# activity_sample, and activity_mode) could actually apply to any
# Sheet.  Maybe they should be moved to topo.base.sheet?

def activity_centroid(sheet,activity=None,threshold=0.0):
    """
    Return the sheet coords of the (weighted) centroid of sheet activity.

    If the activity argument is not None, then it is used instead
    of sheet.activity.  If the sheet activity is all zero, the
    centroid of the sheet bounds is returned.
    """

    if activity is None:
        activity = sheet.activity

    ys = sheet.sheet_rows()
    xs = sheet.sheet_cols()

    xy = array([(x,y) for y in reversed(ys) for x in xs])
    a = activity.flat

    ## Optimization to only compute centroid from
    ## active (non-zero) units. 
    idxs = nonzero(a > threshold)[0]
    if not len(idxs):
        return sheet.bounds.centroid()
    return utils.centroid(take(xy,idxs,axis=0),take(a,idxs))


def activity_sample(sheet,activity=None):
    """
    Sample from the sheet activity as if it were a probability distribution.

    Returns the sheet coordinates of the sampled unit.  If
    activity is not None, it is used instead of sheet.activity.
    """

    if activity is None:
        activity = sheet.activity
    idx = utils.weighted_sample_idx(activity.ravel())
    r,c = utils.idx2rowcol(idx,activity.shape)

    return sheet.matrix2sheet(r,c)


def activity_mode(sheet,activity=None):
    """
    Returns the sheet coordinates of the mode (highest value) of
    the sheet activity.   
    """

    # JPHACKALERT:  The mode is computed using numpy.argmax, and
    # thus for distributions with multiple equal-valued modes, the
    # result will have a systematic bias toward higher x and lower
    # y values. (in that order).  Function may still be useful for
    # unimodal activity distributions, or sheets without limiting/squashing
    # output functions.
    if activity is None:
        activity = sheet.activity
    idx = argmax(activity.flat)
    r,c = utils.idx2rowcol(idx,activity.shape)
    return sheet.matrix2sheet(r,c)
      


class SaccadeController(CFSheet):
    """
    Sheet that decodes activity on CFProjections into a saccade command.
    
    This class accepts CF-projected input and computes its activity
    like a normal CFSheet, then decodes that activity into a saccade
    amplitude and direction as would be specified by activity in the
    superior colliculi.  The X dimension of activity corresponds to
    amplitude, the Y dimension to direction.  The activity is decoded
    to a single (x,y) point according to the parameter decode_method.

    From this (x,y) point an (amplitude,direction) pair, specified in
    degrees, is computed using the parameters amplitude_scale and
    direction scale.  That pair is then sent out on the 'Saccade'
    output port.

    NOTE: Non-linear mappings for saccade commands, as in Ottes, et
    al (below), are assumed to be provided using the coord_mapperg
    parameter of the incoming CFProjection.

    References:
    Ottes, <NAME>, Egglermont. 1986.  Visuomotor fields of the
    superior colliculus: a quantitative model.  Vision Research;
    26(6): 857-73.
    """

    # JPALERT: amplitude_scale and direction scale can be implemented as
    # part of self.command_mapper, so these should probably be removed.

    amplitude_scale = Number(default=120,doc="""
        Scale factor for saccade command amplitude, expressed in
        degrees per unit of sheet.  Indicates how large a saccade is
        represented by the x-component of the command input.""")
    
    direction_scale = Number(default=180,doc="""
        Scale factor for saccade command direction, expressed in
        degrees per unit of sheet.  Indicates what direction of saccade
        is represented by the y-component of the command input.""")


    decode_fn = CallableParameter(default=activity_centroid,
                                  instantiate=False,doc="""
        The function for extracting a single point from sheet activity.
        Should take a sheet as the first argument, and return (x,y).""")

    command_mapper = ClassSelectorParameter(CoordinateMapperFn,default=IdentityMF(),
                                   doc="""
        A CoordinateMapperFn that will be applied to the command vector extracted
        from the sheet activity.""")

    src_ports = ['Activity','Saccade']


    def activate(self):
        super(SaccadeController,self).activate()
            
        # get the input projection activity            
        # decode the input projection activity as a command
        xa,ya = self.decode_fn(self)
        self.verbose("Saccade command centroid = (%.2f,%.2f)"%(xa,ya))

        xa,ya = self.command_mapper(xa,ya)
        
        amplitude = xa * self.amplitude_scale
        direction = ya * self.direction_scale

        self.verbose("Saccade amplitute = %.2f."%amplitude)
        self.verbose("Saccade direction = %.2f."%direction)
        
        self.send_output(src_port='Saccade',data=(amplitude,direction))




        
class ShiftingGeneratorSheet(SequenceGeneratorSheet):
    """
    A GeneratorSheet that takes an extra input on port 'Saccade'
    that specifies a saccade command as a tuple (amplitude,direction),
    indicating the relative size and direction of the saccade in
    degrees.  The parameter visual_angle_scale defines the
{'fraction_non_alphanumeric': 0.06831922611850061, 'fraction_numerical': 0.004534461910519952, 'mean_word_length': 3.411, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 3, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '201003', 'n_tokens_mistral': 3702, 'n_tokens_neox': 3535, 'n_words': 1394}