Source code for IS2view.tools

#!/usr/bin/env python
"""
tools.py
Written by Tyler Sutterley (01/2025)
User interface tools for Jupyter Notebooks

PYTHON DEPENDENCIES:
    numpy: Scientific Computing Tools For Python
        https://numpy.org
        https://numpy.org/doc/stable/user/numpy-for-matlab-users.html
    ipywidgets: interactive HTML widgets for Jupyter notebooks and IPython
        https://ipywidgets.readthedocs.io/en/latest/
    matplotlib: Python 2D plotting library
        http://matplotlib.org/
        https://github.com/matplotlib/matplotlib
    cmocean: Beautiful colormaps for oceanography
        https://matplotlib.org/cmocean/

UPDATE HISTORY:
    Updated 01/2025: added optional cmocean colormaps to dropdown menu
        updated the default group list to include lags for release 004
    Updated 06/2024: use wrapper to importlib for optional dependencies
    Updated 11/2023: set time steps using decimal years rather than lags
        setting dynamic colormap with float64 min and max
    Updated 08/2023: added options for ATL14/15 Release-03 data
    Updated 07/2023: use logging instead of warnings for import attempts
    Updated 06/2023: moved widgets functions to separate module
    Updated 12/2022: added case for warping input image
    Updated 11/2022: modifications for dask-chunked rasters
    Written 07/2022
"""

import os
import copy
import logging
import numpy as np
from IS2view.utilities import import_dependency

# attempt imports
ipywidgets = import_dependency("ipywidgets")
xr = import_dependency("xarray")
cm = import_dependency("matplotlib.cm")
cmocean = import_dependency("cmocean")

# set environmental variable for anonymous s3 access
os.environ["AWS_NO_SIGN_REQUEST"] = "YES"


