def visual_isolines(hemi, retinotopy='<STR_LIT>', visual_area=Ellipsis, mask=None, surface='<STR_LIT>',<EOL>weights=Ellipsis, min_weight=<NUM_LIT>,<EOL>eccentricity_range=None, polar_angle_range=None,<EOL>eccentricity_lines=<NUM_LIT:8>, polar_angle_lines=<NUM_LIT:8>,<EOL>max_step_scale=<NUM_LIT:1.0>/<NUM_LIT>):
from neuropythy import (to_mask, retinotopy_data, isolines, is_cortex, to_mesh)<EOL>from neuropythy.util import curry<EOL>if mask is not None: mask = to_mask(hemi, mask)<EOL>else: mask = hemi.tess.indices<EOL>retino = retinotopy_data(hemi, retinotopy)<EOL>if   visual_area is Ellipsis: visual_area = retino.get('<STR_LIT>', None)<EOL>elif visual_area is not None: visual_area = hemi.property(visual_area, mask=mask, null=<NUM_LIT:0>)<EOL>if visual_area is not None:<EOL><INDENT>vas = np.unique(visual_area)<EOL>kw = dict(weights=weights, min_weight=min_weight, surface=surface, visual_area=None,<EOL>eccentricity_lines=eccentricity_lines, polar_angle_lines=polar_angle_lines,<EOL>eccentricity_range=eccentricity_range, polar_angle_range=polar_angle_range,<EOL>max_step_scale=max_step_scale)<EOL>def recurse(va):<EOL><INDENT>vamask = to_mask(hemi, (visual_area, va))<EOL>return visual_isolines(hemi, retinotopy, mask=np.intersect1d(mask, vamask), **kw)<EOL><DEDENT>return pimms.lazy_map({va:curry(recurse, va) for va in vas if va != <NUM_LIT:0>})<EOL><DEDENT>ve = '<STR_LIT>'<EOL>weights = (retino[ve] if weights is Ellipsis and ve in retino   else<EOL>None       if weights is Ellipsis or weights is None else<EOL>hemi.property(weights))<EOL>if weights is not None and min_weight is not None:<EOL><INDENT>mask = np.intersect1d(mask, np.where(weights >= min_weight)[<NUM_LIT:0>])<EOL><DEDENT>(ang,ecc) = as_retinotopy(retino, '<STR_LIT>')<EOL>retino['<STR_LIT>'] = ang<EOL>retino['<STR_LIT>'] = ecc<EOL>mesh = to_mesh((hemi, surface))<EOL>def calc_isolines(hemi, dat, ln, mask=None):<EOL><INDENT>addrs = isolines(hemi, dat, ln, mask=mask, yield_addresses=True)<EOL>(angs,eccs) = [[hemi.interpolate(addr, retino[nm]) for addr in addrs]<EOL>for nm in ('<STR_LIT>', '<STR_LIT>')]<EOL>vxys = [np.asarray([e*np.cos(t), e*np.sin(t)])<EOL>for (a,e) in zip(angs,eccs) for t in [np.pi/<NUM_LIT>*(<NUM_LIT> - a)]]<EOL>sxys = [mesh.unaddress(addr) for addr in addrs]<EOL>if max_step_scale is not None: <EOL><INDENT>iiss = []<EOL>for (vxy,ecc) in zip(vxys, eccs):<EOL><INDENT>dists = np.sqrt(np.sum((vxy[:,:-<NUM_LIT:1>] - vxy[:,<NUM_LIT:1>:])**<NUM_LIT:2>, <NUM_LIT:0>))<EOL>es = np.mean([ecc[:-<NUM_LIT:1>], ecc[<NUM_LIT:1>:]], <NUM_LIT:0>)<EOL>mx = max_step_scale * es<EOL>bd = dists > mx<EOL>ok = np.where(~bd)[<NUM_LIT:0>]<EOL>bd = np.where(bd)[<NUM_LIT:0>]<EOL>(oi,bi) = (<NUM_LIT:0>,<NUM_LIT:0>)<EOL>iis = []<EOL>while True:<EOL><INDENT>if   oi >= len(ok): break<EOL>elif bi >= len(bd):<EOL><INDENT>iis.append(ok[oi:])<EOL>break<EOL><DEDENT>elif ok[oi] < bd[bi]:<EOL><INDENT>n = bd[bi] - ok[oi]<EOL>iis.append(ok[oi:(oi+n)])<EOL>oi += n<EOL><DEDENT>else: bi += ok[oi] - bd[bi]<EOL><DEDENT>iiss.append(iis)<EOL><DEDENT>(angs,eccs) = [[u[ii]   for (u,iis) in zip(q,iiss) for ii in iis] for q in (angs,eccs)]<EOL>(vxys,sxys) = [[u[:,ii] for (u,iis) in zip(q,iiss) for ii in iis] for q in (vxys,sxys)]<EOL>addrs = [{k:addr[k][:,ii] for k in ('<STR_LIT>','<STR_LIT>')}<EOL>for (addr,iis) in zip(addrs,iiss) for ii in iis]<EOL><DEDENT>return pimms.persist({'<STR_LIT>': addrs, '<STR_LIT>': angs, '<STR_LIT>':eccs,<EOL>'<STR_LIT>': vxys, '<STR_LIT>': sxys})<EOL><DEDENT>r = {}<EOL>(ang,ecc) = as_retinotopy(retino, '<STR_LIT>')<EOL>if eccentricity_range is not None:<EOL><INDENT>rng     = eccentricity_range<EOL>(mn,mx) = rng if pimms.is_vector(rng) else (np.min(ecc), rng)<EOL>mask    = np.setdiff1d(mask, np.where((ecc < mn) | (ecc > mx))[<NUM_LIT:0>])<EOL><DEDENT>if polar_angle_range is not None:<EOL><INDENT>rng     = polar_angle_range<EOL>(mn,mx) = rng if pimms.is_vector(rng) else (np.min(ang), rng)<EOL>aa = np.mean(rng)<EOL>aa = np.mod(ang + aa, <NUM_LIT>) - aa<EOL>mask = np.setdiff1d(mask, np.where((aa < mn) | (aa > mx))[<NUM_LIT:0>])<EOL><DEDENT>for (p,dat,lns,rng) in zip(['<STR_LIT>','<STR_LIT>'], [ang,ecc],<EOL>[polar_angle_lines, eccentricity_lines],<EOL>[polar_angle_range, eccentricity_range]):<EOL><INDENT>if pimms.is_int(lns): lns = np.percentile(dat[mask], np.linspace(<NUM_LIT:0>, <NUM_LIT:100>, <NUM_LIT:2>*lns + <NUM_LIT:1>)[<NUM_LIT:1>::<NUM_LIT:2>])<EOL>r[p] = pimms.lazy_map({ln:curry(calc_isolines, hemi, dat, ln, mask=mask) for ln in lns})<EOL><DEDENT>return pyr.pmap(r)<EOL>
visual_isolines(hemi) yields a dictionary of the iso-angle and iso-eccentricity lines for the
  given hemisphere hemi. The returned structure depends on the optional arguments and is
  documented below.
