Beispiel #1
0
def grad(fld, bnd=True):
    """2nd order centeral diff, 1st order @ boundaries if bnd"""
    # vx, vy, vz = fld.component_views()
    if bnd:
        fld = viscid.extend_boundaries(fld, order=0, crd_order=0)

    if fld.iscentered("Cell"):
        crdx, crdy, crdz = fld.get_crds_cc(shaped=True)
        # divcenter = "Cell"
        # divcrds = coordinate.NonuniformCartesianCrds(fld.crds.get_clist(np.s_[1:-1]))
        # divcrds = fld.crds.slice_keep(np.s_[1:-1, 1:-1, 1:-1])
    elif fld.iscentered("Node"):
        crdx, crdy, crdz = fld.get_crds_nc(shaped=True)
        # divcenter = "Node"
        # divcrds = coordinate.NonuniformCartesianCrds(fld.crds.get_clist(np.s_[1:-1]))
        # divcrds = fld.crds.slice_keep(np.s_[1:-1, 1:-1, 1:-1])
    else:
        raise NotImplementedError("Can only do cell and node centered gradients")

    v = fld.data
    g = viscid.zeros(fld['x=1:-1, y=1:-1, z=1:-1'].crds, nr_comps=3)

    xp, xm = crdx[2:,  :,  :], crdx[:-2, :  , :  ]  # pylint: disable=bad-whitespace
    yp, ym = crdy[ :, 2:,  :], crdy[:  , :-2, :  ]  # pylint: disable=bad-whitespace
    zp, zm = crdz[ :,  :, 2:], crdz[:  , :  , :-2]  # pylint: disable=bad-whitespace

    vxp, vxm = v[2:  , 1:-1, 1:-1], v[ :-2, 1:-1, 1:-1]  # pylint: disable=bad-whitespace
    vyp, vym = v[1:-1, 2:  , 1:-1], v[1:-1,  :-2, 1:-1]  # pylint: disable=bad-whitespace
    vzp, vzm = v[1:-1, 1:-1, 2:  ], v[1:-1, 1:-1,  :-2]  # pylint: disable=bad-whitespace

    g['x'].data[...] = ne.evaluate("(vxp-vxm)/(xp-xm)")
    g['y'].data[...] = ne.evaluate("(vyp-vym)/(yp-ym)")
    g['z'].data[...] = ne.evaluate("(vzp-vzm)/(zp-zm)")
    return g
Beispiel #2
0
def convective_deriv(a, b=None, bnd=True):
    r"""Compute (a \dot \nabla) b for vector fields a and b"""
    # [(B \dot \nabla) B]_j = B_i \partial_i B_j
    # FIXME: this is a lot of temporary arrays
    if bnd:
        if b is None:
            b = viscid.extend_boundaries(a, order=0, crd_order=0)
        else:
            b = viscid.extend_boundaries(b, order=0, crd_order=0)
    else:
        if b is None:
            b = a
        a = a['x=1:-1, y=1:-1, z=1:-1']

    if b.nr_comps > 1:
        diBj = [[None, None, None], [None, None, None], [None, None, None]]
        for j, jcmp in enumerate('xyz'):
            g = grad(b[jcmp], bnd=False)
            for i, icmp in enumerate('xyz'):
                diBj[i][j] = g[icmp]
        dest = viscid.zeros(a.crds, nr_comps=3)
        for i, icmp in enumerate('xyz'):
            for j, jcmp in enumerate('xyz'):
                dest[jcmp][...] += a[icmp] * diBj[i][j]
    else:
        dest = dot(a, grad(b, bnd=False))
    return dest
