Пример #1
0
def isoline_vmag(hemi, isolines=None, surface='midgray', min_length=2, **kw):
    '''
    isoline_vmag(hemi) calculates the visual magnification function f using the default set of
      iso-lines (as returned by neuropythy.vision.visual_isolines()). The hemi argument may
      alternately be a mesh object.
    isoline_vmag(hemi, isolns) uses the given iso-lines rather than the default ones.
    
    The return value of this funciton is a dictionary whose keys are 'tangential', 'radial', and
    'areal', and whose values are the estimated visual magnification functions. These functions
    are of the form f(x,y) where x and y can be numbers or arrays in the visual field.
    '''
    from neuropythy.util import (curry, zinv)
    from neuropythy.mri import is_cortex
    from neuropythy.vision import visual_isolines
    from neuropythy.geometry import to_mesh
    # if there's no isolines, get them
    if isolines is None: isolines = visual_isolines(hemi, **kw)
    # see if the isolines is a lazy map of visual areas; if so return a lazy map recursing...
    if pimms.is_vector(isolines.keys(), 'int'):
        f = lambda k: isoline_vmag(isolines[k], surface=surface, min_length=min_length)
        return pimms.lazy_map({k:curry(f, k) for k in six.iterkeys(isolines)})
    mesh = to_mesh((hemi, surface))
    # filter by min length
    if min_length is not None:
        isolines = {k: {kk: {kkk: [vvv[ii] for ii in iis] for (kkk,vvv) in six.iteritems(vv)}
                        for (kk,vv) in six.iteritems(v)
                        for iis in [[ii for (ii,u) in enumerate(vv['polar_angles'])
                                     if len(u) >= min_length]]
                        if len(iis) > 0}
                    for (k,v) in six.iteritems(isolines)}
    (rlns,tlns) = [isolines[k] for k in ['eccentricity', 'polar_angle']]
    if len(rlns) < 2: raise ValueError('fewer than 2 iso-eccentricity lines found')
    if len(tlns) < 2: raise ValueError('fewer than 2 iso-angle lines found')
    # grab the visual/surface lines
    ((rvlns,tvlns),(rslns,tslns)) = [[[u for lns in six.itervalues(xlns) for u in lns[k]]
                                      for xlns in (rlns,tlns)]
                                     for k in ('visual_coordinates','surface_coordinates')]
    # calculate some distances
    (rslen,tslen) = [[np.sqrt(np.sum((sx[:,:-1] - sx[:,1:])**2, 0)) for sx in slns]
                     for slns in (rslns,tslns)]
    (rvlen,tvlen) = [[np.sqrt(np.sum((vx[:,:-1] - vx[:,1:])**2, 0)) for vx in vlns]
                     for vlns in (rvlns,tvlns)]
    (rvxy, tvxy)  = [[0.5*(vx[:,:-1] + vx[:,1:]) for vx in vlns] for vlns in (rvlns,tvlns)]
    (rvlen,tvlen,rslen,tslen) = [np.concatenate(u) for u in (rvlen,tvlen,rslen,tslen)]
    (rvxy,tvxy)   = [np.hstack(vxy) for vxy in (rvxy,tvxy)]
    (rvmag,tvmag) = [vlen * zinv(slen) for (vlen,slen) in zip([rvlen,tvlen],[rslen,tslen])]
    return {k: {'visual_coordinates':vxy, 'visual_magnification': vmag,
                'visual_lengths': vlen, 'surface_lengths': slen}
            for (k,vxy,vmag,vlen,slen) in zip(['radial','tangential'], [rvxy,tvxy],
                                              [rvmag,tvmag], [rvlen,tvlen], [rslen,tslen])}
