Source code for giant.coverage.visualizer

from typing import Sequence, Literal, Callable

import copy

import matplotlib.cm as cm
import matplotlib.colors as mcol
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import Slider
from matplotlib.collections import PatchCollection
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
from matplotlib.patches import Polygon
from matplotlib.figure import Figure
from matplotlib.axes import Axes

from giant.utilities.spherical_coordinates import unit_to_radec
from giant.rotations import rot_z
from giant.coverage.coverage_class import Coverage
from giant.coverage.utilities.project_triangles_latlon import project_triangles_latlon

from giant._typing import DOUBLE_ARRAY

STAT_SETS = Literal['v_count', 'd_albedo', 'd_x_slope', 'd_y_slope', 'd_total']
"""
Available stats to visualize.

* `v_count` is the number of times each facet was visible (in the FOV and illumintated) in the imaging plan
* `d_albedo` is the albedo dilution of precision value
* `d_x_slope` is the slope in the x direction dilution of precision value
* `d_y_slope` is the slope in the z direction dilution of precision value
* `d_total` is the RSS of the dilution of precision values
"""


[docs] def polar_plot(dop, ra, dec, observations, ax=None, cmap='coolwarm'): """ This function serves to visualize the observed geometries of a single target surface element given a specific dilution of precision metric and the observations where that surface element was visible. It will generate a polar plot with the ra and dec values serving as the polar grid with the observation data overlayed to see the ra/dec pairs of the surface element's orientation when it was observed and the DOP data overlayed to see the confidence level in the observations at each surface element orientation. :param dop: the DOP metric of the surface element evaluated at each permutation of ra and dec values :param ra: np.ndarray with dtype np.float64 and shape (n, m) where n is the number of distinct RA values the surface normal of the element permutated through and m is the number of distinct Dec values the surface normal of the element permutated through :param dec: np.ndarray with dtype np.float64 and shape (n, m) where n is the number of distinct RA values the surface normal of the element permutated through and m is the number of distinct Dec values the surface normal of the element permutated through :param observations: np.array of visibility parameters for all observations where the surface element was visible """ z_axis = observations['normal'][0] east_dir = rot_z(-0.1) @ z_axis north = np.cross(z_axis, east_dir) north /= np.linalg.norm(north) east = np.cross(north, z_axis) east /= np.linalg.norm(east) rotation2enu = np.array([east, north, z_axis]) valid_obs = observations[observations['visible']] local_inc = -rotation2enu @ valid_obs['incidence'].T local_emi = rotation2enu @ valid_obs['exidence'].T ra_sun, dec_sun = unit_to_radec(local_inc) ra_cam, dec_cam = unit_to_radec(local_emi) dop = dop.copy() if np.isnan(dop).any() and np.isfinite(dop).any(): dop[~np.isnan(dop)] = np.max(dop[np.isfinite(dop)]) norm = mcol.Normalize(vmin=0, vmax=10, clip=True) if ax is None: fig = plt.figure() ax = fig.add_subplot(111, polar=True) mappable = ax.pcolor(ra, dec * 180 / np.pi, dop.reshape(ra.shape), cmap=cmap, norm=norm, shading='auto') ax.scatter(ra_sun, 90 - dec_sun * 180 / np.pi, color='black', marker='*') ax.scatter(ra_cam, 90 - dec_cam * 180 / np.pi, color='black', marker='D') plt.colorbar(mappable)
[docs] class UpdateableColorScale: """ This class is used to create a color scale to respresent different numerical data assigned to facets on either a 2D or 3D visualization of a target body. """ def __init__(self, collection, color_data, cmap='hot', location='bottom', integer=True, fig=None, ax=None, xbounds=(-180, 180), ybounds=(-90, 90), zbounds=None, xlabel='longitude, degrees', ylabel='latitude, degrees', zlabel=None, title='dop', absolute_max=100, projection=None, nan_to_inf=True, slider=True, show_cbar=True): """ :param collection: A collection of facets on the target body's surface that are used to visualize the target's surface in the plot :param color_data: An np.array containing a value for each facet, which will be used to determine which color the facet will be shaded on the plot based on where it falls on the color scale. Note that any values above the maximum of the color scale will take on the maximal color, and similarly, any values below the minimum of the color scale will take on the minimal color :param cmap: An optional string representing a color gradient to use for mapping to values to the color scale :param location: An optional string representing the location of the interactive slider :param integer: An optional flag to force the threshold value on the slider to be rounded to the nearest integer :param fig: An optional pre-existing figure to provide as a destination for the color scale to be applied :param ax: An optional pre-existing axes to provide as a destination for the color scale to be applied :param xbounds: An optional tuple containing bounds for x-values to show in the plot :param ybounds: An optional tuple containing bounds for y-values to show in the plot :param zbounds: An optional tuple containing bounds for z-values to show in the plot :param xlabel: An optional string representing a label for the x-axis to show in the plot :param ylabel: An optional string representing a label for the y-axis to show in the plot :param zlabel: An optional string representing a label for the z-axis to show in the plot :param title: An optional string to set as the title of the plot :param absolute_max: An optional number to provide as the highest value the color bar (and slider) can display. If the absolute_max exceeds the largest value in the :attr:`color_data`, the color bar (and slider) will be limited to that largest value. :param nan_to_inf: An optional flag to convert np.nan values to np.inf in the :attr:`color_data` for consistent plotting results :param projection: An optional string representing a matplotlib projection type :param slider: An optional flag to display an interactive slider in the figure. The value of the slider represents the maximum value of the color scale :param show_cbar: An optional flag to display the color bar representing the numerical value associated with each color in the plot """ if (fig is None) and (ax is None): self.fig = plt.figure() self.ax = self.fig.add_subplot(111, projection=projection) elif fig is None: self.fig = ax.figure # type: ignore self.ax = ax elif ax is None: self.fig = fig self.ax = self.fig.add_subplot(111, projection=projection) else: self.fig = fig self.ax = ax assert self.ax is not None assert self.fig is not None self.collection = collection self.color_data = color_data self.integer = integer if nan_to_inf: self.color_data[np.isnan(self.color_data)] = np.inf if slider: if location.lower() == 'bottom': self.fig.subplots_adjust(bottom=0.25) elif location.lower() == 'top': self.fig.subplots_adjust(top=0.75) elif location.lower() == 'left': self.fig.subplots_adjust(left=0.25) elif location.lower() == 'right': self.fig.subplots_adjust(left=0.75) if np.isfinite(self.color_data).any(): cmax = min(self.color_data[np.isfinite(self.color_data)].max(), absolute_max) else: cmax = 1.e20 self.norm = mcol.Normalize(vmin=0, vmax=cmax, clip=True) self.mapper = cm.ScalarMappable(norm=self.norm, cmap=plt.get_cmap(cmap)) self.rgbcols = self.mapper.to_rgba(self.color_data) self.collection.set_facecolor(self.rgbcols) self.mapper.set_array(self.color_data) if show_cbar: self.cbar = self.fig.colorbar(self.mapper, ax=self.ax) self.cbar.update_normal(self.mapper) self.ax.add_collection(self.collection) self.ax.set_xlim(xbounds) self.ax.set_ylim(ybounds) self.ax.set_xlabel(xlabel) self.ax.set_ylabel(ylabel) if zlabel is not None: assert isinstance(self.ax, Axes3D) self.ax.set_zlim(zbounds) self.ax.set_zlabel(zlabel) self.ax.set_title(title) if slider: self.axcolor = self.fig.add_axes((0.15, 0.1, 0.6, 0.03)) self.scolor = Slider(self.axcolor, 'Threshold', 0, cmax, valinit=cmax) self.scolor.drawon = False self.scolor.on_changed(self._update_slider) def _update_slider(self, val): """ This method takes input via an interactive slider in a matplotlib figure and uses this input to update the color scale of the figure. :param val: A numeric value automatically input based on the position of the slider. It will update the maximum value of the color scale to be equal to val, which can change how the target will be shaded. """ if self.integer: cmax = int(np.round(self.scolor.val)) self.scolor.val = cmax self.scolor.valtext.set_text('{}'.format(cmax)) else: cmax = self.scolor.val self.norm.vmax = cmax self.mapper.norm = self.norm self.collection.set_facecolor(self.mapper.to_rgba(self.color_data)) self.cbar.update_normal(self.mapper) self.fig.canvas.draw_idle()
[docs] def create_patches_latlon(lat: DOUBLE_ARRAY, lon: DOUBLE_ARRAY) -> list[Polygon]: """ This helper function creates matplotlib Polygon patches for triangles from lat/lon arrays :param lat: the lattitude values as a 2d array. Generally should be from the :func:`.project_triangles_latlon` function :param lon: the longitude values as a 2d array. Generally should be from the :func:`.project_triangles_latlon` function :returns: a list of matplotlib Polygons as specified in the input """ patches = [] for llat, llon in zip(lat, lon): patches.append(Polygon(np.vstack([llon, llat]).T, closed=True, edgecolor='gray')) return patches
[docs] def percent_below_threshold_reducer(threshold: float) -> Callable[[DOUBLE_ARRAY], float]: """ This helper function returns a callable which computes the percentage of values that is below some specified threshold. :param threshold: the threshold to use :returns: a callable """ def percent_below_threshold(values: DOUBLE_ARRAY) -> float: return (np.asarray(values, dtype=np.float64) < threshold).sum() / np.size(values) * 100 return percent_below_threshold
[docs] def get_coloring(cov: Coverage, stat_set: STAT_SETS = "v_count", label: str | None = None, reduction_function: Callable[[DOUBLE_ARRAY], float] = percent_below_threshold_reducer(1)) -> DOUBLE_ARRAY: """ This helper function chooses the appropriate values from the coverage object for displaying, applying the reduction function if necessary. :param cov: the :class:`.Coverage` object :param stat_set: the stats to get the coloring for :param label: the label to use if labeled analysis was performed :param reduction_function: a callabe to reduce all the DOP values for a facet to a single value for display :returns: a numpy array containing the values to color map in the display """ match stat_set: case "v_count": if isinstance(cov.observation_count, dict): if label is not None: coloring = cov.observation_count[label] else: raise ValueError('labeled analyis was performed but you did not specify the label to visualize') else: coloring = np.array(cov.observation_count, dtype=np.float64) case "d_albedo": if isinstance(cov.albedo_dop, dict): if label is not None: coloring = reduction_function(np.array(cov.albedo_dop[label])) else: raise ValueError('labeled analyis was performed but you did not specify the label to visualize') else: coloring = reduction_function(np.array(cov.albedo_dop)) case "d_x_slope": if isinstance(cov.x_terrain_dop, dict): if label is not None: coloring = reduction_function(np.array(cov.x_terrain_dop[label])) else: raise ValueError('labeled analyis was performed but you did not specify the label to visualize') else: coloring = reduction_function(np.array(cov.x_terrain_dop)) case "d_y_slope": if isinstance(cov.y_terrain_dop, dict): if label is not None: coloring = reduction_function(np.array(cov.y_terrain_dop[label])) else: raise ValueError('labeled analyis was performed but you did not specify the label to visualize') else: coloring = reduction_function(np.array(cov.y_terrain_dop)) case "d_total": if isinstance(cov.total_dop, dict): if label is not None: coloring = reduction_function(np.array(cov.total_dop[label])) else: raise ValueError('labeled analyis was performed but you did not specify the label to visualize') else: coloring = reduction_function(np.array(cov.total_dop)) return np.array(coloring, dtype=np.float64)
[docs] def visualize2d(cov: Coverage, patches: None | Sequence[Polygon], stat_set: STAT_SETS = 'v_count', label: str | None = None, reduction_function: Callable[[DOUBLE_ARRAY], float] = percent_below_threshold_reducer(1), fig: Figure | None = None, ax: Axes | None = None, cmap: str = 'hot') -> UpdateableColorScale: """ This function provides a simplified interface to generate a 2D, lat/lon projected map of the statistics. For more advanced usage, use this function as a template and use the :class:`.UpdateableColorScale` directly. :param cov: the coverage object containing the coverage results :param patches: an option list of Polygons to use (if None, they'll be created for you) :param stat_set: a string specifying what stat to show :param label: the label to visualize (if labeled analysis was performed) :param reduction_function: a callabe to reduce all the DOP values for a facet to a single value for display :param fig: the matplitlib Figure to add the plot to (if None a new figure will be created) :param ax: the matplitlib Axes to add the plot to (if None a new Axes will be created) :param cmap: The color map to use to color the results. :returns: A :class:`.UpdateableColorScale` object set for display """ if patches is None: lat_tris, lon_tris = project_triangles_latlon(cov.targetvecs.T, cov.targetfacets) patches = create_patches_latlon(lat_tris, lon_tris) coloring = get_coloring(cov, stat_set=stat_set, label=label, reduction_function=reduction_function) return UpdateableColorScale(PatchCollection(copy.copy(patches), edgecolors='gray', linewidths=0.1), coloring, cmap=cmap, xlabel='longitude, deg', ylabel='latitude, deg', ax=ax, fig=fig, integer=stat_set == "v_count", title=stat_set)
[docs] def visualize3d(cov: Coverage, stat_set: STAT_SETS = 'v_count', label: str | None = None, reduction_function: Callable[[DOUBLE_ARRAY], float] = percent_below_threshold_reducer(1), fig: Figure | None = None, ax: Axes | None = None, cmap: str = 'hot') -> UpdateableColorScale: """ This function provides a simplified interface to generate a 2D, lat/lon projected map of the statistics. For more advanced usage, use this function as a template and use the :class:`.UpdateableColorScale` directly. :param cov: the coverage object containing the coverage results :param stat_set: a string specifying what stat to show :param label: the label to visualize (if labeled analysis was performed) :param reduction_function: a callabe to reduce all the DOP values for a facet to a single value for display :param fig: the matplitlib Figure to add the plot to (if None a new figure will be created) :param ax: the matplitlib Axes to add the plot to (if None a new Axes will be created) :param cmap: The color map to use to color the results. :returns: A :class:`.UpdateableColorScale` object set for display """ coloring = get_coloring(cov, stat_set=stat_set, label=label, reduction_function=reduction_function) collection = Poly3DCollection(cov.targetvecs[cov.targetfacets], edgecolors='gray', linewidths=0.1) shape_bounds = np.abs(cov.targetvecs).max() return UpdateableColorScale(collection, coloring, cmap=cmap, projection='3d', xbounds=(-shape_bounds, shape_bounds), ybounds=(-shape_bounds, shape_bounds), zbounds=(-shape_bounds, shape_bounds), xlabel='x, km', ylabel='y, km', zlabel='z, km', ax=ax, fig=fig, integer=stat_set == "v_count", title=stat_set)