Example #1
0
def input_skymap(order1, d_order, fraction):
    """Construct a test multi-resolution sky map, with values that are
    proportional to the NESTED pixel index.

    To make the test more interesting by mixing together multiple resolutions,
    part of the sky map is refined to a higher order.

    Parameters
    ----------
    order1 : int
        The HEALPix resolution order.
    d_order : int
        The increase in orer for part of the sky map.
    fraction : float
        The fraction of the original pixels to refine.

    """
    order2 = order1 + d_order
    npix1 = hp.nside2npix(hp.order2nside(order1))
    npix2 = hp.nside2npix(hp.order2nside(order2))
    ipix1 = np.arange(npix1)
    ipix2 = np.arange(npix2)

    # Create a random sky map.
    area = hp.nside2pixarea(hp.order2nside(order1))
    probdensity = np.random.uniform(0, 1, npix1)
    prob = probdensity * area
    normalization = prob.sum()
    prob /= normalization
    probdensity /= normalization
    distmean = np.random.uniform(100, 110, npix1)
    diststd = np.random.uniform(0, 1 / np.sqrt(3) - 0.1, npix1) * distmean
    distmu, distsigma, distnorm = moments_to_parameters(distmean, diststd)
    assert np.all(np.isfinite(distmu))

    data1 = table.Table({
        'UNIQ': moc.nest2uniq(order1, ipix1),
        'PROBDENSITY': probdensity,
        'DISTMU': distmu,
        'DISTSIGMA': distsigma,
        'DISTNORM': distnorm
    })

    # Add some upsampled pixels.
    data2 = table.Table(np.repeat(data1, npix2 // npix1))
    data2['UNIQ'] = moc.nest2uniq(order2, ipix2)
    n = int(npix1 * (1 - fraction))
    result = table.vstack((data1[:n], data2[n * npix2 // npix1:]))

    # Add marginal distance mean and standard deviation.
    rbar = (prob * distmean).sum()
    r2bar = (prob * (np.square(diststd) + np.square(distmean))).sum()
    result.meta['distmean'] = rbar
    result.meta['diststd'] = np.sqrt(r2bar - np.square(rbar))

    return result
Example #2
0
def test_rasterize_downsample(order_in, d_order_in, fraction_in, order_out):
    npix_in = hp.nside2npix(hp.order2nside(order_in))
    npix_out = hp.nside2npix(hp.order2nside(order_out))
    skymap_in = input_skymap(order_in, d_order_in, fraction_in)
    skymap_out = moc.rasterize(skymap_in, order_out)

    assert len(skymap_out) == npix_out
    reps = npix_in // npix_out
    expected = np.mean(np.arange(npix_in).reshape(-1, reps), axis=1)
    np.testing.assert_array_equal(skymap_out['VALUE'], expected)
Example #3
0
def test_rasterize_upsample(order_in, d_order_in, fraction_in, order_out):
    npix_in = hp.nside2npix(hp.order2nside(order_in))
    npix_out = hp.nside2npix(hp.order2nside(order_out))
    skymap_in = input_skymap(order_in, d_order_in, fraction_in)
    skymap_out = moc.rasterize(skymap_in, order_out)

    assert len(skymap_out) == npix_out
    ipix = np.arange(npix_in)
    reps = npix_out // npix_in
    for i in range(reps):
        np.testing.assert_array_equal(skymap_out['VALUE'][i::reps], ipix)
Example #4
0
def make_healpix_map_for_energy_band(energy_band, order):
    log.info(f'Making HEALPix map for energy band: {energy_band} and order: {order}')

    # Select events in energy band
    table = Table.read('input_data/fermi_hgps_events_selected.fits.gz', hdu=1)
    energy = table['ENERGY'].quantity.to('GeV').value
    mask = (energy_band['min'] <= energy) & (energy < energy_band['max'])
    table = table[mask]
    log.info(f'Number of events: {len(table)}')

    # Bin the events into a HEALPix counts map
    nside = hp.order2nside(order + shift_order)
    ipix = hp.ang2pix(
        nside=nside, nest=True, lonlat=True,
        theta=table['L'], phi=table['B'],
    )
    npix = hp.nside2npix(nside)
    log.debug(f'Number of pixels: {npix}')
    resolution = np.rad2deg(hp.nside2resol(nside))
    log.debug(f'Pixel resolution: {resolution} deg')
    image = np.bincount(ipix, minlength=npix)
    image = image.astype('float32')

    # TODO: smoothing the HEALPix map with default setting is very slow.
    # Maybe chunk the data into local WCS maps and then stitch back together?
    # For now: no smoothing
    # image = hp.smoothing(image, sigma=np.deg2rad(0.1))

    path = DATA_DIR / 'maps' / 'Fermi10GeV_healpix_maps' / 'energy_{min}_{max}.fits.gz'.format_map(energy_band)
    path.parent.mkdir(exist_ok=True, parents=True)
    log.info(f'Writing {path}')
    hp.write_map(str(path), image, coord='G', nest=True)
def _reconstruct_nested_breadthfirst(m, extra):
    m = np.asarray(m)
    max_npix = len(m)
    max_nside = hp.npix2nside(max_npix)
    max_order = hp.nside2order(max_nside)
    seen = np.zeros(max_npix, dtype=bool)

    for order in range(max_order + 1):
        nside = hp.order2nside(order)
        npix = hp.nside2npix(nside)
        skip = max_npix // npix
        if skip > 1:
            b = m.reshape(-1, skip)
            a = b[:, 0].reshape(-1, 1)
            b = b[:, 1:]
            aseen = seen.reshape(-1, skip)
            eq = ((a == b) | ((a != a) & (b != b))).all(1) & (~aseen).all(1)
        else:
            eq = ~seen
        for ipix in np.flatnonzero(eq):
            ipix0 = ipix * skip
            ipix1 = (ipix + 1) * skip
            seen[ipix0:ipix1] = True
            if extra:
                yield _HEALPixTreeVisitExtra(
                    nside, max_nside, ipix, ipix0, ipix1, m[ipix0])
            else:
                yield _HEALPixTreeVisit(nside, ipix)
Example #6
0
    def testCcdVisitHpix8(self):
        filters = "ugrizy"
        num_ccd_visits = 10
        cell_id = 2500
        chunker = Chunker(0, num_stripes, num_substripes)
        ccdVisitGenerator = columns.CcdVisitGenerator(chunker,
                                                      num_ccd_visits,
                                                      filters=filters)
        hpix8_values = ccdVisitGenerator._find_hpix8_in_cell(cell_id)
        print(hpix8_values)
        self.assertTrue(len(hpix8_values) > 0)

        nside = healpy.order2nside(8)
        chunks = [
            chunker.locate(healpy.pix2ang(nside, pixel, nest=True,
                                          lonlat=True))
            for pixel in hpix8_values
        ]
        hpix_centers_in_chunk = np.array(chunks) == cell_id
        # Some of the hpix centers will be outside of the chunk area, and that seems ok.
        # The test is to confirm that we get enough of them with centers inside the
        # chunk to confirm that the code is working.
        print(chunks)
        self.assertGreaterEqual(
            np.sum(hpix_centers_in_chunk) / float(len(hpix8_values)), 0.5)
Example #7
0
def _reconstruct_nested_breadthfirst(m, extra):
    max_npix = len(m)
    max_nside = hp.npix2nside(max_npix)
    max_order = hp.nside2order(max_nside)
    seen = np.zeros(max_npix, dtype=bool)

    for order in range(max_order + 1):
        nside = hp.order2nside(order)
        npix = hp.nside2npix(nside)
        skip = max_npix // npix
        if skip > 1:
            b = m.reshape(-1, skip)
            a = b[:, 0].reshape(-1, 1)
            b = b[:, 1:]
            aseen = seen.reshape(-1, skip)
            eq = ((a == b) | ((a != a) & (b != b))).all(1) & (~aseen).all(1)
        else:
            eq = ~seen
        for ipix in np.flatnonzero(eq):
            ipix0 = ipix * skip
            ipix1 = (ipix + 1) * skip
            seen[ipix0:ipix1] = True
            if extra:
                yield _HEALPixTreeVisitExtra(
                    nside, max_nside, ipix, ipix0, ipix1, m[ipix0])
            else:
                yield _HEALPixTreeVisit(nside, ipix)
Example #8
0
def best_order_for(pixel_scale):
    d = pixel_scale
    best_o = 1. / 2. * math.log(math.pi / (3. * d * d)) / math.log(2)
    o = int(best_o + 1)
    logging.info('pixel scale (arcsec): {:.4f} -> {:.4f}'.format(
        rad2arcsec(pixel_scale),
        rad2arcsec(healpy.nside2resol(healpy.order2nside(o)))))
    return o
Example #9
0
 def mapped_tile(self, out_file, tile_order, tile_index, pixel_order):
     tile_size_order = pixel_order - tile_order
     tile_nside = healpy.order2nside(tile_size_order)
     pixels_per_tile = tile_nside * tile_nside
     pixel_nside = healpy.order2nside(pixel_order)
     pixel_index_start = tile_index << (2 * tile_size_order)
     pixel_indices = numpy.arange(
         pixel_index_start,
         pixel_index_start + pixels_per_tile)[hips.nest2ring(tile_nside)]
     THETA, PHI = healpy.pix2ang(pixel_nside, pixel_indices, nest=True)
     RA = numpy.rad2deg(PHI)
     DEC = numpy.rad2deg(numpy.pi / 2 - THETA)
     X, Y = self.wcs.wcs_world2pix(RA, DEC, 0)
     mapped = interpolate.linear(self.hdu.data, X, Y,
                                 dtype=numpy.float32).reshape(
                                     (tile_nside, tile_nside))
     return mapped
def input_skymap(order1, d_order, fraction):
    """Construct a test multi-resolution sky map, with values that are
    proportional to the NESTED pixel index.

    To make the test more interesting by mixing together multiple resolutions,
    part of the sky map is refined to a higher order.

    Parameters
    ----------
    order1 : int
        The HEALPix resolution order.
    d_order : int
        The increase in orer for part of the sky map.
    fraction : float
        The fraction of the original pixels to refine.

    """
    order2 = order1 + d_order
    npix1 = hp.nside2npix(hp.order2nside(order1))
    npix2 = hp.nside2npix(hp.order2nside(order2))
    ipix1 = np.arange(npix1)
    ipix2 = np.arange(npix2)

    data1 = table.Table({
        'UNIQ': moc.nest2uniq(order1, ipix1),
        'VALUE': ipix1.astype(float),
        'VALUE2': np.pi * ipix1.astype(float)
    })

    data2 = table.Table({
        'UNIQ':
        moc.nest2uniq(order2, ipix2),
        'VALUE':
        np.repeat(ipix1, npix2 // npix1).astype(float),
        'VALUE2':
        np.pi * np.repeat(ipix1, npix2 // npix1).astype(float)
    })

    n = int(npix1 * (1 - fraction))
    return table.vstack((data1[:n], data2[n * npix2 // npix1:]))
Example #11
0
    def apply_hpxmoc(self, lon=None, lat=None):

        try:
            moc = MOC()
            moc.read(self.filename, filetype="fits")
        except:
            raise Exception(f"Unable to find/open Healpix MOC file: {self.filename}")

        # get the moc nside at the max resolution of the MOC
        nside = hp.order2nside(moc.order)
        #get the healpix pixels indices of the targets at the max resolution
        idx = hp.ang2pix(nside, lon, lat, lonlat=True, nest=True )

        m_mask = moc.contains(idx)
        return m_mask
Example #12
0
def warp_source_files(fnames, work_dir, pixel_order, tile_order):
    tile_nside = healpy.order2nside(tile_order)
    for fname in fnames:
        with afits.open(fname) as hdul:
            src = Source(find_image_hdu(hdul))
            tile_indices = healpy.query_polygon(tile_nside,
                                                ad2xyz(*src.polygon).T,
                                                nest=True)
            logging.info('{} tiles'.format(len(tile_indices)))

            def warp(tile_index):
                logging.info('warping {}:{}...'.format(fname, tile_index))
                src.warp(work_dir, tile_order, tile_index, pixel_order)

            parallel.map(warp, tile_indices)
Example #13
0
def get_pixlist_for_fields(fields, radius=2, level=12):
    import healpy
    pixlist = []
    radius = np.radians(radius)
    nside = healpy.order2nside(level)
    for ra, dec in fields:
        vec = healpy.ang2vec(ra, dec, lonlat=True)
        pixlist.append(
            healpy.query_disc(nside,
                              vec,
                              radius,
                              inclusive=True,
                              fact=4,
                              nest=True))
    pixlist = np.hstack(pixlist)
    return np.unique(pixlist)
Example #14
0
def get_pixlist(level=7, max_declination=30., plot=False):
    """
    """
    nside = hp.order2nside(level)
    npix = hp.nside2npix(nside)
    ipix = np.arange(npix)
    theta, phi = hp.pix2ang(nside, ipix, nest=True)
    #    dec = -theta * 180. / np.pi + 90.
    dec = -np.degrees(theta - np.pi / 2.)
    idx = dec < 35.

    if plot:
        m = np.zeros(npix)
        m[idx] = 1.
        hp.mollview(m, nest=True)
    return ipix[idx]
Example #15
0
    def _find_hpix8_in_cell(self, chunk_id):
        chunk_box = self.chunker.getChunkBounds(chunk_id)
        grid_size = 9
        lon_arr = np.linspace(chunk_box.getLon().getA().asDegrees(),
                              chunk_box.getLon().getB().asDegrees(), grid_size)
        lat_arr = np.linspace(chunk_box.getLat().getA().asDegrees(),
                              chunk_box.getLat().getB().asDegrees(), grid_size)
        xx, yy = np.meshgrid(lon_arr[1::-2], lat_arr[1::-2])
        nside = healpy.order2nside(8)
        trial_healpix = healpy.ang2pix(nside,
                                       lon_arr,
                                       lat_arr,
                                       nest=True,
                                       lonlat=True)

        return list(set(trial_healpix))
Example #16
0
def fetch(survey, url, geocentric_coords, order, **kwargs):
    """
    Fetch tile from a HiPS server. Performs conversion as well.
    Parameters
    ----------
    survey : str
        Suvery where HiPS is hosted
    url : str
        URL excluding survey
    geocentric_coords : list(int)
        Contains WCS coordinates 
    order : int
        HiPS order 
    """

    # Rule followed: Tile N in order K -> NorderK / DirD / NpixN{.ext}
    nside = hp.order2nside(order)
    c = SkyCoord.from_name('crab')
    theta = (np.pi / 2) - c.dec.radian
    phi = c.ra.radian
    npixel = hp.ang2pix(nside, theta, phi)
    directory = np.around(int(npixel), decimals=-(len(str(npixel)) - 1))

    base_url = url + survey \
        + '/Norder' + str(order) + '/Dir' + str(directory) + \
        '/Npix' + str(npixel) + kwargs['format']

    # URL: Crab image
    # http://alasky.unistra.fr/DSS/DSSColor/Norder6/Dir20000/Npix24185.jpg

    # base_url = 'http://cade.irap.omp.eu/documents/Ancillary/4Aladin/AKARI_WideS/Norder3/Dir0/Npix346.' + \
    #     kwargs['format']

    if (kwargs['package'] is 'urllib'):
        import urllib.request

        data = urllib.request.urlopen(base_url).read()
    elif (kwargs['package'] is 'requests'):
        import requests

        data = requests.get(base_url).content

    print('WCS : ', ang2WCS(theta, phi))

    return data
Example #17
0
def make_healpix_map_for_energy_band(energy_band, order):
    log.info(
        f'Making HEALPix map for energy band: {energy_band} and order: {order}'
    )

    # Select events in energy band
    table = Table.read('input_data/fermi_hgps_events_selected.fits.gz', hdu=1)
    energy = table['ENERGY'].quantity.to('GeV').value
    mask = (energy_band['min'] <= energy) & (energy < energy_band['max'])
    table = table[mask]
    log.info(f'Number of events: {len(table)}')

    # Bin the events into a HEALPix counts map
    nside = hp.order2nside(order + shift_order)
    ipix = hp.ang2pix(
        nside=nside,
        nest=True,
        lonlat=True,
        theta=table['L'],
        phi=table['B'],
    )
    npix = hp.nside2npix(nside)
    log.debug(f'Number of pixels: {npix}')
    resolution = np.rad2deg(hp.nside2resol(nside))
    log.debug(f'Pixel resolution: {resolution} deg')
    image = np.bincount(ipix, minlength=npix)
    image = image.astype('float32')

    # TODO: smoothing the HEALPix map with default setting is very slow.
    # Maybe chunk the data into local WCS maps and then stitch back together?
    # For now: no smoothing
    # image = hp.smoothing(image, sigma=np.deg2rad(0.1))

    path = DATA_DIR / 'maps' / 'Fermi10GeV_healpix_maps' / 'energy_{min}_{max}.fits.gz'.format_map(
        energy_band)
    path.parent.mkdir(exist_ok=True, parents=True)
    log.info(f'Writing {path}')
    hp.write_map(str(path), image, coord='G', nest=True)
 def cone_search(self, center: SkyCoord, radius: Angle) -> Iterator[int]:
     """
     cone_search retrieves the Candidate IDs for alerts that can be found in
     a region of the sky.
     """
     if center.representation_type != CartesianRepresentation:
         center = center.replicate(
             representation_type=CartesianRepresentation)
     pixels = healpy.query_disc(
         nside=healpy.order2nside(self.order),
         vec=(center.x.value, center.y.value, center.z.value),
         radius=radius.degree,
         inclusive=True,
         nest=True,
     )
     logger.info("found %d pixels which might match", len(pixels))
     ranges = self._compact_pixel_ranges(pixels)
     logger.info("compacted range into %d elements", len(ranges))
     for start, stop in ranges:
         logger.info("checking range %d to %d", start, stop)
         for pixel in self.healpixels.iterate(start, stop):
             for candidate_id in pixel:
                 yield candidate_id
Example #19
0
    ranking_samples = hist_samples = samples

# Place the histogram bins.
theta = 0.5 * np.pi - hist_samples['dec']
phi = hist_samples['ra']
p = adaptive_healpix_histogram(theta,
                               phi,
                               opts.samples_per_bin,
                               nside=opts.nside,
                               max_nside=opts.max_nside,
                               nest=True)

# Evaluate per-pixel density.
p = derasterize(Table([p], names=['PROB']))
order, ipix = moc.uniq2nest(p['UNIQ'])
nside = hp.order2nside(order.astype(int))
max_order = order.max().astype(int)
max_nside = hp.order2nside(max_order)
theta = 0.5 * np.pi - ranking_samples['dec']
phi = ranking_samples['ra']
ranking_samples['ipix'] = hp.ang2pix(max_nside, theta, phi, nest=True)
ranking_samples.sort('ipix')
result = np.transpose(
    [pixstats(ranking_samples, max_nside, n, i) for n, i in zip(nside, ipix)])

# Add distance info if necessary.
if 'dist' in ranking_samples.colnames:
    p['PROBDENSITY'], distmean, diststd = result
    p['DISTMU'], p['DISTSIGMA'], p['DISTNORM'] = \
        distance.moments_to_parameters(distmean, diststd)
Example #20
0
def test_rasterize_default(order):
    npix = hp.nside2npix(hp.order2nside(order))
    skymap_in = input_skymap(order, 0, 0)
    skymap_out = moc.rasterize(skymap_in)
    assert len(skymap_out) == npix
def main(args):
    # tr = tracker.SummaryTracker()
    sdt, stars = load_hipparcos_cat()
    # These will be the stars we see in data
    source_star_table = sdt[sdt.vmag < 6.5]
    source_stars = stars[sdt.vmag < 6.5]

    # These are the stars we use to identify the patch of sky in
    # the FOV
    cvmag_lim = 6.1
    # Define telescope frame
    location = EarthLocation.from_geodetic(lon=14.974609, lat=37.693267, height=1750)
    obstime = Time("2019-05-09T01:37:54.728026")
    altaz_frame = AltAz(location=location, obstime=obstime)

    # Get pixel coordinates from TargetCalib

    camera_config = CameraConfiguration("1.1.0")
    mapping = camera_config.GetMapping()
    pos = np.array(
        [np.array(mapping.GetXPixVector()), np.array(mapping.GetYPixVector())]
    )
    focal_length = u.Quantity(2.15191, u.m)

    matcher = StarPatternMatch.from_location(
        altaz_frame=altaz_frame,
        stars=stars,
        sdt=sdt,
        fov=12,
        focal_length=focal_length,
        min_alt=-90,
        vmag_lim=cvmag_lim,
        pixsize=mapping.GetSize(),
    )
    matcher.silence = True

    order = 10
    nside = healpy.order2nside(order)
    npix = healpy.nside2npix(nside)
    npixs_above_horizon = np.where(
        healpy.pix2ang(nside, np.arange(npix))[0] > np.pi / 2
    )[0]

    tstamp0 = 1557360406
    np.random.seed(args.seed)
    context = zmq.Context()
    socket = context.socket(zmq.PUB)
    ip = "127.0.0.1"
    con_str = "tcp://%s:" % ip + str(args.port)
    socket.connect(con_str)

    obstime = Time(tstamp0 + np.random.uniform(-43200, 43200), format="unix")
    pixsize = mapping.GetSize()
    for i in range(args.n_iterations):
        print(f"Sample: {i}", flush=True)
        tmp_timestamp = tstamp0 + np.random.uniform(-43200, 43200)
        obstime = Time(tmp_timestamp, format="unix")
        altaz_frame = AltAz(location=location, obstime=obstime)
        pixid = int(np.random.uniform(0, npixs_above_horizon.shape[0]))
        ang = healpy.pix2ang(nside, pixid)
        alt = ang[0]
        az = ang[1]
        hotspots, tel_pointing, star_ind, hips_in_fov, all_hips = generate_hotspots(
            alt * u.rad,
            az * u.rad,
            altaz_frame,
            source_stars,
            source_star_table,
            pixsize,
            cvmag_lim,
            pos,
        )

        true_hotspots = np.array(hotspots)
        frame = Frame()
        frame.add("hips_in_fov", np.array(hips_in_fov))
        frame.add("hotspots", true_hotspots)
        tel_sky_pointing = tel_pointing.transform_to("icrs")
        frame.add(
            "tel_pointing",
            np.array([tel_sky_pointing.ra.rad, tel_sky_pointing.dec.rad]),
        )

        hotspots = true_hotspots.copy()
        N_change = 1
        # hotspots[N_change, :] = hotspots[N_change, :] + 0.003

        matched_hs = matcher.identify_stars(
            hotspots, horizon_level=0, obstime=obstime, only_one_it=False
        )


        if matched_hs is not None and len(matched_hs) > 0:
            ra, dec = matcher.determine_pointing(matched_hs)
            frame.add("matched_hs", np.array(matched_hs))
            frame.add("est_pointing", np.array([ra, dec]))
        else:
            print(tmp_timestamp, alt, az)

        matched_hs = matcher.identify_stars(
            hotspots, horizon_level=0, obstime=obstime, only_one_it=True
        )
        match = np.array(matched_hs)
        match_quantity = match[:, 2] * match[:, 3] * match[:, 1]
        index = np.where(match[:, 0] == all_hips[0][1])[0]
        matched_match = np.argmax(match_quantity)
        frame.add('true_match', match[index])
        frame.add('matched_match', match[matched_match])
        frame.add('match_quantity_list', match)

        socket.send(frame.serialize())
    ranking_samples = Table(ranking_samples)
    hist_samples = Table(samples)
else:
    ranking_samples = hist_samples = samples

# Place the histogram bins.
theta = 0.5*np.pi - hist_samples['dec']
phi = hist_samples['ra']
p = adaptive_healpix_histogram(
    theta, phi, opts.samples_per_bin,
    nside=opts.nside, max_nside=opts.max_nside, nest=True)

# Evaluate per-pixel density.
p = derasterize(Table([p], names=['PROB']))
order, ipix = moc.uniq2nest(p['UNIQ'])
nside = hp.order2nside(order.astype(int))
max_order = order.max().astype(int)
max_nside = hp.order2nside(max_order)
theta = 0.5*np.pi - ranking_samples['dec']
phi = ranking_samples['ra']
ranking_samples['ipix'] = hp.ang2pix(max_nside, theta, phi, nest=True)
ranking_samples.sort('ipix')
result = np.transpose(
    [pixstats(ranking_samples, max_nside, n, i) for n, i in zip(nside, ipix)])

# Add distance info if necessary.
if 'dist' in ranking_samples.colnames:
    p['PROBDENSITY'], distmean, diststd = result
    p['DISTMU'], p['DISTSIGMA'], p['DISTNORM'] = \
        distance.moments_to_parameters(distmean, diststd)
Example #23
0
def find_injection_moc(sky_map, true_ra=None, true_dec=None, true_dist=None,
                       contours=(), areas=(), modes=False, nest=False):
    """
    Given a sky map and the true right ascension and declination (in radians),
    find the smallest area in deg^2 that would have to be searched to find the
    source, the smallest posterior mass, and the angular offset in degrees from
    the true location to the maximum (mode) of the posterior. Optionally, also
    compute the areas of and numbers of modes within the smallest contours
    containing a given total probability.
    """

    if (true_ra is None) ^ (true_dec is None):
        raise ValueError('Both true_ra and true_dec must be provided or None')

    contours = np.asarray(contours)

    distmean = sky_map.meta.get('distmean', np.nan)

    # Sort the pixels by descending posterior probability.
    sky_map = np.flipud(np.sort(sky_map, order='PROBDENSITY'))

    # Find the pixel that contains the injection.
    order, ipix = moc.uniq2nest(sky_map['UNIQ'])
    max_order = np.max(order)
    max_nside = hp.order2nside(max_order)
    max_ipix = ipix << np.uint64(2 * (max_order - order))
    ipix = ipix.astype(np.int64)
    max_ipix = max_ipix.astype(np.int64)
    if true_ra is not None:
        true_theta = 0.5 * np.pi - true_dec
        true_phi = true_ra
        true_pix = hp.ang2pix(max_nside, true_theta, true_phi, nest=True)
        i = np.argsort(max_ipix)
        true_idx = i[np.digitize(true_pix, max_ipix[i]) - 1]

    # Find the angular offset between the mode and true locations.
    mode_theta, mode_phi = hp.pix2ang(
        hp.order2nside(order[0]), ipix[0].astype(np.int64), nest=True)
    if true_ra is None:
        offset = np.nan
    else:
        offset = np.rad2deg(
            angle_distance(true_theta, true_phi, mode_theta, mode_phi))

    # Calculate the cumulative area in deg2 and the cumulative probability.
    dA = moc.uniq2pixarea(sky_map['UNIQ'])
    dP = sky_map['PROBDENSITY'] * dA
    prob = np.cumsum(dP)
    area = np.cumsum(dA) * np.square(180 / np.pi)

    # Construct linear interpolants to map between probability and area.
    # This allows us to compute more accurate contour areas and probabilities
    # under the approximation that the pixels have constant probability
    # density.
    prob_padded = np.concatenate(([0], prob))
    area_padded = np.concatenate(([0], area))
    # FIXME: we should use the assume_sorted=True argument below, but
    # it was added in Scipy 0.14.0, and our Scientific Linux 7 clusters
    # only have Scipy 0.12.1.
    prob_for_area = interp1d(area_padded, prob_padded)
    area_for_prob = interp1d(prob_padded, area_padded)

    if true_ra is None:
        searched_area = searched_prob = np.nan
    else:
        # Find the smallest area that would have to be searched to find
        # the true location.
        searched_area = area[true_idx]

        # Find the smallest posterior mass that would have to be searched to find
        # the true location.
        searched_prob = prob[true_idx]

    # Find the contours of the given credible levels.
    contour_idxs = np.digitize(contours, prob) - 1

    # For each of the given confidence levels, compute the area of the
    # smallest region containing that probability.
    contour_areas = area_for_prob(contours).tolist()

    # For each listed area, find the probability contained within the
    # smallest credible region of that area.
    area_probs = prob_for_area(areas).tolist()

    if modes:
        if true_ra is None:
            searched_modes = np.nan
        else:
            # Count up the number of modes in each of the given contours.
            searched_modes = count_modes_moc(sky_map['UNIQ'], true_idx)
        contour_modes = [
            count_modes_moc(sky_map['UNIQ'], i) for i in contour_idxs]
    else:
        searched_modes = np.nan
        contour_modes = np.nan

    # Distance stats now...
    if 'DISTMU' in sky_map.dtype.names:
        probdensity = sky_map['PROBDENSITY']
        mu = sky_map['DISTMU']
        sigma = sky_map['DISTSIGMA']
        norm = sky_map['DISTNORM']
        args = (dP, mu, sigma, norm)
        if true_dist is None:
            searched_prob_dist = np.nan
        else:
            searched_prob_dist = distance.marginal_cdf(true_dist, *args)
        # FIXME: old verisons of Numpy can't handle passing zero-length
        # arrays to generalized ufuncs. Remove this workaround once LIGO
        # Data Grid clusters provide a more modern version of Numpy.
        if len(contours) == 0:
            contour_dists = []
        else:
            lo, hi = distance.marginal_ppf(
                np.row_stack((
                    0.5 * (1 - contours),
                    0.5 * (1 + contours)
                )), *args)
            contour_dists = (hi - lo).tolist()

        # Set up distance grid.
        n_r = 1000
        max_r = max(6 * distmean, np.max(true_dist))
        d_r = max_r / n_r
        r = d_r * np.arange(1, n_r)

        # Calculate volume of frustum-shaped voxels with distance centered on r
        # and extending from (r - d_r) to (r + d_r).
        dV = (np.square(r) + np.square(d_r) / 12) * d_r * dA.reshape(-1, 1)

        # Calculate probability within each voxel.
        dP = probdensity.reshape(-1, 1) * dV * np.exp(
            -0.5 * np.square(
                (r.reshape(1, -1) - mu.reshape(-1, 1)) / sigma.reshape(-1, 1)
            )
        ) * (norm / sigma).reshape(-1, 1) / np.sqrt(2 * np.pi)
        dP[np.isnan(dP)] = 0  # Suppress invalid values

        # Calculate probability density per unit volume.
        dP_dV = dP / dV
        i = np.flipud(np.argsort(dP_dV.ravel()))

        P_flat = np.cumsum(dP.ravel()[i])
        V_flat = np.cumsum(dV.ravel()[i])

        contour_vols = interp1d(
            P_flat, V_flat, bounds_error=False)(contours).tolist()
        P = np.empty_like(P_flat)
        V = np.empty_like(V_flat)
        P[i] = P_flat
        V[i] = V_flat
        P = P.reshape(dP.shape)
        V = V.reshape(dV.shape)
        if true_dist is None:
            searched_vol = searched_prob_vol = np.nan
        else:
            i_radec = true_idx
            i_dist = np.digitize(true_dist, r) - 1
            searched_prob_vol = P[i_radec, i_dist]
            searched_vol = V[i_radec, i_dist]
    else:
        searched_vol = searched_prob_vol = searched_prob_dist = np.nan
        contour_dists = [np.nan] * len(contours)
        contour_vols = [np.nan] * len(contours)

    # Done.
    return FoundInjection(
        searched_area, searched_prob, offset, searched_modes, contour_areas,
        area_probs, contour_modes, searched_prob_dist, contour_dists,
        searched_vol, searched_prob_vol, contour_vols)
 def flat_bitmap(self):
     """Return flattened HEALPix representation."""
     m = np.empty(hp.nside2npix(hp.order2nside(self.order)))
     for nside, full_nside, ipix, ipix0, ipix1, samples in self.visit():
         m[ipix0:ipix1] = len(samples) / hp.nside2pixarea(nside)
     return m
"""
from __future__ import division

import numpy as np
import healpy as hp
import collections
import itertools

__all__ = ('HEALPIX_MACHINE_ORDER', 'HEALPIX_MACHINE_NSIDE', 'HEALPixTree',
           'adaptive_healpix_histogram', 'interpolate_nested',
           'reconstruct_nested')


# Maximum 64-bit HEALPix resolution.
HEALPIX_MACHINE_ORDER = 29
HEALPIX_MACHINE_NSIDE = hp.order2nside(HEALPIX_MACHINE_ORDER)


_HEALPixTreeVisitExtra = collections.namedtuple(
    'HEALPixTreeVisit', 'nside full_nside ipix ipix0 ipix1 value')


_HEALPixTreeVisit = collections.namedtuple(
    'HEALPixTreeVisit', 'nside ipix')


class HEALPixTree(object):
    """Data structure used internally by the function
    adaptive_healpix_histogram()."""

    def __init__(
Example #26
0
def find_injection_moc(sky_map,
                       true_ra,
                       true_dec,
                       contours=(),
                       areas=(),
                       modes=False,
                       nest=False):
    """
    Given a sky map and the true right ascension and declination (in radians),
    find the smallest area in deg^2 that would have to be searched to find the
    source, the smallest posterior mass, and the angular offset in degrees from
    the true location to the maximum (mode) of the posterior. Optionally, also
    compute the areas of and numbers of modes within the smallest contours
    containing a given total probability.
    """

    # Sort the pixels by descending posterior probability.
    sky_map = np.flipud(np.sort(sky_map, order='PROBDENSITY'))

    # Find the pixel that contains the injection.
    order, ipix = moc.uniq2nest(sky_map['UNIQ'])
    max_order = np.max(order)
    max_nside = hp.order2nside(max_order)
    max_ipix = ipix << np.uint64(2 * (max_order - order))
    ipix = ipix.astype(np.int64)
    max_ipix = max_ipix.astype(np.int64)
    true_theta = 0.5 * np.pi - true_dec
    true_phi = true_ra
    true_pix = hp.ang2pix(max_nside, true_theta, true_phi, nest=True)
    # At this point, we could sort the dataset by max_ipix and then do a binary
    # (e.g., np.searchsorted) to find true_pix in max_ipix. However, would be
    # slower than the linear search below because the sort would be N log N.
    i = np.flatnonzero(max_ipix <= true_pix)
    true_idx = i[np.argmax(max_ipix[i])]

    # Find the angular offset between the mode and true locations.
    mode_theta, mode_phi = hp.pix2ang(hp.order2nside(order[0]),
                                      ipix[0].astype(np.int64),
                                      nest=True)
    offset = np.rad2deg(
        angle_distance(true_theta, true_phi, mode_theta, mode_phi))

    # Calculate the cumulative area in deg2 and the cumulative probability.
    area = moc.uniq2pixarea(sky_map['UNIQ'])
    prob = np.cumsum(sky_map['PROBDENSITY'] * area)
    area = np.cumsum(area) * np.square(180 / np.pi)

    # Construct linear interpolants to map between probability and area.
    # This allows us to compute more accurate contour areas and probabilities
    # under the approximation that the pixels have constant probability
    # density.
    prob_padded = np.concatenate(([0], prob))
    area_padded = np.concatenate(([0], area))
    # FIXME: we should use the assume_sorted=True argument below, but
    # it was added in Scipy 0.14.0, and our Scientific Linux 7 clusters
    # only have Scipy 0.12.1.
    prob_for_area = interp1d(area_padded, prob_padded)
    area_for_prob = interp1d(prob_padded, area_padded)

    # Find the smallest area that would have to be searched to find
    # the true location.
    searched_area = area[true_idx]

    # Find the smallest posterior mass that would have to be searched to find
    # the true location.
    searched_prob = prob[true_idx]

    # Find the contours of the given credible levels.
    contour_idxs = np.searchsorted(prob, contours)

    # For each of the given confidence levels, compute the area of the
    # smallest region containing that probability.
    contour_areas = area_for_prob(contours).tolist()

    # For each listed area, find the probability contained within the
    # smallest credible region of that area.
    area_probs = prob_for_area(areas).tolist()

    if modes:
        # Count up the number of modes in each of the given contours.
        searched_modes = count_modes_moc(sky_map['UNIQ'], true_idx)
        contour_modes = [
            count_modes_moc(sky_map['UNIQ'], i) for i in contour_idxs
        ]
    else:
        searched_modes = None
        contour_modes = None

    # Done.
    return FoundInjection(searched_area, searched_prob, offset, searched_modes,
                          contour_areas, area_probs, contour_modes)
Example #27
0
def find_injection_moc(sky_map, true_ra=None, true_dec=None, true_dist=None,
                       contours=(), areas=(), modes=False, nest=False):
    """
    Given a sky map and the true right ascension and declination (in radians),
    find the smallest area in deg^2 that would have to be searched to find the
    source, the smallest posterior mass, and the angular offset in degrees from
    the true location to the maximum (mode) of the posterior. Optionally, also
    compute the areas of and numbers of modes within the smallest contours
    containing a given total probability.
    """

    if (true_ra is None) ^ (true_dec is None):
        raise ValueError('Both true_ra and true_dec must be provided or None')

    contours = np.asarray(contours)

    distmean = sky_map.meta.get('distmean', np.nan)

    # Sort the pixels by descending posterior probability.
    sky_map = np.flipud(np.sort(sky_map, order='PROBDENSITY'))

    # Find the pixel that contains the injection.
    order, ipix = moc.uniq2nest(sky_map['UNIQ'])
    max_order = np.max(order)
    max_nside = hp.order2nside(max_order)
    max_ipix = ipix << np.uint64(2 * (max_order - order))
    ipix = ipix.astype(np.int64)
    max_ipix = max_ipix.astype(np.int64)
    if true_ra is not None:
        true_theta = 0.5 * np.pi - true_dec
        true_phi = true_ra
        true_pix = hp.ang2pix(max_nside, true_theta, true_phi, nest=True)
        i = np.argsort(max_ipix)
        true_idx = i[digitize(true_pix, max_ipix[i]) - 1]

    # Find the angular offset between the mode and true locations.
    mode_theta, mode_phi = hp.pix2ang(
        hp.order2nside(order[0]), ipix[0].astype(np.int64), nest=True)
    if true_ra is None:
        offset = np.nan
    else:
        offset = np.rad2deg(
            angle_distance(true_theta, true_phi, mode_theta, mode_phi))

    # Calculate the cumulative area in deg2 and the cumulative probability.
    dA = moc.uniq2pixarea(sky_map['UNIQ'])
    dP = sky_map['PROBDENSITY'] * dA
    prob = np.cumsum(dP)
    area = np.cumsum(dA) * np.square(180 / np.pi)

    # Construct linear interpolants to map between probability and area.
    # This allows us to compute more accurate contour areas and probabilities
    # under the approximation that the pixels have constant probability
    # density.
    prob_padded = np.concatenate(([0], prob))
    area_padded = np.concatenate(([0], area))
    # FIXME: we should use the assume_sorted=True argument below, but
    # it was added in Scipy 0.14.0, and our Scientific Linux 7 clusters
    # only have Scipy 0.12.1.
    prob_for_area = interp1d(area_padded, prob_padded)
    area_for_prob = interp1d(prob_padded, area_padded)

    if true_ra is None:
        searched_area = searched_prob = np.nan
    else:
        # Find the smallest area that would have to be searched to find
        # the true location.
        searched_area = area[true_idx]

        # Find the smallest posterior mass that would have to be searched to find
        # the true location.
        searched_prob = prob[true_idx]

    # Find the contours of the given credible levels.
    contour_idxs = digitize(contours, prob) - 1

    # For each of the given confidence levels, compute the area of the
    # smallest region containing that probability.
    contour_areas = area_for_prob(contours).tolist()

    # For each listed area, find the probability contained within the
    # smallest credible region of that area.
    area_probs = prob_for_area(areas).tolist()

    if modes:
        if true_ra is None:
            searched_modes = np.nan
        else:
            # Count up the number of modes in each of the given contours.
            searched_modes = count_modes_moc(sky_map['UNIQ'], true_idx)
        contour_modes = [
            count_modes_moc(sky_map['UNIQ'], i) for i in contour_idxs]
    else:
        searched_modes = np.nan
        contour_modes = np.nan

    # Distance stats now...
    if 'DISTMU' in sky_map.dtype.names:
        probdensity = sky_map['PROBDENSITY']
        mu = sky_map['DISTMU']
        sigma = sky_map['DISTSIGMA']
        norm = sky_map['DISTNORM']
        args = (dP, mu, sigma, norm)
        if true_dist is None:
            searched_prob_dist = np.nan
        else:
            searched_prob_dist = distance.marginal_cdf(true_dist, *args)
        # FIXME: old verisons of Numpy can't handle passing zero-length
        # arrays to generalized ufuncs. Remove this workaround once LIGO
        # Data Grid clusters provide a more modern version of Numpy.
        if len(contours) == 0:
            contour_dists = []
        else:
            lo, hi = distance.marginal_ppf(
                np.row_stack((
                    0.5 * (1 - contours),
                    0.5 * (1 + contours)
                )), *args)
            contour_dists = (hi - lo).tolist()

        # Set up distance grid.
        n_r = 1000
        max_r = 6 * distmean
        if true_dist is not None and true_dist > max_r:
            max_r = true_dist
        d_r = max_r / n_r
        r = d_r * np.arange(1, n_r)

        # Calculate volume of frustum-shaped voxels with distance centered on r
        # and extending from (r - d_r) to (r + d_r).
        dV = (np.square(r) + np.square(d_r) / 12) * d_r * dA.reshape(-1, 1)

        # Calculate probability within each voxel.
        dP = probdensity.reshape(-1, 1) * dV * np.exp(
            -0.5 * np.square(
                (r.reshape(1, -1) - mu.reshape(-1, 1)) / sigma.reshape(-1, 1)
            )
        ) * (norm / sigma).reshape(-1, 1) / np.sqrt(2 * np.pi)
        dP[np.isnan(dP)] = 0  # Suppress invalid values

        # Calculate probability density per unit volume.
        dP_dV = dP / dV
        i = np.flipud(np.argsort(dP_dV.ravel()))

        P_flat = np.cumsum(dP.ravel()[i])
        V_flat = np.cumsum(dV.ravel()[i])

        contour_vols = interp1d(
            P_flat, V_flat, bounds_error=False)(contours).tolist()
        P = np.empty_like(P_flat)
        V = np.empty_like(V_flat)
        P[i] = P_flat
        V[i] = V_flat
        P = P.reshape(dP.shape)
        V = V.reshape(dV.shape)
        if true_dist is None:
            searched_vol = searched_prob_vol = np.nan
        else:
            i_radec = true_idx
            i_dist = digitize(true_dist, r) - 1
            searched_prob_vol = P[i_radec, i_dist]
            searched_vol = V[i_radec, i_dist]
    else:
        searched_vol = searched_prob_vol = searched_prob_dist = np.nan
        contour_dists = [np.nan] * len(contours)
        contour_vols = [np.nan] * len(contours)

    # Done.
    return FoundInjection(
        searched_area, searched_prob, offset, searched_modes, contour_areas,
        area_probs, contour_modes, searched_prob_dist, contour_dists,
        searched_vol, searched_prob_vol, contour_vols)
Example #28
0
"""
from __future__ import division
__author__ = "Leo Singer <*****@*****.**>"


import numpy as np
import healpy as hp
import collections
import itertools
import lal
import lalsimulation


# Maximum 64-bit HEALPix resolution.
HEALPIX_MACHINE_ORDER = 29
HEALPIX_MACHINE_NSIDE = hp.order2nside(HEALPIX_MACHINE_ORDER)


def nside2order(nside):
    """Convert lateral HEALPix resolution to order.
    FIXME: see https://github.com/healpy/healpy/issues/163"""
    order = np.log2(nside)
    int_order = int(order)
    if order != int_order:
        raise ValueError('not a valid value for nside: {0}'.format(nside))
    return int_order


def order2nside(order):
    return 1 << order
Example #29
0
 def flat_bitmap(self):
     """Return flattened HEALPix representation."""
     m = np.empty(hp.nside2npix(hp.order2nside(self.order)))
     for nside, full_nside, ipix, ipix0, ipix1, samples in self.visit():
         m[ipix0:ipix1] = len(samples) / hp.nside2pixarea(nside)
     return m
Example #30
0
def crossmatch(sky_map, coordinates=None,
               contours=(), areas=(), modes=False, cosmology=False):
    """
    Given a sky map and the true right ascension and declination (in radians),
    find the smallest area in deg^2 that would have to be searched to find the
    source, the smallest posterior mass, and the angular offset in degrees from
    the true location to the maximum (mode) of the posterior. Optionally, also
    compute the areas of and numbers of modes within the smallest contours
    containing a given total probability.

    Parameters
    ----------
    sky_map : :class:`astropy.table.Table`
        A multiresolution sky map, as returned by
        :func:`ligo.skymap.io.fits.read_sky_map` called with the keyword
        argument ``moc=True``.

    coordinates : :class:`astropy.coordinates.SkyCoord`, optional
        The catalog of target positions to match against.

    contours : :class:`tuple`, optional
        Credible levels between 0 and 1. If this argument is present, then
        calculate the areas and volumes of the 2D and 3D credible regions that
        contain these probabilities. For example, for ``contours=(0.5, 0.9)``,
        then areas and volumes of the 50% and 90% credible regions.

    areas : :class:`tuple`, optional
        Credible areas in square degrees. If this argument is present, then
        calculate the probability contained in the 2D credible levels that have
        these areas. For example, for ``areas=(20, 100)``, then compute the
        probability within the smallest credible levels of 20 degĀ² and 100
        degĀ², respectively.

    modes : :class:`bool`, optional
        If True, then enable calculation of the number of distinct modes or
        islands of probability. Note that this option may be computationally
        expensive.

    cosmology : :class:`bool`, optional
        If True, then search space by descending probability density per unit
        comoving volume. If False, then search space by descending probability
        per luminosity distance cubed.

    Returns
    -------
    result : :class:`~ligo.skymap.postprocess.crossmatch.CrossmatchResult`

    Notes
    -----
    This function is also be used for injection finding; see
    :doc:`/ligo/skymap/tool/ligo_skymap_stats`.

    Examples
    --------

    First, some imports:

    >>> from astroquery.vizier import VizierClass
    >>> from astropy.coordinates import SkyCoord
    >>> from ligo.skymap.io import read_sky_map
    >>> from ligo.skymap.postprocess import crossmatch

    Next, retrieve the GLADE catalog using Astroquery and get the coordinates
    of all its entries:

    >>> vizier = VizierClass(
    ...     row_limit=-1, columns=['GWGC', '_RAJ2000', '_DEJ2000', 'Dist'])
    >>> cat, = vizier.get_catalogs('VII/281/glade2')
    >>> coordinates = SkyCoord(cat['_RAJ2000'], cat['_DEJ2000'], cat['Dist'])

    Load the multiresolution sky map for S190814bv:

    >>> url = 'https://gracedb.ligo.org/api/superevents/S190814bv/files/bayestar.multiorder.fits'
    >>> skymap = read_sky_map(url, moc=True)

    Perform the cross match:

    >>> result = crossmatch(skymap, coordinates)

    Using the cross match results, we can list the galaxies within the 90%
    credible volume:

    >>> print(cat[result.searched_prob_vol < 0.9])
       GWGC          _RAJ2000             _DEJ2000               Dist
                       deg                  deg                  Mpc
    ---------- -------------------- -------------------- --------------------
       NGC0171   9.3396699999999999 -19.9342460000000017    57.56212553960000
           ---  20.2009090000000064 -31.1146050000000010   137.16022925600001
    ESO540-003   8.9144679999999994 -20.1252980000000008    49.07809291930000
           ---  10.6762720000000009 -21.7740819999999999   276.46938505499998
           ---  13.5855169999999994 -23.5523850000000010   138.44550704800000
           ---  20.6362969999999990 -29.9825149999999958   160.23313164900000
           ---  13.1923879999999993 -22.9750179999999986   236.96795954500001
           ---  11.7813630000000007 -24.3706470000000017   244.25031189699999
           ---  19.1711120000000008 -31.4339490000000019   152.13614001400001
           ---  13.6367060000000002 -23.4948789999999974   141.25162979500001
           ...                  ...                  ...                  ...
           ---  11.3517000000000010 -25.8596999999999966   335.73800000000000
           ---  11.2073999999999998 -25.7149000000000001   309.02999999999997
           ---  11.1875000000000000 -25.7503999999999991   295.12099999999998
           ---  10.8608999999999991 -25.6904000000000003   291.07200000000000
           ---  10.6938999999999975 -25.6778300000000002   323.59399999999999
           ---  15.4935000000000009 -26.0304999999999964   304.78899999999999
           ---  15.2794000000000008 -27.0410999999999966   320.62700000000001
           ---  14.8323999999999980 -27.0459999999999994   320.62700000000001
           ---  14.5341000000000005 -26.0949000000000026   307.61000000000001
           ---  23.1280999999999963 -31.1109199999999966   320.62700000000001
    Length = 1478 rows
    """  # noqa: E501

    # Astropy coordinates that are constructed without distance have
    # a distance field that is unity (dimensionless).
    if coordinates is None:
        true_ra = true_dec = true_dist = None
    else:
        coordinates = coordinates.icrs
        true_ra = coordinates.ra.rad
        true_dec = coordinates.dec.rad
        if np.any(coordinates.distance != 1):
            true_dist = coordinates.distance.to_value(u.Mpc)
        else:
            true_dist = None

    contours = np.asarray(contours)

    distmean = sky_map.meta.get('distmean', np.nan)

    # Sort the pixels by descending posterior probability.
    sky_map = np.flipud(np.sort(sky_map, order='PROBDENSITY'))

    # Find the pixel that contains the injection.
    order, ipix = moc.uniq2nest(sky_map['UNIQ'])
    max_order = np.max(order)
    max_nside = hp.order2nside(max_order)
    max_ipix = ipix << np.int64(2 * (max_order - order))
    if true_ra is not None:
        true_theta = 0.5 * np.pi - true_dec
        true_phi = true_ra
        true_pix = hp.ang2pix(max_nside, true_theta, true_phi, nest=True)
        i = np.argsort(max_ipix)
        true_idx = i[np.digitize(true_pix, max_ipix[i]) - 1]

    # Find the angular offset between the mode and true locations.
    mode_theta, mode_phi = hp.pix2ang(
        hp.order2nside(order[0]), ipix[0], nest=True)
    if true_ra is None:
        offset = np.nan
    else:
        offset = np.rad2deg(
            angle_distance(true_theta, true_phi, mode_theta, mode_phi))

    # Calculate the cumulative area in deg2 and the cumulative probability.
    dA = moc.uniq2pixarea(sky_map['UNIQ'])
    dP = sky_map['PROBDENSITY'] * dA
    prob = np.cumsum(dP)
    area = np.cumsum(dA) * np.square(180 / np.pi)

    if true_ra is None:
        searched_area = searched_prob = probdensity = np.nan
    else:
        # Find the smallest area that would have to be searched to find
        # the true location.
        searched_area = area[true_idx]

        # Find the smallest posterior mass that would have to be searched to
        # find the true location.
        searched_prob = prob[true_idx]

        # Find the probability density.
        probdensity = sky_map['PROBDENSITY'][true_idx]

    # Find the contours of the given credible levels.
    contour_idxs = np.digitize(contours, prob) - 1

    # For each of the given confidence levels, compute the area of the
    # smallest region containing that probability.
    contour_areas = np.interp(
        contours, prob, area, left=0, right=4*180**2/np.pi).tolist()

    # For each listed area, find the probability contained within the
    # smallest credible region of that area.
    area_probs = np.interp(areas, area, prob, left=0, right=1).tolist()

    if modes:
        if true_ra is None:
            searched_modes = np.nan
        else:
            # Count up the number of modes in each of the given contours.
            searched_modes = count_modes_moc(sky_map['UNIQ'], true_idx)
        contour_modes = [
            count_modes_moc(sky_map['UNIQ'], i) for i in contour_idxs]
    else:
        searched_modes = np.nan
        contour_modes = np.nan

    # Distance stats now...
    if 'DISTMU' in sky_map.dtype.names:
        dP_dA = sky_map['PROBDENSITY']
        mu = sky_map['DISTMU']
        sigma = sky_map['DISTSIGMA']
        norm = sky_map['DISTNORM']

        # Set up distance grid.
        n_r = 1000
        max_r = 6 * distmean
        if true_dist is not None and np.max(true_dist) > max_r:
            max_r = np.max(true_dist)
        d_r = max_r / n_r

        # Calculate searched_prob_dist and contour_dists.
        r = d_r * np.arange(1, n_r)
        P_r = distance.marginal_cdf(r, dP, mu, sigma, norm)
        if true_dist is None:
            searched_prob_dist = np.nan
        else:
            searched_prob_dist = np.interp(true_dist, r, P_r, left=0, right=1)
        if len(contours) == 0:
            contour_dists = []
        else:
            lo, hi = np.interp(
                np.row_stack((
                    0.5 * (1 - contours),
                    0.5 * (1 + contours)
                )), P_r, r, left=0, right=np.inf)
            contour_dists = (hi - lo).tolist()

        # Calculate volume of each voxel, defined as the region within the
        # HEALPix pixel and contained within the two centric spherical shells
        # with radii (r - d_r / 2) and (r + d_r / 2).
        dV = (np.square(r) + np.square(d_r) / 12) * d_r * dA.reshape(-1, 1)

        # Calculate probability within each voxel.
        dP = np.exp(
            -0.5 * np.square(
                (r.reshape(1, -1) - mu.reshape(-1, 1)) / sigma.reshape(-1, 1)
            )
        ) * (dP_dA * norm / (sigma * np.sqrt(2 * np.pi))).reshape(-1, 1) * dV
        dP[np.isnan(dP)] = 0  # Suppress invalid values

        # Calculate probability density per unit volume.

        if cosmology:
            dV *= dVC_dVL_for_DL(r)
        dP_dV = dP / dV
        i = np.flipud(np.argsort(dP_dV.ravel()))

        P_flat = np.cumsum(dP.ravel()[i])
        V_flat = np.cumsum(dV.ravel()[i])

        contour_vols = np.interp(
            contours, P_flat, V_flat, left=0, right=np.inf).tolist()
        P = np.empty_like(P_flat)
        V = np.empty_like(V_flat)
        P[i] = P_flat
        V[i] = V_flat
        P = P.reshape(dP.shape)
        V = V.reshape(dV.shape)
        if true_dist is None:
            searched_vol = searched_prob_vol = probdensity_vol = np.nan
        else:
            i_radec = true_idx
            i_dist = np.digitize(true_dist, r) - 1
            probdensity_vol = dP_dV[i_radec, i_dist]
            searched_prob_vol = P[i_radec, i_dist]
            searched_vol = V[i_radec, i_dist]
    else:
        searched_vol = searched_prob_vol = searched_prob_dist \
            = probdensity_vol = np.nan
        contour_dists = [np.nan] * len(contours)
        contour_vols = [np.nan] * len(contours)

    # Done.
    return CrossmatchResult(
        searched_area, searched_prob, offset, searched_modes, contour_areas,
        area_probs, contour_modes, searched_prob_dist, contour_dists,
        searched_vol, searched_prob_vol, contour_vols, probdensity,
        probdensity_vol)