Пример #2
0
def cmag(mesh, retinotopy='any', surface=None, to='vertices'):
    '''
    cmag(mesh) yields the neighborhood-based cortical magnification for the given mesh.
    cmag(mesh, retinotopy) uses the given retinotopy argument; this must be interpretable by
      the as_retinotopy function, or should be the name of a source (such as 'empirical' or
      'any').

    The neighborhood-based cortical magnification data is yielded as a map whose keys are 'radial',
    'tangential', 'areal', and 'field_sign'; the units of 'radial' and 'tangential' magnifications 
    are cortical-distance/degree and the units on the 'areal' magnification is
    (cortical-distance/degree)^2; the field sign has no unit.

    Note that if the retinotopy source is not given, this function will by default search for any
    source using the retinotopy_data function.

    The option surface (default None) can be provided to indicate that while the retinotopy and
    results should be formatted for the given mesh (i.e., the result should have a value for each
    vertex in mesh), the surface coordinates used to calculate areas on the cortical surface should
    come from the given surface. The surface option may be a super-mesh of mesh.

    The option to='faces' or to='vertices' (the default) specifies whether the return-values should
    be for the vertices or the faces of the given mesh. Vertex data are calculated from the face
    data by summing and averaging.
    '''
    # First, find the retino data
    if pimms.is_str(retinotopy):
        retino = retinotopy_data(mesh, retinotopy)
    else:
        retino = retinotopy
    # If this is a topology, we want to change to white surface
    if isinstance(mesh, geo.Topology): mesh = mesh.white_surface
    # Convert from polar angle/eccen to longitude/latitude
    vcoords = np.asarray(as_retinotopy(retino, 'geographical'))
    # note the surface coordinates
    if surface is None: scoords = mesh.coordinates
    else:
        scoords = surface.coordinates
        if scoords.shape[1] > mesh.vertex_count:
            scoords = scoords[:, surface.index(mesh.labels)]
    faces = mesh.tess.indexed_faces
    sx = mesh.face_coordinates
    # to understand this calculation, see this stack exchange question:
    # https://math.stackexchange.com/questions/2431913/gradient-of-angle-between-scalar-fields
    # each face has a directional magnification; we need to start with the face side lengths
    (s0, s1, s2) = np.sqrt(np.sum((np.roll(sx, -1, axis=0) - sx)**2, axis=1))
    # we want a couple other handy values:
    (s0_2, s1_2, s2_2) = (s0**2, s1**2, s2**2)
    s0_inv = zinv(s0)
    b = 0.5 * (s0_2 - s1_2 + s2_2) * s0_inv
    h = 0.5 * np.sqrt(2 * s0_2 * (s1_2 + s2_2) - s0_2**2 -
                      (s1_2 - s2_2)**2) * s0_inv
    h_inv = zinv(h)
    # get the visual coordinates at each face also
    vx = np.asarray([vcoords[:, f] for f in faces])
    # we already have enough data to calculate areal magnification
    s_areas = geo.triangle_area(*sx)
    v_areas = geo.triangle_area(*vx)
    arl_mag = s_areas * zinv(v_areas)
    # calculate the gradient at each triangle; this array is dimension 2 x 2 x m where m is the
    # number of triangles; the first dimension is (vx,vy) and the second dimension is (fx,fy); fx
    # and fy are the coordinates in an arbitrary coordinate system built for each face.
    # So, to reiterate, grad is ((d(vx0)/d(fx0), d(vx0)/d(fx1)) (d(vx1)/d(fx0), d(vx1)/d(fx1)))
    dvx0_dfx = (vx[2] - vx[1]) * s0_inv
    dvx1_dfx = (vx[0] - (vx[1] + b * dvx0_dfx)) * h_inv
    grad = np.asarray([dvx0_dfx, dvx1_dfx])
    # Okay, we want to know the field signs; this is just whether the cross product of the two grad
    # vectors (dvx0/dfx and dvx1/dfx) has a positive z
    fsgn = np.sign(grad[0, 0] * grad[1, 1] - grad[0, 1] * grad[1, 0])
    # We can calculate the angle too, which is just the arccos of the normalized dot-product
    grad_norms_2 = np.sum(grad**2, axis=1)
    grad_norms = np.sqrt(grad_norms_2)
    (dvx_norms_inv, dvy_norms_inv) = zinv(grad_norms)
    ngrad = grad * ((dvx_norms_inv, dvx_norms_inv),
                    (dvy_norms_inv, dvy_norms_inv))
    dp = np.clip(np.sum(ngrad[0] * ngrad[1], axis=0), -1, 1)
    fang = fsgn * np.arccos(dp)
    # Great; now we can calculate the drad and dtan; we have dx and dy, so we just need to
    # calculate the jacobian of ((drad/dvx, drad/dvy), (dtan/dvx, dtan/dvy))
    vx_ctr = np.mean(vx, axis=0)
    (x0, y0) = vx_ctr
    den_inv = zinv(np.sqrt(x0**2 + y0**2))
    drad_dvx = np.asarray([x0, y0]) * den_inv
    dtan_dvx = np.asarray([-y0, x0]) * den_inv
    # get dtan and drad
    drad_dfx = np.asarray(
        [np.sum(drad_dvx[i] * grad[:, i], axis=0) for i in [0, 1]])
    dtan_dfx = np.asarray(
        [np.sum(dtan_dvx[i] * grad[:, i], axis=0) for i in [0, 1]])
    # we can now turn these into the magnitudes plus the field sign
    rad_mag = zinv(np.sqrt(np.sum(drad_dfx**2, axis=0)))
    tan_mag = zinv(np.sqrt(np.sum(dtan_dfx**2, axis=0)))
    # this is the entire result if we are doing faces only
    if to == 'faces':
        return {
            'radial': rad_mag,
            'tangential': tan_mag,
            'areal': arl_mag,
            'field_sign': fsgn
        }
    # okay, we need to do some averaging!
    mtx = simplex_summation_matrix(mesh.tess.indexed_faces)
    cols = np.asarray(mtx.sum(axis=1), dtype=np.float)[:, 0]
    cols_inv = zinv(cols)
    # for areal magnification, we want to do summation over the s and v areas then divide
    s_areas = mtx.dot(s_areas)
    v_areas = mtx.dot(v_areas)
    arl_mag = s_areas * zinv(v_areas)
    # for the others, we just average
    (rad_mag, tan_mag,
     fsgn) = [cols_inv * mtx.dot(x) for x in (rad_mag, tan_mag, fsgn)]
    return {
        'radial': rad_mag,
        'tangential': tan_mag,
        'areal': arl_mag,
        'field_sign': fsgn
    }