def _main():
    x = np.linspace(-1, 1, 128)
    y = z = np.linspace(-0.25, 0.25, 8)
    B = viscid.zeros((x, y, z), nr_comps=3, layout='interlaced', name="B")
    X, Y, Z = B.get_crds("xyz", shaped=True)  # pylint: disable=unused-variable
    xl, yl, zl = B.xl  # pylint: disable=unused-variable
    xh, yh, zh = B.xh  # pylint: disable=unused-variable
    xm, ym, zm = 0.5 * (B.xl + B.xh)  # pylint: disable=unused-variable

    B['x'] = 0.0  # np.sin(1.0 * np.pi * X / (xh - xl) + 0.5 * np.pi)
    B['y'] = np.sin(1.0 * np.pi * X / (xh - xl) + 0.5 * np.pi)
    B['z'] = np.sin(1.0 * np.pi * X / (xh - xl) - 1.0 * np.pi)
    B += 0.33 * np.random.random_sample(B.shape)

    # R = viscid.a2b_rotm((1, 0, 0), (1, 0, 1))
    # B[...] = np.einsum("ij,lmnj->lmni", R, B)

    lmn = find_minvar_lmn(B, (xl, ym, zm), (xh, ym, zm), l_basis=None)
    # lmn = find_minvar_lmn(B, (xl, ym, zm), (xh, ym, zm), l_basis=(0, 0, 1))
    print("LMN matrix:\n", lmn, sep='')

    ##########
    from viscid.plot import vpyplot as vlt
    from matplotlib import pyplot as plt
    p0 = np.array((xm, ym, zm)).reshape((3,))
    pl = p0 + 0.25 * lmn[:, 0]
    pm = p0 + 0.25 * lmn[:, 1]
    pn = p0 + 0.25 * lmn[:, 2]

    print("p0", p0)
    print("pl", pl)
    print("pm", pm)
    print("pn", pn)

    vlt.subplot(211)
    vlt.plot2d_quiver(B['z=0j'])
    plt.plot([p0[0], pl[0]], [p0[1], pl[1]], color='r', ls='-')
    plt.plot([p0[0], pm[0]], [p0[1], pm[1]], color='c', ls='-')
    plt.plot([p0[0], pn[0]], [p0[1], pn[1]], color='b', ls='-')
    plt.ylabel("Y")

    vlt.subplot(212)
    vlt.plot2d_quiver(B['y=0j'])
    plt.plot([p0[0], pl[0]], [p0[2], pl[2]], color='r', ls='-')
    plt.plot([p0[0], pm[0]], [p0[2], pm[2]], color='c', ls='-')
    plt.plot([p0[0], pn[0]], [p0[2], pn[2]], color='b', ls='-')
    plt.xlabel("X")
    plt.ylabel("Z")

    vlt.show()
    ##########

    return 0
Beispiel #4
0
def make_arcade(eps, xl=(-10.0, 0.0, -10.0), xh=(10.0, 20.0, 10.0),
                L=(5, 5, 5), N=(32, 32, 32), layout='interlaced'):
    xl, xh = np.asarray(xl), np.asarray(xh)
    x = np.linspace(xl[0], xh[0], N[0])
    y = np.linspace(xl[1], xh[1], N[1])
    z = np.linspace(xl[2], xh[2], N[2])

    b = viscid.zeros([x, y, z], nr_comps=3, layout=layout)
    e = viscid.zeros_like(b)
    X, Y, Z = b.get_crds('xyz', shaped=True)
    Y2 = Y**2 / L[1]**2
    Z2 = Z**2 / L[2]**2

    b['x'] = -1 - (eps * ((1 - Y2) / (1 + Y2)) * (1 / (1 + Z2)))
    b['y'] = X
    b['z'] = 0.2

    e['z'] = Y / ((1 + Y2) * (1 + Z2))

    return b, e