visual_isolines(hemi, retino) uses the retinotopy found by retinotopy_data(hemi, retino).

The following optional arguments may be given:
  * visual_area (default: Ellipsis) specifies the property that should be used as the visual
    area labels; if Ellipsis, then uses any visual area property found in the retinotopy data
    and otherwise uses none; if set to None, then the visual area is always ignored. Note that
    visual area values of 0 are ignored as well.
  * mask (default: None) specifies an additional mask that should be applied; when visual_area
    is None or not found, then this is the only masking that is performed.
  * weights (default: Ellipsis) specifies the property that should be used as the weights on
    the polar angle and eccentricity data; if Ellipsis, indicates that the variance explained or
    weight property found in the retinotopy data should be used if any.
  * min_weight (default: 0.05) specifies the minimum weight that should be included.
  * polar_angle_range (default: None) may speficy the (min,max) polar angle to include.
  * eccentricity_range (default: None) may specify the max or (min,max) eccentricity to include.
  * eccentricity_lines (default: 8) specifies the eccentricity values at which to draw
    iso-eccentricity lines. If this is an integer, it specifies that these should be chosen
    using even spacing along the inverse CDF of the eccentricity values above the weight
    threshold.
  * polar_angle_lines (default: 8) specifies the polar angle values at which to draw the
    iso-angular lines. If this is an integer, it specifies that these should be chosen using
    even spacing along the inverse CDF of the eccentricity values above the weight threshold.
  * surface (default: 'midgray') specifies the surface to use for calculating surface
    coordinates if hemi is a topology and not a mesh.

Return value:
  The return value of this function is a nested dictionary structure. If a visual area property
  is used, then the first level of keys are the visual areas (excluding 0) that are found.
  After this level, the next level's keys are 'polar_angle' and 'eccentricity' with the
  following level's keys indicating the polar angle or eccentricity value that was used. These
  internal dicts of iso-angle keys map to values that are lists of lines (there may be many
  contiguous lines at one angle); each line is given by a map of addresses and other meta-data
  about the line.