Пример #3
0
 def make_potential(va):
     global_field_sign = None if va is None else visual_area_field_signs.get(va)
     f_r = f_ret if va is None else f_ret[va]
     # The initial parameter vector is stored in the meta-data:
     X0 = f_r.meta_data['X0']
     # A few other handy pieces of data we can extract:
     fieldsign = visual_area_field_signs.get(va)
     submesh = f_r.meta_data['mesh']
     sxyz = submesh.coordinates
     n = submesh.vertex_count
     (u,v) = submesh.tess.indexed_edges
     selen = submesh.edge_lengths
     sarea = submesh.face_areas
     m = submesh.tess.edge_count
     fs = submesh.tess.indexed_faces
     neis = submesh.tess.indexed_neighborhoods
     fangs = submesh.face_angles
     # we're adding r and t (radial and tangential visual magnification) pseudo-parameters to
     # each vertex; r and t are derived from the position of other vertices; our first step is
     # to derive these values; for this we start with the parameters themselves:
     (x,y) = [op.identity[np.arange(k, 2*n, 2)] for k in (0,1)]
     # okay, we need to setup a bunch of least-squares solutions, one for each vertex:
     nneis = np.asarray([len(nn) for nn in neis])
     maxneis = np.max(nneis)
     thts = op.atan2(y, x)
     eccs = op.compose(op.piecewise(op.identity, ((-1e-9, 1e-9), 1)),
                       op.sqrt(x**2 + y**2))
     coss = x/eccs
     sins = y/eccs
     # organize neighbors:
     # neis becomes a list of rows of 1st neighbor, second neighbor etc. with -1 indicating none
     neis = np.transpose([nei + (-1,)*(maxneis - len(nei)) for nei in neis])
     qnei = (neis > -1) # mark where there are actually neighbors
     neis[~qnei] = 0 # we want the -1s (now 0s) to behave okay when passed to a potential index
     # okay, walk through the neighbors setting up the least squares
     (r, t) = (None, None)
     for (k,q,nei) in zip(range(len(neis)), qnei.astype('float'), neis):
         xx = x[nei] - x
         yy = y[nei] - y
         sd = np.sum((sxyz[:,nei].T - sxyz[:,k])**2, axis=1)
         (xx, yy) = (xx*coss + yy*sins, yy*coss - xx*sins)
         xterm = (op.abs(xx) * q)
         yterm = (op.abs(yy) * q)
         r = xterm if r is None else (r + xterm)
         t = yterm if t is None else (t + yterm)
     (r, t) = [uu * zinv(nneis) for uu in (r, t)]
     # for neighboring edges, we want r and t to be similar to each other
     f_rtsmooth = op.sum((r[v]-r[u])**2 + (t[v]-t[u])**2) / m
     # we also want r and t to predict the radial and tangential magnification of the node, so
     # we want to make sure that edges are the right distances away from each other based on the
     # surface edge lengths and the distance around the vertex at the center
     # for this we'll want some constant info about the surface edges/angles
     # okay, in terms of the visual field coordinates of the parameters, we will want to know
     # the angular position of each node
     # organize face info
     mnden   = 0.0001
     (e,qs,qt) = np.transpose([(i,e[0],e[1]) for (i,e) in enumerate(submesh.tess.edge_faces)
                               if len(e) == 2 and selen[i] > mnden
                               if sarea[e[0]] > mnden and sarea[e[1]] > mnden])
     (fis,q) = np.unique(np.concatenate([qs,qt]), return_inverse=True)
     (qs,qt)   = np.reshape(q, (2,-1))
     o       = len(fis)
     faces   = fs[:,fis]
     fangs   = fangs[:,fis]
     varea   = op.signed_face_areas(faces)
     srfangmtx = sps.csr_matrix(
         (fangs.flatten(),
          (faces.flatten(), np.concatenate([np.arange(o), np.arange(o), np.arange(o)]))),
         (n, o))
     srfangtot = flattest(srfangmtx.sum(axis=1))
     # normalize this angle matrix by the total and put it back in the same order as faces
     srfangmtx = zdivide(srfangmtx, srfangtot / (np.pi*2)).tocsr().T
     nrmsrfang = np.array([sps.find(srfangmtx[k])[2][np.argsort(fs[:,k])] for k in range(o)]).T
     # okay, now compare these to the actual angles;
     # we also want to know, for each edge, the angle relative to the radial axis; let's start
     # by organizing the faces into the units we compute over:
     (fa,fb,fc) = [np.concatenate([faces[k], faces[(k+1)%3], faces[(k+2)%3]]) for k in range(3)]
     atht = thts[fa]
     # we only have to worry about the (a,b) and (a,c) edges now; from the perspective of a...
     bphi = op.atan2(y[fb] - y[fa], x[fb] - x[fa]) - atht
     cphi = op.atan2(y[fc] - y[fa], x[fc] - x[fa]) - atht
     ((bcos,bsin),(ccos,csin)) = bccssn = [(op.cos(q),op.sin(q)) for q in (bphi,cphi)]
     # the distance should be predicted by surface edge length times ellipse-magnification
     # prediction; we have made uphi and vphi so that radial axis is x axis and tan axis is y
     (ra,ta) = (op.abs(r[fa]), op.abs(t[fa]))
     bslen = np.sqrt(np.sum((sxyz[:,fb] - sxyz[:,fa])**2, axis=0))
     cslen = np.sqrt(np.sum((sxyz[:,fc] - sxyz[:,fa])**2, axis=0))
     bpre_x = bcos * ra * bslen
     bpre_y = bsin * ta * bslen
     cpre_x = ccos * ra * cslen
     cpre_y = csin * ta * cslen
     # if there's a global field sign, we want to invert these predictions when the measured
     # angle is the wrong sign
     if global_field_sign is not None:
         varea_f = varea[np.concatenate([np.arange(o) for _ in range(3)])] * global_field_sign
         fspos = (op.sign(varea_f) + 1)/2
         fsneg = 1 - fspos
         (bpre_x,bpre_y,cpre_x,cpre_y) = (
             bpre_x*fspos - cpre_x*fsneg, bpre_y*fspos - cpre_y*fsneg,
             cpre_x*fspos - bpre_x*fsneg, cpre_y*fspos - bpre_y*fsneg)
     (ax,ay,bx,by,cx,cy) = [x[fa],y[fa],x[fb],y[fb],x[fc],y[fc]]
     (cost,sint) = [op.cos(atht), op.sin(atht)]
     (bpre_x, bpre_y) = (bpre_x*cost - bpre_y*sint + ax, bpre_x*sint + bpre_y*cost + ay)
     (cpre_x, cpre_y) = (cpre_x*cost - cpre_y*sint + ax, cpre_x*sint + cpre_y*cost + ay)
     # okay, we can compare the positions now...
     f_rt = op.sum((bpre_x-bx)**2 + (bpre_y-by)**2 + (cpre_x-cx)**2 + (cpre_y-cy)**2) * 0.5/o
     f_vmag = f_rtsmooth # + f_rt #TODO: the rt part of this needs to be debugged
     wgt = 0 if rt_knob is None else 2.0**rt_knob
     f = f_r if rt_knob is None else (f_r + f_vmag) if rt_knob == 0 else (f_r + w*f_vmag)
     md = pimms.merge(f_r.meta_data,
                      dict(f_retinotopy=f_r, f_vmag=f_vmag, f_rtsmooth=f_rtsmooth, f_rt=f_rt))
     object.__setattr__(f, 'meta_data', md)
     return f
