Source code for spotter.viz

"""
Visualization utilities for spherical maps and stellar surfaces.

This module provides functions for plotting HEALPix maps, graticules, and
generating videos of rotating stars.
"""

import numpy as np
from scipy.spatial.transform import Rotation

from spotter import core

_DEFAULT_CMAP = "magma"


[docs] def lon_lat_lines(n: int = 6, pts: int = 100, radius: float = 1.0): """ Generate latitude and longitude lines on a sphere. Parameters ---------- n : int or tuple, optional Number of latitude lines (and longitude lines if tuple). pts : int, optional Number of points per line. radius : float, optional Sphere radius. Returns ------- lat : ndarray Latitude lines. lon : ndarray Longitude lines. """ assert isinstance(n, int) or len(n) == 2 if isinstance(n, int): n = (n, 2 * n) n_lat, n_lon = n sqrt_radius = radius _theta = np.linspace(0, 2 * np.pi, pts) _phi = np.linspace(0, np.pi, n_lat + 1) lat = np.array( [ (r * np.cos(_theta), r * np.sin(_theta), np.ones_like(_theta) * h) for (h, r) in zip(sqrt_radius * np.cos(_phi), sqrt_radius * np.sin(_phi)) ] ) _theta = np.linspace(0, np.pi, pts // 2) _phi = np.linspace(0, 2 * np.pi, n_lon + 1)[0:-1] radii = np.sin(_theta) lon = np.array( [ ( sqrt_radius * radii * np.cos(p), sqrt_radius * radii * np.sin(p), sqrt_radius * np.cos(_theta), ) for p in _phi ] ) return lat, lon
[docs] def rotation(inc, obl, theta): """ Compute the rotation for given inclination, obliquity, and phase. Parameters ---------- inc : float Inclination in radians. obl : float Obliquity in radians. theta : float Rotation phase in radians. Returns ------- R : scipy.spatial.transform.Rotation Rotation object. """ u = [np.cos(obl), np.sin(obl), 0] u /= np.linalg.norm(u) u *= inc R = Rotation.from_rotvec(np.array(u)) R *= Rotation.from_rotvec([0, 0, obl]) R *= Rotation.from_rotvec([np.pi / 2, 0, 0]) R *= Rotation.from_rotvec([0, 0, -theta]) return R
[docs] def rotate_lines(lines, inc, obl, theta): """ Rotate lines by given inclination, obliquity, and phase. Parameters ---------- lines : ndarray Input lines. inc : float Inclination in radians. obl : float Obliquity in radians. theta : float Rotation phase in radians. Returns ------- rotated_lines : ndarray Rotated lines. """ R = rotation(inc, obl, theta) rotated_lines = np.array([R.apply(l.T) for l in lines]).T rotated_lines = np.swapaxes(rotated_lines.T, -1, 1) return rotated_lines
[docs] def plot_lines(lines, axis=(0, 1), ax=None, **kwargs): """ Plot lines on a matplotlib axis. Parameters ---------- lines : ndarray Lines to plot. axis : tuple, optional Axes to plot (default (0, 1)). ax : matplotlib axis, optional Axis to plot on. **kwargs Additional plot arguments. """ import matplotlib.pyplot as plt if ax is None: ax = plt.gca() if ax is None: ax = plt.subplot(111) # hide lines behind other_axis = list(set(axis).symmetric_difference([0, 1, 2]))[0] behind = lines[:, other_axis, :] < 0 _xyzs = lines.copy().swapaxes(1, 2) _xyzs[behind, :] = np.nan _xyzs = _xyzs.swapaxes(1, 2) for i, j in _xyzs[:, axis, :]: ax.plot(i, j, **kwargs)
[docs] def graticule( inc: float, obl: float = 0.0, theta: float = 0.0, pts: int = 100, radius: float = 1.0, n=6, ax=None, **kwargs, ): """ Plot a graticule (latitude/longitude grid) on a sphere. Parameters ---------- inc : float Inclination in radians. obl : float, optional Obliquity in radians. theta : float, optional Rotation phase in radians. pts : int, optional Number of points per line. radius : float, optional Sphere radius. n : int or tuple, optional Number of latitude/longitude lines. ax : matplotlib axis, optional Axis to plot on. **kwargs Additional plot arguments. """ import matplotlib.pyplot as plt _inc = np.pi / 2 - inc if ax is None: ax = plt.gca() if ax is None: ax = plt.subplot(111) kwargs.setdefault("c", kwargs.pop("color", "k")) kwargs.setdefault("lw", kwargs.pop("linewidth", 1)) kwargs.setdefault("alpha", 0.2) # plot lines lat, lon = lon_lat_lines(pts=pts, radius=radius, n=n) lat = rotate_lines(lat, _inc, obl, theta) plot_lines(lat, ax=ax, **kwargs) lon = rotate_lines(lon, _inc, obl, theta) plot_lines(lon, ax=ax, **kwargs) theta = np.linspace(0, 2 * np.pi, 2 * pts)
[docs] def show( y, inc=np.pi / 2, obl=0.0, u=None, xsize=800, phase=0.0, ax=None, radius=None, period=None, rv=False, **kwargs, ): """ Show a rendered map with graticule. Parameters ---------- y : array_like HEALPix map. inc : float, optional Inclination in radians. obl : float, optional Obliquity in radians. u : array_like or None, optional Limb darkening coefficients. xsize : int, optional Output image size. phase : float, optional Rotation phase in radians. ax : matplotlib axis, optional Axis to plot on. **kwargs Additional plot arguments. """ import matplotlib.pyplot as plt kwargs.setdefault("cmap", "RdBu_r" if rv else _DEFAULT_CMAP) kwargs.setdefault("origin", "lower") # kwargs.setdefault("vmin", 0.0) # kwargs.setdefault("vmax", 1.0) ax = ax or plt.gca() img = core.render( y, inc, u, phase, obl, xsize=xsize, radius=radius if rv else None, period=period if rv else None, ) plt.setp(ax.spines.values(), visible=False) ax.tick_params(left=False, labelleft=False) ax.tick_params(bottom=False, labelbottom=False) ax.patch.set_visible(False) ax.imshow(img, extent=(-1, 1, -1, 1), **kwargs) ax.set_xlim(-1.05, 1.05) ax.set_ylim(-1.05, 1.05) graticule(inc, obl, phase, ax=ax)
[docs] def video( y, inc=None, obl=0.0, u=None, duration=4, fps=10, radius=None, period=None, rv=False, **kwargs, ): """ Create an HTML video of a rotating map (for Jupyter notebooks). Parameters ---------- y : array_like HEALPix map. inc : float, optional Inclination in radians. obl : float, optional Obliquity in radians. u : array_like or None, optional Limb darkening coefficients. duration : int, optional Duration of the video in seconds. fps : int, optional Frames per second. render_fun: callable, optional A function of phase that renders the star. Default is core.render **kwargs Additional plot arguments. Returns ------- None """ import matplotlib.animation as animation import matplotlib.pyplot as plt from IPython import display kwargs.setdefault("cmap", "RdBu_r" if rv else _DEFAULT_CMAP) kwargs.setdefault("origin", "lower") def render_fun(phase): return core.render( y, inc, u, phase, obl, period=period if rv else None, radius=radius if rv else None, ) inc = inc or 0.0 fig, ax = plt.subplots(figsize=(3, 3)) im0 = render_fun(0.0) im = plt.imshow(im0, extent=(-1, 1, -1, 1), **kwargs) plt.axis("off") plt.tight_layout() ax.set_frame_on(False) fig.patch.set_alpha(0.0) frames = duration * fps def update(frame): a = im.get_array() phase = np.pi * 2 * frame / frames a = render_fun(phase) for art in list(ax.lines): art.remove() graticule(inc, ax=ax, theta=phase, obl=obl) im.set_array(a) return [im] ani = animation.FuncAnimation( fig=fig, func=update, frames=frames, interval=1000 / fps ) video = ani.to_jshtml(embed_frames=True) html = display.HTML(video) plt.close() return display.display(html)