[docs] class widgets: def __init__(self, **kwargs): # set default keyword options kwargs.setdefault("loglevel", logging.CRITICAL) kwargs.setdefault("directory", os.getcwd()) kwargs.setdefault("style", {}) # set logging level logging.basicConfig(level=kwargs["loglevel"]) # set style self.style = copy.copy(kwargs["style"]) # pass through some ipywidgets objects self.HBox = ipywidgets.HBox self.VBox = ipywidgets.VBox # dropdown menu for setting asset asset_list = ["nsidc-https", "nsidc-s3", "atlas-s3", "atlas-local"] self.asset = ipywidgets.Dropdown( options=asset_list, value="nsidc-https", description="Asset:", description_tooltip=( "Asset: Location to get the data\n\t" "nsidc-https: NSIDC on-prem DAAC\n\t" "nsidc-s3: NSIDC Cumulus s3 bucket`\n\t" "atlas-s3: s3 bucket in `us-west-2`\n\t" "atlas-local: local directory" ), disabled=False, style=self.style, ) # working data directory if local self.directory = ipywidgets.Text( value=kwargs["directory"], description="Directory:", description_tooltip=("Directory: working data directory"), disabled=False, style=self.style, ) self.directory.layout.display = "none" # dropdown menu for setting ATL14/15 release release_list = ["001", "002", "003", "004", "005"] self.release = ipywidgets.Dropdown( options=release_list, value="005", description="Release:", description_tooltip=( "Release: ATL14/15 data release\n\t" "001: Release-01\n\t" "002: Release-02\n\t" "003: Release-03\n\t" "004: Release-04\n\t" "005: Release-05" ), disabled=False, style=self.style, ) # dropdown menu for setting ATL14/15 region # set as a default the release 03+ regions region_list = [ "AA", "A1", "A2", "A3", "A4", "CN", "CS", "GL", "IS", "RA", "SV", ] self.region = ipywidgets.Dropdown( options=region_list, description="Region:", description_tooltip=( "Region: ATL14/15 region\n\t" "AA: Antarctica (merged)\n\t" "A1: Antarctica (0\u00b0 to 90\u00b0)\n\t" "A2: Antarctica (0\u00b0 to -90\u00b0)\n\t" "A3: Antarctica (-90\u00b0 to -180\u00b0)\n\t" "A4: Antarctica (90\u00b0 to 180\u00b0)\n\t" "CN: Northern Canadian Archipelago\n\t" "CS: Southern Canadian Archipelago\n\t" "GL: Greenland\n\t" "IS: Iceland\n\t" "SV: Svalbard\n\t" "RA: Russian High Arctic" ), disabled=False, style=self.style, ) # dropdown menu for setting ATL15 resolution resolution_list = ["01km", "10km", "20km", "40km"] self.resolution = ipywidgets.Dropdown( options=resolution_list, description="Resolution:", description_tooltip=( "Resolution: ATL15 resolution\n\t" "01km: 1 kilometer horizontal\n\t" "10km: 10 kilometers horizontal\n\t" "20km: 20 kilometers horizontal\n\t" "40km: 40 kilometers horizontal" ), disabled=False, style=self.style, ) # dropdown menu for selecting group to read from file group_list = ["delta_h", "dhdt_lag1"] # extend possible time lags to 16 years post-launch for timelag in range(4, 68, 4): group_list.append(f"dhdt_lag{timelag:d}") self.group = ipywidgets.Dropdown( options=group_list, description="Group:", description_tooltip="Group: ATL15 data group to read from file", disabled=False, style=self.style, ) # dropdown menu for selecting data format format_list = ["nc", "zarr"] self.format = ipywidgets.Dropdown( options=format_list, description="Format:", description_tooltip=( "Format: ATL15 data format\n\t" "nc: Native netCDF4\n\t" "zarr: Cloud-optimized zarr" ), disabled=False, style=self.style, ) self.format.layout.display = "none" # dropdown menu for selecting variable to draw on map variable_list = ["delta_h", "dhdt"] self.variable = ipywidgets.Dropdown( options=variable_list, description="Variable:", description_tooltip="Variable: variable to display on leaflet map", disabled=False, style=self.style, ) # slider for selecting time lag to draw on map self.timelag = ipywidgets.IntSlider( description="Lag:", description_tooltip="Lag: time lag to draw on leaflet map", disabled=False, style=self.style, ) # slider for selecting time step to draw on map self.timestep = ipywidgets.FloatSlider( description="Time:", description_tooltip="Time: time step to draw on leaflet map", step=0.25, readout=True, readout_format=".2f", disabled=False, continuous_update=False, style=self.style, ) # Reverse the colormap self.dynamic = ipywidgets.Checkbox( value=False, description="Dynamic", description_tooltip="Dynamic: Dynamically set normalization range", disabled=False, style=self.style, ) # watch widgets for changes self.asset.observe(self.set_directory_visibility) self.asset.observe(self.set_format_visibility) self.release.observe(self.set_groups) self.dynamic.observe(self.set_dynamic) # self.group.observe(self.set_variables) # self.group.observe(self.set_time_steps) # self.group.observe(self.set_atl15_defaults) self.variable.observe(self.set_time_visibility) self.timestep.observe(self.set_lag) # slider for normalization range self.range = ipywidgets.FloatRangeSlider( min=-10, max=10, value=[-5, 5], description="Range:", description_tooltip=("Range: Plot normalization range"), disabled=False, continuous_update=False, orientation="horizontal", readout=True, style=self.style, ) # all listed colormaps in matplotlib version cmap_set = set(cm.datad.keys()) | set(cm.cmaps_listed.keys()) # colormaps available in this program # (no reversed, qualitative or miscellaneous) self.cmaps_listed = {} self.cmaps_listed["Perceptually Uniform Sequential"] = [ "viridis", "plasma", "inferno", "magma", "cividis", ] self.cmaps_listed["Sequential"] = [ "Greys", "Purples", "Blues", "Greens", "Oranges", "Reds", "YlOrBr", "YlOrRd", "OrRd", "PuRd", "RdPu", "BuPu", "GnBu", "PuBu", "YlGnBu", "PuBuGn", "BuGn", "YlGn", ] self.cmaps_listed["Sequential (2)"] = [ "binary", "gist_yarg", "gist_gray", "gray", "bone", "pink", "spring", "summer", "autumn", "winter", "cool", "Wistia", "hot", "afmhot", "gist_heat", "copper", ] self.cmaps_listed["Diverging"] = [ "PiYG", "PRGn", "BrBG", "PuOr", "RdGy", "RdBu", "RdYlBu", "RdYlGn", "Spectral", "coolwarm", "bwr", "seismic", ] self.cmaps_listed["Cyclic"] = ["twilight", "twilight_shifted", "hsv"] # create list of available colormaps in program cmap_list = [] for val in self.cmaps_listed.values(): cmap_list.extend(val) # reduce colormaps to available in program and matplotlib cmap_set &= set(cmap_list) cmap_options = sorted(cmap_set) # attempt to add additional colormaps ext_cmaps = [] try: ext_cmaps.extend([f"cmo.{c}" for c in sorted(cmocean.cm.cmapnames)]) except Exception as exc: pass cmap_options.extend(ext_cmaps) # dropdown menu for setting colormap self.cmap = ipywidgets.Dropdown( options=cmap_options, value="viridis", description="Colormap:", description_tooltip=( "Colormap: matplotlib colormaps for displayed variable" ), disabled=False, style=self.style, ) # Reverse the colormap self.reverse = ipywidgets.Checkbox( value=False, description="Reverse Colormap", description_tooltip=( "Reverse Colormap: reverse matplotlib " "colormap for displayed variable" ), disabled=False, style=self.style, ) @property def projection(self): """return string for map projection based on region""" projections = {} projections["AA"] = "South" projections["A1"] = "South" projections["A2"] = "South" projections["A3"] = "South" projections["A4"] = "South" projections["CN"] = "North" projections["CS"] = "North" projections["GL"] = "North" projections["IS"] = "North" projections["SV"] = "North" projections["RA"] = "North" return projections[self.region.value] @property def center(self): """return default central point latitude and longitude for map based on region """ centers = {} centers["AA"] = (-90.0, 0.0) centers["A1"] = (-90.0, 0.0) centers["A2"] = (-90.0, 0.0) centers["A3"] = (-90.0, 0.0) centers["A4"] = (-90.0, 0.0) centers["CN"] = (79.0, -85.0) centers["CS"] = (70.0, -73.0) centers["GL"] = (72.5, -45.0) centers["IS"] = (64.5, -18.5) centers["SV"] = (79.0, 19.0) centers["RA"] = (79.0, 78.0) return centers[self.region.value] @property def zoom(self): """return default zoom level for map based on region""" zooms = {} zooms["AA"] = 1 zooms["A1"] = 1 zooms["A2"] = 1 zooms["A3"] = 1 zooms["A4"] = 1 zooms["CN"] = 2 zooms["CS"] = 2 zooms["GL"] = 1 zooms["IS"] = 3 zooms["SV"] = 3 zooms["RA"] = 2 return zooms[self.region.value] @property def _r(self): """return string for reversed Matplotlib colormaps""" cmap_reverse_flag = "_r" if self.reverse.value else "" return cmap_reverse_flag @property def colormap(self): """return string for Matplotlib colormaps""" return self.cmap.value + self._r @property def vmin(self): """return minimum of normalization range""" return self.range.value[0] @property def vmax(self): """return maximum of normalization range""" return self.range.value[1]
[docs] def set_directory_visibility(self, sender): """updates the visibility of the directory widget""" if self.asset.value == "atlas-local": self.directory.layout.display = "inline-flex" else: self.directory.layout.display = "none"
[docs] def set_format_visibility(self, sender): """updates the visibility of the data format widget""" if self.asset.value in ("atlas-s3", "atlas-local"): self.format.layout.display = "inline-flex" else: self.format.layout.display = "none" # set the format back to the default self.format.value = "nc"
[docs] def set_atl14_defaults(self, *args, **kwargs): """sets the default widget parameters for ATL14 variables""" # use dynamic normalization self.dynamic.value = True
[docs] def set_atl15_defaults(self, *args, **kwargs): """sets the default widget parameters for ATL15 variables""" group = copy.copy(self.group.value) variables = {} variables["delta_h"] = "delta_h" variables["dhdt_lag1"] = "dhdt" # set annual time lags # extend possible time lags to 16 years post-launch for timelag in range(4, 68, 4): variables[f"dhdt_lag{timelag:d}"] = "dhdt" # set default variable for group self.variable.value = variables[group]
[docs] def set_groups(self, *args): """sets the list of available groups for a release""" group_list = ["delta_h", "dhdt_lag1"] # append additional dhdt groups # extend possible time lags to 16 years post-launch for timelag in range(4, 68, 4): group_list.append(f"dhdt_lag{timelag:d}") # set group list self.group.options = group_list # change regions for Antarctica for Release-03+ if int(self.release.value) > 2: region_list = [ "AA", "A1", "A2", "A3", "A4", "CN", "CS", "GL", "IS", "RA", "SV", ] description_tooltip = ( "Region: ATL14/15 region\n\t" "AA: Antarctica (merged)\n\t" "A1: Antarctica (0\u00b0 to 90\u00b0)\n\t" "A2: Antarctica (0\u00b0 to -90\u00b0)\n\t" "A3: Antarctica (-90\u00b0 to -180\u00b0)\n\t" "A4: Antarctica (90\u00b0 to 180\u00b0)\n\t" "CN: Northern Canadian Archipelago\n\t" "CS: Southern Canadian Archipelago\n\t" "GL: Greenland\n\t" "IS: Iceland\n\t" "SV: Svalbard\n\t" "RA: Russian High Arctic" ) else: region_list = ["AA", "CN", "CS", "GL", "IS", "RA", "SV"] description_tooltip = ( "Region: ATL14/15 region\n\t" "AA: Antarctica\n\t" "CN: Northern Canadian Archipelago\n\t" "CS: Southern Canadian Archipelago\n\t" "GL: Greenland\n\t" "IS: Iceland\n\t" "SV: Svalbard\n\t" "RA: Russian High Arctic" ) # set region list self.region.options = region_list self.region.description_tooltip = description_tooltip
[docs] def set_variables(self, *args): """sets the list of available variables""" if isinstance(self.data_vars, list): # set list of available variables self.variable.options = sorted(self.data_vars) else: # return to temporary defaults self.variable.options = ["delta_h", "dhdt"]
[docs] def set_dynamic(self, *args, **kwargs): """sets variable normalization range if dynamic""" if self.dynamic.value: fmin = np.finfo(np.float64).min fmax = np.finfo(np.float64).max self.range.min = fmin self.range.max = fmax self.range.value = [fmin, fmax] self.range.layout.display = "none" else: self.range.min = -10 self.range.max = 10 self.range.value = [-5, 5] self.range.layout.display = "inline-flex"
[docs] def get_variables(self, d): """ Gets the available variables and time steps Parameters ---------- d : xarray.Dataset xarray.Dataset object """ # data and time variables self.data_vars = sorted(d.data_vars) # check if a object if "time" in d: self.time_vars = d.time.values # set the default groups self.set_groups() # set the default variables self.set_variables() # set the default time steps self.set_time_steps() else: # set the default variables self.set_variables()
[docs] def set_time_steps(self, *args, epoch=2018.0): """sets available time range""" # try setting the min and max time step try: # convert time to units self.time = list(epoch + self.time_vars / 365.25) self.timestep.max = self.time[-1] self.timestep.min = self.time[0] self.timestep.value = self.time[0] except Exception as exc: self.time = [] self.timestep.max = 1 self.timestep.min = 0 self.timestep.value = 0
[docs] def set_lag(self, sender): """sets available time range for lags""" self.timelag.min = 1 # try setting the max lag and value try: self.timelag.value = self.time.index(self.timestep.value) + 1 self.timelag.max = len(self.time) except Exception as exc: self.timelag.value = 1 self.timelag.max = 1
[docs] def set_time_visibility(self, sender): """updates the visibility of the time widget""" # list of invariant parameters invariant_parameters = ["ice_mask"] if int(self.release.value) <= 1: invariant_parameters.append("cell_area") # check if setting an invariant variable if self.variable.value in invariant_parameters: self.timestep.layout.display = "none" else: self.timestep.layout.display = "inline-flex"
@property def lag(self): """return the 0-based index for the time lag""" return self.timelag.value - 1