Пример #4
0
def disk_vmag(hemi, retinotopy='any', to=None, **kw):
    '''
    disk_vmag(mesh) yields the visual magnification based on the projection of disks on the cortical
      surface into the visual field.

    All options accepted by mag_data() are accepted by disk_vmag().
    '''
    mdat = mag_data(hemi, retinotopy=retinotopy, **kw)
    if pimms.is_vector(mdat): return tuple([face_vmag(m, to=to) for m in mdat])
    elif pimms.is_vector(mdat.keys(), 'int'):
        return pimms.lazy_map({k: curry(lambda k: disk_vmag(mdat[k], to=to), k)
                               for k in six.iterkeys(mdat)})
    # for disk cmag we start by making sets of circular points around each vertex
    msh  = mdat['submesh']
    n    = msh.vertex_count
    vxy  = mdat['visual_coordinates'].T
    sxy  = msh.coordinates.T
    neis = msh.tess.indexed_neighborhoods
    nnei = np.asarray(list(map(len, neis)))
    emax = np.max(nnei)
    whs  = [np.where(nnei > k)[0] for k in range(emax)]
    neis = np.asarray([u + (-1,)*(emax - len(u)) for u in neis])
    dist = np.full((n, emax), np.nan)
    for (k,wh) in enumerate(whs):
        nei = neis[wh,k]
        dxy = sxy[nei] - sxy[wh]
        dst = np.sqrt(np.sum(dxy**2, axis=1))
        dist[wh,k] = dst
    # find min dist from each vertex
    ww  = np.where(np.max(np.isfinite(dist), axis=1) == 1)[0]
    whs = [np.intersect1d(wh, ww) for wh in whs]
    radii  = np.full(n, np.nan)
    radii[ww] = np.nanmin(dist[ww], 1)
    dfracs = (radii * zinv(dist.T)).T
    ifracs = 1 - dfracs
    # make points that distance from each edge
    ellipses = np.full((n, emax, 2), np.nan)
    for (k,wh) in enumerate(whs):
        xy0 = vxy[wh]
        xyn = vxy[neis[wh,k]]
        ifr = ifracs[wh,k]
        dfr = dfracs[wh,k]
        uxy1 = xy0 * np.transpose([ifr, ifr])
        uxy2 = xyn * np.transpose([dfr, dfr])
        uxy = (uxy1 + uxy2)
        ellipses[wh,k,:] = uxy - xy0
    # we want to rotate the points to be along their center's implied rad/tan axis
    vrs  = np.sqrt(np.sum(vxy**2, axis=1))
    irs  = zinv(vrs)
    coss = vxy[:,0] * irs
    sins = vxy[:,1] * irs
    # rotating each ellipse by negative-theta gives us x-radial and y=tangential
    cels = (coss * ellipses.T)
    sels = (sins * ellipses.T)
    rots = np.transpose([cels[0] + sels[1], cels[1] - sels[0]], [1,2,0])
    # now we fit the best rad/tan-oriented ellipse we can with the given center
    rsrt = np.sqrt(np.sum(rots**2, axis=2)).T
    (csrt,snrt) = zinv(rsrt) * rots.T
    # ... a*cos(rots) + b*sin(rots) ~= r(rots) where a = radial vmag and b = tangential vmag
    axes = []
    cods = []
    idxs = []
    for (r,c,s,k,irad,i) in zip(rsrt,csrt,snrt,nnei,zinv(radii),range(len(nnei))):
        if k < 3: continue
        (c,s,r) = [u[:k] for u in (c,s,r)]
        # if the center point is way outside the min/max, skip it
        (x,y) = (r*c, r*s)
        if len(np.unique(np.sign(x))) < 2 or len(np.unique(np.sign(y))) < 2: continue
        mudst = np.sqrt(np.sum(np.mean([x, y], axis=1)**2))
        if mudst > np.min(r): continue
        # okay, fit an ellipse...
        fs = np.transpose([c,s])
        try:
            (ab,rss,rnk,svs) = np.linalg.lstsq(fs, r, rcond=None)
            if len(rss) == 0 or rnk < 2 or np.min(svs/np.sum(svs)) < 0.01: continue
            axes.append(np.abs(ab) * irad)
            cods.append(1 - rss[0]*zinv(np.sum(r**2)))
            idxs.append(i)
        except Exception as e: continue
    (axes, cods, idxs) = [np.asarray(u) for u in (axes, cods, idxs)]
    return (idxs, axes, cods)