def _main():
    f = viscid.load_file('~/dev/work/xi_fte_001/*.3d.*.xdmf')
    time_slice = ':'
    times = np.array([grid.time for grid in f.iter_times(time_slice)])

    # XYZ coordinates of virtual satelites in warped "plasma sheet coords"
    x_sat_psc = np.linspace(-30, 0, 31)  # X (GSE == PSC)
    y_sat_psc = np.linspace(-10, 10, 21)  # Y (GSE == PSC)
    z_sat_psc = np.linspace(-2, 2, 5)  # Z in PSC (z=0 is the plasma sheet)

    # the GSE z location of the virtual satelites in the warped plasma sheet
    # coordinates, so sat_z_gse_ts['x=5j, y=1j, z=0j'] would give the
    # plasma sheet location at x=5.0, y=1.0
    # These fields depend on time because the plasma sheet moves in time
    sat_z_gse_ts = viscid.zeros([times, x_sat_psc, y_sat_psc, z_sat_psc],
                                crd_names='txyz', center='node',
                                name='PlasmaSheetZ_GSE')
    vx_ts = viscid.zeros_like(sat_z_gse_ts)
    bz_ts = viscid.zeros_like(sat_z_gse_ts)

    for itime, grid in enumerate(f.iter_times(time_slice)):
        print("Processing time slice", itime, grid.time)

        gse_slice = 'x=-35j:0j, y=-15j:15j, z=-6j:6j'
        bx = grid['bx'][gse_slice]
        bx_argmin = np.argmin(bx**2, axis=2)
        z_gse = bx.get_crd('z')
        # ps_zloc_gse is the plasma sheet z location along the GGCM grid x/y
        ps_z_gse = viscid.zeros_like(bx[:, :, 0:1])
        ps_z_gse[...] = z_gse[bx_argmin]

        # Note: Here you could apply a gaussian filter to
        #       ps_z_gse[:, :, 0].data in order to smooth the surface
        #       if desired. Scipy / Scikit-Image have some functions
        #       that do this

        # ok, we found the plasma sheet z GSE location on the actual GGCM
        # grid, but we just want a subset of that grid for our virtual
        # satelites, so just interpolate the ps z location to our subset
        ps_z_gse_subset = viscid.interp_trilin(ps_z_gse,
                                               sat_z_gse_ts[itime, :, :, 0:1],
                                               wrap=True)
        # now we know the plasma sheet z location in GSE, and how far
        # apart we want the satelites in z, so put those two things together
        # to get a bunch of satelite locations
        sat_z_gse_ts[itime] = ps_z_gse_subset.data + z_sat_psc.reshape(1, 1, -1)

        # make a seed generator that we can use to fill the vx and bz
        # time series for this instant in time
        sat_loc_gse = sat_z_gse_ts[itime].get_points()
        sat_loc_gse[2, :] = sat_z_gse_ts[itime].data.reshape(-1)

        # slicing the field before doing the interpolation makes this
        # faster for hdf5 data, but probably for other data too
        vx_ts[itime] = viscid.interp_trilin(grid['vx'][gse_slice],
                                            sat_loc_gse,
                                            wrap=False
                                            ).reshape(vx_ts.shape[1:])
        bz_ts[itime] = viscid.interp_trilin(grid['bz'][gse_slice],
                                            sat_loc_gse,
                                            wrap=False
                                            ).reshape(bz_ts.shape[1:])

        # 2d plots of the plasma sheet z location to make sure we did the
        # interpolation correctly
        if False:  # pylint: disable=using-constant-test
            from viscid.plot import vpyplot as vlt
            fig, (ax0, ax1) = vlt.subplots(2, 1)  # pylint: disable=unused-variable
            vlt.plot(ps_z_gse, ax=ax0, clim=(-5, 5))
            vlt.plot(ps_z_gse_subset, ax=ax1, clim=(-5, 5))
            vlt.auto_adjust_subplots()
            vlt.show()

        # make a 3d plot of the plasma sheet surface to verify that it
        # makes sense
        if True:  # pylint: disable=using-constant-test
            from viscid.plot import vlab
            fig = vlab.figure(size=(1280, 800), bgcolor=(1, 1, 1),
                              fgcolor=(0, 0, 0))
            vlab.clf()
            # plot the plasma sheet coloured by vx
            # Note: points closer to x = 0 are unsightly since the plasma
            #       sheet criteria starts to fall apart on the flanks, so
            #       just remove the first few rows
            ps_z_gse_tail = ps_z_gse['x=:-2.25j']
            ps_mesh_shape = [3, ps_z_gse_tail.shape[0], ps_z_gse_tail.shape[1]]
            ps_pts = ps_z_gse_tail.get_points().reshape(ps_mesh_shape)
            ps_pts[2, :, :] = ps_z_gse_tail[:, :, 0]
            plasma_sheet = viscid.RectilinearMeshPoints(ps_pts)
            ps_vx = viscid.interp_trilin(grid['vx'][gse_slice], plasma_sheet)
            _ = vlab.mesh_from_seeds(plasma_sheet, scalars=ps_vx)
            vx_clim = (-1400, 1400)
            vx_cmap = 'viridis'
            vlab.colorbar(title='Vx', clim=vx_clim, cmap=vx_cmap,
                          nb_labels=5)
            # plot satelite locations as dots colored by Vx with the same
            # limits and color as the plasma sheet mesh
            sat3d = vlab.points3d(sat_loc_gse[0], sat_loc_gse[1], sat_loc_gse[2],
                                  vx_ts[itime].data.reshape(-1),
                                  scale_mode='none', scale_factor=0.2)
            vlab.apply_cmap(sat3d, clim=vx_clim, cmap=vx_cmap)

            # plot Earth for reference
            cotr = viscid.Cotr(dip_tilt=0.0)  # pylint: disable=not-callable
            vlab.plot_blue_marble(r=1.0, lines=False, ntheta=64, nphi=128,
                                  rotate=cotr, crd_system='mhd')
            vlab.plot_earth_3d(radius=1.01, night_only=True, opacity=0.5,
                               crd_system='gse')
            vlab.view(azimuth=45, elevation=70, distance=35.0,
                      focalpoint=[-9, 3, -1])
            vlab.savefig('plasma_sheet_3d_{0:02d}.png'.format(itime))
            vlab.show()
            try:
                vlab.mlab.close(fig)
            except TypeError:
                pass  # this happens if the figure is already closed

    # now do what we will with the time series... this is not a good
    # presentation of this data, but you get the idea
    from viscid.plot import vpyplot as vlt
    fig, axes = vlt.subplots(4, 4, figsize=(12, 12))
    for ax_row, yloc in zip(axes, np.linspace(-5, 5, len(axes))[::-1]):
        for ax, xloc in zip(ax_row, np.linspace(4, 7, len(ax_row))):
            vlt.plot(vx_ts['x={0}j, y={1}j, z=0j'.format(xloc, yloc)], ax=ax)
            ax.set_ylabel('')
            vlt.plt.title('x = {0:g}, y = {1:g}'.format(xloc, yloc))
    vlt.plt.suptitle('Vx [km/s]')
    vlt.auto_adjust_subplots()
    vlt.show()

    return 0
