# Source code for ligo.skymap.postprocess.contour

```#
# Copyright (C) 2013-2020  Leo Singer
#
# This program is free software: you can redistribute it and/or modify
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.

import astropy_healpix as ah
from astropy import units as u
import healpy as hp
import numpy as np

__all__ = ('contour', 'simplify')

def _norm_squared(vertices):
return np.sum(np.square(vertices), -1)

return 0.25 * _norm_squared(np.cross(
np.roll(vertices, -1, axis=0) - vertices,
np.roll(vertices, +1, axis=0) - vertices))

theta, phi = hp.vec2ang(np.asarray(vertices))
ret = np.column_stack((phi % (2 * np.pi), 0.5 * np.pi - theta))
if degrees:
return ret

[docs]def simplify(vertices, min_area):
"""Simplify a polygon on the unit sphere.

This is a naive, slow implementation of Visvalingam's algorithm (see
http://bost.ocks.org/mike/simplify/), adapted for for linear rings on a
sphere.

Parameters
----------
vertices : `np.ndarray`
An Nx3 array of Cartesian vertex coordinates. Each vertex should be a
unit vector.

min_area : float
The minimum area of triangles formed by adjacent triplets of vertices.

Returns
-------
vertices : `np.ndarray`

"""
min_area_squared = np.square(min_area)

while True:
i_min_area = np.argmin(area_squared)
if area_squared[i_min_area] > min_area_squared:
break

vertices = np.delete(vertices, i_min_area, axis=0)
area_squared = np.delete(area_squared, i_min_area)
area_squared = np.maximum(area_squared, new_area_squared)

return vertices

# A synonym for ``simplify`` to avoid aliasing by the keyword argument of the
# same name below.
_simplify = simplify

[docs]def contour(m, levels, nest=False, degrees=False, simplify=True):
"""Calculate contours from a HEALPix dataset.

Parameters
----------
m : `numpy.ndarray`
The HEALPix dataset.
levels : list
The list of contour values.
nest : bool, default=False
Indicates whether the input sky map is in nested rather than
ring-indexed HEALPix coordinates (default: ring).
degrees : bool, default=False
simplify : bool, default=True
Whether to simplify the paths.

Returns
-------
list
A list with the same length as `levels`.
Each item is a list of disjoint polygons, of which each item is a
list of points, of which each is a list consisting of the right
ascension and declination.

Examples
--------
A very simply example sky map...

>>> nside = 32
>>> npix = ah.nside_to_npix(nside)
>>> ra, dec = hp.pix2ang(nside, np.arange(npix), lonlat=True)
>>> m = dec
>>> contour(m, [10, 20, 30], degrees=True)
[[[[..., ...], ...], ...], ...]

"""
# Infrequently used import
import networkx as nx

# Determine HEALPix resolution.
npix = len(m)
nside = ah.npix_to_nside(npix)
min_area = 0.4 * ah.nside_to_pixel_area(nside).to_value(u.sr)

neighbors = hp.get_all_neighbours(nside, np.arange(npix), nest=nest).T

# Loop over the requested contours.
paths = []
for level in levels:

# Find credible region.
indicator = (m >= level)

# Find all faces that lie on the boundary.
# This speeds up the doubly nested ``for`` loop below by allowing us to
# skip the vast majority of faces that are on the interior or the
# exterior of the contour.
tovisit = np.flatnonzero(
np.any(indicator.reshape(-1, 1) !=
indicator[neighbors[:, ::2]], axis=1))

# Construct a graph of the edges of the contour.
graph = nx.Graph()
face_pairs = set()
for ipix1 in tovisit:
neighborhood = neighbors[ipix1]
for _ in range(4):
neighborhood = np.roll(neighborhood, 2)
ipix2 = neighborhood

# Skip this pair of faces if we have already examined it.
new_face_pair = frozenset((ipix1, ipix2))
if new_face_pair in face_pairs:
continue

# Determine if this pair of faces are on a boundary of the
# credible level.
if indicator[ipix1] == indicator[ipix2]:
continue

# Add the common edge of this pair of faces.
# Label each vertex with the set of faces that they share.
frozenset((ipix1, *neighborhood[2:5])),
frozenset((ipix1, *neighborhood[4:7])))
graph = nx.freeze(graph)

# Find contours by detecting cycles in the graph.
cycles = nx.cycle_basis(graph)

# Construct the coordinates of the vertices by averaging the
# coordinates of the connected faces.
cycles = [[
np.sum(hp.pix2vec(nside, [i for i in v if i != -1], nest=nest), 1)
for v in cycle] for cycle in cycles]

# Simplify paths if requested.
if simplify:
cycles = [_simplify(cycle, min_area) for cycle in cycles]
cycles = [cycle for cycle in cycles if len(cycle) > 2]

# Convert to angles.
cycles = [
_vec2radec(cycle, degrees=degrees).tolist() for cycle in cycles]