Source code for toasty.toast

# -*- mode: python; coding: utf-8 -*-
# Copyright 2013-2020 Chris Beaumont and the AAS WorldWide Telescope project
# Licensed under the MIT License.

"""Computations for the TOAST projection scheme and tile pyramid format.

TODO this all needs to be ported to modern Toasty infrastructure and
wwt_data_formats.

"""
from __future__ import absolute_import, division, print_function

__all__ = '''
generate_tiles
sample_layer
Tile
toast_tile_area
'''.split()

from collections import defaultdict, namedtuple
import os
import logging
import numpy as np
from tqdm import tqdm

from ._libtoasty import subsample, mid
from .image import Image
from .pyramid import Pos, depth2tiles, is_subtile, pos_parent, tiles_at_depth

HALFPI = 0.5 * np.pi
THREEHALFPI = 1.5 * np.pi
TWOPI = 2 * np.pi

Tile = namedtuple('Tile', 'pos corners increasing')

_level1_lonlats = [
    [np.radians(c) for c in row]
    for row in [
        [(0, -90), (90, 0), (0, 90), (180, 0)],
        [(90, 0), (0, -90), (0, 0), (0, 90)],
        [(0, 90), (0, 0), (0, -90), (270, 0)],
        [(180, 0), (0, 90), (270, 0), (0, -90)],
    ]
]

LEVEL1_TILES = [
    Tile(Pos(n=1, x=0, y=0), _level1_lonlats[0], True),
    Tile(Pos(n=1, x=1, y=0), _level1_lonlats[1], False),
    Tile(Pos(n=1, x=1, y=1), _level1_lonlats[2], True),
    Tile(Pos(n=1, x=0, y=1), _level1_lonlats[3], False),
]


def _arclength(lat1, lon1, lat2, lon2):
    """Compute the length of an arc along the great circle defined by spherical
    latitude and longitude coordinates. Inputs and return value are all in
    radians.

    """
    c = np.sin(lat1) * np.sin(lat2) + np.cos(lon1 - lon2) * np.cos(lat1) * np.cos(lat2)
    return np.arccos(c)


def _spherical_triangle_area(lat1, lon1, lat2, lon2, lat3, lon3):
    """Compute the area of the specified spherical triangle in steradians. Inputs
    are in radians. From https://math.stackexchange.com/a/66731 . My initial
    implementation used unit vectors on the sphere instead of latitudes and
    longitudes; there might be a faster way to do things in lat/lon land.

    """
    c = _arclength(lat1, lon1, lat2, lon2)
    a = _arclength(lat2, lon2, lat3, lon3)
    b = _arclength(lat3, lon3, lat1, lon1)
    s = 0.5 * (a + b + c)
    tane4 = np.sqrt(np.tan(0.5 * s) * np.tan(0.5 * (s - a)) * np.tan(0.5 * (s - b)) * np.tan(0.5 * (s - c)))
    e = 4 * np.arctan(tane4)
    return e


[docs]def toast_tile_area(tile): """Calculate the area of a TOAST tile in steradians. Parameters ---------- tile : :class:`Tile` A TOAST tile. Returns ------- The area of the tile in steradians. Notes ----- This computation is not very fast. """ ul, ur, lr, ll = tile.corners if tile.increasing: a1 = _spherical_triangle_area(ul[1], ul[0], ur[1], ur[0], ll[1], ll[0]) a2 = _spherical_triangle_area(ur[1], ur[0], lr[1], lr[0], ll[1], ll[0]) else: a1 = _spherical_triangle_area(ul[1], ul[0], ur[1], ur[0], lr[1], lr[0]) a2 = _spherical_triangle_area(ul[1], ul[0], ll[1], ll[0], lr[1], lr[0]) return a1 + a2
def _postfix_corner(tile, depth, bottom_only): """ Yield subtiles of a given tile, in postfix (deepest-first) order. Parameters ---------- tile : Tile Parameters of the current tile. depth : int The depth to descend to. bottom_only : bool If True, only yield tiles at max_depth. """ n = tile[0].n if n > depth: return for child in _div4(tile): for item in _postfix_corner(child, depth, bottom_only): yield item if n == depth or not bottom_only: yield tile def _div4(tile): """Return the four child tiles of an input tile.""" n, x, y = tile.pos.n, tile.pos.x, tile.pos.y ul, ur, lr, ll = tile.corners increasing = tile.increasing to = mid(ul, ur) ri = mid(ur, lr) bo = mid(lr, ll) le = mid(ll, ul) ce = mid(ll, ur) if increasing else mid(ul, lr) n += 1 x *= 2 y *= 2 return [ Tile(Pos(n=n, x=x, y=y ), (ul, to, ce, le), increasing), Tile(Pos(n=n, x=x + 1, y=y ), (to, ur, ri, ce), increasing), Tile(Pos(n=n, x=x, y=y + 1), (le, ce, bo, ll), increasing), Tile(Pos(n=n, x=x + 1, y=y + 1), (ce, ri, lr, bo), increasing), ]
[docs]def generate_tiles(depth, bottom_only=True): """Generate a pyramid of TOAST tiles in deepest-first order. Parameters ---------- depth : int The tile depth to recurse to. bottom_only : bool If True, then only the lowest tiles will be yielded. Yields ------ tile : Tile An individual tile to process. Tiles are yield deepest-first. The ``n = 0`` depth is not included. """ for t in LEVEL1_TILES: for item in _postfix_corner(t, depth, bottom_only): yield item
[docs]def sample_layer(pio, mode, sampler, depth, cli_progress=False): """Generate a layer of the TOAST tile pyramid through direct sampling. Parameters ---------- pio : :class:`toasty.pyramid.PyramidIO` A :class:`~toasty.pyramid.PyramidIO` instance to manage the I/O with the tiles in the tile pyramid. mode : :class:`toasty.image.ImageMode` The image mode of this data source. sampler : callable The sampler callable that will produce data for tiling. depth : int The depth of the layer of the TOAST tile pyramid to generate. The number of tiles in each layer is ``4**depth``. Each tile is 256×256 TOAST pixels, so the resolution of the pixelization at which the data will be sampled is a refinement level of ``2**(depth + 8)``. cli_progress : optional boolean, defaults False If true, a progress bar will be printed to the terminal using tqdm. """ with tqdm(total=tiles_at_depth(depth), disable=not cli_progress) as progress: for tile in generate_tiles(depth, bottom_only=True): lon, lat = subsample( tile.corners[0], tile.corners[1], tile.corners[2], tile.corners[3], 256, tile.increasing, ) sampled_data = sampler(lon, lat) pio.write_toasty_image(tile.pos, Image.from_array(mode, sampled_data)) progress.update(1) if cli_progress: print()