def generate_DROI_details(self,
                           eccentricity_range=None,
                           angles=None,
                           min_variance_explained=0,
                           method=None):
     '''
     generate_DROI_details() yields a set of lazily computed DROI detailed analyses; these
     analyses are used to generate the DROI table(s).
     '''
     import six
     from neuropythy.util import curry
     f = curry(
         VisualPerformanceFieldsDataset._generate_subject_DROI_details,
         self.subjects)
     m = {
         sid: pimms.lmap({
             h: curry(f,
                      sid,
                      h,
                      eccentricity_range=eccentricity_range,
                      angles=angles,
                      min_variance_explained=min_variance_explained,
                      method=method)
             for h in ['lh', 'rh']
         })
         for sid in six.iterkeys(self.subjects)
     }
     return pimms.persist(m)
    def _generate_subject_DROI_details(subjects,
                                       sid,
                                       h,
                                       eccentricity_range=None,
                                       angles=None,
                                       min_variance_explained=0,
                                       method=None):
        from neuropythy.util import curry
        import six, pyrsistent as pyr, numpy as np
        paradigm_order = ['dorsal', 'ventral', 'vertical', 'horizontal']
        roi_eccens = VisualPerformanceFieldsDataset.roi_eccens
        roi_angles = VisualPerformanceFieldsDataset.roi_angles
        if angles is None or angles is Ellipsis:
            angles = roi_angles
        e = eccentricity_range
        if e is None or e is Ellipsis:
            e = list(VisualPerformanceFieldsDataset.roi_eccens)
        if pimms.is_list(e) and all(pimms.is_tuple(q) for q in e):
            f = VisualPerformanceFieldsDataset._generate_subject_DROI_details
            res = [f(subjects, sid, h, eccentricity_range=q) for q in e]
            q = res[0]

            def _merge(p, a):
                r = {}
                for k in six.iterkeys(q[p][a]):
                    u = [u[p][a][k] for u in res if len(u[p][a][k]) > 0]
                    if len(u) == 0: u = np.asarray([], dtype=np.float)
                    else: u = np.concatenate(u)
                    r[k] = u
                return pyr.pmap(r)

            return pyr.pmap({
                k: pimms.lmap({a: curry(_merge, k, a)
                               for a in angles})
                for k in paradigm_order
            })
        f0 = VisualPerformanceFieldsDataset._generate_subject_DROI_data
        f = lambda sid, h, k: f0(subjects[sid],
                                 h,
                                 k,
                                 eccentricity_range=e,
                                 results='all',
                                 min_variance_explained=min_variance_explained,
                                 method=method)
        lm0 = pimms.lmap({k: curry(f, sid, h, k) for k in angles})
        pfn = lambda p: pimms.lmap(
            {k: curry(lambda k: lm0[k][p], k)
             for k in angles})
        return pimms.lmap({p: curry(pfn, p) for p in paradigm_order})
 def DROI_details(subjects):
     '''
     DROI_details is a nested-dictionary structure of the various DROI details of each subject
     and hemisphere.
     '''
     import neuropythy as ny, os, six
     from neuropythy.util import curry
     f = curry(
         VisualPerformanceFieldsDataset._generate_subject_DROI_details,
         subjects)
     m = {
         sid: pimms.lmap({h: curry(f, sid, h)
                          for h in ['lh', 'rh']})
         for sid in six.iterkeys(subjects)
     }
     return pimms.persist(m)
Exemple #4
0
def face_vmag(hemi, retinotopy='any', to=None, **kw):
    '''
    face_vmag(mesh) yields the visual magnification based on the projection of individual faces on
      the cortical surface into the visual field.
    face_vmag(mdat) uses the given magnification data mdat (as returned from mag_data()); if valid
      magnification data is passed then all options related to the mag_data() function are ignored.

    All options accepted by mag_data() are accepted by disk_vmag().

    The additional optional arguments are also accepted:
      * to (default: None) specifies that the resulting data should be transformed in some way;
        these transformations are:
          * None or 'data': returns the full magnification data without transformation;
          * 'faces': returns a property of the visual magnification value of each face;
          * 'vertices': returns a property of the visual magnification value of each vertex, as
            determined by averaging the magnification 
    '''
    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_map(mdat.keys(), 'int'):
        return pimms.lazy_map({k: curry(lambda k: face_vmag(mdat[k], to=to), k)
                               for k in six.iterkeys(mdat)})
    #TODO: implement the face_vmag calculation using mdat
    # convert to the appropriate type according to the to param
    raise NotImplementedError()
 def subjects(inferred_maps, boundary_distances, subject_list):
     '''
     subjects is a dictionary of subject objects for all subjects used in the visual performance
     fields dataset. All subject objects in the subejcts dict include property data on the native
     hemispheres for inferred retinotopic maps and for V1 boundary distances.
     '''
     from neuropythy.util import curry
     f = VisualPerformanceFieldsDataset._generate_subject
     return pimms.lmap({
         sid: curry(f, sid, inferred_maps, boundary_distances)
         for sid in subject_list
     })
