Source code for IS2view.tools

#!/usr/bin/env python
u"""
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')
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'] self.release = ipywidgets.Dropdown( options=release_list, value='004', 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"), 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 # use Release-01 groups as the initial default group_list = ['delta_h', 'dhdt_lag1', 'dhdt_lag4', 'dhdt_lag8', 'dhdt_lag12', 'dhdt_lag16', 'dhdt_lag20'] 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.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', 'dhdt_lag4', 'dhdt_lag8'] # append additional dhdt groups if (int(self.release.value) > 1): group_list.append('dhdt_lag12') if (int(self.release.value) > 2): group_list.append('dhdt_lag16') if (int(self.release.value) > 3): group_list.append('dhdt_lag20') # 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) 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