Пример #5
0
def mag_data(hemi, retinotopy='any', surface='midgray', mask=None,
             weights=Ellipsis, weight_min=0, weight_transform=Ellipsis,
             visual_area=None, visual_area_mask=Ellipsis,
             eccentricity_range=None, polar_angle_range=None):
    '''
    mag_data(hemi) yields a map of visual/cortical magnification data for the given hemisphere.
    mag_data(mesh) uses the given mesh.
    mag_data([arg1, arg2...]) maps over the given hemisphere or mesh arguments.
    mag_data(subject) is equivalent to mag_data([subject.lh, subject.rh]).
    mag_data(mdata) for a valid magnification data map mdata (i.e., is_mag_data(mdata) is True or
      mdata is a lazy map with integer keys) always yields mdata without considering any additional
      arguments.

    The data structure returned by magdata is a lazy map containing the keys:
      * 'surface_coordinates': a (2 x N) or (3 x N) matrix of the mesh coordinates in the mask
        (usually in mm).
      * 'visual_coordinates': a (2 x N) matrix of the (x,y) visual field coordinates (in degrees).
      * 'surface_areas': a length N vector of the surface areas of the faces in the mesh.
      * 'visual_areas': a length N vector of the areas of the faces in the visual field.
      * 'mesh': the full mesh from which the surface coordinates are obtained.
      * 'submesh': the submesh of mesh of just the vertices in the mask (may be identical to mesh).
      * 'mask': the mask used.
      * 'retinotopy_data': the full set of retinotopy_data from the hemi/mesh; note that this will
        include the key 'weights' of the weights actually used and 'visual_area' of the found or
        specified visual area.
      * 'masked_data': the subsampled retinotopy data from the hemi/mesh.
    Note that if a visual_area property is found or provided (see options below), instead of
    yielding a map of the above, a lazy map whose keys are the visual areas and whose values are the
    maps described above is yielded instead.

    The following named options are accepted (in order):
      * retinotopy ('any') specifies the value passed to the retinotopy_data function to obtain the
        retinotopic mapping data; this may be a map of such data.
      * surface ('midgray') specifies the surface to use.
      * mask (None) specifies the mask to use.
      * weights, weight_min, weight_transform (Ellipsis, 0, Ellipsis) are used as in the
        to_property() function  in neuropythy.geometry except weights, which, if equal to Ellipsis,
        attempts to use the weights found by retinotopy_data() if any.
      * visual_area (Ellipsis) specifies the property to use for the visual area label; Ellipsis is
        equivalent to whatever visual area label is found by the retinotopy_data() function if any.
      * visual_area_mask (Ellipsis) specifies which visual areas to include in the returned maps,
        assuming a visual_area property is found; Ellipsis is equivalent to everything but 0; None
        is equivalent to everything.
      * eccentricity_range (None) specifies the eccentricity range to include.
      * polar_angle_range (None) specifies the polar_angle_range to include.
    '''
    if is_mag_data(hemi): return hemi
    elif pimms.is_lazy_map(hemi) and pimms.is_vector(hemi.keys(), 'int'): return hemi
    if mri.is_subject(hemi): hemi = (hemi.lh. hemi.rh)
    if pimms.is_vector(hemi):
        return tuple([mag_data(h, retinotopy=retinotopy, surface=surface, mask=mask,
                               weights=weights, weight_min=weight_min,
                               weight_transform=weight_transform, visual_area=visual_area,
                               visual_area_mask=visual_area_mask,
                               eccentricity_range=eccentricity_range,
                               polar_angle_range=polar_angle_range)
                      for h in hemi])
    # get the mesh
    mesh = geo.to_mesh((hemi, surface))
    # First, find the retino data
    retino = retinotopy_data(hemi, retinotopy)
    # we can process the rest the mask now, including weights and ranges
    if weights is Ellipsis: weights = retino.get('variance_explained', None)
    mask = hemi.mask(mask, indices=True)
    (arng,erng) = (polar_angle_range, eccentricity_range)
    (ang,ecc) = (retino['polar_angle'], retino['eccentricity'])
    if pimms.is_str(arng):
        tmp = to_hemi_str(arng)
        arng = (-180,0) if tmp == 'rh' else (0,180) if tmp == 'lh' else (-180,180)
    elif arng is None:
        tmp = ang[mask]
        tmp = tmp[np.isfinite(tmp)]
        arng = (np.min(tmp), np.max(tmp))
    if erng is None:
        tmp = ecc[mask]
        tmp = tmp[np.isfinite(tmp)]
        erng = (0, np.max(tmp))
    elif pimms.is_scalar(erng): erng = (0, erng)
    (ang,wgt) = hemi.property(retino['polar_angle'], weights=weights, weight_min=weight_min,
                              weight_transform=weight_transform, yield_weight=True)
    ecc = hemi.property(retino['eccentricity'], weights=weights, weight_min=weight_min,
                        weight_transform=weight_transform, data_range=erng)
    # apply angle range if given
    ((mn,mx),mid) = (arng, np.mean(arng))
    oks = mask[np.isfinite(ang[mask])]
    u = ang[oks]
    u = np.mod(u + 180 - mid, 360) - 180 + mid
    ang[oks[np.where((mn <= u) & (u < mx))[0]]] = np.inf
    # mark/unify the out-of-range ones
    bad = np.where(np.isinf(ang) | np.isinf(ecc))[0]
    ang[bad] = np.inf
    ecc[bad] = np.inf
    wgt[bad] = 0
    wgt *= zinv(np.sum(wgt[mask]))
    # get visual and surface coords
    vcoords = np.asarray(as_retinotopy(retino, 'geographical'))
    scoords = mesh.coordinates
    # now figure out the visual area so we can call down if we need to
    if visual_area is Ellipsis: visual_area = retino.get('visual_area', None)
    if visual_area is not None: retino['visual_area'] = visual_area
    if wgt is not None: retino['weights'] = wgt
    rdata = pimms.persist(retino)
    # calculate the range area
    (tmn,tmx) = [np.pi/180.0 * u for u in arng]
    if tmx - tmn >= 2*np.pi: (tmn,tmx) = (-np.pi,np.pi)
    (emn,emx) = erng
    rarea = 0.5 * (emx*emx - emn*emn) * (tmx - tmn)
    # okay, we have the data organized; we can do the calculation based on this, but we may have a
    # visual area mask to apply as well; here's how we do it regardless of mask
    def finish_mag_data(mask):
        if len(mask) == 0: return None
        # now that we have the mask, we can subsample
        submesh = mesh.submesh(mask)
        mask = mesh.tess.index(submesh.labels)
        mdata = pyr.pmap({k:(v[mask]   if pimms.is_vector(v) else
                             v[:,mask] if pimms.is_matrix(v) else
                             None)
                          for (k,v) in six.iteritems(rdata)})
        fs = submesh.tess.indexed_faces
        (vx, sx)  = [x[:,mask]                        for x in (vcoords, scoords)]
        (vfx,sfx) = [np.asarray([x[:,f] for f in fs]) for x in (vx,      sx)]
        (va, sa)  = [geo.triangle_area(*x)            for x in (vfx, sfx)]
        return pyr.m(surface_coordinates=sx, visual_coordinates=vx,
                     surface_areas=sa,       visual_areas=va,
                     mesh=mesh,              submesh=submesh,
                     retinotopy_data=rdata,  masked_data=mdata,
                     mask=mask,              area_of_range=rarea)
    # if there's no visal area, we just use the mask as is
    if visual_area is None: return finish_mag_data(mask)
    # otherwise, we return a lazy map of the visual area mask values
    visual_area = hemi.property(visual_area, mask=mask, null=0, dtype=np.int)
    vam = (np.unique(visual_area)                    if visual_area_mask is None     else
           np.setdiff1d(np.unique(visual_area), [0]) if visual_area_mask is Ellipsis else
           np.unique(list(visual_area_mask)))
    return pimms.lazy_map({va: curry(finish_mag_data, mask[visual_area[mask] == va])
                           for va in vam})