Exemple #6
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])}
Exemple #7
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: face_vmag(mdat[k], to=to), k)
            for k in six.iterkeys(mdat)
        })
    #TODO: implement the disk_vmag calculation using mdat
    # convert to the appropriate type according to the to param
    raise NotImplementedError()
    def boundary_distances(pseudo_path, subject_list, inferred_maps):
        '''
        boundary_distances is a nested-dictionary structure containing distances between
        each vertex and a V1 boundary. If x is boundar_distances[sid][h][b][k] then x is
        the distance between the k'th vertex and boundary b ("ventral", "dorsal", or
        "horizontal") in the h hemisphere ("lh" or "rh") of the subject with ID sid.
        '''
        import os, six
        from neuropythy.util import curry
        from neuropythy import load

        def _load_distances(sid, h):
            flnm = pseudo_path.local_path('distances', '%s_%s.mgz' % (sid, h))
            (v, d, h) = load(flnm).T
            return pimms.persist({'ventral': v, 'dorsal': d, 'horizontal': h})

        return pimms.persist({
            sid: pimms.lmap(
                {h: curry(_load_distances, sid, h)
                 for h in ['lh', 'rh']})
            for sid in subject_list
        })
    def inferred_maps(pseudo_path, subject_list):
        '''
        inferred_maps is a nested-dictionary structure containing the retinotopic maps inferred by
        using Bayesian inference on the retinotopic maps of the subjects in the HCP 7T Retinotopy
        Dataset.
        '''
        import os, six
        from neuropythy.util import curry
        from neuropythy import load
        inffiles = VisualPerformanceFieldsDataset.inferred_map_files

        def _load_infmaps(sid, h, patt):
            flnm = pseudo_path.local_path('inferred_maps', patt % (sid, h))
            return load(flnm)

        return pimms.persist({
            sid: {
                h: pimms.lmap({('inf_' + k): curry(_load_infmaps, sid, h, v)
                               for (k, v) in six.iteritems(inffiles)})
                for h in ['lh', 'rh']
            }
            for sid in subject_list
        })
Exemple #10
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)
Exemple #11
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})
Exemple #12
0
def rtmag_potential(hemi, retinotopy=Ellipsis, mask=Ellipsis, weight=Ellipsis,
                    surface='midgray', min_weight=Ellipsis, min_eccentricity=0.75,
                    visual_area=None, map_visual_areas=Ellipsis,
                    visual_area_field_signs=Ellipsis,
                    measurement_uncertainty=0.4, measurement_knob=1,
                    magnification_knob=2, fieldsign_knob=8, edge_knob=0, rt_knob=0):
    from neuropythy.vision.retinotopy import clean_retinotopy_potential
    import neuropythy.optimize as op
    f_ret = clean_retinotopy_potential(hemi, retinotopy=retinotopy, mask=mask, weight=weight,
                                       surface=surface, min_weight=min_weight,
                                       min_eccentricity=min_eccentricity,
                                       visual_area=visual_area, map_visual_areas=map_visual_areas,
                                       visual_area_field_signs=visual_area_field_signs,
                                       measurement_uncertainty=measurement_uncertainty,
                                       measurement_knob=measurement_knob,
                                       magnification_knob=magnification_knob,
                                       fieldsign_knob=fieldsign_knob, edge_knob=edge_knob)
    # process a few additional arguments:
    if   visual_area_field_signs is None:     visual_area_field_signs = {}
    elif visual_area_field_signs is Ellipsis: visual_area_field_signs = {1:-1, 2:1, 3:-1, 4:1}
    # this may be a lazy map of visual areas; we want to operate on all of them lazily, so wrap the
    # rest of this model up in a function:
    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
    if pimms.is_map(f_ret):
        return pimms.lazy_map({va: curry(make_potential, va) for va in six.iterkeys(f_ret)})
    else: return make_potential(None)