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])}
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 }
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
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)
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})