docs: 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)
          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})