def apply_labels(labels=None, colors=None, ax=None, magnet=(0.5, 0.75),
                 magnetcoords="axes fraction", padding=None,
                 paddingcoords="offset points", choices="00:02:20:22",
                 n_candidates=32, ignore_filling=False, spacing='linear',
                 _debug=False, **kwargs):
    """Apply labels directly to series in liu of a legend

    The `choices` offsets are as follows::

        ---------------------
        |  02  |  12  | 22  |
        |-------------------|
        |  01  |  XX  | 21  |
        |-------------------|
        |  00  |  10  | 20  |
        ---------------------

    Args:
        labels (sequence): Optional sequence of labels to override the
            labels already in the data series
        colors (str, sequence): color as hex string, list of hex
            strings to color each label, or an Nx4 ndarray of rgba
            values for N labels
        ax (matplotlib.axis): axis; defaults to `plt.gca()`
        magnet (tuple): prefer positions that are closer to the magnet
        magnetcoords (str): 'offset pixels', 'offset points' or 'axes fraction'
        padding (tuple): padding for text in the (x, y) directions
        paddingcoords (str): 'offset pixels', 'offset points' or 'axes fraction'
        choices (str): colon separated list of possible label positions
            relative to the data values. The positions are summarized
            above.
        alpha (float): alpha channel (opacity) of label text. Defaults
            to 1.0 to make text visible. Set to `None` to use the
            underlying alpha from the handle's color.
        n_candidates (int): number of potential label locations to
            consider for each data series.
        ignore_filling (bool): if True, then assume it's ok to place
            labels inside paths that are filled with color
        spacing (str): one of 'linear' or 'random' to specify how far
            apart candidate locations are spaced along path segments
        _debug (bool): Mark up all possible label locations
        **kwargs: passed to plt.annotate

    Returns:
        List: annotation objects
    """
    if not ax:
        ax = plt.gca()

    if isinstance(colors, (list, tuple)):
        pass

    spacing = spacing.strip().lower()
    if spacing not in ('linear', 'random'):
        raise ValueError("Spacing '{0}' not understood".format(spacing))
    rand_state = np.random.get_state() if spacing == 'random' else None
    if rand_state is not None:
        # save the RNG state to restore it later so that plotting functions
        # don't change the results of scripts that use random numbers
        np.random.seed(1)

    _xl, _xh = ax.get_xlim()
    _yl, _yh = ax.get_ylim()
    axbb0 = np.array([_xl, _yl]).reshape(1, 2)
    axbb1 = np.array([_xh, _yh]).reshape(1, 2)

    # choices:: "01:02:22" -> [(0, 1), (0, 2), (2, 2)]
    choices = [(int(c[0]), int(c[1])) for c in choices.split(':')]

    _size = kwargs.get('fontsize', kwargs.get('size', None))
    _fontproperties = kwargs.get('fontproperties', None)
    font_size_pts = text_size_points(size=_size, fontproperties=_fontproperties)

    # set the default padding equal to the font size
    if paddingcoords == 'offset pixels':
        default_padding = font_size_pts * 72 / ax.figure.dpi
    elif paddingcoords == 'offset points':
        default_padding = font_size_pts
    elif paddingcoords == 'axes fraction':
        default_padding = 0.05
    else:
        raise ValueError("Bad padding coords '{0}'".format(paddingcoords))

    # print("fontsize pt:", font_size_pts,
    #       "fontsize px:", xy_as_pixels([font_size_pts, font_size_pts],
    #                                    'offset points')[0])

    if not isinstance(padding, (list, tuple)):
        padding = [padding, padding]
    padding = [default_padding if pd is None else pd for pd in padding]
    # print("padding::", paddingcoords, padding)

    magnet_px = xy_as_pixels(magnet, magnetcoords, ax=ax)
    padding_px = xy_as_pixels(padding, paddingcoords, ax=ax)
    # print("padding px::", padding_px)

    annotations = []

    cand_map = {}
    for choice in choices:
        cand_map[choice] = np.zeros([n_candidates, 2, 2], dtype='f')

    # these paths are all the paths we can get our hands on so that the text
    # doesn't overlap them. bboxes around labels are added as we go
    paths_px = []
    # here is a list of bounding boxes around the text boxes as we add them
    bbox_paths_px = []
    is_filled = []

    ## how many vertices to avoid ?
    # artist
    # collection
    # image
    # line
    # patch
    # table
    # container

    for line in ax.lines:
        paths_px += [ax.transData.transform_path(line.get_path())]
        is_filled += [False]
    for collection in ax.collections:
        for pth in collection.get_paths():
            paths_px += [ax.transData.transform_path(pth)]
            is_filled += [collection.get_fill()]

    if ignore_filling:
        is_filled = [False] * len(is_filled)

    hands, hand_labels = ax.get_legend_handles_labels()

    colors = _cycle_colors(colors, len(hands))

    # >>> debug >>>
    if _debug:
        import viscid
        from matplotlib import patches as mpatches
        from viscid.plot import vpyplot as vlt

        _fig_width = int(ax.figure.bbox.width)
        _fig_height = int(ax.figure.bbox.height)
        fig_fld = viscid.zeros((_fig_width, _fig_height), dtype='f',
                               center='node')
        _X, _Y = fig_fld.get_crds(shaped=True)

        _axXL, _axYL, _axXH, _axYH = ax.bbox.extents

        _mask = np.bitwise_and(np.bitwise_and(_X >= _axXL, _X <= _axXH),
                               np.bitwise_and(_Y >= _axYL, _Y <= _axYH))
        fig_fld.data[_mask] = 1.0

        dfig, dax = plt.subplots(1, 1, figsize=ax.figure.get_size_inches())

        vlt.plot(fig_fld, ax=dax, cmap='ocean', colorbar=None)
        for _, path in enumerate(paths_px):
            dax.plot(path.vertices[:, 0], path.vertices[:, 1])
        dfig.subplots_adjust(bottom=0.0, left=0.0, top=1.0, right=1.0)
    else:
        dfig, dax = None, None
    # <<< debug <<<


    for i, hand, label_i in zip(count(), hands, hand_labels):
        if labels and i < len(labels):
            label = labels[i]
        else:
            label = label_i

        # divine color of label
        if colors[i]:
            color = colors[i]
        else:
            try:
                color = hand.get_color()
            except AttributeError:
                color = hand.get_facecolor()[0]

        # get path vertices to determine candidate label positions
        try:
            verts = hand.get_path().vertices
        except AttributeError:
            verts = [p.vertices for p in hand.get_paths()]
            verts = np.concatenate(verts, axis=0)

        segl_dat = verts[:-1, :]
        segh_dat = verts[1:, :]

        # take out path segments that have one vertex outside the view
        _seg_mask = np.all(np.bitwise_and(segl_dat >= axbb0, segl_dat <= axbb1)
                           & np.bitwise_and(segh_dat >= axbb0, segh_dat <= axbb1),
                           axis=1)
        segl_dat = segl_dat[_seg_mask, :]
        segh_dat = segh_dat[_seg_mask, :]

        if np.prod(segl_dat.shape) == 0:
            print("no full segments are visible, skipping path", i, hand)
            continue

        segl_px = ax.transData.transform(segl_dat)
        segh_px = ax.transData.transform(segh_dat)
        seglen_px = np.linalg.norm(segh_px - segl_px, axis=1)

        # take out path segments that are 0 pixels in length
        _non0_seg_mask = seglen_px > 0
        segl_dat = segl_dat[_non0_seg_mask, :]
        segh_dat = segh_dat[_non0_seg_mask, :]
        segl_px = segl_px[_non0_seg_mask, :]
        segh_px = segh_px[_non0_seg_mask, :]
        seglen_px = seglen_px[_non0_seg_mask]

        if np.prod(segl_dat.shape) == 0:
            print("no non-0 segments are visible, skipping path", i, hand)
            continue

        # i deeply appologize for how convoluted this got, but the punchline
        # is that each line segment gets candidates proportinal to their
        # length in pixels on the figure
        s_src = np.concatenate([[0], np.cumsum(seglen_px)])
        if rand_state is not None:
            s_dest = s_src[-1] * np.sort(np.random.rand(n_candidates))
        else:
            s_dest = np.linspace(0, s_src[-1], n_candidates)
        _diff = s_dest.reshape(1, -1) - s_src.reshape(-1, 1)
        iseg = np.argmin(np.ma.masked_where(_diff <= 0, _diff), axis=0)
        frac = (s_dest - s_src[iseg]) / seglen_px[iseg]

        root_dat = (segl_dat[iseg]
                    + frac.reshape(-1, 1) * (segh_dat[iseg] - segl_dat[iseg]))
        root_px = ax.transData.transform(root_dat)

        # estimate the width and height of the label's text
        txt_size = np.array(estimate_text_size_px(label, fig=ax.figure,
                                                  size=font_size_pts))
        txt_size = txt_size.reshape([1, 2])

        # this initial offset is needed to shift the center of the label
        # to the data point
        offset0 = -txt_size / 2

        # now we can shift the label away from the data point by an amount
        # equal to half the text width/height + the padding
        offset1 = padding_px + txt_size / 2

        for key, abs_px_arr in cand_map.items():
            ioff = np.array(key, dtype='i').reshape(1, 2) - 1
            total_offset = offset0 + ioff * offset1
            # approx lower left corner of the text box in absolute pixels
            abs_px_arr[:, :, 0] = root_px + total_offset
            # approx upper right corner of the text box in absolute pixels
            abs_px_arr[:, :, 1] = abs_px_arr[:, :, 0] + txt_size

        # candidates_abs_px[i] has root @ root_px[i % n_candidates]
        candidates_abs_px = np.concatenate([cand_map[c] for c in choices],
                                           axis=0)

        # find how many other things each candidate overlaps
        n_overlaps = np.zeros_like(candidates_abs_px[:, 0, 0])

        for k, candidate in enumerate(candidates_abs_px):
            cand_bbox = Bbox(candidate.T)

            # penalty for each time a box overlaps a path that's already
            # on the plot
            for ipth, path in enumerate(paths_px):
                if path.intersects_bbox(cand_bbox, filled=is_filled[ipth]):
                    n_overlaps[k] += 1

            # slightly larger penalty if we intersect a text box that we
            # just added to the plot
            for ipth, path in enumerate(bbox_paths_px):
                if path.intersects_bbox(cand_bbox, filled=is_filled[ipth]):
                    n_overlaps[k] += 5

            # big penalty if the candidate is out of the current view
            if not (ax.bbox.contains(*cand_bbox.min) and
                    ax.bbox.contains(*cand_bbox.max)):
                n_overlaps[k] += 100

        # sort candidates by distance between center of text box and magnet
        magnet_dist = np.linalg.norm(np.mean(candidates_abs_px, axis=-1)
                                     - magnet_px, axis=1)
        isorted = np.argsort(magnet_dist)
        magnet_dist = np.array(magnet_dist[isorted])
        candidates_abs_px = np.array(candidates_abs_px[isorted, :, :])
        n_overlaps = np.array(n_overlaps[isorted])
        root_dat = np.array(root_dat[isorted % n_candidates, :])
        root_px = np.array(root_px[isorted % n_candidates, :])

        # sort candidates so the ones with the fewest overlaps are first
        # but do it with a stable algorithm so among the best candidates,
        # choose the one closest to the magnet
        sargs = np.argsort(n_overlaps, kind='mergesort')

        # >>> debug >>>
        if dax is not None:
            for _candidate, n_overlap in zip(candidates_abs_px, n_overlaps):
                _cand_bbox = Bbox(_candidate.T)
                _x0 = _cand_bbox.get_points()[0]
                _bbox_center = np.mean(_candidate, axis=-1)
                _ray_x = [_bbox_center[0], magnet_px[0]]
                _ray_y = [_bbox_center[1], magnet_px[1]]
                dax.plot(_ray_x, _ray_y, '-', alpha=0.3, color='grey')
                _rect = mpatches.Rectangle(_x0, _cand_bbox.width,
                                           _cand_bbox.height, fill=False)
                dax.add_patch(_rect)
                plt.text(_x0[0], _x0[1], label, color='gray')
                plt.text(_x0[0], _x0[1], '{0}'.format(n_overlap))
        # <<< debug <<<

        # pick winning candidate and add its bounding box to this list of
        # paths to avoid
        winner_abs_px = candidates_abs_px[sargs[0], :, :]
        xy_root_px = root_px[sargs[0], :]
        xy_root_dat = np.array(root_dat[sargs[0], :])
        xy_txt_offset = np.array(winner_abs_px[:, 0] - xy_root_px)

        corners = Bbox(winner_abs_px.T).corners()[(0, 1, 3, 2), :]
        bbox_paths_px += [Path(corners)]

        # a = plt.annotate(label, xy=xy_root_dat, xycoords='data',
        #                  xytext=xy_txt_offset, textcoords="offset pixels",
        #                  color=color, **kwargs)
        a = ax.annotate(label, xy=xy_root_dat, xycoords='data',
                        xytext=xy_txt_offset, textcoords="offset pixels",
                        color=color, **kwargs)
        annotations.append(a)

    if rand_state is not None:
        np.random.set_state(rand_state)

    return annotations